{ config, pkgs, lib, ... }: let mail_config = config.mailserver; submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' /^Received:/ IGNORE /^X-Originating-IP:/ IGNORE /^X-Mailer:/ IGNORE /^User-Agent:/ IGNORE /^X-Enigmail:/ IGNORE /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${mail_config.fqdn}> ''); inetSocket = addr: port: "inet:[${toString port}@${addr}]"; unixSocket = sock: "unix:${sock}"; # Merge several lookup tables. A lookup table is a attribute set where # - the key is an address (user@example.com) or a domain (@example.com) # - the value is a list of addresses mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables; # valiases_postfix :: Map String [String] valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList (name: value: let to = name; in map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)) mail_config.accounts)); # all_valiases_postfix :: Map String [String] all_valiases_postfix = mergeLookupTables [ valiases_postfix ]; # lookupTableToString :: Map String [String] -> String lookupTableToString = attrs: let valueToString = value: lib.concatStringsSep ", " value; in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); # valiases_file :: Path valiases_file = let content = lookupTableToString (mergeLookupTables [ all_valiases_postfix ]); in builtins.toFile "valias" content; # vhosts_file :: Path vhosts_file = builtins.toFile "vhosts" (lib.concatStringsSep "\n" mail_config.domains); vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; policyd-spf = pkgs.writeText "policyd-spf.conf" mail_config.policyd_config; submissionOptions = { smtpd_tls_security_level = "encrypt"; smtpd_sasl_auth_enable = "yes"; smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = "$myhostname"; smtpd_client_restrictions = "permit_sasl_authenticated,reject"; smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts"; smtpd_sender_restrictions = "reject_sender_login_mismatch"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; cleanup_service_name = "submission-header-cleanup"; }; tls_allowed = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; tls_disallow = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; in { config = (lib.mkIf (mail_config.enable) { services.postfix = { enable = true; hostname = "${mail_config.fqdn}"; networksStyle = "host"; mapFiles."valias" = valiases_file; mapFiles."vaccounts" = vaccounts_file; sslCert = mail_config.ssl_config.cert; sslKey = mail_config.ssl_config.key; enableSubmission = true; enableSubmissions = true; virtual = lookupTableToString (mergeLookupTables [ all_valiases_postfix ]); config = { # Extra Config mydestination = ""; recipient_delimiter = "+"; smtpd_banner = "${mail_config.fqdn} ESMTP NO UCE"; disable_vrfy_command = true; message_size_limit = "20971520"; virtual_uid_maps = "static:${toString mail_config.vmail_config.user_group_id}"; virtual_gid_maps = "static:${toString mail_config.vmail_config.user_group_id}"; virtual_mailbox_base = "${mail_config.vmail_config.directory}"; virtual_mailbox_domains = vhosts_file; virtual_mailbox_maps = mappedFile "valias"; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; lmtp_destination_recipient_limit = "1"; smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sasl_auth_enable = true; smtpd_relay_restrictions = [ "permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination" ]; policy-spf_time_limit = "3600s"; smtpd_recipient_restrictions = [ #"check_recipient_access ${mappedFile "denied_recipients"}" #"check_recipient_access ${mappedFile "reject_recipients"}" "check_policy_service unix:private/policy-spf" ]; # TLS settings, inspired by https://github.com/jeaye/nix-files # Submission by mail clients is handled in submissionOptions smtpd_tls_security_level = "may"; # strong might suffice and is computationally less expensive smtpd_tls_eecdh_grade = "ultra"; # Only Alow Modern TLS smtp_tls_protocols = tls_allowed; smtpd_tls_protocols = tls_allowed; smtp_tls_mandatory_protocols = tls_allowed; smtpd_tls_mandatory_protocols = tls_allowed; # Disable Old Ciphers smtp_tls_exclude_ciphers = tls_disallow; smtpd_tls_exclude_ciphers = tls_disallow; smtp_tls_mandatory_exclude_ciphers = tls_disallow; smtpd_tls_mandatory_exclude_ciphers = tls_disallow; smtp_tls_ciphers = "high"; smtpd_tls_ciphers = "high"; smtp_tls_mandatory_ciphers = "high"; smtpd_tls_mandatory_ciphers = "high"; tls_preempt_cipherlist = true; smtpd_tls_auth_only = true; smtpd_tls_loglevel = "1"; tls_random_source = "dev:/dev/urandom"; smtpd_milters = [ "unix:/run/opendkim/opendkim.sock" "unix:/run/rspamd/rspamd-milter.sock" ]; non_smtpd_milters = [ "unix:/run/opendkim/opendkim.sock" ]; milter_protocol = "6"; milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; }; submissionOptions = submissionOptions; submissionsOptions = submissionOptions; masterConfig = { "lmtp" = { # Add headers when delivering, see http://www.postfix.org/smtp.8.html # D => Delivered-To, O => X-Original-To, R => Return-Path args = [ "flags=O" ]; }; "policy-spf" = { type = "unix"; privileged = true; chroot = false; command = "spawn"; args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}" ]; }; "submission-header-cleanup" = { type = "unix"; private = false; chroot = false; maxproc = 0; command = "cleanup"; args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ]; }; }; }; }); }