about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/mail/mailman.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/mail/mailman.nix')
-rw-r--r--nixpkgs/nixos/modules/services/mail/mailman.nix249
1 files changed, 208 insertions, 41 deletions
diff --git a/nixpkgs/nixos/modules/services/mail/mailman.nix b/nixpkgs/nixos/modules/services/mail/mailman.nix
index 901f29631936..6a17c6f1a78a 100644
--- a/nixpkgs/nixos/modules/services/mail/mailman.nix
+++ b/nixpkgs/nixos/modules/services/mail/mailman.nix
@@ -6,6 +6,11 @@ let
 
   cfg = config.services.mailman;
 
+  inherit (pkgs.mailmanPackages.buildEnvs { withHyperkitty = cfg.hyperkitty.enable; withLDAP = cfg.ldap.enable; })
+    mailmanEnv webEnv;
+
+  withPostgresql = config.services.postgresql.enable;
+
   # This deliberately doesn't use recursiveUpdate so users can
   # override the defaults.
   webSettings = {
@@ -39,7 +44,8 @@ let
     transport_file_type: hash
   '';
 
-  mailmanCfg = (lib.generators.toINI {} cfg.settings) + cfg.extraConfig;
+  mailmanCfg = pkgs.writeText "mailman.cfg"
+    ((lib.generators.toINI {} cfg.settings) + cfg.extraConfig);
 
   mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
     [general]
@@ -67,6 +73,9 @@ in {
       stored in the world-readable Nix store.  To continue using
       Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
     '')
+    (mkRemovedOptionModule [ "services" "mailman" "package" ] ''
+      Didn't have an effect for several years.
+    '')
   ];
 
   options = {
@@ -76,22 +85,122 @@ in {
       enable = mkOption {
         type = types.bool;
         default = false;
-        description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
+        description = lib.mdDoc "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
       };
 
-      package = mkOption {
-        type = types.package;
-        default = pkgs.mailman;
-        defaultText = literalExpression "pkgs.mailman";
-        example = literalExpression "pkgs.mailman.override { archivers = []; }";
-        description = "Mailman package to use";
+      ldap = {
+        enable = mkEnableOption "LDAP auth";
+        serverUri = mkOption {
+          type = types.str;
+          example = "ldaps://ldap.host";
+          description = lib.mdDoc ''
+            LDAP host to connect against.
+          '';
+        };
+        bindDn = mkOption {
+          type = types.str;
+          example = "cn=root,dc=nixos,dc=org";
+          description = lib.mdDoc ''
+            Service account to bind against.
+          '';
+        };
+        bindPasswordFile = mkOption {
+          type = types.str;
+          example = "/run/secrets/ldap-bind";
+          description = lib.mdDoc ''
+            Path to the file containing the bind password of the servie account
+            defined by [](#opt-services.mailman.ldap.bindDn).
+          '';
+        };
+        superUserGroup = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          example = "cn=admin,ou=groups,dc=nixos,dc=org";
+          description = lib.mdDoc ''
+            Group where a user must be a member of to gain superuser rights.
+          '';
+        };
+        userSearch = {
+          query = mkOption {
+            type = types.str;
+            example = "(&(objectClass=inetOrgPerson)(|(uid=%(user)s)(mail=%(user)s)))";
+            description = lib.mdDoc ''
+              Query to find a user in the LDAP database.
+            '';
+          };
+          ou = mkOption {
+            type = types.str;
+            example = "ou=users,dc=nixos,dc=org";
+            description = lib.mdDoc ''
+              Organizational unit to look up a user.
+            '';
+          };
+        };
+        groupSearch = {
+          type = mkOption {
+            type = types.enum [
+              "posixGroup" "groupOfNames" "memberDNGroup" "nestedMemberDNGroup" "nestedGroupOfNames"
+              "groupOfUniqueNames" "nestedGroupOfUniqueNames" "activeDirectoryGroup" "nestedActiveDirectoryGroup"
+              "organizationalRoleGroup" "nestedOrganizationalRoleGroup"
+            ];
+            default = "posixGroup";
+            apply = v: "${toUpper (substring 0 1 v)}${substring 1 (stringLength v) v}Type";
+            description = lib.mdDoc ''
+              Type of group to perform a group search against.
+            '';
+          };
+          query = mkOption {
+            type = types.str;
+            example = "(objectClass=groupOfNames)";
+            description = lib.mdDoc ''
+              Query to find a group associated to a user in the LDAP database.
+            '';
+          };
+          ou = mkOption {
+            type = types.str;
+            example = "ou=groups,dc=nixos,dc=org";
+            description = lib.mdDoc ''
+              Organizational unit to look up a group.
+            '';
+          };
+        };
+        attrMap = {
+          username = mkOption {
+            default = "uid";
+            type = types.str;
+            description = lib.mdDoc ''
+              LDAP-attribute that corresponds to the `username`-attribute in mailman.
+            '';
+          };
+          firstName = mkOption {
+            default = "givenName";
+            type = types.str;
+            description = lib.mdDoc ''
+              LDAP-attribute that corresponds to the `firstName`-attribute in mailman.
+            '';
+          };
+          lastName = mkOption {
+            default = "sn";
+            type = types.str;
+            description = lib.mdDoc ''
+              LDAP-attribute that corresponds to the `lastName`-attribute in mailman.
+            '';
+          };
+          email = mkOption {
+            default = "mail";
+            type = types.str;
+            description = lib.mdDoc ''
+              LDAP-attribute that corresponds to the `email`-attribute in mailman.
+            '';
+          };
+        };
       };
 
       enablePostfix = mkOption {
         type = types.bool;
         default = true;
         example = false;
-        description = ''
+        description = lib.mdDoc ''
           Enable Postfix integration. Requires an active Postfix installation.
 
           If you want to use another MTA, set this option to false and configure
@@ -104,7 +213,7 @@ in {
       siteOwner = mkOption {
         type = types.str;
         example = "postmaster@example.org";
-        description = ''
+        description = lib.mdDoc ''
           Certain messages that must be delivered to a human, but which can't
           be delivered to a list owner (e.g. a bounce from a list owner), will
           be sent to this address. It should point to a human.
@@ -114,7 +223,7 @@ in {
       webHosts = mkOption {
         type = types.listOf types.str;
         default = [];
-        description = ''
+        description = lib.mdDoc ''
           The list of hostnames and/or IP addresses from which the Mailman Web
           UI will accept requests. By default, "localhost" and "127.0.0.1" are
           enabled. All additional names under which your web server accepts
@@ -126,7 +235,7 @@ in {
       webUser = mkOption {
         type = types.str;
         default = "mailman-web";
-        description = ''
+        description = lib.mdDoc ''
           User to run mailman-web as
         '';
       };
@@ -134,17 +243,25 @@ in {
       webSettings = mkOption {
         type = types.attrs;
         default = {};
-        description = ''
+        description = lib.mdDoc ''
           Overrides for the default mailman-web Django settings.
         '';
       };
 
+      restApiPassFile = mkOption {
+        default = null;
+        type = types.nullOr types.str;
+        description = lib.mdDoc ''
+          Path to the file containing the value for `MAILMAN_REST_API_PASS`.
+        '';
+      };
+
       serve = {
         enable = mkEnableOption "Automatic nginx and uwsgi setup for mailman-web";
       };
 
       settings = mkOption {
-        description = "Settings for mailman.cfg";
+        description = lib.mdDoc "Settings for mailman.cfg";
         type = types.attrsOf (types.attrsOf types.str);
         default = {};
       };
@@ -155,7 +272,7 @@ in {
         baseUrl = mkOption {
           type = types.str;
           default = "http://localhost:18507/archives/";
-          description = ''
+          description = lib.mdDoc ''
             Where can Mailman connect to Hyperkitty's internal API, preferably on
             localhost?
           '';
@@ -180,16 +297,13 @@ in {
       mailman.layout = "fhs";
 
       "paths.fhs" = {
-        # This has to use the unwrapped version because Mailman tries
-        # to load it into the Python interpreter.
-        bin_dir = "${cfg.package.unwrapped}/bin";
+        bin_dir = "${pkgs.mailmanPackages.mailman}/bin";
         var_dir = "/var/lib/mailman";
         queue_dir = "$var_dir/queue";
         template_dir = "$var_dir/templates";
         log_dir = "/var/log/mailman";
         lock_dir = "$var_dir/lock";
         etc_dir = "/etc";
-        ext_dir = "$etc_dir/mailman.d";
         pid_file = "/run/mailman/master.pid";
       };
 
@@ -222,7 +336,14 @@ in {
               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
             '';
           };
-    in (lib.optionals cfg.enablePostfix [
+    in [
+      { assertion = cfg.webHosts != [];
+        message = ''
+          services.mailman.serve.enable requires there to be at least one entry
+          in services.mailman.webHosts.
+        '';
+      }
+    ] ++ (lib.optionals cfg.enablePostfix [
       { assertion = postfix.enable;
         message = ''
           Mailman's default NixOS configuration requires Postfix to be enabled.
@@ -251,8 +372,6 @@ in {
     };
     users.groups.mailman = {};
 
-    environment.etc."mailman3/mailman.cfg".text = mailmanCfg;
-
     environment.etc."mailman3/settings.py".text = ''
       import os
 
@@ -270,20 +389,52 @@ in {
 
       with open('/var/lib/mailman-web/settings_local.json') as f:
           globals().update(json.load(f))
+
+      ${optionalString (cfg.restApiPassFile != null) ''
+        with open('${cfg.restApiPassFile}') as f:
+            MAILMAN_REST_API_PASS = f.read().rstrip('\n')
+      ''}
+
+      ${optionalString (cfg.ldap.enable) ''
+        import ldap
+        from django_auth_ldap.config import LDAPSearch, ${cfg.ldap.groupSearch.type}
+        AUTH_LDAP_SERVER_URI = "${cfg.ldap.serverUri}"
+        AUTH_LDAP_BIND_DN = "${cfg.ldap.bindDn}"
+        with open("${cfg.ldap.bindPasswordFile}") as f:
+            AUTH_LDAP_BIND_PASSWORD = f.read().rstrip('\n')
+        AUTH_LDAP_USER_SEARCH = LDAPSearch("${cfg.ldap.userSearch.ou}",
+            ldap.SCOPE_SUBTREE, "${cfg.ldap.userSearch.query}")
+        AUTH_LDAP_GROUP_TYPE = ${cfg.ldap.groupSearch.type}()
+        AUTH_LDAP_GROUP_SEARCH = LDAPSearch("${cfg.ldap.groupSearch.ou}",
+            ldap.SCOPE_SUBTREE, "${cfg.ldap.groupSearch.query}")
+        AUTH_LDAP_USER_ATTR_MAP = {
+          ${concatStrings (flip mapAttrsToList cfg.ldap.attrMap (key: value: ''
+            "${key}": "${value}",
+          ''))}
+        }
+        ${optionalString (cfg.ldap.superUserGroup != null) ''
+          AUTH_LDAP_USER_FLAGS_BY_GROUP = {
+            "is_superuser": "${cfg.ldap.superUserGroup}"
+          }
+        ''}
+        AUTHENTICATION_BACKENDS = (
+            "django_auth_ldap.backend.LDAPBackend",
+            "django.contrib.auth.backends.ModelBackend"
+        )
+      ''}
     '';
 
-    services.nginx = mkIf cfg.serve.enable {
+    services.nginx = mkIf (cfg.serve.enable && cfg.webHosts != []) {
       enable = mkDefault true;
-      virtualHosts."${lib.head cfg.webHosts}" = {
-        serverAliases = cfg.webHosts;
+      virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
         locations = {
           "/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
           "/static/".alias = webSettings.STATIC_ROOT + "/";
         };
-      };
+      });
     };
 
-    environment.systemPackages = [ cfg.package ] ++ (with pkgs; [ mailman-web ]);
+    environment.systemPackages = [ pkgs.mailmanPackages.mailman ] ++ (with pkgs; [ mailman-web ]);
 
     services.postfix = lib.mkIf cfg.enablePostfix {
       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
@@ -300,12 +451,16 @@ in {
     systemd.services = {
       mailman = {
         description = "GNU Mailman Master Process";
-        after = [ "network.target" ];
-        restartTriggers = [ config.environment.etc."mailman3/mailman.cfg".source ];
+        before = lib.optional cfg.enablePostfix "postfix.service";
+        after = [ "network.target" ]
+          ++ lib.optional cfg.enablePostfix "postfix-setup.service"
+          ++ lib.optional withPostgresql "postgresql.service";
+        restartTriggers = [ mailmanCfg ];
+        requires = optional withPostgresql "postgresql.service";
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
-          ExecStart = "${cfg.package}/bin/mailman start";
-          ExecStop = "${cfg.package}/bin/mailman stop";
+          ExecStart = "${mailmanEnv}/bin/mailman start";
+          ExecStop = "${mailmanEnv}/bin/mailman stop";
           User = "mailman";
           Group = "mailman";
           Type = "forking";
@@ -320,8 +475,18 @@ in {
         before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
         requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
         path = with pkgs; [ jq ];
+        after = optional withPostgresql "postgresql.service";
+        requires = optional withPostgresql "postgresql.service";
         serviceConfig.Type = "oneshot";
         script = ''
+          install -m0750 -o mailman -g mailman ${mailmanCfg} /etc/mailman.cfg
+          ${optionalString (cfg.restApiPassFile != null) ''
+            ${pkgs.replace-secret}/bin/replace-secret \
+              '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \
+              ${cfg.restApiPassFile} \
+              /etc/mailman.cfg
+          ''}
+
           mailmanDir=/var/lib/mailman
           mailmanWebDir=/var/lib/mailman-web
 
@@ -368,9 +533,9 @@ in {
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         script = ''
           [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
-          ${pkgs.mailman-web}/bin/mailman-web migrate
-          ${pkgs.mailman-web}/bin/mailman-web collectstatic
-          ${pkgs.mailman-web}/bin/mailman-web compress
+          ${webEnv}/bin/mailman-web migrate
+          ${webEnv}/bin/mailman-web collectstatic
+          ${webEnv}/bin/mailman-web compress
         '';
         serviceConfig = {
           User = cfg.webUser;
@@ -387,14 +552,16 @@ in {
         uwsgiConfig.uwsgi = {
           type = "normal";
           plugins = ["python3"];
-          home = pkgs.mailman-web;
+          home = webEnv;
           module = "mailman_web.wsgi";
           http = "127.0.0.1:18507";
         };
         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
       in {
         wantedBy = ["multi-user.target"];
-        requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"];
+        after = optional withPostgresql "postgresql.service";
+        requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]
+          ++ optional withPostgresql "postgresql.service";
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         serviceConfig = {
           # Since the mailman-web settings.py obstinately creates a logs
@@ -410,9 +577,9 @@ in {
       mailman-daily = {
         description = "Trigger daily Mailman events";
         startAt = "daily";
-        restartTriggers = [ config.environment.etc."mailman3/mailman.cfg".source ];
+        restartTriggers = [ mailmanCfg ];
         serviceConfig = {
-          ExecStart = "${cfg.package}/bin/mailman digests --send";
+          ExecStart = "${mailmanEnv}/bin/mailman digests --send";
           User = "mailman";
           Group = "mailman";
         };
@@ -424,7 +591,7 @@ in {
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         wantedBy = [ "mailman.service" "multi-user.target" ];
         serviceConfig = {
-          ExecStart = "${pkgs.mailman-web}/bin/mailman-web qcluster";
+          ExecStart = "${webEnv}/bin/mailman-web qcluster";
           User = cfg.webUser;
           Group = "mailman";
           WorkingDirectory = "/var/lib/mailman-web";
@@ -443,7 +610,7 @@ in {
         inherit startAt;
         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
         serviceConfig = {
-          ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs ${name}";
+          ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}";
           User = cfg.webUser;
           Group = "mailman";
           WorkingDirectory = "/var/lib/mailman-web";
@@ -452,7 +619,7 @@ in {
   };
 
   meta = {
-    maintainers = with lib.maintainers; [ lheckemann qyliss ];
+    maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ];
     doc = ./mailman.xml;
   };