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>
This commit is contained in:
nmemmert
2026-06-30 11:23:27 -04:00
parent f83682879b
commit 4b411f763f
6 changed files with 389 additions and 5 deletions
+22 -5
View File
@@ -626,6 +626,8 @@ interface ContactSubmission {
messageType: 'question' | 'testimony' | 'topic' | 'general'
subscribe: boolean
archived?: boolean
source?: 'contact-form' | 'download' | 'inbound-email'
inboundTo?: string
}
interface ContactReplyDraft {
@@ -2722,15 +2724,23 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
function openContactReplyComposer(submission: ContactSubmission) {
const firstName = submission.name?.trim().split(/\s+/)[0] || 'there'
const isInbound = submission.source === 'inbound-email'
// For inbound emails, extract the original subject from the stored message
const subjectLine = isInbound
? (() => {
const match = /^Subject: (.+)/m.exec(submission.message ?? '')
return match ? `Re: ${match[1].trim()}` : 'Re: Your message'
})()
: 'Thanks for reaching out to Verse by Verse with Nate'
setContactReplyDraft({
submissionId: submission.id,
recipientName: submission.name,
recipientEmail: submission.email,
subject: 'Thanks for reaching out to Verse by Verse with Nate',
message: `Thank you for reaching out.\n\nI appreciate your message and wanted to follow up personally.`,
subject: subjectLine,
message: '',
})
setContactReplyStatus('idle')
setContactReplyMsg(`Composing a reply to ${firstName}.`)
setContactReplyMsg(`Composing a reply to ${firstName}.`)
}
function applyContactReplyTemplate(templateId: string) {
@@ -3282,7 +3292,10 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
{c.submissionCount > 1 && <div className="admin-stats-note" style={{ margin: '0.2rem 0 0' }}>{c.submissionCount} submissions</div>}
</td>
<td><a href={`mailto:${c.email}`}>{c.email}</a></td>
<td><span className="admin-badge admin-badge--pending" style={{ fontSize: '0.7rem' }}>{c.messageType ?? 'contact'}</span></td>
<td>
<span className="admin-badge admin-badge--pending" style={{ fontSize: '0.7rem' }}>{c.messageType ?? 'contact'}</span>
{c.source === 'inbound-email' && <span className="admin-badge admin-badge--info" style={{ fontSize: '0.7rem', marginLeft: '0.3rem' }} title={c.inboundTo ? `To: ${c.inboundTo}` : 'Direct email'}>email</span>}
</td>
<td style={{ textAlign: 'center' }}>{c.subscribe ? '✓' : ''}</td>
<td style={{ whiteSpace: 'nowrap' }}>{formatDate(c.submittedAt)}</td>
<td style={{ maxWidth: '280px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={c.message}>{c.message ?? '—'}</td>
@@ -5095,10 +5108,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<textarea
id="reply-message"
rows={8}
placeholder="Type your reply here…"
value={contactReplyDraft.message}
onChange={e => setContactReplyDraft(draft => draft ? { ...draft, message: e.target.value } : draft)}
/>
<p className="admin-stats-note">This will be wrapped in a professional HTML email template automatically.</p>
<div className="admin-stats-note" style={{ borderLeft: '3px solid #ccc', paddingLeft: '0.6rem', marginTop: '0.4rem', color: '#666', fontStyle: 'italic', whiteSpace: 'pre-line' }}>
{'Grace and peace,\nVerse by Verse with Nate'}
<span style={{ display: 'block', marginTop: '0.25rem', fontSize: '0.75em', fontStyle: 'normal', color: '#999' }}>Your signature appended automatically</span>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>