# SPDX-FileCopyrightText: 2021-2024 Simon Bruder # # SPDX-License-Identifier: AGPL-3.0-or-later { config, lib, pkgs, ... }: let cfg = config.sbruder.mailserver; listToString = lib.concatStringsSep ","; # 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)); valiases = pkgs.writeText "valiases" aliasesString; submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (lib.concatMapStringsSep "\n" (regex: "${regex} IGNORE") cfg.cleanHeaders); in lib.mkIf cfg.enable { services.postfix = { enable = true; setSendmail = lib.mkForce false; enableSubmission = true; # plain/STARTTLS (latter is forced in submissionOptions) enableSubmissions = true; # submission with implicit TLS (TCP/465) hostname = cfg.fqdn; networksStyle = "host"; sslCert = "${cfg.certDir}/fullchain.pem"; sslKey = "${cfg.certDir}/key.pem"; recipientDelimiter = "+"; mapFiles = { inherit valiases; restricted_senders = pkgs.writeText "restricted_senders" (lib.concatStringsSep "\n" (lib.flatten (map (user: (map (address: "${address} local_only") ([ user.address ] ++ user.aliases))) (lib.filter (user: user.localOnly) cfg.users)))); local_domains = pkgs.writeText "local_domains" (lib.concatMapStringsSep "\n" (domain: "${domain} OK") cfg.domains); }; 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 [ "reject_non_fqdn_sender" "reject_unknown_sender_domain" ]; # can’t be in submissionOptions (which does not support spaces in NixOS) submission_sender_restrictions = listToString [ "reject_sender_login_mismatch" "check_sender_access hash:/etc/postfix/restricted_senders" ]; smtpd_restriction_classes = listToString [ "local_only" ]; local_only = listToString [ "check_recipient_access hash:/etc/postfix/local_domains" "reject" ]; # 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"; smtpd_tls_received_header = "yes"; # add TLS connection details to Received header 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"; }; # plain/STARTTLS (forced with smtpd_tls_security_level) 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 = "$submission_sender_restrictions"; cleanup_service_name = "submission-header-cleanup"; }; # implicit TLS submissionsOptions = config.services.postfix.submissionOptions; masterConfig = { # Postscreen smtpd = { type = "pass"; args = [ "-o" "smtpd_discard_ehlo_keywords=silent-discard,dsn" ]; }; smtp_inet = { # Partially overrides upstream name = "smtp"; type = "inet"; private = false; command = lib.mkForce "postscreen"; maxproc = 1; }; tlsproxy = { maxproc = 0; }; dnsblog = { maxproc = 0; }; # Heder cleanup submission-header-cleanup = { private = false; maxproc = 0; command = "cleanup"; args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ]; }; }; }; networking.firewall.allowedTCPPorts = [ 25 # SMTP 587 # SMTP submission 465 # SMTP submission (implicit TLS) ]; systemd.services.postfix = { wants = [ "acme-finished-${cfg.fqdn}.target" ]; requires = [ "dovecot2.service" ]; after = [ "acme-finished-${cfg.fqdn}.target" "dovecot2.service" ]; }; security.acme.certs."${cfg.fqdn}".postRun = '' if systemctl is-active postfix; then systemctl --no-block reload postfix fi ''; }