diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/mail/mailman.nix')
-rw-r--r-- | nixpkgs/nixos/modules/services/mail/mailman.nix | 249 |
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; }; |