SMTP settings
This commit is contained in:
@@ -9,6 +9,7 @@ import { opencodeRouter } from './opencode';
|
||||
import { piMonoRouter } from './pi-mono';
|
||||
import { applicationsRouter } from './applications';
|
||||
import { resourcesRouter } from './resources';
|
||||
import { smtpRouter } from './smtp';
|
||||
|
||||
const configDir = `${homedir()}/.config/officer.dev`;
|
||||
export const settingsPath = `${configDir}/server-settings.json`;
|
||||
@@ -26,6 +27,7 @@ serverSettingsRouter.route('/opencode', opencodeRouter);
|
||||
serverSettingsRouter.route('/pi-mono', piMonoRouter);
|
||||
serverSettingsRouter.route('/applications', applicationsRouter);
|
||||
serverSettingsRouter.route('/resources', resourcesRouter);
|
||||
serverSettingsRouter.route('/smtp', smtpRouter);
|
||||
|
||||
serverSettingsRouter.get('/settings', async (ctx) => {
|
||||
const settings = await Bun.file(settingsPath).json();
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
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 === 'resend') {
|
||||
return `smtps://resend:${config.apiKey}@smtp.resend.com:465`;
|
||||
}
|
||||
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 { transport } = await getTransport();
|
||||
await 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;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = buildTransportUrl(body);
|
||||
const transport = createTransport(url);
|
||||
const from = `${body.fromName} <${body.fromEmail}>`;
|
||||
|
||||
await transport.sendMail({
|
||||
from,
|
||||
to: body.to,
|
||||
subject: 'Officer Test Email',
|
||||
html: '<h2>Officer Test Email</h2><p>If you received this, your email configuration is working correctly.</p>',
|
||||
});
|
||||
return ctx.json({ success: true });
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
return ctx.json({ error: message }, 500);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user