at 21.11-pre 16 kB view raw
1{ config, pkgs, lib, ... }: # mailman.nix 2 3with lib; 4 5let 6 7 cfg = config.services.mailman; 8 9 pythonEnv = pkgs.python3.withPackages (ps: 10 [ps.mailman ps.mailman-web] 11 ++ lib.optional cfg.hyperkitty.enable ps.mailman-hyperkitty 12 ++ cfg.extraPythonPackages); 13 14 # This deliberately doesn't use recursiveUpdate so users can 15 # override the defaults. 16 webSettings = { 17 DEFAULT_FROM_EMAIL = cfg.siteOwner; 18 SERVER_EMAIL = cfg.siteOwner; 19 ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts; 20 COMPRESS_OFFLINE = true; 21 STATIC_ROOT = "/var/lib/mailman-web-static"; 22 MEDIA_ROOT = "/var/lib/mailman-web/media"; 23 LOGGING = { 24 version = 1; 25 disable_existing_loggers = true; 26 handlers.console.class = "logging.StreamHandler"; 27 loggers.django = { 28 handlers = [ "console" ]; 29 level = "INFO"; 30 }; 31 }; 32 HAYSTACK_CONNECTIONS.default = { 33 ENGINE = "haystack.backends.whoosh_backend.WhooshEngine"; 34 PATH = "/var/lib/mailman-web/fulltext-index"; 35 }; 36 } // cfg.webSettings; 37 38 webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings); 39 40 # TODO: Should this be RFC42-ised so that users can set additional options without modifying the module? 41 postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" '' 42 [postfix] 43 postmap_command: ${pkgs.postfix}/bin/postmap 44 transport_file_type: hash 45 ''; 46 47 mailmanCfg = lib.generators.toINI {} cfg.settings; 48 49 mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" '' 50 [general] 51 # This is your HyperKitty installation, preferably on the localhost. This 52 # address will be used by Mailman to forward incoming emails to HyperKitty 53 # for archiving. It does not need to be publicly available, in fact it's 54 # better if it is not. 55 base_url: ${cfg.hyperkitty.baseUrl} 56 57 # Shared API key, must be the identical to the value in HyperKitty's 58 # settings. 59 api_key: @API_KEY@ 60 ''; 61 62in { 63 64 ###### interface 65 66 imports = [ 67 (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ] 68 [ "services" "mailman" "hyperkitty" "baseUrl" ]) 69 70 (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] '' 71 The Hyperkitty API key is now generated on first run, and not 72 stored in the world-readable Nix store. To continue using 73 Hyperkitty, you must set services.mailman.hyperkitty.enable = true. 74 '') 75 ]; 76 77 options = { 78 79 services.mailman = { 80 81 enable = mkOption { 82 type = types.bool; 83 default = false; 84 description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix)."; 85 }; 86 87 package = mkOption { 88 type = types.package; 89 default = pkgs.mailman; 90 defaultText = "pkgs.mailman"; 91 example = literalExample "pkgs.mailman.override { archivers = []; }"; 92 description = "Mailman package to use"; 93 }; 94 95 enablePostfix = mkOption { 96 type = types.bool; 97 default = true; 98 example = false; 99 description = '' 100 Enable Postfix integration. Requires an active Postfix installation. 101 102 If you want to use another MTA, set this option to false and configure 103 settings in services.mailman.settings.mta. 104 105 Refer to the Mailman manual for more info. 106 ''; 107 }; 108 109 siteOwner = mkOption { 110 type = types.str; 111 example = "postmaster@example.org"; 112 description = '' 113 Certain messages that must be delivered to a human, but which can't 114 be delivered to a list owner (e.g. a bounce from a list owner), will 115 be sent to this address. It should point to a human. 116 ''; 117 }; 118 119 webHosts = mkOption { 120 type = types.listOf types.str; 121 default = []; 122 description = '' 123 The list of hostnames and/or IP addresses from which the Mailman Web 124 UI will accept requests. By default, "localhost" and "127.0.0.1" are 125 enabled. All additional names under which your web server accepts 126 requests for the UI must be listed here or incoming requests will be 127 rejected. 128 ''; 129 }; 130 131 webUser = mkOption { 132 type = types.str; 133 default = "mailman-web"; 134 description = '' 135 User to run mailman-web as 136 ''; 137 }; 138 139 webSettings = mkOption { 140 type = types.attrs; 141 default = {}; 142 description = '' 143 Overrides for the default mailman-web Django settings. 144 ''; 145 }; 146 147 serve = { 148 enable = mkEnableOption "Automatic nginx and uwsgi setup for mailman-web"; 149 }; 150 151 extraPythonPackages = mkOption { 152 description = "Packages to add to the python environment used by mailman and mailman-web"; 153 type = types.listOf types.package; 154 default = []; 155 }; 156 157 settings = mkOption { 158 description = "Settings for mailman.cfg"; 159 type = types.attrsOf (types.attrsOf types.str); 160 default = {}; 161 }; 162 163 hyperkitty = { 164 enable = mkEnableOption "the Hyperkitty archiver for Mailman"; 165 166 baseUrl = mkOption { 167 type = types.str; 168 default = "http://localhost:18507/archives/"; 169 description = '' 170 Where can Mailman connect to Hyperkitty's internal API, preferably on 171 localhost? 172 ''; 173 }; 174 }; 175 176 }; 177 }; 178 179 ###### implementation 180 181 config = mkIf cfg.enable { 182 183 services.mailman.settings = { 184 mailman.site_owner = lib.mkDefault cfg.siteOwner; 185 mailman.layout = "fhs"; 186 187 "paths.fhs" = { 188 bin_dir = "${pkgs.python3Packages.mailman}/bin"; 189 var_dir = "/var/lib/mailman"; 190 queue_dir = "$var_dir/queue"; 191 template_dir = "$var_dir/templates"; 192 log_dir = "/var/log/mailman"; 193 lock_dir = "$var_dir/lock"; 194 etc_dir = "/etc"; 195 ext_dir = "$etc_dir/mailman.d"; 196 pid_file = "/run/mailman/master.pid"; 197 }; 198 199 mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA."); 200 201 "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable { 202 class = "mailman_hyperkitty.Archiver"; 203 enable = "yes"; 204 configuration = "/var/lib/mailman/mailman-hyperkitty.cfg"; 205 }; 206 } // (let 207 loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"]; 208 loggerSectionNames = map (n: "logging.${n}") loggerNames; 209 in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; }) 210 ); 211 212 assertions = let 213 inherit (config.services) postfix; 214 215 requirePostfixHash = optionPath: dataFile: 216 with lib; 217 let 218 expected = "hash:/var/lib/mailman/data/${dataFile}"; 219 value = attrByPath optionPath [] postfix; 220 in 221 { assertion = postfix.enable -> isList value && elem expected value; 222 message = '' 223 services.postfix.${concatStringsSep "." optionPath} must contain 224 "${expected}". 225 See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>. 226 ''; 227 }; 228 in (lib.optionals cfg.enablePostfix [ 229 { assertion = postfix.enable; 230 message = '' 231 Mailman's default NixOS configuration requires Postfix to be enabled. 232 233 If you want to use another MTA, set services.mailman.enablePostfix 234 to false and configure settings in services.mailman.settings.mta. 235 236 Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html> 237 for more info. 238 ''; 239 } 240 (requirePostfixHash [ "relayDomains" ] "postfix_domains") 241 (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp") 242 (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp") 243 ]); 244 245 users.users.mailman = { 246 description = "GNU Mailman"; 247 isSystemUser = true; 248 group = "mailman"; 249 }; 250 users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") { 251 description = "GNU Mailman web interface"; 252 isSystemUser = true; 253 group = "mailman"; 254 }; 255 users.groups.mailman = {}; 256 257 environment.etc."mailman.cfg".text = mailmanCfg; 258 259 environment.etc."mailman3/settings.py".text = '' 260 import os 261 262 # Required by mailman_web.settings, but will be overridden when 263 # settings_local.json is loaded. 264 os.environ["SECRET_KEY"] = "" 265 266 from mailman_web.settings.base import * 267 from mailman_web.settings.mailman import * 268 269 import json 270 271 with open('${webSettingsJSON}') as f: 272 globals().update(json.load(f)) 273 274 with open('/var/lib/mailman-web/settings_local.json') as f: 275 globals().update(json.load(f)) 276 ''; 277 278 services.nginx = mkIf cfg.serve.enable { 279 enable = mkDefault true; 280 virtualHosts."${lib.head cfg.webHosts}" = { 281 serverAliases = cfg.webHosts; 282 locations = { 283 "/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;"; 284 "/static/".alias = webSettings.STATIC_ROOT + "/"; 285 }; 286 }; 287 }; 288 289 environment.systemPackages = [ (pkgs.buildEnv { 290 name = "mailman-tools"; 291 # We don't want to pollute the system PATH with a python 292 # interpreter etc. so let's pick only the stuff we actually 293 # want from pythonEnv 294 pathsToLink = ["/bin"]; 295 paths = [pythonEnv]; 296 postBuild = '' 297 find $out/bin/ -mindepth 1 -not -name "mailman*" -delete 298 ''; 299 }) ]; 300 301 services.postfix = lib.mkIf cfg.enablePostfix { 302 recipientDelimiter = "+"; # bake recipient addresses in mail envelopes via VERP 303 config = { 304 owner_request_special = "no"; # Mailman handles -owner addresses on its own 305 }; 306 }; 307 308 systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable { 309 wantedBy = ["sockets.target"]; 310 before = ["nginx.service"]; 311 socketConfig.ListenStream = "/run/mailman-web.socket"; 312 }; 313 systemd.services = { 314 mailman = { 315 description = "GNU Mailman Master Process"; 316 after = [ "network.target" ]; 317 restartTriggers = [ config.environment.etc."mailman.cfg".source ]; 318 wantedBy = [ "multi-user.target" ]; 319 serviceConfig = { 320 ExecStart = "${pythonEnv}/bin/mailman start"; 321 ExecStop = "${pythonEnv}/bin/mailman stop"; 322 User = "mailman"; 323 Group = "mailman"; 324 Type = "forking"; 325 RuntimeDirectory = "mailman"; 326 LogsDirectory = "mailman"; 327 PIDFile = "/run/mailman/master.pid"; 328 }; 329 }; 330 331 mailman-settings = { 332 description = "Generate settings files (including secrets) for Mailman"; 333 before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ]; 334 requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ]; 335 path = with pkgs; [ jq ]; 336 script = '' 337 mailmanDir=/var/lib/mailman 338 mailmanWebDir=/var/lib/mailman-web 339 340 mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg 341 mailmanWebCfg=$mailmanWebDir/settings_local.json 342 343 install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static 344 install -m 0770 -o mailman -g mailman -d $mailmanDir 345 install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir 346 347 if [ ! -e $mailmanWebCfg ]; then 348 hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64) 349 secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64) 350 351 mailmanWebCfgTmp=$(mktemp) 352 jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \ 353 --arg archiver_key "$hyperkittyApiKey" \ 354 --arg secret_key "$secretKey" \ 355 >"$mailmanWebCfgTmp" 356 chown root:mailman "$mailmanWebCfgTmp" 357 chmod 440 "$mailmanWebCfgTmp" 358 mv -n "$mailmanWebCfgTmp" "$mailmanWebCfg" 359 fi 360 361 hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")" 362 mailmanCfgTmp=$(mktemp) 363 sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp" 364 chown mailman:mailman "$mailmanCfgTmp" 365 mv "$mailmanCfgTmp" "$mailmanCfg" 366 ''; 367 }; 368 369 mailman-web-setup = { 370 description = "Prepare mailman-web files and database"; 371 before = [ "mailman-uwsgi.service" ]; 372 requiredBy = [ "mailman-uwsgi.service" ]; 373 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 374 script = '' 375 [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete 376 ${pythonEnv}/bin/mailman-web migrate 377 ${pythonEnv}/bin/mailman-web collectstatic 378 ${pythonEnv}/bin/mailman-web compress 379 ''; 380 serviceConfig = { 381 User = cfg.webUser; 382 Group = "mailman"; 383 Type = "oneshot"; 384 WorkingDirectory = "/var/lib/mailman-web"; 385 }; 386 }; 387 388 mailman-uwsgi = mkIf cfg.serve.enable (let 389 uwsgiConfig.uwsgi = { 390 type = "normal"; 391 plugins = ["python3"]; 392 home = pythonEnv; 393 module = "mailman_web.wsgi"; 394 http = "127.0.0.1:18507"; 395 }; 396 uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig); 397 in { 398 wantedBy = ["multi-user.target"]; 399 requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]; 400 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 401 serviceConfig = { 402 # Since the mailman-web settings.py obstinately creates a logs 403 # dir in the cwd, change to the (writable) runtime directory before 404 # starting uwsgi. 405 ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; }}/bin/uwsgi --json ${uwsgiConfigFile}"; 406 User = cfg.webUser; 407 Group = "mailman"; 408 RuntimeDirectory = "mailman-uwsgi"; 409 }; 410 }); 411 412 mailman-daily = { 413 description = "Trigger daily Mailman events"; 414 startAt = "daily"; 415 restartTriggers = [ config.environment.etc."mailman.cfg".source ]; 416 serviceConfig = { 417 ExecStart = "${pythonEnv}/bin/mailman digests --send"; 418 User = "mailman"; 419 Group = "mailman"; 420 }; 421 }; 422 423 hyperkitty = lib.mkIf cfg.hyperkitty.enable { 424 description = "GNU Hyperkitty QCluster Process"; 425 after = [ "network.target" ]; 426 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 427 wantedBy = [ "mailman.service" "multi-user.target" ]; 428 serviceConfig = { 429 ExecStart = "${pythonEnv}/bin/mailman-web qcluster"; 430 User = cfg.webUser; 431 Group = "mailman"; 432 WorkingDirectory = "/var/lib/mailman-web"; 433 }; 434 }; 435 } // flip lib.mapAttrs' { 436 "minutely" = "minutely"; 437 "quarter_hourly" = "*:00/15"; 438 "hourly" = "hourly"; 439 "daily" = "daily"; 440 "weekly" = "weekly"; 441 "yearly" = "yearly"; 442 } (name: startAt: 443 lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable { 444 description = "Trigger ${name} Hyperkitty events"; 445 inherit startAt; 446 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 447 serviceConfig = { 448 ExecStart = "${pythonEnv}/bin/mailman-web runjobs ${name}"; 449 User = cfg.webUser; 450 Group = "mailman"; 451 WorkingDirectory = "/var/lib/mailman-web"; 452 }; 453 })); 454 }; 455 456 meta = { 457 maintainers = with lib.maintainers; [ lheckemann qyliss ]; 458 doc = ./mailman.xml; 459 }; 460 461}