let
cfg = config.services.public-inbox;
-
- inboxesDir = "/var/lib/public-inbox/inboxes";
- inboxPath = name: "${inboxesDir}/${name}";
- gitPath = name: "${inboxPath name}/all.git";
-
+ stateDir = "/var/lib/public-inbox";
inboxes = mapAttrs (name: inbox:
- (recursiveUpdate {
+ recursiveUpdate {
inherit (inbox) address url newsgroup watch;
- mainrepo = inboxPath name;
+ mainrepo = "${stateDir}/inboxes/${name}";
watchheader = inbox.watchHeader;
- } inbox.config))
+ } inbox.config)
cfg.inboxes;
- concat = concatMap id;
-
configToList = attrs:
- concat (mapAttrsToList (name': value':
+ concatLists (mapAttrsToList (name': value':
if isAttrs value' then
map ({ name, value }: nameValuePair "${name'}.${name}" value)
(configToList value')
(concatStrings (map ({ name, value }: gitConfig name value) configList));
environment = {
- PI_EMERGENCY = "/var/lib/public-inbox/emergency";
+ PI_EMERGENCY = "${stateDir}/emergency";
PI_CONFIG = configFile;
};
passAsFile = [ "forward" ];
} ''
mkdir $out
- ln -s /var/lib/public-inbox/spamassassin $out/.spamassassin
+ ln -s ${stateDir}/spamassassin $out/.spamassassin
cp $forwardPath $out/.forward
install -D -p ${configFile} $out/.public-inbox/config
'';
in
{
- options = {
- services.public-inbox = {
- enable = mkEnableOption "the public-inbox mail archiver";
+ 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
+ '';
+ };
- package = mkOption {
- type = types.package;
- default = pkgs.public-inbox;
+ 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:<test@example.com>";
+ 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 = ''
- public-inbox package to use with the public-inbox module
+ Command-line arguments to pass to public-inbox-mda(1).
'';
};
- path = mkOption {
- type = with types; listOf package;
- default = [];
- example = literalExample "with pkgs; [ spamassassin ]";
+ spamCheck = mkOption {
+ type = with types; nullOr (enum [ "spamc" ]);
+ default = "spamc";
description = ''
- Additional packages to place in the path of public-inbox-mda,
- public-inbox-watch, etc.
+ If set to spamc, public-inbox-mda(1) will filter spam
+ using SpamAssassin
'';
};
+ };
- inboxes = mkOption {
+ watch = {
+ spamCheck = mkOption {
+ type = with types; nullOr (enum [ "spamc" ]);
+ default = "spamc";
description = ''
- Inboxes to configure, where attribute names are inbox names
+ If set to spamc, public-inbox-watch(1) will filter spam
+ using SpamAssassin
'';
- type = with types; loaOf (submodule {
- options = {
- address = mkOption {
- type = listOf str;
- example = "example-discuss@example.org";
- };
-
- url = mkOption {
- type = nullOr str;
- default = null;
- example = "https://example.org/lists/example-discuss";
- description = ''
- URL where this inbox can be accessed over HTTP
- '';
- };
-
- description = mkOption {
- type = str;
- example = "user/dev discussion of public-inbox itself";
- description = ''
- User-visible description for the repository
- '';
- };
-
- config = mkOption {
- type = attrs;
- default = {};
- description = ''
- Additional structured config for the inbox
- '';
- };
-
- newsgroup = mkOption {
- type = nullOr str;
- default = null;
- description = ''
- NNTP group name for the inbox
- '';
- };
-
- watch = mkOption {
- type = 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 = nullOr str;
- default = null;
- example = "List-Id:<test@example.com>";
- 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
- '';
- };
+ 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
+ '';
};
+ };
- 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.
+ '';
};
- 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
- '';
- };
+ 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
+ '';
};
+ };
- 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
- '';
- };
+ 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
+ '';
+ };
- 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
- '';
- };
+ 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
+ '';
+ };
- 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.
- '';
- };
+ 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
+ '';
};
+ };
- nntpServer = mkOption {
+ nntp = {
+ listenStreams = mkOption {
type = with types; listOf str;
- default = [];
- example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
+ default = [ "0.0.0.0:119" "0.0.0.0:563" ];
description = ''
- NNTP URLs to this public-inbox instance
+ systemd.socket(5) ListenStream values for the
+ public-inbox-nntpd service to listen on
'';
};
- wwwListing = mkOption {
- type = with types; enum [ "all" "404" "match=domain" ];
- default = "404";
+ cert = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "/path/to/fullchain.pem";
description = ''
- Controls which lists (if any) are listed for when the root
- public-inbox URL is accessed over HTTP.
+ Path to TLS certificate to use for public-inbox NNTP connections
'';
};
- spamAssassinRules = mkOption {
- type = with types; nullOr path;
- default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
+ key = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "/path/to/key.pem";
description = ''
- SpamAssassin configuration specific to public-inbox
+ Path to TLS key to use for public-inbox NNTP connections
'';
};
- config = mkOption {
- type = with types; attrsOf attrs;
- default = {};
+ extraGroups = mkOption {
+ type = with types; listOf str;
+ default = [];
+ example = [ "tls" ];
description = ''
- Additional structured config for the public-inbox config file
+ 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 {
}
];
- users.users.public-inbox = {
- inherit home;
- group = "public-inbox";
- isSystemUser = true;
- };
-
- users.groups.public-inbox = {};
-
- systemd.sockets.public-inbox-httpd = {
- inherit (cfg.http) listenStreams;
- wantedBy = [ "sockets.target" ];
- };
-
- systemd.sockets.public-inbox-nntpd = {
- inherit (cfg.nntp) listenStreams;
- wantedBy = [ "sockets.target" ];
- };
-
- systemd.services.public-inbox-httpd = {
- inherit environment;
- serviceConfig.ExecStart = "${cfg.package}/bin/public-inbox-httpd ${psgi}";
- serviceConfig.NonBlocking = true;
- serviceConfig.DynamicUser = true;
- serviceConfig.SupplementaryGroups = [ "public-inbox" ];
- };
-
- systemd.services.public-inbox-nntpd = {
- inherit environment;
- 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.SupplementaryGroups = [ "public-inbox" ] ++ cfg.nntp.extraGroups;
+ users = {
+ users.public-inbox = {
+ inherit home;
+ group = "public-inbox";
+ isSystemUser = true;
+ };
+ groups.public-inbox = {};
};
- systemd.services.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";
- serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
- serviceConfig.User = "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" ];
+ };
};
- system.activationScripts.public-inbox = stringAfter [ "users" ] ''
- install -m 0755 -o public-inbox -g public-inbox -d /var/lib/public-inbox
- install -m 0750 -o public-inbox -g public-inbox -d ${inboxesDir}
- install -m 0700 -o public-inbox -g public-inbox -d /var/lib/public-inbox/emergency
-
- ${optionalString useSpamAssassin ''
- install -m 0700 -o spamd -d /var/lib/public-inbox/spamassassin
- ${optionalString (cfg.spamAssassinRules != null) ''
- ln -sf ${cfg.spamAssassinRules} /var/lib/public-inbox/spamassassin/user_prefs
- ''}
- ''}
-
- ${concatStrings (mapAttrsToList (name: { address, url, ... } @ inbox: ''
- if [ ! -e ${escapeShellArg (inboxPath 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="$(${pkgs.sudo}/bin/sudo -u public-inbox mktemp -d)"
-
- ${pkgs.sudo}/bin/sudo -u public-inbox \
- env PI_CONFIG=$conf_dir/conf \
+ 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 (inboxPath name) url ] ++ address)}
+ ${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" url ] ++ address)}
- rm -rf $conf_dir
- fi
+ rm -rf $conf_dir
+ fi
- ln -sf ${descriptionFile inbox} ${inboxPath name}/description
+ ln -sf ${descriptionFile inbox} ${stateDir}/inboxes/"${escapeShellArg name}"/description
- if [ -d ${escapeShellArg (gitPath name)} ]; then
- # Config is inherited by each epoch repository,
- # so just needs to be set for all.git.
- ${pkgs.git}/bin/git --git-dir ${gitPath name} \
+ 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)}
+ fi
+ '') cfg.inboxes)}
- for inbox in /var/lib/public-inbox/inboxes/*/; do
- ls -1 "$inbox" | grep -q '^xap' && continue
+ 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.
- ${pkgs.sudo}/bin/sudo -u public-inbox \
- env ${concatStringsSep " " envList} \
+ # 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
-
- '';
+ done
+ '';
+ };
+ };
environment.systemPackages = with pkgs; [ cfg.package ];
-
};
}