Files
platform/src/servers/api/server-settings/smtp.ts
T
2026-02-24 23:29:41 +00:00

134 lines
4.4 KiB
TypeScript

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<SmtpConfig>();
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<SmtpConfig & { to: string }>();
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 = '<h2>Officer Test Email</h2><p>If you received this, your email configuration is working correctly.</p>';
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);
}
});