1{
2 pkgs,
3 lib,
4 config,
5 ...
6}:
7let
8 cfg = config.services.cyrus-imap;
9 cyrus-imapdPkg = pkgs.cyrus-imapd;
10 inherit (lib)
11 mkEnableOption
12 mkIf
13 mkOption
14 optionalAttrs
15 optionalString
16 generators
17 mapAttrsToList
18 ;
19 inherit (lib.strings) concatStringsSep;
20 inherit (lib.types)
21 attrsOf
22 submodule
23 listOf
24 oneOf
25 str
26 int
27 bool
28 nullOr
29 path
30 ;
31
32 mkCyrusConfig =
33 settings:
34 let
35 mkCyrusOptionsList =
36 v:
37 mapAttrsToList (
38 p: q:
39 if (q != null) then
40 if builtins.isInt q then
41 "${p}=${builtins.toString q}"
42 else
43 "${p}=\"${if builtins.isList q then (concatStringsSep " " q) else q}\""
44 else
45 ""
46 ) v;
47 mkCyrusOptionsString = v: concatStringsSep " " (mkCyrusOptionsList v);
48 in
49 concatStringsSep "\n " (mapAttrsToList (n: v: n + " " + (mkCyrusOptionsString v)) settings);
50
51 cyrusConfig = lib.concatStringsSep "\n" (
52 lib.mapAttrsToList (n: v: ''
53 ${n} {
54 ${mkCyrusConfig v}
55 }
56 '') cfg.cyrusSettings
57 );
58
59 imapdConfig =
60 with generators;
61 toKeyValue {
62 mkKeyValue = mkKeyValueDefault {
63 mkValueString =
64 v:
65 if builtins.isBool v then
66 if v then "yes" else "no"
67 else if builtins.isList v then
68 concatStringsSep " " v
69 else
70 mkValueStringDefault { } v;
71 } ": ";
72 } cfg.imapdSettings;
73in
74{
75 imports = [
76 (lib.mkRenamedOptionModule
77 [ "services" "cyrus-imap" "sslServerCert" ]
78 [ "services" "cyrus-imap" "imapdSettings" "tls_server_cert" ]
79 )
80 (lib.mkRenamedOptionModule
81 [ "services" "cyrus-imap" "sslServerKey" ]
82 [ "services" "cyrus-imap" "imapdSettings" "tls_server_key" ]
83 )
84 (lib.mkRenamedOptionModule
85 [ "services" "cyrus-imap" "sslCACert" ]
86 [ "services" "cyrus-imap" "imapdSettings" "tls_client_ca_file" ]
87 )
88 ];
89 options.services.cyrus-imap = {
90 enable = mkEnableOption "Cyrus IMAP, an email, contacts and calendar server";
91 debug = mkEnableOption "debugging messages for the Cyrus master process";
92
93 listenQueue = mkOption {
94 type = int;
95 default = 32;
96 description = ''
97 Socket listen queue backlog size. See {manpage}`listen(2)` for more information about a backlog.
98 Default is 32, which may be increased if you have a very high connection rate.
99 '';
100 };
101 tmpDBDir = mkOption {
102 type = path;
103 default = "/run/cyrus/db";
104 description = ''
105 Location where DB files are stored.
106 Databases in this directory are recreated upon startup, so ideally they should live in ephemeral storage for best performance.
107 '';
108 };
109 cyrusSettings = mkOption {
110 type = submodule {
111 freeformType = attrsOf (
112 attrsOf (oneOf [
113 bool
114 int
115 (listOf str)
116 ])
117 );
118 options = {
119 START = mkOption {
120 default = {
121 recover = {
122 cmd = [
123 "ctl_cyrusdb"
124 "-r"
125 ];
126 };
127 };
128 description = ''
129 This section lists the processes to run before any SERVICES are spawned.
130 This section is typically used to initialize databases.
131 Master itself will not startup until all tasks in START have completed, so put no blocking commands here.
132 '';
133 };
134 SERVICES = mkOption {
135 default = {
136 imap = {
137 cmd = [ "imapd" ];
138 listen = "imap";
139 prefork = 0;
140 };
141 pop3 = {
142 cmd = [ "pop3d" ];
143 listen = "pop3";
144 prefork = 0;
145 };
146 lmtpunix = {
147 cmd = [ "lmtpd" ];
148 listen = "/run/cyrus/lmtp";
149 prefork = 0;
150 };
151 notify = {
152 cmd = [ "notifyd" ];
153 listen = "/run/cyrus/notify";
154 proto = "udp";
155 prefork = 0;
156 };
157 };
158 description = ''
159 This section is the heart of the cyrus.conf file. It lists the processes that should be spawned to handle client connections made on certain Internet/UNIX sockets.
160 '';
161 };
162 EVENTS = mkOption {
163 default = {
164 tlsprune = {
165 cmd = [ "tls_prune" ];
166 at = 400;
167 };
168 delprune = {
169 cmd = [
170 "cyr_expire"
171 "-E"
172 "3"
173 ];
174 at = 400;
175 };
176 deleteprune = {
177 cmd = [
178 "cyr_expire"
179 "-E"
180 "4"
181 "-D"
182 "28"
183 ];
184 at = 430;
185 };
186 expungeprune = {
187 cmd = [
188 "cyr_expire"
189 "-E"
190 "4"
191 "-X"
192 "28"
193 ];
194 at = 445;
195 };
196 checkpoint = {
197 cmd = [
198 "ctl_cyrusdb"
199 "-c"
200 ];
201 period = 30;
202 };
203 };
204 description = ''
205 This section lists processes that should be run at specific intervals, similar to cron jobs. This section is typically used to perform scheduled cleanup/maintenance.
206 '';
207 };
208 DAEMON = mkOption {
209 default = { };
210 description = ''
211 This section lists long running daemons to start before any SERVICES are spawned. {manpage}`master(8)` will ensure that these processes are running, restarting any process which dies or forks. All listed processes will be shutdown when {manpage}`master(8)` is exiting.
212 '';
213 };
214 };
215 };
216 description = "Cyrus configuration settings. See [cyrus.conf(5)](https://www.cyrusimap.org/imap/reference/manpages/configs/cyrus.conf.html)";
217 };
218 imapdSettings = mkOption {
219 type = submodule {
220 freeformType = attrsOf (oneOf [
221 str
222 int
223 bool
224 (listOf str)
225 ]);
226 options = {
227 configdirectory = mkOption {
228 type = path;
229 default = "/var/lib/cyrus";
230 description = ''
231 The pathname of the IMAP configuration directory.
232 '';
233 };
234 lmtpsocket = mkOption {
235 type = path;
236 default = "/run/cyrus/lmtp";
237 description = ''
238 Unix socket that lmtpd listens on, used by {manpage}`deliver(8)`. This should match the path specified in {manpage}`cyrus.conf(5)`.
239 '';
240 };
241 idlesocket = mkOption {
242 type = path;
243 default = "/run/cyrus/idle";
244 description = ''
245 Unix socket that idled listens on.
246 '';
247 };
248 notifysocket = mkOption {
249 type = path;
250 default = "/run/cyrus/notify";
251 description = ''
252 Unix domain socket that the mail notification daemon listens on.
253 '';
254 };
255 };
256 };
257 default = {
258 admins = [ "cyrus" ];
259 allowplaintext = true;
260 defaultdomain = "localhost";
261 defaultpartition = "default";
262 duplicate_db_path = "/run/cyrus/db/deliver.db";
263 hashimapspool = true;
264 httpmodules = [
265 "carddav"
266 "caldav"
267 ];
268 mboxname_lockpath = "/run/cyrus/lock";
269 partition-default = "/var/lib/cyrus/storage";
270 popminpoll = 1;
271 proc_path = "/run/cyrus/proc";
272 ptscache_db_path = "/run/cyrus/db/ptscache.db";
273 sasl_auto_transition = true;
274 sasl_pwcheck_method = [ "saslauthd" ];
275 sievedir = "/var/lib/cyrus/sieve";
276 statuscache_db_path = "/run/cyrus/db/statuscache.db";
277 syslog_prefix = "cyrus";
278 tls_client_ca_dir = "/etc/ssl/certs";
279 tls_session_timeout = 1440;
280 tls_sessions_db_path = "/run/cyrus/db/tls_sessions.db";
281 virtdomains = "on";
282 };
283 description = "IMAP configuration settings. See [imapd.conf(5)](https://www.cyrusimap.org/imap/reference/manpages/configs/imapd.conf.html)";
284 };
285
286 user = mkOption {
287 type = nullOr str;
288 default = null;
289 description = "Cyrus IMAP user name. If this is not set, a user named `cyrus` will be created.";
290 };
291
292 group = mkOption {
293 type = nullOr str;
294 default = null;
295 description = "Cyrus IMAP group name. If this is not set, a group named `cyrus` will be created.";
296 };
297
298 imapdConfigFile = mkOption {
299 type = nullOr path;
300 default = null;
301 description = "Path to the configuration file used for cyrus-imap.";
302 apply = v: if v != null then v else pkgs.writeText "imapd.conf" imapdConfig;
303 };
304
305 cyrusConfigFile = mkOption {
306 type = nullOr path;
307 default = null;
308 description = "Path to the configuration file used for Cyrus.";
309 apply = v: if v != null then v else pkgs.writeText "cyrus.conf" cyrusConfig;
310 };
311 };
312
313 config = mkIf cfg.enable {
314 users.users.cyrus = optionalAttrs (cfg.user == null) {
315 description = "Cyrus IMAP user";
316 isSystemUser = true;
317 group = optionalString (cfg.group == null) "cyrus";
318 };
319
320 users.groups.cyrus = optionalAttrs (cfg.group == null) { };
321
322 environment.etc."imapd.conf".source = cfg.imapdConfigFile;
323 environment.etc."cyrus.conf".source = cfg.cyrusConfigFile;
324
325 systemd.services.cyrus-imap = {
326 description = "Cyrus IMAP server";
327
328 after = [ "network.target" ];
329 wantedBy = [ "multi-user.target" ];
330 restartTriggers = [
331 "/etc/imapd.conf"
332 "/etc/cyrus.conf"
333 ];
334
335 startLimitIntervalSec = 60;
336 environment = {
337 CYRUS_VERBOSE = mkIf cfg.debug "1";
338 LISTENQUEUE = builtins.toString cfg.listenQueue;
339 };
340 serviceConfig = {
341 User = if (cfg.user == null) then "cyrus" else cfg.user;
342 Group = if (cfg.group == null) then "cyrus" else cfg.group;
343 Type = "simple";
344 ExecStart = "${cyrus-imapdPkg}/libexec/master -l $LISTENQUEUE -C /etc/imapd.conf -M /etc/cyrus.conf -p /run/cyrus/master.pid -D";
345 Restart = "on-failure";
346 RestartSec = "1s";
347 RuntimeDirectory = "cyrus";
348 StateDirectory = "cyrus";
349
350 # Hardening
351 AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
352 PrivateTmp = true;
353 PrivateDevices = true;
354 ProtectSystem = "full";
355 CapabilityBoundingSet = [ "~CAP_NET_ADMIN CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_MODULE" ];
356 MemoryDenyWriteExecute = true;
357 ProtectKernelModules = true;
358 ProtectKernelTunables = true;
359 ProtectControlGroups = true;
360 RestrictAddressFamilies = [
361 "AF_INET"
362 "AF_INET6"
363 "AF_NETLINK"
364 "AF_UNIX"
365 ];
366 RestrictNamespaces = true;
367 RestrictRealtime = true;
368 };
369 preStart = ''
370 mkdir -p '${cfg.imapdSettings.configdirectory}/socket' '${cfg.tmpDBDir}' /run/cyrus/proc /run/cyrus/lock
371 '';
372 };
373 environment.systemPackages = [ cyrus-imapdPkg ];
374 };
375}