diff options
author | Ismaël Bouya <ismael.bouya@normalesup.org> | 2023-10-04 01:35:06 +0200 |
---|---|---|
committer | Ismaël Bouya <ismael.bouya@normalesup.org> | 2023-10-04 02:11:48 +0200 |
commit | 1a64deeb894dc95e2645a75771732c6cc53a79ad (patch) | |
tree | 1b9df4838f894577a09b9b260151756272efeb53 /systems/eldiron/mail | |
parent | fa25ffd4583cc362075cd5e1b4130f33306103f0 (diff) | |
download | Nix-1a64deeb894dc95e2645a75771732c6cc53a79ad.tar.gz Nix-1a64deeb894dc95e2645a75771732c6cc53a79ad.tar.zst Nix-1a64deeb894dc95e2645a75771732c6cc53a79ad.zip |
Squash changes containing private information
There were a lot of changes since the previous commit, but a lot of them
contained personnal information about users. All thos changes got
stashed into a single commit (history is kept in a different place) and
private information was moved in a separate private repository
Diffstat (limited to 'systems/eldiron/mail')
-rw-r--r-- | systems/eldiron/mail/default.nix | 44 | ||||
-rw-r--r-- | systems/eldiron/mail/dovecot.nix | 348 | ||||
-rw-r--r-- | systems/eldiron/mail/postfix.nix | 497 | ||||
-rw-r--r-- | systems/eldiron/mail/rspamd.nix | 88 | ||||
-rwxr-xr-x | systems/eldiron/mail/scan_reported_mails | 21 | ||||
-rwxr-xr-x | systems/eldiron/mail/sieve_bin/imapsieve_copy | 8 | ||||
-rw-r--r-- | systems/eldiron/mail/sieve_scripts/backup.sieve | 7 | ||||
-rw-r--r-- | systems/eldiron/mail/sieve_scripts/report_ham.sieve | 11 | ||||
-rw-r--r-- | systems/eldiron/mail/sieve_scripts/report_spam.sieve | 3 | ||||
-rw-r--r-- | systems/eldiron/mail/sympa.nix | 232 |
10 files changed, 1259 insertions, 0 deletions
diff --git a/systems/eldiron/mail/default.nix b/systems/eldiron/mail/default.nix new file mode 100644 index 0000000..4e13f6a --- /dev/null +++ b/systems/eldiron/mail/default.nix | |||
@@ -0,0 +1,44 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | { | ||
3 | imports = [ | ||
4 | ./postfix.nix | ||
5 | ./dovecot.nix | ||
6 | ./rspamd.nix | ||
7 | ./sympa.nix | ||
8 | ]; | ||
9 | |||
10 | options.myServices.mail.enable = lib.mkEnableOption "enable Mail services"; | ||
11 | config = lib.mkIf config.myServices.mail.enable { | ||
12 | myServices.mail.milters.enable = true; | ||
13 | security.acme.certs."mail" = { | ||
14 | postRun = lib.mkBefore '' | ||
15 | cp -f fullchain.pem /etc/dovecot/fullchain.pem | ||
16 | chown :dovecot2 /etc/dovecot/fullchain.pem | ||
17 | chmod a+r /etc/dovecot/fullchain.pem | ||
18 | ''; | ||
19 | domain = config.hostEnv.fqdn; | ||
20 | extraDomainNames = let | ||
21 | zonesWithMx = builtins.attrNames (lib.filterAttrs (n: v: v.hasEmail) config.myServices.dns.zones); | ||
22 | mxs = map (n: "${config.hostEnv.mx.subdomain}.${n}") zonesWithMx; | ||
23 | in mxs; | ||
24 | }; | ||
25 | # This is for clients that don’t support elliptic curves (e.g. | ||
26 | # printer) | ||
27 | security.acme.certs."mail-rsa" = { | ||
28 | postRun = lib.mkBefore '' | ||
29 | cp -f fullchain.pem /etc/dovecot/fullchain-rsa.pem | ||
30 | chown :dovecot2 /etc/dovecot/fullchain-rsa.pem | ||
31 | chmod a+r /etc/dovecot/fullchain-rsa.pem | ||
32 | ''; | ||
33 | domain = config.hostEnv.fqdn; | ||
34 | keyType = "rsa4096"; | ||
35 | extraDomainNames = let | ||
36 | zonesWithMx = builtins.attrNames (lib.filterAttrs (n: v: v.hasEmail) config.myServices.dns.zones); | ||
37 | mxs = map (n: "${config.hostEnv.mx.subdomain}.${n}") zonesWithMx; | ||
38 | in mxs; | ||
39 | }; | ||
40 | systemd.slices.mail = { | ||
41 | description = "Mail slice"; | ||
42 | }; | ||
43 | }; | ||
44 | } | ||
diff --git a/systems/eldiron/mail/dovecot.nix b/systems/eldiron/mail/dovecot.nix new file mode 100644 index 0000000..a1282e3 --- /dev/null +++ b/systems/eldiron/mail/dovecot.nix | |||
@@ -0,0 +1,348 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
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 | ''; | ||
13 | in | ||
14 | { | ||
15 | config = lib.mkIf config.myServices.mail.enable { | ||
16 | myServices.dns.zones."immae.eu".subdomains = | ||
17 | with config.myServices.dns.helpers; | ||
18 | { | ||
19 | imap = ips servers.eldiron.ips.main; | ||
20 | pop3 = ips servers.eldiron.ips.main; | ||
21 | }; | ||
22 | |||
23 | myServices.chatonsProperties.services.email = { | ||
24 | file.datetime = "2022-08-22T01:00:00"; | ||
25 | service = { | ||
26 | name = "E-mail account"; | ||
27 | description = "Compte e-mail avec configuration imap et smtp/pop3"; | ||
28 | logo = "https://www.dovecot.org/wp-content/uploads/2021/09/favicon.ico"; | ||
29 | website = "https://mail.immae.eu/"; | ||
30 | status.level = "OK"; | ||
31 | status.description = "OK"; | ||
32 | registration."" = ["MEMBER" "CLIENT"]; | ||
33 | registration.load = "OPEN"; | ||
34 | install.type = "PACKAGE"; | ||
35 | }; | ||
36 | software = { | ||
37 | name = "Dovecot"; | ||
38 | website = "https://www.dovecot.org/"; | ||
39 | license.url = "https://github.com/dovecot/core/blob/main/COPYING"; | ||
40 | license.name = "MIT and LGPLv2.1 Licenses"; | ||
41 | version = pkgs.dovecot.version; | ||
42 | source.url = "https://github.com/dovecot/core"; | ||
43 | modules = ["roundcube" "rainloop"] ++ map (a: a.pname) config.services.dovecot2.modules; | ||
44 | }; | ||
45 | }; | ||
46 | systemd.services.dovecot2.serviceConfig.Slice = "mail.slice"; | ||
47 | secrets.keys."dovecot/ldap" = { | ||
48 | user = config.services.dovecot2.user; | ||
49 | group = config.services.dovecot2.group; | ||
50 | permissions = "0400"; | ||
51 | text = '' | ||
52 | hosts = ${config.myEnv.mail.dovecot.ldap.host} | ||
53 | tls = yes | ||
54 | |||
55 | dn = ${config.myEnv.mail.dovecot.ldap.dn} | ||
56 | dnpass = ${config.myEnv.mail.dovecot.ldap.password} | ||
57 | |||
58 | auth_bind = yes | ||
59 | |||
60 | ldap_version = 3 | ||
61 | |||
62 | base = ${config.myEnv.mail.dovecot.ldap.base} | ||
63 | scope = subtree | ||
64 | |||
65 | pass_filter = ${config.myEnv.mail.dovecot.ldap.filter} | ||
66 | pass_attrs = ${config.myEnv.mail.dovecot.ldap.pass_attrs} | ||
67 | |||
68 | user_attrs = ${config.myEnv.mail.dovecot.ldap.user_attrs} | ||
69 | user_filter = ${config.myEnv.mail.dovecot.ldap.filter} | ||
70 | iterate_attrs = ${config.myEnv.mail.dovecot.ldap.iterate_attrs} | ||
71 | iterate_filter = ${config.myEnv.mail.dovecot.ldap.iterate_filter} | ||
72 | ''; | ||
73 | }; | ||
74 | |||
75 | users.users.vhost = { | ||
76 | group = "vhost"; | ||
77 | uid = config.ids.uids.vhost; | ||
78 | }; | ||
79 | users.groups.vhost.gid = config.ids.gids.vhost; | ||
80 | users.users."${config.services.dovecot2.user}".extraGroups = [ "acme" ]; | ||
81 | |||
82 | nixpkgs.overlays = [ | ||
83 | (self: super: { | ||
84 | dovecot = super.dovecot.override { openldap = self.openldap_libressl_cyrus; }; | ||
85 | }) | ||
86 | ]; | ||
87 | |||
88 | # https://blog.zeninc.net/index.php?post/2018/04/01/Un-annuaire-pour-les-gouverner-tous....... | ||
89 | services.dovecot2 = { | ||
90 | enable = true; | ||
91 | enablePAM = false; | ||
92 | enablePop3 = true; | ||
93 | enableImap = true; | ||
94 | enableLmtp = true; | ||
95 | protocols = [ "sieve" ]; | ||
96 | modules = [ | ||
97 | pkgs.dovecot_pigeonhole | ||
98 | pkgs.dovecot_fts_xapian | ||
99 | ]; | ||
100 | mailUser = "vhost"; | ||
101 | mailGroup = "vhost"; | ||
102 | createMailUser = false; | ||
103 | mailboxes = { | ||
104 | Trash = { auto = "subscribe"; specialUse = "Trash"; }; | ||
105 | Junk = { auto = "subscribe"; specialUse = "Junk"; }; | ||
106 | Sent = { auto = "subscribe"; specialUse = "Sent"; }; | ||
107 | Drafts = { auto = "subscribe"; specialUse = "Drafts"; }; | ||
108 | }; | ||
109 | mailLocation = "mbox:~/Mail:INBOX=~/Mail/Inbox:INDEX=~/.imap"; | ||
110 | sslServerCert = "/etc/dovecot/fullchain.pem"; | ||
111 | sslServerKey = "/var/lib/acme/mail/key.pem"; | ||
112 | sslCACert = "/etc/dovecot/fullchain.pem"; | ||
113 | extraConfig = builtins.concatStringsSep "\n" [ | ||
114 | # For printer which doesn’t support elliptic curve | ||
115 | '' | ||
116 | ssl_alt_cert = </etc/dovecot/fullchain-rsa.pem | ||
117 | ssl_alt_key = </var/lib/acme/mail-rsa/key.pem | ||
118 | '' | ||
119 | |||
120 | '' | ||
121 | postmaster_address = postmaster@immae.eu | ||
122 | mail_attribute_dict = file:%h/dovecot-attributes | ||
123 | imap_idle_notify_interval = 20 mins | ||
124 | namespace inbox { | ||
125 | type = private | ||
126 | separator = / | ||
127 | inbox = yes | ||
128 | list = yes | ||
129 | } | ||
130 | '' | ||
131 | |||
132 | # ACL | ||
133 | '' | ||
134 | mail_plugins = $mail_plugins acl | ||
135 | plugin { | ||
136 | acl = vfile:${pkgs.writeText "dovecot-acl" '' | ||
137 | Backup/* owner lrp | ||
138 | ''} | ||
139 | acl_globals_only = yes | ||
140 | } | ||
141 | '' | ||
142 | |||
143 | # Full text search | ||
144 | '' | ||
145 | # needs to be bigger than any mailbox size | ||
146 | default_vsz_limit = 2GB | ||
147 | mail_plugins = $mail_plugins fts fts_xapian | ||
148 | plugin { | ||
149 | plugin = fts fts_xapian | ||
150 | fts = xapian | ||
151 | fts_xapian = partial=2 full=20 | ||
152 | fts_autoindex = yes | ||
153 | fts_autoindex_exclude = \Junk | ||
154 | fts_autoindex_exclude2 = \Trash | ||
155 | fts_autoindex_exclude3 = Virtual/* | ||
156 | } | ||
157 | '' | ||
158 | |||
159 | # Antispam | ||
160 | # https://docs.iredmail.org/dovecot.imapsieve.html | ||
161 | '' | ||
162 | # imap_sieve plugin added below | ||
163 | |||
164 | plugin { | ||
165 | sieve_plugins = sieve_imapsieve sieve_extprograms | ||
166 | imapsieve_url = sieve://127.0.0.1:4190 | ||
167 | |||
168 | sieve_before = file:${./sieve_scripts}/backup.sieve;bindir=/var/lib/vhost/.sieve_bin | ||
169 | |||
170 | # From elsewhere to Junk folder | ||
171 | imapsieve_mailbox1_name = Junk | ||
172 | imapsieve_mailbox1_causes = COPY APPEND | ||
173 | imapsieve_mailbox1_before = file:${./sieve_scripts}/report_spam.sieve;bindir=/var/lib/vhost/.imapsieve_bin | ||
174 | |||
175 | # From Junk folder to elsewhere | ||
176 | imapsieve_mailbox2_name = * | ||
177 | imapsieve_mailbox2_from = Junk | ||
178 | imapsieve_mailbox2_causes = COPY | ||
179 | imapsieve_mailbox2_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin | ||
180 | |||
181 | # From anywhere to NoJunk folder | ||
182 | imapsieve_mailbox3_name = NoJunk | ||
183 | imapsieve_mailbox3_causes = COPY APPEND | ||
184 | imapsieve_mailbox3_before = file:${./sieve_scripts}/report_ham.sieve;bindir=/var/lib/vhost/.imapsieve_bin | ||
185 | |||
186 | sieve_pipe_bin_dir = ${sieve_bin} | ||
187 | |||
188 | sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment | ||
189 | } | ||
190 | '' | ||
191 | # Services to listen | ||
192 | '' | ||
193 | service imap-login { | ||
194 | inet_listener imap { | ||
195 | } | ||
196 | inet_listener imaps { | ||
197 | } | ||
198 | } | ||
199 | service pop3-login { | ||
200 | inet_listener pop3 { | ||
201 | } | ||
202 | inet_listener pop3s { | ||
203 | } | ||
204 | } | ||
205 | service imap { | ||
206 | } | ||
207 | service pop3 { | ||
208 | } | ||
209 | service auth { | ||
210 | unix_listener auth-userdb { | ||
211 | } | ||
212 | unix_listener ${config.services.postfix.config.queue_directory}/private/auth { | ||
213 | mode = 0666 | ||
214 | } | ||
215 | } | ||
216 | service auth-worker { | ||
217 | } | ||
218 | service dict { | ||
219 | unix_listener dict { | ||
220 | } | ||
221 | } | ||
222 | service stats { | ||
223 | unix_listener stats-reader { | ||
224 | user = vhost | ||
225 | group = vhost | ||
226 | mode = 0660 | ||
227 | } | ||
228 | unix_listener stats-writer { | ||
229 | user = vhost | ||
230 | group = vhost | ||
231 | mode = 0660 | ||
232 | } | ||
233 | } | ||
234 | '' | ||
235 | |||
236 | # Authentification | ||
237 | '' | ||
238 | first_valid_uid = ${toString config.ids.uids.vhost} | ||
239 | disable_plaintext_auth = yes | ||
240 | passdb { | ||
241 | driver = ldap | ||
242 | args = ${config.secrets.fullPaths."dovecot/ldap"} | ||
243 | } | ||
244 | userdb { | ||
245 | driver = ldap | ||
246 | args = ${config.secrets.fullPaths."dovecot/ldap"} | ||
247 | } | ||
248 | '' | ||
249 | |||
250 | # Zlib | ||
251 | '' | ||
252 | mail_plugins = $mail_plugins zlib | ||
253 | plugin { | ||
254 | zlib_save_level = 6 | ||
255 | zlib_save = gz | ||
256 | } | ||
257 | '' | ||
258 | |||
259 | # Sieve | ||
260 | '' | ||
261 | plugin { | ||
262 | sieve = file:~/sieve;bindir=~/.sieve-bin;active=~/.dovecot.sieve | ||
263 | } | ||
264 | service managesieve-login { | ||
265 | } | ||
266 | service managesieve { | ||
267 | } | ||
268 | '' | ||
269 | |||
270 | # Virtual mailboxes | ||
271 | '' | ||
272 | mail_plugins = $mail_plugins virtual | ||
273 | namespace Virtual { | ||
274 | prefix = Virtual/ | ||
275 | location = virtual:~/Virtual | ||
276 | } | ||
277 | '' | ||
278 | |||
279 | # Protocol specific configuration | ||
280 | # Needs to come last if there are mail_plugins entries | ||
281 | '' | ||
282 | protocol imap { | ||
283 | mail_plugins = $mail_plugins imap_sieve imap_acl | ||
284 | } | ||
285 | protocol lda { | ||
286 | mail_plugins = $mail_plugins sieve | ||
287 | } | ||
288 | '' | ||
289 | ]; | ||
290 | }; | ||
291 | networking.firewall.allowedTCPPorts = [ 110 143 993 995 4190 ]; | ||
292 | system.activationScripts.dovecot = { | ||
293 | deps = [ "users" ]; | ||
294 | text ='' | ||
295 | install -m 0755 -o vhost -g vhost -d /var/lib/vhost | ||
296 | ''; | ||
297 | }; | ||
298 | |||
299 | services.cron.systemCronJobs = let | ||
300 | cron_script = pkgs.writeScriptBin "cleanup-imap-folders" '' | ||
301 | ${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" | ||
302 | ${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" | ||
303 | ${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" | ||
304 | ''; | ||
305 | in | ||
306 | [ | ||
307 | "0 2 * * * root ${cron_script}/bin/cleanup-imap-folders" | ||
308 | ]; | ||
309 | security.acme.certs."mail-rsa" = { | ||
310 | postRun = '' | ||
311 | systemctl restart dovecot2.service | ||
312 | ''; | ||
313 | extraDomainNames = [ "imap.immae.eu" "pop3.immae.eu" ]; | ||
314 | }; | ||
315 | security.acme.certs."mail" = { | ||
316 | postRun = '' | ||
317 | systemctl restart dovecot2.service | ||
318 | ''; | ||
319 | extraDomainNames = [ "imap.immae.eu" "pop3.immae.eu" ]; | ||
320 | }; | ||
321 | myServices.monitoring.fromMasterActivatedPlugins = [ "imap" "tcp" ]; | ||
322 | myServices.monitoring.fromMasterObjects.service = [ | ||
323 | { | ||
324 | service_description = "imap connection works"; | ||
325 | host_name = config.hostEnv.fqdn; | ||
326 | use = "external-service"; | ||
327 | check_command = "check_imap_connection"; | ||
328 | |||
329 | servicegroups = "webstatus-remote-services,webstatus-email"; | ||
330 | _webstatus_name = "IMAP"; | ||
331 | _webstatus_url = "imap.immae.eu"; | ||
332 | } | ||
333 | |||
334 | { | ||
335 | service_description = "imap SSL is up to date"; | ||
336 | host_name = config.hostEnv.fqdn; | ||
337 | use = "external-service"; | ||
338 | check_command = ["check_tcp_ssl" "993"]; | ||
339 | |||
340 | servicegroups = "webstatus-ssl"; | ||
341 | _webstatus_name = "IMAP"; | ||
342 | _webstatus_url = "imap.immae.eu"; | ||
343 | } | ||
344 | |||
345 | ]; | ||
346 | }; | ||
347 | } | ||
348 | |||
diff --git a/systems/eldiron/mail/postfix.nix b/systems/eldiron/mail/postfix.nix new file mode 100644 index 0000000..f95ee1b --- /dev/null +++ b/systems/eldiron/mail/postfix.nix | |||
@@ -0,0 +1,497 @@ | |||
1 | { lib, pkgs, config, options, ... }: | ||
2 | let | ||
3 | getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) (lib.filterAttrs (n: v: v.receive) p.emailPolicies); | ||
4 | bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones; | ||
5 | receiving_domains = lib.flatten (builtins.attrValues bydomain); | ||
6 | in | ||
7 | { | ||
8 | options.services.postfix.submissionOptions' = options.services.postfix.submissionOptions // { | ||
9 | type = with lib.types; attrsOf (either str (listOf str)); | ||
10 | apply = builtins.mapAttrs (n: v: if builtins.isList v then builtins.concatStringsSep "," v else v); | ||
11 | }; | ||
12 | config = lib.mkIf config.myServices.mail.enable { | ||
13 | myServices.dns.zones."immae.eu" = with config.myServices.dns.helpers; lib.mkMerge [ | ||
14 | mailMX | ||
15 | (mailCommon "immae.eu") | ||
16 | mailSend | ||
17 | { | ||
18 | # Virtual forwards and mailboxes for real users | ||
19 | emailPolicies."mail".receive = true; | ||
20 | # multi-domain generic mails: | ||
21 | # hostmaster, cron, httpd, naemon, postmaster | ||
22 | # system virtual mailboxes: | ||
23 | # devnull, printer, testconnect | ||
24 | emailPolicies."".receive = true; | ||
25 | subdomains.mail = lib.mkMerge [ (mailCommon "immae.eu") mailSend ]; | ||
26 | subdomains.smtp = ips servers.eldiron.ips.main; | ||
27 | |||
28 | # DMARC reports | ||
29 | subdomains._dmarc.subdomains._report.subdomains = let | ||
30 | getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) p.emailPolicies; | ||
31 | bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones; | ||
32 | hostsWithMail = lib.flatten (builtins.attrValues bydomain); | ||
33 | nvpairs = builtins.map (e: { name = e; value = { TXT = [ "v=DMARC1;" ]; }; }) hostsWithMail; | ||
34 | in | ||
35 | builtins.listToAttrs nvpairs; | ||
36 | } | ||
37 | ]; | ||
38 | |||
39 | myServices.chatonsProperties.hostings.mx-backup = { | ||
40 | file.datetime = "2022-08-22T01:00:00"; | ||
41 | hosting = { | ||
42 | name = "MX Backup"; | ||
43 | description = "Serveur e-mail secondaire"; | ||
44 | logo = "https://www.postfix.org/favicon.ico"; | ||
45 | website = "https://mail.immae.eu/"; | ||
46 | status.level = "OK"; | ||
47 | status.description = "OK"; | ||
48 | registration.load = "OPEN"; | ||
49 | install.type = "PACKAGE"; | ||
50 | }; | ||
51 | software = { | ||
52 | name = "Postfix"; | ||
53 | website = "http://www.postfix.org/"; | ||
54 | license.url = "http://postfix.mirrors.ovh.net/postfix-release/LICENSE"; | ||
55 | license.name = "Eclipse Public license (EPL 2.0) and IBM Public License (IPL 1.0)"; | ||
56 | version = pkgs.postfix.version; | ||
57 | source.url = "http://www.postfix.org/download.html"; | ||
58 | }; | ||
59 | }; | ||
60 | secrets.keys = { | ||
61 | "postfix/mysql_alias_maps" = { | ||
62 | user = config.services.postfix.user; | ||
63 | group = config.services.postfix.group; | ||
64 | permissions = "0440"; | ||
65 | text = '' | ||
66 | # We need to specify that option to trigger ssl connection | ||
67 | tls_ciphers = TLSv1.2 | ||
68 | user = ${config.myEnv.mail.postfix.mysql.user} | ||
69 | password = ${config.myEnv.mail.postfix.mysql.password} | ||
70 | hosts = unix:${config.myEnv.mail.postfix.mysql.socket} | ||
71 | dbname = ${config.myEnv.mail.postfix.mysql.database} | ||
72 | query = SELECT DISTINCT destination | ||
73 | FROM forwardings | ||
74 | WHERE | ||
75 | ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s')) | ||
76 | AND active = 1 | ||
77 | AND '%s' NOT IN | ||
78 | ( | ||
79 | SELECT source | ||
80 | FROM forwardings_blacklisted | ||
81 | WHERE source = '%s' | ||
82 | ) UNION | ||
83 | SELECT 'devnull@immae.eu' | ||
84 | FROM forwardings_blacklisted | ||
85 | WHERE source = '%s' | ||
86 | ''; | ||
87 | }; | ||
88 | "postfix/ldap_mailboxes" = { | ||
89 | user = config.services.postfix.user; | ||
90 | group = config.services.postfix.group; | ||
91 | permissions = "0440"; | ||
92 | text = '' | ||
93 | server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636 | ||
94 | search_base = ${config.myEnv.mail.dovecot.ldap.base} | ||
95 | query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter} | ||
96 | bind_dn = ${config.myEnv.mail.dovecot.ldap.dn} | ||
97 | bind_pw = ${config.myEnv.mail.dovecot.ldap.password} | ||
98 | result_attribute = immaePostfixAddress | ||
99 | result_format = dummy | ||
100 | version = 3 | ||
101 | ''; | ||
102 | }; | ||
103 | "postfix/mysql_sender_login_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 | query = SELECT DISTINCT destination | ||
115 | FROM forwardings | ||
116 | WHERE | ||
117 | ( | ||
118 | (regex = 1 AND CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') REGEXP CONCAT('^',source,'$') ) | ||
119 | OR | ||
120 | (regex = 0 AND source = CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d')) | ||
121 | ) | ||
122 | AND active = 1 | ||
123 | UNION SELECT CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AS destination | ||
124 | ''; | ||
125 | }; | ||
126 | "postfix/mysql_sender_relays_maps" = { | ||
127 | user = config.services.postfix.user; | ||
128 | group = config.services.postfix.group; | ||
129 | permissions = "0440"; | ||
130 | text = '' | ||
131 | # We need to specify that option to trigger ssl connection | ||
132 | tls_ciphers = TLSv1.2 | ||
133 | user = ${config.myEnv.mail.postfix.mysql.user} | ||
134 | password = ${config.myEnv.mail.postfix.mysql.password} | ||
135 | hosts = unix:${config.myEnv.mail.postfix.mysql.socket} | ||
136 | dbname = ${config.myEnv.mail.postfix.mysql.database} | ||
137 | # INSERT INTO sender_relays | ||
138 | # (`from`, owner, relay, login, password, regex, active) | ||
139 | # VALUES | ||
140 | # ( 'sender@otherhost.org' | ||
141 | # , 'me@mail.immae.eu' | ||
142 | # , '[otherhost.org]:587' | ||
143 | # , 'otherhostlogin' | ||
144 | # , AES_ENCRYPT('otherhostpassword', '${config.myEnv.mail.postfix.mysql.password_encrypt}') | ||
145 | # , '0' | ||
146 | # , '1'); | ||
147 | |||
148 | query = SELECT DISTINCT `owner` | ||
149 | FROM sender_relays | ||
150 | WHERE | ||
151 | ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) | ||
152 | AND active = 1 | ||
153 | ''; | ||
154 | }; | ||
155 | "postfix/mysql_sender_relays_hosts" = { | ||
156 | user = config.services.postfix.user; | ||
157 | group = config.services.postfix.group; | ||
158 | permissions = "0440"; | ||
159 | text = '' | ||
160 | # We need to specify that option to trigger ssl connection | ||
161 | tls_ciphers = TLSv1.2 | ||
162 | user = ${config.myEnv.mail.postfix.mysql.user} | ||
163 | password = ${config.myEnv.mail.postfix.mysql.password} | ||
164 | hosts = unix:${config.myEnv.mail.postfix.mysql.socket} | ||
165 | dbname = ${config.myEnv.mail.postfix.mysql.database} | ||
166 | |||
167 | query = SELECT DISTINCT relay | ||
168 | FROM sender_relays | ||
169 | WHERE | ||
170 | ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) | ||
171 | AND active = 1 | ||
172 | ''; | ||
173 | }; | ||
174 | "postfix/mysql_sender_relays_creds" = { | ||
175 | user = config.services.postfix.user; | ||
176 | group = config.services.postfix.group; | ||
177 | permissions = "0440"; | ||
178 | text = '' | ||
179 | # We need to specify that option to trigger ssl connection | ||
180 | tls_ciphers = TLSv1.2 | ||
181 | user = ${config.myEnv.mail.postfix.mysql.user} | ||
182 | password = ${config.myEnv.mail.postfix.mysql.password} | ||
183 | hosts = unix:${config.myEnv.mail.postfix.mysql.socket} | ||
184 | dbname = ${config.myEnv.mail.postfix.mysql.database} | ||
185 | |||
186 | query = SELECT DISTINCT CONCAT(`login`, ':', AES_DECRYPT(`password`, '${config.myEnv.mail.postfix.mysql.password_encrypt}')) | ||
187 | FROM sender_relays | ||
188 | WHERE | ||
189 | ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) | ||
190 | AND active = 1 | ||
191 | ''; | ||
192 | }; | ||
193 | "postfix/ldap_ejabberd_users_immae_fr" = { | ||
194 | user = config.services.postfix.user; | ||
195 | group = config.services.postfix.group; | ||
196 | permissions = "0440"; | ||
197 | text = '' | ||
198 | server_host = ldaps://${config.myEnv.jabber.ldap.host}:636 | ||
199 | search_base = ${config.myEnv.jabber.ldap.base} | ||
200 | query_filter = ${config.myEnv.jabber.postfix_user_filter} | ||
201 | domain = immae.fr | ||
202 | bind_dn = ${config.myEnv.jabber.ldap.dn} | ||
203 | bind_pw = ${config.myEnv.jabber.ldap.password} | ||
204 | result_attribute = immaeXmppUid | ||
205 | result_format = ejabberd@localhost | ||
206 | version = 3 | ||
207 | ''; | ||
208 | }; | ||
209 | }; | ||
210 | |||
211 | networking.firewall.allowedTCPPorts = [ 25 465 587 ]; | ||
212 | |||
213 | users.users.postfixscripts = { | ||
214 | group = "keys"; | ||
215 | uid = config.ids.uids.postfixscripts; | ||
216 | description = "Postfix scripts user"; | ||
217 | }; | ||
218 | users.users."${config.services.postfix.user}".extraGroups = [ "keys" ]; | ||
219 | services.filesWatcher.postfix = { | ||
220 | restart = true; | ||
221 | paths = [ | ||
222 | config.secrets.fullPaths."postfix/mysql_alias_maps" | ||
223 | config.secrets.fullPaths."postfix/ldap_mailboxes" | ||
224 | config.secrets.fullPaths."postfix/mysql_sender_login_maps" | ||
225 | config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr" | ||
226 | ]; | ||
227 | }; | ||
228 | services.postfix = { | ||
229 | extraAliases = let | ||
230 | testmail = pkgs.writeScript "testmail" '' | ||
231 | #! ${pkgs.stdenv.shell} | ||
232 | ${pkgs.coreutils}/bin/touch \ | ||
233 | "/var/lib/naemon/checks/email/$(${pkgs.procmail}/bin/formail -x To: | ${pkgs.coreutils}/bin/tr -d ' <>')" | ||
234 | ''; | ||
235 | in | ||
236 | ''testmail: "|${testmail}"''; | ||
237 | mapFiles = let | ||
238 | virtual_map = { | ||
239 | virtual = let | ||
240 | cfg = config.myEnv.monitoring.email_check.eldiron; | ||
241 | address = "${cfg.mail_address}@${cfg.mail_domain}"; | ||
242 | aliases = config.myEnv.mail.postfix.common_aliases; | ||
243 | admins = builtins.concatStringsSep "," config.myEnv.mail.postfix.admins; | ||
244 | in pkgs.writeText "postfix-virtual" ( | ||
245 | builtins.concatStringsSep "\n" ( | ||
246 | [ "${address} testmail@localhost" | ||
247 | ] ++ | ||
248 | map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases | ||
249 | ++ lib.lists.flatten ( | ||
250 | map (domain: | ||
251 | map (alias: "${alias}@${domain} ${admins}") aliases | ||
252 | ) receiving_domains | ||
253 | ) | ||
254 | )); | ||
255 | }; | ||
256 | sasl_access = { | ||
257 | host_sender_login = with lib.attrsets; let | ||
258 | addresses = zipAttrs (lib.flatten (mapAttrsToList | ||
259 | (n: v: (map (e: { "${e}" = "${n}@immae.eu"; }) v.emails)) config.myEnv.servers)); | ||
260 | aliases = config.myEnv.mail.postfix.common_aliases; | ||
261 | joined = builtins.concatStringsSep ","; | ||
262 | admins = joined config.myEnv.mail.postfix.admins; | ||
263 | in pkgs.writeText "host-sender-login" | ||
264 | (builtins.concatStringsSep "\n" ( | ||
265 | mapAttrsToList (n: v: "${n} ${joined v}") addresses | ||
266 | ++ lib.lists.flatten ( | ||
267 | map (domain: | ||
268 | map (alias: "${alias}@${domain} ${admins}") aliases | ||
269 | ) receiving_domains | ||
270 | ) | ||
271 | ++ map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases | ||
272 | )); | ||
273 | }; | ||
274 | in | ||
275 | virtual_map // sasl_access; | ||
276 | config = { | ||
277 | ### postfix module overrides | ||
278 | readme_directory = "${pkgs.postfix}/share/postfix/doc"; | ||
279 | smtp_tls_CAfile = lib.mkForce ""; | ||
280 | smtp_tls_cert_file = lib.mkForce ""; | ||
281 | smtp_tls_key_file = lib.mkForce ""; | ||
282 | |||
283 | message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited" | ||
284 | mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts | ||
285 | alias_database = "\$alias_maps"; | ||
286 | |||
287 | ### Aliases scripts user | ||
288 | default_privs = "postfixscripts"; | ||
289 | |||
290 | ### Virtual mailboxes config | ||
291 | virtual_alias_maps = [ | ||
292 | "hash:/etc/postfix/virtual" | ||
293 | "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}" | ||
294 | "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}" | ||
295 | ]; | ||
296 | virtual_mailbox_domains = receiving_domains; | ||
297 | virtual_mailbox_maps = [ | ||
298 | "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}" | ||
299 | ]; | ||
300 | dovecot_destination_recipient_limit = "1"; | ||
301 | virtual_transport = "dovecot"; | ||
302 | |||
303 | ### Relay domains | ||
304 | smtpd_relay_restrictions = [ | ||
305 | "defer_unauth_destination" | ||
306 | ]; | ||
307 | |||
308 | ### Additional smtpd configuration | ||
309 | smtpd_tls_received_header = "yes"; | ||
310 | smtpd_tls_loglevel = "1"; | ||
311 | |||
312 | ### Email sending configuration | ||
313 | smtp_tls_security_level = "may"; | ||
314 | smtp_tls_loglevel = "1"; | ||
315 | |||
316 | ### Force ip bind for smtp | ||
317 | smtp_bind_address = builtins.head config.hostEnv.ips.main.ip4; | ||
318 | smtp_bind_address6 = builtins.head config.hostEnv.ips.main.ip6; | ||
319 | |||
320 | # Use some relays when authorized senders are not myself | ||
321 | smtp_sasl_mechanism_filter = [ | ||
322 | "plain" | ||
323 | "login" | ||
324 | ]; # GSSAPI Not correctly supported by postfix | ||
325 | smtp_sasl_auth_enable = "yes"; | ||
326 | smtp_sasl_password_maps = [ | ||
327 | "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_creds"}" | ||
328 | ]; | ||
329 | smtp_sasl_security_options = "noanonymous"; | ||
330 | smtp_sender_dependent_authentication = "yes"; | ||
331 | sender_dependent_relayhost_maps = [ | ||
332 | "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_hosts"}" | ||
333 | ]; | ||
334 | |||
335 | ### opendkim, opendmarc, openarc milters | ||
336 | non_smtpd_milters = [ | ||
337 | "unix:${config.myServices.mail.milters.sockets.opendkim}" | ||
338 | ]; | ||
339 | smtpd_milters = [ | ||
340 | "unix:${config.myServices.mail.milters.sockets.opendkim}" | ||
341 | "unix:${config.myServices.mail.milters.sockets.openarc}" | ||
342 | "unix:${config.myServices.mail.milters.sockets.opendmarc}" | ||
343 | ]; | ||
344 | |||
345 | smtp_use_tls = true; | ||
346 | smtpd_use_tls = true; | ||
347 | smtpd_tls_chain_files = [ | ||
348 | "/var/lib/acme/mail/full.pem" | ||
349 | "/var/lib/acme/mail-rsa/full.pem" | ||
350 | ]; | ||
351 | |||
352 | maximal_queue_lifetime = "6w"; | ||
353 | bounce_queue_lifetime = "6w"; | ||
354 | }; | ||
355 | enable = true; | ||
356 | enableSmtp = true; | ||
357 | enableSubmission = true; | ||
358 | submissionOptions = config.services.postfix.submissionOptions'; | ||
359 | submissionOptions' = { | ||
360 | # Don’t use "long form", only commas (cf | ||
361 | # http://www.postfix.org/master.5.html long form is not handled | ||
362 | # well by the submission function) | ||
363 | smtpd_tls_security_level = "encrypt"; | ||
364 | smtpd_sasl_auth_enable = "yes"; | ||
365 | smtpd_tls_auth_only = "yes"; | ||
366 | smtpd_sasl_tls_security_options = "noanonymous"; | ||
367 | smtpd_sasl_type = "dovecot"; | ||
368 | smtpd_sasl_path = "private/auth"; | ||
369 | smtpd_reject_unlisted_recipient = "no"; | ||
370 | smtpd_client_restrictions = [ | ||
371 | "permit_sasl_authenticated" | ||
372 | "reject" | ||
373 | ]; | ||
374 | smtpd_relay_restrictions = [ | ||
375 | "permit_sasl_authenticated" | ||
376 | "reject" | ||
377 | ]; | ||
378 | # Refuse to send e-mails with a From that is not handled | ||
379 | smtpd_sender_restrictions = [ | ||
380 | "reject_sender_login_mismatch" | ||
381 | "reject_unlisted_sender" | ||
382 | "permit_sasl_authenticated,reject" | ||
383 | ]; | ||
384 | smtpd_sender_login_maps = [ | ||
385 | "hash:/etc/postfix/host_sender_login" | ||
386 | "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_maps"}" | ||
387 | "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}" | ||
388 | ]; | ||
389 | smtpd_recipient_restrictions = [ | ||
390 | "permit_sasl_authenticated" | ||
391 | "reject" | ||
392 | ]; | ||
393 | milter_macro_daemon_name = "ORIGINATING"; | ||
394 | smtpd_milters = [ | ||
395 | # FIXME: put it back when opensmtpd is upgraded and able to | ||
396 | # rewrite the from header | ||
397 | #"unix:/run/milter_verify_from/verify_from.sock" | ||
398 | "unix:${config.myServices.mail.milters.sockets.opendkim}" | ||
399 | ]; | ||
400 | }; | ||
401 | destination = ["localhost"]; | ||
402 | # This needs to reverse DNS | ||
403 | hostname = config.hostEnv.fqdn; | ||
404 | setSendmail = true; | ||
405 | recipientDelimiter = "+"; | ||
406 | masterConfig = { | ||
407 | submissions = { | ||
408 | type = "inet"; | ||
409 | private = false; | ||
410 | command = "smtpd"; | ||
411 | args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let | ||
412 | mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ]; | ||
413 | in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions) | ||
414 | ); | ||
415 | }; | ||
416 | dovecot = { | ||
417 | type = "unix"; | ||
418 | privileged = true; | ||
419 | chroot = false; | ||
420 | command = "pipe"; | ||
421 | args = let | ||
422 | # rspamd could be used as a milter, but then it cannot apply | ||
423 | # its checks "per user" (milter is not yet dispatched to | ||
424 | # users), so we wrap dovecot-lda inside rspamc per recipient | ||
425 | # here. | ||
426 | rspamc_dovecot = pkgs.writeScriptBin "rspamc_dovecot" '' | ||
427 | #! ${pkgs.stdenv.shell} | ||
428 | set -o pipefail | ||
429 | sender="$1" | ||
430 | original_recipient="$2" | ||
431 | user="$3" | ||
432 | |||
433 | ${pkgs.coreutils}/bin/cat - | \ | ||
434 | ${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d "$user" --mime | \ | ||
435 | ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user" | ||
436 | if echo ''${PIPESTATUS[@]} | ${pkgs.gnugrep}/bin/grep -qE '^[0 ]+$'; then | ||
437 | exit 0 | ||
438 | else | ||
439 | # src/global/sys_exits.h to retry | ||
440 | exit 75 | ||
441 | fi | ||
442 | ''; | ||
443 | in [ | ||
444 | "flags=ODRhu" "user=vhost:vhost" | ||
445 | "argv=${rspamc_dovecot}/bin/rspamc_dovecot \${sender} \${original_recipient} \${user}@\${nexthop}" | ||
446 | ]; | ||
447 | }; | ||
448 | }; | ||
449 | }; | ||
450 | security.acme.certs."mail" = { | ||
451 | postRun = '' | ||
452 | systemctl restart postfix.service | ||
453 | ''; | ||
454 | extraDomainNames = [ "smtp.immae.eu" ]; | ||
455 | }; | ||
456 | security.acme.certs."mail-rsa" = { | ||
457 | postRun = '' | ||
458 | systemctl restart postfix.service | ||
459 | ''; | ||
460 | extraDomainNames = [ "smtp.immae.eu" ]; | ||
461 | }; | ||
462 | system.activationScripts.testmail = { | ||
463 | deps = [ "users" ]; | ||
464 | text = let | ||
465 | allCfg = config.myEnv.monitoring.email_check; | ||
466 | cfg = allCfg.eldiron; | ||
467 | reverseTargets = builtins.attrNames (lib.attrsets.filterAttrs (k: v: builtins.elem "eldiron" v.targets) allCfg); | ||
468 | to_email = cfg': host': | ||
469 | let sep = if lib.strings.hasInfix "+" cfg'.mail_address then "_" else "+"; | ||
470 | in "${cfg'.mail_address}${sep}${host'}@${cfg'.mail_domain}"; | ||
471 | mails_to_receive = builtins.concatStringsSep " " (map (to_email cfg) reverseTargets); | ||
472 | in '' | ||
473 | install -m 0555 -o postfixscripts -g keys -d /var/lib/naemon/checks/email | ||
474 | for f in ${mails_to_receive}; do | ||
475 | if [ ! -f /var/lib/naemon/checks/email/$f ]; then | ||
476 | install -m 0644 -o postfixscripts -g keys /dev/null -T /var/lib/naemon/checks/email/$f | ||
477 | touch -m -d @0 /var/lib/naemon/checks/email/$f | ||
478 | fi | ||
479 | done | ||
480 | ''; | ||
481 | }; | ||
482 | systemd.services.postfix.serviceConfig.Slice = "mail.slice"; | ||
483 | |||
484 | myServices.monitoring.fromMasterObjects.service = [ | ||
485 | { | ||
486 | service_description = "postfix SSL is up to date"; | ||
487 | host_name = config.hostEnv.fqdn; | ||
488 | use = "external-service"; | ||
489 | check_command = "check_smtp"; | ||
490 | |||
491 | servicegroups = "webstatus-ssl"; | ||
492 | _webstatus_name = "SMTP"; | ||
493 | _webstatus_url = "smtp.immae.eu"; | ||
494 | } | ||
495 | ]; | ||
496 | }; | ||
497 | } | ||
diff --git a/systems/eldiron/mail/rspamd.nix b/systems/eldiron/mail/rspamd.nix new file mode 100644 index 0000000..a300cc5 --- /dev/null +++ b/systems/eldiron/mail/rspamd.nix | |||
@@ -0,0 +1,88 @@ | |||
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 | systemd.services.rspamd.serviceConfig.SupplementaryGroups = [ "vhost" ]; | ||
30 | services.rspamd = { | ||
31 | enable = true; | ||
32 | debug = false; | ||
33 | overrides = { | ||
34 | "actions.conf".text = '' | ||
35 | reject = null; | ||
36 | add_header = 6; | ||
37 | greylist = null; | ||
38 | ''; | ||
39 | "milter_headers.conf".text = '' | ||
40 | extended_spam_headers = true; | ||
41 | ''; | ||
42 | }; | ||
43 | locals = { | ||
44 | "redis.conf".text = '' | ||
45 | servers = "${config.myEnv.mail.rspamd.redis.socket}"; | ||
46 | db = "${config.myEnv.mail.rspamd.redis.db}"; | ||
47 | ''; | ||
48 | "classifier-bayes.conf".text = '' | ||
49 | users_enabled = true; | ||
50 | backend = "redis"; | ||
51 | servers = "${config.myEnv.mail.rspamd.redis.socket}"; | ||
52 | database = "${config.myEnv.mail.rspamd.redis.db}"; | ||
53 | autolearn = true; | ||
54 | cache { | ||
55 | backend = "redis"; | ||
56 | } | ||
57 | new_schema = true; | ||
58 | statfile { | ||
59 | BAYES_HAM { | ||
60 | spam = false; | ||
61 | } | ||
62 | BAYES_SPAM { | ||
63 | spam = true; | ||
64 | } | ||
65 | } | ||
66 | ''; | ||
67 | }; | ||
68 | workers = { | ||
69 | controller = { | ||
70 | extraConfig = '' | ||
71 | enable_password = "${config.myEnv.mail.rspamd.write_password_hashed}"; | ||
72 | password = "${config.myEnv.mail.rspamd.read_password_hashed}"; | ||
73 | ''; | ||
74 | bindSockets = [ { | ||
75 | socket = config.myServices.mail.rspamd.sockets.worker-controller; | ||
76 | mode = "0660"; | ||
77 | owner = config.services.rspamd.user; | ||
78 | group = "vhost"; | ||
79 | } ]; | ||
80 | }; | ||
81 | }; | ||
82 | postfix = { | ||
83 | enable = true; | ||
84 | config = {}; | ||
85 | }; | ||
86 | }; | ||
87 | }; | ||
88 | } | ||
diff --git a/systems/eldiron/mail/scan_reported_mails b/systems/eldiron/mail/scan_reported_mails new file mode 100755 index 0000000..fe9f4d6 --- /dev/null +++ b/systems/eldiron/mail/scan_reported_mails | |||
@@ -0,0 +1,21 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | |||
3 | ( flock -n 9 || exit 1 | ||
4 | shopt -s nullglob | ||
5 | for 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 | ||
20 | done | ||
21 | ) 9>/var/lib/vhost/scan_reported_mails.lock | ||
diff --git a/systems/eldiron/mail/sieve_bin/imapsieve_copy b/systems/eldiron/mail/sieve_bin/imapsieve_copy new file mode 100755 index 0000000..2ca1f23 --- /dev/null +++ b/systems/eldiron/mail/sieve_bin/imapsieve_copy | |||
@@ -0,0 +1,8 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | # Inspired from https://docs.iredmail.org/dovecot.imapsieve.html | ||
3 | |||
4 | MSG_TYPE="$1" | ||
5 | OUTPUT_DIR="/var/lib/vhost/.rspamd/${USER}/pending/${MSG_TYPE}" | ||
6 | FILE="${OUTPUT_DIR}/$(date +%Y%m%d%H%M%S)-${RANDOM}${RANDOM}.eml" | ||
7 | mkdir -p "${OUTPUT_DIR}" | ||
8 | cat > ${FILE} < /dev/stdin | ||
diff --git a/systems/eldiron/mail/sieve_scripts/backup.sieve b/systems/eldiron/mail/sieve_scripts/backup.sieve new file mode 100644 index 0000000..3014c0a --- /dev/null +++ b/systems/eldiron/mail/sieve_scripts/backup.sieve | |||
@@ -0,0 +1,7 @@ | |||
1 | # vim: filetype=sieve | ||
2 | require ["copy","mailbox","fileinto","regex"]; | ||
3 | if header :is "X-Spam" "Yes" { | ||
4 | fileinto :create :copy "Backup/Spam"; | ||
5 | } else { | ||
6 | fileinto :create :copy "Backup/Ham"; | ||
7 | } | ||
diff --git a/systems/eldiron/mail/sieve_scripts/report_ham.sieve b/systems/eldiron/mail/sieve_scripts/report_ham.sieve new file mode 100644 index 0000000..f9b8481 --- /dev/null +++ b/systems/eldiron/mail/sieve_scripts/report_ham.sieve | |||
@@ -0,0 +1,11 @@ | |||
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; | ||
2 | |||
3 | if environment :matches "imap.mailbox" "*" { | ||
4 | set "mailbox" "${1}"; | ||
5 | } | ||
6 | |||
7 | if string "${mailbox}" "Trash" { | ||
8 | stop; | ||
9 | } | ||
10 | |||
11 | pipe :copy "imapsieve_copy" [ "ham" ]; | ||
diff --git a/systems/eldiron/mail/sieve_scripts/report_spam.sieve b/systems/eldiron/mail/sieve_scripts/report_spam.sieve new file mode 100644 index 0000000..9a1f794 --- /dev/null +++ b/systems/eldiron/mail/sieve_scripts/report_spam.sieve | |||
@@ -0,0 +1,3 @@ | |||
1 | require ["vnd.dovecot.pipe", "copy", "imapsieve" ]; | ||
2 | |||
3 | pipe :copy "imapsieve_copy" [ "spam" ]; | ||
diff --git a/systems/eldiron/mail/sympa.nix b/systems/eldiron/mail/sympa.nix new file mode 100644 index 0000000..8e801dd --- /dev/null +++ b/systems/eldiron/mail/sympa.nix | |||
@@ -0,0 +1,232 @@ | |||
1 | { lib, pkgs, config, ... }: | ||
2 | let | ||
3 | domain = "lists.immae.eu"; | ||
4 | sympaConfig = config.myEnv.mail.sympa; | ||
5 | in | ||
6 | { | ||
7 | config = lib.mkIf config.myServices.mail.enable { | ||
8 | myServices.dns.zones."immae.eu".emailPolicies."lists".receive = true; | ||
9 | myServices.dns.zones."immae.eu".subdomains.lists = | ||
10 | with config.myServices.dns.helpers; lib.mkMerge [ | ||
11 | (ips servers.eldiron.ips.main) | ||
12 | (mailCommon "immae.eu") | ||
13 | mailSend | ||
14 | ]; | ||
15 | |||
16 | myServices.chatonsProperties.services.sympa = { | ||
17 | file.datetime = "2022-08-22T00:50:00"; | ||
18 | service = { | ||
19 | name = "Sympa"; | ||
20 | description = "Mailing lists service"; | ||
21 | website = "https://mail.immae.eu/sympa"; | ||
22 | logo = "https://mail.immae.eu/static-sympa/icons/favicon_sympa.png"; | ||
23 | status.level = "OK"; | ||
24 | status.description = "OK"; | ||
25 | registration."" = ["MEMBER" "CLIENT"]; | ||
26 | registration.load = "OPEN"; | ||
27 | install.type = "PACKAGE"; | ||
28 | }; | ||
29 | software = { | ||
30 | name = "Sympa"; | ||
31 | website = "https://www.sympa.org/"; | ||
32 | license.url = "https://github.com/sympa-community/sympa/blob/sympa-6.2/COPYING"; | ||
33 | license.name = "GNU General Public License v2.0"; | ||
34 | version = pkgs.sympa.version; | ||
35 | source.url = "https://github.com/sympa-community/sympa/"; | ||
36 | }; | ||
37 | }; | ||
38 | myServices.databases.postgresql.authorizedHosts = { | ||
39 | backup-2 = [ | ||
40 | { | ||
41 | username = "sympa"; | ||
42 | database = "sympa"; | ||
43 | ip4 = config.myEnv.servers.backup-2.ips.main.ip4; | ||
44 | ip6 = map (v: "${v}/128") config.myEnv.servers.backup-2.ips.main.ip6; | ||
45 | } | ||
46 | ]; | ||
47 | }; | ||
48 | services.websites.env.tools.vhostConfs.mail = { | ||
49 | extraConfig = lib.mkAfter [ | ||
50 | '' | ||
51 | Alias /static-sympa/ /var/lib/sympa/static_content/ | ||
52 | <Directory /var/lib/sympa/static_content/> | ||
53 | Require all granted | ||
54 | AllowOverride none | ||
55 | </Directory> | ||
56 | <Location /sympa> | ||
57 | SetHandler "proxy:unix:/run/sympa/wwsympa.socket|fcgi://" | ||
58 | Require all granted | ||
59 | </Location> | ||
60 | '' | ||
61 | ]; | ||
62 | }; | ||
63 | |||
64 | secrets.keys = { | ||
65 | "sympa/db_password" = { | ||
66 | permissions = "0400"; | ||
67 | group = "sympa"; | ||
68 | user = "sympa"; | ||
69 | text = sympaConfig.postgresql.password; | ||
70 | }; | ||
71 | } | ||
72 | // lib.mapAttrs' (n: v: lib.nameValuePair "sympa/data_sources/${n}.incl" { | ||
73 | permissions = "0400"; group = "sympa"; user = "sympa"; text = v; | ||
74 | }) sympaConfig.data_sources | ||
75 | // lib.mapAttrs' (n: v: lib.nameValuePair "sympa/scenari/${n}" { | ||
76 | permissions = "0400"; group = "sympa"; user = "sympa"; text = v; | ||
77 | }) sympaConfig.scenari; | ||
78 | users.users.sympa.extraGroups = [ "keys" ]; | ||
79 | systemd.slices.mail-sympa = { | ||
80 | description = "Sympa slice"; | ||
81 | }; | ||
82 | |||
83 | systemd.services.sympa.serviceConfig.SupplementaryGroups = [ "keys" ]; | ||
84 | systemd.services.sympa-archive.serviceConfig.SupplementaryGroups = [ "keys" ]; | ||
85 | systemd.services.sympa-bounce.serviceConfig.SupplementaryGroups = [ "keys" ]; | ||
86 | systemd.services.sympa-bulk.serviceConfig.SupplementaryGroups = [ "keys" ]; | ||
87 | systemd.services.sympa-task.serviceConfig.SupplementaryGroups = [ "keys" ]; | ||
88 | |||
89 | systemd.services.sympa.serviceConfig.Slice = "mail-sympa.slice"; | ||
90 | systemd.services.sympa-archive.serviceConfig.Slice = "mail-sympa.slice"; | ||
91 | systemd.services.sympa-bounce.serviceConfig.Slice = "mail-sympa.slice"; | ||
92 | systemd.services.sympa-bulk.serviceConfig.Slice = "mail-sympa.slice"; | ||
93 | systemd.services.sympa-task.serviceConfig.Slice = "mail-sympa.slice"; | ||
94 | |||
95 | # https://github.com/NixOS/nixpkgs/pull/84202 | ||
96 | systemd.services.sympa.serviceConfig.ProtectKernelModules = lib.mkForce false; | ||
97 | systemd.services.sympa-archive.serviceConfig.ProtectKernelModules = lib.mkForce false; | ||
98 | systemd.services.sympa-bounce.serviceConfig.ProtectKernelModules = lib.mkForce false; | ||
99 | systemd.services.sympa-bulk.serviceConfig.ProtectKernelModules = lib.mkForce false; | ||
100 | systemd.services.sympa-task.serviceConfig.ProtectKernelModules = lib.mkForce false; | ||
101 | systemd.services.sympa.serviceConfig.ProtectKernelTunables = lib.mkForce false; | ||
102 | systemd.services.sympa-archive.serviceConfig.ProtectKernelTunables = lib.mkForce false; | ||
103 | systemd.services.sympa-bounce.serviceConfig.ProtectKernelTunables = lib.mkForce false; | ||
104 | systemd.services.sympa-bulk.serviceConfig.ProtectKernelTunables = lib.mkForce false; | ||
105 | systemd.services.sympa-task.serviceConfig.ProtectKernelTunables = lib.mkForce false; | ||
106 | |||
107 | systemd.services.wwsympa = { | ||
108 | wantedBy = [ "multi-user.target" ]; | ||
109 | after = [ "sympa.service" ]; | ||
110 | serviceConfig = { | ||
111 | Slice = "mail-sympa.slice"; | ||
112 | Type = "forking"; | ||
113 | PIDFile = "/run/sympa/wwsympa.pid"; | ||
114 | Restart = "always"; | ||
115 | ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \ | ||
116 | -u sympa \ | ||
117 | -g sympa \ | ||
118 | -U wwwrun \ | ||
119 | -M 0600 \ | ||
120 | -F 2 \ | ||
121 | -P /run/sympa/wwsympa.pid \ | ||
122 | -s /run/sympa/wwsympa.socket \ | ||
123 | -- ${pkgs.sympa}/lib/sympa/cgi/wwsympa.fcgi | ||
124 | ''; | ||
125 | StateDirectory = "sympa"; | ||
126 | ProtectHome = true; | ||
127 | ProtectSystem = "full"; | ||
128 | ProtectControlGroups = true; | ||
129 | }; | ||
130 | }; | ||
131 | |||
132 | services.postfix = { | ||
133 | mapFiles = { | ||
134 | # Update relay list when changing one of those | ||
135 | sympa_virtual = pkgs.writeText "virtual.sympa" '' | ||
136 | sympa-request@${domain} postmaster@immae.eu | ||
137 | sympa-owner@${domain} postmaster@immae.eu | ||
138 | ''; | ||
139 | sympa_transport = pkgs.writeText "transport.sympa" '' | ||
140 | ${domain} error:User unknown in recipient table | ||
141 | sympa@${domain} sympa:sympa@${domain} | ||
142 | listmaster@${domain} sympa:listmaster@${domain} | ||
143 | bounce@${domain} sympabounce:sympa@${domain} | ||
144 | abuse-feedback-report@${domain} sympabounce:sympa@${domain} | ||
145 | ''; | ||
146 | }; | ||
147 | config = { | ||
148 | transport_maps = lib.mkAfter [ | ||
149 | "hash:/etc/postfix/sympa_transport" | ||
150 | "hash:/var/lib/sympa/sympa_transport" | ||
151 | ]; | ||
152 | virtual_alias_maps = lib.mkAfter [ | ||
153 | "hash:/etc/postfix/sympa_virtual" | ||
154 | ]; | ||
155 | virtual_mailbox_maps = lib.mkAfter [ | ||
156 | "hash:/etc/postfix/sympa_transport" | ||
157 | "hash:/var/lib/sympa/sympa_transport" | ||
158 | "hash:/etc/postfix/sympa_virtual" | ||
159 | ]; | ||
160 | }; | ||
161 | masterConfig = { | ||
162 | sympa = { | ||
163 | type = "unix"; | ||
164 | privileged = true; | ||
165 | chroot = false; | ||
166 | command = "pipe"; | ||
167 | args = [ | ||
168 | "flags=hqRu" | ||
169 | "user=sympa" | ||
170 | "argv=${pkgs.sympa}/libexec/queue" | ||
171 | "\${nexthop}" | ||
172 | ]; | ||
173 | }; | ||
174 | sympabounce = { | ||
175 | type = "unix"; | ||
176 | privileged = true; | ||
177 | chroot = false; | ||
178 | command = "pipe"; | ||
179 | args = [ | ||
180 | "flags=hqRu" | ||
181 | "user=sympa" | ||
182 | "argv=${pkgs.sympa}/libexec/bouncequeue" | ||
183 | "\${nexthop}" | ||
184 | ]; | ||
185 | }; | ||
186 | }; | ||
187 | }; | ||
188 | services.sympa = { | ||
189 | enable = true; | ||
190 | listMasters = sympaConfig.listmasters; | ||
191 | mainDomain = domain; | ||
192 | domains = { | ||
193 | "${domain}" = { | ||
194 | webHost = "mail.immae.eu"; | ||
195 | webLocation = "/sympa"; | ||
196 | }; | ||
197 | }; | ||
198 | |||
199 | database = { | ||
200 | type = "PostgreSQL"; | ||
201 | user = sympaConfig.postgresql.user; | ||
202 | host = sympaConfig.postgresql.socket; | ||
203 | name = sympaConfig.postgresql.database; | ||
204 | passwordFile = config.secrets.fullPaths."sympa/db_password"; | ||
205 | createLocally = false; | ||
206 | }; | ||
207 | settings = { | ||
208 | sendmail = "/run/wrappers/bin/sendmail"; | ||
209 | log_smtp = "on"; | ||
210 | sendmail_aliases = "/var/lib/sympa/sympa_transport"; | ||
211 | aliases_program = "${pkgs.postfix}/bin/postmap"; | ||
212 | create_list = "listmaster"; | ||
213 | }; | ||
214 | settingsFile = { | ||
215 | "virtual.sympa".enable = false; | ||
216 | "transport.sympa".enable = false; | ||
217 | } // lib.mapAttrs' (n: v: lib.nameValuePair | ||
218 | "etc/${domain}/data_sources/${n}.incl" | ||
219 | { source = config.secrets.fullPaths."sympa/data_sources/${n}.incl"; }) sympaConfig.data_sources | ||
220 | // lib.mapAttrs' (n: v: lib.nameValuePair | ||
221 | "etc/${domain}/scenari/${n}" | ||
222 | { source = config.secrets.fullPaths."sympa/scenari/${n}"; }) sympaConfig.scenari; | ||
223 | web = { | ||
224 | server = "none"; | ||
225 | }; | ||
226 | |||
227 | mta = { | ||
228 | type = "none"; | ||
229 | }; | ||
230 | }; | ||
231 | }; | ||
232 | } | ||