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