This repository has no description
6.7 kB
211 lines
1{
2 self,
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8let
9 relayHosts = map (host: host.nets.retiolum.ip6.addr) [
10 config.krebs.hosts.aergia
11 config.krebs.hosts.ignavia
12 config.krebs.hosts.coaxmetal
13 config.krebs.hosts.green
14 config.krebs.hosts.mors
15 ];
16
17 mynetworks = map (ip: "[${ip}]") relayHosts;
18in
19{
20 imports = [
21 self.inputs.nixos-mailserver.nixosModules.default
22 ];
23
24 mailserver = {
25 enable = true;
26 fqdn = "mail.lassul.us";
27 domains = [ "lassul.us" ];
28
29 loginAccounts = {
30 "lass@lassul.us" = {
31 hashedPasswordFile =
32 config.clan.core.vars.generators.mailserver-lass.files."lass-mail-password-hash".path;
33 aliases = [
34 "postmaster@lassul.us"
35 "root@lassul.us"
36 "abuse@lassul.us"
37 "hostmaster@lassul.us"
38 "webmaster@lassul.us"
39 "security@lassul.us"
40 "noc@lassul.us"
41 "mailer-daemon@lassul.us"
42 "nobody@lassul.us"
43 "lassulus@lassul.us"
44 ];
45 catchAll = [ "lassul.us" ];
46 sieveScript = ''
47 require ["fileinto", "mailbox"];
48
49 # GitHub — sort by repo from List-Id header
50 if header :matches "List-Id" "*.github.com" {
51 if header :contains "List-Id" "nixpkgs" { fileinto :create "GitHub.nixpkgs"; }
52 elsif header :contains "List-Id" "disko" { fileinto :create "GitHub.disko"; }
53 elsif header :contains "List-Id" "clan-core" { fileinto :create "GitHub.clan-core"; }
54 elsif header :contains "List-Id" "data-mesher" { fileinto :create "GitHub.data-mesher"; }
55 elsif header :contains "List-Id" "foundation" { fileinto :create "GitHub.foundation"; }
56 elsif header :contains "List-Id" "nix." { fileinto :create "GitHub.nix"; }
57 elsif header :contains "List-Id" "nixos-wiki" { fileinto :create "GitHub.nixos-wiki"; }
58 elsif header :contains "List-Id" "nixos-homepage" { fileinto :create "GitHub.nixos-homepage"; }
59 else { fileinto :create "GitHub.other"; }
60 stop;
61 }
62
63 # Gitea (clan)
64 if anyof (
65 header :matches "X-Gitea-*" "*",
66 address :domain "From" "clan.lol"
67 ) {
68 fileinto :create "GitHub.clan";
69 stop;
70 }
71
72 # Mailing lists — sort by List-Id
73 if exists "List-Id" {
74 if header :contains "List-Id" "vorstand.c-base" { fileinto :create "Lists.c-base-vorstand"; }
75 elsif header :contains "List-Id" "c-base" { fileinto :create "Lists.c-base"; }
76 elsif header :contains "List-Id" "afra" { fileinto :create "Lists.afra"; }
77 elsif header :contains "List-Id" "soundlab" { fileinto :create "Lists.soundlab"; }
78 elsif header :contains "List-Id" "nixos" { fileinto :create "Lists.nixos"; }
79 elsif header :contains "List-Id" "shack" { fileinto :create "Lists.shack"; }
80 elsif header :contains "List-Id" "dezentrale" { fileinto :create "Lists.dezentrale"; }
81 elsif header :contains "List-Id" "dn42" { fileinto :create "Lists.dn42"; }
82 elsif header :contains "List-Id" "tinc" { fileinto :create "Lists.tinc"; }
83 elsif header :contains "List-Id" "wireguard" { fileinto :create "Lists.wireguard"; }
84 elsif header :contains "List-Id" "retiolum" { fileinto :create "Lists.retiolum"; }
85 else { fileinto :create "Lists.other"; }
86 stop;
87 }
88
89 # Billing/receipts
90 if anyof (
91 address :domain "From" "paypal.de",
92 address :domain "From" "paypal.com",
93 address :domain "From" "steuerberaten.de",
94 address :domain "From" "patreon.com",
95 address :domain "From" "dhl.de",
96 address :domain "From" "eloop.app",
97 header :contains "Subject" "Rechnung",
98 header :contains "Subject" "Invoice",
99 header :contains "Subject" "receipt"
100 ) {
101 fileinto :create "Billing";
102 stop;
103 }
104
105 # Newsletters (has unsubscribe but no List-Id)
106 if allof (
107 header :contains "List-Unsubscribe" "http",
108 not exists "List-Id"
109 ) {
110 fileinto :create "Newsletters";
111 stop;
112 }
113
114 # Everything else -> INBOX
115 keep;
116 '';
117 };
118 "bot@lassul.us" = {
119 hashedPasswordFile =
120 config.clan.core.vars.generators.mailserver-bot.files."bot-mail-password-hash".path;
121 };
122 };
123
124 # TLS via existing ACME cert
125 x509.useACMEHost = "mail.lassul.us";
126
127 # DKIM — auto-managed by module
128 dkimSigning = true;
129 dkimSelector = "mail";
130
131 # Full-text search for IMAP clients
132 fullTextSearch = {
133 enable = true;
134 memoryLimit = 500;
135 };
136
137 stateVersion = 3;
138 };
139
140 # Password generation for mail accounts
141 clan.core.vars.generators.mailserver-lass = {
142 files."lass-mail-password" = { };
143 files."lass-mail-password-hash" = { };
144 runtimeInputs = with pkgs; [
145 coreutils
146 mkpasswd
147 pwgen
148 ];
149 script = ''
150 pwgen -s 32 1 > "$out/lass-mail-password"
151 mkpasswd -sm bcrypt < "$out/lass-mail-password" > "$out/lass-mail-password-hash"
152 '';
153 };
154
155 clan.core.vars.generators.mailserver-bot = {
156 files."bot-mail-password" = { };
157 files."bot-mail-password-hash" = { };
158 runtimeInputs = with pkgs; [
159 coreutils
160 mkpasswd
161 pwgen
162 ];
163 script = ''
164 pwgen -s 32 1 > "$out/bot-mail-password"
165 mkpasswd -sm bcrypt < "$out/bot-mail-password" > "$out/bot-mail-password-hash"
166 '';
167 };
168
169 # Retiolum relay: allow krebs hosts to relay through postfix
170 services.postfix.settings.main.mynetworks = lib.mkForce (
171 [
172 "127.0.0.0/8"
173 "[::1]/128"
174 ]
175 ++ mynetworks
176 );
177
178 # ACME cert — reuse existing setup
179 security.acme.certs."mail.lassul.us" = {
180 group = "lasscert";
181 webroot = "/var/lib/acme/acme-challenge";
182 };
183 users.groups.lasscert.members = [
184 "dovecot2"
185 "postfix"
186 "nginx"
187 ];
188
189 # Firewall rules (krebs.iptables pattern, INPUT policy is DROP)
190 krebs.iptables.tables.filter.INPUT.rules = [
191 {
192 predicate = "-p tcp --dport smtp";
193 target = "ACCEPT";
194 }
195 {
196 predicate = "-p tcp --dport submissions";
197 target = "ACCEPT";
198 }
199 {
200 predicate = "-p tcp --dport submission";
201 target = "ACCEPT";
202 }
203 {
204 predicate = "-p tcp --dport imaps";
205 target = "ACCEPT";
206 }
207 ];
208
209 # msmtp for local submission via SSH
210 environment.systemPackages = [ pkgs.msmtp ];
211}