From 9b6223d1fa2dcb23462a8451d7fafcff8616f16a Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Thu, 19 Nov 2020 23:23:20 +0100 Subject: [PATCH 1/7] public-inbox: 1.2.0 -> 1.6.1 --- ...-msgtime-drop-Date-Parse-for-RFC2822.patch | 172 ------------------ pkgs/servers/mail/public-inbox/default.nix | 103 ++++++++--- 2 files changed, 76 insertions(+), 199 deletions(-) delete mode 100644 pkgs/servers/mail/public-inbox/0002-msgtime-drop-Date-Parse-for-RFC2822.patch diff --git a/pkgs/servers/mail/public-inbox/0002-msgtime-drop-Date-Parse-for-RFC2822.patch b/pkgs/servers/mail/public-inbox/0002-msgtime-drop-Date-Parse-for-RFC2822.patch deleted file mode 100644 index ebc9a6f2237..00000000000 --- a/pkgs/servers/mail/public-inbox/0002-msgtime-drop-Date-Parse-for-RFC2822.patch +++ /dev/null @@ -1,172 +0,0 @@ -From c9b5164c954cd0de80d971f1c4ced16bf41ea81b Mon Sep 17 00:00:00 2001 -From: Eric Wong -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 - diff --git a/pkgs/servers/mail/public-inbox/default.nix b/pkgs/servers/mail/public-inbox/default.nix index affcb0e8b23..7d33984709b 100644 --- a/pkgs/servers/mail/public-inbox/default.nix +++ b/pkgs/servers/mail/public-inbox/default.nix @@ -1,19 +1,57 @@ -{ buildPerlPackage, lib, fetchurl, fetchpatch, makeWrapper -, DBDSQLite, EmailMIME, IOSocketSSL, IPCRun, Plack, PlackMiddlewareReverseProxy -, SearchXapian, TimeDate, URI -, git, highlight, openssl, xapian +{ stdenv, lib, fetchurl, makeWrapper +, gcc, git, gnumake, openssl, xapian +, buildPerlPackage +, coreutils +, curl +, git +, gnumake +, highlight +, libgit2 +, man +, openssl +, pkg-config +, sqlite +, xapian +, AnyURIEscape +, DBDSQLite +, DBI +, EmailAddressXS +, EmailMIME +, highlight +, IOSocketSSL +, IPCRun +, Inline +, InlineC +, 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" + ]; testConditions = with lib; concatMapStringsSep " " (n: "! -name ${escapeShellArg n}.t") skippedTests; @@ -22,21 +60,13 @@ in buildPerlPackage rec { pname = "public-inbox"; - version = "1.2.0"; + version = "1.6.1"; 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 = "0mr8f8qv15l0lx94im9084lmsw1qh8lzyb7mj1s5yhpw48k25709"; }; - 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 = '' @@ -47,8 +77,22 @@ buildPerlPackage rec { 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 ]; checkInputs = [ git openssl xapian ]; @@ -59,7 +103,12 @@ buildPerlPackage rec { 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 @@ -68,7 +117,7 @@ buildPerlPackage rec { meta = with lib; { homepage = "https://public-inbox.org/"; license = licenses.agpl3Plus; - maintainers = with maintainers; [ qyliss ]; + maintainers = with maintainers; [ qyliss julm ]; platforms = platforms.all; }; } From 8514800c42a2d292fcc81b6ecc9f0f10eef60868 Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Tue, 26 May 2020 20:54:52 +0200 Subject: [PATCH 2/7] nixos/public-inbox: init --- nixos/modules/module-list.nix | 1 + nixos/modules/services/mail/public-inbox.nix | 560 +++++++++++++++++++ pkgs/pkgs-lib/formats.nix | 11 + 3 files changed, 572 insertions(+) create mode 100644 nixos/modules/services/mail/public-inbox.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 4b2cb803e20..6e6cc9879ac 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -490,6 +490,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 diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix new file mode 100644 index 00000000000..039eae5670a --- /dev/null +++ b/nixos/modules/services/mail/public-inbox.nix @@ -0,0 +1,560 @@ +{ lib, pkgs, config, ... }: + +with lib; + +let + cfg = config.services.public-inbox; + stateDir = "/var/lib/public-inbox"; + + manref = name: vol: "${name}${toString vol}"; + + 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 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 ${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 { + # 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" + # For Inline::C + "/bin/sh" + ]; + 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 (!needNetwork); + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = mkDefault true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = 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"; + }; +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."; + }; + 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:"; + 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: + 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 ${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 public-inbox config file. + ''; + 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 = { + after = [ "public-inbox-init.service" "public-inbox-watch.service" ]; + requires = [ "public-inbox-init.service" ]; + serviceConfig = mkMerge [(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.http.enable + { public-inbox-httpd = { + after = [ "public-inbox-init.service" "public-inbox-watch.service" ]; + requires = [ "public-inbox-init.service" ]; + serviceConfig = mkMerge [(serviceConfig "httpd") { + 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 = { + after = [ "public-inbox-init.service" "public-inbox-watch.service" ]; + requires = [ "public-inbox-init.service" ]; + serviceConfig = mkMerge [(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 (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 = mkMerge [(serviceConfig "watch") { + 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 { + 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 ${pkgs.writeText "description" 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 = mkMerge [(serviceConfig "init") { + Type = "oneshot"; + RemainAfterExit = true; + StateDirectory = [ + "public-inbox" + "public-inbox/.public-inbox" + "public-inbox/.public-inbox/emergency" + "public-inbox/inboxes" + ]; + StateDirectoryMode = "0750"; + }]; + }; + }) + ]; + environment.systemPackages = with pkgs; [ cfg.package ]; + }; + meta.maintainers = with lib.maintainers; [ julm qyliss ]; +} diff --git a/pkgs/pkgs-lib/formats.nix b/pkgs/pkgs-lib/formats.nix index 5e17519d4ce..e6e6a95c1f4 100644 --- a/pkgs/pkgs-lib/formats.nix +++ b/pkgs/pkgs-lib/formats.nix @@ -123,6 +123,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 [ From 0e290442bae19e6a31511358e74d754f2b16041e Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Wed, 1 Dec 2021 11:06:18 +0100 Subject: [PATCH 3/7] nixos/public-inbox: add tests --- nixos/modules/services/mail/public-inbox.nix | 6 +- nixos/tests/all-tests.nix | 1 + nixos/tests/public-inbox.nix | 227 +++++++++++++++++++ 3 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 nixos/tests/public-inbox.nix diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix index 039eae5670a..99b5119c01a 100644 --- a/nixos/modules/services/mail/public-inbox.nix +++ b/nixos/modules/services/mail/public-inbox.nix @@ -355,9 +355,9 @@ in 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"])); + { 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. diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 940ae11ddd1..b69a55e8af7 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -412,6 +412,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 {}; diff --git a/nixos/tests/public-inbox.nix b/nixos/tests/public-inbox.nix new file mode 100644 index 00000000000..7de40400fcb --- /dev/null +++ b/nixos/tests/public-inbox.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: + 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: '") + + # 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.'") + ''; +}) From 6cf2f46995425cd1d837dbdd21da6c70a250c1a7 Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Wed, 1 Dec 2021 18:51:01 +0100 Subject: [PATCH 4/7] public-inbox: 1.6.1 -> 1.7.0 --- pkgs/servers/mail/public-inbox/default.nix | 59 +++++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/pkgs/servers/mail/public-inbox/default.nix b/pkgs/servers/mail/public-inbox/default.nix index 7d33984709b..d845d21cfd0 100644 --- a/pkgs/servers/mail/public-inbox/default.nix +++ b/pkgs/servers/mail/public-inbox/default.nix @@ -1,5 +1,4 @@ -{ stdenv, lib, fetchurl, makeWrapper -, gcc, git, gnumake, openssl, xapian +{ stdenv, lib, fetchurl, makeWrapper, nixosTests , buildPerlPackage , coreutils , curl @@ -17,11 +16,12 @@ , DBI , EmailAddressXS , EmailMIME -, highlight , IOSocketSSL , IPCRun , Inline , InlineC +, LinuxInotify2 +, MailIMAPClient , ParseRecDescent , Plack , PlackMiddlewareReverseProxy @@ -51,6 +51,31 @@ let # 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" + # TODO: reenable after https://public-inbox.org/meta/20211208010730.f47xxgzj53nwgvja@sourcephile.fr/T/#m38685d23bd686442d91a4890bac3c018d427b96b + # has been merged + "lei-sigpipe" + # Fails at least when TMPDIR is on ZFS + "lei_to_mail" + # Disabled after 1.7.0 + # https://public-inbox.org/public-inbox.git/commit/?id=7cb1f806dfa0173fb689048c56a755cb3874dcaf + "lei-watch" + "lei-auto-watch" ]; testConditions = with lib; @@ -60,11 +85,11 @@ in buildPerlPackage rec { pname = "public-inbox"; - version = "1.6.1"; + version = "1.7.0"; src = fetchurl { url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-${version}.tar.gz"; - sha256 = "0mr8f8qv15l0lx94im9084lmsw1qh8lzyb7mj1s5yhpw48k25709"; + sha256 = "sha256-hQSpmAFFVuPmXZvc7q6yP5Zhl86oar83OLYFn+42yMk="; }; outputs = [ "out" "devdoc" "sa_config" ]; @@ -72,6 +97,8 @@ buildPerlPackage rec { 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 ]; @@ -93,11 +120,25 @@ buildPerlPackage rec { SearchXapian TimeDate URI + libgit2 # For Gcf2 + man ]; - checkInputs = [ git openssl xapian ]; + checkInputs = [ + MailIMAPClient + curl + git + openssl + pkg-config + sqlite + xapian + ] ++ lib.optionals stdenv.isLinux [ + LinuxInotify2 + ]; preCheck = '' perl certs/create-certs.perl + export HOME="$NIX_BUILD_TOP"/home + mkdir -p "$HOME"/.cache/public-inbox/inline-c ''; installTargets = [ "install" ]; @@ -114,10 +155,14 @@ buildPerlPackage rec { 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 julm ]; + maintainers = with maintainers; [ julm qyliss ]; platforms = platforms.all; }; } From 8b2b5be3b5ff994ea33dce231feacf18960399ad Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Tue, 1 Mar 2022 03:52:03 +0100 Subject: [PATCH 5/7] public-inbox: 1.7.0 -> unstable-2022-04-05 --- pkgs/servers/mail/public-inbox/default.nix | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/pkgs/servers/mail/public-inbox/default.nix b/pkgs/servers/mail/public-inbox/default.nix index d845d21cfd0..7455159de9c 100644 --- a/pkgs/servers/mail/public-inbox/default.nix +++ b/pkgs/servers/mail/public-inbox/default.nix @@ -67,15 +67,6 @@ let # expected: anything else # waiting for child to reap grandchild... "spawn" - # TODO: reenable after https://public-inbox.org/meta/20211208010730.f47xxgzj53nwgvja@sourcephile.fr/T/#m38685d23bd686442d91a4890bac3c018d427b96b - # has been merged - "lei-sigpipe" - # Fails at least when TMPDIR is on ZFS - "lei_to_mail" - # Disabled after 1.7.0 - # https://public-inbox.org/public-inbox.git/commit/?id=7cb1f806dfa0173fb689048c56a755cb3874dcaf - "lei-watch" - "lei-auto-watch" ]; testConditions = with lib; @@ -85,11 +76,11 @@ in buildPerlPackage rec { pname = "public-inbox"; - version = "1.7.0"; + version = "unstable-2022-04-05"; src = fetchurl { - url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-${version}.tar.gz"; - sha256 = "sha256-hQSpmAFFVuPmXZvc7q6yP5Zhl86oar83OLYFn+42yMk="; + url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-961690bae47c90a4a6960952587c6f4463fb4b19.tar.gz"; + sha256 = "sha256-dvo+0kCZZQlXudtWyv8Npf3KhSC8pg95CmP7fMFqgww="; }; outputs = [ "out" "devdoc" "sa_config" ]; @@ -124,6 +115,7 @@ buildPerlPackage rec { man ]; + doCheck = !stdenv.isDarwin; checkInputs = [ MailIMAPClient curl @@ -137,6 +129,7 @@ buildPerlPackage rec { ]; 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 ''; From c646d375d3a22cad5e1557f852c3bc449e23b4ea Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Tue, 3 May 2022 01:31:03 +0200 Subject: [PATCH 6/7] nixos/public-inbox: support enabling confinement Add support for enabling confinement but does not enable it by default yet because so far no module within NixOS uses confinement hence that would set a precedent. --- nixos/modules/services/mail/public-inbox.nix | 209 ++++++++++--------- 1 file changed, 114 insertions(+), 95 deletions(-) diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix index 99b5119c01a..0f9bc4ef226 100644 --- a/nixos/modules/services/mail/public-inbox.nix +++ b/nixos/modules/services/mail/public-inbox.nix @@ -48,78 +48,98 @@ let let proto = removeSuffix "d" srv; needNetwork = builtins.hasAttr proto cfg && cfg.${proto}.port == null; in { - # 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" - # For Inline::C - "/bin/sh" - ]; - 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 (!needNetwork); - PrivateTmp = true; - PrivateUsers = true; - ProcSubset = "pid"; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = mkDefault true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = 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"; + 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 @@ -168,6 +188,7 @@ in 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; @@ -409,24 +430,24 @@ in ) [ "imap" "http" "nntp" ]); systemd.services = mkMerge [ (mkIf cfg.imap.enable - { public-inbox-imapd = { + { public-inbox-imapd = mkMerge [(serviceConfig "imapd") { after = [ "public-inbox-init.service" "public-inbox-watch.service" ]; requires = [ "public-inbox-init.service" ]; - serviceConfig = mkMerge [(serviceConfig "imapd") { + 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 = { + { public-inbox-httpd = mkMerge [(serviceConfig "httpd") { after = [ "public-inbox-init.service" "public-inbox-watch.service" ]; requires = [ "public-inbox-init.service" ]; - serviceConfig = mkMerge [(serviceConfig "httpd") { + serviceConfig = { ExecStart = escapeShellArgs ( [ "${cfg.package}/bin/public-inbox-httpd" ] ++ cfg.http.args ++ @@ -458,41 +479,41 @@ in } '') ] ); - }]; - }; + }; + }]; }) (mkIf cfg.nntp.enable - { public-inbox-nntpd = { + { public-inbox-nntpd = mkMerge [(serviceConfig "nntpd") { after = [ "public-inbox-init.service" "public-inbox-watch.service" ]; requires = [ "public-inbox-init.service" ]; - serviceConfig = mkMerge [(serviceConfig "nntpd") { + 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 = { + { 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 = mkMerge [(serviceConfig "watch") { + 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 { + in mkMerge [(serviceConfig "init") { wantedBy = [ "multi-user.target" ]; restartIfChanged = true; restartTriggers = [ PI_CONFIG ]; @@ -520,7 +541,7 @@ in rm -rf $conf_dir fi - ln -sf ${pkgs.writeText "description" inbox.description} \ + ln -sf ${inbox.description} \ ${stateDir}/inboxes/${escapeShellArg name}/description export GIT_DIR=${stateDir}/inboxes/${escapeShellArg name}/all.git @@ -540,18 +561,16 @@ in ${cfg.package}/bin/public-inbox-index "$inbox" done ''; - serviceConfig = mkMerge [(serviceConfig "init") { + serviceConfig = { Type = "oneshot"; RemainAfterExit = true; StateDirectory = [ - "public-inbox" "public-inbox/.public-inbox" "public-inbox/.public-inbox/emergency" "public-inbox/inboxes" ]; - StateDirectoryMode = "0750"; - }]; - }; + }; + }]; }) ]; environment.systemPackages = with pkgs; [ cfg.package ]; From fd2616c92c81ba72ac72e433b2e3a055e324a97d Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Fri, 6 May 2022 23:02:56 +0200 Subject: [PATCH 7/7] public-inbox: unstable-2022-04-05 -> 1.8.0 --- pkgs/servers/mail/public-inbox/default.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkgs/servers/mail/public-inbox/default.nix b/pkgs/servers/mail/public-inbox/default.nix index 7455159de9c..8ffbab1eac1 100644 --- a/pkgs/servers/mail/public-inbox/default.nix +++ b/pkgs/servers/mail/public-inbox/default.nix @@ -76,11 +76,11 @@ in buildPerlPackage rec { pname = "public-inbox"; - version = "unstable-2022-04-05"; + version = "1.8.0"; src = fetchurl { - url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-961690bae47c90a4a6960952587c6f4463fb4b19.tar.gz"; - sha256 = "sha256-dvo+0kCZZQlXudtWyv8Npf3KhSC8pg95CmP7fMFqgww="; + url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-${version}.tar.gz"; + sha256 = "sha256-laJOOCk5NecIGWesv4D30cLGfijQHVkeo55eNqNKzew="; }; outputs = [ "out" "devdoc" "sa_config" ];