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