Files
Siteforge/server/routes/inbound-email.js
T
nmemmert 4b411f763f Add inbound email capture via Cloudflare Email Worker
- Cloudflare Email Worker forwards hello@ and nate@ to Gmail and POSTs
  parsed MIME email to /api/inbound-email as admin inbox entries
- New server route authenticates via shared secret and deduplicates by
  Message-ID before storing inbound emails as contact submissions
- Admin inbox shows "email" badge for inbound messages; reply composer
  opens blank with auto-subject "Re: [original]" and signature preview
- Documents setup steps in cloudflare/email-worker.js and .env.example

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:23:27 -04:00

64 lines
2.5 KiB
JavaScript

import { randomUUID } from 'node:crypto'
import { state } from '../state.js'
import { queueContactSubmissionsWrite } from '../data.js'
import { MAX_CONTACT_SUBMISSIONS } from '../config.js'
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') ?? ''
if (!provided || provided !== secret) {
res.status(401).json({ message: 'Unauthorized.' }); return
}
const { from, to, subject, body, 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()
// Deduplicate by messageId if provided
if (messageId && typeof messageId === 'string' && messageId.trim()) {
const exists = state.contactSubmissions.some(s => s.messageId === messageId.trim())
if (exists) {
res.json({ ok: true, duplicate: true }); return
}
}
const submission = {
id: randomUUID(),
submittedAt: date ? new Date(date).toISOString() : new Date().toISOString(),
name: fromName || fromEmail,
email: fromEmail,
message: [subject ? `Subject: ${subject}` : '', body ?? ''].filter(Boolean).join('\n\n'),
messageType: 'general',
subscribe: false,
archived: false,
source: 'inbound-email',
inboundTo: typeof to === 'string' ? to : '',
messageId: typeof messageId === 'string' ? messageId.trim() : '',
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 })
})
}