about summary refs log tree commit diff stats
path: root/modules/by-name
diff options
context:
space:
mode:
Diffstat (limited to 'modules/by-name')
-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/ng/nginx/module.nix2
-rw-r--r--modules/by-name/ni/nix-sync/module.nix12
-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
37 files changed, 2515 insertions, 108 deletions
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/ng/nginx/module.nix b/modules/by-name/ng/nginx/module.nix
index 27b0302..1cb4e46 100644
--- a/modules/by-name/ng/nginx/module.nix
+++ b/modules/by-name/ng/nginx/module.nix
@@ -6,7 +6,7 @@
   mkRedirect = _: value: {
     forceSSL = true;
     enableACME = true;
-    locations."/".return = "301 ${value}";
+    locations."/".return = "301 ${value}$request_uri";
   };
 
   redirects = builtins.mapAttrs mkRedirect cfg.redirects;
diff --git a/modules/by-name/ni/nix-sync/module.nix b/modules/by-name/ni/nix-sync/module.nix
index 1413920..9ddd210 100644
--- a/modules/by-name/ni/nix-sync/module.nix
+++ b/modules/by-name/ni/nix-sync/module.nix
@@ -2,6 +2,7 @@
   config,
   lib,
   modulesPath,
+  nixLib,
   ...
 }: let
   cfg = config.vhack.nix-sync;
@@ -27,12 +28,15 @@
   }: {
     name = "${domain}";
     value =
-      lib.recursiveUpdate {
+      # FIXME(@bpeetz): We cannot use something like `lib.recursiveUpdate` because the
+      # `extraSettings` are instantiated from the “real” nginx type. As such the
+      # `extaSettings` would override our values here. Therefore, the direct merge. <2025-02-07>
+      extraSettings
+      // {
         forceSSL = true;
         enableACME = true;
         root = "/etc/nginx/websites/${domain}";
-      }
-      extraSettings;
+      };
   };
   virtHosts = builtins.listToAttrs (builtins.map mkVirtHost cfg.domains);
 in {
@@ -66,7 +70,7 @@ in {
             type =
               lib.types.submodule (import (modulesPath + "/services/web-servers/nginx/vhost-options.nix") {inherit config lib;});
             example = {
-              locations."/.well-known/openpgpkey/hu/".extraConfig = "default_type application/octet-stream";
+              locations."/.well-known/openpgpkey/".extraConfig = "default_type application/octet-stream";
             };
             default = {};
             description = ''
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;
+  };
+}