at 25.11-pre 7.3 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 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"; 12 13 parsePorts = 14 listeners: 15 let 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)); 19 in 20 builtins.map (address: extractPort address) (parseAddresses listeners); 21 22in 23{ 24 options.services.stalwart-mail = { 25 enable = lib.mkEnableOption "the Stalwart all-in-one email server"; 26 27 package = lib.mkPackageOption pkgs "stalwart-mail" { }; 28 29 openFirewall = lib.mkOption { 30 type = lib.types.bool; 31 default = false; 32 description = '' 33 Whether to open TCP firewall ports, which are specified in 34 {option}`services.stalwart-mail.settings.listener` on all interfaces. 35 ''; 36 }; 37 38 settings = lib.mkOption { 39 inherit (configFormat) type; 40 default = { }; 41 description = '' 42 Configuration options for the Stalwart email server. 43 See <https://stalw.art/docs/category/configuration> for available options. 44 45 By default, the module is configured to store everything locally. 46 ''; 47 }; 48 49 dataDir = lib.mkOption { 50 type = lib.types.path; 51 default = "/var/lib/stalwart-mail"; 52 description = '' 53 Data directory for stalwart 54 ''; 55 }; 56 57 credentials = lib.mkOption { 58 description = '' 59 Credentials envs used to configure Stalwart-Mail secrets. 60 These secrets can be accessed in configuration values with 61 the macros such as 62 `%{file:/run/credentials/stalwart-mail.service/VAR_NAME}%`. 63 ''; 64 type = lib.types.attrsOf lib.types.str; 65 default = { }; 66 example = { 67 user_admin_password = "/run/keys/stalwart_admin_password"; 68 }; 69 }; 70 71 }; 72 73 config = lib.mkIf cfg.enable { 74 75 # Default config: all local 76 services.stalwart-mail.settings = { 77 tracer.stdout = { 78 type = lib.mkDefault "stdout"; 79 level = lib.mkDefault "info"; 80 ansi = lib.mkDefault false; # no colour markers to journald 81 enable = lib.mkDefault true; 82 }; 83 store = 84 if useLegacyStorage then 85 { 86 # structured data in SQLite, blobs on filesystem 87 db.type = lib.mkDefault "sqlite"; 88 db.path = lib.mkDefault "${cfg.dataDir}/data/index.sqlite3"; 89 fs.type = lib.mkDefault "fs"; 90 fs.path = lib.mkDefault "${cfg.dataDir}/data/blobs"; 91 } 92 else 93 { 94 # everything in RocksDB 95 db.type = lib.mkDefault "rocksdb"; 96 db.path = lib.mkDefault "${cfg.dataDir}/db"; 97 db.compression = lib.mkDefault "lz4"; 98 }; 99 storage.data = lib.mkDefault "db"; 100 storage.fts = lib.mkDefault "db"; 101 storage.lookup = lib.mkDefault "db"; 102 storage.blob = lib.mkDefault (if useLegacyStorage then "fs" else "db"); 103 directory.internal.type = lib.mkDefault "internal"; 104 directory.internal.store = lib.mkDefault "db"; 105 storage.directory = lib.mkDefault "internal"; 106 resolver.type = lib.mkDefault "system"; 107 resolver.public-suffix = lib.mkDefault [ 108 "file://${pkgs.publicsuffix-list}/share/publicsuffix/public_suffix_list.dat" 109 ]; 110 config = { 111 spam-filter.resource = lib.mkDefault "file://${cfg.package}/etc/stalwart/spamfilter.toml"; 112 webadmin = 113 let 114 hasHttpListener = builtins.any (listener: listener.protocol == "http") ( 115 lib.attrValues cfg.settings.server.listener 116 ); 117 in 118 { 119 path = "/var/cache/stalwart-mail"; 120 } 121 // lib.optionalAttrs ((builtins.hasAttr "listener" cfg.settings.server) && hasHttpListener) { 122 resource = lib.mkDefault "file://${cfg.package.webadmin}/webadmin.zip"; 123 }; 124 }; 125 }; 126 127 # This service stores a potentially large amount of data. 128 # Running it as a dynamic user would force chown to be run everytime the 129 # service is restarted on a potentially large number of files. 130 # That would cause unnecessary and unwanted delays. 131 users = { 132 groups.stalwart-mail = { }; 133 users.stalwart-mail = { 134 isSystemUser = true; 135 group = "stalwart-mail"; 136 }; 137 }; 138 139 systemd.tmpfiles.rules = [ 140 "d '${cfg.dataDir}' - stalwart-mail stalwart-mail - -" 141 ]; 142 143 systemd = { 144 packages = [ cfg.package ]; 145 services.stalwart-mail = { 146 wantedBy = [ "multi-user.target" ]; 147 after = [ 148 "local-fs.target" 149 "network.target" 150 ]; 151 152 preStart = 153 if useLegacyStorage then 154 '' 155 mkdir -p ${cfg.dataDir}/data/blobs 156 '' 157 else 158 '' 159 mkdir -p ${cfg.dataDir}/db 160 ''; 161 162 serviceConfig = { 163 ExecStart = [ 164 "" 165 "${cfg.package}/bin/stalwart-mail --config=${configFile}" 166 ]; 167 LoadCredential = lib.mapAttrsToList (key: value: "${key}:${value}") cfg.credentials; 168 169 StandardOutput = "journal"; 170 StandardError = "journal"; 171 172 ReadWritePaths = [ 173 cfg.dataDir 174 ]; 175 CacheDirectory = "stalwart-mail"; 176 StateDirectory = "stalwart-mail"; 177 178 # Bind standard privileged ports 179 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; 180 CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; 181 182 # Hardening 183 DeviceAllow = [ "" ]; 184 LockPersonality = true; 185 MemoryDenyWriteExecute = true; 186 PrivateDevices = true; 187 PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE 188 ProcSubset = "pid"; 189 PrivateTmp = true; 190 ProtectClock = true; 191 ProtectControlGroups = true; 192 ProtectHome = true; 193 ProtectHostname = true; 194 ProtectKernelLogs = true; 195 ProtectKernelModules = true; 196 ProtectKernelTunables = true; 197 ProtectProc = "invisible"; 198 ProtectSystem = "strict"; 199 RestrictAddressFamilies = [ 200 "AF_INET" 201 "AF_INET6" 202 ]; 203 RestrictNamespaces = true; 204 RestrictRealtime = true; 205 RestrictSUIDSGID = true; 206 SystemCallArchitectures = "native"; 207 SystemCallFilter = [ 208 "@system-service" 209 "~@privileged" 210 ]; 211 UMask = "0077"; 212 }; 213 unitConfig.ConditionPathExists = [ 214 "" 215 "${configFile}" 216 ]; 217 }; 218 }; 219 220 # Make admin commands available in the shell 221 environment.systemPackages = [ cfg.package ]; 222 223 networking.firewall = 224 lib.mkIf (cfg.openFirewall && (builtins.hasAttr "listener" cfg.settings.server)) 225 { 226 allowedTCPPorts = parsePorts cfg.settings.server.listener; 227 }; 228 }; 229 230 meta = { 231 maintainers = with lib.maintainers; [ 232 happysalada 233 euxane 234 onny 235 ]; 236 }; 237}