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 cfg = config.mailserver; 23 24 passwdDir = "/run/dovecot2"; 25 passwdFile = "${passwdDir}/passwd"; 26 27 bool2int = x: if x then "1" else "0"; 28 29 maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; 30 31 # maildir in format "/${domain}/${user}" 32 dovecotMaildir = 33 "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}" 34 + (lib.optionalString (cfg.indexDir != null) 35 ":INDEX=${cfg.indexDir}/%d/%n" 36 ); 37 38 postfixCfg = config.services.postfix; 39 dovecot2Cfg = config.services.dovecot2; 40 41 stateDir = "/var/lib/dovecot"; 42 43 pipeBin = pkgs.stdenv.mkDerivation { 44 name = "pipe_bin"; 45 src = ./dovecot/pipe_bin; 46 buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ]; 47 buildCommand = '' 48 mkdir -p $out/pipe/bin 49 cp $src/* $out/pipe/bin/ 50 chmod a+x $out/pipe/bin/* 51 patchShebangs $out/pipe/bin 52 53 for file in $out/pipe/bin/*; do 54 wrapProgram $file \ 55 --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" 56 done 57 ''; 58 }; 59 60 genPasswdScript = pkgs.writeScript "generate-password-file" '' 61 #!${pkgs.stdenv.shell} 62 63 set -euo pipefail 64 65 if (! test -d "${passwdDir}"); then 66 mkdir "${passwdDir}" 67 chmod 755 "${passwdDir}" 68 fi 69 70 for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do 71 if [ ! -f "$f" ]; then 72 echo "Expected password hash file $f does not exist!" 73 exit 1 74 fi 75 done 76 77 cat <<EOF > ${passwdFile} 78 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: 79 "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:" 80 + (if lib.isString value.quota 81 then "userdb_quota_rule=*:storage=${value.quota}" 82 else "") 83 ) cfg.loginAccounts)} 84 EOF 85 86 chmod 600 ${passwdFile} 87 ''; 88 89 junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes); 90 junkMailboxNumber = builtins.length junkMailboxes; 91 # The assertion garantees there is exactly one Junk mailbox. 92 junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else ""; 93 94in 95{ 96 config = with cfg; lib.mkIf enable { 97 assertions = [ 98 { 99 assertion = junkMailboxNumber == 1; 100 message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; 101 } 102 ]; 103 104 services.dovecot2 = { 105 enable = true; 106 enableImap = enableImap || enableImapSsl; 107 enablePop3 = enablePop3 || enablePop3Ssl; 108 enablePAM = false; 109 enableQuota = true; 110 mailGroup = vmailGroupName; 111 mailUser = vmailUserName; 112 mailLocation = dovecotMaildir; 113 sslServerCert = certificatePath; 114 sslServerKey = keyPath; 115 enableLmtp = true; 116 modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian ); 117 mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ]; 118 protocols = lib.optional cfg.enableManageSieve "sieve"; 119 120 sieveScripts = { 121 after = builtins.toFile "spam.sieve" '' 122 require "fileinto"; 123 124 if header :is "X-Spam" "Yes" { 125 fileinto "${junkMailboxName}"; 126 stop; 127 } 128 ''; 129 }; 130 131 mailboxes = cfg.mailboxes; 132 133 extraConfig = '' 134 #Extra Config 135 ${lib.optionalString debug '' 136 mail_debug = yes 137 auth_debug = yes 138 verbose_ssl = yes 139 ''} 140 141 ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' 142 service imap-login { 143 inet_listener imap { 144 ${if cfg.enableImap then '' 145 port = 143 146 '' else '' 147 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html 148 port = 0 149 ''} 150 } 151 inet_listener imaps { 152 ${if cfg.enableImapSsl then '' 153 port = 993 154 ssl = yes 155 '' else '' 156 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html 157 port = 0 158 ''} 159 } 160 } 161 ''} 162 ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' 163 service pop3-login { 164 inet_listener pop3 { 165 ${if cfg.enablePop3 then '' 166 port = 110 167 '' else '' 168 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html 169 port = 0 170 ''} 171 } 172 inet_listener pop3s { 173 ${if cfg.enablePop3Ssl then '' 174 port = 995 175 ssl = yes 176 '' else '' 177 # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html 178 port = 0 179 ''} 180 } 181 } 182 ''} 183 184 protocol imap { 185 mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} 186 mail_plugins = $mail_plugins imap_sieve 187 } 188 189 protocol pop3 { 190 mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} 191 } 192 193 mail_access_groups = ${vmailGroupName} 194 ssl = required 195 ssl_min_protocol = TLSv1.2 196 ssl_prefer_server_ciphers = yes 197 198 service lmtp { 199 unix_listener dovecot-lmtp { 200 group = ${postfixCfg.group} 201 mode = 0600 202 user = ${postfixCfg.user} 203 } 204 } 205 206 recipient_delimiter = ${cfg.recipientDelimiter} 207 lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} 208 209 protocol lmtp { 210 mail_plugins = $mail_plugins sieve 211 } 212 213 passdb { 214 driver = passwd-file 215 args = ${passwdFile} 216 } 217 218 userdb { 219 driver = passwd-file 220 args = ${passwdFile} 221 } 222 223 service auth { 224 unix_listener auth { 225 mode = 0660 226 user = ${postfixCfg.user} 227 group = ${postfixCfg.group} 228 } 229 } 230 231 auth_mechanisms = plain login 232 233 namespace inbox { 234 separator = ${cfg.hierarchySeparator} 235 inbox = yes 236 } 237 238 plugin { 239 sieve_plugins = sieve_imapsieve sieve_extprograms 240 sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve 241 sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve 242 sieve_default_name = default 243 244 # From elsewhere to Spam folder 245 imapsieve_mailbox1_name = ${junkMailboxName} 246 imapsieve_mailbox1_causes = COPY 247 imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve 248 249 # From Spam folder to elsewhere 250 imapsieve_mailbox2_name = * 251 imapsieve_mailbox2_from = ${junkMailboxName} 252 imapsieve_mailbox2_causes = COPY 253 imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve 254 255 sieve_pipe_bin_dir = ${pipeBin}/pipe/bin 256 257 sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment 258 } 259 260 ${lib.optionalString cfg.fullTextSearch.enable '' 261 plugin { 262 plugin = fts fts_xapian 263 fts = xapian 264 fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug} 265 266 fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"} 267 268 ${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude} 269 270 fts_enforced = ${cfg.fullTextSearch.enforced} 271 } 272 273 ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' 274 service indexer-worker { 275 vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)} 276 } 277 ''} 278 ''} 279 280 lda_mailbox_autosubscribe = yes 281 lda_mailbox_autocreate = yes 282 ''; 283 }; 284 285 systemd.services.dovecot2 = { 286 preStart = '' 287 ${genPasswdScript} 288 rm -rf '${stateDir}/imap_sieve' 289 mkdir '${stateDir}/imap_sieve' 290 cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/' 291 for k in "${stateDir}/imap_sieve"/*.sieve ; do 292 ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" 293 done 294 chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' 295 ''; 296 }; 297 298 systemd.services.postfix.restartTriggers = [ genPasswdScript ]; 299 300 systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) { 301 description = "Optimize dovecot indices for fts_xapian"; 302 requisite = [ "dovecot2.service" ]; 303 after = [ "dovecot2.service" ]; 304 startAt = cfg.fullTextSearch.maintenance.onCalendar; 305 serviceConfig = { 306 Type = "oneshot"; 307 ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A"; 308 PrivateDevices = true; 309 PrivateNetwork = true; 310 ProtectKernelTunables = true; 311 ProtectKernelModules = true; 312 ProtectControlGroups = true; 313 ProtectHome = true; 314 ProtectSystem = true; 315 PrivateTmp = true; 316 }; 317 }; 318 systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) { 319 timerConfig = { 320 RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec; 321 }; 322 }; 323 }; 324}