{ lib, pkgs, config, ... }: with lib; let cfg = config.services.public-inbox; stateDir = "/var/lib/public-inbox"; singleIniAtom = with types; nullOr (oneOf [ bool int float str ]) // { description = "INI atom (null, bool, int, float or string)"; }; iniAtom = with types; coercedTo singleIniAtom singleton (listOf singleIniAtom) // { description = singleIniAtom.description + " or a list of them for duplicate keys"; }; iniAttrs = with types; attrsOf (either (attrsOf iniAtom) iniAtom); gitIni = { type = with types; attrsOf iniAttrs; generate = name: value: pkgs.writeText name (generators.toGitINI value); }; environment = { PI_EMERGENCY = "${stateDir}/emergency"; PI_CONFIG = gitIni.generate "public-inbox.ini" (filterAttrsRecursive (n: v: v != null) cfg.settings); }; 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 ${environment.PI_CONFIG} $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} } ''; enableWatch = any (i: i.watch != []) (attrValues cfg.inboxes) || cfg.settings.publicinboxwatch.watchspam != null; useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" || cfg.settings.publicinboxwatch.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. ''; default = {}; type = types.submodule { freeformType = types.attrsOf (types.submodule ({name, ...}: { freeformType = types.attrsOf iniAtom; options.mainrepo = mkOption { type = types.str; default = "${stateDir}/inboxes/${name}"; }; options.address = mkOption { type = with types; listOf str; example = "example-discuss@example.org"; }; options.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 ''; }; options.description = mkOption { type = types.str; example = "user/dev discussion of public-inbox itself"; description = '' User-visible description for the repository ''; }; options.newsgroup = mkOption { type = with types; nullOr str; default = null; description = '' NNTP group name for the inbox ''; }; options.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" ]; }; options.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. ''; }; options.coderepo = mkOption { type = (types.listOf (types.enum (attrNames cfg.settings.coderepo))) // { description = "list of coderepo names"; }; default = []; description = '' Nicknames of a "coderepo" section associated with the inbox. ''; }; })); }; }; mda = { args = mkOption { type = with types; listOf str; default = []; description = '' Command-line arguments to pass to public-inbox-mda(1). ''; }; }; 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 ''; }; args = mkOption { type = with types; listOf str; default = ["-W0"]; description = '' Command-line arguments to pass to public-inbox-httpd(1). ''; }; }; 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 ''; }; args = mkOption { type = with types; listOf str; default = ["-W0"]; description = '' Command-line arguments to pass to public-inbox-imapd(1). ''; }; 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 ''; }; args = mkOption { type = with types; listOf str; default = ["-W0"]; description = '' Command-line arguments to pass to public-inbox-nntpd(1). ''; }; 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 ''; }; }; spamAssassinRules = mkOption { type = with types; nullOr path; default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs"; description = '' SpamAssassin configuration specific to public-inbox ''; }; settings = mkOption { description = '' Settings for the public-inbox config file. ''; default = {}; type = types.submodule { freeformType = gitIni.type; options.publicinbox = mkOption { default = {}; description = '' public-inbox configuration. ''; type = types.submodule { freeformType = iniAttrs; options.css = mkOption { type = with types; listOf str; default = []; }; options.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 ''; }; options.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. ''; }; }; }; options.publicinboxmda = mkOption { default = {}; description = "mailbox delivery agent"; type = types.submodule { freeformType = iniAttrs; options.spamcheck = mkOption { type = with types; enum [ "spamc" "none" ]; default = "none"; description = '' If set to spamc, public-inbox-mda(1) will filter spam using SpamAssassin. ''; }; }; }; options.publicinboxwatch = mkOption { default = {}; description = "mailbox watcher"; type = types.submodule { freeformType = iniAttrs; options.spamcheck = mkOption { type = with types; enum [ "spamc" "none" ]; default = "none"; description = '' If set to spamc, public-inbox-watch(1) will filter spam using SpamAssassin. ''; }; options.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 ''; }; }; }; options.coderepo = mkOption { default = {}; description = "code repositories"; type = types.submodule { freeformType = types.attrsOf (types.submodule { freeformType = types.either (types.attrsOf iniAtom) iniAtom; options.cgitUrl = mkOption { type = types.str; description = "URL of a cgit instance"; }; options.dir = mkOption { type = types.str; description = "Path to a git repository"; }; }); }; }; }; }; }; 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.settings.publicinboxmda.spamcheck and services.public-inbox.settings.publicinboxwatch.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.settings.publicinboxmda.spamcheck and services.public-inbox.settings.publicinboxwatch.spamcheck to null. ''; } ]; services.public-inbox.settings = filterAttrsRecursive (n: v: v != null) { publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes; }; 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); after = [ "public-inbox-watch.service" ]; serviceConfig = { ExecStart = escapeShellArgs ( [ "${cfg.package}/bin/public-inbox-httpd" psgi ] ++ cfg.http.args ); 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" ] ++ cfg.imap.args ++ 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" ] ++ cfg.nntp.args ++ optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ] ++ optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ] ); NonBlocking = true; DynamicUser = true; Group = "public-inbox"; }; }; public-inbox-watch = { inherit environment; inherit (cfg) path; after = optional (cfg.settings.publicinboxwatch.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/emergency" "public-inbox/inboxes" ]; 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: 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}" inbox.url ] ++ inbox.address)} rm -rf $conf_dir fi ln -sf ${pkgs.writeText "description" inbox.description} ${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 ]; }; meta.maintainers = with lib.maintainers; [ julm ]; }