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";