about summary refs log tree commit diff
path: root/nixos/modules/services/mail/mailman.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/mail/mailman.nix')
-rw-r--r--nixos/modules/services/mail/mailman.nix285
1 files changed, 191 insertions, 94 deletions
diff --git a/nixos/modules/services/mail/mailman.nix b/nixos/modules/services/mail/mailman.nix
index e917209f3d1f..f5e78b182933 100644
--- a/nixos/modules/services/mail/mailman.nix
+++ b/nixos/modules/services/mail/mailman.nix
@@ -6,37 +6,18 @@ let
 
   cfg = config.services.mailman;
 
-  mailmanPyEnv = pkgs.python3.withPackages (ps: with ps; [mailman mailman-hyperkitty]);
-
-  mailmanExe = with pkgs; stdenv.mkDerivation {
-    name = "mailman-" + python3Packages.mailman.version;
-    buildInputs = [makeWrapper];
-    unpackPhase = ":";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${mailmanPyEnv}/bin/mailman $out/bin/mailman \
-        --set MAILMAN_CONFIG_FILE /etc/mailman.cfg
-   '';
-  };
-
-  mailmanWeb = pkgs.python3Packages.mailman-web.override {
-    serverEMail = cfg.siteOwner;
-    archiverKey = cfg.hyperkittyApiKey;
-    allowedHosts = cfg.webHosts;
-  };
-
-  mailmanWebPyEnv = pkgs.python3.withPackages (x: with x; [mailman-web]);
-
-  mailmanWebExe = with pkgs; stdenv.mkDerivation {
-    inherit (mailmanWeb) name;
-    buildInputs = [makeWrapper];
-    unpackPhase = ":";
-    installPhase = ''
-      mkdir -p $out/bin
-      makeWrapper ${mailmanWebPyEnv}/bin/django-admin $out/bin/mailman-web \
-        --set DJANGO_SETTINGS_MODULE settings
-    '';
-  };
+  # This deliberately doesn't use recursiveUpdate so users can
+  # override the defaults.
+  settings = {
+    DEFAULT_FROM_EMAIL = cfg.siteOwner;
+    SERVER_EMAIL = cfg.siteOwner;
+    ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
+    COMPRESS_OFFLINE = true;
+    STATIC_ROOT = "/var/lib/mailman-web/static";
+    MEDIA_ROOT = "/var/lib/mailman-web/media";
+  } // cfg.webSettings;
+
+  settingsJSON = pkgs.writeText "settings.json" (builtins.toJSON settings);
 
   mailmanCfg = ''
     [mailman]
@@ -53,30 +34,42 @@ let
     etc_dir: /etc
     ext_dir: $etc_dir/mailman.d
     pid_file: /run/mailman/master.pid
-  '' + optionalString (cfg.hyperkittyApiKey != null) ''
+  '' + optionalString cfg.hyperkitty.enable ''
+
     [archiver.hyperkitty]
     class: mailman_hyperkitty.Archiver
     enable: yes
-    configuration: ${pkgs.writeText "mailman-hyperkitty.cfg" mailmanHyperkittyCfg}
+    configuration: /var/lib/mailman/mailman-hyperkitty.cfg
   '';
 
-  mailmanHyperkittyCfg = ''
+  mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
     [general]
     # This is your HyperKitty installation, preferably on the localhost. This
     # address will be used by Mailman to forward incoming emails to HyperKitty
     # for archiving. It does not need to be publicly available, in fact it's
     # better if it is not.
-    base_url: ${cfg.hyperkittyBaseUrl}
+    base_url: ${cfg.hyperkitty.baseUrl}
 
     # Shared API key, must be the identical to the value in HyperKitty's
     # settings.
-    api_key: ${cfg.hyperkittyApiKey}
+    api_key: @API_KEY@
   '';
 
 in {
 
   ###### interface
 
+  imports = [
+    (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
+      [ "services" "mailman" "hyperkitty" "baseUrl" ])
+
+    (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
+      The Hyperkitty API key is now generated on first run, and not
+      stored in the world-readable Nix store.  To continue using
+      Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
+    '')
+  ];
+
   options = {
 
     services.mailman = {
@@ -87,9 +80,17 @@ in {
         description = "Enable Mailman on this host. Requires an active Postfix installation.";
       };
 
+      package = mkOption {
+        type = types.package;
+        default = pkgs.mailman;
+        defaultText = "pkgs.mailman";
+        example = "pkgs.mailman.override { archivers = []; }";
+        description = "Mailman package to use";
+      };
+
       siteOwner = mkOption {
         type = types.str;
-        default = "postmaster@example.org";
+        example = "postmaster@example.org";
         description = ''
           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
@@ -99,12 +100,13 @@ in {
 
       webRoot = mkOption {
         type = types.path;
-        default = "${mailmanWeb}/${pkgs.python3.sitePackages}";
-        defaultText = "pkgs.python3Packages.mailman-web";
+        default = "${pkgs.mailman-web}/${pkgs.python3.sitePackages}";
+        defaultText = "\${pkgs.mailman-web}/\${pkgs.python3.sitePackages}";
         description = ''
           The web root for the Hyperkity + Postorius apps provided by Mailman.
           This variable can be set, of course, but it mainly exists so that site
-          admins can refer to it in their own hand-written httpd configuration files.
+          admins can refer to it in their own hand-written web server
+          configuration files.
         '';
       };
 
@@ -120,26 +122,35 @@ in {
         '';
       };
 
-      hyperkittyBaseUrl = mkOption {
+      webUser = mkOption {
         type = types.str;
-        default = "http://localhost/hyperkitty/";
+        default = config.services.httpd.user;
         description = ''
-          Where can Mailman connect to Hyperkitty's internal API, preferably on
-          localhost?
+          User to run mailman-web as
         '';
       };
 
-      hyperkittyApiKey = mkOption {
-        type = types.nullOr types.str;
-        default = null;
+      webSettings = mkOption {
+        type = types.attrs;
+        default = {};
         description = ''
-          The shared secret used to authenticate Mailman's internal
-          communication with Hyperkitty. Must be set to enable support for the
-          Hyperkitty archiver. Note that this secret is going to be visible to
-          all local users in the Nix store.
+          Overrides for the default mailman-web Django settings.
         '';
       };
 
+      hyperkitty = {
+        enable = mkEnableOption "the Hyperkitty archiver for Mailman";
+
+        baseUrl = mkOption {
+          type = types.str;
+          default = "http://localhost/hyperkitty/";
+          description = ''
+            Where can Mailman connect to Hyperkitty's internal API, preferably on
+            localhost?
+          '';
+        };
+      };
+
     };
   };
 
@@ -147,25 +158,58 @@ in {
 
   config = mkIf cfg.enable {
 
-    assertions = [
-      { assertion = cfg.enable -> config.services.postfix.enable;
+    assertions = let
+      inherit (config.services) postfix;
+
+      requirePostfixHash = optionPath: dataFile:
+        with lib;
+        let
+          expected = "hash:/var/lib/mailman/data/${dataFile}";
+          value = attrByPath optionPath [] postfix;
+        in
+          { assertion = postfix.enable -> isList value && elem expected value;
+            message = ''
+              services.postfix.${concatStringsSep "." optionPath} must contain
+              "${expected}".
+              See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
+            '';
+          };
+    in [
+      { assertion = postfix.enable;
         message = "Mailman requires Postfix";
       }
+      (requirePostfixHash [ "relayDomains" ] "postfix_domains")
+      (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
+      (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
     ];
 
     users.users.mailman = { description = "GNU Mailman"; isSystemUser = true; };
 
-    environment = {
-      systemPackages = [ mailmanExe mailmanWebExe pkgs.sassc ];
-      etc."mailman.cfg".text = mailmanCfg;
-    };
+    environment.etc."mailman.cfg".text = mailmanCfg;
+
+    environment.etc."mailman3/settings.py".text = ''
+      import os
+
+      # Required by mailman_web.settings, but will be overridden when
+      # settings_local.json is loaded.
+      os.environ["SECRET_KEY"] = ""
+
+      from mailman_web.settings import *
+
+      import json
+
+      with open('${settingsJSON}') as f:
+          globals().update(json.load(f))
+
+      with open('/var/lib/mailman-web/settings_local.json') as f:
+          globals().update(json.load(f))
+    '';
+
+    environment.systemPackages = [ cfg.package ] ++ (with pkgs; [ mailman-web ]);
 
     services.postfix = {
-      relayDomains = [ "hash:/var/lib/mailman/data/postfix_domains" ];
       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
       config = {
-        transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
-        local_recipient_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
         owner_request_special = "no";   # Mailman handles -owner addresses on its own
       };
     };
@@ -173,34 +217,79 @@ in {
     systemd.services.mailman = {
       description = "GNU Mailman Master Process";
       after = [ "network.target" ];
+      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${mailmanExe}/bin/mailman start";
-        ExecStop = "${mailmanExe}/bin/mailman stop";
+        ExecStart = "${cfg.package}/bin/mailman start";
+        ExecStop = "${cfg.package}/bin/mailman stop";
         User = "mailman";
         Type = "forking";
-        StateDirectory = "mailman";
-        StateDirectoryMode = "0700";
         RuntimeDirectory = "mailman";
         PIDFile = "/run/mailman/master.pid";
       };
     };
 
+    systemd.services.mailman-settings = {
+      description = "Generate settings files (including secrets) for Mailman";
+      before = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
+      requiredBy = [ "mailman.service" "mailman-web.service" "hyperkitty.service" "httpd.service" "uwsgi.service" ];
+      path = with pkgs; [ jq ];
+      script = ''
+        mailmanDir=/var/lib/mailman
+        mailmanWebDir=/var/lib/mailman-web
+
+        mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
+        mailmanWebCfg=$mailmanWebDir/settings_local.json
+
+        install -m 0700 -o mailman -g nogroup -d $mailmanDir
+        install -m 0700 -o ${cfg.webUser} -g nogroup -d $mailmanWebDir
+
+        if [ ! -e $mailmanWebCfg ]; then
+            hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+            secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
+
+            mailmanWebCfgTmp=$(mktemp)
+            jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
+                --arg archiver_key "$hyperkittyApiKey" \
+                --arg secret_key "$secretKey" \
+                >"$mailmanWebCfgTmp"
+            chown ${cfg.webUser} "$mailmanWebCfgTmp"
+            mv -n "$mailmanWebCfgTmp" $mailmanWebCfg
+        fi
+
+        hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY $mailmanWebCfg)"
+        mailmanCfgTmp=$(mktemp)
+        sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
+        chown mailman "$mailmanCfgTmp"
+        mv "$mailmanCfgTmp" $mailmanCfg
+      '';
+      serviceConfig = {
+        Type = "oneshot";
+        # RemainAfterExit makes restartIfChanged work for this service, so
+        # downstream services will get updated automatically when things like
+        # services.mailman.hyperkitty.baseUrl change.  Otherwise users have to
+        # restart things manually, which is confusing.
+        RemainAfterExit = "yes";
+      };
+    };
+
     systemd.services.mailman-web = {
       description = "Init Postorius DB";
-      before = [ "httpd.service" ];
-      requiredBy = [ "httpd.service" ];
+      before = [ "httpd.service" "uwsgi.service" ];
+      requiredBy = [ "httpd.service" "uwsgi.service" ];
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       script = ''
-        ${mailmanWebExe}/bin/mailman-web migrate
+        ${pkgs.mailman-web}/bin/mailman-web migrate
         rm -rf static
-        ${mailmanWebExe}/bin/mailman-web collectstatic
-        ${mailmanWebExe}/bin/mailman-web compress
+        ${pkgs.mailman-web}/bin/mailman-web collectstatic
+        ${pkgs.mailman-web}/bin/mailman-web compress
       '';
       serviceConfig = {
-        User = config.services.httpd.user;
+        User = cfg.webUser;
         Type = "oneshot";
-        StateDirectory = "mailman-web";
-        StateDirectoryMode = "0700";
+        # Similar to mailman-settings.service, this makes restartTriggers work
+        # properly for this service.
+        RemainAfterExit = "yes";
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
@@ -208,86 +297,94 @@ in {
     systemd.services.mailman-daily = {
       description = "Trigger daily Mailman events";
       startAt = "daily";
+      restartTriggers = [ config.environment.etc."mailman.cfg".source ];
       serviceConfig = {
-        ExecStart = "${mailmanExe}/bin/mailman digests --send";
+        ExecStart = "${cfg.package}/bin/mailman digests --send";
         User = "mailman";
       };
     };
 
     systemd.services.hyperkitty = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "GNU Hyperkitty QCluster Process";
       after = [ "network.target" ];
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       wantedBy = [ "mailman.service" "multi-user.target" ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web qcluster";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web qcluster";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-minutely = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger minutely Hyperkitty events";
       startAt = "minutely";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs minutely";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs minutely";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-quarter-hourly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger quarter-hourly Hyperkitty events";
       startAt = "*:00/15";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs quarter_hourly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs quarter_hourly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-hourly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger hourly Hyperkitty events";
       startAt = "hourly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs hourly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs hourly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-daily = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger daily Hyperkitty events";
       startAt = "daily";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs daily";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs daily";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-weekly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger weekly Hyperkitty events";
       startAt = "weekly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs weekly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs weekly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };
 
     systemd.services.hyperkitty-yearly = {
-      enable = cfg.hyperkittyApiKey != null;
+      inherit (cfg.hyperkitty) enable;
       description = "Trigger yearly Hyperkitty events";
       startAt = "yearly";
+      restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
       serviceConfig = {
-        ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs yearly";
-        User = config.services.httpd.user;
+        ExecStart = "${pkgs.mailman-web}/bin/mailman-web runjobs yearly";
+        User = cfg.webUser;
         WorkingDirectory = "/var/lib/mailman-web";
       };
     };