290 Commits

Author SHA1 Message Date
Jakub Skokan
f6aa7e2b18 Allow TLSv1 for compatibility with older devices 2026-01-01 20:10:21 +01:00
Martin Weinelt
23f0a53ca6 dovecot: support X25519MLKEM768 hybrid kex
(cherry picked from commit 14156235863358631c3ff0d2917513b9c9280eba)
2025-12-24 01:57:43 +01:00
Martin Weinelt
a14fe3b293 migrations: strongly indicate dry runs
(cherry picked from commit 0696fcbe9b5cd66be6f6b3bb91d1e8e95fe6c21d)
2025-11-26 20:25:22 +01:00
Martin Weinelt
c5bd875089 docs: don't recommend sudo to run the migration script
The migration script tries switching EUID by itself and will error out
with a recommendation to try sudo if it cannot.

(cherry picked from commit a38e14460f7b35fb7ef3f0e4ced87d89754c87f9)
2025-11-26 20:25:18 +01:00
Martin Weinelt
507d5dcef9 docs: recommend wcurl to grab the migration script
(cherry picked from commit 039389ee0449d642d3f3bcccac4edc56f0e0a6df)
2025-11-26 20:03:17 +01:00
Martin Weinelt
faeb1b04d8 Switch nixpkgs to nixos-25.11-small 2025-11-25 13:59:14 +01:00
Martin Weinelt
8d35f004ee Release 25.11 2025-11-25 13:56:52 +01:00
Martin Weinelt
4987d275a9 Merge branch 'flake-update' into 'master'
flake.lock: Update

See merge request simple-nixos-mailserver/nixos-mailserver!468
2025-11-19 15:06:18 +00:00
Martin Weinelt
a35a181671 flake.lock: Update
Flake lock file updates:

• Updated input 'git-hooks':
    'github:cachix/git-hooks.nix/8e7576e79b88c16d7ee3bbd112c8d90070832885' (2025-11-06)
  → 'github:cachix/git-hooks.nix/7275fa67fbbb75891c16d9dee7d88e58aea2d761' (2025-11-16)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/e5d07586ec39f74b390308f2e00040c23bdef530' (2025-11-09)
  → 'github:NixOS/nixpkgs/094318ea16502a7a81ce90dd3638697020f030a2' (2025-11-19)
2025-11-19 15:52:23 +01:00
Martin Weinelt
cbdf90f639 rspamd: fix DKIM signing for subdomains
With the eSLD normalization feature in rspamd subdomains actually use the
DKIM key for their parent domain, which simplifies the setup if you serve
multiple subdomains.

We however currently create DKIM key pairs for every given domain
name, no matter if it is a second-level domain or subdomain for one, so
disabling eSLD normalization aligns with the current intent behind our
configuration.

In the future it would be nice if we could reuse the parent domain DKIM
key for all its subdomains, but that requires some thought on how to
achieve that normalization in nixos-mailserver first.

Reapplies 1a3a618a30 to the correct
configuration file.
2025-11-16 19:29:16 +01:00
Martin Weinelt
b88e6182f0 Revert "rspamd: fix DKIM signing for subdomains"
This reverts commit 1a3a618a30.

This went into the wrong configuration file unfortunately
2025-11-16 19:26:22 +01:00
Martin Weinelt
b946f74261 mail-server/common: fix eval
CI has a shitty failure mode where jobs that don't eval get removed and
hydra-cli will still exit cleanly.
2025-11-16 18:41:47 +01:00
Martin Weinelt
345cbc11df Merge branch 'remove-dovecot-service-name-workaround' into 'master'
Remove dovecot service name compat code

See merge request simple-nixos-mailserver/nixos-mailserver!467
2025-11-16 17:29:57 +00:00
Martin Weinelt
1cb4295b74 Remove dovecot service name compat code 2025-11-16 18:18:22 +01:00
Martin Weinelt
db66559815 Merge branch 'srs' into 'master'
Add support for sender rewriting for forwards using postsrsd

See merge request simple-nixos-mailserver/nixos-mailserver!431
2025-11-16 14:00:07 +00:00
Martin Weinelt
17c6816f67 Merge branch 'rspamd-dmarc-no-esld' into 'master'
rspamd: fix DKIM signing for subdomains

See merge request simple-nixos-mailserver/nixos-mailserver!465
2025-11-16 13:57:30 +00:00
Martin Weinelt
1a3a618a30 rspamd: fix DKIM signing for subdomains
With the eSLD normalization feature in rspamd subdomains actually use the
DKIM key for their parent domain, which simplifies the setup if you serve
multiple subdomains.

We however currently create DKIM key pairs for every given domain
name, no matter if it is a second-level domain or subdomain for one, so
disabling eSLD normalization aligns with the current intent behind our
configuration.

In the future it would be nice if we could reuse the parent domain DKIM
key for all its subdomains, but that requires some thought on how to
achieve that normalization in nixos-mailserver first.
2025-11-16 14:55:41 +01:00
Martin Weinelt
61cff94a28 scripts/generate-options: prefer defaultText over default 2025-11-11 13:45:03 +01:00
Martin Weinelt
eeda8ba39e Add support for sender rewriting using postsrsd
With SRS we support forwarding of mails without (fully) breaking SPF
alignment.
2025-11-11 13:45:03 +01:00
Martin Weinelt
b633223a33 Merge branch 'postfix-warnings' into 'master'
postfix: resolve main/master option deprecation

See merge request simple-nixos-mailserver/nixos-mailserver!464
2025-11-10 02:03:19 +00:00
Martin Weinelt
edb7b661e4 postfix: resolve main/master option deprecation 2025-11-10 02:56:51 +01:00
Martin Weinelt
b99f353ab8 postfix: unquote tls_config_file value
This can now be a path type due to changes applied to nixos unstable.
2025-11-10 02:51:46 +01:00
Martin Weinelt
5965fae920 Merge branch 'pq-support' into 'master'
postfix: enable X5519MLKEM768 key exchange

See merge request simple-nixos-mailserver/nixos-mailserver!463
2025-11-10 00:01:28 +00:00
Martin Weinelt
a1532a552f postfix: enable X25519MLKEM768 key exchange
This migrates the key exchange curve group configuration into the OpenSSL
configuration format, which is the only path forward to configure these.

We now prefer a hybrid key exchange for TLS handshake and as a client
we'll send key shares for that and pure X25519, while keeping backwards-
compat for P256 and P384.

The statistics for my personal mail server over the last month show a
clear trend for X25519 key exchanges:

    156 secp384r1
    225 secp256r1
    19541 x25519
2025-11-10 00:31:43 +01:00
Martin Weinelt
e3ee0fcceb flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/8ea611305a7db12c49446f9c40c609614419ec4b' (2025-11-08)
  → 'github:NixOS/nixpkgs/e5d07586ec39f74b390308f2e00040c23bdef530' (2025-11-09)
2025-11-10 00:31:42 +01:00
Martin Weinelt
44dd1778a0 Merge branch 'tlsrpt' into 'master'
MTA-STS lookups, SMTP TLS  reports

See merge request simple-nixos-mailserver/nixos-mailserver!430
2025-11-08 22:39:42 +00:00
Martin Weinelt
3555a546ab Add support for SMTP TLS reports
When enabled the tlsrpt services will send out aggregated reports about
TLS connections the local Postfix made to interested parties, who set up
a `_smtp._tls` TXT record with a rua attribute.

Introduces mailserver.systemContact to specify an administrative contact
advertised in these automated reports.
2025-11-08 22:39:29 +01:00
Martin Weinelt
bd56d97299 Merge branch 'update-flake-lock' into 'master'
flake.lock: Update

See merge request simple-nixos-mailserver/nixos-mailserver!462
2025-11-08 17:04:04 +00:00
Martin Weinelt
6f17c29eb8 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/ae814fd3904b621d8ab97418f1d0f2eb0d3716f4' (2025-11-05)
  → 'github:NixOS/nixpkgs/8ea611305a7db12c49446f9c40c609614419ec4b' (2025-11-08)
2025-11-08 17:57:18 +01:00
Martin Weinelt
1cedddf425 flake.lock: Update
Flake lock file updates:

• Updated input 'git-hooks':
    'github:cachix/git-hooks.nix/ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37' (2025-10-17)
  → 'github:cachix/git-hooks.nix/8e7576e79b88c16d7ee3bbd112c8d90070832885' (2025-11-06)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/b3d51a0365f6695e7dd5cdf3e180604530ed33b4' (2025-11-02)
  → 'github:NixOS/nixpkgs/ae814fd3904b621d8ab97418f1d0f2eb0d3716f4' (2025-11-05)
2025-11-08 17:55:51 +01:00
Martin Weinelt
0812ca1e48 Use postfix-tlspol for DANE/MTA-STS policy lookups
Postfix with plain DANE only secures domains that configure DNSSEC and
publish TLSA records. With postfix-tlspol we support MTA-STS protected
connections and get caching for its policy results.

Finally, we use this as a stepping stone to build TLSRPT support on top.
2025-11-08 15:49:34 +01:00
Martin Weinelt
ed771e37f7 Merge branch 'release-check' into 'master'
Check release version compat, stop testing stable NixOS

See merge request simple-nixos-mailserver/nixos-mailserver!440
2025-11-08 12:57:49 +00:00
Martin Weinelt
619e35dce2 Stop testing stable nixos
We only test and support matching nixpkgs versions to simpliy alignment
with breaking changes on nixos unstable.
2025-11-08 13:40:56 +01:00
Martin Weinelt
6dbbac29f9 Check release version compat
To move into a better position to align this project with nixpkgs
unstable breaking changes we now default to require a matching nixpkgs
release.
2025-11-08 13:39:33 +01:00
Martin Weinelt
cc54c4fa85 Merge branch 'disable-submission' into 'master'
Disable submission with explicit STARTTLS by default

See merge request simple-nixos-mailserver/nixos-mailserver!461
2025-11-08 11:56:16 +00:00
Martin Weinelt
1337e2eece Disable submission with explicit STARTTLS by default
Deprecated, but not yet scheduled for removal pending user feedback.
2025-11-08 12:50:50 +01:00
Martin Weinelt
58659fbdfd Merge branch 'hotfix-docs-build' into 'master'
docs: fix Read the Docs by using portable-nix

See merge request simple-nixos-mailserver/nixos-mailserver!460
2025-11-05 00:33:50 +00:00
emilylange
9f7291ce68 docs: fix Read the Docs by using portable-nix
As of recently, Nix 2.6 from Ubuntu 22.04 became too old to evaluate
nixpkgs. A new-enough version of Nix is available as part of Ubuntu
24.04, but those newer versions of Nix aren't happy with our rather
primitive proot workaround anymore.

Thankfully, someone already made a version of Nix that does all the
heavy lifting for running in unprivileged environments like the one
Read the Docs provides. So we used that instead.
2025-11-05 01:10:52 +01:00
Martin Weinelt
82c2225914 Merge branch 'flake-update' into 'master'
flake.lock: Update

See merge request simple-nixos-mailserver/nixos-mailserver!459
2025-11-04 00:21:16 +00:00
Martin Weinelt
85f0a94466 flake.nix: update sphinx-rtd-theme package attribute
'sphinx_rtd_theme' has been renamed to/replaced by 'sphinx-rtd-theme'
2025-11-04 00:51:49 +01:00
Martin Weinelt
70256c7d6e flake.lock: Update
Flake lock file updates:

• Updated input 'flake-compat':
    'github:edolstra/flake-compat/9100a0f413b0c601e0533d1d94ffd501ce2e7885' (2025-05-12)
  → 'github:edolstra/flake-compat/f387cd2afec9419c8ee37694406ca490c3f34ee5' (2025-10-27)
• Updated input 'git-hooks':
    'github:cachix/git-hooks.nix/54df955a695a84cd47d4a43e08e1feaf90b1fd9b' (2025-09-17)
  → 'github:cachix/git-hooks.nix/ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37' (2025-10-17)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/e9f00bd893984bc8ce46c895c3bf7cac95331127' (2025-09-28)
  → 'github:NixOS/nixpkgs/b3d51a0365f6695e7dd5cdf3e180604530ed33b4' (2025-11-02)
• Updated input 'nixpkgs-25_05':
    'github:NixOS/nixpkgs/5ed4e25ab58fd4c028b59d5611e14ea64de51d23' (2025-09-29)
  → 'github:NixOS/nixpkgs/3de8f8d73e35724bf9abef41f1bdbedda1e14a31' (2025-11-01)
2025-11-04 00:48:17 +01:00
Martin Weinelt
6005d88bed Merge branch 'fix-acme-extraDomain' into 'master'
Only set acme.extraDomainNames when the certificate scheme is acme

See merge request simple-nixos-mailserver/nixos-mailserver!450
2025-10-03 11:08:18 +00:00
Antoine Eiche
9b57654b31 Only set acme.extraDomainNames when the certificate scheme is acme
Otherwise, certificate domains appear twice in the certificate, since
they are added by the acme module and the nginx module.
2025-10-02 09:36:14 +02:00
lewo
4a05bb1911 Merge branch 'update-flake-lock' into 'master'
flake.lock: Update

See merge request simple-nixos-mailserver/nixos-mailserver!449
2025-10-01 17:55:49 +00:00
Martin Weinelt
1e80fb2594 flake.lock: Update
Flake lock file updates:

• Updated input 'git-hooks':
    'github:cachix/git-hooks.nix/16ec914f6fb6f599ce988427d9d94efddf25fe6d' (2025-06-24)
  → 'github:cachix/git-hooks.nix/54df955a695a84cd47d4a43e08e1feaf90b1fd9b' (2025-09-17)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/94def634a20494ee057c76998843c015909d6311' (2025-07-31)
  → 'github:NixOS/nixpkgs/e9f00bd893984bc8ce46c895c3bf7cac95331127' (2025-09-28)
• Updated input 'nixpkgs-25_05':
    'github:NixOS/nixpkgs/1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a' (2025-07-29)
  → 'github:NixOS/nixpkgs/5ed4e25ab58fd4c028b59d5611e14ea64de51d23' (2025-09-29)
2025-10-01 12:18:02 +02:00
lewo
0ab40d0575 Merge branch 'fix/acme-extra-domains' into 'master'
fix(acme): request certificates for the extra domains too

See merge request simple-nixos-mailserver/nixos-mailserver!448
2025-09-30 05:33:00 +00:00
Giel van Schijndel
bf2b313365 fix(acme): request certificates for the extra domains too
Instead of just making it _possible_ to perform the name validation...
2025-09-28 19:03:32 +02:00
Martin Weinelt
d2534fa431 Merge branch 'fix-jobset-generation' into 'master'
ci: disable command execution in jobset generation

See merge request simple-nixos-mailserver/nixos-mailserver!447
2025-09-22 13:27:17 +00:00
Martin Weinelt
39ead49eb4 ci: disable command execution in jobset generation
When GitLab PR descriptions contain markdown inline code blocks they get
interpreted as command substitutions in bash.

This is because the here-doc string previously allowed for this behavior.
2025-09-22 15:23:26 +02:00
Martin Weinelt
c709476ac5 Merge branch 'disable-plain-access' into 'master'
Disable plaintext access per RFC 8314

See merge request simple-nixos-mailserver/nixos-mailserver!446
2025-09-22 13:19:49 +00:00
Martin Weinelt
54f37811dd Disable plaintext access per RFC 8314
This deprecates the `enableImap` and `enablePop` options and opens them
up for future removal.
2025-09-22 03:46:43 +02:00
Martin Weinelt
b49ae46f22 Merge branch 'rspamd-local-networks' into 'master'
rspamd: restrict addresses we disable checks for to localhost

Closes #326

See merge request simple-nixos-mailserver/nixos-mailserver!444
2025-08-25 13:55:52 +00:00
Martin Weinelt
1a2d7a4bf5 rspamd: restrict addresses we disable checks for to localhost
By default this includes private network subnets, but those should really
use authentication instead, if they want to skip checks.

Closes: #326
2025-08-25 04:12:30 +02:00
Martin Weinelt
cc5f180427 Merge branch 'test-enableSubmissionSsl' into 'master'
tests: also test client submission over `smtps://` instead of just `smtp://` with STARTTLS

See merge request simple-nixos-mailserver/nixos-mailserver!443
2025-08-24 00:41:08 +00:00
emilylange
63b8e1615f tests: also test client submission over smtps://
instead of just smtp:// with STARTTLS.

Opted to call the flag --ssl and not --tls to keep it consistent with
the module option (mailserver.enableSubmissionSsl), dovecot internals
and smtplib in mail-check.py.
2025-08-24 02:29:30 +02:00
Martin Weinelt
958c112fba Merge branch 'dkim-rsa2048' into 'master'
Increase default DKIM key bits to 2048

Closes #333

See merge request simple-nixos-mailserver/nixos-mailserver!442
2025-08-22 20:42:21 +00:00
Martin Weinelt
2204f55329 Increase default DKIM key bits to 2048
This is the current recommendation in RFC 8301 from early 2018.

Fixes: #333
2025-08-22 22:38:31 +02:00
Martin Weinelt
2be40a9653 Merge branch 'docs-fix-dovecot-links' into 'master'
docs/dovecot: fix dovecot URLs (again)

See merge request simple-nixos-mailserver/nixos-mailserver!441
2025-08-22 20:34:21 +00:00
emilylange
b7d2f287f3 docs/dovecot: fix dovecot URLs (again)
https://doc.dovecot.org/configuration_manual moved to
https://doc.dovecot.org/2.3/configuration_manual to make room for
https://doc.dovecot.org/:version/ where :version can be any one of 2.3,
2.4.0, 2.4.1 or main.

Unfortunately, there is no redirect for the 2.3 manual pages, rendering
a few of those dovecot links dead. I figured we want to keep the old
docs at /2.3/ for now until we eventually migrate to 2.4, as there are
some differences in the ldap interface between those versions.

Previously: 90539a1a99
2025-08-22 22:06:29 +02:00
Martin Weinelt
57d9624c71 Merge branch 'dmarc-reporter' into 'master'
Allow AF_UNIX sockets for dmarc reporter, tokenize commandline

Closes #331

See merge request simple-nixos-mailserver/nixos-mailserver!437
2025-08-07 22:31:50 +00:00
Martin Weinelt
fc955088e3 Respect configureLocally flag for redis 2025-08-08 00:01:45 +02:00
Martin Weinelt
43f87f5520 Tokenize dmarc reporter commandline 2025-08-08 00:01:45 +02:00
Martin Weinelt
aa06b2f489 Allow AF_UNIX sockets for dmarc reporter and allow group access
This is required to use redis over UNIX domain sockets.
2025-08-08 00:01:45 +02:00
Martin Weinelt
eb656cd361 Merge branch 'flake-bump' into 'master'
postfix: don't cast message_size_limit to string

See merge request simple-nixos-mailserver/nixos-mailserver!435
2025-08-02 00:27:02 +00:00
Martin Weinelt
b76a547bec treewide: reformat with nixfmt 1.0.0 2025-08-02 02:19:15 +02:00
Martin Weinelt
cea6f25a40 flake.lock: Update
Flake lock file updates:

• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/1fd8bada0b6117e6c7eb54aad5813023eed37ccb' (2025-07-06)
  → 'github:NixOS/nixpkgs/94def634a20494ee057c76998843c015909d6311' (2025-07-31)
• Updated input 'nixpkgs-25_05':
    'github:NixOS/nixpkgs/29e290002bfff26af1db6f64d070698019460302' (2025-07-05)
  → 'github:NixOS/nixpkgs/1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a' (2025-07-29)
2025-08-02 02:12:47 +02:00
Martin Weinelt
027e6bcd76 postfix: don't cast message_size_limit to string
On unstable this will become a signed integer and there was never a good
reason for this to be a string.
2025-08-02 02:11:11 +02:00
Martin Weinelt
ce87c8a977 Merge branch 'options' into 'master'
acmeCertificateName: Set defaultText as the default is dynamic

See merge request simple-nixos-mailserver/nixos-mailserver!432
2025-07-23 15:47:20 +00:00
Tom Hubrecht
29de3e6865 acmeCertificateName: Set defaultText as the default is dynamic 2025-07-23 17:18:30 +02:00
Martin Weinelt
80d21ed7a1 Merge branch 'system-options' into 'master'
Introduce system name and domain options

See merge request simple-nixos-mailserver/nixos-mailserver!427
2025-07-09 11:20:39 +00:00
Martin Weinelt
e9953aa154 ruff: reject implicit string concat
This is a common mistake that could have been prevented.

```
migrations/nixos-mailserver-migration-03.py:42:9: ISC002 Implicitly concatenated string literals over multiple lines
   |
40 |   def is_maildir_related(path: Path, layout: FolderLayout) -> bool:
41 |       if path.name in [
42 | /         "subscriptions"
43 | |         # https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-uid-mapping
44 | |         "dovecot-uidlist",
   | |_________________________^ ISC002
45 |           # https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-keywords
46 |           "dovecot-keywords",
   |
```
2025-07-09 03:59:54 +02:00
Martin Weinelt
dda91cfc15 Merge branch 'patch-1' into 'master'
migrations: add missing comma in list

See merge request simple-nixos-mailserver/nixos-mailserver!429
2025-07-09 01:43:03 +00:00
Yureka
c2df33f76a migrations: add missing comma in list 2025-07-09 01:39:51 +00:00
Martin Weinelt
2b240501e0 Introduce system name and domain options
Bring them up from the DMARC reporting section to the mailserver toplevel
so they become reusable for the upcoming TLSRPT integration.

We default to the first domain in the domains option, if not set
explicitly, so that `systemDomain` doesn't become a blocker for existing
setups. We still encourage picking out the intended one, which is likely
the one used for the MX hostname.

This also simplifies the DMARC reporting configuration, which doesn't
need to be so fine-grained.

Co-Authored-By: Emily <git@emilylange.de>
2025-07-09 01:44:10 +02:00
Martin Weinelt
0aeb2849ad mail-check: fix format string 2025-07-08 04:39:36 +02:00
Martin Weinelt
47786932cb tests: fix deprecate machine config access 2025-07-08 03:58:37 +02:00
Martin Weinelt
358a44674e Merge branch 'flake-bump' into 'master'
flake.lock: Update

See merge request simple-nixos-mailserver/nixos-mailserver!428
2025-07-08 01:29:06 +00:00
Martin Weinelt
679bce8bbb flake.lock: Update
Flake lock file updates:

• Updated input 'git-hooks':
    'github:cachix/git-hooks.nix/623c56286de5a3193aa38891a6991b28f9bab056' (2025-06-11)
  → 'github:cachix/git-hooks.nix/16ec914f6fb6f599ce988427d9d94efddf25fe6d' (2025-06-24)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/3e3afe5174c561dee0df6f2c2b2236990146329f' (2025-06-07)
  → 'github:NixOS/nixpkgs/1fd8bada0b6117e6c7eb54aad5813023eed37ccb' (2025-07-06)
• Updated input 'nixpkgs-25_05':
    'github:NixOS/nixpkgs/fd487183437963a59ba763c0cc4f27e3447dd6dd' (2025-06-12)
  → 'github:NixOS/nixpkgs/29e290002bfff26af1db6f64d070698019460302' (2025-07-05)
2025-07-08 03:20:45 +02:00
Martin Weinelt
334e370c1f Merge branch 'dovecot-unit-name-migration' into 'master'
dovecot: use marker option as unit name migration indicator

See merge request simple-nixos-mailserver/nixos-mailserver!426
2025-07-06 23:24:27 +00:00
Martin Weinelt
d6d2053b80 dovecot: use marker option as unit name migration indicator
In nixpkgs we expose `services.dovecot.hasNewUnitName` option that can be
safely inspected to understand that whether to use the `dovecot` systemd
service name instead of `dovecot2`.
2025-07-07 01:10:19 +02:00
Martin Weinelt
6004878dc6 Merge branch 'dovecot-migration-compat-fixup' into 'master'
dovecot: fix check for dovecot systemd unit name

See merge request simple-nixos-mailserver/nixos-mailserver!425
2025-07-06 03:22:41 +00:00
Martin Weinelt
f9a52ca4b5 dovecot: fix check for dovecot systemd unit name
and migrate the preStart script in systemd.nix as well.
2025-07-06 05:18:01 +02:00
Martin Weinelt
a40574beb5 Merge branch 'dovecot-migration-compat' into 'master'
dovecot: add compat shim for dovecot unit name migration

See merge request simple-nixos-mailserver/nixos-mailserver!424
2025-07-06 00:58:47 +00:00
Martin Weinelt
b38dc8085c dovecot: add compat shim for dovecot unit name migration
In nixpkgs I renamed dovecot2 to dovecot and made dovecot2 an alias, so
adding the script to the alias does us no good.
2025-07-06 02:52:31 +02:00
Martin Weinelt
b10c54606b migrations: ignore maildir when in folder layout
Otherwise we'd be tryhing to move the maildir into itself and error out.
2025-06-26 16:52:49 +02:00
Martin Weinelt
c45b8a1253 Merge branch 'migrate-dovecot-control-files' into 'master'
migrations: also migrate dovecot control files

See merge request simple-nixos-mailserver/nixos-mailserver!423
2025-06-26 00:01:26 +00:00
Martin Weinelt
d91d94be94 migrations: also migrate dovecot control files 2025-06-25 22:09:41 +02:00
Martin Weinelt
b9e28e23af migrations: fix move of subscriptions
It is a file and we skip over files in the location I added it before.
2025-06-23 03:48:18 +02:00
Martin Weinelt
67f0b864cc migrations: also migrate subscriptions file in maildir migration
Otherwise users will be unsubscribed from all maildir folders.
2025-06-23 02:38:01 +02:00
Martin Weinelt
cfb3136cf0 Merge branch 'fix-cannot-compare-null-with-an-integer' into 'master'
assertions: fix eval error when `mailserver.stateVersion` is unset (null)

See merge request simple-nixos-mailserver/nixos-mailserver!421
2025-06-22 13:25:22 +00:00
emilylange
6ef1eb9ce1 assertions: fix eval error when mailserver.stateVersion is unset (null)
Eval does not stop on the first assertion failure it encouters.
Instead, it tries to evaluate all assertions and returns with a list of
those that failed.

This means our very top `config.mailserver.stateVersion != null`
assertion does not gate against any other assertions trying to compare
null against an integer.

The error prior to this commit can be reproduced by removing
`mailserver.stateVersion = 999;` in tests/lib/config.nix and then trying
to evaluate any of the tests:

~~~bash
# nix eval --raw .#checks.x86_64-linux.internal-unstable
error:
       … while evaluating the attribute 'outPath'
         at /nix/store/syvnmj3hhckkbncm94kfkbl76qsdqqj3-source/lib/customisation.nix:421:7:
          420|         drv.drvPath;
          421|       outPath =
             |       ^
          422|         assert condition;

       … while calling the 'getAttr' builtin
         at «internal»:1:500:
       (stack trace truncated; use '--show-trace' to show the full trace)

       error: cannot compare null with an integer
~~~
2025-06-21 20:15:46 +02:00
Martin Weinelt
9d8caf5944 Merge branch 'dovecot-home-mail-migration' into 'master'
dovecot: migrate to dedicated homedir and separate maildir paths

Closes #324

See merge request simple-nixos-mailserver/nixos-mailserver!408
2025-06-21 10:23:58 +00:00
Martin Weinelt
3c1cff431c tests: test for the expected maildir and index dir locations
These are not ideal yet, but we should make them a fixture, so that we
are always aware what they are for the different supported setups.
2025-06-21 10:28:43 +02:00
Martin Weinelt
f25495cabf dovecot: fix custom index dir configuration for ldap users 2025-06-21 09:47:03 +02:00
Martin Weinelt
62ea8a7e00 dovecot: migrate to dedicated homedir and separate maildir paths
Per the dovecot documentation[0] we were previously running with an
unsupported home directory configuration, because we shared them among
all virtual users at /var/vmail.

After resolving this by creating per user home directories at
/var/vmail/%{domain}/%{user} this now also overlaps with the location of
the Maildir, which is not recommended.

As a result we now need to migrate our Maildirs into
/var/vmail/%{domain}/%{user}/mail, for which a small shell script is
provided as part of this change.

The script is included in the documentation because we cannot provide it
in time for users, because they might already be seeing the relevant
assertion and there is no safe waiting period that would allow us to skip
shipping it like that.

[0] https://doc.dovecot.org/2.3/configuration_manual/mail_location/
2025-06-21 09:46:32 +02:00
Martin Weinelt
601b33d2a7 tests/minimal: drop
We have other tests that are minimal, e.g. the multiple test. And this
test wasn't even hooked up in flake.nix, so I'm doubtful that we really
need it.
2025-06-19 01:04:56 +02:00
Martin Weinelt
ed6d699eb4 Merge branch 'nuke-sha1' into 'master'
postfix: disable SHA1 for SMTP connections

See merge request simple-nixos-mailserver/nixos-mailserver!420
2025-06-18 16:54:39 +00:00
Martin Weinelt
64aca4f2ce postfix: disable SHA1 for SMTP connections 2025-06-18 06:58:42 +02:00
Martin Weinelt
217ec6008a Merge branch 'fast-tests' into 'master'
📉 Make tests fast

See merge request simple-nixos-mailserver/nixos-mailserver!419
2025-06-18 00:01:53 +00:00
Martin Weinelt
0774c93ae6 tests: make rspamd not block on dns queries
These will never suceed while running the tests in the Nix sandbox, and
skipping them leads to very noticable (~51%) speedups.

Before:
```
Benchmark 1: nix build .#hydraJobs.x86_64-linux.external-unstable --rebuild
  Time (mean ± σ):     151.737 s ±  1.074 s    [User: 0.310 s, System: 0.289 s]
  Range (min … max):   150.321 s … 153.512 s    10 runs
```

After:
```
Benchmark 1: nix build .#hydraJobs.x86_64-linux.external-unstable --rebuild
  Time (mean ± σ):     74.010 s ±  0.746 s    [User: 0.269 s, System: 0.266 s]
  Range (min … max):   72.814 s … 75.190 s    10 runs
```
2025-06-17 22:04:46 +02:00
Martin Weinelt
f08ee8da38 tests: provide a second cpu core
Provides a small (~7.5%) reduction in the test runtime measured for the external
test:

Before:
```
Benchmark 1: nix build .#hydraJobs.x86_64-linux.external-unstable --rebuild
  Time (mean ± σ):     151.737 s ±  1.074 s    [User: 0.310 s, System: 0.289 s]
  Range (min … max):   150.321 s … 153.512 s    10 runs
```

After:
```
Benchmark 1: nix build .#hydraJobs.x86_64-linux.external-unstable --rebuild
  Time (mean ± σ):     140.647 s ±  1.092 s    [User: 0.331 s, System: 0.296 s]
  Range (min … max):   138.536 s … 142.298 s    10 runs
```
2025-06-17 22:04:08 +02:00
Martin Weinelt
cf6ef5e9ca Create per service debug logging toggles
Enabling the rspamd debug log drowns out everything else and should be
selected explicitly as needed.

The external test does not require it and removing it makes it much
(~40.5%) faster, since it now does not block on terminal output anymore.

Before:
```
Benchmark 1: nix build .#hydraJobs.x86_64-linux.external-unstable --rebuild
  Time (mean ± σ):     151.737 s ±  1.074 s    [User: 0.310 s, System: 0.289 s]
  Range (min … max):   150.321 s … 153.512 s    10 runs
```

After:
```
Benchmark 1: nix build .#hydraJobs.x86_64-linux.external-unstable --rebuild
  Time (mean ± σ):     90.531 s ±  0.557 s    [User: 0.054 s, System: 0.045 s]
  Range (min … max):   89.579 s … 91.278 s    10 runs
```
2025-06-17 22:02:31 +02:00
Martin Weinelt
7405122dde Merge branch 'postfix-config' into 'master'
postfix: migrate more options to services.postfix.config

See merge request simple-nixos-mailserver/nixos-mailserver!418
2025-06-16 05:34:22 +00:00
Martin Weinelt
6652b57dda postfix: rearrange smtpd_tls_chain_files option 2025-06-16 07:27:03 +02:00
Martin Weinelt
c8f809fa76 postfix: migrate more options to services.postfix.config
I'm working on deprecating the top-level options, that configure main.cf
upstream in nixpkgs. With this change we stay ahead of the curve.

The `networks_style` option already defaults to `host` since Postfix 3.0,
so I dropped the setting.

```
$ postconf -d | grep networks_style
mynetworks_style = ${{$compatibility_level} <level {2} ? {subnet} : {host}}
````
2025-06-16 07:03:49 +02:00
Martin Weinelt
5c1b9921e6 Merge branch 'suggest-dmarc' into 'master'
Suggest that folks enable DMARC reporting

See merge request simple-nixos-mailserver/nixos-mailserver!377
2025-06-15 23:15:19 +00:00
Martin Weinelt
67b0a7e946 Merge branch 'cleanup' into 'master'
treewide: remove global `with lib` and overly broad `with cfg`

See merge request simple-nixos-mailserver/nixos-mailserver!416
2025-06-15 03:48:33 +00:00
Martin Weinelt
a2152f9807 treewide: remove overly broad with cfg
Makes it really hard to follow references and we were being explicit in
most places already anyway.
2025-06-15 05:39:20 +02:00
Martin Weinelt
fb56bcf747 treewide: remove global with lib
Instead inherit required functions from lib.
2025-06-15 05:08:47 +02:00
Martin Weinelt
b555b3e8dc Merge branch 'cleanup' into 'master'
Format with nixfmt, drop redundant parentheses

See merge request simple-nixos-mailserver/nixos-mailserver!415
2025-06-15 02:45:24 +00:00
Martin Weinelt
1a7f3d718c treewide: reformat with nixfmt-rfc-style 2025-06-15 03:39:44 +02:00
Martin Weinelt
03433d472f flake.nix: enable nixfmt-rfc-style hook and formatter 2025-06-15 03:34:20 +02:00
Martin Weinelt
c7497cd5f6 treewide: remove redundant parenthesis in nix code 2025-06-15 03:28:48 +02:00
Martin Weinelt
5f592b5960 Merge branch 'crypto-v2' into 'master'
postfix, dovecot: modernize and comment TLS settings

See merge request simple-nixos-mailserver/nixos-mailserver!413
2025-06-14 22:52:29 +00:00
Martin Weinelt
21ce4b4ff8 dovecot: disable Diffie-Hellman support
Recommended in the modern recommendation by Mozilla. Support for elliptic
curves is widespread and they are much faster.
2025-06-15 00:22:58 +02:00
Martin Weinelt
efebf59b13 dovecot: configure preferred elliptic curves 2025-06-15 00:22:57 +02:00
Martin Weinelt
4fd9508d41 postfix: drop tls_random_source config
The setting already defaults to /dev/urandom.
2025-06-15 00:22:57 +02:00
Martin Weinelt
3828b00dea postfix: configure preferred curves and disable FFDHE
This aligns with the intermediate configuration recommended by Mozilla.
2025-06-15 00:22:57 +02:00
Martin Weinelt
e27326d317 postfix: refactor and prune TLS settings
- Groups settings between server and client
- Uses a range comparator for supported TLS versions
- Prune excluded primitives to what affects the supported TLS versions
2025-06-15 00:22:57 +02:00
Martin Weinelt
23cc9a3996 Merge branch 'postfix-cert-key' into 'master'
postfix: configure cert/key using smtpd_tls_chain_files

Closes #183

See merge request simple-nixos-mailserver/nixos-mailserver!410
2025-06-14 12:47:58 +00:00
Martin Weinelt
e0ab4eeb67 docs/setup-guide: bump example stateVersion to 2
If you do a fresh install now you should be able to skip the first
migration step.
2025-06-14 01:20:27 +02:00
Martin Weinelt
8e0074c4e5 Merge branch 'flake-update' into 'master'
flake.lock: Update

See merge request simple-nixos-mailserver/nixos-mailserver!414
2025-06-13 02:13:15 +00:00
Martin Weinelt
3b7cda8cc5 flake.lock: Update
Flake lock file updates:

• Updated input 'git-hooks':
    'github:cachix/git-hooks.nix/dcf5072734cb576d2b0c59b2ac44f5050b5eac82' (2025-03-22)
  → 'github:cachix/git-hooks.nix/623c56286de5a3193aa38891a6991b28f9bab056' (2025-06-11)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/adaa24fbf46737f3f1b5497bf64bae750f82942e' (2025-05-13)
  → 'github:NixOS/nixpkgs/3e3afe5174c561dee0df6f2c2b2236990146329f' (2025-06-07)
• Updated input 'nixpkgs-25_05':
    'github:NixOS/nixpkgs/ca49c4304acf0973078db0a9d200fd2bae75676d' (2025-05-18)
  → 'github:NixOS/nixpkgs/fd487183437963a59ba763c0cc4f27e3447dd6dd' (2025-06-12)
2025-06-13 04:00:52 +02:00
Martin Weinelt
3f1c6960d3 Merge branch 'smptp-smuggling-cleanup' into 'master'
postfix: remove option to toggle SMTP smuggling workarounnd

See merge request simple-nixos-mailserver/nixos-mailserver!411
2025-06-12 22:57:43 +00:00
Martin Weinelt
54cb3e5784 Merge branch 'crypto' into 'master'
postfix: allow client to select the preferred cipher

See merge request simple-nixos-mailserver/nixos-mailserver!412
2025-06-12 22:48:04 +00:00
Martin Weinelt
f1bd4b8215 postfix: remove option to toggle SMTP smuggling workarounnd
It has been default enabled since Postfix 3.9 and can still be configured
from the NixOS option mentioned in the removal warning.

Removing the option makes our interface leaner.

Information is based on https://www.postfix.org/smtp-smuggling.html#long.
2025-06-13 00:21:16 +02:00
Martin Weinelt
e540dc864c postfix: configure cert/key using smtpd_tls_chain_files
The sslCert and sslKey options are going away, because they do too much,
e.g. provision the keypair for client certificate authentication, which
is not at all what we want or need.
2025-06-12 01:05:51 +02:00
Martin Weinelt
8b27add088 Merge branch 'backup_spam_db' into 'master'
docs: mention spam and ham training data in backup guide

See merge request simple-nixos-mailserver/nixos-mailserver!409
2025-06-06 21:16:24 +00:00
Guillaume Girol
49980abd25 mention spam and ham training data in backup guide 2025-06-06 12:00:00 +00:00
Martin Weinelt
f9b15192b8 postfix: allow client to select the preferred cipher
As long as all cipher we support are considered safe we can allow clients
to select one that suits them best.
2025-06-03 00:45:12 +02:00
Martin Weinelt
d6d6308ba2 Merge branch 'doc-backup-sieve' into 'master'
docs/backup-guide: add recommendation for sieveDirectory

See merge request simple-nixos-mailserver/nixos-mailserver!405
2025-06-02 14:57:24 +00:00
Tom Herbers
c4628a4c04 docs/backup-guide: add recommendation for sieveDirectory
Co-authored-by: Martin Weinelt <martin+gitlab@linuxlounge.net>
2025-06-02 11:27:09 +02:00
Martin Weinelt
8c835feaa7 docs/migrations: Improve title scoping for LDAP home dir migration 2025-06-02 04:31:41 +02:00
Martin Weinelt
c9f61e02ae docs/howto-develop: fix stateVersion assertion example 2025-05-31 13:06:29 +02:00
Martin Weinelt
145afc5393 Merge branch 'assertions-guard-reformat' into 'master'
assertions: guard by enable flag and reformat

See merge request simple-nixos-mailserver/nixos-mailserver!407
2025-05-31 10:51:28 +00:00
Martin Weinelt
ea1b0f8e2b assertions: guard by enable flag and reformat
None of these should trigger when you've not enabled mailserver.
2025-05-30 18:28:16 +02:00
Martin Weinelt
c8bc3e4f1f Merge branch 'ldap-mail-directory-assertion' into 'master'
Fix assertion for ldap mail directory

See merge request simple-nixos-mailserver/nixos-mailserver!406
2025-05-30 13:14:11 +00:00
Charlotte Van Petegem
519a85a801 Fix assertion for ldap mail directory 2025-05-30 12:49:02 +00:00
Martin Weinelt
ffd0e6f8f2 Merge branch 'dont-hardcode-ldap-home-base' into 'master'
dovecot: respect the mailDirectory base for LDAP home directories

See merge request simple-nixos-mailserver/nixos-mailserver!400
2025-05-29 21:14:25 +00:00
Martin Weinelt
7cb61e6e3a dovecot: respect the mailDirectory base for LDAP home directories
This change is safe, if you have not altered the default value of the
 `mailserver.mailDirectory` setting.
2025-05-29 23:10:33 +02:00
Martin Weinelt
a1e9276656 Merge branch 'remove-dovecot-module-workaround' into 'master'
dovecot: remove workaround for services.dovecot2.modules removal

See merge request simple-nixos-mailserver/nixos-mailserver!404
2025-05-29 17:41:37 +00:00
Martin Weinelt
233c5e1a70 dovecot: remove workaround for services.dovecot2.modules removal 2025-05-29 14:06:34 +02:00
Martin Weinelt
506c6151d6 Merge branch 'various-things' into 'master'
Cleanup

See merge request simple-nixos-mailserver/nixos-mailserver!403
2025-05-29 06:58:39 +00:00
Martin Weinelt
11bfdbf136 tests: drop dhparam default length configuration
This has been the default value since the option was introduced back in
2018[0].

[0] 81fc2c3509
2025-05-29 08:49:37 +02:00
Martin Weinelt
10cccc7706 docs: fix code block syntax in migration init 2025-05-29 08:48:56 +02:00
Martin Weinelt
6a78dc3375 Merge branch 'stateVersion' into 'master'
Introduce stateVersion concept

See merge request simple-nixos-mailserver/nixos-mailserver!401
2025-05-29 06:14:17 +00:00
Martin Weinelt
792225e256 Introduce stateVersion concept
With upcoming changes to the dovecot home and maildirectories we need to
introduce a way to nudge users to inform themselves about manual
migration steps they might need to carry out.

The idea here is to allow us to safely make breaking changes and notify
the user of required migration steps at eval time, so they can make the
necessary changes in time.
2025-05-27 23:54:15 +02:00
Martin Weinelt
53007af63f Merge branch 'release-25.05' into 'master'
Release 25.05

See merge request simple-nixos-mailserver/nixos-mailserver!399
2025-05-23 01:53:51 +00:00
Martin Weinelt
51d48f1492 Release 25.11 2025-05-22 01:31:46 +02:00
Martin Weinelt
b4ae17d224 Reformat release notes 2025-05-21 00:58:06 +02:00
Martin Weinelt
f7a221bc69 flake.nix: expose packages for custom pre-commit hooks in devshell 2025-05-21 00:56:01 +02:00
Martin Weinelt
dceb60ea7d Merge branch 'master-dovecot-fts-flatcurve' into 'master'
dovecot/fts: switch to fts-flatcurve

Closes #239

See merge request simple-nixos-mailserver/nixos-mailserver!361
2025-05-19 22:44:15 +00:00
euxane
826a3b2fcf tests/external: ignore time adjustments warnings
Seems to be happening randomly during tests:

    dovecot: master: Warning: Time moved forwards by 0.101534 seconds - adjusting timeouts.
2025-05-19 17:15:36 +02:00
euxane
0cbdf465e4 dovecot/fts: warn on stopwords filter with multiple languages 2025-05-19 16:45:09 +02:00
euxane
e287d83ab1 release-notes: mention switch to fts-flatcurve for FTS 2025-05-19 16:45:09 +02:00
euxane
2ed7a94782 dovecot/fts: switch to fts-flatcurve
This switches the full-text search plugin from fts-xapian to
fts-flatcurve, the now preferred indexer still powered by Xapian,
which will be integrated into Dovecot core 2.4.

This sets a sane minimal configuration for the plugin with
international language support.

The plugin options marked as "advanced" in Dovecot's documentation
aren't re-exposed for simplicity. They can nevertheless be overridden
by module consumers by directly setting keys with
`services.dovecot2.pluginSettings.fts_*`.

The `fullTextSearch.maintenance` option is removed as the index is now
incrementally optimised in the background.

GitLab: closes https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/239
2025-05-19 16:45:09 +02:00
Martin Weinelt
433520257a Merge branch 'pre-commit' into 'master'
Pre-Commit Hook

See merge request simple-nixos-mailserver/nixos-mailserver!385
2025-05-15 14:47:14 +00:00
Martin Weinelt
aa8366d234 treewide: remove dead nix references 2025-05-15 16:41:30 +02:00
Martin Weinelt
9a6190ceea rspamd: remove indirection in path to runtime directory 2025-05-15 16:29:06 +02:00
Martin Weinelt
1e51a503b1 dovecot: drop unused pipe scripts
Leftovers from d507bd9c95
2025-05-15 16:29:05 +02:00
Martin Weinelt
fce540024a docs/howto-develop: document the devshell 2025-05-15 16:29:05 +02:00
Martin Weinelt
040f07ff45 docs/howto-develop: update chat room references 2025-05-15 16:29:05 +02:00
Martin Weinelt
a73982f5b4 docs: migrate wiki references to wiki.nixos.org
This has been the official wiki platform for a while now.
2025-05-15 16:29:05 +02:00
Martin Weinelt
fbfd948535 flake.nix: remove clamav from devshell, add glab
With glab we provide the GitLab CLI utility to interact programatically
with the platform. Useful for checking our Merge request branches for
example.
2025-05-15 16:29:05 +02:00
Martin Weinelt
4c25278507 flake.nix: print options.md outpath during build
Helpful for debugging the resulting options file.
2025-05-15 16:29:05 +02:00
Martin Weinelt
3268d8b0d8 scripts/generate-options: refactor
- Extract the md syntax part into reusable functions
- Rename variables so their purpose becomes clearer
2025-05-15 16:29:04 +02:00
Martin Weinelt
4839fa6614 scripts: migrate format strings to f-strings 2025-05-15 16:29:04 +02:00
Martin Weinelt
ddc6ce61db docs: fix linting issues
https://github.com/sphinx-doc/sphinx/issues/3921
2025-05-15 16:29:04 +02:00
Martin Weinelt
a6eb2a8f9a README.md: reformat with markdownlint 2025-05-15 16:29:04 +02:00
Martin Weinelt
a7d580b934 treewide: reformat python code 2025-05-15 16:29:04 +02:00
Martin Weinelt
f9fcbe9430 scripts/generate-options: fix typing issue 2025-05-15 16:29:04 +02:00
Martin Weinelt
1615c93511 scripts/mail-check: fix typing issues
Replaces the body payload parsing with proper handling for multipart
messages.
2025-05-15 16:29:04 +02:00
Martin Weinelt
313f94ed8f flake.nix: create pre-commit hydra job 2025-05-15 16:29:04 +02:00
Martin Weinelt
ff9087adb4 flake.nix: drop CC from devshell
We absolutely do not need a C compiler in here.
2025-05-15 16:29:03 +02:00
Martin Weinelt
d0ac5ce64c flake.nix: annotate flake-compat usage
It is not used within flake.nix, so add a note that it is used elsewhere.
2025-05-15 16:29:03 +02:00
Martin Weinelt
dccca0506a Provide direnv integration for flake devshell 2025-05-15 16:29:03 +02:00
Martin Weinelt
41e513da64 flake.nix: configure pre-commit 2025-05-15 16:29:03 +02:00
Martin Weinelt
1899fbe3fb Merge branch 'nixpkgs-update' into 'master'
Update nixpkgs

See merge request simple-nixos-mailserver/nixos-mailserver!396
2025-05-15 14:27:57 +00:00
Martin Weinelt
dd83a2c7ad dovecot: rename sieve bayes/ham learning script
Updates the spamassasin reference to talk about rspamd.
2025-05-15 16:16:17 +02:00
Martin Weinelt
235dba2d82 tests/external: ignore new xapian warnings
These looks harmless.

Closes: #322
2025-05-15 16:16:17 +02:00
Martin Weinelt
edd828ca88 flake.lock: Update
Flake lock file updates:

• Updated input 'flake-compat':
    'github:edolstra/flake-compat/0f9255e01c2351cc7d116c072cb317785dd33b33' (2023-10-04)
  → 'github:edolstra/flake-compat/9100a0f413b0c601e0533d1d94ffd501ce2e7885' (2025-05-12)
• Updated input 'nixpkgs':
    'github:NixOS/nixpkgs/23e89b7da85c3640bbc2173fe04f4bd114342367' (2024-11-19)
  → 'github:NixOS/nixpkgs/adaa24fbf46737f3f1b5497bf64bae750f82942e' (2025-05-13)
• Updated input 'nixpkgs-24_11':
    'github:NixOS/nixpkgs/314e12ba369ccdb9b352a4db26ff419f7c49fa84' (2024-12-13)
  → 'github:NixOS/nixpkgs/5d736263df906c5da72ab0f372427814de2f52f8' (2025-05-14)
2025-05-15 16:16:16 +02:00
Martin Weinelt
1ce644871b flake.nix: ignore the flake registry
There is no real benefit using it anyway.
2025-05-15 16:16:16 +02:00
Martin Weinelt
da66510f68 Merge branch 'ci-reuse-flake-nixpkgs' into 'master'
ci: use hydra-cli from pinned nixpkgs

See merge request simple-nixos-mailserver/nixos-mailserver!395
2025-05-11 04:22:14 +00:00
Martin Weinelt
1f82d59d67 ci: use hydra-cli from pinned nixpkgs 2025-05-10 21:18:17 +02:00
Martin Weinelt
61b3a2c5ec Merge branch 'runtest-with-pinned-nixpkgs' into 'master'
flake.nix: run tests against pinned nixpkgs

See merge request simple-nixos-mailserver/nixos-mailserver!394
2025-05-10 16:23:55 +00:00
Martin Weinelt
ef1e02e555 flake.nix: run tests against pinned nixpkgs
and migrate to the new runTest, which evaluates much faster.
2025-05-10 02:43:35 +02:00
Martin Weinelt
1feca02008 Merge branch 'drop-nixops' into 'master'
treewide: drop nixops docs and examples

Closes #320

See merge request simple-nixos-mailserver/nixos-mailserver!393
2025-05-08 21:36:38 +00:00
Martin Weinelt
b92870c240 treewide: drop nixops docs and examples
This is not a deployment system we recommend using anymore in 2025.

Closes: #320
2025-05-08 23:22:29 +02:00
Jeremy Fleischman
8970ed0849 Suggest that folks enable DMARC reporting
SNM supports DMARC reporting, but it's disabled by default. For email
greybeards, that's fine, but I think it would be useful to teach email newbies (as I was a few
months ago) that this is something you should seriously consider
enabling.

I opted to put this in a new "Advanced Configurations" section that
points experienced mailserver admins to our howto guides, and newbies to
a couple of important things.

refs: https://github.com/NixOS/infra/pull/635
2025-05-08 13:12:43 -07:00
Martin Weinelt
a7d2b05a99 Merge branch 'quota-status-uds' into 'master'
dovecot: migrate queue-status to UNIX domain socket

See merge request simple-nixos-mailserver/nixos-mailserver!392
2025-05-07 17:05:15 +00:00
Martin Weinelt
4a09d6460a Merge branch 'tests-remove-broken-escape-sequences' into 'master'
tests: remove invalid escape sequences

See merge request simple-nixos-mailserver/nixos-mailserver!391
2025-05-07 16:38:00 +00:00
Martin Weinelt
a1ff289bf9 dovecot: migrate queue-status to UNIX domain socket 2025-05-07 18:00:53 +02:00
lewo
7bb0f43503 Merge branch 'dane-lookups' into 'master'
postfix: Support opportunistic DANE TLS

See merge request simple-nixos-mailserver/nixos-mailserver!389
2025-05-07 07:02:02 +00:00
Martin Weinelt
86b48f368f tests: remove invalid escape sequences
>>> "\@"
<stdin>:1: SyntaxWarning: invalid escape sequence '\@'
'\\@'
2025-05-07 03:56:41 +02:00
Martin Weinelt
e488e3639a Merge branch 'postfix-comments' into 'master'
postfix: adjust comments around smtpd_recipient_restrictions

See merge request simple-nixos-mailserver/nixos-mailserver!390
2025-05-07 00:59:11 +00:00
Martin Weinelt
2e254b4b5e postfix: adjust comments around smtpd_recipient_restrictions 2025-05-07 02:52:28 +02:00
Martin Weinelt
1471e54b92 Merge branch 'no-tls-1.1' into 'master'
postfix: disable TLSv1.1

See merge request simple-nixos-mailserver/nixos-mailserver!234
2025-05-07 00:48:13 +00:00
Martin Weinelt
fac7efe946 postfix: Support opportunistic DANE TLS
This migrates the security level for outgoing SMTP connections to
dane[1]. Either a server is configured for DANE or it now uses mandatory
unauthenticated TLS.

If DANE validation fails, the delivery will be tempfailed.

If DANE is invalid or unusable the connection will fall back to
unauthenticated mandatory TLS

This has been the default in various mail distributions:
- Mailcow since December 2016[2]
- mailinabox since July 2014[3]

[1] https://www.postfix.org/TLS_README.html#client_tls_dane
[2] 47a5166383
[3] e713af5f5a
2025-05-07 02:23:32 +02:00
Martin Weinelt
155ba08be7 Merge branch 'readme' into 'master'
README updates (Matrix, Automatic client configuration)

See merge request simple-nixos-mailserver/nixos-mailserver!388
2025-05-06 15:25:37 +00:00
Robert Schütz
71c5fe04f1 postfix: disable TLSv1.1
In accordance with https://ssl-config.mozilla.org/#server=postfix.
2025-05-06 02:42:13 -07:00
Martin Weinelt
8b4990905c Merge branch 'feature/ldap_forwards' into 'master'
ldap: Allow mailserver.forwards

See merge request simple-nixos-mailserver/nixos-mailserver!313
2025-05-06 03:38:48 +00:00
Martin Weinelt
f6a64f713c docs/release-notes: advertise mailserver.forwards with ldap 2025-05-06 05:32:59 +02:00
Elian Doran
b343c5e8fa assertions: Allow mailserver.forwards with LDAP set up 2025-05-06 05:32:45 +02:00
Martin Weinelt
776162c162 Merge branch 'dev/check-quota-is-null' into 'master'
mail-server/dovecot: check if quota is non-null instead of string

See merge request simple-nixos-mailserver/nixos-mailserver!362
2025-05-06 02:27:36 +00:00
Leon Schuermann
6f3ece9181 mail-server/dovecot: check if quota is non-null instead of string 2025-05-06 02:27:36 +00:00
Martin Weinelt
2d0b3fdeb0 README: Add automatic client configuration support to the roadmap 2025-05-06 03:37:23 +02:00
Martin Weinelt
4320259e34 README: add matrix room, reference libera connection information 2025-05-06 03:29:35 +02:00
Martin Weinelt
7091fad860 Merge branch 'rspamd-dkim-signing' into 'master'
Use rspamd for DKIM signing, drop OpenDKIM

Closes #203, #210, and #279

See merge request simple-nixos-mailserver/nixos-mailserver!374
2025-05-05 23:33:20 +00:00
Martin Weinelt
2520e662f7 tests/external: make DKIM signing test more explicit 2025-05-06 01:05:10 +02:00
Martin Weinelt
630b5c4fdd Use rspamd for DKIM signing, drop OpenDKIM
OpenDKIM has not been updated in the last 7 years and failed to adopt
RFC8463, which introduces Ed25519-SHA256 signatures.

It has thereby held back the DKIM ecosystem, which relies on the DNS
system to publish its public keys. The DNS system in turn does not handle
large record sizes well (see RFC8301), which is why Ed25519 public keys
would be preferable, but I'm not sure the ecosystem has caught up, so we
stay on the conservative side with RSA for now.

Fixes: #203 #210 #279
Obsoletes: !162 !338
Supersedes: !246
2025-05-06 01:05:10 +02:00
Martin Weinelt
2c37e563fd Merge branch 'cleanup' into 'master'
Various cleanups

See merge request simple-nixos-mailserver/nixos-mailserver!387
2025-05-05 20:58:25 +00:00
Martin Weinelt
8800bccab8 dovecot: fix config indent 2025-05-05 22:31:16 +02:00
Martin Weinelt
84bf0c0c07 README.md: remove mailing list information
Has been unused since 2019, so it is not a good recommendation to
subscribe there anymore.
2025-05-05 22:31:16 +02:00
Martin Weinelt
a071813b97 README: reword feature list
and remove the v2.0 release title.
2025-05-05 22:31:15 +02:00
Martin Weinelt
ca69f91f6b update.sh: drop
The section it updates was removed in d460e9ff62.
2025-05-05 21:21:58 +02:00
lewo
35185c023e Merge branch 'fix-rtd' into 'master'
Fix the readthedoc build

See merge request simple-nixos-mailserver/nixos-mailserver!386
2025-05-05 18:28:40 +00:00
Antoine Eiche
75b1908f24 Fix the RTD build 2025-05-05 20:22:45 +02:00
Martin Weinelt
95e2de368f Merge branch 'dovecot-prefer-client-ciphers' into 'master'
dovecot: prefer client cipher list

See merge request simple-nixos-mailserver/nixos-mailserver!383
2025-05-02 21:13:37 +00:00
Marcel
b859c910ab dmarc-reports: report mail message id with domain 2025-04-24 20:32:33 +00:00
Martin Weinelt
46fe2c25c8 dovecot: prefer client cipher list
All ciphers in TLSv1.2/TLSv1.3 are considered secure, so we can allow the
client to choose the most performant cipher according to their hardware
and software configuration.

This is in line with general recommendations, e.g. by Mozilla[1].

[1] https://wiki.mozilla.org/Security/Server_Side_TLS
2025-04-23 19:35:32 +00:00
Martin Weinelt
ab52efd622 ci: update to nixos-24.11 2025-04-23 16:02:07 +02:00
Martin Weinelt
42651ce2d3 docs: update release notes 2025-04-20 18:00:39 +02:00
Sandro Jäckel
bba070a1fe Remove policy-spf
Rspamd can do the same as policy-spf, only better, with more settings, is well integrated and better maintained.
Other projects are going the same route [1].

[1]: https://docker-mailserver.github.io/docker-mailserver/latest/config/best-practices/dkim_dmarc_spf/
2025-04-17 20:26:00 +02:00
Martin Weinelt
745c6ee861 rspamd: Use redis over a unix socket by default
Both rspamd and redis run on the same host by default, so a UNIX domain
socket is the cheapest way to facilitate that communication.

It also allows us to get rid of overly complicated IP adddress parsing
logic, that we can shift onto the user if they need it.
2025-04-15 16:17:30 +02:00
Jeremy Fleischman
7bdf5003c7 docs/dns: update DKIM TXT instructions
I recently went through this, and the generated file looks a bit
different than was previously documented.

I opted to be explicit about `k=rsa` (even though [the default is
"rsa"](https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1)).

I also opted to be explicit about `s=email` ([the default is
"*"](https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1)).
Honestly not sure what the consequences of this are, I don't know if
DKIM is used for anything besides email.
2025-04-14 06:22:32 +00:00
Martin Weinelt
1873ed0908 README: Update existing and future features
As the ecosystems around us evolve so should the NixOS mailserver
project.

DKIM signing could be improved by allowing users to treat DKIM keys like
a secret that they would commonly manage through agenix/sops/etc.

Forwarding mail these days requires SRS and possibly ARC. The latter has
already become a required feature for bulk message to iCloud[1] and
Google Mail[3]. I propose that we stay ahead of the curve by adding
support for these features.

LDAP user management was added, but one pain point is that we currently
prevent it from coexisting with declarative users.

And finally Oauth (via RFC7628[3]) is the new kid on the block that everyone
wants to try out, but most notably client support[4] for hosting this
yourself is not quite there yet.

[1] https://support.apple.com/en-us/102322
[2] https://support.google.com/a/answer/81126?hl=en#zippy=%2Crequirements-for-all-senders%2Crequirements-for-sending-or-more-messages-per-day
[3] https://www.rfc-editor.org/rfc/rfc7628.html
[4] https://bugzilla.mozilla.org/show_bug.cgi?id=1602166
2025-04-13 22:50:19 +02:00
Maximilian Bosch
efe77ce806 mail-server: add dmarcReporting.excludeDomains
The option `exclude_domains` for dmarc reporting in `rspamd`[1] allows
to configure a list of domains and/or eSLDs (external effective second level
domain) to be excluded from dmarc reports.

Helpful because e.g. dmarc reports to hotmail.com always fail for me
with the following undeliverable notification:

    The recipient's mailbox is full and can't accept messages now.

[1] https://www.rspamd.com/doc/modules/dmarc.html
2025-04-13 07:08:44 +00:00
Yureka
b4fbffe79c services.dovecot2.modules option has been removed 2025-03-19 20:52:57 +01:00
Michael Lohmann
0c40a0b2c6 dovecot: use expanded variable names
Since Dovecot 2.4 does not accept short notations for variables any more
https://doc.dovecot.org/2.4.0/installation/upgrade/2.3-to-2.4.html#variable-expansion
the long form needs to be used:
%u => %{user}
%n => %{username}
%d => %{domain}

This is backwards compatible with dovecot 2.3 as well:
https://doc.dovecot.org/2.3/configuration_manual/config_file/config_variables/#user-variables
2025-03-19 19:26:10 +00:00
Philipp Bartsch
9b5df96132 postfix: enable smtp tls logging
Log a summary message on TLS handshake completion.
2025-03-19 19:12:49 +00:00
Michael Lohmann
90539a1a99 Fix URLs for dovecot
The old wiki was deleted and so the new one has to be used
2025-03-14 21:16:26 +00:00
Michael Lohmann
c8ec4d5e43 remove rebootAfterKernelUpgrade option
This is not a feature specific to the mailserver. Indeed, the feature
was added to `system.autoUpgrade.allowReboot` with NixOS 19.09 and it
has better detection if a reboot is necessary.

For the system.autoUpgrade there is no kexec option, but the use was
discouraged.
2025-02-24 23:44:13 +01:00
Michael Lohmann
f23faf97d6 rebootAfterKernelUpgrade: document that this can be done from nixos
Since NixOS 19.09 autoUpgrade also has the ability to do automatic
reboots. Its detection on whether a reboot is necessary is a bit more
sophisticated. Having this option in the mail-server implied to me that
it did something additionally, though it was just a feature which was
not included in NixOS at the time it was introduced for the mail-server.

Mentioning the fact in the documentation might help people not to get
confused why they should turn the `system.autoUpgrade.allowReboot` off
and instead use the mail-servers reboot flag.
2025-02-24 16:11:59 +01:00
Antoine Eiche
8c1c4640b8 Increase the evaluation periodicity from 30s to 5m
This has been asked by the Nix community for debugging and maintenance
purposes.
2025-02-09 18:14:30 +01:00
euxane
6b425d13f5 tests: fix renamed options warnings 2025-01-24 17:40:48 +01:00
Guillaume Girol
ade37b2765 fts xapian: adapt to newer versions
fts xapian does not publish configuration changes in a changelog. As a
result, some options that nixos mailserver was setting for it have been
ignored for several years. New options (process_limit) are now
recommended. This adapts the module to these changes.

The default value of partial= is 2, but fts_xapian 1.8.3 now requires it
to be at least 3, and fails loudly in case it is 2. As a result, this
change is required to support fts_xapian 1.8.3 and later.
2025-01-18 12:00:00 +00:00
Ryan Trinkle
dc0569066e Make imap memory limit configurable 2024-12-26 16:25:46 +00:00
Ryan Trinkle
87ffaad9a3 Add quota-status memory limit 2024-12-26 16:25:46 +00:00
Ryan Trinkle
4a5eb4baea Make LMTP memory limit configurable 2024-12-26 16:25:46 +00:00
Antoine Eiche
63209b1def Release 24.11 2024-12-22 16:20:47 +00:00
lennart
26a56d0a8f Fix example for rejectSender
A domain prepended with an at sign does not work to reject senders on
domain level. Thus misleading documentation is fixed by removing it.
2024-12-20 00:15:57 +01:00
Sandro
c43d8c4a3c Fix wrong userAttrs default 2024-12-16 17:37:58 +00:00
Jeremy Fleischman
6db6c0dc72 Add instructions about creating a AAAA record 2024-12-16 17:35:11 +00:00
Jany Doe
e4aabd3de6 remove new line character if use agenix 2024-12-16 17:07:10 +00:00
Guillaume Girol
1cf6d01989 nix flake update 2024-11-24 00:16:56 +01:00
Guillaume Girol
0a801316cd tests: ignore debug message that looks like an error 2024-11-24 00:16:56 +01:00
Guillaume Girol
9919033068 tests: make the emails sent by mail-check.py look less like spam
rspamd complains that these emails miss these headers
2024-11-23 23:51:49 +01:00
Guillaume Girol
e901c56849 services.dnsmasq.extraConfig was removed on nixos-unstable 2024-11-23 23:51:49 +01:00
Guillaume Girol
3a082011dc recent nixos-unstable requires larger dh params 2024-11-23 12:00:00 +00:00
Sandro Jäckel
af7d3bf5da Wrap rspamc to avoid having to specific socket manually 2024-08-05 19:00:00 +02:00
Sandro Jäckel
059b50b2e7 Allow setting userAttrs to empty string
This allows overwriting the default values for user_attrs to be empty
which is required when using virtual mailboxes with ldap accounts
that have posixAccount attributes set. When user_attrs is empty string
those are ignored then.
2024-07-16 11:15:14 +02:00
Isabel
290a995de5 refactor: policyd-spf -> spf-engine 2024-06-18 09:03:27 +01:00
isabel
54cbacb6eb chore: remove flake utils 2024-06-14 21:52:49 +01:00
Antoine Eiche
29916981e7 Release 24.05 2024-06-11 07:36:43 +02:00
RoastedCheese
0d51a32e47 acme: test acmeCertificateName if module is enabled 2024-06-04 15:31:28 +00:00
Martin Weinelt
ed80b589d3 postfix: remove deprecated smtpd_tls_eecdh_grade
Causes a warning that suggests to just leave it at its default.
2024-06-03 12:34:43 +02:00
Matthew Leach
46a0829aa8 acme: Add new option acmeCertificateName
Allow the user to specify the name of the ACME configuration that the
mailserver should use. This allows users that request certificates that
aren't the FQDN of the mailserver, for example a wildcard certificate.
2024-05-31 09:53:32 +01:00
jopejoe1
41059fc548 docs: use settings instead of config in radicale 2024-05-03 09:14:16 +02:00
Sandro Jäckel
ef4756bcfc Quote ldap password
Otherwise special characters like # do not work
2024-04-28 10:02:48 +00:00
Sandro
9f6635a035 Drop default acmeRoot 2024-04-13 12:42:45 +00:00
Antoine Eiche
79c8cfcd58 Remove the support of 23.05 and 23.11
This is because SNM now supports the new sieve nixpkgs interface,
which is not backward compatible with previous releases.
2024-03-14 21:51:05 +01:00
Gaetan Lepage
799fe34c12 Update nixpkgs 2024-03-14 21:51:05 +01:00
Gaetan Lepage
d507bd9c95 dovecot: no longer need to copy sieve scripts 2024-03-14 21:50:46 +01:00
Raito Bezarius
fe6d325397 dovecot: support new sieve API in nixpkgs
Since https://github.com/NixOS/nixpkgs/pull/275031 things have became more structured
when it comes to the sieve plugin.

Relies on https://github.com/NixOS/nixpkgs/pull/281001 for full
features.
2024-03-09 23:23:17 +01:00
Christian Theune
572c1b4d69 rspamd: fix duplicate and syntactically wrong header settings
Fixes #280
2024-03-08 14:52:52 +01:00
Sleepful
9e36323ae3 Update roundcube example configuration: smtp_server is deprecated
Related issue on GH: https://github.com/roundcube/roundcubemail/issues/8756
2024-01-31 17:08:06 -06:00
Antoine Eiche
e47f3719f1 Release 23.11 2024-01-25 22:52:54 +01:00
Antoine Eiche
b5023b36a1 postfix: exclude $mynetwork from smtpd_forbid_bare_newline 2023-12-27 09:46:26 +01:00
Alvar Penning
3f526c08e8 postfix: SMTP Smuggling Protection
Enable Postfix SMTP Smuggling protection, introduced in Postfix 3.8.4,
which is, currently, only available within the nixpkgs' master branch.

- https://github.com/NixOS/nixpkgs/pull/276104
- https://github.com/NixOS/nixpkgs/pull/276264

For information about SMTP Smuggling:

- https://www.postfix.org/smtp-smuggling.html
- https://www.postfix.org/postconf.5.html#smtpd_forbid_bare_newline
2023-12-23 20:15:16 +01:00
Lafiel
008d78cc21 dovecot: add support store mailbox names on disk using UTF-8 2023-11-16 01:02:33 +03:00
Jean-Baptiste Giraudeau
84783b661e Add tests for regex (PCRE) aliases 2023-09-28 16:13:00 +02:00
Jean-Baptiste Giraudeau
93221e4b25 Add support for regex (PCRE) aliases. 2023-09-05 14:58:10 +02:00
Naïm Favier
c63f6e7b05 docs: fix link 2023-07-21 23:55:54 +02:00
Bjørn Forsman
a3b03d1b5a Use umask for race-free permission setting
Without using umask there's a small time window where paths are world
readable. That is a bad idea to do for secret files (e.g. the dovecot
code path).
2023-07-17 18:22:16 +02:00
Antoine Eiche
69a4b7ad67 ldap: add an entry in the doc 2023-07-11 19:31:20 +00:00
Antoine Eiche
71b4c62d85 dovecot: fix a typo on userAttrs 2023-07-11 19:31:20 +00:00
Antoine Eiche
6775502be3 ldap: set assertions to forbid ldap and loginAccounts simultaneously 2023-07-11 19:31:20 +00:00
Antoine Eiche
7695c856f1 ldap: improve the documentation 2023-07-11 19:31:20 +00:00
Antoine Eiche
fb3210b932 ldap: do not write password to the Nix store 2023-07-11 19:31:20 +00:00
Antoine Eiche
33554e57ce Make the ldap test working
- The smtp/imap user name is now user@domain.tld
- Make the test_lookup function much more robust: it was now getting
  the correct file from the store.
2023-07-11 19:31:20 +00:00
Martin Weinelt
8b03ae5701 Create LDAP test
Sets up a declaratively configured OpenLDAP instance with users alice
and bob. They each own one email address,

First we test that postfix can communicate with LDAP and do the expected
lookups using the defined maps.

Then we use doveadm to make sure it can look up the two accounts.

Next we check the binding between account and mail address, by logging
in as alice and trying to send from bob@example.com, which alice is not
allowed to do. We expect postfix to reject the sender address here.

Finally we check mail delivery between alice and bob. Alice tries to
send a mail from alice@example.com to bob@example.com and bob then
checks whether it arrived in their mailbox.
2023-07-11 19:31:20 +00:00
Martin Weinelt
42e245b069 scripts/mail-check: allow passing the smtp username
Will be prefered over the from address when specified.
2023-07-11 19:31:20 +00:00
Martin Weinelt
08f077c5ca Add support for LDAP users
Allow configuring lookups for users and their mail addresses from an
LDAP directory. The LDAP username will be used as an accountname as
opposed to the email address used as the `loginName` for declarative
accounts. Mailbox for LDAP users will be stored below
`/var/vmail/ldap/<account>`.

Configuring domains is out of scope, since domains require further
configuration within the NixOS mailserver construct to set up all
related services accordingly.

Aliases can already be configured using `mailserver.forwards` but could
be supported using LDAP at a later point.
2023-07-11 19:31:20 +00:00
Nigel Bray
d460e9ff62 Fix and improve the setup guide 2023-07-05 21:53:56 +02:00
Florian Klink
0c1801b489 dovecot: add dovecot_pigeonhole to system packages
`sieve-test` can be used to test sieve scripts.

It's annoying to nix-shell it in, because it reads the dovecot global
config and might stumble over incompatible .so files (as has happened
to me).

Simply providing it in $PATH is easier.
2023-06-29 20:54:57 +02:00
Antoine Eiche
24128c3052 Release 23.05 2023-06-22 21:31:07 +02:00
Antoine Eiche
c4ec122aac readme: remove the announcement public key
Current maintainer no longer has it.
2023-06-11 17:10:19 +02:00
Antoine Eiche
131c48de9b Preserve the compatibility with nixos-22.11 2023-06-11 17:10:14 +02:00
Antoine Eiche
290d00f6db Improve the certificateScheme number deprecation warning message 2023-06-11 07:29:18 +00:00
Mynacol
7e09d8f537 docs: add submissions DNS record for autodiscovery
Add the submissions autodiscovery SRV DNS record for implicit TLS in
SMTP (submission) connections according to
[RFC 8314](https://www.rfc-editor.org/rfc/rfc8314#section-5.1).
2023-05-29 15:09:08 +02:00
63 changed files with 4025 additions and 2057 deletions

3
.envrc Normal file
View File

@@ -0,0 +1,3 @@
# shellcheck shell=bash
use flake

2
.gitignore vendored
View File

@@ -1 +1,3 @@
result
.direnv
.pre-commit-config.yaml

View File

@@ -1,13 +1,18 @@
.hydra-cli:
image: docker.nix-community.org/nixpkgs/nix-flakes
script:
- nix run --inputs-from ./. nixpkgs#hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver "${jobset}"
hydra-pr:
extends: .hydra-cli
only:
- merge_requests
image: nixos/nix
script:
- nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}'
variables:
jobset: $CI_MERGE_REQUEST_IID
hydra-master:
extends: .hydra-cli
only:
- master
image: nixos/nix
script:
- nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master'
variables:
jobset: master

View File

@@ -1,25 +1,24 @@
{ nixpkgs, pulls, ... }:
let
pkgs = import nixpkgs {};
pkgs = import nixpkgs { };
prs = builtins.fromJSON (builtins.readFile pulls);
prJobsets = pkgs.lib.mapAttrs (num: info:
{ enabled = 1;
prJobsets = pkgs.lib.mapAttrs (num: info: {
enabled = 1;
hidden = false;
description = "PR ${num}: ${info.title}";
checkinterval = 30;
checkinterval = 300;
schedulingshares = 20;
enableemail = false;
emailoverride = "";
keepnr = 1;
type = 1;
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
}
) prs;
}) prs;
mkFlakeJobset = branch: {
description = "Build ${branch} branch of Simple NixOS MailServer";
checkinterval = "60";
checkinterval = 300;
enabled = "1";
schedulingshares = 100;
enableemail = false;
@@ -32,8 +31,8 @@ let
desc = prJobsets // {
"master" = mkFlakeJobset "master";
"nixos-22.05" = mkFlakeJobset "nixos-22.05";
"nixos-22.11" = mkFlakeJobset "nixos-22.11";
"nixos-25.05" = mkFlakeJobset "nixos-25.05";
"nixos-25.11" = mkFlakeJobset "nixos-25.11";
};
log = {
@@ -41,13 +40,14 @@ let
jobsets = desc;
};
in {
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
cat >$out <<EOF
in
{
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
cat >$out <<'EOF'
${builtins.toJSON desc}
EOF
# This is to get nice .jobsets build logs on Hydra
cat >tmp <<EOF
cat >tmp <<'EOF'
${builtins.toJSON log}
EOF
${pkgs.jq}/bin/jq . tmp

View File

@@ -5,17 +5,18 @@
version: 2
build:
os: ubuntu-22.04
os: ubuntu-24.04
tools:
python: "3"
apt_packages:
- nix
- curl
- proot
jobs:
pre_install:
- mkdir -p ~/.nix ~/.config/nix
- echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf
- proot -b ~/.nix:/nix /bin/sh -c "nix build -L .#optionsDoc && cp -v result docs/options.md"
- curl -L https://github.com/DavHau/nix-portable/releases/latest/download/nix-portable-$(uname -m) > ./nix-portable
- chmod +x ./nix-portable
- ./nix-portable nix build --print-build-logs .#optionsDoc
- ./nix-portable nix store cat $(readlink result) > docs/options.md
sphinx:
configuration: docs/conf.py

197
README.md
View File

@@ -1,160 +1,109 @@
# ![Simple Nixos MailServer][logo]
![license](https://img.shields.io/badge/license-GPL3-brightgreen.svg)
[![pipeline status](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/badges/master/pipeline.svg)](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
## Release branches
For each NixOS release, we publish a branch. You then have to use the
SNM branch corresponding to your NixOS version.
* For NixOS 22.11
- Use the [SNM branch `nixos-22.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.11)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11)
* For NixOS 22.05
- Use the [SNM branch `nixos-22.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.05)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/release-notes.html#nixos-22-05)
* For NixOS 25.11
* Use the [SNM branch `nixos-25.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.11)
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.11/)
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.11/release-notes.html#nixos-25-11)
* For NixOS 25.05
* Use the [SNM branch `nixos-25.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.05)
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/)
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05)
* For NixOS unstable
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
This is a very low volume list where new releases of SNM are announced, so you
can stay up to date with bug fixes and updates. All announcements are signed by
the gpg key with fingerprint
```
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
```
* Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
* [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
## Features
### v2.0
* [x] Continous Integration Testing
* [x] Multiple Domains
* Postfix MTA
- [x] smtp on port 25
- [x] submission tls on port 465
- [x] submission starttls on port 587
- [x] lmtp with dovecot
* Dovecot
- [x] maildir folders
- [x] imap with tls on port 993
- [x] pop3 with tls on port 995
- [x] imap with starttls on port 143
- [x] pop3 with starttls on port 110
* Certificates
- [x] manual certificates
- [x] on the fly creation
- [x] Let's Encrypt
* Spam Filtering
- [x] via rspamd
* Virus Scanning
- [x] via clamav
* DKIM Signing
- [x] via opendkim
* User Management
- [x] declarative user management
- [x] declarative password management
* Sieves
- [x] A simple standard script that moves spam
- [x] Allow user defined sieve scripts
- [x] ManageSieve support
* User Aliases
- [x] Regular aliases
- [x] Catch all aliases
* [x] Continous Integration Testing
* [x] Multiple Domains
* Postfix
* [x] SMTP on port 25
* [x] Submission TLS on port 465
* [x] Submission StartTLS on port 587
* [x] LMTP with Dovecot
* [x] DANE and MTA-STS validation
* [x] SMTP TLS Reports ([RFC 8460](https://www.rfc-editor.org/rfc/rfc8460))
* Dovecot
* [x] Maildir folders
* [x] IMAP with TLS on port 993
* [x] POP3 with TLS on port 995
* [x] IMAP with StartTLS on port 143
* [x] POP3 with StartTLS on port 110
* Certificates
* [x] ACME
* [x] Custom certificates
* Spam Filtering
* [x] Via Rspamd
* Virus Scanning
* [x] Via ClamAV
* DKIM Signing
* [x] Via Rspamd
* User Management
* [x] Declarative user management
* [x] Declarative password management
* [x] LDAP users
* Sieve
* [x] Allow user defined sieve scripts
* [x] Moving mails from/to junk trains the Bayes filter
* [x] ManageSieve support
* User Aliases
* [x] Regular aliases
* [x] Catch all aliases
* Improve the Forwarding Experience
* [x] [Sender Rewriting Scheme](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme)
### In the future
* DKIM Signing
- [ ] Allow a per domain selector
* Automatic client configuration
* [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)
* [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019)
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
* DKIM Signing
* [ ] Allow per domain selectors
* [ ] Allow passing DKIM signing keys
* Improve the Forwarding Experience
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html)
* User management
* [ ] Allow local and LDAP user to coexist
* OpenID Connect
* Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166)
### Get in touch
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
- Join the Libera Chat IRC channel `#nixos-mailserver`
### Quick Start
```nix
{ config, pkgs, ... }:
let release = "nixos-21.11";
in {
imports = [
(builtins.fetchTarball {
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz";
# This hash needs to be updated
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
loginAccounts = {
"user1@example.com" = {
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' > /hashed/password/file/location
hashedPasswordFile = "/hashed/password/file/location";
aliases = [
"info@example.com"
"postmaster@example.com"
"postmaster@example2.com"
];
};
};
};
}
```
For a complete list of options, see `default.nix`.
* Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org)
* IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect)
## How to Set Up a 10/10 Mail Server Guide
Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
## How to Backup
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
Checkout the [Complete Backup Guide](https://nixos-mailserver.readthedocs.io/en/latest/backup-guide.html). Backups are easy with `SNM`.
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
## Development
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) wiki page.
## Release notes
### nixos-20.03
- Rspamd is upgraded to 2.0 which deprecates the SQLite Bayes
backend. We then moved to the Redis backend (the default since
Rspamd 2.0). If you don't want to relearn the Redis backend from the
scratch, we could manually run
rspamadm statconvert --spam-db /var/lib/rspamd/bayes.spam.sqlite --ham-db /var/lib/rspamd/bayes.ham.sqlite -h 127.0.0.1:6379 --symbol-ham BAYES_HAM --symbol-spam BAYES_SPAM
See the [Rspamd migration
notes](https://rspamd.com/doc/migration.html#migration-to-rspamd-20)
and [this SNM Merge
Request](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/164)
for details.
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
## Contributors
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
### Alternative Implementations
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
### Credits
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
from [TheNounProject](https://thenounproject.com/) is licensed under
[CC BY 3.0](http://creativecommons.org/~/3.0/)
* Logo made with [Logomakr.com](https://logomakr.com)
* Logo made with [Logomakr.com](https://logomakr.com)
[logo]: docs/logo.png

File diff suppressed because it is too large Load Diff

View File

@@ -24,12 +24,13 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`.
in {
services.radicale = {
enable = true;
config = ''
[auth]
type = htpasswd
htpasswd_filename = ${htpasswd}
htpasswd_encryption = bcrypt
'';
settings = {
auth = {
type = "htpasswd";
htpasswd_filename = "${htpasswd}";
htpasswd_encryption = "bcrypt";
};
};
};
services.nginx = {

View File

@@ -20,7 +20,7 @@ servers may require more work.
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_server'] = "tls://${config.mailserver.fqdn}";
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';

View File

@@ -0,0 +1,14 @@
Advanced Configurations
=======================
Congratulations on completing the `Setup Guide <setup-guide.html>`_!
If you're an experienced mailserver admin, then you probably know what you want
to do next. Our How-to guides (accessible in the navigation sidebar)
might help you accomplish your goals. If not, consider contributing a guide!
If this is your first mailserver, consider the following:
- Set up `backups <backup-guide.html>`_.
- Enable `DMARC reporting <options.html#mailserver-dmarcreporting>`_ to be a
good citizen in the mail ecosystem.

View File

@@ -4,13 +4,14 @@ Autodiscovery
`RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
of the mailserver. For that, the following records are required:
================ ==== ==== ======== ====== ==== =================
================= ==== ==== ======== ====== ==== =================
Record TTL Type Priority Weight Port Value
================ ==== ==== ======== ====== ==== =================
================= ==== ==== ======== ====== ==== =================
_submission._tcp 3600 SRV 5 0 587 mail.example.com.
_submissions._tcp 3600 SRV 5 0 465 mail.example.com.
_imap._tcp 3600 SRV 5 0 143 mail.example.com.
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
================ ==== ==== ======== ====== ==== =================
================= ==== ==== ======== ====== ==== =================
Please note that only a few MUAs currently implement this. For vendor-specific
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.

View File

@@ -14,6 +14,13 @@ forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
back (or whatever you specified as ``vmailUserName``, and
``vmailGoupName``).
If you enabled ``enableManageSieve`` then you also may want to backup
``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
The same considerations regarding file ownership apply as for the
Maildir.
To backup spam and ham training data, backup ``/var/lib/redis-rspamd``.
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
you specified as ``dkimKeyDirectory``). If you should lose those dont
worry, new ones will be created on the fly. But you will need to repeat

View File

@@ -17,9 +17,9 @@
# -- Project information -----------------------------------------------------
project = 'NixOS Mailserver'
copyright = '2022, NixOS Mailserver Contributors'
author = 'NixOS Mailserver Contributors'
project = "NixOS Mailserver"
copyright = "2022, NixOS Mailserver Contributors"
author = "NixOS Mailserver Contributors"
# -- General configuration ---------------------------------------------------
@@ -27,33 +27,31 @@ author = 'NixOS Mailserver Contributors'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'myst_parser'
]
extensions = ["myst_parser"]
myst_enable_extensions = [
'colon_fence',
'linkify',
"colon_fence",
"linkify",
]
smartquotes = False
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
master_doc = 'index'
master_doc = "index"
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,

View File

@@ -1,7 +1,7 @@
Nix Flakes
==========
If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
If you're using `flakes <https://wiki.nixos.org/wiki/Flakes>`__, you can use
the following minimal ``flake.nix`` as an example:
.. code:: nix

View File

@@ -4,7 +4,7 @@ Full text search
By default, when your IMAP client searches for an email containing some
text in its *body*, dovecot will read all your email sequentially. This
is very slow and IO intensive. To speed body searches up, it is possible to
*index* emails with a plugin to dovecot, ``fts_xapian``.
*index* emails with a plugin to dovecot, ``fts_flatcurve``.
Enabling full text search
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -20,8 +20,6 @@ To enable indexing for full text search here is an example configuration.
enable = true;
# index new email as they arrive
autoIndex = true;
# this only applies to plain text attachments, binary attachments are never indexed
indexAttachments = true;
enforced = "body";
};
};
@@ -61,8 +59,8 @@ Mitigating resources requirements
You can:
* disable indexation of attachements ``mailserver.fullTextSearch.indexAttachments = false``
* reduce the size of ngrams to be indexed ``mailserver.fullTextSearch.minSize`` and ``maxSize``
* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes``
* disable expensive token normalisation in ``mailserver.fullTextSearch.filters``
* disable automatic indexation for some folders with
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard.

View File

@@ -4,13 +4,33 @@ Contribute or troubleshoot
To report an issue, please go to
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
You can also chat with us on the Libera IRC channel ``#nixos-mailserver``.
If you have questions, feel free to reach out:
* Matrix: `#nixos-mailserver:nixos.org <https://matrix.to/#/#nixos-mailserver:nixos.org>`__
* IRC: `#nixos-mailserver <ircs://irc.libera.chat/nixos-mailserver>`__ on `Libera Chat <https://libera.chat/guides/connect>`__
All our workflows rely on Nix being configured with `Flakes <https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
Development Shell
-----------------
We provide a `flake.nix` devshell that automatically sets up pre-commit hooks,
which allows for fast feedback cycles when making changes to the repository.
::
$ nix develop
We recommend setting up `direnv <https://direnv.net/>`__ to automatically
attach to the development environment when entering the project directories.
Run NixOS tests
---------------
To run the test suite, you need to enable `Nix Flakes
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
You can then run the testsuite via
@@ -37,7 +57,7 @@ For the syntax, see the `RST/Sphinx primer
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
To build the documentation, you need to enable `Nix Flakes
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
::
@@ -45,28 +65,43 @@ To build the documentation, you need to enable `Nix Flakes
$ nix build .#documentation
$ xdg-open result/index.html
Nixops
------
You can test the setup via ``nixops``. After installation, do
Manual migrations
-----------------
::
We need to take great care around providing a migration story around breaking
changes. If manual intervention becomes necessary we provide the `stateVersion`
option to notify the user that they need to complete a migration before
they can deploy an update.
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
$ nixops deploy -d mail
$ nixops info -d mail
If that is the case for your change, find the highest `stateVersion` that is
being asserted on in `mail-server/assertions.nix`. Then pick the next number
and add a new assertion, write a good summary describing the issue and what
remediation steps are necessary. Finally reference the URL to the specific
section on the migration page in the documentation.
You can then test the server via e.g. \ ``telnet``. To log into it, use
.. code-block:: nix
::
{
assertions = [
{
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 1;
message = ''
Problem: The home directory for the foobar service is snafu.
Remediation:
- Stop the `foobar.service`
- Rename `/var/lib/foobaz` to `/var/lib/foobar`
- Increase the `mailserver.stateVersion` to 1.
$ nixops ssh -d mail mailserver
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#specific-anchor-here for further details.
'';
}
];
}
Imap
----
The setup guide should always reference the latest `stateVersion`, since we
don't require any migration steps for new setups.
To test imap manually use
::
$ openssl s_client -host mail.example.com -port 143 -starttls imap
The migration documentation should paint a more complete picture about the steps
that need to be carried out and why this has become necessary. Make sure to
reference the correct anchor in the URL you put into the assertion message.

View File

@@ -14,20 +14,29 @@ Welcome to NixOS Mailserver's documentation!
:maxdepth: 2
setup-guide
advanced-configurations
howto-develop
faq
release-notes
options
migrations
.. toctree::
:maxdepth: 1
:caption: Features
fts
ldap
srs
.. toctree::
:maxdepth: 0
:caption: How-to
backup-guide
add-radicale
add-roundcube
rspamd-tuning
fts
flakes
autodiscovery

14
docs/ldap.rst Normal file
View File

@@ -0,0 +1,14 @@
LDAP Support
============
It is possible to manage mail user accounts with LDAP rather than with
the option `loginAccounts <options.html#mailserver-loginaccounts>`_.
All related LDAP options are described in the `LDAP options section
<options.html#mailserver-ldap>`_ and the `LDAP test
<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/tests/ldap.nix>`_
provides a getting started example.
.. note::
The LDAP support can not be enabled if some accounts are also defined with ``mailserver.loginAccounts``.

117
docs/migrations.rst Normal file
View File

@@ -0,0 +1,117 @@
Migrations
==========
With mail server configuration best practices changing over time we might need
to make changes that require you to complete manual migration steps before you
can deploy a new version of NixOS mailserver.
The initial `mailserver.stateVersion` value should be copied from the setup
guide that you used to initially set up your mail server. If in doubt you can
always initialize it at `1` and walk through all assertions, that might apply
to your setup.
NixOS 25.11
-----------
#3 Dovecot mail directory migration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The way the Dovecot home directory for login accounts were previously set up
resulted in shared home directories for all those users. This is not a
supported Dovecot configuration.
To resolve this we migrated the home directory into the individual
`domain/localpart` subdirectory below the `mailserver.mailDirectory`.
But since this now overlaps with the location of the Maildir, it must be
migrated into the `mail/` directory below the home directory.
And while the LDAP home directory is not affected we use this migration to
keep the Maildir configurations of LDAP users in sync with those of local
accounts.
This is a big step forward, since we can now more cleanly colocate other
data directories, like sieve in the home directory, which in turn simplifies
backups.
This migration is required for every configuration.
For remediating this issue the following steps are required:
1. Copy the `migration script <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/migrations/nixos-mailserver-migration-03.py>`_ script to your mailserver
and make it executable:
.. code-block:: bash
wcurl https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/raw/master/migrations/nixos-mailserver-migration-03.py
chmod +x nixos-mailserver-migration-03.py
2. Stop the ``dovecot2.service``.
.. code-block:: bash
systemctl stop dovecot2.service
3. Create a backup or snapshot of your ``mailserver.mailDirectory``, so you can restore
should anything go wrong.
4. Run the migration script under your virtual mail user with the following arguments:
- ``--layout default`` unless ``useFSLayout`` is enabled, then ``--layout folder``
- The value of ``mailserver.mailDirectory``, which defaults to ``/var/vmail``
The script should be run under the user who owns the ``mailDirectory``.
If run as root it will try to switch into the appropriate user by itself.
The script will not modify your data unless called with ``--execute``.
Example:
.. code-block:: bash
./nixos-mailserver-migration-03.py --layout default /var/vmail
5. Review the commands. They should be
- create a ``mail`` directory for each accounnt,
- move maildir contents from the parent directory into it,
- suggest removal of files that do not belong to the maildir
- their removal is not mandatory and the script **will not** remove them when called with ``--execute``
- review these items carefully if you want to remove them yourself
- remove obsolete files from the old home directory location
6. Rerun the command with ``--execute`` or run the commands manually.
7. Update the ``mailserver.stateVersion`` to ``3``.
#2 Dovecot LDAP home directory migration
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The Dovecot configuration for LDAP home directories previously did not respect
the ``mailserver.mailDirectory`` setting.
This means that home directories were unconditionally located at
``/var/vmail/ldap/%{user}``.
This migration is required if you both:
* enabled the LDAP integration (``mailserver.ldap.enable``)
* and customized the default mail directory (``mailserver.mailDirectory != "/var/vmail"``)
For remediating this issue the following steps are required:
1. Stop ``dovecot2.service``.
2. Move ``/var/vmail/ldap`` below your ``m̀ailserver.mailDirectory``.
3. Update the ``mailserver.stateVersion`` to ``2``.
#1 Initialization
^^^^^^^^^^^^^^^^^
This option was introduced in the NixOS 25.11 release cycle, in which case you
can safely initialize its value at `1`.
.. code-block:: nix
mailserver.stateVersion = 1;

View File

@@ -1,10 +1,105 @@
Release Notes
=============
NixOS 25.11
-----------
- The ``systemName`` and ``systemDomain`` options have been introduced to have
reusable configurations for automated reports (DMARC, TLSRPT). They come with
reasonable defaults, but it is suggested to check and change them as needed.
- Support for the `Sender Rewriting Scheme`_ has been added, which allows
forwarding mail without breaking SPF by rewriting the envelope address.
- The default key length for new DKIM RSA keys was increased to 2048 bits as
recommended in `RFC 8301 3.2`_.
We recommend rotating existing keys, as the RFC advises that signatures from
1024 bit keys should not be considered valid any longer.
- IMAP access over port ``143/tcp`` is now default disabled in line
with `RFC 8314 4.1`_. Use IMAP over implicit TLS on port ``993/tcp``
instead. If you still require this feature you can reenable it using
``mailserver.enableImap``, but it is scheduled for removal after the 25.11
release.
- SMTP server and client now support and prefer a hybrid key exchange
(X25519MLKEM768)
- SMTP access over STARTTLS on port ``587/tcp`` is now default disabled in line
with `RFC 8314 3.3`_. If you still require this feature you can renable it using
``mailserver.enableSubmission``.
- DMARC reports are now sent with the ``noreply-dmarc`` localpart from the
system domain.
- DANE and MTA-STS are now validated for outgoing SMTP connections using
`postfix-tlspol`_.
- SMTP TLS connection reports (`RFC 8460`_) are now supported using
`tlsrpt-reporter`_. They can be enabled with the ``mailserver.tlsrpt.enable``
option.
.. _Sender Rewriting Scheme: srs.html
.. _RFC 8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2
.. _RFC 8314 3.3: https://www.rfc-editor.org/rfc/rfc8314#section-3.3
.. _RFC 8314 4.1: https://www.rfc-editor.org/rfc/rfc8314#section-4.1
.. _RFC 8460: https://www.rfc-editor.org/rfc/rfc8460
.. _postfix-tlspol: https://github.com/Zuplu/postfix-tlspol
.. _tlsrpt-reporter: https://github.com/sys4/tlsrpt-reporter
NixOS 25.05
-----------
- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed`` canoncalizaliaton.
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/374>`__)
- Rspamd now connects to Redis over its Unix Domain Socket by default
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/375>`__)
- If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``.
- The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring.
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/380>`__)
- Switch to the more efficient `fts-flatcurve` indexer for full text search
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/361>`__).
This makes use of a new index, which will be automatically re-generated the
next time a folder is searched.
The operation is now quick enough to be performed "just-in-time".
Alternatively, all indices can be immediately re-generated for all users and
folders by running
.. code-block:: bash
doveadm fts rescan -u '*' && doveadm index -u '*' -q '*'
The previous index (which is not automatically discarded to allow rollbacks)
can be cleaned up by removing all the `xapian-indexes` directories within
``mailserver.indexDir``.
- Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``.
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/297>`__)
- Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP.
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/313>`__)
- Support for TLS 1.1 was disabled in accordance with `Mozilla's recommendations <https://ssl-config.mozilla.org/#server=postfix>`_.
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/234>`__)
NixOS 24.11
-----------
- No new feature, only bug fixes and documentation improvements
NixOS 24.05
-----------
- Add new option ``acmeCertificateName`` which can be used to support
wildcard certificates
NixOS 23.11
-----------
- Add basic support for LDAP users
- Add support for regex (PCRE) aliases
NixOS 23.05
-----------
- Existing ACME certificates can be reused without configuring NGINX
- Certificate scheme is no longer a number, but a meaningful string instead
NixOS 22.11
-----------
- Allow Rspamd to send dmarc reporting
- Allow Rspamd to send DMARC reporting
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
NixOS 22.05
@@ -27,7 +122,6 @@ NixOS 21.11
- New option ``certificateDomains`` to generate certificate for
additional domains (such as ``imap.example.com``)
NixOS 21.05
-----------

View File

@@ -2,3 +2,4 @@ sphinx ~= 5.3
sphinx_rtd_theme ~= 1.1
myst-parser ~= 0.18
linkify-it-py ~= 2.0
standard-imghdr

View File

@@ -24,17 +24,14 @@ You can run the training in a root shell as follows:
.. code:: bash
# Path to the controller socket
export RSOCK="/var/run/rspamd/worker-controller.sock"
# Learn the Junk folder as spam
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
# Learn the INBOX as ham
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/
rspamc learn_ham /var/vmail/$DOMAIN/$USER/cur/
# Check that training was successful
rspamc -h $RSOCK stat | grep learned
rspamc stat | grep learned
Tune symbol weight
~~~~~~~~~~~~~~~~~~

View File

@@ -20,25 +20,30 @@ an up and running mail server. Once the server is deployed, we could
then set all DNS entries required to send and receive mails on this
server.
Setup DNS A record for server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Setup DNS A/AAAA records for server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add a DNS record to the domain ``example.com`` with the following
Add DNS records to the domain ``example.com`` with the following
entries
==================== ===== ==== =============
Name (Subdomain) TTL Type Value
==================== ===== ==== =============
``mail.example.com`` 10800 A ``1.2.3.4``
``mail.example.com`` 10800 AAAA ``2001::1``
==================== ===== ==== =============
If your server does not have an IPv6 address, you must skip the `AAAA` record.
You can check this with
::
$ ping mail.example.com
64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms
...
$ nix-shell -p bind --command "host -t A mail.example.com"
mail.example.com has address 1.2.3.4
$ nix-shell -p bind --command "host -t AAAA mail.example.com"
mail.example.com has address 2001::1
Note that it can take a while until a DNS entry is propagated. This
DNS entry is required for the Let's Encrypt certificate generation
@@ -48,24 +53,26 @@ Setup the server
~~~~~~~~~~~~~~~~
The following describes a server setup that is fairly complete. Even
though there are more possible options (see the ``default.nix`` file),
these should be the most common ones.
though there are more possible options (see the `NixOS Mailserver
options documentation <options.html>`_), these should be the most
common ones.
.. code:: nix
{ config, pkgs, ... }:
{
{ config, pkgs, ... }: {
imports = [
(builtins.fetchTarball {
# Pick a commit from the branch you are interested in
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
# And set its hash
# Pick a release version you are interested in and set its hash, e.g.
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.11/nixos-mailserver-nixos-25.11.tar.gz";
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
# release="nixos-25.11"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
mailserver = {
enable = true;
stateVersion = 3;
fqdn = "mail.example.com";
domains = [ "example.com" ];
@@ -83,6 +90,8 @@ these should be the most common ones.
# down nginx and opens port 80.
certificateScheme = "acme-nginx";
};
security.acme.acceptTerms = true;
security.acme.defaults.email = "security@example.com";
}
After a ``nixos-rebuild switch`` your server should be running all
@@ -95,8 +104,11 @@ Set rDNS (reverse DNS) entry for server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Wherever you have rented your server, you should be able to set reverse
DNS entries for the IPs you own. Add an entry resolving ``1.2.3.4``
to ``mail.example.com``.
DNS entries for the IPs you own:
- Add an entry resolving IPv4 address ``1.2.3.4`` to ``mail.example.com``.
- Add an entry resolving IPv6 ``2001::1`` to ``mail.example.com``. Again, this
must be skipped if your server does not have an IPv6 address.
.. warning::
@@ -112,6 +124,9 @@ You can check this with
$ nix-shell -p bind --command "host 1.2.3.4"
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
$ nix-shell -p bind --command "host 2001::1"
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.2.ip6.arpa domain name pointer mail.example.com.
Note that it can take a while until a DNS entry is propagated.
Set a ``MX`` record
@@ -159,25 +174,26 @@ Note that it can take a while until a DNS entry is propagated.
Set ``DKIM`` signature
^^^^^^^^^^^^^^^^^^^^^^
On your server, the ``opendkim`` systemd service generated a file
On your server, the ``rspamd`` systemd service generated a file
containing your DKIM public key in the file
``/var/dkim/example.com.mail.txt``. The content of this file looks
like
::
mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=<really-long-key>" ; ----- DKIM mail for domain.tld
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"p=<really-long-key>" ) ; ----- DKIM key mail for nixos.org
where ``really-long-key`` is your public key.
Based on the content of this file, we can add a ``DKIM`` record to the
domain ``example.com``.
=========================== ===== ==== ==============================
=========================== ===== ==== ================================================
Name (Subdomain) TTL Type Value
=========================== ===== ==== ==============================
mail._domainkey.example.com 10800 TXT ``v=DKIM1; p=<really-long-key>``
=========================== ===== ==== ==============================
=========================== ===== ==== ================================================
mail._domainkey.example.com 10800 TXT ``v=DKIM1; k=rsa; s=email; p=<really-long-key>``
=========================== ===== ==== ================================================
You can check this with
@@ -222,3 +238,8 @@ Besides that, you can send an email to
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
your setup, but if you followed the steps closely then everything should
be awesome!
Next steps (optional)
~~~~~~~~~~~~~~~~~~~~~
Take a look through our `Advanced Configurations <advanced-configurations.html>`_.

102
docs/srs.rst Normal file
View File

@@ -0,0 +1,102 @@
Sender Rewriting Scheme
=======================
The Sender Rewriting Scheme (SRS) allows mail servers to forward emails without
breaking SPF checks. By rewriting the envelope sender to an address within the
forwarders domain, SRS ensures that forwarded messages pass SPF validation,
preventing them from being rejected as spoofed or unauthorized.
How SRS works in practice
~~~~~~~~~~~~~~~~~~~~~~~~~
1. ``alice@foo.example`` receives an E-Mail from ``bob@bar.example``. Both the
envelope sender as well as the ``From`` header show ``bob@bar.example``. This
results in strict SPF alignment, because ``bar.example`` is the domain used in
both the ``Return-Path`` and ``FROM`` headers.
2. ``alice@foo.example`` forwards the mail to ``charlie@moo.example`` and
uses SRS to rewrite the envelope sender to originate from the local SRS domain
(e.g. `SRS0=HHH=TT=bar.example=alice@foo.example`). The ``FROM`` header remains
unchanged. This ensures that the forwarded mail succeeds SPF checks.
3. The email reaches ``charlie@moo.example``. SPF passes because the sender
domain in the envelope has been rewritten. The mismatch between envelope sender
domain and ``FROM`` domain does however break strict SPF alignment.
Enabling SRS
~~~~~~~~~~~~
In a simple setup just enabling SRS will use your ``mailserver.systemDomain``
when rewriting the envelope sender domain.
.. code:: nix
{
mailserver = {
srs = {
enable = true;
#domain = "srs.example.com";
};
};
};
..
While you can reuse an existing email domain for SRS, it is recommended to
configure a dedicated SRS domain. This is particularly important under the
following conditions:
* Multiple unrelated mail domains are hosted on the mailserver
* The mail domain requires strict SPF alignment in its DMARC policy
Required DNS changes
~~~~~~~~~~~~~~~~~~~~
.. note::
In the following example we assume that you want to set up a dedicated SRS
domain. If that is not the case you already have SPF and DKIM set up for the
system domain. If you have a DMARC record on the system domain, make sure it
uses a relaxed SPF alignment policy (``aspf=r``).
First we set up an MX record. This is so that we can receive and route bounces
that can result from forwards.
======================== ===== ==== ======== =====================
Name (Subdomain) TTL Type Priority Value
======================== ===== ==== ======== =====================
srs.example.com 10800 MX 10 ``mail.example.com``
======================== ===== ==== ==============================
Next up is the SPF record on the SRS domain to allow SPF authentication.
======================== ===== ==== ===================
Name (Subdomain) TTL Type Value
======================== ===== ==== ===================
srs.example.com 10800 TXT ``v=spf1 mx -all``
======================== ===== ==== ===================
Then we deploy the DKIM record with the `p=<value>` taken from
``/var/dkim/srs.example.com.mail.txt``, that appears after deploying with SRS
enabled.
=============================== ===== ==== ========================================
Name (Subdomain) TTL Type Value
=============================== ===== ==== ========================================
mail._domainkey.srs.example.com 10800 TXT ``v=DKIM1; k=rsa; p=<really-long-key>``
=============================== ===== ==== ========================================
Finally we can tie this together in the DMARC record to require receivers to
verify the requested SPF/DKIM alignment.
.. note::
The SRS domain can only support relaxed SPF alignment due to the envelope
sender and ``FROM`` header mismatch.
======================== ===== ==== =========================================
Name (Subdomain) TTL Type Value
======================== ===== ==== =========================================
_dmarc.srs.example.com 10800 TXT ``v=DMARC1; p=reject; aspf=r; adkim=s;``
======================== ===== ==== =========================================
We can safely configure a ``reject`` policy on the SRS domain, to enforce the
SPF and DKIM alignment as configured above.

93
flake.lock generated
View File

@@ -19,11 +19,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1668681692,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"lastModified": 1761588595,
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
"type": "github"
},
"original": {
@@ -32,58 +32,73 @@
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1763319842,
"narHash": "sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "7275fa67fbbb75891c16d9dee7d88e58aea2d761",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1670751203,
"narHash": "sha256-XdoH1v3shKDGlrwjgrNX/EN8s3c+kQV7xY6cLCE8vcI=",
"lastModified": 1764020296,
"narHash": "sha256-6zddwDs2n+n01l+1TG6PlyokDdXzu/oBmEejcH5L5+A=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64e0bf055f9d25928c31fb12924e59ff8ce71e60",
"rev": "a320ce8e6e2cc6b4397eef214d202a50a4583829",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs-22_11": {
"locked": {
"lastModified": 1669558522,
"narHash": "sha256-yqxn+wOiPqe6cxzOo4leeJOp1bXE/fjPEi/3F/bBHv8=",
"owner": "NixOS",
"ref": "nixos-25.11-small",
"repo": "nixpkgs",
"rev": "ce5fe99df1f15a09a91a86be9738d68fadfbad82",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"blobs": "blobs",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"nixpkgs-22_11": "nixpkgs-22_11",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1605370193,
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs"
}
}
},

160
flake.nix
View File

@@ -3,54 +3,80 @@
inputs = {
flake-compat = {
# for shell.nix compat
url = "github:edolstra/flake-compat";
flake = false;
};
utils.url = "github:numtide/flake-utils";
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11";
git-hooks = {
url = "github:cachix/git-hooks.nix";
inputs.flake-compat.follows = "flake-compat";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
blobs = {
url = "gitlab:simple-nixos-mailserver/blobs";
flake = false;
};
};
outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, ... }: let
outputs =
{
self,
blobs,
git-hooks,
nixpkgs,
...
}:
let
lib = nixpkgs.lib;
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
releases = [
{
name = "unstable";
name = "nixos-25.11";
nixpkgs = nixpkgs;
pkgs = nixpkgs.legacyPackages.${system};
}
];
testNames = [
"internal"
"external"
"clamav"
"external"
"internal"
"ldap"
"multiple"
];
genTest = testName: release: {
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
"value"= import (./tests/. + "/${testName}.nix") {
genTest =
testName: release:
let
pkgs = release.pkgs;
inherit blobs;
nixos-lib = import (release.nixpkgs + "/nixos/lib") {
inherit (pkgs) lib;
};
in
{
name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}";
value = nixos-lib.runTest {
hostPkgs = pkgs;
imports = [ ./tests/${testName}.nix ];
_module.args = { inherit blobs; };
extraBaseModules.imports = [ ./default.nix ];
};
};
# Generate an attribute set such as
# {
# external-unstable = <derivation>;
# external-21_05 = <derivation>;
# ...
# }
allTests = lib.listToAttrs (
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames));
mailserverModule = import ./.;
# Generate a MarkDown file describing the options of the NixOS mailserver module
optionsDoc = let
optionsDoc =
let
eval = lib.evalModules {
modules = [
mailserverModule
@@ -58,35 +84,46 @@
_module.check = false;
mailserver = {
fqdn = "mx.example.com";
systemDomain = "example.com";
domains = [
"example.com"
];
dmarcReporting = {
organizationName = "Example Corp";
domain = "example.com";
};
};
}
];
};
options = builtins.toFile "options.json" (builtins.toJSON
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
(lib.optionAttrSetToDocList eval.options)));
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
options = builtins.toFile "options.json" (
builtins.toJSON (
lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") (
lib.optionAttrSetToDocList eval.options
)
)
);
in
pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } ''
echo "Generating options.md from ${options}"
python ${./scripts/generate-options.py} ${options} > $out
echo $out
'';
documentation = pkgs.stdenv.mkDerivation {
name = "documentation";
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
buildInputs = [(
pkgs.python3.withPackages (p: with p; [
src = lib.sourceByRegex ./docs [
"logo\\.png"
"conf\\.py"
"Makefile"
".*\\.rst"
];
buildInputs = [
(pkgs.python3.withPackages (
p: with p; [
sphinx
sphinx_rtd_theme
sphinx-rtd-theme
myst-parser
])
)];
linkify-it-py
]
))
];
buildPhase = ''
cp ${optionsDoc} options.md
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
@@ -98,7 +135,8 @@
'';
};
in {
in
{
nixosModules = rec {
mailserver = mailserverModule;
default = mailserver;
@@ -106,17 +144,71 @@
nixosModule = self.nixosModules.default; # compatibility
hydraJobs.${system} = allTests // {
inherit documentation;
inherit (self.checks.${system}) pre-commit;
};
checks.${system} = allTests // {
pre-commit = git-hooks.lib.${system}.run {
src = ./.;
hooks = {
# docs
markdownlint = {
enable = true;
settings.configuration = {
# Max line length, doesn't seem to correclty account for lines containing links
# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
MD013 = false;
};
};
rstcheck = {
enable = true;
package = pkgs.rstcheckWithSphinx;
entry = lib.getExe pkgs.rstcheckWithSphinx;
files = "\\.rst$";
};
# nix
deadnix.enable = true;
nixfmt-rfc-style.enable = true;
# python
pyright.enable = true;
ruff = {
enable = true;
args = [
"--extend-select"
"I"
];
};
ruff-format.enable = true;
# scripts
shellcheck.enable = true;
# sieve
check-sieve = {
enable = true;
package = pkgs.check-sieve;
entry = lib.getExe pkgs.check-sieve;
files = "\\.sieve$";
};
};
};
};
checks.${system} = allTests;
packages.${system} = {
inherit optionsDoc documentation;
};
devShells.${system}.default = pkgs.mkShell {
devShells.${system}.default = pkgs.mkShellNoCC {
inputsFrom = [ documentation ];
packages = with pkgs; [
clamav
];
packages =
with pkgs;
[
glab
]
++ self.checks.${system}.pre-commit.enabledPackages;
shellHook = self.checks.${system}.pre-commit.shellHook;
};
devShell.${system} = self.devShells.${system}.default; # compatibility
formatter.${system} = pkgs.nixfmt-tree;
};
}

View File

@@ -0,0 +1,85 @@
{
config,
lib,
...
}:
let
mailserverRelease = "25.11";
nixpkgsRelease = lib.trivial.release;
releaseMismatch =
config.mailserver.enableNixpkgsReleaseCheck && mailserverRelease != nixpkgsRelease;
in
{
warnings = lib.optional releaseMismatch ''
You are using
NixOS Mailserver version ${mailserverRelease} and
Nixpkgs version ${nixpkgsRelease}.
Using mismatched versions is likely to cause compatibility issues
and may require migrations that make an eventual rollback tricky.
It is therefore highly recommended to use a release of
NixOS mailserver that corresponds with your chosen release of Nixpkgs.
If you insist then you can disable this warning by adding
mailserver.enableNixpkgsReleaseCheck = false;
to your configuration.
'';
# We guard all assertions by requiring mailserver to be actually enabled
assertions = lib.optionals config.mailserver.enable (
[
{
assertion = config.mailserver.stateVersion != null;
message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at.";
}
]
++ lib.optionals config.mailserver.ldap.enable [
{
assertion = config.mailserver.loginAccounts == { };
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
}
{
assertion = config.mailserver.extraVirtualAliases == { };
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases";
}
]
++
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
[
{
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
message = ''
Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`.
Remediation:
- Stop the `dovecot2.service`
- Move `/var/vmail/ldap` below your `mailserver.mailDirectory`
- Increase the `stateVersion` to 2.
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information.
'';
}
]
++ [
{
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 3;
message = ''
Issue: The dovecot mail location for all users has changed and need to be migrated.
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration for the required remediation steps.
'';
}
]
++ lib.optionals (config.mailserver.certificateScheme != "acme") [
{
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
}
]
);
}

View File

@@ -14,28 +14,44 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.mailserver.borgbackup;
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
autoFragment =
if cfg.compression.auto && cfg.compression.method == null
then throw "compression.method must be set when using auto."
else lib.optional cfg.compression.auto "auto";
if cfg.compression.auto && cfg.compression.method == null then
throw "compression.method must be set when using auto."
else
lib.optional cfg.compression.auto "auto";
levelFragment =
if cfg.compression.level != null && cfg.compression.method == null
then throw "compression.method must be set when using compression.level."
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
if cfg.compression.level != null && cfg.compression.method == null then
throw "compression.method must be set when using compression.level."
else
lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
compressionFragment = lib.concatStringsSep "," (
lib.flatten [
autoFragment
methodFragment
levelFragment
]
);
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
encryptionFragment = cfg.encryption.method;
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
else throw "passphraseFile must be set when using encryption.");
passphraseFragment = lib.optionalString (cfg.encryption.method != "none") (
if cfg.encryption.passphraseFile != null then
''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
else
throw "passphraseFile must be set when using encryption."
);
locations = lib.escapeShellArgs cfg.locations;
name = lib.escapeShellArg cfg.name;
@@ -55,7 +71,8 @@ let
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
${cmdPostexec}
'';
in {
in
{
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
environment.systemPackages = with pkgs; [
borgbackup

View File

@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, options, ... }:
{ config, lib, ... }:
let
cfg = config.mailserver;

View File

@@ -14,35 +14,77 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.mailserver;
in
{
# cert :: PATH
certificatePath = if cfg.certificateScheme == "manual"
then cfg.certificateFile
else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
else throw "unknown certificate scheme";
certificatePath =
if cfg.certificateScheme == "manual" then
cfg.certificateFile
else if cfg.certificateScheme == "selfsigned" then
"${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
else
throw "unknown certificate scheme";
# key :: PATH
keyPath = if cfg.certificateScheme == "manual"
then cfg.keyFile
else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
else throw "unknown certificate scheme";
keyPath =
if cfg.certificateScheme == "manual" then
cfg.keyFile
else if cfg.certificateScheme == "selfsigned" then
"${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
else
throw "unknown certificate scheme";
passwordFiles = let
passwordFiles =
let
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
in
lib.mapAttrs (name: value:
lib.mapAttrs (
name: value:
if value.hashedPasswordFile == null then
builtins.toString (mkHashFile name value.hashedPassword)
else value.hashedPasswordFile) cfg.loginAccounts;
else
value.hashedPasswordFile
) cfg.loginAccounts;
# Appends the LDAP bind password to files to avoid writing this
# password into the Nix store.
appendLdapBindPwd =
{
name,
file,
prefix,
suffix ? "",
passwordFile,
destination,
}:
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
#!${pkgs.stdenv.shell}
set -euo pipefail
baseDir=$(dirname ${destination})
if (! test -d "$baseDir"); then
mkdir -p $baseDir
chmod 755 $baseDir
fi
cat ${file} > ${destination}
echo -n '${prefix}' >> ${destination}
cat ${passwordFile} | tr -d '\n' >> ${destination}
echo -n '${suffix}' >> ${destination}
chmod 600 ${destination}
'';
}

View File

@@ -1,4 +0,0 @@
{ config, lib, ... }:
{
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
}

View File

@@ -14,50 +14,88 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
with (import ./common.nix { inherit config pkgs lib; });
with (import ./common.nix {
inherit
config
options
pkgs
lib
;
});
let
cfg = config.mailserver;
passwdDir = "/run/dovecot2";
passdbFile = "${passwdDir}/passdb";
passwdFile = "${passwdDir}/passwd";
userdbFile = "${passwdDir}/userdb";
bool2int = x: if x then "1" else "0";
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
# maildir in format "/${domain}/${user}"
dovecotMaildir =
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n"
# This file contains the ldap bind password
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
boolToYesNo = x: if x then "yes" else "no";
listToLine = lib.concatStringsSep " ";
listToMultiAttrs =
keyPrefix: attrs:
lib.listToAttrs (
lib.imap1 (n: x: {
name = "${keyPrefix}${if n == 1 then "" else toString n}";
value = x;
}) attrs
);
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
# https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory
# Mail directory below the home directory
dovecotMaildir =
"maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
postfixCfg = config.services.postfix;
dovecot2Cfg = config.services.dovecot2;
stateDir = "/var/lib/dovecot";
pipeBin = pkgs.stdenv.mkDerivation {
name = "pipe_bin";
src = ./dovecot/pipe_bin;
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
buildCommand = ''
mkdir -p $out/pipe/bin
cp $src/* $out/pipe/bin/
chmod a+x $out/pipe/bin/*
patchShebangs $out/pipe/bin
for file in $out/pipe/bin/*; do
wrapProgram $file \
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
done
ldapConfig = pkgs.writeTextFile {
name = "dovecot-ldap.conf.ext.template";
text = ''
ldap_version = 3
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
${lib.optionalString cfg.ldap.startTls ''
tls = yes
''}
tls_require_cert = hard
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
dn = ${cfg.ldap.bind.dn}
sasl_bind = no
auth_bind = yes
base = ${cfg.ldap.searchBase}
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
user_attrs = ${cfg.ldap.dovecot.userAttrs}
''}
user_filter = ${cfg.ldap.dovecot.userFilter}
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
''}
pass_filter = ${cfg.ldap.dovecot.passFilter}
'';
};
setPwdInLdapConfFile = appendLdapBindPwd {
name = "ldap-conf-file";
file = ldapConfig;
prefix = ''dnpass = "'';
suffix = ''"'';
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapConfFile;
};
genPasswdScript = pkgs.writeScript "generate-password-file" ''
#!${pkgs.stdenv.shell}
@@ -68,40 +106,74 @@ let
chmod 755 "${passwdDir}"
fi
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
# Prevent world-readable password files, even temporarily.
umask 077
for f in ${
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)
}; do
if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!"
exit 1
fi
done
cat <<EOF > ${passdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts)}
cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts
)}
EOF
cat <<EOF > ${userdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: value:
"${name}:::::::"
+ (if lib.isString value.quota
then "userdb_quota_rule=*:storage=${value.quota}"
else "")
) cfg.loginAccounts)}
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
) cfg.loginAccounts
)}
EOF
chmod 600 ${passdbFile}
chmod 600 ${userdbFile}
'';
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
junkMailboxes = builtins.attrNames (
lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes
);
junkMailboxNumber = builtins.length junkMailboxes;
# The assertion garantees there is exactly one Junk mailbox.
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
mkLdapSearchScope =
scope:
(
if scope == "sub" then
"subtree"
else if scope == "one" then
"onelevel"
else
scope
);
ftsPluginSettings = {
fts = "flatcurve";
fts_languages = listToLine cfg.fullTextSearch.languages;
fts_tokenizers = listToLine [
"generic"
"email-address"
];
fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian
fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch;
fts_filters = listToLine cfg.fullTextSearch.filters;
fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes;
fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex;
fts_enforced = cfg.fullTextSearch.enforced;
}
// (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude);
in
{
config = with cfg; lib.mkIf enable {
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = junkMailboxNumber == 1;
@@ -109,24 +181,64 @@ in
}
];
warnings =
lib.optional
(
(builtins.length cfg.fullTextSearch.languages > 1)
&& (builtins.elem "stopwords" cfg.fullTextSearch.filters)
)
''
Using stopwords in `mailserver.fullTextSearch.filters` with multiple
languages in `mailserver.fullTextSearch.languages` configured WILL
cause some searches to fail.
The recommended solution is to NOT use the stopword filter when
multiple languages are present in the configuration.
'';
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
# the global config and tries to open shared libraries configured in there,
# which are usually not compatible.
environment.systemPackages = [
pkgs.dovecot_pigeonhole
]
++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
# For compatibility with python imaplib
environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
services.dovecot2 = {
enable = true;
enableImap = enableImap || enableImapSsl;
enablePop3 = enablePop3 || enablePop3Ssl;
enableImap = cfg.enableImap || cfg.enableImapSsl;
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
enablePAM = false;
enableQuota = true;
mailGroup = vmailGroupName;
mailUser = vmailUserName;
mailGroup = cfg.vmailGroupName;
mailUser = cfg.vmailUserName;
mailLocation = dovecotMaildir;
sslServerCert = certificatePath;
sslServerKey = keyPath;
enableDHE = lib.mkDefault false;
enableLmtp = true;
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
"fts"
"fts_flatcurve"
];
protocols = lib.optional cfg.enableManageSieve "sieve";
sieveScripts = {
after = builtins.toFile "spam.sieve" ''
pluginSettings = {
sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve";
sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve";
sieve_default_name = "default";
}
// (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
sieve = {
extensions = [
"fileinto"
];
scripts.after = builtins.toFile "spam.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
@@ -134,13 +246,35 @@ in
stop;
}
'';
pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
(pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
};
imapsieve.mailbox = [
{
name = junkMailboxName;
causes = [
"COPY"
"APPEND"
];
before = ./dovecot/imap_sieve/report-spam.sieve;
}
{
name = "*";
from = junkMailboxName;
causes = [ "COPY" ];
before = ./dovecot/imap_sieve/report-ham.sieve;
}
];
mailboxes = cfg.mailboxes;
extraConfig = ''
#Extra Config
${lib.optionalString debug ''
${lib.optionalString cfg.debug.dovecot ''
mail_debug = yes
auth_debug = yes
verbose_ssl = yes
@@ -149,42 +283,62 @@ in
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
service imap-login {
inet_listener imap {
${if cfg.enableImap then ''
${
if cfg.enableImap then
''
port = 143
'' else ''
''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
''
}
}
inet_listener imaps {
${if cfg.enableImapSsl then ''
${
if cfg.enableImapSsl then
''
port = 993
ssl = yes
'' else ''
''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
''
}
}
}
''}
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
service pop3-login {
inet_listener pop3 {
${if cfg.enablePop3 then ''
${
if cfg.enablePop3 then
''
port = 110
'' else ''
''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
''
}
}
inet_listener pop3s {
${if cfg.enablePop3Ssl then ''
${
if cfg.enablePop3Ssl then
''
port = 995
ssl = yes
'' else ''
''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
''
}
}
}
''}
@@ -194,14 +348,21 @@ in
mail_plugins = $mail_plugins imap_sieve
}
service imap {
vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB
}
protocol pop3 {
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
}
mail_access_groups = ${vmailGroupName}
mail_access_groups = ${cfg.vmailGroupName}
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_min_protocol = TLSv1
ssl_prefer_server_ciphers = no
ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1
service lmtp {
unix_listener dovecot-lmtp {
@@ -209,6 +370,17 @@ in
mode = 0600
user = ${postfixCfg.user}
}
vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB
}
service quota-status {
inet_listener {
port = 0
}
unix_listener quota-status {
user = postfix
}
vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB
}
recipient_delimiter = ${cfg.recipientDelimiter}
@@ -220,15 +392,38 @@ in
passdb {
driver = passwd-file
args = ${passdbFile}
args = ${passwdFile}
}
userdb {
driver = passwd-file
args = ${userdbFile}
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
default_fields = \
home=${cfg.mailDirectory}/%{domain}/%{username} \
uid=${builtins.toString cfg.vmailUID} \
gid=${builtins.toString cfg.vmailUID}
}
${lib.optionalString cfg.ldap.enable ''
passdb {
driver = ldap
args = ${ldapConfFile}
}
userdb {
driver = ldap
args = ${ldapConfFile}
default_fields = \
home=${cfg.mailDirectory}/ldap/%{user} \
uid=${toString cfg.vmailUID} \
gid=${toString cfg.vmailUID} \
mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}"
}
}
''}
service auth {
unix_listener auth {
mode = 0660
@@ -244,90 +439,27 @@ in
inbox = yes
}
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
sieve_default_name = default
# From elsewhere to Spam folder
imapsieve_mailbox1_name = ${junkMailboxName}
imapsieve_mailbox1_causes = COPY,APPEND
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = ${junkMailboxName}
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
}
${lib.optionalString cfg.fullTextSearch.enable ''
plugin {
plugin = fts fts_xapian
fts = xapian
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
fts_enforced = ${cfg.fullTextSearch.enforced}
}
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
service indexer-worker {
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
''}
}
''}
''}
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
'';
};
systemd.services.dovecot2 = {
systemd.services.dovecot = {
preStart = ''
${genPasswdScript}
rm -rf '${stateDir}/imap_sieve'
mkdir '${stateDir}/imap_sieve'
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
for k in "${stateDir}/imap_sieve"/*.sieve ; do
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
done
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
'';
''
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
};
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
description = "Optimize dovecot indices for fts_xapian";
requisite = [ "dovecot2.service" ];
after = [ "dovecot2.service" ];
startAt = cfg.fullTextSearch.maintenance.onCalendar;
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
PrivateDevices = true;
PrivateNetwork = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectSystem = true;
PrivateTmp = true;
};
};
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
timerConfig = {
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
};
};
systemd.services.postfix.restartTriggers = [
genPasswdScript
]
++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
};
}

View File

@@ -12,4 +12,4 @@ if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
pipe :copy "rspamd-learn-ham.sh" [ "${username}" ];

View File

@@ -4,4 +4,4 @@ if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
pipe :copy "rspamd-learn-spam.sh" [ "${username}" ];

View File

@@ -1,3 +0,0 @@
#!/bin/bash
set -o errexit
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham

View File

@@ -1,3 +0,0 @@
#!/bin/bash
set -o errexit
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam

View File

@@ -14,15 +14,26 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.mailserver;
in
{
config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
config = lib.mkIf cfg.enable {
environment.systemPackages =
with pkgs;
[
dovecot
openssh
postfix
rspamd
]
++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]);
};
}

View File

@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{ config, lib, ... }:
let
cfg = config.mailserver;
@@ -24,4 +24,3 @@ in
services.kresd.enable = true;
};
}

View File

@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{ config, lib, ... }:
let
cfg = config.mailserver;

View File

@@ -20,18 +20,20 @@ let
cfg = config.mailserver;
in
{
config = with cfg; lib.mkIf (enable && openFirewall) {
config = lib.mkIf (cfg.enable && cfg.openFirewall) {
networking.firewall = {
allowedTCPPorts = [ 25 ]
++ lib.optional enableSubmission 587
++ lib.optional enableSubmissionSsl 465
++ lib.optional enableImap 143
++ lib.optional enableImapSsl 993
++ lib.optional enablePop3 110
++ lib.optional enablePop3Ssl 995
++ lib.optional enableManageSieve 4190
++ lib.optional (certificateScheme == "acme-nginx") 80;
allowedTCPPorts = [
25
]
++ lib.optional cfg.enableSubmission 587
++ lib.optional cfg.enableSubmissionSsl 465
++ lib.optional cfg.enableImap 143
++ lib.optional cfg.enableImapSsl 993
++ lib.optional cfg.enablePop3 110
++ lib.optional cfg.enablePop3Ssl 995
++ lib.optional cfg.enableManageSieve 4190
++ lib.optional (cfg.certificateScheme == "acme-nginx") 80;
};
};
}

View File

@@ -14,17 +14,30 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{
config,
options,
pkgs,
lib,
...
}:
{ config, pkgs, lib, ... }:
with (import ./common.nix { inherit config; });
with (import ./common.nix {
inherit
config
options
lib
pkgs
;
});
let
cfg = config.mailserver;
acmeRoot = "/var/lib/acme/acme-challenge";
in
{
config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
config =
lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"))
{
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
enable = true;
virtualHosts."${cfg.fqdn}" = {
@@ -32,13 +45,15 @@ in
serverAliases = cfg.certificateDomains;
forceSSL = true;
enableACME = true;
acmeRoot = acmeRoot;
};
};
security.acme.certs."${cfg.fqdn}".reloadServices = [
security.acme.certs."${cfg.acmeCertificateName}" = {
extraDomainNames = lib.mkIf (cfg.certificateScheme == "acme") cfg.certificateDomains;
reloadServices = [
"postfix.service"
"dovecot2.service"
"dovecot.service"
];
};
};
}

View File

@@ -1,89 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2017 Brian Olsen
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.mailserver;
dkimUser = config.services.opendkim.user;
dkimGroup = config.services.opendkim.group;
createDomainDkimCert = dom:
let
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
in
''
if [ ! -f "${dkim_key}" ]
then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \
--bits="${toString cfg.dkimKeyBits}" \
--directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
chmod 644 "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
keyTable = pkgs.writeText "opendkim-KeyTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
signingTable = pkgs.writeText "opendkim-SigningTable"
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
dkim = config.services.opendkim;
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
in
{
config = mkIf (cfg.dkimSigning && cfg.enable) {
services.opendkim = {
enable = true;
selector = cfg.dkimSelector;
keyPath = cfg.dkimKeyDirectory;
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
UMask 0002
Socket ${dkim.socket}
KeyTable file:${keyTable}
SigningTable file:${signingTable}
'' + (lib.optionalString cfg.debug ''
Syslog yes
SyslogSuccess yes
LogWhy yes
''));
};
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
postfix.extraGroups = [ "${dkimGroup}" ];
};
systemd.services.opendkim = {
preStart = lib.mkForce createAllCerts;
serviceConfig = {
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
PermissionsStartOnly = lib.mkForce false;
};
};
systemd.tmpfiles.rules = [
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
];
};
}

View File

@@ -1,46 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.mailserver;
in
{
config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) {
systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" ''
#!${pkgs.stdenv.shell}
# Checks whether the "current" kernel is different from the booted kernel
# and then triggers a reboot so that the "current" kernel will be the booted one.
# This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos.
current=$(readlink -f /run/current-system/kernel)
booted=$(readlink -f /run/booted-system/kernel)
if [ "$current" == "$booted" ]; then
echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check";
else
echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check"
echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
${cfg.rebootAfterKernelUpgrade.method}
fi
'';
};
}

View File

@@ -14,40 +14,84 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
with (import ./common.nix { inherit config pkgs lib; });
with (import ./common.nix {
inherit
config
options
lib
pkgs
;
});
let
inherit (lib.strings) concatStringsSep;
cfg = config.mailserver;
iniFormat = pkgs.formats.iniWithGlobalSection { };
# Merge several lookup tables. A lookup table is a attribute set where
# - the key is an address (user@example.com) or a domain (@example.com)
# - the value is a list of addresses
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
# valiases_postfix :: Map String [String]
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value:
let to = name;
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
cfg.loginAccounts));
valiases_postfix = mergeLookupTables (
lib.flatten (
lib.mapAttrsToList (
name: value:
let
to = name;
in
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
) cfg.loginAccounts
)
);
regex_valiases_postfix = mergeLookupTables (
lib.flatten (
lib.mapAttrsToList (
name: value:
let
to = name;
in
map (from: { "${from}" = to; }) value.aliasesRegexp
) cfg.loginAccounts
)
);
# catchAllPostfix :: Map String [String]
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value:
let to = name;
in map (from: {"@${from}" = to;}) value.catchAll)
cfg.loginAccounts));
catchAllPostfix = mergeLookupTables (
lib.flatten (
lib.mapAttrsToList (
name: value:
let
to = name;
in
map (from: { "@${from}" = to; }) value.catchAll
) cfg.loginAccounts
)
);
# all_valiases_postfix :: Map String [String]
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
all_valiases_postfix = mergeLookupTables [
valiases_postfix
extra_valiases_postfix
];
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
attrsToLookupTable = aliases: let
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
in mergeLookupTables lookupTables;
attrsToLookupTable =
aliases:
let
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
in
mergeLookupTables lookupTables;
# extra_valiases_postfix :: Map String [String]
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
@@ -56,33 +100,49 @@ let
forwards = attrsToLookupTable cfg.forwards;
# lookupTableToString :: Map String [String] -> String
lookupTableToString = attrs: let
lookupTableToString =
attrs:
let
valueToString = value: lib.concatStringsSep ", " value;
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
in
lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs
);
# valiases_file :: Path
valiases_file = let
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
in builtins.toFile "valias" content;
valiases_file =
let
content = lookupTableToString (mergeLookupTables [
all_valiases_postfix
catchAllPostfix
]);
in
builtins.toFile "valias" content;
regex_valiases_file =
let
content = lookupTableToString regex_valiases_postfix;
in
builtins.toFile "regex_valias" content;
# denied_recipients_postfix :: [ String ]
denied_recipients_postfix = (map
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
);
denied_recipients_file = builtins.toFile "denied_recipients" (
lib.concatStringsSep "\n" denied_recipients_postfix
);
reject_senders_postfix = (map
(sender:
"${sender} REJECT")
(cfg.rejectSender));
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
reject_senders_postfix = map (sender: "${sender} REJECT") cfg.rejectSender;
reject_senders_file = builtins.toFile "reject_senders" (
lib.concatStringsSep "\n" reject_senders_postfix
);
reject_recipients_postfix = (map
(recipient:
"${recipient} REJECT")
(cfg.rejectRecipients));
reject_recipients_postfix = map (recipient: "${recipient} REJECT") cfg.rejectRecipients;
# rejectRecipients :: [ Path ]
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
reject_recipients_file = builtins.toFile "reject_recipients" (
lib.concatStringsSep "\n" reject_recipients_postfix
);
# vhosts_file :: Path
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
@@ -94,8 +154,12 @@ let
# every alias is owned (uniquely) by its user.
# The user's own address is already in all_valiases_postfix.
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (
lookupTableToString regex_valiases_postfix
);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (
''
# Removes sensitive headers from mails handed in via the submission port.
# See https://thomas-leister.de/mailserver-debian-stretch/
# Uses "pcre" style regex.
@@ -105,27 +169,22 @@ let
/^X-Mailer:/ IGNORE
/^User-Agent:/ IGNORE
/^X-Enigmail:/ IGNORE
'' + lib.optionalString cfg.rewriteMessageId ''
''
+ lib.optionalString cfg.rewriteMessageId ''
# Replaces the user submitted hostname with the server's FQDN to hide the
# user's host or network.
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
'');
''
);
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
unixSocket = sock: "unix:${sock}";
smtpdMilters =
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
submissionOptions =
{
submissionOptions = {
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
@@ -133,45 +192,150 @@ let
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${
lib.optionalString (regex_valiases_postfix != { }) ",pcre:/etc/postfix/regex_vaccounts"
}";
smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup";
};
commonLdapConfig = ''
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
version = 3
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
tls_require_cert = yes
search_base = ${cfg.ldap.searchBase}
scope = ${cfg.ldap.searchScope}
bind = yes
bind_dn = ${cfg.ldap.bind.dn}
'';
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.mailAttribute}
'';
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
appendPwdInSenderLoginMap = appendLdapBindPwd {
name = "ldap-sender-login-map";
file = ldapSenderLoginMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapSenderLoginMapFile;
};
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.uidAttribute}
'';
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
name = "ldap-virtual-mailbox-map";
file = ldapVirtualMailboxMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapVirtualMailboxMapFile;
};
in
{
config = with cfg; lib.mkIf enable {
config = lib.mkIf cfg.enable {
# SMTP TLS error reporting (RFC 8460)
services.tlsrpt = {
inherit (cfg.tlsrpt) enable;
configurePostfix = true;
reportd.settings = {
organization_name = cfg.systemName;
contact_info = "${cfg.systemContact}";
sender_address = "noreply-tlsrpt@${cfg.systemDomain}";
};
};
# SMTP client policy mapping for DANE (RFC 6698) and MTA-STS (RFC 8461)
services.postfix-tlspol = {
enable = true;
configurePostfix = true;
};
# Sender Rewriting Scheme (https://www.libsrs2.net/srs/srs.pdf)
services.postsrsd = {
inherit (cfg.srs) enable;
configurePostfix = true;
settings = {
domains = lib.unique (
[
cfg.fqdn
cfg.sendingFqdn
cfg.systemDomain
]
++ cfg.domains
);
separator = "=";
srs-domain = cfg.srs.domain;
};
};
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = ''
${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap}
'';
restartTriggers = [
appendPwdInVirtualMailboxMap
appendPwdInSenderLoginMap
];
};
services.postfix = {
enable = true;
hostname = "${sendingFqdn}";
networksStyle = "host";
mapFiles."valias" = valiases_file;
mapFiles."regex_valias" = regex_valiases_file;
mapFiles."vaccounts" = vaccounts_file;
mapFiles."regex_vaccounts" = regex_vaccounts_file;
mapFiles."denied_recipients" = denied_recipients_file;
mapFiles."reject_senders" = reject_senders_file;
mapFiles."reject_recipients" = reject_recipients_file;
sslCert = certificatePath;
sslKey = keyPath;
enableSubmission = cfg.enableSubmission;
enableSubmissions = cfg.enableSubmissionSsl;
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
virtual = lookupTableToString (mergeLookupTables [
all_valiases_postfix
catchAllPostfix
forwards
]);
config = {
# Extra Config
mydestination = "";
settings.main = {
myhostname = cfg.sendingFqdn;
mydestination = ""; # disable local mail delivery
recipient_delimiter = cfg.recipientDelimiter;
smtpd_banner = "${fqdn} ESMTP NO UCE";
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
disable_vrfy_command = true;
message_size_limit = toString cfg.messageSizeLimit;
message_size_limit = cfg.messageSizeLimit;
# virtual mail system
virtual_uid_maps = "static:5000";
virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory;
virtual_mailbox_base = cfg.mailDirectory;
virtual_mailbox_domains = vhosts_file;
virtual_mailbox_maps = mappedFile "valias";
virtual_mailbox_maps = [
(mappedFile "valias")
]
++ lib.optionals cfg.ldap.enable [
"ldap:${ldapVirtualMailboxMapFile}"
]
++ lib.optionals (regex_valiases_postfix != { }) [
(mappedRegexFile "regex_valias")
];
virtual_alias_maps = lib.mkAfter (
lib.optionals (regex_valiases_postfix != { }) [
(mappedRegexFile "regex_valias")
]
);
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
@@ -180,88 +344,125 @@ in
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_auth_enable = true;
smtpd_relay_restrictions = [
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
"permit_mynetworks"
"permit_sasl_authenticated"
"reject_unauth_destination"
];
policy-spf_time_limit = "3600s";
# reject selected senders
smtpd_sender_restrictions = [
"check_sender_access ${mappedFile "reject_senders"}"
];
# quota and spf checking
smtpd_recipient_restrictions = [
# reject selected recipients
"check_recipient_access ${mappedFile "denied_recipients"}"
"check_recipient_access ${mappedFile "reject_recipients"}"
"check_policy_service inet:localhost:12340"
"check_policy_service unix:private/policy-spf"
# quota checking
"check_policy_service unix:/run/dovecot2/quota-status"
];
# TLS settings, inspired by https://github.com/jeaye/nix-files
# Submission by mail clients is handled in submissionOptions
# The X509 private key followed by the corresponding certificate
smtpd_tls_chain_files = [
"${keyPath}"
"${certificatePath}"
];
# TLS for incoming mail is optional
smtpd_tls_security_level = "may";
# strong might suffice and is computationally less expensive
smtpd_tls_eecdh_grade = "ultra";
# But required for authentication attempts
smtpd_tls_auth_only = true;
# Disable obselete protocols
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
# TLS versions supported for the SMTP server
smtpd_tls_protocols = ">=TLSv1";
smtpd_tls_mandatory_protocols = ">=TLSv1";
smtp_tls_ciphers = "high";
# Require ciphersuites that OpenSSL classifies as "High"
smtpd_tls_ciphers = "high";
smtp_tls_mandatory_ciphers = "high";
smtpd_tls_mandatory_ciphers = "high";
# Disable deprecated ciphers
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
# Exclude cipher suites with undesirable properties
smtpd_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
smtpd_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL";
tls_preempt_cipherlist = true;
# Enable DNSSEC/DANE support for outgoing SMTP connections
# https://www.postfix.org/postconf.5.html#smtp_tls_security_level
smtp_dns_support_level = "dnssec";
smtp_tls_security_level = "dane";
# TLS versions supported for the SMTP client
smtp_tls_protocols = ">=TLSv1.2";
smtp_tls_mandatory_protocols = ">=TLSv1.2";
# Require ciphersuites that OpenSSL classifies as "High"
smtp_tls_ciphers = "high";
smtp_tls_mandatory_ciphers = "high";
# Exclude ciphersuites with undesirable properties
smtp_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
smtp_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL";
# Restrict and prioritize the following curves in the given order
# Excludes curves that have no widespread support, so we don't bloat the handshake needlessly.
# https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves
tls_config_file =
let
mkGroupString = groups: concatStringsSep " / " (map (concatStringsSep ":") groups);
in
iniFormat.generate "postfix-openssl.cnf" {
globalSection.postfix = "postfix_settings";
sections = {
postfix_settings.ssl_conf = "postfix_ssl_settings";
postfix_ssl_settings.system_default = "baseline_postfix_settings";
baseline_postfix_settings.Groups = mkGroupString [
[ "*X25519MLKEM768" ]
[ "*X25519" ]
[
"P-256"
"P-384"
]
];
};
};
tls_config_name = "postfix";
# Algorithm selection happens through `tls_config_file` instead.
tls_eecdh_auto_curves = [ ];
tls_ffdhe_auto_groups = [ ];
# As long as all cipher suites are considered safe, let the client use its preferred cipher
tls_preempt_cipherlist = false;
# Allowing AUTH on a non encrypted connection poses a security risk
smtpd_tls_auth_only = true;
# Log only a summary message on TLS handshake completion
smtp_tls_loglevel = "1";
smtpd_tls_loglevel = "1";
# Configure a non blocking source of randomness
tls_random_source = "dev:/dev/urandom";
smtpd_milters = smtpdMilters;
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"];
non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
milter_protocol = "6";
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
};
submissionOptions = submissionOptions;
submissionsOptions = submissionOptions;
masterConfig = {
settings.master = {
"lmtp" = {
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
# D => Delivered-To, O => X-Original-To, R => Return-Path
args = [ "flags=O" ];
};
"policy-spf" = {
type = "unix";
privileged = true;
chroot = false;
command = "spawn";
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
};
"submission-header-cleanup" = {
type = "unix";
private = false;
chroot = false;
maxproc = 0;
command = "cleanup";
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
args = [
"-o"
"header_checks=pcre:${submissionHeaderCleanupRules}"
];
};
};
};

View File

@@ -14,11 +14,19 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
with lib;
{
config,
pkgs,
lib,
...
}:
let
inherit (lib)
optionalString
mkIf
;
cfg = config.mailserver;
preexecDefined = cfg.backup.cmdPreexec != null;
@@ -38,7 +46,8 @@ let
${cfg.backup.cmdPostexec}
'';
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
in {
in
{
config = mkIf (cfg.enable && cfg.backup.enable) {
services.rsnapshot = {
enable = true;

View File

@@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
let
cfg = config.mailserver;
@@ -22,27 +27,77 @@ let
postfixCfg = config.services.postfix;
rspamdCfg = config.services.rspamd;
rspamdSocket = "rspamd.service";
rspamdUser = config.services.rspamd.user;
rspamdGroup = config.services.rspamd.group;
createDkimKeypair =
domain:
let
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
in
pkgs.writeShellScript "dkim-keygen-${domain}" ''
if [ ! -f "${privateKey}" ]
then
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
--domain "${domain}" \
--selector "${cfg.dkimSelector}" \
--type "${cfg.dkimKeyType}" \
--bits ${toString cfg.dkimKeyBits} \
--privkey "${privateKey}" > "${publicKey}"
chmod 0644 "${publicKey}"
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
fi
'';
dkimDomains = lib.unique (cfg.domains ++ (lib.optionals cfg.srs.enable [ cfg.srs.domain ]));
in
{
config = with cfg; lib.mkIf enable {
config = lib.mkIf cfg.enable {
environment.systemPackages = lib.mkBefore [
(pkgs.runCommand "rspamc-wrapped"
{
nativeBuildInputs = with pkgs; [ makeWrapper ];
}
''
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
--add-flags "-h /run/rspamd/worker-controller.sock"
''
)
];
services.rspamd = {
enable = true;
inherit debug;
debug = cfg.debug.rspamd;
locals = {
"milter_headers.conf" = { text = ''
extended_spam_headers = yes;
''; };
"redis.conf" = { text = ''
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
'' + (lib.optionalString (cfg.redis.password != null) ''
"milter_headers.conf" = {
text = ''
extended_spam_headers = true;
'';
};
"redis.conf" = {
text = ''
servers = "${
if cfg.redis.port == null then
cfg.redis.address
else
"${cfg.redis.address}:${toString cfg.redis.port}"
}";
''
+ (lib.optionalString (cfg.redis.password != null) ''
password = "${cfg.redis.password}";
''); };
"classifier-bayes.conf" = { text = ''
'');
};
"classifier-bayes.conf" = {
text = ''
cache {
backend = "redis";
}
''; };
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
'';
};
"antivirus.conf" = lib.mkIf cfg.virusScanning {
text = ''
clamav {
action = "reject";
symbol = "CLAM_VIRUS";
@@ -51,38 +106,52 @@ in
servers = "/run/clamav/clamd.ctl";
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
}
''; };
"dkim_signing.conf" = { text = ''
# Disable outbound email signing, we use opendkim for this
enabled = false;
''; };
"dmarc.conf" = { text = ''
'';
};
"dkim_signing.conf" = {
text = ''
enabled = ${lib.boolToString cfg.dkimSigning};
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
selector = "${cfg.dkimSelector}";
# Allow for usernames w/o domain part
allow_username_mismatch = true;
# Don't normalize DKIM key selection for subdomains
use_esld = false;
'';
};
"dmarc.conf" = {
text = ''
${lib.optionalString cfg.dmarcReporting.enable ''
reporting {
enabled = true;
email = "${cfg.dmarcReporting.email}";
domain = "${cfg.dmarcReporting.domain}";
org_name = "${cfg.dmarcReporting.organizationName}";
from_name = "${cfg.dmarcReporting.fromName}";
msgid_from = "dmarc-rua";
email = "noreply-dmarc@${cfg.systemDomain}";
domain = "${cfg.systemDomain}";
org_name = "${cfg.systemName}";
from_name = "${cfg.systemName}";
msgid_from = "${cfg.systemDomain}";
${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
''}
}''}
''; };
'';
};
};
overrides = {
"milter_headers.conf" = {
"options.inc" = {
text = ''
extended_spam_headers = true;
local_addrs = [::1/128, 127.0.0.0/8]
'';
};
};
workers.rspamd_proxy = {
type = "rspamd_proxy";
bindSockets = [{
bindSockets = [
{
socket = "/run/rspamd/rspamd-milter.sock";
mode = "0664";
}];
}
];
count = 1; # Do not spawn too many processes of this type
extraConfig = ''
milter = yes; # Enable milter mode
@@ -97,11 +166,13 @@ in
workers.controller = {
type = "controller";
count = 1;
bindSockets = [{
bindSockets = [
{
socket = "/run/rspamd/worker-controller.sock";
mode = "0666";
}];
includes = [];
}
];
includes = [ ];
extraConfig = ''
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
'';
@@ -109,28 +180,51 @@ in
};
services.redis.servers.rspamd = {
enable = lib.mkDefault true;
port = lib.mkDefault 6380;
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
systemd.tmpfiles.settings."10-rspamd.conf" = {
"${cfg.dkimKeyDirectory}" = {
d = {
# Create /var/dkim owned by rspamd user/group
user = rspamdUser;
group = rspamdGroup;
};
Z = {
# Recursively adjust permissions in /var/dkim
user = rspamdUser;
group = rspamdGroup;
};
};
};
systemd.services.rspamd = {
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
serviceConfig = lib.mkMerge [
{
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
}
(lib.optionalAttrs cfg.dkimSigning {
ExecStartPre = map createDkimKeypair dkimDomains;
ReadWritePaths = [ cfg.dkimKeyDirectory ];
})
];
};
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
# Explicitly select yesterday's date to work around broken
# default behaviour when called without a date.
# https://github.com/rspamd/rspamd/issues/4062
script = ''
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
'';
script = toString [
(lib.getExe' pkgs.rspamd "rspamadm")
"dmarc_report"
"$(date -d 'yesterday' '+%Y%m%d')"
];
serviceConfig = {
User = "${config.services.rspamd.user}";
Group = "${config.services.rspamd.group}";
AmbientCapabilities = [];
AmbientCapabilities = [ ];
CapabilityBoundingSet = "";
DevicePolicy = "closed";
IPAddressAllow = "localhost";
@@ -151,10 +245,17 @@ in
ProcSubset = "pid";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SupplementaryGroups = lib.optionals cfg.redis.configureLocally [
config.services.redis.servers.rspamd.group
];
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
@@ -164,7 +265,7 @@ in
};
};
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
description = "Daily delivery of aggregated DMARC reports";
wantedBy = [
"timers.target"
@@ -185,4 +286,3 @@ in
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
};
}

View File

@@ -14,22 +14,40 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
with (import ./common.nix {
inherit
config
options
lib
pkgs
;
});
let
cfg = config.mailserver;
certificatesDeps =
if cfg.certificateScheme == "manual" then
[]
[ ]
else if cfg.certificateScheme == "selfsigned" then
[ "mailserver-selfsigned-certificate.service" ]
else
[ "acme-finished-${cfg.fqdn}.target" ];
in
{
config = with cfg; lib.mkIf enable {
config = lib.mkIf cfg.enable {
# Create self signed certificate
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") {
systemd.services.mailserver-selfsigned-certificate =
lib.mkIf (cfg.certificateScheme == "selfsigned")
{
after = [ "local-fs.target" ];
script = ''
# Create certificates if they do not exist yet
@@ -53,19 +71,22 @@ in
};
# Create maildir folder before dovecot startup
systemd.services.dovecot2 = {
systemd.services.dovecot = {
wants = certificatesDeps;
after = certificatesDeps;
preStart = let
preStart =
let
directories = lib.strings.escapeShellArgs (
[ mailDirectory ]
++ lib.optional (cfg.indexDir != null) cfg.indexDir
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
);
in ''
in
''
# Create mail directory and set permissions. See
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
# <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>.
# Prevent world-readable paths, even temporarily.
umask 007
mkdir -p ${directories}
chgrp "${vmailGroupName}" ${directories}
chgrp "${cfg.vmailGroupName}" ${directories}
chmod 02770 ${directories}
'';
};
@@ -73,11 +94,12 @@ in
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = {
wants = certificatesDeps;
after = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service"
after = [
"dovecot.service"
]
++ lib.optional cfg.dkimSigning "rspamd.service"
++ certificatesDeps;
requires = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service";
requires = [ "dovecot.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
};
};
}

View File

@@ -14,7 +14,22 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
options,
pkgs,
lib,
...
}:
with (import ./common.nix {
inherit
config
options
lib
pkgs
;
});
with config.mailserver;
@@ -28,12 +43,14 @@ let
group = vmailGroupName;
};
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
#!${pkgs.stdenv.shell}
set -euo pipefail
# Prevent world-readable paths, even temporarily.
umask 007
# Create directory to store user sieve scripts if it doesn't exist
if (! test -d "${sieveDirectory}"); then
mkdir "${sieveDirectory}"
@@ -43,8 +60,10 @@ let
# Copy user's sieve script to the correct location (if it exists). If it
# is null, remove the file.
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
if lib.isString sieveScript then ''
${lib.concatMapStringsSep "\n" (
{ name, sieveScript }:
if lib.isString sieveScript then
''
if (! test -d "${sieveDirectory}/${name}"); then
mkdir -p "${sieveDirectory}/${name}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
@@ -54,34 +73,41 @@ let
${sieveScript}
EOF
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
'' else ''
''
else
''
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
rm "${sieveDirectory}/${name}/default.sieve"
fi
if (test -f "${sieveDirectory}/${name}.svbin"); then
rm "${sieveDirectory}/${name}/default.svbin"
fi
'') (map (user: { inherit (user) name sieveScript; })
(lib.attrValues loginAccounts))}
''
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))}
'';
in {
in
{
config = lib.mkIf enable {
# assert that all accounts provide a password
assertions = (map (acct: {
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
assertions = map (acct: {
assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
message = "${acct.name} must provide either a hashed password or a password hash file";
}) (lib.attrValues loginAccounts));
}) (lib.attrValues loginAccounts);
# warn for accounts that specify both password and file
warnings = (map
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
(lib.filter
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
(lib.attrValues loginAccounts)));
warnings =
map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
(
lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
lib.attrValues loginAccounts
)
);
# set the vmail gid to a specific value
users.groups = {
"${vmailGroupName}" = { gid = vmailUID; };
"${vmailGroupName}" = {
gid = vmailUID;
};
};
# define all users
@@ -91,7 +117,7 @@ in {
systemd.services.activate-virtual-mail-users = {
wantedBy = [ "multi-user.target" ];
before = [ "dovecot2.service" ];
before = [ "dovecot.service" ];
serviceConfig = {
ExecStart = virtualMailUsersActivationScript;
};

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p python3
import argparse
import os
import shutil
import sys
from enum import Enum
from pathlib import Path
from pwd import getpwnam
class FolderLayout(Enum):
Default = 1
Folder = 2
def check_user(vmail_root: Path):
owner = vmail_root.owner()
owner_uid = getpwnam(owner).pw_uid
if os.geteuid() == owner_uid:
return
try:
print(
f"Trying to switch effective user id to {owner_uid} ({owner})",
file=sys.stderr,
)
os.seteuid(owner_uid)
return
except PermissionError:
print(
f"Failed switching to virtual mail user. Please run this script under it, for example by using `sudo -u {owner}`)",
file=sys.stderr,
)
sys.exit(1)
def is_maildir_related(path: Path, layout: FolderLayout) -> bool:
if path.name in [
"subscriptions",
# https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-uid-mapping
"dovecot-uidlist",
# https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-keywords
"dovecot-keywords",
]:
return True
if not path.is_dir():
return False
if path.name in ["cur", "new", "tmp"]:
return True
if layout is FolderLayout.Default and path.name.startswith("."):
return True
if layout is FolderLayout.Folder:
if path.name in ["mail"]:
return False
return True
return False
def mkdir(dst: Path, dry_run: bool = True):
print(f'mkdir "{dst}"')
if not dry_run:
# u+rwx, setgid
dst.mkdir(mode=0o2700)
def move(src: Path, dst: Path, dry_run: bool = True):
print(f'mv "{src}" "{dst}"')
if not dry_run:
src.rename(dst)
def delete(dst: Path, dry_run: bool = True):
if not dst.exists():
return
if dst.is_dir():
print(f'rm --recursive "{dst}"')
if not dry_run:
shutil.rmtree(dst)
else:
print(f'rm "{dst}"')
if not dry_run:
dst.unlink()
def main(vmail_root: Path, layout: FolderLayout, dry_run: bool = True):
maildirs = {path.parent for path in vmail_root.glob("*/*/cur")}
maybe_delete = []
# The old maildir will be the new home directory
for homedir in maildirs:
maildir = homedir / "mail"
mkdir(maildir, dry_run)
for path in homedir.iterdir():
if is_maildir_related(path, layout):
move(path, maildir / path.name, dry_run)
else:
maybe_delete.append(path)
# Files that are part of the previous home directory, but now obsolete
for path in [
vmail_root / ".dovecot.lda-dupes",
vmail_root / ".dovecot.lda-dupes.locks",
]:
delete(path, dry_run)
# The remaining files are likely obsolete, but should still be checked with care
for path in maybe_delete:
print(f"# rm {str(path)}")
if dry_run:
print("\nNo changes were made.")
print("Run the script with `--execute` to apply the listed changes.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="""
NixOS Mailserver Migration #3: Dovecot mail directory migration
(https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration)
"""
)
parser.add_argument(
"vmail_root", type=Path, help="Path to the `mailserver.mailDirectory`"
)
parser.add_argument(
"--layout",
choices=["default", "folder"],
required=True,
help="Folder layout: 'default' unless `mailserver.useFsLayout` was enabled, then'folder'",
)
parser.add_argument(
"--execute", action="store_true", help="Actually perform changes"
)
args = parser.parse_args()
layout = FolderLayout.Default if args.layout == "default" else FolderLayout.Folder
check_user(args.vmail_root)
main(args.vmail_root, layout, not args.execute)

View File

@@ -1,31 +0,0 @@
{
network.description = "mail server";
mailserver =
{ config, pkgs, ... }:
{
imports = [
../default.nix
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
};
};
extraVirtualAliases = {
"info@example.com" = "user1@example.com";
"postmaster@example.com" = "user1@example.com";
"abuse@example.com" = "user1@example.com";
"user1@example2.com" = "user1@example.com";
"info@example2.com" = "user1@example.com";
"postmaster@example2.com" = "user1@example.com";
"abuse@example2.com" = "user1@example.com";
};
};
};
}

View File

@@ -1,9 +0,0 @@
{
mailserver =
{ config, pkgs, ... }:
{ deployment.targetEnv = "virtualbox";
deployment.virtualbox.memorySize = 1024; # megabytes
deployment.virtualbox.vcpu = 2; # number of cpus
deployment.virtualbox.headless = true;
};
}

5
pyproject.toml Normal file
View File

@@ -0,0 +1,5 @@
[tool.ruff.lint]
extend-select = ["ISC"]
[tool.ruff.lint.flake8-implicit-str-concat]
allow-multiline = false

View File

@@ -1,5 +1,7 @@
import json
import sys
from textwrap import indent
from typing import Any, Mapping
header = """
# Mailserver options
@@ -21,61 +23,90 @@ template = """
f = open(sys.argv[1])
options = json.load(f)
groups = ["mailserver.loginAccounts",
groups = [
"mailserver.loginAccounts",
"mailserver.certificate",
"mailserver.dkim",
"mailserver.srs",
"mailserver.dmarcReporting",
"mailserver.fullTextSearch",
"mailserver.redis",
"mailserver.ldap",
"mailserver.monitoring",
"mailserver.backup",
"mailserver.borgbackup"]
"mailserver.borgbackup",
]
def render_option_value(opt, attr):
if attr in opt:
if isinstance(opt[attr], dict) and '_type' in opt[attr]:
if opt[attr]['_type'] == 'literalExpression':
if '\n' in opt[attr]['text']:
res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```'
else:
res = '```{}```'.format(opt[attr]['text'])
elif opt[attr]['_type'] == 'literalMD':
res = opt[attr]['text']
else:
s = str(opt[attr])
if s == "":
res = '`""`'
elif '\n' in s:
res = '\n```\n' + s.rstrip('\n') + '\n```'
else:
res = '```{}```'.format(s)
res = '- ' + attr + ': ' + res
else:
res = ""
return res
def print_option(opt):
if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc
description = opt['description']['text']
def md_literal(value: str) -> str:
return f"`{value}`"
def md_codefence(value: str, language: str = "nix") -> str:
return indent(
f"\n```{language}\n{value}\n```",
prefix=2 * " ",
)
def render_option_value(option: Mapping[str, Any], key: str) -> str:
if key not in option:
return ""
if isinstance(option[key], dict) and "_type" in option[key]:
if option[key]["_type"] == "literalExpression":
# multi-line codeblock
if "\n" in option[key]["text"]:
text = option[key]["text"].rstrip("\n")
value = md_codefence(text)
# inline codeblock
else:
description = opt['description']
print(template.format(
key=opt['name'],
value = md_literal(option[key]["text"])
# literal markdown
elif option[key]["_type"] == "literalMD":
value = option[key]["text"]
else:
assert RuntimeError(f"Unhandled option type {option[key]['_type']}")
else:
text = str(option[key])
if text == "":
value = md_literal('""')
elif "\n" in text:
value = md_codefence(text.rstrip("\n"))
else:
value = md_literal(text)
return f"- {key}: {value}" # type: ignore
def print_option(option):
if (
isinstance(option["description"], dict) and "_type" in option["description"]
): # mdDoc
description = option["description"]["text"]
else:
description = option["description"]
print(
template.format(
key=option["name"],
description=description or "",
type="- type: ```{}```".format(opt['type']),
default=render_option_value(opt, 'default'),
example=render_option_value(opt, 'example')))
type=f"- type: {md_literal(option['type'])}",
default=render_option_value(option, "defaultText")
if "defaultText" in option
else render_option_value(option, "default"),
example=render_option_value(option, "example"),
)
)
print(header)
for opt in options:
if any([opt['name'].startswith(c) for c in groups]):
if any([opt["name"].startswith(c) for c in groups]):
continue
print_option(opt)
for c in groups:
print('## `{}`'.format(c))
print()
print(f"## `{c}`\n")
for opt in options:
if opt['name'].startswith(c):
if opt["name"].startswith(c):
print_option(opt)

View File

@@ -1,43 +1,59 @@
import smtplib, sys
import argparse
import os
import uuid
import imaplib
from datetime import datetime, timedelta
import email
import email.utils
import imaplib
import smtplib
import time
import uuid
from datetime import datetime, timedelta
from typing import cast
RETRY = 100
def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, starttls):
print("Sending mail with subject '{}'".format(subject))
message = "\n".join([
"From: {from_addr}",
"To: {to_addr}",
"Subject: {subject}",
"",
"This validates our mail server can send to Gmail :/"]).format(
from_addr=from_addr,
to_addr=to_addr,
subject=subject)
def _send_mail(
smtp_host,
smtp_port,
smtp_username,
from_addr,
from_pwd,
to_addr,
subject,
starttls,
ssl,
):
print(f"Sending mail with subject '{subject}'")
message = "\n".join(
[
f"From: {from_addr}",
f"To: {to_addr}",
f"Subject: {subject}",
f"Message-ID: {uuid.uuid4()}@mail-check.py",
f"Date: {email.utils.formatdate()}",
"",
"This validates our mail server can send to Gmail :/",
]
)
retry = RETRY
smtp_class = smtplib.SMTP_SSL if ssl else smtplib.SMTP
while True:
try:
with smtplib.SMTP(smtp_host, port=smtp_port) as smtp:
with smtp_class(smtp_host, port=smtp_port) as smtp:
try:
if starttls:
smtp.starttls()
if from_pwd is not None:
smtp.login(from_addr, from_pwd)
smtp.login(smtp_username or from_addr, from_pwd)
smtp.sendmail(from_addr, [to_addr], message)
return
except smtplib.SMTPResponseException as e:
if e.smtp_code == 451: # service unavailable error
print(e)
elif e.smtp_code == 454: # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
elif (
e.smtp_code == 454
): # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
print(e)
else:
raise
@@ -55,6 +71,7 @@ def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, star
print("Retry attempts exhausted")
exit(5)
def _read_mail(
imap_host,
imap_port,
@@ -63,8 +80,9 @@ def _read_mail(
subject,
ignore_dkim_spf,
show_body=False,
delete=True):
print("Reading mail from %s" % imap_username)
delete=True,
):
print(f"Reading mail from {imap_username}")
message = None
@@ -74,49 +92,62 @@ def _read_mail(
today = datetime.today()
cutoff = today - timedelta(days=1)
dt = cutoff.strftime('%d-%b-%Y')
dt = cutoff.strftime("%d-%b-%Y")
for _ in range(0, RETRY):
print("Retrying")
obj.select()
typ, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")'%(dt, subject))
if data == [b'']:
_, data = obj.search(None, f'(SINCE {dt}) (SUBJECT "{subject}")')
if data == [b""]:
time.sleep(1)
continue
uids = data[0].decode("utf-8").split(" ")
if len(uids) != 1:
print("Warning: %d messages have been found with subject containing %s " % (len(uids), subject))
print(
f"Warning: {len(uids)} messages have been found with subject containing {subject}"
)
# FIXME: we only consider the first matching message...
uid = uids[0]
_, raw = obj.fetch(uid, '(RFC822)')
_, raw = obj.fetch(uid, "(RFC822)")
if delete:
obj.store(uid, '+FLAGS', '\\Deleted')
obj.store(uid, "+FLAGS", "\\Deleted")
obj.expunge()
message = email.message_from_bytes(raw[0][1])
print("Message with subject '%s' has been found" % message['subject'])
assert raw[0] and raw[0][1]
message = email.message_from_bytes(cast(bytes, raw[0][1]))
print(f"Message with subject '{message['subject']}' has been found")
if show_body:
for m in message.get_payload():
if m.get_content_type() == 'text/plain':
print("Body:\n%s" % m.get_payload(decode=True).decode('utf-8'))
if message.is_multipart():
for part in message.walk():
ctype = part.get_content_type()
if ctype == "text/plain":
body = cast(bytes, part.get_payload(decode=True)).decode()
print(f"Body:\n{body}")
else:
print(f"Body with content type {ctype} not printed")
else:
body = cast(bytes, message.get_payload(decode=True)).decode()
print(f"Body:\n{body}")
break
if message is None:
print("Error: no message with subject '%s' has been found in INBOX of %s" % (subject, imap_username))
print(
f"Error: no message with subject '{subject}' has been found in INBOX of {imap_username}"
)
exit(1)
if ignore_dkim_spf:
return
# gmail set this standardized header
if 'ARC-Authentication-Results' in message:
if "dkim=pass" in message['ARC-Authentication-Results']:
if "ARC-Authentication-Results" in message:
if "dkim=pass" in message["ARC-Authentication-Results"]:
print("DKIM ok")
else:
print("Error: no DKIM validation found in message:")
print(message.as_string())
exit(2)
if "spf=pass" in message['ARC-Authentication-Results']:
if "spf=pass" in message["ARC-Authentication-Results"]:
print("SPF ok")
else:
print("Error: no SPF validation found in message:")
@@ -126,69 +157,110 @@ def _read_mail(
print("DKIM and SPF verification failed")
exit(4)
def send_and_read(args):
src_pwd = None
if args.src_password_file is not None:
src_pwd = args.src_password_file.readline().rstrip()
dst_pwd = args.dst_password_file.readline().rstrip()
if args.imap_username != '':
if args.imap_username != "":
imap_username = args.imap_username
else:
imap_username = args.to_addr
subject = "{}".format(uuid.uuid4())
subject = f"{uuid.uuid4()}"
_send_mail(smtp_host=args.smtp_host,
_send_mail(
smtp_host=args.smtp_host,
smtp_port=args.smtp_port,
smtp_username=args.smtp_username,
from_addr=args.from_addr,
from_pwd=src_pwd,
to_addr=args.to_addr,
subject=subject,
starttls=args.smtp_starttls)
starttls=args.smtp_starttls,
ssl=args.smtp_ssl,
)
_read_mail(imap_host=args.imap_host,
_read_mail(
imap_host=args.imap_host,
imap_port=args.imap_port,
imap_username=imap_username,
to_pwd=dst_pwd,
subject=subject,
ignore_dkim_spf=args.ignore_dkim_spf)
ignore_dkim_spf=args.ignore_dkim_spf,
)
def read(args):
_read_mail(imap_host=args.imap_host,
_read_mail(
imap_host=args.imap_host,
imap_port=args.imap_port,
to_addr=args.imap_username,
imap_username=args.imap_username,
to_pwd=args.imap_password,
subject=args.subject,
ignore_dkim_spf=args.ignore_dkim_spf,
show_body=args.show_body,
delete=False)
delete=False,
)
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers()
parser_send_and_read = subparsers.add_parser('send-and-read', description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.")
parser_send_and_read.add_argument('--smtp-host', type=str)
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
parser_send_and_read.add_argument('--from-addr', type=str)
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
parser_send_and_read.add_argument('--to-addr', type=str, required=True)
parser_send_and_read.add_argument('--imap-username', type=str, default='', help="username used for imap login. If not specified, the to-addr value is used")
parser_send_and_read.add_argument('--src-password-file', type=argparse.FileType('r'))
parser_send_and_read.add_argument('--dst-password-file', required=True, type=argparse.FileType('r'))
parser_send_and_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
parser_send_and_read = subparsers.add_parser(
"send-and-read",
description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.",
)
parser_send_and_read.add_argument("--smtp-host", type=str)
parser_send_and_read.add_argument("--smtp-port", type=str, default=25)
parser_send_and_read.add_argument("--smtp-starttls", action="store_true")
parser_send_and_read.add_argument("--smtp-ssl", action="store_true")
parser_send_and_read.add_argument(
"--smtp-username",
type=str,
default="",
help="username used for smtp login. If not specified, the from-addr value is used",
)
parser_send_and_read.add_argument("--from-addr", type=str)
parser_send_and_read.add_argument("--imap-host", required=True, type=str)
parser_send_and_read.add_argument("--imap-port", type=str, default=993)
parser_send_and_read.add_argument("--to-addr", type=str, required=True)
parser_send_and_read.add_argument(
"--imap-username",
type=str,
default="",
help="username used for imap login. If not specified, the to-addr value is used",
)
parser_send_and_read.add_argument("--src-password-file", type=argparse.FileType("r"))
parser_send_and_read.add_argument(
"--dst-password-file", required=True, type=argparse.FileType("r")
)
parser_send_and_read.add_argument(
"--ignore-dkim-spf",
action="store_true",
help="to ignore the dkim and spf verification on the read mail",
)
parser_send_and_read.set_defaults(func=send_and_read)
parser_read = subparsers.add_parser('read', description="Search for an email with a subject containing 'subject' in the INBOX.")
parser_read.add_argument('--imap-host', type=str, default="localhost")
parser_read.add_argument('--imap-port', type=str, default=993)
parser_read.add_argument('--imap-username', required=True, type=str)
parser_read.add_argument('--imap-password', required=True, type=str)
parser_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
parser_read.add_argument('--show-body', action='store_true', help="print mail text/plain payload")
parser_read.add_argument('subject', type=str)
parser_read = subparsers.add_parser(
"read",
description="Search for an email with a subject containing 'subject' in the INBOX.",
)
parser_read.add_argument("--imap-host", type=str, default="localhost")
parser_read.add_argument("--imap-port", type=str, default=993)
parser_read.add_argument("--imap-username", required=True, type=str)
parser_read.add_argument("--imap-password", required=True, type=str)
parser_read.add_argument(
"--ignore-dkim-spf",
action="store_true",
help="to ignore the dkim and spf verification on the read mail",
)
parser_read.add_argument(
"--show-body", action="store_true", help="print mail text/plain payload"
)
parser_read.add_argument("subject", type=str)
parser_read.set_defaults(func=read)
args = parser.parse_args()

View File

@@ -1,10 +1,9 @@
(import
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{ src = ./.; }
).shellNix
) { src = ./.; }).shellNix

View File

@@ -14,12 +14,18 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ pkgs ? import <nixpkgs> {}, blobs}:
{
lib,
blobs,
...
}:
pkgs.nixosTest {
{
name = "clamav";
nodes = {
server = { config, pkgs, lib, ... }:
server =
{ pkgs, ... }:
{
imports = [
../default.nix
@@ -28,6 +34,8 @@ pkgs.nixosTest {
virtualisation.memorySize = 1500;
environment.systemPackages = with pkgs; [ netcat ];
services.rsyslogd = {
enable = true;
defaultConfig = ''
@@ -63,7 +71,10 @@ pkgs.nixosTest {
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
domains = [
"example.com"
"example2.com"
];
virusScanning = true;
loginAccounts = {
@@ -83,21 +94,28 @@ pkgs.nixosTest {
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
};
};
client = { nodes, config, pkgs, ... }: let
serverIP = nodes.server.config.networking.primaryIPAddress;
clientIP = nodes.client.config.networking.primaryIPAddress;
client =
{ nodes, pkgs, ... }:
let
serverIP = nodes.server.networking.primaryIPAddress;
clientIP = nodes.client.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
#!${pkgs.stdenv.shell}
echo grep '${clientIP}' "$@" >&2
exec grep '${clientIP}' "$@"
'';
in {
in
{
imports = [
./lib/config.nix
];
environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip
fetchmail
msmtp
procmail
findutils
grep-ip
];
environment.etc = {
"root/.fetchmailrc" = {
@@ -180,8 +198,7 @@ pkgs.nixosTest {
};
};
testScript = { nodes, ... }:
''
testScript = ''
start_all()
server.wait_for_unit("multi-user.target")
@@ -189,10 +206,10 @@ pkgs.nixosTest {
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
"set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
)
client.execute("cp -p /etc/root/.* ~/")
@@ -222,7 +239,7 @@ pkgs.nixosTest {
with subtest("virus scan email"):
client.succeed(
'set +o pipefail; msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
)
server.succeed("journalctl -u rspamd | grep -i eicar")
# give the mail server some time to process the mail

View File

@@ -14,18 +14,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ pkgs ? import <nixpkgs> {}, ...}:
pkgs.nixosTest {
{
name = "external";
nodes = {
server = { config, pkgs, ... }:
server =
{ pkgs, ... }:
{
imports = [
../default.nix
./lib/config.nix
];
environment.systemPackages = with pkgs; [ netcat ];
virtualisation.memorySize = 1024;
services.rsyslogd = {
@@ -35,19 +37,17 @@ pkgs.nixosTest {
'';
};
mailserver = {
enable = true;
debug = true;
debug.dovecot = true; # enabled for sieve script logging
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
domains = [
"example.com"
"example2.com"
];
rewriteMessageId = true;
dkimKeyBits = 1535;
dmarcReporting = {
enable = true;
domain = "example.com";
organizationName = "ACME Corp";
};
dmarcReporting.enable = true;
loginAccounts = {
"user1@example.com" = {
@@ -70,7 +70,10 @@ pkgs.nixosTest {
extraVirtualAliases = {
"single-alias@example.com" = "user1@example.com";
"multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ];
"multi-alias@example.com" = [
"user1@example.com"
"user2@example.com"
];
};
enableImap = true;
@@ -79,16 +82,18 @@ pkgs.nixosTest {
enable = true;
autoIndex = true;
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ];
autoIndexExclude = [
(if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk")
];
enforced = "yes";
# fts-xapian warns when memory is low, which makes the test fail
memoryLimit = 100000;
};
};
};
client = { nodes, config, pkgs, ... }: let
serverIP = nodes.server.config.networking.primaryIPAddress;
clientIP = nodes.client.config.networking.primaryIPAddress;
client =
{ nodes, pkgs, ... }:
let
serverIP = nodes.server.networking.primaryIPAddress;
clientIP = nodes.client.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
#!${pkgs.stdenv.shell}
echo grep '${clientIP}' "$@" >&2
@@ -173,12 +178,21 @@ pkgs.nixosTest {
assert needle in repr(response)
imap.close()
'';
in {
in
{
imports = [
./lib/config.nix
];
environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search
fetchmail
msmtp
procmail
findutils
grep-ip
check-mail-id
test-imap-spam
test-imap-ham
search
];
environment.etc = {
"root/.fetchmailrc" = {
@@ -272,7 +286,7 @@ pkgs.nixosTest {
To: Chuck <chuck@example.com>
Cc:
Bcc:
Subject: This is a test Email from postmaster\@example.com to chuck
Subject: This is a test Email from postmaster@example.com to chuck
Reply-To:
Hello Chuck,
@@ -286,7 +300,7 @@ pkgs.nixosTest {
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from single-alias\@example.com to user1
Subject: This is a test Email from single-alias@example.com to user1
Reply-To:
Hello User1,
@@ -301,7 +315,7 @@ pkgs.nixosTest {
To: Multi Alias <multi-alias@example.com>
Cc:
Bcc:
Subject: This is a test Email from user2\@example.com to multi-alias
Subject: This is a test Email from user2@example.com to multi-alias
Reply-To:
Hello Multi Alias,
@@ -341,8 +355,7 @@ pkgs.nixosTest {
};
};
testScript = { nodes, ... }:
''
testScript = ''
start_all()
server.wait_for_unit("multi-user.target")
@@ -350,7 +363,7 @@ pkgs.nixosTest {
# TODO put this blocking into the systemd units?
server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
client.execute("cp -p /etc/root/.* ~/")
@@ -367,7 +380,7 @@ pkgs.nixosTest {
with subtest("submission port send mail"):
# send email from user2 to user1
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
)
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
@@ -395,20 +408,20 @@ pkgs.nixosTest {
client.execute("rm ~/mail/*")
# send email from user2 to user1
client.succeed(
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
client.succeed("cat ~/mail/* >&2")
# make sure it is dkim signed
client.succeed("grep DKIM ~/mail/*")
client.succeed("grep DKIM-Signature: ~/mail/*")
with subtest("aliases"):
client.execute("rm ~/mail/*")
# send email from chuck to postmaster
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < /etc/root/email2 >&2"
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
@@ -418,7 +431,7 @@ pkgs.nixosTest {
client.execute("rm ~/mail/*")
# send email from chuck to non exsitent account
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < /etc/root/email2 >&2"
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
@@ -427,7 +440,7 @@ pkgs.nixosTest {
client.execute("rm ~/mail/*")
# send email from user1 to chuck
client.succeed(
"msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < /etc/root/email2 >&2"
"msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 1 when no new mail
@@ -438,7 +451,7 @@ pkgs.nixosTest {
client.execute("rm ~/mail/*")
# send email from single-alias to user1
client.succeed(
"msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email4 >&2"
"msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
@@ -447,7 +460,7 @@ pkgs.nixosTest {
client.execute("rm ~/mail/*")
# send email from user1 to multi-alias (user{1,2}@example.com)
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias\@example.com < /etc/root/email5 >&2"
"msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
@@ -458,7 +471,7 @@ pkgs.nixosTest {
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota\@example.com < /etc/root/email2 >&2"
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
@@ -467,23 +480,23 @@ pkgs.nixosTest {
with subtest("imap sieve junk trainer"):
# send email from user2 to user1
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
)
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
client.succeed("imap-mark-spam >&2")
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2")
server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-spam.sh >&2")
client.succeed("imap-mark-ham >&2")
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2")
server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-ham.sh >&2")
with subtest("full text search and indexation"):
# send 2 email from user2 to user1
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email6 >&2"
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2"
)
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email7 >&2"
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2"
)
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
@@ -493,23 +506,23 @@ pkgs.nixosTest {
# should fail because this folder is not indexed
client.fail("search Junk a >&2")
# check that search really goes through the indexer
server.succeed(
"journalctl -u dovecot2 | grep -E 'indexer-worker.* Done indexing .INBOX.' >&2"
)
server.succeed("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
# check that Junk is not indexed
server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2")
server.fail("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
with subtest("dmarc reporting"):
server.systemctl("start rspamd-dmarc-reporter.service")
server.wait_until_succeeds("journalctl -eu rspamd-dmarc-reporter.service -o cat | grep -q 'No reports for '")
with subtest("no warnings or errors"):
server.fail("journalctl -u postfix | grep -i error >&2")
server.fail("journalctl -u postfix | grep -i warning >&2")
server.fail("journalctl -u dovecot2 | grep -i error >&2")
server.fail("journalctl -u dovecot -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2")
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
server.fail(
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -i warning >&2"
"journalctl -u dovecot -u dovecot2 | \
grep -v 'Expunged message reappeared, giving a new UID' | \
grep -v 'Time moved forwards' | \
grep -i warning >&2"
)
'';
}

View File

@@ -14,7 +14,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ pkgs ? import <nixpkgs> {}, ...}:
{
pkgs,
...
}:
let
sendMail = pkgs.writeTextFile {
@@ -27,19 +30,27 @@ let
'';
};
hashPassword = password: pkgs.runCommand
"password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
hashPassword =
password:
pkgs.runCommand "password-${password}-hashed"
{
buildInputs = [ pkgs.mkpasswd ];
inherit password;
}
''
mkpasswd -sm bcrypt <<<"$password" > $out
'';
hashedPasswordFile = hashPassword "my-password";
passwordFile = pkgs.writeText "password" "my-password";
in
pkgs.nixosTest {
{
name = "internal";
nodes = {
machine = { config, pkgs, ... }: {
machine =
{ pkgs, ... }:
{
imports = [
./../default.nix
./lib/config.nix
@@ -50,12 +61,21 @@ pkgs.nixosTest {
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')];
'')
]
++ (with pkgs; [
curl
openssl
netcat
]);
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
domains = [
"example.com"
"domain.com"
];
localDnsResolver = false;
loginAccounts = {
@@ -64,6 +84,7 @@ pkgs.nixosTest {
};
"user2@example.com" = {
hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ];
};
"send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only";
@@ -78,18 +99,24 @@ pkgs.nixosTest {
vmailGroupName = "vmail";
vmailUID = 5000;
indexDir = "/var/lib/dovecot/indices";
enableImap = false;
};
};
};
testScript = ''
testScript =
{
nodes,
...
}:
''
machine.start()
machine.wait_for_unit("multi-user.target")
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
with subtest("mail forwarded can are locally kept"):
# A mail sent to user2@example.com is in the user1@example.com mailbox
# A mail sent to user2@example.com via explicit TLS is in the user1@example.com mailbox
machine.succeed(
" ".join(
[
@@ -107,13 +134,13 @@ pkgs.nixosTest {
]
)
)
# A mail sent to user2@example.com is in the user2@example.com mailbox
# A mail sent to user2@example.com via implicit TLS is in the user2@example.com mailbox
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-port 465",
"--smtp-ssl",
"--smtp-host localhost",
"--imap-host localhost",
"--imap-username user2@example.com",
@@ -126,29 +153,75 @@ pkgs.nixosTest {
)
)
with subtest("regex email alias are received"):
# A mail sent to user2-regex-alias@domain.com via explicit TLS is in the user2@example.com mailbox
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--imap-host localhost",
"--imap-username user2@example.com",
"--from-addr user1@example.com",
"--to-addr user2-regex-alias@domain.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
with subtest("user can send from regex email alias"):
# A mail sent to user1@example.com from user2-regex-alias@domain.com by
# user2@example.com via implicit TLS is in the user1@example.com mailbox
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 465",
"--smtp-ssl",
"--smtp-host localhost",
"--imap-host localhost",
"--smtp-username user2@example.com",
"--from-addr user2-regex-alias@domain.com",
"--to-addr user1@example.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
with subtest("vmail gid is set correctly"):
machine.succeed("getent group vmail | grep 5000")
with subtest("Check dovecot maildir and index locations"):
# If these paths change we need a migration
machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.mailDirectory}/example.com/user1")
machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'")
with subtest("mail to send only accounts is rejected"):
machine.wait_for_open_port(25)
# TODO put this blocking into the systemd units
machine.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
machine.succeed(
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'"
"cat ${sendMail} | nc localhost 25 | grep -q '554 5.5.0 Error'"
)
with subtest("rspamd controller serves web ui"):
machine.succeed(
"set +o pipefail; ${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
"set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
)
with subtest("imap port 143 is closed and imaps is serving SSL"):
machine.wait_for_closed_port(143)
machine.wait_for_open_port(993)
machine.succeed(
"echo | ${pkgs.openssl}/bin/openssl s_client -connect localhost:993 | grep 'New, TLS'"
"echo | openssl s_client -connect localhost:993 | grep 'New, TLS'"
)
'';
}

231
tests/ldap.nix Normal file
View File

@@ -0,0 +1,231 @@
let
bindPassword = "unsafegibberish";
alicePassword = "testalice";
bobPassword = "testbob";
in
{
name = "ldap";
nodes = {
machine =
{ pkgs, ... }:
{
imports = [
./../default.nix
./lib/config.nix
];
virtualisation.memorySize = 1024;
services.openssh = {
enable = true;
settings.PermitRootLogin = "yes";
};
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')
];
environment.etc.bind-password.text = bindPassword;
services.openldap = {
enable = true;
settings = {
children = {
"cn=schema".includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
];
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [
"olcDatabaseConfig"
"olcMdbConfig"
];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/example";
olcSuffix = "dc=example";
};
};
};
};
declarativeContents."dc=example" = ''
dn: dc=example
objectClass: domain
dc: example
dn: cn=mail,dc=example
objectClass: organizationalRole
objectClass: simpleSecurityObject
objectClass: top
cn: mail
userPassword: ${bindPassword}
dn: ou=users,dc=example
objectClass: organizationalUnit
ou: users
dn: cn=alice,ou=users,dc=example
objectClass: inetOrgPerson
cn: alice
sn: Foo
mail: alice@example.com
userPassword: ${alicePassword}
dn: cn=bob,ou=users,dc=example
objectClass: inetOrgPerson
cn: bob
sn: Bar
mail: bob@example.com
userPassword: ${bobPassword}
'';
};
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
localDnsResolver = false;
indexDir = "/var/lib/dovecot/indices";
ldap = {
enable = true;
uris = [
"ldap://"
];
bind = {
dn = "cn=mail,dc=example";
passwordFile = "/etc/bind-password";
};
searchBase = "ou=users,dc=example";
searchScope = "sub";
};
forwards = {
"bob_fw@example.com" = "bob@example.com";
};
vmailGroupName = "vmail";
vmailUID = 5000;
enableImap = false;
};
};
};
testScript =
{
nodes,
...
}:
''
import sys
import re
machine.start()
machine.wait_for_unit("multi-user.target")
# This function retrieves the ldap table file from a postconf
# command.
# A key lookup is achived and the returned value is compared
# to the expected value.
def test_lookup(postconf_cmdline, key, expected):
conf = machine.succeed(postconf_cmdline).rstrip()
ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1)
value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip()
try:
assert value == expected
except AssertionError:
print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr)
raise
with subtest("Test postmap lookups"):
test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com")
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com")
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com")
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com")
with subtest("Test doveadm lookups"):
machine.succeed("doveadm user -u alice@example.com")
machine.succeed("doveadm user -u bob@example.com")
with subtest("Files containing secrets are only readable by root"):
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
with subtest("Test account/mail address binding via explicit TLS"):
machine.fail(" ".join([
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--smtp-username alice@example.com",
"--imap-host localhost",
"--imap-username bob@example.com",
"--from-addr bob@example.com",
"--to-addr aliceb@example.com",
"--src-password-file <(echo '${alicePassword}')",
"--dst-password-file <(echo '${bobPassword}')",
"--ignore-dkim-spf"
]))
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
with subtest("Test mail delivery via implicit TLS"):
machine.succeed(" ".join([
"mail-check send-and-read",
"--smtp-port 465",
"--smtp-ssl",
"--smtp-host localhost",
"--smtp-username alice@example.com",
"--imap-host localhost",
"--imap-username bob@example.com",
"--from-addr alice@example.com",
"--to-addr bob@example.com",
"--src-password-file <(echo '${alicePassword}')",
"--dst-password-file <(echo '${bobPassword}')",
"--ignore-dkim-spf"
]))
with subtest("Test mail forwarding via explicit TLS works"):
machine.succeed(" ".join([
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--smtp-username alice@example.com",
"--imap-host localhost",
"--imap-username bob@example.com",
"--from-addr alice@example.com",
"--to-addr bob_fw@example.com",
"--src-password-file <(echo '${alicePassword}')",
"--dst-password-file <(echo '${bobPassword}')",
"--ignore-dkim-spf"
]))
with subtest("Test cannot send mail via implicit TLS from forwarded address"):
machine.fail(" ".join([
"mail-check send-and-read",
"--smtp-port 465",
"--smtp-ssl",
"--smtp-host localhost",
"--smtp-username bob@example.com",
"--imap-host localhost",
"--imap-username alice@example.com",
"--from-addr bob_fw@example.com",
"--to-addr alice@example.com",
"--src-password-file <(echo '${bobPassword}')",
"--dst-password-file <(echo '${alicePassword}')",
"--ignore-dkim-spf"
]))
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'")
with subtest("Check dovecot mail and index locations"):
# If these paths change we need a migration
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/bob@example.com")
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/bob@example.com'")
'';
}

View File

@@ -1,3 +1,28 @@
{
security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot
lib,
...
}:
{
# Testing eval failures that result from stateVersion assertion is out of scope
mailserver.stateVersion = 999;
# Keep testing submission with explicit TLS
mailserver.enableSubmission = true;
# Enable second CPU core
virtualisation.cores = lib.mkDefault 2;
services.rspamd = {
# Don't make tests block on DNS requests that will never succeed
locals."options.inc".text = ''
dns {
nameservers = ["127.0.0.1"];
timeout = 0.0s;
retransmits = 0;
}
'';
# Relax `local_addrs` definition to default for tests, so mail doesn't get flagged as spam
overrides."options.inc".enable = false;
};
}

View File

@@ -1,31 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
import <nixpkgs/nixos/tests/make-test-python.nix> {
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
./../default.nix
];
};
testScript =
''
machine.wait_for_unit("multi-user.target");
'';
}

View File

@@ -1,19 +1,34 @@
# This tests is used to test features requiring several mail domains.
{ pkgs ? import <nixpkgs> {}, ...}:
{
lib,
pkgs,
...
}:
let
hashPassword = password: pkgs.runCommand
"password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; }
hashPassword =
password:
pkgs.runCommand "password-${password}-hashed"
{
buildInputs = [ pkgs.mkpasswd ];
inherit password;
}
''
mkpasswd -sm bcrypt <<<"$password" > $out
'';
password = pkgs.writeText "password" "password";
domainGenerator = domain: { config, pkgs, ... }: {
imports = [../default.nix];
domainGenerator =
domain:
{ pkgs, ... }:
{
imports = [
../default.nix
./lib/config.nix
];
environment.systemPackages = with pkgs; [ netcat ];
virtualisation.memorySize = 1024;
mailserver = {
enable = true;
@@ -30,31 +45,49 @@ let
};
services.dnsmasq = {
enable = true;
settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
settings.mx-host = [
"domain1.com,domain1,10"
"domain2.com,domain2,10"
];
};
# breaks the test, due to running into DNS timeouts
services.postfix-tlspol.configurePostfix = lib.mkForce false;
};
in
pkgs.nixosTest {
{
name = "multiple";
nodes = {
domain1 = {...}: {
domain1 =
{ ... }:
{
imports = [
../default.nix
(domainGenerator "domain1.com")
];
mailserver.forwards = {
"non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"];
"non@domain1.com" = ["user@domain2.com" "user@domain1.com"];
"non-local@domain1.com" = [
"user@domain2.com"
"user@domain1.com"
];
"non@domain1.com" = [
"user@domain2.com"
"user@domain1.com"
];
};
};
domain2 = domainGenerator "domain2.com";
client = { config, pkgs, ... }: {
client =
{ pkgs, ... }:
{
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')];
'')
];
};
};
testScript = ''
@@ -65,20 +98,20 @@ pkgs.nixosTest {
# TODO put this blocking into the systemd units?
domain1.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
domain2.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
# user@domain1.com sends a mail to user@domain2.com
# user@domain1.com sends a mail to user@domain2.com via explicit TLS
client.succeed(
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
)
# Send a mail to the address forwarded and check it is in the recipient mailbox
# Send a mail to the address forwarded via implicit TLS and check it is in the recipient mailbox
client.succeed(
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
"mail-check send-and-read --smtp-port 465 --smtp-ssl --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
)
'';
}

View File

@@ -1,7 +0,0 @@
#!/usr/bin/env bash
sed -i -e "s/v[0-9]\+\.[0-9]\+\.[0-9]\+/$1/g" README.md
HASH=$(nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.3.0/nixos-mailserver-$1.tar.gz" --unpack)
sed -i -e "s/sha256 = \"[0-9a-z]\{52\}\"/sha256 = \"$HASH\"/g" README.md