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