{ config, lib, pkgs, ... }: with lib; let cfg = config.local.mailHost; imapHostname = config.local.domains.imap.main; inherit (config.local) users virtual; in { options.local.mailHost = { enable = mkEnableOption "mailbox host service"; mdaListen = mkOption { type = types.str; }; saslPort = mkOption { type = types.port; }; lmtpPort = mkOption { type = types.port; }; }; config = mkIf cfg.enable { services.dovecot2 = let cert = config.security.acme.certs.${imapHostname}.directory; in { enable = true; enablePAM = false; enableLmtp = true; sslServerKey = "${cert}/key.pem"; sslServerCert = "${cert}/fullchain.pem"; modules = [ pkgs.dovecot_pigeonhole ]; mailUser = "vmail"; mailGroup = "vmail"; mailLocation = "maildir:~/mail"; mailPlugins.perProtocol.lmtp.enable = [ "sieve" ]; extraConfig = let inherit (config.networking) domain; # https://dovecot.org/list/dovecot/2019-March/115250.html # Otra solución posible (https://serverfault.com/a/1062274/980378): # auth_username_format = %{if;%d;eq;${domain};%Ln;%Lu} localEntry = canonical: username: '' ${username}:::::::user=${canonical} nopassword userdb_user=${canonical} ''; localMailboxes = pkgs.writeText "local-mailboxes" (concatStrings (flatten (mapAttrsToList (canonical: user: map (localEntry canonical) ([ canonical ] ++ user.hardAliases)) users))); localCerts = flatten (mapAttrsToList (canonical: user: let certNames = { inherit canonical; logins = [ canonical ] ++ user.hardAliases; }; in map (flip nameValuePair certNames) user.mail.certs) users); vmailCerts = flatten (flatten (mapAttrsToList (domain: virtual: mapAttrsToList (username: user: let address = "${username}@${domain}"; certNames = { canonical = address; logins = [ address ]; }; in map (flip nameValuePair certNames) user.mail.certs) virtual.users) virtual)); certLogins = pkgs.writeText "cert-logins" (concatStrings (flatten (mapAttrsToList (uuid: names: map (addr: '' ${uuid}.mail-client@nodomain,${addr}:::::::user=${names.canonical} '') names.logins) (listToAttrs (localCerts ++ vmailCerts))))); vmailPath = "/var/lib/vmail/%{if;%d;ne;;%Ld;${domain}}"; in '' auth_mechanisms = plain login external #TODO: automatizar implantación de archivo de CA ssl_ca = <${config.local.ca.chains.mail-fullchain-crl} ssl_require_crl = yes ssl_verify_client_cert = yes # Esto descarta @domain.tld de locales explícitos, pero lo exige para los demás. # Implicación: locales implícitos sin dominio fallan en autenticar auth_username_format = %{if;%Ld;eq;${domain};%Ln;%{if;%d;ne;;%Lu;%Ln@nodomain}} auth_ssl_username_from_cert = yes # TODO: los defaults de nixpkgs dejan los sockets bajo # /run/dovecot2 con demasiados permisos rwx, arreglar service auth { inet_listener mta-sasl { port = ${toString cfg.saslPort} address = ${cfg.mdaListen} } } service lmtp { inet_listener mta-lmtp { port = ${toString cfg.lmtpPort} address = ${cfg.mdaListen} } } # FIXME: Esta cadena de passdbs hace que 'doveadm user lookup' # falle para usuarios locales, pero todo lo demás sirve. Parece # ser debido a que pam no puede enumerar. passdb { driver = static args = nopassword master = yes mechanisms = external result_success = continue-fail result_failure = return-fail result_internalfail = return-fail } passdb { driver = passwd-file args = scheme=PLAIN username_format=%{master_user},%Lu ${certLogins} mechanisms = external override_fields = nopassword result_failure = return-fail result_internalfail = return-fail } passdb { driver = passwd-file args = username_format=%Ln ${vmailPath}/passwd } passdb { driver = passwd-file args = scheme=PLAIN ${localMailboxes} # Esta es una forma de determinar si se encontró el usuario en # el passwd-file por medio de nopassword sin realmente # autenticarlo. Cuidado con result_success, porque si eso se # configura mal se permite inicio de sesión con cualquier # contraseña (!!!). result_success = continue result_failure = return-fail result_internalfail = return-fail username_filter = !*@* } passdb { driver = pam args = dovecot2 username_filter = !*@* #TODO: algo como 'override_fields = allow_nets=...' } userdb { driver = passwd-file args = username_format=%Ln ${vmailPath}/passwd override_fields = uid=vmail gid=vmail home=${vmailPath}/home/%Ln } userdb { driver = passwd-file args = ${localMailboxes} result_success = continue-ok result_internalfail = return-fail skip = found } userdb { driver = passwd args = blocking=no skip = notfound } ''; }; security = { # Necesario debido a 'enablePAM = false' pam.services.dovecot2 = { }; acme.certs.${imapHostname} = { inherit (config.services.dovecot2) group; }; }; users = { users.${config.services.dovecot2.mailUser}.uid = 995; groups.${config.services.dovecot2.mailGroup}.gid = 993; }; networking.firewall.allowedTCPPorts = [ 143 993 ]; local.certs.imap.enable = true; }; }