256 lines
7.5 KiB
TypeScript
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();
|
|
}
|