aboutsummaryrefslogtreecommitdiff
path: root/modules/private/mail
diff options
context:
space:
mode:
Diffstat (limited to 'modules/private/mail')
-rw-r--r--modules/private/mail/default.nix42
-rw-r--r--modules/private/mail/dovecot.nix292
-rwxr-xr-xmodules/private/mail/filter-rewrite-from.py68
-rw-r--r--modules/private/mail/milters.nix88
-rw-r--r--modules/private/mail/opensmtpd.nix57
-rw-r--r--modules/private/mail/postfix.nix471
-rw-r--r--modules/private/mail/relay.nix235
-rw-r--r--modules/private/mail/rspamd.nix87
-rwxr-xr-xmodules/private/mail/scan_reported_mails21
-rwxr-xr-xmodules/private/mail/sieve_bin/imapsieve_copy8
-rw-r--r--modules/private/mail/sieve_scripts/backup.sieve7
-rw-r--r--modules/private/mail/sieve_scripts/report_ham.sieve11
-rw-r--r--modules/private/mail/sieve_scripts/report_spam.sieve3
-rw-r--r--modules/private/mail/sympa.nix213
-rwxr-xr-xmodules/private/mail/verify_from.py60
15 files changed, 0 insertions, 1663 deletions
diff --git a/modules/private/mail/default.nix b/modules/private/mail/default.nix
deleted file mode 100644
index 2d405c6..0000000
--- a/modules/private/mail/default.nix
+++ /dev/null
@@ -1,42 +0,0 @@
1{ lib, pkgs, config, ... }:
2{
3 imports = [
4 ./milters.nix
5 ./postfix.nix
6 ./dovecot.nix
7 ./relay.nix
8 ./rspamd.nix
9 ./opensmtpd.nix
10 ./sympa.nix
11 ];
12 options.myServices.mail.enable = lib.mkEnableOption "enable Mail services";
13 options.myServices.mailRelay.enable = lib.mkEnableOption "enable Mail relay services";
14 options.myServices.mailBackup.enable = lib.mkEnableOption "enable MX backup services";
15
16 config = lib.mkIf config.myServices.mail.enable {
17 security.acme.certs."mail" = config.myServices.certificates.certConfig // {
18 domain = config.hostEnv.fqdn;
19 extraDomains = let
20 zonesWithMx = builtins.filter (zone:
21 lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0
22 ) config.myEnv.dns.masterZones;
23 mxs = map (zone: "${config.hostEnv.mx.subdomain}.${zone.name}") zonesWithMx;
24 in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs);
25 };
26 # This is for clients that don’t support elliptic curves (e.g.
27 # printer)
28 security.acme.certs."mail-rsa" = config.myServices.certificates.certConfig // {
29 domain = config.hostEnv.fqdn;
30 keyType = "rsa4096";
31 extraDomains = let
32 zonesWithMx = builtins.filter (zone:
33 lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0
34 ) config.myEnv.dns.masterZones;
35 mxs = map (zone: "${config.hostEnv.mx.subdomain}.${zone.name}") zonesWithMx;
36 in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs);
37 };
38 systemd.slices.mail = {
39 description = "Mail slice";
40 };
41 };
42}
diff --git a/modules/private/mail/dovecot.nix b/modules/private/mail/dovecot.nix
deleted file mode 100644
index b6fdc02..0000000
--- a/modules/private/mail/dovecot.nix
+++ /dev/null
@@ -1,292 +0,0 @@
1{ lib, pkgs, config, ... }:
2let
3 sieve_bin = pkgs.runCommand "sieve_bin" {
4 buildInputs = [ pkgs.makeWrapper ];
5 } ''
6 cp -a ${./sieve_bin} $out
7 chmod -R u+w $out
8 patchShebangs $out
9 for i in $out/*; do
10 wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils ]}
11 done
12 '';
13in
14{
15 config = lib.mkIf config.myServices.mail.enable {
16 systemd.services.dovecot2.serviceConfig.Slice = "mail.slice";
17 secrets.keys."dovecot/ldap" = {
18 user = config.services.dovecot2.user;
19 group = config.services.dovecot2.group;
20 permissions = "0400";
21 text = ''
22 hosts = ${config.myEnv.mail.dovecot.ldap.host}
23 tls = yes
24
25 dn = ${config.myEnv.mail.dovecot.ldap.dn}
26 dnpass = ${config.myEnv.mail.dovecot.ldap.password}
27
28 auth_bind = yes
29
30 ldap_version = 3
31
32 base = ${config.myEnv.mail.dovecot.ldap.base}
33 scope = subtree
34
35 pass_filter = ${config.myEnv.mail.dovecot.ldap.filter}
36 pass_attrs = ${config.myEnv.mail.dovecot.ldap.pass_attrs}
37
38 user_attrs = ${config.myEnv.mail.dovecot.ldap.user_attrs}
39 user_filter = ${config.myEnv.mail.dovecot.ldap.filter}
40 iterate_attrs = ${config.myEnv.mail.dovecot.ldap.iterate_attrs}
41 iterate_filter = ${config.myEnv.mail.dovecot.ldap.iterate_filter}
42 '';
43 };
44
45 users.users.vhost = {
46 group = "vhost";
47 uid = config.ids.uids.vhost;
48 };
49 users.groups.vhost.gid = config.ids.gids.vhost;
50
51 # https://blog.zeninc.net/index.php?post/2018/04/01/Un-annuaire-pour-les-gouverner-tous.......
52 services.dovecot2 = {
53 enable = true;
54 enablePAM = false;
55 enablePop3 = true;
56 enableImap = true;
57 enableLmtp = true;
58 protocols = [ "sieve" ];
59 modules = [
60 pkgs.dovecot_pigeonhole
61 pkgs.dovecot_fts-xapian
62 ];
63 mailUser = "vhost";
64 mailGroup = "vhost";
65 createMailUser = false;
66 mailboxes = {
67 Trash = { auto = "subscribe"; specialUse = "Trash"; };
68 Junk = { auto = "subscribe"; specialUse = "Junk"; };
69 Sent = { auto = "subscribe"; specialUse = "Sent"; };
70 Drafts = { auto = "subscribe"; specialUse = "Drafts"; };
71 };
72 mailLocation = "mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap";
73 sslServerCert = "/var/lib/acme/mail/fullchain.pem";
74 sslServerKey = "/var/lib/acme/mail/key.pem";
75 sslCACert = "/var/lib/acme/mail/fullchain.pem";
76 extraConfig = builtins.concatStringsSep "\n" [
77 # For printer which doesn’t support elliptic curve
78 ''
79 ssl_alt_cert = </var/lib/acme/mail-rsa/fullchain.pem
80 ssl_alt_key = </var/lib/acme/mail-rsa/key.pem
81 ''
82
83 ''
84 postmaster_address = postmaster@immae.eu
85 mail_attribute_dict = file:%h/dovecot-attributes
86 imap_idle_notify_interval = 20 mins
87 namespace inbox {
88 type = private
89 separator = /
90 inbox = yes
91 list = yes
92 }
93 ''
94
95 # ACL
96 ''
97 mail_plugins = $mail_plugins acl
98 plugin {
99 acl = vfile:${pkgs.writeText "dovecot-acl" ''
100 Backup/* owner lrp
101 ''}
102 acl_globals_only = yes
103 }
104 ''
105
106 # Full text search
107 ''
108 # needs to be bigger than any mailbox size
109 default_vsz_limit = 2GB
110 mail_plugins = $mail_plugins fts fts_xapian
111 plugin {
112 plugin = fts fts_xapian
113 fts = xapian
114 fts_xapian = partial=2 full=20
115 fts_autoindex = yes
116 fts_autoindex_exclude = \Junk
117 fts_autoindex_exclude2 = \Trash
118 fts_autoindex_exclude3 = Virtual/*
119 }
120 ''
121
122 # Antispam
123 # https://docs.iredmail.org/dovecot.imapsieve.html
124 ''
125 # imap_sieve plugin added below
126
127 plugin {
128 sieve_plugins = sieve_imapsieve sieve_extprograms
129 imapsieve_url = sieve://127.0.0.1:4190
130
131 sieve_before = file:${./sieve_scripts}/backup.sieve;bindir=/var/lib/vhost/.sieve_bin
132
133 # From elsewhere to Junk folder
134 imapsieve_mailbox1_name = Junk
135 imapsieve_mailbox1_causes = COPY APPEND
136 imapsieve_mailbox1_before = file:${./sieve_scripts}/report_spam.sieve;bindir=/var/lib/vhost/.imapsieve_bin
137
138 # From Junk folder to elsewhere
139 imapsieve_mailbox2_name = *
140 imapsieve_mailbox2_from = Junk
141 imapsieve_mailbox2_causes = COPY
142 imapsieve_mailbox2_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin
143
144 # From anywhere to NoJunk folder
145 imapsieve_mailbox3_name = NoJunk
146 imapsieve_mailbox3_causes = COPY APPEND
147 imapsieve_mailbox3_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin
148
149 sieve_pipe_bin_dir = ${sieve_bin}
150
151 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
152 }
153 ''
154 # Services to listen
155 ''
156 service imap-login {
157 inet_listener imap {
158 }
159 inet_listener imaps {
160 }
161 }
162 service pop3-login {
163 inet_listener pop3 {
164 }
165 inet_listener pop3s {
166 }
167 }
168 service imap {
169 }
170 service pop3 {
171 }
172 service auth {
173 unix_listener auth-userdb {
174 }
175 unix_listener ${config.services.postfix.config.queue_directory}/private/auth {
176 mode = 0666
177 }
178 }
179 service auth-worker {
180 }
181 service dict {
182 unix_listener dict {
183 }
184 }
185 service stats {
186 unix_listener stats-reader {
187 user = vhost
188 group = vhost
189 mode = 0660
190 }
191 unix_listener stats-writer {
192 user = vhost
193 group = vhost
194 mode = 0660
195 }
196 }
197 ''
198
199 # Authentification
200 ''
201 first_valid_uid = ${toString config.ids.uids.vhost}
202 disable_plaintext_auth = yes
203 passdb {
204 driver = ldap
205 args = ${config.secrets.fullPaths."dovecot/ldap"}
206 }
207 userdb {
208 driver = ldap
209 args = ${config.secrets.fullPaths."dovecot/ldap"}
210 }
211 ''
212
213 # Zlib
214 ''
215 mail_plugins = $mail_plugins zlib
216 plugin {
217 zlib_save_level = 6
218 zlib_save = gz
219 }
220 ''
221
222 # Sieve
223 ''
224 plugin {
225 sieve = file:~/sieve;bindir=~/.sieve-bin;active=~/.dovecot.sieve
226 }
227 service managesieve-login {
228 }
229 service managesieve {
230 }
231 ''
232
233 # Virtual mailboxes
234 ''
235 mail_plugins = $mail_plugins virtual
236 namespace Virtual {
237 prefix = Virtual/
238 location = virtual:~/Virtual
239 }
240 ''
241
242 # Protocol specific configuration
243 # Needs to come last if there are mail_plugins entries
244 ''
245 protocol imap {
246 mail_plugins = $mail_plugins imap_sieve imap_acl
247 }
248 protocol lda {
249 mail_plugins = $mail_plugins sieve
250 }
251 ''
252 ];
253 };
254 networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ];
255 system.activationScripts.dovecot = {
256 deps = [ "users" ];
257 text =''
258 install -m 0755 -o vhost -g vhost -d /var/lib/vhost
259 '';
260 };
261
262 services.cron.systemCronJobs = let
263 cron_script = pkgs.writeScriptBin "cleanup-imap-folders" ''
264 ${pkgs.dovecot}/bin/doveadm expunge -A MAILBOX "Backup/*" NOT FLAGGED BEFORE 8w 2>&1 > /dev/null | grep -v "Mailbox doesn't exist:" | grep -v "Info: Opening DB"
265 ${pkgs.dovecot}/bin/doveadm expunge -A MAILBOX Junk SEEN NOT FLAGGED BEFORE 4w 2>&1 > /dev/null | grep -v "Mailbox doesn't exist:" | grep -v "Info: Opening DB"
266 ${pkgs.dovecot}/bin/doveadm expunge -A MAILBOX Trash NOT FLAGGED BEFORE 4w 2>&1 > /dev/null | grep -v "Mailbox doesn't exist:" | grep -v "Info: Opening DB"
267 '';
268 in
269 [
270 "0 2 * * * root ${cron_script}/bin/cleanup-imap-folders"
271 ];
272 security.acme.certs."mail-rsa" = {
273 postRun = ''
274 systemctl restart dovecot2.service
275 '';
276 extraDomains = {
277 "imap.immae.eu" = null;
278 "pop3.immae.eu" = null;
279 };
280 };
281 security.acme.certs."mail" = {
282 postRun = ''
283 systemctl restart dovecot2.service
284 '';
285 extraDomains = {
286 "imap.immae.eu" = null;
287 "pop3.immae.eu" = null;
288 };
289 };
290 };
291}
292
diff --git a/modules/private/mail/filter-rewrite-from.py b/modules/private/mail/filter-rewrite-from.py
deleted file mode 100755
index aad9c69..0000000
--- a/modules/private/mail/filter-rewrite-from.py
+++ /dev/null
@@ -1,68 +0,0 @@
1#! /usr/bin/env python3
2import sys
3
4sys.stdin.reconfigure(encoding='utf-8')
5sys.stdout.reconfigure(encoding='utf-8')
6stdin = sys.stdin
7stdout = sys.stdout
8
9mailaddr = sys.argv[1]
10inheader = {}
11
12# Change to actual file for logging
13logfile = open("/dev/null", "a")
14
15def log(l, i):
16 logfile.write("{} {}\n".format(i, l))
17 logfile.flush()
18
19def send(l):
20 log(l, ">")
21 stdout.write("{}\n".format(l))
22 stdout.flush()
23
24def token_and_sid(version, sid, token):
25 if version < "0.5":
26 return "{}|{}".format(token, sid)
27 else:
28 return "{}|{}".format(sid, token)
29
30log("started", "l")
31while True:
32 line = stdin.readline().strip()
33 log(line, "<")
34 if not line:
35 log("finished", "l")
36 break
37 splitted = line.split("|")
38 if line == "config|ready":
39 log("in config ready", "l")
40 send("register|filter|smtp-in|mail-from")
41 send("register|filter|smtp-in|data-line")
42 send("register|ready")
43 if splitted[0] != "filter":
44 continue
45 if len(splitted) < 7:
46 send("invalid filter command: expected >6 fields!")
47 sys.exit(1)
48 version = splitted[1]
49 action = splitted[4]
50 sid = splitted[5]
51 token = splitted[6]
52 token_sid = token_and_sid(version, sid, token)
53 rest = "|".join(splitted[7:])
54 if action == "mail-from":
55 inheader[sid] = True
56 send("filter-result|{}|rewrite|<{}>".format(token_sid, mailaddr))
57 continue
58 if action == "data-line":
59 if rest == "" and inheader.get(sid, False):
60 inheader[sid] = False
61 if rest == "." and not inheader.get(sid):
62 del(inheader[sid])
63 if inheader.get(sid, False) and rest.upper().startswith("FROM:"):
64 send("filter-dataline|{}|From: {}".format(token_sid, mailaddr))
65 else:
66 send("filter-dataline|{}|{}".format(token_sid, rest))
67 continue
68 send("filter-result|{}|proceed".format(token_sid))
diff --git a/modules/private/mail/milters.nix b/modules/private/mail/milters.nix
deleted file mode 100644
index 4b93a7a..0000000
--- a/modules/private/mail/milters.nix
+++ /dev/null
@@ -1,88 +0,0 @@
1{ lib, pkgs, config, name, ... }:
2{
3 imports =
4 builtins.attrValues (import ../../../lib/flake-compat.nix ../../../flakes/private/openarc).nixosModules
5 ++ builtins.attrValues (import ../../../lib/flake-compat.nix ../../../flakes/private/opendmarc).nixosModules;
6
7 options.myServices.mail.milters.sockets = lib.mkOption {
8 type = lib.types.attrsOf lib.types.path;
9 default = {
10 opendkim = "/run/opendkim/opendkim.sock";
11 opendmarc = config.services.opendmarc.socket;
12 openarc = config.services.openarc.socket;
13 };
14 readOnly = true;
15 description = ''
16 milters sockets
17 '';
18 };
19 config = lib.mkIf (config.myServices.mail.enable || config.myServices.mailBackup.enable) {
20 secrets.keys = {
21 "opendkim" = {
22 isDir = true;
23 user = config.services.opendkim.user;
24 group = config.services.opendkim.group;
25 permissions = "0550";
26 };
27 "opendkim/eldiron.private" = {
28 user = config.services.opendkim.user;
29 group = config.services.opendkim.group;
30 permissions = "0400";
31 text = config.myEnv.mail.dkim.eldiron.private;
32 };
33 "opendkim/eldiron.txt" = {
34 user = config.services.opendkim.user;
35 group = config.services.opendkim.group;
36 permissions = "0444";
37 text = ''
38 eldiron._domainkey IN TXT ${config.myEnv.mail.dkim.eldiron.public}'';
39 };
40 };
41 users.users."${config.services.opendkim.user}".extraGroups = [ "keys" ];
42 services.opendkim = {
43 enable = true;
44 socket = "local:${config.myServices.mail.milters.sockets.opendkim}";
45 domains = builtins.concatStringsSep "," (lib.flatten (map
46 (zone: map
47 (e: "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}")
48 (zone.withEmail or [])
49 )
50 config.myEnv.dns.masterZones
51 ));
52 keyPath = config.secrets.fullPaths."opendkim";
53 selector = "eldiron";
54 configFile = pkgs.writeText "opendkim.conf" ''
55 SubDomains yes
56 UMask 002
57 AlwaysAddARHeader yes
58 '';
59 group = config.services.postfix.group;
60 };
61 systemd.services.opendkim.serviceConfig.Slice = "mail.slice";
62 systemd.services.opendkim.preStart = lib.mkBefore ''
63 # Skip the prestart script as keys are handled in secrets
64 exit 0
65 '';
66 services.filesWatcher.opendkim = {
67 restart = true;
68 paths = [
69 config.secrets.fullPaths."opendkim/eldiron.private"
70 ];
71 };
72
73 systemd.services.milter_verify_from = {
74 description = "Verify from milter";
75 after = [ "network.target" ];
76 wantedBy = [ "multi-user.target" ];
77
78 serviceConfig = {
79 Slice = "mail.slice";
80 User = "postfix";
81 Group = "postfix";
82 ExecStart = let python = pkgs.python3.withPackages (p: [ p.pymilter ]);
83 in "${python}/bin/python ${./verify_from.py} -s /run/milter_verify_from/verify_from.sock";
84 RuntimeDirectory = "milter_verify_from";
85 };
86 };
87 };
88}
diff --git a/modules/private/mail/opensmtpd.nix b/modules/private/mail/opensmtpd.nix
deleted file mode 100644
index e05bba9..0000000
--- a/modules/private/mail/opensmtpd.nix
+++ /dev/null
@@ -1,57 +0,0 @@
1{ lib, pkgs, config, name, ... }:
2{
3 config = lib.mkIf config.myServices.mailRelay.enable {
4 secrets.keys."opensmtpd/creds" = {
5 user = "smtpd";
6 group = "smtpd";
7 permissions = "0400";
8 text = ''
9 eldiron ${name}:${config.hostEnv.ldap.password}
10 '';
11 };
12 users.users.smtpd.extraGroups = [ "keys" ];
13 services.opensmtpd = {
14 enable = true;
15 serverConfiguration = let
16 filter-rewrite-from = pkgs.runCommand "filter-rewrite-from.py" {
17 buildInputs = [ pkgs.python3 ];
18 } ''
19 cp ${./filter-rewrite-from.py} $out
20 patchShebangs $out
21 '';
22 in ''
23 table creds \
24 "${config.secrets.fullPaths."opensmtpd/creds"}"
25 # FIXME: filtering requires 6.6, uncomment following lines when
26 # upgrading
27 # filter "fixfrom" \
28 # proc-exec "${filter-rewrite-from} ${name}@immae.eu"
29 # listen on socket filter "fixfrom"
30 action "relay-rewrite-from" relay \
31 helo ${config.hostEnv.fqdn} \
32 host smtp+tls://eldiron@eldiron.immae.eu:587 \
33 auth <creds> \
34 mail-from ${name}@immae.eu
35 action "relay" relay \
36 helo ${config.hostEnv.fqdn} \
37 host smtp+tls://eldiron@eldiron.immae.eu:587 \
38 auth <creds>
39 match for any !mail-from "@immae.eu" action "relay-rewrite-from"
40 match for any mail-from "@immae.eu" action "relay"
41 '';
42 };
43 environment.systemPackages = [ config.services.opensmtpd.package ];
44 services.mail.sendmailSetuidWrapper = {
45 program = "sendmail";
46 source = "${config.services.opensmtpd.package}/bin/smtpctl";
47 setuid = false;
48 setgid = false;
49 };
50 security.wrappers.mailq = {
51 program = "mailq";
52 source = "${config.services.opensmtpd.package}/bin/smtpctl";
53 setuid = false;
54 setgid = false;
55 };
56 };
57}
diff --git a/modules/private/mail/postfix.nix b/modules/private/mail/postfix.nix
deleted file mode 100644
index ae98a8a..0000000
--- a/modules/private/mail/postfix.nix
+++ /dev/null
@@ -1,471 +0,0 @@
1{ lib, pkgs, config, nodes, ... }:
2{
3 config = lib.mkIf config.myServices.mail.enable {
4 secrets.keys = {
5 "postfix/mysql_alias_maps" = {
6 user = config.services.postfix.user;
7 group = config.services.postfix.group;
8 permissions = "0440";
9 text = ''
10 # We need to specify that option to trigger ssl connection
11 tls_ciphers = TLSv1.2
12 user = ${config.myEnv.mail.postfix.mysql.user}
13 password = ${config.myEnv.mail.postfix.mysql.password}
14 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
15 dbname = ${config.myEnv.mail.postfix.mysql.database}
16 query = SELECT DISTINCT destination
17 FROM forwardings
18 WHERE
19 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
20 AND active = 1
21 AND '%s' NOT IN
22 (
23 SELECT source
24 FROM forwardings_blacklisted
25 WHERE source = '%s'
26 ) UNION
27 SELECT 'devnull@immae.eu'
28 FROM forwardings_blacklisted
29 WHERE source = '%s'
30 '';
31 };
32 "postfix/ldap_mailboxes" = {
33 user = config.services.postfix.user;
34 group = config.services.postfix.group;
35 permissions = "0440";
36 text = ''
37 server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636
38 search_base = ${config.myEnv.mail.dovecot.ldap.base}
39 query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter}
40 bind_dn = ${config.myEnv.mail.dovecot.ldap.dn}
41 bind_pw = ${config.myEnv.mail.dovecot.ldap.password}
42 result_attribute = immaePostfixAddress
43 result_format = dummy
44 version = 3
45 '';
46 };
47 "postfix/mysql_sender_login_maps" = {
48 user = config.services.postfix.user;
49 group = config.services.postfix.group;
50 permissions = "0440";
51 text = ''
52 # We need to specify that option to trigger ssl connection
53 tls_ciphers = TLSv1.2
54 user = ${config.myEnv.mail.postfix.mysql.user}
55 password = ${config.myEnv.mail.postfix.mysql.password}
56 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
57 dbname = ${config.myEnv.mail.postfix.mysql.database}
58 query = SELECT DISTINCT destination
59 FROM forwardings
60 WHERE
61 (
62 (regex = 1 AND CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') REGEXP CONCAT('^',source,'$') )
63 OR
64 (regex = 0 AND source = CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d'))
65 )
66 AND active = 1
67 UNION SELECT CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AS destination
68 '';
69 };
70 "postfix/mysql_sender_relays_maps" = {
71 user = config.services.postfix.user;
72 group = config.services.postfix.group;
73 permissions = "0440";
74 text = ''
75 # We need to specify that option to trigger ssl connection
76 tls_ciphers = TLSv1.2
77 user = ${config.myEnv.mail.postfix.mysql.user}
78 password = ${config.myEnv.mail.postfix.mysql.password}
79 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
80 dbname = ${config.myEnv.mail.postfix.mysql.database}
81 # INSERT INTO sender_relays
82 # (`from`, owner, relay, login, password, regex, active)
83 # VALUES
84 # ( 'sender@otherhost.org'
85 # , 'me@mail.immae.eu'
86 # , '[otherhost.org]:587'
87 # , 'otherhostlogin'
88 # , AES_ENCRYPT('otherhostpassword', '${config.myEnv.mail.postfix.mysql.password_encrypt}')
89 # , '0'
90 # , '1');
91
92 query = SELECT DISTINCT `owner`
93 FROM sender_relays
94 WHERE
95 ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
96 AND active = 1
97 '';
98 };
99 "postfix/mysql_sender_relays_hosts" = {
100 user = config.services.postfix.user;
101 group = config.services.postfix.group;
102 permissions = "0440";
103 text = ''
104 # We need to specify that option to trigger ssl connection
105 tls_ciphers = TLSv1.2
106 user = ${config.myEnv.mail.postfix.mysql.user}
107 password = ${config.myEnv.mail.postfix.mysql.password}
108 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
109 dbname = ${config.myEnv.mail.postfix.mysql.database}
110
111 query = SELECT DISTINCT relay
112 FROM sender_relays
113 WHERE
114 ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
115 AND active = 1
116 '';
117 };
118 "postfix/mysql_sender_relays_creds" = {
119 user = config.services.postfix.user;
120 group = config.services.postfix.group;
121 permissions = "0440";
122 text = ''
123 # We need to specify that option to trigger ssl connection
124 tls_ciphers = TLSv1.2
125 user = ${config.myEnv.mail.postfix.mysql.user}
126 password = ${config.myEnv.mail.postfix.mysql.password}
127 hosts = unix:${config.myEnv.mail.postfix.mysql.socket}
128 dbname = ${config.myEnv.mail.postfix.mysql.database}
129
130 query = SELECT DISTINCT CONCAT(`login`, ':', AES_DECRYPT(`password`, '${config.myEnv.mail.postfix.mysql.password_encrypt}'))
131 FROM sender_relays
132 WHERE
133 ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s'))
134 AND active = 1
135 '';
136 };
137 "postfix/ldap_ejabberd_users_immae_fr" = {
138 user = config.services.postfix.user;
139 group = config.services.postfix.group;
140 permissions = "0440";
141 text = ''
142 server_host = ldaps://${config.myEnv.jabber.ldap.host}:636
143 search_base = ${config.myEnv.jabber.ldap.base}
144 query_filter = ${config.myEnv.jabber.postfix_user_filter}
145 domain = immae.fr
146 bind_dn = ${config.myEnv.jabber.ldap.dn}
147 bind_pw = ${config.myEnv.jabber.ldap.password}
148 result_attribute = immaeXmppUid
149 result_format = ejabberd@localhost
150 version = 3
151 '';
152 };
153 } // lib.mapAttrs' (name: v: lib.nameValuePair "postfix/scripts/${name}-env" {
154 user = "postfixscripts";
155 group = "root";
156 permissions = "0400";
157 text = builtins.toJSON v.env;
158 }) config.myEnv.mail.scripts;
159
160 networking.firewall.allowedTCPPorts = [ 25 465 587 ];
161
162 users.users.postfixscripts = {
163 group = "keys";
164 uid = config.ids.uids.postfixscripts;
165 description = "Postfix scripts user";
166 };
167 users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
168 services.filesWatcher.postfix = {
169 restart = true;
170 paths = [
171 config.secrets.fullPaths."postfix/mysql_alias_maps"
172 config.secrets.fullPaths."postfix/ldap_mailboxes"
173 config.secrets.fullPaths."postfix/mysql_sender_login_maps"
174 config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"
175 ];
176 };
177 services.postfix = {
178 extraAliases = let
179 toScript = name: script: pkgs.writeScript name ''
180 #! ${pkgs.stdenv.shell}
181 mail=$(${pkgs.coreutils}/bin/cat -)
182 output=$(echo "$mail" | ${script} 2>&1)
183 ret=$?
184
185 if [ "$ret" != "0" ]; then
186 echo "$mail" \
187 | ${pkgs.procmail}/bin/formail -i "X-Return-Code: $ret" \
188 | /run/wrappers/bin/sendmail -i scripts_error+${name}@mail.immae.eu
189
190 messageId=$(echo "$mail" | ${pkgs.procmail}/bin/formail -x "Message-Id:")
191 repeat=$(echo "$mail" | ${pkgs.procmail}/bin/formail -X "From:" -X "Received:")
192
193 ${pkgs.coreutils}/bin/cat <<EOF | /run/wrappers/bin/sendmail -i scripts_error+${name}@mail.immae.eu
194 $repeat
195 To: scripts_error+${name}@mail.immae.eu
196 Subject: Log from script error
197 Content-Type: text/plain; charset="UTF-8"
198 Content-Transfer-Encoding: 8bit
199 References:$messageId
200 MIME-Version: 1.0
201 X-Return-Code: $ret
202
203 Error code: $ret
204 Output of message:
205 --------------
206 $output
207 --------------
208 EOF
209 fi
210 '';
211 scripts = lib.attrsets.mapAttrs (n: v:
212 toScript n (pkgs.callPackage (builtins.fetchGit { url = v.src.url; ref = "master"; rev = v.src.rev; }) { scriptEnv = config.secrets.fullPaths."postfix/scripts/${n}-env"; })
213 ) config.myEnv.mail.scripts // {
214 testmail = pkgs.writeScript "testmail" ''
215 #! ${pkgs.stdenv.shell}
216 ${pkgs.coreutils}/bin/touch \
217 "/var/lib/naemon/checks/email/$(${pkgs.procmail}/bin/formail -x To: | ${pkgs.coreutils}/bin/tr -d ' <>')"
218 '';
219 };
220 in builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: ''${n}: "|${v}"'') scripts);
221 mapFiles = let
222 recipient_maps = let
223 name = n: i: "relay_${n}_${toString i}";
224 pair = n: i: m: lib.attrsets.nameValuePair (name n i) (
225 if m.type == "hash"
226 then pkgs.writeText (name n i) m.content
227 else null
228 );
229 pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
230 in lib.attrsets.filterAttrs (k: v: v != null) (
231 lib.attrsets.listToAttrs (lib.flatten (
232 lib.attrsets.mapAttrsToList pairs config.myEnv.mail.postfix.backup_domains
233 ))
234 );
235 relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) (
236 lib.attrsets.mapAttrs' (n: v:
237 lib.attrsets.nameValuePair "recipient_access_${n}" (
238 if lib.attrsets.hasAttr "relay_restrictions" v
239 then pkgs.writeText "recipient_access_${n}" v.relay_restrictions
240 else null
241 )
242 ) config.myEnv.mail.postfix.backup_domains
243 );
244 virtual_map = {
245 virtual = let
246 cfg = config.myEnv.monitoring.email_check.eldiron;
247 address = "${cfg.mail_address}@${cfg.mail_domain}";
248 in pkgs.writeText "postfix-virtual" (
249 builtins.concatStringsSep "\n" (
250 ["${address} testmail@localhost"] ++
251 lib.attrsets.mapAttrsToList (
252 n: v: lib.optionalString v.external ''
253 script_${n}@mail.immae.eu ${n}@localhost, scripts@mail.immae.eu
254 ''
255 ) config.myEnv.mail.scripts
256 )
257 );
258 };
259 sasl_access = {
260 host_sender_login = with lib.attrsets; let
261 addresses = zipAttrs (lib.flatten (mapAttrsToList
262 (n: v: (map (e: { "${e}" = "${n}@immae.eu"; }) v.emails)) config.myEnv.servers));
263 joined = builtins.concatStringsSep ",";
264 in pkgs.writeText "host-sender-login"
265 (builtins.concatStringsSep "\n" (mapAttrsToList (n: v: "${n} ${joined v}") addresses));
266 };
267 in
268 recipient_maps // relay_restrictions // virtual_map // sasl_access;
269 config = {
270 ### postfix module overrides
271 readme_directory = "${pkgs.postfix}/share/postfix/doc";
272 smtp_tls_CAfile = lib.mkForce "";
273 smtp_tls_cert_file = lib.mkForce "";
274 smtp_tls_key_file = lib.mkForce "";
275
276 message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
277 mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts
278 alias_database = "\$alias_maps";
279
280 ### Aliases scripts user
281 default_privs = "postfixscripts";
282
283 ### Virtual mailboxes config
284 virtual_alias_maps = [
285 "hash:/etc/postfix/virtual"
286 "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"
287 "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}"
288 ];
289 virtual_mailbox_domains = config.myEnv.mail.postfix.additional_mailbox_domains
290 ++ lib.remove null (lib.flatten (map
291 (zone: map
292 (e: if e.receive
293 then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}"
294 else null
295 )
296 (zone.withEmail or [])
297 )
298 config.myEnv.dns.masterZones
299 ));
300 virtual_mailbox_maps = [
301 "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}"
302 ];
303 dovecot_destination_recipient_limit = "1";
304 virtual_transport = "dovecot";
305
306 ### Relay domains
307 relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) config.myEnv.mail.postfix.backup_domains);
308 relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v:
309 lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps
310 ) config.myEnv.mail.postfix.backup_domains);
311 smtpd_relay_restrictions = [
312 "defer_unauth_destination"
313 ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v:
314 if lib.attrsets.hasAttr "relay_restrictions" v
315 then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ]
316 else []
317 ) config.myEnv.mail.postfix.backup_domains);
318
319 ### Additional smtpd configuration
320 smtpd_tls_received_header = "yes";
321 smtpd_tls_loglevel = "1";
322
323 ### Email sending configuration
324 smtp_tls_security_level = "may";
325 smtp_tls_loglevel = "1";
326
327 ### Force ip bind for smtp
328 smtp_bind_address = config.hostEnv.ips.main.ip4;
329 smtp_bind_address6 = builtins.head config.hostEnv.ips.main.ip6;
330
331 # Use some relays when authorized senders are not myself
332 smtp_sasl_mechanism_filter = "plain,login"; # GSSAPI Not correctly supported by postfix
333 smtp_sasl_auth_enable = "yes";
334 smtp_sasl_password_maps =
335 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_creds"}";
336 smtp_sasl_security_options = "noanonymous";
337 smtp_sender_dependent_authentication = "yes";
338 sender_dependent_relayhost_maps =
339 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_hosts"}";
340
341 ### opendkim, opendmarc, openarc milters
342 non_smtpd_milters = [
343 "unix:${config.myServices.mail.milters.sockets.opendkim}"
344 ];
345 smtpd_milters = [
346 "unix:${config.myServices.mail.milters.sockets.opendkim}"
347 "unix:${config.myServices.mail.milters.sockets.openarc}"
348 "unix:${config.myServices.mail.milters.sockets.opendmarc}"
349 ];
350
351 smtp_use_tls = true;
352 smtpd_use_tls = true;
353 smtpd_tls_chain_files = builtins.concatStringsSep "," [ "/var/lib/acme/mail/full.pem" "/var/lib/acme/mail-rsa/full.pem" ];
354
355 maximal_queue_lifetime = "6w";
356 bounce_queue_lifetime = "6w";
357 };
358 enable = true;
359 enableSmtp = true;
360 enableSubmission = true;
361 submissionOptions = {
362 # Don’t use "long form", only commas (cf
363 # http://www.postfix.org/master.5.html long form is not handled
364 # well by the submission function)
365 smtpd_tls_security_level = "encrypt";
366 smtpd_sasl_auth_enable = "yes";
367 smtpd_tls_auth_only = "yes";
368 smtpd_sasl_tls_security_options = "noanonymous";
369 smtpd_sasl_type = "dovecot";
370 smtpd_sasl_path = "private/auth";
371 smtpd_reject_unlisted_recipient = "no";
372 smtpd_client_restrictions = "permit_sasl_authenticated,reject";
373 smtpd_relay_restrictions = "permit_sasl_authenticated,reject";
374 # Refuse to send e-mails with a From that is not handled
375 smtpd_sender_restrictions =
376 "reject_sender_login_mismatch,reject_unlisted_sender,permit_sasl_authenticated,reject";
377 smtpd_sender_login_maps = builtins.concatStringsSep "," [
378 "hash:/etc/postfix/host_sender_login"
379 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_maps"}"
380 "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}"
381 ];
382 smtpd_recipient_restrictions = "permit_sasl_authenticated,reject";
383 milter_macro_daemon_name = "ORIGINATING";
384 smtpd_milters = builtins.concatStringsSep "," [
385 # FIXME: put it back when opensmtpd is upgraded and able to
386 # rewrite the from header
387 #"unix:/run/milter_verify_from/verify_from.sock"
388 "unix:${config.myServices.mail.milters.sockets.opendkim}"
389 ];
390 };
391 destination = ["localhost"];
392 # This needs to reverse DNS
393 hostname = config.hostEnv.fqdn;
394 setSendmail = true;
395 recipientDelimiter = "+";
396 masterConfig = {
397 submissions = {
398 type = "inet";
399 private = false;
400 command = "smtpd";
401 args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let
402 mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
403 in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions)
404 );
405 };
406 dovecot = {
407 type = "unix";
408 privileged = true;
409 chroot = false;
410 command = "pipe";
411 args = let
412 # rspamd could be used as a milter, but then it cannot apply
413 # its checks "per user" (milter is not yet dispatched to
414 # users), so we wrap dovecot-lda inside rspamc per recipient
415 # here.
416 rspamc_dovecot = pkgs.writeScriptBin "rspamc_dovecot" ''
417 #! ${pkgs.stdenv.shell}
418 sender="$1"
419 original_recipient="$2"
420 user="$3"
421
422 ${pkgs.coreutils}/bin/cat - | \
423 (${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d "$user" --mime || true) | \
424 ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user"
425 '';
426 in [
427 "flags=ODRhu" "user=vhost:vhost"
428 "argv=${rspamc_dovecot}/bin/rspamc_dovecot \${sender} \${original_recipient} \${user}@\${nexthop}"
429 ];
430 };
431 };
432 };
433 security.acme.certs."mail" = {
434 postRun = ''
435 systemctl restart postfix.service
436 '';
437 extraDomains = {
438 "smtp.immae.eu" = null;
439 };
440 };
441 security.acme.certs."mail-rsa" = {
442 postRun = ''
443 systemctl restart postfix.service
444 '';
445 extraDomains = {
446 "smtp.immae.eu" = null;
447 };
448 };
449 system.activationScripts.testmail = {
450 deps = [ "users" ];
451 text = let
452 allCfg = config.myEnv.monitoring.email_check;
453 cfg = allCfg.eldiron;
454 reverseTargets = builtins.attrNames (lib.attrsets.filterAttrs (k: v: builtins.elem "eldiron" v.targets) allCfg);
455 to_email = cfg': host':
456 let sep = if lib.strings.hasInfix "+" cfg'.mail_address then "_" else "+";
457 in "${cfg'.mail_address}${sep}${host'}@${cfg'.mail_domain}";
458 mails_to_receive = builtins.concatStringsSep " " (map (to_email cfg) reverseTargets);
459 in ''
460 install -m 0555 -o postfixscripts -g keys -d /var/lib/naemon/checks/email
461 for f in ${mails_to_receive}; do
462 if [ ! -f /var/lib/naemon/checks/email/$f ]; then
463 install -m 0644 -o postfixscripts -g keys /dev/null -T /var/lib/naemon/checks/email/$f
464 touch -m -d @0 /var/lib/naemon/checks/email/$f
465 fi
466 done
467 '';
468 };
469 systemd.services.postfix.serviceConfig.Slice = "mail.slice";
470 };
471}
diff --git a/modules/private/mail/relay.nix b/modules/private/mail/relay.nix
deleted file mode 100644
index 668d365..0000000
--- a/modules/private/mail/relay.nix
+++ /dev/null
@@ -1,235 +0,0 @@
1{ lib, pkgs, config, nodes, name, ... }:
2{
3 config = lib.mkIf config.myServices.mailBackup.enable {
4 security.acme.certs."mail" = config.myServices.certificates.certConfig // {
5 postRun = ''
6 systemctl restart postfix.service
7 '';
8 domain = config.hostEnv.fqdn;
9 extraDomains = let
10 zonesWithMx = builtins.filter (zone:
11 lib.attrsets.hasAttr "withEmail" zone && lib.lists.length zone.withEmail > 0
12 ) config.myEnv.dns.masterZones;
13 mxs = map (zone: "${config.myEnv.servers."${name}".mx.subdomain}.${zone.name}") zonesWithMx;
14 in builtins.listToAttrs (map (mx: lib.attrsets.nameValuePair mx null) mxs);
15 };
16 secrets.keys = {
17 "postfix/mysql_alias_maps" = {
18 user = config.services.postfix.user;
19 group = config.services.postfix.group;
20 permissions = "0440";
21 text = ''
22 # We need to specify that option to trigger ssl connection
23 tls_ciphers = TLSv1.2
24 user = ${config.myEnv.mail.postfix.mysql.user}
25 password = ${config.myEnv.mail.postfix.mysql.password}
26 hosts = ${config.myEnv.mail.postfix.mysql.remoteHost}
27 dbname = ${config.myEnv.mail.postfix.mysql.database}
28 query = SELECT DISTINCT 1
29 FROM forwardings
30 WHERE
31 ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s'))
32 AND active = 1
33 AND '%s' NOT IN
34 (
35 SELECT source
36 FROM forwardings_blacklisted
37 WHERE source = '%s'
38 ) UNION
39 SELECT 'devnull@immae.eu'
40 FROM forwardings_blacklisted
41 WHERE source = '%s'
42 '';
43 };
44 "postfix/ldap_mailboxes" = {
45 user = config.services.postfix.user;
46 group = config.services.postfix.group;
47 permissions = "0440";
48 text = ''
49 server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636
50 search_base = ${config.myEnv.mail.dovecot.ldap.base}
51 query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter}
52 bind_dn = ${config.myEnv.mail.dovecot.ldap.dn}
53 bind_pw = ${config.myEnv.mail.dovecot.ldap.password}
54 result_attribute = immaePostfixAddress
55 result_format = dummy
56 version = 3
57 '';
58 };
59 "postfix/sympa_mailbox_maps" = {
60 user = config.services.postfix.user;
61 group = config.services.postfix.group;
62 permissions = "0440";
63 text = ''
64 hosts = ${config.myEnv.mail.sympa.postgresql.host}
65 user = ${config.myEnv.mail.sympa.postgresql.user}
66 password = ${config.myEnv.mail.sympa.postgresql.password}
67 dbname = ${config.myEnv.mail.sympa.postgresql.database}
68 query = SELECT DISTINCT 1 FROM list_table WHERE '%s' IN (
69 CONCAT(name_list, '@', robot_list),
70 CONCAT(name_list, '-request@', robot_list),
71 CONCAT(name_list, '-editor@', robot_list),
72 CONCAT(name_list, '-unsubscribe@', robot_list),
73 CONCAT(name_list, '-owner@', robot_list),
74 CONCAT('sympa-request@', robot_list),
75 CONCAT('sympa-owner@', robot_list),
76 CONCAT('sympa@', robot_list),
77 CONCAT('listmaster@', robot_list),
78 CONCAT('bounce@', robot_list),
79 CONCAT('abuse-feedback-report@', robot_list)
80 )
81 '';
82 };
83 "postfix/ldap_ejabberd_users_immae_fr" = {
84 user = config.services.postfix.user;
85 group = config.services.postfix.group;
86 permissions = "0440";
87 text = ''
88 server_host = ldaps://${config.myEnv.jabber.ldap.host}:636
89 search_base = ${config.myEnv.jabber.ldap.base}
90 query_filter = ${config.myEnv.jabber.postfix_user_filter}
91 domain = immae.fr
92 bind_dn = ${config.myEnv.jabber.ldap.dn}
93 bind_pw = ${config.myEnv.jabber.ldap.password}
94 result_attribute = immaeXmppUid
95 result_format = ejabberd@localhost
96 version = 3
97 '';
98 };
99 };
100
101 networking.firewall.allowedTCPPorts = [ 25 ];
102
103 users.users."${config.services.postfix.user}".extraGroups = [ "keys" ];
104 services.filesWatcher.postfix = {
105 restart = true;
106 paths = [
107 config.secrets.fullPaths."postfix/mysql_alias_maps"
108 config.secrets.fullPaths."postfix/sympa_mailbox_maps"
109 config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"
110 config.secrets.fullPaths."postfix/ldap_mailboxes"
111 ];
112 };
113 services.postfix = {
114 mapFiles = let
115 recipient_maps = let
116 name = n: i: "relay_${n}_${toString i}";
117 pair = n: i: m: lib.attrsets.nameValuePair (name n i) (
118 if m.type == "hash"
119 then pkgs.writeText (name n i) m.content
120 else null
121 );
122 pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps;
123 in lib.attrsets.filterAttrs (k: v: v != null) (
124 lib.attrsets.listToAttrs (lib.flatten (
125 lib.attrsets.mapAttrsToList pairs config.myEnv.mail.postfix.backup_domains
126 ))
127 );
128 relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) (
129 lib.attrsets.mapAttrs' (n: v:
130 lib.attrsets.nameValuePair "recipient_access_${n}" (
131 if lib.attrsets.hasAttr "relay_restrictions" v
132 then pkgs.writeText "recipient_access_${n}" v.relay_restrictions
133 else null
134 )
135 ) config.myEnv.mail.postfix.backup_domains
136 );
137 virtual_map = {
138 virtual = let
139 cfg = config.myEnv.monitoring.email_check.eldiron;
140 address = "${cfg.mail_address}@${cfg.mail_domain}";
141 in pkgs.writeText "postfix-virtual" (
142 builtins.concatStringsSep "\n" (
143 ["${address} 1"] ++
144 lib.attrsets.mapAttrsToList (
145 n: v: lib.optionalString v.external ''
146 script_${n}@mail.immae.eu 1
147 ''
148 ) config.myEnv.mail.scripts
149 )
150 );
151 };
152 in
153 recipient_maps // relay_restrictions // virtual_map;
154 config = {
155 ### postfix module overrides
156 readme_directory = "${pkgs.postfix}/share/postfix/doc";
157 smtp_tls_CAfile = lib.mkForce "";
158 smtp_tls_cert_file = lib.mkForce "";
159 smtp_tls_key_file = lib.mkForce "";
160
161 message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited"
162 mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts
163 alias_database = "\$alias_maps";
164
165 ### Relay domains
166 relay_domains = let
167 backups = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) config.myEnv.mail.postfix.backup_domains);
168 virtual_domains = config.myEnv.mail.postfix.additional_mailbox_domains
169 ++ lib.remove null (lib.flatten (map
170 (zone: map
171 (e: if e.receive
172 then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}"
173 else null
174 )
175 (zone.withEmail or [])
176 )
177 config.myEnv.dns.masterZones
178 ));
179 in
180 backups ++ virtual_domains;
181 relay_recipient_maps = let
182 backup_recipients = lib.flatten (lib.attrsets.mapAttrsToList (n: v:
183 lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps
184 ) config.myEnv.mail.postfix.backup_domains);
185 virtual_alias_maps = [
186 "hash:/etc/postfix/virtual"
187 "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}"
188 "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}"
189 ];
190 virtual_mailbox_maps = [
191 "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}"
192 "pgsql:${config.secrets.fullPaths."postfix/sympa_mailbox_maps"}"
193 ];
194 in
195 backup_recipients ++ virtual_alias_maps ++ virtual_mailbox_maps;
196 smtpd_relay_restrictions = [
197 "defer_unauth_destination"
198 ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v:
199 if lib.attrsets.hasAttr "relay_restrictions" v
200 then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ]
201 else []
202 ) config.myEnv.mail.postfix.backup_domains);
203
204 ### Additional smtpd configuration
205 smtpd_tls_received_header = "yes";
206 smtpd_tls_loglevel = "1";
207
208 ### Email sending configuration
209 smtp_tls_security_level = "may";
210 smtp_tls_loglevel = "1";
211
212 ### Force ip bind for smtp
213 smtp_bind_address = config.myEnv.servers."${name}".ips.main.ip4;
214 smtp_bind_address6 = builtins.head config.myEnv.servers."${name}".ips.main.ip6;
215
216 smtpd_milters = [
217 "unix:${config.myServices.mail.milters.sockets.opendkim}"
218 "unix:${config.myServices.mail.milters.sockets.openarc}"
219 "unix:${config.myServices.mail.milters.sockets.opendmarc}"
220 ];
221 };
222 enable = true;
223 enableSmtp = true;
224 enableSubmission = false;
225 destination = ["localhost"];
226 # This needs to reverse DNS
227 hostname = config.hostEnv.fqdn;
228 setSendmail = false;
229 sslCert = "/var/lib/acme/mail/fullchain.pem";
230 sslKey = "/var/lib/acme/mail/key.pem";
231 recipientDelimiter = "+";
232 };
233 };
234}
235
diff --git a/modules/private/mail/rspamd.nix b/modules/private/mail/rspamd.nix
deleted file mode 100644
index 05f1300..0000000
--- a/modules/private/mail/rspamd.nix
+++ /dev/null
@@ -1,87 +0,0 @@
1{ lib, pkgs, config, ... }:
2{
3 options.myServices.mail.rspamd.sockets = lib.mkOption {
4 type = lib.types.attrsOf lib.types.path;
5 default = {
6 worker-controller = "/run/rspamd/worker-controller.sock";
7 };
8 readOnly = true;
9 description = ''
10 rspamd sockets
11 '';
12 };
13 config = lib.mkIf config.myServices.mail.enable {
14 services.cron.systemCronJobs = let
15 cron_script = pkgs.runCommand "cron_script" {
16 buildInputs = [ pkgs.makeWrapper ];
17 } ''
18 mkdir -p $out
19 cp ${./scan_reported_mails} $out/scan_reported_mails
20 patchShebangs $out
21 for i in $out/*; do
22 wrapProgram "$i" --prefix PATH : ${lib.makeBinPath [ pkgs.coreutils pkgs.rspamd pkgs.flock ]}
23 done
24 '';
25 in
26 [ "*/20 * * * * vhost ${cron_script}/scan_reported_mails" ];
27
28 systemd.services.rspamd.serviceConfig.Slice = "mail.slice";
29 services.rspamd = {
30 enable = true;
31 debug = false;
32 overrides = {
33 "actions.conf".text = ''
34 reject = null;
35 add_header = 6;
36 greylist = null;
37 '';
38 "milter_headers.conf".text = ''
39 extended_spam_headers = true;
40 '';
41 };
42 locals = {
43 "redis.conf".text = ''
44 servers = "${config.myEnv.mail.rspamd.redis.socket}";
45 db = "${config.myEnv.mail.rspamd.redis.db}";
46 '';
47 "classifier-bayes.conf".text = ''
48 users_enabled = true;
49 backend = "redis";
50 servers = "${config.myEnv.mail.rspamd.redis.socket}";
51 database = "${config.myEnv.mail.rspamd.redis.db}";
52 autolearn = true;
53 cache {
54 backend = "redis";
55 }
56 new_schema = true;
57 statfile {
58 BAYES_HAM {
59 spam = false;
60 }
61 BAYES_SPAM {
62 spam = true;
63 }
64 }
65 '';
66 };
67 workers = {
68 controller = {
69 extraConfig = ''
70 enable_password = "${config.myEnv.mail.rspamd.write_password_hashed}";
71 password = "${config.myEnv.mail.rspamd.read_password_hashed}";
72 '';
73 bindSockets = [ {
74 socket = config.myServices.mail.rspamd.sockets.worker-controller;
75 mode = "0660";
76 owner = config.services.rspamd.user;
77 group = "vhost";
78 } ];
79 };
80 };
81 postfix = {
82 enable = true;
83 config = {};
84 };
85 };
86 };
87}
diff --git a/modules/private/mail/scan_reported_mails b/modules/private/mail/scan_reported_mails
deleted file mode 100755
index fe9f4d6..0000000
--- a/modules/private/mail/scan_reported_mails
+++ /dev/null
@@ -1,21 +0,0 @@
1#!/usr/bin/env bash
2
3( flock -n 9 || exit 1
4shopt -s nullglob
5for spool in /var/lib/vhost/.rspamd/*/pending; do
6 rspamd_folder=$(dirname $spool)
7 mail_user=$(basename $rspamd_folder)
8 mv $rspamd_folder/pending $rspamd_folder/processing
9
10 for mtype in ham spam; do
11 if [ -d $rspamd_folder/processing/$mtype ]; then
12 output="$(rspamc -h /run/rspamd/worker-controller.sock -c bayes -d $mail_user learn_$mtype $rspamd_folder/processing/$mtype/*)"
13 echo "[$mtype: $mail_user]" ${output} >> /var/lib/vhost/.rspamd/rspamd.log
14 mkdir -p $rspamd_folder/processed/$mtype
15 cp $rspamd_folder/processing/$mtype/* $rspamd_folder/processed/$mtype/
16 fi
17 done
18
19 rm -rf $rspamd_folder/processing
20done
21) 9>/var/lib/vhost/scan_reported_mails.lock
diff --git a/modules/private/mail/sieve_bin/imapsieve_copy b/modules/private/mail/sieve_bin/imapsieve_copy
deleted file mode 100755
index 2ca1f23..0000000
--- a/modules/private/mail/sieve_bin/imapsieve_copy
+++ /dev/null
@@ -1,8 +0,0 @@
1#!/usr/bin/env bash
2# Inspired from https://docs.iredmail.org/dovecot.imapsieve.html
3
4MSG_TYPE="$1"
5OUTPUT_DIR="/var/lib/vhost/.rspamd/${USER}/pending/${MSG_TYPE}"
6FILE="${OUTPUT_DIR}/$(date +%Y%m%d%H%M%S)-${RANDOM}${RANDOM}.eml"
7mkdir -p "${OUTPUT_DIR}"
8cat > ${FILE} < /dev/stdin
diff --git a/modules/private/mail/sieve_scripts/backup.sieve b/modules/private/mail/sieve_scripts/backup.sieve
deleted file mode 100644
index 3014c0a..0000000
--- a/modules/private/mail/sieve_scripts/backup.sieve
+++ /dev/null
@@ -1,7 +0,0 @@
1# vim: filetype=sieve
2require ["copy","mailbox","fileinto","regex"];
3if header :is "X-Spam" "Yes" {
4 fileinto :create :copy "Backup/Spam";
5} else {
6 fileinto :create :copy "Backup/Ham";
7}
diff --git a/modules/private/mail/sieve_scripts/report_ham.sieve b/modules/private/mail/sieve_scripts/report_ham.sieve
deleted file mode 100644
index f9b8481..0000000
--- a/modules/private/mail/sieve_scripts/report_ham.sieve
+++ /dev/null
@@ -1,11 +0,0 @@
1require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
2
3if environment :matches "imap.mailbox" "*" {
4 set "mailbox" "${1}";
5}
6
7if string "${mailbox}" "Trash" {
8 stop;
9}
10
11pipe :copy "imapsieve_copy" [ "ham" ];
diff --git a/modules/private/mail/sieve_scripts/report_spam.sieve b/modules/private/mail/sieve_scripts/report_spam.sieve
deleted file mode 100644
index 9a1f794..0000000
--- a/modules/private/mail/sieve_scripts/report_spam.sieve
+++ /dev/null
@@ -1,3 +0,0 @@
1require ["vnd.dovecot.pipe", "copy", "imapsieve" ];
2
3pipe :copy "imapsieve_copy" [ "spam" ];
diff --git a/modules/private/mail/sympa.nix b/modules/private/mail/sympa.nix
deleted file mode 100644
index 0626ac0..0000000
--- a/modules/private/mail/sympa.nix
+++ /dev/null
@@ -1,213 +0,0 @@
1{ lib, pkgs, config, ... }:
2let
3 domain = "lists.immae.eu";
4 sympaConfig = config.myEnv.mail.sympa;
5in
6{
7 config = lib.mkIf config.myServices.mail.enable {
8 myServices.databases.postgresql.authorizedHosts = {
9 backup-2 = [
10 {
11 username = "sympa";
12 database = "sympa";
13 ip4 = [config.myEnv.servers.backup-2.ips.main.ip4];
14 ip6 = config.myEnv.servers.backup-2.ips.main.ip6;
15 }
16 ];
17 };
18 services.websites.env.tools.vhostConfs.mail = {
19 extraConfig = lib.mkAfter [
20 ''
21 Alias /static-sympa/ /var/lib/sympa/static_content/
22 <Directory /var/lib/sympa/static_content/>
23 Require all granted
24 AllowOverride none
25 </Directory>
26 <Location /sympa>
27 SetHandler "proxy:unix:/run/sympa/wwsympa.socket|fcgi://"
28 Require all granted
29 </Location>
30 ''
31 ];
32 };
33
34 secrets.keys = {
35 "sympa/db_password" = {
36 permissions = "0400";
37 group = "sympa";
38 user = "sympa";
39 text = sympaConfig.postgresql.password;
40 };
41 }
42 // lib.mapAttrs' (n: v: lib.nameValuePair "sympa/data_sources/${n}.incl" {
43 permissions = "0400"; group = "sympa"; user = "sympa"; text = v;
44 }) sympaConfig.data_sources
45 // lib.mapAttrs' (n: v: lib.nameValuePair "sympa/scenari/${n}" {
46 permissions = "0400"; group = "sympa"; user = "sympa"; text = v;
47 }) sympaConfig.scenari;
48 users.users.sympa.extraGroups = [ "keys" ];
49 systemd.slices.mail-sympa = {
50 description = "Sympa slice";
51 };
52
53 systemd.services.sympa.serviceConfig.SupplementaryGroups = [ "keys" ];
54 systemd.services.sympa-archive.serviceConfig.SupplementaryGroups = [ "keys" ];
55 systemd.services.sympa-bounce.serviceConfig.SupplementaryGroups = [ "keys" ];
56 systemd.services.sympa-bulk.serviceConfig.SupplementaryGroups = [ "keys" ];
57 systemd.services.sympa-task.serviceConfig.SupplementaryGroups = [ "keys" ];
58
59 systemd.services.sympa.serviceConfig.Slice = "mail-sympa.slice";
60 systemd.services.sympa-archive.serviceConfig.Slice = "mail-sympa.slice";
61 systemd.services.sympa-bounce.serviceConfig.Slice = "mail-sympa.slice";
62 systemd.services.sympa-bulk.serviceConfig.Slice = "mail-sympa.slice";
63 systemd.services.sympa-task.serviceConfig.Slice = "mail-sympa.slice";
64
65 # https://github.com/NixOS/nixpkgs/pull/84202
66 systemd.services.sympa.serviceConfig.ProtectKernelModules = lib.mkForce false;
67 systemd.services.sympa-archive.serviceConfig.ProtectKernelModules = lib.mkForce false;
68 systemd.services.sympa-bounce.serviceConfig.ProtectKernelModules = lib.mkForce false;
69 systemd.services.sympa-bulk.serviceConfig.ProtectKernelModules = lib.mkForce false;
70 systemd.services.sympa-task.serviceConfig.ProtectKernelModules = lib.mkForce false;
71 systemd.services.sympa.serviceConfig.ProtectKernelTunables = lib.mkForce false;
72 systemd.services.sympa-archive.serviceConfig.ProtectKernelTunables = lib.mkForce false;
73 systemd.services.sympa-bounce.serviceConfig.ProtectKernelTunables = lib.mkForce false;
74 systemd.services.sympa-bulk.serviceConfig.ProtectKernelTunables = lib.mkForce false;
75 systemd.services.sympa-task.serviceConfig.ProtectKernelTunables = lib.mkForce false;
76
77 systemd.services.wwsympa = {
78 wantedBy = [ "multi-user.target" ];
79 after = [ "sympa.service" ];
80 serviceConfig = {
81 Slice = "mail-sympa.slice";
82 Type = "forking";
83 PIDFile = "/run/sympa/wwsympa.pid";
84 Restart = "always";
85 ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
86 -u sympa \
87 -g sympa \
88 -U wwwrun \
89 -M 0600 \
90 -F 2 \
91 -P /run/sympa/wwsympa.pid \
92 -s /run/sympa/wwsympa.socket \
93 -- ${pkgs.sympa}/lib/sympa/cgi/wwsympa.fcgi
94 '';
95 StateDirectory = "sympa";
96 ProtectHome = true;
97 ProtectSystem = "full";
98 ProtectControlGroups = true;
99 };
100 };
101
102 services.postfix = {
103 mapFiles = {
104 # Update relay list when changing one of those
105 sympa_virtual = pkgs.writeText "virtual.sympa" ''
106 sympa-request@${domain} postmaster@immae.eu
107 sympa-owner@${domain} postmaster@immae.eu
108
109 sympa-request@cip-ca.fr postmaster@immae.eu
110 sympa-owner@cip-ca.fr postmaster@immae.eu
111 '';
112 sympa_transport = pkgs.writeText "transport.sympa" ''
113 ${domain} error:User unknown in recipient table
114 sympa@${domain} sympa:sympa@${domain}
115 listmaster@${domain} sympa:listmaster@${domain}
116 bounce@${domain} sympabounce:sympa@${domain}
117 abuse-feedback-report@${domain} sympabounce:sympa@${domain}
118
119 sympa@cip-ca.fr sympa:sympa@cip-ca.fr
120 listmaster@cip-ca.fr sympa:listmaster@cip-ca.fr
121 bounce@cip-ca.fr sympabounce:sympa@cip-ca.fr
122 abuse-feedback-report@cip-ca.fr sympabounce:sympa@cip-ca.fr
123 '';
124 };
125 config = {
126 transport_maps = lib.mkAfter [
127 "hash:/etc/postfix/sympa_transport"
128 "hash:/var/lib/sympa/sympa_transport"
129 ];
130 virtual_alias_maps = lib.mkAfter [
131 "hash:/etc/postfix/sympa_virtual"
132 ];
133 virtual_mailbox_maps = lib.mkAfter [
134 "hash:/etc/postfix/sympa_transport"
135 "hash:/var/lib/sympa/sympa_transport"
136 "hash:/etc/postfix/sympa_virtual"
137 ];
138 };
139 masterConfig = {
140 sympa = {
141 type = "unix";
142 privileged = true;
143 chroot = false;
144 command = "pipe";
145 args = [
146 "flags=hqRu"
147 "user=sympa"
148 "argv=${pkgs.sympa}/libexec/queue"
149 "\${nexthop}"
150 ];
151 };
152 sympabounce = {
153 type = "unix";
154 privileged = true;
155 chroot = false;
156 command = "pipe";
157 args = [
158 "flags=hqRu"
159 "user=sympa"
160 "argv=${pkgs.sympa}/libexec/bouncequeue"
161 "\${nexthop}"
162 ];
163 };
164 };
165 };
166 services.sympa = {
167 enable = true;
168 listMasters = sympaConfig.listmasters;
169 mainDomain = domain;
170 domains = {
171 "${domain}" = {
172 webHost = "mail.immae.eu";
173 webLocation = "/sympa";
174 };
175 "cip-ca.fr" = {
176 webHost = "mail.cip-ca.fr";
177 webLocation = "/sympa";
178 };
179 };
180
181 database = {
182 type = "PostgreSQL";
183 user = sympaConfig.postgresql.user;
184 host = sympaConfig.postgresql.socket;
185 name = sympaConfig.postgresql.database;
186 passwordFile = config.secrets.fullPaths."sympa/db_password";
187 createLocally = false;
188 };
189 settings = {
190 sendmail = "/run/wrappers/bin/sendmail";
191 log_smtp = "on";
192 sendmail_aliases = "/var/lib/sympa/sympa_transport";
193 aliases_program = "${pkgs.postfix}/bin/postmap";
194 };
195 settingsFile = {
196 "virtual.sympa".enable = false;
197 "transport.sympa".enable = false;
198 } // lib.mapAttrs' (n: v: lib.nameValuePair
199 "etc/${domain}/data_sources/${n}.incl"
200 { source = config.secrets.fullPaths."sympa/data_sources/${n}.incl"; }) sympaConfig.data_sources
201 // lib.mapAttrs' (n: v: lib.nameValuePair
202 "etc/${domain}/scenari/${n}"
203 { source = config.secrets.fullPaths."sympa/scenari/${n}"; }) sympaConfig.scenari;
204 web = {
205 server = "none";
206 };
207
208 mta = {
209 type = "none";
210 };
211 };
212 };
213}
diff --git a/modules/private/mail/verify_from.py b/modules/private/mail/verify_from.py
deleted file mode 100755
index b75001e..0000000
--- a/modules/private/mail/verify_from.py
+++ /dev/null
@@ -1,60 +0,0 @@
1#!/usr/bin/env python3
2import Milter
3import argparse
4from email.header import decode_header
5from email.utils import parseaddr
6
7class CheckMilter(Milter.Base):
8 def __init__(self):
9 self.envelope_from = None
10 self.header_from = None
11
12 @Milter.noreply
13 def connect(self, IPname, family, hostaddr):
14 return Milter.CONTINUE
15
16 def hello(self, heloname):
17 return Milter.CONTINUE
18
19 def envfrom(self, mailfrom, *args):
20 self.envelope_from = parseaddr(mailfrom)[1]
21 return Milter.CONTINUE
22
23 @Milter.noreply
24 def envrcpt(self, to, *str):
25 return Milter.CONTINUE
26
27 @Milter.noreply
28 def header(self, name, hval):
29 if name.lower() == "from":
30 self.header_from = parseaddr(decode_header(hval)[-1][0])[1]
31 return Milter.CONTINUE
32
33 def eoh(self):
34 if self.header_from is not None and self.header_from != "" and self.header_from != self.envelope_from:
35 self.setreply("553", xcode="5.7.1", msg="<%s>: From header rejected: not matching envelope From %s"
36 % (self.header_from, self.envelope_from))
37 return Milter.REJECT
38
39 return Milter.CONTINUE
40
41 @Milter.noreply
42 def body(self, chunk):
43 return Milter.CONTINUE
44
45 def eom(self):
46 return Milter.ACCEPT
47
48 def close(self):
49 return Milter.CONTINUE
50
51 def abort(self):
52 return Milter.CONTINUE
53
54if __name__ == "__main__":
55 parser = argparse.ArgumentParser()
56 parser.add_argument("--socket", "-s", type=str, help="socket to listen to")
57 config = parser.parse_args()
58
59 Milter.factory = CheckMilter
60 Milter.runmilter("check_from", config.socket, timeout=300)