Files
Siteforge/server/routes/inbound-email.js
T
nmemmert d0c949070f Fix base64-encoded HTML email bodies not rendering; v1.1.18
Inbound emails from some clients (Outlook, mobile) send htmlBody as
base64. Decode it on the server before storing (future emails) and on
the client before passing to the iframe srcDoc (existing stored emails).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-28 12:28:21 -04:00

84 lines
3.3 KiB
JavaScript

import { randomUUID, timingSafeEqual } from 'node:crypto'
import { state } from '../state.js'
import { queueContactSubmissionsWrite } from '../data.js'
import { MAX_CONTACT_SUBMISSIONS } from '../config.js'
// RFC 5322 msg-id: "<" printable-ASCII-no-whitespace ">"
const MESSAGE_ID_RE = /^<[\x21-\x7E]+>$/
function decodeHtmlBody(raw) {
if (typeof raw !== 'string' || !raw.trim()) return null
const s = raw.trim()
if (s.startsWith('<')) return s // already plain HTML
try {
const decoded = Buffer.from(s.replace(/\s+/g, ''), 'base64').toString('utf8')
return decoded.trimStart().startsWith('<') ? decoded : s
} catch {
return s
}
}
export function register(app) {
app.post('/api/inbound-email', (req, res) => {
const secret = process.env.INBOUND_EMAIL_SECRET
if (!secret) {
res.status(503).json({ message: 'Inbound email not configured.' }); return
}
const provided = req.get('x-webhook-secret') ?? ''
const a = Buffer.from(provided, 'utf8')
const b = Buffer.from(secret, 'utf8')
if (!provided || a.length !== b.length || !timingSafeEqual(a, b)) {
res.status(401).json({ message: 'Unauthorized.' }); return
}
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
}
// Extract display name and email address from "Name <email>" format
const fromMatch = /^(.*?)\s*<([^>]+)>$/.exec(from.trim())
const fromEmail = fromMatch ? fromMatch[2].trim() : from.trim()
const fromName = fromMatch ? fromMatch[1].trim() : from.trim()
const normalizedMessageId = typeof messageId === 'string' && MESSAGE_ID_RE.test(messageId.trim()) ? messageId.trim() : ''
// Deduplicate by messageId if provided
if (normalizedMessageId) {
const exists = state.contactSubmissions.some(s => s.messageId === normalizedMessageId)
if (exists) {
res.json({ ok: true, duplicate: true }); return
}
}
const submission = {
id: randomUUID(),
submittedAt: (typeof date === 'string' || typeof date === 'number') && Number.isFinite(Date.parse(date)) ? new Date(date).toISOString() : new Date().toISOString(),
name: fromName || fromEmail,
email: fromEmail,
message: [subject ? `Subject: ${subject}` : '', body ?? ''].filter(Boolean).join('\n\n'),
htmlBody: decodeHtmlBody(htmlBody),
messageType: 'general',
subscribe: false,
archived: false,
source: 'inbound-email',
inboundTo: typeof to === 'string' ? to : '',
messageId: normalizedMessageId,
emailStatus: {
welcome: { status: 'not-applicable', lastEventAt: null, lastEventType: null, resendEmailId: null, error: null },
adminNotification: { status: 'not-applicable', lastEventAt: null, lastEventType: null, resendEmailId: null, error: null },
adminReply: { status: 'pending', lastEventAt: null, lastEventType: null, resendEmailId: null, error: null },
},
}
state.contactSubmissions.unshift(submission)
state.contactSubmissions = state.contactSubmissions.slice(0, MAX_CONTACT_SUBMISSIONS)
queueContactSubmissionsWrite()
console.log(`[inbound-email] received from ${fromEmail} — subject: ${subject ?? '(none)'}`)
res.json({ ok: true })
})
}