{ config, lib, pkgs, ... }: with lib; let cfg = config.services.postfix; user = cfg.user; group = cfg.group; setgidGroup = cfg.setgidGroup; haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" || cfg.extraAliases != ""; haveTransport = cfg.transport != ""; haveVirtual = cfg.virtual != ""; clientAccess = if (cfg.dnsBlacklistOverrides != "") then [ "check_client_access hash:/etc/postfix/client_access" ] else []; dnsBl = if (cfg.dnsBlacklists != []) then [ (concatStringsSep ", " (map (s: "reject_rbl_client " + s) cfg.dnsBlacklists)) ] else []; clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl); mainCf = '' compatibility_level = 9999 mail_owner = ${user} default_privs = nobody # NixOS specific locations data_directory = /var/lib/postfix/data queue_directory = /var/lib/postfix/queue # Default location of everything in package meta_directory = ${pkgs.postfix}/etc/postfix command_directory = ${pkgs.postfix}/bin sample_directory = /etc/postfix newaliases_path = ${pkgs.postfix}/bin/newaliases mailq_path = ${pkgs.postfix}/bin/mailq readme_directory = no sendmail_path = ${pkgs.postfix}/bin/sendmail daemon_directory = ${pkgs.postfix}/libexec/postfix manpage_directory = ${pkgs.postfix}/share/man html_directory = ${pkgs.postfix}/share/postfix/doc/html shlib_directory = no '' + optionalString config.networking.enableIPv6 '' inet_protocols = all '' + (if cfg.networks != null then '' mynetworks = ${concatStringsSep ", " cfg.networks} '' else if cfg.networksStyle != "" then '' mynetworks_style = ${cfg.networksStyle} '' else "") + optionalString (cfg.hostname != "") '' myhostname = ${cfg.hostname} '' + optionalString (cfg.domain != "") '' mydomain = ${cfg.domain} '' + optionalString (cfg.origin != "") '' myorigin = ${cfg.origin} '' + optionalString (cfg.destination != null) '' mydestination = ${concatStringsSep ", " cfg.destination} '' + optionalString (cfg.relayDomains != null) '' relay_domains = ${concatStringsSep ", " cfg.relayDomains} '' + '' local_recipient_maps = relayhost = ${if cfg.lookupMX || cfg.relayHost == "" then cfg.relayHost else "[" + cfg.relayHost + "]"} mail_spool_directory = /var/spool/mail/ setgid_group = ${setgidGroup} '' + optionalString (cfg.sslCert != "") '' smtp_tls_CAfile = ${cfg.sslCACert} smtp_tls_cert_file = ${cfg.sslCert} smtp_tls_key_file = ${cfg.sslKey} smtp_use_tls = yes smtpd_tls_CAfile = ${cfg.sslCACert} smtpd_tls_cert_file = ${cfg.sslCert} smtpd_tls_key_file = ${cfg.sslKey} smtpd_use_tls = yes '' + optionalString (cfg.recipientDelimiter != "") '' recipient_delimiter = ${cfg.recipientDelimiter} '' + optionalString haveAliases '' alias_maps = hash:/etc/postfix/aliases '' + optionalString haveTransport '' transport_maps = hash:/etc/postfix/transport '' + optionalString haveVirtual '' virtual_alias_maps = hash:/etc/postfix/virtual '' + optionalString (cfg.dnsBlacklists != []) '' smtpd_client_restrictions = ${clientRestrictions} '' + cfg.extraConfig; masterCf = '' # ========================================================================== # service type private unpriv chroot wakeup maxproc command + args # (yes) (yes) (no) (never) (100) # ========================================================================== smtp inet n - n - - smtpd #submission inet n - n - - smtpd # -o smtpd_tls_security_level=encrypt # -o smtpd_sasl_auth_enable=yes # -o smtpd_client_restrictions=permit_sasl_authenticated,reject # -o milter_macro_daemon_name=ORIGINATING pickup unix n - n 60 1 pickup cleanup unix n - n - 0 cleanup qmgr unix n - n 300 1 qmgr tlsmgr unix - - n 1000? 1 tlsmgr rewrite unix - - n - - trivial-rewrite bounce unix - - n - 0 bounce defer unix - - n - 0 bounce trace unix - - n - 0 bounce verify unix - - n - 1 verify flush unix n - n 1000? 0 flush proxymap unix - - n - - proxymap proxywrite unix - - n - 1 proxymap '' + optionalString cfg.enableSmtp '' smtp unix - - n - - smtp relay unix - - n - - smtp -o smtp_fallback_relay= # -o smtp_helo_timeout=5 -o smtp_connect_timeout=5 '' + '' showq unix n - n - - showq error unix - - n - - error retry unix - - n - - error discard unix - - n - - discard local unix - n n - - local virtual unix - n n - - virtual lmtp unix - - n - - lmtp anvil unix - - n - 1 anvil scache unix - - n - 1 scache ${cfg.extraMasterConf} ''; aliases = optionalString (cfg.postmasterAlias != "") '' postmaster: ${cfg.postmasterAlias} '' + optionalString (cfg.rootAlias != "") '' root: ${cfg.rootAlias} '' + cfg.extraAliases ; aliasesFile = pkgs.writeText "postfix-aliases" aliases; virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual; checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides; mainCfFile = pkgs.writeText "postfix-main.cf" mainCf; masterCfFile = pkgs.writeText "postfix-master.cf" masterCf; transportFile = pkgs.writeText "postfix-transport" cfg.transport; in { ###### interface options = { services.postfix = { enable = mkOption { type = types.bool; default = false; description = "Whether to run the Postfix mail server."; }; enableSmtp = mkOption { default = true; description = "Whether to enable smtp in master.cf."; }; setSendmail = mkOption { type = types.bool; default = true; description = "Whether to set the system sendmail to postfix's."; }; user = mkOption { type = types.str; default = "postfix"; description = "What to call the Postfix user (must be used only for postfix)."; }; group = mkOption { type = types.str; default = "postfix"; description = "What to call the Postfix group (must be used only for postfix)."; }; setgidGroup = mkOption { type = types.str; default = "postdrop"; description = " How to call postfix setgid group (for postdrop). Should be uniquely used group. "; }; networks = mkOption { type = types.nullOr (types.listOf types.str); default = null; example = ["192.168.0.1/24"]; description = " Net masks for trusted - allowed to relay mail to third parties - hosts. Leave empty to use mynetworks_style configuration or use default (localhost-only). "; }; networksStyle = mkOption { type = types.str; default = ""; description = " Name of standard way of trusted network specification to use, leave blank if you specify it explicitly or if you want to use default (localhost-only). "; }; hostname = mkOption { type = types.str; default = ""; description =" Hostname to use. Leave blank to use just the hostname of machine. It should be FQDN. "; }; domain = mkOption { type = types.str; default = ""; description =" Domain to use. Leave blank to use hostname minus first component. "; }; origin = mkOption { type = types.str; default = ""; description =" Origin to use in outgoing e-mail. Leave blank to use hostname. "; }; destination = mkOption { type = types.nullOr (types.listOf types.str); default = null; example = ["localhost"]; description = " Full (!) list of domains we deliver locally. Leave blank for acceptable Postfix default. "; }; relayDomains = mkOption { type = types.nullOr (types.listOf types.str); default = null; example = ["localdomain"]; description = " List of domains we agree to relay to. Default is empty. "; }; relayHost = mkOption { type = types.str; default = ""; description = " Mail relay for outbound mail. "; }; lookupMX = mkOption { type = types.bool; default = false; description = " Whether relay specified is just domain whose MX must be used. "; }; postmasterAlias = mkOption { type = types.str; default = "root"; description = "Who should receive postmaster e-mail."; }; rootAlias = mkOption { type = types.str; default = ""; description = " Who should receive root e-mail. Blank for no redirection. "; }; extraAliases = mkOption { type = types.lines; default = ""; description = " Additional entries to put verbatim into aliases file, cf. man-page aliases(8). "; }; extraConfig = mkOption { type = types.lines; default = ""; description = " Extra lines to be added verbatim to the main.cf configuration file. "; }; sslCert = mkOption { type = types.str; default = ""; description = "SSL certificate to use."; }; sslCACert = mkOption { type = types.str; default = ""; description = "SSL certificate of CA."; }; sslKey = mkOption { type = types.str; default = ""; description = "SSL key to use."; }; recipientDelimiter = mkOption { type = types.str; default = ""; example = "+"; description = " Delimiter for address extension: so mail to user+test can be handled by ~user/.forward+test "; }; virtual = mkOption { type = types.lines; default = ""; description = " Entries for the virtual alias map, cf. man-page virtual(8). "; }; transport = mkOption { default = ""; description = " Entries for the transport map, cf. man-page transport(8). "; }; dnsBlacklists = mkOption { default = []; type = with types; listOf string; description = "dns blacklist servers to use with smtpd_client_restrictions"; }; dnsBlacklistOverrides = mkOption { default = ""; description = "contents of check_client_access for overriding dnsBlacklists"; }; extraMasterConf = mkOption { type = types.lines; default = ""; example = "submission inet n - n - - smtpd"; description = "Extra lines to append to the generated master.cf file."; }; aliasFiles = mkOption { type = types.attrsOf types.path; default = {}; description = "Aliases' tables to be compiled and placed into /var/lib/postfix/conf."; }; mapFiles = mkOption { type = types.attrsOf types.path; default = {}; description = "Maps to be compiled and placed into /var/lib/postfix/conf."; }; }; }; ###### implementation config = mkIf config.services.postfix.enable (mkMerge [ { environment = { etc = singleton { source = "/var/lib/postfix/conf"; target = "postfix"; }; # This makes comfortable for root to run 'postqueue' for example. systemPackages = [ pkgs.postfix ]; }; services.mail.sendmailSetuidWrapper = mkIf config.services.postfix.setSendmail { program = "sendmail"; source = "${pkgs.postfix}/bin/sendmail"; group = setgidGroup; setuid = false; setgid = true; }; users.extraUsers = optional (user == "postfix") { name = "postfix"; description = "Postfix mail server user"; uid = config.ids.uids.postfix; group = group; }; users.extraGroups = optional (group == "postfix") { name = group; gid = config.ids.gids.postfix; } ++ optional (setgidGroup == "postdrop") { name = setgidGroup; gid = config.ids.gids.postdrop; }; systemd.services.postfix = { description = "Postfix mail server"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; path = [ pkgs.postfix ]; serviceConfig = { Type = "forking"; Restart = "always"; PIDFile = "/var/lib/postfix/queue/pid/master.pid"; ExecStart = "${pkgs.postfix}/bin/postfix start"; ExecStop = "${pkgs.postfix}/bin/postfix stop"; ExecReload = "${pkgs.postfix}/bin/postfix reload"; }; preStart = '' # Backwards compatibility if [ ! -d /var/lib/postfix ] && [ -d /var/postfix ]; then mkdir -p /var/lib mv /var/postfix /var/lib/postfix fi # All permissions set according ${pkgs.postfix}/etc/postfix/postfix-files script mkdir -p /var/lib/postfix /var/lib/postfix/queue/{pid,public,maildrop} chmod 0755 /var/lib/postfix chown root:root /var/lib/postfix rm -rf /var/lib/postfix/conf mkdir -p /var/lib/postfix/conf chmod 0755 /var/lib/postfix/conf ln -sf ${pkgs.postfix}/etc/postfix/postfix-files /var/lib/postfix/conf/postfix-files ln -sf ${mainCfFile} /var/lib/postfix/conf/main.cf ln -sf ${masterCfFile} /var/lib/postfix/conf/master.cf ${concatStringsSep "\n" (mapAttrsToList (to: from: '' ln -sf ${from} /var/lib/postfix/conf/${to} ${pkgs.postfix}/bin/postalias /var/lib/postfix/conf/${to} '') cfg.aliasFiles)} ${concatStringsSep "\n" (mapAttrsToList (to: from: '' ln -sf ${from} /var/lib/postfix/conf/${to} ${pkgs.postfix}/bin/postmap /var/lib/postfix/conf/${to} '') cfg.mapFiles)} mkdir -p /var/spool/mail chown root:root /var/spool/mail chmod a+rwxt /var/spool/mail ln -sf /var/spool/mail /var/ #Finally delegate to postfix checking remain directories in /var/lib/postfix and set permissions on them ${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf ''; }; } (mkIf haveAliases { services.postfix.aliasFiles."aliases" = aliasesFile; }) (mkIf haveTransport { services.postfix.mapFiles."transport" = transportFile; }) (mkIf haveVirtual { services.postfix.mapFiles."virtual" = virtualFile; }) (mkIf (cfg.dnsBlacklists != []) { services.postfix.mapFiles."client_access" = checkClientAccessFile; }) ]); }