Self-host your own digital island
1# nixos-mailserver: a simple mail server 2# Copyright (C) 2016-2018 Robin Raymond 3# 4# This program is free software: you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation, either version 3 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program. If not, see <http://www.gnu.org/licenses/> 16 17{ config, pkgs, lib, ... }: 18 19with (import ./common.nix { inherit config pkgs lib; }); 20 21let 22 inherit (lib.strings) concatStringsSep; 23 cfg = config.mailserver; 24 25 # Merge several lookup tables. A lookup table is a attribute set where 26 # - the key is an address (user@example.com) or a domain (@example.com) 27 # - the value is a list of addresses 28 mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables; 29 30 # valiases_postfix :: Map String [String] 31 valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList 32 (name: value: 33 let to = name; 34 in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name)) 35 cfg.loginAccounts)); 36 37 # catchAllPostfix :: Map String [String] 38 catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList 39 (name: value: 40 let to = name; 41 in map (from: {"@${from}" = to;}) value.catchAll) 42 cfg.loginAccounts)); 43 44 # all_valiases_postfix :: Map String [String] 45 all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix]; 46 47 # attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String] 48 attrsToLookupTable = aliases: let 49 lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases; 50 in mergeLookupTables lookupTables; 51 52 # extra_valiases_postfix :: Map String [String] 53 extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases; 54 55 # forwards :: Map String [String] 56 forwards = attrsToLookupTable cfg.forwards; 57 58 # lookupTableToString :: Map String [String] -> String 59 lookupTableToString = attrs: let 60 valueToString = value: lib.concatStringsSep ", " value; 61 in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); 62 63 # valiases_file :: Path 64 valiases_file = let 65 content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]); 66 in builtins.toFile "valias" content; 67 68 # denied_recipients_postfix :: [ String ] 69 denied_recipients_postfix = (map 70 (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") 71 (lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts))); 72 denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix); 73 74 reject_senders_postfix = (map 75 (sender: 76 "${sender} REJECT") 77 (cfg.rejectSender)); 78 reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ; 79 80 reject_recipients_postfix = (map 81 (recipient: 82 "${recipient} REJECT") 83 (cfg.rejectRecipients)); 84 # rejectRecipients :: [ Path ] 85 reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ; 86 87 # vhosts_file :: Path 88 vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains); 89 90 # vaccounts_file :: Path 91 # see 92 # https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/ 93 # for details on how this file looks. By using the same file as valiases, 94 # every alias is owned (uniquely) by its user. 95 # The user's own address is already in all_valiases_postfix. 96 vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); 97 98 submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' 99 # Removes sensitive headers from mails handed in via the submission port. 100 # See https://thomas-leister.de/mailserver-debian-stretch/ 101 # Uses "pcre" style regex. 102 103 /^Received:/ IGNORE 104 /^X-Originating-IP:/ IGNORE 105 /^X-Mailer:/ IGNORE 106 /^User-Agent:/ IGNORE 107 /^X-Enigmail:/ IGNORE 108 '' + lib.optionalString cfg.rewriteMessageId '' 109 110 # Replaces the user submitted hostname with the server's FQDN to hide the 111 # user's host or network. 112 113 /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> 114 ''); 115 116 inetSocket = addr: port: "inet:[${toString port}@${addr}]"; 117 unixSocket = sock: "unix:${sock}"; 118 119 smtpdMilters = 120 (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") 121 ++ [ "unix:/run/rspamd/rspamd-milter.sock" ]; 122 123 policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig; 124 125 mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; 126 127 submissionOptions = 128 { 129 smtpd_tls_security_level = "encrypt"; 130 smtpd_sasl_auth_enable = "yes"; 131 smtpd_sasl_type = "dovecot"; 132 smtpd_sasl_path = "/run/dovecot2/auth"; 133 smtpd_sasl_security_options = "noanonymous"; 134 smtpd_sasl_local_domain = "$myhostname"; 135 smtpd_client_restrictions = "permit_sasl_authenticated,reject"; 136 smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts"; 137 smtpd_sender_restrictions = "reject_sender_login_mismatch"; 138 smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; 139 cleanup_service_name = "submission-header-cleanup"; 140 }; 141in 142{ 143 config = with cfg; lib.mkIf enable { 144 145 services.postfix = { 146 enable = true; 147 hostname = "${sendingFqdn}"; 148 networksStyle = "host"; 149 mapFiles."valias" = valiases_file; 150 mapFiles."vaccounts" = vaccounts_file; 151 mapFiles."denied_recipients" = denied_recipients_file; 152 mapFiles."reject_senders" = reject_senders_file; 153 mapFiles."reject_recipients" = reject_recipients_file; 154 sslCert = certificatePath; 155 sslKey = keyPath; 156 enableSubmission = cfg.enableSubmission; 157 enableSubmissions = cfg.enableSubmissionSsl; 158 virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]); 159 160 config = { 161 # Extra Config 162 mydestination = ""; 163 recipient_delimiter = cfg.recipientDelimiter; 164 smtpd_banner = "${fqdn} ESMTP NO UCE"; 165 disable_vrfy_command = true; 166 message_size_limit = toString cfg.messageSizeLimit; 167 168 # virtual mail system 169 virtual_uid_maps = "static:5000"; 170 virtual_gid_maps = "static:5000"; 171 virtual_mailbox_base = mailDirectory; 172 virtual_mailbox_domains = vhosts_file; 173 virtual_mailbox_maps = mappedFile "valias"; 174 virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; 175 # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients 176 lmtp_destination_recipient_limit = "1"; 177 178 # sasl with dovecot 179 smtpd_sasl_type = "dovecot"; 180 smtpd_sasl_path = "/run/dovecot2/auth"; 181 smtpd_sasl_auth_enable = true; 182 smtpd_relay_restrictions = [ 183 "permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination" 184 ]; 185 186 policy-spf_time_limit = "3600s"; 187 188 # reject selected senders 189 smtpd_sender_restrictions = [ 190 "check_sender_access ${mappedFile "reject_senders"}" 191 ]; 192 193 # quota and spf checking 194 smtpd_recipient_restrictions = [ 195 "check_recipient_access ${mappedFile "denied_recipients"}" 196 "check_recipient_access ${mappedFile "reject_recipients"}" 197 "check_policy_service inet:localhost:12340" 198 "check_policy_service unix:private/policy-spf" 199 ]; 200 201 # TLS settings, inspired by https://github.com/jeaye/nix-files 202 # Submission by mail clients is handled in submissionOptions 203 smtpd_tls_security_level = "may"; 204 205 # strong might suffice and is computationally less expensive 206 smtpd_tls_eecdh_grade = "ultra"; 207 208 # Disable obselete protocols 209 smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; 210 smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; 211 smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; 212 smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; 213 214 smtp_tls_ciphers = "high"; 215 smtpd_tls_ciphers = "high"; 216 smtp_tls_mandatory_ciphers = "high"; 217 smtpd_tls_mandatory_ciphers = "high"; 218 219 # Disable deprecated ciphers 220 smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; 221 smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; 222 smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; 223 smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL"; 224 225 tls_preempt_cipherlist = true; 226 227 # Allowing AUTH on a non encrypted connection poses a security risk 228 smtpd_tls_auth_only = true; 229 # Log only a summary message on TLS handshake completion 230 smtpd_tls_loglevel = "1"; 231 232 # Configure a non blocking source of randomness 233 tls_random_source = "dev:/dev/urandom"; 234 235 smtpd_milters = smtpdMilters; 236 non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"]; 237 milter_protocol = "6"; 238 milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; 239 240 }; 241 242 submissionOptions = submissionOptions; 243 submissionsOptions = submissionOptions; 244 245 masterConfig = { 246 "lmtp" = { 247 # Add headers when delivering, see http://www.postfix.org/smtp.8.html 248 # D => Delivered-To, O => X-Original-To, R => Return-Path 249 args = [ "flags=O" ]; 250 }; 251 "policy-spf" = { 252 type = "unix"; 253 privileged = true; 254 chroot = false; 255 command = "spawn"; 256 args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"]; 257 }; 258 "submission-header-cleanup" = { 259 type = "unix"; 260 private = false; 261 chroot = false; 262 maxproc = 0; 263 command = "cleanup"; 264 args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"]; 265 }; 266 }; 267 }; 268 }; 269}