aboutsummaryrefslogtreecommitdiffstats
path: root/modules/by-name/st/stalwart-mail/settings.nix
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-30 14:05:39 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-30 14:05:39 +0200
commitc153a351659e4596acfc31f00bf594343cbfcd68 (patch)
treea9ed83de07711d6424fbea09aad28891bae5bfe4 /modules/by-name/st/stalwart-mail/settings.nix
parenthosts/server3: Setup prometheus server in agent mode (diff)
downloadnixos-server-c153a351659e4596acfc31f00bf594343cbfcd68.zip
modules: Use namespaces
That might make it easier in the future to merge different server configs together (and thusly facilitate code-reuse.).
Diffstat (limited to 'modules/by-name/st/stalwart-mail/settings.nix')
-rw-r--r--modules/by-name/st/stalwart-mail/settings.nix552
1 files changed, 0 insertions, 552 deletions
diff --git a/modules/by-name/st/stalwart-mail/settings.nix b/modules/by-name/st/stalwart-mail/settings.nix
deleted file mode 100644
index dfaf63d..0000000
--- a/modules/by-name/st/stalwart-mail/settings.nix
+++ /dev/null
@@ -1,552 +0,0 @@
-{
- 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";
-
- directory =
- if cfg.principals == null
- then "internal"
- else "in-memory";
-in {
- config.services.stalwart.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.spam-filter}/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 = "'${directory}'";
- 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 = "'${directory}'";
- 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 = {
- # HTTP (used for jmap)
- "http" = {
- bind = ["127.0.0.1:8112"];
- protocol = "http";
- # handled by ngnix
- tls.implicit = false;
- };
-
- # 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";
- };
-
- http = {
- url = "protocol + '://' + config_get('server.hostname') + ':' + local_port";
-
- # We are behind a nginx proxy, and can thus trust this header.
- use-x-forwarded = true;
- };
-
- 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-directory" = lib.mkIf (cfg.principals == null) {
- type = "rocksdb";
- path = "${cfg.dataDirectory}/storage/directory";
- compression = "lz4";
-
- # Perform “maintenance” every day at 1 am local time.
- purge.frequency = "0 1 *";
- };
- "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 = "${directory}";
-
- 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" = lib.mkIf (cfg.principals != null) {
- type = "memory";
- inherit (cfg) principals;
- };
- "internal" = lib.mkIf (cfg.principals == null) {
- type = "internal";
- store = "rocksdb-directory";
- };
- };
-
- 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;
- };
- };
- };
-}