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:
+22
-5
@@ -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' }}>
|
||||
|
||||
Reference in New Issue
Block a user