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 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)}

${escapeHtml(welcomeSpotifyBtnLabel)} ${escapeHtml(welcomeAppleBtnLabel)}

What to expect

${escapeHtml(welcomeExpect1)}

${escapeHtml(welcomeExpect2)}

${escapeHtml(welcomeExpect3)}

“${escapeHtml(welcomeScripture)}”

${escapeHtml(welcomeScriptureRef)}

Find the podcast on

Spotify · Apple Podcasts · Amazon Music · Website

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, signature }) { const safeRecipientName = escapeHtml(recipientName || 'friend') const safeMessage = escapeHtml(message).replace(/\n/g, '
') const sig = typeof signature === 'string' && signature.trim() ? signature.trim() : 'Grace and peace,\nVerse by Verse with Nate' const safeSig = escapeHtml(sig).replace(/\n/g, '
') return `
Verse by Verse with Nate

A Personal Reply

Hi ${safeRecipientName},

${safeMessage}

${safeSig}

From: hello@versebyversewithnate.us

` } export function buildQuestionAnsweredEmailTemplate({ firstName, question, answer, questionUrl }) { const cfg = state.cachedSiteContent ?? {} const subject = cfg.qaAnsweredEmailSubject?.trim() || 'Your question has been answered — Verse by Verse with Nate' const bodyText = cfg.qaAnsweredEmailBody?.trim() || 'Nate has answered your question on Verse by Verse with Nate.' const ctaLabel = cfg.qaAnsweredEmailCtaLabel?.trim() || 'Read Full Answer →' const signoff = cfg.qaAnsweredEmailSignoff?.trim() || 'Grace and peace,\nNate' 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 safeBody = escapeHtml(bodyText) const safeSignoff = escapeHtml(signoff).replace(/\n/g, '
') const text = `Hi ${firstName || 'friend'},\n\n${bodyText}\n\nYour question: ${question}\n\nAnswer: ${answer}\n\nRead it at: ${questionUrl}\n\n${signoff}` const html = `
Verse by Verse with Nate

Your Question Was Answered

Hi ${safeName},

${safeBody}

Your question

${safeQuestion}

Answer

${safeAnswer}

${escapeHtml(ctaLabel)}

${safeSignoff}

You received this because you requested a notification when this question was answered.

` return { subject, 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) } } const REENGAGEMENT_COPY = { '7d': { subject: 'Your study is waiting for you', eyebrow: 'Come Back', headline: 'Pick up where you left off', body: "It's been a week — your progress is saved and your next lesson is ready whenever you are.", cta: 'Continue Studying', }, '14d': { subject: "Don't lose your momentum", eyebrow: 'Still With You', headline: 'Your spot is still saved', body: 'Two weeks have passed, but every note and completed lesson is still right there. A little each day adds up.', cta: 'Return to Your Study', }, '30d': { subject: 'Your progress is still here', eyebrow: 'We Saved Your Spot', headline: "It's been a month — come back anytime", body: 'Your study progress is still intact and waiting. There\'s no deadline — come back whenever you\'re ready.', cta: 'Open Your Study', }, } export async function sendStudyReengagementEmail(email, displayName, studyTitle, studyUrl, tier, unsubUrl) { if (!process.env.RESEND_API_KEY) return const copy = REENGAGEMENT_COPY[tier] if (!copy) return try { const resend = new Resend(process.env.RESEND_API_KEY) const namePart = typeof displayName === 'string' && displayName.trim() ? displayName.trim() : 'friend' const subject = `${copy.subject}${studyTitle ? ` — ${studyTitle}` : ''}` const bodyHtml = ( `

Hi ${escapeHtml(namePart)},

` + `

${escapeHtml(copy.body)}${studyTitle ? ` Your current study: ${escapeHtml(studyTitle)}.` : ''}

` ) const unsubLine = unsubUrl ? `

Not interested? Unsubscribe from these reminders.

` : '' const footerHtml = `

Grace and peace,
Verse by Verse with Nate

${unsubLine}` const { error } = await resend.emails.send({ from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM, to: [email], subject, text: `Hi ${namePart},\n\n${copy.body}${studyTitle ? ` Your current study: ${studyTitle}.` : ''}\n\n${studyUrl}\n\nGrace and peace,\nVerse by Verse with Nate${unsubUrl ? `\n\nUnsubscribe: ${unsubUrl}` : ''}`, html: buildBrandedEmailHtml({ title: copy.headline, eyebrow: copy.eyebrow, bodyHtml, ctaLabel: copy.cta, ctaUrl: studyUrl, footerHtml, }), }) if (error) console.error('[study-reengagement] send error:', error) } catch (err) { console.error('[study-reengagement] send exception:', err) } }