orchard: 0.26.3 -> 0.26.4 (#377709)
[NixPkgs.git] / nixos / modules / services / mail / stalwart-mail.nix
blob849661646977b2ae8dd0ed0b67171ef6ccbf6faf
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 let
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";
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);
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;
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     };
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.
45         By default, the module is configured to store everything locally.
46       '';
47     };
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   };
58   config = lib.mkIf cfg.enable {
60     # Default config: all local
61     services.stalwart-mail.settings = {
62       tracer.stdout = {
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;
67       };
68       store =
69         if useLegacyStorage then
70           {
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";
76           }
77         else
78           {
79             # everything in RocksDB
80             db.type = lib.mkDefault "rocksdb";
81             db.path = lib.mkDefault "${cfg.dataDir}/db";
82             db.compression = lib.mkDefault "lz4";
83           };
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"
94       ];
95       config.resource =
96         let
97           hasHttpListener = builtins.any (listener: listener.protocol == "http") (
98             lib.attrValues cfg.settings.server.listener
99           );
100         in
101         {
102           spam-filter = lib.mkDefault "file://${cfg.package}/etc/stalwart/spamfilter.toml";
103         }
104         // lib.optionalAttrs ((builtins.hasAttr "listener" cfg.settings.server) && hasHttpListener) {
105           webadmin = lib.mkDefault "file://${cfg.package.webadmin}/webadmin.zip";
106         };
107       webadmin.path = "/var/cache/stalwart-mail";
108     };
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.
114     users = {
115       groups.stalwart-mail = { };
116       users.stalwart-mail = {
117         isSystemUser = true;
118         group = "stalwart-mail";
119       };
120     };
122     systemd.tmpfiles.rules = [
123       "d '${cfg.dataDir}' - stalwart-mail stalwart-mail - -"
124     ];
126     systemd = {
127       packages = [ cfg.package ];
128       services.stalwart-mail = {
129         wantedBy = [ "multi-user.target" ];
130         after = [
131           "local-fs.target"
132           "network.target"
133         ];
135         preStart =
136           if useLegacyStorage then
137             ''
138               mkdir -p ${cfg.dataDir}/data/blobs
139             ''
140           else
141             ''
142               mkdir -p ${cfg.dataDir}/db
143             '';
145         serviceConfig = {
146           ExecStart = [
147             ""
148             "${cfg.package}/bin/stalwart-mail --config=${configFile}"
149           ];
151           StandardOutput = "journal";
152           StandardError = "journal";
154           ReadWritePaths = [
155             cfg.dataDir
156           ];
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" ];
164           # Hardening
165           DeviceAllow = [ "" ];
166           LockPersonality = true;
167           MemoryDenyWriteExecute = true;
168           PrivateDevices = true;
169           PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE
170           ProcSubset = "pid";
171           PrivateTmp = true;
172           ProtectClock = true;
173           ProtectControlGroups = true;
174           ProtectHome = true;
175           ProtectHostname = true;
176           ProtectKernelLogs = true;
177           ProtectKernelModules = true;
178           ProtectKernelTunables = true;
179           ProtectProc = "invisible";
180           ProtectSystem = "strict";
181           RestrictAddressFamilies = [
182             "AF_INET"
183             "AF_INET6"
184           ];
185           RestrictNamespaces = true;
186           RestrictRealtime = true;
187           RestrictSUIDSGID = true;
188           SystemCallArchitectures = "native";
189           SystemCallFilter = [
190             "@system-service"
191             "~@privileged"
192           ];
193           UMask = "0077";
194         };
195         unitConfig.ConditionPathExists = [
196           ""
197           "${configFile}"
198         ];
199       };
200     };
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))
207         {
208           allowedTCPPorts = parsePorts cfg.settings.server.listener;
209         };
210   };
212   meta = {
213     maintainers = with lib.maintainers; [
214       happysalada
215       euxane
216       onny
217     ];
218   };