1 { config, lib, pkgs, ... }:
9 cfg = config.services.maddy;
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!
18 auth.pass_table local_authdb {
26 storage.imapsql local_mailboxes {
31 table.chain local_rewrites {
32 optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
33 optional_step static {
34 entry postmaster postmaster@$(primary_domain)
36 optional_step file /etc/maddy/aliases
38 msgpipeline local_routing {
39 destination postmaster $(local_domains) {
41 replace_rcpt &local_rewrites
43 deliver_to &local_mailboxes
46 reject 550 5.1.1 "User doesn't exist"
50 smtp tcp://0.0.0.0:25 {
61 source $(local_domains) {
62 reject 501 5.1.8 "Use Submission for outgoing SMTP"
65 destination postmaster $(local_domains) {
66 deliver_to &local_routing
69 reject 550 5.1.1 "User doesn't exist"
74 submission tcp://0.0.0.0:587 {
79 source $(local_domains) {
82 prepare_email &local_rewrites
83 user_to_email identity
86 destination postmaster $(local_domains) {
87 deliver_to &local_routing
91 dkim $(primary_domain) $(local_domains) default
93 deliver_to &remote_queue
97 reject 501 5.1.8 "Non-local sender domain"
101 target.remote outbound_delivery {
103 destination rate 20 1s
104 destination concurrency 10
113 min_tls_level encrypted
119 target.queue remote_queue {
120 target &outbound_delivery
121 autogenerated_msg_domain $(primary_domain)
123 destination postmaster $(local_domains) {
124 deliver_to &local_routing
126 default_destination {
127 reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
132 imap tcp://0.0.0.0:143 {
134 storage &local_mailboxes
142 enable = mkEnableOption (lib.mdDoc "Maddy, a free an open source mail server");
146 type = with types; uniq string;
147 description = lib.mdDoc ''
148 User account under which maddy runs.
151 If left as the default value this user will automatically be created
152 on system activation, otherwise the sysadmin is responsible for
153 ensuring the user exists before the maddy service starts.
160 type = with types; uniq string;
161 description = lib.mdDoc ''
162 Group account under which maddy runs.
165 If left as the default value this group will automatically be created
166 on system activation, otherwise the sysadmin is responsible for
167 ensuring the group exists before the maddy service starts.
172 hostname = mkOption {
173 default = "localhost";
174 type = with types; uniq string;
175 example = ''example.com'';
176 description = lib.mdDoc ''
177 Hostname to use. It should be FQDN.
181 primaryDomain = mkOption {
182 default = "localhost";
183 type = with types; uniq string;
184 example = ''mail.example.com'';
185 description = lib.mdDoc ''
186 Primary MX domain to use. It should be FQDN.
190 localDomains = mkOption {
191 type = with types; listOf str;
192 default = ["$(primary_domain)"];
198 description = lib.mdDoc ''
199 Define list of allowed domains.
204 type = with types; nullOr lines;
205 default = defaultConfig;
206 description = lib.mdDoc ''
207 Server configuration, see
208 [https://maddy.email](https://maddy.email) for
209 more information. The default configuration of this module will setup
210 minimal maddy instance for mail transfer without TLS encryption.
213 This should not be used in a production environment.
218 openFirewall = mkOption {
221 description = lib.mdDoc ''
222 Open the configured incoming and outgoing mail server ports.
229 config = mkIf cfg.enable {
232 packages = [ pkgs.maddy ];
237 StateDirectory = [ "maddy" ];
239 restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
240 wantedBy = [ "multi-user.target" ];
244 environment.etc."maddy/maddy.conf" = {
246 $(hostname) = ${cfg.hostname}
247 $(primary_domain) = ${cfg.primaryDomain}
248 $(local_domains) = ${toString cfg.localDomains}
249 hostname ${cfg.hostname}
254 users.users = optionalAttrs (cfg.user == name) {
258 description = "Maddy mail transfer agent user";
262 users.groups = optionalAttrs (cfg.group == name) {
266 networking.firewall = mkIf cfg.openFirewall {
267 allowedTCPPorts = [ 25 143 587 ];
270 environment.systemPackages = [