{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.services.public-inbox;
stateDir = "/var/lib/public-inbox";
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 = "${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 ${stateDir}/spamassassin $out/.spamassassin
cp $forwardPath $out/.forward
install -D -p ${environment.PI_CONFIG} $out/.public-inbox/config
'';
psgi = pkgs.writeText "public-inbox.psgi" ''
#!${cfg.package.fullperl} -w
# Copyright (C) 2014-2019 all contributors
# License: GPL-3.0+
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}
}
'';
enableWatch = any (i: i.watch != []) (attrValues cfg.inboxes)
|| cfg.settings.publicinboxwatch.watchspam != null;
useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" ||
cfg.settings.publicinboxwatch.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
'';
};
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.mainrepo = mkOption {
type = types.str;
default = "${stateDir}/inboxes/${name}";
};
options.address = mkOption {
type = with types; listOf str;
example = "example-discuss@example.org";
};
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 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:";
description = ''
If specified, 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 = {
args = mkOption {
type = with types; listOf str;
default = [];
description = ''
Command-line arguments to pass to public-inbox-mda(1).
'';
};
};
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
'';
};
args = mkOption {
type = with types; listOf str;
default = ["-W0"];
description = ''
Command-line arguments to pass to public-inbox-httpd(1).
'';
};
};
imap = {
listenStreams = mkOption {
type = with types; listOf str;
default = [ "0.0.0.0:993" ];
description = ''
systemd.socket(5) ListenStream values for the
public-inbox-imapd service to listen on
'';
};
args = mkOption {
type = with types; listOf str;
default = ["-W0"];
description = ''
Command-line arguments to pass to public-inbox-imapd(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 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 = {
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
'';
};
args = mkOption {
type = with types; listOf str;
default = ["-W0"];
description = ''
Command-line arguments to pass to 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 = [];
};
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.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.
'';
};
};
};
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 = ''
If set to spamc, public-inbox-mda(1) will filter spam
using SpamAssassin.
'';
};
};
};
options.publicinboxwatch = mkOption {
default = {};
description = "mailbox watcher";
type = types.submodule {
freeformType = iniAttrs;
options.spamcheck = mkOption {
type = with types; enum [ "spamc" "none" ];
default = "none";
description = ''
If set to spamc, public-inbox-watch(1) will filter spam
using SpamAssassin.
'';
};
options.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
'';
};
};
};
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";
};
});
};
};
};
};
};
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.settings.publicinboxmda.spamcheck and
services.public-inbox.settings.publicinboxwatch.spamcheck to null.
'';
}
{ 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.settings.publicinboxmda.spamcheck and
services.public-inbox.settings.publicinboxwatch.spamcheck to null.
'';
}
];
services.public-inbox.settings =
filterAttrsRecursive (n: v: v != null) {
publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes;
};
users = {
users.public-inbox = {
inherit home;
group = "public-inbox";
isSystemUser = true;
};
groups.public-inbox = {};
};
systemd.sockets = {
public-inbox-httpd = {
inherit (cfg.http) listenStreams;
wantedBy = [ "sockets.target" ];
};
public-inbox-imapd = {
inherit (cfg.imap) listenStreams;
wantedBy = [ "sockets.target" ];
};
public-inbox-nntpd = {
inherit (cfg.nntp) listenStreams;
wantedBy = [ "sockets.target" ];
};
};
systemd.services = {
public-inbox-httpd = {
inherit (environment);
after = [ "public-inbox-watch.service" ];
serviceConfig = {
ExecStart = escapeShellArgs (
[ "${cfg.package}/bin/public-inbox-httpd" psgi ] ++
cfg.http.args
);
NonBlocking = true;
DynamicUser = true;
Group = "public-inbox";
};
};
public-inbox-imapd = {
inherit environment;
after = [ "public-inbox-watch.service" ];
#environment.PERL_INLINE_DIRECTORY = "/tmp/.pub-inline";
#environment.LimitNOFILE = 30000;
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 ]
);
# NonBlocking is REQUIRED to avoid a race condition
# if running simultaneous services
NonBlocking = true;
DynamicUser = true;
Group = "public-inbox";
};
};
public-inbox-nntpd = {
inherit environment;
after = [ "public-inbox-watch.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 ]
);
NonBlocking = true;
DynamicUser = true;
Group = "public-inbox";
};
};
public-inbox-watch = {
inherit environment;
inherit (cfg) path;
after = optional (cfg.settings.publicinboxwatch.spamcheck == "spamc") "spamassassin.service";
wantedBy = optional enableWatch "multi-user.target";
serviceConfig = {
ExecStart = "${cfg.package}/bin/public-inbox-watch";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
User = "public-inbox";
Group = "public-inbox";
StateDirectory = [
"public-inbox/emergency"
"public-inbox/inboxes"
];
StateDirectoryMode = "0750";
PrivateTmp = true;
};
preStart = ''
${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)"
env 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 ${pkgs.writeText "description" inbox.description} ${stateDir}/inboxes/"${escapeShellArg name}"/description
git=${stateDir}/inboxes/"${escapeShellArg name}"/all.git
if [ -d "$git" ]; then
# Config is inherited by each epoch repository,
# so just needs to be set for all.git.
${pkgs.git}/bin/git --git-dir "$git" \
config core.sharedRepository 0640
fi
'') cfg.inboxes)}
for inbox in ${stateDir}/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.
env ${concatStringsSep " " envList} \
${cfg.package}/bin/public-inbox-index "$inbox"
done
'';
};
};
environment.systemPackages = with pkgs; [ cfg.package ];
};
meta.maintainers = with lib.maintainers; [ julm ];
}