Quick Email Preview in Email Queue Form

Hello comunity!

I’ve created a Frappe Client Script that lets you quickly preview email content directly from the Email Queue form. It extracts headers, recipients (To/CC/BCC), attachments, and renders the HTML body in a gmail style dialog. Super handy for debugging or reviewing emails without opening them externally.

Features:

  • :envelope: Quick Preview” button for emails

  • Parses Subject, From, Date, Reply-To

  • Handles recipients including <!--recipient--> placeholders

  • Displays attachments

  • Renders HTML email properly in an iframe

  • Fallback to plain text if HTML is missing

  • Shows colored status for each recipient (green = sent, red = failed, orange = pending)

  • Attachments are listed nicely below headers

Usage: Add this as a Client Script for the Email Queue doctype.

frappe.ui.form.on('Email Queue', {
    refresh: function(frm) {
        if (!frm.is_new()) {
            if (!frm.doc.message) return;
    
            frm.add_custom_button(__('<i class="fa fa-envelope-open"></i> Quick View'), function () {
                let raw = frm.doc.message;
    
                // -------------------------------
                // Extract Header
                // -------------------------------
                function getHeader(name) {
                    let match = raw.match(new RegExp(name + ":(.*)", "i"));
                    return match ? match[1].trim() : "";
                }
    
                let subject = getHeader("Subject");
                let from = getHeader("From");
                let date = getHeader("Date");
                let replyto = getHeader("Reply-To");
                let cc = getHeader("CC");
                let bcc = getHeader("BCC");
    
                // -------------------------------
                // Recipients HTML - Enterprise Version with <!--recipient--> check
                // -------------------------------
                
                // Helper to extract multiple emails from headers
                function getEmailsFromHeader(message, headerName) {
                    if (!message) return [];
                    const regex = new RegExp(`^${headerName}:\\s*(.+)$`, 'mi'); // case-insensitive
                    const match = message.match(regex);
                    if (match && match[1]) {
                        return match[1].split(',').map(e => e.trim()).filter(e => e);
                    }
                    return [];
                }
                
                // Extract To and CC arrays
                let to_emails = getEmailsFromHeader(frm.doc.message, "To");
                const cc_emails = getEmailsFromHeader(frm.doc.message, "CC");
                
                // Special case: if message has <!--recipient--> in To header, treat all as TO
                if (frm.doc.message && /To:\s*<!--recipient-->/.test(frm.doc.message)) {
                    to_emails = frm.doc.recipients.map(r => r.recipient);
                }
                
                // Group recipients by type
                let to_group = [];
                let cc_group = [];
                let bcc_group = [];
                
                if (frm.doc.recipients && frm.doc.recipients.length) {
                    frm.doc.recipients.forEach(r => {
                        const color = r.status === 'Sent' ? 'green' : (r.status === 'Failed' ? 'red' : 'orange');
                        const html_email = `${r.recipient} <span style="color:${color}">(${r.status})</span>`;
                
                        if (to_emails.includes(r.recipient)) {
                            to_group.push(html_email);
                        } else if (cc_emails.includes(r.recipient) || r.recipient === frm.doc.show_as_cc) {
                            cc_group.push(html_email);
                        } else {
                            bcc_group.push(html_email);
                        }
                    });
                }
                
                // Build final HTML - single line per type, comma-separated
                let to_html = '';
                if (to_group.length) to_html += `<p><b>To:</b> ${to_group.join(', ')}</p>`;
                if (cc_group.length) to_html += `<p><b>CC:</b> ${cc_group.join(', ')}</p>`;
                if (bcc_group.length) to_html += `<p><b>BCC:</b> ${bcc_group.join(', ')}</p>`;
                
                // Assign to your UI placeholder
                frm.get_field('recipients_html')?.$wrapper.html(to_html);
                // -------------------------------
                // Attachments HTML
                // -------------------------------
                let attachments_html = '';
                if(frm.doc.attachments) {
                    let attachments = [];
                    try {
                        attachments = typeof frm.doc.attachments === "string" ? JSON.parse(frm.doc.attachments) : frm.doc.attachments;
                    } catch(e) {
                        console.error("Failed to parse attachments JSON", e);
                    }
    
                    if (attachments.length) {
                        attachments_html = `<div style="margin-top:10px;">
                            <b>Attachments:</b>
                            <ul>
                                ${attachments.map(a => `<li>${a.name || a.filename || ''}</li>`).join('')}
                            </ul>
                        </div>`;
                    }
                }
    
                // -------------------------------
                // Extract HTML (Robust)
                // -------------------------------
                function extractHTML(raw) {
                    let parts = raw.split("Content-Type: text/html");
                    if (parts.length < 2) return null;
    
                    let htmlPart = parts[1];
                    let startIndex = htmlPart.indexOf("\r\n\r\n");
                    if (startIndex === -1) return null;
    
                    htmlPart = htmlPart.substring(startIndex + 4);
    
                    let boundaryIndex = htmlPart.indexOf("\r\n--");
                    if (boundaryIndex !== -1) htmlPart = htmlPart.substring(0, boundaryIndex);
    
                    return htmlPart.trim();
                }
    
                // -------------------------------
                // Extract Plain Text (fallback)
                // -------------------------------
                function extractText(raw) {
                    let parts = raw.split("Content-Type: text/plain");
                    if (parts.length < 2) return "No content found";
    
                    let textPart = parts[1];
                    let startIndex = textPart.indexOf("\r\n\r\n");
                    if (startIndex === -1) return "No content found";
    
                    textPart = textPart.substring(startIndex + 4);
                    let boundaryIndex = textPart.indexOf("\r\n--");
                    if (boundaryIndex !== -1) textPart = textPart.substring(0, boundaryIndex);
    
                    return textPart.trim();
                }
    
                // -------------------------------
                // Decode quoted-printable
                // -------------------------------
                function decodeQP(str) {
                    return str
                        .replace(/=\r?\n/g, '') // remove soft breaks
                        .replace(/=([A-Fa-f0-9]{2})/g, function(match, hex) {
                            return String.fromCharCode(parseInt(hex, 16));
                        });
                }
    
                let html = extractHTML(raw);
                let text = extractText(raw);
                let finalContent = html
                    ? decodeQP(html)
                    : `<pre style="white-space:pre-wrap;">${decodeQP(text)}</pre>`;
    
                // -------------------------------
                // Create Dialog
                // -------------------------------
                let d = new frappe.ui.Dialog({
                    title: "Email Preview",
                    size: "extra-large",
                    fields: [{ fieldtype: "HTML", fieldname: "preview_html" }]
                });
                d.show();
    
                // -------------------------------
                // Layout (Header + Body + Recipients + CC/BCC + Attachments)
                // -------------------------------
                let wrapperHTML = `
                    <div style="background:#f4f5f6;padding:20px;">
                        <div style="max-width:900px;margin:auto;background:#fff;border-radius:10px;padding:20px;box-shadow:0 2px 8px rgba(0,0,0,0.1);">
    
                            <div style="border-bottom:1px solid #e5e5e5;padding-bottom:10px;margin-bottom:15px;">
                                <h3 style="margin:0;">${subject || '(No Subject)'}</h3>
                                <p style="margin:5px 0;"><b>From:</b> ${from}</p>
                                <p style="margin:5px 0;"><b>Reply-To:</b> ${replyto}</p>
                                ${to_html}
                                <p style="margin:5px 0;"><b>Date:</b> ${date}</p>
                                ${attachments_html}
                            </div>
    
                            <div id="email-body"></div>
                        </div>
                    </div>
                `;
                d.fields_dict.preview_html.$wrapper.html(wrapperHTML);
    
                // -------------------------------
                // Render email body inside iframe
                // -------------------------------
                let iframe = document.createElement("iframe");
                iframe.style.width = "100%";
                iframe.style.height = "600px";
                iframe.style.border = "none";
    
                d.$wrapper.find("#email-body").html(iframe);
    
                setTimeout(() => {
                    if (iframe.contentWindow) {
                        iframe.contentWindow.document.open();
                        iframe.contentWindow.document.write(finalContent);
                        iframe.contentWindow.document.close();
                    } else {
                        console.error("iframe not ready");
                    }
                }, 200);
    
            }).addClass('btn-primary');
        }
    }
});

Good luck

1 Like