aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/st/stalwart-mail/module.nix
diff options
context:
space:
mode:
Diffstat (limited to 'modules/by-name/st/stalwart-mail/module.nix')
-rw-r--r--modules/by-name/st/stalwart-mail/module.nix334
1 files changed, 334 insertions, 0 deletions
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..e5a681e
--- /dev/null
+++ b/modules/by-name/st/stalwart-mail/module.nix
@@ -0,0 +1,334 @@
+{
+ lib,
+ config,
+ pkgs,
+ pkgsUnstable,
+ nixLib,
+ ...
+}: let
+ cfg = config.vhack.stalwart-mail;
+ topCfg = config.services.stalwart-mail;
+
+ configFormat = pkgs.formats.toml {};
+ configFile = configFormat.generate "stalwart-mail.toml" topCfg.settings;
+
+ spamfilter = pkgs.callPackage ./spam-filter.nix {};
+
+ stalwart-mail = pkgsUnstable.stalwart-mail.overrideAttrs (final: prev: {
+ passthru = nixLib.warnMerge (prev.passthru or {}) {
+ inherit spamfilter;
+ } "stalwart-mail passthru";
+
+ checkFlags =
+ (prev.checkFlags or [])
+ ++ [
+ # This started to fail?
+ # TODO(@bpeetz): Find out why. <2025-02-08>
+ "--skip=smtp::outbound::lmtp::lmtp_delivery"
+ ];
+
+ # `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 =
+ (prev.patches or [])
+ ++ [
+ ./patches/build-crates-main-Cargo.toml-Activate-appropriate-de.patch
+ ./patches/fix-crates-directory-Guard-all-enterprise-only-featu.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
+ '';
+ });
+in {
+ imports = [
+ ./settings.nix
+ ];
+
+ options.vhack.stalwart-mail = {
+ enable = lib.mkEnableOption "starwart-mail";
+
+ # package = lib.mkPackageOption pkgsUnstable "stalwart-mail" {pkgsText = "pkgsUnstable";};
+ package = lib.mkOption {
+ description = "The stalwart-mail package to use";
+ type = lib.types.package;
+ default = stalwart-mail;
+ };
+
+ 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 {
+ 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 = {
+ dkimPrivateKeyPath = lib.mkOption {
+ type = lib.types.path;
+ description = ''
+ The path to the dkim private key agenix file.
+ '';
+ };
+ };
+ });
+ description = ''
+ Security options. This should only be set to `null` when testing.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ 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 config.
+ # However, this decision could obviously be reverse in the future. <2025-02-08>
+ enable = false;
+ inherit (cfg) package;
+ # dataDir = cfg.dataDirectory;
+ };
+
+ security.acme.certs = {
+ "${cfg.fqdn}" = {
+ domain = cfg.fqdn;
+ group = "stalwart-mail";
+ };
+ };
+
+ age.secrets = lib.mkIf (cfg.security != null) {
+ stalwartMailDkim = {
+ file = cfg.security.dkimPrivateKeyPath;
+ mode = "600";
+ owner = "stalwart-mail";
+ group = "stalwart-mail";
+ };
+ };
+
+ 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";
+ }
+ ];
+
+ services.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";
+ };
+ };
+ };
+ };
+
+ # 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 = {
+ packages = [cfg.package];
+ services.stalwart-mail = {
+ wantedBy = ["multi-user.target"];
+ after = [
+ "local-fs.target"
+ "network.target"
+ ];
+
+ 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 = [
+ ""
+ "${cfg.package}/bin/stalwart-mail --config=$CACHE_DIRECTORY/mutable_config_file.toml"
+ ];
+
+ StandardOutput = "journal";
+ StandardError = "journal";
+
+ ReadWritePaths = [
+ cfg.dataDirectory
+ ];
+ CacheDirectory = "stalwart-mail";
+ StateDirectory = "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";
+ };
+ unitConfig.ConditionPathExists = [
+ ""
+ "${configFile}"
+ ];
+ };
+ };
+
+ # 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;
+ };
+ };
+}