{ lib, pkgs, config, options, ... }: let getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) (lib.filterAttrs (n: v: v.receive) p.emailPolicies); bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones; receiving_domains = lib.flatten (builtins.attrValues bydomain); in { options.services.postfix.submissionOptions' = options.services.postfix.submissionOptions // { type = with lib.types; attrsOf (either str (listOf str)); apply = builtins.mapAttrs (n: v: if builtins.isList v then builtins.concatStringsSep "," v else v); }; config = lib.mkIf config.myServices.mail.enable { myServices.dns.zones."immae.eu" = with config.myServices.dns.helpers; lib.mkMerge [ mailMX (mailCommon "immae.eu") mailSend { # Virtual forwards and mailboxes for real users emailPolicies."mail".receive = true; # multi-domain generic mails: # hostmaster, cron, httpd, naemon, postmaster # system virtual mailboxes: # devnull, printer, testconnect emailPolicies."".receive = true; subdomains.mail = lib.mkMerge [ (mailCommon "immae.eu") mailSend ]; subdomains.smtp = ips servers.eldiron.ips.main; # DMARC reports subdomains._dmarc.subdomains._report.subdomains = let getDomains = p: lib.mapAttrsToList (n: v: v.fqdn) p.emailPolicies; bydomain = builtins.mapAttrs (n: getDomains) config.myServices.dns.zones; hostsWithMail = lib.flatten (builtins.attrValues bydomain); nvpairs = builtins.map (e: { name = e; value = { TXT = [ "v=DMARC1;" ]; }; }) hostsWithMail; in builtins.listToAttrs nvpairs; } ]; myServices.chatonsProperties.hostings.mx-backup = { file.datetime = "2022-08-22T01:00:00"; hosting = { name = "MX Backup"; description = "Serveur e-mail secondaire"; logo = "https://www.postfix.org/favicon.ico"; website = "https://mail.immae.eu/"; status.level = "OK"; status.description = "OK"; registration.load = "OPEN"; install.type = "PACKAGE"; }; software = { name = "Postfix"; website = "http://www.postfix.org/"; license.url = "http://postfix.mirrors.ovh.net/postfix-release/LICENSE"; license.name = "Eclipse Public license (EPL 2.0) and IBM Public License (IPL 1.0)"; version = pkgs.postfix.version; source.url = "http://www.postfix.org/download.html"; }; }; secrets.keys = { "postfix/mysql_alias_maps" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' # We need to specify that option to trigger ssl connection tls_ciphers = TLSv1.2 user = ${config.myEnv.mail.postfix.mysql.user} password = ${config.myEnv.mail.postfix.mysql.password} hosts = unix:${config.myEnv.mail.postfix.mysql.socket} dbname = ${config.myEnv.mail.postfix.mysql.database} query = SELECT DISTINCT destination FROM forwardings WHERE ((regex = 1 AND '%s' REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = '%s')) AND active = 1 AND '%s' NOT IN ( SELECT source FROM forwardings_blacklisted WHERE source = '%s' ) UNION SELECT 'devnull@immae.eu' FROM forwardings_blacklisted WHERE source = '%s' ''; }; "postfix/ldap_mailboxes" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' server_host = ldaps://${config.myEnv.mail.dovecot.ldap.host}:636 search_base = ${config.myEnv.mail.dovecot.ldap.base} query_filter = ${config.myEnv.mail.dovecot.ldap.postfix_mailbox_filter} bind_dn = ${config.myEnv.mail.dovecot.ldap.dn} bind_pw = ${config.myEnv.mail.dovecot.ldap.password} result_attribute = immaePostfixAddress result_format = dummy version = 3 ''; }; "postfix/mysql_sender_login_maps" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' # We need to specify that option to trigger ssl connection tls_ciphers = TLSv1.2 user = ${config.myEnv.mail.postfix.mysql.user} password = ${config.myEnv.mail.postfix.mysql.password} hosts = unix:${config.myEnv.mail.postfix.mysql.socket} dbname = ${config.myEnv.mail.postfix.mysql.database} query = SELECT DISTINCT destination FROM forwardings WHERE ( (regex = 1 AND CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') REGEXP CONCAT('^',source,'$') ) OR (regex = 0 AND source = CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d')) ) AND active = 1 UNION SELECT CONCAT(SUBSTRING_INDEX('%u', '+', 1), '@%d') AS destination ''; }; "postfix/mysql_sender_relays_maps" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' # We need to specify that option to trigger ssl connection tls_ciphers = TLSv1.2 user = ${config.myEnv.mail.postfix.mysql.user} password = ${config.myEnv.mail.postfix.mysql.password} hosts = unix:${config.myEnv.mail.postfix.mysql.socket} dbname = ${config.myEnv.mail.postfix.mysql.database} # INSERT INTO sender_relays # (`from`, owner, relay, login, password, regex, active) # VALUES # ( 'sender@otherhost.org' # , 'me@mail.immae.eu' # , '[otherhost.org]:587' # , 'otherhostlogin' # , AES_ENCRYPT('otherhostpassword', '${config.myEnv.mail.postfix.mysql.password_encrypt}') # , '0' # , '1'); query = SELECT DISTINCT `owner` FROM sender_relays WHERE ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) AND active = 1 ''; }; "postfix/mysql_sender_relays_hosts" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' # We need to specify that option to trigger ssl connection tls_ciphers = TLSv1.2 user = ${config.myEnv.mail.postfix.mysql.user} password = ${config.myEnv.mail.postfix.mysql.password} hosts = unix:${config.myEnv.mail.postfix.mysql.socket} dbname = ${config.myEnv.mail.postfix.mysql.database} query = SELECT DISTINCT relay FROM sender_relays WHERE ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) AND active = 1 ''; }; "postfix/mysql_sender_relays_creds" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' # We need to specify that option to trigger ssl connection tls_ciphers = TLSv1.2 user = ${config.myEnv.mail.postfix.mysql.user} password = ${config.myEnv.mail.postfix.mysql.password} hosts = unix:${config.myEnv.mail.postfix.mysql.socket} dbname = ${config.myEnv.mail.postfix.mysql.database} query = SELECT DISTINCT CONCAT(`login`, ':', AES_DECRYPT(`password`, '${config.myEnv.mail.postfix.mysql.password_encrypt}')) FROM sender_relays WHERE ((regex = 1 AND '%s' REGEXP CONCAT('^',`from`,'$') ) OR (regex = 0 AND `from` = '%s')) AND active = 1 ''; }; "postfix/ldap_ejabberd_users_immae_fr" = { user = config.services.postfix.user; group = config.services.postfix.group; permissions = "0440"; text = '' server_host = ldaps://${config.myEnv.jabber.ldap.host}:636 search_base = ${config.myEnv.jabber.ldap.base} query_filter = ${config.myEnv.jabber.postfix_user_filter} domain = immae.fr bind_dn = ${config.myEnv.jabber.ldap.dn} bind_pw = ${config.myEnv.jabber.ldap.password} result_attribute = immaeXmppUid result_format = ejabberd@localhost version = 3 ''; }; }; networking.firewall.allowedTCPPorts = [ 25 465 587 ]; users.users.postfixscripts = { group = "keys"; uid = config.ids.uids.postfixscripts; description = "Postfix scripts user"; }; users.users."${config.services.postfix.user}".extraGroups = [ "keys" ]; services.filesWatcher.postfix = { restart = true; paths = [ config.secrets.fullPaths."postfix/mysql_alias_maps" config.secrets.fullPaths."postfix/ldap_mailboxes" config.secrets.fullPaths."postfix/mysql_sender_login_maps" config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr" ]; }; services.postfix = { extraAliases = let testmail = pkgs.writeScript "testmail" '' #! ${pkgs.stdenv.shell} ${pkgs.coreutils}/bin/touch \ "/var/lib/naemon/checks/email/$(${pkgs.procmail}/bin/formail -x To: | ${pkgs.coreutils}/bin/tr -d ' <>')" ''; in ''testmail: "|${testmail}"''; mapFiles = let virtual_map = { virtual = let cfg = config.myEnv.monitoring.email_check.eldiron; address = "${cfg.mail_address}@${cfg.mail_domain}"; aliases = config.myEnv.mail.postfix.common_aliases; admins = builtins.concatStringsSep "," config.myEnv.mail.postfix.admins; in pkgs.writeText "postfix-virtual" ( builtins.concatStringsSep "\n" ( [ "${address} testmail@localhost" ] ++ map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases ++ lib.lists.flatten ( map (domain: map (alias: "${alias}@${domain} ${admins}") aliases ) receiving_domains ) )); }; sasl_access = { host_sender_login = with lib.attrsets; let addresses = zipAttrs (lib.flatten (mapAttrsToList (n: v: (map (e: { "${e}" = "${n}@immae.eu"; }) v.emails)) config.myEnv.servers)); aliases = config.myEnv.mail.postfix.common_aliases; joined = builtins.concatStringsSep ","; admins = joined config.myEnv.mail.postfix.admins; in pkgs.writeText "host-sender-login" (builtins.concatStringsSep "\n" ( mapAttrsToList (n: v: "${n} ${joined v}") addresses ++ lib.lists.flatten ( map (domain: map (alias: "${alias}@${domain} ${admins}") aliases ) receiving_domains ) ++ map (a: "${a} ${admins}") config.myEnv.mail.postfix.other_aliases )); }; in virtual_map // sasl_access; config = { ### postfix module overrides readme_directory = "${pkgs.postfix}/share/postfix/doc"; smtp_tls_CAfile = lib.mkForce ""; smtp_tls_cert_file = lib.mkForce ""; smtp_tls_key_file = lib.mkForce ""; message_size_limit = "1073741824"; # Don't put 0 here, it's not equivalent to "unlimited" mailbox_size_limit = "1073741825"; # Workaround, local delivered mails should all go through scripts alias_database = "\$alias_maps"; ### Aliases scripts user default_privs = "postfixscripts"; ### Virtual mailboxes config virtual_alias_maps = [ "hash:/etc/postfix/virtual" "mysql:${config.secrets.fullPaths."postfix/mysql_alias_maps"}" "ldap:${config.secrets.fullPaths."postfix/ldap_ejabberd_users_immae_fr"}" ]; virtual_mailbox_domains = receiving_domains; virtual_mailbox_maps = [ "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}" ]; dovecot_destination_recipient_limit = "1"; virtual_transport = "dovecot"; ### Relay domains smtpd_relay_restrictions = [ "defer_unauth_destination" ]; ### Additional smtpd configuration smtpd_tls_received_header = "yes"; smtpd_tls_loglevel = "1"; ### Email sending configuration smtp_tls_security_level = "may"; smtp_tls_loglevel = "1"; ### Force ip bind for smtp smtp_bind_address = builtins.head config.hostEnv.ips.main.ip4; smtp_bind_address6 = builtins.head config.hostEnv.ips.main.ip6; # Use some relays when authorized senders are not myself smtp_sasl_mechanism_filter = [ "plain" "login" ]; # GSSAPI Not correctly supported by postfix smtp_sasl_auth_enable = "yes"; smtp_sasl_password_maps = [ "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_creds"}" ]; smtp_sasl_security_options = "noanonymous"; smtp_sender_dependent_authentication = "yes"; sender_dependent_relayhost_maps = [ "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_hosts"}" ]; ### opendkim, opendmarc, openarc milters non_smtpd_milters = [ "unix:${config.myServices.mail.milters.sockets.opendkim}" ]; smtpd_milters = [ "unix:${config.myServices.mail.milters.sockets.opendkim}" "unix:${config.myServices.mail.milters.sockets.openarc}" "unix:${config.myServices.mail.milters.sockets.opendmarc}" ]; smtp_use_tls = true; smtpd_use_tls = true; smtpd_tls_chain_files = [ "/var/lib/acme/mail/full.pem" "/var/lib/acme/mail-rsa/full.pem" ]; maximal_queue_lifetime = "6w"; bounce_queue_lifetime = "6w"; }; enable = true; enableSmtp = true; enableSubmission = true; submissionOptions = config.services.postfix.submissionOptions'; submissionOptions' = { # Don’t use "long form", only commas (cf # http://www.postfix.org/master.5.html long form is not handled # well by the submission function) smtpd_tls_security_level = "encrypt"; smtpd_sasl_auth_enable = "yes"; smtpd_tls_auth_only = "yes"; smtpd_sasl_tls_security_options = "noanonymous"; smtpd_sasl_type = "dovecot"; smtpd_sasl_path = "private/auth"; smtpd_reject_unlisted_recipient = "no"; smtpd_client_restrictions = [ "permit_sasl_authenticated" "reject" ]; smtpd_relay_restrictions = [ "permit_sasl_authenticated" "reject" ]; # Refuse to send e-mails with a From that is not handled smtpd_sender_restrictions = [ "reject_sender_login_mismatch" "reject_unlisted_sender" "permit_sasl_authenticated,reject" ]; smtpd_sender_login_maps = [ "hash:/etc/postfix/host_sender_login" "mysql:${config.secrets.fullPaths."postfix/mysql_sender_relays_maps"}" "mysql:${config.secrets.fullPaths."postfix/mysql_sender_login_maps"}" ]; smtpd_recipient_restrictions = [ "permit_sasl_authenticated" "reject" ]; milter_macro_daemon_name = "ORIGINATING"; smtpd_milters = [ # FIXME: put it back when opensmtpd is upgraded and able to # rewrite the from header #"unix:/run/milter_verify_from/verify_from.sock" "unix:${config.myServices.mail.milters.sockets.opendkim}" ]; }; destination = ["localhost"]; # This needs to reverse DNS hostname = config.hostEnv.fqdn; setSendmail = true; recipientDelimiter = "+"; masterConfig = { submissions = { type = "inet"; private = false; command = "smtpd"; args = ["-o" "smtpd_tls_wrappermode=yes" ] ++ (let mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ]; in lib.concatLists (lib.mapAttrsToList mkKeyVal config.services.postfix.submissionOptions) ); }; dovecot = { type = "unix"; privileged = true; chroot = false; command = "pipe"; args = let # rspamd could be used as a milter, but then it cannot apply # its checks "per user" (milter is not yet dispatched to # users), so we wrap dovecot-lda inside rspamc per recipient # here. rspamc_dovecot = pkgs.writeScriptBin "rspamc_dovecot" '' #! ${pkgs.stdenv.shell} set -o pipefail sender="$1" original_recipient="$2" user="$3" ${pkgs.coreutils}/bin/cat - | \ ${pkgs.rspamd}/bin/rspamc -h ${config.myServices.mail.rspamd.sockets.worker-controller} -c bayes -d "$user" --mime | \ ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user" if echo ''${PIPESTATUS[@]} | ${pkgs.gnugrep}/bin/grep -qE '^[0 ]+$'; then exit 0 else # src/global/sys_exits.h to retry exit 75 fi ''; in [ "flags=ODRhu" "user=vhost:vhost" "argv=${rspamc_dovecot}/bin/rspamc_dovecot \${sender} \${original_recipient} \${user}@\${nexthop}" ]; }; }; }; security.acme.certs."mail" = { postRun = '' systemctl restart postfix.service ''; extraDomainNames = [ "smtp.immae.eu" ]; }; security.acme.certs."mail-rsa" = { postRun = '' systemctl restart postfix.service ''; extraDomainNames = [ "smtp.immae.eu" ]; }; system.activationScripts.testmail = { deps = [ "users" ]; text = let allCfg = config.myEnv.monitoring.email_check; cfg = allCfg.eldiron; reverseTargets = builtins.attrNames (lib.attrsets.filterAttrs (k: v: builtins.elem "eldiron" v.targets) allCfg); to_email = cfg': host': let sep = if lib.strings.hasInfix "+" cfg'.mail_address then "_" else "+"; in "${cfg'.mail_address}${sep}${host'}@${cfg'.mail_domain}"; mails_to_receive = builtins.concatStringsSep " " (map (to_email cfg) reverseTargets); in '' install -m 0555 -o postfixscripts -g keys -d /var/lib/naemon/checks/email for f in ${mails_to_receive}; do if [ ! -f /var/lib/naemon/checks/email/$f ]; then install -m 0644 -o postfixscripts -g keys /dev/null -T /var/lib/naemon/checks/email/$f touch -m -d @0 /var/lib/naemon/checks/email/$f fi done ''; }; systemd.services.postfix.serviceConfig.Slice = "mail.slice"; myServices.monitoring.fromMasterObjects.service = [ { service_description = "postfix SSL is up to date"; host_name = config.hostEnv.fqdn; use = "external-service"; check_command = "check_smtp"; servicegroups = "webstatus-ssl"; _webstatus_name = "SMTP"; _webstatus_url = "smtp.immae.eu"; } ]; }; }