export type MailProfilePatternKey = "smtp_hosts" | "imap_hosts" | "envelope_senders" | "from_headers" | "recipient_domains"; export type MailProfilePolicy = { whitelist?: Partial> | null; blacklist?: Partial> | null; }; export type MailPolicyValidationInput = { smtpHost?: string | null; imapHost?: string | null; envelopeSender?: string | null; fromHeader?: string | null; recipientDomains?: Array | null; }; export type MailPolicyValidationMessage = { key: MailProfilePatternKey; label: string; value: string; message: string; severity?: "warning" | "danger"; }; const patternLabels: Record = { smtp_hosts: "i18n:govoplan-mail.smtp_host.2d4a434b", imap_hosts: "i18n:govoplan-mail.imap_host.b53c3751", envelope_senders: "i18n:govoplan-mail.envelope_sender.5ec276a0", from_headers: "i18n:govoplan-mail.from_header.bb78e85d", recipient_domains: "i18n:govoplan-mail.recipient_domain.778f2dcf" }; type ValueCheck = { key: MailProfilePatternKey; value: string; }; export function validateMailPolicy( policy: MailProfilePolicy | null | undefined, input: MailPolicyValidationInput) : MailPolicyValidationMessage[] { if (!policy) return []; const checks: ValueCheck[] = [ { key: "smtp_hosts", value: input.smtpHost ?? "" }, { key: "imap_hosts", value: input.imapHost ?? "" }, { key: "envelope_senders", value: input.envelopeSender ?? "" }, { key: "from_headers", value: input.fromHeader ?? "" }, ...Array.from(new Set((input.recipientDomains ?? []).map(normalizeDomain).filter(Boolean))). map((value) => ({ key: "recipient_domains" as const, value }))]; return checks.flatMap(({ key, value }) => { const result = mailPolicyValueAllowed(policy, key, value); if (result.allowed) return []; const label = patternLabels[key]; return [{ key, label, value: result.value, severity: "danger" as const, message: result.reason === "blacklist" ? `${label} ${result.value} is denied by pattern ${result.pattern}.` : `${label} ${result.value} is outside the effective allow-list.` }]; }); } export function mailPolicyValueAllowed( policy: MailProfilePolicy | null | undefined, key: MailProfilePatternKey, rawValue: string | null | undefined) : {allowed: true;value: string;} | {allowed: false;value: string;reason: "blacklist" | "whitelist";pattern?: string;} { const value = normalizePolicyValue(key, rawValue); if (!value || !policy) return { allowed: true, value }; const deniedBy = patternsFor(policy.blacklist, key).find((pattern) => wildcardPatternMatches(pattern, value)); if (deniedBy) return { allowed: false, value, reason: "blacklist", pattern: deniedBy }; const allowPatterns = meaningfulAllowPatterns(patternsFor(policy.whitelist, key)); if (allowPatterns.length > 0 && !allowPatterns.some((pattern) => wildcardPatternMatches(pattern, value))) { return { allowed: false, value, reason: "whitelist" }; } return { allowed: true, value }; } export function wildcardPatternMatches(pattern: string, rawValue: string): boolean { const normalizedPattern = pattern.trim(); const value = rawValue.trim(); if (!normalizedPattern || !value) return false; return wildcardMatch(normalizedPattern.toLowerCase(), value.toLowerCase()); } function wildcardMatch(pattern: string, value: string): boolean { let patternIndex = 0; let valueIndex = 0; let starIndex = -1; let valueRetryIndex = 0; while (valueIndex < value.length) { const token = pattern[patternIndex]; if (token === "?" || token === value[valueIndex]) { patternIndex += 1; valueIndex += 1; continue; } if (token === "*") { starIndex = patternIndex; valueRetryIndex = valueIndex; patternIndex += 1; continue; } if (starIndex !== -1) { patternIndex = starIndex + 1; valueRetryIndex += 1; valueIndex = valueRetryIndex; continue; } return false; } while (pattern[patternIndex] === "*") patternIndex += 1; return patternIndex === pattern.length; } function patternsFor( rules: MailProfilePolicy["whitelist"] | MailProfilePolicy["blacklist"], key: MailProfilePatternKey) : string[] { return (rules?.[key] ?? []).map((pattern) => String(pattern).trim()).filter(Boolean); } function meaningfulAllowPatterns(patterns: string[]): string[] { return patterns.filter((pattern) => pattern !== "*"); } function normalizePolicyValue(key: MailProfilePatternKey, value: string | null | undefined): string { const trimmed = String(value ?? "").trim(); if (!trimmed) return ""; if (key === "recipient_domains") return normalizeDomain(trimmed); return trimmed; } function normalizeDomain(value: string | null | undefined): string { const trimmed = String(value ?? "").trim().toLowerCase(); if (!trimmed) return ""; const emailDomain = trimmed.includes("@") ? trimmed.split("@").pop() ?? "" : trimmed; return emailDomain.replace(/^@+/, ""); }