{ lib, pkgs, config, nodes, ... }: { config = lib.mkIf config.myServices.mail.enable { services.duplyBackup.profiles.mail.excludeFile = '' + /var/lib/postfix ''; secrets.keys = [ { dest = "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' ''; } { dest = "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 ''; } { dest = "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 ''; } { dest = "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 ''; } { dest = "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 ''; } { dest = "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 ''; } { dest = "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 ''; } ] ++ (lib.mapAttrsToList (name: v: { dest = "postfix/scripts/${name}-env"; user = "postfixscripts"; group = "root"; permissions = "0400"; text = builtins.toJSON v.env; }) config.myEnv.mail.scripts); 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 toScript = name: script: pkgs.writeScript name '' #! ${pkgs.stdenv.shell} mail=$(${pkgs.coreutils}/bin/cat -) output=$(echo "$mail" | ${script} 2>&1) ret=$? if [ "$ret" != "0" ]; then echo "$mail" \ | ${pkgs.procmail}/bin/formail -i "X-Return-Code: $ret" \ | /run/wrappers/bin/sendmail -i scripts_error+${name}@mail.immae.eu messageId=$(echo "$mail" | ${pkgs.procmail}/bin/formail -x "Message-Id:") repeat=$(echo "$mail" | ${pkgs.procmail}/bin/formail -X "From:" -X "Received:") ${pkgs.coreutils}/bin/cat <')" ''; }; in builtins.concatStringsSep "\n" (lib.attrsets.mapAttrsToList (n: v: ''${n}: "|${v}"'') scripts); mapFiles = let recipient_maps = let name = n: i: "relay_${n}_${toString i}"; pair = n: i: m: lib.attrsets.nameValuePair (name n i) ( if m.type == "hash" then pkgs.writeText (name n i) m.content else null ); pairs = n: v: lib.imap1 (i: m: pair n i m) v.recipient_maps; in lib.attrsets.filterAttrs (k: v: v != null) ( lib.attrsets.listToAttrs (lib.flatten ( lib.attrsets.mapAttrsToList pairs config.myEnv.mail.postfix.backup_domains )) ); relay_restrictions = lib.attrsets.filterAttrs (k: v: v != null) ( lib.attrsets.mapAttrs' (n: v: lib.attrsets.nameValuePair "recipient_access_${n}" ( if lib.attrsets.hasAttr "relay_restrictions" v then pkgs.writeText "recipient_access_${n}" v.relay_restrictions else null ) ) config.myEnv.mail.postfix.backup_domains ); virtual_map = { virtual = let cfg = config.myEnv.monitoring.email_check.eldiron; address = "${cfg.mail_address}@${cfg.mail_domain}"; in pkgs.writeText "postfix-virtual" ( builtins.concatStringsSep "\n" ( ["${address} testmail@localhost"] ++ lib.attrsets.mapAttrsToList ( n: v: lib.optionalString v.external '' script_${n}@mail.immae.eu ${n}@localhost, scripts@mail.immae.eu '' ) config.myEnv.mail.scripts ) ); }; 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)); joined = builtins.concatStringsSep ","; in pkgs.writeText "host-sender-login" (builtins.concatStringsSep "\n" (mapAttrsToList (n: v: "${n} ${joined v}") addresses)); }; in recipient_maps // relay_restrictions // 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 = config.myEnv.mail.postfix.additional_mailbox_domains ++ lib.remove null (lib.flatten (map (zone: map (e: if e.receive then "${e.domain}${lib.optionalString (e.domain != "") "."}${zone.name}" else null ) (zone.withEmail or []) ) config.myEnv.dns.masterZones )); virtual_mailbox_maps = [ "ldap:${config.secrets.fullPaths."postfix/ldap_mailboxes"}" ]; dovecot_destination_recipient_limit = "1"; virtual_transport = "dovecot"; ### Relay domains relay_domains = lib.flatten (lib.attrsets.mapAttrsToList (n: v: v.domains or []) config.myEnv.mail.postfix.backup_domains); relay_recipient_maps = lib.flatten (lib.attrsets.mapAttrsToList (n: v: lib.imap1 (i: m: "${m.type}:/etc/postfix/relay_${n}_${toString i}") v.recipient_maps ) config.myEnv.mail.postfix.backup_domains); smtpd_relay_restrictions = [ "defer_unauth_destination" ] ++ lib.flatten (lib.attrsets.mapAttrsToList (n: v: if lib.attrsets.hasAttr "relay_restrictions" v then [ "check_recipient_access hash:/etc/postfix/recipient_access_${n}" ] else [] ) config.myEnv.mail.postfix.backup_domains); ### 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 = 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 = builtins.concatStringsSep "," [ "/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 = { # 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 = builtins.concatStringsSep "," [ "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 = builtins.concatStringsSep "," [ # 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} 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 || true) | \ ${pkgs.dovecot}/libexec/dovecot/dovecot-lda -f "$sender" -a "$original_recipient" -d "$user" ''; 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 ''; extraDomains = { "smtp.immae.eu" = null; }; }; security.acme.certs."mail-rsa" = { postRun = '' systemctl restart postfix.service ''; extraDomains = { "smtp.immae.eu" = null; }; }; 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"; }; }