Render inbound email HTML body in admin inbox
Worker now extracts both text/plain and text/html MIME parts and sends htmlBody in the webhook payload. Server stores it on the submission. Admin inbox renders htmlBody in a sandboxed iframe when present, falling back to plain text with whitespace preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+12
-10
@@ -84,37 +84,39 @@ function parseEmail(raw, message) {
|
||||
const date = headers['date'] ?? new Date().toISOString()
|
||||
const messageId = headers['message-id'] ?? ''
|
||||
|
||||
// Extract plain text body — skip MIME boundaries and HTML parts
|
||||
const bodyLines = lines.slice(bodyStart)
|
||||
const plainText = extractPlainText(raw, bodyLines)
|
||||
const plainText = extractPart(raw, bodyLines, 'text/plain')
|
||||
const htmlBody = extractPart(raw, bodyLines, 'text/html')
|
||||
|
||||
return {
|
||||
from,
|
||||
to,
|
||||
subject,
|
||||
body: plainText,
|
||||
htmlBody: htmlBody || null,
|
||||
date,
|
||||
messageId,
|
||||
source: 'inbound-email',
|
||||
}
|
||||
}
|
||||
|
||||
function extractPlainText(raw, bodyLines) {
|
||||
// Look for Content-Type: text/plain section in multipart emails
|
||||
const textPlainMatch = /Content-Type: text\/plain[^\r\n]*\r?\n(?:[^\r\n]+\r?\n)*\r?\n([\s\S]*?)(?=--|\z)/i.exec(raw)
|
||||
if (textPlainMatch) {
|
||||
return decodeEmailBody(textPlainMatch[1]).trim()
|
||||
}
|
||||
function extractPart(raw, bodyLines, contentType) {
|
||||
const escaped = contentType.replace('/', '\\/')
|
||||
const re = new RegExp(`Content-Type: ${escaped}[^\\r\\n]*\\r?\\n(?:[^\\r\\n]+\\r?\\n)*\\r?\\n([\\s\\S]*?)(?=--|$)`, 'i')
|
||||
const match = re.exec(raw)
|
||||
if (match) return decodeEmailBody(match[1]).trim()
|
||||
|
||||
// Fallback: join body lines, strip HTML tags if present
|
||||
if (contentType === 'text/plain') {
|
||||
const joined = bodyLines.join('\n')
|
||||
if (/<[a-z][\s\S]*>/i.test(joined)) {
|
||||
return joined.replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/\s{3,}/g, '\n\n').trim()
|
||||
}
|
||||
|
||||
return joined.trim()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function decodeEmailBody(text) {
|
||||
// Handle quoted-printable encoding (=XX hex sequences and soft line breaks)
|
||||
return text
|
||||
|
||||
@@ -15,7 +15,7 @@ export function register(app) {
|
||||
res.status(401).json({ message: 'Unauthorized.' }); return
|
||||
}
|
||||
|
||||
const { from, to, subject, body, date, messageId, source } = req.body ?? {}
|
||||
const { from, to, subject, body, htmlBody, date, messageId, source } = req.body ?? {}
|
||||
|
||||
if (!from || typeof from !== 'string') {
|
||||
res.status(400).json({ message: 'Missing from address.' }); return
|
||||
@@ -40,6 +40,7 @@ export function register(app) {
|
||||
name: fromName || fromEmail,
|
||||
email: fromEmail,
|
||||
message: [subject ? `Subject: ${subject}` : '', body ?? ''].filter(Boolean).join('\n\n'),
|
||||
htmlBody: typeof htmlBody === 'string' && htmlBody.trim() ? htmlBody.trim() : null,
|
||||
messageType: 'general',
|
||||
subscribe: false,
|
||||
archived: false,
|
||||
|
||||
+15
-1
@@ -628,6 +628,7 @@ interface ContactSubmission {
|
||||
archived?: boolean
|
||||
source?: 'contact-form' | 'download' | 'inbound-email'
|
||||
inboundTo?: string
|
||||
htmlBody?: string | null
|
||||
}
|
||||
|
||||
interface ContactReplyDraft {
|
||||
@@ -5051,7 +5052,20 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
</div>
|
||||
|
||||
<div className="admin-email-body">
|
||||
<p>{selected.message}</p>
|
||||
{selected.htmlBody ? (
|
||||
<iframe
|
||||
srcDoc={selected.htmlBody}
|
||||
sandbox="allow-same-origin"
|
||||
style={{ width: '100%', minHeight: '320px', border: 'none', background: '#fff', borderRadius: '4px' }}
|
||||
onLoad={e => {
|
||||
const iframe = e.currentTarget
|
||||
const h = iframe.contentDocument?.documentElement?.scrollHeight
|
||||
if (h) iframe.style.height = `${h}px`
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<p style={{ whiteSpace: 'pre-wrap' }}>{selected.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="admin-actions admin-actions--maintenance">
|
||||
|
||||
Reference in New Issue
Block a user