]> git.immae.eu Git - perso/Immae/Config/Nix.git/blob - modules/private/mail/postfix.nix
Postfix common aliases
[perso/Immae/Config/Nix.git] / modules / private / mail / postfix.nix
1 { lib, pkgs, config, nodes, ... }:
2 let all_domains = config.myEnv.mail.postfix.additional_mailbox_domains
3 ++ lib.remove null (lib.flatten (map
4 (zone: map
5 (e: if e.receive
6 then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}"
7 else null
8 )
9 (zone.withEmail or [])
10 )
11 config.myEnv.dns.masterZones
12 ));
13 in
14 {
15 config = lib.mkIf config.myServices.mail.enable {
16 myServices.chatonsProperties.hostings.mx-backup = {
17 file.datetime = "2022-08-22T01:00:00";
18 hosting = {
19 name = "MX Backup";
20 description = "Serveur e-mail secondaire";
21 logo = "https://www.postfix.org/favicon.ico";
22 website = "https://mail.immae.eu/";
23 status.level = "OK";
24 status.description = "OK";
25 registration.load = "OPEN";
26 install.type = "PACKAGE";
27 };
28 software = {
29 name = "Postfix";
30 website = "http://www.postfix.org/";
31 license.url = "http://postfix.mirrors.ovh.net/postfix-release/LICENSE";
32 license.name = "Eclipse Public license (EPL 2.0) and IBM Public License (IPL 1.0)";
33 version = pkgs.postfix.version;
34 source.url = "http://www.postfix.org/download.html";
35 };
36 };
37 secrets.keys = {
38 "postfix/mysql_alias_maps" = {
39 user = config.services.postfix.user;
40 group = config.services.postfix.group;
41 permissions = "0440";
42 text = ''
43 # We need to specify that option to trigger ssl connection
44 tls_ciphers = TLSv1.2
45 user = ${config.myEnv.mail.postfix.mysql.user}
46 password = ${config.myEnv.mail.postfix.mysql.password}
47 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
48 dbname = ${config.myEnv.mail.postfix.mysql.database}
49 query = SELECT DISTINCT destination
50 FROM forwardings
51 WHERE
52 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
53 AND active = 1
54 AND '%s' NOT IN
55 (
56 SELECT source
57 FROM forwardings_blacklisted
58 WHERE source = '%s'
59 ) UNION
60 SELECT 'devnull@immae.eu'
61 FROM forwardings_blacklisted
62 WHERE source = '%s'
63 '';
64 };
65 "postfix/ldap_mailboxes" = {
66 user = config.services.postfix.user;
67 group = config.services.postfix.group;
68 permissions = "0440";
69 text = ''
70 server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636
71 search_base = ${config.myEnv.mail.dovecot.ldap.base}
72 query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter}
73 bind_dn = ${config.myEnv.mail.dovecot.ldap.dn}
74 bind_pw = ${config.myEnv.mail.dovecot.ldap.password}
75 result_attribute = immaePostfixAddress
76 result_format = dummy
77 version = 3
78 '';
79 };
80 "postfix/mysql_sender_login_maps" = {
81 user = config.services.postfix.user;
82 group = config.services.postfix.group;
83 permissions = "0440";
84 text = ''
85 # We need to specify that option to trigger ssl connection
86 tls_ciphers = TLSv1.2
87 user = ${config.myEnv.mail.postfix.mysql.user}
88 password = ${config.myEnv.mail.postfix.mysql.password}
89 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
90 dbname = ${config.myEnv.mail.postfix.mysql.database}
91 query = SELECT DISTINCT destination
92 FROM forwardings
93 WHERE
94 (
95 (regex = 1 AND CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') REGEXP CONCAT('^',source,'$') )
96 OR
97 (regex = 0 AND source = CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d'))
98 )
99 AND active = 1
100 UNION SELECT CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AS destination
101 '';
102 };
103 "postfix/mysql_sender_relays_maps" = {
104 user = config.services.postfix.user;
105 group = config.services.postfix.group;
106 permissions = "0440";
107 text = ''
108 # We need to specify that option to trigger ssl connection
109 tls_ciphers = TLSv1.2
110 user = ${config.myEnv.mail.postfix.mysql.user}
111 password = ${config.myEnv.mail.postfix.mysql.password}
112 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
113 dbname = ${config.myEnv.mail.postfix.mysql.database}
114 # INSERT INTO sender_relays
115 # (`from`, owner, relay, login, password, regex, active)
116 # VALUES
117 # ( 'sender@otherhost.org'
118 # , 'me@mail.immae.eu'
119 # , '[otherhost.org]:587'
120 # , 'otherhostlogin'
121 # , AES_ENCRYPT('otherhostpassword', '${config.myEnv.mail.postfix.mysql.password_encrypt}')
122 # , '0'
123 # , '1');
124
125 query = SELECT DISTINCT `owner`
126 FROM sender_relays
127 WHERE
128 ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
129 AND active = 1
130 '';
131 };
132 "postfix/mysql_sender_relays_hosts" = {
133 user = config.services.postfix.user;
134 group = config.services.postfix.group;
135 permissions = "0440";
136 text = ''
137 # We need to specify that option to trigger ssl connection
138 tls_ciphers = TLSv1.2
139 user = ${config.myEnv.mail.postfix.mysql.user}
140 password = ${config.myEnv.mail.postfix.mysql.password}
141 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
142 dbname = ${config.myEnv.mail.postfix.mysql.database}
143
144 query = SELECT DISTINCT relay
145 FROM sender_relays
146 WHERE
147 ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
148 AND active = 1
149 '';
150 };
151 "postfix/mysql_sender_relays_creds" = {
152 user = config.services.postfix.user;
153 group = config.services.postfix.group;
154 permissions = "0440";
155 text = ''
156 # We need to specify that option to trigger ssl connection
157 tls_ciphers = TLSv1.2
158 user = ${config.myEnv.mail.postfix.mysql.user}
159 password = ${config.myEnv.mail.postfix.mysql.password}
160 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
161 dbname = ${config.myEnv.mail.postfix.mysql.database}
162
163 query = SELECT DISTINCT CONCAT(`login`, ':', AES_DECRYPT(`password`, '${config.myEnv.mail.postfix.mysql.password_encrypt}'))
164 FROM sender_relays
165 WHERE
166 ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
167 AND active = 1
168 '';
169 };
170 "postfix/ldap_ejabberd_users_immae_fr" = {
171 user = config.services.postfix.user;
172 group = config.services.postfix.group;
173 permissions = "0440";
174 text = ''
175 server_host = ldaps://${config.myEnv.jabber.ldap.host}:636
176 search_base = ${config.myEnv.jabber.ldap.base}
177 query_filter = ${config.myEnv.jabber.postfix_user_filter}
178 domain = immae.fr
179 bind_dn = ${config.myEnv.jabber.ldap.dn}
180 bind_pw = ${config.myEnv.jabber.ldap.password}
181 result_attribute = immaeXmppUid
182 result_format = ejabberd@localhost
183 version = 3
184 '';
185 };
186 } // lib.mapAttrs' (name: v: lib.nameValuePair "postfix/scripts/${name}-env" {
187 user = "postfixscripts";
188 group = "root";
189 permissions = "0400";
190 text = builtins.toJSON v.env;
191 }) config.myEnv.mail.scripts;
192
193 networking.firewall.allowedTCPPorts = [ 25 465 587 ];
194
195 users.users.postfixscripts = {
196 group = "keys";
197 uid = config.ids.uids.postfixscripts;
198 description = "Postfix scripts user";
199 };
200 users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
201 services.filesWatcher.postfix = {
202 restart = true;
203 paths = [
204 config.secrets.fullPaths."postfix/mysql_alias_maps"
205 config.secrets.fullPaths."postfix/ldap_mailboxes"
206 config.secrets.fullPaths."postfix/mysql_sender_login_maps"
207 config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"
208 ];
209 };
210 services.postfix = {
211 extraAliases = let
212 toScript = name: script: pkgs.writeScript name ''
213 #! ${pkgs.stdenv.shell}
214 mail=$(${pkgs.coreutils}/bin/cat -)
215 output=$(echo "$mail" | ${script} 2>&1)
216 ret=$?
217
218 if [ "$ret" != "0" ]; then
219 echo "$mail" \
220 | ${pkgs.procmail}/bin/formail -i "X-Return-Code: $ret" \
221 | /run/wrappers/bin/sendmail -i scripts_error+${name}@mail.immae.eu
222
223 messageId=$(echo "$mail" | ${pkgs.procmail}/bin/formail -x "Message-Id:")
224 repeat=$(echo "$mail" | ${pkgs.procmail}/bin/formail -X "From:" -X "Received:")
225
226 ${pkgs.coreutils}/bin/cat <<EOF | /run/wrappers/bin/sendmail -i scripts_error+${name}@mail.immae.eu
227 $repeat
228 To: scripts_error+${name}@mail.immae.eu
229 Subject: Log from script error
230 Content-Type: text/plain; charset="UTF-8"
231 Content-Transfer-Encoding: 8bit
232 References:$messageId
233 MIME-Version: 1.0
234 X-Return-Code: $ret
235
236 Error code: $ret
237 Output of message:
238 --------------
239 $output
240 --------------
241 EOF
242 fi
243 '';
244 scripts = lib.attrsets.mapAttrs (n: v:
245 toScript n (
246 (builtins.getFlake "git+${v.src.url}?rev=${v.src.rev}"
247 #(builtins.fetchGit { url = v.src.url; ref = "master"; rev = v.src.rev; })
248 ).outputs.envToScript.x86_64-linux
249 config.secrets.fullPaths."postfix/scripts/${n}-env"
250 )
251 ) config.myEnv.mail.scripts // {
252 testmail = pkgs.writeScript "testmail" ''
253 #! ${pkgs.stdenv.shell}
254 ${pkgs.coreutils}/bin/touch \
255 "/var/lib/naemon/checks/email/$(${pkgs.procmail}/bin/formail -x To: | ${pkgs.coreutils}/bin/tr -d ' <>')"
256 '';
257 };
258 in builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: ''${n}: "|${v}"'') scripts);
259 mapFiles = let
260 recipient_maps = let
261 name = n: i: "relay_${n}_${toString i}";
262 pair = n: i: m: lib.attrsets.nameValuePair (name n i) (
263 if m.type == "hash"
264 then pkgs.writeText (name n i) m.content
265 else null
266 );
267 pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
268 in lib.attrsets.filterAttrs (k: v: v != null) (
269 lib.attrsets.listToAttrs (lib.flatten (
270 lib.attrsets.mapAttrsToList pairs config.myEnv.mail.postfix.backup_domains
271 ))
272 );
273 relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) (
274 lib.attrsets.mapAttrs' (n: v:
275 lib.attrsets.nameValuePair "recipient_access_${n}" (
276 if lib.attrsets.hasAttr "relay_restrictions" v
277 then pkgs.writeText "recipient_access_${n}" v.relay_restrictions
278 else null
279 )
280 ) config.myEnv.mail.postfix.backup_domains
281 );
282 virtual_map = {
283 virtual = let
284 cfg = config.myEnv.monitoring.email_check.eldiron;
285 address = "${cfg.mail_address}@${cfg.mail_domain}";
286 aliases = config.myEnv.mail.postfix.common_aliases;
287 admins = builtins.concatStringsSep "," config.myEnv.mail.postfix.admins;
288 in pkgs.writeText "postfix-virtual" (
289 builtins.concatStringsSep "\n" (
290 [ "${address} testmail@localhost"
291 ] ++
292 map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases ++
293 lib.attrsets.mapAttrsToList (
294 n: v: lib.optionalString v.external ''
295 script_${n}@mail.immae.eu ${n}@localhost, scripts@mail.immae.eu
296 ''
297 ) config.myEnv.mail.scripts
298 ++ lib.lists.flatten (
299 map (domain:
300 map (alias: "${alias}@${domain} ${admins}") aliases
301 ) all_domains
302 )
303 ));
304 };
305 sasl_access = {
306 host_sender_login = with lib.attrsets; let
307 addresses = zipAttrs (lib.flatten (mapAttrsToList
308 (n: v: (map (e: { "${e}" = "${n}@immae.eu"; }) v.emails)) config.myEnv.servers));
309 aliases = config.myEnv.mail.postfix.common_aliases;
310 joined = builtins.concatStringsSep ",";
311 admins = joined config.myEnv.mail.postfix.admins;
312 in pkgs.writeText "host-sender-login"
313 (builtins.concatStringsSep "\n" (
314 mapAttrsToList (n: v: "${n} ${joined v}") addresses
315 ++ lib.lists.flatten (
316 map (domain:
317 map (alias: "${alias}@${domain} ${admins}") aliases
318 ) all_domains
319 )
320 ++ map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases
321 ));
322 };
323 in
324 recipient_maps // relay_restrictions // virtual_map // sasl_access;
325 config = {
326 ### postfix module overrides
327 readme_directory = "${pkgs.postfix}/share/postfix/doc";
328 smtp_tls_CAfile = lib.mkForce "";
329 smtp_tls_cert_file = lib.mkForce "";
330 smtp_tls_key_file = lib.mkForce "";
331
332 message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
333 mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts
334 alias_database = "\$alias_maps";
335
336 ### Aliases scripts user
337 default_privs = "postfixscripts";
338
339 ### Virtual mailboxes config
340 virtual_alias_maps = [
341 "hash:/etc/postfix/virtual"
342 "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"
343 "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}"
344 ];
345 virtual_mailbox_domains = all_domains;
346 virtual_mailbox_maps = [
347 "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}"
348 ];
349 dovecot_destination_recipient_limit = "1";
350 virtual_transport = "dovecot";
351
352 ### Relay domains
353 relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) config.myEnv.mail.postfix.backup_domains);
354 relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v:
355 lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps
356 ) config.myEnv.mail.postfix.backup_domains);
357 smtpd_relay_restrictions = [
358 "defer_unauth_destination"
359 ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v:
360 if lib.attrsets.hasAttr "relay_restrictions" v
361 then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ]
362 else []
363 ) config.myEnv.mail.postfix.backup_domains);
364
365 ### Additional smtpd configuration
366 smtpd_tls_received_header = "yes";
367 smtpd_tls_loglevel = "1";
368
369 ### Email sending configuration
370 smtp_tls_security_level = "may";
371 smtp_tls_loglevel = "1";
372
373 ### Force ip bind for smtp
374 smtp_bind_address = builtins.head config.hostEnv.ips.main.ip4;
375 smtp_bind_address6 = builtins.head config.hostEnv.ips.main.ip6;
376
377 # Use some relays when authorized senders are not myself
378 smtp_sasl_mechanism_filter = "plain,login"; # GSSAPI Not correctly supported by postfix
379 smtp_sasl_auth_enable = "yes";
380 smtp_sasl_password_maps =
381 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_creds"}";
382 smtp_sasl_security_options = "noanonymous";
383 smtp_sender_dependent_authentication = "yes";
384 sender_dependent_relayhost_maps =
385 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_hosts"}";
386
387 ### opendkim, opendmarc, openarc milters
388 non_smtpd_milters = [
389 "unix:${config.myServices.mail.milters.sockets.opendkim}"
390 ];
391 smtpd_milters = [
392 "unix:${config.myServices.mail.milters.sockets.opendkim}"
393 "unix:${config.myServices.mail.milters.sockets.openarc}"
394 "unix:${config.myServices.mail.milters.sockets.opendmarc}"
395 ];
396
397 smtp_use_tls = true;
398 smtpd_use_tls = true;
399 smtpd_tls_chain_files = builtins.concatStringsSep "," [ "/var/lib/acme/mail/full.pem" "/var/lib/acme/mail-rsa/full.pem" ];
400
401 maximal_queue_lifetime = "6w";
402 bounce_queue_lifetime = "6w";
403 };
404 enable = true;
405 enableSmtp = true;
406 enableSubmission = true;
407 submissionOptions = {
408 # Don’t use "long form", only commas (cf
409 # http://www.postfix.org/master.5.html long form is not handled
410 # well by the submission function)
411 smtpd_tls_security_level = "encrypt";
412 smtpd_sasl_auth_enable = "yes";
413 smtpd_tls_auth_only = "yes";
414 smtpd_sasl_tls_security_options = "noanonymous";
415 smtpd_sasl_type = "dovecot";
416 smtpd_sasl_path = "private/auth";
417 smtpd_reject_unlisted_recipient = "no";
418 smtpd_client_restrictions = "permit_sasl_authenticated,reject";
419 smtpd_relay_restrictions = "permit_sasl_authenticated,reject";
420 # Refuse to send e-mails with a From that is not handled
421 smtpd_sender_restrictions =
422 "reject_sender_login_mismatch,reject_unlisted_sender,permit_sasl_authenticated,reject";
423 smtpd_sender_login_maps = builtins.concatStringsSep "," [
424 "hash:/etc/postfix/host_sender_login"
425 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_maps"}"
426 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}"
427 ];
428 smtpd_recipient_restrictions = "permit_sasl_authenticated,reject";
429 milter_macro_daemon_name = "ORIGINATING";
430 smtpd_milters = builtins.concatStringsSep "," [
431 # FIXME: put it back when opensmtpd is upgraded and able to
432 # rewrite the from header
433 #"unix:/run/milter_verify_from/verify_from.sock"
434 "unix:${config.myServices.mail.milters.sockets.opendkim}"
435 ];
436 };
437 destination = ["localhost"];
438 # This needs to reverse DNS
439 hostname = config.hostEnv.fqdn;
440 setSendmail = true;
441 recipientDelimiter = "+";
442 masterConfig = {
443 submissions = {
444 type = "inet";
445 private = false;
446 command = "smtpd";
447 args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let
448 mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
449 in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions)
450 );
451 };
452 dovecot = {
453 type = "unix";
454 privileged = true;
455 chroot = false;
456 command = "pipe";
457 args = let
458 # rspamd could be used as a milter, but then it cannot apply
459 # its checks "per user" (milter is not yet dispatched to
460 # users), so we wrap dovecot-lda inside rspamc per recipient
461 # here.
462 rspamc_dovecot = pkgs.writeScriptBin "rspamc_dovecot" ''
463 #! ${pkgs.stdenv.shell}
464 set -o pipefail
465 sender="$1"
466 original_recipient="$2"
467 user="$3"
468
469 ${pkgs.coreutils}/bin/cat - | \
470 ${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d "$user" --mime | \
471 ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user"
472 if echo ''${PIPESTATUS[@]} | ${pkgs.gnugrep}/bin/grep -qE '^[0 ]+$'; then
473 exit 0
474 else
475 # src/global/sys_exits.h to retry
476 exit 75
477 fi
478 '';
479 in [
480 "flags=ODRhu" "user=vhost:vhost"
481 "argv=${rspamc_dovecot}/bin/rspamc_dovecot \${sender} \${original_recipient} \${user}@\${nexthop}"
482 ];
483 };
484 };
485 };
486 security.acme.certs."mail" = {
487 postRun = ''
488 systemctl restart postfix.service
489 '';
490 extraDomainNames = [ "smtp.immae.eu" ];
491 };
492 security.acme.certs."mail-rsa" = {
493 postRun = ''
494 systemctl restart postfix.service
495 '';
496 extraDomainNames = [ "smtp.immae.eu" ];
497 };
498 system.activationScripts.testmail = {
499 deps = [ "users" ];
500 text = let
501 allCfg = config.myEnv.monitoring.email_check;
502 cfg = allCfg.eldiron;
503 reverseTargets = builtins.attrNames (lib.attrsets.filterAttrs (k: v: builtins.elem "eldiron" v.targets) allCfg);
504 to_email = cfg': host':
505 let sep = if lib.strings.hasInfix "+" cfg'.mail_address then "_" else "+";
506 in "${cfg'.mail_address}${sep}${host'}@${cfg'.mail_domain}";
507 mails_to_receive = builtins.concatStringsSep " " (map (to_email cfg) reverseTargets);
508 in ''
509 install -m 0555 -o postfixscripts -g keys -d /var/lib/naemon/checks/email
510 for f in ${mails_to_receive}; do
511 if [ ! -f /var/lib/naemon/checks/email/$f ]; then
512 install -m 0644 -o postfixscripts -g keys /dev/null -T /var/lib/naemon/checks/email/$f
513 touch -m -d @0 /var/lib/naemon/checks/email/$f
514 fi
515 done
516 '';
517 };
518 systemd.services.postfix.serviceConfig.Slice = "mail.slice";
519 };
520 }