diff --git a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml index 6b706e4aeaa16..e2bda7604e48f 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2111.section.xml @@ -273,6 +273,13 @@ services.peertube. + + + maddy, a + composable all-in-one mail server. Available as + services.maddy. + + sourcehut, a diff --git a/nixos/doc/manual/release-notes/rl-2111.section.md b/nixos/doc/manual/release-notes/rl-2111.section.md index 48adc4ad33cba..2520d176096aa 100644 --- a/nixos/doc/manual/release-notes/rl-2111.section.md +++ b/nixos/doc/manual/release-notes/rl-2111.section.md @@ -74,6 +74,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [PeerTube](https://joinpeertube.org/), developed by Framasoft, is the free and decentralized alternative to video platforms. Available at [services.peertube](options.html#opt-services.peertube.enable). +- [maddy](https://maddy.email), a composable all-in-one mail server. Available as [services.maddy](options.html#opt-services.maddy.enable). + - [sourcehut](https://sr.ht), a collection of tools useful for software development. Available as [services.sourcehut](options.html#opt-services.sourcehut.enable). - [ucarp](https://download.pureftpd.org/pub/ucarp/README), an userspace implementation of the Common Address Redundancy Protocol (CARP). Available as [networking.ucarp](options.html#opt-networking.ucarp.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index f36e7dd67eaee..8cb7c39005c52 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -467,6 +467,7 @@ ./services/mail/dovecot.nix ./services/mail/dspam.nix ./services/mail/exim.nix + ./services/mail/maddy.nix ./services/mail/mail.nix ./services/mail/mailcatcher.nix ./services/mail/mailhog.nix diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix new file mode 100644 index 0000000000000..44cfa3c2908d7 --- /dev/null +++ b/nixos/modules/services/mail/maddy.nix @@ -0,0 +1,247 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + name = "maddy"; + cfg = config.services.maddy; + defaultConfig = '' + tls off + + auth.pass_table local_authdb { + table sql_table { + driver sqlite3 + dsn credentials.db + table_name passwords + } + } + + storage.imapsql local_mailboxes { + driver sqlite3 + dsn imapsql.db + } + + table.chain local_rewrites { + optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" + optional_step static { + entry postmaster postmaster@$(primary_domain) + } + optional_step file /etc/maddy/aliases + } + msgpipeline local_routing { + destination postmaster $(local_domains) { + modify { + replace_rcpt &local_rewrites + } + deliver_to &local_mailboxes + } + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } + + smtp tcp://0.0.0.0:25 { + limits { + all rate 20 1s + all concurrency 10 + } + dmarc yes + check { + require_mx_record + dkim + spf + } + source $(local_domains) { + reject 501 5.1.8 "Use Submission for outgoing SMTP" + } + default_source { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.1.1 "User doesn't exist" + } + } + } + + submission tcp://0.0.0.0:587 { + limits { + all rate 50 1s + } + auth &local_authdb + source $(local_domains) { + check { + authorize_sender { + prepare_email &local_rewrites + user_to_email identity + } + } + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + modify { + dkim $(primary_domain) $(local_domains) default + } + deliver_to &remote_queue + } + } + default_source { + reject 501 5.1.8 "Non-local sender domain" + } + } + + target.remote outbound_delivery { + limits { + destination rate 20 1s + destination concurrency 10 + } + mx_auth { + dane + mtasts { + cache fs + fs_dir mtasts_cache/ + } + local_policy { + min_tls_level encrypted + min_mx_level none + } + } + } + + target.queue remote_queue { + target &outbound_delivery + autogenerated_msg_domain $(primary_domain) + bounce { + destination postmaster $(local_domains) { + deliver_to &local_routing + } + default_destination { + reject 550 5.0.0 "Refusing to send DSNs to non-local addresses" + } + } + } + + imap tcp://0.0.0.0:143 { + auth &local_authdb + storage &local_mailboxes + } + ''; + +in { + options = { + services.maddy = { + enable = mkEnableOption "Maddy, a free an open source mail server"; + + user = mkOption { + default = "maddy"; + type = with types; uniq string; + description = '' + Name of the user under which maddy will run. If not specified, a + default user will be created. + ''; + }; + group = mkOption { + default = "maddy"; + type = with types; uniq string; + description = '' + Name of the group under which maddy will run. If not specified, a + default group will be created. + ''; + }; + + hostname = mkOption { + default = "localhost"; + type = with types; uniq string; + example = ''example.com''; + description = '' + Hostname to use. It should be FQDN. + ''; + }; + primaryDomain = mkOption { + default = "localhost"; + type = with types; uniq string; + example = ''mail.example.com''; + description = '' + Primary MX domain to use. It should be FQDN. + ''; + }; + localDomains = mkOption { + type = with types; listOf str; + default = ["$(primary_domain)"]; + example = [ + "$(primary_domain)" + "example.com" + "other.example.com" + ]; + description = '' + Define list of allowed domains. + ''; + }; + config = mkOption { + type = with types; nullOr lines; + default = defaultConfig; + description = '' + Server configuration. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = '' + Open the configured incoming and outgoing mail server ports. + ''; + }; + + }; + }; + + config = mkIf cfg.enable { + + systemd = { + packages = [ pkgs.maddy ]; + services.maddy = { + serviceConfig = { + User = "${cfg.user}"; + Group = "${cfg.group}"; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; + + environment.etc."maddy/maddy.conf" = { + text = '' + $(hostname) = ${cfg.hostname} + $(primary_domain) = ${cfg.primaryDomain} + $(local_domains) = ${toString cfg.localDomains} + hostname ${cfg.hostname} + ${cfg.config} + ''; + }; + + users.users = optionalAttrs (cfg.user == "maddy") { + maddy = { + description = "Maddy service user"; + group = cfg.group; + home = "/var/lib/maddy"; + createHome = true; + isSystemUser = true; + }; + }; + + users.groups = mkIf (cfg.group == "maddy") { + maddy = pkgs.lib.mkForce { + name = cfg.group; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ 25 143 587 ]; + }; + + environment.systemPackages = [ + pkgs.maddy + ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index bee2935b84fbb..63be67892017b 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -247,6 +247,7 @@ in lxd-image-server = handleTest ./lxd-image-server.nix {}; #logstash = handleTest ./logstash.nix {}; lorri = handleTest ./lorri/default.nix {}; + maddy = handleTest ./maddy.nix {}; magic-wormhole-mailbox-server = handleTest ./magic-wormhole-mailbox-server.nix {}; magnetico = handleTest ./magnetico.nix {}; mailcatcher = handleTest ./mailcatcher.nix {}; diff --git a/nixos/tests/maddy.nix b/nixos/tests/maddy.nix new file mode 100644 index 0000000000000..581748c1fa59b --- /dev/null +++ b/nixos/tests/maddy.nix @@ -0,0 +1,58 @@ +import ./make-test-python.nix ({ pkgs, ... }: { + name = "maddy"; + meta = with pkgs.lib.maintainers; { maintainers = [ onny ]; }; + + nodes = { + server = { ... }: { + services.maddy = { + enable = true; + hostname = "server"; + primaryDomain = "server"; + openFirewall = true; + }; + }; + + client = { ... }: { + environment.systemPackages = [ + (pkgs.writers.writePython3Bin "send-testmail" { } '' + import smtplib + from email.mime.text import MIMEText + + msg = MIMEText("Hello World") + msg['Subject'] = 'Test' + msg['From'] = "postmaster@server" + msg['To'] = "postmaster@server" + with smtplib.SMTP('server', 587) as smtp: + smtp.login('postmaster@server', 'test') + smtp.sendmail('postmaster@server', 'postmaster@server', msg.as_string()) + '') + (pkgs.writers.writePython3Bin "test-imap" { } '' + import imaplib + + with imaplib.IMAP4('server') as imap: + imap.login('postmaster@server', 'test') + imap.select() + status, refs = imap.search(None, 'ALL') + assert status == 'OK' + assert len(refs) == 1 + status, msg = imap.fetch(refs[0], 'BODY[TEXT]') + assert status == 'OK' + assert msg[0][1].strip() == b"Hello World" + '') + ]; + }; + }; + + testScript = '' + start_all() + server.wait_for_unit("maddy.service") + server.wait_for_open_port(143) + server.wait_for_open_port(587) + + server.succeed("echo test | maddyctl creds create postmaster@server") + server.succeed("maddyctl imap-acct create postmaster@server") + + client.succeed("send-testmail") + client.succeed("test-imap") + ''; +}) diff --git a/pkgs/servers/maddy/default.nix b/pkgs/servers/maddy/default.nix index 3381b7858beb8..b4ddb80c16532 100644 --- a/pkgs/servers/maddy/default.nix +++ b/pkgs/servers/maddy/default.nix @@ -1,4 +1,4 @@ -{ lib, buildGoModule, fetchFromGitHub, coreutils, installShellFiles, scdoc }: +{ lib, buildGoModule, fetchFromGitHub, coreutils, installShellFiles, scdoc, nixosTests }: buildGoModule rec { pname = "maddy"; @@ -37,6 +37,8 @@ buildGoModule rec { --replace "/bin/kill" "${coreutils}/bin/kill" ''; + passthru.tests.nixos = nixosTests.maddy; + meta = with lib; { description = "Composable all-in-one mail server"; homepage = "https://maddy.email";