{ config, lib, pkgs, ... }: let cfg = config.sbruder.mailserver; certDir = config.security.acme.certs."${cfg.fqdn}".directory; in { options.sbruder.mailserver = with lib; with lib.types; { enable = mkEnableOption "simple mail server"; fqdn = mkOption { type = str; description = '' FQDN of the mail server It needs to have a matching reverse DNS record. Also, an acme certificate with this name has to be present. ''; example = "mail.example.com"; }; storage = mkOption { type = path; description = "Location of the storage for mails"; default = "/var/vmail"; }; domains = mkOption { type = listOf str; description = "Domains to serve"; example = [ "example.com" "example.org" ]; }; users = mkOption { type = listOf (submodule { options = { address = mkOption { type = str; description = "Primary e-mail address of the user"; example = "jdoe@example.com"; }; passwordHash = mkOption { type = str; description = '' Bcrypt hash of the user’s password. Please note that it will be world-readable in the nix store. You can generate a password with `nix run nixpkgs.apacheHttpd -c htpasswd -nBC 12 "" | cut -d: -f2` ''; example = "$2y$05$SHxhwVGx.XCd19HAcb1NKuidUxW1BwU7GeO0ZIcMTc5t2uZoYLVRK"; }; aliases = mkOption { type = listOf str; description = '' A list of aliases for the user. If multiple users have the same alias defined, mail will be delivered to both of them. ''; default = [ ]; example = [ "j.doe@example.com" "jane.doe@example.com" "postmaster@example.com" ]; }; }; }); description = "Users of the mail server"; }; cleanHeaders = mkOption { type = listOf str; description = "A list of regular expressions that define what headers are filtered"; default = [ "/^\\s*Received:/" "/^\\s*User-Agent:/" "/^\\s*X-Mailer:/" "/^\\s*X-Originating-IP:/" ]; }; rejectSenders = mkOption { type = listOf str; description = "A list of senders to reject mails from"; default = [ ]; example = [ "newsletter@example.com" "spammer@example.com" ]; }; }; config = lib.mkIf cfg.enable { # Users and groups users.users.vmail = { uid = 10000; group = "vmail"; home = cfg.storage; createHome = true; }; users.groups.vmail.gid = 10000; # Firewall networking.firewall.allowedTCPPorts = [ 143 # IMAP 25 # SMTP 587 # SMTP submission ]; # Service dependencies systemd.services.dovecot2 = { wants = [ "acme-finished-${cfg.fqdn}.target" ]; after = [ "acme-finished-${cfg.fqdn}.target" ]; }; systemd.services.postfix = { wants = [ "acme-finished-${cfg.fqdn}.target" ]; requires = [ "dovecot2.service" ]; after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ]; }; # Reload on certificate renewal security.acme.certs."${cfg.fqdn}".postRun = '' if systemctl is-active dovecot2; then systemctl --no-block reload dovecot2 fi if systemctl is-active postfix; then systemctl --no-block reload postfix fi ''; # Postfix security.dhparams.params.postfix = { }; services.postfix = let listToString = lib.concatStringsSep ","; valiases = let # List of attribute sets with single key-value pair plainAliases = (lib.flatten (map ({ address, aliases, ... }: map (alias: { "${alias}" = address; }) (aliases ++ lib.singleton address)) cfg.users)); # Attribute set with every alias mapped to a list of receivers mergedAliases = (lib.attrsets.foldAttrs (val: col: lib.singleton val ++ col) [ ] plainAliases); # Contents of the aliases file aliasesString = (lib.concatStringsSep "\n" (lib.mapAttrsToList (alias: addresses: "${alias} ${listToString addresses}") mergedAliases)); in pkgs.writeText "valiases" aliasesString; access_sender = pkgs.writeText "access_sender" (lib.concatMapStringsSep "\n" (sender: "${sender} REJECT") cfg.rejectSenders); submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (lib.concatMapStringsSep "\n" (regex: "${regex} IGNORE") cfg.cleanHeaders); in { enable = true; enableSubmission = true; hostname = cfg.fqdn; networksStyle = "host"; sslCert = "${certDir}/fullchain.pem"; sslKey = "${certDir}/key.pem"; recipientDelimiter = "+"; mapFiles = { inherit access_sender valiases; }; config = { # General smtpd_banner = "${cfg.fqdn} ESMTP NO UCE"; disable_vrfy_command = true; # disable check if mailbox exists enable_long_queue_ids = true; # better for debugging strict_rfc821_envelopes = true; # only accept properly formatted envelope message_size_limit = "50331648"; # 48 MiB virtual_mailbox_domains = listToString cfg.domains; virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases"; virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases"; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; smtpd_recipient_restrictions = listToString [ "reject_non_fqdn_recipient" "reject_rbl_client ix.dnsbl.manitu.net" "reject_unknown_recipient_domain" "reject_unverified_recipient" ]; smtpd_client_restrictions = listToString [ "reject_rbl_client ix.dnsbl.manitu.net" "reject_unknown_client_hostname" ]; smtpd_sender_restrictions = listToString [ "check_sender_access hash:/var/lib/postfix/conf/access_sender" "reject_non_fqdn_sender" "reject_unknown_sender_domain" ]; # generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration # https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6 smtpd_tls_security_level = "may"; smtpd_tls_auth_only = "yes"; smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1"; smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1"; smtpd_tls_mandatory_ciphers = "medium"; smtpd_tls_loglevel = "1"; tls_medium_cipherlist = listToString [ "ECDHE-ECDSA-AES128-GCM-SHA256" "ECDHE-RSA-AES128-GCM-SHA256" "ECDHE-ECDSA-AES256-GCM-SHA384" "ECDHE-RSA-AES256-GCM-SHA384" "ECDHE-ECDSA-CHACHA20-POLY1305" "ECDHE-RSA-CHACHA20-POLY1305" "DHE-RSA-AES128-GCM-SHA256" "DHE-RSA-AES256-GCM-SHA384" ]; tls_preempt_cipherlist = "no"; smtpd_tls_dh1024_param_file = config.security.dhparams.params.postfix.path; }; submissionOptions = { smtpd_tls_security_level = "encrypt"; smtpd_sasl_auth_enable = "yes"; smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sender_login_maps = "hash:/etc/postfix/valiases"; smtpd_recipient_restrictions = listToString [ ]; smtpd_client_restrictions = listToString [ "permit_sasl_authenticated" "reject" ]; smtpd_sender_restrictions = listToString [ "reject_sender_login_mismatch" ]; cleanup_service_name = "submission-header-cleanup"; }; masterConfig = { submission-header-cleanup = { private = false; maxproc = 0; command = "cleanup"; args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ]; }; }; }; # Dovecot services.dovecot2 = let postfixCfg = config.services.postfix; passdb = pkgs.writeText "dovecot-users" (lib.concatMapStringsSep "\n" ({ address, passwordHash, ... }: "${address}:{BLF-CRYPT}${passwordHash}") cfg.users); in { enable = true; enableLmtp = true; enablePAM = false; mailUser = "vmail"; mailGroup = "vmail"; mailLocation = "maildir:${cfg.storage}/%d/%n"; sslServerCert = "${certDir}/fullchain.pem"; sslServerKey = "${certDir}/key.pem"; extraConfig = '' # generated 2021-02-04, Mozilla Guideline v5.6, Dovecot 2.3.13, OpenSSL 1.1.1i, intermediate configuration # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.13&config=intermediate&openssl=1.1.1i&guideline=5.6 ssl = required ssl_min_protocol = TLSv1.2 ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384 ssl_prefer_server_ciphers = no service imap-login { inet_listener imap { } } service lmtp { unix_listener dovecot-lmtp { mode = 0600 user = ${postfixCfg.user} group = ${postfixCfg.group} } } passdb { driver = passwd-file args = username_format=%u ${passdb} } userdb { driver = static args = uid=vmail gid=vmail home=${cfg.storage}/%d/%n } service auth { unix_listener auth { mode = 0660 user = ${postfixCfg.user} group = ${postfixCfg.group} } } lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes ''; }; }; }