portail-associations/server/mailSettings.ts

256 lines
7.5 KiB
TypeScript

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<StoredMailSettings> | 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<keyof MailEnvInput> = [
"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<AdminMailSettings> {
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();
}