This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / configs / mailserver.nix
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}