Files
nlcc-itinerary/server/utils/email.ts
Joshua Ryder 47b4a14c4b fix: Ensure email configuration from environment variables is properly used
Fixed an issue where SMTP configuration would fall back to defaults despite
environment variables being set in docker-compose.yml. The email utility now
properly accesses runtime configuration by accepting the H3 event context.

Changes:
- Created getEmailConfig() helper with dual-strategy config access
- Pass event context from API handlers to email functions
- Added fallback to direct process.env access for reliability
- Added debug logging to diagnose configuration issues in production

This ensures Office365 and other SMTP providers work correctly when configured
via environment variables.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-05 18:11:13 -05:00

198 lines
7.1 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) {
// Try to use runtime config if event is provided
if (event) {
try {
const config = useRuntimeConfig(event)
return {
emailHost: config.emailHost,
emailPort: config.emailPort,
emailUser: config.emailUser,
emailPassword: config.emailPassword,
emailFrom: config.emailFrom,
}
} catch (e) {
console.warn('[EMAIL] Failed to access runtime config, falling back to env vars')
}
}
// Fallback to direct environment variable access
return {
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>',
}
}
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: `
<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; white-space: pre-wrap;">${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>
`,
}
await transporter.sendMail(mailOptions)
}