Self-host your own digital island
1# nixos-mailserver: a simple mail server
2# Copyright (C) 2016-2018 Robin Raymond
3#
4# This program is free software: you can redistribute it and/or modify
5# it under the terms of the GNU General Public License as published by
6# the Free Software Foundation, either version 3 of the License, or
7# (at your option) any later version.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <http://www.gnu.org/licenses/>
16
17{ config, pkgs, lib, ... }:
18
19with (import ./common.nix { inherit config pkgs lib; });
20
21let
22 cfg = config.mailserver;
23
24 passwdDir = "/run/dovecot2";
25 passwdFile = "${passwdDir}/passwd";
26
27 bool2int = x: if x then "1" else "0";
28
29 maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
30
31 # maildir in format "/${domain}/${user}"
32 dovecotMaildir =
33 "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
34 + (lib.optionalString (cfg.indexDir != null)
35 ":INDEX=${cfg.indexDir}/%d/%n"
36 );
37
38 postfixCfg = config.services.postfix;
39 dovecot2Cfg = config.services.dovecot2;
40
41 stateDir = "/var/lib/dovecot";
42
43 pipeBin = pkgs.stdenv.mkDerivation {
44 name = "pipe_bin";
45 src = ./dovecot/pipe_bin;
46 buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
47 buildCommand = ''
48 mkdir -p $out/pipe/bin
49 cp $src/* $out/pipe/bin/
50 chmod a+x $out/pipe/bin/*
51 patchShebangs $out/pipe/bin
52
53 for file in $out/pipe/bin/*; do
54 wrapProgram $file \
55 --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
56 done
57 '';
58 };
59
60 genPasswdScript = pkgs.writeScript "generate-password-file" ''
61 #!${pkgs.stdenv.shell}
62
63 set -euo pipefail
64
65 if (! test -d "${passwdDir}"); then
66 mkdir "${passwdDir}"
67 chmod 755 "${passwdDir}"
68 fi
69
70 for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
71 if [ ! -f "$f" ]; then
72 echo "Expected password hash file $f does not exist!"
73 exit 1
74 fi
75 done
76
77 cat <<EOF > ${passwdFile}
78 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
79 "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
80 + (if lib.isString value.quota
81 then "userdb_quota_rule=*:storage=${value.quota}"
82 else "")
83 ) cfg.loginAccounts)}
84 EOF
85
86 chmod 600 ${passwdFile}
87 '';
88
89 junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
90 junkMailboxNumber = builtins.length junkMailboxes;
91 # The assertion garantees there is exactly one Junk mailbox.
92 junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
93
94in
95{
96 config = with cfg; lib.mkIf enable {
97 assertions = [
98 {
99 assertion = junkMailboxNumber == 1;
100 message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
101 }
102 ];
103
104 services.dovecot2 = {
105 enable = true;
106 enableImap = enableImap || enableImapSsl;
107 enablePop3 = enablePop3 || enablePop3Ssl;
108 enablePAM = false;
109 enableQuota = true;
110 mailGroup = vmailGroupName;
111 mailUser = vmailUserName;
112 mailLocation = dovecotMaildir;
113 sslServerCert = certificatePath;
114 sslServerKey = keyPath;
115 enableLmtp = true;
116 modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
117 mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
118 protocols = lib.optional cfg.enableManageSieve "sieve";
119
120 sieveScripts = {
121 after = builtins.toFile "spam.sieve" ''
122 require "fileinto";
123
124 if header :is "X-Spam" "Yes" {
125 fileinto "${junkMailboxName}";
126 stop;
127 }
128 '';
129 };
130
131 mailboxes = cfg.mailboxes;
132
133 extraConfig = ''
134 #Extra Config
135 ${lib.optionalString debug ''
136 mail_debug = yes
137 auth_debug = yes
138 verbose_ssl = yes
139 ''}
140
141 ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
142 service imap-login {
143 inet_listener imap {
144 ${if cfg.enableImap then ''
145 port = 143
146 '' else ''
147 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
148 port = 0
149 ''}
150 }
151 inet_listener imaps {
152 ${if cfg.enableImapSsl then ''
153 port = 993
154 ssl = yes
155 '' else ''
156 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
157 port = 0
158 ''}
159 }
160 }
161 ''}
162 ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
163 service pop3-login {
164 inet_listener pop3 {
165 ${if cfg.enablePop3 then ''
166 port = 110
167 '' else ''
168 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
169 port = 0
170 ''}
171 }
172 inet_listener pop3s {
173 ${if cfg.enablePop3Ssl then ''
174 port = 995
175 ssl = yes
176 '' else ''
177 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
178 port = 0
179 ''}
180 }
181 }
182 ''}
183
184 protocol imap {
185 mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
186 mail_plugins = $mail_plugins imap_sieve
187 }
188
189 protocol pop3 {
190 mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
191 }
192
193 mail_access_groups = ${vmailGroupName}
194 ssl = required
195 ssl_min_protocol = TLSv1.2
196 ssl_prefer_server_ciphers = yes
197
198 service lmtp {
199 unix_listener dovecot-lmtp {
200 group = ${postfixCfg.group}
201 mode = 0600
202 user = ${postfixCfg.user}
203 }
204 }
205
206 recipient_delimiter = ${cfg.recipientDelimiter}
207 lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
208
209 protocol lmtp {
210 mail_plugins = $mail_plugins sieve
211 }
212
213 passdb {
214 driver = passwd-file
215 args = ${passwdFile}
216 }
217
218 userdb {
219 driver = passwd-file
220 args = ${passwdFile}
221 }
222
223 service auth {
224 unix_listener auth {
225 mode = 0660
226 user = ${postfixCfg.user}
227 group = ${postfixCfg.group}
228 }
229 }
230
231 auth_mechanisms = plain login
232
233 namespace inbox {
234 separator = ${cfg.hierarchySeparator}
235 inbox = yes
236 }
237
238 plugin {
239 sieve_plugins = sieve_imapsieve sieve_extprograms
240 sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
241 sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
242 sieve_default_name = default
243
244 # From elsewhere to Spam folder
245 imapsieve_mailbox1_name = ${junkMailboxName}
246 imapsieve_mailbox1_causes = COPY
247 imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
248
249 # From Spam folder to elsewhere
250 imapsieve_mailbox2_name = *
251 imapsieve_mailbox2_from = ${junkMailboxName}
252 imapsieve_mailbox2_causes = COPY
253 imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
254
255 sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
256
257 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
258 }
259
260 ${lib.optionalString cfg.fullTextSearch.enable ''
261 plugin {
262 plugin = fts fts_xapian
263 fts = xapian
264 fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
265
266 fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
267
268 ${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
269
270 fts_enforced = ${cfg.fullTextSearch.enforced}
271 }
272
273 ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
274 service indexer-worker {
275 vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
276 }
277 ''}
278 ''}
279
280 lda_mailbox_autosubscribe = yes
281 lda_mailbox_autocreate = yes
282 '';
283 };
284
285 systemd.services.dovecot2 = {
286 preStart = ''
287 ${genPasswdScript}
288 rm -rf '${stateDir}/imap_sieve'
289 mkdir '${stateDir}/imap_sieve'
290 cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
291 for k in "${stateDir}/imap_sieve"/*.sieve ; do
292 ${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
293 done
294 chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
295 '';
296 };
297
298 systemd.services.postfix.restartTriggers = [ genPasswdScript ];
299
300 systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
301 description = "Optimize dovecot indices for fts_xapian";
302 requisite = [ "dovecot2.service" ];
303 after = [ "dovecot2.service" ];
304 startAt = cfg.fullTextSearch.maintenance.onCalendar;
305 serviceConfig = {
306 Type = "oneshot";
307 ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
308 PrivateDevices = true;
309 PrivateNetwork = true;
310 ProtectKernelTunables = true;
311 ProtectKernelModules = true;
312 ProtectControlGroups = true;
313 ProtectHome = true;
314 ProtectSystem = true;
315 PrivateTmp = true;
316 };
317 };
318 systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
319 timerConfig = {
320 RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
321 };
322 };
323 };
324}