{ lib, pkgs, config, ... }: with lib; let cfg = config.services.public-inbox; stateDir = "/var/lib/public-inbox"; inboxes = mapAttrs (name: inbox: recursiveUpdate { inherit (inbox) address url newsgroup watch; mainrepo = "${stateDir}/inboxes/${name}"; watchheader = inbox.watchHeader; } inbox.config) cfg.inboxes; configToList = attrs: concatLists (mapAttrsToList (name': value': if isAttrs value' then map ({ name, value }: nameValuePair "${name'}.${name}" value) (configToList value') else if isList value' then map (nameValuePair name') value' else if value' == null then [] else [ (nameValuePair name' value') ]) attrs); configFull = recursiveUpdate { publicinbox = inboxes // { nntpserver = cfg.nntpServer; wwwlisting = cfg.wwwListing; }; publicinboxmda.spamcheck = if (cfg.mda.spamCheck == null) then "none" else cfg.mda.spamCheck; publicinboxwatch.spamcheck = if (cfg.watch.spamCheck == null) then "none" else cfg.watch.spamCheck; publicinboxwatch.watchspam = cfg.watch.watchSpam; } cfg.config; configList = configToList configFull; gitConfig = key: val: '' ${pkgs.git}/bin/git config --add --file $out ${escapeShellArgs [ key val ]} ''; configFile = pkgs.runCommand "public-inbox-config" {} (concatStrings (map ({ name, value }: gitConfig name value) configList)); environment = { PI_EMERGENCY = "${stateDir}/emergency"; PI_CONFIG = configFile; }; envList = mapAttrsToList (n: v: "${n}=${v}") environment; # Can't use pkgs.linkFarm, # because Postfix rejects .forward if it's a symlink. home = pkgs.runCommand "public-inbox-home" { forward = '' |"env ${concatStringsSep " " envList} PATH=\"${makeBinPath cfg.path}:$PATH\" ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args} ''; passAsFile = [ "forward" ]; } '' mkdir $out ln -s ${stateDir}/spamassassin $out/.spamassassin cp $forwardPath $out/.forward install -D -p ${configFile} $out/.public-inbox/config ''; psgi = pkgs.writeText "public-inbox.psgi" '' #!${cfg.package.fullperl} -w # Copyright (C) 2014-2019 all contributors # License: GPL-3.0+ use strict; use PublicInbox::WWW; use Plack::Builder; my $www = PublicInbox::WWW->new; $www->preload; builder { enable 'Head'; enable 'ReverseProxy'; ${concatMapStrings (path: '' mount q(${path}) => sub { $www->call(@_); }; '') cfg.http.mounts} } ''; descriptionFile = { description, ... }: pkgs.writeText "description" description; enableWatch = (any (i: i.watch != []) (attrValues cfg.inboxes)) || (cfg.watch.watchSpam != null); useSpamAssassin = cfg.mda.spamCheck == "spamc" || cfg.watch.spamCheck == "spamc"; in { options.services.public-inbox = { enable = mkEnableOption "the public-inbox mail archiver"; package = mkOption { type = types.package; default = pkgs.public-inbox; description = '' public-inbox package to use with the public-inbox module ''; }; path = mkOption { type = with types; listOf package; default = []; example = literalExample "with pkgs; [ spamassassin ]"; description = '' Additional packages to place in the path of public-inbox-mda, public-inbox-watch, etc. ''; }; inboxes = mkOption { description = '' Inboxes to configure, where attribute names are inbox names ''; type = types.attrsOf (types.submodule { options = { address = mkOption { type = with types; listOf str; example = "example-discuss@example.org"; }; url = mkOption { type = with types; nullOr str; default = null; example = "https://example.org/lists/example-discuss"; description = '' URL where this inbox can be accessed over HTTP ''; }; description = mkOption { type = types.str; example = "user/dev discussion of public-inbox itself"; description = '' User-visible description for the repository ''; }; config = mkOption { type = types.attrs; default = {}; description = '' Additional structured config for the inbox ''; }; newsgroup = mkOption { type = with types; nullOr str; default = null; description = '' NNTP group name for the inbox ''; }; watch = mkOption { type = with types; listOf str; default = []; description = '' Paths for public-inbox-watch(1) to monitor for new mail ''; example = [ "maildir:/path/to/test.example.com.git" ]; }; watchHeader = mkOption { type = with types; nullOr str; default = null; example = "List-Id:"; description = '' If specified, public-inbox-watch(1) will only process mail containing a matching header. ''; }; }; }); }; mda = { args = mkOption { type = with types; listOf str; default = []; description = '' Command-line arguments to pass to public-inbox-mda(1). ''; }; spamCheck = mkOption { type = with types; nullOr (enum [ "spamc" ]); default = "spamc"; description = '' If set to spamc, public-inbox-mda(1) will filter spam using SpamAssassin ''; }; }; watch = { spamCheck = mkOption { type = with types; nullOr (enum [ "spamc" ]); default = "spamc"; description = '' If set to spamc, public-inbox-watch(1) will filter spam using SpamAssassin ''; }; watchSpam = mkOption { type = with types; nullOr str; default = null; example = "maildir:/path/to/spam"; description = '' If set, mail in this maildir will be trained as spam and deleted from all watched inboxes ''; }; }; http = { mounts = mkOption { type = with types; listOf str; default = [ "/" ]; example = [ "/lists/archives" ]; description = '' Root paths or URLs that public-inbox will be served on. If domain parts are present, only requests to those domains will be accepted. ''; }; listenStreams = mkOption { type = with types; listOf str; default = [ "/run/public-inbox-httpd.sock" ]; description = '' systemd.socket(5) ListenStream values for the public-inbox-httpd service to listen on ''; }; }; imap = { listenStreams = mkOption { type = with types; listOf str; default = [ "0.0.0.0:993" ]; description = '' systemd.socket(5) ListenStream values for the public-inbox-imapd service to listen on ''; }; cert = mkOption { type = with types; nullOr str; default = null; example = "/path/to/fullchain.pem"; description = '' Path to TLS certificate to use for public-inbox IMAP connections ''; }; key = mkOption { type = with types; nullOr str; default = null; example = "/path/to/key.pem"; description = '' Path to TLS key to use for public-inbox IMAP connections ''; }; }; nntp = { listenStreams = mkOption { type = with types; listOf str; default = [ "0.0.0.0:119" "0.0.0.0:563" ]; description = '' systemd.socket(5) ListenStream values for the public-inbox-nntpd service to listen on ''; }; cert = mkOption { type = with types; nullOr str; default = null; example = "/path/to/fullchain.pem"; description = '' Path to TLS certificate to use for public-inbox NNTP connections ''; }; key = mkOption { type = with types; nullOr str; default = null; example = "/path/to/key.pem"; description = '' Path to TLS key to use for public-inbox NNTP connections ''; }; extraGroups = mkOption { type = with types; listOf str; default = []; example = [ "tls" ]; description = '' Secondary groups to assign to the systemd DynamicUser running public-inbox-nntpd, in addition to the public-inbox group. This is useful for giving public-inbox-nntpd access to a TLS certificate / key, for example. ''; }; }; nntpServer = mkOption { type = with types; listOf str; default = []; example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ]; description = '' NNTP URLs to this public-inbox instance ''; }; wwwListing = mkOption { type = with types; enum [ "all" "404" "match=domain" ]; default = "404"; description = '' Controls which lists (if any) are listed for when the root public-inbox URL is accessed over HTTP. ''; }; spamAssassinRules = mkOption { type = with types; nullOr path; default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs"; description = '' SpamAssassin configuration specific to public-inbox ''; }; config = mkOption { type = with types; attrsOf attrs; default = {}; description = '' Additional structured config for the public-inbox config file ''; }; }; config = mkIf cfg.enable { assertions = [ { assertion = config.services.spamassassin.enable || !useSpamAssassin; message = '' public-inbox is configured to use SpamAssassin, but services.spamassassin.enable is false. If you don't need spam checking, set services.public-inbox.mda.spamCheck and services.public-inbox.watch.spamCheck to null. ''; } { assertion = cfg.path != [] || !useSpamAssassin; message = '' public-inbox is configured to use SpamAssassin, but there is no spamc executable in services.public-inbox.path. If you don't need spam checking, set services.public-inbox.mda.spamCheck and services.public-inbox.watch.spamCheck to null. ''; } ]; users = { users.public-inbox = { inherit home; group = "public-inbox"; isSystemUser = true; }; groups.public-inbox = {}; }; systemd.sockets = { public-inbox-httpd = { inherit (cfg.http) listenStreams; wantedBy = [ "sockets.target" ]; }; public-inbox-imapd = { inherit (cfg.imap) listenStreams; wantedBy = [ "sockets.target" ]; }; public-inbox-nntpd = { inherit (cfg.nntp) listenStreams; wantedBy = [ "sockets.target" ]; }; }; systemd.services = { public-inbox-httpd = { inherit (environment); /* environment = environment // { PATH = mkForce (lib.makeBinPath [ pkgs.coreutils pkgs.findutils pkgs.gnugrep pkgs.gnused pkgs.systemd (pkgs.writeShellScriptBin "git" '' set -x ${pkgs.git}/bin/git "$@" '') ]); }; */ after = [ "public-inbox-watch.service" ]; serviceConfig = { ExecStart = "${cfg.package}/bin/public-inbox-httpd ${psgi}"; NonBlocking = true; DynamicUser = true; Group = "public-inbox"; }; }; public-inbox-imapd = { inherit environment; after = [ "public-inbox-watch.service" ]; #environment.PERL_INLINE_DIRECTORY = "/tmp/.pub-inline"; #environment.LimitNOFILE = 30000; serviceConfig = { ExecStart = escapeShellArgs ( [ "${cfg.package}/bin/public-inbox-imapd" "-W0" ] ++ optionals (cfg.imap.cert != null) [ "--cert" cfg.imap.cert ] ++ optionals (cfg.imap.key != null) [ "--key" cfg.imap.key ] ); # NonBlocking is REQUIRED to avoid a race condition # if running simultaneous services NonBlocking = true; DynamicUser = true; Group = "public-inbox"; }; }; public-inbox-nntpd = { inherit environment; after = [ "public-inbox-watch.service" ]; serviceConfig.ExecStart = escapeShellArgs ( [ "${cfg.package}/bin/public-inbox-nntpd" ] ++ optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ] ++ optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ] ); serviceConfig.NonBlocking = true; serviceConfig.DynamicUser = true; serviceConfig.Group = "public-inbox"; }; public-inbox-watch = { inherit environment; inherit (cfg) path; after = optional (cfg.watch.spamCheck == "spamc") "spamassassin.service"; wantedBy = optional enableWatch "multi-user.target"; serviceConfig = { ExecStart = "${cfg.package}/bin/public-inbox-watch"; ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; User = "public-inbox"; Group = "public-inbox"; StateDirectory = [ "public-inbox/inboxes" "public-inbox/emergency" ]; StateDirectoryMode = "0750"; PrivateTmp = true; }; preStart = '' ${optionalString useSpamAssassin '' install -m 0700 -o spamd -d ${stateDir}/spamassassin ${optionalString (cfg.spamAssassinRules != null) '' ln -sf ${cfg.spamAssassinRules} ${stateDir}/spamassassin/user_prefs ''} ''} ${concatStrings (mapAttrsToList (name: { address, url, ... } @ inbox: '' if [ ! -e ${stateDir}/inboxes/"${escapeShellArg name}" ]; then # public-inbox-init creates an inbox and adds it to a config file. # It tries to atomically write the config file by creating # another file in the same directory, and renaming it. # This has the sad consequence that we can't use # /dev/null, or it would try to create a file in /dev. conf_dir="$(mktemp -d)" env PI_CONFIG=$conf_dir/conf \ ${cfg.package}/bin/public-inbox-init -V2 \ ${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" url ] ++ address)} rm -rf $conf_dir fi ln -sf ${descriptionFile inbox} ${stateDir}/inboxes/"${escapeShellArg name}"/description git=${stateDir}/inboxes/"${escapeShellArg name}"/all.git if [ -d "$git" ]; then # Config is inherited by each epoch repository, # so just needs to be set for all.git. ${pkgs.git}/bin/git --git-dir "$git" \ config core.sharedRepository 0640 fi '') cfg.inboxes)} for inbox in ${stateDir}/inboxes/*/; do ls -1 "$inbox" | grep -q '^xap' && continue # This should be idempotent, but only do it for new # inboxes anyway because it's only needed once, and could # be slow for large pre-existing inboxes. env ${concatStringsSep " " envList} \ ${cfg.package}/bin/public-inbox-index "$inbox" done ''; }; }; environment.systemPackages = with pkgs; [ cfg.package ]; }; }