1 { config, lib, pkgs, ... }:
3 cfg = config.services.stalwart-mail;
4 configFormat = pkgs.formats.toml { };
5 configFile = configFormat.generate "stalwart-mail.toml" cfg.settings;
6 dataDir = "/var/lib/stalwart-mail";
7 useLegacyStorage = lib.versionOlder config.system.stateVersion "24.11";
9 parsePorts = listeners: let
10 parseAddresses = listeners: lib.flatten(lib.mapAttrsToList (name: value: value.bind) listeners);
11 splitAddress = addr: lib.splitString ":" addr;
12 extractPort = addr: lib.toInt(builtins.foldl' (a: b: b) "" (splitAddress addr));
14 builtins.map(address: extractPort address) (parseAddresses listeners);
17 options.services.stalwart-mail = {
18 enable = lib.mkEnableOption "the Stalwart all-in-one email server";
20 package = lib.mkPackageOption pkgs "stalwart-mail" { };
22 openFirewall = lib.mkOption {
23 type = lib.types.bool;
26 Whether to open TCP firewall ports, which are specified in
27 {option}`services.stalwart-mail.settings.listener` on all interfaces.
31 settings = lib.mkOption {
32 inherit (configFormat) type;
35 Configuration options for the Stalwart email server.
36 See <https://stalw.art/docs/category/configuration> for available options.
38 By default, the module is configured to store everything locally.
43 config = lib.mkIf cfg.enable {
45 # Default config: all local
46 services.stalwart-mail.settings = {
48 type = lib.mkDefault "stdout";
49 level = lib.mkDefault "info";
50 ansi = lib.mkDefault false; # no colour markers to journald
51 enable = lib.mkDefault true;
53 store = if useLegacyStorage then {
54 # structured data in SQLite, blobs on filesystem
55 db.type = lib.mkDefault "sqlite";
56 db.path = lib.mkDefault "${dataDir}/data/index.sqlite3";
57 fs.type = lib.mkDefault "fs";
58 fs.path = lib.mkDefault "${dataDir}/data/blobs";
60 # everything in RocksDB
61 db.type = lib.mkDefault "rocksdb";
62 db.path = lib.mkDefault "${dataDir}/db";
63 db.compression = lib.mkDefault "lz4";
65 storage.data = lib.mkDefault "db";
66 storage.fts = lib.mkDefault "db";
67 storage.lookup = lib.mkDefault "db";
68 storage.blob = lib.mkDefault (if useLegacyStorage then "fs" else "db");
69 directory.internal.type = lib.mkDefault "internal";
70 directory.internal.store = lib.mkDefault "db";
71 storage.directory = lib.mkDefault "internal";
72 resolver.type = lib.mkDefault "system";
73 resolver.public-suffix = lib.mkDefault [
74 "file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat"
77 hasHttpListener = builtins.any (listener: listener.protocol == "http") (lib.attrValues cfg.settings.server.listener);
79 spam-filter = lib.mkDefault "file://${cfg.package}/etc/stalwart/spamfilter.toml";
80 } // lib.optionalAttrs (
81 (builtins.hasAttr "listener" cfg.settings.server) && hasHttpListener
83 webadmin = lib.mkDefault "file://${cfg.package.webadmin}/webadmin.zip";
85 webadmin.path = "/var/cache/stalwart-mail";
88 # This service stores a potentially large amount of data.
89 # Running it as a dynamic user would force chown to be run everytime the
90 # service is restarted on a potentially large number of files.
91 # That would cause unnecessary and unwanted delays.
93 groups.stalwart-mail = { };
94 users.stalwart-mail = {
96 group = "stalwart-mail";
101 packages = [ cfg.package ];
102 services.stalwart-mail = {
103 wantedBy = [ "multi-user.target" ];
104 after = [ "local-fs.target" "network.target" ];
106 preStart = if useLegacyStorage then ''
107 mkdir -p ${dataDir}/data/blobs
109 mkdir -p ${dataDir}/db
115 "${cfg.package}/bin/stalwart-mail --config=${configFile}"
118 StandardOutput = "journal";
119 StandardError = "journal";
121 CacheDirectory = "stalwart-mail";
122 StateDirectory = "stalwart-mail";
124 # Bind standard privileged ports
125 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
126 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
129 DeviceAllow = [ "" ];
130 LockPersonality = true;
131 MemoryDenyWriteExecute = true;
132 PrivateDevices = true;
133 PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE
137 ProtectControlGroups = true;
139 ProtectHostname = true;
140 ProtectKernelLogs = true;
141 ProtectKernelModules = true;
142 ProtectKernelTunables = true;
143 ProtectProc = "invisible";
144 ProtectSystem = "strict";
145 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
146 RestrictNamespaces = true;
147 RestrictRealtime = true;
148 RestrictSUIDSGID = true;
149 SystemCallArchitectures = "native";
150 SystemCallFilter = [ "@system-service" "~@privileged" ];
153 unitConfig.ConditionPathExists = [
160 # Make admin commands available in the shell
161 environment.systemPackages = [ cfg.package ];
163 networking.firewall = lib.mkIf (cfg.openFirewall
164 && (builtins.hasAttr "listener" cfg.settings.server)) {
165 allowedTCPPorts = parsePorts cfg.settings.server.listener;
170 maintainers = with lib.maintainers; [ happysalada pacien onny ];