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