-{ lib, pkgs, config, ... }:
+{
+ lib,
+ pkgs,
+ config,
+ ...
+}:
with lib;
let
cfg = config.services.public-inbox;
+ stateDir = "/var/lib/public-inbox";
- inboxesDir = "/var/lib/public-inbox/inboxes";
- inboxPath = name: "${inboxesDir}/${name}";
- gitPath = name: "${inboxPath name}/all.git";
-
- inboxes = mapAttrs (name: inbox:
- (recursiveUpdate {
- inherit (inbox) address url newsgroup watch;
- mainrepo = inboxPath name;
- watchheader = inbox.watchHeader;
- } inbox.config))
- cfg.inboxes;
-
- concat = concatMap id;
+ gitIni = pkgs.formats.gitIni { listsAsDuplicateKeys = true; };
+ iniAtom = elemAt gitIni.type/*attrsOf*/.functor.wrapped/*attrsOf*/.functor.wrapped/*either*/.functor.wrapped 0;
- configToList = attrs:
- concat (mapAttrsToList (name': value':
- if isAttrs value' then
- map ({ name, value }: nameValuePair "${name'}.${name}" value)
- (configToList value')
- else if isList value' then map (nameValuePair name') value'
- else if value' == null then []
- else [ (nameValuePair name' value') ]) attrs);
+ useSpamAssassin =
+ cfg.settings.publicinboxmda.spamcheck == "spamc"
+ || cfg.settings.publicinboxwatch.spamcheck == "spamc";
- configFull = recursiveUpdate {
- publicinbox = inboxes // {
- nntpserver = cfg.nntpServer;
- wwwlisting = cfg.wwwListing;
+ publicInboxDaemonOptions = proto: defaultPort: {
+ args = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ description = "Command-line arguments to pass to {manpage}`public-inbox-${proto}d(1)`.";
+ };
+ port = mkOption {
+ type = with types; nullOr (either str port);
+ default = defaultPort;
+ description = ''
+ Listening port.
+ Beware that public-inbox uses well-known ports number to decide whether to enable TLS or not.
+ Set to null and use `systemd.sockets.public-inbox-${proto}d.listenStreams`
+ if you need a more advanced listening.
+ '';
+ };
+ cert = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "/path/to/fullchain.pem";
+ description = "Path to TLS certificate to use for connections to {manpage}`public-inbox-${proto}d(1)`.";
+ };
+ key = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "/path/to/key.pem";
+ description = "Path to TLS key to use for connections to {manpage}`public-inbox-${proto}d(1)`.";
};
- publicinboxmda.spamcheck =
- if (cfg.mda.spamCheck == null) then "none" else cfg.mda.spamCheck;
- publicinboxwatch.spamcheck =
- if (cfg.watch.spamCheck == null) then "none" else cfg.watch.spamCheck;
- publicinboxwatch.watchspam = cfg.watch.watchSpam;
- } cfg.config;
-
- configList = configToList configFull;
-
- gitConfig = key: val: ''
- ${pkgs.git}/bin/git config --add --file $out ${escapeShellArgs [ key val ]}
- '';
-
- configFile = pkgs.runCommand "public-inbox-config" {}
- (concatStrings (map ({ name, value }: gitConfig name value) configList));
-
- environment = {
- PI_EMERGENCY = "/var/lib/public-inbox/emergency";
- PI_CONFIG = configFile;
};
- 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 /var/lib/public-inbox/spamassassin $out/.spamassassin
- cp $forwardPath $out/.forward
- '';
-
- psgi = pkgs.writeText "public-inbox.psgi" ''
- #!${cfg.package.fullperl} -w
- # Copyright (C) 2014-2019 all contributors <meta@public-inbox.org>
- # License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
- 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}
- }
- '';
-
- descriptionFile = { description, ... }:
- pkgs.writeText "description" description;
-
- enableWatch = (any (i: i.watch != []) (attrValues cfg.inboxes))
- || (cfg.watch.watchSpam != null);
-
- useSpamAssassin = cfg.mda.spamCheck == "spamc" ||
- cfg.watch.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
- '';
+ serviceConfig =
+ srv:
+ let
+ proto = removeSuffix "d" srv;
+ needNetwork = builtins.hasAttr proto cfg && cfg.${proto}.port == null;
+ in
+ {
+ serviceConfig = {
+ # Enable JIT-compiled C (via Inline::C)
+ Environment = [ "PERL_INLINE_DIRECTORY=/run/public-inbox-${srv}/perl-inline" ];
+ # NonBlocking is REQUIRED to avoid a race condition
+ # if running simultaneous services.
+ NonBlocking = true;
+ #LimitNOFILE = 30000;
+ User = config.users.users."public-inbox".name;
+ Group = config.users.groups."public-inbox".name;
+ RuntimeDirectory = [
+ "public-inbox-${srv}/perl-inline"
+ ];
+ RuntimeDirectoryMode = "700";
+ # This is for BindPaths= and BindReadOnlyPaths=
+ # to allow traversal of directories they create inside RootDirectory=
+ UMask = "0066";
+ StateDirectory = [ "public-inbox" ];
+ StateDirectoryMode = "0750";
+ WorkingDirectory = stateDir;
+ BindReadOnlyPaths =
+ [
+ "/etc"
+ "/run/systemd"
+ "${config.i18n.glibcLocales}"
+ ]
+ ++ mapAttrsToList (name: inbox: inbox.description) cfg.inboxes
+ ++ filter (x: x != null) [
+ cfg.${proto}.cert or null
+ cfg.${proto}.key or null
+ ];
+ # The following options are only for optimizing:
+ # systemd-analyze security public-inbox-'*'
+ AmbientCapabilities = "";
+ CapabilityBoundingSet = "";
+ # ProtectClock= adds DeviceAllow=char-rtc r
+ DeviceAllow = "";
+ LockPersonality = true;
+ MemoryDenyWriteExecute = true;
+ NoNewPrivileges = true;
+ PrivateNetwork = mkDefault (!needNetwork);
+ ProcSubset = "pid";
+ ProtectClock = true;
+ ProtectHome = "tmpfs";
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectProc = "invisible";
+ ProtectSystem = "strict";
+ RemoveIPC = true;
+ RestrictAddressFamilies =
+ [ "AF_UNIX" ]
+ ++ optionals needNetwork [
+ "AF_INET"
+ "AF_INET6"
+ ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallFilter = [
+ "@system-service"
+ "~@aio"
+ "~@chown"
+ "~@keyring"
+ "~@memlock"
+ "~@resources"
+ # Not removing @setuid and @privileged because Inline::C needs them.
+ # Not removing @timer because git upload-pack needs it.
+ ];
+ SystemCallArchitectures = "native";
};
-
- 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.
- '';
+ confinement = {
+ enable = true;
+ mode = "full-apivfs";
+ # Inline::C needs a /bin/sh, and dash is enough
+ binSh = "${pkgs.dash}/bin/dash";
+ packages = [
+ pkgs.iana-etc
+ (getLib pkgs.nss)
+ pkgs.tzdata
+ ];
};
+ };
+in
- inboxes = mkOption {
- description = ''
- Inboxes to configure, where attribute names are inbox names
- '';
- type = with types; loaOf (submodule {
- options = {
- address = mkOption {
- type = listOf str;
+{
+ options.services.public-inbox = {
+ enable = mkEnableOption "the public-inbox mail archiver";
+ package = mkPackageOption pkgs "public-inbox" { };
+ path = mkOption {
+ type = with types; listOf package;
+ default = [ ];
+ example = literalExpression "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.attrsOf (
+ types.submodule (
+ { name, ... }:
+ {
+ freeformType = types.attrsOf iniAtom;
+ options.inboxdir = mkOption {
+ type = types.str;
+ default = "${stateDir}/inboxes/${name}";
+ description = "The absolute path to the directory which hosts the public-inbox.";
+ };
+ options.address = mkOption {
+ type = with types; listOf str;
example = "example-discuss@example.org";
+ description = "The email addresses of the public-inbox.";
};
-
- url = mkOption {
- type = nullOr str;
- default = null;
+ options.url = mkOption {
+ type = types.nonEmptyStr;
example = "https://example.org/lists/example-discuss";
- description = ''
- URL where this inbox can be accessed over HTTP
- '';
+ description = "URL where this inbox can be accessed over HTTP.";
};
-
- description = mkOption {
- type = str;
+ options.description = mkOption {
+ type = types.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
- '';
+ description = "User-visible description for the repository.";
+ apply = pkgs.writeText "public-inbox-description-${name}";
};
-
- newsgroup = mkOption {
- type = nullOr str;
+ options.newsgroup = mkOption {
+ type = with types; nullOr str;
default = null;
- description = ''
- NNTP group name for the inbox
- '';
+ 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
- '';
+ options.watch = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ description = "Paths for {manpage}`public-inbox-watch(1)` to monitor for new mail.";
example = [ "maildir:/path/to/test.example.com.git" ];
};
-
- watchHeader = mkOption {
- type = nullOr str;
+ options.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
+ If specified, {manpage}`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.";
+ };
+ }
+ )
+ );
+ };
+ imap = {
+ enable = mkEnableOption "the public-inbox IMAP server";
+ } // publicInboxDaemonOptions "imap" 993;
+ http = {
+ enable = mkEnableOption "the public-inbox HTTP server";
+ 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.
+ '';
};
-
- mda = {
- args = mkOption {
- type = with types; listOf str;
- default = [];
- description = ''
- Command-line arguments to pass to public-inbox-mda(1).
- '';
+ args = (publicInboxDaemonOptions "http" 80).args;
+ port = mkOption {
+ type = with types; nullOr (either str port);
+ default = 80;
+ example = "/run/public-inbox-httpd.sock";
+ description = ''
+ Listening port or systemd's ListenStream= entry
+ to be used as a reverse proxy, eg. in nginx:
+ `locations."/inbox".proxyPass = "http://unix:''${config.services.public-inbox.http.port}:/inbox";`
+ Set to null and use `systemd.sockets.public-inbox-httpd.listenStreams`
+ if you need a more advanced listening.
+ '';
+ };
+ };
+ mda = {
+ enable = mkEnableOption "the public-inbox Mail Delivery Agent";
+ args = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ description = "Command-line arguments to pass to {manpage}`public-inbox-mda(1)`.";
+ };
+ };
+ postfix.enable = mkEnableOption "the integration into Postfix";
+ nntp = {
+ enable = mkEnableOption "the public-inbox NNTP server";
+ } // publicInboxDaemonOptions "nntp" 563;
+ spamAssassinRules = mkOption {
+ type = with types; nullOr path;
+ default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
+ defaultText = literalExpression "\${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](https://public-inbox.org/public-inbox-config.html).
+ '';
+ default = { };
+ type = types.submodule {
+ freeformType = gitIni.type;
+ options.publicinbox = mkOption {
+ default = { };
+ description = "public inboxes";
+ type = types.submodule {
+ # Support both global options like `services.public-inbox.settings.publicinbox.imapserver`
+ # and inbox specific options like `services.public-inbox.settings.publicinbox.foo.address`.
+ freeformType =
+ with types;
+ attrsOf (oneOf [
+ iniAtom
+ (attrsOf iniAtom)
+ ]);
+
+ options.css = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ description = "The local path name of a CSS file for the PSGI web interface.";
+ };
+ options.imapserver = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ example = [ "imap.public-inbox.org" ];
+ description = "IMAP URLs to this public-inbox instance";
+ };
+ 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.pop3server = mkOption {
+ type = with types; listOf str;
+ default = [ ];
+ example = [ "pop.public-inbox.org" ];
+ description = "POP3 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.
+ '';
+ };
+ };
};
-
- spamCheck = mkOption {
- type = with types; nullOr (enum [ "spamc" ]);
- default = "spamc";
+ options.publicinboxmda.spamcheck = mkOption {
+ type =
+ with types;
+ enum [
+ "spamc"
+ "none"
+ ];
+ default = "none";
description = ''
- If set to spamc, public-inbox-mda(1) will filter spam
- using SpamAssassin
+ If set to spamc, {manpage}`public-inbox-watch(1)` will filter spam
+ using SpamAssassin.
'';
};
- };
-
- watch = {
- spamCheck = mkOption {
- type = with types; nullOr (enum [ "spamc" ]);
- default = "spamc";
+ options.publicinboxwatch.spamcheck = mkOption {
+ type =
+ with types;
+ enum [
+ "spamc"
+ "none"
+ ];
+ default = "none";
description = ''
- If set to spamc, public-inbox-watch(1) will filter spam
- using SpamAssassin
+ If set to spamc, {manpage}`public-inbox-watch(1)` will filter spam
+ using SpamAssassin.
'';
};
-
- watchSpam = mkOption {
+ options.publicinboxwatch.watchspam = mkOption {
type = with types; nullOr str;
default = null;
example = "maildir:/path/to/spam";
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.
- '';
- };
-
- 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
- '';
- };
-
- 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
- '';
- };
-
- 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.
- '';
+ options.coderepo = mkOption {
+ default = { };
+ description = "code repositories";
+ type = types.attrsOf (
+ types.submodule {
+ freeformType = types.attrsOf 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";
+ };
+ }
+ );
};
};
-
- 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
- '';
- };
};
+ openFirewall = mkEnableOption "opening the firewall when using a port option";
};
-
config = mkIf cfg.enable {
-
assertions = [
- { assertion = config.services.spamassassin.enable || !useSpamAssassin;
+ {
+ 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.mda.spamCheck and
- services.public-inbox.watch.spamCheck to null.
+ spam checking, set `services.public-inbox.settings.publicinboxmda.spamcheck' and
+ `services.public-inbox.settings.publicinboxwatch.spamcheck' to null.
'';
}
- { assertion = cfg.path != [] || !useSpamAssassin;
+ {
+ 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.mda.spamCheck and
- services.public-inbox.watch.spamCheck to null.
+ `services.public-inbox.settings.publicinboxmda.spamcheck' and
+ `services.public-inbox.settings.publicinboxwatch.spamcheck' to null.
'';
}
];
-
- 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" ];
+ services.public-inbox.settings = filterAttrsRecursive (n: v: v != null) {
+ publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes;
};
-
- systemd.sockets.public-inbox-nntpd = {
- inherit (cfg.nntp) listenStreams;
- wantedBy = [ "sockets.target" ];
+ users = {
+ users.public-inbox = {
+ home = stateDir;
+ group = "public-inbox";
+ isSystemUser = true;
+ };
+ groups.public-inbox = { };
};
-
- 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" ];
+ networking.firewall = mkIf cfg.openFirewall {
+ allowedTCPPorts = mkMerge (
+ map
+ (proto: (mkIf (cfg.${proto}.enable && types.port.check cfg.${proto}.port) [ cfg.${proto}.port ]))
+ [
+ "imap"
+ "http"
+ "nntp"
+ ]
+ );
};
+ services.postfix = mkIf (cfg.postfix.enable && cfg.mda.enable) {
+ # Not sure limiting to 1 is necessary, but better safe than sorry.
+ config.public-inbox_destination_recipient_limit = "1";
+
+ # Register the addresses as existing
+ virtual = concatStringsSep "\n" (
+ mapAttrsToList (
+ _: inbox: concatMapStringsSep "\n" (address: "${address} ${address}") inbox.address
+ ) cfg.inboxes
+ );
- 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 ])
+ # Deliver the addresses with the public-inbox transport
+ transport = concatStringsSep "\n" (
+ mapAttrsToList (
+ _: inbox: concatMapStringsSep "\n" (address: "${address} public-inbox:${address}") inbox.address
+ ) cfg.inboxes
);
- serviceConfig.NonBlocking = true;
- serviceConfig.DynamicUser = true;
- serviceConfig.SupplementaryGroups = [ "public-inbox" ] ++ cfg.nntp.extraGroups;
- };
- 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";
+ # The public-inbox transport
+ masterConfig.public-inbox = {
+ type = "unix";
+ privileged = true; # Required for user=
+ command = "pipe";
+ args = [
+ "flags=X" # Report as a final delivery
+ "user=${with config.users; users."public-inbox".name + ":" + groups."public-inbox".name}"
+ # Specifying a nexthop when using the transport
+ # (eg. test public-inbox:test) allows to
+ # receive mails with an extension (eg. test+foo).
+ "argv=${pkgs.writeShellScript "public-inbox-transport" ''
+ export HOME="${stateDir}"
+ export ORIGINAL_RECIPIENT="''${2:-1}"
+ export PATH="${makeBinPath cfg.path}:$PATH"
+ exec ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
+ ''} \${original_recipient} \${nexthop}"
+ ];
+ };
};
-
- 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 \
- ${cfg.package}/bin/public-inbox-init -V2 \
- ${escapeShellArgs ([ name (inboxPath name) url ] ++ address)}
-
- rm -rf $conf_dir
- fi
-
- ln -sf ${descriptionFile inbox} ${inboxPath 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} \
- config core.sharedRepository 0640
- fi
- '') cfg.inboxes)}
-
- for inbox in /var/lib/public-inbox/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} \
- ${cfg.package}/bin/public-inbox-index "$inbox"
- done
-
- '';
-
+ systemd.sockets = mkMerge (
+ map
+ (
+ proto:
+ mkIf (cfg.${proto}.enable && cfg.${proto}.port != null) {
+ "public-inbox-${proto}d" = {
+ listenStreams = [ (toString cfg.${proto}.port) ];
+ wantedBy = [ "sockets.target" ];
+ };
+ }
+ )
+ [
+ "imap"
+ "http"
+ "nntp"
+ ]
+ );
+ systemd.services = mkMerge [
+ (mkIf cfg.imap.enable {
+ public-inbox-imapd = mkMerge [
+ (serviceConfig "imapd")
+ {
+ after = [
+ "public-inbox-init.service"
+ "public-inbox-watch.service"
+ ];
+ requires = [ "public-inbox-init.service" ];
+ 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
+ ]
+ );
+ };
+ }
+ ];
+ })
+ (mkIf cfg.http.enable {
+ public-inbox-httpd = mkMerge [
+ (serviceConfig "httpd")
+ {
+ after = [
+ "public-inbox-init.service"
+ "public-inbox-watch.service"
+ ];
+ requires = [ "public-inbox-init.service" ];
+ serviceConfig = {
+ BindReadOnlyPaths = map (c: c.dir) (lib.attrValues cfg.settings.coderepo);
+ ExecStart = escapeShellArgs (
+ [ "${cfg.package}/bin/public-inbox-httpd" ]
+ ++ cfg.http.args
+ ++
+ # See https://public-inbox.org/public-inbox.git/tree/examples/public-inbox.psgi
+ # for upstream's example.
+ [
+ (pkgs.writeText "public-inbox.psgi" ''
+ #!${cfg.package.fullperl} -w
+ use strict;
+ use warnings;
+ use Plack::Builder;
+ use PublicInbox::WWW;
+
+ my $www = PublicInbox::WWW->new;
+ $www->preload;
+
+ builder {
+ # If reached through a reverse proxy,
+ # make it transparent by resetting some HTTP headers
+ # used by public-inbox to generate URIs.
+ enable 'ReverseProxy';
+
+ # No need to send a response body if it's an HTTP HEAD requests.
+ enable 'Head';
+
+ # Route according to configured domains and root paths.
+ ${concatMapStrings (path: ''
+ mount q(${path}) => sub { $www->call(@_); };
+ '') cfg.http.mounts}
+ }
+ '')
+ ]
+ );
+ };
+ }
+ ];
+ })
+ (mkIf cfg.nntp.enable {
+ public-inbox-nntpd = mkMerge [
+ (serviceConfig "nntpd")
+ {
+ after = [
+ "public-inbox-init.service"
+ "public-inbox-watch.service"
+ ];
+ requires = [ "public-inbox-init.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
+ ]
+ );
+ };
+ }
+ ];
+ })
+ (mkIf
+ (
+ any (inbox: inbox.watch != [ ]) (attrValues cfg.inboxes)
+ || cfg.settings.publicinboxwatch.watchspam != null
+ )
+ {
+ public-inbox-watch = mkMerge [
+ (serviceConfig "watch")
+ {
+ inherit (cfg) path;
+ wants = [ "public-inbox-init.service" ];
+ requires = [
+ "public-inbox-init.service"
+ ] ++ optional (cfg.settings.publicinboxwatch.spamcheck == "spamc") "spamassassin.service";
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ ExecStart = "${cfg.package}/bin/public-inbox-watch";
+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+ };
+ }
+ ];
+ }
+ )
+ ({
+ public-inbox-init =
+ let
+ PI_CONFIG = gitIni.generate "public-inbox.ini" (
+ filterAttrsRecursive (n: v: v != null) cfg.settings
+ );
+ in
+ mkMerge [
+ (serviceConfig "init")
+ {
+ wantedBy = [ "multi-user.target" ];
+ restartIfChanged = true;
+ restartTriggers = [ PI_CONFIG ];
+ script =
+ ''
+ set -ux
+ install -D -p ${PI_CONFIG} ${stateDir}/.public-inbox/config
+ ''
+ + 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)"
+
+ 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 ${inbox.description} \
+ ${stateDir}/inboxes/${escapeShellArg name}/description
+
+ export GIT_DIR=${stateDir}/inboxes/${escapeShellArg name}/all.git
+ if test -d "$GIT_DIR"; then
+ # Config is inherited by each epoch repository,
+ # so just needs to be set for all.git.
+ ${pkgs.git}/bin/git config core.sharedRepository 0640
+ fi
+ '') cfg.inboxes
+ );
+ serviceConfig = {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ StateDirectory = [
+ "public-inbox/.public-inbox"
+ "public-inbox/.public-inbox/emergency"
+ "public-inbox/inboxes"
+ ];
+ };
+ }
+ ];
+ })
+ ];
environment.systemPackages = with pkgs; [ cfg.package ];
-
};
+ meta.maintainers = with lib.maintainers; [
+ julm
+ qyliss
+ ];
}