--- /dev/null
+{ 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";
+ }
+ ];
+ };
+}