Add per-reply from-address selector and inbox address badges

- Reply composer shows a From dropdown (hello@ or nate@), defaulting
  to whichever address the inbound email was sent to
- Server validates the chosen address against an allowlist before sending
- Inbox list shows colored address badges: blue for hello@, purple for nate@

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmert
2026-06-30 12:14:34 -04:00
parent edc17b8710
commit fbb9a24f32
2 changed files with 40 additions and 5 deletions
+8 -2
View File
@@ -455,6 +455,7 @@ export function register(app) {
const { id } = req.params const { id } = req.params
const subject = typeof req.body?.subject === 'string' ? req.body.subject.trim() : '' const subject = typeof req.body?.subject === 'string' ? req.body.subject.trim() : ''
const message = typeof req.body?.message === 'string' ? req.body.message.trim() : '' const message = typeof req.body?.message === 'string' ? req.body.message.trim() : ''
const requestedFrom = typeof req.body?.fromAddress === 'string' ? req.body.fromAddress.trim() : ''
if (!id || typeof id !== 'string') { if (!id || typeof id !== 'string') {
res.status(400).json({ message: 'Invalid submission id.' }); return res.status(400).json({ message: 'Invalid submission id.' }); return
@@ -477,7 +478,12 @@ export function register(app) {
const recipientName = splitName(submission.name).firstName || submission.name || 'friend' const recipientName = splitName(submission.name).firstName || submission.name || 'friend'
const html = buildAdminReplyTemplate({ recipientName, message }) const html = buildAdminReplyTemplate({ recipientName, message })
const replyToAddress = getResendReplyToAddress() const replyToAddress = getResendReplyToAddress()
const fromAddress = getResendFromAddress() const defaultFrom = getResendFromAddress() || ADMIN_REPLY_FROM
const ALLOWED_FROM = [
'Verse by Verse with Nate <hello@versebyversewithnate.us>',
'Verse by Verse with Nate <nate@versebyversewithnate.us>',
]
const fromAddress = ALLOWED_FROM.includes(requestedFrom) ? requestedFrom : defaultFrom
const text = `Hi ${recipientName},\n\n${message}\n\nGrace and peace,\nVerse by Verse with Nate\n${replyToAddress}` const text = `Hi ${recipientName},\n\n${message}\n\nGrace and peace,\nVerse by Verse with Nate\n${replyToAddress}`
const resend = new Resend(process.env.RESEND_API_KEY) const resend = new Resend(process.env.RESEND_API_KEY)
@@ -485,7 +491,7 @@ export function register(app) {
resend, resend,
context: 'admin-contact-reply', context: 'admin-contact-reply',
payload: { payload: {
from: fromAddress || ADMIN_REPLY_FROM, from: fromAddress,
to: [submission.email], to: [submission.email],
subject, subject,
replyTo: replyToAddress, replyTo: replyToAddress,
+32 -3
View File
@@ -637,6 +637,7 @@ interface ContactReplyDraft {
recipientEmail: string recipientEmail: string
subject: string subject: string
message: string message: string
fromAddress: string
} }
interface ContactReplyTemplate { interface ContactReplyTemplate {
@@ -2751,12 +2752,18 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
return match ? `Re: ${match[1].trim()}` : 'Re: Your message' return match ? `Re: ${match[1].trim()}` : 'Re: Your message'
})() })()
: 'Thanks for reaching out to Verse by Verse with Nate' : 'Thanks for reaching out to Verse by Verse with Nate'
// Default reply-from to whichever address the email was sent to
const inboundTo = submission.inboundTo ?? ''
const defaultFrom = inboundTo.includes('nate@')
? 'Verse by Verse with Nate <nate@versebyversewithnate.us>'
: 'Verse by Verse with Nate <hello@versebyversewithnate.us>'
setContactReplyDraft({ setContactReplyDraft({
submissionId: submission.id, submissionId: submission.id,
recipientName: submission.name, recipientName: submission.name,
recipientEmail: submission.email, recipientEmail: submission.email,
subject: subjectLine, subject: subjectLine,
message: '', message: '',
fromAddress: defaultFrom,
}) })
setContactReplyStatus('idle') setContactReplyStatus('idle')
setContactReplyMsg(`Composing a reply to ${firstName}.`) setContactReplyMsg(`Composing a reply to ${firstName}.`)
@@ -2827,6 +2834,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
body: JSON.stringify({ body: JSON.stringify({
subject: contactReplyDraft.subject, subject: contactReplyDraft.subject,
message: contactReplyDraft.message, message: contactReplyDraft.message,
fromAddress: contactReplyDraft.fromAddress,
}), }),
}) })
@@ -2836,7 +2844,8 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
} }
setContactReplyStatus('sent') setContactReplyStatus('sent')
setContactReplyMsg(`Reply sent to ${contactReplyDraft.recipientEmail} from hello@versebyversewithnate.us.`) const sentFrom = contactReplyDraft.fromAddress.match(/<([^>]+)>/)?.[1] ?? contactReplyDraft.fromAddress
setContactReplyMsg(`Reply sent to ${contactReplyDraft.recipientEmail} from ${sentFrom}.`)
await reloadContactReplyHistory() await reloadContactReplyHistory()
setContactReplyDraft(null) setContactReplyDraft(null)
} catch (err) { } catch (err) {
@@ -3312,7 +3321,20 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
<td><a href={`mailto:${c.email}`}>{c.email}</a></td> <td><a href={`mailto:${c.email}`}>{c.email}</a></td>
<td> <td>
<span className="admin-badge admin-badge--pending" style={{ fontSize: '0.7rem' }}>{c.messageType ?? 'contact'}</span> <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>} {c.source === 'inbound-email' && (
<span
className="admin-badge"
style={{
fontSize: '0.7rem',
marginLeft: '0.3rem',
background: c.inboundTo?.includes('nate@') ? '#7c3aed' : '#0369a1',
color: '#fff',
}}
title={c.inboundTo ?? 'Direct email'}
>
{c.inboundTo?.includes('nate@') ? 'nate@' : 'hello@'}
</span>
)}
</td> </td>
<td style={{ textAlign: 'center' }}>{c.subscribe ? '✓' : ''}</td> <td style={{ textAlign: 'center' }}>{c.subscribe ? '✓' : ''}</td>
<td style={{ whiteSpace: 'nowrap' }}>{formatDate(c.submittedAt)}</td> <td style={{ whiteSpace: 'nowrap' }}>{formatDate(c.submittedAt)}</td>
@@ -5123,7 +5145,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</div> </div>
<div className="admin-field"> <div className="admin-field">
<label htmlFor="reply-from">From</label> <label htmlFor="reply-from">From</label>
<input id="reply-from" type="text" value="hello@versebyversewithnate.us" readOnly /> <select
id="reply-from"
value={contactReplyDraft.fromAddress}
onChange={e => setContactReplyDraft(draft => draft ? { ...draft, fromAddress: e.target.value } : draft)}
>
<option value="Verse by Verse with Nate <hello@versebyversewithnate.us>">hello@versebyversewithnate.us</option>
<option value="Verse by Verse with Nate <nate@versebyversewithnate.us>">nate@versebyversewithnate.us</option>
</select>
</div> </div>
<div className="admin-field"> <div className="admin-field">
<label htmlFor="reply-subject">Subject</label> <label htmlFor="reply-subject">Subject</label>