1 { lib, pkgs, config, ... }:
6 cfg = config.services.public-inbox;
7 stateDir = "/var/lib/public-inbox";
8 inboxes = mapAttrs (name: inbox:
10 inherit (inbox) address url newsgroup watch;
11 mainrepo = "${stateDir}/inboxes/${name}";
12 watchheader = inbox.watchHeader;
17 concatLists (mapAttrsToList (name': value':
18 if isAttrs value' then
19 map ({ name, value }: nameValuePair "${name'}.${name}" value)
21 else if isList value' then map (nameValuePair name') value'
22 else if value' == null then []
23 else [ (nameValuePair name' value') ]) attrs);
25 configFull = recursiveUpdate {
26 publicinbox = inboxes // {
27 nntpserver = cfg.nntpServer;
28 wwwlisting = cfg.wwwListing;
30 publicinboxmda.spamcheck =
31 if (cfg.mda.spamCheck == null) then "none" else cfg.mda.spamCheck;
32 publicinboxwatch.spamcheck =
33 if (cfg.watch.spamCheck == null) then "none" else cfg.watch.spamCheck;
34 publicinboxwatch.watchspam = cfg.watch.watchSpam;
37 configList = configToList configFull;
39 gitConfig = key: val: ''
40 ${pkgs.git}/bin/git config --add --file $out ${escapeShellArgs [ key val ]}
43 configFile = pkgs.runCommand "public-inbox-config" {}
44 (concatStrings (map ({ name, value }: gitConfig name value) configList));
47 PI_EMERGENCY = "${stateDir}/emergency";
48 PI_CONFIG = configFile;
51 envList = mapAttrsToList (n: v: "${n}=${v}") environment;
53 # Can't use pkgs.linkFarm,
54 # because Postfix rejects .forward if it's a symlink.
55 home = pkgs.runCommand "public-inbox-home" {
57 |"env ${concatStringsSep " " envList} PATH=\"${makeBinPath cfg.path}:$PATH\" ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
59 passAsFile = [ "forward" ];
62 ln -s ${stateDir}/spamassassin $out/.spamassassin
63 cp $forwardPath $out/.forward
64 install -D -p ${configFile} $out/.public-inbox/config
67 psgi = pkgs.writeText "public-inbox.psgi" ''
68 #!${cfg.package.fullperl} -w
69 # Copyright (C) 2014-2019 all contributors <meta@public-inbox.org>
70 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
75 my $www = PublicInbox::WWW->new;
80 enable 'ReverseProxy';
81 ${concatMapStrings (path: ''
82 mount q(${path}) => sub { $www->call(@_); };
87 descriptionFile = { description, ... }:
88 pkgs.writeText "description" description;
90 enableWatch = (any (i: i.watch != []) (attrValues cfg.inboxes))
91 || (cfg.watch.watchSpam != null);
93 useSpamAssassin = cfg.mda.spamCheck == "spamc" ||
94 cfg.watch.spamCheck == "spamc";
99 options.services.public-inbox = {
100 enable = mkEnableOption "the public-inbox mail archiver";
103 type = types.package;
104 default = pkgs.public-inbox;
106 public-inbox package to use with the public-inbox module
111 type = with types; listOf package;
113 example = literalExample "with pkgs; [ spamassassin ]";
115 Additional packages to place in the path of public-inbox-mda,
116 public-inbox-watch, etc.
122 Inboxes to configure, where attribute names are inbox names
124 type = types.attrsOf (types.submodule {
127 type = with types; listOf str;
128 example = "example-discuss@example.org";
132 type = with types; nullOr str;
134 example = "https://example.org/lists/example-discuss";
136 URL where this inbox can be accessed over HTTP
140 description = mkOption {
142 example = "user/dev discussion of public-inbox itself";
144 User-visible description for the repository
152 Additional structured config for the inbox
156 newsgroup = mkOption {
157 type = with types; nullOr str;
160 NNTP group name for the inbox
165 type = with types; listOf str;
168 Paths for public-inbox-watch(1) to monitor for new mail
170 example = [ "maildir:/path/to/test.example.com.git" ];
173 watchHeader = mkOption {
174 type = with types; nullOr str;
176 example = "List-Id:<test@example.com>";
178 If specified, public-inbox-watch(1) will only process
179 mail containing a matching header.
188 type = with types; listOf str;
191 Command-line arguments to pass to public-inbox-mda(1).
195 spamCheck = mkOption {
196 type = with types; nullOr (enum [ "spamc" ]);
199 If set to spamc, public-inbox-mda(1) will filter spam
206 spamCheck = mkOption {
207 type = with types; nullOr (enum [ "spamc" ]);
210 If set to spamc, public-inbox-watch(1) will filter spam
215 watchSpam = mkOption {
216 type = with types; nullOr str;
218 example = "maildir:/path/to/spam";
220 If set, mail in this maildir will be trained as spam and
221 deleted from all watched inboxes
228 type = with types; listOf str;
230 example = [ "/lists/archives" ];
232 Root paths or URLs that public-inbox will be served on.
233 If domain parts are present, only requests to those
234 domains will be accepted.
238 listenStreams = mkOption {
239 type = with types; listOf str;
240 default = [ "/run/public-inbox-httpd.sock" ];
242 systemd.socket(5) ListenStream values for the
243 public-inbox-httpd service to listen on
249 listenStreams = mkOption {
250 type = with types; listOf str;
251 default = [ "0.0.0.0:993" ];
253 systemd.socket(5) ListenStream values for the
254 public-inbox-imapd service to listen on
259 type = with types; nullOr str;
261 example = "/path/to/fullchain.pem";
263 Path to TLS certificate to use for public-inbox IMAP connections
268 type = with types; nullOr str;
270 example = "/path/to/key.pem";
272 Path to TLS key to use for public-inbox IMAP connections
278 listenStreams = mkOption {
279 type = with types; listOf str;
280 default = [ "0.0.0.0:119" "0.0.0.0:563" ];
282 systemd.socket(5) ListenStream values for the
283 public-inbox-nntpd service to listen on
288 type = with types; nullOr str;
290 example = "/path/to/fullchain.pem";
292 Path to TLS certificate to use for public-inbox NNTP connections
297 type = with types; nullOr str;
299 example = "/path/to/key.pem";
301 Path to TLS key to use for public-inbox NNTP connections
305 extraGroups = mkOption {
306 type = with types; listOf str;
310 Secondary groups to assign to the systemd DynamicUser
311 running public-inbox-nntpd, in addition to the
312 public-inbox group. This is useful for giving
313 public-inbox-nntpd access to a TLS certificate / key, for
319 nntpServer = mkOption {
320 type = with types; listOf str;
322 example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
324 NNTP URLs to this public-inbox instance
328 wwwListing = mkOption {
329 type = with types; enum [ "all" "404" "match=domain" ];
332 Controls which lists (if any) are listed for when the root
333 public-inbox URL is accessed over HTTP.
337 spamAssassinRules = mkOption {
338 type = with types; nullOr path;
339 default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
341 SpamAssassin configuration specific to public-inbox
346 type = with types; attrsOf attrs;
349 Additional structured config for the public-inbox config file
354 config = mkIf cfg.enable {
357 { assertion = config.services.spamassassin.enable || !useSpamAssassin;
359 public-inbox is configured to use SpamAssassin, but
360 services.spamassassin.enable is false. If you don't need
361 spam checking, set services.public-inbox.mda.spamCheck and
362 services.public-inbox.watch.spamCheck to null.
365 { assertion = cfg.path != [] || !useSpamAssassin;
367 public-inbox is configured to use SpamAssassin, but there is
368 no spamc executable in services.public-inbox.path. If you
369 don't need spam checking, set
370 services.public-inbox.mda.spamCheck and
371 services.public-inbox.watch.spamCheck to null.
377 users.public-inbox = {
379 group = "public-inbox";
382 groups.public-inbox = {};
386 public-inbox-httpd = {
387 inherit (cfg.http) listenStreams;
388 wantedBy = [ "sockets.target" ];
390 public-inbox-imapd = {
391 inherit (cfg.imap) listenStreams;
392 wantedBy = [ "sockets.target" ];
394 public-inbox-nntpd = {
395 inherit (cfg.nntp) listenStreams;
396 wantedBy = [ "sockets.target" ];
401 public-inbox-httpd = {
402 inherit (environment);
404 environment = environment // {
405 PATH = mkForce (lib.makeBinPath [
406 pkgs.coreutils pkgs.findutils pkgs.gnugrep pkgs.gnused pkgs.systemd
407 (pkgs.writeShellScriptBin "git" ''
409 ${pkgs.git}/bin/git "$@"
414 after = [ "public-inbox-watch.service" ];
416 ExecStart = "${cfg.package}/bin/public-inbox-httpd ${psgi}";
419 Group = "public-inbox";
422 public-inbox-imapd = {
424 after = [ "public-inbox-watch.service" ];
425 #environment.PERL_INLINE_DIRECTORY = "/tmp/.pub-inline";
426 #environment.LimitNOFILE = 30000;
428 ExecStart = escapeShellArgs (
429 [ "${cfg.package}/bin/public-inbox-imapd" "-W0" ] ++
430 optionals (cfg.imap.cert != null) [ "--cert" cfg.imap.cert ] ++
431 optionals (cfg.imap.key != null) [ "--key" cfg.imap.key ]
433 # NonBlocking is REQUIRED to avoid a race condition
434 # if running simultaneous services
437 Group = "public-inbox";
440 public-inbox-nntpd = {
442 after = [ "public-inbox-watch.service" ];
443 serviceConfig.ExecStart = escapeShellArgs (
444 [ "${cfg.package}/bin/public-inbox-nntpd" ] ++
445 optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ] ++
446 optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ]
448 serviceConfig.NonBlocking = true;
449 serviceConfig.DynamicUser = true;
450 serviceConfig.Group = "public-inbox";
452 public-inbox-watch = {
455 after = optional (cfg.watch.spamCheck == "spamc") "spamassassin.service";
456 wantedBy = optional enableWatch "multi-user.target";
458 ExecStart = "${cfg.package}/bin/public-inbox-watch";
459 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
460 User = "public-inbox";
461 Group = "public-inbox";
462 StateDirectory = [ "public-inbox/inboxes" "public-inbox/emergency" ];
463 StateDirectoryMode = "0750";
467 ${optionalString useSpamAssassin ''
468 install -m 0700 -o spamd -d ${stateDir}/spamassassin
469 ${optionalString (cfg.spamAssassinRules != null) ''
470 ln -sf ${cfg.spamAssassinRules} ${stateDir}/spamassassin/user_prefs
474 ${concatStrings (mapAttrsToList (name: { address, url, ... } @ inbox: ''
475 if [ ! -e ${stateDir}/inboxes/"${escapeShellArg name}" ]; then
476 # public-inbox-init creates an inbox and adds it to a config file.
477 # It tries to atomically write the config file by creating
478 # another file in the same directory, and renaming it.
479 # This has the sad consequence that we can't use
480 # /dev/null, or it would try to create a file in /dev.
481 conf_dir="$(mktemp -d)"
483 env PI_CONFIG=$conf_dir/conf \
484 ${cfg.package}/bin/public-inbox-init -V2 \
485 ${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" url ] ++ address)}
490 ln -sf ${descriptionFile inbox} ${stateDir}/inboxes/"${escapeShellArg name}"/description
492 git=${stateDir}/inboxes/"${escapeShellArg name}"/all.git
493 if [ -d "$git" ]; then
494 # Config is inherited by each epoch repository,
495 # so just needs to be set for all.git.
496 ${pkgs.git}/bin/git --git-dir "$git" \
497 config core.sharedRepository 0640
501 for inbox in ${stateDir}/inboxes/*/; do
502 ls -1 "$inbox" | grep -q '^xap' && continue
504 # This should be idempotent, but only do it for new
505 # inboxes anyway because it's only needed once, and could
506 # be slow for large pre-existing inboxes.
507 env ${concatStringsSep " " envList} \
508 ${cfg.package}/bin/public-inbox-index "$inbox"
514 environment.systemPackages = with pkgs; [ cfg.package ];