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
+32 -3
View File
@@ -637,6 +637,7 @@ interface ContactReplyDraft {
recipientEmail: string
subject: string
message: string
fromAddress: string
}
interface ContactReplyTemplate {
@@ -2751,12 +2752,18 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
return match ? `Re: ${match[1].trim()}` : 'Re: Your message'
})()
: '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({
submissionId: submission.id,
recipientName: submission.name,
recipientEmail: submission.email,
subject: subjectLine,
message: '',
fromAddress: defaultFrom,
})
setContactReplyStatus('idle')
setContactReplyMsg(`Composing a reply to ${firstName}.`)
@@ -2827,6 +2834,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
body: JSON.stringify({
subject: contactReplyDraft.subject,
message: contactReplyDraft.message,
fromAddress: contactReplyDraft.fromAddress,
}),
})
@@ -2836,7 +2844,8 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
}
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()
setContactReplyDraft(null)
} 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>
<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 style={{ textAlign: 'center' }}>{c.subscribe ? '✓' : ''}</td>
<td style={{ whiteSpace: 'nowrap' }}>{formatDate(c.submittedAt)}</td>
@@ -5123,7 +5145,14 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
</div>
<div className="admin-field">
<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 className="admin-field">
<label htmlFor="reply-subject">Subject</label>