about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.envrc3
-rw-r--r--flake.lock74
-rw-r--r--flake.nix2
-rw-r--r--hosts/by-name/server2/configuration.nix10
-rw-r--r--modules/by-name/ba/back/module.nix163
-rw-r--r--modules/by-name/dn/dns/dns/default.nix13
-rw-r--r--modules/by-name/dn/dns/dns/types/default.nix16
-rw-r--r--modules/by-name/dn/dns/dns/types/record.nix75
-rw-r--r--modules/by-name/dn/dns/dns/types/records/A.nix19
-rw-r--r--modules/by-name/dn/dns/dns/types/records/AAAA.nix19
-rw-r--r--modules/by-name/dn/dns/dns/types/records/CAA.nix42
-rw-r--r--modules/by-name/dn/dns/dns/types/records/CNAME.nix27
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DKIM.nix75
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DMARC.nix108
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DNAME.nix15
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DNSKEY.nix63
-rw-r--r--modules/by-name/dn/dns/dns/types/records/DS.nix48
-rw-r--r--modules/by-name/dn/dns/dns/types/records/HTTPS.nix5
-rw-r--r--modules/by-name/dn/dns/dns/types/records/MTA-STS.nix42
-rw-r--r--modules/by-name/dn/dns/dns/types/records/MX.nix32
-rw-r--r--modules/by-name/dn/dns/dns/types/records/NS.nix24
-rw-r--r--modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix18
-rw-r--r--modules/by-name/dn/dns/dns/types/records/PTR.nix92
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SOA.nix65
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SRV.nix51
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SSHFP.nix39
-rw-r--r--modules/by-name/dn/dns/dns/types/records/SVCB.nix100
-rw-r--r--modules/by-name/dn/dns/dns/types/records/TLSA.nix50
-rw-r--r--modules/by-name/dn/dns/dns/types/records/TXT.nix24
-rw-r--r--modules/by-name/dn/dns/dns/types/records/default.nix43
-rw-r--r--modules/by-name/dn/dns/dns/types/records/dnssec.nix48
-rw-r--r--modules/by-name/dn/dns/dns/types/simple.nix9
-rw-r--r--modules/by-name/dn/dns/dns/types/zone.nix119
-rw-r--r--modules/by-name/dn/dns/dns/util/default.nix76
-rw-r--r--modules/by-name/dn/dns/module.nix86
-rw-r--r--modules/by-name/re/redlib/module.nix11
-rw-r--r--modules/by-name/st/stalwart-mail/module.nix392
-rw-r--r--modules/by-name/st/stalwart-mail/settings.nix532
-rw-r--r--modules/by-name/sy/system-info/module.nix68
-rw-r--r--pkgs/by-name/ba/back/.envrc3
-rw-r--r--pkgs/by-name/ba/back/Cargo.lock1482
-rw-r--r--pkgs/by-name/ba/back/Cargo.toml26
-rw-r--r--pkgs/by-name/ba/back/README.md61
-rw-r--r--pkgs/by-name/ba/back/assets/style.css192
-rw-r--r--pkgs/by-name/ba/back/contrib/config.json5
-rw-r--r--pkgs/by-name/ba/back/flake.lock6
-rw-r--r--pkgs/by-name/ba/back/package.nix3
-rw-r--r--pkgs/by-name/ba/back/src/config/mod.rs132
-rw-r--r--pkgs/by-name/ba/back/src/error/mod.rs66
-rw-r--r--pkgs/by-name/ba/back/src/error/responder.rs23
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/dag/mod.rs14
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/format/mod.rs15
-rw-r--r--pkgs/by-name/ba/back/src/git_bug/issue/mod.rs2
-rw-r--r--pkgs/by-name/ba/back/src/main.rs33
-rw-r--r--pkgs/by-name/ba/back/src/web/generate/mod.rs225
-rw-r--r--pkgs/by-name/ba/back/src/web/issue_html.rs166
-rw-r--r--pkgs/by-name/ba/back/src/web/mod.rs297
-rw-r--r--pkgs/by-name/ba/back/src/web/prefix.rs35
-rw-r--r--pkgs/by-name/ba/back/src/web/responses.rs50
-rw-r--r--pkgs/by-name/ba/back/templates/issue.html57
-rw-r--r--pkgs/by-name/ba/back/templates/issues.html60
-rw-r--r--pkgs/by-name/ba/back/templates/repos.html47
-rwxr-xr-xpkgs/by-name/ba/back/update.sh2
-rw-r--r--pkgs/by-name/fe/fetchmail-common-name/package.nix15
-rw-r--r--pkgs/by-name/fe/fetchmail-common-name/patches/fix-socket.c-Correctly-check-the-common-name-even-if.patch40
-rw-r--r--pkgs/by-name/st/stalwart-mail-free/mail-send.nix20
-rw-r--r--pkgs/by-name/st/stalwart-mail-free/package.nix77
-rw-r--r--pkgs/by-name/st/stalwart-mail-free/patches/crates-Use-the-platform-CA-bundle-instead-of-the-bun.patch879
-rw-r--r--pkgs/by-name/st/stalwart-mail-free/patches/crates-main-Cargo.toml-Use-libre-features.patch25
-rw-r--r--pkgs/by-name/st/stalwart-mail-free/spam-filter.nix24
-rw-r--r--pkgs/default.nix3
-rwxr-xr-xscripts/get_dns.sh55
-rwxr-xr-xscripts/system_info.sh25
-rwxr-xr-xscripts/test_build.sh13
-rwxr-xr-xscripts/test_interactive.sh4
-rw-r--r--secrets.nix9
-rw-r--r--tests/by-name/ba/back/test.nix136
-rw-r--r--tests/by-name/dn/dns/test.nix129
-rwxr-xr-xtests/by-name/em/email-dns/nodes/acme/certs/generate66
-rwxr-xr-xtests/by-name/em/email-dns/nodes/acme/certs/generate.ca38
-rwxr-xr-xtests/by-name/em/email-dns/nodes/acme/certs/generate.client44
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem11
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem25
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template5
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem10
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem25
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template5
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix13
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/client.nix21
-rw-r--r--tests/by-name/em/email-dns/nodes/acme/default.nix114
-rw-r--r--tests/by-name/em/email-dns/nodes/mail_server.nix56
-rw-r--r--tests/by-name/em/email-dns/nodes/name_server.nix320
-rw-r--r--tests/by-name/em/email-dns/nodes/user.nix74
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age11
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/alice.com/public1
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age13
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/bob.com/public1
-rwxr-xr-xtests/by-name/em/email-dns/secrets/dkim/gen_key.sh33
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age10
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public1
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age13
-rw-r--r--tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public1
-rw-r--r--tests/by-name/em/email-dns/secrets/hostKey7
-rw-r--r--tests/by-name/em/email-dns/test.nix203
-rw-r--r--tests/by-name/em/email-ip/test.nix174
105 files changed, 6662 insertions, 1787 deletions
diff --git a/.envrc b/.envrc
index 189401d..71bc7ad 100644
--- a/.envrc
+++ b/.envrc
@@ -1,6 +1,9 @@
 #! /usr/bin/env sh
 
 use flake
+
+PATH_add ./scripts/
+
 if on_git_branch; then
     echo && git status --short --branch
 fi
diff --git a/flake.lock b/flake.lock
index 19b236c..277b4f5 100644
--- a/flake.lock
+++ b/flake.lock
@@ -12,11 +12,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1723293904,
-        "narHash": "sha256-b+uqzj+Wa6xgMS9aNbX4I+sXeb5biPDi39VgvSFqFvU=",
+        "lastModified": 1736955230,
+        "narHash": "sha256-uenf8fv2eG5bKM8C/UvFaiJMZ4IpUFaQxk9OH5t/1gA=",
         "owner": "ryantm",
         "repo": "agenix",
-        "rev": "f6291c5935fdc4e0bef208cfc0dcab7e3f7a1c41",
+        "rev": "e600439ec4c273cf11e06fe4d9d906fb98fa097c",
         "type": "github"
       },
       "original": {
@@ -43,11 +43,11 @@
     },
     "crane": {
       "locked": {
-        "lastModified": 1734541973,
-        "narHash": "sha256-1wIgLmhvtfxbJVnhFHUYhPqL3gpLn5JhiS4maaD9RRk=",
+        "lastModified": 1741148495,
+        "narHash": "sha256-EV8KUaIZ2/CdBXlutXrHoZYbWPeB65p5kKZk71gvDRI=",
         "owner": "ipetkov",
         "repo": "crane",
-        "rev": "fdd502f921936105869eba53db6593fc2a424c16",
+        "rev": "75390a36cd0c2cdd5f1aafd8a9f827d7107f2e53",
         "type": "github"
       },
       "original": {
@@ -111,11 +111,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1734343412,
-        "narHash": "sha256-b7G8oFp0Nj01BYUJ6ENC9Qf/HsYAIZvN9k/p0Kg/PFU=",
+        "lastModified": 1740485968,
+        "narHash": "sha256-WK+PZHbfDjLyveXAxpnrfagiFgZWaTJglewBWniTn2Y=",
         "owner": "nix-community",
         "repo": "disko",
-        "rev": "a08bfe06b39e94eec98dd089a2c1b18af01fef19",
+        "rev": "19c1140419c4f1cdf88ad4c1cfb6605597628940",
         "type": "github"
       },
       "original": {
@@ -183,11 +183,11 @@
     },
     "impermanence": {
       "locked": {
-        "lastModified": 1734200366,
-        "narHash": "sha256-0NursoP4BUdnc+wy+Mq3icHkXu/RgP1Sjo0MJxV2+Dw=",
+        "lastModified": 1737831083,
+        "narHash": "sha256-LJggUHbpyeDvNagTUrdhe/pRVp4pnS6wVKALS782gRI=",
         "owner": "nix-community",
         "repo": "impermanence",
-        "rev": "c6323585fa0035d780e3d8906eb1b24b65d19a48",
+        "rev": "4b3e914cdf97a5b536a889e939fb2fd2b043a170",
         "type": "github"
       },
       "original": {
@@ -198,11 +198,11 @@
     },
     "library": {
       "locked": {
-        "lastModified": 1738161079,
-        "narHash": "sha256-YDiu0DbtwOSo1GO3D9A0Q+dHzsYJ6pJ8ZYEkgmK1szI=",
+        "lastModified": 1738443114,
+        "narHash": "sha256-IV7n/l3rFoz5UuavrDv0a7IIOPne0jDQVmJAR8bve8U=",
         "ref": "prime",
-        "rev": "847a8167fe3b52c3b8e19017b31a97e12ad411ea",
-        "revCount": 16,
+        "rev": "65bf71bb6ef05ce684924a1dc248bb2e8e2869fb",
+        "revCount": 17,
         "type": "git",
         "url": "https://git.foss-syndicate.org/vhack.eu/nix-library"
       },
@@ -214,11 +214,11 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1734522913,
-        "narHash": "sha256-tyReZKZRdyODkbcwYnO7xowXx7VCFJ6XzAY7w2aFjs0=",
+        "lastModified": 1741318725,
+        "narHash": "sha256-3ShROHs7BXBDH3VNoPmbG4mL8DvRpDM8s4NxkmRVz1Q=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "bf383789c497270e8e20ccc2261cf2c6e18dbda8",
+        "rev": "9290fda826610430b3fc8cc98443c3a2faaaf151",
         "type": "github"
       },
       "original": {
@@ -228,28 +228,28 @@
         "type": "github"
       }
     },
-    "nixpkgs-24_05": {
+    "nixpkgs-24_11": {
       "locked": {
-        "lastModified": 1731797254,
-        "narHash": "sha256-df3dJApLPhd11AlueuoN0Q4fHo/hagP75LlM5K1sz9g=",
+        "lastModified": 1734083684,
+        "narHash": "sha256-5fNndbndxSx5d+C/D0p/VF32xDiJCJzyOqorOYW4JEo=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "e8c38b73aeb218e27163376a2d617e61a2ad9b59",
+        "rev": "314e12ba369ccdb9b352a4db26ff419f7c49fa84",
         "type": "github"
       },
       "original": {
         "id": "nixpkgs",
-        "ref": "nixos-24.05",
+        "ref": "nixos-24.11",
         "type": "indirect"
       }
     },
     "nixpkgs-unstable": {
       "locked": {
-        "lastModified": 1734536697,
-        "narHash": "sha256-G/HnRTtU+ob8x967kjzMRqjNFbAdllrcjYc+IcaR15Y=",
+        "lastModified": 1741323510,
+        "narHash": "sha256-zQL0iErtVTxywxyWc7ajRmRNCncny95uD+2wmBHYOzc=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "9c40bef08a5bdc0ccc3207f4282a1ded83e77a7a",
+        "rev": "f104cca31ba6c0403b678ad9428726476b503782",
         "type": "github"
       },
       "original": {
@@ -317,11 +317,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1734575524,
-        "narHash": "sha256-BxQ/4JuHEi0zRjF0P8B5xnbXOLulgsK2gfwVRXGZ4a4=",
+        "lastModified": 1741314698,
+        "narHash": "sha256-6Yp0CTwAY/jq/F81Sa8NM0Zi1EwxAdASO6y4A5neGuc=",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "573c674a3ad06e8a525263185ebef336a411d1d5",
+        "rev": "4e9af61c1a631886cdc7e13032af4fc9e75bb76b",
         "type": "github"
       },
       "original": {
@@ -339,14 +339,14 @@
         "nixpkgs": [
           "nixpkgs"
         ],
-        "nixpkgs-24_05": "nixpkgs-24_05"
+        "nixpkgs-24_11": "nixpkgs-24_11"
       },
       "locked": {
-        "lastModified": 1734370678,
-        "narHash": "sha256-a8zkti1QM5Oxkdfnzr/NjrFlyqI36/kYV/X8G1jOmB4=",
+        "lastModified": 1740437053,
+        "narHash": "sha256-exPTta4qI1ka9sk+jPcLogGffJ1OVXnAsTRqpeAXeNw=",
         "owner": "simple-nixos-mailserver",
         "repo": "nixos-mailserver",
-        "rev": "c43d8c4a3ce84a7bebd110b06e69365484db6208",
+        "rev": "c8ec4d5e432f5df4838eacd39c11828d23ce66ec",
         "type": "gitlab"
       },
       "original": {
@@ -378,11 +378,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1734704479,
-        "narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=",
+        "lastModified": 1739829690,
+        "narHash": "sha256-mL1szCeIsjh6Khn3nH2cYtwO5YXG6gBiTw1A30iGeDU=",
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f",
+        "rev": "3d0579f5cc93436052d94b73925b48973a104204",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 6f780d0..4d127f1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -135,7 +135,7 @@
     ];
 
     tests = import ./tests {inherit pkgs specialArgs nixLib;};
-    vhackPackages = import ./pkgs {inherit pkgs nixLib;};
+    vhackPackages = import ./pkgs {inherit pkgs nixLib pkgsUnstable;};
 
     hosts = import ./hosts {inherit pkgs nixLib nixpkgs specialArgs extraModules deployPackage;};
 
diff --git a/hosts/by-name/server2/configuration.nix b/hosts/by-name/server2/configuration.nix
index 95f0ade..c373d28 100644
--- a/hosts/by-name/server2/configuration.nix
+++ b/hosts/by-name/server2/configuration.nix
@@ -14,11 +14,10 @@
   vhack = {
     back = {
       enable = true;
-      repositories = {
-        "${config.services.gitolite.dataDir}/repositories/vhack.eu/nixos-server.git" = {
-          domain = "issues.foss-syndicate.org";
-          port = 9220;
-        };
+      domain = "issues.foss-syndicate.org";
+      settings = {
+        scan_path = "${config.services.gitolite.dataDir}/repositories";
+        project_list = "${config.services.gitolite.dataDir}/projects.list";
       };
     };
     backup = {
@@ -53,6 +52,7 @@
       enable = true;
       redirects = {
         "source.foss-syndicate.org" = "https://git.foss-syndicate.org/vhack.eu/nixos-server";
+        "source.vhack.eu" = "https://source.foss-syndicate.org";
       };
     };
     nixconfig.enable = true;
diff --git a/modules/by-name/ba/back/module.nix b/modules/by-name/ba/back/module.nix
index 520acdb..d47ffce 100644
--- a/modules/by-name/ba/back/module.nix
+++ b/modules/by-name/ba/back/module.nix
@@ -6,116 +6,87 @@
   ...
 }: let
   cfg = config.vhack.back;
+in {
+  options.vhack.back = {
+    enable = lib.mkEnableOption "Back issue tracker (inspired by tvix's panettone)";
 
-  mkConfigFile = repoPath: domain:
-    (pkgs.formats.json {}).generate "config.json"
-    {
-      inherit (cfg) source_code_repository_url;
-      repository_path = repoPath;
-      root_url = "https://${domain}";
-    };
-
-  mkUnit = repoPath: port: domain: {
-    description = "Back service for ${repoPath}";
-    wants = ["network-online.target"];
-    after = ["network-online.target"];
-    wantedBy = ["default.target"];
-
-    environment = {
-      ROCKET_PORT = builtins.toString port;
+    domain = lib.mkOption {
+      type = lib.types.str;
+      description = "The domain to host this `back` instance on.";
     };
 
-    serviceConfig = {
-      ExecStart = "${lib.getExe vhackPackages.back} ${mkConfigFile repoPath domain}";
+    settings = {
+      scan_path = lib.mkOption {
+        type = lib.types.path;
+        description = "The path to the directory under which all the repositories reside";
+      };
+      project_list = lib.mkOption {
+        type = lib.types.path;
+        description = "The path to the `projects.list` file.";
+      };
 
-      # Ensure that the service can read the repository
-      # FIXME(@bpeetz): This has the implied assumption, that all the exposed git
-      # repositories are readable for the git group. This should not be necessary. <2024-12-23>
-      User = "git";
-      Group = "git";
+      source_code_repository_url = lib.mkOption {
+        description = "The url to the source code of this instance of back";
+        default = "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back";
+        type = lib.types.str;
+      };
 
-      DynamicUser = true;
-      Restart = "always";
-
-      # Sandboxing
-      ProtectSystem = "strict";
-      ProtectHome = true;
-      PrivateTmp = true;
-      PrivateDevices = true;
-      ProtectHostname = true;
-      ProtectClock = true;
-      ProtectKernelTunables = true;
-      ProtectKernelModules = true;
-      ProtectKernelLogs = true;
-      ProtectControlGroups = true;
-      RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
-      RestrictNamespaces = true;
-      LockPersonality = true;
-      MemoryDenyWriteExecute = true;
-      RestrictRealtime = true;
-      RestrictSUIDSGID = true;
-      RemoveIPC = true;
-      PrivateMounts = true;
-      # System Call Filtering
-      SystemCallArchitectures = "native";
-      SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"];
+      root_url = lib.mkOption {
+        type = lib.types.str;
+        description = "The url to this instance of back.";
+        default = "https://${cfg.domain}";
+      };
     };
   };
 
-  mkVirtalHost = port: {
-    locations."/".proxyPass = "http://127.0.0.1:${builtins.toString port}";
+  config = lib.mkIf cfg.enable {
+    systemd.services."back" = {
+      description = "Back issue tracking system.";
+      requires = ["network-online.target"];
+      after = ["network-online.target"];
+      wantedBy = ["default.target"];
 
-    enableACME = true;
-    forceSSL = true;
-  };
+      serviceConfig = {
+        ExecStart = "${lib.getExe vhackPackages.back} ${(pkgs.formats.json {}).generate "config.json" cfg.settings}";
 
-  services =
-    lib.mapAttrs' (gitPath: config: {
-      name = builtins.replaceStrings ["/"] ["_"] "back-${config.domain}";
-      value = mkUnit gitPath config.port config.domain;
-    })
-    cfg.repositories;
+        # Ensure that the service can read the repository
+        # FIXME(@bpeetz): This has the implied assumption, that all the exposed git
+        # repositories are readable for the git group. This should not be necessary. <2024-12-23>
+        User = "git";
+        Group = "git";
 
-  virtualHosts =
-    lib.mapAttrs' (gitPath: config: {
-      name = config.domain;
-      value = mkVirtalHost config.port;
-    })
-    cfg.repositories;
-in {
-  options.vhack.back = {
-    enable = lib.mkEnableOption "Back issue tracker (inspired by tvix's panettone)";
+        DynamicUser = true;
+        Restart = "always";
 
-    source_code_repository_url = lib.mkOption {
-      description = "The url to the source code of this instance of back";
-      default = "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back";
-      type = lib.types.str;
+        # Sandboxing
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHostname = true;
+        ProtectClock = true;
+        ProtectKernelTunables = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectControlGroups = true;
+        RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"];
+        RestrictNamespaces = true;
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        PrivateMounts = true;
+        # System Call Filtering
+        SystemCallArchitectures = "native";
+        SystemCallFilter = ["~@cpu-emulation @debug @keyring @mount @obsolete @privileged @setuid"];
+      };
     };
+    services.nginx.virtualHosts."${cfg.domain}" = {
+      locations."/".proxyPass = "http://127.0.0.1:8000";
 
-    repositories = lib.mkOption {
-      description = "An attibute set of repos to launch `back` services for.";
-      type = lib.types.attrsOf (lib.types.submodule {
-        options = {
-          enable = (lib.mkEnableOption "`back` for this repository.") // {default = true;};
-          domain = lib.mkOption {
-            type = lib.types.str;
-            description = "The domain to host this `back` instance on.";
-          };
-          port = lib.mkOption {
-            type = lib.types.port;
-
-            # TODO: This _should_ be an implementation detail, but I've no real approach to
-            # automatically generate them without encountering weird bugs. <2024-12-23>
-            description = "The port to use for this back instance. This must be unique.";
-          };
-        };
-      });
-      default = {};
+      enableACME = true;
+      forceSSL = true;
     };
   };
-
-  config = lib.mkIf cfg.enable {
-    systemd = {inherit services;};
-    services.nginx = {inherit virtualHosts;};
-  };
 }
diff --git a/modules/by-name/dn/dns/dns/default.nix b/modules/by-name/dn/dns/dns/default.nix
new file mode 100644
index 0000000..4ce07d8
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/default.nix
@@ -0,0 +1,13 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{lib}: let
+  util = import ./util {inherit lib;};
+  types = import ./types {inherit lib util;};
+in {
+  inherit
+    types
+    ;
+}
diff --git a/modules/by-name/dn/dns/dns/types/default.nix b/modules/by-name/dn/dns/dns/types/default.nix
new file mode 100644
index 0000000..ece315f
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/default.nix
@@ -0,0 +1,16 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{
+  lib,
+  util,
+}: let
+  simple = {types = import ./simple.nix {inherit lib;};};
+in {
+  record = import ./record.nix {inherit lib util;};
+  records = import ./records {inherit lib util simple;};
+
+  zone = import ./zone.nix {inherit lib util simple;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/record.nix b/modules/by-name/dn/dns/dns/types/record.nix
new file mode 100644
index 0000000..e992bf9
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/record.nix
@@ -0,0 +1,75 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+# SPDX-FileCopyrightText: 2021 Naïm Favier <n@monade.li>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{lib, ...}: let
+  inherit (lib) hasSuffix isString mkOption removeSuffix types;
+
+  recordType = rsubt: let
+    submodule = types.submodule {
+      options =
+        {
+          class = mkOption {
+            type = types.enum ["IN"];
+            default = "IN";
+            example = "IN";
+            description = "Resource record class. Only IN is supported";
+          };
+          ttl = mkOption {
+            type = types.nullOr types.ints.unsigned; # TODO: u32
+            default = null;
+            example = 300;
+            description = "Record caching duration (in seconds)";
+          };
+        }
+        // rsubt.options;
+    };
+  in
+    (
+      if rsubt ? fromString
+      then types.either types.str
+      else lib.id
+    )
+    submodule;
+
+  # name == "@" : use unqualified domain name
+  writeRecord = name: rsubt: data: let
+    data' =
+      if isString data && rsubt ? fromString
+      then
+        # add default values for the record type
+        (recordType rsubt).merge [] [
+          {
+            file = "";
+            value = rsubt.fromString data;
+          }
+        ]
+      else data;
+    name' = let
+      fname = rsubt.nameFixup or (n: _: n) name data';
+    in
+      if name == "@"
+      then name
+      else if (hasSuffix ".@" name)
+      then removeSuffix ".@" fname
+      else "${fname}.";
+    inherit (rsubt) rtype;
+  in
+    lib.concatStringsSep " " (with data';
+      [
+        name'
+      ]
+      ++ lib.optionals (ttl != null) [
+        (toString ttl)
+      ]
+      ++ [
+        class
+        rtype
+        (rsubt.dataToString data')
+      ]);
+in {
+  inherit recordType;
+  inherit writeRecord;
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/A.nix b/modules/by-name/dn/dns/dns/types/records/A.nix
new file mode 100644
index 0000000..296943e
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/A.nix
@@ -0,0 +1,19 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{lib, ...}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "A";
+  options = {
+    address = mkOption {
+      type = types.str;
+      example = "26.3.0.103";
+      description = "IP address of the host";
+    };
+  };
+  dataToString = {address, ...}: address;
+  fromString = address: {inherit address;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/AAAA.nix b/modules/by-name/dn/dns/dns/types/records/AAAA.nix
new file mode 100644
index 0000000..4717176
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/AAAA.nix
@@ -0,0 +1,19 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{lib, ...}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "AAAA";
+  options = {
+    address = mkOption {
+      type = types.str;
+      example = "4321:0:1:2:3:4:567:89ab";
+      description = "IPv6 address of the host";
+    };
+  };
+  dataToString = {address, ...}: address;
+  fromString = address: {inherit address;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/CAA.nix b/modules/by-name/dn/dns/dns/types/records/CAA.nix
new file mode 100644
index 0000000..4b40510
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/CAA.nix
@@ -0,0 +1,42 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 8659
+{lib, ...}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "CAA";
+  options = {
+    issuerCritical = mkOption {
+      type = types.bool;
+      example = true;
+      description = ''
+        If set to '1', indicates that the corresponding property tag
+        MUST be understood if the semantics of the CAA record are to be
+        correctly interpreted by an issuer
+      '';
+    };
+    tag = mkOption {
+      type = types.enum ["issue" "issuewild" "iodef"];
+      example = "issue";
+      description = "One of the defined property tags";
+    };
+    value = mkOption {
+      type = types.str; # section 4.1.1: not limited in length
+      example = "ca.example.net";
+      description = "Value of the property";
+    };
+  };
+  dataToString = {
+    issuerCritical,
+    tag,
+    value,
+    ...
+  }: ''${
+      if issuerCritical
+      then "128"
+      else "0"
+    } ${tag} "${value}"'';
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/CNAME.nix b/modules/by-name/dn/dns/dns/types/records/CNAME.nix
new file mode 100644
index 0000000..095b078
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/CNAME.nix
@@ -0,0 +1,27 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 1035, 3.3.1
+{
+  lib,
+  simple,
+  ...
+}: let
+  inherit (lib) mkOption;
+in {
+  rtype = "CNAME";
+  options = {
+    cname = mkOption {
+      type = simple.types.domain-name;
+      example = "www.test.com";
+      description = ''
+        A <domain-name> which specifies the canonical or primary name
+        for the owner. The owner name is an alias.
+      '';
+    };
+  };
+  dataToString = {cname, ...}: "${cname}";
+  fromString = cname: {inherit cname;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/DKIM.nix b/modules/by-name/dn/dns/dns/types/records/DKIM.nix
new file mode 100644
index 0000000..31b2f67
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/DKIM.nix
@@ -0,0 +1,75 @@
+#
+# SPDX-FileCopyrightText: 2020 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# This is a “fake” record type, not actually part of DNS.
+# It gets compiled down to a TXT record.
+# RFC 6376
+{
+  lib,
+  util,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in rec {
+  rtype = "TXT";
+  options = {
+    selector = mkOption {
+      type = types.str;
+      example = "mail";
+      description = "DKIM selector name";
+    };
+    h = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = ["sha1" "sha256"];
+      description = "Acceptable hash algorithms. Empty means all of them";
+      apply = lib.concatStringsSep ":";
+    };
+    k = mkOption {
+      type = types.nullOr types.str;
+      default = "rsa";
+      example = "rsa";
+      description = "Key type";
+    };
+    n = mkOption {
+      type = types.str;
+      default = "";
+      example = "Just any kind of arbitrary notes.";
+      description = "Notes that might be of interest to a human";
+    };
+    p = mkOption {
+      type = types.str;
+      example = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFCjxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gbto/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB";
+      description = "Public-key data (base64)";
+    };
+    s = mkOption {
+      type = types.listOf (types.enum ["*" "email"]);
+      default = ["*"];
+      example = ["email"];
+      description = "Service Type";
+      apply = lib.concatStringsSep ":";
+    };
+    t = mkOption {
+      type = types.listOf (types.enum ["y" "s"]);
+      default = [];
+      example = ["y"];
+      description = "Flags";
+      apply = lib.concatStringsSep ":";
+    };
+  };
+  dataToString = data: let
+    items =
+      ["v=DKIM1"]
+      ++ lib.pipe data [
+        (builtins.intersectAttrs options) # remove garbage list `_module`
+        (lib.filterAttrs (_k: v: v != null && v != ""))
+        (lib.filterAttrs (k: _v: k != "selector"))
+        (lib.mapAttrsToList (k: v: "${k}=${v}"))
+      ];
+    result = lib.concatStringsSep "; " items + ";";
+  in
+    util.writeCharacterString result;
+  nameFixup = name: self: "${self.selector}._domainkey.${name}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/DMARC.nix b/modules/by-name/dn/dns/dns/types/records/DMARC.nix
new file mode 100644
index 0000000..0f10f2c
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/DMARC.nix
@@ -0,0 +1,108 @@
+#
+# SPDX-FileCopyrightText: 2020 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# This is a “fake” record type, not actually part of DNS.
+# It gets compiled down to a TXT record.
+# RFC 7489
+{
+  lib,
+  util,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in rec {
+  rtype = "TXT";
+  options = {
+    adkim = mkOption {
+      type = types.enum ["relaxed" "strict"];
+      default = "relaxed";
+      example = "strict";
+      description = "DKIM Identifier Alignment mode";
+      apply = builtins.substring 0 1;
+    };
+    aspf = mkOption {
+      type = types.enum ["relaxed" "strict"];
+      default = "relaxed";
+      example = "strict";
+      description = "SPF Identifier Alignment mode";
+      apply = builtins.substring 0 1;
+    };
+    fo = mkOption {
+      type = types.listOf (types.enum ["0" "1" "d" "s"]);
+      default = ["0"];
+      example = ["0" "1" "s"];
+      description = "Failure reporting options";
+      apply = lib.concatStringsSep ":";
+    };
+    p = mkOption {
+      type = types.enum ["none" "quarantine" "reject"];
+      example = "quarantine";
+      description = "Requested Mail Receiver policy";
+    };
+    pct = mkOption {
+      type = types.ints.between 0 100;
+      default = 100;
+      example = 30;
+      description = "Percentage of messages to which the DMARC policy is to be applied";
+      apply = builtins.toString;
+    };
+    rf = mkOption {
+      type = types.listOf (types.enum ["afrf"]);
+      default = ["afrf"];
+      example = ["afrf"];
+      description = "Format to be used for message-specific failure reports";
+      apply = lib.concatStringsSep ":";
+    };
+    ri = mkOption {
+      type = types.ints.unsigned; # FIXME: u32
+      default = 86400;
+      example = 12345;
+      description = "Interval requested between aggregate reports";
+      apply = builtins.toString;
+    };
+    rua = mkOption {
+      type = types.oneOf [types.str (types.listOf types.str)];
+      default = [];
+      example = "mailto:dmarc+rua@example.com";
+      description = "Addresses to which aggregate feedback is to be sent";
+      apply = val:
+      # FIXME: need to encode commas in URIs
+        if builtins.isList val
+        then lib.concatStringsSep "," val
+        else val;
+    };
+    ruf = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      example = ["mailto:dmarc+ruf@example.com" "mailto:another+ruf@example.com"];
+      description = "Addresses to which message-specific failure information is to be reported";
+      apply = val:
+      # FIXME: need to encode commas in URIs
+        if builtins.isList val
+        then lib.concatStringsSep "," val
+        else val;
+    };
+    sp = mkOption {
+      type = types.nullOr (types.enum ["none" "quarantine" "reject"]);
+      default = null;
+      example = "quarantine";
+      description = "Requested Mail Receiver policy for all subdomains";
+    };
+  };
+  dataToString = data: let
+    # The specification could be more clear on this, but `v` and `p` MUST
+    # be the first two tags in the record.
+    items =
+      ["v=DMARC1; p=${data.p}"]
+      ++ lib.pipe data [
+        (builtins.intersectAttrs options) # remove garbage list `_module`
+        (lib.filterAttrs (k: v: v != null && v != "" && k != "p"))
+        (lib.mapAttrsToList (k: v: "${k}=${v}"))
+      ];
+    result = lib.concatStringsSep "; " items + ";";
+  in
+    util.writeCharacterString result;
+  nameFixup = name: _self: "_dmarc.${name}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/DNAME.nix b/modules/by-name/dn/dns/dns/types/records/DNAME.nix
new file mode 100644
index 0000000..042ce95
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/DNAME.nix
@@ -0,0 +1,15 @@
+# RFC 6672
+{lib, ...}: let
+  inherit (lib) dns mkOption;
+in {
+  rtype = "DNAME";
+  options = {
+    dname = mkOption {
+      type = dns.types.domain-name;
+      example = "www.test.com";
+      description = "A <domain-name> which provides redirection from a part of the DNS name tree to another part of the DNS name tree";
+    };
+  };
+  dataToString = {dname, ...}: "${dname}";
+  fromString = dname: {inherit dname;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix b/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix
new file mode 100644
index 0000000..86ce3a1
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/DNSKEY.nix
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+# RFC 4034, 2
+{lib, ...}: let
+  inherit (builtins) isInt split;
+  inherit (lib) concatStrings flatten mkOption types;
+
+  dnssecOptions = import ./dnssec.nix {inherit lib;};
+  inherit (dnssecOptions) mkDNSSECAlgorithmOption;
+in {
+  rtype = "DNSKEY";
+  options = {
+    flags = mkOption {
+      description = "Flags pertaining to this RR.";
+      type = types.either types.ints.u16 (types.submodule {
+        options = {
+          zoneSigningKey = mkOption {
+            description = "Whether this RR holds a zone signing key (ZSK).";
+            type = types.bool;
+            default = false;
+          };
+          secureEntryPoint = mkOption {
+            type = types.bool;
+            description = ''
+              Whether this RR holds a secure entry point.
+              In general, this means the key is a key-signing key (KSK), as opposed to a zone-signing key.
+            '';
+            default = false;
+          };
+        };
+      });
+      apply = value:
+        if isInt value
+        then value
+        else
+          (
+            if value.zoneSigningKey
+            then 256
+            else 0
+          )
+          + (
+            if value.secureEntryPoint
+            then 1
+            else 0
+          );
+    };
+    algorithm = mkDNSSECAlgorithmOption {
+      description = "Algorithm of the key referenced by this RR.";
+    };
+    publicKey = mkOption {
+      type = types.str;
+      description = "Base64-encoded public key.";
+      apply = value: concatStrings (flatten (split "[[:space:]]" value));
+    };
+  };
+  dataToString = {
+    flags,
+    algorithm,
+    publicKey,
+    ...
+  }: "${toString flags} 3 ${toString algorithm} ${publicKey}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/DS.nix b/modules/by-name/dn/dns/dns/types/records/DS.nix
new file mode 100644
index 0000000..76fac9a
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/DS.nix
@@ -0,0 +1,48 @@
+# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+# RFC 4034, 5
+{lib, ...}: let
+  inherit (lib) mkOption types;
+
+  dnssecOptions = import ./dnssec.nix {inherit lib;};
+  inherit (dnssecOptions) mkRegisteredNumberOption mkDNSSECAlgorithmOption;
+
+  mkDSDigestTypeOption = args:
+    mkRegisteredNumberOption {
+      registryName = "Delegation Signer (DS) Resource Record (RR) Type Digest Algorithms";
+      numberType = types.ints.u8;
+      # These mnemonics are unofficial, unlike the DNSSEC algorithm ones.
+      mnemonics = {
+        "sha-1" = 1;
+        "sha-256" = 2;
+        "gost" = 3;
+        "sha-384" = 4;
+      };
+    };
+in {
+  rtype = "DS";
+  options = {
+    keyTag = mkOption {
+      description = "Tag computed over the DNSKEY referenced by this RR to identify it.";
+      type = types.ints.u16;
+    };
+    algorithm = mkDNSSECAlgorithmOption {
+      description = "Algorithm of the key referenced by this RR.";
+    };
+    digestType = mkDSDigestTypeOption {
+      description = "Type of the digest given in the `digest` attribute.";
+    };
+    digest = mkOption {
+      description = "Digest of the DNSKEY referenced by this RR.";
+      type = types.strMatching "[[:xdigit:]]+";
+    };
+  };
+  dataToString = {
+    keyTag,
+    algorithm,
+    digestType,
+    digest,
+    ...
+  }: "${toString keyTag} ${toString algorithm} ${toString digestType} ${digest}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/HTTPS.nix b/modules/by-name/dn/dns/dns/types/records/HTTPS.nix
new file mode 100644
index 0000000..6e2ef3d
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/HTTPS.nix
@@ -0,0 +1,5 @@
+args:
+import ./SVCB.nix args
+// {
+  rtype = "HTTPS";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/MTA-STS.nix b/modules/by-name/dn/dns/dns/types/records/MTA-STS.nix
new file mode 100644
index 0000000..030490e
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/MTA-STS.nix
@@ -0,0 +1,42 @@
+#
+# SPDX-FileCopyrightText: 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# This is a “fake” record type, not actually part of DNS.
+# It gets compiled down to a TXT record.
+# RFC 8461
+{
+  lib,
+  util,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in rec {
+  rtype = "TXT";
+  options = {
+    id = mkOption {
+      type = types.str;
+      example = "20160831085700Z";
+      description = ''
+        A short string used to track policy updates.  This string MUST
+        uniquely identify a given instance of a policy, such that senders
+        can determine when the policy has been updated by comparing to the
+        "id" of a previously seen policy.  There is no implied ordering of
+        "id" fields between revisions.
+      '';
+    };
+  };
+  dataToString = data: let
+    items =
+      ["v=STSv1"]
+      ++ lib.pipe data [
+        (builtins.intersectAttrs options) # remove garbage list `_module`
+        (lib.filterAttrs (k: v: v != null && v != ""))
+        (lib.mapAttrsToList (k: v: "${k}=${v}"))
+      ];
+    result = lib.concatStringsSep "; " items + ";";
+  in
+    util.writeCharacterString result;
+  nameFixup = name: _self: "_mta-sts.${name}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/MX.nix b/modules/by-name/dn/dns/dns/types/records/MX.nix
new file mode 100644
index 0000000..c25b89c
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/MX.nix
@@ -0,0 +1,32 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 1035, 3.3.9
+{
+  lib,
+  simple,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "MX";
+  options = {
+    preference = mkOption {
+      type = types.ints.u16;
+      example = 10;
+      description = "The preference given to this RR among others at the same owner. Lower values are preferred";
+    };
+    exchange = mkOption {
+      type = simple.types.domain-name;
+      example = "smtp.example.com.";
+      description = "A <domain-name> which specifies a host willing to act as a mail exchange for the owner name";
+    };
+  };
+  dataToString = {
+    preference,
+    exchange,
+    ...
+  }: "${toString preference} ${exchange}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/NS.nix b/modules/by-name/dn/dns/dns/types/records/NS.nix
new file mode 100644
index 0000000..ea60a91
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/NS.nix
@@ -0,0 +1,24 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 1035, 3.3.11
+{
+  lib,
+  simple,
+  ...
+}: let
+  inherit (lib) mkOption;
+in {
+  rtype = "NS";
+  options = {
+    nsdname = mkOption {
+      type = simple.types.domain-name;
+      example = "ns2.example.com";
+      description = "A <domain-name> which specifies a host which should be authoritative for the specified class and domain";
+    };
+  };
+  dataToString = {nsdname, ...}: "${nsdname}";
+  fromString = nsdname: {inherit nsdname;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix b/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix
new file mode 100644
index 0000000..1f39cb9
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/OPENPGPKEY.nix
@@ -0,0 +1,18 @@
+# RFC7929
+{
+  lib,
+  util,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "OPENPGPKEY";
+  options = {
+    data = mkOption {
+      type = types.str;
+    };
+  };
+
+  dataToString = {data, ...}: util.writeCharacterString data;
+  fromString = data: {inherit data;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/PTR.nix b/modules/by-name/dn/dns/dns/types/records/PTR.nix
new file mode 100644
index 0000000..075f82e
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/PTR.nix
@@ -0,0 +1,92 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 1035, 3.3.12
+{
+  lib,
+  simple,
+  ...
+}: let
+  inherit (lib) mkOption;
+
+  inherit (lib.strings) stringToCharacters splitString;
+
+  reverseIpv4 = input:
+    builtins.concatStringsSep "." (lib.lists.reverseList (splitString "."
+        input));
+
+  reverseIpv6 = input: let
+    split = splitString ":" input;
+    elementLength = builtins.length split;
+
+    reverseString = string:
+      builtins.concatStringsSep "" (lib.lists.reverseList
+        (stringToCharacters string));
+  in
+    reverseString (builtins.concatStringsSep "." (stringToCharacters (builtins.concatStringsSep
+      "" (builtins.map (
+          part: let
+            c = stringToCharacters part;
+          in
+            if builtins.length c == 4
+            then
+              # valid part
+              part
+            else if builtins.length c < 4 && builtins.length c > 0
+            then
+              # leading zeros were elided
+              (builtins.concatStringsSep "" (
+                builtins.map builtins.toString (
+                  builtins.genList (_: 0) (4 - (builtins.length c))
+                )
+              ))
+              + part
+            else if builtins.length c == 0
+            then
+              # Multiple full blocks were elided. Only one of these can be in an
+              # IPv6 address, as such we can simply add (8 - (elementLength - 1)) `0000`
+              # blocks. We need to substract one from `elementLength` because
+              # this empty part is included in the `elementLength`.
+              builtins.concatStringsSep "" (builtins.genList (_: "0000") (8 - (elementLength - 1)))
+            else builtins.throw "Impossible"
+        )
+        split))));
+in {
+  rtype = "PTR";
+  options = {
+    name = mkOption {
+      type = simple.types.domain-name;
+      example = "mail2.server.com";
+      description = "The <domain-name> which is defined by the IP.";
+    };
+    ip = {
+      v4 = mkOption {
+        type = lib.types.nullOr lib.types.str;
+        example = "192.168.1.4";
+        description = "The IPv4 address of the host.";
+        default = null;
+        apply = v:
+          if v != null
+          then reverseIpv4 v
+          else v;
+      };
+      v6 = mkOption {
+        type = lib.types.nullOr lib.types.str;
+        example = "192.168.1.4";
+        description = "The IPv6 address of the host.";
+        default = null;
+        apply = v:
+          if v != null
+          then reverseIpv6 v
+          else v;
+      };
+    };
+  };
+  dataToString = {name, ...}: "${name}.";
+  nameFixup = name: self:
+    if self.ip.v6 == null
+    then "${self.ip.v4}.in-addr.arpa"
+    else "${self.ip.v6}.ip6.arpa";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/SOA.nix b/modules/by-name/dn/dns/dns/types/records/SOA.nix
new file mode 100644
index 0000000..db7436e
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/SOA.nix
@@ -0,0 +1,65 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 1035, 3.3.13
+{
+  lib,
+  simple,
+  ...
+}: let
+  inherit (lib) concatStringsSep removeSuffix replaceStrings;
+  inherit (lib) mkOption types;
+in {
+  rtype = "SOA";
+  options = {
+    nameServer = mkOption {
+      type = simple.types.domain-name;
+      example = "ns1.example.com";
+      description = "The <domain-name> of the name server that was the original or primary source of data for this zone. Don't forget the dot at the end!";
+    };
+    adminEmail = mkOption {
+      type = simple.types.domain-name;
+      example = "admin@example.com";
+      description = "An email address of the person responsible for this zone. (Note: in traditional zone files you are supposed to put a dot instead of `@` in your address; you can use `@` with this module and it is recommended to do so. Also don't put the dot at the end!)";
+      apply = s: replaceStrings ["@"] ["."] (removeSuffix "." s);
+    };
+    serial = mkOption {
+      type = types.ints.unsigned; # TODO: u32
+      example = 20;
+      description = "Version number of the original copy of the zone";
+    };
+    refresh = mkOption {
+      type = types.ints.unsigned; # TODO: u32
+      default = 24 * 60 * 60;
+      example = 7200;
+      description = "Time interval before the zone should be refreshed";
+    };
+    retry = mkOption {
+      type = types.ints.unsigned; # TODO: u32
+      default = 10 * 60;
+      example = 600;
+      description = "Time interval that should elapse before a failed refresh should be retried";
+    };
+    expire = mkOption {
+      type = types.ints.unsigned; # TODO: u32
+      default = 10 * 24 * 60 * 60;
+      example = 3600000;
+      description = "Time value that specifies the upper limit on the time interval that can elapse before the zone is no longer authoritative";
+    };
+    minimum = mkOption {
+      type = types.ints.unsigned; # TODO: u32
+      default = 60;
+      example = 60;
+      description = "Minimum TTL field that should be exported with any RR from this zone";
+    };
+  };
+  dataToString = data @ {
+    nameServer,
+    adminEmail,
+    ...
+  }: let
+    numbers = map toString (with data; [serial refresh retry expire minimum]);
+  in "${nameServer} ${adminEmail}. (${concatStringsSep " " numbers})";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/SRV.nix b/modules/by-name/dn/dns/dns/types/records/SRV.nix
new file mode 100644
index 0000000..5f558ed
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/SRV.nix
@@ -0,0 +1,51 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 2782
+{
+  lib,
+  simple,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "SRV";
+  options = {
+    service = mkOption {
+      type = types.str;
+      example = "foobar";
+      description = "The symbolic name of the desired service. Do not add the underscore!";
+    };
+    proto = mkOption {
+      type = types.str;
+      example = "tcp";
+      description = "The symbolic name of the desired protocol. Do not add the underscore!";
+    };
+    priority = mkOption {
+      type = types.ints.u16;
+      default = 0;
+      example = 0;
+      description = "The priority of this target host";
+    };
+    weight = mkOption {
+      type = types.ints.u16;
+      default = 100;
+      example = 20;
+      description = "The weight field specifies a relative weight for entries with the same priority. Larger weights SHOULD be given a proportionately higher probability of being selected";
+    };
+    port = mkOption {
+      type = types.ints.u16;
+      example = 9;
+      description = "The port on this target host of this service";
+    };
+    target = mkOption {
+      type = simple.types.domain-name;
+      example = "";
+      description = "The domain name of the target host";
+    };
+  };
+  dataToString = data: with data; "${toString priority} ${toString weight} ${toString port} ${target}";
+  nameFixup = name: self: "_${self.service}._${self.proto}.${name}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/SSHFP.nix b/modules/by-name/dn/dns/dns/types/records/SSHFP.nix
new file mode 100644
index 0000000..1409860
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/SSHFP.nix
@@ -0,0 +1,39 @@
+# RFC 4255
+{lib, ...}: let
+  inherit (lib) mkOption types;
+  inherit (builtins) attrNames;
+  algorithm = {
+    "rsa" = 1;
+    "dsa" = 2;
+    "ecdsa" = 3; # RFC 6594
+    "ed25519" = 4; # RFC 7479 / RFC 8709
+    "ed448" = 6; # RFC 8709
+  };
+  mode = {
+    "sha1" = 1;
+    "sha256" = 2; # RFC 6594
+  };
+in {
+  rtype = "SSHFP";
+  options = {
+    algorithm = mkOption {
+      example = "ed25519";
+      type = types.enum (attrNames algorithm);
+      apply = value: algorithm.${value};
+    };
+    fingerprintType = mkOption {
+      example = "sha256";
+      type = types.enum (attrNames mode);
+      apply = value: mode.${value};
+    };
+    fingerprint = mkOption {
+      type = types.str;
+    };
+  };
+  dataToString = {
+    algorithm,
+    fingerprintType,
+    fingerprint,
+    ...
+  }: "${toString algorithm} ${toString fingerprintType} ${fingerprint}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/SVCB.nix b/modules/by-name/dn/dns/dns/types/records/SVCB.nix
new file mode 100644
index 0000000..62cbc3d
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/SVCB.nix
@@ -0,0 +1,100 @@
+# rfc9460
+{lib, ...}: let
+  inherit
+    (lib)
+    concatStringsSep
+    filter
+    isInt
+    isList
+    mapAttrsToList
+    mkOption
+    types
+    ;
+
+  mkSvcParams = params:
+    concatStringsSep " " (
+      filter (s: s != "") (
+        mapAttrsToList (
+          name: value:
+            if value
+            then name
+            else if isList value
+            then "${name}=${concatStringsSep "," value}"
+            else if isInt value
+            then "${name}=${builtins.toString value}"
+            else ""
+        )
+        params
+      )
+    );
+in {
+  rtype = "SVCB";
+  options = {
+    svcPriority = mkOption {
+      example = 1;
+      type = types.ints.u16;
+    };
+    targetName = mkOption {
+      example = ".";
+      type = types.str;
+    };
+    mandatory = mkOption {
+      example = ["ipv4hint"];
+      default = null;
+      type = types.nullOr (types.nonEmptyListOf types.str);
+    };
+    alpn = mkOption {
+      example = ["h2"];
+      default = null;
+      type = types.nullOr (types.nonEmptyListOf types.str);
+    };
+    no-default-alpn = mkOption {
+      example = true;
+      default = false;
+      type = types.bool;
+    };
+    port = mkOption {
+      example = 443;
+      default = null;
+      type = types.nullOr types.port;
+    };
+    ipv4hint = mkOption {
+      example = ["127.0.0.1"];
+      default = null;
+      type = types.nullOr (types.nonEmptyListOf types.str);
+    };
+    ipv6hint = mkOption {
+      example = ["::1"];
+      default = null;
+      type = types.nullOr (types.nonEmptyListOf types.str);
+    };
+    ech = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+    };
+  };
+  dataToString = {
+    svcPriority,
+    targetName,
+    mandatory ? null,
+    alpn ? null,
+    no-default-alpn ? null,
+    port ? null,
+    ipv4hint ? null,
+    ipv6hint ? null,
+    ech ? null,
+    ...
+  }: "${toString svcPriority} ${targetName} ${
+    mkSvcParams {
+      inherit
+        alpn
+        ech
+        ipv4hint
+        ipv6hint
+        mandatory
+        no-default-alpn
+        port
+        ;
+    }
+  }";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/TLSA.nix b/modules/by-name/dn/dns/dns/types/records/TLSA.nix
new file mode 100644
index 0000000..d92a29b
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/TLSA.nix
@@ -0,0 +1,50 @@
+# RFC 6698
+{lib, ...}: let
+  inherit (lib) mkOption types;
+  inherit (builtins) attrNames;
+
+  certUsage = {
+    "pkix-ta" = 0;
+    "pkix-ee" = 1;
+    "dane-ta" = 2;
+    "dane-ee" = 3;
+  };
+  selectors = {
+    "cert" = 0;
+    "spki" = 1;
+  };
+  match = {
+    "exact" = 0;
+    "sha256" = 1;
+    "sha512" = 2;
+  };
+in {
+  rtype = "TLSA";
+  options = {
+    certUsage = mkOption {
+      example = "dane-ee";
+      type = types.enum (attrNames certUsage);
+      apply = value: certUsage.${value};
+    };
+    selector = mkOption {
+      example = "spki";
+      type = types.enum (attrNames selectors);
+      apply = value: selectors.${value};
+    };
+    matchingType = mkOption {
+      example = "sha256";
+      type = types.enum (attrNames match);
+      apply = value: match.${value};
+    };
+    certificate = mkOption {
+      type = types.str;
+    };
+  };
+  dataToString = {
+    certUsage,
+    selector,
+    matchingType,
+    certificate,
+    ...
+  }: "${toString certUsage} ${toString selector} ${toString matchingType} ${certificate}";
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/TXT.nix b/modules/by-name/dn/dns/dns/types/records/TXT.nix
new file mode 100644
index 0000000..d605ce8
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/TXT.nix
@@ -0,0 +1,24 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+# RFC 1035, 3.3.14
+{
+  lib,
+  util,
+  ...
+}: let
+  inherit (lib) mkOption types;
+in {
+  rtype = "TXT";
+  options = {
+    data = mkOption {
+      type = types.str;
+      example = "favorite drink=orange juice";
+      description = "Arbitrary information";
+    };
+  };
+  dataToString = {data, ...}: util.writeCharacterString data;
+  fromString = data: {inherit data;};
+}
diff --git a/modules/by-name/dn/dns/dns/types/records/default.nix b/modules/by-name/dn/dns/dns/types/records/default.nix
new file mode 100644
index 0000000..76a86cd
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/default.nix
@@ -0,0 +1,43 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{
+  lib,
+  util,
+  simple,
+}: let
+  inherit (lib.attrsets) genAttrs;
+
+  types = [
+    "A"
+    "AAAA"
+    "CAA"
+    "CNAME"
+    "DNAME"
+    "MX"
+    "NS"
+    "SOA"
+    "SRV"
+    "TXT"
+    "PTR"
+
+    # DNSSEC types
+    "DNSKEY"
+    "DS"
+
+    # DANE types
+    "SSHFP"
+    "TLSA"
+    "OPENPGPKEY"
+    "SVCB"
+    "HTTPS"
+
+    # Pseudo types
+    "DKIM"
+    "DMARC"
+    "MTA-STS"
+  ];
+in
+  genAttrs types (t: import (./. + "/${t}.nix") {inherit lib simple util;})
diff --git a/modules/by-name/dn/dns/dns/types/records/dnssec.nix b/modules/by-name/dn/dns/dns/types/records/dnssec.nix
new file mode 100644
index 0000000..648f676
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/records/dnssec.nix
@@ -0,0 +1,48 @@
+# SPDX-FileCopyrightText: 2020 Aluísio Augusto Silva Gonçalves <https://aasg.name>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+{lib}: let
+  inherit (builtins) attrNames isInt removeAttrs;
+  inherit (lib) mkOption types;
+in rec {
+  mkRegisteredNumberOption = {
+    registryName,
+    numberType,
+    mnemonics,
+  } @ args:
+    mkOption
+    {
+      type =
+        types.either numberType (types.enum (attrNames mnemonics))
+        // {
+          name = "registeredNumber";
+          description = "number in IANA registry '${registryName}'";
+        };
+      apply = value:
+        if isInt value
+        then value
+        else mnemonics.${value};
+    }
+    // removeAttrs args ["registryName" "numberType" "mnemonics"];
+
+  mkDNSSECAlgorithmOption = args:
+    mkRegisteredNumberOption {
+      registryName = "Domain Name System Security (DNSSEC) Algorithm Numbers";
+      numberType = types.ints.u8;
+      mnemonics = {
+        "dsa" = 3;
+        "rsasha1" = 5;
+        "dsa-nsec3-sha1" = 6;
+        "rsasha1-nsec3-sha1" = 7;
+        "rsasha256" = 8;
+        "rsasha512" = 10;
+        "ecc-gost" = 12;
+        "ecdsap256sha256" = 13;
+        "ecdsap384sha384" = 14;
+        "ed25519" = 15;
+        "ed448" = 16;
+        "privatedns" = 253;
+        "privateoid" = 254;
+      };
+    };
+}
diff --git a/modules/by-name/dn/dns/dns/types/simple.nix b/modules/by-name/dn/dns/dns/types/simple.nix
new file mode 100644
index 0000000..fece2c9
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/simple.nix
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+{lib}: let
+  inherit (builtins) stringLength;
+in {
+  # RFC 1035, 3.1
+  domain-name = lib.types.addCheck lib.types.str (s: stringLength s <= 255);
+}
diff --git a/modules/by-name/dn/dns/dns/types/zone.nix b/modules/by-name/dn/dns/dns/types/zone.nix
new file mode 100644
index 0000000..44ccb15
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/types/zone.nix
@@ -0,0 +1,119 @@
+#
+# SPDX-FileCopyrightText: 2019 Kirill Elagin <https://kir.elagin.me/>
+# SPDX-FileCopyrightText: 2021 Naïm Favier <n@monade.li>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+#
+{
+  lib,
+  util,
+  simple,
+}: let
+  inherit (builtins) filter removeAttrs;
+  inherit
+    (lib)
+    concatMapStringsSep
+    concatStringsSep
+    mapAttrs
+    mapAttrsToList
+    optionalString
+    ;
+  inherit (lib) mkOption literalExample types;
+
+  inherit (import ./record.nix {inherit lib;}) recordType writeRecord;
+
+  rsubtypes = import ./records {inherit lib util simple;};
+  rsubtypes' = removeAttrs rsubtypes ["SOA"];
+
+  subzoneOptions =
+    {
+      subdomains = mkOption {
+        type = types.attrsOf subzone;
+        default = {};
+        example = {
+          www = {
+            A = [{address = "1.1.1.1";}];
+          };
+          staging = {
+            A = [{address = "1.0.0.1";}];
+          };
+        };
+        description = "Records for subdomains of the domain";
+      };
+    }
+    // mapAttrs (n: t:
+      mkOption {
+        type = types.listOf (recordType t);
+        default = [];
+        # example = [ t.example ];  # TODO: any way to auto-generate an example for submodule?
+        description = "List of ${n} records for this zone/subzone";
+      })
+    rsubtypes';
+
+  subzone = types.submodule {
+    options = subzoneOptions;
+  };
+
+  writeSubzone = name: zone: let
+    groupToString = pseudo: subt:
+      concatMapStringsSep "\n" (writeRecord name subt) zone."${pseudo}";
+    groups = mapAttrsToList groupToString rsubtypes';
+    groups' = filter (s: s != "") groups;
+
+    writeSubzone' = subname: writeSubzone "${subname}.${name}";
+    sub = concatStringsSep "\n\n" (mapAttrsToList writeSubzone' zone.subdomains);
+  in
+    concatStringsSep "\n\n" groups'
+    + optionalString (sub != "") ("\n\n" + sub);
+  zone = types.submodule ({name, ...}: {
+    options =
+      {
+        useOrigin = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Wether to use $ORIGIN and unqualified name or fqdn when exporting the zone.";
+        };
+
+        TTL = mkOption {
+          type = types.ints.unsigned;
+          default = 24 * 60 * 60;
+          example = literalExample "60 * 60";
+          description = "Default record caching duration. Sets the $TTL variable";
+        };
+        SOA = mkOption rec {
+          type = recordType rsubtypes.SOA;
+          example =
+            {
+              ttl = 24 * 60 * 60;
+            }
+            // type.example;
+          description = "SOA record";
+        };
+      }
+      // subzoneOptions;
+  });
+  renderToString = name: {
+    useOrigin,
+    TTL,
+    SOA,
+    ...
+  } @ zone:
+    if useOrigin
+    then ''
+      $ORIGIN ${name}.
+      $TTL ${toString TTL}
+
+      ${writeRecord "@" rsubtypes.SOA SOA}
+
+      ${writeSubzone "@" zone}
+    ''
+    else ''
+      $TTL ${toString TTL}
+
+      ${writeRecord name rsubtypes.SOA SOA}
+
+      ${writeSubzone name zone}
+    '';
+in {
+  inherit zone subzone renderToString;
+}
diff --git a/modules/by-name/dn/dns/dns/util/default.nix b/modules/by-name/dn/dns/dns/util/default.nix
new file mode 100644
index 0000000..59e661d
--- /dev/null
+++ b/modules/by-name/dn/dns/dns/util/default.nix
@@ -0,0 +1,76 @@
+# SPDX-FileCopyrightText: 2021 Kirill Elagin <https://kir.elagin.me/>
+#
+# SPDX-License-Identifier: MPL-2.0 or MIT
+{lib}: let
+  inherit
+    (builtins)
+    concatStringsSep
+    genList
+    stringLength
+    substring
+    ;
+  inherit
+    (lib.strings)
+    concatMapStrings
+    concatMapStringsSep
+    fixedWidthString
+    splitString
+    stringToCharacters
+    ;
+  inherit (lib.lists) filter reverseList;
+
+  /*
+  Split a string into byte chunks, such that each output String is less then or equal to
+  `n` bytes.
+
+  # Type
+
+  splitInGroupsOf :: Integer -> String -> [String]
+
+  # Arguments
+
+    n
+    : The number of bytes to put into each String.
+
+    s
+    : The String to split.
+  */
+  splitInGroupsOf = n: s: let
+    groupCount = (stringLength s - 1) / n + 1;
+  in
+    genList (i: substring (i * n) n s) groupCount;
+
+  # : str -> str
+  # Prepares a Nix string to be written to a zone file as a character-string
+  # literal: breaks it into chunks of 255 (per RFC 1035, 3.3) and encloses
+  # each chunk in quotation marks.
+  writeCharacterString = s:
+    if stringLength s <= 255
+    then ''"${s}"''
+    else concatMapStringsSep " " (x: ''"${x}"'') (splitInGroupsOf 255 s);
+
+  # : str -> str, with length 4 (zeros are padded to the left)
+  align4Bytes = fixedWidthString 4 "0";
+
+  # : int -> str -> str
+  # Expands "" to 4n zeros and aligns the rest on 4 bytes
+  align4BytesOrExpand = n: v:
+    if v == ""
+    then (fixedWidthString (4 * n) "0" "")
+    else align4Bytes v;
+
+  # : str -> [ str ]
+  # Returns the record of the ipv6 as a list
+  mkRecordAux = v6: let
+    splitted = splitString ":" v6;
+    n = 8 - builtins.length (filter (x: x != "") splitted);
+  in
+    stringToCharacters (concatMapStrings (align4BytesOrExpand n) splitted);
+
+  # : str -> str
+  # Returns the reversed record of the ipv6
+  mkReverseRecord = v6:
+    concatStringsSep "." (reverseList (mkRecordAux v6)) + ".ip6.arpa";
+in {
+  inherit writeCharacterString mkReverseRecord;
+}
diff --git a/modules/by-name/dn/dns/module.nix b/modules/by-name/dn/dns/module.nix
new file mode 100644
index 0000000..8f4ad37
--- /dev/null
+++ b/modules/by-name/dn/dns/module.nix
@@ -0,0 +1,86 @@
+{
+  config,
+  lib,
+  ...
+}: let
+  cfg = config.vhack.dns;
+
+  zones =
+    builtins.mapAttrs (name: value: {
+      data =
+        dns.types.zone.renderToString name value;
+    })
+    cfg.zones;
+
+  dns = import ./dns {inherit lib;};
+
+  ports = let
+    parsePorts = listeners: let
+      splitAddress = addr: lib.splitString "@" addr;
+
+      extractPort = addr: let
+        split = splitAddress addr;
+      in
+        lib.toInt (
+          if (builtins.length split) == 2
+          then builtins.elemAt split 1
+          else "53"
+        );
+    in
+      builtins.map extractPort listeners;
+  in
+    lib.unique (parsePorts cfg.interfaces);
+in {
+  options.vhack.dns = {
+    enable = lib.mkEnableOption "custom dns server";
+
+    openFirewall = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Open the following ports:
+        TCP (${lib.concatStringsSep ", " (map toString ports)})
+        UDP (${lib.concatStringsSep ", " (map toString ports)})
+      '';
+    };
+
+    interfaces = lib.mkOption {
+      type = lib.types.listOf lib.types.str;
+      description = ''
+        A list of the interfaces to bind to. To select the port add `@` to the end of the
+        interface. The default port is 53.
+      '';
+      example = [
+        "192.168.1.3"
+        "2001:db8:1::3"
+      ];
+    };
+
+    zones = lib.mkOption {
+      type = lib.types.attrsOf dns.types.zone.zone;
+      description = "DNS zones";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    services.nsd = {
+      enable = true;
+      verbosity = 4;
+      inherit (cfg) interfaces;
+      inherit zones;
+    };
+
+    networking.firewall.allowedUDPPorts = lib.mkIf cfg.openFirewall ports;
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall ports;
+
+    systemd.services.nsd = {
+      requires = [
+        "network-online.target"
+      ];
+      after = [
+        "network.target"
+        "network-online.target"
+      ];
+    };
+  };
+}
diff --git a/modules/by-name/re/redlib/module.nix b/modules/by-name/re/redlib/module.nix
index 2b20c66..eb5edba 100644
--- a/modules/by-name/re/redlib/module.nix
+++ b/modules/by-name/re/redlib/module.nix
@@ -31,14 +31,11 @@ in {
         enableACME = true;
         forceSSL = true;
       };
+    };
 
-      # TODO: Remove this at a certain point. <2024-12-19>
-      virtualHosts."libreddit.vhack.eu" = {
-        locations."/".return = "301 https://${domain}";
-
-        forceSSL = true;
-        enableACME = true;
-      };
+    # TODO(@bpeetz): Remove this at some point. <2025-02-04>
+    vhack.nginx.redirects = {
+      "libreddit.vhack.eu" = "${domain}";
     };
   };
 }
diff --git a/modules/by-name/st/stalwart-mail/module.nix b/modules/by-name/st/stalwart-mail/module.nix
new file mode 100644
index 0000000..6905005
--- /dev/null
+++ b/modules/by-name/st/stalwart-mail/module.nix
@@ -0,0 +1,392 @@
+{
+  lib,
+  config,
+  pkgs,
+  vhackPackages,
+  ...
+}: let
+  cfg = config.vhack.stalwart-mail;
+  topCfg = config.services.stalwart-mail;
+
+  configFormat = pkgs.formats.toml {};
+  configFile = configFormat.generate "stalwart-mail.toml" topCfg.settings;
+in {
+  imports = [
+    ./settings.nix
+  ];
+
+  options.vhack.stalwart-mail = {
+    enable = lib.mkEnableOption "starwart-mail";
+
+    package = lib.mkPackageOption vhackPackages "stalwart-mail-free" {};
+
+    admin = lib.mkOption {
+      description = ''
+        Email address to advertise as administrator. This is the address, where dkim, spv
+        etc. refusal reports are sent to.
+
+        The format should be: `mailto:<name>@<domain>`
+      '';
+      type = lib.types.str;
+      example = "mailto:dmarc+rua@example.com";
+      default = "";
+    };
+
+    fqdn = lib.mkOption {
+      type = lib.types.str;
+      example = "mail.foss-syndicate.org";
+      description = ''
+        The fully qualified domain name for this mail server.
+      '';
+    };
+
+    principals = lib.mkOption {
+      default = [];
+      type = lib.types.listOf (lib.types.submodule {
+        options = {
+          name = lib.mkOption {
+            type = lib.types.str;
+            description = "Specifies the username of the account";
+          };
+
+          class = lib.mkOption {
+            type = lib.types.enum ["individual" "admin"];
+            description = "Specifies the account type";
+          };
+
+          description = lib.mkOption {
+            type = lib.types.str;
+            description = "Provides a description or full name for the user";
+            default = "";
+          };
+
+          secret = lib.mkOption {
+            type = lib.types.str;
+            description = ''
+              Sets the password for the user account.
+              Passwords can be stored hashed or in plain text (not recommended).
+              See <https://stalw.art/docs/auth/authentication/password/> for a description
+              of password encoding.
+            '';
+          };
+          email = lib.mkOption {
+            type = lib.types.listOf lib.types.str;
+            description = ''
+              A list of email addresses associated with the user.
+              The first address in the list is considered the primary address.
+            '';
+          };
+        };
+      });
+    };
+
+    dataDirectory = lib.mkOption {
+      description = ''
+        The directory in which to store all storage things.
+      '';
+      default = "/var/lib/stalwart-mail";
+      type = lib.types.path;
+      readOnly = true;
+    };
+
+    openFirewall = lib.mkOption {
+      type = lib.types.bool;
+      default = false;
+      description = ''
+        Whether to open TCP firewall ports, which are specified in
+        {option}`services.stalwart-mail.settings.listener` on all interfaces.
+      '';
+    };
+
+    security = lib.mkOption {
+      type = lib.types.nullOr (lib.types.submodule {
+        options = {
+          verificationMode = lib.mkOption {
+            type = lib.types.enum ["relaxed" "strict"];
+            description = ''
+              Whether to allow invalid signatures/checks or not.
+            '';
+            default = "relaxed";
+          };
+
+          dkimKeys = lib.mkOption {
+            type = lib.types.attrsOf (lib.types.submodule {
+              options = {
+                dkimPublicKey = lib.mkOption {
+                  type = lib.types.str;
+                  description = ''
+                    The base 64 encoded representation of the public dkim key.
+                  '';
+                };
+                dkimPrivateKeyPath = lib.mkOption {
+                  type = lib.types.path;
+                  description = ''
+                    The path to the dkim private key agenix file.
+                    Generate it via the `./gen_key` script:
+                  '';
+                };
+                keyAlgorithm = lib.mkOption {
+                  type = lib.types.enum ["ed25519-sha256" "rsa-sha-256" "rsa-sha-1"];
+                  description = "The algorithm of the used key";
+                };
+              };
+            });
+            description = ''
+              Which key to use for which domain. The attr keys are the domains
+            '';
+            default = {};
+          };
+        };
+      });
+      description = ''
+        Security options. This should only be set to `null` when testing.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.admin != "";
+        message = "You need to specify an admin address.";
+      }
+    ];
+
+    vhack.nginx.enable = true;
+    services = {
+      stalwart-mail = {
+        # NOTE(@bpeetz): We do not use the NixOS service, as it comes with too much
+        # bothersome default configuration and not really any useful configuration.
+        # However, this decision could obviously be reversed in the future. <2025-02-08>
+        enable = false;
+        inherit (cfg) package;
+        # dataDir = cfg.dataDirectory;
+      };
+
+      # FIXME(@bpeetz): This is currently needed for a successful acme http-01 challenge.
+      # We could also use the DNS challenge. <2025-03-01>
+      nginx.virtualHosts."${cfg.fqdn}" = {
+        enableACME = false;
+        extraConfig =
+          # This is copied directly from the nixos nginx module.
+          # Rule for legitimate ACME Challenge requests (like /.well-known/acme-challenge/xxxxxxxxx)
+          # We use ^~ here, so that we don't check any regexes (which could
+          # otherwise easily override this intended match accidentally).
+          ''
+            location ^~ /.well-known/acme-challenge/ {
+              root ${config.security.acme.certs.${cfg.fqdn}.webroot};
+              auth_basic off;
+              auth_request off;
+            }
+          '';
+      };
+
+      redis = {
+        servers = {
+          "stalwart-mail" = {
+            enable = true;
+
+            user = "stalwart-mail";
+
+            # Disable TCP listening. (We have a UNIX socket)
+            port = 0;
+            bind = null;
+
+            settings = {
+              protected-mode = true;
+              enable-protected-configs = false;
+              enable-debug-command = false;
+              enable-module-command = false;
+
+              supervised = "systemd";
+              stop-writes-on-bgsave-error = true;
+              sanitize-dump-payload = "clients";
+            };
+          };
+        };
+      };
+    };
+    security.acme.certs = {
+      "${cfg.fqdn}" = {
+        domain = cfg.fqdn;
+        group = "stalwart-mail";
+      };
+    };
+
+    age.secrets = let
+      keys =
+        lib.mapAttrs' (
+          keyDomain: keyConfig:
+            lib.nameValuePair "stalwartMail${keyDomain}"
+            {
+              file = keyConfig.dkimPrivateKeyPath;
+              mode = "600";
+              owner = "stalwart-mail";
+              group = "stalwart-mail";
+            }
+        )
+        cfg.security.dkimKeys;
+    in
+      lib.mkIf (cfg.security != null) keys;
+
+    vhack.persist.directories = [
+      {
+        directory = "${cfg.dataDirectory}";
+        user = "stalwart-mail";
+        group = "stalwart-mail";
+        mode = "0700";
+      }
+      {
+        directory = "${config.services.redis.servers."stalwart-mail".settings.dir}";
+        user = "stalwart-mail";
+        group = "redis";
+        mode = "0770";
+      }
+    ];
+
+    # This service stores a potentially large amount of data.
+    # Running it as a dynamic user would force chown to be run every time the
+    # service is restarted on a potentially large number of files.
+    # That would cause unnecessary and unwanted delays.
+    users = {
+      groups.stalwart-mail = {};
+      users.stalwart-mail = {
+        isSystemUser = true;
+        group = "stalwart-mail";
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.dataDirectory}' - stalwart-mail stalwart-mail - -"
+    ];
+
+    systemd = {
+      services.stalwart-mail = {
+        wantedBy = ["multi-user.target"];
+        requires =
+          [
+            "redis-stalwart-mail.service"
+            "network-online.target"
+          ]
+          ++ (lib.optional (cfg.security != null) "acme-${cfg.fqdn}.service");
+        after = [
+          "local-fs.target"
+          "network.target"
+          "network-online.target"
+          "redis-stalwart-mail.service"
+          "acme-${cfg.fqdn}.service"
+        ];
+        conflicts = [
+          "postfix.service"
+          "sendmail.service"
+          "exim4.service"
+        ];
+        description = "Stalwart Mail Server";
+
+        environment = {
+          SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
+          NIX_SSL_CERT_FILE = "/etc/ssl/certs/ca-certificates.crt";
+        };
+
+        preStart = let
+          esa = lib.strings.escapeShellArg;
+          mkTmpFile = path: "[ -d ${esa path} ] || mkdir --parents ${esa path}";
+
+          # Create the directories for stalwart
+          storageDirectories = lib.lists.filter (v: v != null) (lib.attrsets.mapAttrsToList (_: {path ? null, ...}:
+            if (path != null)
+            then mkTmpFile path
+            else null)
+          topCfg.settings.store);
+        in
+          ''
+            # Stalwart actually wants to store _data_ (e.g., blocked ips) in it's own config file.
+            # Thus we need to make it writable.
+            cat ${esa configFile} >$CACHE_DIRECTORY/mutable_config_file.toml
+          ''
+          + (builtins.concatStringsSep "\n" storageDirectories);
+
+        serviceConfig = {
+          ExecStart = pkgs.writers.writeDash "start-stalwart-mail" ''
+            ${lib.getExe cfg.package} --config="$CACHE_DIRECTORY/mutable_config_file.toml"
+          '';
+
+          Restart = "on-failure";
+          RestartSec = 5;
+
+          KillMode = "process";
+          KillSignal = "SIGINT";
+
+          Type = "simple";
+          LimitNOFILE = 65536;
+
+          StandardOutput = "journal";
+          StandardError = "journal";
+
+          ReadWritePaths = [
+            cfg.dataDirectory
+          ];
+          CacheDirectory = "stalwart-mail";
+          StateDirectory = "stalwart-mail";
+
+          User = "stalwart-mail";
+          Group = "stalwart-mail";
+
+          SyslogIdentifier = "stalwart-mail";
+
+          # Bind standard privileged ports
+          AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+          CapabilityBoundingSet = ["CAP_NET_BIND_SERVICE"];
+
+          # Hardening
+          DeviceAllow = [""];
+          LockPersonality = true;
+          MemoryDenyWriteExecute = true;
+          PrivateDevices = true;
+          PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE
+          ProcSubset = "pid";
+          PrivateTmp = true;
+          ProtectClock = true;
+          ProtectControlGroups = true;
+          ProtectHome = true;
+          ProtectHostname = true;
+          ProtectKernelLogs = true;
+          ProtectKernelModules = true;
+          ProtectKernelTunables = true;
+          ProtectProc = "invisible";
+          ProtectSystem = "strict";
+          RestrictAddressFamilies = [
+            "AF_INET"
+            "AF_INET6"
+            "AF_UNIX"
+          ];
+          RestrictNamespaces = true;
+          RestrictRealtime = true;
+          RestrictSUIDSGID = true;
+          SystemCallArchitectures = "native";
+          SystemCallFilter = [
+            "@system-service"
+            "~@privileged"
+          ];
+          UMask = "0077";
+        };
+      };
+    };
+
+    # Make admin commands available in the shell
+    environment.systemPackages = [cfg.package];
+
+    networking.firewall = let
+      parsePorts = listeners: let
+        parseAddresses = listeners: lib.flatten (lib.mapAttrsToList (name: value: value.bind) listeners);
+        splitAddress = addr: lib.splitString ":" addr;
+        extractPort = addr: lib.toInt (builtins.foldl' (a: b: b) "" (splitAddress addr));
+      in
+        builtins.map extractPort (parseAddresses listeners);
+    in
+      lib.mkIf (cfg.openFirewall && (builtins.hasAttr "listener" topCfg.settings.server))
+      {
+        allowedTCPPorts = parsePorts topCfg.settings.server.listener;
+      };
+  };
+}
diff --git a/modules/by-name/st/stalwart-mail/settings.nix b/modules/by-name/st/stalwart-mail/settings.nix
new file mode 100644
index 0000000..7032ae0
--- /dev/null
+++ b/modules/by-name/st/stalwart-mail/settings.nix
@@ -0,0 +1,532 @@
+{
+  config,
+  lib,
+  pkgs,
+  ...
+}: let
+  cfg = config.vhack.stalwart-mail;
+
+  signaturesByDomain =
+    (builtins.map ({name, ...}: {
+        "if" = "sender_domain = '${name}'";
+        "then" = "'${name}'";
+      })
+      (lib.attrsToList cfg.security.dkimKeys))
+    ++ [{"else" = false;}];
+
+  maybeVerificationMode =
+    if cfg.security != null
+    then cfg.security.verificationMode
+    else "disable";
+in {
+  config.services.stalwart-mail.settings = lib.mkIf cfg.enable {
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.3
+    signature = let
+      signatures =
+        lib.mapAttrs (keyDomain: keyConfig: {
+          private-key = "%{file:${config.age.secrets."stalwartMail${keyDomain}".path}}%";
+
+          domain = keyDomain;
+
+          selector = "mail";
+          headers = ["From" "To" "Cc" "Date" "Subject" "Message-ID" "Organization" "MIME-Version" "Content-Type" "In-Reply-To" "References" "List-Id" "User-Agent" "Thread-Topic" "Thread-Index"];
+          algorithm = keyConfig.keyAlgorithm;
+
+          # How do we canonicalize the headers/body?
+          # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.4
+          canonicalization = "simple/simple";
+
+          expire = "50d";
+          report = true;
+        })
+        cfg.security.dkimKeys;
+    in
+      lib.mkIf (cfg.security != null) signatures;
+
+    auth = let
+      # NOTE(@bpeetz): We disable all the checks if the `listener` is submissions, because the
+      # user's email client will obviously not have the right IP address to pass SPF or
+      # IPREV. It will also not be able to sign the message with DKIM (as we has to key). <2025-02-25>
+      ifNotSmpt = valueTrue: valueFalse: [
+        {
+          "if" = "listener != 'submissions'";
+          "then" = valueTrue;
+        }
+        {"else" = valueFalse;}
+      ];
+    in {
+      iprev = {
+        verify = ifNotSmpt maybeVerificationMode "disable";
+      };
+      spf = {
+        verify = {
+          ehlo = ifNotSmpt maybeVerificationMode "disable";
+
+          mail-from = ifNotSmpt maybeVerificationMode "disable";
+        };
+      };
+      dmarc = {
+        verify = ifNotSmpt maybeVerificationMode "disable";
+      };
+      arc = {
+        seal = lib.mkIf (cfg.security != null) signaturesByDomain;
+        verify = ifNotSmpt maybeVerificationMode "disable";
+      };
+      dkim = {
+        verify = ifNotSmpt maybeVerificationMode "disable";
+
+        # Ignore insecure dkim signed messages (i.e., messages containing both
+        # signed and appended not-signed content.)
+        strict = true;
+
+        sign =
+          lib.mkIf (cfg.security != null) signaturesByDomain;
+      };
+    };
+    report = {
+      domain = "${cfg.fqdn}";
+      submitter = "'${cfg.fqdn}'";
+      analysis = {
+        addresses = ["dmarc@*" "abuse@*"];
+        forward = true;
+        store = "30d";
+      };
+      tls.aggregate = {
+        from-name = "'TLS Report'";
+        from-address = "'noreply-tls@${cfg.fqdn}'";
+        org-name = "'Foss Syndicate Mail Handling'";
+        contact-info = "'${cfg.admin}'";
+        send = "daily";
+        max-size = 26214400; # 25 MiB
+        sign = lib.mkIf (cfg.security != null) "'${cfg.fqdn}'";
+      };
+      dmarc = {
+        aggregate = {
+          from-name = "'DMARC Report'";
+          from-address = "'noreply-dmarc@${cfg.fqdn}'";
+          org-name = "'Foss Syndicate Mail Handling'";
+          contact-info = "'${cfg.admin}'";
+          send = "weekly";
+          max-size = 26214400; # 25MiB
+          sign = lib.mkIf (cfg.security != null) "'${cfg.fqdn}'";
+        };
+        from-name = "'Report Subsystem'";
+        from-address = "'noreply-dmarc@${cfg.fqdn}'";
+        subject = "'DMARC Authentication Failure Report'";
+        send = "1/1d";
+        sign = lib.mkIf (cfg.security != null) signaturesByDomain;
+      };
+      spf = {
+        from-name = "'Report Subsystem'";
+        from-address = "'noreply-spf@${cfg.fqdn}'";
+        subject = "'SPF Authentication Failure Report'";
+        send = "1/1d";
+        sign = lib.mkIf (cfg.security != null) signaturesByDomain;
+      };
+      dkim = {
+        from-name = "'Report Subsystem'";
+        from-address = "'noreply-dkim@${cfg.fqdn}'";
+        subject = "'DKIM Authentication Failure Report'";
+        send = "1/1d";
+        sign = lib.mkIf (cfg.security != null) signaturesByDomain;
+      };
+      dsn = {
+        from-name = "'Mail Delivery Subsystem'";
+        from-address = "'MAILER-DAEMON@${cfg.fqdn}'";
+        sign = lib.mkIf (cfg.security != null) signaturesByDomain;
+      };
+    };
+    queue = {
+      schedule = {
+        retry = "[2m, 5m, 10m, 15m, 30m, 1h, 2h]";
+        notify = "[2h, 7h, 1d, 3d]";
+        expire = "5d";
+      };
+      outbound = {
+        tls = {
+          starttls =
+            if maybeVerificationMode == "strict"
+            then "require"
+            else "optional";
+          allow-invalid-certs = false;
+          ip-strategy = "ipv6_then_ipv4";
+          mta-sts =
+            if maybeVerificationMode == "strict"
+            then "require"
+            else "optional";
+        };
+      };
+    };
+    resolver = {
+      type = "system";
+      preserve-intermediates = true;
+      concurrency = 2;
+      timeout = "5s";
+      attempts = 2;
+      try-tcp-on-error = true;
+      public-suffix = [
+        "file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat"
+      ];
+    };
+
+    spam-filter = {
+      enable = true;
+      header = {
+        status = {
+          enable = true;
+          name = "X-Spam-Status";
+        };
+        result = {
+          enable = true;
+          name = "X-Spam-Result";
+        };
+      };
+      bayes = {
+        enable = true;
+
+        # Learn from users putting mail into JUNK or taking mail out of it.
+        account = {
+          enable = true;
+        };
+      };
+
+      # Fetch the newest spam-filter rules not from github, but from the nix
+      # package.
+      resource = "file://${cfg.package.passthru.spamfilter}/spam-filter.toml";
+      auto-update = false;
+    };
+
+    webadmin = {
+      # Fetch the newest webadmin bundle not from github, but from the nix
+      # package.
+      resource = "file://${cfg.package.passthru.webadmin}/webadmin.zip";
+      auto-update = false;
+      path = "/var/cache/stalwart-mail";
+    };
+
+    session = {
+      milter = {
+        # TODO: Add this <2025-02-07>
+        # "clamav" = {
+        #   enable = true;
+        #   hostname = "127.0.0.1";
+        #   port = 15112;
+        #   tls = false;
+        #   allow-invalid-certs = false;
+        # };
+      };
+      ehlo = {
+        require = true;
+      };
+      rcpt = {
+        directory = "'in-memory'";
+        catch-all = true;
+        subaddressing = true;
+      };
+      data = {
+        spam-filter = true;
+        add-headers = {
+          received = true;
+          received-spf = true;
+          auth-results = true;
+          message-id = true;
+          date = true;
+          return-path = true;
+          delivered-to = true;
+        };
+        auth = {
+          mechanisms = ["LOGIN" "PLAIN"];
+          directory = "'in-memory'";
+          require = true;
+          must-match-sender = true;
+          errors = {
+            total = 3;
+            wait = "5s";
+          };
+        };
+      };
+      extensions = {
+        pipelining = true;
+        chunking = true;
+        requiretls = true;
+        no-soliciting = "";
+        dsn = [
+          {
+            "if" = "!is_empty(authenticated_as)";
+            "then" = true;
+          }
+          {"else" = false;}
+        ];
+        future-release = [
+          {
+            "if" = "!is_empty(authenticated_as)";
+            "then" = "7d";
+          }
+          {"else" = false;}
+        ];
+        deliver-by = [
+          {
+            "if" = "!is_empty(authenticated_as)";
+            "then" = "15d";
+          }
+          {"else" = false;}
+        ];
+        mt-priority = [
+          {
+            "if" = "!is_empty(authenticated_as)";
+            "then" = "mixer";
+          }
+          {"else" = false;}
+        ];
+        vrfy = [
+          {
+            "if" = "!is_empty(authenticated_as)";
+            "then" = true;
+          }
+          {"else" = false;}
+        ];
+        expn = [
+          {
+            "if" = "!is_empty(authenticated_as)";
+            "then" = true;
+          }
+          {"else" = false;}
+        ];
+      };
+    };
+
+    jmap = {
+      account = {
+        purge.frequency = "0 0 *";
+      };
+      protocol = {
+        changes.max-history = "14d";
+      };
+      email = {
+        # NOTE(@bpeetz): We probably want to enable the auto-deletion of emails in
+        # the "Junk" and "Deleted" items mail folders, but this should be
+        # communicated to the users. <2025-02-07>
+        auto-expunge = false;
+      };
+      mailbox = {
+        max-depth = 50;
+        max-name-length = 255;
+      };
+      folders = let
+        mkFolder = name: {
+          inherit name;
+          create = true;
+          subscribe = true;
+        };
+      in {
+        inbox = mkFolder "INBOX";
+        drafts = mkFolder "DRAFTS";
+        sent = mkFolder "SENT";
+        trash = mkFolder "TRASH";
+        archive = mkFolder "ARCHIVE";
+        junk = mkFolder "JUNK";
+        shared = {name = "SHARED";};
+      };
+    };
+    imap = {
+      auth = {
+        # Allow password login over non tls connection
+        allow-plain-text = false;
+      };
+    };
+
+    server = {
+      hostname = cfg.fqdn;
+
+      listener = {
+        # TODO(@bpeetz): Add this <2025-02-08>
+        # # HTTP (used for jmap)
+        # "http" = {
+        #   bind = ["[::]:8080"];
+        #   protocol = "http";
+        #   tls.implicit = true;
+        # };
+
+        # IMAP
+        "imap" = {
+          bind = ["[::]:993"];
+          protocol = "imap";
+          tls.implicit = true;
+        };
+
+        # SMTP
+        "submissions" = {
+          bind = ["[::]:465"];
+          protocol = "smtp";
+          tls.implicit = true;
+        };
+        "input" = {
+          bind = ["[::]:25"];
+          protocol = "smtp";
+          tls = {
+            enable = true;
+            # Require an explicit `STARTTLS`
+            implicit = false;
+          };
+        };
+
+        # # POP3 (should be disabled, unless there is a real reason to use it)
+        # "pop3" = {
+        #   bind = ["[::]:995"];
+        #   protocol = "pop3";
+        #   tls.implicit = true;
+        # };
+
+        # # LMTP
+        # "lmtp" = {
+        #   bind = ["[::]:24"];
+        #   protocol = "lmtp";
+        # };
+
+        # ManageSieve
+        "managesieve" = {
+          bind = ["[::]:4190"];
+          protocol = "managesieve";
+          tls.implicit = true;
+        };
+      };
+
+      tls = {
+        enable = true;
+
+        # Expect the client connection to be encrypted from the start (i.e.,
+        # without STARTTLS)
+        implicit = true;
+
+        certificate = "default";
+      };
+
+      # TODO(@bpeetz): Configure that <2025-02-07>
+      # http = {
+      #   url = "";
+      #   allowed-endpoint = ["404"];
+      # };
+
+      auto-ban = {
+        # Ban if the same IP fails to login 10 times in a day
+        rate = "10/1d";
+
+        # Ban the login for an user account, if different IP-Addresses tried and
+        # failed to login 100 times in single day
+        auth.rate = "100/1d";
+
+        abuse.rate = "35/1d";
+
+        loiter.rate = "150/1d";
+
+        scan.rate = "150/1d";
+      };
+
+      cache = let
+        MiB = 1024 * 1024;
+      in {
+        access-token.size = 10 * MiB;
+        http-auth.size = 1 * MiB;
+        permission.size = 5 * MiB;
+        account.size = 10 * MiB;
+        mailbox.size = 10 * MiB;
+        thread.size = 10 * MiB;
+        bayes.size = 10 * MiB;
+        dns = {
+          txt.size = 5 * MiB;
+          mx.size = 5 * MiB;
+          ptr.size = 1 * MiB;
+          ipv4.size = 5 * MiB;
+          ipv6.size = 5 * MiB;
+          tlsa.size = 1 * MiB;
+          mta-sts.size = 1 * MiB;
+          rbl.size = 5 * MiB;
+        };
+      };
+    };
+
+    tracer = {
+      # NOTE(@bpeetz):
+      # We are using the console logger, because that has nice color output.
+      # Simply using the console should be fine, as systemd pipes that to the journal
+      # either way. <2025-02-08>
+      console = {
+        enable = true;
+        ansi = true;
+        level = "info";
+        type = "console";
+      };
+    };
+
+    store = {
+      "rocksdb-data" = {
+        type = "rocksdb";
+        path = "${cfg.dataDirectory}/storage/data";
+        compression = "lz4";
+
+        # Perform “maintenance” every day at 3 am local time.
+        purge.frequency = "0 3 *";
+      };
+      "rocksdb-full-text-search" = {
+        type = "rocksdb";
+        path = "${cfg.dataDirectory}/storage/full-text-search";
+        compression = "lz4";
+
+        # Perform “maintenance” every day at 2 am local time.
+        purge.frequency = "0 2 *";
+      };
+      "file-system" = {
+        type = "fs";
+        path = "${cfg.dataDirectory}/storage/blobs";
+        depth = 2;
+        compression = "lz4";
+
+        # Perform “maintenance” every day at 5:30 am local time.
+        purge.frequency = "30 5 *";
+      };
+      "redis" = {
+        type = "redis";
+        redis-type = "single";
+        urls = "unix://${config.services.redis.servers."stalwart-mail".unixSocket}";
+        timeout = "10s";
+
+        # Perform “maintenance” every day at 2:30 am local time.
+        purge.frequency = "30 2 *";
+      };
+    };
+    storage = {
+      # PostgreSQL is an option, but this is recommended for single node
+      # configurations.
+      data = "rocksdb-data";
+
+      # We could also re-use the data storage backend for that.
+      blob = "file-system";
+
+      full-text.default-language = "en";
+      fts = "rocksdb-full-text-search";
+
+      directory = "in-memory";
+
+      lookup = "redis";
+
+      # NOTE(@bpeetz): This will encrypt all emails with the users pgp key (if it
+      # can be determined.) This is a wonderful feature, but quite tiresome, if
+      # the user intends to read their email without a their pgp key present (for
+      # example via their smartphone.) <2025-02-07>
+      encryption.enable = false;
+    };
+
+    directory."in-memory" = {
+      type = "memory";
+      inherit (cfg) principals;
+    };
+
+    certificate = {
+      "default" = {
+        cert = "%{file:${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem}%";
+        private-key = "%{file:${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem}%";
+        default = true;
+      };
+    };
+  };
+}
diff --git a/modules/by-name/sy/system-info/module.nix b/modules/by-name/sy/system-info/module.nix
new file mode 100644
index 0000000..de75e29
--- /dev/null
+++ b/modules/by-name/sy/system-info/module.nix
@@ -0,0 +1,68 @@
+{
+  lib,
+  config,
+  pkgs,
+  ...
+}: let
+  mkVirtualHostDisplay = name: value: let
+    aliases =
+      if value.serverAliases != []
+      then
+        ": "
+        + builtins.concatStringsSep " " value.serverAliases
+      else "";
+  in ''
+    ${name}${aliases}
+  '';
+  vHosts = builtins.concatStringsSep "" (builtins.attrValues (builtins.mapAttrs mkVirtualHostDisplay config.services.nginx.virtualHosts));
+
+  mkOpenPortDisplay = mode: port: let
+    checkEnabled = service: name:
+      if config.vhack.${service}.enable
+      then name
+      else "<port is '${name}' but service 'vhack.${service}' is not enabled.>";
+    mappings = {
+      "22" = checkEnabled "openssh" "ssh";
+      "80" = checkEnabled "nginx" "http";
+      "443" = checkEnabled "nginx" "https";
+
+      "24" = checkEnabled "mail" "mail-lmtp";
+      "465" = checkEnabled "mail" "mail-smtp-tls";
+      "25" = checkEnabled "mail" "mail-smtp";
+      "993" = checkEnabled "mail" "mail-imap-tls";
+      "995" = checkEnabled "mail" "mail-pop3-tls";
+
+      # TODO(@bpeetz): Check which service opens these ports: <2025-01-28>
+      "64738" = "???";
+    };
+  in ''
+    ${mode} ${builtins.toString port}: ${mappings.${builtins.toString port}}
+  '';
+
+  # TODO(@bpeetz): This should probably also include the allowed TCP/UDP port ranges. <2025-01-28>
+  openTCPPorts = builtins.concatStringsSep "" (builtins.map (mkOpenPortDisplay "TCP") config.networking.firewall.allowedTCPPorts);
+  openUDPPorts = builtins.concatStringsSep "" (builtins.map (mkOpenPortDisplay "UDP") config.networking.firewall.allowedUDPPorts);
+
+  markdown = pkgs.writeText "${config.networking.hostName}-system-info.md" ''
+    ## Virtual Hosts
+    ${vHosts}
+    ## Open ports
+    ${openTCPPorts}
+    ${openUDPPorts}
+  '';
+in {
+  options.vhack.system-info = {
+    markdown = lib.mkOption {
+      type = lib.types.package;
+      description = ''
+        A derivation, that builds a markdown file, showing relevant system
+        information for this host.
+      '';
+      readOnly = true;
+    };
+  };
+
+  config.vhack.system-info = {
+    inherit markdown;
+  };
+}
diff --git a/pkgs/by-name/ba/back/.envrc b/pkgs/by-name/ba/back/.envrc
index e86f040..66c2f00 100644
--- a/pkgs/by-name/ba/back/.envrc
+++ b/pkgs/by-name/ba/back/.envrc
@@ -17,6 +17,5 @@ watch_file flake.nix
 PATH_add ./scripts
 
 if on_git_branch; then
-    echo && git status --short --branch &&
-        echo && git fetch --verbose
+    echo && git status --short --branch
 fi
diff --git a/pkgs/by-name/ba/back/Cargo.lock b/pkgs/by-name/ba/back/Cargo.lock
index a200c3c..228a22f 100644
--- a/pkgs/by-name/ba/back/Cargo.lock
+++ b/pkgs/by-name/ba/back/Cargo.lock
@@ -34,7 +34,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
 dependencies = [
  "cfg-if",
- "getrandom",
  "once_cell",
  "version_check",
  "zerocopy",
@@ -111,11 +110,12 @@ dependencies = [
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.6"
+version = "3.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
+checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
 dependencies = [
  "anstyle",
+ "once_cell",
  "windows-sys 0.59.0",
 ]
 
@@ -132,43 +132,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
 
 [[package]]
-name = "async-stream"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
-dependencies = [
- "async-stream-impl",
- "futures-core",
- "pin-project-lite",
-]
-
-[[package]]
-name = "async-stream-impl"
-version = "0.3.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "async-trait"
-version = "0.1.83"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
 name = "atom_syndication"
-version = "0.12.6"
+version = "0.12.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec03a6e158ee0f38bfba811976ae909bc2505a4a2f4049c7e8df47df3497b119"
+checksum = "d2f68d23e2cb4fd958c705b91a6b4c80ceeaf27a9e11651272a8389d5ce1a4a3"
 dependencies = [
  "chrono",
  "derive_builder",
@@ -178,19 +145,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "atomic"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba"
-
-[[package]]
-name = "atomic"
-version = "0.6.0"
+name = "atomic-waker"
+version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994"
-dependencies = [
- "bytemuck",
-]
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
 
 [[package]]
 name = "autocfg"
@@ -202,16 +160,24 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
 name = "back"
 version = "0.1.0"
 dependencies = [
+ "bytes",
  "chrono",
  "clap",
  "gix",
+ "http",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "log",
  "markdown",
- "rocket",
+ "rinja",
  "rss",
  "serde",
  "serde_json",
  "sha2",
+ "stderrlog",
  "thiserror",
+ "tokio",
  "url",
 ]
 
@@ -227,20 +193,23 @@ dependencies = [
  "miniz_oxide",
  "object",
  "rustc-demangle",
- "windows-targets 0.52.6",
+ "windows-targets",
 ]
 
 [[package]]
-name = "binascii"
-version = "0.1.4"
+name = "basic-toml"
+version = "0.1.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72"
+checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
+dependencies = [
+ "serde",
+]
 
 [[package]]
 name = "bitflags"
-version = "2.6.0"
+version = "2.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
 
 [[package]]
 name = "block-buffer"
@@ -253,50 +222,38 @@ dependencies = [
 
 [[package]]
 name = "bstr"
-version = "1.11.1"
+version = "1.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8"
+checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
 dependencies = [
  "memchr",
- "regex-automata 0.4.9",
+ "regex-automata",
  "serde",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
-
-[[package]]
-name = "bytemuck"
-version = "1.21.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3"
-
-[[package]]
-name = "byteorder"
-version = "1.5.0"
+version = "3.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
 
 [[package]]
 name = "bytes"
-version = "1.9.0"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
 
 [[package]]
 name = "bytesize"
-version = "1.3.0"
+version = "1.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
+checksum = "2d2c12f985c78475a6b8d629afd0c360260ef34cfef52efccdcfd31972f81c2e"
 
 [[package]]
 name = "cc"
-version = "1.2.5"
+version = "1.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
+checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
 dependencies = [
  "shlex",
 ]
@@ -309,23 +266,23 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "chrono"
-version = "0.4.39"
+version = "0.4.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
 dependencies = [
  "android-tzdata",
  "iana-time-zone",
  "js-sys",
  "num-traits",
  "wasm-bindgen",
- "windows-targets 0.52.6",
+ "windows-link",
 ]
 
 [[package]]
 name = "clap"
-version = "4.5.23"
+version = "4.5.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84"
+checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -333,9 +290,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.23"
+version = "4.5.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838"
+checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
 dependencies = [
  "anstream",
  "anstyle",
@@ -345,9 +302,9 @@ dependencies = [
 
 [[package]]
 name = "clap_derive"
-version = "4.5.18"
+version = "4.5.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
+checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -374,17 +331,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
 
 [[package]]
-name = "cookie"
-version = "0.18.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
-dependencies = [
- "percent-encoding",
- "time",
- "version_check",
-]
-
-[[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -392,9 +338,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.16"
+version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
 dependencies = [
  "libc",
 ]
@@ -483,15 +429,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "deranged"
-version = "0.3.11"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
-dependencies = [
- "powerfmt",
-]
-
-[[package]]
 name = "derive_builder"
 version = "0.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -523,39 +460,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "devise"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d"
-dependencies = [
- "devise_codegen",
- "devise_core",
-]
-
-[[package]]
-name = "devise_codegen"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867"
-dependencies = [
- "devise_core",
- "quote",
-]
-
-[[package]]
-name = "devise_core"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7"
-dependencies = [
- "bitflags",
- "proc-macro2",
- "proc-macro2-diagnostics",
- "quote",
- "syn",
-]
-
-[[package]]
 name = "digest"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -592,12 +496,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
 
 [[package]]
-name = "either"
-version = "1.13.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
-
-[[package]]
 name = "encoding_rs"
 version = "0.8.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -608,9 +506,9 @@ dependencies = [
 
 [[package]]
 name = "equivalent"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "errno"
@@ -638,20 +536,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
 
 [[package]]
-name = "figment"
-version = "0.10.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
-dependencies = [
- "atomic 0.6.0",
- "pear",
- "serde",
- "toml",
- "uncased",
- "version_check",
-]
-
-[[package]]
 name = "filetime"
 version = "0.2.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -665,9 +549,9 @@ dependencies = [
 
 [[package]]
 name = "flate2"
-version = "1.0.35"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
+checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
@@ -680,6 +564,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
+name = "foldhash"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+
+[[package]]
 name = "form_urlencoded"
 version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -689,27 +579,12 @@ dependencies = [
 ]
 
 [[package]]
-name = "futures"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
-dependencies = [
- "futures-channel",
- "futures-core",
- "futures-io",
- "futures-sink",
- "futures-task",
- "futures-util",
-]
-
-[[package]]
 name = "futures-channel"
 version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
 dependencies = [
  "futures-core",
- "futures-sink",
 ]
 
 [[package]]
@@ -719,12 +594,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 
 [[package]]
-name = "futures-io"
-version = "0.3.31"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
-
-[[package]]
 name = "futures-sink"
 version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -742,28 +611,10 @@ version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 dependencies = [
- "futures-channel",
  "futures-core",
- "futures-io",
- "futures-sink",
  "futures-task",
- "memchr",
  "pin-project-lite",
  "pin-utils",
- "slab",
-]
-
-[[package]]
-name = "generator"
-version = "0.7.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e"
-dependencies = [
- "cc",
- "libc",
- "log",
- "rustversion",
- "windows",
 ]
 
 [[package]]
@@ -778,13 +629,14 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.15"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi",
+ "wasi 0.13.3+wasi-0.2.2",
+ "windows-targets",
 ]
 
 [[package]]
@@ -795,9 +647,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 
 [[package]]
 name = "gix"
-version = "0.69.1"
+version = "0.70.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d0eebdaecdcf405d5433a36f85e4f058cf4de48ee2604388be0dbccbaad353e"
+checksum = "736f14636705f3a56ea52b553e67282519418d9a35bb1e90b3a9637a00296b68"
 dependencies = [
  "gix-actor",
  "gix-archive",
@@ -855,9 +707,9 @@ dependencies = [
 
 [[package]]
 name = "gix-actor"
-version = "0.33.1"
+version = "0.33.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32b24171f514cef7bb4dfb72a0b06dacf609b33ba8ad2489d4c4559a03b7afb3"
+checksum = "20018a1a6332e065f1fcc8305c1c932c6b8c9985edea2284b3c79dc6fa3ee4b2"
 dependencies = [
  "bstr",
  "gix-date",
@@ -869,9 +721,9 @@ dependencies = [
 
 [[package]]
 name = "gix-archive"
-version = "0.18.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b63ef086543dce4f2cf9cb1ded1216bbd40332d3abcdd8d876e97f7812d9a26"
+checksum = "3d22c6ecdb350461a975159ebe514294064b9542a4cbc4a12d00c3f46a1107ce"
 dependencies = [
  "bstr",
  "gix-date",
@@ -883,9 +735,9 @@ dependencies = [
 
 [[package]]
 name = "gix-attributes"
-version = "0.23.1"
+version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddf9bf852194c0edfe699a2d36422d2c1f28f73b7c6d446c3f0ccd3ba232cadc"
+checksum = "f151000bf662ef5f641eca6102d942ee31ace80f271a3ef642e99776ce6ddb38"
 dependencies = [
  "bstr",
  "gix-glob",
@@ -900,27 +752,27 @@ dependencies = [
 
 [[package]]
 name = "gix-bitmap"
-version = "0.2.13"
+version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d48b897b4bbc881aea994b4a5bbb340a04979d7be9089791304e04a9fbc66b53"
+checksum = "b1db9765c69502650da68f0804e3dc2b5f8ccc6a2d104ca6c85bc40700d37540"
 dependencies = [
  "thiserror",
 ]
 
 [[package]]
 name = "gix-chunk"
-version = "0.4.10"
+version = "0.4.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6ffbeb3a5c0b8b84c3fe4133a6f8c82fa962f4caefe8d0762eced025d3eb4f7"
+checksum = "0b1f1d8764958699dc764e3f727cef280ff4d1bd92c107bbf8acd85b30c1bd6f"
 dependencies = [
  "thiserror",
 ]
 
 [[package]]
 name = "gix-command"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9405c0a56e17f8365a46870cd2c7db71323ecc8bda04b50cb746ea37bd091e90"
+checksum = "cb410b84d6575db45e62025a9118bdbf4d4b099ce7575a76161e898d9ca98df1"
 dependencies = [
  "bstr",
  "gix-path",
@@ -930,9 +782,9 @@ dependencies = [
 
 [[package]]
 name = "gix-commitgraph"
-version = "0.25.1"
+version = "0.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8da6591a7868fb2b6dabddea6b09988b0b05e0213f938dbaa11a03dd7a48d85"
+checksum = "e23a8ec2d8a16026a10dafdb6ed51bcfd08f5d97f20fa52e200bc50cb72e4877"
 dependencies = [
  "bstr",
  "gix-chunk",
@@ -944,9 +796,9 @@ dependencies = [
 
 [[package]]
 name = "gix-config"
-version = "0.42.0"
+version = "0.43.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6649b406ca1f99cb148959cf00468b231f07950f8ec438cc0903cda563606f19"
+checksum = "377c1efd2014d5d469e0b3cd2952c8097bce9828f634e04d5665383249f1d9e9"
 dependencies = [
  "bstr",
  "gix-config-value",
@@ -965,9 +817,9 @@ dependencies = [
 
 [[package]]
 name = "gix-config-value"
-version = "0.14.10"
+version = "0.14.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49aaeef5d98390a3bcf9dbc6440b520b793d1bf3ed99317dc407b02be995b28e"
+checksum = "11365144ef93082f3403471dbaa94cfe4b5e72743bdb9560719a251d439f4cee"
 dependencies = [
  "bitflags",
  "bstr",
@@ -978,9 +830,9 @@ dependencies = [
 
 [[package]]
 name = "gix-credentials"
-version = "0.26.0"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82a50c56b785c29a151ab4ccf74a83fe4e21d2feda0d30549504b4baed353e0a"
+checksum = "cf950f9ee1690bb9c4388b5152baa8a9f41ad61e5cf1ba0ec8c207b08dab9e45"
 dependencies = [
  "bstr",
  "gix-command",
@@ -1007,17 +859,20 @@ dependencies = [
 
 [[package]]
 name = "gix-diff"
-version = "0.49.0"
+version = "0.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8e92566eccbca205a0a0f96ffb0327c061e85bc5c95abbcddfe177498aa04f6"
+checksum = "62afb7f4ca0acdf4e9dad92065b2eb1bf2993bcc5014b57bc796e3a365b17c4d"
 dependencies = [
  "bstr",
+ "gix-attributes",
  "gix-command",
  "gix-filter",
  "gix-fs",
  "gix-hash",
+ "gix-index",
  "gix-object",
  "gix-path",
+ "gix-pathspec",
  "gix-tempfile",
  "gix-trace",
  "gix-traverse",
@@ -1028,9 +883,9 @@ dependencies = [
 
 [[package]]
 name = "gix-dir"
-version = "0.11.0"
+version = "0.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fba2ffbcf4bd34438e8a8367ccbc94870549903d1f193a14f47eb6b0967e1293"
+checksum = "c1d78db3927a12f7d1b788047b84efacaab03ef25738bd1c77856ad8966bd57b"
 dependencies = [
  "bstr",
  "gix-discover",
@@ -1048,9 +903,9 @@ dependencies = [
 
 [[package]]
 name = "gix-discover"
-version = "0.37.0"
+version = "0.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83bf6dfa4e266a4a9becb4d18fc801f92c3f7cc6c433dd86fdadbcf315ffb6ef"
+checksum = "d0c2414bdf04064e0f5a5aa029dfda1e663cf9a6c4bfc8759f2d369299bb65d8"
 dependencies = [
  "bstr",
  "dunce",
@@ -1064,9 +919,9 @@ dependencies = [
 
 [[package]]
 name = "gix-features"
-version = "0.39.1"
+version = "0.40.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d85d673f2e022a340dba4713bed77ef2cf4cd737d2f3e0f159d45e0935fd81f"
+checksum = "8bfdd4838a8d42bd482c9f0cb526411d003ee94cc7c7b08afe5007329c71d554"
 dependencies = [
  "bytes",
  "bytesize",
@@ -1087,9 +942,9 @@ dependencies = [
 
 [[package]]
 name = "gix-filter"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d0ecdee5667f840ba20c7fe56d63f8e1dc1e6b3bfd296151fe5ef07c874790a"
+checksum = "bdcc36cd7dbc63ed0ec3558645886553d1afd3cd09daa5efb9cba9cceb942bbb"
 dependencies = [
  "bstr",
  "encoding_rs",
@@ -1108,9 +963,9 @@ dependencies = [
 
 [[package]]
 name = "gix-fs"
-version = "0.12.1"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b3d4fac505a621f97e5ce2c69fdc425742af00c0920363ca4074f0eb48b1db9"
+checksum = "182e7fa7bfdf44ffb7cfe7451b373cdf1e00870ac9a488a49587a110c562063d"
 dependencies = [
  "fastrand",
  "gix-features",
@@ -1119,9 +974,9 @@ dependencies = [
 
 [[package]]
 name = "gix-glob"
-version = "0.17.1"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaf69a6bec0a3581567484bf99a4003afcaf6c469fd4214352517ea355cf3435"
+checksum = "4e9c7249fa0a78f9b363aa58323db71e0a6161fd69860ed6f48dedf0ef3a314e"
 dependencies = [
  "bitflags",
  "bstr",
@@ -1131,9 +986,9 @@ dependencies = [
 
 [[package]]
 name = "gix-hash"
-version = "0.15.1"
+version = "0.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b5eccc17194ed0e67d49285e4853307e4147e95407f91c1c3e4a13ba9f4e4ce"
+checksum = "e81c5ec48649b1821b3ed066a44efb95f1a268b35c1d91295e61252539fbe9f8"
 dependencies = [
  "faster-hex",
  "thiserror",
@@ -1141,9 +996,9 @@ dependencies = [
 
 [[package]]
 name = "gix-hashtable"
-version = "0.6.0"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ef65b256631078ef733bc5530c4e6b1c2e7d5c2830b75d4e9034ab3997d18fe"
+checksum = "189130bc372accd02e0520dc5ab1cef318dcc2bc829b76ab8d84bbe90ac212d1"
 dependencies = [
  "gix-hash",
  "hashbrown 0.14.5",
@@ -1152,9 +1007,9 @@ dependencies = [
 
 [[package]]
 name = "gix-ignore"
-version = "0.12.1"
+version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6b1fb24d2a4af0aa7438e2771d60c14a80cf2c9bd55c29cf1712b841f05bb8a"
+checksum = "4f529dcb80bf9855c0a7c49f0ac588df6d6952d63a63fefc254b9c869d2cdf6f"
 dependencies = [
  "bstr",
  "gix-glob",
@@ -1165,9 +1020,9 @@ dependencies = [
 
 [[package]]
 name = "gix-index"
-version = "0.37.0"
+version = "0.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "270645fd20556b64c8ffa1540d921b281e6994413a0ca068596f97e9367a257a"
+checksum = "acd12e3626879369310fffe2ac61acc828613ef656b50c4ea984dd59d7dc85d8"
 dependencies = [
  "bitflags",
  "bstr",
@@ -1186,16 +1041,16 @@ dependencies = [
  "itoa",
  "libc",
  "memmap2",
- "rustix",
+ "rustix 0.38.44",
  "smallvec",
  "thiserror",
 ]
 
 [[package]]
 name = "gix-lock"
-version = "15.0.1"
+version = "16.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd3ab68a452db63d9f3ebdacb10f30dba1fa0d31ac64f4203d395ed1102d940"
+checksum = "9739815270ff6940968441824d162df9433db19211ca9ba8c3fc1b50b849c642"
 dependencies = [
  "gix-tempfile",
  "gix-utils",
@@ -1204,9 +1059,9 @@ dependencies = [
 
 [[package]]
 name = "gix-mailmap"
-version = "0.25.1"
+version = "0.25.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6a108b866e00b8a59b8746906cccf2648ffc3e393dc9cca97254dd75c2ddf8c"
+checksum = "017996966133afb1e631796d8cf32e43300f8f76233f2a15ce9af5be5069b0a6"
 dependencies = [
  "bstr",
  "gix-actor",
@@ -1216,9 +1071,9 @@ dependencies = [
 
 [[package]]
 name = "gix-negotiate"
-version = "0.17.0"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d27f830a16405386e9c83b9d5be8261fe32bbd6b3caf15bd1b284c6b2b7ef1a8"
+checksum = "a6a8af1ef7bbe303d30b55312b7f4d33e955de43a3642ae9b7347c623d80ef80"
 dependencies = [
  "bitflags",
  "gix-commitgraph",
@@ -1232,9 +1087,9 @@ dependencies = [
 
 [[package]]
 name = "gix-object"
-version = "0.46.1"
+version = "0.47.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e42d58010183ef033f31088479b4eb92b44fe341b35b62d39eb8b185573d77ea"
+checksum = "ddc4b3a0044244f0fe22347fb7a79cca165e37829d668b41b85ff46a43e5fd68"
 dependencies = [
  "bstr",
  "gix-actor",
@@ -1253,9 +1108,9 @@ dependencies = [
 
 [[package]]
 name = "gix-odb"
-version = "0.66.0"
+version = "0.67.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb780eceb3372ee204469478de02eaa34f6ba98247df0186337e0333de97d0ae"
+checksum = "3e93457df69cd09573608ce9fa4f443fbd84bc8d15d8d83adecd471058459c1b"
 dependencies = [
  "arc-swap",
  "gix-date",
@@ -1274,9 +1129,9 @@ dependencies = [
 
 [[package]]
 name = "gix-pack"
-version = "0.56.0"
+version = "0.57.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4158928929be29cae7ab97afc8e820a932071a7f39d8ba388eed2380c12c566c"
+checksum = "fc13a475b3db735617017fb35f816079bf503765312d4b1913b18cf96f3fa515"
 dependencies = [
  "clru",
  "gix-chunk",
@@ -1293,9 +1148,9 @@ dependencies = [
 
 [[package]]
 name = "gix-packetline"
-version = "0.18.2"
+version = "0.18.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "911aeea8b2dabeed2f775af9906152a1f0109787074daf9e64224e3892dde453"
+checksum = "c7e5ae6bc3ac160a6bf44a55f5537813ca3ddb08549c0fd3e7ef699c73c439cd"
 dependencies = [
  "bstr",
  "faster-hex",
@@ -1305,9 +1160,9 @@ dependencies = [
 
 [[package]]
 name = "gix-packetline-blocking"
-version = "0.18.1"
+version = "0.18.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce9004ce1bc00fd538b11c1ec8141a1558fb3af3d2b7ac1ac5c41881f9e42d2a"
+checksum = "c1cbf8767c6abd5a6779f586702b5bcd8702380f4208219449cf1c9d0cd1e17c"
 dependencies = [
  "bstr",
  "faster-hex",
@@ -1317,9 +1172,9 @@ dependencies = [
 
 [[package]]
 name = "gix-path"
-version = "0.10.13"
+version = "0.10.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc292ef1a51e340aeb0e720800338c805975724c1dfbd243185452efd8645b7"
+checksum = "c40f12bb65a8299be0cfb90fe718e3be236b7a94b434877012980863a883a99f"
 dependencies = [
  "bstr",
  "gix-trace",
@@ -1330,9 +1185,9 @@ dependencies = [
 
 [[package]]
 name = "gix-pathspec"
-version = "0.8.1"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c472dfbe4a4e96fcf7efddcd4771c9037bb4fdea2faaabf2f4888210c75b81e"
+checksum = "6430d3a686c08e9d59019806faa78c17315fe22ae73151a452195857ca02f86c"
 dependencies = [
  "bitflags",
  "bstr",
@@ -1345,22 +1200,22 @@ dependencies = [
 
 [[package]]
 name = "gix-prompt"
-version = "0.9.0"
+version = "0.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82433a19aa44688e3bde05c692870eda50b5db053df53ed5ae6d8ea594a6babd"
+checksum = "79f2185958e1512b989a007509df8d61dca014aa759a22bee80cfa6c594c3b6d"
 dependencies = [
  "gix-command",
  "gix-config-value",
  "parking_lot",
- "rustix",
+ "rustix 0.38.44",
  "thiserror",
 ]
 
 [[package]]
 name = "gix-protocol"
-version = "0.47.0"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c84642e8b6fed7035ce9cc449593019c55b0ec1af7a5dce1ab8a0636eaaeb067"
+checksum = "6c61bd61afc6b67d213241e2100394c164be421e3f7228d3521b04f48ca5ba90"
 dependencies = [
  "bstr",
  "gix-date",
@@ -1377,9 +1232,9 @@ dependencies = [
 
 [[package]]
 name = "gix-quote"
-version = "0.4.14"
+version = "0.4.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "64a1e282216ec2ab2816cd57e6ed88f8009e634aec47562883c05ac8a7009a63"
+checksum = "e49357fccdb0c85c0d3a3292a9f6db32d9b3535959b5471bb9624908f4a066c6"
 dependencies = [
  "bstr",
  "gix-utils",
@@ -1388,9 +1243,9 @@ dependencies = [
 
 [[package]]
 name = "gix-ref"
-version = "0.49.1"
+version = "0.50.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a91b61776c839d0f1b7114901179afb0947aa7f4d30793ca1c56d335dfef485f"
+checksum = "47adf4c5f933429f8554e95d0d92eee583cfe4b95d2bf665cd6fd4a1531ee20c"
 dependencies = [
  "gix-actor",
  "gix-features",
@@ -1409,9 +1264,9 @@ dependencies = [
 
 [[package]]
 name = "gix-refspec"
-version = "0.27.0"
+version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00c056bb747868c7eb0aeb352c9f9181ab8ca3d0a2550f16470803500c6c413d"
+checksum = "59650228d8f612f68e7f7a25f517fcf386c5d0d39826085492e94766858b0a90"
 dependencies = [
  "bstr",
  "gix-hash",
@@ -1423,9 +1278,9 @@ dependencies = [
 
 [[package]]
 name = "gix-revision"
-version = "0.31.1"
+version = "0.32.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e1ddc474405a68d2ce8485705dd72fe6ce959f2f5fe718601ead5da2c8f9e7"
+checksum = "3fe28bbccca55da6d66e6c6efc6bb4003c29d407afd8178380293729733e6b53"
 dependencies = [
  "bitflags",
  "bstr",
@@ -1441,9 +1296,9 @@ dependencies = [
 
 [[package]]
 name = "gix-revwalk"
-version = "0.17.0"
+version = "0.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "510026fc32f456f8f067d8f37c34088b97a36b2229d88a6a5023ef179fcb109d"
+checksum = "d4ecb80c235b1e9ef2b99b23a81ea50dd569a88a9eb767179793269e0e616247"
 dependencies = [
  "gix-commitgraph",
  "gix-date",
@@ -1456,9 +1311,9 @@ dependencies = [
 
 [[package]]
 name = "gix-sec"
-version = "0.10.10"
+version = "0.10.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8b876ef997a955397809a2ec398d6a45b7a55b4918f2446344330f778d14fd6"
+checksum = "d84dae13271f4313f8d60a166bf27e54c968c7c33e2ffd31c48cafe5da649875"
 dependencies = [
  "bitflags",
  "gix-path",
@@ -1468,9 +1323,9 @@ dependencies = [
 
 [[package]]
 name = "gix-shallow"
-version = "0.1.0"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d2673242e87492cb6ff671f0c01f689061ca306c4020f137197f3abc84ce01"
+checksum = "ab72543011e303e52733c85bef784603ef39632ddf47f69723def52825e35066"
 dependencies = [
  "bstr",
  "gix-hash",
@@ -1480,9 +1335,9 @@ dependencies = [
 
 [[package]]
 name = "gix-status"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1665770e277608bd6b0eaf86adbf6cb3ffc6fb97749e7bc6f9318ac5f37564df"
+checksum = "414cc1d85079d7ca32c3ab4a6479bf7e174cd251c74a82339c6cc393da3f4883"
 dependencies = [
  "bstr",
  "filetime",
@@ -1503,9 +1358,9 @@ dependencies = [
 
 [[package]]
 name = "gix-submodule"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2455f8c0fcb6ebe2a6e83c8f522d30615d763eb2ef7a23c7d929f9476e89f5c"
+checksum = "74972fe8d46ac8a09490ae1e843b4caf221c5b157c5ac17057e8e1c38417a3ac"
 dependencies = [
  "bstr",
  "gix-config",
@@ -1518,9 +1373,9 @@ dependencies = [
 
 [[package]]
 name = "gix-tempfile"
-version = "15.0.0"
+version = "16.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2feb86ef094cc77a4a9a5afbfe5de626897351bbbd0de3cb9314baf3049adb82"
+checksum = "2558f423945ef24a8328c55d1fd6db06b8376b0e7013b1bb476cc4ffdf678501"
 dependencies = [
  "dashmap",
  "gix-fs",
@@ -1534,15 +1389,15 @@ dependencies = [
 
 [[package]]
 name = "gix-trace"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952"
+checksum = "7c396a2036920c69695f760a65e7f2677267ccf483f25046977d87e4cb2665f7"
 
 [[package]]
 name = "gix-transport"
-version = "0.44.0"
+version = "0.45.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd04d91e507a8713cfa2318d5a85d75b36e53a40379cc7eb7634ce400ecacbaf"
+checksum = "11187418489477b1b5b862ae1aedbbac77e582f2c4b0ef54280f20cfe5b964d9"
 dependencies = [
  "bstr",
  "gix-command",
@@ -1556,9 +1411,9 @@ dependencies = [
 
 [[package]]
 name = "gix-traverse"
-version = "0.43.1"
+version = "0.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed47d648619e23e93f971d2bba0d10c1100e54ef95d2981d609907a8cabac89"
+checksum = "2bec70e53896586ef32a3efa7e4427b67308531ed186bb6120fb3eca0f0d61b4"
 dependencies = [
  "bitflags",
  "gix-commitgraph",
@@ -1573,9 +1428,9 @@ dependencies = [
 
 [[package]]
 name = "gix-url"
-version = "0.28.2"
+version = "0.29.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d096fb733ba6bd3f5403dba8bd72bdd8809fe2b347b57844040b8f49c93492d9"
+checksum = "29218c768b53dd8f116045d87fec05b294c731a4b2bdd257eeca2084cc150b13"
 dependencies = [
  "bstr",
  "gix-features",
@@ -1587,9 +1442,9 @@ dependencies = [
 
 [[package]]
 name = "gix-utils"
-version = "0.1.13"
+version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba427e3e9599508ed98a6ddf8ed05493db114564e338e41f6a996d2e4790335f"
+checksum = "ff08f24e03ac8916c478c8419d7d3c33393da9bb41fa4c24455d5406aeefd35f"
 dependencies = [
  "bstr",
  "fastrand",
@@ -1598,9 +1453,9 @@ dependencies = [
 
 [[package]]
 name = "gix-validate"
-version = "0.9.2"
+version = "0.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd520d09f9f585b34b32aba1d0b36ada89ab7fefb54a8ca3fe37fc482a750937"
+checksum = "9eaa01c3337d885617c0a42e92823922a2aea71f4caeace6fe87002bdcadbd90"
 dependencies = [
  "bstr",
  "thiserror",
@@ -1608,9 +1463,9 @@ dependencies = [
 
 [[package]]
 name = "gix-worktree"
-version = "0.38.0"
+version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "756dbbe15188fa22540d5eab941f8f9cf511a5364d5aec34c88083c09f4bea13"
+checksum = "6673512f7eaa57a6876adceca6978a501d6c6569a4f177767dc405f8b9778958"
 dependencies = [
  "bstr",
  "gix-attributes",
@@ -1627,9 +1482,9 @@ dependencies = [
 
 [[package]]
 name = "gix-worktree-state"
-version = "0.16.0"
+version = "0.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "672a5416fae50538a0af0374bf67e0c97a932fd9e9b72f7d4bfd25355967cbe1"
+checksum = "86f5e199ad5af972086683bd31d640c82cb85885515bf86d86236c73ce575bf0"
 dependencies = [
  "bstr",
  "gix-features",
@@ -1647,9 +1502,9 @@ dependencies = [
 
 [[package]]
 name = "gix-worktree-stream"
-version = "0.18.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34005eae2c0482eeb840e67bdd317ffe6e34057ea4bd8c910fecaee521db69cf"
+checksum = "f61b0463c3cf4d07f2c72a10bdb03a2e4d70a9c26416c639346ad67456834485"
 dependencies = [
  "gix-attributes",
  "gix-features",
@@ -1664,23 +1519,17 @@ dependencies = [
 ]
 
 [[package]]
-name = "glob"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
-
-[[package]]
 name = "h2"
-version = "0.3.26"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
 dependencies = [
+ "atomic-waker",
  "bytes",
  "fnv",
  "futures-core",
  "futures-sink",
- "futures-util",
- "http 0.2.12",
+ "http",
  "indexmap",
  "slab",
  "tokio",
@@ -1703,6 +1552,9 @@ name = "hashbrown"
 version = "0.15.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+dependencies = [
+ "foldhash",
+]
 
 [[package]]
 name = "heck"
@@ -1712,15 +1564,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
 name = "hermit-abi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
-
-[[package]]
-name = "hermit-abi"
-version = "0.4.0"
+version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+checksum = "fbd780fe5cc30f81464441920d82ac8740e2e46b29a6fad543ddd075229ce37e"
 
 [[package]]
 name = "home"
@@ -1733,9 +1579,9 @@ dependencies = [
 
 [[package]]
 name = "http"
-version = "0.2.12"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
 dependencies = [
  "bytes",
  "fnv",
@@ -1743,32 +1589,33 @@ dependencies = [
 ]
 
 [[package]]
-name = "http"
-version = "1.2.0"
+name = "http-body"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
 dependencies = [
  "bytes",
- "fnv",
- "itoa",
+ "http",
 ]
 
 [[package]]
-name = "http-body"
-version = "0.4.6"
+name = "http-body-util"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
 dependencies = [
  "bytes",
- "http 0.2.12",
+ "futures-util",
+ "http",
+ "http-body",
  "pin-project-lite",
 ]
 
 [[package]]
 name = "httparse"
-version = "1.9.5"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
 
 [[package]]
 name = "httpdate"
@@ -1783,27 +1630,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5c3b1f728c459d27b12448862017b96ad4767b1ec2ec5e6434e99f1577f085b8"
 
 [[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
 name = "hyper"
-version = "0.14.32"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
 dependencies = [
  "bytes",
  "futures-channel",
- "futures-core",
  "futures-util",
  "h2",
- "http 0.2.12",
+ "http",
  "http-body",
  "httparse",
  "httpdate",
  "itoa",
  "pin-project-lite",
- "socket2",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
  "tokio",
- "tower-service",
- "tracing",
- "want",
 ]
 
 [[package]]
@@ -1976,32 +1843,24 @@ dependencies = [
 
 [[package]]
 name = "imara-diff"
-version = "0.1.7"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
+checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2"
 dependencies = [
- "ahash",
- "hashbrown 0.14.5",
+ "hashbrown 0.15.2",
 ]
 
 [[package]]
 name = "indexmap"
-version = "2.7.0"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
 dependencies = [
  "equivalent",
  "hashbrown 0.15.2",
- "serde",
 ]
 
 [[package]]
-name = "inlinable_string"
-version = "0.1.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb"
-
-[[package]]
 name = "io-close"
 version = "0.3.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2013,13 +1872,13 @@ dependencies = [
 
 [[package]]
 name = "is-terminal"
-version = "0.4.13"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
 dependencies = [
- "hermit-abi 0.4.0",
+ "hermit-abi",
  "libc",
- "windows-sys 0.52.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -2030,40 +1889,44 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
 
 [[package]]
 name = "itoa"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
 name = "jiff"
-version = "0.1.15"
+version = "0.1.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db69f08d4fb10524cacdb074c10b296299d71274ddbc830a8ee65666867002e9"
+checksum = "c04ef77ae73f3cf50510712722f0c4e8b46f5aaa1bf5ffad2ae213e6495e78e5"
 dependencies = [
  "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "jiff-tzdb"
-version = "0.1.1"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91335e575850c5c4c673b9bd467b0e025f164ca59d0564f69d0c2ee0ffad4653"
+checksum = "962e1dfe9b2d75a84536cf5bf5eaaa4319aa7906c7160134a22883ac316d5f31"
 
 [[package]]
 name = "jiff-tzdb-platform"
-version = "0.1.1"
+version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9835f0060a626fe59f160437bc725491a6af23133ea906500027d1bd2f8f4329"
+checksum = "a63c62e404e7b92979d2792352d885a7f8f83fd1d0d31eea582d77b2ceca697e"
 dependencies = [
  "jiff-tzdb",
 ]
 
 [[package]]
 name = "js-sys"
-version = "0.3.76"
+version = "0.3.77"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
 dependencies = [
  "once_cell",
  "wasm-bindgen",
@@ -2079,16 +1942,16 @@ dependencies = [
 ]
 
 [[package]]
-name = "lazy_static"
-version = "1.5.0"
+name = "libc"
+version = "0.2.170"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
 
 [[package]]
-name = "libc"
-version = "0.2.169"
+name = "libm"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
 
 [[package]]
 name = "libredox"
@@ -2103,15 +1966,21 @@ dependencies = [
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.14"
+version = "0.4.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+checksum = "6db9c683daf087dc577b7506e9695b3d556a9f3849903fa28186283afd6809e9"
 
 [[package]]
 name = "litemap"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
 
 [[package]]
 name = "lock_api"
@@ -2125,44 +1994,20 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.22"
+version = "0.4.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
-
-[[package]]
-name = "loom"
-version = "0.5.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5"
-dependencies = [
- "cfg-if",
- "generator",
- "scoped-tls",
- "serde",
- "serde_json",
- "tracing",
- "tracing-subscriber",
-]
+checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
 
 [[package]]
 name = "markdown"
-version = "1.0.0-alpha.21"
+version = "1.0.0-alpha.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a6491e6c702bf7e3b24e769d800746d5f2c06a6c6a2db7992612e0f429029e81"
+checksum = "9047e0a37a596d4e15411a1ffbdabe71c328908cb90a721cb9bf8dcf3434e6d2"
 dependencies = [
  "unicode-id",
 ]
 
 [[package]]
-name = "matchers"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
-dependencies = [
- "regex-automata 0.1.10",
-]
-
-[[package]]
 name = "maybe-async"
 version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2195,10 +2040,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
 [[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
 name = "miniz_oxide"
-version = "0.8.2"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
+checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
 dependencies = [
  "adler2",
 ]
@@ -2210,52 +2071,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
 dependencies = [
  "libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
  "windows-sys 0.52.0",
 ]
 
 [[package]]
-name = "multer"
-version = "3.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b"
-dependencies = [
- "bytes",
- "encoding_rs",
- "futures-util",
- "http 1.2.0",
- "httparse",
- "memchr",
- "mime",
- "spin",
- "tokio",
- "tokio-util",
- "version_check",
-]
-
-[[package]]
 name = "never"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
 
 [[package]]
-name = "nu-ansi-term"
-version = "0.46.0"
+name = "nom"
+version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
 dependencies = [
- "overload",
- "winapi",
+ "memchr",
+ "minimal-lexical",
 ]
 
 [[package]]
-name = "num-conv"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
-
-[[package]]
 name = "num-traits"
 version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2265,16 +2101,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "num_cpus"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
-dependencies = [
- "hermit-abi 0.3.9",
- "libc",
-]
-
-[[package]]
 name = "object"
 version = "0.36.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2285,15 +2111,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.2"
+version = "1.20.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
-
-[[package]]
-name = "overload"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
 
 [[package]]
 name = "parking_lot"
@@ -2315,30 +2135,7 @@ dependencies = [
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "pear"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467"
-dependencies = [
- "inlinable_string",
- "pear_codegen",
- "yansi",
-]
-
-[[package]]
-name = "pear_codegen"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147"
-dependencies = [
- "proc-macro2",
- "proc-macro2-diagnostics",
- "quote",
- "syn",
+ "windows-targets",
 ]
 
 [[package]]
@@ -2349,9 +2146,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
 name = "pin-project-lite"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
 
 [[package]]
 name = "pin-utils"
@@ -2361,48 +2158,29 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
 
 [[package]]
 name = "portable-atomic"
-version = "1.10.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
-
-[[package]]
-name = "powerfmt"
-version = "0.2.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
 
 [[package]]
-name = "ppv-lite86"
-version = "0.2.20"
+name = "portable-atomic-util"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
 dependencies = [
- "zerocopy",
+ "portable-atomic",
 ]
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.92"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
-name = "proc-macro2-diagnostics"
-version = "0.10.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
- "version_check",
- "yansi",
-]
-
-[[package]]
 name = "prodash"
 version = "29.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2416,9 +2194,9 @@ dependencies = [
 
 [[package]]
 name = "quick-xml"
-version = "0.37.1"
+version = "0.37.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
+checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
 dependencies = [
  "encoding_rs",
  "memchr",
@@ -2426,73 +2204,23 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.37"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
-name = "rand"
-version = "0.8.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
-dependencies = [
- "libc",
- "rand_chacha",
- "rand_core",
-]
-
-[[package]]
-name = "rand_chacha"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
-dependencies = [
- "ppv-lite86",
- "rand_core",
-]
-
-[[package]]
-name = "rand_core"
-version = "0.6.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
-dependencies = [
- "getrandom",
-]
-
-[[package]]
 name = "redox_syscall"
-version = "0.5.8"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1"
 dependencies = [
  "bitflags",
 ]
 
 [[package]]
-name = "ref-cast"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931"
-dependencies = [
- "ref-cast-impl",
-]
-
-[[package]]
-name = "ref-cast-impl"
-version = "1.0.23"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
 name = "regex"
 version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2500,17 +2228,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-automata 0.4.9",
- "regex-syntax 0.8.5",
-]
-
-[[package]]
-name = "regex-automata"
-version = "0.1.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
-dependencies = [
- "regex-syntax 0.6.29",
+ "regex-automata",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -2521,107 +2240,61 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
 dependencies = [
  "aho-corasick",
  "memchr",
- "regex-syntax 0.8.5",
+ "regex-syntax",
 ]
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.29"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
-
-[[package]]
-name = "regex-syntax"
 version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
-name = "rocket"
-version = "0.5.1"
+name = "rinja"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f"
+checksum = "3dc4940d00595430b3d7d5a01f6222b5e5b51395d1120bdb28d854bb8abb17a5"
 dependencies = [
- "async-stream",
- "async-trait",
- "atomic 0.5.3",
- "binascii",
- "bytes",
- "either",
- "figment",
- "futures",
- "indexmap",
- "log",
- "memchr",
- "multer",
- "num_cpus",
- "parking_lot",
- "pin-project-lite",
- "rand",
- "ref-cast",
- "rocket_codegen",
- "rocket_http",
- "serde",
- "state",
- "tempfile",
- "time",
- "tokio",
- "tokio-stream",
- "tokio-util",
- "ubyte",
- "version_check",
- "yansi",
+ "humansize",
+ "itoa",
+ "percent-encoding",
+ "rinja_derive",
 ]
 
 [[package]]
-name = "rocket_codegen"
-version = "0.5.1"
+name = "rinja_derive"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46"
+checksum = "08d9ed0146aef6e2825f1b1515f074510549efba38d71f4554eec32eb36ba18b"
 dependencies = [
- "devise",
- "glob",
- "indexmap",
+ "basic-toml",
+ "memchr",
+ "mime",
+ "mime_guess",
  "proc-macro2",
  "quote",
- "rocket_http",
+ "rinja_parser",
+ "rustc-hash",
+ "serde",
  "syn",
- "unicode-xid",
- "version_check",
 ]
 
 [[package]]
-name = "rocket_http"
-version = "0.5.1"
+name = "rinja_parser"
+version = "0.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9"
+checksum = "93f9a866e2e00a7a1fb27e46e9e324a6f7c0e7edc4543cae1d38f4e4a100c610"
 dependencies = [
- "cookie",
- "either",
- "futures",
- "http 0.2.12",
- "hyper",
- "indexmap",
- "log",
  "memchr",
- "pear",
- "percent-encoding",
- "pin-project-lite",
- "ref-cast",
+ "nom",
  "serde",
- "smallvec",
- "stable-pattern",
- "state",
- "time",
- "tokio",
- "uncased",
 ]
 
 [[package]]
 name = "rss"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "531af70fce504d369cf42ac0a9645f5a62a8ea9265de71cfa25087e9f6080c7c"
+checksum = "b2107738f003660f0a91f56fd3e3bd3ab5d918b2ddaf1e1ec2136fb1c46f71bf"
 dependencies = [
  "atom_syndication",
  "derive_builder",
@@ -2636,29 +2309,48 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
 
 [[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
 name = "rustix"
-version = "0.38.42"
+version = "0.38.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
 dependencies = [
  "bitflags",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustix"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dade4812df5c384711475be5fcd8c162555352945401aed22a35bffeab61f657"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.9.2",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "rustversion"
-version = "1.0.18"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
+checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
 
 [[package]]
 name = "ryu"
-version = "1.0.18"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 
 [[package]]
 name = "same-file"
@@ -2670,12 +2362,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "scoped-tls"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
-
-[[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2683,18 +2369,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "serde"
-version = "1.0.216"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
+checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.216"
+version = "1.0.218"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
+checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2703,9 +2389,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.134"
+version = "1.0.140"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
 dependencies = [
  "itoa",
  "memchr",
@@ -2714,15 +2400,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "serde_spanned"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
-dependencies = [
- "serde",
-]
-
-[[package]]
 name = "sha1_smol"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2740,15 +2417,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "sharded-slab"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
-dependencies = [
- "lazy_static",
-]
-
-[[package]]
 name = "shell-words"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2790,9 +2458,9 @@ dependencies = [
 
 [[package]]
 name = "smallvec"
-version = "1.13.2"
+version = "1.14.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
 
 [[package]]
 name = "socket2"
@@ -2805,42 +2473,31 @@ dependencies = [
 ]
 
 [[package]]
-name = "spin"
-version = "0.9.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
-
-[[package]]
-name = "stable-pattern"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045"
-dependencies = [
- "memchr",
-]
-
-[[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
 [[package]]
-name = "state"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8"
-dependencies = [
- "loom",
-]
-
-[[package]]
 name = "static_assertions"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
+name = "stderrlog"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c910772f992ab17d32d6760e167d2353f4130ed50e796752689556af07dc6b"
+dependencies = [
+ "chrono",
+ "is-terminal",
+ "log",
+ "termcolor",
+ "thread_local",
+]
+
+[[package]]
 name = "strsim"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2848,9 +2505,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "syn"
-version = "2.0.91"
+version = "2.0.99"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
+checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2870,31 +2527,41 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.14.0"
+version = "3.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+checksum = "2c317e0a526ee6120d8dabad239c8dadca62b24b6f168914bbbc8e2fb1f0e567"
 dependencies = [
  "cfg-if",
  "fastrand",
+ "getrandom",
  "once_cell",
- "rustix",
+ "rustix 1.0.1",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
+name = "termcolor"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
 name = "thiserror"
-version = "2.0.9"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.9"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2912,37 +2579,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "time"
-version = "0.3.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
-dependencies = [
- "deranged",
- "itoa",
- "num-conv",
- "powerfmt",
- "serde",
- "time-core",
- "time-macros",
-]
-
-[[package]]
-name = "time-core"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
-
-[[package]]
-name = "time-macros"
-version = "0.2.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de"
-dependencies = [
- "num-conv",
- "time-core",
-]
-
-[[package]]
 name = "tinystr"
 version = "0.7.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2954,9 +2590,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.8.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2969,16 +2605,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.42.0"
+version = "1.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
+checksum = "9975ea0f48b5aa3972bf2d888c238182458437cc2a19374b81b25cdf1023fb3a"
 dependencies = [
  "backtrace",
  "bytes",
  "libc",
  "mio",
  "pin-project-lite",
- "signal-hook-registry",
  "socket2",
  "tokio-macros",
  "windows-sys 0.52.0",
@@ -2986,9 +2621,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-macros"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2996,17 +2631,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "tokio-stream"
-version = "0.1.17"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
-dependencies = [
- "futures-core",
- "pin-project-lite",
- "tokio",
-]
-
-[[package]]
 name = "tokio-util"
 version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3020,126 +2644,29 @@ dependencies = [
 ]
 
 [[package]]
-name = "toml"
-version = "0.8.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
-dependencies = [
- "serde",
- "serde_spanned",
- "toml_datetime",
- "toml_edit",
-]
-
-[[package]]
-name = "toml_datetime"
-version = "0.6.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "toml_edit"
-version = "0.22.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
-dependencies = [
- "indexmap",
- "serde",
- "serde_spanned",
- "toml_datetime",
- "winnow",
-]
-
-[[package]]
-name = "tower-service"
-version = "0.3.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
-
-[[package]]
 name = "tracing"
 version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
 dependencies = [
  "pin-project-lite",
- "tracing-attributes",
  "tracing-core",
 ]
 
 [[package]]
-name = "tracing-attributes"
-version = "0.1.28"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
 name = "tracing-core"
 version = "0.1.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
 dependencies = [
  "once_cell",
- "valuable",
-]
-
-[[package]]
-name = "tracing-log"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
-dependencies = [
- "log",
- "once_cell",
- "tracing-core",
-]
-
-[[package]]
-name = "tracing-subscriber"
-version = "0.3.19"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
-dependencies = [
- "matchers",
- "nu-ansi-term",
- "once_cell",
- "regex",
- "sharded-slab",
- "smallvec",
- "thread_local",
- "tracing",
- "tracing-core",
- "tracing-log",
 ]
 
 [[package]]
-name = "try-lock"
-version = "0.2.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
-
-[[package]]
 name = "typenum"
-version = "1.17.0"
+version = "1.18.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
-
-[[package]]
-name = "ubyte"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea"
-dependencies = [
- "serde",
-]
+checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
 
 [[package]]
 name = "uluru"
@@ -3151,14 +2678,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "uncased"
-version = "0.9.10"
+name = "unicase"
+version = "2.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697"
-dependencies = [
- "serde",
- "version_check",
-]
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
 
 [[package]]
 name = "unicode-bom"
@@ -3174,9 +2697,9 @@ checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.14"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
 
 [[package]]
 name = "unicode-normalization"
@@ -3188,12 +2711,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "unicode-xid"
-version = "0.2.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
-
-[[package]]
 name = "url"
 version = "2.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3224,12 +2741,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
-name = "valuable"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
-
-[[package]]
 name = "version_check"
 version = "0.9.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3246,36 +2757,37 @@ dependencies = [
 ]
 
 [[package]]
-name = "want"
-version = "0.3.1"
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
-dependencies = [
- "try-lock",
-]
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.13.3+wasi-0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+dependencies = [
+ "wit-bindgen-rt",
+]
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
 dependencies = [
  "cfg-if",
  "once_cell",
+ "rustversion",
  "wasm-bindgen-macro",
 ]
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
 dependencies = [
  "bumpalo",
  "log",
@@ -3287,9 +2799,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -3297,9 +2809,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3310,9 +2822,12 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.99"
+version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+dependencies = [
+ "unicode-ident",
+]
 
 [[package]]
 name = "winapi"
@@ -3346,30 +2861,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
-name = "windows"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
-dependencies = [
- "windows-targets 0.48.5",
-]
-
-[[package]]
 name = "windows-core"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.52.6",
+ "windows-targets",
 ]
 
 [[package]]
+name = "windows-link"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
+
+[[package]]
 name = "windows-sys"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.6",
+ "windows-targets",
 ]
 
 [[package]]
@@ -3378,22 +2890,7 @@ version = "0.59.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
 dependencies = [
- "windows-targets 0.52.6",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
-dependencies = [
- "windows_aarch64_gnullvm 0.48.5",
- "windows_aarch64_msvc 0.48.5",
- "windows_i686_gnu 0.48.5",
- "windows_i686_msvc 0.48.5",
- "windows_x86_64_gnu 0.48.5",
- "windows_x86_64_gnullvm 0.48.5",
- "windows_x86_64_msvc 0.48.5",
+ "windows-targets",
 ]
 
 [[package]]
@@ -3402,48 +2899,30 @@ version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
 dependencies = [
- "windows_aarch64_gnullvm 0.52.6",
- "windows_aarch64_msvc 0.52.6",
- "windows_i686_gnu 0.52.6",
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
  "windows_i686_gnullvm",
- "windows_i686_msvc 0.52.6",
- "windows_x86_64_gnu 0.52.6",
- "windows_x86_64_gnullvm 0.52.6",
- "windows_x86_64_msvc 0.52.6",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
 ]
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
-
-[[package]]
-name = "windows_aarch64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
-
-[[package]]
-name = "windows_i686_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
@@ -3456,62 +2935,47 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
-
-[[package]]
-name = "windows_i686_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
-
-[[package]]
-name = "windows_x86_64_gnu"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.48.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
-
-[[package]]
-name = "windows_x86_64_msvc"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
 name = "winnow"
-version = "0.6.20"
+version = "0.6.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
+checksum = "1e90edd2ac1aa278a5c4599b1d89cf03074b610800f866d4026dc199d7929a28"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
+name = "wit-bindgen-rt"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
 name = "write16"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3524,15 +2988,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
 
 [[package]]
-name = "yansi"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
-dependencies = [
- "is-terminal",
-]
-
-[[package]]
 name = "yoke"
 version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3562,7 +3017,6 @@ version = "0.7.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
 dependencies = [
- "byteorder",
  "zerocopy-derive",
 ]
 
@@ -3579,18 +3033,18 @@ dependencies = [
 
 [[package]]
 name = "zerofrom"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
 dependencies = [
  "zerofrom-derive",
 ]
 
 [[package]]
 name = "zerofrom-derive"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/pkgs/by-name/ba/back/Cargo.toml b/pkgs/by-name/ba/back/Cargo.toml
index f1abe32..7fabeed 100644
--- a/pkgs/by-name/ba/back/Cargo.toml
+++ b/pkgs/by-name/ba/back/Cargo.toml
@@ -22,16 +22,24 @@ license = "AGPL-3.0-or-later"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 [dependencies]
-chrono = "0.4.39"
-clap = { version = "4.5.23", features = ["derive"] }
-gix = "0.69.1"
-markdown = "1.0.0-alpha.21"
-rocket = "0.5.1"
-rss = "2.0.11"
-serde = "1.0.216"
-serde_json = "1.0.134"
+bytes = "1.10.1"
+chrono = "0.4.40"
+clap = { version = "4.5.31", features = ["derive"] }
+gix = "0.70.0"
+http = "1.2.0"
+http-body-util = "0.1.2"
+hyper = { version = "1.6.0", features = ["http1", "http2", "server"] }
+hyper-util = { version = "0.1.10", features = ["tokio"] }
+log = "0.4.26"
+markdown = "1.0.0-alpha.23"
+rinja = "0.3.5"
+rss = "2.0.12"
+serde = "1.0.218"
+serde_json = "1.0.140"
 sha2 = "0.10.8"
-thiserror = "2.0.9"
+stderrlog = "0.6.0"
+thiserror = "2.0.12"
+tokio = { version = "1.44.0", features = ["macros", "net", "rt-multi-thread"] }
 url = { version = "2.5.4", features = ["serde"] }
 
 [profile.release]
diff --git a/pkgs/by-name/ba/back/README.md b/pkgs/by-name/ba/back/README.md
index 4bbd9c0..222ccf3 100644
--- a/pkgs/by-name/ba/back/README.md
+++ b/pkgs/by-name/ba/back/README.md
@@ -17,29 +17,60 @@ If not, see <https://www.gnu.org/licenses/agpl.txt>.
 
 ## Usage
 
-Currently, `back` only visualizes a `git-bug` repository. As such it takes exactly one
-argument, being the repository to visualize.
-The server is than started at `http://127.0.0.1:8000` and provides access to the issues
-(bugs) tracked via `git-bug`.
+`back` is modelled after `cgit`, only for `git-bug` initialized repositories. The server is than
+started at `http://127.0.0.1:8000` and provides access to the issues (bugs) tracked via `git-bug`,
+via multiple routes:
 
-### Note
+### `/`
+
+The default index is a list of all repositories that have `git-bug` data in them.
+
+### `<repo_path>/issues/<state>`
+
+This path displays all issues in `<state>` (i.e., open or closed) for the repository at
+`<repo_path>`.
+
+### `<repo_path>/issue/<issue_id>`
+
+Displays the actual issue with `id` `<issue_id>`. Beware, that the `<isuse_id>` is sourced from the
+actual git object associated with the issue create commit. As such, it is not the same ID, as
+displayed by the `git-bug` CLI.
+
+### `<repo_path>/issues/feed`
+
+An RSS feed usable to subscribe to. This includes all issues and all comments of issues.
 
-`back` needs write access to the repository, because of internal `gix` and `git` object
-reasons.
+## Configuration file
 
-## Relevant Environment Variables
+The config file is passed to `back` via the first command line argument. It is written in JSON.
+An example configuration file is available at [`./contrib/config.json`](./contrib/config.json).
 
-### `ROCKET_PORT`
+Following keys are required:
 
-> Default: 8000
+### `source_code_repository_url`
 
-This is the port the server binds to.
+The URL to the source code of this instance of `back`.
 
-### `SOURCE_CODE_REPOSITORY_URL`
+### `root_url`
+
+The root URL this instance of `back` is hosted at. For example: `https://issues.foss-syndicate.org`.
+This is required by the RSS feed to generate links to the various issues/comments.
+
+### `scan_path`
+
+The path under which to search for the repositories as specified by the `projects.list` file. This
+is semantically the same as `cgit`'s `scan-path`.
+
+### `project_list`
+
+The path to the file specifying the repositories to search. A repository path per line. This is
+semantically the same as `cgit`'s `project-list`.
+
+### Note
 
-The URL to the back's source.
+`back` needs write access to the repository, because of internal `gix` and `git` object reasons.
 
 ## Licensing
 
-This project complies with the REUSE v3.3 specification. This means that every file
-clearly states its copyright.
+This project complies with the REUSE v3.3 specification. This means that every file clearly states
+its copyright.
diff --git a/pkgs/by-name/ba/back/assets/style.css b/pkgs/by-name/ba/back/assets/style.css
index b789f17..3cf4a00 100644
--- a/pkgs/by-name/ba/back/assets/style.css
+++ b/pkgs/by-name/ba/back/assets/style.css
@@ -17,9 +17,9 @@
 * It was originally licensed under the MIT license.
 */
 
-input[type="text"],
-input[type="password"],
-textarea{
+input[type='text'],
+input[type='password'],
+textarea {
     width: 100%;
     padding: 0.5rem;
     outline: none;
@@ -30,18 +30,18 @@ textarea{
     margin-bottom: 1rem;
 }
 
-textarea{
+textarea {
     resize: vertical;
 }
 
-input[type="submit"]{
+input[type='submit'] {
     -webkit-appearance: none;
     border: none;
     cursor: pointer;
     font-size: 1rem;
 }
 
-input[type="submit"]{
+input[type='submit'] {
     background-color: var(--success);
     padding: 0.5rem;
     text-decoration: none;
@@ -52,37 +52,35 @@ input[type="submit"]{
     transition: box-shadow 0.15s ease-in-out;
 }
 
-input[type="submit"]:hover{
-    -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
+input[type='submit']:hover {
+    -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
 }
 
-input[type="submit"]:active,
-input[type="submit"]:focus{
-    -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
+input[type='submit']:active,
+input[type='submit']:focus {
+    -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
     outline: none;
     border: none;
 }
 
-
-
-.form-link input[type="submit"]{
+.form-link input[type='submit'] {
     background-color: initial;
     color: inherit;
     padding: 0;
     text-decoration: underline;
 }
 
-.form-link input[type="submit"]:hover,
-.form-link input[type="submit"]:active,
-.form-link input[type="submit"]:focus{
+.form-link input[type='submit']:hover,
+.form-link input[type='submit']:active,
+.form-link input[type='submit']:focus {
     -moz-box-shadow: 0 0 0 0;
     -o-box-shadow: 0 0 0 0;
     -webkit-box-shadow: 0 0 0 0;
@@ -90,49 +88,49 @@ input[type="submit"]:focus{
     box-shadow: 0 0 0 0;
 }
 
-.form-group{
+.form-group {
     margin-top: 1rem;
 }
 
-label.checkbox{
+label.checkbox {
     cursor: pointer;
 }
 
-.issue-list{
+.issue-list {
     list-style-type: none;
     padding-left: 0;
 }
 
-.issue-list .issue-subject{
+.issue-list .issue-subject {
     font-weight: bold;
 }
 
-.issue-list li{
+.issue-list li {
     padding-bottom: 1rem;
 }
 
-.issue-list li + li{
+.issue-list li + li {
     border-top: 1px solid var(--gray);
 }
 
-.issue-list a{
+.issue-list a {
     text-decoration: none;
     display: block;
 }
 
-.issue-list a:hover{
+.issue-list a:hover {
     outline: none;
 }
 
-.issue-list a:hover .issue-subject{
+.issue-list a:hover .issue-subject {
     color: var(--primary);
 }
 
-.comment-count{
+.comment-count {
     color: var(--gray);
 }
 
-.issue-links{
+.issue-links {
     display: flex;
     flex-direction: row;
     align-items: center;
@@ -140,9 +138,7 @@ label.checkbox{
     flex-wrap: wrap;
 }
 
-
-
-.issue-search input[type="search"]{
+.issue-search input[type='search'] {
     padding: 0.5rem;
     background-image: url('static/search.png');
     background-position: 10px 10px;
@@ -152,13 +148,13 @@ label.checkbox{
     border: 1px solid var(--gray);
 }
 
-.issue-info{
+.issue-info {
     display: flex;
     justify-content: space-between;
     align-items: center;
 }
 
-.issue-info .edit-issue{
+.issue-info .edit-issue {
     background-color: var(--success);
     padding: 0.5rem;
     text-decoration: none;
@@ -169,111 +165,111 @@ label.checkbox{
     transition: box-shadow 0.15s ease-in-out;
 }
 
-.issue-info .edit-issue:hover{
-    -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
+.issue-info .edit-issue:hover {
+    -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
 }
 
 .issue-info .edit-issue:active,
-.issue-info .edit-issue:focus{
-    -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
+.issue-info .edit-issue:focus {
+    -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
     outline: none;
     border: none;
 }
 
-.issue-info .created-by-at{
+.issue-info .created-by-at {
     flex: 1;
 }
 
-.issue-info .edit-issue{
+.issue-info .edit-issue {
     background-color: var(--light) -gray;
     flex: 0;
     margin-right: 0.5rem;
 }
 
-.issue-info .close-issue{
+.issue-info .close-issue {
     background-color: var(--failure);
 }
 
-.issue-history{
+.issue-history {
     list-style: none;
     border-top: 1px solid var(--gray);
     padding-top: 1rem;
     padding-left: 2rem;
 }
 
-.issue-history .comment-info{
+.issue-history .comment-info {
     color: var(--gray);
     margin: 0;
     padding-top: 1rem;
 }
 
-.issue-history .comment-info a{
+.issue-history .comment-info a {
     text-decoration: none;
 }
 
-.issue-history .comment-info a:hover{
+.issue-history .comment-info a:hover {
     text-decoration: underline;
 }
 
 .issue-history .comment,
-.issue-history .event{
+.issue-history .event {
     padding-top: 1rem;
     padding-bottom: 1rem;
     border-bottom: 1px solid var(--gray);
 }
 
 .issue-history .comment p,
-.issue-history .event p{
+.issue-history .event p {
     margin: 0;
 }
 
 .issue-history .comment:target,
-.issue-history .event:target{
+.issue-history .event:target {
     border-color: var(--primary);
     border-bottom-width: 3px;
 }
 
-.issue-history .event{
+.issue-history .event {
     color: var(--gray);
 }
 
-blockquote{
+blockquote {
     border-left: 5px solid var(--light) -gray;
     padding-left: 1rem;
     margin-left: 0rem;
 }
 
-pre{
+pre {
     overflow-x: auto;
 }
 
-body{
+body {
     font-family: sans-serif;
     color: var(--text);
     background: var(--bg);
     --text: rgb(24, 24, 24);
     --bg: white;
-    --gray: #8D8D8D;
+    --gray: #8d8d8d;
     --primary: rgb(106, 154, 255);
     --primary-light: rgb(150, 166, 200);
     --success: rgb(168, 249, 166);
     --failure: rgb(247, 167, 167);
-    --light-gray: #EEE;
+    --light-gray: #eee;
 }
 
-@media (prefers-color-scheme: dark){
-    body{
+@media (prefers-color-scheme: dark) {
+    body {
         --text: rgb(240, 240, 240);
         --bg: black;
-        --gray: #8D8D8D;
+        --gray: #8d8d8d;
         --primary: rgb(106, 154, 255);
         --primary-light: rgb(150, 166, 200);
         --success: rgb(14, 130, 11);
@@ -282,54 +278,54 @@ body{
     }
 }
 
-a{
+a {
     color: inherit;
 }
 
-.content{
+.content {
     max-width: 800px;
     margin: 0 auto;
 }
 
-header{
+header {
     display: flex;
     align-items: center;
     border-bottom: 1px solid var(--text);
     margin-bottom: 1rem;
 }
 
-header h1{
+header h1 {
     padding: 0;
     flex: 1;
 }
 
-header .issue-number{
+header .issue-number {
     color: var(--gray);
     font-size: 1.5rem;
 }
 
-nav{
+nav {
     display: flex;
     color: var(--gray);
     justify-content: space-between;
 }
 
-nav .nav-group{
+nav .nav-group {
     display: flex;
 }
 
-nav .nav-group >*{
+nav .nav-group > * {
     margin-left: 0.5rem;
 }
 
-footer{
+footer {
     border-top: 1px solid var(--gray);
     padding-top: 1rem;
     margin-top: 1rem;
     color: var(--gray);
 }
 
-.new-issue{
+.new-issue {
     background-color: var(--success);
     padding: 0.5rem;
     text-decoration: none;
@@ -340,41 +336,41 @@ footer{
     transition: box-shadow 0.15s ease-in-out;
 }
 
-.new-issue:hover{
-    -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
-    box-shadow: 0.25rem 0.25rem 0 0 rgba(0,0,0,0.08);
+.new-issue:hover {
+    -moz-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -o-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -webkit-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    -ms-box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
+    box-shadow: 0.25rem 0.25rem 0 0 rgba(0, 0, 0, 0.08);
 }
 
 .new-issue:active,
-.new-issue:focus{
-    -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
-    box-shadow: 0.1rem 0.1rem 0 0 rgba(0,0,0,0.05);
+.new-issue:focus {
+    -moz-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -o-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -webkit-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    -ms-box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
+    box-shadow: 0.1rem 0.1rem 0 0 rgba(0, 0, 0, 0.05);
     outline: none;
     border: none;
 }
 
-.alert{
+.alert {
     padding: 0.5rem;
     margin-bottom: 1rem;
     background-color: var(--failure);
 }
 
-.login-form{
+.login-form {
     max-width: 300px;
     margin: 0 auto;
 }
 
-.created-by-at{
+.created-by-at {
     color: var(--gray);
 }
 
-.sr-only{
+.sr-only {
     border: 0;
     clip: rect(0 0 0 0);
     height: 1px;
diff --git a/pkgs/by-name/ba/back/contrib/config.json b/pkgs/by-name/ba/back/contrib/config.json
index 2347bf2..81d1041 100644
--- a/pkgs/by-name/ba/back/contrib/config.json
+++ b/pkgs/by-name/ba/back/contrib/config.json
@@ -1,5 +1,6 @@
 {
     "source_code_repository_url": "https://git.foss-syndicate.org/vhack.eu/nixos-server/tree/pkgs/by-name/ba/back",
-    "repository_path": "/path/to/your/repository"
-    "root_url": "https://issues.foss-syndicate.org"
+    "root_url": "https://issues.foss-syndicate.org",
+    "scan_path": "/path/to/the/scan/path",
+    "project_list": "/path/to/the/projects.list"
 }
diff --git a/pkgs/by-name/ba/back/flake.lock b/pkgs/by-name/ba/back/flake.lock
index ca4a6b9..9e20410 100644
--- a/pkgs/by-name/ba/back/flake.lock
+++ b/pkgs/by-name/ba/back/flake.lock
@@ -2,11 +2,11 @@
   "nodes": {
     "nixpkgs": {
       "locked": {
-        "lastModified": 1734435836,
-        "narHash": "sha256-kMBQ5PRiFLagltK0sH+08aiNt3zGERC2297iB6vrvlU=",
+        "lastModified": 1741310760,
+        "narHash": "sha256-aizILFrPgq/W53Jw8i0a1h1GZAAKtlYOrG/A5r46gVM=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "4989a246d7a390a859852baddb1013f825435cee",
+        "rev": "de0fe301211c267807afd11b12613f5511ff7433",
         "type": "github"
       },
       "original": {
diff --git a/pkgs/by-name/ba/back/package.nix b/pkgs/by-name/ba/back/package.nix
index faa0e1d..d70052b 100644
--- a/pkgs/by-name/ba/back/package.nix
+++ b/pkgs/by-name/ba/back/package.nix
@@ -21,7 +21,8 @@ rustPlatform.buildRustPackage {
     filter = name: type:
       (type == "directory")
       || (builtins.elem (builtins.baseNameOf name) ["Cargo.toml" "Cargo.lock" "style.css"])
-      || (lib.strings.hasSuffix ".rs" (builtins.baseNameOf name));
+      || (lib.strings.hasSuffix ".rs" (builtins.baseNameOf name))
+      || (lib.strings.hasSuffix ".html" (builtins.baseNameOf name));
   };
 
   doCheck = true;
diff --git a/pkgs/by-name/ba/back/src/config/mod.rs b/pkgs/by-name/ba/back/src/config/mod.rs
index 7351ad8..832d060 100644
--- a/pkgs/by-name/ba/back/src/config/mod.rs
+++ b/pkgs/by-name/ba/back/src/config/mod.rs
@@ -18,55 +18,119 @@ use gix::ThreadSafeRepository;
 use serde::Deserialize;
 use url::Url;
 
-use crate::error::{self, Error};
+use crate::{
+    error::{self, Error},
+    git_bug::dag::is_git_bug,
+};
 
+#[derive(Deserialize)]
 pub struct BackConfig {
-    // NOTE(@bpeetz): We do not need to html escape this, as the value must be a valid url. As such
-    // `<tags>` of all kinds _should_ be invalid.  <2024-12-26>
+    /// The url to the source code of back. This is needed, because back is licensed under the
+    /// AGPL.
     pub source_code_repository_url: Url,
-    pub repository: ThreadSafeRepository,
-    pub root: Url,
+
+    /// The root url this instance of back is hosted on.
+    /// For example:
+    ///     `issues.foss-syndicate.org`
+    pub root_url: Url,
+
+    project_list: PathBuf,
+
+    /// The path that is the common parent of all the repositories.
+    pub scan_path: PathBuf,
 }
 
-#[derive(Deserialize)]
-struct RawBackConfig {
-    source_code_repository_url: Url,
-    repository_path: PathBuf,
-    root_url: Url,
+pub struct BackRepositories {
+    repositories: Vec<BackRepository>,
 }
 
-impl BackConfig {
-    pub fn from_config_file(path: &Path) -> error::Result<Self> {
-        let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead {
-            file: path.to_owned(),
-            error: err,
-        })?;
+impl BackRepositories {
+    pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
+        self.into_iter()
+    }
+}
 
-        let raw: RawBackConfig =
-            serde_json::from_str(&value).map_err(|err| Error::ConfigParse {
-                file: path.to_owned(),
-                error: err,
-            })?;
+impl<'a> IntoIterator for &'a BackRepositories {
+    type Item = <&'a Vec<BackRepository> as IntoIterator>::Item;
+
+    type IntoIter = <&'a Vec<BackRepository> as IntoIterator>::IntoIter;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.repositories.iter()
+    }
+}
 
-        Self::try_from(raw)
+impl BackRepositories {
+    /// Try to get the repository at path `path`.
+    /// If no repository was registered/found at `path`, returns an error.
+    pub fn get(&self, path: &Path) -> Result<&BackRepository, error::Error> {
+        self.repositories
+            .iter()
+            .find(|p| p.repo_path == path)
+            .ok_or(error::Error::RepoFind {
+                repository_path: path.to_owned(),
+            })
     }
 }
 
-impl TryFrom<RawBackConfig> for BackConfig {
-    type Error = error::Error;
+pub struct BackRepository {
+    repo_path: PathBuf,
+}
 
-    fn try_from(value: RawBackConfig) -> Result<Self, Self::Error> {
-        let repository = {
-            ThreadSafeRepository::open(&value.repository_path).map_err(|err| Error::RepoOpen {
-                repository_path: value.repository_path,
-                error: Box::new(err),
+impl BackRepository {
+    pub fn open(&self, scan_path: &Path) -> Result<ThreadSafeRepository, error::Error> {
+        let path = {
+            let base = scan_path.join(&self.repo_path);
+            if base.is_dir() {
+                base
+            } else {
+                PathBuf::from(base.display().to_string() + ".git")
+            }
+        };
+        let repo = ThreadSafeRepository::open(path).map_err(|err| Error::RepoOpen {
+            repository_path: self.repo_path.to_owned(),
+            error: Box::new(err),
+        })?;
+        if is_git_bug(&repo.to_thread_local())? {
+            Ok(repo)
+        } else {
+            Err(error::Error::NotGitBug {
+                path: self.repo_path.clone(),
             })
-        }?;
+        }
+    }
+    pub fn path(&self) -> &Path {
+        &self.repo_path
+    }
+}
+
+impl BackConfig {
+    pub fn repositories(&self) -> error::Result<BackRepositories> {
+        let repositories = fs::read_to_string(&self.project_list)
+            .map_err(|err| error::Error::ProjectListRead {
+                error: err,
+                file: self.project_list.to_owned(),
+            })?
+            .lines()
+            .try_fold(vec![], |mut acc, path| {
+                acc.push(BackRepository {
+                    repo_path: PathBuf::from(path),
+                });
+
+                Ok::<_, error::Error>(acc)
+            })?;
+        Ok(BackRepositories { repositories })
+    }
+
+    pub fn from_config_file(path: &Path) -> error::Result<Self> {
+        let value = fs::read_to_string(path).map_err(|err| Error::ConfigRead {
+            file: path.to_owned(),
+            error: err,
+        })?;
 
-        Ok(Self {
-            repository,
-            source_code_repository_url: value.source_code_repository_url,
-            root: value.root_url,
+        serde_json::from_str(&value).map_err(|err| Error::ConfigParse {
+            file: path.to_owned(),
+            error: err,
         })
     }
 }
diff --git a/pkgs/by-name/ba/back/src/error/mod.rs b/pkgs/by-name/ba/back/src/error/mod.rs
index 8b71700..8889033 100644
--- a/pkgs/by-name/ba/back/src/error/mod.rs
+++ b/pkgs/by-name/ba/back/src/error/mod.rs
@@ -9,37 +9,53 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/agpl.txt>.
 
-use std::{fmt::Display, io, path::PathBuf};
+use std::{fmt::Display, io, net::SocketAddr, path::PathBuf};
 
+use gix::hash::Prefix;
 use thiserror::Error;
 
-use crate::web::prefix::BackPrefix;
-
 pub type Result<T> = std::result::Result<T, Error>;
 
-pub mod responder;
-
 #[derive(Error, Debug)]
 pub enum Error {
     ConfigParse {
         file: PathBuf,
         error: serde_json::Error,
     },
+
+    ProjectListRead {
+        file: PathBuf,
+        error: io::Error,
+    },
     ConfigRead {
         file: PathBuf,
         error: io::Error,
     },
-    RocketLaunch(#[from] rocket::Error),
-
+    NotGitBug {
+        path: PathBuf,
+    },
     RepoOpen {
         repository_path: PathBuf,
         error: Box<gix::open::Error>,
     },
+    RepoFind {
+        repository_path: PathBuf,
+    },
     RepoRefsIter(#[from] gix::refs::packed::buffer::open::Error),
-    RepoRefsPrefixed(#[from] std::io::Error),
+    RepoRefsPrefixed {
+        error: io::Error,
+    },
+
+    TcpBind {
+        addr: SocketAddr,
+        err: io::Error,
+    },
+    TcpAccept {
+        err: io::Error,
+    },
 
     IssuesPrefixMissing {
-        prefix: BackPrefix,
+        prefix: Prefix,
     },
     IssuesPrefixParse(#[from] gix::hash::prefix::from_hex::Error),
 }
@@ -54,6 +70,13 @@ impl Display for Error {
                     file.display()
                 )
             }
+            Error::ProjectListRead { file, error } => {
+                write!(
+                    f,
+                    "while trying to read the project.list file ({}): {error}",
+                    file.display()
+                )
+            }
             Error::ConfigRead { file, error } => {
                 write!(
                     f,
@@ -61,9 +84,6 @@ impl Display for Error {
                     file.display()
                 )
             }
-            Error::RocketLaunch(error) => {
-                write!(f, "while trying to start back: {error}")
-            }
             Error::RepoOpen {
                 repository_path,
                 error,
@@ -74,10 +94,24 @@ impl Display for Error {
                     repository_path.display()
                 )
             }
+            Error::NotGitBug { path } => {
+                write!(
+                    f,
+                    "Repository ('{}') has no initialized git-bug data",
+                    path.display()
+                )
+            }
+            Error::RepoFind { repository_path } => {
+                write!(
+                    f,
+                    "failed to find the repository at path: '{}'",
+                    repository_path.display()
+                )
+            }
             Error::RepoRefsIter(error) => {
                 write!(f, "while iteration over the refs in a repository: {error}",)
             }
-            Error::RepoRefsPrefixed(error) => {
+            Error::RepoRefsPrefixed { error, .. } => {
                 write!(f, "while prefixing the refs with a path: {error}")
             }
             Error::IssuesPrefixMissing { prefix } => {
@@ -89,6 +123,12 @@ impl Display for Error {
             Error::IssuesPrefixParse(error) => {
                 write!(f, "The given prefix can not be parsed as prefix: {error}")
             }
+            Error::TcpBind { addr, err } => {
+                write!(f, "while trying to open tcp {addr} for listening: {err}.")
+            }
+            Error::TcpAccept { err } => {
+                write!(f, "while trying to accept a tcp connection: {err}.")
+            }
         }
     }
 }
diff --git a/pkgs/by-name/ba/back/src/error/responder.rs b/pkgs/by-name/ba/back/src/error/responder.rs
deleted file mode 100644
index 7bea961..0000000
--- a/pkgs/by-name/ba/back/src/error/responder.rs
+++ /dev/null
@@ -1,23 +0,0 @@
-// Back - An extremely simple git issue tracking system. Inspired by tvix's
-// panettone
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This file is part of Back.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/agpl.txt>.
-
-use rocket::{
-    response::{self, Responder, Response},
-    Request,
-};
-
-use super::Error;
-
-impl<'r> Responder<'r, 'static> for Error {
-    fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> {
-        Response::build_from(self.to_string().respond_to(req)?).ok()
-    }
-}
diff --git a/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs b/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs
index 9c158a7..3d22b04 100644
--- a/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs
+++ b/pkgs/by-name/ba/back/src/git_bug/dag/mod.rs
@@ -123,11 +123,23 @@ impl Dag {
     }
 }
 
+/// Check whether `git-bug` has been initialized in this repository
+pub fn is_git_bug(repo: &Repository) -> error::Result<bool> {
+    Ok(repo
+        .refs
+        .iter()?
+        .prefixed(Path::new("refs/bugs/"))
+        .map_err(|err| error::Error::RepoRefsPrefixed { error: err })?
+        .count()
+        > 0)
+}
+
 pub fn issues_from_repository(repo: &Repository) -> error::Result<Vec<Dag>> {
     let dags = repo
         .refs
         .iter()?
-        .prefixed(Path::new("refs/bugs/"))?
+        .prefixed(Path::new("refs/bugs/"))
+        .map_err(|err| error::Error::RepoRefsPrefixed { error: err })?
         .map(|val| {
             let reference = val.expect("All `git-bug` references in 'refs/bugs' should be objects");
 
diff --git a/pkgs/by-name/ba/back/src/git_bug/format/mod.rs b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
index b3b6bcc..ffe44fd 100644
--- a/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
+++ b/pkgs/by-name/ba/back/src/git_bug/format/mod.rs
@@ -16,8 +16,8 @@ use markdown::to_html;
 use serde::Deserialize;
 use serde_json::Value;
 
-#[derive(Debug, Default, Clone)]
 /// Markdown content.
+#[derive(Debug, Default, Clone)]
 pub struct MarkDown {
     value: String,
 }
@@ -88,6 +88,19 @@ pub struct HtmlString {
     value: String,
 }
 
+impl From<String> for HtmlString {
+    fn from(value: String) -> Self {
+        Self { value }
+    }
+}
+impl From<&str> for HtmlString {
+    fn from(value: &str) -> Self {
+        Self {
+            value: value.to_owned(),
+        }
+    }
+}
+
 impl From<MarkDown> for HtmlString {
     fn from(value: MarkDown) -> Self {
         Self { value: value.value }
diff --git a/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs b/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs
index f27bfec..d382b54 100644
--- a/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs
+++ b/pkgs/by-name/ba/back/src/git_bug/issue/mod.rs
@@ -128,7 +128,7 @@ impl RawCollapsedIssue {
                 } => {
                     self.id = Some(entity.id.clone());
                     self.author = Some(entity.author.clone());
-                    self.timestamp = Some(timestamp.clone());
+                    self.timestamp = Some(timestamp);
                     self.title = Some(title);
                     self.message = Some(message);
                     self.status = Some(Status::Open); // This is the default in git_bug
diff --git a/pkgs/by-name/ba/back/src/main.rs b/pkgs/by-name/ba/back/src/main.rs
index 961c39b..61953c4 100644
--- a/pkgs/by-name/ba/back/src/main.rs
+++ b/pkgs/by-name/ba/back/src/main.rs
@@ -9,14 +9,11 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/agpl.txt>.
 
-use std::process;
+use std::{process, sync::Arc};
 
 use clap::Parser;
-use config::BackConfig;
-use rocket::routes;
-use web::feed;
 
-use crate::web::{closed, open, show_issue, styles};
+use crate::config::BackConfig;
 
 mod cli;
 pub mod config;
@@ -25,7 +22,7 @@ pub mod git_bug;
 mod web;
 
 fn main() -> Result<(), String> {
-    if let Err(err) = rocket_main() {
+    if let Err(err) = server_main() {
         eprintln!("Error {err}");
         process::exit(1);
     } else {
@@ -33,20 +30,24 @@ fn main() -> Result<(), String> {
     }
 }
 
-#[rocket::main]
-async fn rocket_main() -> Result<(), error::Error> {
+#[tokio::main]
+async fn server_main() -> Result<(), error::Error> {
     let args = cli::Cli::parse();
 
+    stderrlog::new()
+        .module(module_path!())
+        .modules(["hyper", "http"])
+        .quiet(false)
+        .show_module_names(false)
+        .color(stderrlog::ColorChoice::Auto)
+        .verbosity(2)
+        .timestamp(stderrlog::Timestamp::Off)
+        .init()
+        .expect("Let's just hope that this does not panic");
+
     let config = BackConfig::from_config_file(&args.config_file)?;
 
-    rocket::build()
-        .mount("/", routes![open, closed, show_issue, styles, feed])
-        .manage(config)
-        .ignite()
-        .await
-        .expect("This error should only happen on a miss-configuration.")
-        .launch()
-        .await?;
+    web::main(Arc::new(config)).await?;
 
     Ok(())
 }
diff --git a/pkgs/by-name/ba/back/src/web/generate/mod.rs b/pkgs/by-name/ba/back/src/web/generate/mod.rs
new file mode 100644
index 0000000..ae783a3
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/web/generate/mod.rs
@@ -0,0 +1,225 @@
+use std::{fs, path::Path};
+
+use gix::hash::Prefix;
+use log::info;
+use rinja::Template;
+use url::Url;
+
+use crate::{
+    config::BackConfig,
+    error,
+    git_bug::{
+        dag::issues_from_repository,
+        issue::{CollapsedIssue, Status},
+    },
+};
+
+#[derive(Template)]
+#[template(path = "./issues.html")]
+struct IssuesTemplate {
+    wanted_status: Status,
+    counter_status: Status,
+    issues: Vec<CollapsedIssue>,
+
+    /// The path to the repository
+    repo_path: String,
+
+    /// The URL to `back`'s source code
+    source_code_repository_url: Url,
+}
+pub fn issues(
+    config: &BackConfig,
+    wanted_status: Status,
+    counter_status: Status,
+    repo_path: &Path,
+) -> error::Result<String> {
+    let repository = config
+        .repositories()?
+        .get(repo_path)?
+        .open(&config.scan_path)?;
+
+    let mut issue_list = issues_from_repository(&repository.to_thread_local())?
+        .into_iter()
+        .map(|issue| issue.collapse())
+        .filter(|issue| issue.status == wanted_status)
+        .collect::<Vec<CollapsedIssue>>();
+
+    // Sort by date descending.
+    // SAFETY:
+    // The time stamp is only used for sorting, so a malicious attacker could only affect the issue
+    // sorting.
+    issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() });
+    issue_list.reverse();
+
+    Ok(IssuesTemplate {
+        wanted_status,
+        counter_status,
+        source_code_repository_url: config.source_code_repository_url.clone(),
+        issues: issue_list,
+        repo_path: repo_path.display().to_string(),
+    }
+    .render()
+    .expect("This should always work"))
+}
+
+use crate::git_bug::format::HtmlString;
+#[derive(Template)]
+#[template(path = "./issue.html")]
+struct IssueTemplate {
+    issue: CollapsedIssue,
+
+    /// The path to the repository
+    repo_path: String,
+
+    /// The URL to `back`'s source code
+    source_code_repository_url: Url,
+}
+pub fn issue(config: &BackConfig, repo_path: &Path, prefix: Prefix) -> error::Result<String> {
+    let repository = config
+        .repositories()?
+        .get(repo_path)?
+        .open(&config.scan_path)?
+        .to_thread_local();
+
+    let maybe_issue = issues_from_repository(&repository)?
+        .into_iter()
+        .map(|val| val.collapse())
+        .find(|issue| issue.id.to_string().starts_with(&prefix.to_string()));
+
+    match maybe_issue {
+        Some(issue) => Ok(IssueTemplate {
+            issue,
+            repo_path: repo_path.display().to_string(),
+            source_code_repository_url: config.source_code_repository_url.clone(),
+        }
+        .render()
+        .expect("This should always work")),
+        None => Err(error::Error::IssuesPrefixMissing { prefix }),
+    }
+}
+
+#[derive(Template)]
+#[template(path = "./repos.html")]
+struct ReposTemplate {
+    repos: Vec<RepoValue>,
+
+    /// The URL to `back`'s source code
+    source_code_repository_url: Url,
+}
+struct RepoValue {
+    description: String,
+    owner: String,
+    path: String,
+}
+pub fn repos(config: &BackConfig) -> error::Result<String> {
+    let repos: Vec<RepoValue> = config
+        .repositories()?
+        .iter()
+        .filter_map(|raw_repo| match raw_repo.open(&config.scan_path) {
+            Ok(repo) => {
+                let repo = repo.to_thread_local();
+                let git_config = repo.config_snapshot();
+
+                let path = raw_repo.path().to_string_lossy().to_string();
+
+                let owner = git_config
+                    .string("cgit.owner")
+                    .map(|v| v.to_string())
+                    .unwrap_or("<No owner>".to_owned());
+
+                let description = fs::read_to_string(repo.git_dir().join("description"))
+                    .unwrap_or("<No description>".to_owned());
+
+                Some(RepoValue {
+                    description,
+                    owner,
+                    path,
+                })
+            }
+            Err(err) => {
+                info!(
+                    "Repo '{}' could not be opened: '{err}'",
+                    raw_repo.path().display()
+                );
+                None
+            }
+        })
+        .collect();
+
+    Ok(ReposTemplate {
+        repos,
+        source_code_repository_url: config.source_code_repository_url.clone(),
+    }
+    .render()
+    .expect("this should work"))
+}
+
+pub fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> {
+    use rss::{ChannelBuilder, Item, ItemBuilder};
+
+    let repository = config
+        .repositories()?
+        .get(repo_path)?
+        .open(&config.scan_path)?
+        .to_thread_local();
+
+    let issues: Vec<CollapsedIssue> = issues_from_repository(&repository)?
+        .into_iter()
+        .map(|issue| issue.collapse())
+        .collect();
+
+    // Collect all Items as rss items
+    let mut items: Vec<Item> = issues
+        .iter()
+        .map(|issue| {
+            ItemBuilder::default()
+                .title(issue.title.to_string())
+                .author(issue.author.to_string())
+                .description(issue.message.to_string())
+                .pub_date(issue.timestamp.to_string())
+                .link(format!(
+                    "/{}/{}/issue/{}",
+                    repo_path.display(),
+                    &config.root_url,
+                    issue.id
+                ))
+                .build()
+        })
+        .collect();
+
+    // Append all comments after converting them to rss items
+    items.extend(
+        issues
+            .iter()
+            .filter(|issue| !issue.comments.is_empty())
+            .flat_map(|issue| {
+                issue
+                    .comments
+                    .iter()
+                    .map(|comment| {
+                        ItemBuilder::default()
+                            .title(issue.title.to_string())
+                            .author(comment.author.to_string())
+                            .description(comment.message.to_string())
+                            .pub_date(comment.timestamp.to_string())
+                            .link(format!(
+                                "/{}/{}/issue/{}",
+                                repo_path.display(),
+                                &config.root_url,
+                                issue.id
+                            ))
+                            .build()
+                    })
+                    .collect::<Vec<Item>>()
+            })
+            .collect::<Vec<Item>>(),
+    );
+
+    let channel = ChannelBuilder::default()
+        .title("Issues")
+        .link(config.root_url.to_string())
+        .description(format!("The rss feed for issues on {}.", &config.root_url))
+        .items(items)
+        .build();
+    Ok(channel.to_string())
+}
diff --git a/pkgs/by-name/ba/back/src/web/issue_html.rs b/pkgs/by-name/ba/back/src/web/issue_html.rs
deleted file mode 100644
index 45c0281..0000000
--- a/pkgs/by-name/ba/back/src/web/issue_html.rs
+++ /dev/null
@@ -1,166 +0,0 @@
-// Back - An extremely simple git issue tracking system. Inspired by tvix's
-// panettone
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This file is part of Back.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/agpl.txt>.
-
-use rocket::response::content::RawHtml;
-
-use crate::{
-    config::BackConfig,
-    git_bug::{
-        format::HtmlString,
-        issue::{identity::Author, CollapsedIssue, Comment},
-    },
-};
-
-impl CollapsedIssue {
-    pub fn to_list_entry(&self) -> RawHtml<String> {
-        let comment_list = if self.comments.is_empty() {
-            String::new()
-        } else {
-            let comments_string = if self.comments.len() > 1 {
-                "comments"
-            } else {
-                "comment"
-            };
-
-            format!(
-                r#"
-                <span class="comment-count"> - {} {}</span>
-            "#,
-                self.comments.len(),
-                comments_string
-            )
-        };
-
-        let CollapsedIssue {
-            id,
-            title,
-            message: _,
-            author,
-            timestamp,
-            comments: _,
-            status: _,
-            last_status_change: _,
-            labels: _,
-        } = self;
-
-        let Author { name, email, id: _ } = author;
-
-        RawHtml(format!(
-            r#"
-               <li>
-                  <a href="/issue/{id}">
-                     <p>
-                        <span class="issue-subject">{title}</span>
-                     </p>
-                     <span class="issue-number">{id}</span> - <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email">&lt;{email}&gt;</span> at <span class="timestamp">{timestamp}</span></span>{comment_list}                  </a>
-               </li>
-"#,
-        ))
-    }
-
-    pub fn to_html(&self, config: &BackConfig) -> RawHtml<String> {
-        let comments = if self.comments.is_empty() {
-            String::new()
-        } else {
-            let fmt_comments: String = self
-                .comments
-                .iter()
-                .map(|val| {
-                    let Comment {
-                        id,
-                        author,
-                        message,
-                        timestamp,
-                    } = val;
-                    let Author {
-                        name,
-                        email: _,
-                        id: _,
-                    } = author;
-
-                    format!(
-                        r#"
-               <li class="comment" id="{id}">
-                  {message}
-                  <p class="comment-info"><span class="user-name">{name} at {timestamp}</span></p>
-               </li>
-                "#,
-                    )
-                })
-                .collect::<Vec<String>>()
-                .join("\n");
-
-            format!(
-                r#"
-            <ol class="issue-history">
-            {fmt_comments}
-            </ol>
-            "#
-            )
-        };
-
-        {
-            let CollapsedIssue {
-                id,
-                title,
-                message,
-                author,
-                timestamp,
-                comments: _,
-                status: _,
-                last_status_change: _,
-                labels: _,
-            } = self;
-            let Author { name, email, id: _ } = author;
-            let html_title = HtmlString::from(title.clone());
-
-            RawHtml(format!(
-                r#"
-<!DOCTYPE html>
-<html lang="en">
-   <head>
-      <title>{html_title} | Back</title>
-      <link href="/style.css" rel="stylesheet" type="text/css">
-      <meta content="width=device-width,initial-scale=1" name="viewport">
-   </head>
-   <body>
-      <div class="content">
-         <nav>
-         <a href="/issues/open">Open Issues</a>
-         <a href="/issues/closed">Closed Issues</a>
-         </nav>
-         <header>
-            <h1>{title}</h1>
-            <div class="issue-number">{id}</div>
-         </header>
-         <main>
-            <div class="issue-info">
-                <span class="created-by-at">Opened by <span class="user-name">{name}</span> <span class="user-email">&lt;{email}&gt;</span> at <span class="timestamp">{timestamp}</span></span>
-            </div>
-            {message}
-            {comments}
-         </main>
-         <footer>
-            <nav>
-            <a href="/issues/open">Open Issues</a>
-            <a href="{}">Source code</a>
-            <a href="/issues/closed">Closed Issues</a>
-            </nav>
-         </footer>
-      </div>
-   </body>
-</html>
-"#,
-                config.source_code_repository_url
-            ))
-        }
-    }
-}
diff --git a/pkgs/by-name/ba/back/src/web/mod.rs b/pkgs/by-name/ba/back/src/web/mod.rs
index f7a4077..cc087ab 100644
--- a/pkgs/by-name/ba/back/src/web/mod.rs
+++ b/pkgs/by-name/ba/back/src/web/mod.rs
@@ -1,186 +1,127 @@
-// Back - An extremely simple git issue tracking system. Inspired by tvix's
-// panettone
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This file is part of Back.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/agpl.txt>.
-
-use crate::{
-    config::BackConfig,
-    error::{self, Error},
-    git_bug::{
-        dag::issues_from_repository,
-        issue::{CollapsedIssue, Status},
-    },
-};
-use prefix::BackPrefix;
-use rocket::{
-    get,
-    response::content::{RawCss, RawHtml},
-    State,
-};
-
-mod issue_html;
-pub mod prefix;
-
-#[get("/style.css")]
-pub fn styles() -> RawCss<String> {
-    RawCss(include_str!("../../assets/style.css").to_owned())
-}
+use bytes::Bytes;
+use http_body_util::combinators::BoxBody;
+use hyper::{server::conn::http1, service::service_fn, Method, Request, Response, StatusCode};
+use hyper_util::rt::TokioIo;
+use log::{error, info};
+use responses::{html_response, html_response_status, html_response_status_content_type};
+use tokio::net::TcpListener;
+
+use std::{convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc};
+
+use crate::{config::BackConfig, error, git_bug::issue::Status};
+
+mod generate;
+mod responses;
+
+async fn match_uri(
+    config: Arc<BackConfig>,
+    req: Request<hyper::body::Incoming>,
+) -> Result<Response<BoxBody<Bytes, Infallible>>, hyper::Error> {
+    if req.method() != Method::GET {
+        return Ok(html_response_status(
+            "Only get requests are supported",
+            StatusCode::NOT_ACCEPTABLE,
+        ));
+    }
 
-pub fn issue_list_boilerplate(
-    config: &State<BackConfig>,
-    wanted_status: Status,
-    counter_status: Status,
-) -> error::Result<RawHtml<String>> {
-    let repository = &config.repository;
-
-    let mut issue_list = issues_from_repository(&repository.to_thread_local())?
-        .into_iter()
-        .map(|issue| issue.collapse())
-        .collect::<Vec<CollapsedIssue>>();
-
-    // Sort by date descending.
-    issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() });
-    issue_list.reverse();
-
-    let issue_list_str = issue_list.into_iter().fold(String::new(), |acc, issue| {
-        format!("{}{}", acc, {
-            if issue.status == wanted_status {
-                let issue_entry = issue.to_list_entry();
-                issue_entry.0
-            } else {
-                String::new()
+    let output = || -> Result<Response<BoxBody<Bytes, Infallible>>, error::Error> {
+        match req.uri().path().trim_end_matches("/") {
+            "" => Ok(html_response(generate::repos(&config)?)),
+
+            "/style.css" => Ok(responses::html_response_status_content_type(
+                include_str!("../../assets/style.css"),
+                StatusCode::OK,
+                "text/css",
+            )),
+
+            path if path.ends_with("/issues/open") => {
+                let repo_path = PathBuf::from(
+                    path.strip_suffix("/issues/open")
+                        .expect("This suffix exists")
+                        .strip_prefix("/")
+                        .expect("This also exists"),
+                );
+
+                let issues = generate::issues(&config, Status::Open, Status::Closed, &repo_path)?;
+                Ok(html_response(issues))
+            }
+            path if path.ends_with("/issues/closed") => {
+                let repo_path = PathBuf::from(
+                    path.strip_suffix("/issues/closed")
+                        .expect("This suffix exists")
+                        .strip_prefix("/")
+                        .expect("This also exists"),
+                );
+
+                let issues = generate::issues(&config, Status::Closed, Status::Open, &repo_path)?;
+                Ok(html_response(issues))
+            }
+            path if path.ends_with("/issues/feed") => {
+                let repo_path = PathBuf::from(
+                    path.strip_suffix("/issues/feed")
+                        .expect("This suffix exists")
+                        .strip_prefix("/")
+                        .expect("This also exists"),
+                );
+
+                let feed = generate::feed(&config, &repo_path)?;
+                Ok(html_response_status_content_type(
+                    feed,
+                    StatusCode::OK,
+                    "text/xml",
+                ))
             }
-        })
-    });
-
-    let counter_status_lower = counter_status.to_string().to_lowercase();
-    Ok(RawHtml(format!(
-        r#"
-    <!DOCTYPE html>
-    <html lang="en">
-       <head>
-          <title>Back</title>
-          <link href="/style.css" rel="stylesheet" type="text/css">
-          <meta content="width=device-width,initial-scale=1" name="viewport">
-       </head>
-       <body>
-          <div class="content">
-             <header>
-                <h1>{wanted_status} Issues</h1>
-             </header>
-             <main>
-                <div class="issue-links">
-                   <a href="/issues/{counter_status_lower}/">View {counter_status} issues</a>
-                   <a href="{}">Source code</a>
-                   <!--
-                   <form class="issue-search" method="get">
-                       <input name="search" title="Issue search query" type="search">
-                       <input class="sr-only" type="submit" value="Search Issues">
-                   </form>
-                   -->
-                </div>
-                <ol class="issue-list">
-                {issue_list_str}
-                </ol>
-             </main>
-          </div>
-       </body>
-    </html>
-    "#,
-        config.source_code_repository_url
-    )))
-}
 
-#[get("/issues/open")]
-pub fn open(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
-    issue_list_boilerplate(config, Status::Open, Status::Closed)
-}
-#[get("/issues/closed")]
-pub fn closed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
-    issue_list_boilerplate(config, Status::Closed, Status::Open)
-}
+            path if path.contains("/issue/") => {
+                let (repo_path, prefix) = {
+                    let split: Vec<&str> = path.split("/issue/").collect();
+
+                    let prefix =
+                        gix::hash::Prefix::from_hex(split[1]).map_err(error::Error::from)?;
+
+                    let repo_path =
+                        PathBuf::from(split[0].strip_prefix("/").expect("This prefix exists"));
 
-#[get("/issues/feed")]
-pub fn feed(config: &State<BackConfig>) -> error::Result<RawHtml<String>> {
-    use rss::{ChannelBuilder, Item, ItemBuilder};
-
-    //Collect all Items as rss items
-    let mut items: Vec<Item> = issues_from_repository(&config.repository.to_thread_local())?
-        .into_iter()
-        .map(|issue| issue.collapse())
-        .map(|issue| {
-            ItemBuilder::default()
-                .title(issue.title.to_string())
-                .author(issue.author.to_string())
-                .description(issue.message.to_string())
-                .pub_date(issue.timestamp.to_string())
-                .link(format!("{}/issue/{}", &config.root.to_string(), issue.id))
-                .build()
-        })
-        .collect();
-    //Append all comments after converting them to rss items
-    items.extend(
-        issues_from_repository(&config.repository.to_thread_local())?
-            .into_iter()
-            .map(|issue| issue.collapse())
-            .filter(|issue| issue.comments.len() > 0)
-            .map(|issue| {
-                issue
-                    .comments
-                    .into_iter()
-                    .map(|comment| {
-                        ItemBuilder::default()
-                            .title(issue.title.to_string())
-                            .author(comment.author.to_string())
-                            .description(comment.message.to_string())
-                            .pub_date(comment.timestamp.to_string())
-                            .link(format!("{}/issue/{}", &config.root.to_string(), issue.id))
-                            .build()
-                    })
-                    .collect::<Vec<Item>>()
-            })
-            .flatten()
-            .collect::<Vec<Item>>(),
-    );
-
-    let channel = ChannelBuilder::default()
-        .title("Issues")
-        .link(config.root.to_string())
-        .description(format!("The rss feed for issues on {}.", config.root))
-        .items(items)
-        .build();
-    Ok(RawHtml(channel.to_string()))
+                    (repo_path, prefix)
+                };
+                Ok(html_response(generate::issue(&config, &repo_path, prefix)?))
+            }
+
+            other => Ok(responses::html_response_status_content_type(
+                format!("'{}' not found", other),
+                StatusCode::NOT_FOUND,
+                "text/plain",
+            )),
+        }
+    };
+    match output() {
+        Ok(response) => Ok(response),
+        Err(err) => Ok(err.into_response()),
+    }
 }
 
-#[get("/issue/<prefix>")]
-pub fn show_issue(
-    config: &State<BackConfig>,
-    prefix: Result<BackPrefix, gix::hash::prefix::from_hex::Error>,
-) -> error::Result<RawHtml<String>> {
-    // NOTE(@bpeetz): Explicitly unwrap the `prefix` here (instead of taking the unwrapped value as
-    // argument), to avoid triggering rockets "errors forward to the next route" feature.
-    // This ensures, that our error message actually reaches the user. <2024-12-26>
-    let prefix = prefix?;
-
-    let repository = config.repository.to_thread_local();
-
-    let all_issues: Vec<CollapsedIssue> = issues_from_repository(&repository)?
-        .into_iter()
-        .map(|val| val.collapse())
-        .collect();
-
-    let maybe_issue = all_issues
-        .iter()
-        .find(|issue| issue.id.to_string().starts_with(&prefix.to_string()));
-
-    match maybe_issue {
-        Some(issue) => Ok(issue.to_html(config)),
-        None => Err(Error::IssuesPrefixMissing { prefix }),
+pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> {
+    let addr: SocketAddr = ([127, 0, 0, 1], 8000).into();
+
+    let listener = TcpListener::bind(addr)
+        .await
+        .map_err(|err| error::Error::TcpBind { addr, err })?;
+    info!("Listening on http://{}", addr);
+    loop {
+        let (stream, _) = listener
+            .accept()
+            .await
+            .map_err(|err| error::Error::TcpAccept { err })?;
+        let io = TokioIo::new(stream);
+
+        let local_config = Arc::clone(&config);
+
+        let service = service_fn(move |req| match_uri(Arc::clone(&local_config), req));
+
+        tokio::task::spawn(async move {
+            if let Err(err) = http1::Builder::new().serve_connection(io, service).await {
+                error!("Error serving connection: {:?}", err);
+            }
+        });
     }
 }
diff --git a/pkgs/by-name/ba/back/src/web/prefix.rs b/pkgs/by-name/ba/back/src/web/prefix.rs
deleted file mode 100644
index 5143799..0000000
--- a/pkgs/by-name/ba/back/src/web/prefix.rs
+++ /dev/null
@@ -1,35 +0,0 @@
-// Back - An extremely simple git issue tracking system. Inspired by tvix's
-// panettone
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This file is part of Back.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/agpl.txt>.
-
-use std::fmt::Display;
-
-use gix::hash::Prefix;
-use rocket::request::FromParam;
-
-#[derive(Debug)]
-pub struct BackPrefix {
-    prefix: Prefix,
-}
-impl Display for BackPrefix {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.prefix.fmt(f)
-    }
-}
-
-impl<'a> FromParam<'a> for BackPrefix {
-    type Error = gix::hash::prefix::from_hex::Error;
-
-    fn from_param(param: &'a str) -> Result<Self, Self::Error> {
-        let prefix = Prefix::from_hex(param)?;
-
-        Ok(Self { prefix })
-    }
-}
diff --git a/pkgs/by-name/ba/back/src/web/responses.rs b/pkgs/by-name/ba/back/src/web/responses.rs
new file mode 100644
index 0000000..e50f8c2
--- /dev/null
+++ b/pkgs/by-name/ba/back/src/web/responses.rs
@@ -0,0 +1,50 @@
+use std::convert::Infallible;
+
+use bytes::Bytes;
+use http::{Response, StatusCode, Version};
+use http_body_util::{combinators::BoxBody, BodyExt, Full};
+
+use crate::{error, git_bug::format::HtmlString};
+
+pub(super) fn html_response<T: Into<Bytes>>(html_text: T) -> Response<BoxBody<Bytes, Infallible>> {
+    html_response_status(html_text, StatusCode::OK)
+}
+
+pub(super) fn html_response_status<T: Into<Bytes>>(
+    html_text: T,
+    status: StatusCode,
+) -> Response<BoxBody<Bytes, Infallible>> {
+    html_response_status_content_type(html_text, status, "text/html")
+}
+
+pub(super) fn html_response_status_content_type<T: Into<Bytes>>(
+    html_text: T,
+    status: StatusCode,
+    content_type: &str,
+) -> Response<BoxBody<Bytes, Infallible>> {
+    Response::builder()
+        .status(status)
+        .version(Version::HTTP_2)
+        .header("Content-Type", format!("{}; charset=utf-8", content_type))
+        .header("x-content-type-options", "nosniff")
+        .header("x-frame-options", "SAMEORIGIN")
+        .body(full(html_text))
+        .expect("This will always build")
+}
+
+fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Infallible> {
+    Full::new(chunk.into()).boxed()
+}
+
+// FIXME: Not all errors should return `INTERNAL_SERVER_ERROR`. <2025-03-08>
+impl error::Error {
+    pub fn into_response(self) -> Response<BoxBody<Bytes, Infallible>> {
+        html_response_status(
+            format!(
+                "<h1> Internal server error. </h1> <pre>Error: {}</pre>",
+                HtmlString::from(self.to_string())
+            ),
+            StatusCode::INTERNAL_SERVER_ERROR,
+        )
+    }
+}
diff --git a/pkgs/by-name/ba/back/templates/issue.html b/pkgs/by-name/ba/back/templates/issue.html
new file mode 100644
index 0000000..5b452c5
--- /dev/null
+++ b/pkgs/by-name/ba/back/templates/issue.html
@@ -0,0 +1,57 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <title>{{ HtmlString::from(issue.title.clone()) }} | Back</title>
+        <link
+            href="/style.css"
+            rel="stylesheet"
+            type="text/css" />
+        <meta
+            content="width=device-width,initial-scale=1"
+            name="viewport" />
+    </head>
+    <body>
+        <div class="content">
+            <nav>
+                <a href="/{{repo_path}}/issues/open">Open Issues</a>
+                <a href="/{{repo_path}}/issues/closed">Closed Issues</a>
+            </nav>
+            <header>
+                <h1>{{issue.title|safe}}</h1>
+                <div class="issue-number">{{issue.id}}</div>
+            </header>
+            <main>
+                <div class="issue-info">
+                    <span class="created-by-at"
+                        >Opened by <span class="user-name">{{issue.author.name|safe}}</span>
+                        <span class="user-email">&lt;{{issue.author.email|safe}}&gt;</span> at
+                        <span class="timestamp">{{issue.timestamp}}</span></span
+                    >
+                </div>
+                {{issue.message|safe}} {% if !issue.comments.is_empty() %}
+                <ol class="issue-history">
+                    {% for comment in issue.comments %}
+                    <li
+                        class="comment"
+                        id="{{comment.id}}">
+                        {{comment.message|safe}}
+                        <p class="comment-info">
+                            <span class="user-name"
+                                >{{comment.author.name|safe}} at {{comment.timestamp}}</span
+                            >
+                        </p>
+                    </li>
+                    {% endfor %}
+                </ol>
+                {% endif %}
+            </main>
+            <footer>
+                <nav>
+                    <a href="/{{repo_path}}/issues/open">Open Issues</a>
+                    <a href="{{source_code_repository_url}}">Source code</a>
+                    <a href="/{{repo_path}}/issues/closed">Closed Issues</a>
+                </nav>
+            </footer>
+        </div>
+    </body>
+</html>
diff --git a/pkgs/by-name/ba/back/templates/issues.html b/pkgs/by-name/ba/back/templates/issues.html
new file mode 100644
index 0000000..b6cc9b8
--- /dev/null
+++ b/pkgs/by-name/ba/back/templates/issues.html
@@ -0,0 +1,60 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <title>Back</title>
+        <link
+            href="/style.css"
+            rel="stylesheet"
+            type="text/css" />
+        <meta
+            content="width=device-width,initial-scale=1"
+            name="viewport" />
+    </head>
+    <body>
+        <div class="content">
+            <header>
+                <h1>{{wanted_status}} Issues</h1>
+            </header>
+            <main>
+                <div class="issue-links">
+                    <a href="/{{repo_path}}/issues/{{counter_status|lowercase}}/"
+                        >View {{counter_status}} issues</a
+                    >
+                    <a href="{{source_code_repository_url}}">Source code</a>
+                    <!--
+               <form class="issue-search" method="get">
+                   <input name="search" title="Issue search query" type="search">
+                   <input class="sr-only" type="submit" value="Search Issues">
+               </form>
+               -->
+                </div>
+                <ol class="issue-list">
+                    {% for issue in issues -%}
+                    <li>
+                        <a href="/{{repo_path}}/issue/{{issue.id}}">
+                            <p>
+                                <span class="issue-subject">{{issue.title|safe}}</span>
+                            </p>
+                            <span class="issue-number">{{issue.id}}</span>
+                            <span class="created-by-at"
+                                >Opened by {{ " " }}
+                                <span class="user-name">{{issue.author.name|safe}}</span>
+                                {{ " " }}
+                                <span class="user-email">&lt;{{issue.author.email|safe}}&gt;</span>
+                                {{ "at" }}
+                                <span class="timestamp">{{issue.timestamp}}</span>
+                            </span>
+                            {% if !issue.comments.is_empty() +%}
+                            <span class="comment-count">
+                                - {{issue.comments.len()}}
+                                comment{{issue.comments.len()|pluralize}}</span
+                            >
+                            {%+ endif %}
+                        </a>
+                    </li>
+                    {%- endfor %}
+                </ol>
+            </main>
+        </div>
+    </body>
+</html>
diff --git a/pkgs/by-name/ba/back/templates/repos.html b/pkgs/by-name/ba/back/templates/repos.html
new file mode 100644
index 0000000..dbccba0
--- /dev/null
+++ b/pkgs/by-name/ba/back/templates/repos.html
@@ -0,0 +1,47 @@
+<!doctype html>
+<html lang="en">
+    <head>
+        <title>Back</title>
+        <link
+            href="/style.css"
+            rel="stylesheet"
+            type="text/css" />
+        <meta
+            content="width=device-width,initial-scale=1"
+            name="viewport" />
+    </head>
+    <body>
+        <div class="content">
+            <header>
+                <h1>Repositories</h1>
+            </header>
+            <main>
+                <div class="issue-links">
+                    <a href="{{source_code_repository_url}}">Source code</a>
+                    <!--
+               <form class="issue-search" method="get">
+                   <input name="search" title="Issue search query" type="search">
+                   <input class="sr-only" type="submit" value="Search Issues">
+               </form>
+               -->
+                </div>
+                <ol class="issue-list">
+                    {% for repo in repos -%}
+                    <li>
+                        <a href="/{{repo.path}}/issues/open">
+                            <p>
+                                <span class="issue-subject">{{repo.path}}</span>
+                            </p>
+                            <span class="created-by-at">
+                                <span class="timestamp">{{repo.description}}</span>
+                                {{ "-" }}
+                                <span class="user-name">{{repo.owner}}</span>
+                            </span>
+                        </a>
+                    </li>
+                    {%- endfor %}
+                </ol>
+            </main>
+        </div>
+    </body>
+</html>
diff --git a/pkgs/by-name/ba/back/update.sh b/pkgs/by-name/ba/back/update.sh
index c715a63..11bd23a 100755
--- a/pkgs/by-name/ba/back/update.sh
+++ b/pkgs/by-name/ba/back/update.sh
@@ -11,5 +11,5 @@
 # You should have received a copy of the License along with this program.
 # If not, see <https://www.gnu.org/licenses/agpl.txt>.
 
-[ "$1" = "upgrade" ] && cargo upgrade
+[ "$1" = "upgrade" ] && cargo upgrade --incompatible
 cargo update
diff --git a/pkgs/by-name/fe/fetchmail-common-name/package.nix b/pkgs/by-name/fe/fetchmail-common-name/package.nix
new file mode 100644
index 0000000..9e89bed
--- /dev/null
+++ b/pkgs/by-name/fe/fetchmail-common-name/package.nix
@@ -0,0 +1,15 @@
+{
+  pkgs,
+  callPackage,
+}:
+pkgs.fetchmail.overrideAttrs (final: prev: {
+  pname = "fetchmail-common-name";
+
+  patches =
+    (prev.patches or [])
+    ++ [
+      ./patches/fix-socket.c-Correctly-check-the-common-name-even-if.patch
+    ];
+
+  meta.mainProgram = prev.meta.mainProgram or "fetchmail";
+})
diff --git a/pkgs/by-name/fe/fetchmail-common-name/patches/fix-socket.c-Correctly-check-the-common-name-even-if.patch b/pkgs/by-name/fe/fetchmail-common-name/patches/fix-socket.c-Correctly-check-the-common-name-even-if.patch
new file mode 100644
index 0000000..aa17799
--- /dev/null
+++ b/pkgs/by-name/fe/fetchmail-common-name/patches/fix-socket.c-Correctly-check-the-common-name-even-if.patch
@@ -0,0 +1,40 @@
+From 77a13d5625890d6a0dc3ec312b9d237e61791033 Mon Sep 17 00:00:00 2001
+From: Benedikt Peetz <benedikt.peetz@b-peetz.de>
+Date: Sat, 1 Mar 2025 18:20:35 +0100
+Subject: [PATCH] fix(socket.c): Correctly check the common name, even if not
+ set (only SAN)
+
+---
+ socket.c | 9 ---------
+ 1 file changed, 9 deletions(-)
+
+diff --git a/socket.c b/socket.c
+index 42b8f1a5..c81bca63 100644
+--- a/socket.c
++++ b/socket.c
+@@ -728,7 +728,6 @@ static int SSL_verify_callback(int ok_return, X509_STORE_CTX *ctx, const int str
+ 			_depth0ck = 1;
+ 		}
+ 
+-		if ((i = X509_NAME_get_text_by_NID(subj, NID_commonName, buf, sizeof(buf))) != -1) {
+ 			if (_ssl_server_cname != NULL) {
+ 				char *p1 = buf;
+ 				char *p2 = _ssl_server_cname;
+@@ -779,14 +778,6 @@ static int SSL_verify_callback(int ok_return, X509_STORE_CTX *ctx, const int str
+ 				report(stderr, GT_("Server name not set, could not verify certificate!\n"));
+ 				if (strict) return (0);
+ 			}
+-		} else {
+-			if (outlevel >= O_VERBOSE)
+-				report(stdout, GT_("Unknown Server CommonName\n"));
+-			if (ok_return && strict) {
+-				report(stderr, GT_("Server name not specified in certificate!\n"));
+-				return (0);
+-			}
+-		}
+ 		/* Print the finger print. Note that on errors, we might print it more than once
+ 		 * normally; we kluge around that by using a global variable. */
+ 		if (_check_fp == 1) {
+-- 
+2.47.2
+
diff --git a/pkgs/by-name/st/stalwart-mail-free/mail-send.nix b/pkgs/by-name/st/stalwart-mail-free/mail-send.nix
new file mode 100644
index 0000000..e0d8c57
--- /dev/null
+++ b/pkgs/by-name/st/stalwart-mail-free/mail-send.nix
@@ -0,0 +1,20 @@
+{
+  stdenv,
+  fetchFromGitHub,
+}:
+stdenv.mkDerivation (finalAttrs: {
+  pname = "mail-send";
+  version = "0.5.0";
+
+  src = fetchFromGitHub {
+    owner = "stalwartlabs";
+    repo = "mail-send";
+    tag = "v${finalAttrs.version}";
+    hash = "sha256-uDD4GLwjRpNqjtXPMask0twGW2Gcm1PFyDGXcfPS0F4=";
+  };
+
+  installPhase = ''
+    mkdir --parents "$out"
+    cp --recursive ./. "$out/"
+  '';
+})
diff --git a/pkgs/by-name/st/stalwart-mail-free/package.nix b/pkgs/by-name/st/stalwart-mail-free/package.nix
new file mode 100644
index 0000000..bb2c1db
--- /dev/null
+++ b/pkgs/by-name/st/stalwart-mail-free/package.nix
@@ -0,0 +1,77 @@
+{
+  pkgsUnstable,
+  callPackage,
+  nixLib,
+}: let
+  spamfilter = callPackage ./spam-filter.nix {};
+
+  mail-send = callPackage ./mail-send.nix {};
+
+  # Need to use the newer `rustPlatform`
+  inherit (pkgsUnstable) rustPlatform;
+in
+  pkgsUnstable.stalwart-mail.override {
+    rustPlatform =
+      rustPlatform
+      // {
+        buildRustPackage = prev:
+          rustPlatform.buildRustPackage (
+            prev
+            // {
+              pname = "stalwart-mail-free";
+              passthru = nixLib.warnMerge (prev.passthru or {}) {
+                inherit spamfilter;
+              } "stalwart-mail passthru";
+
+              useFetchCargoVendor = true;
+              cargoHash = "sha256-Qg01QXP/ImRCUw3aXcZbnM1hysHUwozCdQ7LecjUa0o=";
+
+              # The tests should check if this works.
+              # And this shaves of around 50% of the build time.
+              doCheck = false;
+
+              buildNoDefaultFeatures = true;
+              buildFeatures = [
+                "rocks"
+                "redis"
+              ];
+
+              postUnpack =
+                (prev.postUnpack or "")
+                + ''
+                  cp --recursive "${mail-send}" ./source/crates/mail-send
+                  chmod -R +w "./source/crates/mail-send"
+                '';
+
+              cargoPatches =
+                (prev.cargoPatches or [])
+                ++ [
+                  # `stalwart-mail` does enable their `enterprise` feature per default.
+                  # We want a AGPL only build (i.e., without unfree dependencies), therefore disable the
+                  # `enterprise` feature here.
+                  # We cannot use the `buildFeatures` attribute because it does not actually change the
+                  # correct features. As such we simply patch the correct `Cargo.toml` file.
+                  ./patches/crates-main-Cargo.toml-Use-libre-features.patch
+
+                  # `stalwart-mail` uses their bundled store, which makes it impossible to use our
+                  # own CA certificate (e.g., for tests). Thus use a native version.
+                  ./patches/crates-Use-the-platform-CA-bundle-instead-of-the-bun.patch
+                ];
+
+              # Check that the enterprise feature is really disabled.
+              postCheck =
+                (prev.postCheck or "")
+                +
+                # bash
+                ''
+                  if grep "enterprise" ./target/*/release/stalwart-mail.d; then
+                    echo "ERROR: Proprietary 'enterprise' feature active."
+                    exit 1
+                  fi
+                '';
+
+              meta.mainProgram = prev.meta.mainProgram or "stalwart-mail";
+            }
+          );
+      };
+  }
diff --git a/pkgs/by-name/st/stalwart-mail-free/patches/crates-Use-the-platform-CA-bundle-instead-of-the-bun.patch b/pkgs/by-name/st/stalwart-mail-free/patches/crates-Use-the-platform-CA-bundle-instead-of-the-bun.patch
new file mode 100644
index 0000000..e6c3d4b
--- /dev/null
+++ b/pkgs/by-name/st/stalwart-mail-free/patches/crates-Use-the-platform-CA-bundle-instead-of-the-bun.patch
@@ -0,0 +1,879 @@
+From 6825a35213d604a7149265af2346a69143c0853b Mon Sep 17 00:00:00 2001
+From: Benedikt Peetz <benedikt.peetz@b-peetz.de>
+Date: Tue, 4 Mar 2025 19:15:06 +0100
+Subject: [PATCH] crates/*: Use the platform CA bundle instead of the
+ bundled certificates
+
+---
+ Cargo.lock                       | 284 ++++++++++++++++++++++++++++++-
+ crates/cli/Cargo.toml            |   2 +-
+ crates/common/Cargo.toml         |   4 +-
+ crates/directory/Cargo.toml      |   4 +-
+ crates/imap/Cargo.toml           |   2 +-
+ crates/jmap/Cargo.toml           |   4 +-
+ crates/mail-send/Cargo.toml      |   1 +
+ crates/mail-send/src/smtp/tls.rs |  22 +--
+ crates/managesieve/Cargo.toml    |   2 +-
+ crates/pop3/Cargo.toml           |   2 +-
+ crates/smtp/Cargo.toml           |   4 +-
+ crates/spam-filter/Cargo.toml    |   4 +-
+ crates/store/Cargo.toml          |   2 +-
+ crates/trc/Cargo.toml            |   2 +-
+ crates/utils/Cargo.toml          |   5 +-
+ crates/utils/src/lib.rs          |  16 +-
+ tests/Cargo.toml                 |  10 +-
+ 17 files changed, 314 insertions(+), 56 deletions(-)
+
+diff --git a/Cargo.lock b/Cargo.lock
+index be36759b..eca9699f 100644
+--- a/Cargo.lock
++++ b/Cargo.lock
+@@ -440,6 +440,47 @@ dependencies = [
+  "url",
+ ]
+ 
++[[package]]
++name = "aws-lc-fips-sys"
++version = "0.13.3"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "29003a681b2b9465c1139bfb726da452a841a8b025f35953f3bce71139f10b21"
++dependencies = [
++ "bindgen 0.69.5",
++ "cc",
++ "cmake",
++ "dunce",
++ "fs_extra",
++ "paste",
++ "regex",
++]
++
++[[package]]
++name = "aws-lc-rs"
++version = "1.12.5"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "5e4e8200b9a4a5801a769d50eeabc05670fec7e959a8cb7a63a93e4e519942ae"
++dependencies = [
++ "aws-lc-fips-sys",
++ "aws-lc-sys",
++ "paste",
++ "zeroize",
++]
++
++[[package]]
++name = "aws-lc-sys"
++version = "0.26.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "0f9dd2e03ee80ca2822dd6ea431163d2ef259f2066a4d6ccaca6d9dcb386aa43"
++dependencies = [
++ "bindgen 0.69.5",
++ "cc",
++ "cmake",
++ "dunce",
++ "fs_extra",
++ "paste",
++]
++
+ [[package]]
+ name = "aws-region"
+ version = "0.25.5"
+@@ -673,12 +714,15 @@ dependencies = [
+  "itertools 0.12.1",
+  "lazy_static",
+  "lazycell",
++ "log",
++ "prettyplease",
+  "proc-macro2",
+  "quote",
+  "regex",
+  "rustc-hash 1.1.0",
+  "shlex",
+  "syn 2.0.96",
++ "which",
+ ]
+ 
+ [[package]]
+@@ -1035,6 +1079,12 @@ dependencies = [
+  "smallvec",
+ ]
+ 
++[[package]]
++name = "cesu8"
++version = "1.1.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
++
+ [[package]]
+ name = "cexpr"
+ version = "0.6.0"
+@@ -1347,6 +1397,16 @@ dependencies = [
+  "libc",
+ ]
+ 
++[[package]]
++name = "core-foundation"
++version = "0.10.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
++dependencies = [
++ "core-foundation-sys",
++ "libc",
++]
++
+ [[package]]
+ name = "core-foundation-sys"
+ version = "0.8.7"
+@@ -1912,6 +1972,12 @@ dependencies = [
+  "zeroize",
+ ]
+ 
++[[package]]
++name = "dunce"
++version = "1.0.5"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
++
+ [[package]]
+ name = "dyn-clone"
+ version = "1.0.17"
+@@ -2117,6 +2183,29 @@ dependencies = [
+  "syn 2.0.96",
+ ]
+ 
++[[package]]
++name = "env_filter"
++version = "0.1.3"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0"
++dependencies = [
++ "log",
++ "regex",
++]
++
++[[package]]
++name = "env_logger"
++version = "0.11.6"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
++dependencies = [
++ "anstream",
++ "anstyle",
++ "env_filter",
++ "humantime",
++ "log",
++]
++
+ [[package]]
+ name = "equivalent"
+ version = "1.0.1"
+@@ -2423,6 +2512,12 @@ dependencies = [
+  "syn 2.0.96",
+ ]
+ 
++[[package]]
++name = "fs_extra"
++version = "1.3.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
++
+ [[package]]
+ name = "funty"
+ version = "2.0.0"
+@@ -2974,6 +3069,12 @@ version = "0.4.3"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "9994b79e8c1a39b3166c63ae7823bb2b00831e2a96a31399c50fe69df408eaeb"
+ 
++[[package]]
++name = "humantime"
++version = "2.1.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
++
+ [[package]]
+ name = "hyper"
+ version = "0.14.32"
+@@ -3044,6 +3145,7 @@ dependencies = [
+  "hyper 1.6.0",
+  "hyper-util",
+  "rustls 0.23.21",
++ "rustls-native-certs 0.8.1",
+  "rustls-pki-types",
+  "tokio",
+  "tokio-rustls 0.26.1",
+@@ -3607,6 +3709,28 @@ dependencies = [
+  "utils",
+ ]
+ 
++[[package]]
++name = "jni"
++version = "0.21.1"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
++dependencies = [
++ "cesu8",
++ "cfg-if",
++ "combine",
++ "jni-sys",
++ "log",
++ "thiserror 1.0.69",
++ "walkdir",
++ "windows-sys 0.45.0",
++]
++
++[[package]]
++name = "jni-sys"
++version = "0.3.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
++
+ [[package]]
+ name = "jobserver"
+ version = "0.1.32"
+@@ -3959,14 +4083,18 @@ dependencies = [
+ [[package]]
+ name = "mail-send"
+ version = "0.5.0"
+-source = "registry+https://github.com/rust-lang/crates.io-index"
+-checksum = "b12277cdcacfc15af67fe9cf155f31ff68ad8c301304573ea116ed8870f192d5"
+ dependencies = [
+  "base64 0.22.1",
++ "env_logger",
+  "gethostname",
++ "mail-auth",
++ "mail-builder",
++ "mail-parser",
+  "md5",
++ "rand 0.8.5",
+  "rustls 0.23.21",
+  "rustls-pki-types",
++ "rustls-platform-verifier",
+  "smtp-proto",
+  "tokio",
+  "tokio-rustls 0.26.1",
+@@ -5552,6 +5680,7 @@ dependencies = [
+  "pin-project-lite",
+  "quinn",
+  "rustls 0.23.21",
++ "rustls-native-certs 0.8.1",
+  "rustls-pemfile 2.2.0",
+  "rustls-pki-types",
+  "serde",
+@@ -5920,6 +6049,8 @@ version = "0.23.21"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
+ dependencies = [
++ "aws-lc-rs",
++ "log",
+  "once_cell",
+  "ring 0.17.8",
+  "rustls-pki-types",
+@@ -5937,7 +6068,7 @@ dependencies = [
+  "openssl-probe",
+  "rustls-pemfile 1.0.4",
+  "schannel",
+- "security-framework",
++ "security-framework 2.11.1",
+ ]
+ 
+ [[package]]
+@@ -5950,7 +6081,19 @@ dependencies = [
+  "rustls-pemfile 2.2.0",
+  "rustls-pki-types",
+  "schannel",
+- "security-framework",
++ "security-framework 2.11.1",
++]
++
++[[package]]
++name = "rustls-native-certs"
++version = "0.8.1"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3"
++dependencies = [
++ "openssl-probe",
++ "rustls-pki-types",
++ "schannel",
++ "security-framework 3.2.0",
+ ]
+ 
+ [[package]]
+@@ -5980,6 +6123,33 @@ dependencies = [
+  "web-time",
+ ]
+ 
++[[package]]
++name = "rustls-platform-verifier"
++version = "0.5.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "e012c45844a1790332c9386ed4ca3a06def221092eda277e6f079728f8ea99da"
++dependencies = [
++ "core-foundation 0.10.0",
++ "core-foundation-sys",
++ "jni",
++ "log",
++ "once_cell",
++ "rustls 0.23.21",
++ "rustls-native-certs 0.8.1",
++ "rustls-platform-verifier-android",
++ "rustls-webpki 0.102.8",
++ "security-framework 3.2.0",
++ "security-framework-sys",
++ "webpki-root-certs",
++ "windows-sys 0.52.0",
++]
++
++[[package]]
++name = "rustls-platform-verifier-android"
++version = "0.1.1"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
++
+ [[package]]
+ name = "rustls-webpki"
+ version = "0.101.7"
+@@ -5996,6 +6166,7 @@ version = "0.102.8"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
+ dependencies = [
++ "aws-lc-rs",
+  "ring 0.17.8",
+  "rustls-pki-types",
+  "untrusted 0.9.0",
+@@ -6125,7 +6296,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+ dependencies = [
+  "bitflags 2.8.0",
+- "core-foundation",
++ "core-foundation 0.9.4",
++ "core-foundation-sys",
++ "libc",
++ "security-framework-sys",
++]
++
++[[package]]
++name = "security-framework"
++version = "3.2.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316"
++dependencies = [
++ "bitflags 2.8.0",
++ "core-foundation 0.10.0",
+  "core-foundation-sys",
+  "libc",
+  "security-framework-sys",
+@@ -6817,7 +7001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
+ dependencies = [
+  "bitflags 1.3.2",
+- "core-foundation",
++ "core-foundation 0.9.4",
+  "system-configuration-sys",
+ ]
+ 
+@@ -7569,6 +7753,7 @@ dependencies = [
+  "rustls 0.23.21",
+  "rustls-pemfile 2.2.0",
+  "rustls-pki-types",
++ "rustls-platform-verifier",
+  "serde",
+  "serde_json",
+  "smtp-proto",
+@@ -7764,6 +7949,15 @@ dependencies = [
+  "untrusted 0.9.0",
+ ]
+ 
++[[package]]
++name = "webpki-root-certs"
++version = "0.26.8"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "09aed61f5e8d2c18344b3faa33a4c837855fe56642757754775548fee21386c4"
++dependencies = [
++ "rustls-pki-types",
++]
++
+ [[package]]
+ name = "webpki-roots"
+ version = "0.25.4"
+@@ -7789,6 +7983,18 @@ dependencies = [
+  "once_cell",
+ ]
+ 
++[[package]]
++name = "which"
++version = "4.4.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
++dependencies = [
++ "either",
++ "home",
++ "once_cell",
++ "rustix",
++]
++
+ [[package]]
+ name = "whoami"
+ version = "1.5.2"
+@@ -7886,6 +8092,15 @@ dependencies = [
+  "windows-targets 0.52.6",
+ ]
+ 
++[[package]]
++name = "windows-sys"
++version = "0.45.0"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
++dependencies = [
++ "windows-targets 0.42.2",
++]
++
+ [[package]]
+ name = "windows-sys"
+ version = "0.48.0"
+@@ -7913,6 +8128,21 @@ dependencies = [
+  "windows-targets 0.52.6",
+ ]
+ 
++[[package]]
++name = "windows-targets"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
++dependencies = [
++ "windows_aarch64_gnullvm 0.42.2",
++ "windows_aarch64_msvc 0.42.2",
++ "windows_i686_gnu 0.42.2",
++ "windows_i686_msvc 0.42.2",
++ "windows_x86_64_gnu 0.42.2",
++ "windows_x86_64_gnullvm 0.42.2",
++ "windows_x86_64_msvc 0.42.2",
++]
++
+ [[package]]
+ name = "windows-targets"
+ version = "0.48.5"
+@@ -7944,6 +8174,12 @@ dependencies = [
+  "windows_x86_64_msvc 0.52.6",
+ ]
+ 
++[[package]]
++name = "windows_aarch64_gnullvm"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
++
+ [[package]]
+ name = "windows_aarch64_gnullvm"
+ version = "0.48.5"
+@@ -7956,6 +8192,12 @@ version = "0.52.6"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+ 
++[[package]]
++name = "windows_aarch64_msvc"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
++
+ [[package]]
+ name = "windows_aarch64_msvc"
+ version = "0.48.5"
+@@ -7968,6 +8210,12 @@ version = "0.52.6"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+ 
++[[package]]
++name = "windows_i686_gnu"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
++
+ [[package]]
+ name = "windows_i686_gnu"
+ version = "0.48.5"
+@@ -7986,6 +8234,12 @@ version = "0.52.6"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+ 
++[[package]]
++name = "windows_i686_msvc"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
++
+ [[package]]
+ name = "windows_i686_msvc"
+ version = "0.48.5"
+@@ -7998,6 +8252,12 @@ version = "0.52.6"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+ 
++[[package]]
++name = "windows_x86_64_gnu"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
++
+ [[package]]
+ name = "windows_x86_64_gnu"
+ version = "0.48.5"
+@@ -8010,6 +8270,12 @@ version = "0.52.6"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+ 
++[[package]]
++name = "windows_x86_64_gnullvm"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
++
+ [[package]]
+ name = "windows_x86_64_gnullvm"
+ version = "0.48.5"
+@@ -8022,6 +8288,12 @@ version = "0.52.6"
+ source = "registry+https://github.com/rust-lang/crates.io-index"
+ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+ 
++[[package]]
++name = "windows_x86_64_msvc"
++version = "0.42.2"
++source = "registry+https://github.com/rust-lang/crates.io-index"
++checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
++
+ [[package]]
+ name = "windows_x86_64_msvc"
+ version = "0.48.5"
+diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
+index a2d19a06..76866b80 100644
+--- a/crates/cli/Cargo.toml
++++ b/crates/cli/Cargo.toml
+@@ -13,7 +13,7 @@ resolver = "2"
+ [dependencies]
+ jmap-client = { version = "0.3", features = ["async"] } 
+ mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } 
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2"]}
+ tokio = { version = "1.23", features = ["full"] }
+ num_cpus = "1.13.1"
+ clap = { version = "4.1.6", features = ["derive"] }
+diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml
+index 3da0183f..93c49bb5 100644
+--- a/crates/common/Cargo.toml
++++ b/crates/common/Cargo.toml
+@@ -16,7 +16,7 @@ sieve-rs = { version = "0.6" }
+ mail-parser = { version = "0.10", features = ["full_encoding"] } 
+ mail-builder = { version = "0.4" }
+ mail-auth = { version = "0.6" }
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ smtp-proto = { version = "0.1", features = ["serde_support"] }
+ dns-update = { version = "0.1" }
+ ahash = { version = "0.8.2", features = ["serde"] }
+@@ -32,7 +32,7 @@ tokio = { version = "1.23", features = ["net", "macros"] }
+ tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
+ futures = "0.3"
+ rcgen = "0.12"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "stream"]}
+ serde = { version = "1.0", features = ["derive"]}
+ serde_json = "1.0"
+ base64 = "0.22"
+diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml
+index dc022e7a..10e0c00a 100644
+--- a/crates/directory/Cargo.toml
++++ b/crates/directory/Cargo.toml
+@@ -12,7 +12,7 @@ trc = { path = "../trc" }
+ jmap_proto = { path =  "../jmap-proto" }
+ smtp-proto = { version = "0.1" }
+ mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } 
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ mail-builder = { version = "0.4" }
+ tokio = { version = "1.23", features = ["net"] }
+ tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
+@@ -34,7 +34,7 @@ futures = "0.3"
+ regex = "1.7.0"
+ serde = { version = "1.0", features = ["derive"]}
+ totp-rs = { version = "5.5.1", features = ["otpauth"] }
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] }
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2"] }
+ serde_json = "1.0"
+ base64 = "0.22"
+ 
+diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml
+index 640ca4fd..d91931c1 100644
+--- a/crates/imap/Cargo.toml
++++ b/crates/imap/Cargo.toml
+@@ -16,7 +16,7 @@ email = { path = "../email" }
+ nlp = { path = "../nlp" }
+ utils = { path = "../utils" }
+ mail-parser = { version = "0.10", features = ["full_encoding"] } 
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
+ rustls-pemfile = "2.0"
+ tokio = { version = "1.23", features = ["full"] }
+diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml
+index 7be56e44..ad5ed795 100644
+--- a/crates/jmap/Cargo.toml
++++ b/crates/jmap/Cargo.toml
+@@ -18,7 +18,7 @@ email = { path = "../email" }
+ smtp-proto = { version = "0.1" }
+ mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } 
+ mail-builder = { version = "0.4" }
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ mail-auth = { version = "0.6", features = ["generate"] }
+ sieve-rs = { version = "0.6" } 
+ serde = { version = "1.0", features = ["derive"]}
+@@ -38,7 +38,7 @@ p256 = { version = "0.13", features = ["ecdh"] }
+ hkdf = "0.12.3"
+ sha1 = "0.10"
+ sha2 = "0.10"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2"]}
+ tokio-tungstenite = "0.26"
+ tungstenite = "0.26"
+ chrono = "0.4"
+diff --git a/crates/mail-send/Cargo.toml b/crates/mail-send/Cargo.toml
+index fb5f402d..6760afab 100644
+--- a/crates/mail-send/Cargo.toml
++++ b/crates/mail-send/Cargo.toml
+@@ -27,6 +27,7 @@ rustls = { version = "0.23", default-features = false, features = ["std"]}
+ tokio-rustls = { version = "0.26", default-features = false }
+ webpki-roots = { version = "0.26"}
+ rustls-pki-types = { version = "1" }
++rustls-platform-verifier = "0.5"
+ gethostname = { version = "0.5"}
+ 
+ [dev-dependencies]
+diff --git a/crates/mail-send/src/smtp/tls.rs b/crates/mail-send/src/smtp/tls.rs
+index b15a6db8..7ddd0798 100644
+--- a/crates/mail-send/src/smtp/tls.rs
++++ b/crates/mail-send/src/smtp/tls.rs
+@@ -12,9 +12,9 @@ use std::{convert::TryFrom, io, sync::Arc};
+ 
+ use rustls::{
+     client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
+-    ClientConfig, ClientConnection, RootCertStore, SignatureScheme,
++    ClientConfig, ClientConnection, SignatureScheme,
+ };
+-use rustls_pki_types::{ServerName, TrustAnchor};
++use rustls_pki_types::ServerName;
+ use tokio::net::TcpStream;
+ use tokio_rustls::{client::TlsStream, TlsConnector};
+ 
+@@ -78,20 +78,14 @@ impl SmtpClient<TlsStream<TcpStream>> {
+ }
+ 
+ pub fn build_tls_connector(allow_invalid_certs: bool) -> TlsConnector {
++    use rustls_platform_verifier::BuilderVerifierExt;
++
++    let config = ClientConfig::builder();
++
+     let config = if !allow_invalid_certs {
+-        let mut root_cert_store = RootCertStore::empty();
+-
+-        root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| TrustAnchor {
+-            subject: ta.subject.clone(),
+-            subject_public_key_info: ta.subject_public_key_info.clone(),
+-            name_constraints: ta.name_constraints.clone(),
+-        }));
+-
+-        ClientConfig::builder()
+-            .with_root_certificates(root_cert_store)
+-            .with_no_client_auth()
++        config.with_platform_verifier().with_no_client_auth()
+     } else {
+-        ClientConfig::builder()
++        config
+             .dangerous()
+             .with_custom_certificate_verifier(Arc::new(DummyVerifier {}))
+             .with_no_client_auth()
+diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml
+index 650ab23b..42738e68 100644
+--- a/crates/managesieve/Cargo.toml
++++ b/crates/managesieve/Cargo.toml
+@@ -15,7 +15,7 @@ store = { path = "../store" }
+ utils = { path = "../utils" }
+ trc = { path = "../trc" }
+ mail-parser = { version = "0.10", features = ["full_encoding"] } 
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ sieve-rs = { version = "0.6" } 
+ rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
+ rustls-pemfile = "2.0"
+diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml
+index 5f86ed00..89e7b732 100644
+--- a/crates/pop3/Cargo.toml
++++ b/crates/pop3/Cargo.toml
+@@ -15,7 +15,7 @@ trc = { path = "../trc" }
+ jmap_proto = { path = "../jmap-proto" }
+ email = { path = "../email" }
+ mail-parser = { version = "0.10", features = ["full_encoding"] } 
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
+ tokio = { version = "1.23", features = ["full"] }
+ tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
+diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml
+index 5997c1c3..5f5badc2 100644
+--- a/crates/smtp/Cargo.toml
++++ b/crates/smtp/Cargo.toml
+@@ -21,7 +21,7 @@ email = { path =  "../email" }
+ spam-filter = { path =  "../spam-filter" }
+ trc = { path = "../trc" }
+ mail-auth = { version = "0.6" }
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ mail-parser = { version = "0.10", features = ["full_encoding"] } 
+ mail-builder = { version = "0.4" } 
+ smtp-proto = { version = "0.1", features = ["serde_support"] }
+@@ -47,7 +47,7 @@ blake3 = "1.3"
+ lru-cache = "0.1.2"
+ rand = "0.8.5"
+ x509-parser = "0.16.0"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] }
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2"] }
+ serde = { version = "1.0", features = ["derive", "rc"] }
+ serde_json = "1.0"
+ num_cpus = "1.15.0"
+diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml
+index f5b63353..c9176cf6 100644
+--- a/crates/spam-filter/Cargo.toml
++++ b/crates/spam-filter/Cargo.toml
+@@ -14,12 +14,12 @@ smtp-proto = { version = "0.1", features = ["serde_support"] }
+ mail-parser = { version = "0.10", features = ["full_encoding"] } 
+ mail-builder = { version = "0.4" }
+ mail-auth = { version = "0.6" }
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ tokio = { version = "1.23", features = ["net", "macros"] }
+ psl = "2"
+ hyper = { version = "1.0.1", features = ["server", "http1", "http2"] }
+ idna = "1.0"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "stream"]}
+ decancer = "3.0.1"
+ unicode-security = "0.1.0"
+ infer = "0.16"
+diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml
+index b0cf7d77..67c2d742 100644
+--- a/crates/store/Cargo.toml
++++ b/crates/store/Cargo.toml
+@@ -15,7 +15,7 @@ rust-s3 = { version = "=0.35.0-alpha.2", default-features = false, features = ["
+ azure_core = { version = "0.21.0", optional = true }
+ azure_storage = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true }
+ azure_storage_blobs = { version = "0.21.0", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"], optional = true }
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "stream"]}
+ tokio = { version = "1.23", features = ["sync", "fs", "io-util"] }
+ r2d2 = { version = "0.8.10", optional = true }
+ futures = { version = "0.3", optional = true }
+diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml
+index e4f2ca7c..f294e469 100644
+--- a/crates/trc/Cargo.toml
++++ b/crates/trc/Cargo.toml
+@@ -11,7 +11,7 @@ mail-parser = { version = "0.10", features = ["full_encoding"] }
+ base64 = "0.22.1"
+ serde = "1.0"
+ serde_json = "1.0.120"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2"]}
+ bincode = "1.3.3"
+ rtrb = "0.3.1"
+ parking_lot = "0.12.3"
+diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
+index e0a7ef9d..14b1d675 100644
+--- a/crates/utils/Cargo.toml
++++ b/crates/utils/Cargo.toml
+@@ -9,12 +9,13 @@ trc = { path = "../trc" }
+ rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
+ rustls-pemfile = "2.0"
+ rustls-pki-types = { version = "1" }
++rustls-platform-verifier = "0.5"
+ tokio = { version = "1.23", features = ["net", "macros"] }
+ tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
+ serde = { version = "1.0", features = ["derive"]}
+ mail-auth = { version = "0.6" }
+ smtp-proto = { version = "0.1" }
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ ahash = { version = "0.8" }
+ chrono = "0.4"
+ rand = "0.8.5"
+@@ -23,7 +24,7 @@ ring = { version = "0.17" }
+ base64 = "0.22"
+ serde_json = "1.0"
+ rcgen = "0.13"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2", "stream"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "http2", "stream"]}
+ x509-parser = "0.16.0"
+ pem = "3.0"
+ parking_lot = "0.12"
+diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs
+index acec2f04..b2cdaf65 100644
+--- a/crates/utils/src/lib.rs
++++ b/crates/utils/src/lib.rs
+@@ -18,9 +18,9 @@ use futures::StreamExt;
+ use reqwest::Response;
+ use rustls::{
+     client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
+-    ClientConfig, RootCertStore, SignatureScheme,
++    ClientConfig, SignatureScheme,
+ };
+-use rustls_pki_types::TrustAnchor;
++use rustls_platform_verifier::BuilderVerifierExt;
+ 
+ pub const BLOB_HASH_LEN: usize = 32;
+ 
+@@ -280,17 +280,7 @@ pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig {
+     let config = ClientConfig::builder();
+ 
+     if !allow_invalid_certs {
+-        let mut root_cert_store = RootCertStore::empty();
+-
+-        root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().map(|ta| TrustAnchor {
+-            subject: ta.subject.clone(),
+-            subject_public_key_info: ta.subject_public_key_info.clone(),
+-            name_constraints: ta.name_constraints.clone(),
+-        }));
+-
+-        config
+-            .with_root_certificates(root_cert_store)
+-            .with_no_client_auth()
++        config.with_platform_verifier().with_no_client_auth()
+     } else {
+         config
+             .dangerous()
+diff --git a/tests/Cargo.toml b/tests/Cargo.toml
+index 6aa6d35b..256a574b 100644
+--- a/tests/Cargo.toml
++++ b/tests/Cargo.toml
+@@ -34,12 +34,12 @@ spam-filter = { path = "../crates/spam-filter", features = ["test_mode", "enterp
+ trc = { path = "../crates/trc" }
+ managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] }
+ smtp-proto = { version = "0.1" }
+-mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
++mail-send = { path = "../crates/mail-send", default-features = false, features = ["cram-md5", "ring", "tls12"] }
+ mail-auth = { version = "0.6", features = ["test"] }
+-sieve-rs = { version = "0.6" } 
++sieve-rs = { version = "0.6" }
+ utils = { path = "../crates/utils", features = ["test_mode"] }
+-jmap-client = { version = "0.3", features = ["websockets", "debug", "async"] } 
+-mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] } 
++jmap-client = { version = "0.3", features = ["websockets", "debug", "async"] }
++mail-parser = { version = "0.10", features = ["full_encoding", "serde_support"] }
+ tokio = { version = "1.23", features = ["full"] }
+ tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
+ rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
+@@ -50,7 +50,7 @@ rayon = { version = "1.5.1" }
+ flate2 = { version = "1.0.17", features = ["zlib"], default-features = false }
+ serde = { version = "1.0", features = ["derive"]}
+ serde_json = "1.0"
+-reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "multipart", "http2"]}
++reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots", "multipart", "http2"]}
+ bytes = "1.4.0"
+ futures = "0.3"
+ ece = "2.2"
+-- 
+2.47.2
+
diff --git a/pkgs/by-name/st/stalwart-mail-free/patches/crates-main-Cargo.toml-Use-libre-features.patch b/pkgs/by-name/st/stalwart-mail-free/patches/crates-main-Cargo.toml-Use-libre-features.patch
new file mode 100644
index 0000000..5844793
--- /dev/null
+++ b/pkgs/by-name/st/stalwart-mail-free/patches/crates-main-Cargo.toml-Use-libre-features.patch
@@ -0,0 +1,25 @@
+From 763d22e53a9c99addbd7f9ca84f0083733f3d10d Mon Sep 17 00:00:00 2001
+From: Benedikt Peetz <benedikt.peetz@b-peetz.de>
+Date: Tue, 4 Mar 2025 19:00:15 +0100
+Subject: [PATCH] crates/main/Cargo.toml: Use libre features
+
+---
+ crates/main/Cargo.toml | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml
+index 036d2dcf..709ae5da 100644
+--- a/crates/main/Cargo.toml
++++ b/crates/main/Cargo.toml
+@@ -35,7 +35,7 @@ jemallocator = "0.5.0"
+ 
+ [features]
+ #default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis", "azure", "enterprise"]
+-default = ["rocks", "enterprise"]
++default = ["rocks", "redis"]
+ sqlite = ["store/sqlite"]
+ foundationdb = ["store/foundation", "common/foundation"]
+ postgres = ["store/postgres"]
+-- 
+2.47.2
+
diff --git a/pkgs/by-name/st/stalwart-mail-free/spam-filter.nix b/pkgs/by-name/st/stalwart-mail-free/spam-filter.nix
new file mode 100644
index 0000000..ce3466d
--- /dev/null
+++ b/pkgs/by-name/st/stalwart-mail-free/spam-filter.nix
@@ -0,0 +1,24 @@
+{
+  stdenv,
+  fetchFromGitHub,
+}:
+stdenv.mkDerivation (finalAttrs: {
+  pname = "spam-filter";
+  version = "2.0.2";
+
+  src = fetchFromGitHub {
+    owner = "stalwartlabs";
+    repo = "spam-filter";
+    tag = "v${finalAttrs.version}";
+    hash = "sha256-p2F0bwYBNWeoHjp9csWWaqeOISk9dNQT28OqkhUr7ew=";
+  };
+
+  buildPhase = ''
+    bash ./build.sh
+  '';
+
+  installPhase = ''
+    mkdir --parents "$out"
+    cp ./spam-filter.toml "$out/"
+  '';
+})
diff --git a/pkgs/default.nix b/pkgs/default.nix
index e665eb2..5456ad4 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -1,6 +1,7 @@
 {
   pkgs,
   nixLib,
+  pkgsUnstable,
 }: let
   inherit (pkgs) lib;
 
@@ -10,7 +11,7 @@
     lib.callPackageWith
     (nixLib.warnMerge
       pkgs
-      vhackPkgs
+      (nixLib.warnMerge vhackPkgs {inherit nixLib pkgsUnstable;} wMM)
       wMM);
 
   vhackPkgs = nixLib.mkByName {
diff --git a/scripts/get_dns.sh b/scripts/get_dns.sh
new file mode 100755
index 0000000..2d82925
--- /dev/null
+++ b/scripts/get_dns.sh
@@ -0,0 +1,55 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p dig -p dash -i dash --impure
+# shellcheck shell=dash
+
+get_dns_types() {
+    cat <<EOF
+    A
+    AAAA
+    CAA
+    CNAME
+    DNAME
+    MX
+    NS
+    SOA
+    SRV
+    TXT
+    PTR
+    DNSKEY
+    DS
+    SSHFP
+    TLSA
+    OPENPGPKEY
+    SVCB
+    HTTPS
+EOF
+}
+
+check_type() {
+    domain="$1"
+    type="$2"
+
+    if [ "$(dig +short -t "$type" "$domain" | wc -c)" -ne 0 ]; then
+        dig +short -t "$type" "$domain" | while IFS="$(printf "\n")" read -r output; do
+            printf "(%s) %s [%s]\n" "$type" "$output" "$domain"
+        done
+    else
+        printf "(%s) <Not set> [%s]\n" "$type" "$domain"
+    fi
+}
+
+get_dns() {
+    original_domain="$1"
+
+    get_dns_types | while read -r type; do
+        check_type "$original_domain" "$type"
+    done
+
+    # DKIM
+    check_type "mail._domainkey.$original_domain" "TXT"
+
+    # DMARC
+    check_type "_dmarc.$original_domain" "TXT"
+}
+
+get_dns "$1"
diff --git a/scripts/system_info.sh b/scripts/system_info.sh
new file mode 100755
index 0000000..940406a
--- /dev/null
+++ b/scripts/system_info.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env sh
+
+# Take a host name and return the nix store path to the host's system info.
+# Type
+# _system_info :: String -> Path
+_system_info() {
+    nix --option warn-dirty false build .#nixosConfigurations."$1".config.vhack.system-info.markdown --print-out-paths --no-link
+}
+
+_glow() {
+    if command -v glow >/dev/null; then
+        glow --width 0
+    else
+        cat
+    fi
+}
+
+# The expression is not meant to be expanded by the shell
+# shellcheck disable=SC2016
+nix eval --expr '"${builtins.concatStringsSep "\n" (builtins.attrNames (builtins.fromTOML (builtins.readFile ./hosts/host-names.toml)))}\n"' --impure --raw | while read -r host; do
+    echo "# $host" | _glow
+    _glow <"$(_system_info "$host")"
+done
+
+# vim: ft=sh
diff --git a/scripts/test_build.sh b/scripts/test_build.sh
new file mode 100755
index 0000000..eeb8572
--- /dev/null
+++ b/scripts/test_build.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env sh
+
+[ "$#" -ne 2 ] && {
+    echo "Usage: test_build <TEST_TARGET> <BUILD_NODE>" 1>&2
+    exit 2
+}
+
+test_target="$1"
+build_node="$2"
+
+nix build .#checks.x86_64-linux."$test_target".nodes."$build_node".system.build.toplevel
+
+# vim: ft=sh
diff --git a/scripts/test_interactive.sh b/scripts/test_interactive.sh
index 3b3fe0d..230f5a0 100755
--- a/scripts/test_interactive.sh
+++ b/scripts/test_interactive.sh
@@ -8,7 +8,9 @@ test_target="$1"
     exit 1
 }
 
-nix build .#checks.x86_64-linux."$test_target".driver
+nix build .#checks.x86_64-linux."$test_target".driverInteractive || {
+    exit 1
+}
 
 ./result/bin/nixos-test-driver --interactive
 
diff --git a/secrets.nix b/secrets.nix
index d90b504..8d3ae92 100644
--- a/secrets.nix
+++ b/secrets.nix
@@ -5,6 +5,9 @@ let
   server2HostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIL1TUFoCTplkqTVbXQ6qDCyeo2h8+C0vjrIlKu6vmq5f";
   server3HostKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIP3s4FjGx7LEVf/GE3WeCl8TmCtPt8gW1J0mp0fUJBNm";
 
+  # WARNING(@bpeetz): ONLY use this key on age files that are meant to be public! <2025-02-23>
+  testingKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILxdvBk/PC9fC7B5vqe9TvygZKY6LgDQ2mXRdVrthBM/";
+
   publicKeys = {
     "server2" = [
       soispha
@@ -62,3 +65,9 @@ let
     );
 in
   secrets
+  // {
+    "./tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age".publicKeys = [soispha sils testingKey];
+    "./tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age".publicKeys = [soispha sils testingKey];
+    "./tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age".publicKeys = [soispha sils testingKey];
+    "./tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age".publicKeys = [soispha sils testingKey];
+  }
diff --git a/tests/by-name/ba/back/test.nix b/tests/by-name/ba/back/test.nix
index 63f2837..85cb611 100644
--- a/tests/by-name/ba/back/test.nix
+++ b/tests/by-name/ba/back/test.nix
@@ -8,9 +8,19 @@
   nixLib,
   ...
 }: let
-  gitRepoPath = "/srv/test/repo";
-
   domain = "server";
+
+  sshKeys =
+    import ../../gi/git-server/ssh_keys.nix {inherit pkgs;};
+
+  gitoliteAdminConfSnippet = pkgs.writeText "gitolite-admin-conf-snippet" ''
+    repo CREATOR/[a-zA-Z0-9].*
+      C     = @all
+      RW+   = CREATOR
+      RW    = WRITERS
+      R     = READERS
+      option user-configs = cgit\.owner cgit\.desc cgit\.section cgit\.homepage
+  '';
 in
   nixos-lib.runTest {
     hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
@@ -26,7 +36,7 @@ in
 
     nodes = {
       server = {config, ...}: {
-        environment.systemPackages = [pkgs.git pkgs.git-bug pkgs.gawk];
+        environment.systemPackages = [pkgs.git];
 
         imports =
           extraModules
@@ -35,28 +45,46 @@ in
           ];
 
         vhack = {
+          persist.enable = true;
+          openssh.enable = true;
           nginx = {
             enable = true;
             selfsign = true;
           };
           git-server = {
             enable = true;
+            domain = "git.${domain}";
+            gitolite.adminPubkey = sshKeys.admin.pub;
           };
           back = {
             enable = true;
-            repositories = {
-              "${gitRepoPath}" = {
-                enable = true;
-                domain = "${domain}";
-                port = 9220;
-              };
+            domain = "issues.${domain}";
+
+            settings = {
+              scan_path = "${config.services.gitolite.dataDir}/repositories";
+              project_list = "${config.services.gitolite.dataDir}/projects.list";
             };
           };
         };
       };
 
-      client = {...}: {
-        environment.systemPackages = [pkgs.curl];
+      client = {nodes, ...}: {
+        environment.systemPackages = [pkgs.git pkgs.curl pkgs.git-bug pkgs.gawk];
+        programs.ssh.extraConfig = ''
+          Host *
+            UserKnownHostsFile /dev/null
+            StrictHostKeyChecking no
+            # there's nobody around that can input password
+            PreferredAuthentications publickey
+        '';
+        users.users.alice = {isNormalUser = true;};
+        networking.hosts = {
+          "${nodes.server.networking.primaryIPAddress}" = [
+            "git.${domain}"
+            "issues.${domain}"
+            "${domain}"
+          ];
+        };
       };
     };
 
@@ -67,16 +95,64 @@ in
     ''
       start_all()
 
-      with subtest("can setup git-bug issues on server"):
-        server.succeed("${pkgs.writeShellScript "setup-git-repo" ''
-        set -ex
+      with subtest("can setup ssh keys on client"):
+        client.succeed(
+            "mkdir -p ~root/.ssh",
+            "cp ${sshKeys.admin.priv} ~root/.ssh/id_ed25519",
+            "chmod 600 ~root/.ssh/id_ed25519",
+        )
+        client.succeed(
+            "sudo -u alice mkdir -p ~alice/.ssh",
+            "sudo -u alice cp ${sshKeys.alice.priv} ~alice/.ssh/id_ed25519",
+            "sudo -u alice chmod 600 ~alice/.ssh/id_ed25519",
+        )
+
+      with subtest("gitolite server starts"):
+        server.wait_for_unit("gitolite-init.service")
+        server.wait_for_unit("sshd.service")
+        client.succeed("ssh -n git@git.${domain} info")
+
+
+      with subtest("admin can clone and configure gitolite-admin.git"):
+        client.succeed("${pkgs.writeShellScript "setup-gitolite-admin.git" ''
+        set -xe
+
+        git clone git@git.${domain}:gitolite-admin.git
+        git config --global user.name 'System Administrator'
+        git config --global user.email root\@domain.example
+
+        cp ${sshKeys.alice.pub} gitolite-admin/keydir/alice.pub
 
-        mkdir --parents "${gitRepoPath}"
-        cd "${gitRepoPath}"
+        (cd gitolite-admin && git switch -c master && git branch -D main)
 
-        git init
+        (cd gitolite-admin && git add . && git commit -m 'Add keys for alice' && git push -u origin master)
+        cat ${gitoliteAdminConfSnippet} >> gitolite-admin/conf/gitolite.conf
+        (cd gitolite-admin && git add . && git commit -m 'Add support for wild repos' && git push)
+        (cd gitolite-admin && git push -d origin main)
+      ''}")
+
+      with subtest("alice can create a repo"):
+        client.succeed("sudo -u alice ${pkgs.writeShellScript "alice-create-repo" ''
+        set -xe
 
-        git bug user create --avatar "" --email "test@email.org" --name "test user" --non-interactive
+        mkdir --parents ./alice/repo1 && cd alice/repo1;
+
+        git init --initial-branch main
+        echo "# Alice's Repo" > README.md
+        git add README.md
+        git -c user.name=Alice -c user.email=alice@domain.example commit -m 'Add readme'
+
+        git remote add origin git@git.${domain}:alice/repo1.git
+        git push --set-upstream origin main
+      ''}")
+
+      with subtest("can setup git-bug issues in alice/repo1"):
+        client.succeed("sudo -u alice ${pkgs.writeShellScript "setup-git-repo" ''
+        set -ex
+
+        cd alice/repo1
+
+        git bug user create --avatar "" --email "alice@server.org" --name "alice" --non-interactive
 
         git bug add \
         --title "Some bug title" \
@@ -98,24 +174,32 @@ in
         git bug comment add --message "Some comment message" --non-interactive
         git bug comment add --message "Second comment message" --non-interactive
 
-        # NOTE(@bpeetz): Currently, the `back` module assumes that the git user can write
-        # to the repository, as such we need to provide write access here <2024-12-24>
-        chown --recursive git:git "${gitRepoPath}"
+        # TODO: This should use `git bug push`, but their ssh implementation is just
+        # too special to work in a VM test <2025-03-08>
+        git push origin +refs/bugs/*
+        git push origin +refs/identities/*
+
+        ssh git@${domain} -- config alice/repo1 --add cgit.owner Alice
+        ssh git@${domain} -- perms alice/repo1 + READERS @all
       ''}")
 
       with subtest("back server starts"):
-        server.wait_for_unit("${builtins.replaceStrings ["/"] ["_"] "back-${domain}.service"}")
+        server.wait_for_unit("back.service")
 
       with subtest("client can access the server"):
         client.succeed("${pkgs.writeShellScript "curl-back" ''
         set -xe
 
-        curl --insecure --silent --fail --show-error "https://${domain}/issues/open" --output /root/issues.html
-
-        grep -- '- 2 comments' /root/issues.html
+        curl --insecure --fail --show-error "https://issues.${domain}/alice/repo1.git/issues/open" --output /root/issues.html
         grep -- 'Second bug title' /root/issues.html
-      ''}")
+
+        curl --insecure --fail --show-error "https://issues.${domain}/" --output /root/repos.html
+        grep -- 'repo' /root/repos.html
+        grep -- "&#60;No description&#62;" /root/repos.html
+        grep -- '<span class="user-name">Alice</span>' /root/repos.html
+      ''} >&2")
 
       client.copy_from_vm("/root/issues.html", "");
+      client.copy_from_vm("/root/repos.html", "");
     '';
   }
diff --git a/tests/by-name/dn/dns/test.nix b/tests/by-name/dn/dns/test.nix
new file mode 100644
index 0000000..07a6e8c
--- /dev/null
+++ b/tests/by-name/dn/dns/test.nix
@@ -0,0 +1,129 @@
+# Inspired by this file: /nixpkgs/nixos/tests/nsd.nix
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  common = {...}: {
+    networking.firewall.enable = false;
+    networking.dhcpcd.enable = false;
+  };
+
+  mkClient = version: {
+    lib,
+    nodes,
+    ...
+  }: {
+    environment.systemPackages = [pkgs.dig pkgs.dig.dnsutils];
+
+    imports = [common];
+    networking.nameservers = lib.mkForce [
+      (lib.head nodes.server.networking.interfaces.eth1."${version}".addresses).address
+    ];
+  };
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "dns";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      server = {
+        config,
+        lib,
+        ...
+      }: {
+        imports =
+          extraModules
+          ++ [
+            ../../../../modules
+            common
+          ];
+
+        vhack = {
+          dns = {
+            enable = true;
+            interfaces = lib.debug.traceValSeqN 2 [
+              (lib.head config.networking.interfaces.eth1.ipv4.addresses).address
+              (lib.head config.networking.interfaces.eth1.ipv6.addresses).address
+            ];
+            zones = {
+              "example.com" = {
+                SOA = {
+                  nameServer = "ns";
+                  adminEmail = "admin@example.com";
+                  serial = 2024012301;
+                };
+
+                useOrigin = false;
+                NS = [
+                  "ns.example.com."
+                ];
+
+                subdomains = {
+                  ns = {
+                    A = ["192.168.1.3"];
+                  };
+                  ipv4 = {
+                    A = ["1.2.3.4"];
+                  };
+                  ipv6 = {
+                    AAAA = ["dead:beef::1"];
+                  };
+                  openpgpkey = {
+                    TXT = ["Hi!"];
+                  };
+                };
+              };
+            };
+          };
+        };
+      };
+
+      clientV4 = mkClient "ipv4";
+      clientV6 = mkClient "ipv6";
+    };
+
+    testScript = {nodes, ...}:
+    /*
+    python
+    */
+    ''
+      start_all()
+
+      clientV4.wait_for_unit("network.target")
+      clientV6.wait_for_unit("network.target")
+      server.wait_for_unit("nsd.service")
+
+      def assert_host(ipVersion, dnsRecordType, dnsQuery, expected):
+          self = clientV4 if ipVersion == 4 else clientV6
+          out = self.succeed(f"host -{ipVersion} -t {dnsRecordType} {dnsQuery}").rstrip()
+          self.log(f"output: {out}")
+          import re
+          assert re.search(expected, out), f"DNS IPv{ipVersion} dnsQuery on {dnsQuery} gave '{out}' instead of '{expected}'"
+
+
+      for ipv in 4, 6:
+          with subtest(f"IPv{ipv}"):
+              assert_host(ipv, "a", "example.com", "has no [^ ]+ record")
+              assert_host(ipv, "aaaa", "example.com", "has no [^ ]+ record")
+
+              assert_host(ipv, "soa", "example.com", "SOA.*?admin\\.example\\.com")
+              assert_host(ipv, "a", "ipv4.example.com", "address 1.2.3.4$")
+              assert_host(ipv, "aaaa", "ipv6.example.com", "address dead:beef::1$")
+
+              assert_host(ipv, "txt", "openpgpkey.example.com", "descriptive text \"Hi!\"$")
+    '';
+  }
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate b/tests/by-name/em/email-dns/nodes/acme/certs/generate
new file mode 100755
index 0000000..0d6258e
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate
@@ -0,0 +1,66 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p gnutls -p dash -i dash --impure
+# shellcheck shell=dash
+
+# For development and testing.
+# Create a CA key and cert, and use that to generate a server key and cert.
+# Creates:
+#   ca.key.pem
+#   ca.cert.pem
+#   server.key.pem
+#   server.cert.pem
+
+export SEC_PARAM=ultra
+export EXPIRATION_DAYS=123456
+export ORGANIZATION="Vhack.eu Test Keys"
+export COUNTRY=EU
+export SAN="acme.test"
+export KEY_TYPE="ed25519"
+
+BASEDIR="$(dirname "$0")"
+GENERATION_LOCATION="$BASEDIR/output"
+cd "$BASEDIR" || {
+    echo "(BUG?) No basedir ('$BASEDIR')" 1>&2
+    exit 1
+}
+
+ca=false
+clients=false
+
+usage() {
+    echo "Usage: $0 --ca|--clients"
+    exit 2
+}
+
+if [ "$#" -eq 0 ]; then
+    usage
+fi
+
+for arg in "$@"; do
+    case "$arg" in
+    "--ca")
+        ca=true
+        ;;
+    "--clients")
+        clients=true
+        ;;
+    *)
+        usage
+        ;;
+    esac
+done
+
+[ -d "$GENERATION_LOCATION" ] || mkdir --parents "$GENERATION_LOCATION"
+cd "$GENERATION_LOCATION" || echo "(BUG?) No generation location fould!" 1>&2
+
+[ "$ca" = true ] && ../generate.ca
+
+# Creates:
+#   <client_name>.key.pem
+#   <client_name>.cert.pem
+#
+[ "$clients" = true ] && ../generate.client "acme.test"
+
+echo "(INFO) Look for the keys at: $GENERATION_LOCATION"
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca b/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca
new file mode 100755
index 0000000..92832c5
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate.ca
@@ -0,0 +1,38 @@
+#! /usr/bin/env sh
+
+# Take the correct binary to create the certificates
+CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null)
+if [ -z "$CERTTOOL" ]; then
+    echo "ERROR: No certtool found" >&2
+    exit 1
+fi
+
+# Create a CA key.
+$CERTTOOL \
+    --generate-privkey \
+    --sec-param "$SEC_PARAM" \
+    --key-type "$KEY_TYPE" \
+    --outfile ca.key.pem
+
+chmod 600 ca.key.pem
+
+# Sign a CA cert.
+cat <<EOF >ca.template
+country = $COUNTRY
+dns_name = "$SAN"
+expiration_days = $EXPIRATION_DAYS
+organization = $ORGANIZATION
+ca
+EOF
+#state = $STATE
+#locality = $LOCALITY
+
+$CERTTOOL \
+    --generate-self-signed \
+    --load-privkey ca.key.pem \
+    --template ca.template \
+    --outfile ca.cert.pem
+
+chmod 600 ca.cert.pem
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/generate.client b/tests/by-name/em/email-dns/nodes/acme/certs/generate.client
new file mode 100755
index 0000000..5930298
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/generate.client
@@ -0,0 +1,44 @@
+#! /usr/bin/env sh
+
+# Take the correct binary to create the certificates
+CERTTOOL=$(command -v gnutls-certtool 2>/dev/null || command -v certtool 2>/dev/null)
+if [ -z "$CERTTOOL" ]; then
+    echo "ERROR: No certtool found" >&2
+    exit 1
+fi
+
+NAME=client
+if [ $# -gt 0 ]; then
+    NAME="$1"
+fi
+
+# Create a client key.
+$CERTTOOL \
+    --generate-privkey \
+    --sec-param "$SEC_PARAM" \
+    --key-type "$KEY_TYPE" \
+    --outfile "$NAME".key.pem
+
+chmod 600 "$NAME".key.pem
+
+# Sign a client cert with the key.
+cat <<EOF >"$NAME".template
+dns_name = "$NAME"
+dns_name = "$SAN"
+expiration_days = $EXPIRATION_DAYS
+organization = $ORGANIZATION
+encryption_key
+signing_key
+EOF
+
+$CERTTOOL \
+    --generate-certificate \
+    --load-privkey "$NAME".key.pem \
+    --load-ca-certificate ca.cert.pem \
+    --load-ca-privkey ca.key.pem \
+    --template "$NAME".template \
+    --outfile "$NAME".cert.pem
+
+chmod 600 "$NAME".cert.pem
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem
new file mode 100644
index 0000000..687101d
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.cert.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBjTCCAT+gAwIBAgIUfiDKld3eiPKuFhsaiHpPNmbMJU8wBQYDK2VwMCoxCzAJ
+BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx
+MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMB0xGzAZBgNVBAoTElZoYWNrLmV1IFRl
+c3QgS2V5czAqMAUGAytlcAMhAHYq2cjrfrlslWxvcKjs2cD7THbpmtq+jf/dlrKW
+UEo8o4GBMH8wDAYDVR0TAQH/BAIwADAfBgNVHREEGDAWgglhY21lLnRlc3SCCWFj
+bWUudGVzdDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFN/1UyS0jnC3LoryMIL2
+/6cdsYBBMB8GA1UdIwQYMBaAFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytlcANB
+ALz3u7lBreHeVZ0YXrwK3SDwlhWIH/SeUQwbxQlarzR47qu3cwQQ93Y1xjtOdu+h
+hOM/ig3nLGVOT6qL8IsZrQk=
+-----END CERTIFICATE-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem
new file mode 100644
index 0000000..06195b8
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.key.pem
@@ -0,0 +1,25 @@
+Public Key Info:
+	Public Key Algorithm: EdDSA (Ed25519)
+	Key Security Level: High (256 bits)
+
+curve:	Ed25519
+private key:
+	9d:25:38:89:f2:37:d7:65:41:f5:24:ba:4c:19:fb:0f
+	86:c8:a3:cf:f7:08:57:69:cc:64:cf:55:2d:8e:99:3e
+	
+
+x:
+	76:2a:d9:c8:eb:7e:b9:6c:95:6c:6f:70:a8:ec:d9:c0
+	fb:4c:76:e9:9a:da:be:8d:ff:dd:96:b2:96:50:4a:3c
+	
+
+
+Public Key PIN:
+	pin-sha256:NPwZitkDv4isUmdiicSsM1t1OtYoxqhdvBUnqSc4bFQ=
+Public Key ID:
+	sha256:34fc198ad903bf88ac52676289c4ac335b753ad628c6a85dbc1527a927386c54
+	sha1:dff55324b48e70b72e8af23082f6ffa71db18041
+
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIJ0lOInyN9dlQfUkukwZ+w+GyKPP9whXacxkz1Utjpk+
+-----END PRIVATE KEY-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template
new file mode 100644
index 0000000..320a170
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/acme.test.template
@@ -0,0 +1,5 @@
+dns_name = "acme.test"
+dns_name = "acme.test"
+expiration_days = 123456
+organization = Vhack.eu Test Keys
+encryption_key
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem
new file mode 100644
index 0000000..0fa9d14
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.cert.pem
@@ -0,0 +1,10 @@
+-----BEGIN CERTIFICATE-----
+MIIBYDCCARKgAwIBAgIUdhVVcf+NgElqGuutU55FUDBtFVMwBQYDK2VwMCoxCzAJ
+BgNVBAYTAkVVMRswGQYDVQQKExJWaGFjay5ldSBUZXN0IEtleXMwIBcNMjUwMzAx
+MTEyNjU2WhgPMjM2MzAzMDYxMTI2NTZaMCoxCzAJBgNVBAYTAkVVMRswGQYDVQQK
+ExJWaGFjay5ldSBUZXN0IEtleXMwKjAFBgMrZXADIQCkO1LhHINvJjt41JD6UEc4
+ZKKUubB8lKPxSOyTkFBOgqNIMEYwDwYDVR0TAQH/BAUwAwEB/zAUBgNVHREEDTAL
+gglhY21lLnRlc3QwHQYDVR0OBBYEFLUZcL/zguHlulHg5GYyYhXmVt/6MAUGAytl
+cANBAFMFFy5tjuQtp5GVEN6qM50L4lteQuxfhlQqmOOfl06HV6153wJnrlKaTOYO
+t0dKlSqKROMYUYeU39xDp07MLAc=
+-----END CERTIFICATE-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem
new file mode 100644
index 0000000..64263bc
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.key.pem
@@ -0,0 +1,25 @@
+Public Key Info:
+	Public Key Algorithm: EdDSA (Ed25519)
+	Key Security Level: High (256 bits)
+
+curve:	Ed25519
+private key:
+	82:0d:fc:f0:d6:82:89:63:e5:bc:23:78:ba:98:38:83
+	09:2d:e0:78:4c:53:92:e3:db:5b:2f:e4:39:ce:96:3d
+	
+
+x:
+	a4:3b:52:e1:1c:83:6f:26:3b:78:d4:90:fa:50:47:38
+	64:a2:94:b9:b0:7c:94:a3:f1:48:ec:93:90:50:4e:82
+	
+
+
+Public Key PIN:
+	pin-sha256:jpzYZMOHDPCeSXxfL+YUXgSPcbO9MAs8foGMP5CJiD8=
+Public Key ID:
+	sha256:8e9cd864c3870cf09e497c5f2fe6145e048f71b3bd300b3c7e818c3f9089883f
+	sha1:b51970bff382e1e5ba51e0e466326215e656dffa
+
+-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIIIN/PDWgolj5bwjeLqYOIMJLeB4TFOS49tbL+Q5zpY9
+-----END PRIVATE KEY-----
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template
new file mode 100644
index 0000000..a2295d8
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/output/ca.template
@@ -0,0 +1,5 @@
+country = EU
+dns_name = "acme.test"
+expiration_days = 123456
+organization = Vhack.eu Test Keys
+ca
diff --git a/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix b/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix
new file mode 100644
index 0000000..aeb6dfc
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/certs/snakeoil-certs.nix
@@ -0,0 +1,13 @@
+let
+  domain = "acme.test";
+in {
+  inherit domain;
+  ca = {
+    cert = ./output/ca.cert.pem;
+    key = ./output/ca.key.pem;
+  };
+  "${domain}" = {
+    cert = ./output/. + "/${domain}.cert.pem";
+    key = ./output/. + "/${domain}.key.pem";
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/acme/client.nix b/tests/by-name/em/email-dns/nodes/acme/client.nix
new file mode 100644
index 0000000..2b870e8
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/client.nix
@@ -0,0 +1,21 @@
+{
+  nodes,
+  lib,
+  ...
+}: let
+  inherit (nodes.acme.test-support.acme) caCert;
+  inherit (nodes.acme.test-support.acme) caDomain;
+in {
+  security = {
+    acme = {
+      acceptTerms = true;
+      defaults = {
+        server = "https://${caDomain}/dir";
+      };
+    };
+
+    pki = {
+      certificateFiles = lib.mkForce [caCert];
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/acme/default.nix b/tests/by-name/em/email-dns/nodes/acme/default.nix
new file mode 100644
index 0000000..236ba6a
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/acme/default.nix
@@ -0,0 +1,114 @@
+# The certificate for the ACME service is exported as:
+#
+#   config.test-support.acme.caCert
+#
+# This value can be used inside the configuration of other test nodes to inject
+# the test certificate into security.pki.certificateFiles or into package
+# overlays.
+#
+# {
+#   acme = { nodes, lib, ... }: {
+#     imports = [ ./common/acme/server ];
+#     networking.nameservers = lib.mkForce [
+#       nodes.mydnsresolver.networking.primaryIPAddress
+#     ];
+#   };
+#
+#   dnsmyresolver = ...;
+# }
+#
+# Keep in mind, that currently only _one_ resolver is supported, if you have
+# more than one resolver in networking.nameservers only the first one will be
+# used.
+#
+# Also make sure that whenever you use a resolver from a different test node
+# that it has to be started _before_ the ACME service.
+{
+  config,
+  pkgs,
+  lib,
+  ...
+}: let
+  testCerts = import ./certs/snakeoil-certs.nix;
+  inherit (testCerts) domain;
+
+  pebbleConf.pebble = {
+    listenAddress = "0.0.0.0:443";
+    managementListenAddress = "0.0.0.0:15000";
+
+    # The cert and key are used only for the Web Front End (WFE)
+    certificate = testCerts.${domain}.cert;
+    privateKey = testCerts.${domain}.key;
+
+    httpPort = 80;
+    tlsPort = 443;
+    ocspResponderURL = "http://${domain}:4002";
+    strict = true;
+  };
+
+  pebbleConfFile = pkgs.writeText "pebble.conf" (builtins.toJSON pebbleConf);
+in {
+  options.test-support.acme = {
+    caDomain = lib.mkOption {
+      type = lib.types.str;
+      default = domain;
+      readOnly = true;
+      description = ''
+        A domain name to use with the `nodes` attribute to
+        identify the CA server in the `client` config.
+      '';
+    };
+    caCert = lib.mkOption {
+      type = lib.types.path;
+      readOnly = true;
+      default = testCerts.ca.cert;
+      description = ''
+        A certificate file to use with the `nodes` attribute to
+        inject the test CA certificate used in the ACME server into
+        {option}`security.pki.certificateFiles`.
+      '';
+    };
+  };
+
+  config = {
+    networking = {
+      # This has priority 140, because modules/testing/test-instrumentation.nix
+      # already overrides this with priority 150.
+      nameservers = lib.mkOverride 140 ["127.0.0.1"];
+      firewall.allowedTCPPorts = [
+        80
+        443
+        15000
+        4002
+      ];
+
+      extraHosts = ''
+        127.0.0.1 ${domain}
+        ${config.networking.primaryIPAddress} ${domain}
+      '';
+    };
+
+    systemd.services = {
+      pebble = {
+        enable = true;
+        description = "Pebble ACME server";
+        wantedBy = ["network.target"];
+        environment = {
+          # We're not testing lego, we're just testing our configuration.
+          # No need to sleep.
+          PEBBLE_VA_NOSLEEP = "1";
+        };
+
+        serviceConfig = {
+          RuntimeDirectory = "pebble";
+          WorkingDirectory = "/run/pebble";
+
+          # Required to bind on privileged ports.
+          AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
+
+          ExecStart = "${pkgs.pebble}/bin/pebble -config ${pebbleConfFile}";
+        };
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/mail_server.nix b/tests/by-name/em/email-dns/nodes/mail_server.nix
new file mode 100644
index 0000000..a8c528a
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/mail_server.nix
@@ -0,0 +1,56 @@
+{
+  extraModules,
+  pkgs,
+  vhackPackages,
+}: {
+  mkMailServer = serverName: principal: {
+    config,
+    lib,
+    nodes,
+    ...
+  }: {
+    imports =
+      extraModules
+      ++ [
+        ../../../../../modules
+        ./acme/client.nix
+      ];
+
+    environment.systemPackages = [
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    networking.nameservers = lib.mkForce [
+      nodes.name_server.networking.primaryIPAddress
+      nodes.name_server.networking.primaryIPv6Address
+    ];
+
+    age.identityPaths = ["${../secrets/hostKey}"];
+
+    vhack = {
+      stalwart-mail = {
+        enable = true;
+        fqdn = "${serverName}.server.com";
+        admin = "admin@${serverName}.server.com";
+        security = {
+          dkimKeys = let
+            loadKey = name: {
+              dkimPublicKey = builtins.readFile (../secrets/dkim + "/${name}/public");
+              dkimPrivateKeyPath = ../secrets/dkim + "/${name}/private.age";
+              keyAlgorithm = "ed25519-sha256";
+            };
+          in {
+            "mail1.server.com" = loadKey "mail1.server.com";
+            "mail2.server.com" = loadKey "mail2.server.com";
+            "alice.com" = loadKey "alice.com";
+            "bob.com" = loadKey "bob.com";
+          };
+          verificationMode = "strict";
+        };
+        openFirewall = true;
+        principals = [principal];
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/name_server.nix b/tests/by-name/em/email-dns/nodes/name_server.nix
new file mode 100644
index 0000000..ef657f4
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/name_server.nix
@@ -0,0 +1,320 @@
+{extraModules}: {
+  config,
+  lib,
+  nodes,
+  pkgs,
+  ...
+}: let
+  keyAlgoToKeyType = keyAlgo:
+    if keyAlgo == "ed25519-sha256"
+    then "ed25519"
+    else if keyAlgo == "rsa-sha-256" || keyAlgo == "rsa-sha-1"
+    then "rsa"
+    else builtins.throw "Impossible";
+
+  mkZone = user: nodes: lib: cfg: {
+    SOA = {
+      nameServer = "ns.server.com";
+      adminEmail = "${user}@${user}.com";
+      serial = 2024012301;
+    };
+
+    MX = [
+      {
+        preference = 10;
+        exchange = "${cfg.fqdn}.";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.1
+    # Also see the policy in the hmtl part.
+    MTA-STS = [
+      {
+        id = "20250228Z";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7208.html
+    # https://en.wikipedia.org/wiki/Sender_Policy_Framework
+    TXT = [
+      (builtins.concatStringsSep " "
+        [
+          "v=spf1" # The version.
+          "+mx" # Allow mail from this domain MX record.
+          "-all" # Reject all other emails if the previous mechanism did not match.
+        ])
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5
+    DKIM = [
+      {
+        selector = "mail";
+        k = keyAlgoToKeyType cfg.security.dkimKeys."${user}.com".keyAlgorithm;
+        p = cfg.security.dkimKeys."${user}.com".dkimPublicKey;
+        s = ["email"];
+        t = ["s"];
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3
+    DMARC = [
+      {
+        adkim = "strict";
+        aspf = "strict";
+        fo = ["0" "1" "d" "s"];
+        p = "quarantine";
+        rua = cfg.admin;
+        ruf = [cfg.admin];
+      }
+    ];
+
+    A = [
+      nodes.${user}.networking.primaryIPAddress
+    ];
+    AAAA = [
+      nodes.${user}.networking.primaryIPv6Address
+    ];
+  };
+  mkServerZone = serverName: nodes: lib: let
+    cfg = nodes."${serverName}_server".vhack.stalwart-mail;
+  in {
+    SOA = {
+      nameServer = "ns.server.com";
+      adminEmail = "admin@server.com";
+      serial = 2024012301;
+    };
+    MX = [
+      {
+        preference = 10;
+        exchange = "${serverName}.server.com.";
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-3.6.1
+    # https://www.rfc-editor.org/rfc/rfc6376.html#section-7.5
+    DKIM = [
+      {
+        selector = "mail";
+        k = keyAlgoToKeyType cfg.security.dkimKeys."${serverName}.server.com".keyAlgorithm;
+        p = cfg.security.dkimKeys."${serverName}.server.com".dkimPublicKey;
+        s = ["email"];
+        t = ["s"];
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7489.html#section-6.3
+    DMARC = [
+      {
+        adkim = "strict";
+        aspf = "strict";
+        fo = ["0" "1" "d" "s"];
+        p = "quarantine";
+        rua = cfg.admin;
+        ruf = [cfg.admin];
+      }
+    ];
+
+    # https://www.rfc-editor.org/rfc/rfc7208.html
+    # NOTE(@bpeetz): This server might not be directly sending mail, but it is still required for
+    # the SMTP EHLO check. <2025-02-25>
+    TXT = [
+      (builtins.concatStringsSep " "
+        [
+          "v=spf1" # The version.
+          "+mx" # Allow mail from this domain MX record.
+          "-all" # Reject all other emails if the previous mechanism did not match.
+        ])
+    ];
+
+    A = [
+      nodes."${serverName}_server".networking.primaryIPAddress
+    ];
+    AAAA = [
+      nodes."${serverName}_server".networking.primaryIPv6Address
+    ];
+  };
+in {
+  imports =
+    extraModules
+    ++ [
+      ../../../../../modules
+      ./acme/client.nix
+    ];
+
+  networking.nameservers = lib.mkForce [
+    nodes.name_server.networking.primaryIPAddress
+    nodes.name_server.networking.primaryIPv6Address
+  ];
+
+  services.nginx = {
+    logError = "stderr debug";
+    virtualHosts = let
+      mkStsHost = mx: {
+        forceSSL = true;
+        enableACME = true;
+        root = pkgs.runCommandLocal "mkPolicy" {} ''
+          mkdir --parents $out/.well-known/
+
+          # https://www.rfc-editor.org/rfc/rfc8461.html#section-3.2
+          cat << EOF > $out/.well-known/mta-sts.txt
+          version: STSv1
+          mode: enforce
+          mx: ${mx}
+          max_age: 604800
+          EOF
+        '';
+      };
+    in {
+      "mta-sts.alice.com" = mkStsHost "mail2.server.com";
+      "mta-sts.bob.com" = mkStsHost "mail1.server.com";
+    };
+  };
+
+  vhack = {
+    nginx = {
+      enable = true;
+    };
+    dns = {
+      enable = true;
+      openFirewall = true;
+      interfaces = [
+        nodes.name_server.networking.primaryIPAddress
+        nodes.name_server.networking.primaryIPv6Address
+      ];
+
+      zones = let
+        stsZone = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+
+          useOrigin = false;
+
+          A = [
+            nodes.name_server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.name_server.networking.primaryIPv6Address
+          ];
+        };
+      in {
+        "arpa" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          PTR = [
+            {
+              name = "acme.test";
+              ip.v4 = nodes.acme.networking.primaryIPAddress;
+            }
+            {
+              name = "acme.test";
+              ip.v6 = nodes.acme.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "alice.com";
+              ip.v4 = nodes.alice.networking.primaryIPAddress;
+            }
+            {
+              name = "alice.com";
+              ip.v6 = nodes.alice.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "bob";
+              ip.v4 = nodes.bob.networking.primaryIPAddress;
+            }
+            {
+              name = "bob";
+              ip.v6 = nodes.bob.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "mail1.server.com";
+              ip.v4 = nodes.mail1_server.networking.primaryIPAddress;
+            }
+            {
+              name = "mail1.server.com";
+              ip.v6 = nodes.mail1_server.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "mail2.server.com";
+              ip.v4 = nodes.mail2_server.networking.primaryIPAddress;
+            }
+            {
+              name = "mail2.server.com";
+              ip.v6 = nodes.mail2_server.networking.primaryIPv6Address;
+            }
+
+            {
+              name = "ns.server.com";
+              ip.v4 = nodes.name_server.networking.primaryIPAddress;
+            }
+            {
+              name = "ns.server.com";
+              ip.v6 = nodes.name_server.networking.primaryIPv6Address;
+            }
+          ];
+        };
+
+        "alice.com" = mkZone "alice" nodes lib nodes.mail2_server.vhack.stalwart-mail;
+        "mta-sts.alice.com" = stsZone;
+        "bob.com" = mkZone "bob" nodes lib nodes.mail1_server.vhack.stalwart-mail;
+        "mta-sts.bob.com" = stsZone;
+        "mail1.server.com" = mkServerZone "mail1" nodes lib;
+        "mail2.server.com" = mkServerZone "mail2" nodes lib;
+        "ns.server.com" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.name_server.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.name_server.networking.primaryIPv6Address
+          ];
+        };
+        "acme.test" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+          useOrigin = false;
+
+          A = [
+            nodes.acme.networking.primaryIPAddress
+          ];
+          AAAA = [
+            nodes.acme.networking.primaryIPv6Address
+          ];
+        };
+        "server.com" = {
+          SOA = {
+            nameServer = "ns";
+            adminEmail = "admin@server.com";
+            serial = 2025012301;
+          };
+
+          useOrigin = false;
+          NS = [
+            "ns.server.com."
+          ];
+        };
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/nodes/user.nix b/tests/by-name/em/email-dns/nodes/user.nix
new file mode 100644
index 0000000..e4db347
--- /dev/null
+++ b/tests/by-name/em/email-dns/nodes/user.nix
@@ -0,0 +1,74 @@
+{
+  pkgs,
+  vhackPackages,
+}: {
+  mkUser = user: serverName: {
+    nodes,
+    lib,
+    ...
+  }: {
+    imports = [
+      ./acme/client.nix
+    ];
+
+    environment.systemPackages = [
+      vhackPackages.fetchmail-common-name
+      pkgs.msmtp
+      pkgs.procmail
+
+      pkgs.bind
+      pkgs.openssl
+    ];
+
+    networking.nameservers = lib.mkForce [
+      nodes.name_server.networking.primaryIPAddress
+      nodes.name_server.networking.primaryIPv6Address
+    ];
+
+    users.users."${user}" = {isNormalUser = true;};
+
+    systemd.tmpfiles.rules = [
+      "d /home/${user}/mail         0700 ${user} users - -"
+      "L /home/${user}/.fetchmailrc -    -       -     - /etc/homeSetup/.fetchmailrc"
+      "L /home/${user}/.procmailrc  -    -       -     - /etc/homeSetup/.procmailrc"
+      "L /home/${user}/.msmtprc     -    -       -     - /etc/homeSetup/.msmtprc"
+    ];
+
+    environment.etc = {
+      "homeSetup/.fetchmailrc" = {
+        text = ''
+          poll "${serverName}.server.com" protocol IMAP
+            username "${user}"
+            password "${user}-password"
+            ssl
+            mda procmail;
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.procmailrc" = {
+        text = ''
+          DEFAULT=$HOME/mail
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.msmtprc" = {
+        text = ''
+          account        ${user}
+          host           ${serverName}.server.com
+          domain         ${user}.com
+          port           465
+          from           ${user}@${user}.com
+          user           ${user}
+          password       ${user}-password
+          auth           on
+          tls            on
+          tls_starttls   off
+        '';
+        mode = "0600";
+        inherit user;
+      };
+    };
+  };
+}
diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age
new file mode 100644
index 0000000..97b9be7
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/alice.com/private.age
@@ -0,0 +1,11 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSAxanpG
+VWxCYjF3aWFUUEM0b2Q2bGJUNEJZMGFlVkJpTFg3eUc1UGdyblF3CnlJVXY2Ti9z
+SmltMmIzM25jajl3WE5kMkVsY05NUkpDdzlJVHdJYXByZjQKLT4gU0t5SSxMLWdy
+ZWFzZSB6Tgova0Juc0x3RlFrR1NSVzBIMllYRmZiRXlzN2hSdHZQaFVDS3FPUFNr
+NzBiM0Q5dExOREgydFpKWm1MaGQ5QkxBCgotLS0gNC81ZHQ5eFBrUGJxWXF6dWF4
+Mlg0WHBXS2RqeW1uY1hGUVJXbHpUaDhlWQpih0QTGjejnwIQ2lvDFS1wbNiiOJ+c
+awJ2tX8chzWm+wOECaIZAqwW2NwVZj5Sj+Vzv6LQ1BVaQAiEN41GRvjyP/u3X+d+
+LKI3bPa8DWxQNd7/zAhFjSB1KEIBrqGb2GtW/Yv8Mu07V8IV/MaGUwpDOXgvFQVH
+UQ1qpM0R1r190IuV2Y7M558J42crH9/5mIvMH5rW++Ru
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/alice.com/public b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public
new file mode 100644
index 0000000..0f3c3b2
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/alice.com/public
@@ -0,0 +1 @@
+cLWzd3zg51ITME1Fnu16/h07lXIUxfhdLivktUMoVQs=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age
new file mode 100644
index 0000000..6bd9e28
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/bob.com/private.age
@@ -0,0 +1,13 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBtQ1lC
+bmtXZWlqeE1SWmR2d1JTcEZMZFRKZW1jVnRGSHpUMEM1cnpvTkdFCkRVbmlNV0ZS
+SDdobTFFQUcyU3dMVXlTditvZXN0L0pPN1ZHaWtxOFZSL1EKLT4gfSM6JGpjOlot
+Z3JlYXNlICdRYiBLKlV6CmZmczdSa2U5cWl6OG5QL0VaUGsyNUlCVFJ1UjJxVnpV
+ZE1sN2lSRTgzVjI1S3pJVzdqN05WUVZmaTRYMXptb0kKczJEOG9EM2xtMFRHd3Vt
+TUpiK2RzZkRwZTZqb3lEOGpKNy80Vk9BVDlSNjhYSkROYlVGQ1ZESGhIV3ZJWVEK
+LS0tIEk0MXVEci9ITERYRzZFbTJJQWxSQzhFV3NqV2o3M0NvVlNhLzhhVkJYcTQK
+GJtIH4AxSSwZhnLn5IUhOihz9Ai2lLnf00uhvF6+i29TtyEgxgWhisBJtzShB/Aq
+Bct5em093jryJPNQBNDJpImEViP9WS/kTqQG0bnu2i/Nr5+vZyRcK8qv75guMxki
+p7sUirbzCNtA+5JGGJb30PqOAWpflBPL0fkC5L7JyAjhNRCOgIL+QQS3mosU1AYJ
+izFOdod2DA==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/bob.com/public b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public
new file mode 100644
index 0000000..ddea670
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/bob.com/public
@@ -0,0 +1 @@
+3yrKD52yd5hBA6ue5uQVl7FXGK8UOlUE9Y+yCdBRfVQ=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh b/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh
new file mode 100755
index 0000000..1e090f4
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/gen_key.sh
@@ -0,0 +1,33 @@
+#! /usr/bin/env nix-shell
+#! nix-shell -p rage -p openssl -p dash -i dash --impure
+
+cd "$(dirname "$0")" || {
+    echo "No basedir?!"
+    exit 1
+}
+
+key_name="$1"
+[ -z "$key_name" ] && {
+    echo "Usage: $0 KEY_NAME"
+    exit 2
+}
+
+[ -d "$key_name" ] || mkdir "$key_name"
+cd "$key_name" || {
+    echo "Just created."
+    exit 1
+}
+
+openssl genpkey -algorithm ed25519 -out "private"
+openssl pkey -in "private" -pubout -out "public.tmp"
+
+openssl asn1parse -in "public.tmp" -offset 12 -noout -out /dev/stdout | base64 --wrap 0 >"public"
+rm "public.tmp"
+
+rage --encrypt \
+    --armor \
+    --recipient "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILxdvBk/PC9fC7B5vqe9TvygZKY6LgDQ2mXRdVrthBM/" \
+    "private" >"private.age"
+rm "private"
+
+# vim: ft=sh
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age
new file mode 100644
index 0000000..03bb0b1
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/private.age
@@ -0,0 +1,10 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBiZmcz
+OVlacDhjS3pCWlZCYlpyVVFoQWxFN1Z0eTJIY2ovNVg2MjQ1SXpVClRZUnhkblFI
+c0VyelMxYzBsZ2NMUEVzWmtTSzJuVTdoVHFiZEc5cEd3aHMKLT4gJyctZ3JlYXNl
+IG1tIFt2YSAwCkQvY2VnMlBTSHhPbXJ2NE55ck0KLS0tIGkxWHdQb0NIVmZyaTdW
+bmorU2NLMjByakpTMlo1NUtFQ0NFd1YvOC9EaFEKtDNLHVtnsFiyhsREJOPq1xlk
+74MURNlYnlF1IMrUaA3oUQSR5M34Crg7rHtjF54OsRhm79Y1dGHWeeC3evVNVpY3
+1dn/q/12aWIzT/TgGcSi3bK5fPkv+nMs/WPKTREHJ1HcWLGDeH6e8uTV7lAwiSuP
+PjYhDbnNUCMMyaBsgbtCnMe8HuHdTwXQWuh0dApS5iL3z8qoSQ==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public
new file mode 100644
index 0000000..4941b85
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail1.server.com/public
@@ -0,0 +1 @@
+quDd9+ogqiIUWybfegosFFkG7jAsblij2VrkuUXEzzY=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age
new file mode 100644
index 0000000..6768973
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/private.age
@@ -0,0 +1,13 @@
+-----BEGIN AGE ENCRYPTED FILE-----
+YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFJzZ1dwUSBBZ3Ex
+ZWNUU01JK0ZGOEhEWUpEM3JLdXpjc3doMVA2RUI5TVVpblQxS0JJCnlHRmdVVk05
+MW56cEt4M00raE1nU3JZaTMvTXdUQXdzbTYvVElqVjdvNWcKLT4gRnJFLWdyZWFz
+ZSA5bEhvWn4Kb01McHBBekVTalcwM0tob3VUd3NuVFlvZUpnSFQxbXVBaEJNMVlQ
+K1BiSjRCL1YrZDZoSnFBNU9aQkQyNjRoSwpqcnBnd2NJQlMxaHdoa0pPWGR0SEZO
+SU5DNjFxb3JQTTZITVZNRGF1VUR4Zm9laWhYd3lHZityRTNJVVF1bXdwCnhGYzMK
+LS0tIFFsN0Q3V1pxWUduSU9xd21uVEF2R0tJcURYa1FOTS9kMDh6RGkwNS9SMUEK
+Ni+1WbmAiavBCwLg8r1nvVipXQJ2/cItN1MgWlYe0+UrgLxRU5VLhoWi9BEulGEV
+KHkNWyMCK4Tl/NJt1PAQVJ6QBVHYYxIYQWY1QkNCqXe1YdaJ5jDcWGSZdhbCrzMN
+3tx3EPhigU2DiQZB6l4OOaHLjAw2a+POVwwsCavnRp7vEhs/5O2t5Lo2vCoDGCot
+6o+Sdr86mw==
+-----END AGE ENCRYPTED FILE-----
diff --git a/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public
new file mode 100644
index 0000000..5c4406d
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/dkim/mail2.server.com/public
@@ -0,0 +1 @@
+th9exwaYvoAjxW1tAj3k/VNLl5jKzSC/dxKrxM2mTZE=
\ No newline at end of file
diff --git a/tests/by-name/em/email-dns/secrets/hostKey b/tests/by-name/em/email-dns/secrets/hostKey
new file mode 100644
index 0000000..79c9d6c
--- /dev/null
+++ b/tests/by-name/em/email-dns/secrets/hostKey
@@ -0,0 +1,7 @@
+-----BEGIN OPENSSH PRIVATE KEY-----
+b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
+QyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPwAAAJjFZPqHxWT6
+hwAAAAtzc2gtZWQyNTUxOQAAACC8XbwZPzwvXwuweb6nvU78oGSmOi4A0Npl0XVa7YQTPw
+AAAEA9D5AP+Uqhrg8rPx2DjgucjfnJknkk7lkeKHMV04ZZv7xdvBk/PC9fC7B5vqe9Tvyg
+ZKY6LgDQ2mXRdVrthBM/AAAAFSAnUHVibGljIHRlc3Rpbmcga2V5Jw==
+-----END OPENSSH PRIVATE KEY-----
diff --git a/tests/by-name/em/email-dns/test.nix b/tests/by-name/em/email-dns/test.nix
new file mode 100644
index 0000000..32447ae
--- /dev/null
+++ b/tests/by-name/em/email-dns/test.nix
@@ -0,0 +1,203 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  mail_server = import ./nodes/mail_server.nix {inherit extraModules pkgs vhackPackages;};
+  inherit (mail_server) mkMailServer;
+  user = import ./nodes/user.nix {inherit pkgs vhackPackages;};
+  inherit (user) mkUser;
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "email-dns";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      acme = {
+        nodes,
+        lib,
+        ...
+      }: {
+        imports = [./nodes/acme];
+        networking.nameservers = lib.mkForce [
+          nodes.name_server.networking.primaryIPAddress
+        ];
+      };
+
+      name_server = import ./nodes/name_server.nix {inherit extraModules;};
+
+      mail1_server =
+        mkMailServer "mail1"
+        {
+          class = "individual";
+          name = "bob";
+          secret = "bob-password";
+          email = ["bob@bob.com"];
+        };
+
+      mail2_server =
+        mkMailServer "mail2"
+        {
+          class = "individual";
+          name = "alice";
+          secret = "alice-password";
+          email = ["alice@alice.com"];
+        };
+
+      bob = mkUser "bob" "mail1";
+      alice = mkUser "alice" "mail2";
+    };
+
+    testScript = {...}: let
+      checkEmailEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+        set -xe
+
+        # fetchmail returns EXIT_CODE 1 when no new mail
+        fetchmail --verbose >&2 || [ "$?" -eq 1 ] || {
+          echo "Mail was not empty" >&2
+          exit 1
+        }
+      '';
+      checkEmailNotEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+        set -xe
+
+        # fetchmail returns EXIT_CODE 1 when no new mail
+        fetchmail --verbose >&2 || [ "$?" -ne 1 ] || {
+          echo "No new mail" >&2
+          exit 1
+        }
+      '';
+      checkSpamEmailNotEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+        set -xe
+
+        # fetchmail returns EXIT_CODE 1 when no new mail
+        fetchmail --folder JUNK --verbose >&2 || [ "$?" -ne 1 ] || {
+          echo "No new mail" >&2
+          exit 1
+        }
+      '';
+      inherit (pkgs) lib;
+    in
+      /*
+      python
+      */
+      ''
+        from time import sleep
+
+        # Start dependencies for the other services
+        acme.start()
+        acme.wait_for_unit("pebble.service")
+        name_server.start()
+        name_server.wait_for_unit("nsd.service")
+
+        # Start the actual testing machines
+        start_all()
+
+        mail1_server.wait_for_unit("stalwart-mail.service")
+        mail1_server.wait_for_open_port(993) # imap
+        mail1_server.wait_for_open_port(465) # smtp
+        mail2_server.wait_for_unit("stalwart-mail.service")
+        mail2_server.wait_for_open_port(993) # imap
+        mail2_server.wait_for_open_port(465) # smtp
+
+        alice.wait_for_unit("multi-user.target")
+        bob.wait_for_unit("multi-user.target")
+
+        name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.alice.com/cert.pem")
+        name_server.wait_until_succeeds("stat /var/lib/acme/mta-sts.bob.com/cert.pem")
+
+        with subtest("Add pebble ca key to all services"):
+          for node in [name_server, mail1_server, mail2_server, alice, bob]:
+            node.succeed("${pkgs.writeShellScript "fetch-and-set-ca" ''
+          set -xe
+
+          # Fetch the randomly generated ca certificate
+          curl https://acme.test:15000/roots/0 > /tmp/ca.crt
+          curl https://acme.test:15000/intermediates/0 >> /tmp/ca.crt
+
+          # Append it to the various system stores
+          # The file paths are from <nixpgks>/modules/security/ca.nix
+          for cert_path in "ssl/certs/ca-certificates.crt" "ssl/certs/ca-bundle.crt" "pki/tls/certs/ca-bundle.crt"; do
+            cert_path="/etc/$cert_path"
+
+            mv "$cert_path" "$cert_path.old"
+            cat "$cert_path.old" > "$cert_path"
+            cat /tmp/ca.crt >> "$cert_path"
+          done
+
+          export NIX_SSL_CERT_FILE=/tmp/ca.crt
+          export SSL_CERT_FILE=/tmp/ca.crt
+
+          # TODO
+          # # P11-Kit trust source.
+          # environment.etc."ssl/trust-source".source = "$${cacertPackage.p11kit}/etc/ssl/trust-source";
+        ''}")
+
+        with subtest("Both mailserver successfully started all services"):
+          import json
+          def all_services_running(host):
+            (status, output) = host.systemctl("list-units --state=failed --plain --no-pager --output=json")
+            host_failed = json.loads(output)
+            assert len(host_failed) == 0, f"Expected zero failing services, but found: {json.dumps(host_failed, indent=4)}"
+          all_services_running(mail1_server)
+          all_services_running(mail2_server)
+
+        with subtest("Both start without mail"):
+          alice.succeed("sudo -u alice ${checkEmailEmpty}")
+          bob.succeed("sudo -u bob ${checkEmailEmpty}")
+
+        with subtest("Alice can send an empty email to bob"):
+          alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+          set -xe
+
+          echo "" | msmtp --debug --account alice bob@bob.com >&2
+        ''}")
+
+          # Give `mail2_server` some time to send the email.
+          sleep(160)
+
+          bob.succeed("sudo -u bob ${checkSpamEmailNotEmpty}")
+
+        with subtest("Alice can send an non-empty email to bob"):
+          alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+          set -xe
+
+          cat << EOF | msmtp --debug --account alice bob@bob.com >&2
+          Subject: Hi bob, I'm Alice!
+
+          Good day, Bob!
+
+          This is an email.
+          It contains a subject and a body.
+          I also assert utf8 support by including my last name in this very message.
+
+          XOXO
+          Alice van Dåligen.
+
+          .
+          EOF
+        ''}")
+
+          # Give `mail2_server` some time to send the email.
+          sleep(120)
+
+          bob.succeed("sudo -u bob ${checkEmailNotEmpty}")
+
+        mail1_server.copy_from_vm("/var/lib/", "server1")
+        mail2_server.copy_from_vm("/var/lib/", "server2")
+        bob.copy_from_vm("/home/bob/mail", "bob")
+      '';
+  }
diff --git a/tests/by-name/em/email-ip/test.nix b/tests/by-name/em/email-ip/test.nix
new file mode 100644
index 0000000..688cd8f
--- /dev/null
+++ b/tests/by-name/em/email-ip/test.nix
@@ -0,0 +1,174 @@
+{
+  nixos-lib,
+  pkgsUnstable,
+  nixpkgs-unstable,
+  vhackPackages,
+  pkgs,
+  extraModules,
+  nixLib,
+  ...
+}: let
+  domain = "mail.server.test";
+
+  scripts = {
+    checkEmailEmpty = pkgs.writeShellScript "assert-empty-emails" ''
+      set -xe
+
+      # fetchmail returns EXIT_CODE 1 when no new mail
+      fetchmail --nosslcertck --verbose >&2 || [ "$?" -eq 1 ] || {
+        echo "Expected exit code 1." >&2
+        exit 1
+      }
+    '';
+  };
+
+  mkUser = user: {nodes, ...}: let
+    domainIp = nodes.server.networking.primaryIPAddress;
+  in {
+    environment.systemPackages = with pkgs; [
+      fetchmail
+      msmtp
+      procmail
+    ];
+
+    users.users."${user}" = {isNormalUser = true;};
+
+    systemd.tmpfiles.rules = [
+      "d /home/${user}/mail         0700 ${user} users - -"
+      "L /home/${user}/.fetchmailrc -    -       -     - /etc/homeSetup/.fetchmailrc"
+      "L /home/${user}/.procmailrc  -    -       -     - /etc/homeSetup/.procmailrc"
+      "L /home/${user}/.msmtprc     -    -       -     - /etc/homeSetup/.msmtprc"
+    ];
+
+    environment.etc = {
+      "homeSetup/.fetchmailrc" = {
+        text = ''
+          poll "${domainIp}" protocol IMAP
+            username "${user}"
+            password "${user}-password"
+            ssl
+            mda procmail;
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.procmailrc" = {
+        text = ''
+          DEFAULT=$HOME/mail
+        '';
+        mode = "0600";
+        inherit user;
+      };
+      "homeSetup/.msmtprc" = {
+        text = ''
+          account        ${user}
+          host           ${domainIp}
+          port           465
+          from           ${user}@${domain}
+          user           ${user}
+          password       ${user}-password
+          auth           on
+          tls            on
+          tls_starttls   off
+        '';
+        mode = "0600";
+        inherit user;
+      };
+    };
+  };
+in
+  nixos-lib.runTest {
+    hostPkgs = pkgs; # the Nixpkgs package set used outside the VMs
+
+    name = "email";
+
+    node = {
+      specialArgs = {inherit pkgsUnstable vhackPackages nixpkgs-unstable nixLib;};
+
+      # Use the nixpkgs as constructed by the `nixpkgs.*` options
+      pkgs = null;
+    };
+
+    nodes = {
+      server = {config, ...}: {
+        imports =
+          extraModules
+          ++ [
+            ../../../../modules
+          ];
+
+        vhack = {
+          nginx = {
+            enable = true;
+            selfsign = true;
+          };
+
+          stalwart-mail = {
+            enable = true;
+            fqdn = domain;
+            admin = "mailto:admin@${domain}";
+            security = null;
+            openFirewall = true;
+            principals = [
+              {
+                class = "individual";
+                name = "alice";
+                secret = "alice-password";
+                email = ["alice@${domain}"];
+              }
+              {
+                class = "individual";
+                name = "bob";
+                secret = "bob-password";
+                email = ["bob@${domain}"];
+              }
+            ];
+          };
+        };
+      };
+
+      alice = mkUser "alice";
+      bob = mkUser "bob";
+    };
+
+    testScript = {...}:
+    /*
+    python
+    */
+    ''
+      start_all()
+
+      server.wait_for_unit("stalwart-mail.service")
+      server.wait_for_open_port(993) # imap
+      server.wait_for_open_port(465) # smtp
+
+      with subtest("Both start without mail"):
+        alice.succeed("sudo -u alice ${scripts.checkEmailEmpty}")
+        bob.succeed("sudo -u bob ${scripts.checkEmailEmpty}")
+
+      with subtest("Alice can send an email to bob"):
+        alice.succeed("sudo -u alice ${pkgs.writeShellScript "alice-send" ''
+        set -xe
+
+        cat << EOF | msmtp --debug --account alice --tls-certcheck=off bob@${domain} >&2
+        Hi Bob!
+
+        This is an email.
+        It contains a subject and a body.
+
+        ALICE
+        EOF
+      ''}")
+        bob.succeed("sudo -u bob ${pkgs.writeShellScript "bob-receive" ''
+        set -xe
+
+        fetchmail --nosslcertck --verbose >&2 || {
+          echo New Mail did not arrive
+          exit 1
+        }
+      ''}")
+
+      server.copy_from_vm("/var/lib/", "server")
+      bob.copy_from_vm("/home/bob/mail", "bob")
+    '';
+  }