Merge pull request #104457 from ju1m/public-inbox

Update public-inbox to 1.8.0 and add systemd services
main
Silvan Mosberger 2 years ago committed by GitHub
commit fd50826952
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      nixos/modules/module-list.nix
  2. 579
      nixos/modules/services/mail/public-inbox.nix
  3. 1
      nixos/tests/all-tests.nix
  4. 227
      nixos/tests/public-inbox.nix
  5. 11
      pkgs/pkgs-lib/formats.nix
  6. 172
      pkgs/servers/mail/public-inbox/0002-msgtime-drop-Date-Parse-for-RFC2822.patch
  7. 143
      pkgs/servers/mail/public-inbox/default.nix

@ -505,6 +505,7 @@
./services/mail/postfixadmin.nix
./services/mail/postsrsd.nix
./services/mail/postgrey.nix
./services/mail/public-inbox.nix
./services/mail/spamassassin.nix
./services/mail/rspamd.nix
./services/mail/rss2email.nix

@ -0,0 +1,579 @@
{ lib, pkgs, config, ... }:
with lib;
let
cfg = config.services.public-inbox;
stateDir = "/var/lib/public-inbox";
manref = name: vol: "<citerefentry><refentrytitle>${name}</refentrytitle><manvolnum>${toString vol}</manvolnum></citerefentry>";
gitIni = pkgs.formats.gitIni { listsAsDuplicateKeys = true; };
iniAtom = elemAt gitIni.type/*attrsOf*/.functor.wrapped/*attrsOf*/.functor.wrapped/*either*/.functor.wrapped 0;
useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" ||
cfg.settings.publicinboxwatch.spamcheck == "spamc";
publicInboxDaemonOptions = proto: defaultPort: {
args = mkOption {
type = with types; listOf str;
default = [];
description = "Command-line arguments to pass to ${manref "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 <code>systemd.sockets.public-inbox-${proto}d.listenStreams</code>
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 ${manref "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 ${manref "public-inbox-${proto}d" 1}.";
};
};
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 ++
# Without confinement the whole Nix store
# is made available to the service
optionals (!config.systemd.services."public-inbox-${srv}".confinement.enable) [
"${pkgs.dash}/bin/dash:/bin/sh"
builtins.storeDir
];
# 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 = mkDefault true;
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";
# The following options are redundant when confinement is enabled
RootDirectory = "/var/empty";
TemporaryFileSystem = "/";
PrivateMounts = true;
MountAPIVFS = true;
PrivateDevices = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectControlGroups = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
};
confinement = {
# Until we agree upon doing it directly here in NixOS
# https://github.com/NixOS/nixpkgs/pull/104457#issuecomment-1115768447
# let the user choose to enable the confinement with:
# systemd.services.public-inbox-httpd.confinement.enable = true;
# systemd.services.public-inbox-imapd.confinement.enable = true;
# systemd.services.public-inbox-init.confinement.enable = true;
# systemd.services.public-inbox-nntpd.confinement.enable = true;
#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
{
options.services.public-inbox = {
enable = mkEnableOption "the public-inbox mail archiver";
package = mkOption {
type = types.package;
default = pkgs.public-inbox;
defaultText = literalExpression "pkgs.public-inbox";
description = "public-inbox package to use.";
};
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.";
};
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.";
apply = pkgs.writeText "public-inbox-description-${name}";
};
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.";
};
}));
};
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.
'';
};
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:
<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.
'';
};
};
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}.";
};
};
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 <link xlink:href="https://public-inbox.org/public-inbox-config.html">public-inbox config file</link>.
'';
default = {};
type = types.submodule {
freeformType = gitIni.type;
options.publicinbox = mkOption {
default = {};
description = "public inboxes";
type = types.submodule {
freeformType = with types; /*inbox name*/attrsOf (/*inbox option name*/attrsOf /*inbox option value*/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.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.spamcheck = mkOption {
type = with types; enum [ "spamc" "none" ];
default = "none";
description = ''
If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
using SpamAssassin.
'';
};
options.publicinboxwatch.spamcheck = mkOption {
type = with types; enum [ "spamc" "none" ];
default = "none";
description = ''
If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
using SpamAssassin.
'';
};
options.publicinboxwatch.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.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";
};
});
};
};
};
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.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 = {
home = stateDir;
group = "public-inbox";
isSystemUser = true;
};
groups.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);
# Deliver the addresses with the public-inbox transport
transport =
concatStringsSep "\n" (mapAttrsToList (_: inbox:
concatMapStringsSep "\n" (address:
"${address} public-inbox:${address}"
) inbox.address
) cfg.inboxes);
# 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}"
];
};
};
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 = {
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
) + ''
shopt -s nullglob
for inbox in ${stateDir}/inboxes/*/; do
# 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.
ls -1 "$inbox" | grep -q '^xap' ||
${cfg.package}/bin/public-inbox-index "$inbox"
done
'';
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 ];
}

@ -456,6 +456,7 @@ in
proxy = handleTest ./proxy.nix {};
prowlarr = handleTest ./prowlarr.nix {};
pt2-clone = handleTest ./pt2-clone.nix {};
public-inbox = handleTest ./public-inbox.nix {};
pulseaudio = discoverTests (import ./pulseaudio.nix);
qboot = handleTestOn ["x86_64-linux" "i686-linux"] ./qboot.nix {};
quorum = handleTest ./quorum.nix {};

@ -0,0 +1,227 @@
import ./make-test-python.nix ({ pkgs, lib, ... }:
let
orga = "example";
domain = "${orga}.localdomain";
tls-cert = pkgs.runCommand "selfSignedCert" { buildInputs = [ pkgs.openssl ]; } ''
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -nodes -days 36500 \
-subj '/CN=machine.${domain}'
install -D -t $out key.pem cert.pem
'';
in
{
name = "public-inbox";
meta.maintainers = with pkgs.lib.maintainers; [ julm ];
machine = { config, pkgs, nodes, ... }: let
inherit (config.services) gitolite public-inbox;
# Git repositories paths in Gitolite.
# Only their baseNameOf is used for configuring public-inbox.
repositories = [
"user/repo1"
"user/repo2"
];
in
{
virtualisation.diskSize = 1 * 1024;
virtualisation.memorySize = 1 * 1024;
networking.domain = domain;
security.pki.certificateFiles = [ "${tls-cert}/cert.pem" ];
# If using security.acme:
#security.acme.certs."${domain}".postRun = ''
# systemctl try-restart public-inbox-nntpd public-inbox-imapd
#'';
services.public-inbox = {
enable = true;
postfix.enable = true;
openFirewall = true;
settings.publicinbox = {
css = [ "href=https://machine.${domain}/style/light.css" ];
nntpserver = [ "nntps://machine.${domain}" ];
wwwlisting = "match=domain";
};
mda = {
enable = true;
args = [ "--no-precheck" ]; # Allow Bcc:
};
http = {
enable = true;
port = "/run/public-inbox-http.sock";
#port = 8080;
args = ["-W0"];
mounts = [
"https://machine.${domain}/inbox"
];
};
nntp = {
enable = true;
#port = 563;
args = ["-W0"];
cert = "${tls-cert}/cert.pem";
key = "${tls-cert}/key.pem";
};
imap = {
enable = true;
#port = 993;
args = ["-W0"];
cert = "${tls-cert}/cert.pem";
key = "${tls-cert}/key.pem";
};
inboxes = lib.recursiveUpdate (lib.genAttrs (map baseNameOf repositories) (repo: {
address = [
# Routed to the "public-inbox:" transport in services.postfix.transport
"${repo}@${domain}"
];
description = ''
${repo}@${domain} :
discussions about ${repo}.
'';
url = "https://machine.${domain}/inbox/${repo}";
newsgroup = "inbox.comp.${orga}.${repo}";
coderepo = [ repo ];
}))
{
repo2 = {
hide = [
"imap" # FIXME: doesn't work for IMAP as of public-inbox 1.6.1
"manifest"
"www"
];
};
};
settings.coderepo = lib.listToAttrs (map (path: lib.nameValuePair (baseNameOf path) {
dir = "/var/lib/gitolite/repositories/${path}.git";
cgitUrl = "https://git.${domain}/${path}.git";
}) repositories);
};
# Use gitolite to store Git repositories listed in coderepo entries
services.gitolite = {
enable = true;
adminPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJmoTOQnGqX+//us5oye8UuE+tQBx9QEM7PN13jrwgqY root@localhost";
};
systemd.services.public-inbox-httpd = {
serviceConfig.SupplementaryGroups = [ gitolite.group ];
};
# Use nginx as a reverse proxy for public-inbox-httpd
services.nginx = {
enable = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
recommendedTlsSettings = true;
recommendedProxySettings = true;
virtualHosts."machine.${domain}" = {
forceSSL = true;
sslCertificate = "${tls-cert}/cert.pem";
sslCertificateKey = "${tls-cert}/key.pem";
locations."/".return = "302 /inbox";
locations."= /inbox".return = "302 /inbox/";
locations."/inbox".proxyPass = "http://unix:${public-inbox.http.port}:/inbox";
# If using TCP instead of a Unix socket:
#locations."/inbox".proxyPass = "http://127.0.0.1:${toString public-inbox.http.port}/inbox";
# Referred to by settings.publicinbox.css
# See http://public-inbox.org/meta/_/text/color/
locations."= /style/light.css".alias = pkgs.writeText "light.css" ''
* { background:#fff; color:#000 }
a { color:#00f; text-decoration:none }
a:visited { color:#808 }
*.q { color:#008 }
*.add { color:#060 }
*.del {color:#900 }
*.head { color:#000 }
*.hunk { color:#960 }
.hl.num { color:#f30 } /* number */
.hl.esc { color:#f0f } /* escape character */
.hl.str { color:#f30 } /* string */
.hl.ppc { color:#c3c } /* preprocessor */
.hl.pps { color:#f30 } /* preprocessor string */
.hl.slc { color:#099 } /* single-line comment */
.hl.com { color:#099 } /* multi-line comment */
/* .hl.opt { color:#ccc } */ /* operator */
/* .hl.ipl { color:#ccc } */ /* interpolation */
/* keyword groups kw[a-z] */
.hl.kwa { color:#f90 }
.hl.kwb { color:#060 }
.hl.kwc { color:#f90 }
/* .hl.kwd { color:#ccc } */
'';
};
};
services.postfix = {
enable = true;
setSendmail = true;
#sslCert = "${tls-cert}/cert.pem";
#sslKey = "${tls-cert}/key.pem";
recipientDelimiter = "+";
};
environment.systemPackages = [
pkgs.mailutils
pkgs.openssl
];
};
testScript = ''
start_all()
machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("public-inbox-init.service")
# Very basic check that Gitolite can work;
# Gitolite is not needed for the rest of this testScript
machine.wait_for_unit("gitolite-init.service")
# List inboxes through public-inbox-httpd
machine.wait_for_unit("nginx.service")
machine.succeed("curl -L https://machine.${domain} | grep repo1@${domain}")
# The repo2 inbox is hidden
machine.fail("curl -L https://machine.${domain} | grep repo2@${domain}")
machine.wait_for_unit("public-inbox-httpd.service")
# Send a mail and read it through public-inbox-httpd
# Must work too when using a recipientDelimiter.
machine.wait_for_unit("postfix.service")
machine.succeed("mail -t <${pkgs.writeText "mail" ''
Subject: Testing mail
From: root@localhost
To: repo1+extension@${domain}
Message-ID: <repo1@root-1>
Content-Type: text/plain; charset=utf-8
Content-Disposition: inline
This is a testing mail.
''}")
machine.sleep(5)
machine.succeed("curl -L 'https://machine.${domain}/inbox/repo1/repo1@root-1/T/#u' | grep 'This is a testing mail.'")
# Read a mail through public-inbox-imapd
machine.wait_for_open_port(993)
machine.wait_for_unit("public-inbox-imapd.service")
machine.succeed("openssl s_client -ign_eof -crlf -connect machine.${domain}:993 <${pkgs.writeText "imap-commands" ''
tag login anonymous@${domain} anonymous
tag SELECT INBOX.comp.${orga}.repo1.0
tag FETCH 1 (BODY[HEADER])
tag LOGOUT
''} | grep '^Message-ID: <repo1@root-1>'")
# TODO: Read a mail through public-inbox-nntpd
#machine.wait_for_open_port(563)
#machine.wait_for_unit("public-inbox-nntpd.service")
# Delete a mail.
# Note that the use of an extension not listed in the addresses
# require to use --all
machine.succeed("curl -L https://machine.example.localdomain/inbox/repo1/repo1@root-1/raw | sudo -u public-inbox public-inbox-learn rm --all")
machine.fail("curl -L https://machine.example.localdomain/inbox/repo1/repo1@root-1/T/#u | grep 'This is a testing mail.'")
'';
})

@ -135,6 +135,17 @@ rec {
};
gitIni = { listsAsDuplicateKeys ? false, ... }@args: {
type = with lib.types; let
iniAtom = (ini args).type/*attrsOf*/.functor.wrapped/*attrsOf*/.functor.wrapped;
in attrsOf (attrsOf (either iniAtom (attrsOf iniAtom)));
generate = name: value: pkgs.writeText name (lib.generators.toGitINI value);
};
toml = {}: json {} // {
type = with lib.types; let
valueType = oneOf [

@ -1,172 +0,0 @@
From c9b5164c954cd0de80d971f1c4ced16bf41ea81b Mon Sep 17 00:00:00 2001
From: Eric Wong <e@80x24.org>
Date: Fri, 29 Nov 2019 12:25:07 +0000
Subject: [PATCH 2/2] msgtime: drop Date::Parse for RFC2822
Date::Parse is not optimized for RFC2822 dates and isn't
packaged on OpenBSD. It's still useful for historical
email when email clients were less conformant, but is
less relevant for new emails.
---
lib/PublicInbox/MsgTime.pm | 115 ++++++++++++++++++++++++++++++++-----
t/msgtime.t | 6 ++
2 files changed, 107 insertions(+), 14 deletions(-)
diff --git a/lib/PublicInbox/MsgTime.pm b/lib/PublicInbox/MsgTime.pm
index 58e11d72..e9b27a49 100644
--- a/lib/PublicInbox/MsgTime.pm
+++ b/lib/PublicInbox/MsgTime.pm
@@ -7,24 +7,114 @@ use strict;
use warnings;
use base qw(Exporter);
our @EXPORT_OK = qw(msg_timestamp msg_datestamp);
-use Date::Parse qw(str2time strptime);
+use Time::Local qw(timegm);
+my @MoY = qw(january february march april may june
+ july august september october november december);
+my %MoY;
+@MoY{@MoY} = (0..11);
+@MoY{map { substr($_, 0, 3) } @MoY} = (0..11);
+
+my %OBSOLETE_TZ = ( # RFC2822 4.3 (Obsolete Date and Time)
+ EST => '-0500', EDT => '-0400',
+ CST => '-0600', CDT => '-0500',
+ MST => '-0700', MDT => '-0600',
+ PST => '-0800', PDT => '-0700',
+ UT => '+0000', GMT => '+0000', Z => '+0000',
+
+ # RFC2822 states:
+ # The 1 character military time zones were defined in a non-standard
+ # way in [RFC822] and are therefore unpredictable in their meaning.
+);
+my $OBSOLETE_TZ = join('|', keys %OBSOLETE_TZ);
sub str2date_zone ($) {
my ($date) = @_;
+ my ($ts, $zone);
+
+ # RFC822 is most likely for email, but we can tolerate an extra comma
+ # or punctuation as long as all the data is there.
+ # We'll use '\s' since Unicode spaces won't affect our parsing.
+ # SpamAssassin ignores commas and redundant spaces, too.
+ if ($date =~ /(?:[A-Za-z]+,?\s+)? # day-of-week
+ ([0-9]+),?\s+ # dd
+ ([A-Za-z]+)\s+ # mon
+ ([0-9]{2,})\s+ # YYYY or YY (or YYY :P)
+ ([0-9]+)[:\.] # HH:
+ ((?:[0-9]{2})|(?:\s?[0-9])) # MM
+ (?:[:\.]((?:[0-9]{2})|(?:\s?[0-9])))? # :SS
+ \s+ # a TZ offset is required:
+ ([\+\-])? # TZ sign
+ [\+\-]* # I've seen extra "-" e.g. "--500"
+ ([0-9]+|$OBSOLETE_TZ)(?:\s|$) # TZ offset
+ /xo) {
+ my ($dd, $m, $yyyy, $hh, $mm, $ss, $sign, $tz) =
+ ($1, $2, $3, $4, $5, $6, $7, $8);
+ # don't accept non-English months
+ defined(my $mon = $MoY{lc($m)}) or return;
+
+ if (defined(my $off = $OBSOLETE_TZ{$tz})) {
+ $sign = substr($off, 0, 1);
+ $tz = substr($off, 1);
+ }
+
+ # Y2K problems: 3-digit years, follow RFC2822
+ if (length($yyyy) <= 3) {
+ $yyyy += 1900;
+
+ # and 2-digit years from '09 (2009) (0..49)
+ $yyyy += 100 if $yyyy < 1950;
+ }
+
+ $ts = timegm($ss // 0, $mm, $hh, $dd, $mon, $yyyy);
- my $ts = str2time($date);
- return undef unless(defined $ts);
+ # Compute the time offset from [+-]HHMM
+ $tz //= 0;
+ my ($tz_hh, $tz_mm);
+ if (length($tz) == 1) {
+ $tz_hh = $tz;
+ $tz_mm = 0;
+ } elsif (length($tz) == 2) {
+ $tz_hh = 0;
+ $tz_mm = $tz;
+ } else {
+ $tz_hh = $tz;
+ $tz_hh =~ s/([0-9]{2})\z//;
+ $tz_mm = $1;
+ }
+ while ($tz_mm >= 60) {
+ $tz_mm -= 60;
+ $tz_hh += 1;
+ }
+ $sign //= '+';
+ my $off = $sign . ($tz_mm * 60 + ($tz_hh * 60 * 60));
+ $ts -= $off;
+ $sign = '+' if $off == 0;
+ $zone = sprintf('%s%02d%02d', $sign, $tz_hh, $tz_mm);
- # off is the time zone offset in seconds from GMT
- my ($ss,$mm,$hh,$day,$month,$year,$off) = strptime($date);
- return undef unless(defined $off);
+ # Time::Zone and Date::Parse are part of the same distibution,
+ # and we need Time::Zone to deal with tz names like "EDT"
+ } elsif (eval { require Date::Parse }) {
+ $ts = Date::Parse::str2time($date);
+ return undef unless(defined $ts);
- # Compute the time zone from offset
- my $sign = ($off < 0) ? '-' : '+';
- my $hour = abs(int($off / 3600));
- my $min = ($off / 60) % 60;
- my $zone = sprintf('%s%02d%02d', $sign, $hour, $min);
+ # off is the time zone offset in seconds from GMT
+ my ($ss,$mm,$hh,$day,$month,$year,$off) =
+ Date::Parse::strptime($date);
+ return undef unless(defined $off);
+
+ # Compute the time zone from offset
+ my $sign = ($off < 0) ? '-' : '+';
+ my $hour = abs(int($off / 3600));
+ my $min = ($off / 60) % 60;
+
+ $zone = sprintf('%s%02d%02d', $sign, $hour, $min);
+ } else {
+ warn "Date::Parse missing for non-RFC822 date: $date\n";
+ return undef;
+ }
+ # Note: we've already applied the offset to $ts at this point,
+ # but we want to keep "git fsck" happy.
# "-1200" is the furthest westermost zone offset,
# but git fast-import is liberal so we use "-1400"
if ($zone >= 1400 || $zone <= -1400) {
@@ -59,9 +149,6 @@ sub msg_date_only ($) {
my @date = $hdr->header_raw('Date');
my ($ts);
foreach my $d (@date) {
- # Y2K problems: 3-digit years
- $d =~ s!([A-Za-z]{3}) ([0-9]{3}) ([0-9]{2}:[0-9]{2}:[0-9]{2})!
- my $yyyy = $2 + 1900; "$1 $yyyy $3"!e;
$ts = eval { str2date_zone($d) } and return $ts;
if ($@) {
my $mid = $hdr->header_raw('Message-ID');
diff --git a/t/msgtime.t b/t/msgtime.t
index 6b396602..d9643b65 100644
--- a/t/msgtime.t
+++ b/t/msgtime.t
@@ -84,4 +84,10 @@ is_deeply(datestamp('Fri, 28 Jun 2002 12:54:40 -700'), [1025294080, '-0700']);
is_deeply(datestamp('Sat, 12 Jan 2002 12:52:57 -200'), [1010847177, '-0200']);
is_deeply(datestamp('Mon, 05 Nov 2001 10:36:16 -800'), [1004985376, '-0800']);
+# obsolete formats described in RFC2822
+for (qw(UT GMT Z)) {
+ is_deeply(datestamp('Fri, 02 Oct 1993 00:00:00 '.$_), [ 749520000, '+0000']);
+}
+is_deeply(datestamp('Fri, 02 Oct 1993 00:00:00 EDT'), [ 749534400, '-0400']);
+
done_testing();
--
2.24.1

@ -1,19 +1,73 @@
{ buildPerlPackage, lib, fetchurl, fetchpatch, makeWrapper
, DBDSQLite, EmailMIME, IOSocketSSL, IPCRun, Plack, PlackMiddlewareReverseProxy
, SearchXapian, TimeDate, URI
, git, highlight, openssl, xapian
{ stdenv, lib, fetchurl, makeWrapper, nixosTests
, buildPerlPackage
, coreutils
, curl
, git
, gnumake
, highlight
, libgit2
, man
, openssl
, pkg-config
, sqlite
, xapian
, AnyURIEscape
, DBDSQLite
, DBI
, EmailAddressXS
, EmailMIME
, IOSocketSSL
, IPCRun
, Inline
, InlineC
, LinuxInotify2
, MailIMAPClient
, ParseRecDescent
, Plack
, PlackMiddlewareReverseProxy
, SearchXapian
, TimeDate
, URI
}:
let
# These tests would fail, and produce "Operation not permitted"
# errors from git, because they use git init --shared. This tries
# to set the setgid bit, which isn't permitted inside build
# sandboxes.
#
# These tests were indentified with
# grep -r shared t/
skippedTests = [ "convert-compact" "search" "v2writable" "www_listing" ];
skippedTests = [
# These tests would fail, and produce "Operation not permitted"
# errors from git, because they use git init --shared. This tries
# to set the setgid bit, which isn't permitted inside build
# sandboxes.
#
# These tests were indentified with
# grep -r shared t/
"convert-compact" "search" "v2writable" "www_listing"
# perl5.32.0-public-inbox> t/eml.t ...................... 1/? Cannot parse parameter '=?ISO-8859-1?Q?=20charset=3D=1BOF?=' at t/eml.t line 270.
# perl5.32.0-public-inbox> # Failed test 'got wide character by assuming utf-8'
# perl5.32.0-public-inbox> # at t/eml.t line 272.
# perl5.32.0-public-inbox> Wide character in print at /nix/store/38vxlxrvg3yji3jms44qn94lxdysbj5j-perl-5.32.0/lib/perl5/5.32.0/Test2/Formatter/TAP.pm line 125.
"eml"
# Failed test 'Makefile OK'
# at t/hl_mod.t line 19.
# got: 'makefile'
# expected: 'make'
"hl_mod"
# Failed test 'clone + index v1 synced ->created_at'
# at t/lei-mirror.t line 175.
# got: '1638378723'
# expected: undef
# Failed test 'clone + index v1 synced ->created_at'
# at t/lei-mirror.t line 178.
# got: '1638378723'
# expected: undef
# May be due to the use of $ENV{HOME}.
"lei-mirror"
# Failed test 'child error (pure-Perl)'
# at t/spawn.t line 33.
# got: '0'
# expected: anything else
# waiting for child to reap grandchild...
"spawn"
];
testConditions = with lib;
concatMapStringsSep " " (n: "! -name ${escapeShellArg n}.t") skippedTests;
@ -22,53 +76,86 @@ in
buildPerlPackage rec {
pname = "public-inbox";
version = "1.2.0";
version = "1.8.0";
src = fetchurl {
url = "https://public-inbox.org/releases/public-inbox-${version}.tar.gz";
sha256 = "0sa2m4f2x7kfg3mi4im7maxqmqvawafma8f7g92nyfgybid77g6s";
url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-${version}.tar.gz";
sha256 = "sha256-laJOOCk5NecIGWesv4D30cLGfijQHVkeo55eNqNKzew=";
};
patches = [
(fetchpatch {
url = "https://public-inbox.org/meta/20200101032822.GA13063@dcvr/raw";
sha256 = "0ncxqqkvi5lwi8zaa7lk7l8mf8h278raxsvbvllh3z7jhfb48r3l";
})
./0002-msgtime-drop-Date-Parse-for-RFC2822.patch
];
outputs = [ "out" "devdoc" "sa_config" ];
postConfigure = ''
substituteInPlace Makefile --replace 'TEST_FILES = t/*.t' \
'TEST_FILES = $(shell find t -name *.t ${testConditions})'
substituteInPlace lib/PublicInbox/TestCommon.pm \
--replace /bin/cp ${coreutils}/bin/cp
'';
nativeBuildInputs = [ makeWrapper ];
buildInputs = [
DBDSQLite EmailMIME IOSocketSSL IPCRun Plack PlackMiddlewareReverseProxy
SearchXapian TimeDate URI highlight
AnyURIEscape
DBDSQLite
DBI
EmailAddressXS
EmailMIME
highlight
IOSocketSSL
IPCRun
Inline
InlineC
ParseRecDescent
Plack
PlackMiddlewareReverseProxy
SearchXapian
TimeDate
URI
libgit2 # For Gcf2
man
];
checkInputs = [ git openssl xapian ];
doCheck = !stdenv.isDarwin;
checkInputs = [
MailIMAPClient
curl
git
openssl
pkg-config
sqlite
xapian
] ++ lib.optionals stdenv.isLinux [
LinuxInotify2
];
preCheck = ''
perl certs/create-certs.perl
export TEST_LEI_ERR_LOUD=1
export HOME="$NIX_BUILD_TOP"/home
mkdir -p "$HOME"/.cache/public-inbox/inline-c
'';
installTargets = [ "install" ];
postInstall = ''
for prog in $out/bin/*; do
wrapProgram $prog --prefix PATH : ${lib.makeBinPath [ git ]}
wrapProgram $prog --prefix PATH : ${lib.makeBinPath [
git
/* for InlineC */
gnumake
stdenv.cc.cc
]}
done
mv sa_config $sa_config
'';
passthru.tests = {
nixos-public-inbox = nixosTests.public-inbox;
};
meta = with lib; {
homepage = "https://public-inbox.org/";
license = licenses.agpl3Plus;
maintainers = with maintainers; [ qyliss ];
maintainers = with maintainers; [ julm qyliss ];
platforms = platforms.all;
};
}

Loading…
Cancel
Save