import { createTransport } from 'nodemailer'; import { createRouter } from '../../create-router'; import { settingsPath } from './server-settings'; import { getTransport } from 'emailer'; type SmtpConfig = { provider: 'resend' | 'smtp' | 'mailhog'; apiKey?: string; host?: string; port?: number; username?: string; password?: string; secure?: boolean; fromName: string; fromEmail: string; }; function maskSecret(value: string | undefined): string | undefined { if (!value || value.length < 8) return value ? '****' : undefined; return value.slice(0, 4) + '****' + value.slice(-4); } function serializeConfig(smtp: SmtpConfig) { return { provider: smtp.provider, apiKey: maskSecret(smtp.apiKey), host: smtp.host, port: smtp.port, username: smtp.username, password: maskSecret(smtp.password), secure: smtp.secure, fromName: smtp.fromName, fromEmail: smtp.fromEmail, }; } function buildTransportUrl(config: SmtpConfig): string { if (config.provider === 'mailhog') { return `smtp://${config.host ?? 'localhost'}:${config.port ?? 1025}`; } const auth = config.username ? `${encodeURIComponent(config.username)}:${encodeURIComponent(config.password ?? '')}@` : ''; const protocol = config.secure ? 'smtps' : 'smtp'; return `${protocol}://${auth}${config.host}:${config.port ?? 587}`; } export const smtpRouter = createRouter(); smtpRouter.get('/', async (ctx) => { const settings = await Bun.file(settingsPath).json().catch(() => ({})); const smtp: SmtpConfig | undefined = settings.smtp; if (smtp) return ctx.json(serializeConfig(smtp)); return ctx.json(null); }); smtpRouter.put('/', async (ctx) => { const body = await ctx.req.json(); const settings = await Bun.file(settingsPath).json().catch(() => ({})); const existing: SmtpConfig | undefined = settings.smtp; if (existing) { if (body.apiKey && body.apiKey.includes('****')) body.apiKey = existing.apiKey; if (body.password && body.password.includes('****')) body.password = existing.password; } settings.smtp = body; await Bun.write(settingsPath, JSON.stringify(settings, null, 2)); return ctx.json({ success: true }); }); smtpRouter.post('/test-connection', async (ctx) => { try { const result = await getTransport(); if (result.type === 'resend') { const res = await fetch('https://api.resend.com/domains', { headers: { 'Authorization': `Bearer ${result.apiKey}` }, }); if (!res.ok) { const err = await res.json(); throw new Error(err.message ?? `Resend API error: ${res.status}`); } return ctx.json({ success: true }); } await result.transport.verify(); return ctx.json({ success: true }); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; return ctx.json({ error: message }, 500); } }); smtpRouter.post('/test', async (ctx) => { const body = await ctx.req.json(); if (!body.to) return ctx.json({ error: 'Recipient address required' }, 400); // Resolve masked secrets from saved config const settings = await Bun.file(settingsPath).json().catch(() => ({})); const saved: SmtpConfig | undefined = settings.smtp; if (saved) { if (body.apiKey?.includes('****')) body.apiKey = saved.apiKey; if (body.password?.includes('****')) body.password = saved.password; } const from = `${body.fromName} <${body.fromEmail}>`; const testHtml = '

Officer Test Email

If you received this, your email configuration is working correctly.

'; try { if (body.provider === 'resend') { const res = await fetch('https://api.resend.com/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${body.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ from, to: body.to, subject: 'Officer Test Email', html: testHtml }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.message ?? `Resend API error: ${res.status}`); } return ctx.json({ success: true }); } const url = buildTransportUrl(body); const transport = createTransport(url); await transport.sendMail({ from, to: body.to, subject: 'Officer Test Email', html: testHtml }); return ctx.json({ success: true }); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; return ctx.json({ error: message }, 500); } });