1 { lib, pkgs, config, ... }:
6 cfg = config.services.public-inbox;
8 inboxesDir = "/var/lib/public-inbox/inboxes";
9 inboxPath = name: "${inboxesDir}/${name}";
10 gitPath = name: "${inboxPath name}/all.git";
12 inboxes = mapAttrs (name: inbox:
14 inherit (inbox) address url newsgroup watch;
15 mainrepo = inboxPath name;
16 watchheader = inbox.watchHeader;
20 concat = concatMap id;
23 concat (mapAttrsToList (name': value':
24 if isAttrs value' then
25 map ({ name, value }: nameValuePair "${name'}.${name}" value)
27 else if isList value' then map (nameValuePair name') value'
28 else if value' == null then []
29 else [ (nameValuePair name' value') ]) attrs);
31 configFull = recursiveUpdate {
32 publicinbox = inboxes // {
33 nntpserver = cfg.nntpServer;
34 wwwlisting = cfg.wwwListing;
36 publicinboxmda.spamcheck =
37 if (cfg.mda.spamCheck == null) then "none" else cfg.mda.spamCheck;
38 publicinboxwatch.spamcheck =
39 if (cfg.watch.spamCheck == null) then "none" else cfg.watch.spamCheck;
40 publicinboxwatch.watchspam = cfg.watch.watchSpam;
43 configList = configToList configFull;
45 gitConfig = key: val: ''
46 ${pkgs.git}/bin/git config --add --file $out ${escapeShellArgs [ key val ]}
49 configFile = pkgs.runCommand "public-inbox-config" {}
50 (concatStrings (map ({ name, value }: gitConfig name value) configList));
53 PI_EMERGENCY = "/var/lib/public-inbox/emergency";
54 PI_CONFIG = configFile;
57 envList = mapAttrsToList (n: v: "${n}=${v}") environment;
59 # Can't use pkgs.linkFarm,
60 # because Postfix rejects .forward if it's a symlink.
61 home = pkgs.runCommand "public-inbox-home" {
63 |"env ${concatStringsSep " " envList} PATH=\"${makeBinPath cfg.path}:$PATH\" ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
65 passAsFile = [ "forward" ];
68 ln -s /var/lib/public-inbox/spamassassin $out/.spamassassin
69 cp $forwardPath $out/.forward
70 install -D -p ${configFile} $out/.public-inbox/config
73 psgi = pkgs.writeText "public-inbox.psgi" ''
74 #!${cfg.package.fullperl} -w
75 # Copyright (C) 2014-2019 all contributors <meta@public-inbox.org>
76 # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
81 my $www = PublicInbox::WWW->new;
86 enable 'ReverseProxy';
87 ${concatMapStrings (path: ''
88 mount q(${path}) => sub { $www->call(@_); };
93 descriptionFile = { description, ... }:
94 pkgs.writeText "description" description;
96 enableWatch = (any (i: i.watch != []) (attrValues cfg.inboxes))
97 || (cfg.watch.watchSpam != null);
99 useSpamAssassin = cfg.mda.spamCheck == "spamc" ||
100 cfg.watch.spamCheck == "spamc";
106 services.public-inbox = {
107 enable = mkEnableOption "the public-inbox mail archiver";
110 type = types.package;
111 default = pkgs.public-inbox;
113 public-inbox package to use with the public-inbox module
118 type = with types; listOf package;
120 example = literalExample "with pkgs; [ spamassassin ]";
122 Additional packages to place in the path of public-inbox-mda,
123 public-inbox-watch, etc.
129 Inboxes to configure, where attribute names are inbox names
131 type = with types; loaOf (submodule {
135 example = "example-discuss@example.org";
141 example = "https://example.org/lists/example-discuss";
143 URL where this inbox can be accessed over HTTP
147 description = mkOption {
149 example = "user/dev discussion of public-inbox itself";
151 User-visible description for the repository
159 Additional structured config for the inbox
163 newsgroup = mkOption {
167 NNTP group name for the inbox
175 Paths for public-inbox-watch(1) to monitor for new mail
177 example = [ "maildir:/path/to/test.example.com.git" ];
180 watchHeader = mkOption {
183 example = "List-Id:<test@example.com>";
185 If specified, public-inbox-watch(1) will only process
186 mail containing a matching header.
195 type = with types; listOf str;
198 Command-line arguments to pass to public-inbox-mda(1).
202 spamCheck = mkOption {
203 type = with types; nullOr (enum [ "spamc" ]);
206 If set to spamc, public-inbox-mda(1) will filter spam
213 spamCheck = mkOption {
214 type = with types; nullOr (enum [ "spamc" ]);
217 If set to spamc, public-inbox-watch(1) will filter spam
222 watchSpam = mkOption {
223 type = with types; nullOr str;
225 example = "maildir:/path/to/spam";
227 If set, mail in this maildir will be trained as spam and
228 deleted from all watched inboxes
235 type = with types; listOf str;
237 example = [ "/lists/archives" ];
239 Root paths or URLs that public-inbox will be served on.
240 If domain parts are present, only requests to those
241 domains will be accepted.
245 listenStreams = mkOption {
246 type = with types; listOf str;
247 default = [ "/run/public-inbox-httpd.sock" ];
249 systemd.socket(5) ListenStream values for the
250 public-inbox-httpd service to listen on
256 listenStreams = mkOption {
257 type = with types; listOf str;
258 default = [ "0.0.0.0:119" "0.0.0.0:563" ];
260 systemd.socket(5) ListenStream values for the
261 public-inbox-nntpd service to listen on
266 type = with types; nullOr str;
268 example = "/path/to/fullchain.pem";
270 Path to TLS certificate to use for public-inbox NNTP connections
275 type = with types; nullOr str;
277 example = "/path/to/key.pem";
279 Path to TLS key to use for public-inbox NNTP connections
283 extraGroups = mkOption {
284 type = with types; listOf str;
288 Secondary groups to assign to the systemd DynamicUser
289 running public-inbox-nntpd, in addition to the
290 public-inbox group. This is useful for giving
291 public-inbox-nntpd access to a TLS certificate / key, for
297 nntpServer = mkOption {
298 type = with types; listOf str;
300 example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
302 NNTP URLs to this public-inbox instance
306 wwwListing = mkOption {
307 type = with types; enum [ "all" "404" "match=domain" ];
310 Controls which lists (if any) are listed for when the root
311 public-inbox URL is accessed over HTTP.
315 spamAssassinRules = mkOption {
316 type = with types; nullOr path;
317 default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
319 SpamAssassin configuration specific to public-inbox
324 type = with types; attrsOf attrs;
327 Additional structured config for the public-inbox config file
333 config = mkIf cfg.enable {
336 { assertion = config.services.spamassassin.enable || !useSpamAssassin;
338 public-inbox is configured to use SpamAssassin, but
339 services.spamassassin.enable is false. If you don't need
340 spam checking, set services.public-inbox.mda.spamCheck and
341 services.public-inbox.watch.spamCheck to null.
344 { assertion = cfg.path != [] || !useSpamAssassin;
346 public-inbox is configured to use SpamAssassin, but there is
347 no spamc executable in services.public-inbox.path. If you
348 don't need spam checking, set
349 services.public-inbox.mda.spamCheck and
350 services.public-inbox.watch.spamCheck to null.
355 users.users.public-inbox = {
357 group = "public-inbox";
361 users.groups.public-inbox = {};
363 systemd.sockets.public-inbox-httpd = {
364 inherit (cfg.http) listenStreams;
365 wantedBy = [ "sockets.target" ];
368 systemd.sockets.public-inbox-nntpd = {
369 inherit (cfg.nntp) listenStreams;
370 wantedBy = [ "sockets.target" ];
373 systemd.services.public-inbox-httpd = {
375 serviceConfig.ExecStart = "${cfg.package}/bin/public-inbox-httpd ${psgi}";
376 serviceConfig.NonBlocking = true;
377 serviceConfig.DynamicUser = true;
378 serviceConfig.SupplementaryGroups = [ "public-inbox" ];
381 systemd.services.public-inbox-nntpd = {
383 serviceConfig.ExecStart = escapeShellArgs (
384 [ "${cfg.package}/bin/public-inbox-nntpd" ] ++
385 (optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ]) ++
386 (optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ])
388 serviceConfig.NonBlocking = true;
389 serviceConfig.DynamicUser = true;
390 serviceConfig.SupplementaryGroups = [ "public-inbox" ] ++ cfg.nntp.extraGroups;
393 systemd.services.public-inbox-watch = {
396 after = optional (cfg.watch.spamCheck == "spamc") "spamassassin.service";
397 wantedBy = optional enableWatch "multi-user.target";
398 serviceConfig.ExecStart = "${cfg.package}/bin/public-inbox-watch";
399 serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
400 serviceConfig.User = "public-inbox";
403 system.activationScripts.public-inbox = stringAfter [ "users" ] ''
404 install -m 0755 -o public-inbox -g public-inbox -d /var/lib/public-inbox
405 install -m 0750 -o public-inbox -g public-inbox -d ${inboxesDir}
406 install -m 0700 -o public-inbox -g public-inbox -d /var/lib/public-inbox/emergency
408 ${optionalString useSpamAssassin ''
409 install -m 0700 -o spamd -d /var/lib/public-inbox/spamassassin
410 ${optionalString (cfg.spamAssassinRules != null) ''
411 ln -sf ${cfg.spamAssassinRules} /var/lib/public-inbox/spamassassin/user_prefs
415 ${concatStrings (mapAttrsToList (name: { address, url, ... } @ inbox: ''
416 if [ ! -e ${escapeShellArg (inboxPath name)} ]; then
417 # public-inbox-init creates an inbox and adds it to a config file.
418 # It tries to atomically write the config file by creating
419 # another file in the same directory, and renaming it.
420 # This has the sad consequence that we can't use
421 # /dev/null, or it would try to create a file in /dev.
422 conf_dir="$(${pkgs.sudo}/bin/sudo -u public-inbox mktemp -d)"
424 ${pkgs.sudo}/bin/sudo -u public-inbox \
425 env PI_CONFIG=$conf_dir/conf \
426 ${cfg.package}/bin/public-inbox-init -V2 \
427 ${escapeShellArgs ([ name (inboxPath name) url ] ++ address)}
432 ln -sf ${descriptionFile inbox} ${inboxPath name}/description
434 if [ -d ${escapeShellArg (gitPath name)} ]; then
435 # Config is inherited by each epoch repository,
436 # so just needs to be set for all.git.
437 ${pkgs.git}/bin/git --git-dir ${gitPath name} \
438 config core.sharedRepository 0640
442 for inbox in /var/lib/public-inbox/inboxes/*/; do
443 ls -1 "$inbox" | grep -q '^xap' && continue
445 # This should be idempotent, but only do it for new
446 # inboxes anyway because it's only needed once, and could
447 # be slow for large pre-existing inboxes.
448 ${pkgs.sudo}/bin/sudo -u public-inbox \
449 env ${concatStringsSep " " envList} \
450 ${cfg.package}/bin/public-inbox-index "$inbox"
455 environment.systemPackages = with pkgs; [ cfg.package ];