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