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:
-
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
