From 2d3e02ad7858b8f27591425ff626ef906a0f0403 Mon Sep 17 00:00:00 2001 From: Kaare Hoff Skovgaard Date: Wed, 30 Jul 2025 00:36:51 +0200 Subject: [PATCH] Attempt at using stalwart again LDAP directory setup seems to work --- .../nixos/infrastructure/mailserver/acme.nix | 33 ++- .../mailserver/admin_password.nix | 67 ++++++ .../infrastructure/mailserver/default.nix | 87 +++++--- .../nixos/infrastructure/mailserver/dkim.nix | 140 ++++++++----- .../nixos/infrastructure/mailserver/ldap.nix | 70 +++---- .../mailserver/openid-connect.nix | 68 +++--- .../mailserver/package/nixos-module.nix | 23 +++ .../mailserver/package/package.nix | 194 ++++++++++++++++++ .../mailserver/package/spam-filter.nix | 43 ++++ .../mailserver/package/webadmin.nix | 77 +++++++ .../infrastructure/mailserver/prometheus.nix | 9 +- 11 files changed, 649 insertions(+), 162 deletions(-) create mode 100644 nix/modules/nixos/infrastructure/mailserver/admin_password.nix create mode 100644 nix/modules/nixos/infrastructure/mailserver/package/nixos-module.nix create mode 100644 nix/modules/nixos/infrastructure/mailserver/package/package.nix create mode 100644 nix/modules/nixos/infrastructure/mailserver/package/spam-filter.nix create mode 100644 nix/modules/nixos/infrastructure/mailserver/package/webadmin.nix diff --git a/nix/modules/nixos/infrastructure/mailserver/acme.nix b/nix/modules/nixos/infrastructure/mailserver/acme.nix index 1321901..c13d0ca 100644 --- a/nix/modules/nixos/infrastructure/mailserver/acme.nix +++ b/nix/modules/nixos/infrastructure/mailserver/acme.nix @@ -1,9 +1,40 @@ { lib, config, ... }: let cfg = config.khscodes.infrastructure.mailserver; + fqdn = config.khscodes.networking.fqdn; + acmeDir = config.security.acme.certs.${fqdn}.directory; + # extraDomainNames = lib.lists.filter (d: d != fqdn) (lib.lists.map (d: "mx.${d}") cfg.domains); + extraDomainNames = [ ]; + user = "stalwart-mail"; in { config = lib.mkIf cfg.enable { - khscodes.services.nginx.virtualHosts."${config.khscodes.networking.fqdn}" = { }; + khscodes.services.nginx.virtualHosts."${fqdn}" = { + locations."/" = { + proxyPass = "http://127.0.0.1:8080"; + proxyWebsockets = true; + recommendedProxySettings = true; + }; + }; + services.stalwart-mail.settings = { + certificate.default = { + cert = "%{file:${acmeDir}/fullchain.pem}%"; + private-key = "%{file:${config.security.acme.certs.${fqdn}.directory}/key.pem}%"; + }; + }; + security.acme.certs.${fqdn} = { + inherit extraDomainNames; + postRun = '' + systemctl restart stalwart-mail.service + ''; + }; + systemd.services.stalwart-mail = { + after = [ "acme-selfsigned-${fqdn}.service" ]; + wants = [ "acme-finished-${fqdn}.service" ]; + serviceConfig.ReadOnlyPaths = [ acmeDir ]; + }; + users.users.${user}.extraGroups = [ + config.security.acme.certs.${fqdn}.group + ]; }; } diff --git a/nix/modules/nixos/infrastructure/mailserver/admin_password.nix b/nix/modules/nixos/infrastructure/mailserver/admin_password.nix new file mode 100644 index 0000000..12a454d --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/admin_password.nix @@ -0,0 +1,67 @@ +{ config, lib, ... }: +let + cfg = config.khscodes.infrastructure.mailserver; +in +{ + config = lib.mkIf cfg.enable { + khscodes.infrastructure.vault-server-approle.policy = { + # TODO: Make this configurable + "mx.kaareskovgaard.net/data/logins/admin" = { + capabilities = [ "read" ]; + }; + }; + khscodes.services.vault-agent.templates = [ + { + contents = '' + {{- with secret "mx.kaareskovgaard.net/data/logins/admin" -}} + {{ .Data.data.hashed_password }} + {{- end -}} + ''; + destination = "/run/secret/stalwart/users/admin"; + owner = "stalwart-mail"; + group = "stalwart-mail"; + restartUnits = [ "stalwart-mail.service" ]; + } + ]; + systemd.services.stalwart-mail = { + unitConfig.ConditionPathExists = [ "/run/secret/stalwart/users/admin" ]; + serviceConfig.ReadOnlyPaths = [ "/run/secret/stalwart/users/admin" ]; + }; + services.stalwart-mail = { + settings = { + authentication.fallback-admin = { + user = "admin"; + secret = "%{file:/run/secret/stalwart/users/admin}%"; + }; + }; + }; + khscodes.infrastructure.provisioning.pre.modules = [ + { + terraform.required_providers.random = { + source = "hashicorp/random"; + version = "3.7.2"; + }; + provider.random = { }; + + resource.random_password.stalwart_fallback_admin_password = { + length = 48; + numeric = true; + lower = true; + upper = true; + special = false; + }; + + resource.vault_kv_secret_v2.stalwart_fallback_admin_password = { + mount = "mx.kaareskovgaard.net"; + name = "logins/admin"; + data_json = '' + { + "hashed_password": ''${ jsonencode(resource.random_password.stalwart_fallback_admin_password.bcrypt_hash) }, + "password": ''${ jsonencode(resource.random_password.stalwart_fallback_admin_password.result) } + } + ''; + }; + } + ]; + }; +} diff --git a/nix/modules/nixos/infrastructure/mailserver/default.nix b/nix/modules/nixos/infrastructure/mailserver/default.nix index f59e4e1..a4b7289 100644 --- a/nix/modules/nixos/infrastructure/mailserver/default.nix +++ b/nix/modules/nixos/infrastructure/mailserver/default.nix @@ -1,7 +1,7 @@ { config, lib, - inputs, + pkgs, ... }: let @@ -17,7 +17,8 @@ in }; }; imports = [ - inputs.simple-nixos-mailserver.nixosModules.mailserver + # inputs.simple-nixos-mailserver.nixosModules.mailserver + ./admin_password.nix ./acme.nix ./dmarc.nix ./dane.nix @@ -28,8 +29,58 @@ in ./prometheus.nix ./openid-connect.nix ./ldap.nix + ./package/nixos-module.nix ]; config = lib.mkIf cfg.enable { + services.stalwart-mail = { + enable = true; + package = pkgs.callPackage ./package/package.nix { }; + settings = { + http = { + url = "https://${fqdn}"; + use-x-forwarded = true; + }; + server = { + hostname = fqdn; + tls = { + enable = true; + certificate = "default"; + implicit = true; + ignore-client-order = true; + }; + listener = { + smtp = { + protocol = "smtp"; + bind = "[::]:25"; + }; + submissions = { + bind = "[::]:465"; + protocol = "smtp"; + tls.implicit = true; + }; + imaps = { + bind = "[::]:993"; + protocol = "imap"; + tls.implicit = true; + }; + jmap = { + bind = "[::]:8080"; + url = "https://${fqdn}"; + protocol = "jmap"; + }; + management = { + bind = "[::]:8080"; + protocol = "http"; + }; + }; + }; + lookup.default = { + hostname = fqdn; + domain = "kaareskovgaard.net"; + }; + spam-filter.resource = "${config.services.stalwart-mail.package.spam-filter}/spam-filter.toml"; + }; + }; # TODO: Include a similiar rule for openstack khscodes.infrastructure.hetzner-instance.extraFirewallRules = [ { @@ -78,38 +129,6 @@ in } ) ]; - mailserver = { - enable = true; - enableImap = false; - enableImapSsl = true; - enableSubmission = false; - enableSubmissionSsl = true; - fqdn = config.khscodes.networking.fqdn; - useUTF8FolderNames = true; - domains = cfg.domains; - certificateScheme = "acme"; - }; - services.fail2ban.jails = { - postfix = { - settings = { - enabled = true; - mode = "aggressive"; - findtime = 600; - bantime = "1d"; - maxretry = 3; - }; - }; - dovecot = { - settings = { - enabled = true; - mode = "aggressive"; - findtime = 600; - bantime = "1d"; - maxretry = 3; - }; - }; - }; - networking.firewall.allowedTCPPorts = [ 25 465 diff --git a/nix/modules/nixos/infrastructure/mailserver/dkim.nix b/nix/modules/nixos/infrastructure/mailserver/dkim.nix index 2bef981..894c6e5 100644 --- a/nix/modules/nixos/infrastructure/mailserver/dkim.nix +++ b/nix/modules/nixos/infrastructure/mailserver/dkim.nix @@ -9,28 +9,82 @@ let publicKeyEnd = ''"-----END PUBLIC KEY-----\n"''; rsaKeyPath = domain: "/run/secret/dkim/${domain}.snm_rsa.key"; ed25519KeyPath = domain: "/run/secret/dkim/${domain}.snm_ed25519.key"; - domainKeyPaths = lib.lists.flatten ( + keyFiles = lib.lists.flatten ( lib.lists.map (domain: [ (rsaKeyPath domain) (ed25519KeyPath domain) ]) cfg.domains ); - # Currently (2025-07-25) I canot get rspamd to sign with ed25519 key, - # it appears it attempts to parse it as an RSA key - # { - # path: "${ed25519KeyPath domain}"; - # selector: "snm_ed25519"; - # } - dkimSigningForDomain = domain: '' - ${domain} { - selectors [ - { - path: "${rsaKeyPath domain}"; - selector: "snm_rsa"; - } - ] + ifthen = condition: expr: { + "if" = condition; + "then" = expr; + }; + otherwise = expr: { "else" = expr; }; + authDkimForDomain = domain: [ + (ifthen "sender_domain = '${domain}'" "['${domain}_rsa', '${domain}_ed25519']") + ]; + authDkim = lib.lists.flatten (lib.lists.map authDkimForDomain cfg.domains); + signatureForDomain = domain: [ + { + name = "${domain}_rsa"; + value = { + inherit domain; + private-key = "%{file:${rsaKeyPath domain}}%"; + selector = "snm_rsa"; + 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 = "rsa-sha256"; + canonicalization = "relaxed/relaxed"; + report = true; + }; } - ''; + { + name = "${domain}_ed25519"; + value = { + inherit domain; + private-key = "%{file:${ed25519KeyPath domain}}%"; + selector = "snm_ed25519"; + 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"; + report = true; + }; + } + ]; + dkimSignatures = { + signature = lib.listToAttrs (lib.lists.flatten (lib.lists.map signatureForDomain cfg.domains)); + }; dkimPublicKey = tls_key: ''''${ replace(trimprefix(trimsuffix(${tls_key}.public_key_pem, ${publicKeyEnd}), ${publicKeyBegin}), "\n", "") }''; @@ -136,10 +190,10 @@ in ''; destination = rsaKeyPath domain; perms = "0600"; - owner = "rspamd"; - group = "rspamd"; + owner = "stalwart-mail"; + group = "stalwart-mail"; restartUnits = [ - "rspamd.service" + "stalwart-mail.service" ]; } { @@ -150,47 +204,25 @@ in ''; destination = ed25519KeyPath domain; perms = "0600"; - owner = "rspamd"; - group = "rspamd"; + owner = "stalwart-mail"; + group = "stalwart-mail"; restartUnits = [ - "rspamd.service" + "stalwart-mail.service" ]; } ]) cfg.domains ); - mailserver = { - dkimSigning = false; - }; - services.rspamd.locals."dkim_signing.conf" = lib.mkForce { - text = '' - enabled = true; - allow_username_mismatch = true; - domain { - ${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)} - } - ''; - }; - services.rspamd.locals."arc.conf" = lib.mkForce { - text = '' - enabled = true; - allow_username_mismatch = true; - domain { - ${lib.strings.concatStringsSep "\n " (lib.lists.map dkimSigningForDomain cfg.domains)} - } - ''; - }; - services.postfix.config = { - # Need to include this as I disabled the in built support for dkim signing - # without this postfix won't forward the mails to rspamd to be signed. - non_smtpd_milters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; - }; - systemd.services.rspamd = { - unitConfig = { - ConditionPathExists = domainKeyPaths; - }; - serviceConfig = { - ReadOnlyPaths = domainKeyPaths; + services.stalwart-mail.settings = { + auth.dkim = { + sign = authDkim ++ [ + (otherwise false) + ]; }; + } + // dkimSignatures; + systemd.services.stalwart-mail = { + unitConfig.ConditionPathExists = keyFiles; + serviceConfig.ReadOnlyPaths = keyFiles; }; }; } diff --git a/nix/modules/nixos/infrastructure/mailserver/ldap.nix b/nix/modules/nixos/infrastructure/mailserver/ldap.nix index 8699fbc..c009b5a 100644 --- a/nix/modules/nixos/infrastructure/mailserver/ldap.nix +++ b/nix/modules/nixos/infrastructure/mailserver/ldap.nix @@ -16,44 +16,43 @@ in }; config = lib.mkIf cfg.enable { - mailserver.ldap = { - enable = true; - uris = [ "ldaps://login.kaareskovgaard.net" ]; - searchBase = "dc=login,dc=kaareskovgaard,dc=net"; - searchScope = "sub"; - bind = { - dn = "dn=token"; - passwordFile = secretFile; + services.stalwart-mail.settings = { + storage = { + directory = "ldap"; }; - dovecot = { - # Map LDAP uid to dovecot user, and ldap userPassword to dovecot password - passAttrs = "uid=user"; - passFilter = "(&(class=account)(memberOf=mail_user)(uid=%u))"; - # This filter is used both when receiving mail (thus needing to lookup by mail address, and when authenticating, requriing the lookup by uid.) - # Note that the pass filter only allows looking up by uid, so should still only be able to authenticate using that. - userFilter = "(&(class=account)(memberOf=mail_user)(|(mail=%u)(uid=%u)))"; - userAttrs = "uid=user"; - }; - postfix = { - filter = "(&(class=account)(memberOf=mail_user)(mail=%s))"; - mailAttribute = "uid"; - uidAttribute = "uid"; + directory.ldap = { + type = "ldap"; + url = "ldaps://login.kaareskovgaard.net"; + base-dn = "dc=login,dc=kaareskovgaard,dc=net"; + bind = { + dn = "dn=token"; + secret = "%{env:STALWART_LDAP_SECRET}%"; + auth = { + method = "lookup"; + }; + }; + filter = { + name = "(&(class=account)(memberOf=mail_user)(uid=?))"; + email = "(&(class=account)(memberOf=mail_user)(mail=?))"; + }; + attributes = { + name = "name"; + class = "class"; + description = "name"; + groups = "memberOf"; + email = "mail;primary"; + email-alias = "mail;alternative"; + quota = "diskQuota"; + }; }; }; systemd.services = { - dovecot2 = { + stalwart-mail = { unitConfig = { ConditionPathExists = [ secretFile ]; }; - }; - postfix = { - unitConfig = { - ConditionPathExists = [ secretFile ]; - }; - }; - postfix-setup = { - unitConfig = { - ConditionPathExists = [ secretFile ]; + serviceConfig = { + EnvironmentFile = secretFile; }; }; }; @@ -62,15 +61,14 @@ in { contents = '' {{- with secret "${cfg.ldap.mount}/data/${cfg.ldap.path}" -}} - {{ .Data.data.apiToken }} + STALWART_LDAP_SECRET={{ .Data.data.apiToken }} {{- end -}} ''; destination = secretFile; - owner = "dovecot"; - group = "dovecot"; + owner = "stalwart-mail"; + group = "stalwart-mail"; restartUnits = [ - "dovecot2.service" - "postfix.service" + "stalwart-mail.service" ]; } ]; diff --git a/nix/modules/nixos/infrastructure/mailserver/openid-connect.nix b/nix/modules/nixos/infrastructure/mailserver/openid-connect.nix index 120886d..43c383f 100644 --- a/nix/modules/nixos/infrastructure/mailserver/openid-connect.nix +++ b/nix/modules/nixos/infrastructure/mailserver/openid-connect.nix @@ -5,41 +5,41 @@ let in { config = lib.mkIf cfg.enable { - khscodes.services.vault-agent.templates = [ - { - contents = '' - {{- with secret "kanidm/data/apps/dovecot" -}} - scope = email openid profile - username_attribute = username - debug = yes - introspection_url = https://dovecot:{{ .Data.data.basic_secret }}@login.kaareskovgaard.net/oauth2/token/introspect - introspection_mode = post - {{- end -}} - ''; - destination = oauthConfigFile; - perms = "0600"; - owner = "root"; - group = "root"; - restartUnits = [ "dovecot2.service" ]; - } - ]; - services.dovecot2.extraConfig = '' - auth_mechanisms = $auth_mechanisms oauthbearer xoauth2 + # khscodes.services.vault-agent.templates = [ + # { + # contents = '' + # {{- with secret "kanidm/data/apps/dovecot" -}} + # scope = email openid profile + # username_attribute = username + # debug = yes + # introspection_url = https://dovecot:{{ .Data.data.basic_secret }}@login.kaareskovgaard.net/oauth2/token/introspect + # introspection_mode = post + # {{- end -}} + # ''; + # destination = oauthConfigFile; + # perms = "0600"; + # owner = "root"; + # group = "root"; + # restartUnits = [ "dovecot2.service" ]; + # } + # ]; + # services.dovecot2.extraConfig = '' + # auth_mechanisms = $auth_mechanisms oauthbearer xoauth2 - passdb { - driver = oauth2 - mechanisms = xoauth2 oauthbearer - args = ${oauthConfigFile} - } - ''; - systemd.services.dovecot2 = { - serviceConfig.ReadOnlyPaths = [ - oauthConfigFile - ]; - unitConfig.ConditionPathExists = [ - oauthConfigFile - ]; - }; + # passdb { + # driver = oauth2 + # mechanisms = xoauth2 oauthbearer + # args = ${oauthConfigFile} + # } + # ''; + # systemd.services.dovecot2 = { + # serviceConfig.ReadOnlyPaths = [ + # oauthConfigFile + # ]; + # unitConfig.ConditionPathExists = [ + # oauthConfigFile + # ]; + # }; }; } diff --git a/nix/modules/nixos/infrastructure/mailserver/package/nixos-module.nix b/nix/modules/nixos/infrastructure/mailserver/package/nixos-module.nix new file mode 100644 index 0000000..5d4160c --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/package/nixos-module.nix @@ -0,0 +1,23 @@ +# This file contains patches for Nixos 25.05 to be compatible with new stalwart mail +{ + lib, + config, + pkgs, + ... +}: +let + configFormat = pkgs.formats.toml { }; + configFile = configFormat.generate "stalwart-mail.toml" config.services.stalwart-mail.settings; +in +{ + systemd.services.stalwart-mail = lib.mkIf config.services.stalwart-mail.enable { + serviceConfig = { + User = "stalwart-mail"; + Group = "stalwart-mail"; + ExecStart = lib.mkForce [ + "" + "${lib.getExe config.services.stalwart-mail.package} --config=${configFile}" + ]; + }; + }; +} diff --git a/nix/modules/nixos/infrastructure/mailserver/package/package.nix b/nix/modules/nixos/infrastructure/mailserver/package/package.nix new file mode 100644 index 0000000..b1c1256 --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/package/package.nix @@ -0,0 +1,194 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + pkg-config, + protobuf, + bzip2, + openssl, + sqlite, + foundationdb, + zstd, + stdenv, + nix-update-script, + nixosTests, + rocksdb, + callPackage, + withFoundationdb ? false, + stalwartEnterprise ? false, +}: + +rustPlatform.buildRustPackage (finalAttrs: { + pname = "stalwart-mail" + (lib.optionalString stalwartEnterprise "-enterprise"); + version = "0.13.2"; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "stalwart"; + rev = "51a0a1445d74a8cfb880e9d88f5be390fa0e9365"; + hash = "sha256-VdeHb1HVGXA5RPenhhK4r/kkQiLG8/4qhdxoJ3xIqR4="; + }; + + cargoHash = "sha256-Wu6skjs3Stux5nCX++yoQPeA33Qln67GoKcob++Ldng="; + + nativeBuildInputs = [ + pkg-config + protobuf + rustPlatform.bindgenHook + ]; + + buildInputs = [ + bzip2 + openssl + sqlite + zstd + ] + ++ lib.optionals (stdenv.hostPlatform.isLinux && withFoundationdb) [ foundationdb ]; + + # Issue: https://github.com/stalwartlabs/stalwart/issues/1104 + buildNoDefaultFeatures = true; + buildFeatures = [ + "postgres" + "rocks" + "elastic" + "redis" + ] + ++ lib.optionals withFoundationdb [ "foundationdb" ] + ++ lib.optionals stalwartEnterprise [ "enterprise" ]; + + env = { + OPENSSL_NO_VENDOR = true; + ZSTD_SYS_USE_PKG_CONFIG = true; + ROCKSDB_INCLUDE_DIR = "${rocksdb}/include"; + ROCKSDB_LIB_DIR = "${rocksdb}/lib"; + }; + + postInstall = '' + mkdir -p $out/etc/stalwart + + mkdir -p $out/lib/systemd/system + + substitute resources/systemd/stalwart-mail.service $out/lib/systemd/system/stalwart-mail.service \ + --replace "__PATH__" "$out" + ''; + + checkFlags = lib.forEach [ + # Require running mysql, postgresql daemon + "directory::imap::imap_directory" + "directory::internal::internal_directory" + "directory::ldap::ldap_directory" + "directory::sql::sql_directory" + "directory::oidc::oidc_directory" + "store::blob::blob_tests" + "store::lookup::lookup_tests" + "smtp::lookup::sql::lookup_sql" + # thread 'directory::smtp::lmtp_directory' panicked at tests/src/store/mod.rs:122:44: + # called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" } + "directory::smtp::lmtp_directory" + # thread 'imap::imap_tests' panicked at tests/src/imap/mod.rs:436:14: + # Missing store type. Try running `STORE= cargo test`: NotPresent + "imap::imap_tests" + # thread 'jmap::jmap_tests' panicked at tests/src/jmap/mod.rs:303:14: + # Missing store type. Try running `STORE= cargo test`: NotPresent + "jmap::jmap_tests" + # Failed to read system DNS config: io error: No such file or directory (os error 2) + "smtp::inbound::data::data" + # Expected "X-My-Header: true" but got Received: from foobar.net (unknown [10.0.0.123]) + "smtp::inbound::scripts::sieve_scripts" + # thread 'smtp::outbound::lmtp::lmtp_delivery' panicked at tests/src/smtp/session.rs:313:13: + # Expected " (failed to lookup" but got From: "Mail Delivery Subsystem" + "smtp::outbound::lmtp::lmtp_delivery" + # thread 'smtp::outbound::extensions::extensions' panicked at tests/src/smtp/inbound/mod.rs:45:23: + # No queue event received. + "smtp::outbound::extensions::extensions" + # panicked at tests/src/smtp/outbound/smtp.rs:173:5: + "smtp::outbound::smtp::smtp_delivery" + # panicked at tests/src/smtp/outbound/lmtp.rs + "smtp::outbound::lmtp::lmtp_delivery" + # thread 'smtp::queue::retry::queue_retry' panicked at tests/src/smtp/queue/retry.rs:119:5: + # assertion `left == right` failed + # left: [1, 2, 2] + # right: [1, 2, 3] + "smtp::queue::retry::queue_retry" + # thread 'smtp::queue::virtualq::virtual_queue' panicked at /build/source/crates/store/src/dispatch/store.rs:548:14: + # called `Result::unwrap()` on an `Err` value: Error(Event { inner: Store(MysqlError), keys: [(Reason, String("Input/output error: Input/output error: Connection refused (os error 111)")), (CausedBy, String("crates/store/src/dispatch/store.rs:301"))] }) + "smtp::queue::virtualq::virtual_queue" + # Missing store type. Try running `STORE= cargo test`: NotPresent + "store::store_tests" + # Missing store type. Try running `STORE= cargo test`: NotPresent + "cluster::cluster_tests" + # Missing store type. Try running `STORE= cargo test`: NotPresent + "webdav::webdav_tests" + # thread 'config::parser::tests::toml_parse' panicked at crates/utils/src/config/parser.rs:463:58: + # called `Result::unwrap()` on an `Err` value: "Expected ['\\n'] but found '!' in value at line 70." + "config::parser::tests::toml_parse" + # error[E0432]: unresolved import `r2d2_sqlite` + # use of undeclared crate or module `r2d2_sqlite` + "backend::sqlite::pool::SqliteConnectionManager::with_init" + # thread 'smtp::reporting::analyze::report_analyze' panicked at tests/src/smtp/reporting/analyze.rs:88:5: + # assertion `left == right` failed + # left: 0 + # right: 12 + "smtp::reporting::analyze::report_analyze" + # thread 'smtp::inbound::dmarc::dmarc' panicked at tests/src/smtp/inbound/mod.rs:59:26: + # Expected empty queue but got Reload + "smtp::inbound::dmarc::dmarc" + # thread 'smtp::queue::concurrent::concurrent_queue' panicked at tests/src/smtp/inbound/mod.rs:65:9: + # assertion `left == right` failed + "smtp::queue::concurrent::concurrent_queue" + # Failed to read system DNS config: io error: No such file or directory (os error 2) + "smtp::inbound::auth::auth" + # Failed to read system DNS config: io error: No such file or directory (os error 2) + "smtp::inbound::antispam::antispam" + # Failed to read system DNS config: io error: No such file or directory (os error 2) + "smtp::inbound::vrfy::vrfy_expn" + # thread 'smtp::management::queue::manage_queue' panicked at tests/src/smtp/inbound/mod.rs:45:23: + # No queue event received. + # NOTE: Test unreliable on high load systems + "smtp::management::queue::manage_queue" + # thread 'responses::tests::parse_responses' panicked at crates/dav-proto/src/responses/mod.rs:671:17: + # assertion `left == right` failed: failed for 008.xml + # left: ElementEnd + # right: Bytes([...]) + "responses::tests::parse_responses" + ] (test: "--skip=${test}"); + + doCheck = !(stdenv.hostPlatform.isLinux && stdenv.hostPlatform.isAarch64); + + # Allow network access during tests on Darwin/macOS + __darwinAllowLocalNetworking = true; + + passthru = { + inherit rocksdb; # make used rocksdb version available (e.g., for backup scripts) + webadmin = callPackage ./webadmin.nix { }; + spam-filter = callPackage ./spam-filter.nix { }; + updateScript = nix-update-script { }; + tests.stalwart-mail = nixosTests.stalwart-mail; + }; + + meta = { + description = "Secure & Modern All-in-One Mail Server (IMAP, JMAP, SMTP)"; + homepage = "https://github.com/stalwartlabs/mail-server"; + changelog = "https://github.com/stalwartlabs/mail-server/blob/main/CHANGELOG.md"; + license = [ + lib.licenses.agpl3Only + ] + ++ lib.optionals stalwartEnterprise [ + { + fullName = "Stalwart Enterprise License 1.0 (SELv1) Agreement"; + url = "https://github.com/stalwartlabs/mail-server/blob/main/LICENSES/LicenseRef-SEL.txt"; + free = false; + redistributable = false; + } + ]; + + mainProgram = "stalwart"; + maintainers = with lib.maintainers; [ + happysalada + onny + oddlama + pandapip1 + norpol + ]; + }; +}) diff --git a/nix/modules/nixos/infrastructure/mailserver/package/spam-filter.nix b/nix/modules/nixos/infrastructure/mailserver/package/spam-filter.nix new file mode 100644 index 0000000..d0ca6c6 --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/package/spam-filter.nix @@ -0,0 +1,43 @@ +{ + lib, + fetchFromGitHub, + stdenv, + stalwart-mail, + nix-update-script, +}: + +stdenv.mkDerivation (finalAttrs: { + pname = "spam-filter"; + version = "2.0.3"; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "spam-filter"; + tag = "v${finalAttrs.version}"; + hash = "sha256-NhD/qUiGhgESwR2IOzAHfDATRlgWMcCktlktvVfDONk="; + }; + + buildPhase = '' + bash ./build.sh + ''; + + installPhase = '' + mkdir -p $out + cp spam-filter.toml $out/ + ''; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Secure & modern all-in-one mail server Stalwart (spam-filter module)"; + homepage = "https://github.com/stalwartlabs/spam-filter"; + changelog = "https://github.com/stalwartlabs/spam-filter/blob/${finalAttrs.src.tag}/CHANGELOG.md"; + license = with lib.licenses; [ + mit + asl20 + ]; + inherit (stalwart-mail.meta) maintainers; + }; +}) diff --git a/nix/modules/nixos/infrastructure/mailserver/package/webadmin.nix b/nix/modules/nixos/infrastructure/mailserver/package/webadmin.nix new file mode 100644 index 0000000..18785be --- /dev/null +++ b/nix/modules/nixos/infrastructure/mailserver/package/webadmin.nix @@ -0,0 +1,77 @@ +{ + lib, + rustPlatform, + stalwart-mail, + fetchFromGitHub, + trunk, + tailwindcss_3, + fetchNpmDeps, + nix-update-script, + nodejs, + npmHooks, + llvmPackages, + wasm-bindgen-cli_0_2_93, + binaryen, + zip, +}: + +rustPlatform.buildRustPackage (finalAttrs: { + pname = "webadmin"; + version = "0.1.31"; + + src = fetchFromGitHub { + owner = "stalwartlabs"; + repo = "webadmin"; + rev = "6f1368b8a1160341b385980accea489ee0e45440"; + hash = "sha256-/EWn/wiY6zFNhObfo11OkoGufcUODMYs18P3vTBbB8s="; + }; + + npmDeps = fetchNpmDeps { + name = "${finalAttrs.pname}-npm-deps"; + hash = "sha256-na1HEueX8w7kuDp8LEtJ0nD1Yv39cyk6sEMpS1zix2s="; + }; + + cargoHash = "sha256-Q05+wH9+NfkfmEDJFLuWVQ7wuDeEu9h1XmOMN6SYdyU="; + + postPatch = '' + # Using local tailwindcss for compilation + substituteInPlace Trunk.toml --replace-fail "npx tailwindcss" "tailwindcss" + ''; + + nativeBuildInputs = [ + binaryen + llvmPackages.bintools-unwrapped + nodejs + npmHooks.npmConfigHook + tailwindcss_3 + trunk + # needs to match with wasm-bindgen version in upstreams Cargo.lock + wasm-bindgen-cli_0_2_93 + + zip + ]; + + NODE_PATH = "$npmDeps"; + + buildPhase = '' + trunk build --offline --frozen --release + ''; + + installPhase = '' + cd dist + mkdir -p $out + zip -r $out/webadmin.zip * + ''; + + passthru = { + updateScript = nix-update-script { }; + }; + + meta = { + description = "Secure & modern all-in-one mail server Stalwart (webadmin module)"; + homepage = "https://github.com/stalwartlabs/webadmin"; + changelog = "https://github.com/stalwartlabs/webadmin/blob/${finalAttrs.src.tag}/CHANGELOG.md"; + license = lib.licenses.agpl3Only; + inherit (stalwart-mail.meta) maintainers; + }; +}) diff --git a/nix/modules/nixos/infrastructure/mailserver/prometheus.nix b/nix/modules/nixos/infrastructure/mailserver/prometheus.nix index 3318a54..9c1e7f5 100644 --- a/nix/modules/nixos/infrastructure/mailserver/prometheus.nix +++ b/nix/modules/nixos/infrastructure/mailserver/prometheus.nix @@ -1,13 +1,16 @@ { config, lib, ... }: let + fqdn = config.khscodes.networking.fqdn; cfg = config.khscodes.infrastructure.mailserver; in { config = lib.mkIf cfg.enable { - - services.prometheus.exporters.postfix = { + services.stalwart-mail.settings.metrics.prometheus = { enable = true; }; - khscodes.infrastructure.vault-prometheus-sender.exporters.enabled = [ "postfix" ]; + # Don't expose the endpoint + khscodes.services.nginx.virtualHosts."${fqdn}".locations."=/metrics/prometheus" = { + return = 404; + }; }; }