import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; import * as db from "./db"; import { ENV } from "./_core/env"; import { providerDefaults, resolveMailConfig, type MailEnvInput, type SupportedMailProvider } from "@shared/mailProviders"; export const MAIL_SETTINGS_KEY = "system.mail.settings"; type StoredMailSettings = { smtpProvider: SupportedMailProvider; smtpHost?: string; smtpPort?: string; smtpUser?: string; smtpFrom?: string; smtpSecure?: boolean; smtpRequireTls?: boolean; smtpPassEncrypted?: string; updatedByUserId?: number; updatedByUserName?: string | null; }; export type MailSettingsFormInput = { smtpProvider: SupportedMailProvider; smtpHost?: string; smtpPort?: string; smtpUser?: string; smtpFrom?: string; smtpSecure?: boolean; smtpRequireTls?: boolean; smtpPass?: string; }; export type AdminMailSettings = { smtpProvider: SupportedMailProvider; smtpHost: string; smtpPort: string; smtpUser: string; smtpFrom: string; smtpSecure: boolean; smtpRequireTls: boolean; smtpPassConfigured: boolean; ready: boolean; missing: string[]; source: "environment" | "admin" | "admin+environment"; hasSavedConfig: boolean; updatedAt: string | null; updatedByUserId: number | null; updatedByUserName: string | null; }; function buildCipherKey() { return createHash("sha256") .update(ENV.cookieSecret || "local-dev-mail-settings") .digest(); } function encryptSecret(value: string) { const iv = randomBytes(12); const key = buildCipherKey(); const cipher = createCipheriv("aes-256-gcm", key, iv); const encrypted = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]); const tag = cipher.getAuthTag(); return `${iv.toString("base64")}.${tag.toString("base64")}.${encrypted.toString("base64")}`; } function decryptSecret(value?: string) { if (!value) return ""; const [ivBase64, tagBase64, encryptedBase64] = value.split("."); if (!ivBase64 || !tagBase64 || !encryptedBase64) { return ""; } try { const key = buildCipherKey(); const decipher = createDecipheriv( "aes-256-gcm", key, Buffer.from(ivBase64, "base64") ); decipher.setAuthTag(Buffer.from(tagBase64, "base64")); const decrypted = Buffer.concat([ decipher.update(Buffer.from(encryptedBase64, "base64")), decipher.final(), ]); return decrypted.toString("utf8"); } catch { return ""; } } function trimOrEmpty(value?: string | null) { return String(value || "").trim(); } function normalizeStoredSettings(input: Partial | null | undefined): StoredMailSettings | null { if (!input?.smtpProvider) return null; return { smtpProvider: input.smtpProvider, smtpHost: trimOrEmpty(input.smtpHost), smtpPort: trimOrEmpty(input.smtpPort), smtpUser: trimOrEmpty(input.smtpUser), smtpFrom: trimOrEmpty(input.smtpFrom), smtpSecure: Boolean(input.smtpSecure), smtpRequireTls: Boolean(input.smtpRequireTls), smtpPassEncrypted: trimOrEmpty(input.smtpPassEncrypted), updatedByUserId: input.updatedByUserId ? Number(input.updatedByUserId) : undefined, updatedByUserName: input.updatedByUserName?.trim() || null, }; } async function getStoredMailSettingsRecord() { const setting = await db.getPortalSettingRecord(MAIL_SETTINGS_KEY); if (!setting?.valeur) { return { setting: null, payload: null as StoredMailSettings | null }; } try { const parsed = JSON.parse(setting.valeur); return { setting, payload: normalizeStoredSettings(parsed), }; } catch { return { setting, payload: null, }; } } function mergeMailInput(stored: StoredMailSettings | null) { const decryptedPassword = decryptSecret(stored?.smtpPassEncrypted); const sourceEntries: Array = [ "smtpProvider", "smtpHost", "smtpPort", "smtpUser", "smtpPass", "smtpFrom", "smtpSecure", "smtpRequireTls", ]; const storedInput: MailEnvInput = { smtpProvider: stored?.smtpProvider, smtpHost: stored?.smtpHost, smtpPort: stored?.smtpPort, smtpUser: stored?.smtpUser, smtpPass: decryptedPassword, smtpFrom: stored?.smtpFrom, smtpSecure: stored ? String(Boolean(stored.smtpSecure)) : "", smtpRequireTls: stored ? String(Boolean(stored.smtpRequireTls)) : "", }; const envInput: MailEnvInput = { smtpProvider: ENV.smtpProvider, smtpHost: ENV.smtpHost, smtpPort: ENV.smtpPort, smtpUser: ENV.smtpUser, smtpPass: ENV.smtpPass, smtpFrom: ENV.smtpFrom, smtpSecure: ENV.smtpSecure, smtpRequireTls: ENV.smtpRequireTls, }; const merged: MailEnvInput = {}; let usesEnvironmentFallback = false; for (const key of sourceEntries) { const storedValue = trimOrEmpty(storedInput[key]); const envValue = trimOrEmpty(envInput[key]); if (storedValue) { merged[key] = storedInput[key]; } else if (envValue) { merged[key] = envInput[key]; if (stored) { usesEnvironmentFallback = true; } } } const source: AdminMailSettings["source"] = stored ? usesEnvironmentFallback ? "admin+environment" : "admin" : "environment"; return { merged, source }; } export async function getResolvedMailConfiguration() { const { setting, payload } = await getStoredMailSettingsRecord(); const { merged, source } = mergeMailInput(payload); return { config: resolveMailConfig(merged), source, stored: payload, updatedAt: setting?.updatedAt?.toISOString?.() ?? null, }; } export async function getAdminMailSettings(): Promise { const { config, source, stored, updatedAt } = await getResolvedMailConfiguration(); const defaults = stored?.smtpProvider && stored.smtpProvider !== "custom" ? providerDefaults[stored.smtpProvider] : null; return { smtpProvider: config.provider, smtpHost: stored?.smtpHost || config.host || defaults?.host || "", smtpPort: stored?.smtpPort || (config.port ? String(config.port) : defaults?.port ? String(defaults.port) : ""), smtpUser: stored?.smtpUser || config.user || "", smtpFrom: stored?.smtpFrom || config.from || "", smtpSecure: stored ? Boolean(stored.smtpSecure) : config.secure, smtpRequireTls: stored ? Boolean(stored.smtpRequireTls) : config.requireTLS, smtpPassConfigured: Boolean(stored?.smtpPassEncrypted || ENV.smtpPass), ready: config.ready, missing: config.missing, source, hasSavedConfig: Boolean(stored), updatedAt, updatedByUserId: stored?.updatedByUserId ?? null, updatedByUserName: stored?.updatedByUserName ?? null, }; } export async function saveAdminMailSettings( input: MailSettingsFormInput, actor?: { id: number; name?: string | null } ) { const existing = await getStoredMailSettingsRecord(); const nextPassword = trimOrEmpty(input.smtpPass) ? encryptSecret(trimOrEmpty(input.smtpPass)) : existing.payload?.smtpPassEncrypted || ""; const payload: StoredMailSettings = { smtpProvider: input.smtpProvider, smtpHost: trimOrEmpty(input.smtpHost), smtpPort: trimOrEmpty(input.smtpPort), smtpUser: trimOrEmpty(input.smtpUser), smtpFrom: trimOrEmpty(input.smtpFrom), smtpSecure: Boolean(input.smtpSecure), smtpRequireTls: Boolean(input.smtpRequireTls), smtpPassEncrypted: nextPassword, updatedByUserId: actor?.id, updatedByUserName: actor?.name?.trim() || null, }; await db.setPortalSetting( MAIL_SETTINGS_KEY, JSON.stringify(payload), "Configuration SMTP et messagerie du portail" ); return getAdminMailSettings(); }