import { parseCsvList } from "./utils/strings.js";
/**
* Configuration for a single SMTP account loaded from environment variables.
*
* These values are loaded from environment variables with the prefix MAIL_SMTP_<ID>_
* where ID is the account identifier (e.g., "DEFAULT" for MAIL_SMTP_DEFAULT_HOST).
*/
export interface AccountConfig {
readonly accountId: string;
readonly host: string;
readonly port: number;
readonly secure: boolean;
readonly user: string;
readonly pass: string;
readonly defaultFrom?: string;
}
/**
* Policy configuration governing message sending limits and security restrictions.
*
* These controls help prevent abuse and ensure messages stay within operational limits.
* All limits are enforced before attempting to send via SMTP.
*/
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;
}
/**
* Complete server configuration including all accounts and policy settings.
*
* This is the top-level configuration object used by the MCP server to initialize
* tool handlers and enforce policies.
*/
export interface ServerConfig {
readonly accounts: readonly AccountConfig[];
readonly policy: PolicyConfig;
}
// Default policy limits when not specified in environment variables
export const DEFAULT_MAX_RECIPIENTS = 10;
export const DEFAULT_MAX_MESSAGE_BYTES = 2_500_000; // ~2.4 MB
export const DEFAULT_MAX_ATTACHMENTS = 5;
export const DEFAULT_MAX_ATTACHMENT_BYTES = 2_000_000; // ~1.9 MB per file
export const DEFAULT_MAX_TEXT_CHARS = 20_000;
export const DEFAULT_MAX_HTML_CHARS = 50_000;
export const DEFAULT_CONNECT_TIMEOUT_MS = 10_000; // 10 seconds
export const DEFAULT_SOCKET_TIMEOUT_MS = 20_000; // 20 seconds
/**
* Read and trim an environment variable string value.
*
* @returns The trimmed value or undefined if not set.
*/
function readEnvString(env: NodeJS.ProcessEnv, key: string): string | undefined {
const value = env[key];
if (!value) {
return undefined;
}
return value.trim();
}
/**
* Read an environment variable and parse as a number.
*
* @param fallback - Default value if the environment variable is not set or invalid.
* @returns The parsed number or the fallback value.
*/
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;
}
/**
* Read an environment variable and parse as a boolean.
*
* Accepts "true" (case-insensitive) as true, all other values are false.
*
* @param fallback - Default value if the environment variable is not set.
* @returns The parsed boolean or the fallback value.
*/
function readEnvBoolean(env: NodeJS.ProcessEnv, key: string, fallback: boolean): boolean {
const value = readEnvString(env, key);
if (!value) {
return fallback;
}
return value.toLowerCase() === "true";
}
/**
* Extract SMTP account IDs from environment variable names.
*
* Scans for environment variables matching the pattern MAIL_SMTP_<ID>_<KEY>
* and extracts unique account IDs from the ID position.
*
* For example, MAIL_SMTP_DEFAULT_HOST and MAIL_SMTP_GMAIL_USER would return ["default", "gmail"].
*/
function getAccountIds(env: NodeJS.ProcessEnv): string[] {
const ids = new Set<string>();
Object.keys(env).forEach((key) => {
// Skip non-SMTP environment variables
if (!key.startsWith("MAIL_SMTP_")) {
return;
}
// Extract account ID from pattern MAIL_SMTP_<ID>_<FIELD>
// ID must be uppercase alphanumeric (e.g., DEFAULT, GMAIL, WORK)
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);
}
/**
* Load configuration for a specific SMTP account from environment variables.
*
* Validates that all required fields (HOST, USER, PASS) are present and returns
* the configuration or a list of missing required variables.
*
* @returns An object with the loaded config and/or missing required keys.
*/
function loadAccountConfig(
env: NodeJS.ProcessEnv,
accountId: string,
): {
config?: AccountConfig;
missing: string[];
} {
// Build environment variable prefix for this account (e.g., MAIL_SMTP_DEFAULT_)
const prefix = `MAIL_SMTP_${accountId.toUpperCase()}_`;
// Load required and optional configuration fields
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);
// Use default ports: 465 for SSL/TLS (secure=true), 587 for STARTTLS (secure=false)
const defaultPort = secure ? 465 : 587;
const port = readEnvNumber(env, `${prefix}PORT`, defaultPort);
const defaultFrom = readEnvString(env, `${prefix}FROM`);
// Validate required fields are present
const missing: string[] = [];
if (!host) {
missing.push(`${prefix}HOST`);
}
if (!user) {
missing.push(`${prefix}USER`);
}
if (!pass) {
missing.push(`${prefix}PASS`);
}
// Return early with missing fields if any required field is absent
if (missing.length > 0 || !host || !user || !pass) {
return { missing };
}
// Build complete account configuration
const baseConfig: AccountConfig = {
accountId,
host,
port,
secure,
user,
pass,
// Only include defaultFrom if it's set (optional field)
...(defaultFrom ? { defaultFrom } : {}),
};
return { config: baseConfig, missing };
}
/**
* Return missing required environment variables for a given account ID.
*
* Checks if the specified account has all required configuration fields
* (HOST, USER, PASS) set in the environment.
*
* @param env - The environment variables to check.
* @param accountId - The account identifier (e.g., "default", "gmail").
* @returns An array of missing environment variable keys. Empty array if fully configured.
*/
export function getMissingAccountEnv(env: NodeJS.ProcessEnv, accountId: string): string[] {
return loadAccountConfig(env, accountId).missing;
}
/**
* Load all configured accounts and policy from environment variables.
*
* Scans the environment for all SMTP account configurations and loads them
* along with the global policy settings. Accounts with missing required fields
* are silently skipped.
*
* @param env - The environment variables to load configuration from.
* @returns A complete server configuration with all valid accounts and policy settings.
*/
export function loadServerConfig(env: NodeJS.ProcessEnv): ServerConfig {
// Discover and load all SMTP account configurations
const accountIds = getAccountIds(env);
const accounts: AccountConfig[] = [];
accountIds.forEach((accountId) => {
const { config } = loadAccountConfig(env, accountId);
// Only add fully configured accounts (those without missing fields)
if (config) {
accounts.push(config);
}
});
// Load policy configuration with fallback to defaults
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.
*
* Loads the configuration for a single account, returning either the
* configuration object or a list of missing required environment variables.
*
* @param env - The environment variables to load configuration from.
* @param accountId - The account identifier (e.g., "default", "gmail").
* @returns An object containing the account configuration if valid, otherwise missing fields.
*/
export function resolveAccountConfig(
env: NodeJS.ProcessEnv,
accountId: string,
): {
config?: AccountConfig;
missing: string[];
} {
return loadAccountConfig(env, accountId);
}
/**
* Return listable account metadata for tool outputs.
*
* Converts account configurations to a simplified format safe for exposing
* to tool consumers, removing sensitive credentials and using snake_case keys.
*
* @param accounts - The account configurations to format.
* @returns An array of account metadata objects with non-sensitive information only.
*/
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,
// Only include default_from if configured (optional field)
...(account.defaultFrom ? { default_from: account.defaultFrom } : {}),
}));
}