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