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 { 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
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user