From a1e87f70fa60525127b79f37956b0e437b617bce Mon Sep 17 00:00:00 2001 From: Thomas Preisner Date: Sat, 22 Feb 2025 19:52:13 +0100 Subject: [PATCH] Merge options virtualAccountAliases and virtualDomainAliases into virtualAliases --- mailsystem/default.nix | 50 +++++++++++++++++++++--------------------- mailsystem/postfix.nix | 48 +++++++++++++++++----------------------- tests/aliases.nix | 8 +++---- 3 files changed, 49 insertions(+), 57 deletions(-) diff --git a/mailsystem/default.nix b/mailsystem/default.nix index 2dc96d9..cae1a62 100644 --- a/mailsystem/default.nix +++ b/mailsystem/default.nix @@ -126,43 +126,43 @@ in { default = {}; }; - virtualDomainAliases = lib.mkOption { - type = with lib.types; attrsOf str; - example = { - "aliasdomain.com" = "domain.com"; - }; - description = '' - Virtual aliasing of domains. A virtual alias `"aliasdomain.com" = "domain.com"` - means that all mail directed at `@aliasdomain.com` are forwarded to `@domain.com`. - This also entails, that any account or alias of `domain.com` is partially valid - for `aliasdomain.com`. For example, `user@domain.com` can receive mails at - `user@aliasdomain.com`. However, if `user@domain.com` shall be able to dispatch - mails using `user@aliasdomain.com`, an explicit alias needs to be configured. - ''; - default = {}; - }; - - virtualAccountAliases = lib.mkOption { + virtualAliases = lib.mkOption { type = let + isAccount = value: builtins.elem value (builtins.attrNames cfg.accounts); + isDomain = value: !(lib.hasInfix "@" value) && (builtins.elem value cfg.domains); account = lib.mkOptionType { name = "Mail Account"; - check = account: builtins.elem account (builtins.attrNames cfg.accounts); + check = isAccount; + }; + accountOrDomain = lib.mkOptionType { + name = "Mail Account or Domain"; + check = value: (isAccount value) || (isDomain value); }; in - with lib.types; attrsOf (either account (nonEmptyListOf account)); + with lib.types; attrsOf (either (nonEmptyListOf account) accountOrDomain); example = { "info@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com"; "multi@example.com" = ["user1@example.com" "user2@example.com"]; + "aliasdomain.com" = "domain.com"; }; description = '' - Virtual account aliases. A virtual alias `"info@example.com" = "user1@example.com"` - means that all mail to `info@example.com` is forwarded to `user1@example.com`. - Furthermore, it also allows the user `user1@example.com` to send emails as - `info@example.com`. It is also possible to create an alias for multiple accounts. - In this example, all mails for `multi@example.com` will be forwarded to both - `user1@example.com` and `user2@example.com`. + Virtual account and domain aliases. A virtual alias means, that all mail directed + at a given target are forwarded to the specified other destinations, too. + + For account aliases, this means that, e.g., `"user1@example.com"` receives all mail + sent to `"info@example.com"`. In addition, `"user1@example.com"` is also able to + impersonate `"info@example.com"` when sending mails. It is also possible to create + an alias for multiple accounts. In this example, all mails for `"multi@example.com"` + will be forwarded to both + `"user1@example.com"` and `"user2@example.com"`. + + For domain aliases, this means that all mails directed at an aliased domain, e.g., + `"aliasdomain.com"` are forwarded to `"domain.com"` This also entails, that any + account or alias of `"domain.com"` receives mails directed at `"aliasdomain.com"`. + However, if `"user@domain.com"` shall be able to send mails using + `"user@aliasdomain.com"`, an explicit alias needs to be configured. ''; default = {}; }; diff --git a/mailsystem/postfix.nix b/mailsystem/postfix.nix index 3549afe..e5ebbfa 100644 --- a/mailsystem/postfix.nix +++ b/mailsystem/postfix.nix @@ -15,33 +15,27 @@ with (import ./common.nix {inherit config;}); let mergeLookupTables lookupTables; lookupTableToString = attrs: let - valueToString = value: lib.concatStringsSep ", " value; + isDomain = value: !(lib.hasInfix "@" value); + valueToString = value: + if (isDomain value) + then "@${value}" + else value; + listToString = list: lib.concatStringsSep ", " (map valueToString list); in - lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: list: "${valueToString name} ${listToString list}") attrs); mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables; virtual_accounts = mergeLookupTables (lib.map (name: {"${name}" = name;}) (lib.attrNames cfg.accounts)); - virtual_account_aliases = attrsToLookupTable cfg.virtualAccountAliases; - - virtual_domain_aliases = let - alias_domains = lib.mapAttrs' (src: dst: lib.nameValuePair "@${src}" "@${dst}") cfg.virtualDomainAliases; - in - attrsToLookupTable alias_domains; - - all_virtual_aliases = mergeLookupTables [virtual_accounts virtual_account_aliases virtual_domain_aliases]; + virtual_aliases = attrsToLookupTable cfg.virtualAliases; + all_virtual_aliases = mergeLookupTables [virtual_accounts virtual_aliases]; + # File containing all mappings of aliases/authenticated accounts and their sender mail addresses. aliases_file = let content = lookupTableToString all_virtual_aliases; in builtins.toFile "virtual_aliases" content; - # File containing all mappings of authenticated accounts and their sender mail addresses. - virtual_accounts_file = let - content = lookupTableToString all_virtual_aliases; - in - builtins.toFile "virtual_accounts" content; - virtual_domains_file = builtins.toFile "virtual_domains" (lib.concatStringsSep "\n" cfg.domains); denied_recipients = map (account: "${account.name} REJECT ${account.rejectMessage}") (lib.filter (account: account.isSystemUser) (lib.attrValues cfg.accounts)); @@ -67,14 +61,15 @@ with (import ./common.nix {inherit config;}); let tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; in { config = lib.mkIf cfg.enable { - assertions = - lib.mapAttrsToList ( - src: dst: { - assertion = (builtins.elem src cfg.domains) && (builtins.elem dst cfg.domains); - message = "Both aliased domain (${src}) and actual domain (${dst}) need to be managed by the mailserver."; - } - ) - cfg.virtualDomainAliases; + assertions = let + isDomain = value: !lib.hasInfix "@" value; + aliasedDomains = builtins.filter isDomain (builtins.attrNames cfg.virtualAliases); + in + map (domain: { + assertion = builtins.elem domain cfg.domains; + message = "The domain to be aliased (${domain}) must be managed by the mailserver."; + }) + aliasedDomains; services.postfix = { enable = true; @@ -86,9 +81,7 @@ in { enableSubmissions = true; - # TODO: create function to simplify this? mapFiles."virtual_aliases" = aliases_file; - mapFiles."virtual_accounts" = virtual_accounts_file; mapFiles."denied_recipients" = denied_recipients_file; virtual = lookupTableToString all_virtual_aliases; @@ -100,8 +93,7 @@ in { smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = "$myhostname"; smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - # use mappedFile -> different path? - smtpd_sender_login_maps = "hash:/etc/postfix/virtual_accounts"; + smtpd_sender_login_maps = mappedFile "virtual_aliases"; smtpd_sender_restrictions = "reject_sender_login_mismatch"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; cleanup_service_name = "submission-header-cleanup"; diff --git a/tests/aliases.nix b/tests/aliases.nix index ba063e1..4db867d 100644 --- a/tests/aliases.nix +++ b/tests/aliases.nix @@ -42,13 +42,13 @@ in fqdn = "mail.example.com"; domains = ["example.com" "aliased.com" "otherdomain.com"]; accounts = mkAccounts accounts; - virtualDomainAliases = { + virtualAliases = { + # domain aliases "aliased.com" = "example.com"; - }; - virtualAccountAliases = { + # account aliases "alias@example.com" = accounts."alias".address; "multi-alias@example.com" = lib.map (x: accounts.${x}.address) ["multi-alias1" "multi-alias2"]; - "@example.com" = accounts."catchall".address; + "example.com" = accounts."catchall".address; "user@otherdomain.com" = accounts."otherdomain".address; }; };