import { parseCsvList } from "./utils/strings.js";
export interface AccountConfig {
readonly accountId: string;
readonly host: string;
readonly port: number;
readonly secure: boolean;
readonly user: string;
readonly pass: string;
readonly defaultFrom?: string;
}
export interface PolicyConfig {
readonly sendEnabled: boolean;
readonly allowlistDomains: readonly string[];
readonly allowlistAddresses: readonly string[];
readonly maxRecipients: number;
readonly maxMessageBytes: number;
readonly maxAttachmentBytes: number;
readonly maxAttachments: number;
readonly maxTextChars: number;
readonly maxHtmlChars: number;
readonly connectTimeoutMs: number;
readonly socketTimeoutMs: number;
}
export interface ServerConfig {
readonly accounts: readonly AccountConfig[];
readonly policy: PolicyConfig;
}
const DEFAULT_MAX_RECIPIENTS = 10;
const DEFAULT_MAX_MESSAGE_BYTES = 2_500_000;
const DEFAULT_MAX_ATTACHMENTS = 5;
const DEFAULT_MAX_ATTACHMENT_BYTES = 2_000_000;
const DEFAULT_MAX_TEXT_CHARS = 20_000;
const DEFAULT_MAX_HTML_CHARS = 50_000;
const DEFAULT_CONNECT_TIMEOUT_MS = 10_000;
const DEFAULT_SOCKET_TIMEOUT_MS = 20_000;
function readEnvString(env: NodeJS.ProcessEnv, key: string): string | undefined {
const value = env[key];
if (!value) {
return undefined;
}
return value.trim();
}
function readEnvNumber(env: NodeJS.ProcessEnv, key: string, fallback: number): number {
const value = readEnvString(env, key);
if (!value) {
return fallback;
}
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
return fallback;
}
function readEnvBoolean(env: NodeJS.ProcessEnv, key: string, fallback: boolean): boolean {
const value = readEnvString(env, key);
if (!value) {
return fallback;
}
return value.toLowerCase() === "true";
}
function getAccountIds(env: NodeJS.ProcessEnv): string[] {
const ids = new Set<string>();
Object.keys(env).forEach((key) => {
if (!key.startsWith("MAIL_SMTP_")) {
return;
}
const match = key.match(/^MAIL_SMTP_([A-Z0-9]+)_.+$/);
if (match?.[1]) {
ids.add(match[1].toLowerCase());
}
});
if (ids.size === 0) {
return [];
}
return Array.from(ids.values()).sort();
}
/**
* List account IDs discovered in the environment.
*/
export function listAccountIds(env: NodeJS.ProcessEnv): string[] {
return getAccountIds(env);
}
function loadAccountConfig(
env: NodeJS.ProcessEnv,
accountId: string,
): {
config?: AccountConfig;
missing: string[];
} {
const prefix = `MAIL_SMTP_${accountId.toUpperCase()}_`;
const host = readEnvString(env, `${prefix}HOST`);
const user = readEnvString(env, `${prefix}USER`);
const pass = readEnvString(env, `${prefix}PASS`);
const secure = readEnvBoolean(env, `${prefix}SECURE`, false);
const defaultPort = secure ? 465 : 587;
const port = readEnvNumber(env, `${prefix}PORT`, defaultPort);
const defaultFrom = readEnvString(env, `${prefix}FROM`);
const missing: string[] = [];
if (!host) {
missing.push(`${prefix}HOST`);
}
if (!user) {
missing.push(`${prefix}USER`);
}
if (!pass) {
missing.push(`${prefix}PASS`);
}
if (missing.length > 0 || !host || !user || !pass) {
return { missing };
}
const baseConfig: AccountConfig = {
accountId,
host,
port,
secure,
user,
pass,
...(defaultFrom ? { defaultFrom } : {}),
};
return { config: baseConfig, missing };
}
/**
* Return missing required environment variables for a given account ID.
*/
export function getMissingAccountEnv(env: NodeJS.ProcessEnv, accountId: string): string[] {
return loadAccountConfig(env, accountId).missing;
}
/**
* Load all configured accounts and policy from environment variables.
*/
export function loadServerConfig(env: NodeJS.ProcessEnv): ServerConfig {
const accountIds = getAccountIds(env);
const accounts: AccountConfig[] = [];
accountIds.forEach((accountId) => {
const { config } = loadAccountConfig(env, accountId);
if (config) {
accounts.push(config);
}
});
const policy: PolicyConfig = {
sendEnabled: readEnvBoolean(env, "MAIL_SMTP_SEND_ENABLED", false),
allowlistDomains: parseCsvList(env["MAIL_SMTP_ALLOWLIST_DOMAINS"]),
allowlistAddresses: parseCsvList(env["MAIL_SMTP_ALLOWLIST_ADDRESSES"]),
maxRecipients: readEnvNumber(env, "MAIL_SMTP_MAX_RECIPIENTS", DEFAULT_MAX_RECIPIENTS),
maxMessageBytes: readEnvNumber(env, "MAIL_SMTP_MAX_MESSAGE_BYTES", DEFAULT_MAX_MESSAGE_BYTES),
maxAttachmentBytes: readEnvNumber(
env,
"MAIL_SMTP_MAX_ATTACHMENT_BYTES",
DEFAULT_MAX_ATTACHMENT_BYTES,
),
maxAttachments: readEnvNumber(env, "MAIL_SMTP_MAX_ATTACHMENTS", DEFAULT_MAX_ATTACHMENTS),
maxTextChars: readEnvNumber(env, "MAIL_SMTP_MAX_TEXT_CHARS", DEFAULT_MAX_TEXT_CHARS),
maxHtmlChars: readEnvNumber(env, "MAIL_SMTP_MAX_HTML_CHARS", DEFAULT_MAX_HTML_CHARS),
connectTimeoutMs: readEnvNumber(
env,
"MAIL_SMTP_CONNECT_TIMEOUT_MS",
DEFAULT_CONNECT_TIMEOUT_MS,
),
socketTimeoutMs: readEnvNumber(env, "MAIL_SMTP_SOCKET_TIMEOUT_MS", DEFAULT_SOCKET_TIMEOUT_MS),
};
return { accounts, policy };
}
/**
* Resolve a specific account configuration by ID.
*/
export function resolveAccountConfig(
env: NodeJS.ProcessEnv,
accountId: string,
): {
config?: AccountConfig;
missing: string[];
} {
return loadAccountConfig(env, accountId);
}
/**
* Return listable account metadata for tool outputs.
*/
export function listAccountMetadata(accounts: readonly AccountConfig[]): Array<{
account_id: string;
host: string;
port: number;
secure: boolean;
default_from?: string;
}> {
return accounts.map((account) => ({
account_id: account.accountId,
host: account.host,
port: account.port,
secure: account.secure,
...(account.defaultFrom ? { default_from: account.defaultFrom } : {}),
}));
}