diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 2d29d94..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: "Test" -on: - pull_request: - push: - branches: - - main - schedule: - - cron: '42 7 * * *' -jobs: - tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Install Nix - uses: cachix/install-nix-action@v30 - - name: Execute unit tests - run: nix flake check diff --git a/flake.lock b/flake.lock index 598bc62..43c3d18 100644 --- a/flake.lock +++ b/flake.lock @@ -1,20 +1,5 @@ { "nodes": { - "crane": { - "locked": { - "lastModified": 1748047550, - "narHash": "sha256-t0qLLqb4C1rdtiY8IFRH5KIapTY/n3Lqt57AmxEv9mk=", - "owner": "ipetkov", - "repo": "crane", - "rev": "b718a78696060df6280196a6f992d04c87a16aef", - "type": "github" - }, - "original": { - "owner": "ipetkov", - "repo": "crane", - "type": "github" - } - }, "flake-compat": { "flake": false, "locked": { @@ -38,11 +23,11 @@ ] }, "locked": { - "lastModified": 1743550720, - "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", + "lastModified": 1730504689, + "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "c621e8422220273271f52058f618c94e405bb0f5", + "rev": "506278e768c2a08bec68eb62932193e341f55c90", "type": "github" }, "original": { @@ -57,14 +42,15 @@ "gitignore": "gitignore", "nixpkgs": [ "nixpkgs" - ] + ], + "nixpkgs-stable": "nixpkgs-stable" }, "locked": { - "lastModified": 1747372754, - "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", + "lastModified": 1733318908, + "narHash": "sha256-SVQVsbafSM1dJ4fpgyBqLZ+Lft+jcQuMtEL3lQWx2Sk=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", + "rev": "6f4e2a2112050951a314d2733a994fbab94864c6", "type": "github" }, "original": { @@ -96,23 +82,38 @@ }, "nixpkgs": { "locked": { - "lastModified": 1748162331, - "narHash": "sha256-rqc2RKYTxP3tbjA+PB3VMRQNnjesrT0pEofXQTrMsS8=", + "lastModified": 1731755305, + "narHash": "sha256-v5P3dk5JdiT+4x69ZaB18B8+Rcu3TIOrcdG4uEX7WZ8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", + "rev": "057f63b6dc1a2c67301286152eb5af20747a9cb4", "type": "github" }, "original": { "owner": "NixOS", - "ref": "nixos-25.05", + "ref": "nixos-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1730741070, + "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", "repo": "nixpkgs", "type": "github" } }, "root": { "inputs": { - "crane": "crane", "flake-parts": "flake-parts", "git-hooks-nix": "git-hooks-nix", "nixpkgs": "nixpkgs", @@ -126,11 +127,11 @@ ] }, "locked": { - "lastModified": 1748243702, - "narHash": "sha256-9YzfeN8CB6SzNPyPm2XjRRqSixDopTapaRsnTpXUEY8=", + "lastModified": 1733440889, + "narHash": "sha256-qKL3vjO+IXFQ0nTinFDqNq/sbbnnS5bMI1y0xX215fU=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "1f3f7b784643d488ba4bf315638b2b0a4c5fb007", + "rev": "50862ba6a8a0255b87377b9d2d4565e96f29b410", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 082f348..80dcfc5 100644 --- a/flake.nix +++ b/flake.nix @@ -2,12 +2,11 @@ description = "An opinionated Nixos Mailsystem"; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; flake-parts.url = "github:hercules-ci/flake-parts"; flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs"; treefmt-nix.url = "github:numtide/treefmt-nix"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; - crane.url = "github:ipetkov/crane"; git-hooks-nix.url = "github:cachix/git-hooks.nix"; git-hooks-nix.inputs.nixpkgs.follows = "nixpkgs"; }; @@ -24,7 +23,6 @@ "aarch64-linux" ]; imports = [ - flake-parts.flakeModules.easyOverlay inputs.treefmt-nix.flakeModule inputs.git-hooks-nix.flakeModule ]; @@ -36,12 +34,9 @@ pkgs, system, ... - }: let - craneLib = inputs.crane.mkLib pkgs; - pkgs = nixpkgs.legacyPackages.${system}.extend self.overlays.default; - in { + }: { checks = let - tests = ["internal" "basic" "aliases" "rspamd"]; + tests = ["internal" "basic" "aliases"]; genTest = testName: { "name" = testName; "value" = import (./tests + "/${testName}.nix") {inherit pkgs;}; @@ -49,20 +44,9 @@ in pkgs.lib.listToAttrs (map genTest tests); - packages = rec { - default = mailnix; - mailnix = craneLib.buildPackage { - src = craneLib.cleanCargoSource ./pkgs/mailnix; - }; - }; - overlayAttrs = { - mailnix = config.packages.mailnix; - }; - - devShells.default = craneLib.devShell { + devShells.default = pkgs.mkShell { packages = with pkgs; [ self'.formatter.outPath # Add all formatters to environment - mailnix ]; shellHook = '' ${config.pre-commit.installationScript} @@ -75,9 +59,7 @@ treefmt = { programs = { - actionlint.enable = true; alejandra.enable = true; - rustfmt.enable = true; }; settings.global.excludes = [ ".envrc" @@ -86,7 +68,7 @@ }; }; - flake.nixosModules = rec { + flake.flakeModules = rec { default = mailsystem; mailsystem = import ./mailsystem; }; diff --git a/mailsystem/common.nix b/mailsystem/common.nix index 48dcdcb..ab7174c 100644 --- a/mailsystem/common.nix +++ b/mailsystem/common.nix @@ -1,8 +1,4 @@ -{ - config, - pkgs, - ... -}: let +{config, ...}: let cfg = config.mailsystem; in rec { certificateDirectory = "/var/certs"; @@ -21,17 +17,6 @@ in rec { then ["acme-finished-${cfg.fqdn}.target"] else ["mailsystem-selfsigned-certificate.service"]; - mailnixCmd = let - mailnixCfgFile = pkgs.writeText "mailnix-public.json" (builtins.toJSON { - inherit (cfg) accounts domains; - aliases = cfg.virtualAliases; - }); - extraCfgFile = - if (cfg.extraSettingsFile != null) - then cfg.extraSettingsFile - else ""; - in "${pkgs.mailnix}/bin/mailnix ${extraCfgFile} ${mailnixCfgFile}"; - dovecotDynamicStateDir = "/var/lib/dovecot"; dovecotDynamicPasswdFile = "${dovecotDynamicStateDir}/passwd"; diff --git a/mailsystem/default.nix b/mailsystem/default.nix index 8b6b7bb..afd69bc 100644 --- a/mailsystem/default.nix +++ b/mailsystem/default.nix @@ -92,6 +92,27 @@ in { ''; }; + aliases = lib.mkOption { + type = with lib.types; listOf types.str; + example = ["abuse@example.com" "postmaster@example.com"]; + default = []; + description = '' + A list of aliases of this login account. + Note: Use list entries like "@example.com" to create a catchAll + that allows sending from all email addresses in these domain. + ''; + }; + + quota = lib.mkOption { + type = with lib.types; nullOr types.str; + default = null; + example = "2G"; + description = '' + Sets quota for the this login account. The size has to be suffixed with `k/M/G/T`. + Not setting a quota results in a standard quota of `100G`. + ''; + }; + isSystemUser = lib.mkOption { type = lib.types.bool; default = false; @@ -101,17 +122,7 @@ in { account will be rejected. ''; }; - - rejectMessage = lib.mkOption { - type = lib.types.str; - default = "This account cannot receive emails."; - description = '' - The message that will be returned to the sender when an email is - sent to a system account. - ''; - }; }; - config.name = lib.mkDefault name; })); example = { @@ -122,102 +133,54 @@ in { hashedPassword = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/"; }; }; - description = "All available accounts for the mailsystem."; + description = "All available login account for the mailsystem."; default = {}; }; - virtualAliases = lib.mkOption { + 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 = {}; + }; + + extraVirtualAliases = 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 = isAccount; - }; - accountOrDomain = lib.mkOptionType { - name = "Mail Account or Domain"; - check = value: (isAccount value) || (isDomain value); + name = "Login Account"; + check = account: builtins.elem account (builtins.attrNames cfg.accounts); }; in - with lib.types; attrsOf (either (nonEmptyListOf account) accountOrDomain); + with lib.types; attrsOf (either account (nonEmptyListOf account)); 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 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. + Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that + all mail to `info@example.com` is forwarded to `user1@example.com`. Note + that it is expected that `postmaster@example.com` and `abuse@example.com` is + forwarded to some valid email address. (Alternatively you can create login + accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows + the user `user1@example.com` to send emails as `info@example.com`. + It's 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`. ''; default = {}; }; - dkimSettings = lib.mkOption { - type = with lib.types; - attrsOf (listOf (submodule { - options = { - selector = lib.mkOption { - type = lib.types.str; - example = "mail"; - description = "DKIM Selector"; - }; - keyFile = lib.mkOption { - type = lib.types.path; - example = "/run/secrets/dkim/example.com.mail.key"; - description = '' - Path to DKIM private-key-file. A public-private-pair can be generated as follows: - - ``` - nix-shell -p rspamd --run 'rspamadm dkim_keygen -s "selector" -t ed25519 -d example.com - nix-shell -p rspamd --run 'rspamadm dkim_keygen -s "selector" -b 2048 -d example.com - ``` - ''; - }; - }; - })); - example = { - "example.com" = [ - { - selector = "mail"; - keyFile = "/run/secrets/dkim/example.com.mail.key"; - } - ]; - }; - description = '' - Per-domain DKIM configuration. - This option allows to optionally set one or more DKIM private keys - and their respective selectors for each domain individually. - ''; - default = {}; - }; - - extraSettingsFile = lib.mkOption { - type = lib.types.nullOr lib.types.path; - description = '' - YAML file to merge into the mailsystem configuration at runtime. - This can be used to store secrets, and, more importantly, keep your email - addresses out of the hands of spammers. This `extraSettingsFile` currently - supports `domains`, `accounts` and `virtualAliases` which can be defined in - the same manner as they can be via nix. - ''; - default = null; - }; - certificateScheme = lib.mkOption { type = lib.types.enum ["acme" "selfsigned"]; default = "acme"; diff --git a/mailsystem/dovecot.nix b/mailsystem/dovecot.nix index 877c3e0..60a3d8e 100644 --- a/mailsystem/dovecot.nix +++ b/mailsystem/dovecot.nix @@ -4,7 +4,7 @@ pkgs, ... }: -with (import ./common.nix {inherit config pkgs;}); let +with (import ./common.nix {inherit config;}); let cfg = config.mailsystem; postfixCfg = config.services.postfix; dovecot2Cfg = config.services.dovecot2; @@ -19,7 +19,13 @@ with (import ./common.nix {inherit config pkgs;}); let systemUsers = lib.filterAttrs (user: value: value.isSystemUser) cfg.accounts; normalUsers = lib.filterAttrs (user: value: !value.isSystemUser) cfg.accounts; - genUserdbEntry = user: value: "${user}:::::::"; + genUserdbEntry = user: value: + "${user}:::::::" + + ( + if lib.isString value.quota + then "userdb_quota_rule=*:storage=${value.quota}" + else "" + ); genPasswdEntry = user: value: "${user}:${"$(head -n 1 ${value.hashedPasswordFile})"}::::::"; genAuthDbsScript = pkgs.writeScript "generate-dovecot-auth-dbs" '' @@ -39,22 +45,51 @@ with (import ./common.nix {inherit config pkgs;}); let # Ensure passwd files are not world-readable at any time umask 077 + # Ensure we have a file for every user's (initial) password hash. + for f in ${builtins.toString (lib.mapAttrsToList (user: value: value.hashedPasswordFile) cfg.accounts)}; do + if [ ! -f "$f" ]; then + echo "Expected password hash file $f does not exist!" + exit 1 + fi + done + # Prepare static passwd-file for system users - ${mailnixCmd} generate-static-passdb > "${staticPasswdFile}" + cat < "${staticPasswdFile}" + ${lib.concatStringsSep "\n" (lib.mapAttrsToList genPasswdEntry systemUsers)} + EOF - # Prepare/Update passwd-file for dynamic users - ${mailnixCmd} update-dynamic-passdb ${dovecotDynamicPasswdFile} > "${dovecotDynamicPasswdFile}" + # Prepare initial passwd-file for dynamic users + # (used for lookup during actual passwd-file generation) + cat < "${initialPasswdFile}" + ${lib.concatStringsSep "\n" (lib.mapAttrsToList genPasswdEntry normalUsers)} + EOF - ${lib.optionalString cfg.roundcube.enable '' - # Ensure roundcube has access to dynamic passwd file - ${pkgs.acl.bin}/bin/setfacl -m "u:${config.services.phpfpm.pools.roundcube.user}:rw" "${dovecotDynamicPasswdFile}" - ''} + # Check for existence of dynamic passwd-file + touch "${dovecotDynamicPasswdFile}" + if (! test -f "${dovecotDynamicPasswdFile}"); then + echo "${dovecotDynamicPasswdFile} exists and is no regular file" + exit 1 + fi + # Ensure that only configured users are actually present and remove any others + truncate -s 0 "${dovecotDynamicPasswdFile}-filtered" + for u in ${builtins.toString (lib.mapAttrsToList (user: value: value.name) normalUsers)}; do + if grep -q "^$u:" "${dovecotDynamicPasswdFile}"; then + # User already has some password set -> Keep currently set password + grep "^$u:" "${dovecotDynamicPasswdFile}" >> "${dovecotDynamicPasswdFile}-filtered" + else + # User has no password set, yet -> Take password from initialPasswdFile + grep "^$u:" "${initialPasswdFile}" >> "${dovecotDynamicPasswdFile}-filtered" + fi + done + mv "${dovecotDynamicPasswdFile}-filtered" "${dovecotDynamicPasswdFile}" # Prepare userdb-file - ${mailnixCmd} generate-userdb > "${userdbFile}" + cat < "${userdbFile}" + ${lib.concatStringsSep "\n" (lib.mapAttrsToList genUserdbEntry cfg.accounts)} + EOF ''; - genMaildirScript = pkgs.writeScript "generate-maildir" '' + genMaildir = pkgs.writeScript "generate-maildir" '' #!${pkgs.stdenv.shell} # Create mail directory and set permissions accordingly. @@ -94,14 +129,6 @@ in { ) cfg.accounts; - environment.systemPackages = [ - # sieves + managesieve - pkgs.dovecot_pigeonhole - ]; - - # For compatibility with python imaplib - environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; - services.dovecot2 = { enable = true; enableImap = true; @@ -117,6 +144,11 @@ in { enableLmtp = true; + modules = [ + # sieves + managesieve + pkgs.dovecot_pigeonhole + ]; + # enable managesieve protocols = ["sieve"]; @@ -236,7 +268,7 @@ in { userdb { driver = passwd-file args = ${userdbFile} - default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}/%{domain}/%{username} + default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}/%d/%n } service auth { @@ -264,7 +296,7 @@ in { systemd.services.dovecot2 = { preStart = '' ${genAuthDbsScript} - ${genMaildirScript} + ${genMaildir} ''; wants = sslCertService; after = sslCertService; diff --git a/mailsystem/nginx.nix b/mailsystem/nginx.nix index 919bf7c..03e8f26 100644 --- a/mailsystem/nginx.nix +++ b/mailsystem/nginx.nix @@ -4,22 +4,19 @@ lib, ... }: -with (import ./common.nix {inherit config pkgs;}); let +with (import ./common.nix {inherit config;}); let cfg = config.mailsystem; in { config = lib.mkIf cfg.enable { services.nginx = { enable = true; - virtualHosts."${cfg.fqdn}" = - { - forceSSL = true; - enableACME = cfg.certificateScheme == "acme"; - } - // lib.optionalAttrs (cfg.certificateScheme == "selfsigned") { - sslCertificate = sslCertPath; - sslCertificateKey = sslKeyPath; - }; + virtualHosts."${cfg.fqdn}" = { + forceSSL = true; + enableACME = cfg.certificateScheme == "acme"; + sslCertificate = lib.mkIf (cfg.certificateScheme == "selfsigned") sslCertPath; + sslCertificateKey = lib.mkIf (cfg.certificateScheme == "selfsigned") sslKeyPath; + }; }; networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [80 443]; diff --git a/mailsystem/postfix.nix b/mailsystem/postfix.nix index 3e31cb6..c1f41da 100644 --- a/mailsystem/postfix.nix +++ b/mailsystem/postfix.nix @@ -4,29 +4,55 @@ pkgs, ... }: -with (import ./common.nix {inherit config pkgs;}); let +with (import ./common.nix {inherit config;}); let cfg = config.mailsystem; mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; - runtimeDir = "/run/postfix"; - aliases_file = "${runtimeDir}/virtual_aliases"; - virtual_domains_file = "${runtimeDir}/virtual_domains"; - denied_recipients_file = "${runtimeDir}/denied_recipients"; + attrsToLookupTable = aliases: let + lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases; + in + mergeLookupTables lookupTables; - genPostmapsScript = pkgs.writeScript "generate-postfix-postmaps" '' - #!${pkgs.stdenv.shell} - set -euo pipefail + lookupTableToString = attrs: let + valueToString = value: lib.concatStringsSep ", " value; + in + lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); - if (! test -d "${runtimeDir}"); then - mkdir "${runtimeDir}" - chmod 755 "${runtimeDir}" - fi + mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables; - ${mailnixCmd} "generate-aliases" > "${aliases_file}" - ${mailnixCmd} "generate-domains" > "${virtual_domains_file}" - ${mailnixCmd} "generate-denied-recipients" > "${denied_recipients_file}" - ''; + account_virtual_aliases = mergeLookupTables (lib.flatten (lib.mapAttrsToList + (name: value: let + to = name; + in + map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name)) + cfg.accounts)); + + virtual_domain_aliases = let + alias_domains = + lib.concatMapAttrs (src: dst: { + "@${src}" = "@${dst}"; + }) + cfg.virtualDomainAliases; + in + attrsToLookupTable alias_domains; + + extra_virtual_aliases = attrsToLookupTable cfg.extraVirtualAliases; + + all_virtual_aliases = mergeLookupTables [account_virtual_aliases virtual_domain_aliases extra_virtual_aliases]; + + 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); submission_header_cleanup_rules = pkgs.writeText "submission_header_cleanup_rules" '' # Removes sensitive headers from mails handed in via the submission port. @@ -48,15 +74,14 @@ with (import ./common.nix {inherit config pkgs;}); let tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; in { config = lib.mkIf cfg.enable { - 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; + 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; services.postfix = { enable = true; @@ -68,8 +93,10 @@ in { enableSubmissions = true; + # TODO: create function to simplify this? mapFiles."virtual_aliases" = aliases_file; - mapFiles."denied_recipients" = denied_recipients_file; + mapFiles."virtual_accounts" = virtual_accounts_file; + virtual = lookupTableToString all_virtual_aliases; submissionsOptions = { smtpd_tls_security_level = "encrypt"; @@ -79,7 +106,8 @@ in { smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = "$myhostname"; smtpd_client_restrictions = "permit_sasl_authenticated,reject"; - smtpd_sender_login_maps = mappedFile "virtual_aliases"; + # use mappedFile -> different path? + smtpd_sender_login_maps = "hash:/etc/postfix/virtual_accounts"; 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"; @@ -96,9 +124,6 @@ in { virtual_gid_maps = "static:${toString cfg.vmailUID}"; virtual_mailbox_base = cfg.mailDirectory; virtual_mailbox_domains = virtual_domains_file; - virtual_alias_maps = [ - (mappedFile "virtual_aliases") - ]; virtual_mailbox_maps = [ (mappedFile "virtual_aliases") ]; @@ -115,9 +140,11 @@ in { "permit_sasl_authenticated" "reject_unauth_destination" ]; - smtpd_recipient_restrictions = [ - "check_recipient_access ${mappedFile "denied_recipients"}" - ]; + + # quota checking # TODO: wo ist hier quota?? + # smtpd_recipient_restrictions = [ + # "check_policy_service inet:localhost:12340" # XXX + # ]; # TLS settings, inspired by https://github.com/jeaye/nix-files # Submission by mail clients is handled in submissionOptions @@ -182,11 +209,6 @@ in { }; }; - systemd.services.postfix-setup = { - preStart = '' - ${genPostmapsScript} - ''; - }; systemd.services.postfix = { wants = sslCertService; after = diff --git a/mailsystem/roundcube.nix b/mailsystem/roundcube.nix index 4c11c51..bebffaf 100644 --- a/mailsystem/roundcube.nix +++ b/mailsystem/roundcube.nix @@ -4,14 +4,14 @@ pkgs, ... }: -with (import ./common.nix {inherit config pkgs;}); let +with (import ./common.nix {inherit config;}); let cfg = config.mailsystem; roundcubeCfg = config.mailsystem.roundcube; in { options.mailsystem.roundcube = { enable = lib.mkOption { type = lib.types.bool; - default = false; + default = true; description = "Whether to enable roundcube in order to provide a webmail interface"; }; hostName = lib.mkOption { @@ -32,9 +32,8 @@ in { hostName = roundcubeCfg.hostName; plugins = ["managesieve" "password"]; extraConfig = '' - // Use implicitly encrypted communications for imap and imap (implicit tls) - $config['imap_host'] = "ssl://${cfg.fqdn}"; - $config['smtp_host'] = "ssl://${cfg.fqdn}"; + // Use starttls for authentication + $config['smtp_host'] = "tls://${cfg.fqdn}"; $config['smtp_user'] = "%u"; $config['smtp_pass'] = "%p"; @@ -47,8 +46,8 @@ in { // Enables saving the new password even if it machtes the old password. Useful // for upgrading the stored passwords after the encryption scheme has changed. $config['password_force_save'] = true; - $config['password_dovecot_passwdfile_path'] = "${dovecotDynamicPasswdFile}"; - $config['password_dovecotpw'] = "${pkgs.dovecot}/bin/doveadm pw"; + $config['password_dovecot_passwdfile_path'] = "${pkgs.dovecot}/bin/doveadm pw"; + $config['password_dovecotpw'] = "${dovecotDynamicPasswdFile}"; $config['password_dovecotpw_method'] = "${roundcubeCfg.passwordHashingAlgorithm}"; $config['password_dovecotpw_with_method'] = true; ''; diff --git a/mailsystem/rspamd.nix b/mailsystem/rspamd.nix index 851693a..46cb0c4 100644 --- a/mailsystem/rspamd.nix +++ b/mailsystem/rspamd.nix @@ -4,15 +4,14 @@ pkgs, ... }: -with (import ./common.nix {inherit config pkgs;}); let +with (import ./common.nix {inherit config;}); let cfg = config.mailsystem; - dovecot2Cfg = config.services.dovecot2; - nginxCfg = config.services.nginx; + nginxcfg = config.services.nginx; postfixCfg = config.services.postfix; redisCfg = config.services.redis.servers.rspamd; rspamdCfg = config.services.rspamd; - genSystemdSocketCfg = name: socketPath: additionalUsers: { + genSystemdSocketCfg = name: socketPath: additionalUser: { description = "rspamd ${name} worker socket"; listenStreams = [socketPath]; requiredBy = ["rspamd.service"]; @@ -21,48 +20,31 @@ with (import ./common.nix {inherit config pkgs;}); let SocketUser = rspamdCfg.user; SocketMode = 0600; ExecStartPost = - lib.mkIf (additionalUsers != []) - (pkgs.writeShellScript "set-systemd-socket-permissions" - (lib.concatMapStringsSep "\n" (user: ''${pkgs.acl.bin}/bin/setfacl -m "u:${user}:rw" "${socketPath}"'') - additionalUsers)); + lib.mkIf (additionalUser != "") + ''${pkgs.acl.bin}/bin/setfacl -m "u:${additionalUser}:rw" "${socketPath}"''; }; }; in { options.mailsystem.rspamd.webUi = { enable = lib.mkOption { type = lib.types.bool; - default = false; + default = true; description = "Whether to enable the rspamd webui on `https://${config.mailsystem.fqdn}/rspamd`"; }; basicAuthFile = lib.mkOption { type = lib.types.str; - description = "Path to basic auth file (entries can be generated using htpasswd)"; + description = "Path to basic auth file"; }; }; config = lib.mkIf cfg.enable { - assertions = - [ - { - assertion = !cfg.rspamd.webUi.enable || cfg.rspamd.webUi.basicAuthFile != null; - message = "Setting basicAuthFile is required if rspamd's web interface is enabled"; - } - ] - ++ lib.mapAttrsToList ( - domain: dkimList: { - assertion = builtins.elem domain cfg.domains; - message = "Domain ${domain} as per `config.mailsystem.dkimSettings` needs to be managed by the mailserver."; - } - ) - cfg.dkimSettings - ++ lib.mapAttrsToList ( - domain: dkimList: { - assertion = dkimList != []; - message = "Entry ${domain} as per `config.mailsystem.dkimSettings` must not be an empty list."; - } - ) - cfg.dkimSettings; + assertions = [ + { + assertion = !cfg.rspamd.webUi.enable || cfg.rspamd.webUi.basicAuthFile != null; + message = "Setting basicAuthFile is required if rspamd's web interface is enabled"; + } + ]; services.rspamd = { enable = true; @@ -75,38 +57,6 @@ in { } ''; }; - "dkim_signing.conf" = let - genDkimSelectorList = entry: '' - { - path: "${entry.keyFile}"; - selector: "${entry.selector}"; - } - ''; - genDkimDomainCfg = domain: domainSettings: '' - ${domain} { - selectors [ - ${lib.concatStringsSep "\n" (map genDkimSelectorList domainSettings)} - ] - } - ''; - in { - text = - '' - sign_authenticated = true; - use_esld = true; - use_domain = "header"; - check_pubkey = true; - allow_username_mismatch = true; - allow_hdrfrom_mismatch = true; - allow_hdrfrom_mismatch_sign_networks = true; - - '' - + lib.optionalString (cfg.dkimSettings != {}) '' - domain { - ${lib.concatStringsSep "\n" (lib.mapAttrsToList genDkimDomainCfg cfg.dkimSettings)} - } - ''; - }; "milter_headers.conf" = { text = '' # Add headers related to spam-detection @@ -152,8 +102,10 @@ in { }; systemd.sockets = { - rspamd-proxy = genSystemdSocketCfg "proxy" rspamdProxySocket [postfixCfg.user]; - rspamd-controller = genSystemdSocketCfg "controller" rspamdControllerSocket ([dovecot2Cfg.mailUser] ++ lib.optional cfg.rspamd.webUi.enable nginxCfg.user); + rspamd-proxy = genSystemdSocketCfg "proxy" rspamdProxySocket postfixCfg.user; + rspamd-controller = genSystemdSocketCfg "controller" rspamdControllerSocket ( + lib.optionalString cfg.rspamd.webUi.enable nginxCfg.user + ); }; systemd.services.rspamd = { @@ -168,10 +120,6 @@ in { locations."/rspamd" = { proxyPass = "http://unix:${rspamdControllerSocket}:/"; basicAuthFile = cfg.rspamd.webUi.basicAuthFile; - extraConfig = '' - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - ''; }; sslCertificate = lib.mkIf (cfg.certificateScheme == "selfsigned") sslCertPath; sslCertificateKey = lib.mkIf (cfg.certificateScheme == "selfsigned") sslKeyPath; diff --git a/mailsystem/selfsigned.nix b/mailsystem/selfsigned.nix index 1a27318..4506fcf 100644 --- a/mailsystem/selfsigned.nix +++ b/mailsystem/selfsigned.nix @@ -4,7 +4,7 @@ lib, ... }: -with (import ./common.nix {inherit config pkgs;}); let +with (import ./common.nix {inherit config;}); let cfg = config.mailsystem; in { config = lib.mkIf (cfg.enable && cfg.certificateScheme == "selfsigned") { diff --git a/pkgs/mailnix/Cargo.lock b/pkgs/mailnix/Cargo.lock deleted file mode 100644 index b916c85..0000000 --- a/pkgs/mailnix/Cargo.lock +++ /dev/null @@ -1,806 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anstream" -version = "0.6.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" - -[[package]] -name = "anstyle-parse" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "cc" -version = "1.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "clap" -version = "4.5.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", - "serde", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "iana-time-zone" -version = "0.1.63" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" -dependencies = [ - "equivalent", - "hashbrown 0.15.3", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "libc" -version = "0.2.172" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" - -[[package]] -name = "libyml" -version = "0.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" -dependencies = [ - "anyhow", - "version_check", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "mailnix" -version = "0.1.0" -dependencies = [ - "clap", - "regex", - "serde", - "serde_with", - "serde_yml", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.95" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rustversion" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.140" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "serde_with" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" -dependencies = [ - "base64", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.9.0", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_yml" -version = "0.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" -dependencies = [ - "indexmap 2.9.0", - "itoa", - "libyml", - "memchr", - "ryu", - "serde", - "version_check", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/pkgs/mailnix/Cargo.toml b/pkgs/mailnix/Cargo.toml deleted file mode 100644 index accee63..0000000 --- a/pkgs/mailnix/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "mailnix" -version = "0.1.0" -edition = "2021" - -[dependencies] -clap = { version = "4.5.28", features = ["derive"] } -regex = "1.11.1" -serde = { version = "1.0.217", features = ["derive", "rc"] } -serde_with = "3.12.0" -serde_yml = "0.0.12" diff --git a/pkgs/mailnix/src/cli.rs b/pkgs/mailnix/src/cli.rs deleted file mode 100644 index ab995fa..0000000 --- a/pkgs/mailnix/src/cli.rs +++ /dev/null @@ -1,22 +0,0 @@ -use clap::{Parser, Subcommand}; - -#[derive(Subcommand, Debug)] -pub enum Commands { - Check, - GenerateUserdb, - GenerateStaticPassdb, - UpdateDynamicPassdb { path: String }, - GenerateAliases, - GenerateDeniedRecipients, - GenerateDomains, -} - -#[derive(Parser, Debug)] -#[command(author, version, about)] -pub struct Cli { - pub config_path: String, - pub additional_config_path: Option, - - #[command(subcommand)] - pub command: Commands, -} diff --git a/pkgs/mailnix/src/config.rs b/pkgs/mailnix/src/config.rs deleted file mode 100644 index b3dc274..0000000 --- a/pkgs/mailnix/src/config.rs +++ /dev/null @@ -1,226 +0,0 @@ -use serde::Deserialize; -use serde_with::formats::PreferMany; -use serde_with::{MapPreventDuplicates, OneOrMany, serde_as}; - -use regex::Regex; -use std::collections::{HashMap, HashSet}; -use std::error::Error; -use std::fs::{self, File}; -use std::io::BufReader; -use std::path::Path; - -#[serde_as] -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Config { - #[serde(default)] - pub domains: Vec, - - #[serde(default)] - #[serde_as(deserialize_as = "MapPreventDuplicates<_, _>")] - pub accounts: HashMap, - - #[serde(default)] - #[serde_as(deserialize_as = "MapPreventDuplicates<_, OneOrMany<_, PreferMany>>")] - pub aliases: HashMap>, -} - -fn default_reject_message() -> String { - "This account cannot receive emails.".to_string() -} - -#[derive(Clone, Debug, Deserialize)] -pub struct AccountConfig { - #[serde(rename(deserialize = "hashedPassword"))] - #[serde(skip)] - pub hashed_password: String, - - #[serde(rename(deserialize = "hashedPassword"))] - hashed_password_: Option, - - #[serde(rename(deserialize = "hashedPasswordFile"))] - hashed_password_file_: Option, - - #[serde(default, rename(deserialize = "isSystemUser"))] - pub is_system_user: bool, - - #[serde( - default = "default_reject_message", - rename(deserialize = "rejectMessage") - )] - pub reject_message: String, -} - -fn sanitize_vec(vec: &mut [String]) -> Result<(), Box> { - let mut seen = HashSet::new(); - for item in vec.iter_mut() { - *item = item.to_lowercase(); - if !seen.insert(item.clone()) { - return Err(format!("Duplicate entry ({item}) detected, aborting...").into()); - } - } - Ok(()) -} - -fn sanitize_map_keys(map: &mut HashMap) -> Result<(), Box> { - let mut new_map = HashMap::::new(); - let mut seen = HashSet::new(); - for (k, v) in map.iter() { - let new_k = k.to_lowercase(); - new_map.insert(new_k.clone(), v.clone()); - if !seen.insert(new_k.clone()) { - return Err(format!("Duplicate entry ({new_k}) detected, aborting...").into()); - } - } - *map = new_map; - Ok(()) -} - -fn is_domain(address: &str) -> bool { - !address.contains("@") -} - -fn parse_address(address: &str) -> Result<(String, String), Box> { - let address_pattern = Regex::new(r"^(?P.*)@(?P.*)$").unwrap(); - let caps = address_pattern - .captures(address) - .ok_or("Mail address regex does not match for \"{address}\"") - .unwrap(); - Ok((caps["local_part"].to_string(), caps["domain"].to_string())) -} - -impl Config { - pub fn load>(path: P) -> Result> { - let file = File::open(path)?; - let reader = BufReader::new(file); - - let mut cfg: Config = serde_yml::from_reader(reader)?; - cfg.sanitize()?; - for (name, acc) in cfg.accounts.iter_mut() { - match ( - acc.hashed_password_.clone(), - acc.hashed_password_file_.clone(), - ) { - (Some(_hash), Some(_file)) => { - return Err("Account {name} has both HashedPassword and HashedPasswordFile set, aborting...".into()); - } - (Some(hash), None) => { - acc.hashed_password = hash; - } - (None, Some(file)) => { - let hash = fs::read_to_string(file.clone()).unwrap_or_else(|err| { - panic!("Account ({name}): Reading {file} failed: {err:?}") - }); - acc.hashed_password = hash.trim().to_string(); - } - (None, None) => { - return Err("Account {name} is missing HashedPassword or HashedPasswordFile, aborting...".into()); - } - } - } - Ok(cfg) - } - - fn sanitize(&mut self) -> Result<(), Box> { - // standardize capitalization, ... - sanitize_vec(&mut self.domains).unwrap_or_else(|err| panic!("domain: {err:?}")); - sanitize_map_keys(&mut self.accounts).unwrap_or_else(|err| panic!("accounts: {err:?}")); - sanitize_map_keys(&mut self.aliases).unwrap_or_else(|err| panic!("aliases: {err:?}")); - for (from, dests) in self.aliases.iter_mut() { - sanitize_vec(&mut *dests).unwrap_or_else(|err| panic!("aliases ({from}): {err:?}")); - } - Ok(()) - } - - fn is_valid_destination(&self, address: &String) -> bool { - if self.accounts.contains_key(address) { - return true; - } - - // There is no explicit account matching 'address'. However, 'address' may still be - // implicitly valid due to the existence of a catchall-alias or the domain being aliased. - let (local_part, domain) = parse_address(address).unwrap(); - if let Some(dests) = self.aliases.get(&domain) { - if dests.iter().filter(|dest| !is_domain(dest)).count() > 0 { - // At least one explicit catchall-address exists! - return true; - } - // At this point, we need to (recursively) iterate over domain-aliases to check whether - // there is an exact match (or a catchall-alias for the aliased domain). - for dest in dests.iter().filter(|addr| is_domain(addr)) { - let aliased_addr = format!("{local_part}@{dest}"); - // FIXME: This current implementation may not terminate if the configured aliases - // contain a loop! - if self.is_valid_destination(&aliased_addr) { - return true; - } - } - } - // alias destination is not valid - false - } - - pub fn check(&self) -> Result<(), Box> { - // check whether all account domains exist - for name in self.accounts.keys() { - let (_, domain) = parse_address(name).unwrap(); - if !self.domains.contains(&domain) { - panic!("Domain of account \"{name}\" does not exist"); - } - } - - // check whether aliases have corresponding accounts/domains - for (from, dests) in self.aliases.iter() { - if is_domain(from) && !self.domains.contains(from) { - panic!("Aliased from-domain \"{from}\" does not exist"); - } - for dest in dests.iter() { - if is_domain(dest) && !self.domains.contains(dest) { - panic!("Aliased dest-domain \"{dest}\" does not exist"); - } else if !is_domain(dest) && !self.is_valid_destination(dest) { - panic!("Aliased dest-account \"{dest}\" does not exist"); - } - } - } - Ok(()) - } - - pub fn merge>(&mut self, path: P) -> Result<(), Box> { - let mut other = Config::load(path).unwrap(); - - for domain in self.domains.iter() { - if other.domains.contains(domain) { - return Err( - "domains: Duplicate entry ({domain}) during merge detected, aborting..." - .to_string() - .into(), - ); - } - } - self.domains.append(&mut other.domains); - - for name in self.accounts.keys() { - if other.accounts.contains_key(name) { - return Err( - "accounts: Duplicate entry ({name}) during merge detected, aborting..." - .to_string() - .into(), - ); - } - } - self.accounts.extend(other.accounts); - - for alias in self.aliases.keys() { - if other.aliases.contains_key(alias) { - return Err( - "aliases: Duplicate entry ({alias}) during merge detected, aborting..." - .to_string() - .into(), - ); - } - } - self.aliases.extend(other.aliases); - - Ok(()) - } -} diff --git a/pkgs/mailnix/src/dovecot.rs b/pkgs/mailnix/src/dovecot.rs deleted file mode 100644 index 341afa7..0000000 --- a/pkgs/mailnix/src/dovecot.rs +++ /dev/null @@ -1,67 +0,0 @@ -use regex::Regex; -use std::collections::HashMap; -use std::error::Error; -use std::fs; -use std::path::Path; - -use crate::config::{AccountConfig, Config}; - -pub fn generate_userdb(cfg: Config) { - for name in cfg.accounts.into_keys() { - println!("{}:::::::", name); - } -} - -pub fn generate_static_passdb(cfg: Config) { - let system_accounts = cfg - .accounts - .into_iter() - .filter(|(_, acc)| acc.is_system_user); - for (name, acc) in system_accounts { - println!("{}:{}::::::", name, acc.hashed_password); - } -} - -fn try_load_passdb>(path: P) -> Result, Box> { - let mut curr_dynamic_users = HashMap::new(); - - if path.as_ref().exists() { - let re = Regex::new(r"^(?P.*):(?P.*)::::::$").unwrap(); - - let curr_passdb = fs::read_to_string(path.as_ref()).unwrap(); - for line in curr_passdb.lines() { - let caps = re - .captures(line) - .unwrap_or_else(|| panic!("Regex does not match line: {line}")); - curr_dynamic_users.insert( - caps["name"].to_string(), - caps["hashed_password"].to_string(), - ); - } - eprintln!("current passdb entries: {curr_dynamic_users:#?}"); - } - Ok(curr_dynamic_users) -} - -pub fn update_dynamic_passdb>(cfg: Config, path: P) -> Result<(), Box> { - // create hashmap of all accounts with their initial passdb-lines - let mut accounts: HashMap = cfg - .accounts - .into_iter() - .filter(|(_, acc)| !acc.is_system_user) - .collect(); - eprintln!("settings: {:#?}", accounts); - - // load current passdb and update account password hashes - let curr_dynamic_users = try_load_passdb(path)?; - for (name, hashed_password) in curr_dynamic_users.into_iter() { - accounts.entry(name).and_modify(|e| { - e.hashed_password = hashed_password; - }); - } - - for (name, acc) in accounts.into_iter() { - println!("{}:{}::::::", name, acc.hashed_password); - } - Ok(()) -} diff --git a/pkgs/mailnix/src/main.rs b/pkgs/mailnix/src/main.rs deleted file mode 100644 index 86847ab..0000000 --- a/pkgs/mailnix/src/main.rs +++ /dev/null @@ -1,33 +0,0 @@ -mod cli; -mod config; -mod dovecot; -mod postfix; - -use crate::{ - cli::{Cli, Commands}, - config::Config, -}; - -use clap::Parser; - -fn main() { - let args = Cli::parse(); - let mut cfg = Config::load(args.config_path).unwrap(); - if let Some(additional_cfg_path) = args.additional_config_path { - cfg.merge(additional_cfg_path).unwrap(); - } - let cfg = cfg; - cfg.check().unwrap(); - - match &args.command { - Commands::Check => println!("Check: {:#?}", cfg), - Commands::GenerateUserdb => dovecot::generate_userdb(cfg), - Commands::GenerateStaticPassdb => dovecot::generate_static_passdb(cfg), - Commands::UpdateDynamicPassdb { path } => { - dovecot::update_dynamic_passdb(cfg, path).unwrap() - } - Commands::GenerateAliases => postfix::generate_aliases(cfg), - Commands::GenerateDeniedRecipients => postfix::generate_denied_recipients(cfg), - Commands::GenerateDomains => postfix::generate_domains(cfg), - } -} diff --git a/pkgs/mailnix/src/postfix.rs b/pkgs/mailnix/src/postfix.rs deleted file mode 100644 index d7b456b..0000000 --- a/pkgs/mailnix/src/postfix.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::config::Config; - -fn prefix_domain(value: String) -> String { - if !value.contains("@") { - return format!("@{value}"); - } - value -} - -pub fn generate_aliases(cfg: Config) { - for name in cfg.accounts.into_keys() { - println!("{} {}", name, name); - } - for (src, dests) in cfg.aliases.into_iter() { - let dests: Vec<_> = dests.into_iter().map(prefix_domain).collect(); - println!("{} {}", prefix_domain(src), dests.join(", ")); - } -} - -pub fn generate_denied_recipients(cfg: Config) { - let system_accounts = cfg - .accounts - .into_iter() - .filter(|(_, acc)| acc.is_system_user); - for (name, acc) in system_accounts { - println!("{} REJECT {}", name, acc.reject_message); - } -} - -pub fn generate_domains(cfg: Config) { - println!("{}", cfg.domains.into_iter().collect::>().join("\n")); -} diff --git a/tests/aliases.nix b/tests/aliases.nix index 4db867d..cd3aae3 100644 --- a/tests/aliases.nix +++ b/tests/aliases.nix @@ -1,6 +1,5 @@ {pkgs, ...}: with (import ./common/lib.nix {inherit pkgs;}); let - lib = pkgs.lib; accounts = { "normal" = { address = "user1@example.com"; @@ -12,23 +11,32 @@ with (import ./common/lib.nix {inherit pkgs;}); let }; "alias" = { address = "user3@example.com"; + aliases = ["alias@example.com"]; password = "secret-password3"; }; + "extra-alias" = { + address = "user4@example.com"; + password = "secret-password4;"; + }; "multi-alias1" = { address = "multi-alias1@example.com"; - password = "secret-password4;"; + aliases = ["multi-alias@example.com"]; + password = "secret-password5;"; }; "multi-alias2" = { address = "multi-alias2@example.com"; - password = "secret-password5;"; + aliases = ["multi-alias@example.com"]; + password = "secret-password6;"; }; "catchall" = { address = "catchall@example.com"; - password = "secret-password6;"; + aliases = ["@example.com"]; + password = "secret-password7;"; }; "otherdomain" = { address = "otherdomain@example.com"; - password = "secret-password7;"; + aliases = ["user@otherdomain.com"]; + password = "secret-password8;"; }; }; in @@ -42,14 +50,11 @@ in fqdn = "mail.example.com"; domains = ["example.com" "aliased.com" "otherdomain.com"]; accounts = mkAccounts accounts; - virtualAliases = { - # domain aliases + virtualDomainAliases = { "aliased.com" = "example.com"; - # 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; - "user@otherdomain.com" = accounts."otherdomain".address; + }; + extraVirtualAliases = { + "extra-alias@example.com" = accounts."extra-alias".address; }; }; }; @@ -66,6 +71,7 @@ in }; sendMail = mkSendMail smtpSettings accounts; recvMail = mkRecvMail serverAddr accounts; + cfg = nodes.server.mailsystem; in '' start_all() @@ -177,6 +183,18 @@ in # fetchmail returns EXIT_CODE 0 when it retrieves mail client.succeed("${recvMail "otherdomain"} >&2") + with subtest("mail incoming on extraVirtualAlias"): + client.succeed("${sendMail "normal" "" "extra-alias@example.com" '' + Subject: extraVirtualAliases-Test + + Hello User4, + this is mail is sent to you by using an extraVirtualAlias as recipient. + ''}") + server.wait_until_fails('${pendingPostqueue}') + client.execute("${cleanupMail}") + # fetchmail returns EXIT_CODE 0 when it retrieves mail + client.succeed("${recvMail "extra-alias"} >&2") + with subtest("receiving mail on aliased domain using normal account"): client.succeed("${sendMail "normal" "" "user2@aliased.com" '' Subject: aliasedDomain with normal account diff --git a/tests/basic.nix b/tests/basic.nix index cdee2ad..615d2a1 100644 --- a/tests/basic.nix +++ b/tests/basic.nix @@ -9,11 +9,6 @@ with (import ./common/lib.nix {inherit pkgs;}); let address = "user2@example.com"; password = "secret-password2"; }; - "system" = { - address = "system@example.com"; - password = "secret-password3"; - isSystemUser = true; - }; }; in pkgs.nixosTest { @@ -82,27 +77,6 @@ in I'm pretending to be someotheraddress@example.com and the mailserver should reject this attempt. ''}") - with subtest("send succeeds for system user"): - client.succeed("${sendMail "system" "" accounts."normal".address '' - Subject: Testmail2 - - Hello User1, - this is some text! - ''}") - # give the mail server some time to process the mail - server.wait_until_fails('${pendingPostqueue}') - - with subtest("mail can be retrieved via imap"): - client.succeed("${recvMail "normal"} >&2") - - with subtest("mail sent to system-account is rejected"): - client.fail("${sendMail "normal" "someotheraddress@example.com" accounts."system".address '' - Subject: Mail to system-account - - Hello System user, - this mail should never reach you as it should be rejected by postfix. - ''}") - with subtest("server issues no warnings nor errors"): ${checkLogs "server"} ''; diff --git a/tests/common/lib.nix b/tests/common/lib.nix index a2b1a15..1930be9 100644 --- a/tests/common/lib.nix +++ b/tests/common/lib.nix @@ -2,7 +2,7 @@ lib = pkgs.lib; in rec { waitForRspamd = node: let - inherit (import ../../mailsystem/common.nix {inherit (node) config pkgs;}) rspamdProxySocket; + inherit (import ../../mailsystem/common.nix {inherit (node) config;}) rspamdProxySocket; in "set +e; timeout 1 ${node.nixpkgs.pkgs.netcat}/bin/nc -U ${rspamdProxySocket} < /dev/null; [ $? -eq 124 ]"; mkHashedPasswordFile = password: diff --git a/tests/common/server.nix b/tests/common/server.nix index 561d1bb..9a47284 100644 --- a/tests/common/server.nix +++ b/tests/common/server.nix @@ -1,26 +1,13 @@ -{lib, ...}: { +{...}: { imports = [./../../mailsystem]; config = { virtualisation.memorySize = 1024; mailsystem = { enable = true; + + roundcube.enable = false; + rspamd.webUi.enable = false; certificateScheme = "selfsigned"; }; - - # Prevent tests deadlocking on DNS requests which will never succeed. - services.rspamd.locals."options.inc".text = '' - dns { - nameservers = ["127.0.0.1"]; - timeout = 0.0s; - retransmits = 0; - } - ''; - - # Enable more verbose logging (required for, e.g., sieve testing) - services.dovecot2.extraConfig = lib.mkAfter '' - mail_debug = yes - auth_debug = yes - verbose_ssl = yes - ''; }; } diff --git a/tests/rspamd.nix b/tests/rspamd.nix deleted file mode 100644 index eb71ed6..0000000 --- a/tests/rspamd.nix +++ /dev/null @@ -1,178 +0,0 @@ -{pkgs, ...}: -with (import ./common/lib.nix {inherit pkgs;}); let - lib = pkgs.lib; - accounts = { - "normal" = { - address = "user@example.com"; - password = "secret-password1"; - }; - "normal2" = { - address = "user@example.org"; - password = "secret-password2;"; - }; - }; - genDkimSecret = domain: name: type: - pkgs.runCommand "mk-dkim-secrets-${domain}-${selector}" { - buildInputs = [pkgs.rspamd]; - inherit domain name type; - } '' - rspamadm dkim_keygen -d $domain -s $name -t $type ${lib.optionalString (type == "rsa") "-b 2048"} -k $out - ''; - - mkDkimSettings = domains: selectors: - lib.listToAttrs ( - map (domain: - lib.nameValuePair domain (map (entry: { - selector = entry.name; - keyFile = genDkimSecret domain entry.name entry.type; - }) - selectors)) - domains - ); -in - pkgs.nixosTest { - name = "rspamd"; - nodes = { - server = {pkgs, ...}: { - imports = [./common/server.nix]; - mailsystem = { - fqdn = "mail.example.com"; - domains = ["example.com" "example.org"]; - accounts = mkAccounts accounts; - dkimSettings = mkDkimSettings ["example.com" "example.org"] [ - { - name = "elliptic"; - type = "ed25519"; - } - { - name = "selector"; - type = "rsa"; - } - ]; - }; - }; - client = {...}: { - imports = [./common/client.nix]; - }; - }; - testScript = {nodes, ...}: let - cfg = nodes.server.mailsystem; - serverAddr = nodes.server.networking.primaryIPAddress; - clientAddr = nodes.client.networking.primaryIPAddress; - smtpSettings = { - address = serverAddr; - port = 465; - }; - sendMail = mkSendMail smtpSettings accounts; - recvMail = mkRecvMail serverAddr accounts; - test-mark-spam = accountName: - pkgs.writeScript "imap-mark-spam" '' - #!${pkgs.python3.interpreter} - import imaplib - - with imaplib.IMAP4_SSL('${serverAddr}') as imap: - imap.login('${accounts."${accountName}".address}', '${accounts."${accountName}".password}') - imap.select() - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.copy(','.join(msg_ids), 'Junk') - for num in msg_ids: - imap.store(num, '+FLAGS', '\\Deleted') - imap.expunge() - - imap.select('Junk') - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.close() - ''; - test-mark-ham = accountName: - pkgs.writeScript "imap-mark-ham" '' - #!${pkgs.python3.interpreter} - import imaplib - - with imaplib.IMAP4_SSL('${serverAddr}') as imap: - imap.login('${accounts."${accountName}".address}', '${accounts."${accountName}".password}') - imap.select('Junk') - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.copy(','.join(msg_ids), 'INBOX') - for num in msg_ids: - imap.store(num, '+FLAGS', '\\Deleted') - imap.expunge() - - imap.select('INBOX') - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.close() - ''; - in '' - start_all() - - server.wait_for_unit("multi-user.target") - client.wait_for_unit("multi-user.target") - server.wait_until_succeeds("${waitForRspamd nodes.server}") - - with subtest("rspamd configuration is valid"): - server.succeed("${pkgs.rspamd}/bin/rspamadm configtest >&2") - - with subtest("rspamd rejects spam"): - client.fail("${sendMail "normal" "" accounts."normal2".address '' - Subject: GTUBE-Test - - Hello User2, - this is a mail containing a GTUBE pattern that should result in the rejection of this mail. - - XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X - ''}") - - with subtest("imap sieve junk trainer"): - client.succeed("${sendMail "normal" "" accounts."normal2".address '' - Subject: Testmail - - Hello User2, - this is a testmail. - ''}") - server.wait_until_fails('${pendingPostqueue}') - - client.succeed("${test-mark-spam "normal2"} >&2") - server.wait_until_succeeds("journalctl -u dovecot2 | grep -i learn-spam.sh >&2") - server.fail("journalctl -u dovecot2 | grep -i learn-spam.sh | grep -i error >&2") - - client.succeed("${test-mark-ham "normal2"} >&2") - server.wait_until_succeeds("journalctl -u dovecot2 | grep -i learn-ham.sh >&2") - server.fail("journalctl -u dovecot2 | grep -i learn-ham.sh | grep -i error >&2") - - with subtest("dkim signing"): - client.succeed("${sendMail "normal2" "" accounts."normal".address '' - Subject: Testmail - - Hello User1, - this is also a testmail. - ''}") - server.wait_until_fails('${pendingPostqueue}') - client.execute("${cleanupMail}") - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("${recvMail "normal"} >&2") - - client.succeed("cat ~/mail/* >&2") - # make sure the mail has all configured dkim signatures - client.succeed("grep ${(builtins.elemAt cfg.dkimSettings."example.com" 0).selector} ~/mail/*") - client.succeed("grep ${(builtins.elemAt cfg.dkimSettings."example.com" 1).selector} ~/mail/*") - ''; - }