Files
nlcc-itinerary/server/utils/email.ts
Joshua Ryder 1515fba6c9 fix: Improve rich text formatting in emails and add button hints
Fix highlighting display in emailed notes and add clear formatting hints to email/download buttons.

Changes:
- Added proper HTML/CSS structure to email template for rich text support
- Added CSS styles for mark (highlight), strong, em, u, s, headings, and lists
- Highlight now renders with yellow background (#fef08a) in emails
- All rich text formatting now properly displays in email clients
- Added formatting hints to buttons: "Email Notes (Formatting included)" and "Download Notes (No formatting)"
- Button hints use smaller text with opacity for subtle visual hierarchy

Email template improvements:
- Proper DOCTYPE and HTML structure
- Style block in head for rich text elements
- Removed white-space: pre-wrap from notes div to allow HTML rendering
- Maintained all existing sermon content styling

This ensures users understand that:
- Email preserves all rich text formatting (bold, italic, highlights, lists, etc.)
- Download converts to plain text for universal compatibility

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-07 10:16:31 -05:00

239 lines
8.2 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>
<head>
<style>
/* Rich text formatting styles */
mark {
background-color: #fef08a !important;
padding: 2px 4px;
border-radius: 2px;
}
strong {
font-weight: 700;
}
em {
font-style: italic;
}
u {
text-decoration: underline;
}
s {
text-decoration: line-through;
}
h2 {
font-size: 1.5em;
font-weight: 700;
margin: 1em 0 0.5em;
}
h3 {
font-size: 1.25em;
font-weight: 600;
margin: 0.75em 0 0.5em;
}
ul, ol {
padding-left: 1.5em;
margin: 0.5em 0;
}
li {
margin: 0.25em 0;
}
p {
margin: 0.5em 0;
}
</style>
</head>
<body>
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #333; border-bottom: 3px solid #4CAF50; padding-bottom: 10px;">Sermon Notes</h1>
<div style="margin: 20px 0;">
<h2 style="color: #4CAF50; margin-bottom: 5px;">${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;">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;">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;">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;">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)
}