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, 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}
${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_DEFAULTS = {
t1: { subject: 'Your study is waiting for you', body: "It's been a while — your progress is saved and your next lesson is ready whenever you are.", cta: 'Continue Studying' },
t2: { subject: "Don't lose your momentum", body: 'Every note and completed lesson is still right there. A little each day adds up — come back and continue.', cta: 'Return to Your Study' },
t3: { subject: 'Your progress is still here', body: "Your study progress is still intact and waiting. There's no deadline — come back whenever you're ready.", cta: 'Open Your Study' },
}
function getReengagementCopy(tier) {
const cfg = state.cachedSiteContent ?? {}
const t = tier === 't1' ? '1' : tier === 't2' ? '2' : tier === 't3' ? '3' : null
const def = REENGAGEMENT_DEFAULTS[tier] ?? REENGAGEMENT_DEFAULTS.t1
if (!t) return def
return {
subject: cfg[`studyReengagementT${t}Subject`]?.trim() || def.subject,
body: cfg[`studyReengagementT${t}Body`]?.trim() || def.body,
cta: cfg[`studyReengagementT${t}Cta`]?.trim() || def.cta,
}
}
export async function sendStudyReengagementEmail(email, displayName, studyTitle, studyUrl, tier, unsubUrl) {
if (!process.env.RESEND_API_KEY) return
try {
const resend = new Resend(process.env.RESEND_API_KEY)
const copy = getReengagementCopy(tier)
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.subject,
eyebrow: 'Study Reminder',
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)
}
}
export async function sendStudyCertificateEmail(email, displayName, studyTitle, certUrl) {
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 subject = cfg.studyCertificateEmailSubject?.trim() || 'Your certificate is ready'
const bodyText = cfg.studyCertificateEmailBody?.trim() || 'Congratulations — you have completed every lesson in this study. Your certificate is ready to view and share.'
const ctaLabel = cfg.studyCertificateEmailCtaLabel?.trim() || 'View Certificate'
const signoff = cfg.studyCertificateEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
const bodyHtml = (
`Congratulations, ${escapeHtml(namePart)}!
` +
`${escapeHtml(bodyText)}
` +
`${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: `Congratulations, ${namePart}!\n\n${bodyText}\n\n${studyTitle}\n\nView your certificate: ${certUrl}\n\n${signoff}`,
html: buildBrandedEmailHtml({
title: 'Study Complete',
eyebrow: 'Certificate of Completion',
bodyHtml,
ctaLabel,
ctaUrl: certUrl,
footerHtml,
}),
})
if (error) console.error('[study-certificate] email send error:', error)
} catch (err) {
console.error('[study-certificate] email exception:', err)
}
}
export async function sendStudyWeeklyDigestEmail(email, displayName, lessons, unsubUrl) {
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 subject = cfg.studyWeeklyDigestEmailSubject?.trim() || 'Your weekly study digest'
const introText = cfg.studyWeeklyDigestEmailBody?.trim() || 'Here are the lessons released this week in your study track. Open any to continue where you left off.'
const signoff = cfg.studyWeeklyDigestEmailSignoff?.trim() || 'Grace and peace,\nVerse by Verse with Nate'
const lessonListHtml = lessons.map(l =>
`${escapeHtml(l.sectionTitle)}` +
(l.sectionReference ? ` (${escapeHtml(l.sectionReference)})` : '') +
(l.studyTitle ? ` — ${escapeHtml(l.studyTitle)}` : '') +
`
`
).join('')
const lessonListText = lessons.map(l => `• ${l.sectionTitle}${l.sectionReference ? ` (${l.sectionReference})` : ''}: ${l.sectionUrl}`).join('\n')
const bodyHtml = (
`Hi ${escapeHtml(namePart)},
` +
`${escapeHtml(introText)}
` +
`${lessonListHtml}
`
)
const unsubLine = unsubUrl ? `Not interested? Unsubscribe from these reminders.
` : ''
const footerHtml = `${escapeHtml(signoff).replace(/\n/g, '
')}
${unsubLine}`
const { error } = await resend.emails.send({
from: process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM,
to: [email],
subject,
text: `Hi ${namePart},\n\n${introText}\n\n${lessonListText}\n\n${signoff}${unsubUrl ? `\n\nUnsubscribe: ${unsubUrl}` : ''}`,
html: buildBrandedEmailHtml({
title: 'Weekly Study Digest',
eyebrow: 'Study Reminder',
bodyHtml,
footerHtml,
}),
})
if (error) console.error('[study-digest] send error:', error)
} catch (err) {
console.error('[study-digest] send exception:', err)
}
}
// ── Nuggets upload notifications ───────────────────────────────────────────
export async function sendNuggetsUploadNotification({ to, source, hostName, episodeName, passage, description, filename, infoFile }) {
if (!process.env.RESEND_API_KEY) return
const resend = new Resend(process.env.RESEND_API_KEY)
const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
const sourceLabel = source === 'portal' ? 'Upload Portal' : 'Direct to Seafile'
const subject = `New Nuggets Episode Upload: ${episodeName || filename}`
const bodyHtml = `
A new episode file has arrived in Seafile.
| Source | ${escapeHtml(sourceLabel)} |
${hostName ? `| Host | ${escapeHtml(hostName)} |
` : ''}
${episodeName ? `| Episode | ${escapeHtml(episodeName)} |
` : ''}
${passage ? `| Passage | ${escapeHtml(passage)} |
` : ''}
| Audio File | ${escapeHtml(filename)} |
${infoFile ? `| Info File | ${escapeHtml(infoFile)} |
` : ''}
${description ? `Description:
${escapeHtml(description).replace(/\n/g, '
')}
` : ''}
`
const textBody = [
`New Nuggets Episode Upload (${sourceLabel})`,
'',
hostName ? `Host: ${hostName}` : '',
episodeName ? `Episode: ${episodeName}` : '',
passage ? `Passage: ${passage}` : '',
`Audio File: ${filename}`,
infoFile ? `Info File: ${infoFile}` : '',
description ? `\nDescription:\n${description}` : '',
].filter(Boolean).join('\n')
try {
const { error } = await resend.emails.send({
from,
to: [to],
subject,
text: textBody,
html: buildBrandedEmailHtml({
title: 'New Episode Upload',
eyebrow: 'Nuggets',
bodyHtml,
footerHtml: 'This notification was sent because a file was uploaded to the Nuggets Seafile library.
',
}),
})
if (error) console.error('[nuggets-notify] send error:', error)
} catch (err) {
console.error('[nuggets-notify] send exception:', err)
}
}
export async function sendNuggetsHostWelcomeEmail({ to, name, username, password, portalUrl }) {
if (!process.env.RESEND_API_KEY || !to) return
const resend = new Resend(process.env.RESEND_API_KEY)
const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
const bodyHtml = `
Hi ${escapeHtml(name)},
You've been set up as a host on the Nuggets episode upload portal.
Use the details below to sign in and submit your episodes.
When you're ready to submit an episode, fill in the episode name, the Bible passage, a short description, and attach your audio file. Everything uploads in one step.
If you have any trouble signing in, reply to this email and we'll get you sorted.
`
try {
const { error } = await resend.emails.send({
from,
to: [to],
subject: 'Your Nuggets Host Upload Portal Access',
text: `Hi ${name},\n\nYou've been set up as a host on the Nuggets episode upload portal.\n\nPortal URL: ${portalUrl}\nUsername: ${username}\nPassword: ${password}\n\nWhen submitting an episode, fill in the episode name, Bible passage, a short description, and attach your audio file.\n\nIf you have any trouble, reply to this email.`,
html: buildBrandedEmailHtml({
title: 'Upload Portal Access',
eyebrow: 'Nuggets',
bodyHtml,
ctaLabel: 'Open Upload Portal',
ctaUrl: portalUrl,
footerHtml: 'You received this because an admin created an upload account for you on the Nuggets podcast portal.
',
}),
})
if (error) console.error('[nuggets-welcome] send error:', error)
} catch (err) {
console.error('[nuggets-welcome] send exception:', err)
}
}
export async function sendNuggetsHostOtpEmail({ to, name, code }) {
if (!process.env.RESEND_API_KEY || !to) return
const resend = new Resend(process.env.RESEND_API_KEY)
const from = process.env.RESEND_FROM ?? DEFAULT_RESEND_FROM
const bodyHtml = `
Hi ${escapeHtml(name)},
Your one-time sign-in code for the Nuggets host portal is below. It expires in 10 minutes.
${escapeHtml(code)}
If you did not request this code, you can safely ignore this email.
`
try {
const { error } = await resend.emails.send({
from,
to: [to],
subject: 'Your Nuggets Portal Sign-In Code',
text: `Hi ${name},\n\nYour one-time sign-in code is: ${code}\n\nIt expires in 10 minutes. If you did not request this, ignore this email.`,
html: buildBrandedEmailHtml({
title: 'Sign-In Code',
eyebrow: 'Nuggets',
bodyHtml,
footerHtml: 'You received this because a sign-in was attempted on the Nuggets podcast host portal.
',
}),
})
if (error) console.error('[nuggets-otp] send error:', error)
} catch (err) {
console.error('[nuggets-otp] send exception:', err)
}
}