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 tls off
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 msgpipeline local_routing {
39 destination postmaster $(local_domains) {
40 modify {
41 replace_rcpt &local_rewrites
42 }
43 deliver_to &local_mailboxes
44 }
45 default_destination {
46 reject 550 5.1.1 "User doesn't exist"
47 }
48 }
49
50 smtp tcp://0.0.0.0:25 {
51 limits {
52 all rate 20 1s
53 all concurrency 10
54 }
55 dmarc yes
56 check {
57 require_mx_record
58 dkim
59 spf
60 }
61 source $(local_domains) {
62 reject 501 5.1.8 "Use Submission for outgoing SMTP"
63 }
64 default_source {
65 destination postmaster $(local_domains) {
66 deliver_to &local_routing
67 }
68 default_destination {
69 reject 550 5.1.1 "User doesn't exist"
70 }
71 }
72 }
73
74 submission tcp://0.0.0.0:587 {
75 limits {
76 all rate 50 1s
77 }
78 auth &local_authdb
79 source $(local_domains) {
80 check {
81 authorize_sender {
82 prepare_email &local_rewrites
83 user_to_email identity
84 }
85 }
86 destination postmaster $(local_domains) {
87 deliver_to &local_routing
88 }
89 default_destination {
90 modify {
91 dkim $(primary_domain) $(local_domains) default
92 }
93 deliver_to &remote_queue
94 }
95 }
96 default_source {
97 reject 501 5.1.8 "Non-local sender domain"
98 }
99 }
100
101 target.remote outbound_delivery {
102 limits {
103 destination rate 20 1s
104 destination concurrency 10
105 }
106 mx_auth {
107 dane
108 mtasts {
109 cache fs
110 fs_dir mtasts_cache/
111 }
112 local_policy {
113 min_tls_level encrypted
114 min_mx_level none
115 }
116 }
117 }
118
119 target.queue remote_queue {
120 target &outbound_delivery
121 autogenerated_msg_domain $(primary_domain)
122 bounce {
123 destination postmaster $(local_domains) {
124 deliver_to &local_routing
125 }
126 default_destination {
127 reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
128 }
129 }
130 }
131
132 imap tcp://0.0.0.0:143 {
133 auth &local_authdb
134 storage &local_mailboxes
135 }
136 '';
137
138in {
139 options = {
140 services.maddy = {
141
142 enable = mkEnableOption (lib.mdDoc "Maddy, a free an open source mail server");
143
144 user = mkOption {
145 default = "maddy";
146 type = with types; uniq string;
147 description = lib.mdDoc ''
148 User account under which maddy runs.
149
150 ::: {.note}
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.
154 :::
155 '';
156 };
157
158 group = mkOption {
159 default = "maddy";
160 type = with types; uniq string;
161 description = lib.mdDoc ''
162 Group account under which maddy runs.
163
164 ::: {.note}
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.
168 :::
169 '';
170 };
171
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.
178 '';
179 };
180
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.
187 '';
188 };
189
190 localDomains = mkOption {
191 type = with types; listOf str;
192 default = ["$(primary_domain)"];
193 example = [
194 "$(primary_domain)"
195 "example.com"
196 "other.example.com"
197 ];
198 description = lib.mdDoc ''
199 Define list of allowed domains.
200 '';
201 };
202
203 config = mkOption {
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.
211
212 ::: {.note}
213 This should not be used in a production environment.
214 :::
215 '';
216 };
217
218 openFirewall = mkOption {
219 type = types.bool;
220 default = false;
221 description = lib.mdDoc ''
222 Open the configured incoming and outgoing mail server ports.
223 '';
224 };
225
226 };
227 };
228
229 config = mkIf cfg.enable {
230
231 systemd = {
232 packages = [ pkgs.maddy ];
233 services.maddy = {
234 serviceConfig = {
235 User = cfg.user;
236 Group = cfg.group;
237 StateDirectory = [ "maddy" ];
238 };
239 restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
240 wantedBy = [ "multi-user.target" ];
241 };
242 };
243
244 environment.etc."maddy/maddy.conf" = {
245 text = ''
246 $(hostname) = ${cfg.hostname}
247 $(primary_domain) = ${cfg.primaryDomain}
248 $(local_domains) = ${toString cfg.localDomains}
249 hostname ${cfg.hostname}
250 ${cfg.config}
251 '';
252 };
253
254 users.users = optionalAttrs (cfg.user == name) {
255 ${name} = {
256 isSystemUser = true;
257 group = cfg.group;
258 description = "Maddy mail transfer agent user";
259 };
260 };
261
262 users.groups = optionalAttrs (cfg.group == name) {
263 ${cfg.group} = { };
264 };
265
266 networking.firewall = mkIf cfg.openFirewall {
267 allowedTCPPorts = [ 25 143 587 ];
268 };
269
270 environment.systemPackages = [
271 pkgs.maddy
272 ];
273 };
274}