at 23.11-pre 14 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 7 name = "maddy"; 8 9 cfg = config.services.maddy; 10 11 defaultConfig = '' 12 # Minimal configuration with TLS disabled, adapted from upstream example 13 # configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf 14 # Do not use this in production! 15 16 auth.pass_table local_authdb { 17 table sql_table { 18 driver sqlite3 19 dsn credentials.db 20 table_name passwords 21 } 22 } 23 24 storage.imapsql local_mailboxes { 25 driver sqlite3 26 dsn imapsql.db 27 } 28 29 table.chain local_rewrites { 30 optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" 31 optional_step static { 32 entry postmaster postmaster@$(primary_domain) 33 } 34 optional_step file /etc/maddy/aliases 35 } 36 37 msgpipeline local_routing { 38 destination postmaster $(local_domains) { 39 modify { 40 replace_rcpt &local_rewrites 41 } 42 deliver_to &local_mailboxes 43 } 44 default_destination { 45 reject 550 5.1.1 "User doesn't exist" 46 } 47 } 48 49 smtp tcp://0.0.0.0:25 { 50 limits { 51 all rate 20 1s 52 all concurrency 10 53 } 54 dmarc yes 55 check { 56 require_mx_record 57 dkim 58 spf 59 } 60 source $(local_domains) { 61 reject 501 5.1.8 "Use Submission for outgoing SMTP" 62 } 63 default_source { 64 destination postmaster $(local_domains) { 65 deliver_to &local_routing 66 } 67 default_destination { 68 reject 550 5.1.1 "User doesn't exist" 69 } 70 } 71 } 72 73 submission tcp://0.0.0.0:587 { 74 limits { 75 all rate 50 1s 76 } 77 auth &local_authdb 78 source $(local_domains) { 79 check { 80 authorize_sender { 81 prepare_email &local_rewrites 82 user_to_email identity 83 } 84 } 85 destination postmaster $(local_domains) { 86 deliver_to &local_routing 87 } 88 default_destination { 89 modify { 90 dkim $(primary_domain) $(local_domains) default 91 } 92 deliver_to &remote_queue 93 } 94 } 95 default_source { 96 reject 501 5.1.8 "Non-local sender domain" 97 } 98 } 99 100 target.remote outbound_delivery { 101 limits { 102 destination rate 20 1s 103 destination concurrency 10 104 } 105 mx_auth { 106 dane 107 mtasts { 108 cache fs 109 fs_dir mtasts_cache/ 110 } 111 local_policy { 112 min_tls_level encrypted 113 min_mx_level none 114 } 115 } 116 } 117 118 target.queue remote_queue { 119 target &outbound_delivery 120 autogenerated_msg_domain $(primary_domain) 121 bounce { 122 destination postmaster $(local_domains) { 123 deliver_to &local_routing 124 } 125 default_destination { 126 reject 550 5.0.0 "Refusing to send DSNs to non-local addresses" 127 } 128 } 129 } 130 131 imap tcp://0.0.0.0:143 { 132 auth &local_authdb 133 storage &local_mailboxes 134 } 135 ''; 136 137in { 138 options = { 139 services.maddy = { 140 141 enable = mkEnableOption (lib.mdDoc "Maddy, a free an open source mail server"); 142 143 user = mkOption { 144 default = "maddy"; 145 type = with types; uniq string; 146 description = lib.mdDoc '' 147 User account under which maddy runs. 148 149 ::: {.note} 150 If left as the default value this user will automatically be created 151 on system activation, otherwise the sysadmin is responsible for 152 ensuring the user exists before the maddy service starts. 153 ::: 154 ''; 155 }; 156 157 group = mkOption { 158 default = "maddy"; 159 type = with types; uniq string; 160 description = lib.mdDoc '' 161 Group account under which maddy runs. 162 163 ::: {.note} 164 If left as the default value this group will automatically be created 165 on system activation, otherwise the sysadmin is responsible for 166 ensuring the group exists before the maddy service starts. 167 ::: 168 ''; 169 }; 170 171 hostname = mkOption { 172 default = "localhost"; 173 type = with types; uniq string; 174 example = ''example.com''; 175 description = lib.mdDoc '' 176 Hostname to use. It should be FQDN. 177 ''; 178 }; 179 180 primaryDomain = mkOption { 181 default = "localhost"; 182 type = with types; uniq string; 183 example = ''mail.example.com''; 184 description = lib.mdDoc '' 185 Primary MX domain to use. It should be FQDN. 186 ''; 187 }; 188 189 localDomains = mkOption { 190 type = with types; listOf str; 191 default = ["$(primary_domain)"]; 192 example = [ 193 "$(primary_domain)" 194 "example.com" 195 "other.example.com" 196 ]; 197 description = lib.mdDoc '' 198 Define list of allowed domains. 199 ''; 200 }; 201 202 config = mkOption { 203 type = with types; nullOr lines; 204 default = defaultConfig; 205 description = lib.mdDoc '' 206 Server configuration, see 207 [https://maddy.email](https://maddy.email) for 208 more information. The default configuration of this module will setup 209 minimal Maddy instance for mail transfer without TLS encryption. 210 211 ::: {.note} 212 This should not be used in a production environment. 213 ::: 214 ''; 215 }; 216 217 tls = { 218 loader = mkOption { 219 type = with types; nullOr (enum [ "off" "file" "acme" ]); 220 default = "off"; 221 description = lib.mdDoc '' 222 TLS certificates are obtained by modules called "certificate 223 loaders". 224 225 The `file` loader module reads certificates from files specified by 226 the `certificates` option. 227 228 Alternatively the `acme` module can be used to automatically obtain 229 certificates using the ACME protocol. 230 231 Module configuration is done via the `tls.extraConfig` option. 232 233 Secrets such as API keys or passwords should not be supplied in 234 plaintext. Instead the `secrets` option can be used to read secrets 235 at runtime as environment variables. Secrets can be referenced with 236 `{env:VAR}`. 237 ''; 238 }; 239 240 certificates = mkOption { 241 type = with types; listOf (submodule { 242 options = { 243 keyPath = mkOption { 244 type = types.path; 245 example = "/etc/ssl/mx1.example.org.key"; 246 description = lib.mdDoc '' 247 Path to the private key used for TLS. 248 ''; 249 }; 250 certPath = mkOption { 251 type = types.path; 252 example = "/etc/ssl/mx1.example.org.crt"; 253 description = lib.mdDoc '' 254 Path to the certificate used for TLS. 255 ''; 256 }; 257 }; 258 }); 259 default = []; 260 example = lib.literalExpression '' 261 [{ 262 keyPath = "/etc/ssl/mx1.example.org.key"; 263 certPath = "/etc/ssl/mx1.example.org.crt"; 264 }] 265 ''; 266 description = lib.mdDoc '' 267 A list of attribute sets containing paths to TLS certificates and 268 keys. Maddy will use SNI if multiple pairs are selected. 269 ''; 270 }; 271 272 extraConfig = mkOption { 273 type = with types; nullOr lines; 274 description = lib.mdDoc '' 275 Arguments for the specified certificate loader. 276 277 In case the `tls` loader is set, the defaults are considered secure 278 and there is no need to change anything in most cases. 279 For available options see [upstream manual](https://maddy.email/reference/tls/). 280 281 For ACME configuration, see [following page](https://maddy.email/reference/tls-acme). 282 ''; 283 default = ""; 284 }; 285 }; 286 287 openFirewall = mkOption { 288 type = types.bool; 289 default = false; 290 description = lib.mdDoc '' 291 Open the configured incoming and outgoing mail server ports. 292 ''; 293 }; 294 295 ensureAccounts = mkOption { 296 type = with types; listOf str; 297 default = []; 298 description = lib.mdDoc '' 299 List of IMAP accounts which get automatically created. Note that for 300 a complete setup, user credentials for these accounts are required 301 and can be created using the `ensureCredentials` option. 302 This option does not delete accounts which are not (anymore) listed. 303 ''; 304 example = [ 305 "user1@localhost" 306 "user2@localhost" 307 ]; 308 }; 309 310 ensureCredentials = mkOption { 311 default = {}; 312 description = lib.mdDoc '' 313 List of user accounts which get automatically created if they don't 314 exist yet. Note that for a complete setup, corresponding mail boxes 315 have to get created using the `ensureAccounts` option. 316 This option does not delete accounts which are not (anymore) listed. 317 ''; 318 example = { 319 "user1@localhost".passwordFile = /secrets/user1-localhost; 320 "user2@localhost".passwordFile = /secrets/user2-localhost; 321 }; 322 type = types.attrsOf (types.submodule { 323 options = { 324 passwordFile = mkOption { 325 type = types.path; 326 example = "/path/to/file"; 327 default = null; 328 description = lib.mdDoc '' 329 Specifies the path to a file containing the 330 clear text password for the user. 331 ''; 332 }; 333 }; 334 }); 335 }; 336 337 secrets = lib.mkOption { 338 type = lib.types.path; 339 description = lib.mdDoc '' 340 A file containing the various secrets. Should be in the format 341 expected by systemd's `EnvironmentFile` directory. Secrets can be 342 referenced in the format `{env:VAR}`. 343 ''; 344 }; 345 346 }; 347 }; 348 349 config = mkIf cfg.enable { 350 351 assertions = [ 352 { 353 assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != []; 354 message = '' 355 If Maddy is configured to use TLS, tls.certificates with attribute sets 356 of certPath and keyPath must be provided. 357 Read more about obtaining TLS certificates here: 358 https://maddy.email/tutorials/setting-up/#tls-certificates 359 ''; 360 } 361 { 362 assertion = cfg.tls.loader == "acme" -> cfg.tls.extraConfig != ""; 363 message = '' 364 If Maddy is configured to obtain TLS certificates using the ACME 365 loader, extra configuration options must be supplied via 366 tls.extraConfig option. 367 See upstream documentation for more details: 368 https://maddy.email/reference/tls-acme 369 ''; 370 } 371 ]; 372 373 systemd = { 374 375 packages = [ pkgs.maddy ]; 376 services = { 377 maddy = { 378 serviceConfig = { 379 User = cfg.user; 380 Group = cfg.group; 381 StateDirectory = [ "maddy" ]; 382 EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}"; 383 }; 384 restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ]; 385 wantedBy = [ "multi-user.target" ]; 386 }; 387 maddy-ensure-accounts = { 388 script = '' 389 ${optionalString (cfg.ensureAccounts != []) '' 390 ${concatMapStrings (account: '' 391 if ! ${pkgs.maddy}/bin/maddyctl imap-acct list | grep "${account}"; then 392 ${pkgs.maddy}/bin/maddyctl imap-acct create ${account} 393 fi 394 '') cfg.ensureAccounts} 395 ''} 396 ${optionalString (cfg.ensureCredentials != {}) '' 397 ${concatStringsSep "\n" (mapAttrsToList (name: cfg: '' 398 if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then 399 ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${escapeShellArg cfg.passwordFile}) ${name} 400 fi 401 '') cfg.ensureCredentials)} 402 ''} 403 ''; 404 serviceConfig = { 405 Type = "oneshot"; 406 User= "maddy"; 407 }; 408 after = [ "maddy.service" ]; 409 wantedBy = [ "multi-user.target" ]; 410 }; 411 412 }; 413 414 }; 415 416 environment.etc."maddy/maddy.conf" = { 417 text = '' 418 $(hostname) = ${cfg.hostname} 419 $(primary_domain) = ${cfg.primaryDomain} 420 $(local_domains) = ${toString cfg.localDomains} 421 hostname ${cfg.hostname} 422 423 ${if (cfg.tls.loader == "file") then '' 424 tls file ${concatStringsSep " " ( 425 map (x: x.certPath + " " + x.keyPath 426 ) cfg.tls.certificates)} ${optionalString (cfg.tls.extraConfig != "") '' 427 { ${cfg.tls.extraConfig} } 428 ''} 429 '' else if (cfg.tls.loader == "acme") then '' 430 tls { 431 loader acme { 432 ${cfg.tls.extraConfig} 433 } 434 } 435 '' else if (cfg.tls.loader == "off") then '' 436 tls off 437 '' else ""} 438 439 ${cfg.config} 440 ''; 441 }; 442 443 users.users = optionalAttrs (cfg.user == name) { 444 ${name} = { 445 isSystemUser = true; 446 group = cfg.group; 447 description = "Maddy mail transfer agent user"; 448 }; 449 }; 450 451 users.groups = optionalAttrs (cfg.group == name) { 452 ${cfg.group} = { }; 453 }; 454 455 networking.firewall = mkIf cfg.openFirewall { 456 allowedTCPPorts = [ 25 143 587 ]; 457 }; 458 459 environment.systemPackages = [ 460 pkgs.maddy 461 ]; 462 }; 463}