102 Commits

Author SHA1 Message Date
Jakub Skokan
284a1e4041 Allow TLSv1 for compatibility with older devices 2025-05-25 21:06:21 +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
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
44 changed files with 1016 additions and 828 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

@@ -8,7 +8,7 @@ let
{ enabled = 1;
hidden = false;
description = "PR ${num}: ${info.title}";
checkinterval = 30;
checkinterval = 300;
schedulingshares = 20;
enableemail = false;
emailoverride = "";
@@ -19,7 +19,7 @@ let
) prs;
mkFlakeJobset = branch: {
description = "Build ${branch} branch of Simple NixOS MailServer";
checkinterval = "60";
checkinterval = 300;
enabled = "1";
schedulingshares = 100;
enableemail = false;
@@ -32,8 +32,8 @@ let
desc = prJobsets // {
"master" = mkFlakeJobset "master";
"nixos-23.11" = mkFlakeJobset "nixos-23.11";
"nixos-24.05" = mkFlakeJobset "nixos-24.05";
"nixos-24.11" = mkFlakeJobset "nixos-24.11";
"nixos-25.05" = mkFlakeJobset "nixos-25.05";
};
log = {

125
README.md
View File

@@ -1,75 +1,82 @@
# ![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 24.05
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05)
* For NixOS 23.11
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-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 24.11
* Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11)
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/)
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11)
* 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.
* 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
* 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
### 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)
* [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd)
* 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`
* 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
@@ -82,16 +89,18 @@ For a complete list of options, [see in readthedocs](https://nixos-mailserver.re
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

View File

@@ -278,7 +278,7 @@ in
dovecot = {
userAttrs = mkOption {
type = types.nullOr types.str;
default = "";
default = null;
description = ''
LDAP attributes to be retrieved during userdb lookups.
@@ -290,8 +290,8 @@ in
userFilter = mkOption {
type = types.str;
default = "mail=%u";
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
default = "mail=%{user}";
example = "(&(objectClass=inetOrgPerson)(mail=%{user}))";
description = ''
Filter for user lookups in Dovecot.
@@ -315,8 +315,8 @@ in
passFilter = mkOption {
type = types.nullOr types.str;
default = "mail=%u";
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
default = "mail=%{user}";
example = "(&(objectClass=inetOrgPerson)(mail=%{user}))";
description = ''
Filter for password lookups in Dovecot.
@@ -380,7 +380,21 @@ in
};
fullTextSearch = {
enable = mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
enable = mkEnableOption ''
Full text search indexing with Xapian through the fts_flatcurve plugin.
This has significant performance and disk space cost.
'';
memoryLimit = mkOption {
type = types.nullOr types.int;
default = null;
example = 2000;
description = ''
Memory limit for the indexer process, in MiB.
If null, leaves the default (which is rather low),
and if 0, no limit.
'';
};
autoIndex = mkOption {
type = types.bool;
default = true;
@@ -395,12 +409,6 @@ in
'';
};
indexAttachments = mkOption {
type = types.bool;
default = false;
description = "Also index text-only attachements. Binary attachements are never indexed.";
};
enforced = mkOption {
type = types.enum [ "yes" "no" "body" ];
default = "no";
@@ -412,41 +420,54 @@ in
'';
};
minSize = mkOption {
type = types.int;
default = 2;
description = "Size of the smallest n-gram to index.";
};
maxSize = mkOption {
type = types.int;
default = 20;
description = "Size of the largest n-gram to index.";
};
memoryLimit = mkOption {
type = types.nullOr types.int;
default = null;
example = 2000;
description = "Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit.";
languages = mkOption {
type = types.nonEmptyListOf types.str;
default = [ "en" ];
example = [ "en" "de" ];
description = ''
A list of languages that the full text search should detect.
At least one language must be specified.
The language listed first is the default and is used when language recognition fails.
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_languages>.
'';
};
maintenance = {
enable = mkOption {
substringSearch = mkOption {
type = types.bool;
default = true;
description = "Regularly optmize indices, as recommended by upstream.";
default = false;
description = ''
If enabled, allows substring searches.
See <https://doc.dovecot.org/main/core/plugins/fts_flatcurve.html#fts_flatcurve_substring_search>.
Enabling this requires significant additional storage space.
'';
};
onCalendar = mkOption {
type = types.str;
default = "daily";
description = "When to run the maintenance job. See systemd.time(7) for more information about the format.";
headerExcludes = mkOption {
type = types.listOf types.str;
default = [
"Received"
"DKIM-*"
"X-*"
"Comments"
];
description = ''
The list of headers to exclude.
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_header_excludes>.
'';
};
randomizedDelaySec = mkOption {
type = types.int;
default = 1000;
description = "Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds.";
};
filters = mkOption {
type = types.listOf types.str;
default = [
"normalizer-icu"
"snowball"
"stopwords"
];
description = ''
The list of filters to apply.
<https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration>.
'';
};
};
@@ -460,6 +481,22 @@ in
'';
};
lmtpMemoryLimit = mkOption {
type = types.int;
default = 256;
description = ''
The memory limit for the LMTP service, in megabytes.
'';
};
quotaStatusMemoryLimit = mkOption {
type = types.int;
default = 256;
description = ''
The memory limit for the quota-status service, in megabytes.
'';
};
extraVirtualAliases = mkOption {
type = let
loginAccount = mkOptionType {
@@ -506,7 +543,7 @@ in
rejectSender = mkOption {
type = types.listOf types.str;
example = [ "@example.com" "spammer@example.net" ];
example = [ "example.com" "spammer@example.net" ];
description = ''
Reject emails from these addresses from unauthorized senders.
Use if a spammer is using the same domain or the same sender over and over.
@@ -570,7 +607,7 @@ in
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
See https://wiki2.dovecot.org/MailboxFormat/Maildir for details.
See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details.
'';
};
@@ -591,7 +628,7 @@ in
This affects how mailboxes appear to mail clients and sieve scripts.
For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example".
This does not determine the way your mails are stored on disk.
See https://wiki.dovecot.org/Namespaces for details.
See https://doc.dovecot.org/main/core/config/namespaces.html#namespaces for details.
'';
};
@@ -696,6 +733,14 @@ in
'';
};
imapMemoryLimit = mkOption {
type = types.int;
default = 256;
description = ''
The memory limit for the imap service, in megabytes.
'';
};
enableImapSsl = mkOption {
type = types.bool;
default = true;
@@ -789,6 +834,19 @@ in
'';
};
dkimKeyType = mkOption {
type = types.enum [ "rsa" "ed25519" ];
default = "rsa";
description = ''
The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018).
If you have already deployed a key with a different type than specified
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
this package to generate a key with the new type, you will either have to
change the selector or delete the old key file.
'';
};
dkimKeyBits = mkOption {
type = types.int;
default = 1024;
@@ -802,26 +860,6 @@ in
'';
};
dkimHeaderCanonicalization = mkOption {
type = types.enum ["relaxed" "simple"];
default = "relaxed";
description = ''
DKIM canonicalization algorithm for message headers.
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
'';
};
dkimBodyCanonicalization = mkOption {
type = types.enum ["relaxed" "simple"];
default = "relaxed";
description = ''
DKIM canonicalization algorithm for message bodies.
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
'';
};
dmarcReporting = {
enable = mkOption {
type = types.bool;
@@ -881,6 +919,14 @@ in
The sender name for DMARC reports. Defaults to the organization name.
'';
};
excludeDomains = mkOption {
type = types.listOf types.str;
default = [];
description = ''
List of domains or eSLDs to be excluded from DMARC reports.
'';
};
};
debug = mkOption {
@@ -923,28 +969,19 @@ in
address = mkOption {
type = types.str;
# read the default from nixos' redis module
default = let
cf = config.services.redis.servers.rspamd.bind;
cfdefault = if cf == null then "127.0.0.1" else cf;
ips = lib.strings.splitString " " cfdefault;
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
isIpv6 = ip: lib.lists.elem ":" (lib.stringToCharacters ip);
in
if (ip == "0.0.0.0" || ip == "::")
then "127.0.0.1"
else if isIpv6 ip then "[${ip}]" else ip;
defaultText = lib.literalMD "computed from `config.services.redis.servers.rspamd.bind`";
default = config.services.redis.servers.rspamd.unixSocket;
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.unixSocket";
description = ''
Address that rspamd should use to contact redis.
Path, IP address or hostname that Rspamd should use to contact Redis.
'';
};
port = mkOption {
type = types.port;
default = config.services.redis.servers.rspamd.port;
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
type = with types; nullOr port;
default = null;
example = lib.literalExpression "config.services.redis.servers.rspamd.port";
description = ''
Port that rspamd should use to contact redis.
Port that Rspamd should use to contact Redis.
'';
};
@@ -1010,18 +1047,6 @@ in
'';
};
policydSPFExtraConfig = mkOption {
type = types.lines;
default = "";
example = ''
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
'';
description = ''
Extra configuration options for policyd-spf. This can be use to among
other things skip spf checking for some IP addresses.
'';
};
monitoring = {
enable = mkEnableOption "monitoring via monit";
@@ -1215,27 +1240,6 @@ in
};
rebootAfterKernelUpgrade = {
enable = mkOption {
type = types.bool;
default = false;
example = true;
description = ''
Whether to enable automatic reboot after kernel upgrades.
This is to be used in conjunction with `system.autoUpgrade.enable = true;`
'';
};
method = mkOption {
type = types.enum [ "reboot" "systemctl kexec" ];
default = "reboot";
description = ''
Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot.
It is recommended to use the default value because the quicker kexec reboot has a number of problems.
Also if your server is running in a virtual machine the regular reboot will already be very quick.
'';
};
};
backup = {
enable = mkEnableOption "backup via rsnapshot";
@@ -1297,9 +1301,33 @@ in
};
imports = [
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] ''
This option is not needed for fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] ''
This option is not needed for fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] ''
This option is not needed for fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] ''
This option is not supported by fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] ''
This option is not needed since fts-xapian 1.8.3
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] ''
Text attachments are always indexed since fts-xapian 1.4.8
'')
(lib.mkRenamedOptionModule
[ "mailserver" "rebootAfterKernelUpgrade" "enable" ]
[ "system" "autoUpgrade" "allowReboot" ]
)
(lib.mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] ''
Use `system.autoUpgrade` instead.
'')
./mail-server/assertions.nix
./mail-server/borgbackup.nix
./mail-server/debug.nix
./mail-server/rsnapshot.nix
./mail-server/clamav.nix
./mail-server/monit.nix
@@ -1308,11 +1336,19 @@ in
./mail-server/networking.nix
./mail-server/systemd.nix
./mail-server/dovecot.nix
./mail-server/opendkim.nix
./mail-server/postfix.nix
./mail-server/rspamd.nix
./mail-server/nginx.nix
./mail-server/kresd.nix
./mail-server/post-upgrade-check.nix
(lib.mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] ''
SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings.
It may be that they are redundant and are already configured in rspamd like for skip_addresses.
'')
(lib.mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] ''
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
'')
(lib.mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] ''
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
'')
];
}

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,36 +57,10 @@ 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>`__.
::
$ nix build .#documentation
$ xdg-open result/index.html
Nixops
------
You can test the setup via ``nixops``. After installation, do
::
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
$ nixops deploy -d mail
$ nixops info -d mail
You can then test the server via e.g. \ ``telnet``. To log into it, use
::
$ nixops ssh -d mail mailserver
Imap
----
To test imap manually use
::
$ openssl s_client -host mail.example.com -port 143 -starttls imap

View File

@@ -1,6 +1,45 @@
Release Notes
=============
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
-----------
@@ -45,7 +84,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

@@ -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
@@ -58,9 +63,9 @@ common ones.
imports = [
(builtins.fetchTarball {
# 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-23.05/nixos-mailserver-nixos-23.05.tar.gz";
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.05/nixos-mailserver-nixos-25.05.tar.gz";
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
# release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
# release="nixos-25.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
@@ -98,8 +103,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::
@@ -115,6 +123,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
@@ -162,25 +173,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

80
flake.lock generated
View File

@@ -19,11 +19,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@@ -32,42 +32,90 @@
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1742649964,
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"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": 1717602782,
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
"lastModified": 1747179050,
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"owner": "NixOS",
"ref": "nixos-unstable",
"type": "indirect"
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-24_05": {
"nixpkgs-25_05": {
"locked": {
"lastModified": 1717144377,
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
"lastModified": 1747610100,
"narHash": "sha256-rpR5ZPMkWzcnCcYYo3lScqfuzEw5Uyfh+R0EKZfroAc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "805a384895c696f802a9bf5bf4720f37385df547",
"rev": "ca49c4304acf0973078db0a9d200fd2bae75676d",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.05",
"type": "indirect"
"owner": "NixOS",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"blobs": "blobs",
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"nixpkgs": "nixpkgs",
"nixpkgs-24_05": "nixpkgs-24_05"
"nixpkgs-25_05": "nixpkgs-25_05"
}
}
},

View File

@@ -3,45 +3,62 @@
inputs = {
flake-compat = {
# for shell.nix compat
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05";
git-hooks = {
url = "github:cachix/git-hooks.nix";
inputs.flake-compat.follows = "flake-compat";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs-25_05.url = "github:NixOS/nixpkgs/nixos-25.05";
blobs = {
url = "gitlab:simple-nixos-mailserver/blobs";
flake = false;
};
};
outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let
outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-25_05, ... }: let
lib = nixpkgs.lib;
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
releases = [
{
name = "unstable";
nixpkgs = nixpkgs;
pkgs = nixpkgs.legacyPackages.${system};
}
{
name = "24.05";
pkgs = nixpkgs-24_05.legacyPackages.${system};
name = "25.05";
nixpkgs = nixpkgs-25_05;
pkgs = nixpkgs-25_05.legacyPackages.${system};
}
];
testNames = [
"internal"
"external"
"clamav"
"multiple"
"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>;
@@ -79,6 +96,7 @@
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 {
@@ -111,16 +129,64 @@
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;
# 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
];
glab
] ++ self.checks.${system}.pre-commit.enabledPackages;
shellHook = self.checks.${system}.pre-commit.shellHook;
};
devShell.${system} = self.devShells.${system}.default; # compatibility
};

View File

@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ config, lib, ... }:
{
assertions = lib.optionals config.mailserver.ldap.enable [
{
@@ -9,10 +9,6 @@
assertion = config.mailserver.extraVirtualAliases == {};
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases";
}
{
assertion = config.mailserver.forwards == {};
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards";
}
] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [
{
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;

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

@@ -62,7 +62,7 @@ in
cat ${file} > ${destination}
echo -n '${prefix}' >> ${destination}
cat ${passwordFile} >> ${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,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, pkgs, lib, ... }:
with (import ./common.nix { inherit config pkgs lib; });
@@ -26,40 +26,24 @@ let
userdbFile = "${passwdDir}/userdb";
# This file contains the ldap bind password
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
bool2int = x: if x then "1" else "0";
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";
# maildir in format "/${domain}/${user}"
dovecotMaildir =
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
"maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n"
":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";
@@ -109,7 +93,7 @@ let
# Prevent world-readable password files, even temporarily.
umask 077
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
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
@@ -117,7 +101,7 @@ let
done
cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _:
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts)}
EOF
@@ -125,14 +109,12 @@ let
cat <<EOF > ${userdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:::::::"
+ (if lib.isString value.quota
then "userdb_quota_rule=*:storage=${value.quota}"
else "")
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
) cfg.loginAccounts)}
EOF
'';
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 "";
@@ -143,6 +125,24 @@ let
else scope
);
dovecotModules = [
pkgs.dovecot_pigeonhole
] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
# Remove and assume `false` after NixOS 25.05
haveDovecotModulesOption = options.services.dovecot2 ? "modules" && (options.services.dovecot2.modules.visible or true);
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 {
@@ -153,14 +153,33 @@ 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.optionals (!haveDovecotModulesOption) dovecotModules;
services.dovecot2 = {
# For compatibility with python imaplib
environment.etc = lib.mkIf (!haveDovecotModulesOption) {
"dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
};
services.dovecot2 = lib.mkMerge [{
enable = true;
enableImap = enableImap || enableImapSsl;
enablePop3 = enablePop3 || enablePop3Ssl;
@@ -172,15 +191,17 @@ in
sslServerCert = certificatePath;
sslServerKey = keyPath;
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";
pluginSettings = {
sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve";
sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve";
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 = [
@@ -197,9 +218,9 @@ in
'';
pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "sa-learn-ham.sh"
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
(pkgs.writeShellScriptBin "sa-learn-spam.sh"
(pkgs.writeShellScriptBin "rspamd-learn-spam.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
};
@@ -276,14 +297,18 @@ 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}
ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
ssl_min_protocol = TLSv1
ssl_prefer_server_ciphers = no
service lmtp {
unix_listener dovecot-lmtp {
@@ -291,6 +316,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}
@@ -320,7 +356,7 @@ in
userdb {
driver = ldap
args = ${ldapConfFile}
default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
default_fields = home=/var/vmail/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
}
''}
@@ -339,30 +375,20 @@ in
inbox = yes
}
${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 {
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
''}
}
''}
''}
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
'';
};
}
(lib.mkIf haveDovecotModulesOption {
modules = dovecotModules;
})
];
systemd.services.dovecot2 = {
preStart = ''
@@ -371,29 +397,5 @@ in
};
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
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;
};
};
};
}

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

@@ -22,7 +22,7 @@ in
{
config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd
dovecot openssh postfix rspamd
] ++ (if 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;

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

@@ -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

@@ -25,7 +25,7 @@ let
# 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
@@ -123,14 +123,7 @@ let
/^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}";
@@ -247,6 +240,11 @@ in
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
# Opportunistic DANE support
# https://www.postfix.org/postconf.5.html#smtp_tls_security_level
smtp_dns_support_level = "dnssec";
smtp_tls_security_level = "dane";
# sasl with dovecot
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
@@ -255,19 +253,17 @@ in
"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
@@ -275,10 +271,10 @@ in
smtpd_tls_security_level = "may";
# 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";
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";
smtp_tls_ciphers = "high";
smtpd_tls_ciphers = "high";
@@ -296,15 +292,16 @@ in
# 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}";
# Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
@@ -320,13 +317,6 @@ in
# 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.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
};
"submission-header-cleanup" = {
type = "unix";
private = false;

View File

@@ -22,6 +22,26 @@ 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
'';
in
{
config = with cfg; lib.mkIf enable {
@@ -30,7 +50,7 @@ in
nativeBuildInputs = with pkgs; [ makeWrapper ];
}''
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
--add-flags "-h /var/run/rspamd/worker-controller.sock"
--add-flags "-h /run/rspamd/worker-controller.sock"
'')
];
@@ -42,7 +62,11 @@ in
extended_spam_headers = true;
''; };
"redis.conf" = { text = ''
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
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}";
''); };
@@ -62,8 +86,11 @@ in
}
''; };
"dkim_signing.conf" = { text = ''
# Disable outbound email signing, we use opendkim for this
enabled = false;
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
''; };
"dmarc.conf" = { text = ''
${lib.optionalString cfg.dmarcReporting.enable ''
@@ -73,7 +100,10 @@ in
domain = "${cfg.dmarcReporting.domain}";
org_name = "${cfg.dmarcReporting.organizationName}";
from_name = "${cfg.dmarcReporting.fromName}";
msgid_from = "dmarc-rua";
msgid_from = "${cfg.dmarcReporting.domain}";
${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) ''
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
''}
}''}
''; };
};
@@ -110,14 +140,35 @@ in
};
services.redis.servers.rspamd = {
enable = lib.mkDefault true;
port = lib.mkDefault 6380;
services.redis.servers.rspamd.enable = lib.mkDefault true;
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 cfg.domains;
ReadWritePaths = [ cfg.dkimKeyDirectory ];
})
];
};
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {

View File

@@ -63,7 +63,7 @@ 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}
@@ -76,10 +76,10 @@ in
systemd.services.postfix = {
wants = certificatesDeps;
after = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service"
++ lib.optional cfg.dkimSigning "rspamd.service"
++ certificatesDeps;
requires = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service";
++ lib.optional cfg.dkimSigning "rspamd.service";
};
};
}

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

View File

@@ -1,5 +1,7 @@
import json
import sys
from textwrap import indent
from typing import Any, Mapping
header = """
# Mailserver options
@@ -21,7 +23,8 @@ template = """
f = open(sys.argv[1])
options = json.load(f)
groups = ["mailserver.loginAccounts",
groups = [
"mailserver.loginAccounts",
"mailserver.certificate",
"mailserver.dkim",
"mailserver.dmarcReporting",
@@ -30,53 +33,77 @@ groups = ["mailserver.loginAccounts",
"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, "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,26 +1,31 @@
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, smtp_username, 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
):
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
while True:
@@ -37,7 +42,9 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr
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 +62,7 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr
print("Retry attempts exhausted")
exit(5)
def _read_mail(
imap_host,
imap_port,
@@ -63,8 +71,9 @@ def _read_mail(
subject,
ignore_dkim_spf,
show_body=False,
delete=True):
print("Reading mail from %s" % imap_username)
delete=True,
):
print("Reading mail from {imap_username}")
message = None
@@ -74,49 +83,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,71 +148,108 @@ 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,
)
_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('--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 = 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-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

@@ -14,12 +14,17 @@
# 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 +33,8 @@ pkgs.nixosTest {
virtualisation.memorySize = 1500;
environment.systemPackages = with pkgs; [ netcat ];
services.rsyslogd = {
enable = true;
defaultConfig = ''
@@ -83,9 +90,9 @@ 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
@@ -180,8 +187,7 @@ pkgs.nixosTest {
};
};
testScript = { nodes, ... }:
''
testScript = ''
start_all()
server.wait_for_unit("multi-user.target")
@@ -189,10 +195,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 +228,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,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/>
{ 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 = {
@@ -81,14 +82,12 @@ pkgs.nixosTest {
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
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
@@ -272,7 +271,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 +285,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 +300,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 +340,7 @@ pkgs.nixosTest {
};
};
testScript = { nodes, ... }:
''
testScript = ''
start_all()
server.wait_for_unit("multi-user.target")
@@ -350,7 +348,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 +365,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 +393,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 +416,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 +425,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 +436,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 +445,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 +456,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 +465,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 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 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,11 +491,9 @@ 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 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 dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
with subtest("dmarc reporting"):
server.systemctl("start rspamd-dmarc-reporter.service")
@@ -505,10 +501,13 @@ pkgs.nixosTest {
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 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 -v 'FTS Xapian: Box is empty' | grep -i warning >&2"
"journalctl -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 {
@@ -36,10 +39,11 @@ let
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,7 +54,12 @@ pkgs.nixosTest {
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')];
'')
] ++ (with pkgs; [
curl
openssl
netcat
]);
mailserver = {
enable = true;
@@ -174,22 +183,22 @@ pkgs.nixosTest {
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 '554 5.5.0 Error'"
"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'"
)
'';
}

View File

@@ -1,16 +1,13 @@
{ pkgs ? import <nixpkgs> {}
, ...
}:
let
bindPassword = "unsafegibberish";
alicePassword = "testalice";
bobPassword = "testbob";
in
pkgs.nixosTest {
{
name = "ldap";
nodes = {
machine = { config, pkgs, ... }: {
machine = { pkgs, ... }: {
imports = [
./../default.nix
./lib/config.nix
@@ -20,7 +17,7 @@ pkgs.nixosTest {
services.openssh = {
enable = true;
permitRootLogin = "yes";
settings.PermitRootLogin = "yes";
};
environment.systemPackages = [
@@ -104,6 +101,10 @@ pkgs.nixosTest {
searchScope = "sub";
};
forwards = {
"bob_fw@example.com" = "bob@example.com";
};
vmailGroupName = "vmail";
vmailUID = 5000;
@@ -179,5 +180,39 @@ pkgs.nixosTest {
"--dst-password-file <(echo '${bobPassword}')",
"--ignore-dkim-spf"
]))
with subtest("Test mail forwarding 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 from forwarded address"):
machine.fail(" ".join([
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--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'")
'';
}

View File

@@ -1,3 +1,3 @@
{
security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot
security.dhparams.defaultBitSize = 2048; # minimum size required by dovecot
}

View File

@@ -14,18 +14,14 @@
# 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> {
{
name = "minimal";
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
./../default.nix
];
nodes.machine = {
imports = [ ./../default.nix ];
};
testScript =
''
testScript = ''
machine.wait_for_unit("multi-user.target");
'';
}

View File

@@ -1,6 +1,9 @@
# This tests is used to test features requiring several mail domains.
{ pkgs ? import <nixpkgs> {}, ...}:
{
pkgs,
...
}:
let
hashPassword = password: pkgs.runCommand
@@ -12,8 +15,9 @@ let
password = pkgs.writeText "password" "password";
domainGenerator = domain: { config, pkgs, ... }: {
domainGenerator = domain: { pkgs, ... }: {
imports = [../default.nix];
environment.systemPackages = with pkgs; [ netcat ];
virtualisation.memorySize = 1024;
mailserver = {
enable = true;
@@ -30,19 +34,15 @@ let
};
services.dnsmasq = {
enable = true;
# Fixme: once nixos-22.11 has been removed, could be replaced by
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
extraConfig = ''
mx-host=domain1.com,domain1,10
mx-host=domain2.com,domain2,10
'';
settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
};
};
in
pkgs.nixosTest {
{
name = "multiple";
nodes = {
domain1 = {...}: {
imports = [
@@ -55,7 +55,7 @@ pkgs.nixosTest {
};
};
domain2 = domainGenerator "domain2.com";
client = { config, pkgs, ... }: {
client = { pkgs, ... }: {
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
@@ -70,10 +70,10 @@ 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

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