This repository has no description
13 kB
386 lines
1{
2 self,
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8let
9 relayHosts = map (name: self.inputs.kartei.hosts.${name}.nets.retiolum.ip6.addr) [
10 "aergia"
11 "ignavia"
12 "coaxmetal"
13 "green"
14 "mors"
15 ];
16
17 mynetworks = map (ip: "[${ip}]") relayHosts;
18in
19{
20 imports = [
21 self.inputs.nixos-mailserver.nixosModules.default
22 ./hermes-mail-vars.nix
23 ];
24
25 mailserver = {
26 enable = true;
27 fqdn = "mail.lassul.us";
28 # neoprism.r: retiolum-internal mail (cron, root@neoprism.r from other
29 # krebs hosts) lands in lass's mailbox via catchAll.
30 domains = [
31 "lassul.us"
32 "neoprism.r"
33 ];
34
35 loginAccounts = {
36 "lass@lassul.us" = {
37 hashedPasswordFile =
38 config.clan.core.vars.generators.mailserver-lass.files."lass-mail-password-hash".path;
39 aliases = [
40 "postmaster@lassul.us"
41 "root@lassul.us"
42 "abuse@lassul.us"
43 "hostmaster@lassul.us"
44 "webmaster@lassul.us"
45 "security@lassul.us"
46 "noc@lassul.us"
47 "mailer-daemon@lassul.us"
48 "nobody@lassul.us"
49 "lassulus@lassul.us"
50 ];
51 catchAll = [
52 "lassul.us"
53 "neoprism.r"
54 ];
55 };
56 "bot@lassul.us" = {
57 hashedPasswordFile =
58 config.clan.core.vars.generators.mailserver-bot.files."bot-mail-password-hash".path;
59 };
60 # Zitadel (configs/zitadel.nix): verification codes and password resets.
61 # Zitadel 2.71 needs a password for an SMTP provider.
62 "id@lassul.us" = {
63 hashedPasswordFile = config.clan.core.vars.generators.mailserver-id.files."password-hash".path;
64 sendOnly = true;
65 };
66 # Hermes agent on coaxmetal; shared password var, see hermes-mail-vars.nix.
67 "hermes@lassul.us" = {
68 hashedPasswordFile = config.clan.core.vars.generators.hermes-mail.files."password-hash".path;
69 };
70 };
71
72 # TLS via existing ACME cert
73 x509.useACMEHost = "mail.lassul.us";
74
75 # DKIM — auto-managed by module
76 dkimSigning = true;
77 dkimSelector = "mail";
78
79 # ManageSieve: lets mail clients manage sieve rules dynamically (port 4190)
80 enableManageSieve = true;
81
82 # Full-text search for IMAP clients
83 fullTextSearch = {
84 enable = true;
85 memoryLimit = 500;
86 };
87
88 # 5: Sieve scripts now live in each user's Dovecot home (~/sieve) instead of
89 # the old global /var/sieve. Before deploying this bump, run the migration
90 # against /var/sieve on neoprism (see migrations/nixos-mailserver-migration-05.py
91 # in the nixos-mailserver input, or the migration docs).
92 stateVersion = 5;
93
94 # virtual.All mailbox needs more memory to sync all folders
95 imapMemoryLimit = 1024;
96 };
97
98 # nixos-mailserver mkForces virtualMail without homeMode; match its priority
99 users.users.virtualMail = lib.mkOverride 50 { homeMode = "2770"; };
100
101 # POSIX ACL default ensures new files inherit lass access (needed for muchsync hardlinks)
102 systemd.services.dovecot.serviceConfig.ExecStartPost = [
103 "+${pkgs.acl}/bin/setfacl -R -m u:lass:rwX /var/vmail/lassul.us/lass/mail"
104 "+${pkgs.acl}/bin/setfacl -R -d -m u:lass:rwX /var/vmail/lassul.us/lass/mail"
105 ];
106
107 # Password generation for mail accounts
108 clan.core.vars.generators.mailserver-lass = {
109 files."lass-mail-password" = { };
110 files."lass-mail-password-hash" = { };
111 runtimeInputs = with pkgs; [
112 coreutils
113 mkpasswd
114 pwgen
115 ];
116 script = ''
117 pwgen -s 32 1 > "$out/lass-mail-password"
118 mkpasswd -sm bcrypt < "$out/lass-mail-password" > "$out/lass-mail-password-hash"
119 '';
120 };
121
122 clan.core.vars.generators.mailserver-bot = {
123 files."bot-mail-password" = { };
124 files."bot-mail-password-hash" = { };
125 files."mail.env" = { };
126 runtimeInputs = with pkgs; [
127 coreutils
128 mkpasswd
129 pwgen
130 ];
131 script = ''
132 pwgen -s 32 1 > "$out/bot-mail-password"
133 mkpasswd -sm bcrypt < "$out/bot-mail-password" > "$out/bot-mail-password-hash"
134 cat > "$out/mail.env" << EOF
135 IMAP_USER=bot@lassul.us
136 IMAP_PASSWORD=$(cat "$out/bot-mail-password")
137 IMAP_HOST=localhost
138 EOF
139 '';
140 };
141
142 # the plaintext is read by zitadel-reconcile (root)
143 clan.core.vars.generators.mailserver-id = {
144 files."password" = { };
145 files."password-hash" = { };
146 runtimeInputs = with pkgs; [
147 coreutils
148 mkpasswd
149 pwgen
150 ];
151 script = ''
152 pwgen -s 32 1 | tr -d '\n' > "$out/password"
153 mkpasswd -sm bcrypt < "$out/password" > "$out/password-hash"
154 '';
155 };
156
157 # Retiolum relay: allow krebs hosts to relay through postfix
158 services.postfix.settings.main.mynetworks = lib.mkForce (
159 [
160 "127.0.0.0/8"
161 "[::1]/128"
162 ]
163 ++ mynetworks
164 );
165
166 # ACME cert — reuse existing setup
167 security.acme.certs."mail.lassul.us" = {
168 group = "lasscert";
169 webroot = "/var/lib/acme/acme-challenge";
170 };
171 users.groups.lasscert.members = [
172 "dovecot2"
173 "postfix"
174 "nginx"
175 ];
176
177 # Firewall: SMTP(25), submission(587), submissions(465), IMAPS(993), ManageSieve(4190)
178 networking.firewall.allowedTCPPorts = [
179 25
180 587
181 465
182 993
183 4190
184 ];
185
186 # muchsync: lass user needs access to vmail for notmuch/muchsync
187 users.groups.virtualMail.members = [ "lass" ];
188 systemd.tmpfiles.rules = [
189 "L+ /home/lass/Maildir - - - - /var/vmail/lassul.us/lass/mail"
190 "z /var/vmail/lassul.us 2770 virtualMail virtualMail -"
191 "z /var/vmail/lassul.us/lass 2770 virtualMail virtualMail -"
192 "d /var/vmail/lassul.us/lass/mail 2770 virtualMail virtualMail -"
193 ];
194
195 # Thunderbird autoconfig
196 services.nginx.virtualHosts."autoconfig.lassul.us" = {
197 forceSSL = true;
198 enableACME = true;
199 locations."= /mail/config-v1.1.xml".extraConfig = ''
200 default_type application/xml;
201 return 200 '<?xml version="1.0" encoding="UTF-8"?>
202 <clientConfig version="1.1">
203 <emailProvider id="lassul.us">
204 <domain>lassul.us</domain>
205 <displayName>lassul.us Mail</displayName>
206 <displayShortName>lassul.us</displayShortName>
207 <incomingServer type="imap">
208 <hostname>mail.lassul.us</hostname>
209 <port>993</port>
210 <socketType>SSL</socketType>
211 <authentication>password-cleartext</authentication>
212 <username>%EMAILADDRESS%</username>
213 </incomingServer>
214 <outgoingServer type="smtp">
215 <hostname>mail.lassul.us</hostname>
216 <port>465</port>
217 <socketType>SSL</socketType>
218 <authentication>password-cleartext</authentication>
219 <username>%EMAILADDRESS%</username>
220 </outgoingServer>
221 </emailProvider>
222 </clientConfig>';
223 '';
224 };
225
226 # Provision virtual mailbox config to a writable directory (nix store is read-only,
227 # and the ACL plugin needs to write dovecot-acl-list next to the virtual config)
228 systemd.tmpfiles.settings."dovecot-virtual" = {
229 "/var/vmail/virtual-config".d = {
230 user = "virtualMail";
231 group = "virtualMail";
232 mode = "0750";
233 };
234 "/var/vmail/virtual-config/Unread".d = {
235 user = "virtualMail";
236 group = "virtualMail";
237 mode = "0750";
238 };
239 "/var/vmail/virtual-config/Unread/dovecot-virtual".f = {
240 user = "virtualMail";
241 group = "virtualMail";
242 mode = "0640";
243 argument = "*\n-notmuch\n-fts-flatcurve\n unseen\n";
244 };
245 "/var/vmail/virtual-config/All".d = {
246 user = "virtualMail";
247 group = "virtualMail";
248 mode = "0750";
249 };
250 "/var/vmail/virtual-config/All/dovecot-virtual".f = {
251 user = "virtualMail";
252 group = "virtualMail";
253 mode = "0640";
254 argument = "*\n-notmuch\n-fts-flatcurve\n all\n";
255 };
256 };
257
258 # Dovecot virtual mailbox + ACL-restricted shared namespace for bot
259 services.dovecot2.settings = {
260 mail_plugins = {
261 virtual = true;
262 acl = true;
263 };
264
265 # ACL plugin (dovecot 2.4): `acl_driver` is top-level; per-identifier
266 # default rights live in `acl <identifier> { acl_rights = ...; }` named
267 # filters where <identifier> is owner/anyone/authenticated/user=.../etc.
268 # `acl_shared_dict` was removed; the bot accesses shared mail via
269 # explicit subscriptions.
270 acl_driver = "vfile";
271 "acl owner" = {
272 acl_rights = "lrwstipekxa";
273 };
274
275 # In dovecot 2.4 the per-namespace `location = "driver:path:opts"` was
276 # replaced by separate mail_driver / mail_path / mail_index_path settings.
277 "namespace shared" = {
278 type = "shared";
279 separator = ".";
280 prefix = "shared.%{user | username}.";
281 mail_driver = "maildir";
282 mail_path = "/var/vmail/lassul.us/%{user | username}/mail";
283 subscriptions = false;
284 list = "children";
285 };
286
287 "namespace virtual" = {
288 prefix = "virtual.";
289 separator = ".";
290 mail_driver = "virtual";
291 mail_path = "/var/vmail/virtual-config";
292 mail_index_path = "/var/vmail/virtual-indexes/%{user}";
293 # virtual storage doesn't support maildir++ layout, override the global
294 mailbox_list_layout = "fs";
295 subscriptions = false;
296 "mailbox Unread".auto = "subscribe";
297 "mailbox All".auto = "subscribe";
298 };
299 };
300
301 # Write dovecot-acl files to grant bot read-only + flag access to lass's mail
302 systemd.services.dovecot-acl-sync = {
303 description = "Sync dovecot ACL files for bot shared access";
304 after = [ "dovecot.service" ];
305 serviceConfig = {
306 Type = "oneshot";
307 User = "virtualMail";
308 Group = "virtualMail";
309 };
310 # l=lookup, r=read, w=write-flags, s=write-seen (no insert/delete/expunge)
311 script = ''
312 acl_line="user=bot@lassul.us lrws"
313 # Only write ACL in mailbox root dirs, skip cur/new/tmp/notmuch subdirs
314 find /var/vmail/lassul.us/lass/mail -type d \
315 -not -name cur -not -name new -not -name tmp \
316 -not -path '*/.notmuch/*' -not -name .notmuch \
317 -not -path '*/fts-flatcurve/*' -not -name fts-flatcurve \
318 | while IFS= read -r dir; do
319 acl_file="$dir/dovecot-acl"
320 if [ ! -f "$acl_file" ] || [ "$(cat "$acl_file")" != "$acl_line" ]; then
321 echo "$acl_line" > "$acl_file"
322 fi
323 done
324 '';
325 };
326 systemd.timers.dovecot-acl-sync = {
327 wantedBy = [ "timers.target" ];
328 timerConfig = {
329 OnBootSec = "1min";
330 OnUnitActiveSec = "1h";
331 };
332 };
333
334 # notmuch + muchsync + msmtp for CLI mail access
335 environment.systemPackages = [
336 self.packages.${pkgs.system}.notmuch
337 pkgs.muchsync
338 pkgs.msmtp
339 ];
340
341 # After muchsync writes tags to notmuch DB, the Maildir filenames don't get updated
342 # (muchsync bypasses notmuch's flag sync). This watches the xapian DB for changes and
343 # renames files to add/remove the S (Seen) flag based on the notmuch unread tag.
344 systemd.paths.notmuch-flag-sync = {
345 wantedBy = [ "multi-user.target" ];
346 pathConfig.PathChanged = "/var/vmail/lassul.us/lass/mail/.notmuch/xapian";
347 };
348 systemd.services.notmuch-flag-sync = {
349 description = "Sync notmuch tags to Maildir flags after muchsync";
350 serviceConfig = {
351 Type = "oneshot";
352 User = "lass";
353 Group = "virtualMail";
354 ExecStart = pkgs.writeShellScript "notmuch-flag-sync" ''
355 export NOTMUCH_CONFIG="${
356 self.packages.${pkgs.system}.notmuch.passthru.configuration.configFile.path
357 }"
358 # First, pick up any IMAP flag changes into notmuch
359 ${pkgs.notmuch}/bin/notmuch new --quiet 2>/dev/null || true
360 # Add S flag to files notmuch considers read but that lack it
361 ${pkgs.notmuch}/bin/notmuch search --output=files 'NOT tag:unread' \
362 | ${pkgs.gnugrep}/bin/grep '/cur/' \
363 | while IFS= read -r f; do
364 [ -f "$f" ] || continue
365 flags="''${f##*:2,}"
366 case "$flags" in *S*) continue ;; esac
367 base="''${f%:2,*}"
368 mv "$f" "''${base}:2,''${flags}S"
369 done
370 # Remove S flag from files notmuch considers unread but that have it
371 ${pkgs.notmuch}/bin/notmuch search --output=files 'tag:unread' \
372 | ${pkgs.gnugrep}/bin/grep '/cur/' \
373 | while IFS= read -r f; do
374 [ -f "$f" ] || continue
375 flags="''${f##*:2,}"
376 case "$flags" in *S*) ;; *) continue ;; esac
377 base="''${f%:2,*}"
378 newflags="''${flags//S/}"
379 mv "$f" "''${base}:2,''${newflags}"
380 done
381 # Update notmuch DB with the renames
382 ${pkgs.notmuch}/bin/notmuch new --quiet 2>/dev/null || true
383 '';
384 };
385 };
386}