diff options
5 files changed, 947 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; + }; + }; +} diff --git a/modules/by-name/st/stalwart-mail/patches/build-crates-main-Cargo.toml-Activate-appropriate-de.patch b/modules/by-name/st/stalwart-mail/patches/build-crates-main-Cargo.toml-Activate-appropriate-de.patch new file mode 100644 index 0000000..bfea568 --- /dev/null +++ b/modules/by-name/st/stalwart-mail/patches/build-crates-main-Cargo.toml-Activate-appropriate-de.patch @@ -0,0 +1,26 @@ +From 42cbd55c21196bdb57ac9795a938b94a781bb1e9 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Sun, 9 Feb 2025 00:17:52 +0100 +Subject: [PATCH] build(crates/main/Cargo.toml): Activate appropriate default + features + +--- + crates/main/Cargo.toml | 14 +++++++------- + 1 file changed, 7 insertions(+), 7 deletions(-) + +diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml +index 036d2dcf..baa320ef 100644 +--- a/crates/main/Cargo.toml ++++ b/crates/main/Cargo.toml +@@ -35,7 +35,7 @@ jemallocator = "0.5.0" + + [features] + #default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis", "azure", "enterprise"] +-default = ["rocks", "enterprise"] ++default = ["rocks", "redis"] + sqlite = ["store/sqlite"] + foundationdb = ["store/foundation", "common/foundation"] + postgres = ["store/postgres"] +-- +2.47.1 + diff --git a/modules/by-name/st/stalwart-mail/patches/fix-crates-directory-Guard-all-enterprise-only-featu.patch b/modules/by-name/st/stalwart-mail/patches/fix-crates-directory-Guard-all-enterprise-only-featu.patch new file mode 100644 index 0000000..80c4b60 --- /dev/null +++ b/modules/by-name/st/stalwart-mail/patches/fix-crates-directory-Guard-all-enterprise-only-featu.patch @@ -0,0 +1,86 @@ +From 3d20702a481bfa4ecc692cd07a7f1fe0a66bb5d9 Mon Sep 17 00:00:00 2001 +From: Benedikt Peetz <benedikt.peetz@b-peetz.de> +Date: Sun, 9 Feb 2025 00:38:13 +0100 +Subject: [PATCH] fix(crates/directory): Guard all enterprise only features + behind a `enterprise` cfg block + +--- + crates/directory/src/core/config.rs | 1 + + crates/directory/src/core/dispatch.rs | 7 +++++++ + 2 files changed, 8 insertions(+) + +diff --git a/crates/directory/src/core/config.rs b/crates/directory/src/core/config.rs +index dfb7bf9b..0c3ce96a 100644 +--- a/crates/directory/src/core/config.rs ++++ b/crates/directory/src/core/config.rs +@@ -84,6 +84,7 @@ impl Directories { + "memory" => MemoryDirectory::from_config(config, prefix, data_store.clone()) + .await + .map(DirectoryInner::Memory), ++ #[cfg(feature = "enterprise")] + "oidc" => OpenIdDirectory::from_config(config, prefix, data_store.clone()) + .map(DirectoryInner::OpenId), + unknown => { +diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs +index a99e54fe..062f29c9 100644 +--- a/crates/directory/src/core/dispatch.rs ++++ b/crates/directory/src/core/dispatch.rs +@@ -24,6 +24,7 @@ impl Directory { + DirectoryInner::Imap(store) => store.query(by).await, + DirectoryInner::Smtp(store) => store.query(by).await, + DirectoryInner::Memory(store) => store.query(by).await, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(store) => store.query(by, return_member_of).await, + } + .caused_by(trc::location!()) +@@ -37,6 +38,7 @@ impl Directory { + DirectoryInner::Imap(store) => store.email_to_id(address).await, + DirectoryInner::Smtp(store) => store.email_to_id(address).await, + DirectoryInner::Memory(store) => store.email_to_id(address).await, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(store) => store.email_to_id(address).await, + } + .caused_by(trc::location!()) +@@ -57,6 +59,7 @@ impl Directory { + DirectoryInner::Imap(store) => store.is_local_domain(domain).await, + DirectoryInner::Smtp(store) => store.is_local_domain(domain).await, + DirectoryInner::Memory(store) => store.is_local_domain(domain).await, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(store) => store.is_local_domain(domain).await, + } + .caused_by(trc::location!())?; +@@ -84,6 +87,7 @@ impl Directory { + DirectoryInner::Imap(store) => store.rcpt(email).await, + DirectoryInner::Smtp(store) => store.rcpt(email).await, + DirectoryInner::Memory(store) => store.rcpt(email).await, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(store) => store.rcpt(email).await, + } + .caused_by(trc::location!())?; +@@ -104,6 +108,7 @@ impl Directory { + DirectoryInner::Imap(store) => store.vrfy(address).await, + DirectoryInner::Smtp(store) => store.vrfy(address).await, + DirectoryInner::Memory(store) => store.vrfy(address).await, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(store) => store.vrfy(address).await, + } + .caused_by(trc::location!()) +@@ -117,6 +122,7 @@ impl Directory { + DirectoryInner::Imap(store) => store.expn(address).await, + DirectoryInner::Smtp(store) => store.expn(address).await, + DirectoryInner::Memory(store) => store.expn(address).await, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(store) => store.expn(address).await, + } + .caused_by(trc::location!()) +@@ -130,6 +136,7 @@ impl Directory { + | DirectoryInner::Imap(_) + | DirectoryInner::Smtp(_) + | DirectoryInner::Memory(_) => false, ++ #[cfg(feature = "enterprise")] + DirectoryInner::OpenId(_) => true, + } + } +-- +2.47.1 + 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..0f382ee --- /dev/null +++ b/modules/by-name/st/stalwart-mail/settings.nix @@ -0,0 +1,477 @@ +{ + config, + lib, + pkgs, + ... +}: let + cfg = config.vhack.stalwart-mail; +in { + config.services.stalwart-mail.settings = lib.mkIf cfg.enable { + signature."ed25519" = lib.mkIf (cfg.security != null) { + private-key = "%{file:${config.age.secrets.stalwartMailDkim.path}}%"; + domain = "${cfg.fqdn}"; + selector = "ed-default"; + 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 = "ed25519-sha256"; + canonicalization = "relaxed/relaxed"; # TODO: What does this do? <2025-02-07> + expire = "50d"; + report = true; + }; + + auth = { + iprev = { + verify = "relaxed"; + }; + spf = { + verify = { + ehlo = "relaxed"; + mail-from = "relaxed"; + }; + }; + dmarc = { + verify = "relaxed"; + }; + arc = { + seal = lib.mkIf (cfg.security != null) "ed25519"; + verify = "relaxed"; + }; + dkim = { + verify = "relaxed"; + + # Ignore insecure dkim signed messages (i.e., messages containing both + # signed and appended not-signed content.) + strict = true; + + sign = lib.mkIf (cfg.security != null) "['ed25519']"; + }; + }; + 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 = "'admin@vhack.eu'"; + send = "daily"; + max-size = 26214400; # 25 MiB + sign = lib.mkIf (cfg.security != null) "['ed25519']"; + }; + dmarc = { + aggregate = { + from-name = "'DMARC Report'"; + from-address = "'noreply-dmarc@${cfg.fqdn}'"; + org-name = "'Foss Syndicate Mail Handling'"; + contact-info = "'admin@vhack.eu'"; + send = "weekly"; + max-size = 26214400; # 25MiB + sign = lib.mkIf (cfg.security != null) "['ed25519']"; + }; + from-name = "'Report Subsystem'"; + from-address = "'noreply-dmarc@${cfg.fqdn}'"; + subject = "'DMARC Authentication Failure Report'"; + send = "1/1d"; + sign = lib.mkIf (cfg.security != null) "['ed25519']"; + }; + 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) "['ed25519']"; + }; + 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) "['ed25519']"; + }; + dsn = { + from-name = "'Mail Delivery Subsystem'"; + from-address = "'MAILER-DAEMON@${cfg.fqdn}'"; + sign = lib.mkIf (cfg.security != null) "['ed25519']"; + }; + }; + queue = { + schedule = { + retry = "[2m, 5m, 10m, 15m, 30m, 1h, 2h]"; + notify = "[2h, 7h, 1d, 3d]"; + expire = "5d"; + }; + outbound = { + tls = { + starttls = "require"; + allow-invalid-certs = false; + ip-strategy = "ipv6_then_ipv4"; + }; + }; + }; + 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 + "smtp" = { + bind = ["[::]:465"]; + protocol = "smtp"; + tls.implicit = true; + }; + + # # 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/st/stalwart-mail/spam-filter.nix b/modules/by-name/st/stalwart-mail/spam-filter.nix new file mode 100644 index 0000000..ce3466d --- /dev/null +++ b/modules/by-name/st/stalwart-mail/spam-filter.nix @@ -0,0 +1,24 @@ +{ + stdenv, + fetchFromGitHub, +}: +stdenv.mkDerivation (finalAttrs: { + pname = "spam-filter"; + version = "2.0.2"; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "spam-filter"; + tag = "v${finalAttrs.version}"; + hash = "sha256-p2F0bwYBNWeoHjp9csWWaqeOISk9dNQT28OqkhUr7ew="; + }; + + buildPhase = '' + bash ./build.sh + ''; + + installPhase = '' + mkdir --parents "$out" + cp ./spam-filter.toml "$out/" + ''; +}) |