]> git.immae.eu Git - perso/Immae/Config/Nix.git/blobdiff - modules/private/mail/relay.nix
Add backup MX
[perso/Immae/Config/Nix.git] / modules / private / mail / relay.nix
diff --git a/modules/private/mail/relay.nix b/modules/private/mail/relay.nix
new file mode 100644 (file)
index 0000000..9111350
--- /dev/null
@@ -0,0 +1,233 @@
+{ lib, pkgs, config, nodes, name, ... }:
+{
+  config = lib.mkIf config.myServices.mailBackup.enable {
+    security.acme.certs."mail" = config.myServices.certificates.certConfig // {
+      postRun = ''
+        systemctl restart postfix.service
+        '';
+      domain = config.hostEnv.fqdn;
+      extraDomains = let
+        zonesWithMx = builtins.filter (zone:
+          lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0
+        ) config.myEnv.dns.masterZones;
+        mxs = map (zone: "${config.myEnv.servers."${name}".mx.subdomain}.${zone.name}") zonesWithMx;
+      in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs);
+    };
+    secrets.keys = [
+      {
+        dest = "postfix/mysql_alias_maps";
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = ${config.myEnv.mail.postfix.mysql.remoteHost}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+          query = SELECT DISTINCT 1
+            FROM forwardings_merge
+            WHERE
+              ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
+              AND active = 1
+              AND '%s' NOT IN
+              (
+                SELECT source
+                FROM forwardings_blacklisted
+                WHERE source = '%s'
+              ) UNION
+              SELECT 'devnull@immae.eu'
+              FROM forwardings_blacklisted
+              WHERE source = '%s'
+          '';
+      }
+      {
+        dest = "postfix/mysql_mailbox_maps";
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          # We need to specify that option to trigger ssl connection
+          tls_ciphers = TLSv1.2
+          user = ${config.myEnv.mail.postfix.mysql.user}
+          password = ${config.myEnv.mail.postfix.mysql.password}
+          hosts = ${config.myEnv.mail.postfix.mysql.remoteHost}
+          dbname = ${config.myEnv.mail.postfix.mysql.database}
+          query = SELECT DISTINCT 1
+            FROM mailboxes
+            WHERE active = 1
+            AND (
+              (domain = '%d' AND user = '%u' AND regex = 0)
+              OR (
+                regex = 1
+                AND '%d' REGEXP CONCAT('^',domain,'$')
+                AND '%u' REGEXP CONCAT('^',user,'$')
+              )
+            )
+            LIMIT 1
+        '';
+      }
+      {
+        dest = "postfix/ldap_ejabberd_users_immae_fr";
+        user = config.services.postfix.user;
+        group = config.services.postfix.group;
+        permissions = "0440";
+        text = ''
+          server_host = ldaps://${config.myEnv.jabber.ldap.host}:636
+          search_base = ${config.myEnv.jabber.ldap.base}
+          query_filter = ${config.myEnv.jabber.postfix_user_filter}
+          domain = immae.fr
+          bind_dn = ${config.myEnv.jabber.ldap.dn}
+          bind_pw = ${config.myEnv.jabber.ldap.password}
+          result_attribute = immaeXmppUid
+          result_format = ejabberd@localhost
+          version = 3
+          '';
+      }
+    ];
+
+    networking.firewall.allowedTCPPorts = [ 25 ];
+
+    nixpkgs.overlays = [ (self: super: {
+      postfix = super.postfix.override { withMySQL = true; };
+    }) ];
+    users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
+    services.filesWatcher.postfix = {
+      restart = true;
+      paths = [
+        config.secrets.fullPaths."postfix/mysql_alias_maps"
+        config.secrets.fullPaths."postfix/mysql_mailbox_maps"
+        config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"
+      ];
+    };
+    services.postfix = {
+      mapFiles = let
+        recipient_maps = let
+          name = n: i: "relay_${n}_${toString i}";
+          pair = n: i: m: lib.attrsets.nameValuePair (name n i) (
+            if m.type == "hash"
+            then pkgs.writeText (name n i) m.content
+            else null
+          );
+          pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
+        in lib.attrsets.filterAttrs (k: v: v != null) (
+          lib.attrsets.listToAttrs (lib.flatten (
+            lib.attrsets.mapAttrsToList pairs config.myEnv.mail.postfix.backup_domains
+          ))
+        );
+        relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) (
+          lib.attrsets.mapAttrs' (n: v:
+            lib.attrsets.nameValuePair "recipient_access_${n}" (
+              if lib.attrsets.hasAttr "relay_restrictions" v
+              then pkgs.writeText "recipient_access_${n}" v.relay_restrictions
+              else null
+            )
+          ) config.myEnv.mail.postfix.backup_domains
+        );
+        virtual_map = {
+          virtual = pkgs.writeText "postfix-virtual" (
+            builtins.concatStringsSep "\n" (
+              lib.attrsets.mapAttrsToList (
+                n: v: lib.optionalString v.external ''
+                  script_${n}@mail.immae.eu 1
+                ''
+              ) config.myEnv.mail.scripts
+            )
+          );
+        };
+        sasl_access = {
+          host_dummy_mailboxes = pkgs.writeText "host-virtual-mailbox"
+            (builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: "${n}@immae.eu  1") nodes));
+        };
+      in
+        recipient_maps // relay_restrictions // virtual_map // sasl_access;
+      config = {
+        ### postfix module overrides
+        readme_directory = "${pkgs.postfix}/share/postfix/doc";
+        smtp_tls_CAfile = lib.mkForce "";
+        smtp_tls_cert_file = lib.mkForce "";
+        smtp_tls_key_file = lib.mkForce "";
+
+        message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
+        mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts
+        alias_database = "\$alias_maps";
+
+        ### Relay domains
+        relay_domains = let
+          backups = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) config.myEnv.mail.postfix.backup_domains);
+          virtual_domains = config.myEnv.mail.postfix.additional_mailbox_domains
+            ++ lib.remove "localhost.immae.eu" (lib.remove null (lib.flatten (map
+                (zone: map
+                  (e: if e.receive
+                  then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}"
+                  else null
+                  )
+                  (zone.withEmail or [])
+                )
+                config.myEnv.dns.masterZones
+              )));
+        in
+          backups ++ virtual_domains;
+        relay_recipient_maps = let
+          backup_recipients = lib.flatten (lib.attrsets.mapAttrsToList (n: v:
+            lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps
+          ) config.myEnv.mail.postfix.backup_domains);
+          virtual_alias_maps = [
+            "hash:/etc/postfix/virtual"
+            "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"
+            "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}"
+          ];
+          virtual_mailbox_maps = [
+            "hash:/etc/postfix/host_dummy_mailboxes"
+            "mysql:${config.secrets.fullPaths."postfix/mysql_mailbox_maps"}"
+          ];
+        in
+          backup_recipients ++ virtual_alias_maps ++ virtual_mailbox_maps;
+        smtpd_relay_restrictions = [
+          "defer_unauth_destination"
+        ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v:
+          if lib.attrsets.hasAttr "relay_restrictions" v
+          then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ]
+          else []
+        ) config.myEnv.mail.postfix.backup_domains);
+
+        ### Additional smtpd configuration
+        smtpd_tls_received_header = "yes";
+        smtpd_tls_loglevel = "1";
+
+        ### Email sending configuration
+        smtp_tls_security_level = "may";
+        smtp_tls_loglevel = "1";
+
+        ### Force ip bind for smtp
+        smtp_bind_address  = config.myEnv.servers."${name}".ips.main.ip4;
+        smtp_bind_address6 = builtins.head config.myEnv.servers."${name}".ips.main.ip6;
+
+        smtpd_milters = [
+          "unix:${config.myServices.mail.milters.sockets.opendkim}"
+          "unix:${config.myServices.mail.milters.sockets.openarc}"
+          "unix:${config.myServices.mail.milters.sockets.opendmarc}"
+        ];
+      };
+      enable = true;
+      enableSmtp = true;
+      enableSubmission = false;
+      # FIXME: Mail adressed to localhost.immae.eu will still have mx-1 as
+      # prioritized MX, which provokes "mail for localhost.immae.eu loops
+      # back to myself" errors. This transport entry forces to push
+      # e-mails to its right destination.
+      transport = ''
+        localhost.immae.eu   smtp:[immae.eu]:25
+        '';
+      destination = ["localhost"];
+      # This needs to reverse DNS
+      hostname = config.hostEnv.fqdn;
+      setSendmail = false;
+      sslCert = "/var/lib/acme/mail/fullchain.pem";
+      sslKey = "/var/lib/acme/mail/key.pem";
+      recipientDelimiter = "+";
+    };
+  };
+}
+