8 cfg = config.services.stalwart-mail;
9 configFormat = pkgs.formats.toml { };
10 configFile = configFormat.generate "stalwart-mail.toml" cfg.settings;
11 useLegacyStorage = lib.versionOlder config.system.stateVersion "24.11";
16 parseAddresses = listeners: lib.flatten (lib.mapAttrsToList (name: value: value.bind) listeners);
17 splitAddress = addr: lib.splitString ":" addr;
18 extractPort = addr: lib.toInt (builtins.foldl' (a: b: b) "" (splitAddress addr));
20 builtins.map (address: extractPort address) (parseAddresses listeners);
24 options.services.stalwart-mail = {
25 enable = lib.mkEnableOption "the Stalwart all-in-one email server";
27 package = lib.mkPackageOption pkgs "stalwart-mail" { };
29 openFirewall = lib.mkOption {
30 type = lib.types.bool;
33 Whether to open TCP firewall ports, which are specified in
34 {option}`services.stalwart-mail.settings.listener` on all interfaces.
38 settings = lib.mkOption {
39 inherit (configFormat) type;
42 Configuration options for the Stalwart email server.
43 See <https://stalw.art/docs/category/configuration> for available options.
45 By default, the module is configured to store everything locally.
49 dataDir = lib.mkOption {
50 type = lib.types.path;
51 default = "/var/lib/stalwart-mail";
53 Data directory for stalwart
58 config = lib.mkIf cfg.enable {
60 # Default config: all local
61 services.stalwart-mail.settings = {
63 type = lib.mkDefault "stdout";
64 level = lib.mkDefault "info";
65 ansi = lib.mkDefault false; # no colour markers to journald
66 enable = lib.mkDefault true;
69 if useLegacyStorage then
71 # structured data in SQLite, blobs on filesystem
72 db.type = lib.mkDefault "sqlite";
73 db.path = lib.mkDefault "${cfg.dataDir}/data/index.sqlite3";
74 fs.type = lib.mkDefault "fs";
75 fs.path = lib.mkDefault "${cfg.dataDir}/data/blobs";
79 # everything in RocksDB
80 db.type = lib.mkDefault "rocksdb";
81 db.path = lib.mkDefault "${cfg.dataDir}/db";
82 db.compression = lib.mkDefault "lz4";
84 storage.data = lib.mkDefault "db";
85 storage.fts = lib.mkDefault "db";
86 storage.lookup = lib.mkDefault "db";
87 storage.blob = lib.mkDefault (if useLegacyStorage then "fs" else "db");
88 directory.internal.type = lib.mkDefault "internal";
89 directory.internal.store = lib.mkDefault "db";
90 storage.directory = lib.mkDefault "internal";
91 resolver.type = lib.mkDefault "system";
92 resolver.public-suffix = lib.mkDefault [
93 "file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat"
97 hasHttpListener = builtins.any (listener: listener.protocol == "http") (
98 lib.attrValues cfg.settings.server.listener
102 spam-filter = lib.mkDefault "file://${cfg.package}/etc/stalwart/spamfilter.toml";
104 // lib.optionalAttrs ((builtins.hasAttr "listener" cfg.settings.server) && hasHttpListener) {
105 webadmin = lib.mkDefault "file://${cfg.package.webadmin}/webadmin.zip";
107 webadmin.path = "/var/cache/stalwart-mail";
110 # This service stores a potentially large amount of data.
111 # Running it as a dynamic user would force chown to be run everytime the
112 # service is restarted on a potentially large number of files.
113 # That would cause unnecessary and unwanted delays.
115 groups.stalwart-mail = { };
116 users.stalwart-mail = {
118 group = "stalwart-mail";
122 systemd.tmpfiles.rules = [
123 "d '${cfg.dataDir}' - stalwart-mail stalwart-mail - -"
127 packages = [ cfg.package ];
128 services.stalwart-mail = {
129 wantedBy = [ "multi-user.target" ];
136 if useLegacyStorage then
138 mkdir -p ${cfg.dataDir}/data/blobs
142 mkdir -p ${cfg.dataDir}/db
148 "${cfg.package}/bin/stalwart-mail --config=${configFile}"
151 StandardOutput = "journal";
152 StandardError = "journal";
157 CacheDirectory = "stalwart-mail";
158 StateDirectory = "stalwart-mail";
160 # Bind standard privileged ports
161 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
162 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
165 DeviceAllow = [ "" ];
166 LockPersonality = true;
167 MemoryDenyWriteExecute = true;
168 PrivateDevices = true;
169 PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE
173 ProtectControlGroups = true;
175 ProtectHostname = true;
176 ProtectKernelLogs = true;
177 ProtectKernelModules = true;
178 ProtectKernelTunables = true;
179 ProtectProc = "invisible";
180 ProtectSystem = "strict";
181 RestrictAddressFamilies = [
185 RestrictNamespaces = true;
186 RestrictRealtime = true;
187 RestrictSUIDSGID = true;
188 SystemCallArchitectures = "native";
195 unitConfig.ConditionPathExists = [
202 # Make admin commands available in the shell
203 environment.systemPackages = [ cfg.package ];
205 networking.firewall =
206 lib.mkIf (cfg.openFirewall && (builtins.hasAttr "listener" cfg.settings.server))
208 allowedTCPPorts = parsePorts cfg.settings.server.listener;
213 maintainers = with lib.maintainers; [