about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--nixos/doc/manual/release-notes/rl-2305.section.md4
-rw-r--r--nixos/modules/services/mail/maddy.nix83
-rw-r--r--nixos/tests/all-tests.nix2
-rw-r--r--nixos/tests/maddy/default.nix6
-rw-r--r--nixos/tests/maddy/tls.nix94
-rw-r--r--nixos/tests/maddy/unencrypted.nix (renamed from nixos/tests/maddy.nix)4
6 files changed, 186 insertions, 7 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md
index 601109ccee5e..75915870ca77 100644
--- a/nixos/doc/manual/release-notes/rl-2305.section.md
+++ b/nixos/doc/manual/release-notes/rl-2305.section.md
@@ -292,7 +292,9 @@ In addition to numerous new and upgraded packages, this release has the followin
   replacement. It stores backups as volume dump files and thus better integrates
   into contemporary backup solutions.
 
-- `services.maddy` now allows to configure users and their credentials using `services.maddy.ensureCredentials`.
+- `services.maddy` got several updates:
+  - Configuration of users and their credentials using `services.maddy.ensureCredentials`.
+  - Configuration of TLS key and certificate files using `services.maddy.tls`.
 
 - The `dnsmasq` service now takes configuration via the
   `services.dnsmasq.settings` attribute set. The option
diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix
index d0b525bcb002..e11a18cc1428 100644
--- a/nixos/modules/services/mail/maddy.nix
+++ b/nixos/modules/services/mail/maddy.nix
@@ -13,8 +13,6 @@ let
     # configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
     # Do not use this in production!
 
-    tls off
-
     auth.pass_table local_authdb {
       table sql_table {
         driver sqlite3
@@ -35,6 +33,7 @@ let
       }
       optional_step file /etc/maddy/aliases
     }
+
     msgpipeline local_routing {
       destination postmaster $(local_domains) {
         modify {
@@ -215,6 +214,63 @@ in {
         '';
       };
 
+      tls = {
+        loader = mkOption {
+          type = with types; nullOr (enum [ "file" "off" ]);
+          default = "off";
+          description = lib.mdDoc ''
+            TLS certificates are obtained by modules called "certificate
+            loaders". Currently only the file loader is supported which reads
+            certificates from files specifying the options `keyPaths` and
+            `certPaths`.
+          '';
+        };
+
+        certificates = mkOption {
+          type = with types; listOf (submodule {
+            options = {
+              keyPath = mkOption {
+                type = types.path;
+                example = "/etc/ssl/mx1.example.org.key";
+                description = lib.mdDoc ''
+                  Path to the private key used for TLS.
+                '';
+              };
+              certPath = mkOption {
+                type = types.path;
+                example = "/etc/ssl/mx1.example.org.crt";
+                description = lib.mdDoc ''
+                  Path to the certificate used for TLS.
+                '';
+              };
+            };
+          });
+          default = [];
+          example = lib.literalExpression ''
+            [{
+              keyPath = "/etc/ssl/mx1.example.org.key";
+              certPath = "/etc/ssl/mx1.example.org.crt";
+            }]
+          '';
+          description = lib.mdDoc ''
+            A list of attribute sets containing paths to TLS certificates and
+            keys. Maddy will use SNI if multiple pairs are selected.
+          '';
+        };
+
+        extraConfig = mkOption {
+          type = with types; nullOr lines;
+          description = lib.mdDoc ''
+            Arguments for the specific certificate loader. Note that Maddy uses
+            secure defaults for the TLS configuration so there is no need to
+            change anything in most cases.
+            See [upstream manual](https://maddy.email/reference/tls/) for
+            available options.
+          '';
+          default = "";
+        };
+      };
+
       openFirewall = mkOption {
         type = types.bool;
         default = false;
@@ -224,7 +280,7 @@ in {
       };
 
       ensureAccounts = mkOption {
-        type = types.listOf types.str;
+        type = with types; listOf str;
         default = [];
         description = lib.mdDoc ''
           List of IMAP accounts which get automatically created. Note that for
@@ -270,6 +326,16 @@ in {
 
   config = mkIf cfg.enable {
 
+    assertions = [{
+      assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != [];
+      message = ''
+        If maddy is configured to use TLS, tls.certificates with attribute sets
+        of certPath and keyPath must be provided.
+        Read more about obtaining TLS certificates here:
+        https://maddy.email/tutorials/setting-up/#tls-certificates
+      '';
+    }];
+
     systemd = {
 
       packages = [ pkgs.maddy ];
@@ -318,6 +384,17 @@ in {
         $(primary_domain) = ${cfg.primaryDomain}
         $(local_domains) = ${toString cfg.localDomains}
         hostname ${cfg.hostname}
+
+        ${if (cfg.tls.loader == "file") then ''
+          tls file ${concatStringsSep " " (
+            map (x: x.certPath + " " + x.keyPath
+          ) cfg.tls.certificates)} ${optionalString (cfg.tls.extraConfig != "") ''
+            { ${cfg.tls.extraConfig} }
+          ''}
+        '' else if (cfg.tls.loader == "off") then ''
+          tls off
+        '' else ""}
+
         ${cfg.config}
       '';
     };
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 5b802fb26304..6e45dcc9e503 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -390,7 +390,7 @@ in {
   lxd-image-server = handleTest ./lxd-image-server.nix {};
   #logstash = handleTest ./logstash.nix {};
   lorri = handleTest ./lorri/default.nix {};
-  maddy = handleTest ./maddy.nix {};
+  maddy = discoverTests (import ./maddy { inherit handleTest; });
   maestral = handleTest ./maestral.nix {};
   magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {};
   magnetico = handleTest ./magnetico.nix {};
diff --git a/nixos/tests/maddy/default.nix b/nixos/tests/maddy/default.nix
new file mode 100644
index 000000000000..043906863e64
--- /dev/null
+++ b/nixos/tests/maddy/default.nix
@@ -0,0 +1,6 @@
+{ handleTest }:
+
+{
+  unencrypted = handleTest ./unencrypted.nix { };
+  tls = handleTest ./tls.nix { };
+}
diff --git a/nixos/tests/maddy/tls.nix b/nixos/tests/maddy/tls.nix
new file mode 100644
index 000000000000..44da4cf2a3cf
--- /dev/null
+++ b/nixos/tests/maddy/tls.nix
@@ -0,0 +1,94 @@
+import ../make-test-python.nix ({ pkgs, ... }:
+let
+  certs = import ../common/acme/server/snakeoil-certs.nix;
+  domain = certs.domain;
+in {
+  name = "maddy-tls";
+  meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
+
+  nodes = {
+    server = { options, ... }: {
+      services.maddy = {
+        enable = true;
+        hostname = domain;
+        primaryDomain = domain;
+        openFirewall = true;
+        ensureAccounts = [ "postmaster@${domain}" ];
+        ensureCredentials = {
+          # Do not use this in production. This will make passwords world-readable
+          # in the Nix store
+          "postmaster@${domain}".passwordFile = "${pkgs.writeText "postmaster" "test"}";
+        };
+        tls = {
+          loader = "file";
+          certificates = [{
+            certPath = "${certs.${domain}.cert}";
+            keyPath = "${certs.${domain}.key}";
+          }];
+        };
+        # Enable TLS listeners. Configuring this via the module is not yet
+        # implemented.
+        config = builtins.replaceStrings [
+          "imap tcp://0.0.0.0:143"
+          "submission tcp://0.0.0.0:587"
+        ] [
+          "imap tls://0.0.0.0:993 tcp://0.0.0.0:143"
+          "submission tls://0.0.0.0:465 tcp://0.0.0.0:587"
+        ] options.services.maddy.config.default;
+      };
+      # Not covered by openFirewall yet
+      networking.firewall.allowedTCPPorts = [ 993 465 ];
+    };
+
+    client = { nodes, ... }: {
+      security.pki.certificateFiles = [
+        certs.ca.cert
+      ];
+      networking.extraHosts = ''
+        ${nodes.server.networking.primaryIPAddress} ${domain}
+     '';
+      environment.systemPackages = [
+        (pkgs.writers.writePython3Bin "send-testmail" { } ''
+          import smtplib
+          import ssl
+          from email.mime.text import MIMEText
+
+          context = ssl.create_default_context()
+          msg = MIMEText("Hello World")
+          msg['Subject'] = 'Test'
+          msg['From'] = "postmaster@${domain}"
+          msg['To'] = "postmaster@${domain}"
+          with smtplib.SMTP_SSL(host='${domain}', port=465, context=context) as smtp:
+              smtp.login('postmaster@${domain}', 'test')
+              smtp.sendmail(
+                'postmaster@${domain}', 'postmaster@${domain}', msg.as_string()
+              )
+        '')
+        (pkgs.writers.writePython3Bin "test-imap" { } ''
+          import imaplib
+
+          with imaplib.IMAP4_SSL('${domain}') as imap:
+              imap.login('postmaster@${domain}', 'test')
+              imap.select()
+              status, refs = imap.search(None, 'ALL')
+              assert status == 'OK'
+              assert len(refs) == 1
+              status, msg = imap.fetch(refs[0], 'BODY[TEXT]')
+              assert status == 'OK'
+              assert msg[0][1].strip() == b"Hello World"
+        '')
+      ];
+    };
+  };
+
+  testScript = ''
+    start_all()
+    server.wait_for_unit("maddy.service")
+    server.wait_for_open_port(143)
+    server.wait_for_open_port(993)
+    server.wait_for_open_port(587)
+    server.wait_for_open_port(465)
+    client.succeed("send-testmail")
+    client.succeed("test-imap")
+  '';
+})
diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy/unencrypted.nix
index 742043033337..2420d461e4e7 100644
--- a/nixos/tests/maddy.nix
+++ b/nixos/tests/maddy/unencrypted.nix
@@ -1,5 +1,5 @@
-import ./make-test-python.nix ({ pkgs, ... }: {
-  name = "maddy";
+import ../make-test-python.nix ({ pkgs, ... }: {
+  name = "maddy-unencrypted";
   meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; };
 
   nodes = {