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";
+ manref = name: vol: "<citerefentry><refentrytitle>${name}</refentrytitle><manvolnum>${toString vol}</manvolnum></citerefentry>";
- inboxes = mapAttrs (name: inbox:
- (recursiveUpdate {
- inherit (inbox) address url newsgroup watch;
- mainrepo = inboxPath name;
- watchheader = inbox.watchHeader;
- } inbox.config))
- cfg.inboxes;
-
- concat = concatMap id;
-
- 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);
-
- configFull = recursiveUpdate {
- publicinbox = inboxes // {
- nntpserver = cfg.nntpServer;
- wwwlisting = cfg.wwwListing;
- };
- 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));
+ singleIniAtom = with types; nullOr (oneOf [ bool int float str ]) // {
+ description = "INI atom (null, bool, int, float or string)";
+ };
+ iniAtom = with types; coercedTo singleIniAtom singleton (listOf singleIniAtom) // {
+ description = singleIniAtom.description + " or a list of them for duplicate keys";
+ };
+ iniAttrs = with types; attrsOf (either (attrsOf iniAtom) iniAtom);
+ gitIni = {
+ type = with types; attrsOf iniAttrs;
+ generate = name: value: pkgs.writeText name (generators.toGitINI value);
+ };
environment = {
- PI_EMERGENCY = "/var/lib/public-inbox/emergency";
- PI_CONFIG = configFile;
+ PI_EMERGENCY = "${stateDir}/emergency";
+ PI_CONFIG = gitIni.generate "public-inbox.ini"
+ (filterAttrsRecursive (n: v: v != null) cfg.settings);
};
- 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
- install -D -p ${configFile} $out/.public-inbox/config
- '';
-
- 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";
-
+ useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" ||
+ cfg.settings.publicinboxwatch.spamcheck == "spamc";
+
+ serviceConfig = srv: {
+ # 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"
+ # Create RootDirectory= in the host's mount namespace.
+ "public-inbox-${srv}/root"
+ ];
+ RuntimeDirectoryMode = "700";
+ # Avoid mounting RootDirectory= in the own RootDirectory= of ExecStart='s mount namespace.
+ InaccessiblePaths = ["-+/run/public-inbox-${srv}/root"];
+ # This is for BindPaths= and BindReadOnlyPaths=
+ # to allow traversal of directories they create in RootDirectory=.
+ UMask = "0066";
+ RootDirectory = "/run/public-inbox-${srv}/root";
+ RootDirectoryStartOnly = true;
+ WorkingDirectory = stateDir;
+ MountAPIVFS = true;
+ BindReadOnlyPaths = [
+ builtins.storeDir
+ "/etc"
+ "/run"
+ ];
+ BindPaths = [
+ stateDir
+ ];
+ # 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;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateNetwork = mkDefault false;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectSystem = "strict";
+ RemoveIPC = true;
+ RestrictAddressFamilies = [ "AF_UNIX" ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallFilter = optionals (srv != "init") [
+ "@system-service"
+ "~@aio" "~@chown" "~@keyring" "~@memlock"
+ "~@resources" "~@setuid" "~@timer" "~@privileged"
+ ];
+ SystemCallArchitectures = "native";
+ SystemCallErrorNumber = "EPERM";
+ };
in
{
- options = {
- services.public-inbox = {
- enable = mkEnableOption "the public-inbox mail archiver";
-
- package = mkOption {
- type = types.package;
- default = pkgs.public-inbox;
+ 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.";
+ };
+ 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.
+ '';
+ default = {};
+ type = types.submodule {
+ freeformType = 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.";
+ };
+ options.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.";
+ };
+ options.description = mkOption {
+ type = types.str;
+ example = "user/dev discussion of public-inbox itself";
+ description = "User-visible description for the repository.";
+ };
+ options.newsgroup = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ description = "NNTP group name for the inbox.";
+ };
+ options.watch = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = "Paths for ${manref "public-inbox-watch" 1} to monitor for new mail.";
+ example = [ "maildir:/path/to/test.example.com.git" ];
+ };
+ options.watchheader = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "List-Id:<test@example.com>";
+ description = ''
+ If specified, ${manref "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.";
+ };
+ }));
+ };
+ };
+ mda = {
+ enable = mkEnableOption "the public-inbox Mail Delivery Agent";
+ args = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = "Command-line arguments to pass to ${manref "public-inbox-mda" 1}.";
+ };
+ };
+ http = {
+ enable = mkEnableOption "the public-inbox HTTP server";
+ mounts = mkOption {
+ type = with types; listOf str;
+ default = [ "/" ];
+ example = [ "/lists/archives" ];
description = ''
- public-inbox package to use with the public-inbox module
+ Root paths or URLs that public-inbox will be served on.
+ If domain parts are present, only requests to those
+ domains will be accepted.
'';
};
-
- path = mkOption {
- type = with types; listOf package;
- default = [];
- example = literalExample "with pkgs; [ spamassassin ]";
+ args = mkOption {
+ type = with types; listOf str;
+ default = ["-W0"];
+ description = "Command-line arguments to pass to ${manref "public-inbox-httpd" 1}.";
+ };
+ port = mkOption {
+ type = with types; nullOr (either str port);
+ default = 80;
+ example = "/run/public-inbox-httpd.sock";
description = ''
- Additional packages to place in the path of public-inbox-mda,
- public-inbox-watch, etc.
+ Listening port or systemd's ListenStream= entry
+ to be used as a reverse proxy, eg. in nginx:
+ <code>locations."/inbox".proxyPass = "http://unix:''${config.services.public-inbox.http.port}:/inbox";</code>
+ Set to null and use <code>systemd.sockets.public-inbox-httpd.listenStreams</code>
+ if you need a more advanced listening.
'';
};
-
- inboxes = mkOption {
+ };
+ imap = {
+ enable = mkEnableOption "the public-inbox IMAP server";
+ args = mkOption {
+ type = with types; listOf str;
+ default = ["-W0"];
+ description = "Command-line arguments to pass to ${manref "public-inbox-imapd" 1}.";
+ };
+ port = mkOption {
+ type = with types; nullOr port;
+ default = 993;
description = ''
- Inboxes to configure, where attribute names are inbox names
+ Listening port.
+ Set to null and use <code>systemd.sockets.public-inbox-imapd.listenStreams</code>
+ if you need a more advanced listening.
'';
- 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
- '';
+ };
+ 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.";
+ };
+ 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.";
+ };
+ };
+ nntp = {
+ enable = mkEnableOption "the public-inbox NNTP server";
+ port = mkOption {
+ type = with types; nullOr port;
+ default = 563;
+ description = ''
+ Listening port.
+ Set to null and use <code>systemd.sockets.public-inbox-nntpd.listenStreams</code>
+ if you need a more advanced listening.
+ '';
+ };
+ args = mkOption {
+ type = with types; listOf str;
+ default = ["-W0"];
+ description = "Command-line arguments to pass to ${manref "public-inbox-nntpd" 1}.";
+ };
+ 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.";
+ };
+ };
+ spamAssassinRules = mkOption {
+ type = with types; nullOr path;
+ default = "${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.";
+ default = {};
+ type = types.submodule {
+ freeformType = gitIni.type;
+ options.publicinbox = mkOption {
+ default = {};
+ description = "public-inbox configuration.";
+ type = types.submodule {
+ freeformType = iniAttrs;
+ options.css = mkOption {
+ type = with types; listOf str;
+ default = [];
+ description = "The local path name of a CSS file for the PSGI web interface.";
};
-
- description = mkOption {
- type = str;
- example = "user/dev discussion of public-inbox itself";
- description = ''
- User-visible description for the repository
- '';
+ 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";
};
-
- config = mkOption {
- type = attrs;
- default = {};
+ options.wwwlisting = mkOption {
+ type = with types; enum [ "all" "404" "match=domain" ];
+ default = "404";
description = ''
- Additional structured config for the inbox
+ Controls which lists (if any) are listed for when the root
+ public-inbox URL is accessed over HTTP.
'';
};
-
- newsgroup = mkOption {
- type = nullOr str;
- default = null;
+ };
+ };
+ options.publicinboxmda = mkOption {
+ default = {};
+ description = "mailbox delivery agent";
+ type = types.submodule {
+ freeformType = iniAttrs;
+ options.spamcheck = mkOption {
+ type = with types; enum [ "spamc" "none" ];
+ default = "none";
description = ''
- NNTP group name for the inbox
+ If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
+ using SpamAssassin.
'';
};
-
- watch = mkOption {
- type = listOf str;
- default = [];
+ };
+ };
+ options.publicinboxwatch = mkOption {
+ default = {};
+ description = "mailbox watcher";
+ type = types.submodule {
+ freeformType = iniAttrs;
+ options.spamcheck = mkOption {
+ type = with types; enum [ "spamc" "none" ];
+ default = "none";
description = ''
- Paths for public-inbox-watch(1) to monitor for new mail
+ If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
+ using SpamAssassin.
'';
- example = [ "maildir:/path/to/test.example.com.git" ];
};
-
- watchHeader = mkOption {
- type = nullOr str;
+ options.watchspam = mkOption {
+ type = with types; nullOr str;
default = null;
- example = "List-Id:<test@example.com>";
+ example = "maildir:/path/to/spam";
description = ''
- If specified, public-inbox-watch(1) will only process
- mail containing a matching header.
+ If set, mail in this maildir will be trained as spam and
+ deleted from all watched inboxes
'';
};
};
- });
- };
-
- 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
- '';
- };
- };
-
- 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.
- '';
- };
-
- 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.submodule {
+ freeformType = types.attrsOf (types.submodule {
+ freeformType = types.either (types.attrsOf iniAtom) 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;
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;
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;
+ services.public-inbox.settings =
+ filterAttrsRecursive (n: v: v != null) {
+ publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes;
};
-
- 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;
- };
-
- 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";
+ users = {
+ users.public-inbox = {
+ # Use runCommand instead of linkFarm,
+ # because Postfix rejects .forward if it's a symlink.
+ home = pkgs.runCommand "public-inbox-home" {} (''
+ install -D -p ${environment.PI_CONFIG} $out/.public-inbox/config
+ ln -s ${stateDir}/emergency $out/.public-inbox/emergency
+ ln -s ${stateDir}/spamassassin $out/.spamassassin
+ '' + optionalString cfg.mda.enable ''
+ cp ${let env = concatStringsSep " " (mapAttrsToList (n: v: "${n}=${escapeShellArg v}") environment); in
+ pkgs.writeText "forward" ''
+ |"env ${env} PATH=\"${makeBinPath cfg.path}:$PATH\" ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
+ ''} $out/.forward
+ '');
+ group = "public-inbox";
+ isSystemUser = true;
+ };
+ groups.public-inbox = {};
};
-
- 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 \
+ networking.firewall = mkIf cfg.openFirewall
+ { allowedTCPPorts = mkMerge [
+ (mkIf (cfg.http.enable && types.port.check cfg.http.port) [ cfg.http.port ])
+ (mkIf (cfg.imap.enable && types.port.check cfg.imap.port) [ cfg.imap.port ])
+ (mkIf (cfg.nntp.enable && types.port.check cfg.nntp.port) [ cfg.nntp.port ])
+ ];
+ };
+ 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" ];
+ };
+ }
+ ) [ "http" "imap" "nntp" ]);
+ systemd.services = mkMerge [
+ (mkIf cfg.http.enable
+ { public-inbox-httpd = {
+ inherit environment;
+ after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
+ requires = [ "public-inbox-init.service" ];
+ serviceConfig = serviceConfig "httpd" // {
+ ExecStart = escapeShellArgs (
+ [ "${cfg.package}/bin/public-inbox-httpd" ] ++
+ cfg.http.args ++
+ [ (pkgs.writeText "public-inbox.psgi" ''
+ #!${cfg.package.fullperl} -w
+ 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}
+ }
+ '') ]
+ );
+ };
+ };
+ })
+ (mkIf cfg.imap.enable
+ { public-inbox-imapd = {
+ inherit environment;
+ after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
+ requires = [ "public-inbox-init.service" ];
+ serviceConfig = serviceConfig "imapd" // {
+ 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.nntp.enable
+ { public-inbox-nntpd = {
+ inherit environment;
+ after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
+ requires = [ "public-inbox-init.service" ];
+ serviceConfig = serviceConfig "nntpd" // {
+ 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 = {
+ inherit environment;
+ 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 = serviceConfig "watch" // {
+ ExecStart = "${cfg.package}/bin/public-inbox-watch";
+ ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+ };
+ };
+ })
+ ({ public-inbox-init = {
+ inherit environment;
+ wantedBy = [ "multi-user.target" ];
+ restartIfChanged = true;
+ restartTriggers = [ environment.PI_CONFIG ];
+ script = ''
+ set -ux
+ ${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 (inboxPath name) url ] ++ address)}
+ ${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" inbox.url ] ++ inbox.address)}
- rm -rf $conf_dir
- fi
+ rm -rf $conf_dir
+ fi
- ln -sf ${descriptionFile inbox} ${inboxPath name}/description
+ ln -sf ${pkgs.writeText "description" inbox.description} \
+ ${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} \
- config core.sharedRepository 0640
- fi
- '') cfg.inboxes)}
+ 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)}
- for inbox in /var/lib/public-inbox/inboxes/*/; do
- ls -1 "$inbox" | grep -q '^xap' && continue
+ shopt -s nullglob
+ for inbox in ${stateDir}/inboxes/*/; do
+ ls -1 "$inbox" | ${pkgs.gnugrep}/bin/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.
${cfg.package}/bin/public-inbox-index "$inbox"
- done
-
- '';
-
+ done
+ '';
+ serviceConfig = serviceConfig "init" // {
+ Type = "oneshot";
+ RemainAfterExit = true;
+ StateDirectory = [
+ "public-inbox"
+ "public-inbox/emergency"
+ "public-inbox/inboxes"
+ ];
+ StateDirectoryMode = "0750";
+ };
+ };
+ })
+ ];
environment.systemPackages = with pkgs; [ cfg.package ];
-
};
+ meta.maintainers = with lib.maintainers; [ julm ];
}