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:
@@ -455,6 +455,7 @@ export function register(app) {
|
||||
const { id } = req.params
|
||||
const subject = typeof req.body?.subject === 'string' ? req.body.subject.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') {
|
||||
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 html = buildAdminReplyTemplate({ recipientName, message })
|
||||
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 resend = new Resend(process.env.RESEND_API_KEY)
|
||||
|
||||
@@ -485,7 +491,7 @@ export function register(app) {
|
||||
resend,
|
||||
context: 'admin-contact-reply',
|
||||
payload: {
|
||||
from: fromAddress || ADMIN_REPLY_FROM,
|
||||
from: fromAddress,
|
||||
to: [submission.email],
|
||||
subject,
|
||||
replyTo: replyToAddress,
|
||||
|
||||
+32
-3
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user