Fix highlighting and all rich text formatting in emails by converting HTML tags to inline styles before sending. Email clients strip <style> blocks, so inline styles are the only reliable method. Changes: - Convert <mark> tags to <span> with inline background-color (#fef08a) - Add inline styles to all rich text elements (strong, em, u, s, headings, lists) - Process HTML conversion in email API before sending - Simplified email template by removing unreliable <style> block - All formatting now uses inline styles directly on elements This ensures highlights and all other formatting (bold, italic, underline, strikethrough, headings, lists) render correctly across all email clients including Gmail, Outlook, Apple Mail, etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
197 lines
7.4 KiB
TypeScript
197 lines
7.4 KiB
TypeScript
import nodemailer from 'nodemailer'
|
|
import crypto from 'crypto'
|
|
import type { H3Event } from 'h3'
|
|
|
|
/**
|
|
* Get email configuration from runtime config or environment variables
|
|
* In production, environment variables are accessed directly for reliability
|
|
*/
|
|
function getEmailConfig(event?: H3Event) {
|
|
// In production, always use environment variables directly
|
|
// This ensures Docker runtime env vars are used, not build-time values
|
|
const config = {
|
|
emailHost: process.env.EMAIL_HOST || 'smtp.example.com',
|
|
emailPort: process.env.EMAIL_PORT || '587',
|
|
emailUser: process.env.EMAIL_USER || 'noreply@example.com',
|
|
emailPassword: process.env.EMAIL_PASSWORD || '',
|
|
emailFrom: process.env.EMAIL_FROM || 'New Life Christian Church <noreply@example.com>',
|
|
}
|
|
|
|
// Debug: Log what we're reading from environment
|
|
console.log('[EMAIL DEBUG] Reading from process.env:')
|
|
console.log('[EMAIL DEBUG] EMAIL_HOST env var:', process.env.EMAIL_HOST)
|
|
console.log('[EMAIL DEBUG] EMAIL_PORT env var:', process.env.EMAIL_PORT)
|
|
console.log('[EMAIL DEBUG] EMAIL_USER env var:', process.env.EMAIL_USER)
|
|
console.log('[EMAIL DEBUG] EMAIL_FROM env var:', process.env.EMAIL_FROM)
|
|
|
|
return config
|
|
}
|
|
|
|
export async function sendPasswordResetEmail(email: string, code: string, event?: H3Event) {
|
|
const config = getEmailConfig(event)
|
|
|
|
// Debug logging for email configuration
|
|
console.log('[EMAIL CONFIG] Host:', config.emailHost)
|
|
console.log('[EMAIL CONFIG] Port:', config.emailPort)
|
|
console.log('[EMAIL CONFIG] User:', config.emailUser)
|
|
console.log('[EMAIL CONFIG] From:', config.emailFrom)
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: config.emailHost,
|
|
port: parseInt(config.emailPort),
|
|
secure: parseInt(config.emailPort) === 465,
|
|
auth: {
|
|
user: config.emailUser,
|
|
pass: config.emailPassword,
|
|
},
|
|
})
|
|
|
|
const mailOptions = {
|
|
from: config.emailFrom,
|
|
to: email,
|
|
subject: 'Password Reset Code - New Life Christian Church',
|
|
text: `Please enter this code to reset your password for the New Life Christian Church sermon page: ${code}\n\nThis code will expire in 15 minutes.\n\nIf you did not request a password reset, please ignore this email.`,
|
|
html: `
|
|
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<h2 style="color: #333; border-bottom: 2px solid #4CAF50; padding-bottom: 10px;">Password Reset Request</h2>
|
|
<p style="font-size: 16px; color: #555;">Please enter this code to reset your password for the New Life Christian Church sermon page:</p>
|
|
<div style="background-color: #f4f4f4; padding: 25px; text-align: center; font-size: 36px; font-weight: bold; letter-spacing: 8px; margin: 30px 0; border-radius: 8px; border: 2px solid #4CAF50; font-family: 'Courier New', monospace;">
|
|
${code}
|
|
</div>
|
|
<p style="color: #666; font-size: 14px; background-color: #fff3cd; padding: 12px; border-radius: 5px; border-left: 4px solid #ffc107;">
|
|
⏱️ This code will expire in <strong>15 minutes</strong>.
|
|
</p>
|
|
<p style="color: #666; font-size: 14px; margin-top: 20px;">
|
|
If you did not request a password reset, please ignore this email. Your password will not be changed.
|
|
</p>
|
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #999; font-size: 12px; text-align: center;">
|
|
<p>New Life Christian Church</p>
|
|
</div>
|
|
</div>
|
|
`,
|
|
}
|
|
|
|
await transporter.sendMail(mailOptions)
|
|
}
|
|
|
|
/**
|
|
* Generate a cryptographically secure password reset code
|
|
*
|
|
* Format: 8-character alphanumeric code (0-9, A-Z)
|
|
* Character set: 36 characters (10 digits + 26 uppercase letters)
|
|
* Total combinations: 36^8 = 2,821,109,907,456 (2.8 trillion)
|
|
*
|
|
* Security improvements over 6-digit numeric:
|
|
* - 6-digit numeric: 1,000,000 combinations
|
|
* - 8-char alphanumeric: 2,821,109,907,456 combinations
|
|
* - 2.8 million times more secure
|
|
*
|
|
* Why this is secure:
|
|
* - Uses crypto.randomInt() for cryptographic randomness
|
|
* - Case-insensitive for better user experience (uppercase only)
|
|
* - Excludes confusing characters like O/0, I/1 for better UX
|
|
* - Still fits well in emails and is easy to type
|
|
*/
|
|
export function generateResetCode(): string {
|
|
// Character set: uppercase letters and numbers (excluding confusing chars)
|
|
// Excluded: I, O (look like 1, 0)
|
|
const chars = '0123456789ABCDEFGHJKLMNPQRSTUVWXYZ' // 34 chars (removed I, O)
|
|
|
|
let code = ''
|
|
for (let i = 0; i < 8; i++) {
|
|
const randomIndex = crypto.randomInt(chars.length)
|
|
code += chars[randomIndex]
|
|
}
|
|
|
|
return code
|
|
}
|
|
|
|
export async function sendSermonNotesEmail(
|
|
email: string,
|
|
firstName: string,
|
|
sermonTitle: string,
|
|
sermonDate: string,
|
|
bibleReferences: string,
|
|
personalAppliance: string,
|
|
pastorsChallenge: string,
|
|
userNotes: string,
|
|
event?: H3Event
|
|
) {
|
|
const config = getEmailConfig(event)
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: config.emailHost,
|
|
port: parseInt(config.emailPort),
|
|
secure: parseInt(config.emailPort) === 465,
|
|
auth: {
|
|
user: config.emailUser,
|
|
pass: config.emailPassword,
|
|
},
|
|
})
|
|
|
|
const mailOptions = {
|
|
from: config.emailFrom,
|
|
to: email,
|
|
subject: `Sermon Notes: ${sermonTitle}`,
|
|
text: `
|
|
Sermon Notes for ${firstName}
|
|
|
|
Title: ${sermonTitle}
|
|
Date: ${sermonDate}
|
|
|
|
Bible References:
|
|
${bibleReferences}
|
|
|
|
Personal Appliance:
|
|
${personalAppliance}
|
|
|
|
Pastor's Challenge:
|
|
${pastorsChallenge}
|
|
|
|
My Notes:
|
|
${userNotes || 'No notes taken'}
|
|
`,
|
|
html: `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<body style="font-family: Arial, sans-serif; margin: 0; padding: 0;">
|
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
|
|
<h1 style="color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px; margin-top: 0;">Sermon Notes</h1>
|
|
|
|
<div style="margin: 20px 0;">
|
|
<h2 style="color: #4CAF50; margin-bottom: 5px; font-size: 1.5em;">${sermonTitle}</h2>
|
|
<p style="color: #666; margin-top: 0;">${sermonDate}</p>
|
|
</div>
|
|
|
|
<div style="background-color: #E3F2FD; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<h3 style="color: #1976D2; margin-top: 0; font-size: 1.25em;">Bible References</h3>
|
|
<div style="color: #333;">${bibleReferences}</div>
|
|
</div>
|
|
|
|
<div style="background-color: #E8F5E9; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<h3 style="color: #388E3C; margin-top: 0; font-size: 1.25em;">Personal Appliance</h3>
|
|
<div style="color: #333; white-space: pre-wrap;">${personalAppliance}</div>
|
|
</div>
|
|
|
|
<div style="background-color: #F3E5F5; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<h3 style="color: #7B1FA2; margin-top: 0; font-size: 1.25em;">Pastor's Challenge</h3>
|
|
<div style="color: #333; white-space: pre-wrap;">${pastorsChallenge}</div>
|
|
</div>
|
|
|
|
<div style="background-color: #FFF9C4; padding: 15px; border-radius: 5px; margin: 20px 0;">
|
|
<h3 style="color: #F57F17; margin-top: 0; font-size: 1.25em;">My Notes</h3>
|
|
<div style="color: #333;">${userNotes || '<em>No notes taken</em>'}</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; font-size: 12px;">
|
|
<p>This email was sent from New Life Christian Church.</p>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
`,
|
|
}
|
|
|
|
await transporter.sendMail(mailOptions)
|
|
}
|