1 { lib, pkgs, config, ... }:
6 cfg = config.services.public-inbox;
7 stateDir = "/var/lib/public-inbox";
9 singleIniAtom = with types; nullOr (oneOf [ bool int float str ]) // {
10 description = "INI atom (null, bool, int, float or string)";
12 iniAtom = with types; coercedTo singleIniAtom singleton (listOf singleIniAtom) // {
13 description = singleIniAtom.description + " or a list of them for duplicate keys";
15 iniAttrs = with types; attrsOf (either (attrsOf iniAtom) iniAtom);
17 type = with types; attrsOf iniAttrs;
18 generate = name: value: pkgs.writeText name (generators.toGitINI value);
22 PI_EMERGENCY = "${stateDir}/emergency";
23 PI_CONFIG = gitIni.generate "public-inbox.ini"
24 (filterAttrsRecursive (n: v: v != null) cfg.settings);
26 envList = mapAttrsToList (n: v: "${n}=${v}") environment;
28 # Can't use pkgs.linkFarm,
29 # because Postfix rejects .forward if it's a symlink.
30 home = pkgs.runCommand "public-inbox-home"
32 |"env ${concatStringsSep " " envList} PATH=\"${makeBinPath cfg.path}:$PATH\" ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
34 passAsFile = [ "forward" ];
37 ln -s ${stateDir}/spamassassin $out/.spamassassin
38 cp $forwardPath $out/.forward
39 install -D -p ${environment.PI_CONFIG} $out/.public-inbox/config
42 psgi = pkgs.writeText "public-inbox.psgi" ''
43 #!${cfg.package.fullperl} -w
44 # Copyright (C) 2014-2019 all contributors <meta@public-inbox.org>
45 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
50 my $www = PublicInbox::WWW->new;
55 enable 'ReverseProxy';
56 ${concatMapStrings (path: ''
57 mount q(${path}) => sub { $www->call(@_); };
62 enableWatch = any (i: i.watch != []) (attrValues cfg.inboxes)
63 || cfg.settings.publicinboxwatch.watchspam != null;
65 useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" ||
66 cfg.settings.publicinboxwatch.spamcheck == "spamc";
71 options.services.public-inbox = {
72 enable = mkEnableOption "the public-inbox mail archiver";
75 default = pkgs.public-inbox;
77 public-inbox package to use with the public-inbox module
81 type = with types; listOf package;
83 example = literalExample "with pkgs; [ spamassassin ]";
85 Additional packages to place in the path of public-inbox-mda,
86 public-inbox-watch, etc.
91 Inboxes to configure, where attribute names are inbox names.
94 type = types.submodule {
95 freeformType = types.attrsOf (types.submodule ({name, ...}: {
96 freeformType = types.attrsOf iniAtom;
97 options.mainrepo = mkOption {
99 default = "${stateDir}/inboxes/${name}";
101 options.address = mkOption {
102 type = with types; listOf str;
103 example = "example-discuss@example.org";
105 options.url = mkOption {
106 type = with types; nullOr str;
108 example = "https://example.org/lists/example-discuss";
110 URL where this inbox can be accessed over HTTP
113 options.description = mkOption {
115 example = "user/dev discussion of public-inbox itself";
117 User-visible description for the repository
120 options.newsgroup = mkOption {
121 type = with types; nullOr str;
124 NNTP group name for the inbox
127 options.watch = mkOption {
128 type = with types; listOf str;
131 Paths for public-inbox-watch(1) to monitor for new mail
133 example = [ "maildir:/path/to/test.example.com.git" ];
135 options.watchheader = mkOption {
136 type = with types; nullOr str;
138 example = "List-Id:<test@example.com>";
140 If specified, public-inbox-watch(1) will only process
141 mail containing a matching header.
144 options.coderepo = mkOption {
145 type = (types.listOf (types.enum (attrNames cfg.settings.coderepo))) // {
146 description = "list of coderepo names";
150 Nicknames of a "coderepo" section associated with the inbox.
158 type = with types; listOf str;
161 Command-line arguments to pass to public-inbox-mda(1).
167 type = with types; listOf str;
169 example = [ "/lists/archives" ];
171 Root paths or URLs that public-inbox will be served on.
172 If domain parts are present, only requests to those
173 domains will be accepted.
176 listenStreams = mkOption {
177 type = with types; listOf str;
178 default = [ "/run/public-inbox-httpd.sock" ];
180 systemd.socket(5) ListenStream values for the
181 public-inbox-httpd service to listen on
185 type = with types; listOf str;
188 Command-line arguments to pass to public-inbox-httpd(1).
193 listenStreams = mkOption {
194 type = with types; listOf str;
195 default = [ "0.0.0.0:993" ];
197 systemd.socket(5) ListenStream values for the
198 public-inbox-imapd service to listen on
202 type = with types; listOf str;
205 Command-line arguments to pass to public-inbox-imapd(1).
209 type = with types; nullOr str;
211 example = "/path/to/fullchain.pem";
213 Path to TLS certificate to use for public-inbox IMAP connections
217 type = with types; nullOr str;
219 example = "/path/to/key.pem";
221 Path to TLS key to use for public-inbox IMAP connections
226 listenStreams = mkOption {
227 type = with types; listOf str;
228 default = [ "0.0.0.0:119" "0.0.0.0:563" ];
230 systemd.socket(5) ListenStream values for the
231 public-inbox-nntpd service to listen on
235 type = with types; listOf str;
238 Command-line arguments to pass to public-inbox-nntpd(1).
242 type = with types; nullOr str;
244 example = "/path/to/fullchain.pem";
246 Path to TLS certificate to use for public-inbox NNTP connections
250 type = with types; nullOr str;
252 example = "/path/to/key.pem";
254 Path to TLS key to use for public-inbox NNTP connections
258 spamAssassinRules = mkOption {
259 type = with types; nullOr path;
260 default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
262 SpamAssassin configuration specific to public-inbox
265 settings = mkOption {
267 Settings for the public-inbox config file.
270 type = types.submodule {
271 freeformType = gitIni.type;
272 options.publicinbox = mkOption {
275 public-inbox configuration.
277 type = types.submodule {
278 freeformType = iniAttrs;
279 options.css = mkOption {
280 type = with types; listOf str;
283 options.nntpserver = mkOption {
284 type = with types; listOf str;
286 example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
288 NNTP URLs to this public-inbox instance
291 options.wwwlisting = mkOption {
292 type = with types; enum [ "all" "404" "match=domain" ];
295 Controls which lists (if any) are listed for when the root
296 public-inbox URL is accessed over HTTP.
301 options.publicinboxmda = mkOption {
303 description = "mailbox delivery agent";
304 type = types.submodule {
305 freeformType = iniAttrs;
306 options.spamcheck = mkOption {
307 type = with types; enum [ "spamc" "none" ];
310 If set to spamc, public-inbox-mda(1) will filter spam
316 options.publicinboxwatch = mkOption {
318 description = "mailbox watcher";
319 type = types.submodule {
320 freeformType = iniAttrs;
321 options.spamcheck = mkOption {
322 type = with types; enum [ "spamc" "none" ];
325 If set to spamc, public-inbox-watch(1) will filter spam
329 options.watchspam = mkOption {
330 type = with types; nullOr str;
332 example = "maildir:/path/to/spam";
334 If set, mail in this maildir will be trained as spam and
335 deleted from all watched inboxes
340 options.coderepo = mkOption {
342 description = "code repositories";
343 type = types.submodule {
344 freeformType = types.attrsOf (types.submodule {
345 freeformType = types.either (types.attrsOf iniAtom) iniAtom;
346 options.cgitUrl = mkOption {
348 description = "URL of a cgit instance";
350 options.dir = mkOption {
352 description = "Path to a git repository";
360 config = mkIf cfg.enable {
362 { assertion = config.services.spamassassin.enable || !useSpamAssassin;
364 public-inbox is configured to use SpamAssassin, but
365 services.spamassassin.enable is false. If you don't need
366 spam checking, set services.public-inbox.settings.publicinboxmda.spamcheck and
367 services.public-inbox.settings.publicinboxwatch.spamcheck to null.
370 { assertion = cfg.path != [] || !useSpamAssassin;
372 public-inbox is configured to use SpamAssassin, but there is
373 no spamc executable in services.public-inbox.path. If you
374 don't need spam checking, set
375 services.public-inbox.settings.publicinboxmda.spamcheck and
376 services.public-inbox.settings.publicinboxwatch.spamcheck to null.
380 services.public-inbox.settings =
381 filterAttrsRecursive (n: v: v != null) {
382 publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes;
385 users.public-inbox = {
387 group = "public-inbox";
390 groups.public-inbox = {};
393 public-inbox-httpd = {
394 inherit (cfg.http) listenStreams;
395 wantedBy = [ "sockets.target" ];
397 public-inbox-imapd = {
398 inherit (cfg.imap) listenStreams;
399 wantedBy = [ "sockets.target" ];
401 public-inbox-nntpd = {
402 inherit (cfg.nntp) listenStreams;
403 wantedBy = [ "sockets.target" ];
407 public-inbox-httpd = {
408 inherit (environment);
409 after = [ "public-inbox-watch.service" ];
411 ExecStart = escapeShellArgs (
412 [ "${cfg.package}/bin/public-inbox-httpd" psgi ] ++
417 Group = "public-inbox";
420 public-inbox-imapd = {
422 after = [ "public-inbox-watch.service" ];
423 #environment.PERL_INLINE_DIRECTORY = "/tmp/.pub-inline";
424 #environment.LimitNOFILE = 30000;
426 ExecStart = escapeShellArgs (
427 [ "${cfg.package}/bin/public-inbox-imapd" ] ++
429 optionals (cfg.imap.cert != null) [ "--cert" cfg.imap.cert ] ++
430 optionals (cfg.imap.key != null) [ "--key" cfg.imap.key ]
432 # NonBlocking is REQUIRED to avoid a race condition
433 # if running simultaneous services
436 Group = "public-inbox";
439 public-inbox-nntpd = {
441 after = [ "public-inbox-watch.service" ];
443 ExecStart = escapeShellArgs (
444 [ "${cfg.package}/bin/public-inbox-nntpd" ] ++
446 optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ] ++
447 optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ]
451 Group = "public-inbox";
454 public-inbox-watch = {
457 after = optional (cfg.settings.publicinboxwatch.spamcheck == "spamc") "spamassassin.service";
458 wantedBy = optional enableWatch "multi-user.target";
460 ExecStart = "${cfg.package}/bin/public-inbox-watch";
461 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
462 User = "public-inbox";
463 Group = "public-inbox";
465 "public-inbox/emergency"
466 "public-inbox/inboxes"
468 StateDirectoryMode = "0750";
472 ${optionalString useSpamAssassin ''
473 install -m 0700 -o spamd -d ${stateDir}/spamassassin
474 ${optionalString (cfg.spamAssassinRules != null) ''
475 ln -sf ${cfg.spamAssassinRules} ${stateDir}/spamassassin/user_prefs
479 ${concatStrings (mapAttrsToList (name: inbox: ''
480 if [ ! -e ${stateDir}/inboxes/"${escapeShellArg name}" ]; then
481 # public-inbox-init creates an inbox and adds it to a config file.
482 # It tries to atomically write the config file by creating
483 # another file in the same directory, and renaming it.
484 # This has the sad consequence that we can't use
485 # /dev/null, or it would try to create a file in /dev.
486 conf_dir="$(mktemp -d)"
488 env PI_CONFIG=$conf_dir/conf \
489 ${cfg.package}/bin/public-inbox-init -V2 \
490 ${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" inbox.url ] ++ inbox.address)}
495 ln -sf ${pkgs.writeText "description" inbox.description} ${stateDir}/inboxes/"${escapeShellArg name}"/description
497 git=${stateDir}/inboxes/"${escapeShellArg name}"/all.git
498 if [ -d "$git" ]; then
499 # Config is inherited by each epoch repository,
500 # so just needs to be set for all.git.
501 ${pkgs.git}/bin/git --git-dir "$git" \
502 config core.sharedRepository 0640
506 for inbox in ${stateDir}/inboxes/*/; do
507 ls -1 "$inbox" | grep -q '^xap' && continue
509 # This should be idempotent, but only do it for new
510 # inboxes anyway because it's only needed once, and could
511 # be slow for large pre-existing inboxes.
512 env ${concatStringsSep " " envList} \
513 ${cfg.package}/bin/public-inbox-index "$inbox"
518 environment.systemPackages = with pkgs; [ cfg.package ];
520 meta.maintainers = with lib.maintainers; [ julm ];