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>
198 lines
7.1 KiB
TypeScript
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)
|
|
}
|