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

View File

@@ -22,6 +22,14 @@ export const env = {
smsApiKey: process.env.ACS_SMS_API_KEY,
smsSender: process.env.ACS_SMS_SENDER,
smsTimeoutMs: Number(process.env.ACS_SMS_TIMEOUT_MS ?? 10000),
mailHost: process.env.ACS_MAIL_HOST,
mailPort: Number(process.env.ACS_MAIL_PORT ?? 25),
mailFrom: process.env.ACS_MAIL_FROM,
mailUsername: process.env.ACS_MAIL_USERNAME,
mailPassword: process.env.ACS_MAIL_PASSWORD,
mailSmtpAuth: (process.env.ACS_MAIL_SMTP_AUTH ?? '').toLowerCase() === 'true',
mailStartTls: (process.env.ACS_MAIL_STARTTLS ?? '').toLowerCase() === 'true',
mailTimeoutMs: Number(process.env.ACS_MAIL_TIMEOUT_MS ?? 10000),
cookieSecure:
(process.env.ACS_COOKIE_SECURE ?? '').toLowerCase() === 'true' ||
(process.env.ACS_PUBLIC_BASE_URL ?? '').startsWith('https://'),

View File

@@ -5,6 +5,7 @@ import ExcelJS from 'exceljs';
import { ok } from '../http/apiResponse.js';
import { ApiError } from '../http/errors.js';
import { query } from '../db/pool.js';
import { diagnoseEmailConnectivity } from '../services/mail.js';
import { diagnoseSmsConnectivity } from '../services/sms.js';
import { parseRole, parseRoles, requireAdmin, requireCurrentUser, type QueryFn, type UserRow } from './security.js';
@@ -475,6 +476,11 @@ export function createAdminRouter(deps: AdminRouterDeps = { query }): Router {
ok(res, await diagnoseSmsConnectivity());
}));
router.post('/email/diagnostics', asyncRoute(async (req, res) => {
await requireAdmin(dbQuery, req);
ok(res, await diagnoseEmailConnectivity());
}));
router.get('/teams', asyncRoute(async (req, res) => {
await requireAdmin(dbQuery, req);
ok(res, await listTeams(dbQuery));

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),
});
}
}