feat: add email diagnostics

This commit is contained in:
unknown
2026-07-16 17:50:58 +09:00
parent 9c4dac9a70
commit 6180161225
7 changed files with 212 additions and 2 deletions

123
server/services/mail.ts Normal file
View File

@@ -0,0 +1,123 @@
import net from 'node:net';
import { env } from '../config/env.js';
export interface EmailDiagnosticsResult {
host: string;
port: number;
from: string;
smtpAuth: boolean;
startTls: boolean;
usernameConfigured: boolean;
passwordConfigured: boolean;
ok: boolean;
reachable: boolean;
elapsedMs: number;
checkedAt: string;
error: string | null;
}
function mailHost(): string {
return String(process.env.ACS_MAIL_HOST ?? env.mailHost ?? '').trim();
}
function mailPort(): number {
const port = Number(process.env.ACS_MAIL_PORT ?? env.mailPort);
return Number.isFinite(port) && port > 0 ? port : 25;
}
function mailFrom(): string {
return String(process.env.ACS_MAIL_FROM ?? env.mailFrom ?? '').trim();
}
function mailUsername(): string {
return String(process.env.ACS_MAIL_USERNAME ?? env.mailUsername ?? '').trim();
}
function mailPassword(): string {
return String(process.env.ACS_MAIL_PASSWORD ?? env.mailPassword ?? '').trim();
}
function mailSmtpAuth(): boolean {
return (process.env.ACS_MAIL_SMTP_AUTH ?? String(env.mailSmtpAuth)).toLowerCase() === 'true';
}
function mailStartTls(): boolean {
return (process.env.ACS_MAIL_STARTTLS ?? String(env.mailStartTls)).toLowerCase() === 'true';
}
function mailTimeoutMs(): number {
const timeout = Number(process.env.ACS_MAIL_TIMEOUT_MS ?? env.mailTimeoutMs);
return Number.isFinite(timeout) && timeout > 0 ? timeout : 10000;
}
function result(partial: Partial<EmailDiagnosticsResult>): EmailDiagnosticsResult {
return {
host: mailHost(),
port: mailPort(),
from: mailFrom(),
smtpAuth: mailSmtpAuth(),
startTls: mailStartTls(),
usernameConfigured: Boolean(mailUsername()),
passwordConfigured: Boolean(mailPassword()),
ok: false,
reachable: false,
elapsedMs: 0,
checkedAt: new Date().toISOString(),
error: null,
...partial,
};
}
function connectSmtp(host: string, port: number, timeoutMs: number): Promise<void> {
return new Promise((resolve, reject) => {
const socket = net.createConnection({ host, port });
let settled = false;
const finish = (error?: Error) => {
if (settled) return;
settled = true;
socket.destroy();
if (error) reject(error);
else resolve();
};
socket.setTimeout(timeoutMs);
socket.once('connect', () => finish());
socket.once('timeout', () => finish(new Error(`SMTP connection timed out after ${timeoutMs} ms`)));
socket.once('error', (error) => finish(error));
});
}
export async function diagnoseEmailConnectivity(): Promise<EmailDiagnosticsResult> {
const host = mailHost();
const port = mailPort();
const startedAt = Date.now();
if (!host) {
return result({
ok: false,
reachable: false,
checkedAt: new Date().toISOString(),
error: 'ACS_MAIL_HOST가 설정되지 않았습니다.',
});
}
try {
await connectSmtp(host, port, mailTimeoutMs());
return result({
ok: true,
reachable: true,
elapsedMs: Date.now() - startedAt,
checkedAt: new Date().toISOString(),
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return result({
ok: false,
reachable: false,
elapsedMs: Date.now() - startedAt,
checkedAt: new Date().toISOString(),
error: message.slice(0, 500),
});
}
}