Compare commits

...

162 commits

Author SHA1 Message Date
a98a93cf22 ci: deploy upstream on changes
All checks were successful
Build / deploy (push) Successful in 8s
2024-08-09 20:55:49 +01:00
806a4cfd21 test: Checking if virtual aliases are functional.
Relates to https://gitlab.skynet.ie/compsoc1/skynet/nixos/-/issues/22

test: Remove the account type limiatation
2024-07-21 13:12:53 +01:00
Sandro Jäckel
059b50b2e7
Allow setting userAttrs to empty string
This allows overwriting the default values for user_attrs to be empty
which is required when using virtual mailboxes with ldap accounts
that have posixAccount attributes set. When user_attrs is empty string
those are ignored then.
2024-07-16 11:15:14 +02:00
Isabel
290a995de5
refactor: policyd-spf -> spf-engine 2024-06-18 09:03:27 +01:00
isabel
54cbacb6eb
chore: remove flake utils 2024-06-14 21:52:49 +01:00
Antoine Eiche
29916981e7 Release 24.05 2024-06-11 07:36:43 +02:00
RoastedCheese
0d51a32e47 acme: test acmeCertificateName if module is enabled 2024-06-04 15:31:28 +00:00
Martin Weinelt
ed80b589d3
postfix: remove deprecated smtpd_tls_eecdh_grade
Causes a warning that suggests to just leave it at its default.
2024-06-03 12:34:43 +02:00
Matthew Leach
46a0829aa8 acme: Add new option acmeCertificateName
Allow the user to specify the name of the ACME configuration that the
mailserver should use. This allows users that request certificates that
aren't the FQDN of the mailserver, for example a wildcard certificate.
2024-05-31 09:53:32 +01:00
jopejoe1
41059fc548 docs: use settings instead of config in radicale 2024-05-03 09:14:16 +02:00
Sandro Jäckel
ef4756bcfc Quote ldap password
Otherwise special characters like # do not work
2024-04-28 10:02:48 +00:00
Sandro
9f6635a035 Drop default acmeRoot 2024-04-13 12:42:45 +00:00
Antoine Eiche
79c8cfcd58 Remove the support of 23.05 and 23.11
This is because SNM now supports the new sieve nixpkgs interface,
which is not backward compatible with previous releases.
2024-03-14 21:51:05 +01:00
Gaetan Lepage
799fe34c12 Update nixpkgs 2024-03-14 21:51:05 +01:00
Gaetan Lepage
d507bd9c95 dovecot: no longer need to copy sieve scripts 2024-03-14 21:50:46 +01:00
Raito Bezarius
fe6d325397 dovecot: support new sieve API in nixpkgs
Since https://github.com/NixOS/nixpkgs/pull/275031 things have became more structured
when it comes to the sieve plugin.

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

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

For information about SMTP Smuggling:

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

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

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

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

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

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

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

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

Simply providing it in $PATH is easier.
2023-06-29 20:54:57 +02:00
Antoine Eiche
24128c3052 Release 23.05 2023-06-22 21:31:07 +02:00
Antoine Eiche
c4ec122aac readme: remove the announcement public key
Current maintainer no longer has it.
2023-06-11 17:10:19 +02:00
Antoine Eiche
131c48de9b Preserve the compatibility with nixos-22.11 2023-06-11 17:10:14 +02:00
Antoine Eiche
290d00f6db Improve the certificateScheme number deprecation warning message 2023-06-11 07:29:18 +00:00
Mynacol
7e09d8f537 docs: add submissions DNS record for autodiscovery
Add the submissions autodiscovery SRV DNS record for implicit TLS in
SMTP (submission) connections according to
[RFC 8314](https://www.rfc-editor.org/rfc/rfc8314#section-5.1).
2023-05-29 15:09:08 +02:00
Antoine Eiche
1bcfcf786b Remove the NixOS 22.11 support
Because the option `nodes.domain1.services.dnsmasq.settings' does not
exist.
2023-05-24 23:37:17 +02:00
Naïm Favier
a948c49ca7 Allow using existing ACME certificates
Add a certificate scheme for using an existing ACME certificate without
setting up Nginx.

Also use names instead of magic numbers for certificate schemes.
2023-05-24 21:10:02 +00:00
Naïm Favier
42c5564791 tests: use services.dnsmasq.settings
Gets rid of the warning about `extraConfig` being deprecated.
2023-05-24 21:10:02 +00:00
Antoine Eiche
fd605a419b Fix test names 2023-05-24 23:06:29 +02:00
Lafiel
d8131ffc61 dovecot: split passdb and userdb 2023-05-23 20:41:36 +00:00
Maximilian Bosch
bd99079363 mail-server/dovecot: also learn spam/ham on APPEND
The current configuration doesn't work when moving spam from the INBOX
to Junk on a local maildir and then syncing the result to the IMAP
server with `mbsync(1)`. This is because `mbsync(1)` doesn't support a
mvoe-detection[1] (i.e. an IMAP MOVE which subsequently causes a Sieve
COPY according to RFC6851 which then triggers report{h,sp}am.sieve), but
instead sends `APPEND` (and removes the message in the src mailbox after
that).

Tested on my own mailserver that this fixes spam learning.

This doesn't work the other way round though because `APPEND` doesn't
have an origin. However, learning mails as spam happens more often than
learning spam as ham, so this is IMHO still useful.

[1] https://sourceforge.net/p/isync/mailman/isync-devel/thread/87y2p1tihz.fsf%40ericabrahamsen.net/#msg37030483
2023-05-23 19:49:59 +00:00
Juergen Fitschen
c04e4f22da opendkim: make public key world-readable 2023-05-14 07:11:48 +00:00
Maximilian Bosch
e2ca6e45f3 docs: add instructions for rfc6186-compliant setup 2023-05-14 07:08:27 +00:00
Naïm Favier
6d0d9fb966
Update nixpkgs
Option values are now rendered correctly as Nix thanks to
https://github.com/NixOS/nixpkgs/pull/199363
2022-12-22 20:45:03 +01:00
Naïm Favier
0bbb2ac74e
docs: drop options.md from the repository
Generate the file on the readthedocs builder using Nix. Since there is
no root access or user namespaces, we have to use proot (see
https://nixos.wiki/wiki/Nix_Installation_Guide#PRoot).
2022-12-22 20:45:03 +01:00
Naïm Favier
4fcab839d7
docs: use MarkDown for option docs 2022-12-22 20:45:01 +01:00
Antoine Eiche
bc667fb6af Release 22.11 2022-12-21 22:46:04 +01:00
Antoine Eiche
31eadb6388 doc: regenerate it 2022-11-30 21:03:13 +01:00
Antoine Eiche
033b3d2a45 Removing 22.05 release
Because of some incompabilities with the 22.11 release.
2022-11-30 20:59:39 +01:00
Naïm Favier
694e7d34f6
docs: option docs improvements
- add missing description and defaultText fields
- add dmarcReporting option group
- render examples
2022-11-30 12:30:29 +01:00
Martin Weinelt
fe36e7ae0d rspamd: allow configuring dmarc reporting
Enabling collects DMARC results in Redis and sends out aggregated
reports (RUA) on a daily basis.
2022-11-27 20:34:38 +00:00
Antoine Eiche
3f0b7a1b5c ci: pin nixpkgs to 22.05
Because hydra-cli build is currently broken on unstable.
2022-11-27 20:43:25 +01:00
Antoine Eiche
737eb4f398 docs: explicitly mention a reverse DNS entry is required
Fixes https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/234
2022-11-27 19:14:52 +00:00
Linus Heckemann
a40e9c3abb htpasswd -> mkpasswd 2022-11-27 19:14:22 +00:00
Martin Weinelt
004c229ca4
Convert minimal test to python test driver 2022-07-19 23:54:04 +02:00
Antoine Eiche
f535d8123c Release 22.05 2022-06-22 22:39:06 +02:00
Ryan Mulligan
15cf252a0d monit/rspamd: monitor by process name 2022-05-24 20:15:37 +00:00
Niklas Hambüchen
6284a20f77 acme: Switch from postRun to reloadServices to fix hangs. Fixes #232 2022-05-24 20:11:52 +00:00
Ryan Mulligan
4396125ebb docs/full text search: fix typo; improve ux
docecot -> dovecot

Also, `indexDir` is not expecting to see %d/%n being passed to that
parameter, so remove that to make it easier to cpy the path into
there.
2022-05-08 16:02:12 -07:00
Fatih Altinok
4ce864f52a Fix typo in title 2022-04-16 18:17:48 +00:00
Guillaume Girol
75728d2686 tests: compatibility with fts xapian 1.5.4 2022-03-05 12:00:00 +00:00
Guillaume Girol
7de138037f docs: add how-to to setup roundcube 2022-02-26 17:06:52 +00:00
Antoine Eiche
021b5c8f73 ci: enable the nix-command feature 2022-02-25 09:24:52 +01:00
Naïm Favier
46ef908c91
rspamd: set default port for redis
Since we are now using services.redis.servers.rspamd, the port defaults
to 0 (i.e. do not bind a TCP socket). We still want rspamd to connect to
redis via TCP, so set a default port that is one above the default redis port.
2022-02-24 22:06:20 +01:00
Naïm Favier
53af883255 Regenerate options.rst 2022-02-24 20:51:40 +00:00
Naïm Favier
4ed684481b Update nixos-unstable and drop 21.11 2022-02-24 20:51:40 +00:00
Naïm Favier
f4c14572fc Drop 21.05 branch 2022-02-24 20:51:40 +00:00
Naïm Favier
ef03562eba make option documentation compatible with nixos-search 2022-02-24 20:51:40 +00:00
Antoine Eiche
11ad4742aa Fix CI job because of Nix new CLI options 2022-02-24 20:49:27 +00:00
Antoine Eiche
665aa181e6 ci: make release-21.11 a flake job 2022-02-20 11:29:33 +01:00
Antoine Eiche
6e3a7b2ea6 Release nixos-21.11 2021-12-07 22:09:14 +01:00
Izorkin
f3d967f830
nginx: generate certificates for custom domains and subdomains 2021-12-05 20:53:21 +03:00
Kerstin Humm
7c7ed5ce06 Revert "rspamd: make sure redis is started over TCP socket"
This reverts commit 4f0f0128d8.

Redis does seem to run fine with both unixSocket and TCP enabled. This
broke people's setups.
2021-12-01 01:01:03 +01:00
Lionello Lunesu
822c5f22bd Fix fullTextSearch.enable=false 2021-11-26 04:57:43 +00:00
DwarfMaster
4f0f0128d8 rspamd: make sure redis is started over TCP socket 2021-11-17 17:59:32 +01:00
Lionello Lunesu
6e8142862f opendkim: don't recreate keys if private key is present 2021-11-07 19:57:12 +00:00
Guillaume Girol
a13526a6e3 nginx.nix: don't reload nginx
Fixes #227

Reloading nginx manually is actually not needed (see
nginx-config-reload.service) and causes deadlocks.
2021-11-07 19:10:00 +00:00
Antoine Eiche
9d3a87905e docs: add .readthedocs.yml conf file to pin Python dependencies 2021-11-07 11:13:06 +01:00
Lionello
ef8ca96c5d Fix typos in indexDir example 2021-11-01 23:18:18 +00:00
Ero Sennin
0d9a880c0e Set DKIM policy to relaxed/relaxed
And make this policy configurable.
2021-10-14 18:45:21 +00:00
Antoine Eiche
acaba31d8f docs: fix the test which could never fail 2021-10-14 09:07:32 +02:00
Antoine Eiche
74bb227990 docs: remove output paths from generated documentation
Otherwise, the `testRstOptions` test would fail too often!
2021-10-14 09:06:14 +02:00
Steve Purcell
fb85a3fe9e Ensure locally-delivered mails have the X-Original-To header
See #223
2021-08-11 12:20:16 +00:00
Antoine Eiche
72748d7b6d Use the Junk mailbox name defined in the mailboxes attrs
Previously, the static Junk mailbox was used in sieve script to move
spam messages. This patch gets the Junk mailbox defined in the dovecot
mailboxes attribute instead.

Fixes #224
2021-08-06 16:21:03 +00:00
Antoine Eiche
42db23553d Nixify the documentation build 2021-07-28 21:35:58 +02:00
Antoine Eiche
68b9397a30 Move the logo 2021-07-27 19:58:33 +00:00
Antoine Eiche
4d087532b6 docs: generate the list of options
To generate the list of options, we need to generate and commit a rst
file to make all files available for ReadTheDoc.

An Hydra test ensures this generated file is up-to-date. If it is not
up-to-date, the error message explains the user how to generate it:
the user just needs to run `nix-shell --run generate-rst-options`.
2021-07-27 19:58:33 +00:00
Antoine Eiche
9578dbac69 Remove non longer supported configurations (<21.05) 2021-07-24 09:57:44 +02:00
Antoine Eiche
864ea5bfef Update nixpkgs-unstable 2021-07-24 09:42:30 +02:00
Antoine Eiche
a37dac9d66 ci: reenable 20.09 and 21.05 jobs :/
They haven't been moved to flake so we still need to keep the non
flake Hydra configuration.
2021-07-12 23:28:02 +02:00
Antoine Eiche
2fa9c7c4df tests: update fts indexer log messages 2021-07-12 22:57:01 +02:00
Antoine Eiche
87735ed077 Remove Niv
It is now useless since we are using Nix Flakes
2021-07-12 22:57:01 +02:00
Antoine Eiche
a0f9688a31 Switch CI to Nix flakes
We also move tests to Flakes.

This would allow users to submit PRs with a fork of nixpkgs when they
want to test nixpkgs PRs against SNM.
2021-07-12 22:57:01 +02:00
Antoine Eiche
a9f87ca461 Update nixpkgs-unstable
Because of
b7749c7671
we need to `set +o pipefail` several asserts.
2021-06-24 23:02:58 +02:00
Antoine Eiche
5675b122a9 readme: switch from freenode to libera 2021-06-06 10:21:14 +02:00
Antoine Eiche
92a0939896 ci: simplify declarative-jobsets.nix 2021-06-06 10:20:14 +02:00
Antoine Eiche
bbcc6863b5 Release nixos-21.05 2021-06-06 10:20:14 +02:00
Antoine Eiche
ddafdfbde7 Make Niv working in restricted evaluation mode 2021-06-06 09:54:03 +02:00
Antoine Eiche
3fc047bc64 Remove nixos-20.03 job
We only support 2 releases.
2021-06-06 09:44:41 +02:00
Robert Schütz
49074b7835 kresd: no need to explicitly set nameserver
Since https://github.com/NixOS/nixpkgs/pull/124391, enabling kreasd also
sets `networking.resolvconf.useLocalResolver = true`.
2021-06-03 05:58:42 +00:00
Antoine Eiche
2ca02f32c8 hydra: provide nixpkgs to allow Niv to use pkgs.fetchzip 2021-05-31 09:53:52 +02:00
Evan Hanson
190ac7ca60 Remove duplicate default attribute on mailserver.forwards option 2021-05-31 18:29:11 +12:00
Antoine Eiche
500685bc38 hydra: remove useless declInput argument 2021-05-27 23:03:20 +02:00
Antoine Eiche
2eab26e05c Switch from Freenode to Libera 2021-05-27 09:00:29 +02:00
Luflosi
37376efbbf
docs: link to an english Wikipedia article instead of a french one 2021-05-23 20:15:38 +02:00
Antoine Eiche
8b28705621 Rename intern/extern tests to internal/external 2021-05-01 08:21:27 +02:00
Guillaume Girol
5248dce1ea tests: increase memory limit for indexer process
otherwise fts-xapian with recent versions (1.4.9 at least) prints a
warning and the test fails
2021-04-24 17:01:52 +02:00
Antoine Eiche
f4c8d4b298 Update nixpkgs-unstable 2021-04-18 18:58:44 +02:00
Milan Pässler
9c80a66f57
Make vmail_user a system user
This is required since https://github.com/NixOS/nixpkgs/pull/115332
2021-04-18 15:41:05 +02:00
Stefan Ladwig
3069998c0f corrected some pasting 2021-04-12 20:32:47 +00:00
Antoine Eiche
93330c5453 Move indexDir option to the mailserver scope
This option has been initially in the mailserver.fullTextSearch
scope. However, this option modifies the location of all index files
of dovecot and not only those used by the full text search feature. It
is then more relevant to have this option in the mailserver top level
scope.

Moreover, the default option has been changed to null in order to keep
existing index files where they are: changing the index location means
recreating all index files. The fts documentation however recommend to
change this default location when enabling the fts feature.
2021-04-07 22:22:38 +02:00
Antoine Eiche
66e8baa6f2 Rework the setup guide 2021-03-23 18:40:44 +00:00
Emmanouil Kampitakis
d75614a653 Feature/configurable delimiter 2021-03-22 19:05:34 +00:00
Matt Votava
d0a2e74574 Use services.clamav.daemon.settings if it is available 2021-03-21 14:32:54 +00:00
Antoine Eiche
06cf3557df Mention the Freenode IRC chan #nixos-mailserver 2021-03-10 18:46:03 +01:00
Andreas Rammhold
7627c29268
Store FTS index in directory per domain & user to avoid collisions
Previously all the xapian files and logs would be stored in the same
folder for all users. This couid probably lead to weird situations where
all users get the same search results.
2021-03-07 11:26:35 +01:00
Guillaume Girol
49d65a4d05 add doc for full text search 2021-03-04 22:19:03 +01:00
Symphorien Gibol
06b989c1e7 add full text search support 2021-03-04 22:17:25 +01:00
Antoine Eiche
326766126c doc: minor improvments 2021-03-03 08:36:08 +00:00
Antoine Eiche
548e6b5a04 doc: add a FAQ section 2021-03-03 08:36:08 +00:00
Antoine Eiche
7e84fd4c93 doc: add a section howto
This section contains advanced configuration howtos.
2021-03-03 08:36:08 +00:00
Simon Žlender
0c4b9a8985 Make opening ports in the firewall optional 2021-02-09 21:09:36 +01:00
Antoine Eiche
5f431207b3 postfix: forwarding emails of login accounts with keeping local copy
When a local account address is forwarded, the mails were not locally
kept. This was due to the way lookup tables were internally managed.

Instead of using lists to represent Postfix lookup tables, we now use
attribute sets: they can then be easily merged.

A regression test for
https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/
has been added: it sets a forward on a local address and ensure an
email sent to this address is locally kept.

Fixes #205
2021-02-06 08:17:43 +00:00
Izorkin
17eec31cae rspamd: disable dkim signing 2021-01-31 19:36:07 +00:00
Antoine Eiche
ee3d38a157 Set mailserver.policydSPFExtraConfig in a debug module
The line type of this option make its concatenation cleaner: the user
doesn't have to manually add `\n` to its policydSPFExtraConfig value
when debug in set.
2020-12-23 09:39:55 +01:00
Naïm Favier
ae89eafb81
add flake support 2020-12-15 16:14:44 +01:00
Naïm Favier
7c06f610f1 Update systemd.nix 2020-12-04 08:20:25 +00:00
Naïm Favier
de84ba1aeb Do not hardcode paths to acme certificates 2020-11-30 19:49:48 +00:00
Antoine Eiche
bee80564d8 ci: simplify the hydra-cli call
The version 0.0.3 of hydra-cli prints the jobset details on error.
2020-11-30 08:56:57 +01:00
Antoine Eiche
4ce3e1bf4e readme: mention the unstable documentation 2020-11-30 08:55:26 +01:00
Henri Menke
89bd89c706 Recommend bcrypt passwords everywhere 2020-11-29 20:19:46 +01:00
Henri Menke
c00fc587f5
Configurable sieve script directory 2020-11-21 14:39:49 +01:00
Antoine Eiche
ee1ad50830 Add 20.09 Hydra jobset and remove 19.09 2020-11-20 09:12:15 +01:00
Antoine Eiche
7d2020cb36 Move clamav database to the blobs repository
This database is huge and can be fetched at build time.

Fixes #197
2020-11-11 20:27:59 +01:00
Antoine Eiche
c04260cf5e Update nixpkgs-unstable 2020-10-31 08:34:36 +01:00
Antoine Eiche
99f843de47 Release nixos-20.09 branch 2020-10-31 08:34:36 +01:00
Antoine Eiche
bb9fd8bc17 docs: add missing Sphinx Makefile:/ 2020-10-31 08:34:36 +01:00
Antoine Eiche
843e66864f docs: no longer use tagged release but branch instead in docs 2020-10-31 08:34:36 +01:00
Niklas Hambüchen
eba19686fb setup-guide: Improve commands/outputs 2020-10-22 22:40:31 +02:00
Antoine Eiche
4818b57a92 test.dovecot: ensure port 143 is closed when enableImap is not set
The test also checks the connection on the imap port 993 is a SSL
connection.
2020-10-05 21:18:36 +02:00
Milan Pässler
beba28ae14 add release notes for tls wrapped-mode changes 2020-10-05 20:54:46 +02:00
Milan Pässler
e272a2755b remove support for 20.03 2020-10-05 20:54:46 +02:00
Milan Pässler
cc526a2700 add full support for tls wrapped mode 2020-10-05 20:54:46 +02:00
Antoine Eiche
823c26fa69 Update nixpkgs-unstable 2020-10-04 10:54:23 +02:00
Antoine Eiche
9d7f02e67b Support sandboxed opendkim 2020-10-04 10:49:57 +02:00
Antoine Eiche
c813f1205f Add multiple.nix test
This test is used to test feature requiring several mail domains, such
as the `forwards` option.
2020-09-28 20:51:32 +02:00
Antoine Eiche
24600377af Add forwards option
This option allow to forward mails to external addresses.
2020-09-28 20:50:45 +02:00
James ‘Twey’ Kay
5cd6f8e7b3 Add a separate sendingFqdn option 2020-09-18 21:38:15 +00:00
Matt Votava
358cfcdfbe Declare default dovecot2 mailboxes as attrset for 20.09+ 2020-09-14 10:49:32 -07:00
Matt Votava
e2ed4541d4 remove deprecated types.loaOf 2020-09-13 06:12:14 -07:00
Antoine Eiche
4008d0cb53 Move tests to the Python framework 2020-07-27 23:11:54 +02:00
Xavier Vello
6ad2004ed1 Add rspamd documentation page 2020-07-09 00:18:04 +02:00
Xavier Vello
45f80def41 Setup rspamd controller to serve web UI assets 2020-07-06 23:14:33 +02:00
Antoine Eiche
31cf3818df readme: switch doc links from wiki to readthedocs 2020-07-06 22:33:19 +02:00
Antoine Eiche
8db0e18438 docs: how to contribute to documentation 2020-07-06 22:33:19 +02:00
62 changed files with 3050 additions and 1136 deletions

View file

@ -0,0 +1,17 @@
name: Build
on:
push:
branches:
- 'master'
jobs:
# deploy it upstream
deploy:
runs-on: docker
steps:
- name: "Deploy to Skynet"
uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
with:
input: 'simple-nixos-mailserver'
token: ${{ secrets.API_TOKEN_FORGEJO }}

View file

@ -3,11 +3,11 @@ hydra-pr:
- merge_requests - merge_requests
image: nixos/nix image: nixos/nix
script: script:
- nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} || (echo Go to https://hydra.nix-community.org/jobset/simple-nixos-mailserver/${CI_MERGE_REQUEST_IID}/latest-eval for details; exit 1) - 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}'
hydra-master: hydra-master:
only: only:
- master - master
image: nixos/nix image: nixos/nix
script: script:
- nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master || (echo Go to https://hydra.nix-community.org/jobset/simple-nixos-mailserver/master/latest-eval for details; exit 1) - 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'

View file

@ -1,4 +1,4 @@
{ nixpkgs, declInput, pulls }: { nixpkgs, pulls, ... }:
let let
pkgs = import nixpkgs {}; pkgs = import nixpkgs {};
@ -8,85 +8,37 @@ let
{ enabled = 1; { enabled = 1;
hidden = false; hidden = false;
description = "PR ${num}: ${info.title}"; description = "PR ${num}: ${info.title}";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
checkinterval = 30; checkinterval = 30;
schedulingshares = 20; schedulingshares = 20;
enableemail = false; enableemail = false;
emailoverride = ""; emailoverride = "";
keepnr = 1; keepnr = 1;
type = 0; type = 1;
inputs = { flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
snm = {
type = "git";
value = "${info.target_repo_url} merge-requests/${info.iid}/head";
emailresponsible = false;
};
};
} }
) prs; ) prs;
mkFlakeJobset = branch: {
description = "Build ${branch} branch of Simple NixOS MailServer";
checkinterval = "60";
enabled = "1";
schedulingshares = 100;
enableemail = false;
emailoverride = "";
keepnr = 3;
hidden = false;
type = 1;
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}";
};
desc = prJobsets // { desc = prJobsets // {
master = { "master" = mkFlakeJobset "master";
description = "Build master branch of Simple NixOS MailServer"; "nixos-23.11" = mkFlakeJobset "nixos-23.11";
checkinterval = "60"; "nixos-24.05" = mkFlakeJobset "nixos-24.05";
enabled = "1";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
schedulingshares = 100;
enableemail = false;
emailoverride = "";
keepnr = 3;
hidden = false;
type = 0;
inputs = {
snm = {
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master";
type = "git";
emailresponsible = false;
};
};
};
"nixos-20.03" = {
description = "Build the nixos-20.03 branch of Simple NixOS MailServer";
checkinterval = "60";
enabled = "1";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
schedulingshares = 100;
enableemail = false;
emailoverride = "";
keepnr = 3;
hidden = false;
type = 0;
inputs = {
snm = {
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.03";
type = "git";
emailresponsible = false;
};
};
};
"nixos-19.09" = {
description = "Build the nixos-19.09 branch of Simple NixOS MailServer";
checkinterval = "60";
enabled = "1";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
schedulingshares = 100;
enableemail = false;
emailoverride = "";
keepnr = 3;
hidden = false;
type = 0;
inputs = {
snm = {
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-19.09";
type = "git";
emailresponsible = false;
};
};
}; };
log = {
pulls = prs;
jobsets = desc;
}; };
in { in {
@ -94,5 +46,10 @@ in {
cat >$out <<EOF cat >$out <<EOF
${builtins.toJSON desc} ${builtins.toJSON desc}
EOF EOF
# This is to get nice .jobsets build logs on Hydra
cat >tmp <<EOF
${builtins.toJSON log}
EOF
${pkgs.jq}/bin/jq . tmp
''; '';
} }

29
.readthedocs.yaml Normal file
View file

@ -0,0 +1,29 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
build:
os: ubuntu-22.04
tools:
python: "3"
apt_packages:
- nix
- proot
jobs:
pre_install:
- mkdir -p ~/.nix ~/.config/nix
- echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf
- proot -b ~/.nix:/nix /bin/sh -c "nix build -L .#optionsDoc && cp -v result docs/options.md"
sphinx:
configuration: docs/conf.py
formats:
- pdf
- epub
python:
install:
- requirements: docs/requirements.txt

103
README.md
View file

@ -8,25 +8,21 @@
For each NixOS release, we publish a branch. You then have to use the For each NixOS release, we publish a branch. You then have to use the
SNM branch corresponding to your NixOS version. SNM branch corresponding to your NixOS version.
* For NixOS 20.03 * For NixOS 24.05
- Use the [SNM branch `nixos-20.03`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.03) - Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05)
- [Release notes](#nixos-2003) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/)
* For NixOS 19.09 - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05)
- Use the [SNM branch `nixos-19.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-19.09) * 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 unstable * For NixOS unstable
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
- This branch is currently still supporting the NixOS release 20.03 - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
but we could remove this support on any NixOS unstable breaking
change.
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm) [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 This is a very low volume list where new releases of SNM are announced, so you
can stay up to date with bug fixes and updates. All announcements are signed by can stay up to date with bug fixes and updates.
the gpg key with fingerprint
```
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
```
## Features ## Features
@ -35,12 +31,15 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
* [x] Multiple Domains * [x] Multiple Domains
* Postfix MTA * Postfix MTA
- [x] smtp on port 25 - [x] smtp on port 25
- [x] submission port 587 - [x] submission tls on port 465
- [x] submission starttls on port 587
- [x] lmtp with dovecot - [x] lmtp with dovecot
* Dovecot * Dovecot
- [x] maildir folders - [x] maildir folders
- [x] imap starttls on port 143 - [x] imap with tls on port 993
- [x] pop3 starttls on port 110 - [x] pop3 with tls on port 995
- [x] imap with starttls on port 143
- [x] pop3 with starttls on port 110
* Certificates * Certificates
- [x] manual certificates - [x] manual certificates
- [x] on the fly creation - [x] on the fly creation
@ -67,74 +66,20 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
* DKIM Signing * DKIM Signing
- [ ] Allow a per domain selector - [ ] Allow a per domain selector
### Changelog and How to Stay Up-to-Date ### Get in touch
See the [mailing list archive](https://www.freelists.org/archive/snm/)
### Quick Start
```nix
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball {
# Pick a commit from the branch you are interested in
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
# And set its hash
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [
"info@example.com"
"postmaster@example.com"
"postmaster@example2.com"
];
};
};
};
}
```
For a complete list of options, see `default.nix`.
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
- Join the Libera Chat IRC channel `#nixos-mailserver`
## How to Set Up a 10/10 Mail Server Guide ## How to Set Up a 10/10 Mail Server Guide
Check out the [Complete Setup Guide](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/wikis/A-Complete-Setup-Guide) in the project's wiki.
## How to Backup Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
Checkout the [Complete Backup Guide](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/wikis/A-Complete-Backup-Guide). Backups are easy with `SNM`. For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
## Development ## Development
See the [How to Develop SNM](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/wikis/How-to-Develop-SNM) wiki page. See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
## Release notes
### nixos-20.03
- Rspamd is upgraded to 2.0 which deprecates the SQLite Bayes
backend. We then moved to the Redis backend (the default since
Rspamd 2.0). If you don't want to relearn the Redis backend from the
scratch, we could manually run
rspamadm statconvert --spam-db /var/lib/rspamd/bayes.spam.sqlite --ham-db /var/lib/rspamd/bayes.ham.sqlite -h 127.0.0.1:6379 --symbol-ham BAYES_HAM --symbol-spam BAYES_SPAM
See the [Rspamd migration
notes](https://rspamd.com/doc/migration.html#migration-to-rspamd-20)
and [this SNM Merge
Request](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/164)
for details.
## Contributors ## Contributors
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master) See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
@ -149,6 +94,4 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails
* Logo made with [Logomakr.com](https://logomakr.com) * Logo made with [Logomakr.com](https://logomakr.com)
[logo]: docs/logo.png
[logo]: logo/logo.png

View file

@ -1,4 +1,3 @@
# nixos-mailserver: a simple mail server # nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond # Copyright (C) 2016-2018 Robin Raymond
# #
@ -26,6 +25,12 @@ in
options.mailserver = { options.mailserver = {
enable = mkEnableOption "nixos-mailserver"; enable = mkEnableOption "nixos-mailserver";
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Automatically open ports in the firewall.";
};
fqdn = mkOption { fqdn = mkOption {
type = types.str; type = types.str;
example = "mx.example.com"; example = "mx.example.com";
@ -39,6 +44,17 @@ in
description = "The domains that this mail server serves."; description = "The domains that this mail server serves.";
}; };
certificateDomains = mkOption {
type = types.listOf types.str;
example = [ "imap.example.com" "pop3.example.com" ];
default = [];
description = ''
({option}`mailserver.certificateScheme` == `acme-nginx`)
Secondary domains and subdomains for which it is necessary to generate a certificate.
'';
};
messageSizeLimit = mkOption { messageSizeLimit = mkOption {
type = types.int; type = types.int;
example = 52428800; example = 52428800;
@ -47,7 +63,7 @@ in
}; };
loginAccounts = mkOption { loginAccounts = mkOption {
type = types.loaOf (types.submodule ({ name, ... }: { type = types.attrsOf (types.submodule ({ name, ... }: {
options = { options = {
name = mkOption { name = mkOption {
type = types.str; type = types.str;
@ -63,11 +79,11 @@ in
The user's hashed password. Use `mkpasswd` as follows The user's hashed password. Use `mkpasswd` as follows
``` ```
mkpasswd -m sha-512 "super secret password" nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
``` ```
Warning: this is stored in plaintext in the Nix store! Warning: this is stored in plaintext in the Nix store!
Use `hashedPasswordFile` instead. Use {option}`mailserver.loginAccounts.<name>.hashedPasswordFile` instead.
''; '';
}; };
@ -79,7 +95,7 @@ in
A file containing the user's hashed password. Use `mkpasswd` as follows A file containing the user's hashed password. Use `mkpasswd` as follows
``` ```
mkpasswd -m sha-512 "super secret password" nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
``` ```
''; '';
}; };
@ -95,6 +111,15 @@ in
''; '';
}; };
aliasesRegexp = mkOption {
type = with types; listOf types.str;
example = [''/^tom\..*@domain\.com$/''];
default = [];
description = ''
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
'';
};
catchAll = mkOption { catchAll = mkOption {
type = with types; listOf (enum cfg.domains); type = with types; listOf (enum cfg.domains);
example = ["example.com" "example2.com"]; example = ["example.com" "example2.com"];
@ -144,7 +169,7 @@ in
description = '' description = ''
Specifies if the account should be a send-only account. Specifies if the account should be a send-only account.
Emails sent to send-only accounts will be rejected from Emails sent to send-only accounts will be rejected from
unauthorized senders with the sendOnlyRejectMessage unauthorized senders with the `sendOnlyRejectMessage`
stating the reason. stating the reason.
''; '';
}; };
@ -176,12 +201,255 @@ in
follows follows
``` ```
mkpasswd -m sha-512 "super secret password" nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
``` ```
''; '';
default = {}; default = {};
}; };
ldap = {
enable = mkEnableOption "LDAP support";
uris = mkOption {
type = types.listOf types.str;
example = literalExpression ''
[
"ldaps://ldap1.example.com"
"ldaps://ldap2.example.com"
]
'';
description = ''
URIs where your LDAP server can be reached
'';
};
startTls = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable StartTLS upon connection to the server.
'';
};
tlsCAFile = mkOption {
type = types.path;
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
description = ''
Certifificate trust anchors used to verify the LDAP server certificate.
'';
};
bind = {
dn = mkOption {
type = types.str;
example = "cn=mail,ou=accounts,dc=example,dc=com";
description = ''
Distinguished name used by the mail server to do lookups
against the LDAP servers.
'';
};
passwordFile = mkOption {
type = types.str;
example = "/run/my-secret";
description = ''
A file containing the password required to authenticate against the LDAP servers.
'';
};
};
searchBase = mkOption {
type = types.str;
example = "ou=people,ou=accounts,dc=example,dc=com";
description = ''
Base DN at below which to search for users accounts.
'';
};
searchScope = mkOption {
type = types.enum [ "sub" "base" "one" ];
default = "sub";
description = ''
Search scope below which users accounts are looked for.
'';
};
dovecot = {
userAttrs = mkOption {
type = types.nullOr types.str;
default = "";
description = ''
LDAP attributes to be retrieved during userdb lookups.
See the users_attrs reference at
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs
in the Dovecot manual.
'';
};
userFilter = mkOption {
type = types.str;
default = "mail=%u";
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
description = ''
Filter for user lookups in Dovecot.
See the user_filter reference at
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-filter
in the Dovecot manual.
'';
};
passAttrs = mkOption {
type = types.str;
default = "userPassword=password";
description = ''
LDAP attributes to be retrieved during passdb lookups.
See the pass_attrs reference at
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
in the Dovecot manual.
'';
};
passFilter = mkOption {
type = types.nullOr types.str;
default = "mail=%u";
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
description = ''
Filter for password lookups in Dovecot.
See the pass_filter reference for
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-filter
in the Dovecot manual.
'';
};
};
postfix = {
filter = mkOption {
type = types.str;
default = "mail=%s";
example = "(&(objectClass=inetOrgPerson)(mail=%s))";
description = ''
LDAP filter used to search for an account by mail, where
`%s` is a substitute for the address in
question.
'';
};
uidAttribute = mkOption {
type = types.str;
default = "mail";
example = "uid";
description = ''
The LDAP attribute referencing the account name for a user.
'';
};
mailAttribute = mkOption {
type = types.str;
default = "mail";
description = ''
The LDAP attribute holding mail addresses for a user.
'';
};
};
};
indexDir = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Folder to store search indices. If null, indices are stored
along with email, which could not necessarily be desirable,
especially when {option}`mailserver.fullTextSearch.enable` is `true` since
indices it creates are voluminous and do not need to be backed
up.
Be careful when changing this option value since all indices
would be recreated at the new location (and clients would need
to resynchronize).
Note the some variables can be used in the file path. See
https://doc.dovecot.org/configuration_manual/mail_location/#variables
for details.
'';
example = "/var/lib/dovecot/indices";
};
fullTextSearch = {
enable = mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
autoIndex = mkOption {
type = types.bool;
default = true;
description = "Enable automatic indexing of messages as they are received or modified.";
};
autoIndexExclude = mkOption {
type = types.listOf types.str;
default = [ ];
example = [ "\\Trash" "SomeFolder" "Other/*" ];
description = ''
Mailboxes to exclude from automatic indexing.
'';
};
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";
description = ''
Fail searches when no index is available. If set to
`body`, then only body searches (as opposed to
header) are affected. If set to `no`, searches may
fall back to a very slow brute force search.
'';
};
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.";
};
maintenance = {
enable = mkOption {
type = types.bool;
default = true;
description = "Regularly optmize indices, as recommended by upstream.";
};
onCalendar = mkOption {
type = types.str;
default = "daily";
description = "When to run the maintenance job. See systemd.time(7) for more information about the format.";
};
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.";
};
};
};
lmtpSaveToDetailMailbox = mkOption { lmtpSaveToDetailMailbox = mkOption {
type = types.enum ["yes" "no"]; type = types.enum ["yes" "no"];
default = "yes"; default = "yes";
@ -193,14 +461,11 @@ in
}; };
extraVirtualAliases = mkOption { extraVirtualAliases = mkOption {
type = types.loaOf (mkOptionType { type = let
loginAccount = mkOptionType {
name = "Login Account"; name = "Login Account";
check = (ele: };
let accounts = builtins.attrNames cfg.loginAccounts; in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
in if (builtins.isList ele)
then (builtins.all (x: builtins.elem x accounts) ele) && (builtins.length ele > 0)
else (builtins.elem ele accounts));
});
example = { example = {
"info@example.com" = "user1@example.com"; "info@example.com" = "user1@example.com";
"postmaster@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com";
@ -221,6 +486,23 @@ in
default = {}; default = {};
}; };
forwards = mkOption {
type = with types; attrsOf (either (listOf str) str);
example = {
"user@example.com" = "user@elsewhere.com";
};
description = ''
To forward mails to an external address. For instance,
the value {`"user@example.com" = "user@elsewhere.com";}`
means that mails to `user@example.com` are forwarded to
`user@elsewhere.com`. The difference with the
{option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com`
can't send mail as `user@example.com`. Also, this option
allows to forward mails to external addresses.
'';
default = {};
};
rejectSender = mkOption { rejectSender = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
example = [ "@example.com" "spammer@example.net" ]; example = [ "@example.com" "spammer@example.net" ];
@ -248,7 +530,7 @@ in
description = '' description = ''
The unix UID of the virtual mail user. Be mindful that if this is The unix UID of the virtual mail user. Be mindful that if this is
changed, you will need to manually adjust the permissions of changed, you will need to manually adjust the permissions of
mailDirectory. `mailDirectory`.
''; '';
}; };
@ -291,6 +573,14 @@ in
''; '';
}; };
useUTF8FolderNames = mkOption {
type = types.bool;
default = false;
description = ''
Store mailbox names on disk using UTF-8 instead of modified UTF-7 (mUTF-7).
'';
};
hierarchySeparator = mkOption { hierarchySeparator = mkOption {
type = types.str; type = types.str;
default = "."; default = ".";
@ -309,46 +599,46 @@ in
The mailboxes for dovecot. The mailboxes for dovecot.
Depending on the mail client used it might be necessary to change some mailbox's name. Depending on the mail client used it might be necessary to change some mailbox's name.
''; '';
default = [ default = {
{ Trash = {
name = "Trash";
auto = "no"; auto = "no";
specialUse = "Trash"; specialUse = "Trash";
} };
Junk = {
{
name = "Junk";
auto = "subscribe"; auto = "subscribe";
specialUse = "Junk"; specialUse = "Junk";
} };
Drafts = {
{
name = "Drafts";
auto = "subscribe"; auto = "subscribe";
specialUse = "Drafts"; specialUse = "Drafts";
} };
Sent = {
{
name = "Sent";
auto = "subscribe"; auto = "subscribe";
specialUse = "Sent"; specialUse = "Sent";
} };
]; };
}; };
certificateScheme = mkOption { certificateScheme = let
type = types.enum [ 1 2 3 ]; schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
default = 2; translate = i: warn "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${(builtins.elemAt schemes (i - 1))}\"'."
(builtins.elemAt schemes (i - 1));
in mkOption {
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
default = "selfsigned";
description = '' description = ''
Certificate Files. There are three options for these. The scheme to use for managing TLS certificates:
1) You specify locations and manually copy certificates there. 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and
2) You let the server create new (self signed) certificates on the fly. {option}`mailserver.keyFile` and manually copy certificates there.
3) You let the server create a certificate via `Let's Encrypt`. Note that 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly.
this implies that a stripped down webserver has to be started. This also 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org)
implies that the FQDN must be set as an `A` record to point to the IP of via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for
the server. In particular port 80 on the server will be opened. For details {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly
on how to set up the domain records, see the guide in the readme. configured to point to your server (see the [setup guide](setup-guide.rst) for more information).
4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled
Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded
when the certificate is renewed.
''; '';
}; };
@ -356,8 +646,9 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.crt"; example = "/root/mail-server.crt";
description = '' description = ''
Scheme 1) ({option}`mailserver.certificateScheme` == `manual`)
Location of the certificate
Location of the certificate.
''; '';
}; };
@ -365,8 +656,9 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.key"; example = "/root/mail-server.key";
description = '' description = ''
Scheme 1) ({option}`mailserver.certificateScheme` == `manual`)
Location of the key file
Location of the key file.
''; '';
}; };
@ -374,32 +666,56 @@ in
type = types.path; type = types.path;
default = "/var/certs"; default = "/var/certs";
description = '' description = ''
Sceme 2) ({option}`mailserver.certificateScheme` == `selfsigned`)
This is the folder where the certificate will be created. The name is
hardcoded to "cert-<domain>.pem" and "key-<domain>.pem" and the This is the folder where the self-signed certificate will be created. The name is
hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the
certificate is valid for 10 years. certificate is valid for 10 years.
''; '';
}; };
acmeCertificateName = mkOption {
type = types.str;
default = cfg.fqdn;
example = "example.com";
description = ''
({option}`mailserver.certificateScheme` == `acme`)
When the `acme` `certificateScheme` is selected, you can use this option
to override the default certificate name. This is useful if you've
generated a wildcard certificate, for example.
'';
};
enableImap = mkOption { enableImap = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
description = '' description = ''
Whether to enable imap / pop3. Both variants are only supported in the Whether to enable IMAP with STARTTLS on port 143.
(sane) startTLS configuration. The ports are
110 - Pop3
143 - IMAP
587 - SMTP with login
''; '';
}; };
enableImapSsl = mkOption { enableImapSsl = mkOption {
type = types.bool; type = types.bool;
default = false; default = true;
description = '' description = ''
Whether to enable IMAPS, setting this option to true will open port 993 Whether to enable IMAP with TLS in wrapper-mode on port 993.
in the firewall. '';
};
enableSubmission = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable SMTP with STARTTLS on port 587.
'';
};
enableSubmissionSsl = mkOption {
type = types.bool;
default = true;
description = ''
Whether to enable SMTP with TLS in wrapper-mode on port 465.
''; '';
}; };
@ -407,12 +723,7 @@ in
type = types.bool; type = types.bool;
default = false; default = false;
description = '' description = ''
Whether to enable POP3. Both variants are only supported in the (sane) Whether to enable POP3 with STARTTLS on port on port 110.
startTLS configuration. The ports are
110 - Pop3
143 - IMAP
587 - SMTP with login
''; '';
}; };
@ -420,8 +731,7 @@ in
type = types.bool; type = types.bool;
default = false; default = false;
description = '' description = ''
Whether to enable POP3S, setting this option to true will open port 995 Whether to enable POP3 with TLS in wrapper-mode on port 995.
in the firewall.
''; '';
}; };
@ -437,6 +747,14 @@ in
''; '';
}; };
sieveDirectory = mkOption {
type = types.path;
default = "/var/sieve";
description = ''
Where to store the sieve scripts.
'';
};
virusScanning = mkOption { virusScanning = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@ -458,7 +776,7 @@ in
type = types.str; type = types.str;
default = "mail"; default = "mail";
description = '' description = ''
The DKIM selector.
''; '';
}; };
@ -466,7 +784,7 @@ in
type = types.path; type = types.path;
default = "/var/dkim"; default = "/var/dkim";
description = '' description = ''
The DKIM directory.
''; '';
}; };
@ -477,12 +795,93 @@ in
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys. How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
If you have already deployed a key with a different number of bits than specified If you have already deployed a key with a different number of bits than specified
here, then you should use a different selector (dkimSelector). In order to get here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
this package to generate a key with the new number of bits, you will either have to this package to generate a key with the new number of bits, you will either have to
change the selector or delete the old key file. change the selector or delete the old key file.
''; '';
}; };
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;
default = false;
description = ''
Whether to send out aggregated, daily DMARC reports in response to incoming
mail, when the sender domain defines a DMARC policy including the RUA tag.
This is helpful for the mail ecosystem, because it allows third parties to
get notified about SPF/DKIM violations originating from their sender domains.
See https://rspamd.com/doc/modules/dmarc.html#reporting
'';
};
localpart = mkOption {
type = types.str;
default = "dmarc-noreply";
example = "dmarc-report";
description = ''
The local part of the email address used for outgoing DMARC reports.
'';
};
domain = mkOption {
type = types.enum (cfg.domains);
example = "example.com";
description = ''
The domain from which outgoing DMARC reports are served.
'';
};
email = mkOption {
type = types.str;
default = with cfg.dmarcReporting; "${localpart}@${domain}";
defaultText = literalExpression ''"''${localpart}@''${domain}"'';
readOnly = true;
description = ''
The email address used for outgoing DMARC reports. Read-only.
'';
};
organizationName = mkOption {
type = types.str;
example = "ACME Corp.";
description = ''
The name of your organization used in the `org_name` attribute in
DMARC reports.
'';
};
fromName = mkOption {
type = types.str;
default = cfg.dmarcReporting.organizationName;
defaultText = literalMD "{option}`mailserver.dmarcReporting.organizationName`";
description = ''
The sender name for DMARC reports. Defaults to the organization name.
'';
};
};
debug = mkOption { debug = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@ -511,12 +910,20 @@ in
''; '';
}; };
recipientDelimiter = mkOption {
type = types.str;
default = "+";
description = ''
Configure the recipient delimiter.
'';
};
redis = { redis = {
address = mkOption { address = mkOption {
type = types.str; type = types.str;
# read the default from nixos' redis module # read the default from nixos' redis module
default = let default = let
cf = config.services.redis.bind; cf = config.services.redis.servers.rspamd.bind;
cfdefault = if cf == null then "127.0.0.1" else cf; cfdefault = if cf == null then "127.0.0.1" else cf;
ips = lib.strings.splitString " " cfdefault; ips = lib.strings.splitString " " cfdefault;
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]); ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
@ -525,28 +932,27 @@ in
if (ip == "0.0.0.0" || ip == "::") if (ip == "0.0.0.0" || ip == "::")
then "127.0.0.1" then "127.0.0.1"
else if isIpv6 ip then "[${ip}]" else ip; else if isIpv6 ip then "[${ip}]" else ip;
defaultText = lib.literalMD "computed from `config.services.redis.servers.rspamd.bind`";
description = '' description = ''
Address that rspamd should use to contact redis. The default value Address that rspamd should use to contact redis.
is read from <literal>config.services.redis.bind</literal>.
''; '';
}; };
port = mkOption { port = mkOption {
type = types.port; type = types.port;
default = config.services.redis.port; default = config.services.redis.servers.rspamd.port;
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
description = '' description = ''
Port that rspamd should use to contact redis. The default value is Port that rspamd should use to contact redis.
read from <literal>config.services.redis.port<literal>.
''; '';
}; };
password = mkOption { password = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = config.services.redis.requirePass; default = config.services.redis.servers.rspamd.requirePass;
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
description = '' description = ''
Password that rspamd should use to contact redis, or null if not Password that rspamd should use to contact redis, or null if not required.
required. The default value is read from
<literal>config.services.redis.requirePass<literal>.
''; '';
}; };
}; };
@ -561,6 +967,48 @@ in
''; '';
}; };
smtpdForbidBareNewline = mkOption {
type = types.bool;
default = true;
description = ''
With "smtpd_forbid_bare_newline = yes", the Postfix SMTP server
disconnects a remote SMTP client that sends a line ending in a 'bare
newline'.
This feature was added in Postfix 3.8.4 against SMTP Smuggling and will
default to "yes" in Postfix 3.9.
https://www.postfix.org/smtp-smuggling.html
'';
};
sendingFqdn = mkOption {
type = types.str;
default = cfg.fqdn;
defaultText = lib.literalMD "{option}`mailserver.fqdn`";
example = "myserver.example.com";
description = ''
The fully qualified domain name of the mail server used to
identify with remote servers.
If this server's IP serves purposes other than a mail server,
it may be desirable for the server to have a name other than
that to which the user will connect. For example, the user
might connect to mx.example.com, but the server's IP has
reverse DNS that resolves to myserver.example.com; in this
scenario, some mail servers may reject or penalize the
message.
This setting allows the server to identify as
myserver.example.com when forwarding mail, independently of
{option}`mailserver.fqdn` (which, for SSL reasons, should generally be the name
to which the user connects).
Set this to the name to which the sending IP's reverse DNS
resolves.
'';
};
policydSPFExtraConfig = mkOption { policydSPFExtraConfig = mkOption {
type = types.lines; type = types.lines;
default = ""; default = "";
@ -621,10 +1069,11 @@ in
stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2" stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2"
if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart
check process rspamd with pidfile /var/run/rspamd.pid check process rspamd with matching "rspamd: main process"
start program = "${pkgs.systemd}/bin/systemctl start rspamd" start program = "${pkgs.systemd}/bin/systemctl start rspamd"
stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" stop program = "${pkgs.systemd}/bin/systemctl stop rspamd"
''; '';
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
description = '' description = ''
The configuration used for monitoring via monit. The configuration used for monitoring via monit.
Use a mail address that you actively check and set it via 'set alert ...'. Use a mail address that you actively check and set it via 'set alert ...'.
@ -641,7 +1090,8 @@ in
description = '' description = ''
The location where borg saves the backups. The location where borg saves the backups.
This can be a local path or a remote location such as user@host:/path/to/repo. This can be a local path or a remote location such as user@host:/path/to/repo.
It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec. It is exported and thus available as an environment variable to
{option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`.
''; '';
}; };
@ -701,13 +1151,14 @@ in
default = "none"; default = "none";
description = '' description = ''
The backup can be encrypted by choosing any other value than 'none'. The backup can be encrypted by choosing any other value than 'none'.
When using encryption the password / passphrase must be provided in passphraseFile. When using encryption the password/passphrase must be provided in `passphraseFile`.
''; '';
}; };
passphraseFile = mkOption { passphraseFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
description = "Path to a file containing the encryption password or passphrase.";
}; };
}; };
@ -723,6 +1174,7 @@ in
locations = mkOption { locations = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
default = [cfg.mailDirectory]; default = [cfg.mailDirectory];
defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]";
description = "The locations that are to be backed up by borg."; description = "The locations that are to be backed up by borg.";
}; };
@ -743,8 +1195,9 @@ in
default = null; default = null;
description = '' description = ''
The command to be executed before each backup operation. The command to be executed before each backup operation.
This is called prior to borg init in the same script that runs borg init and create and cmdPostexec. This is called prior to borg init in the same script that runs borg init and create and `cmdPostexec`.
Example: '';
example = ''
export BORG_RSH="ssh -i /path/to/private/key" export BORG_RSH="ssh -i /path/to/private/key"
''; '';
}; };
@ -755,7 +1208,7 @@ in
description = '' description = ''
The command to be executed after each backup operation. The command to be executed after each backup operation.
This is called after borg create completed successfully and in the same script that runs This is called after borg create completed successfully and in the same script that runs
cmdPreexec, borg init and create. `cmdPreexec`, borg init and create.
''; '';
}; };
@ -768,7 +1221,7 @@ in
example = true; example = true;
description = '' description = ''
Whether to enable automatic reboot after kernel upgrades. Whether to enable automatic reboot after kernel upgrades.
This is to be used in conjunction with system.autoUpgrade.enable = true" This is to be used in conjunction with `system.autoUpgrade.enable = true;`
''; '';
}; };
method = mkOption { method = mkOption {
@ -843,7 +1296,9 @@ in
}; };
imports = [ imports = [
./mail-server/assertions.nix
./mail-server/borgbackup.nix ./mail-server/borgbackup.nix
./mail-server/debug.nix
./mail-server/rsnapshot.nix ./mail-server/rsnapshot.nix
./mail-server/clamav.nix ./mail-server/clamav.nix
./mail-server/monit.nix ./mail-server/monit.nix

20
docs/Makefile Normal file
View file

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View file

@ -1,8 +1,12 @@
How to Add Radicale to SNM Add Radicale
========================== ============
Configuration by @dotlambda Configuration by @dotlambda
Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional
crypt passwords are no longer supported. Instead bcrypt passwords
have to be used. These can still be generated using `mkpasswd -m bcrypt`.
.. code:: nix .. code:: nix
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
@ -20,12 +24,13 @@ Configuration by @dotlambda
in { in {
services.radicale = { services.radicale = {
enable = true; enable = true;
config = '' settings = {
[auth] auth = {
type = htpasswd type = "htpasswd";
htpasswd_filename = ${htpasswd} htpasswd_filename = "${htpasswd}";
htpasswd_encryption = crypt htpasswd_encryption = "bcrypt";
''; };
};
}; };
services.nginx = { services.nginx = {

32
docs/add-roundcube.rst Normal file
View file

@ -0,0 +1,32 @@
Add Roundcube, a webmail
========================
The NixOS module for roundcube nearly works out of the box with SNM. By
default, it sets up a nginx virtual host to serve the webmail, other web
servers may require more work.
.. code:: nix
{ config, pkgs, lib, ... }:
with lib;
{
services.roundcube = {
enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver
hostName = "webmail.example.com";
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
};
services.nginx.enable = true;
networking.firewall.allowedTCPPorts = [ 80 443 ];
}

18
docs/autodiscovery.rst Normal file
View file

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

View file

@ -1,9 +1,9 @@
A Complete Backup Guide Backup Guide
======================= ============
This is really easy. First off you should have a backup of your First off you should have a backup of your ``configuration.nix`` file
``configuration.nix`` file where you have the server config (but that is where you have the server config (but that is already in a git
already in a git repository right?) repository right?)
Next you need to backup ``/var/vmail`` or whatever you have specified Next you need to backup ``/var/vmail`` or whatever you have specified
for the option ``mailDirectory``. This is where all the mails reside. for the option ``mailDirectory``. This is where all the mails reside.

View file

@ -18,7 +18,7 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'NixOS Mailserver' project = 'NixOS Mailserver'
copyright = '2020, NixOS Mailserver Contributors' copyright = '2022, NixOS Mailserver Contributors'
author = 'NixOS Mailserver Contributors' author = 'NixOS Mailserver Contributors'
@ -28,8 +28,16 @@ author = 'NixOS Mailserver Contributors'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'myst_parser'
] ]
myst_enable_extensions = [
'colon_fence',
'linkify',
]
smartquotes = False
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@ -50,4 +58,4 @@ html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here, # 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, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static'] html_static_path = []

22
docs/faq.rst Normal file
View file

@ -0,0 +1,22 @@
FAQ
===
``catchAll`` users can't send email as user other than themself
---------------------------------------------------------------
To allow a ``catchAll`` user to send mail with the address used as
recipient, the option ``aliases`` has to be used instead of ``catchAll``.
For instance, to allow ``user@example.com`` to catch all mails to the
domain ``example.com`` and send mails with any address of this domain:
.. code:: nix
mailserver.loginAccounts = {
"user@example.com" = {
aliases = [ "@example.com" ];
};
};
See also `this discussion <https://github.com/r-raymond/nixos-mailserver/issues/49>`__ for details.

30
docs/flakes.rst Normal file
View file

@ -0,0 +1,30 @@
Nix Flakes
==========
If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
the following minimal ``flake.nix`` as an example:
.. code:: nix
{
description = "NixOS configuration";
inputs.simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-20.09";
outputs = { self, nixpkgs, simple-nixos-mailserver }: {
nixosConfigurations = {
hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
simple-nixos-mailserver.nixosModule
{
mailserver = {
enable = true;
# ...
};
}
];
};
};
};
}

69
docs/fts.rst Normal file
View file

@ -0,0 +1,69 @@
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``.
Enabling full text search
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To enable indexing for full text search here is an example configuration.
.. code:: nix
{
mailserver = {
# ...
fullTextSearch = {
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";
};
};
}
The ``enforced`` parameter tells dovecot to fail any body search query that cannot
use an index. This prevents dovecot to fall back to the IO-intensive brute
force search.
If you set ``autoIndex`` to ``false``, indices will be created when the IMAP client
issues a search query, so latency will be high.
Resource requirements
~~~~~~~~~~~~~~~~~~~~~~~~
Indices created by the full text search feature can take more disk
space than the emails themselves. By default, they are kept in the
emails location. When enabling the full text search feature, it is
recommended to move indices in a different location, such as
(``/var/lib/dovecot/indices``) by using the option
``mailserver.indexDir``.
.. warning::
When the value of the ``indexDir`` option is changed, all dovecot
indices needs to be recreated: clients would need to resynchronize.
Indexation itself is rather resouces intensive, in CPU, and for emails with
large headers, in memory as well. Initial indexation of existing emails can take
hours. If the indexer worker is killed or segfaults during indexation, it can
be that it tried to allocate more memory than allowed. You can increase the memory
limit by eg ``mailserver.fullTextSearch.memoryLimit = 2000`` (in MiB).
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``
* 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

@ -1,16 +1,49 @@
How to Develop SNM Contribute or troubleshoot
================== ==========================
Run tests To report an issue, please go to
--------- `<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
You can run the testsuite via You can also chat with us on the Libera IRC channel ``#nixos-mailserver``.
Run NixOS tests
---------------
To run the test suite, you need to enable `Nix Flakes
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
You can then run the testsuite via
:: ::
nix-build tests -A extern.nixpkgs_20_03 $ nix flake check -L
nix-build tests -A intern.nixpkgs_unstable
... Since Nix doesn't garantee your machine have enough resources to run
all test VMs in parallel, some tests can fail. You would then haev to
run tests manually. For instance:
::
$ nix build .#hydraJobs.x86_64-linux.external-unstable -L
Contributing to the documentation
---------------------------------
The documentation is written in RST (except option documentation which is in CommonMark),
built with Sphinx and published by `Read the Docs <https://readthedocs.org/>`_.
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>`_.
::
$ nix build .#documentation
$ xdg-open result/index.html
Nixops Nixops
------ ------
@ -19,15 +52,15 @@ You can test the setup via ``nixops``. After installation, do
:: ::
nixops create nixops/single-server.nix nixops/vbox.nix -d mail $ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
nixops deploy -d mail $ nixops deploy -d mail
nixops info -d mail $ nixops info -d mail
You can then test the server via e.g. \ ``telnet``. To log into it, use You can then test the server via e.g. \ ``telnet``. To log into it, use
:: ::
nixops ssh -d mail mailserver $ nixops ssh -d mail mailserver
Imap Imap
---- ----
@ -36,4 +69,4 @@ To test imap manually use
:: ::
openssl s_client -host mail.example.com -port 143 -starttls imap $ openssl s_client -host mail.example.com -port 143 -starttls imap

View file

@ -6,18 +6,31 @@
Welcome to NixOS Mailserver's documentation! Welcome to NixOS Mailserver's documentation!
============================================ ============================================
.. image:: ../logo/logo.png .. image:: logo.png
:width: 400 :width: 400
:alt: SNM Logo :alt: SNM Logo
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
quick-start
setup-guide setup-guide
howto-develop howto-develop
faq
release-notes
options
.. toctree::
:maxdepth: 1
:caption: How-to
backup-guide backup-guide
howto-add-radicale add-radicale
add-roundcube
rspamd-tuning
fts
flakes
autodiscovery
ldap
Indices and tables Indices and tables
================== ==================

14
docs/ldap.rst Normal file
View file

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

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,32 +0,0 @@
Quick Start
===========
.. code:: nix
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball {
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.2.1/nixos-mailserver-v2.2.1.tar.gz";
sha256 = "03d49v8qnid9g9rha0wg2z6vic06mhp0b049s3whccn1axvs2zzx";
})
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [
"info@example.com"
"postmaster@example.com"
"postmaster@example2.com"
];
};
};
};
}

70
docs/release-notes.rst Normal file
View file

@ -0,0 +1,70 @@
Release Notes
=============
NixOS 24.05
-----------
- Add new option ``acmeCertificateName`` which can be used to support
wildcard certificates
NixOS 23.11
-----------
- Add basic support for LDAP users
- Add support for regex (PCRE) aliases
NixOS 23.05
-----------
- Existing ACME certificates can be reused without configuring NGINX
- Certificate scheme is no longer a number, but a meaningful string instead
NixOS 22.11
-----------
- Allow Rspamd to send DMARC reporting
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
NixOS 22.05
-----------
- Make NixOS Mailserver options discoverable from search.nixos.org
- Add a roundcube setup guide in the documentation
NixOS 21.11
-----------
- Switch default DKIM body policy from simple to relaxed
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/247>`__)
- Ensure locally-delivered mails have the X-Original-To header
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/243>`__)
- NixOS Mailserver options are detailed in the `documentation
<https://nixos-mailserver.readthedocs.io/en/latest/options.html>`__
- New options ``dkimBodyCanonicalization`` and
``dkimHeaderCanonicalization``
- New option ``certificateDomains`` to generate certificate for
additional domains (such as ``imap.example.com``)
NixOS 21.05
-----------
- New `fullTextSearch` option to search in messages (based on Xapian)
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/212>`__)
- Flake support
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/200>`__)
- New `openFirewall` option defaulting to `true`
- We moved from Freenode to Libera Chat
NixOS 20.09
-----------
- IMAP and Submission with TLS wrapped-mode are now enabled by default
on ports 993 and 465 respectively
- OpenDKIM is now sandboxed with Systemd
- New `forwards` option to forwards emails to external addresses
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/193>`__)
- New `sendingFqdn` option to specify the fqdn of the machine sending
email (`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/187>`__)
- Move the Gitlab wiki to `ReadTheDocs
<https://nixos-mailserver.readthedocs.io/en/latest/>`_

4
docs/requirements.txt Normal file
View file

@ -0,0 +1,4 @@
sphinx ~= 5.3
sphinx_rtd_theme ~= 1.1
myst-parser ~= 0.18
linkify-it-py ~= 2.0

113
docs/rspamd-tuning.rst Normal file
View file

@ -0,0 +1,113 @@
Tune spam filtering
===================
SNM comes with the `rspamd spam filtering system <https://rspamd.com/>`_
enabled by default. Although its out-of-the-box performance is good, you
can increase its efficiency by tuning its behaviour.
Auto-learning
~~~~~~~~~~~~~
Moving spam email to the Junk folder (and false-positives out of it) will
trigger an automatic training of the Bayesian filters, improving filtering
of future emails.
Train from existing folders
~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you kept previous spam, you can train the filter from it. Note that the
`rspamd FAQ <https://rspamd.com/doc/faq.html#how-can-i-learn-messages>`_
indicates that *you should always learn both classes with almost equal
amount of messages to increase performance of the statistical engine.*
You can run the training in a root shell as follows:
.. code:: bash
# Path to the controller socket
export RSOCK="/var/run/rspamd/worker-controller.sock"
# Learn the Junk folder as spam
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
# Learn the INBOX as ham
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/
# Check that training was successful
rspamc -h $RSOCK stat | grep learned
Tune symbol weight
~~~~~~~~~~~~~~~~~~
The ``X-Spamd-Result`` header is automatically added to your emails, detailing
the scoring decisions. The `modules documentation <https://rspamd.com/doc/modules/>`_
details the meaning of each symbol. You can tune the weight if a symbol if needed.
.. code:: nix
services.rspamd.locals = {
"groups.conf".text = ''
symbols {
"FORGED_RECIPIENTS" { weight = 0; }
}'';
};
Tune action thresholds
~~~~~~~~~~~~~~~~~~~~~~
After scoring the message, rspamd decides on an action based on configurable thresholds.
By default, rspamd will tell postfix to reject any message with a score higher than 15.
If you experience issues in scoring or want to stay on the safe side, you can disable
this behaviour by tuning the configuration. For example:
.. code:: nix
services.rspamd.extraConfig = ''
actions {
reject = null; # Disable rejects, default is 15
add_header = 6; # Add header when reaching this score
greylist = 4; # Apply greylisting when reaching this score
}
'';
Access the rspamd web UI
~~~~~~~~~~~~~~~~~~~~~~~~
Rspamd comes with `a web interface <https://rspamd.com/webui/>`_ that displays statistics
and history of past scans. **We do NOT recommend using it to change the configuration**
as doing so will override values from the configuration set in the previous sections.
The UI is served on the ``/var/run/rspamd/worker-controller.sock`` Unix socket. Here are
two ways to access it from your browser.
With ssh forwarding
^^^^^^^^^^^^^^^^^^^
For occasional access, the simplest way is to forward the socket to localhost and open
http://localhost:3333 in your browser.
.. code:: shell
ssh -L 3333:/run/rspamd/worker-controller.sock $HOSTNAME
With an nginx reverse-proxy
^^^^^^^^^^^^^^^^^^^^^^^^^^^
If you have a secured nginx reverse proxy set on the host, you can use it to expose the socket.
**Keep in mind the UI is unsecured by default, you need to setup an authentication scheme**, for
exemple with `basic auth <https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/>`_:
.. code:: nix
services.nginx.virtualHosts.rspamd = {
forceSSL = true;
enableACME = true;
basicAuthFile = "/basic/auth/hashes/file";
serverName = "rspamd.example.com";
locations = {
"/" = {
proxyPass = "http://unix:/run/rspamd/worker-controller.sock:/";
};
};
};

View file

@ -1,234 +1,219 @@
A Complete Setup Guide Setup Guide
====================== ===========
Mail servers can be a tricky thing to set up. This guide is supposed to Mail servers can be a tricky thing to set up. This guide is supposed to
run you through the most important steps to achieve a 10/10 score on run you through the most important steps to achieve a 10/10 score on
``mail-tester.com``. `<https://mail-tester.com>`_.
What you need: What you need is:
- A server with a public IP (referred to as ``server-IP``) - a server running NixOS with a public IP
- A Fully Qualified Domain Name (``FQDN``) where your server is - a domain name.
reachable, so that other servers can find yours. Common FQDN include
``mx.example.com`` (where ``example.com`` is a domain you own) or
``mail.example.com``. The domain is referred to as ``server-domain``
(``example.com`` in the above example) and the ``FQDN`` is referred
to by ``server-FQDN`` (``mx.example.com`` above).
- A list of domains you want to your email server to serve. (Note that
this does not have to include ``server-domain``, but may of course).
These will be referred to as ``domains``. As an example,
``domains = [ example1.com, example2.com ]``.
A) Setup server .. note::
~~~~~~~~~~~~~~~
In the following, we consider a server with the public IP ``1.2.3.4``
and the domain ``example.com``.
First, we will set the minimum DNS configuration to be able to deploy
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add a DNS record to the domain ``example.com`` with the following
entries
==================== ===== ==== =============
Name (Subdomain) TTL Type Value
==================== ===== ==== =============
``mail.example.com`` 10800 A ``1.2.3.4``
==================== ===== ==== =============
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
...
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
(which is used in the below configuration example).
Setup the server
~~~~~~~~~~~~~~~~
The following describes a server setup that is fairly complete. Even The following describes a server setup that is fairly complete. Even
though there are more possible options (see ``default.nix``), these though there are more possible options (see the `NixOS Mailserver
should be the most common ones. options documentation <options.html>`_), these should be the most
common ones.
.. code:: nix .. code:: nix
{ config, pkgs, ... }: { config, pkgs, ... }: {
{
imports = [ imports = [
(builtins.fetchTarball { (builtins.fetchTarball {
# Pick a commit from the branch you are interested in # Pick a release version you are interested in and set its hash, e.g.
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz"; url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
# And set its hash # 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
sha256 = "0000000000000000000000000000000000000000000000000000"; sha256 = "0000000000000000000000000000000000000000000000000000";
}) })
]; ];
mailserver = { mailserver = {
enable = true; enable = true;
fqdn = <server-FQDN>; fqdn = "mail.example.com";
domains = [ <domains> ]; domains = [ "example.com" ];
# A list of all login accounts. To create the password hashes, use # A list of all login accounts. To create the password hashes, use
# mkpasswd -m sha-512 "super secret password" # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
loginAccounts = { loginAccounts = {
"user1@example.com" = { "user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; hashedPasswordFile = "/a/file/containing/a/hashed/password";
aliases = ["postmaster@example.com"];
aliases = [
"postmaster@example.com"
"postmaster@example2.com"
];
# Make this user the catchAll address for domains example.com and
# example2.com
catchAll = [
"example.com"
"example2.com"
];
}; };
"user2@example.com" = { ... }; "user2@example.com" = { ... };
}; };
# Extra virtual aliases. These are email addresses that are forwarded to
# loginAccounts addresses.
extraVirtualAliases = {
# address = forward address;
"abuse@example.com" = "user1@example.com";
};
# Use Let's Encrypt certificates. Note that this needs to set up a stripped # Use Let's Encrypt certificates. Note that this needs to set up a stripped
# down nginx and opens port 80. # down nginx and opens port 80.
certificateScheme = 3; certificateScheme = "acme-nginx";
# Enable IMAP and POP3
enableImap = true;
enablePop3 = true;
enableImapSsl = true;
enablePop3Ssl = true;
# Enable the ManageSieve protocol
enableManageSieve = true;
# whether to scan inbound emails for viruses (note that this requires at least
# 1 Gb RAM for the server. Without virus scanning 256 MB RAM should be plenty)
virusScanning = false;
}; };
security.acme.acceptTerms = true;
security.acme.defaults.email = "security@example.com";
} }
After a ``nixos-rebuild switch --upgrade`` your server should be good to After a ``nixos-rebuild switch`` your server should be running all
go. If you want to use ``nixops`` to deploy the server, look in the mail components.
subfolder ``nixops`` for some inspiration.
B) Setup everything else Setup all other DNS requirements
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Step 1: Set DNS entry for server Set rDNS (reverse DNS) entry for server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add a DNS record to the domain ``server-domain`` with the following
entries
================ ===== ==== ======== =============
Name (Subdomain) TTL Type Priority Value
================ ===== ==== ======== =============
``server-FQDN`` 10800 A ``server-IP``
================ ===== ==== ======== =============
This resolves DNS queries for ``server-FQDN`` to ``server-IP``. You can
test if your setting is correct by
::
ping <server-FQDN>
64 bytes from <server-FQDN> (<server-IP>): icmp_seq=1 ttl=46 time=21.3 ms
...
Note that it can take a while until a DNS entry is propagated.
Step 2: Set rDNS (reverse DNS) entry for server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Wherever you have rented your server, you should be able to set reverse Wherever you have rented your server, you should be able to set reverse
DNS entries for the IPs you own. Add an entry resolving ``server-IP`` DNS entries for the IPs you own. Add an entry resolving ``1.2.3.4``
to ``server-FQDN`` to ``mail.example.com``.
You can test if your setting is correct by .. warning::
We don't recommend setting up a mail server if you are not able to
set a reverse DNS on your public IP because sent emails would be
mostly marked as spam. Note that many residential ISP providers
don't allow you to set a reverse DNS entry.
You can check this with
:: ::
host <server-IP> $ nix-shell -p bind --command "host 1.2.3.4"
<server-IP>.in-addr.arpa domain name pointer <server-FQDN>. 4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
Note that it can take a while until a DNS entry is propagated. Note that it can take a while until a DNS entry is propagated.
Step 3: Set ``MX`` Records Set a ``MX`` record
^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
For every ``domain`` in ``domains`` do: \* Add a ``MX`` record to the
domain ``domain`` Add a ``MX`` record to the domain ``example.com``.
================ ==== ======== =================
Name (Subdomain) Type Priority Value
================ ==== ======== =================
example.com MX 10 mail.example.com
================ ==== ======== =================
You can check this with
:: ::
| Name (Subdomain) | TTL | Type | Priority | Value | $ nix-shell -p bind --command "host -t mx example.com"
| ---------------- | ----- | ---- | -------- | ----------------- | example.com mail is handled by 10 mail.example.com.
| `domain` | | MX | 10 | `server-FQDN` |
You can test this via
::
dig -t MX <domain>
...
;; ANSWER SECTION:
<domain> 10800 IN MX 10 <server-FQDN>
...
Note that it can take a while until a DNS entry is propagated. Note that it can take a while until a DNS entry is propagated.
Step 4: Set ``SPF`` Records Set a ``SPF`` record
^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^
For every ``domain`` in ``domains`` do: \* Add a ``SPF`` record to the Add a `SPF <https://en.wikipedia.org/wiki/Sender_Policy_Framework>`_
domain ``domain`` record to the domain ``example.com``.
::
| Name (Subdomain) | TTL | Type | Priority | Value |
| ---------------- | ----- | ---- | -------- | ----------------- |
| `domain` | 10800 | TXT | | `v=spf1 ip4:<server-IP> -all` |
You can check this with ``dig -t TXT <domain>`` similar to the last
section. Note that ``SPF`` records are set as ``TXT`` records since
RFC1035.
Note that it can take a while until a DNS entry is propagated. If you
want to use multiple servers for your email handling, dont forget to
add all server IPs to this list.
Step 5: Set ``DKIM`` signature
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In this section we assume that your ``dkimSelector`` is set to ``mail``.
If you have a different selector, replace all ``mail``\ s below
accordingly.
For every ``domain`` in ``domains`` do: \* Go to your server and
navigate to the dkim key directory (by default ``/var/dkim``). There you
will find a public key for any domain in the ``domain.txt`` file. It
will look like
``mail._domainkey IN TXT "v=DKIM1; r=postmaster; g=*; k=rsa; p=<really-long-key>" ; ----- DKIM mail for domain.tld``
\* Add a ``DKIM`` record to the domain ``domain``
::
| Name (Subdomain) | TTL | Type | Priority | Value |
| ---------------- | ----- | ---- | -------- | ----------------- |
| mail._domainkey.`domain` | 10800 | TXT | | `v=DKIM1; p=<really-long-key>` |
You can check this with ``dig -t TXT mail._domainkey.<domain>`` similar
to the last section.
Note that it can take a while until a DNS entry is propagated.
Step 6: Set ``DMARC`` record
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For every ``domain`` in ``domains`` do:
- Add a ``DMARC`` record to the domain ``domain``
================ ===== ==== ================================ ================ ===== ==== ================================
Name (Subdomain) TTL Type Priority Value Name (Subdomain) TTL Type Value
================ ===== ==== ================================ ================ ===== ==== ================================
\_dmarc.\ ``domain`` 10800 TXT ``v=DMARC1; p=none`` example.com 10800 TXT `v=spf1 a:mail.example.com -all`
================ ===== ==== ================================ ================ ===== ==== ================================
You can check this with ``dig -t TXT _dmarc.<domain>`` similar to the You can check this with
last section.
::
$ nix-shell -p bind --command "host -t TXT example.com"
example.com descriptive text "v=spf1 a:mail.example.com -all"
Note that it can take a while until a DNS entry is propagated. Note that it can take a while until a DNS entry is propagated.
C) Test your Setup Set ``DKIM`` signature
~~~~~~~~~~~~~~~~~~ ^^^^^^^^^^^^^^^^^^^^^^
On your server, the ``opendkim`` 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
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>``
=========================== ===== ==== ==============================
You can check this with
::
$ nix-shell -p bind --command "host -t txt mail._domainkey.example.com"
mail._domainkey.example.com descriptive text "v=DKIM1;p=<really-long-key>"
Note that it can take a while until a DNS entry is propagated.
Set a ``DMARC`` record
^^^^^^^^^^^^^^^^^^^^^^
Add a ``DMARC`` record to the domain ``example.com``.
======================== ===== ==== ====================
Name (Subdomain) TTL Type Value
======================== ===== ==== ====================
_dmarc.example.com 10800 TXT ``v=DMARC1; p=none``
======================== ===== ==== ====================
You can check this with
::
$ nix-shell -p bind --command "host -t TXT _dmarc.example.com"
_dmarc.example.com descriptive text "v=DMARC1; p=none"
Note that it can take a while until a DNS entry is propagated.
Test your Setup
~~~~~~~~~~~~~~~
Write an email to your aunt (who has been waiting for your reply far too Write an email to your aunt (who has been waiting for your reply far too
long), and sign up for some of the finest newsletters the Internet has. long), and sign up for some of the finest newsletters the Internet has.

76
flake.lock Normal file
View file

@ -0,0 +1,76 @@
{
"nodes": {
"blobs": {
"flake": false,
"locked": {
"lastModified": 1604995301,
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
"type": "gitlab"
},
"original": {
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"type": "gitlab"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1717602782,
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"nixpkgs-24_05": {
"locked": {
"lastModified": 1717144377,
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "805a384895c696f802a9bf5bf4720f37385df547",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.05",
"type": "indirect"
}
},
"root": {
"inputs": {
"blobs": "blobs",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"nixpkgs-24_05": "nixpkgs-24_05"
}
}
},
"root": "root",
"version": 7
}

127
flake.nix Normal file
View file

@ -0,0 +1,127 @@
{
description = "A complete and Simple Nixos Mailserver";
inputs = {
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05";
blobs = {
url = "gitlab:simple-nixos-mailserver/blobs";
flake = false;
};
};
outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let
lib = nixpkgs.lib;
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
releases = [
{
name = "unstable";
pkgs = nixpkgs.legacyPackages.${system};
}
{
name = "24.05";
pkgs = nixpkgs-24_05.legacyPackages.${system};
}
];
testNames = [
"internal"
"external"
"clamav"
"multiple"
"ldap"
];
genTest = testName: release: {
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
"value"= import (./tests/. + "/${testName}.nix") {
pkgs = release.pkgs;
inherit blobs;
};
};
# Generate an attribute set such as
# {
# external-unstable = <derivation>;
# external-21_05 = <derivation>;
# ...
# }
allTests = lib.listToAttrs (
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
mailserverModule = import ./.;
# Generate a MarkDown file describing the options of the NixOS mailserver module
optionsDoc = let
eval = lib.evalModules {
modules = [
mailserverModule
{
_module.check = false;
mailserver = {
fqdn = "mx.example.com";
domains = [
"example.com"
];
dmarcReporting = {
organizationName = "Example Corp";
domain = "example.com";
};
};
}
];
};
options = builtins.toFile "options.json" (builtins.toJSON
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
(lib.optionAttrSetToDocList eval.options)));
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
echo "Generating options.md from ${options}"
python ${./scripts/generate-options.py} ${options} > $out
'';
documentation = pkgs.stdenv.mkDerivation {
name = "documentation";
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
buildInputs = [(
pkgs.python3.withPackages (p: with p; [
sphinx
sphinx_rtd_theme
myst-parser
linkify-it-py
])
)];
buildPhase = ''
cp ${optionsDoc} options.md
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
unset SOURCE_DATE_EPOCH
make html
'';
installPhase = ''
cp -Tr _build/html $out
'';
};
in {
nixosModules = rec {
mailserver = mailserverModule;
default = mailserver;
};
nixosModule = self.nixosModules.default; # compatibility
hydraJobs.${system} = allTests // {
inherit documentation;
};
checks.${system} = allTests;
packages.${system} = {
inherit optionsDoc documentation;
};
devShells.${system}.default = pkgs.mkShell {
inputsFrom = [ documentation ];
packages = with pkgs; [
clamav
];
};
devShell.${system} = self.devShells.${system}.default; # compatibility
};
}

View file

@ -0,0 +1,18 @@
{ config, lib, pkgs, ... }:
{
assertions = lib.optionals config.mailserver.ldap.enable [
{
assertion = config.mailserver.loginAccounts == {};
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
}
{
assertion = config.mailserver.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;
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
}
];
}

View file

@ -14,19 +14,17 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: { config, pkgs, lib, options, ... }:
let let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
config = lib.mkIf (cfg.enable && cfg.virusScanning) { config = lib.mkIf (cfg.enable && cfg.virusScanning) {
services.clamav.daemon.enable = true; services.clamav.daemon = {
enable = true;
settings.PhishingScanURLs = "no";
};
services.clamav.updater.enable = true; services.clamav.updater.enable = true;
services.clamav.daemon.extraConfig = ''
PhishingScanURLs no
'';
}; };
} }

View file

@ -21,22 +21,22 @@ let
in in
{ {
# cert :: PATH # cert :: PATH
certificatePath = if cfg.certificateScheme == 1 certificatePath = if cfg.certificateScheme == "manual"
then cfg.certificateFile then cfg.certificateFile
else if cfg.certificateScheme == 2 else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
else if cfg.certificateScheme == 3 else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "/var/lib/acme/${cfg.fqdn}/fullchain.pem" then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; else throw "unknown certificate scheme";
# key :: PATH # key :: PATH
keyPath = if cfg.certificateScheme == 1 keyPath = if cfg.certificateScheme == "manual"
then cfg.keyFile then cfg.keyFile
else if cfg.certificateScheme == 2 else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else if cfg.certificateScheme == 3 else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "/var/lib/acme/${cfg.fqdn}/key.pem" then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; else throw "unknown certificate scheme";
passwordFiles = let passwordFiles = let
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
@ -45,4 +45,26 @@ in
if value.hashedPasswordFile == null then if value.hashedPasswordFile == null then
builtins.toString (mkHashFile name value.hashedPassword) builtins.toString (mkHashFile name value.hashedPassword)
else value.hashedPasswordFile) cfg.loginAccounts; else value.hashedPasswordFile) cfg.loginAccounts;
# Appends the LDAP bind password to files to avoid writing this
# password into the Nix store.
appendLdapBindPwd = {
name, file, prefix, suffix ? "", passwordFile, destination
}: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
#!${pkgs.stdenv.shell}
set -euo pipefail
baseDir=$(dirname ${destination})
if (! test -d "$baseDir"); then
mkdir -p $baseDir
chmod 755 $baseDir
fi
cat ${file} > ${destination}
echo -n '${prefix}' >> ${destination}
cat ${passwordFile} >> ${destination}
echo -n '${suffix}' >> ${destination}
chmod 600 ${destination}
'';
} }

4
mail-server/debug.nix Normal file
View file

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

View file

@ -23,11 +23,20 @@ let
passwdDir = "/run/dovecot2"; passwdDir = "/run/dovecot2";
passwdFile = "${passwdDir}/passwd"; passwdFile = "${passwdDir}/passwd";
userdbFile = "${passwdDir}/userdb";
# This file contains the ldap bind password
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
bool2int = x: if x then "1" else "0";
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
# maildir in format "/${domain}/${user}" # maildir in format "/${domain}/${user}"
dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"; dovecotMaildir =
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n"
);
postfixCfg = config.services.postfix; postfixCfg = config.services.postfix;
dovecot2Cfg = config.services.dovecot2; dovecot2Cfg = config.services.dovecot2;
@ -51,6 +60,42 @@ let
''; '';
}; };
ldapConfig = pkgs.writeTextFile {
name = "dovecot-ldap.conf.ext.template";
text = ''
ldap_version = 3
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
${lib.optionalString cfg.ldap.startTls ''
tls = yes
''}
tls_require_cert = hard
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
dn = ${cfg.ldap.bind.dn}
sasl_bind = no
auth_bind = yes
base = ${cfg.ldap.searchBase}
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
user_attrs = ${cfg.ldap.dovecot.userAttrs}
''}
user_filter = ${cfg.ldap.dovecot.userFilter}
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
''}
pass_filter = ${cfg.ldap.dovecot.passFilter}
'';
};
setPwdInLdapConfFile = appendLdapBindPwd {
name = "ldap-conf-file";
file = ldapConfig;
prefix = ''dnpass = "'';
suffix = ''"'';
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapConfFile;
};
genPasswdScript = pkgs.writeScript "generate-password-file" '' genPasswdScript = pkgs.writeScript "generate-password-file" ''
#!${pkgs.stdenv.shell} #!${pkgs.stdenv.shell}
@ -61,6 +106,9 @@ let
chmod 755 "${passwdDir}" chmod 755 "${passwdDir}"
fi fi
# 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: value: passwordFiles."${name}") cfg.loginAccounts)}; do
if [ ! -f "$f" ]; then if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!" echo "Expected password hash file $f does not exist!"
@ -70,22 +118,52 @@ let
cat <<EOF > ${passwdFile} cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:${"$(cat ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:" "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts)}
EOF
cat <<EOF > ${userdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:::::::"
+ (if lib.isString value.quota + (if lib.isString value.quota
then "userdb_quota_rule=*:storage=${value.quota}" then "userdb_quota_rule=*:storage=${value.quota}"
else "") else "")
) cfg.loginAccounts)} ) cfg.loginAccounts)}
EOF EOF
chmod 600 ${passwdFile}
''; '';
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
junkMailboxNumber = builtins.length junkMailboxes;
# The assertion garantees there is exactly one Junk mailbox.
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
mkLdapSearchScope = scope: (
if scope == "sub" then "subtree"
else if scope == "one" then "onelevel"
else scope
);
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
assertions = [
{
assertion = junkMailboxNumber == 1;
message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
}
];
# 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
];
services.dovecot2 = { services.dovecot2 = {
enable = true; enable = true;
enableImap = enableImap; enableImap = enableImap || enableImapSsl;
enablePop3 = enablePop3; enablePop3 = enablePop3 || enablePop3Ssl;
enablePAM = false; enablePAM = false;
enableQuota = true; enableQuota = true;
mailGroup = vmailGroupName; mailGroup = vmailGroupName;
@ -94,20 +172,52 @@ in
sslServerCert = certificatePath; sslServerCert = certificatePath;
sslServerKey = keyPath; sslServerKey = keyPath;
enableLmtp = true; enableLmtp = true;
modules = [ pkgs.dovecot_pigeonhole ]; modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
protocols = [ "sieve" ]; mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
protocols = lib.optional cfg.enableManageSieve "sieve";
sieveScripts = { pluginSettings = {
after = builtins.toFile "spam.sieve" '' sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve";
sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve";
sieve_default_name = "default";
};
sieve = {
extensions = [
"fileinto"
];
scripts.after = builtins.toFile "spam.sieve" ''
require "fileinto"; require "fileinto";
if header :is "X-Spam" "Yes" { if header :is "X-Spam" "Yes" {
fileinto "Junk"; fileinto "${junkMailboxName}";
stop; stop;
} }
''; '';
pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "sa-learn-ham.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
(pkgs.writeShellScriptBin "sa-learn-spam.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
}; };
imapsieve.mailbox = [
{
name = junkMailboxName;
causes = [ "COPY" "APPEND" ];
before = ./dovecot/imap_sieve/report-spam.sieve;
}
{
name = "*";
from = junkMailboxName;
causes = [ "COPY" ];
before = ./dovecot/imap_sieve/report-ham.sieve;
}
];
mailboxes = cfg.mailboxes; mailboxes = cfg.mailboxes;
extraConfig = '' extraConfig = ''
@ -118,6 +228,49 @@ in
verbose_ssl = yes verbose_ssl = yes
''} ''}
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
service imap-login {
inet_listener imap {
${if cfg.enableImap then ''
port = 143
'' else ''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
}
inet_listener imaps {
${if cfg.enableImapSsl then ''
port = 993
ssl = yes
'' else ''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
}
}
''}
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
service pop3-login {
inet_listener pop3 {
${if cfg.enablePop3 then ''
port = 110
'' else ''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
}
inet_listener pop3s {
${if cfg.enablePop3Ssl then ''
port = 995
ssl = yes
'' else ''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0
''}
}
}
''}
protocol imap { protocol imap {
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
mail_plugins = $mail_plugins imap_sieve mail_plugins = $mail_plugins imap_sieve
@ -140,7 +293,7 @@ in
} }
} }
recipient_delimiter = + recipient_delimiter = ${cfg.recipientDelimiter}
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
protocol lmtp { protocol lmtp {
@ -154,9 +307,23 @@ in
userdb { userdb {
driver = passwd-file driver = passwd-file
args = ${passwdFile} args = ${userdbFile}
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
} }
${lib.optionalString cfg.ldap.enable ''
passdb {
driver = ldap
args = ${ldapConfFile}
}
userdb {
driver = ldap
args = ${ldapConfFile}
default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
}
''}
service auth { service auth {
unix_listener auth { unix_listener auth {
mode = 0660 mode = 0660
@ -172,28 +339,26 @@ in
inbox = yes inbox = yes
} }
${lib.optionalString cfg.fullTextSearch.enable ''
plugin { plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms plugin = fts fts_xapian
sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve fts = xapian
sieve_default = file:/var/sieve/%u/default.sieve fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
sieve_default_name = default
# From elsewhere to Spam folder fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
imapsieve_mailbox1_name = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
# From Spam folder to elsewhere ${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin fts_enforced = ${cfg.fullTextSearch.enforced}
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
} }
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
service indexer-worker {
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
}
''}
''}
lda_mailbox_autosubscribe = yes lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes lda_mailbox_autocreate = yes
''; '';
@ -202,16 +367,33 @@ in
systemd.services.dovecot2 = { systemd.services.dovecot2 = {
preStart = '' preStart = ''
${genPasswdScript} ${genPasswdScript}
rm -rf '${stateDir}/imap_sieve' '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
mkdir '${stateDir}/imap_sieve'
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
for k in "${stateDir}/imap_sieve"/*.sieve ; do
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
done
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
'';
}; };
systemd.services.postfix.restartTriggers = [ genPasswdScript ]; 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

@ -23,6 +23,6 @@ in
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd dovecot opendkim openssh postfix rspamd
] ++ (if certificateScheme == 2 then [ openssl ] else []); ] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
}; };
} }

View file

@ -22,7 +22,6 @@ in
{ {
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) { config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
services.kresd.enable = true; services.kresd.enable = true;
networking.nameservers = [ "127.0.0.1" ];
}; };
} }

View file

@ -14,22 +14,24 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: { config, lib, ... }:
let let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf (enable && openFirewall) {
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ 25 587 ] allowedTCPPorts = [ 25 ]
++ lib.optional enableSubmission 587
++ lib.optional enableSubmissionSsl 465
++ lib.optional enableImap 143 ++ lib.optional enableImap 143
++ lib.optional enableImapSsl 993 ++ lib.optional enableImapSsl 993
++ lib.optional enablePop3 110 ++ lib.optional enablePop3 110
++ lib.optional enablePop3Ssl 995 ++ lib.optional enablePop3Ssl 995
++ lib.optional enableManageSieve 4190 ++ lib.optional enableManageSieve 4190
++ lib.optional (certificateScheme == 3) 80; ++ lib.optional (certificateScheme == "acme-nginx") 80;
}; };
}; };
} }

View file

@ -17,28 +17,26 @@
{ config, pkgs, lib, ... }: { config, pkgs, lib, ... }:
with (import ./common.nix { inherit config; }); with (import ./common.nix { inherit config lib pkgs; });
let let
cfg = config.mailserver; cfg = config.mailserver;
acmeRoot = "/var/lib/acme/acme-challenge";
in in
{ {
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) { config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
services.nginx = { services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
enable = true; enable = true;
virtualHosts."${cfg.fqdn}" = { virtualHosts."${cfg.fqdn}" = {
serverName = cfg.fqdn; serverName = cfg.fqdn;
serverAliases = cfg.certificateDomains;
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
acmeRoot = acmeRoot;
}; };
}; };
security.acme.certs."${cfg.fqdn}".postRun = '' security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
systemctl reload nginx "postfix.service"
systemctl reload postfix "dovecot2.service"
systemctl reload dovecot2 ];
'';
}; };
} }

View file

@ -29,7 +29,7 @@ let
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
in in
'' ''
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] if [ ! -f "${dkim_key}" ]
then then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \ -d "${dom}" \
@ -37,6 +37,7 @@ let
--directory="${cfg.dkimKeyDirectory}" --directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
chmod 644 "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}" echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
fi fi
''; '';
@ -56,9 +57,10 @@ in
services.opendkim = { services.opendkim = {
enable = true; enable = true;
selector = cfg.dkimSelector; selector = cfg.dkimSelector;
keyPath = cfg.dkimKeyDirectory;
domains = "csl:${builtins.concatStringsSep "," cfg.domains}"; domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" ('' configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization relaxed/simple Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
UMask 0002 UMask 0002
Socket ${dkim.socket} Socket ${dkim.socket}
KeyTable file:${keyTable} KeyTable file:${keyTable}

View file

@ -22,33 +22,57 @@ let
inherit (lib.strings) concatStringsSep; inherit (lib.strings) concatStringsSep;
cfg = config.mailserver; cfg = config.mailserver;
# valiases_postfix :: [ String ] # Merge several lookup tables. A lookup table is a attribute set where
valiases_postfix = lib.flatten (lib.mapAttrsToList # - 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;
# valiases_postfix :: Map String [String]
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value: (name: value:
let to = name; let to = name;
in map (from: "${from} ${to}") (value.aliases ++ lib.singleton name)) in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
cfg.loginAccounts); cfg.loginAccounts));
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
# catchAllPostfix :: [ String ]
catchAllPostfix = lib.flatten (lib.mapAttrsToList
(name: value: (name: value:
let to = name; let to = name;
in map (from: "@${from} ${to}") value.catchAll) in map (from: {"${from}" = to;}) value.aliasesRegexp)
cfg.loginAccounts); cfg.loginAccounts));
# extra_valiases_postfix :: [ String ] # catchAllPostfix :: Map String [String]
extra_valiases_postfix = catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(map (name: value:
(from: let to = name;
let to = cfg.extraVirtualAliases.${from}; in map (from: {"@${from}" = to;}) value.catchAll)
aliasList = (l: let aliasStr = builtins.foldl' (x: y: x + y + ", ") "" l; cfg.loginAccounts));
in builtins.substring 0 (builtins.stringLength aliasStr - 2) aliasStr);
in if (builtins.isList to) then "${from} " + (aliasList to)
else "${from} ${to}")
(builtins.attrNames cfg.extraVirtualAliases));
# all_valiases_postfix :: [ String ] # all_valiases_postfix :: Map String [String]
all_valiases_postfix = valiases_postfix ++ extra_valiases_postfix; all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
attrsToLookupTable = aliases: let
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
in mergeLookupTables lookupTables;
# extra_valiases_postfix :: Map String [String]
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
# forwards :: Map String [String]
forwards = attrsToLookupTable cfg.forwards;
# lookupTableToString :: Map String [String] -> String
lookupTableToString = attrs: let
valueToString = value: lib.concatStringsSep ", " value;
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
# valiases_file :: Path
valiases_file = let
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
in builtins.toFile "valias" content;
regex_valiases_file = let
content = lookupTableToString regex_valiases_postfix;
in builtins.toFile "regex_valias" content;
# denied_recipients_postfix :: [ String ] # denied_recipients_postfix :: [ String ]
denied_recipients_postfix = (map denied_recipients_postfix = (map
@ -56,19 +80,12 @@ let
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts))); (lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix); denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
# valiases_file :: Path
valiases_file = builtins.toFile "valias"
(lib.concatStringsSep "\n" (all_valiases_postfix ++
catchAllPostfix));
reject_senders_postfix = (map reject_senders_postfix = (map
(sender: (sender:
"${sender} REJECT") "${sender} REJECT")
(cfg.rejectSender)); (cfg.rejectSender));
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ; reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
reject_recipients_postfix = (map reject_recipients_postfix = (map
(recipient: (recipient:
"${recipient} REJECT") "${recipient} REJECT")
@ -85,7 +102,8 @@ let
# for details on how this file looks. By using the same file as valiases, # for details on how this file looks. By using the same file as valiases,
# every alias is owned (uniquely) by its user. # every alias is owned (uniquely) by its user.
# The user's own address is already in all_valiases_postfix. # The user's own address is already in all_valiases_postfix.
vaccounts_file = builtins.toFile "vaccounts" (lib.concatStringsSep "\n" all_valiases_postfix); vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
# Removes sensitive headers from mails handed in via the submission port. # Removes sensitive headers from mails handed in via the submission port.
@ -112,36 +130,100 @@ let
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
++ [ "unix:/run/rspamd/rspamd-milter.sock" ]; ++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
policyd-spf = pkgs.writeText "policyd-spf.conf" ( policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
cfg.policydSPFExtraConfig
+ (lib.optionalString cfg.debug ''
debugLevel = 4
''));
mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
submissionOptions =
{
smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}";
smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup";
};
commonLdapConfig = ''
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
version = 3
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
tls_require_cert = yes
search_base = ${cfg.ldap.searchBase}
scope = ${cfg.ldap.searchScope}
bind = yes
bind_dn = ${cfg.ldap.bind.dn}
'';
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.mailAttribute}
'';
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
appendPwdInSenderLoginMap = appendLdapBindPwd {
name = "ldap-sender-login-map";
file = ldapSenderLoginMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapSenderLoginMapFile;
};
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.uidAttribute}
'';
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
name = "ldap-virtual-mailbox-map";
file = ldapVirtualMailboxMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapVirtualMailboxMapFile;
};
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = ''
${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap}
'';
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
};
services.postfix = { services.postfix = {
enable = true; enable = true;
hostname = "${fqdn}"; hostname = "${sendingFqdn}";
networksStyle = "host"; networksStyle = "host";
mapFiles."valias" = valiases_file; mapFiles."valias" = valiases_file;
mapFiles."regex_valias" = regex_valiases_file;
mapFiles."vaccounts" = vaccounts_file; mapFiles."vaccounts" = vaccounts_file;
mapFiles."regex_vaccounts" = regex_vaccounts_file;
mapFiles."denied_recipients" = denied_recipients_file; mapFiles."denied_recipients" = denied_recipients_file;
mapFiles."reject_senders" = reject_senders_file; mapFiles."reject_senders" = reject_senders_file;
mapFiles."reject_recipients" = reject_recipients_file; mapFiles."reject_recipients" = reject_recipients_file;
sslCert = certificatePath; sslCert = certificatePath;
sslKey = keyPath; sslKey = keyPath;
enableSubmission = true; enableSubmission = cfg.enableSubmission;
virtual = enableSubmissions = cfg.enableSubmissionSsl;
(lib.concatStringsSep "\n" (all_valiases_postfix ++ catchAllPostfix)); virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
config = { config = {
# Extra Config # Extra Config
mydestination = ""; mydestination = "";
recipient_delimiter = "+"; recipient_delimiter = cfg.recipientDelimiter;
smtpd_banner = "${fqdn} ESMTP NO UCE"; smtpd_banner = "${fqdn} ESMTP NO UCE";
disable_vrfy_command = true; disable_vrfy_command = true;
message_size_limit = toString cfg.messageSizeLimit; message_size_limit = toString cfg.messageSizeLimit;
@ -151,8 +233,19 @@ in
virtual_gid_maps = "static:5000"; virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory; virtual_mailbox_base = mailDirectory;
virtual_mailbox_domains = vhosts_file; virtual_mailbox_domains = vhosts_file;
virtual_mailbox_maps = mappedFile "valias"; virtual_mailbox_maps = [
(mappedFile "valias")
] ++ lib.optionals (cfg.ldap.enable) [
"ldap:${ldapVirtualMailboxMapFile}"
] ++ lib.optionals (regex_valiases_postfix != {}) [
(mappedRegexFile "regex_valias")
];
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
(mappedRegexFile "regex_valias")
]);
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
# sasl with dovecot # sasl with dovecot
smtpd_sasl_type = "dovecot"; smtpd_sasl_type = "dovecot";
@ -181,9 +274,6 @@ in
# Submission by mail clients is handled in submissionOptions # Submission by mail clients is handled in submissionOptions
smtpd_tls_security_level = "may"; smtpd_tls_security_level = "may";
# strong might suffice and is computationally less expensive
smtpd_tls_eecdh_grade = "ultra";
# Disable obselete protocols # Disable obselete protocols
smtpd_tls_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"; smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
@ -216,28 +306,26 @@ in
milter_protocol = "6"; 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_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
# Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
}; };
submissionOptions =
{ submissionOptions = submissionOptions;
smtpd_tls_security_level = "encrypt"; submissionsOptions = submissionOptions;
smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup";
};
masterConfig = { masterConfig = {
"lmtp" = {
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
# D => Delivered-To, O => X-Original-To, R => Return-Path
args = [ "flags=O" ];
};
"policy-spf" = { "policy-spf" = {
type = "unix"; type = "unix";
privileged = true; privileged = true;
chroot = false; chroot = false;
command = "spawn"; command = "spawn";
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"]; args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
}; };
"submission-header-cleanup" = { "submission-header-cleanup" = {
type = "unix"; type = "unix";

View file

@ -30,7 +30,7 @@ in
inherit debug; inherit debug;
locals = { locals = {
"milter_headers.conf" = { text = '' "milter_headers.conf" = { text = ''
extended_spam_headers = yes; extended_spam_headers = true;
''; }; ''; };
"redis.conf" = { text = '' "redis.conf" = { text = ''
servers = "${cfg.redis.address}:${toString cfg.redis.port}"; servers = "${cfg.redis.address}:${toString cfg.redis.port}";
@ -52,14 +52,21 @@ in
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
} }
''; }; ''; };
}; "dkim_signing.conf" = { text = ''
# Disable outbound email signing, we use opendkim for this
overrides = { enabled = false;
"milter_headers.conf" = { ''; };
text = '' "dmarc.conf" = { text = ''
extended_spam_headers = true; ${lib.optionalString cfg.dmarcReporting.enable ''
''; reporting {
}; enabled = true;
email = "${cfg.dmarcReporting.email}";
domain = "${cfg.dmarcReporting.domain}";
org_name = "${cfg.dmarcReporting.organizationName}";
from_name = "${cfg.dmarcReporting.fromName}";
msgid_from = "dmarc-rua";
}''}
''; };
}; };
workers.rspamd_proxy = { workers.rspamd_proxy = {
@ -87,15 +94,79 @@ in
mode = "0666"; mode = "0666";
}]; }];
includes = []; includes = [];
extraConfig = ''
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
'';
}; };
}; };
services.redis.enable = true; services.redis.servers.rspamd = {
enable = lib.mkDefault true;
port = lib.mkDefault 6380;
};
systemd.services.rspamd = { systemd.services.rspamd = {
requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
after = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
};
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
# Explicitly select yesterday's date to work around broken
# default behaviour when called without a date.
# https://github.com/rspamd/rspamd/issues/4062
script = ''
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
'';
serviceConfig = {
User = "${config.services.rspamd.user}";
Group = "${config.services.rspamd.group}";
AmbientCapabilities = [];
CapabilityBoundingSet = "";
DevicePolicy = "closed";
IPAddressAllow = "localhost";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
description = "Daily delivery of aggregated DMARC reports";
wantedBy = [
"timers.target"
];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
RandomizedDelaySec = 86400;
FixedRandomDelay = true;
};
}; };
systemd.services.postfix = { systemd.services.postfix = {

View file

@ -18,40 +18,32 @@
let let
cfg = config.mailserver; cfg = config.mailserver;
preliminarySelfsigned = config.security.acme.preliminarySelfsigned; certificatesDeps =
acmeWantsTarget = [ "acme-certificates.target" ] if cfg.certificateScheme == "manual" then
++ (lib.optional preliminarySelfsigned "acme-selfsigned-certificates.target"); []
acmeAfterTarget = if preliminarySelfsigned else if cfg.certificateScheme == "selfsigned" then
then [ "acme-selfsigned-certificates.target" ] [ "mailserver-selfsigned-certificate.service" ]
else [ "acme-certificates.target" ]; else
[ "acme-finished-${cfg.fqdn}.target" ];
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
# Add target for when certificates are available
systemd.targets."mailserver-certificates" = {
wants = lib.mkIf (cfg.certificateScheme == 3) acmeWantsTarget;
after = lib.mkIf (cfg.certificateScheme == 3) acmeAfterTarget;
};
# Create self signed certificate # Create self signed certificate
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) { systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") {
wantedBy = [ "mailserver-certificates.target" ];
after = [ "local-fs.target" ]; after = [ "local-fs.target" ];
before = [ "mailserver-certificates.target" ];
script = '' script = ''
# Create certificates if they do not exist yet # Create certificates if they do not exist yet
dir="${cfg.certificateDirectory}" dir="${cfg.certificateDirectory}"
fqdn="${cfg.fqdn}" fqdn="${cfg.fqdn}"
case $fqdn in /*) fqdn=$(cat "$fqdn");; esac [[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
key="''${dir}/key-${cfg.fqdn}.pem"; key="$dir/key-${cfg.fqdn}.pem";
cert="''${dir}/cert-${cfg.fqdn}.pem"; cert="$dir/cert-${cfg.fqdn}.pem";
if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ] if [[ ! -f $key || ! -f $cert ]]; then
then
mkdir -p "${cfg.certificateDirectory}" mkdir -p "${cfg.certificateDirectory}"
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) && (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \ "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
-days 3650 -out "''${cert}" -days 3650 -out "$cert"
fi fi
''; '';
serviceConfig = { serviceConfig = {
@ -62,24 +54,32 @@ in
# Create maildir folder before dovecot startup # Create maildir folder before dovecot startup
systemd.services.dovecot2 = { systemd.services.dovecot2 = {
after = [ "mailserver-certificates.target" ]; wants = certificatesDeps;
wants = [ "mailserver-certificates.target" ]; after = certificatesDeps;
preStart = '' preStart = let
directories = lib.strings.escapeShellArgs (
[ mailDirectory ]
++ lib.optional (cfg.indexDir != null) cfg.indexDir
);
in ''
# Create mail directory and set permissions. See # Create mail directory and set permissions. See
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>. # <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
mkdir -p "${mailDirectory}" # Prevent world-readable paths, even temporarily.
chgrp "${vmailGroupName}" "${mailDirectory}" umask 007
chmod 02770 "${mailDirectory}" mkdir -p ${directories}
chgrp "${vmailGroupName}" ${directories}
chmod 02770 ${directories}
''; '';
}; };
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = { systemd.services.postfix = {
after = [ "dovecot2.service" "mailserver-certificates.target" ] wants = certificatesDeps;
++ (lib.optional cfg.dkimSigning "opendkim.service"); after = [ "dovecot2.service" ]
wants = [ "mailserver-certificates.target" ]; ++ lib.optional cfg.dkimSigning "opendkim.service"
++ certificatesDeps;
requires = [ "dovecot2.service" ] requires = [ "dovecot2.service" ]
++ (lib.optional cfg.dkimSigning "opendkim.service"); ++ lib.optional cfg.dkimSigning "opendkim.service";
}; };
}; };
} }

View file

@ -21,7 +21,7 @@ with config.mailserver;
let let
vmail_user = { vmail_user = {
name = vmailUserName; name = vmailUserName;
isNormalUser = false; isSystemUser = true;
uid = vmailUID; uid = vmailUID;
home = mailDirectory; home = mailDirectory;
createHome = true; createHome = true;
@ -34,32 +34,35 @@ let
set -euo pipefail set -euo pipefail
# Prevent world-readable paths, even temporarily.
umask 007
# Create directory to store user sieve scripts if it doesn't exist # Create directory to store user sieve scripts if it doesn't exist
if (! test -d "/var/sieve"); then if (! test -d "${sieveDirectory}"); then
mkdir "/var/sieve" mkdir "${sieveDirectory}"
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve" chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
chmod 770 "/var/sieve" chmod 770 "${sieveDirectory}"
fi fi
# Copy user's sieve script to the correct location (if it exists). If it # Copy user's sieve script to the correct location (if it exists). If it
# is null, remove the file. # is null, remove the file.
${lib.concatMapStringsSep "\n" ({ name, sieveScript }: ${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
if lib.isString sieveScript then '' if lib.isString sieveScript then ''
if (! test -d "/var/sieve/${name}"); then if (! test -d "${sieveDirectory}/${name}"); then
mkdir -p "/var/sieve/${name}" mkdir -p "${sieveDirectory}/${name}"
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve/${name}" chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
chmod 770 "/var/sieve/${name}" chmod 770 "${sieveDirectory}/${name}"
fi fi
cat << 'EOF' > "/var/sieve/${name}/default.sieve" cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
${sieveScript} ${sieveScript}
EOF EOF
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve/${name}/default.sieve" chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
'' else '' '' else ''
if (test -f "/var/sieve/${name}/default.sieve"); then if (test -f "${sieveDirectory}/${name}/default.sieve"); then
rm "/var/sieve/${name}/default.sieve" rm "${sieveDirectory}/${name}/default.sieve"
fi fi
if (test -f "/var/sieve/${name}.svbin"); then if (test -f "${sieveDirectory}/${name}.svbin"); then
rm "/var/sieve/${name}/default.svbin" rm "${sieveDirectory}/${name}/default.svbin"
fi fi
'') (map (user: { inherit (user) name sieveScript; }) '') (map (user: { inherit (user) name sieveScript; })
(lib.attrValues loginAccounts))} (lib.attrValues loginAccounts))}

View file

@ -1,26 +0,0 @@
{
"nixpkgs-20.03": {
"branch": "nixos-20.03",
"description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to",
"homepage": "https://github.com/NixOS/nixpkgs",
"owner": "NixOS",
"repo": "nixpkgs-channels",
"rev": "29eddfc36d720dcc4822581175217543b387b1e8",
"sha256": "1gqv2m7plkladd3va664xyqb962pqs4pizzibvkm1nh0f4rfpxvy",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs-channels/archive/29eddfc36d720dcc4822581175217543b387b1e8.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs-unstable": {
"branch": "nixos-unstable",
"description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to",
"homepage": "https://github.com/NixOS/nixpkgs",
"owner": "NixOS",
"repo": "nixpkgs-channels",
"rev": "fce7562cf46727fdaf801b232116bc9ce0512049",
"sha256": "14rvi69ji61x3z88vbn17rg5vxrnw2wbnanxb7y0qzyqrj7spapx",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs-channels/archive/fce7562cf46727fdaf801b232116bc9ce0512049.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

View file

@ -1,134 +0,0 @@
# This file has been generated by Niv.
let
#
# The fetchers. fetch_<type> fetches specs of type <type>.
#
fetch_file = pkgs: spec:
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; }
else
pkgs.fetchurl { inherit (spec) url sha256; };
fetch_tarball = pkgs: spec:
if spec.builtin or true then
builtins_fetchTarball { inherit (spec) url sha256; }
else
pkgs.fetchzip { inherit (spec) url sha256; };
fetch_git = spec:
builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
fetch_builtin-tarball = spec:
builtins.trace
''
WARNING:
The niv type "builtin-tarball" will soon be deprecated. You should
instead use `builtin = true`.
$ niv modify <package> -a type=tarball -a builtin=true
''
builtins_fetchTarball { inherit (spec) url sha256; };
fetch_builtin-url = spec:
builtins.trace
''
WARNING:
The niv type "builtin-url" will soon be deprecated. You should
instead use `builtin = true`.
$ niv modify <package> -a type=file -a builtin=true
''
(builtins_fetchurl { inherit (spec) url sha256; });
#
# Various helpers
#
# The set of packages used when specs are fetched using non-builtins.
mkPkgs = sources:
let
sourcesNixpkgs =
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {};
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
in
if builtins.hasAttr "nixpkgs" sources
then sourcesNixpkgs
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
import <nixpkgs> {}
else
abort
''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
# The actual fetching function.
fetch = pkgs: name: spec:
if ! builtins.hasAttr "type" spec then
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
else if spec.type == "file" then fetch_file pkgs spec
else if spec.type == "tarball" then fetch_tarball pkgs spec
else if spec.type == "git" then fetch_git spec
else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec
else if spec.type == "builtin-url" then fetch_builtin-url spec
else
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
# Ports of functions for older nix versions
# a Nix version of mapAttrs if the built-in doesn't exist
mapAttrs = builtins.mapAttrs or (
f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
);
# fetchTarball version that is compatible between all the versions of Nix
builtins_fetchTarball = { url, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchTarball;
in
if lessThan nixVersion "1.12" then
fetchTarball { inherit url; }
else
fetchTarball attrs;
# fetchurl version that is compatible between all the versions of Nix
builtins_fetchurl = { url, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchurl;
in
if lessThan nixVersion "1.12" then
fetchurl { inherit url; }
else
fetchurl attrs;
# Create the final "sources" from the config
mkSources = config:
mapAttrs (
name: spec:
if builtins.hasAttr "outPath" spec
then abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = fetch config.pkgs name spec; }
) config.sources;
# The "config" used by the fetchers
mkConfig =
{ sourcesFile ? ./sources.json
, sources ? builtins.fromJSON (builtins.readFile sourcesFile)
, pkgs ? mkPkgs sources
}: rec {
# The sources, i.e. the attribute set of spec name to spec
inherit sources;
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
inherit pkgs;
};
in
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }

View file

@ -5,7 +5,7 @@
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
imports = [ imports = [
./../default.nix ../default.nix
]; ];
mailserver = { mailserver = {

View file

@ -0,0 +1,82 @@
import json
import sys
header = """
# Mailserver options
## `mailserver`
"""
template = """
`````{{option}} {key}
{description}
{type}
{default}
{example}
`````
"""
f = open(sys.argv[1])
options = json.load(f)
groups = ["mailserver.loginAccounts",
"mailserver.certificate",
"mailserver.dkim",
"mailserver.dmarcReporting",
"mailserver.fullTextSearch",
"mailserver.redis",
"mailserver.ldap",
"mailserver.monitoring",
"mailserver.backup",
"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']
else:
description = opt['description']
print(template.format(
key=opt['name'],
description=description or "",
type="- type: ```{}```".format(opt['type']),
default=render_option_value(opt, 'default'),
example=render_option_value(opt, 'example')))
print(header)
for opt in options:
if any([opt['name'].startswith(c) for c in groups]):
continue
print_option(opt)
for c in groups:
print('## `{}`'.format(c))
print()
for opt in options:
if opt['name'].startswith(c):
print_option(opt)

197
scripts/mail-check.py Normal file
View file

@ -0,0 +1,197 @@
import smtplib, sys
import argparse
import os
import uuid
import imaplib
from datetime import datetime, timedelta
import email
import time
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)
retry = RETRY
while True:
try:
with smtplib.SMTP(smtp_host, port=smtp_port) as smtp:
try:
if starttls:
smtp.starttls()
if from_pwd is not None:
smtp.login(smtp_username or from_addr, from_pwd)
smtp.sendmail(from_addr, [to_addr], message)
return
except smtplib.SMTPResponseException as e:
if e.smtp_code == 451: # service unavailable error
print(e)
elif e.smtp_code == 454: # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
print(e)
else:
raise
except OSError as e:
if e.errno in [16, -2]:
print("OSError exception message: ", e)
else:
raise
if retry > 0:
retry = retry - 1
time.sleep(1)
print("Retrying")
else:
print("Retry attempts exhausted")
exit(5)
def _read_mail(
imap_host,
imap_port,
imap_username,
to_pwd,
subject,
ignore_dkim_spf,
show_body=False,
delete=True):
print("Reading mail from %s" % imap_username)
message = None
obj = imaplib.IMAP4_SSL(imap_host, imap_port)
obj.login(imap_username, to_pwd)
obj.select()
today = datetime.today()
cutoff = today - timedelta(days=1)
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'']:
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))
# FIXME: we only consider the first matching message...
uid = uids[0]
_, raw = obj.fetch(uid, '(RFC822)')
if delete:
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'])
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'))
break
if message is None:
print("Error: no message with subject '%s' has been found in INBOX of %s" % (subject, 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']:
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']:
print("SPF ok")
else:
print("Error: no SPF validation found in message:")
print(message.as_string())
exit(3)
else:
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 != '':
imap_username = args.imap_username
else:
imap_username = args.to_addr
subject = "{}".format(uuid.uuid4())
_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)
_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)
def read(args):
_read_mail(imap_host=args.imap_host,
imap_port=args.imap_port,
to_addr=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)
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.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.set_defaults(func=read)
args = parser.parse_args()
args.func(args)

View file

@ -1,11 +1,10 @@
let (import
nixpkgs = (import ./nix/sources.nix).nixpkgs-unstable; (
pkgs = import nixpkgs {}; let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
in fetchTarball {
pkgs.mkShell { url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
buildInputs = with pkgs; [ sha256 = lock.nodes.flake-compat.locked.narHash;
(python3.withPackages(p: [p.sphinx p.sphinx_rtd_theme]))
niv
jq clamav
];
} }
)
{ src = ./.; }
).shellNix

View file

@ -14,23 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ pkgs ? import <nixpkgs> {}}: { pkgs ? import <nixpkgs> {}, blobs}:
import (pkgs.path + "/nixos/tests/make-test.nix") {
pkgs.nixosTest {
name = "clamav";
nodes = { nodes = {
server = { config, pkgs, lib, ... }: server = { config, pkgs, lib, ... }:
let
clamav-db-files = pkgs.stdenv.mkDerivation rec {
name = "clamav-db-files";
src = lib.cleanSource ./clamav;
dontUnpack = true;
installPhase = ''
mkdir $out
cp -R $src/*.cvd $out/
'';
};
in
{ {
imports = [ imports = [
../default.nix ../default.nix
@ -58,9 +47,9 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
''; '';
script = '' script = ''
cp ${clamav-db-files}/main.cvd /var/lib/clamav/ cp ${blobs}/clamav/main.cvd /var/lib/clamav/
cp ${clamav-db-files}/daily.cvd /var/lib/clamav/ cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
cp ${clamav-db-files}/bytecode.cvd /var/lib/clamav/ cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
chown clamav:clamav /var/lib/clamav/* chown clamav:clamav /var/lib/clamav/*
''; '';
@ -73,7 +62,6 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
mailserver = { mailserver = {
enable = true; enable = true;
debug = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ]; domains = [ "example.com" "example2.com" ];
virusScanning = true; virusScanning = true;
@ -194,52 +182,56 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
testScript = { nodes, ... }: testScript = { nodes, ... }:
'' ''
startAll; start_all()
$server->waitForUnit("multi-user.target"); server.wait_for_unit("multi-user.target")
$client->waitForUnit("multi-user.target"); client.wait_for_unit("multi-user.target")
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket. # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
$server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ \$? -eq 124 ]"); server.wait_until_succeeds(
$server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ \$? -eq 124 ]"); "set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/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 ]"
)
$client->execute("cp -p /etc/root/.* ~/"); client.execute("cp -p /etc/root/.* ~/")
$client->succeed("mkdir -p ~/mail"); client.succeed("mkdir -p ~/mail")
$client->succeed("ls -la ~/ >&2"); client.succeed("ls -la ~/ >&2")
$client->succeed("cat ~/.fetchmailrc >&2"); client.succeed("cat ~/.fetchmailrc >&2")
$client->succeed("cat ~/.procmailrc >&2"); client.succeed("cat ~/.procmailrc >&2")
$client->succeed("cat ~/.msmtprc >&2"); client.succeed("cat ~/.msmtprc >&2")
# fetchmail returns EXIT_CODE 1 when no new mail # fetchmail returns EXIT_CODE 1 when no new mail
$client->succeed("fetchmail --nosslcertck -v || [ \$? -eq 1 ] >&2"); client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
# Verify that mail can be sent and received before testing virus scanner # Verify that mail can be sent and received before testing virus scanner
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
$client->succeed("msmtp -a user2 user1\@example.com < /etc/root/safe-email >&2"); client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
# give the mail server some time to process the mail # give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# fetchmail returns EXIT_CODE 0 when it retrieves mail # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v >&2"); client.succeed("fetchmail --nosslcertck -v >&2")
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
subtest "virus scan file", sub { with subtest("virus scan file"):
$server->succeed("clamdscan \$(readlink -f /etc/root/eicar.com.txt) | grep \"Txt\\.Malware\\.Agent-1787597 FOUND\" >&2"); server.succeed(
}; 'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
)
subtest "virus scan email", sub { with subtest("virus scan email"):
$client->succeed("msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep \"server message: 554 5\\.7\\.1\" >&2"); client.succeed(
$server->succeed("journalctl -u rspamd | grep -i eicar"); '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 # give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
};
subtest "no warnings or errors", sub {
$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 -i warning >&2");
};
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 -i warning >&2")
''; '';
} }

View file

@ -1 +0,0 @@
*cvd filter=lfs diff=lfs merge=lfs -text

View file

@ -1 +0,0 @@
mirrors.dat

Binary file not shown.

Binary file not shown.

View file

@ -1 +0,0 @@
DatabaseMirror database.clamav.net

View file

@ -1,5 +0,0 @@
{
"bytecode.cvd": "633d4f0a2054249e23df12db5a9e76bcaac23cadaef5ee8f644986f600d8d81e",
"daily.cvd": "0b6798b54e490be168b873d39ebda41ff4a027720aed855f879779b88982838f",
"main.cvd": "9694933f37148ec39c1f2ef7b97211ded9b03b140bb48a5eeb27270120844b24"
}

Binary file not shown.

View file

@ -1,15 +0,0 @@
#!/bin/sh
set -e
cd "$(dirname "${0}")"
rm ./*.cvd hashes.json || :
freshclam --datadir=. --config-file=freshclam.conf
(for i in ./*.cvd;
do echo '{}' |
jq --arg path "$(basename "${i}")" \
--arg sha256sum "$(sha256sum "${i}" | awk '{ print $1; }')" \
'.[$path] = $sha256sum'; done) |
jq -s add > hashes.json

View file

@ -1,46 +0,0 @@
# Generate an attribute sets containing all tests for all releaeses
# It looks like:
# - extern.nixpkgs_20.03
# - extern.nixpkgs_unstable
# - intern.nixpkgs_20.03
# - intern.nixpkgs_unstable
with builtins;
let
sources = import ../nix/sources.nix;
releases = listToAttrs (map genRelease releaseNames);
genRelease = name: {
name = name;
value = import sources."${name}" {};
};
genTest = testName: release:
let
pkgs = releases."${release}";
test = pkgs.callPackage (./. + "/${testName}.nix") { };
in {
"name"= builtins.replaceStrings ["." "-"] ["_" "_"] release;
"value"= test { inherit pkgs; };
};
releaseNames = [
"nixpkgs-20.03"
"nixpkgs-unstable"
];
testNames = [
"intern"
"extern"
"clamav"
];
# Generate an attribute set containing one test per releases
genTests = testName: {
name = testName;
value = listToAttrs (map (genTest testName) (builtins.attrNames releases));
};
in listToAttrs (map genTests testNames)

View file

@ -14,10 +14,10 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ pkgs ? import <nixpkgs> {}}: { pkgs ? import <nixpkgs> {}, ...}:
import (pkgs.path + "/nixos/tests/make-test.nix") {
pkgs.nixosTest {
name = "external";
nodes = { nodes = {
server = { config, pkgs, ... }: server = { config, pkgs, ... }:
{ {
@ -43,6 +43,11 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
domains = [ "example.com" "example2.com" ]; domains = [ "example.com" "example2.com" ];
rewriteMessageId = true; rewriteMessageId = true;
dkimKeyBits = 1535; dkimKeyBits = 1535;
dmarcReporting = {
enable = true;
domain = "example.com";
organizationName = "ACME Corp";
};
loginAccounts = { loginAccounts = {
"user1@example.com" = { "user1@example.com" = {
@ -70,6 +75,15 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
enableImap = true; enableImap = true;
enableImapSsl = true; enableImapSsl = true;
fullTextSearch = {
enable = true;
autoIndex = true;
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ];
enforced = "yes";
# fts-xapian warns when memory is low, which makes the test fail
memoryLimit = 100000;
};
}; };
}; };
client = { nodes, config, pkgs, ... }: let client = { nodes, config, pkgs, ... }: let
@ -139,12 +153,32 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
imap.close() imap.close()
''; '';
search = pkgs.writeScriptBin "search" ''
#!${pkgs.python3.interpreter}
import imaplib
import sys
[_, mailbox, needle] = sys.argv
with imaplib.IMAP4_SSL('${serverIP}') as imap:
imap.login('user1@example.com', 'user1')
imap.select(mailbox)
status, [response] = imap.search(None, 'BODY', repr(needle))
msg_ids = [ i for i in response.decode("utf-8").split(' ') if i ]
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
status, response = imap.fetch(msg_ids[0], '(RFC822)')
assert status == "OK"
assert needle in repr(response)
imap.close()
'';
in { in {
imports = [ imports = [
./lib/config.nix ./lib/config.nix
]; ];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search
]; ];
environment.etc = { environment.etc = {
"root/.fetchmailrc" = { "root/.fetchmailrc" = {
@ -276,143 +310,205 @@ import (pkgs.path + "/nixos/tests/make-test.nix") {
XOXO User1 XOXO User1
''; '';
"root/email6".text = ''
Message-ID: <123457qwerty@host.local.network>
From: User2 <user2@example.com>
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from user2 to user1
Reply-To:
Hello User1,
this email contains the needle:
576a4565b70f5a4c1a0925cabdb587a6
'';
"root/email7".text = ''
Message-ID: <1234578qwerty@host.local.network>
From: User2 <user2@example.com>
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from user2 to user1
Reply-To:
Hello User1,
this email does not contain the needle :(
'';
}; };
}; };
}; };
testScript = { nodes, ... }: testScript = { nodes, ... }:
'' ''
startAll; start_all()
$server->waitForUnit("multi-user.target"); server.wait_for_unit("multi-user.target")
$client->waitForUnit("multi-user.target"); client.wait_for_unit("multi-user.target")
# TODO put this blocking into the systemd units? # TODO put this blocking into the systemd units?
$server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/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/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
$client->execute("cp -p /etc/root/.* ~/"); client.execute("cp -p /etc/root/.* ~/")
$client->succeed("mkdir -p ~/mail"); client.succeed("mkdir -p ~/mail")
$client->succeed("ls -la ~/ >&2"); client.succeed("ls -la ~/ >&2")
$client->succeed("cat ~/.fetchmailrc >&2"); client.succeed("cat ~/.fetchmailrc >&2")
$client->succeed("cat ~/.procmailrc >&2"); client.succeed("cat ~/.procmailrc >&2")
$client->succeed("cat ~/.msmtprc >&2"); client.succeed("cat ~/.msmtprc >&2")
subtest "imap retrieving mail", sub { with subtest("imap retrieving mail"):
# fetchmail returns EXIT_CODE 1 when no new mail # fetchmail returns EXIT_CODE 1 when no new mail
$client->succeed("fetchmail --nosslcertck -v || [ \$? -eq 1 ] >&2"); client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
};
subtest "submission port send mail", sub { with subtest("submission port send mail"):
# send email from user2 to user1 # 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"); client.succeed(
"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 # give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
};
subtest "imap retrieving mail 2", sub { with subtest("imap retrieving mail 2"):
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# fetchmail returns EXIT_CODE 0 when it retrieves mail # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v >&2"); client.succeed("fetchmail --nosslcertck -v >&2")
};
subtest "remove sensitive information on submission port", sub { with subtest("remove sensitive information on submission port"):
$client->succeed("cat ~/mail/* >&2"); client.succeed("cat ~/mail/* >&2")
## make sure our IP is _not_ in the email header ## make sure our IP is _not_ in the email header
$client->fail("grep-ip ~/mail/*"); client.fail("grep-ip ~/mail/*")
$client->succeed("check-mail-id ~/mail/*"); client.succeed("check-mail-id ~/mail/*")
};
subtest "have correct fqdn as sender", sub { with subtest("have correct fqdn as sender"):
$client->succeed("grep 'Received: from mail.example.com' ~/mail/*"); client.succeed("grep 'Received: from mail.example.com' ~/mail/*")
};
subtest "dkim has user-specified size", sub { with subtest("dkim has user-specified size"):
$server->succeed("openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'"); server.succeed(
}; "openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'"
)
subtest "dkim singing, multiple domains", sub { with subtest("dkim singing, multiple domains"):
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# send email from user2 to user1 # 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"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v"); client.succeed("fetchmail --nosslcertck -v")
$client->succeed("cat ~/mail/* >&2"); client.succeed("cat ~/mail/* >&2")
# make sure it is dkim signed # make sure it is dkim signed
$client->succeed("grep DKIM ~/mail/*"); client.succeed("grep DKIM ~/mail/*")
};
subtest "aliases", sub { with subtest("aliases"):
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# send email from chuck to postmaster # 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"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v"); client.succeed("fetchmail --nosslcertck -v")
};
subtest "catchAlls", sub { with subtest("catchAlls"):
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# send email from chuck to non exsitent account # 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"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v"); client.succeed("fetchmail --nosslcertck -v")
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# send email from user1 to chuck # 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"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 1 when no new mail
# if this succeeds, it means that user1 recieved the mail that was intended for chuck. # if this succeeds, it means that user1 recieved the mail that was intended for chuck.
$client->fail("fetchmail --nosslcertck -v"); client.fail("fetchmail --nosslcertck -v")
};
subtest "extraVirtualAliases", sub { with subtest("extraVirtualAliases"):
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# send email from single-alias to user1 # 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"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v"); client.succeed("fetchmail --nosslcertck -v")
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
# send email from user1 to multi-alias (user{1,2}@example.com) # 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"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail --nosslcertck -v"); client.succeed("fetchmail --nosslcertck -v")
};
subtest "quota", sub { with subtest("quota"):
$client->execute("rm ~/mail/*"); client.execute("rm ~/mail/*")
$client->execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc"); client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
$client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota\@example.com < /etc/root/email2 >&2"); client.succeed(
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); "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 # fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->fail("fetchmail --nosslcertck -v"); client.fail("fetchmail --nosslcertck -v")
}; with subtest("imap sieve junk trainer"):
subtest "imap sieve junk trainer", sub {
# send email from user2 to user1 # 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"); client.succeed(
"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 # give the mail server some time to process the mail
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
$client->succeed("imap-mark-spam >&2"); client.succeed("imap-mark-spam >&2")
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2"); server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2")
$client->succeed("imap-mark-ham >&2"); client.succeed("imap-mark-ham >&2")
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2"); server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2")
};
subtest "no warnings or errors", sub { with subtest("full text search and indexation"):
$server->fail("journalctl -u postfix | grep -i error >&2"); # send 2 email from user2 to user1
$server->fail("journalctl -u postfix | grep -i warning >&2"); client.succeed(
$server->fail("journalctl -u dovecot2 | grep -i error >&2"); "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email6 >&2"
$server->fail("journalctl -u dovecot2 | grep -i warning >&2"); )
}; client.succeed(
"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" ]')
# should find exactly one email containing this
client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2")
# 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"
)
# check that Junk is not indexed
server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2")
with subtest("dmarc reporting"):
server.systemctl("start rspamd-dmarc-reporter.service")
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")
# 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"
)
''; '';
} }

View file

@ -1,85 +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/>
{ pkgs ? import <nixpkgs> {}}:
let
sendMail = pkgs.writeTextFile {
"name" = "send-mail-to-send-only-account";
"text" = ''
EHLO mail.example.com
MAIL FROM: none@example.com
RCPT TO: send-only@example.com
QUIT
'';
};
hashPassword = password: pkgs.runCommand
"password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; } ''
mkpasswd -m sha-512 ${password} > $out
'';
in
import (pkgs.path + "/nixos/tests/make-test.nix") {
machine =
{ config, pkgs, ... }:
{
imports = [
./../default.nix
./lib/config.nix
];
virtualisation.memorySize = 1024;
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
};
"send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only";
sendOnly = true;
};
};
vmailGroupName = "vmail";
vmailUID = 5000;
};
};
testScript =
''
$machine->start;
$machine->waitForUnit("multi-user.target");
subtest "vmail gid is set correctly", sub {
$machine->succeed("getent group vmail | grep 5000");
};
subtest "mail to send only accounts is rejected", sub {
$machine->waitForOpenPort(25);
# TODO put this blocking into the systemd units?
$machine->waitUntilSucceeds("timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ \$? -eq 124 ]");
$machine->succeed("cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'" );
};
'';
}

195
tests/internal.nix Normal file
View file

@ -0,0 +1,195 @@
# 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/>
{ pkgs ? import <nixpkgs> {}, ...}:
let
sendMail = pkgs.writeTextFile {
"name" = "send-mail-to-send-only-account";
"text" = ''
EHLO mail.example.com
MAIL FROM: none@example.com
RCPT TO: send-only@example.com
QUIT
'';
};
hashPassword = password: pkgs.runCommand
"password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
mkpasswd -sm bcrypt <<<"$password" > $out
'';
hashedPasswordFile = hashPassword "my-password";
passwordFile = pkgs.writeText "password" "my-password";
in
pkgs.nixosTest {
name = "internal";
nodes = {
machine = { config, pkgs, ... }: {
imports = [
./../default.nix
./lib/config.nix
];
virtualisation.memorySize = 1024;
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "domain.com" ];
localDnsResolver = false;
loginAccounts = {
"user1@example.com" = {
hashedPasswordFile = hashedPasswordFile;
};
"user2@example.com" = {
hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [''/^user2.*@domain\.com$/''];
};
"send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only";
sendOnly = true;
};
};
forwards = {
# user2@example.com is a local account and its mails are
# also forwarded to user1@example.com
"user2@example.com" = "user1@example.com";
};
vmailGroupName = "vmail";
vmailUID = 5000;
enableImap = false;
};
};
};
testScript = ''
machine.start()
machine.wait_for_unit("multi-user.target")
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
with subtest("mail forwarded can are locally kept"):
# A mail sent to user2@example.com is in the user1@example.com mailbox
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--imap-host localhost",
"--imap-username user1@example.com",
"--from-addr user1@example.com",
"--to-addr user2@example.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
# A mail sent to user2@example.com is in the user2@example.com mailbox
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--imap-host localhost",
"--imap-username user2@example.com",
"--from-addr user1@example.com",
"--to-addr user2@example.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
with subtest("regex email alias are received"):
# A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--imap-host localhost",
"--imap-username user2@example.com",
"--from-addr user1@example.com",
"--to-addr user2-regex-alias@domain.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
with subtest("user can send from regex email alias"):
# A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received
machine.succeed(
" ".join(
[
"mail-check send-and-read",
"--smtp-port 587",
"--smtp-starttls",
"--smtp-host localhost",
"--imap-host localhost",
"--smtp-username user2@example.com",
"--from-addr user2-regex-alias@domain.com",
"--to-addr user1@example.com",
"--src-password-file ${passwordFile}",
"--dst-password-file ${passwordFile}",
"--ignore-dkim-spf",
]
)
)
with subtest("vmail gid is set correctly"):
machine.succeed("getent group vmail | grep 5000")
with subtest("mail to send only accounts is rejected"):
machine.wait_for_open_port(25)
# TODO put this blocking into the systemd units
machine.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
machine.succeed(
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/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>'"
)
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'"
)
'';
}

183
tests/ldap.nix Normal file
View file

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

View file

@ -14,9 +14,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
import <nixpkgs/nixos/tests/make-test.nix> { import <nixpkgs/nixos/tests/make-test-python.nix> {
machine = nodes.machine =
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
imports = [ imports = [
@ -26,6 +26,6 @@ import <nixpkgs/nixos/tests/make-test.nix> {
testScript = testScript =
'' ''
$machine->waitForUnit("multi-user.target"); machine.wait_for_unit("multi-user.target");
''; '';
} }

89
tests/multiple.nix Normal file
View file

@ -0,0 +1,89 @@
# This tests is used to test features requiring several mail domains.
{ pkgs ? import <nixpkgs> {}, ...}:
let
hashPassword = password: pkgs.runCommand
"password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; }
''
mkpasswd -sm bcrypt <<<"$password" > $out
'';
password = pkgs.writeText "password" "password";
domainGenerator = domain: { config, pkgs, ... }: {
imports = [../default.nix];
virtualisation.memorySize = 1024;
mailserver = {
enable = true;
fqdn = "mail.${domain}";
domains = [ domain ];
localDnsResolver = false;
loginAccounts = {
"user@${domain}" = {
hashedPasswordFile = hashPassword "password";
};
};
enableImap = true;
enableImapSsl = true;
};
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
'';
};
};
in
pkgs.nixosTest {
name = "multiple";
nodes = {
domain1 = {...}: {
imports = [
../default.nix
(domainGenerator "domain1.com")
];
mailserver.forwards = {
"non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"];
"non@domain1.com" = ["user@domain2.com" "user@domain1.com"];
};
};
domain2 = domainGenerator "domain2.com";
client = { config, pkgs, ... }: {
environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')];
};
};
testScript = ''
start_all()
domain1.wait_for_unit("multi-user.target")
domain2.wait_for_unit("multi-user.target")
# 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 ]"
)
domain2.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
# user@domain1.com sends a mail to user@domain2.com
client.succeed(
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
)
# Send a mail to the address forwarded and check it is in the recipient mailbox
client.succeed(
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
)
'';
}