import { Resend } from 'resend'
import { escapeHtml, buildAbsoluteUrl, splitName } from './helpers.js'
import {
DEFAULT_RESEND_FROM,
DEFAULT_SEO,
} from './config.js'
import { state } from './state.js'
// ── Address helpers ────────────────────────────────────────────────────────
export function getResendFromAddress() {
return process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
}
export function getResendReplyToAddress() {
return process.env.RESEND_REPLY_TO ?? 'hello@versebyversewithnate.us'
}
export function getResendInboxAddress() {
return process.env.RESEND_TO ?? 'hello@versebyversewithnate.us'
}
export function getCanonicalBaseUrl() {
const configured = state.cachedSiteContent?.seo?.canonicalUrl ?? DEFAULT_SEO.canonicalUrl
if (typeof configured !== 'string' || !configured.trim()) return DEFAULT_SEO.canonicalUrl
return configured.trim()
}
export function getAddressDomain(addressValue) {
const raw = String(addressValue || '').trim()
if (!raw) return ''
const candidate = raw.includes('<') && raw.includes('>')
? raw.slice(raw.lastIndexOf('<') + 1, raw.lastIndexOf('>')).trim()
: raw
const at = candidate.lastIndexOf('@')
if (at <= 0 || at === candidate.length - 1) return ''
return candidate.slice(at + 1).toLowerCase()
}
export function logResendEmailAlignmentWarnings() {
const warnings = []
const fromAddress = getResendFromAddress()
const replyToAddress = getResendReplyToAddress()
const fromDomain = getAddressDomain(fromAddress)
const replyDomain = getAddressDomain(replyToAddress)
const hasApiKey = Boolean(process.env.RESEND_API_KEY)
if (!hasApiKey) warnings.push('RESEND_API_KEY is missing. Contact and reply emails cannot send.')
if (!fromDomain) warnings.push('RESEND_FROM is missing or malformed. Use a verified domain sender identity.')
if (fromDomain.endsWith('resend.dev')) warnings.push('RESEND_FROM uses resend.dev. Move to your own verified domain for best deliverability.')
if (fromDomain && replyDomain && fromDomain !== replyDomain) warnings.push('RESEND_FROM and RESEND_REPLY_TO use different domains. This can weaken alignment.')
if (!process.env.RESEND_WEBHOOK_TOKEN) warnings.push('RESEND_WEBHOOK_TOKEN is not set. Delivery webhooks are not authenticated.')
if (hasApiKey) warnings.push('Verify SPF, DKIM, and DMARC for the sender domain to improve inbox placement.')
if (warnings.length > 0) {
console.warn('[email-health] Resend alignment checks:')
for (const warning of warnings) {
console.warn(`[email-health] - ${warning}`)
}
}
}
// ── Send helpers ───────────────────────────────────────────────────────────
export async function sendResendEmailWithRetry({ resend, payload, context, maxAttempts = 2 }) {
let lastError = null
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
const result = await resend.emails.send(payload)
if (!result?.error) return result
lastError = result.error
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 250 * attempt))
}
} catch (err) {
lastError = err
if (attempt < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 250 * attempt))
}
}
}
throw lastError ?? new Error(`[${context}] email send failed`)
}
export async function syncContactToResend(name, email) {
if (!process.env.RESEND_API_KEY) return
const { firstName, lastName } = splitName(name)
const contactResend = new Resend(process.env.RESEND_CONTACTS_API_KEY ?? process.env.RESEND_API_KEY)
try {
const { error: contactError } = await contactResend.contacts.create({
email,
firstName,
lastName,
unsubscribed: false,
...(process.env.RESEND_SEGMENT_ID ? { segments: [{ id: process.env.RESEND_SEGMENT_ID }] } : {}),
})
if (contactError) {
const { error: updateError } = await contactResend.contacts.update({ email, firstName, lastName, unsubscribed: false })
if (updateError) console.error('[resend] contact sync error:', updateError)
}
} catch (err) {
console.error('[resend] contact sync exception:', err)
}
}
// ── Email HTML builders ────────────────────────────────────────────────────
export function buildBrandedEmailHtml({ title, eyebrow, bodyHtml, ctaLabel, ctaUrl, footerHtml }) {
const ctaBlock = ctaLabel && ctaUrl
? `
${escapeHtml(ctaLabel)}
`
: ''
return (
`` +
`
` +
`` +
`` +
`| ` +
` ${escapeHtml(eyebrow ?? 'Verse by Verse with Nate')} ` +
`${escapeHtml(title)} ` +
` | ` +
`| ` +
` ${bodyHtml} ` +
`${ctaBlock}` +
` | ` +
`| ${footerHtml ?? ''} | ` +
` ` +
` |
` +
`
`
)
}
export function buildContactWelcomeEmailTemplate({
greetingName,
welcomeIntro,
welcomeCurrentSeries,
welcomeStartHereTitle,
welcomeStartHereSummary,
welcomeExpect1,
welcomeExpect2,
welcomeExpect3,
welcomeScripture,
welcomeScriptureRef,
welcomeSignoff,
welcomeGreetingPrefix = "Glad you're here",
welcomeSpotifyUrl,
welcomeAppleUrl,
welcomeAmazonUrl,
welcomeWebsiteUrl,
welcomeEpisodeUrl,
welcomeImageUrl,
welcomeSpotifyBtnLabel = 'Listen on Spotify',
welcomeAppleBtnLabel = 'Apple Podcasts',
welcomeStartHereLinkLabel = 'Open Start Here page',
}) {
const heading = greetingName
? `${escapeHtml(welcomeGreetingPrefix)}, ${escapeHtml(greetingName)}.`
: `${escapeHtml(welcomeGreetingPrefix)}.`
return {
text:
`Welcome to Verse by Verse with Nate!\n\n` +
`${heading}\n\n` +
`${welcomeIntro}\n\n` +
`${welcomeCurrentSeries}\n\n` +
`Start here: ${welcomeEpisodeUrl}\n` +
`${welcomeStartHereTitle}\n` +
`${welcomeStartHereSummary}\n` +
`${welcomeSpotifyBtnLabel}: ${welcomeSpotifyUrl}\n` +
`${welcomeAppleBtnLabel}: ${welcomeAppleUrl}\n` +
`Amazon Music: ${welcomeAmazonUrl}\n` +
`Website: ${welcomeWebsiteUrl}\n\n` +
`What to expect:\n` +
`- ${welcomeExpect1}\n` +
`- ${welcomeExpect2}\n` +
`- ${welcomeExpect3}\n\n` +
`"${welcomeScripture}"\n${welcomeScriptureRef}\n\n` +
`${welcomeSignoff}`,
html: `
Verse by Verse with Nate
Verse by verse. Nugget by nugget.
|
|
Welcome
${heading}
|
|
${escapeHtml(welcomeIntro)}
${escapeHtml(welcomeCurrentSeries)}
If you’re just joining us, the best place to start is Episode 1. It sets the table for everything that follows.
|
|
|
Start here
${escapeHtml(welcomeStartHereTitle)}
${escapeHtml(welcomeStartHereSummary)}
|
|
|
What to expect
|
${escapeHtml(welcomeExpect1)} |
|
${escapeHtml(welcomeExpect2)} |
|
${escapeHtml(welcomeExpect3)} |
|
|
|
“${escapeHtml(welcomeScripture)}”
${escapeHtml(welcomeScriptureRef)}
|
|
|
Find the podcast on
You’re receiving this because you subscribed to Verse by Verse with Nate.
Unsubscribe
“Your word is a lamp to my feet and a light to my path.” — Psalm 119:105
|
|
|
|
`,
}
}
export function buildContactAdminNotificationTemplate({
normalizedMessageType,
trimmedName,
trimmedEmail,
submittedAt,
trimmedMessage,
}) {
return {
subject: `Verse by Verse contact (${normalizedMessageType}): ${trimmedName}`,
text:
`New contact form submission\n\n` +
`Message Type: ${normalizedMessageType}\n` +
`Name: ${trimmedName}\n` +
`Email: ${trimmedEmail}\n` +
`Submitted: ${submittedAt}\n\n` +
`Message:\n${trimmedMessage}`,
html:
`` +
`
` +
`
` +
`
Verse by Verse with Nate
` +
`
New Contact Form Submission
` +
`
` +
`
` +
`
A new message was sent from the website contact form. Reply directly to this email to respond to ${escapeHtml(trimmedName)}.
` +
`
` +
`| Type | ${escapeHtml(normalizedMessageType)} |
` +
`| Name | ${escapeHtml(trimmedName)} |
` +
`| Email | ${escapeHtml(trimmedEmail)} |
` +
`| Submitted | ${escapeHtml(submittedAt)} |
` +
`
` +
`
` +
`
Message
` +
`
${escapeHtml(trimmedMessage)}
` +
`
` +
`
` +
`
` +
`
`,
}
}
export function buildAdminReplyTemplate({ recipientName, message }) {
const safeRecipientName = escapeHtml(recipientName || 'friend')
const safeMessage = escapeHtml(message).replace(/\n/g, '
')
return `
|
Verse by Verse with Nate
A Personal Reply
|
|
Hi ${safeRecipientName},
${safeMessage}
Grace and peace, Verse by Verse with Nate
|
|
From: hello@versebyversewithnate.us
|
|
`
}
export function buildQuestionAnsweredEmailTemplate({ firstName, question, answer, questionUrl }) {
const safeName = escapeHtml(firstName || 'friend')
const safeQuestion = escapeHtml(question)
const safeAnswer = escapeHtml(answer.length > 600 ? answer.slice(0, 597) + '…' : answer).replace(/\n/g, '
')
const safeUrl = escapeHtml(questionUrl)
const text = `Hi ${firstName || 'friend'},\n\nYour question has been answered on Verse by Verse with Nate.\n\nYour question: ${question}\n\nAnswer: ${answer}\n\nRead it at: ${questionUrl}\n\nGrace and peace,\nNate`
const html = `
|
Verse by Verse with Nate
Your Question Was Answered
|
|
Hi ${safeName},
Nate has answered your question on Verse by Verse with Nate.
Your question
${safeQuestion}
Read Full Answer →
|
|
You received this because you requested a notification when this question was answered.
|
|
`
return { subject: 'Your question has been answered — Verse by Verse with Nate', text, html }
}
// ── Transactional email senders ────────────────────────────────────────────
export async function sendStudyWelcomeEmail(email, displayName) {
if (!process.env.RESEND_API_KEY) return
try {
const resend = new Resend(process.env.RESEND_API_KEY)
const cfg = state.cachedSiteContent ?? {}
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
const baseUrl = getCanonicalBaseUrl()
const accountUrl = buildAbsoluteUrl(baseUrl, '/study/account')
const subject = cfg.studyWelcomeEmailSubject?.trim() || 'Welcome to the Study Community'
const bodyText = cfg.studyWelcomeEmailBody?.trim() || 'Your student account is ready. Open your studies and continue learning, or manage your account details anytime.'
const ctaLabel = cfg.studyWelcomeEmailCtaLabel?.trim() || 'Open Studies'
const studiesUrl = buildAbsoluteUrl(baseUrl, cfg.studyWelcomeEmailCtaPath?.trim() || '/study')
const signoff = cfg.studyWelcomeEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
const bodyHtml = (
`Welcome, ${escapeHtml(namePart)}.
` +
`${escapeHtml(bodyText)}
`
)
const footerHtml = `${escapeHtml(signoff).replace(/\n/g, '
')}
`
const { error } = await resend.emails.send({
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
to: [email],
subject,
text: `Welcome, ${namePart}.\n\n${bodyText}\n\nOpen studies: ${studiesUrl}\nManage account: ${accountUrl}\n\n${signoff}`,
html: buildBrandedEmailHtml({
title: 'Welcome to the Study Community',
eyebrow: 'Study Account',
bodyHtml,
ctaLabel,
ctaUrl: studiesUrl,
footerHtml: footerHtml + `Manage your account
`,
}),
})
if (error) console.error('[study-signup] welcome email send error:', error)
} catch (err) {
console.error('[study-signup] welcome email exception:', err)
}
}
export async function sendEmailOtp(email, code) {
if (!process.env.RESEND_API_KEY) return
try {
const resend = new Resend(process.env.RESEND_API_KEY)
const cfg = state.cachedSiteContent ?? {}
const subject = cfg.twoFaOtpEmailSubject?.trim() || 'Your sign-in code — Verse by Verse with Nate'
const bodyText = cfg.twoFaOtpEmailBody?.trim() || 'Your two-factor sign-in code is below. Enter it to complete sign-in.'
const expiryText = cfg.twoFaOtpEmailExpiry?.trim() || 'This code expires in 10 minutes. If you did not request this, you can ignore this message.'
await resend.emails.send({
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
to: [email],
subject,
text: `${bodyText}\n\n${code}\n\n${expiryText}\n\nVerse by Verse with Nate`,
html: buildBrandedEmailHtml({
title: 'Your Sign-In Code',
eyebrow: 'Account Security',
bodyHtml:
`${escapeHtml(bodyText)}
` +
`${code}
` +
`${escapeHtml(expiryText)}
`,
footerHtml: `Verse by Verse with Nate
`,
}),
})
} catch (err) {
console.error('[email-otp] send error:', err)
}
}
export async function sendStudyAccountDeletedEmail(email, displayName) {
if (!process.env.RESEND_API_KEY) return
try {
const resend = new Resend(process.env.RESEND_API_KEY)
const cfg = state.cachedSiteContent ?? {}
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
const baseUrl = getCanonicalBaseUrl()
const subject = cfg.studyDeletedEmailSubject?.trim() || 'Your study account was deleted'
const bodyText = cfg.studyDeletedEmailBody?.trim() || 'This confirms your study account and saved notes were deleted. If this was not you, please contact us immediately.'
const ctaLabel = cfg.studyDeletedEmailCtaLabel?.trim() || 'Create a New Account'
const signupUrl = buildAbsoluteUrl(baseUrl, cfg.studyDeletedEmailCtaPath?.trim() || '/study/signup')
const signoff = cfg.studyDeletedEmailSignoff?.trim() || 'Verse by Verse with Nate'
const bodyHtml = (
`Hi ${escapeHtml(namePart)},
` +
`${escapeHtml(bodyText)}
`
)
const footerHtml = `${escapeHtml(signoff).replace(/\n/g, '
')}
`
const { error } = await resend.emails.send({
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
to: [email],
subject,
text: `Hi ${namePart},\n\n${bodyText}\n\nCreate a new account anytime: ${signupUrl}\n\n${signoff}`,
html: buildBrandedEmailHtml({
title: 'Study Account Deleted',
eyebrow: 'Account Update',
bodyHtml,
ctaLabel,
ctaUrl: signupUrl,
footerHtml,
}),
})
if (error) console.error('[study-account] delete email send error:', error)
} catch (err) {
console.error('[study-account] delete email exception:', err)
}
}
export async function sendStudyReminderEmail(email, displayName, studyTitle, sectionTitle, sectionReference, sectionUrl) {
if (!process.env.RESEND_API_KEY) return
try {
const resend = new Resend(process.env.RESEND_API_KEY)
const cfg = state.cachedSiteContent ?? {}
const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend'
const subjectPrefix = cfg.studyReminderEmailSubjectPrefix?.trim() || 'New lesson available:'
const bodyText = cfg.studyReminderEmailBody?.trim() || 'A new lesson has been unlocked in your study track. Open it below to continue where you left off.'
const ctaLabel = cfg.studyReminderEmailCtaLabel?.trim() || 'Open the Lesson'
const signoff = cfg.studyReminderEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
const subject = process.env.RESEND_REMINDER_SUBJECT ?? `${subjectPrefix} ${sectionTitle}`
const bodyHtml = (
`Hi ${escapeHtml(namePart)},
` +
`${escapeHtml(bodyText)}
` +
`${escapeHtml(sectionTitle)} (${escapeHtml(sectionReference)}) — ${escapeHtml(studyTitle)}
`
)
const footerHtml = `${escapeHtml(signoff).replace(/\n/g, '
')}
`
const { error } = await resend.emails.send({
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
to: [email],
subject,
text: `Hi ${namePart},\n\n${bodyText}\n\n${sectionTitle} (${sectionReference}) — ${studyTitle}\n\nOpen it here: ${sectionUrl}\n\n${signoff}`,
html: buildBrandedEmailHtml({
title: 'New Lesson Available',
eyebrow: 'Study Reminder',
bodyHtml,
ctaLabel,
ctaUrl: sectionUrl,
footerHtml,
}),
})
if (error) console.error('[study-reminder] send error:', error)
} catch (err) {
console.error('[study-reminder] send exception:', err)
}
}