diff options
author | Silvan Mosberger <contact@infinisil.com> | 2022-05-12 20:46:39 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-12 20:46:39 +0200 |
commit | fd5082695297c4ab4e551ff771b8fe9e2f110d0c (patch) | |
tree | 191d6d050ab5150c14529c2d85465bc21a4d0ec9 /nixos | |
parent | 914c76f1004c6e0770e6129b6c2b3673fb2c887f (diff) | |
parent | fd2616c92c81ba72ac72e433b2e3a055e324a97d (diff) |
Merge pull request #104457 from ju1m/public-inbox
Update public-inbox to 1.8.0 and add systemd services
Diffstat (limited to 'nixos')
-rw-r--r-- | nixos/modules/module-list.nix | 1 | ||||
-rw-r--r-- | nixos/modules/services/mail/public-inbox.nix | 579 | ||||
-rw-r--r-- | nixos/tests/all-tests.nix | 1 | ||||
-rw-r--r-- | nixos/tests/public-inbox.nix | 227 |
4 files changed, 808 insertions, 0 deletions
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index e0d1c03793583..84309f90132f1 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.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 diff --git a/nixos/modules/services/mail/public-inbox.nix b/nixos/modules/services/mail/public-inbox.nix new file mode 100644 index 0000000000000..0f9bc4ef226d2 --- /dev/null +++ b/nixos/modules/services/mail/public-inbox.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 ]; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0c085b64efa90..c00f7829ac67e 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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 {}; diff --git a/nixos/tests/public-inbox.nix b/nixos/tests/public-inbox.nix new file mode 100644 index 0000000000000..7de40400fcbf5 --- /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: <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.'") + ''; +}) |