Compare commits

...

361 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
Antoine Eiche
781073b64d docs: create a readthedocs manual
The goal is to remove the WIKI since modifications can not be
submitted via PRs.
2020-07-06 19:48:51 +00:00
Brian Olsen
eb70dd1f55
Fix passwfile update
Currently oneshot services are not re-run on NixOS activate and that
meant that you needed to manually restart `gen-passwd-file` for new
users to be available.

This commit removes the `gen-passwd-file` service and instead simply
calls the same script in the dovecot2 preStart.
2020-06-19 16:02:21 +02:00
Benjamin Asbach
fb8886547b Enable dovecot option to prefer server ciphers
This might prevent misconfigured clients to use a weak cipher when stronger ciphers are available.
2020-05-27 10:10:59 +02:00
Benjamin Asbach
9b98746515 Set TLSv1.2 as minimal TLS version for dovecot
TLSv1 and TLSv1.1 are commonly opinioned as deprecated.
2020-05-27 10:10:59 +02:00
Benjamin Asbach
87e66046c1 Fixed url to contributor tab 2020-05-26 21:00:37 +02:00
Ryan Trinkle
066dba1b2f Fix spelling of lmtp in option 2020-05-25 18:19:32 -04:00
Antoine Eiche
e1b0bb42b4 ci: add 20.03 jobset 2020-05-24 20:41:03 +02:00
Antoine Eiche
54ecf17810 Release nixos-20.03 2020-05-24 20:41:03 +02:00
Brian Olsen
aed5d9e523
Switch from using postfix extraConfig to config
`services.postfix.extraConfig` is just a string while the
`services.postfix.config` option configures the same thing but with a
typed attrset instead which is easier to manipulate and override in Nix.
2020-05-22 12:19:50 +02:00
Benjamin Asbach
c2ee9f217a Enabled TLS 1.3 support 2020-05-13 00:20:22 +00:00
Antoine Eiche
b8e4ed00c3 Remove 19.09 support
Note there is still the 19.09 branch.
2020-05-11 21:46:01 +00:00
Antoine Eiche
830c66f1be readme: no more release but branch instead
For each NixOS release, we publish a branch. This would allow us to
continue to apply patches to these branches, in case of bug or
security fixes.
2020-05-11 21:46:01 +00:00
Antoine Eiche
7788eccc24 Merge branch 'eqyiel/nixos-mailserver-feat/make-lmtp_save_to_detail_mailbox-optional' 2020-05-08 21:02:55 +02:00
Antoine Eiche
9b5779de16 tests/intern: set memorySize to 1G
There is an OOM when running this test.
2020-05-08 20:43:46 +02:00
Antoine Eiche
abe3c4aedc Add send-only test
In the intern test, we send a mail to a send-only account and check
the reject message is correcly returned.
2020-05-08 20:43:46 +02:00
Antoine Eiche
41219cc690 Rename rejectMessage to sendOnlyRejectMessage 2020-05-08 20:43:46 +02:00
Galen Abell
d47e4ead88 Add send-only accounts 2020-05-08 20:43:46 +02:00
Symphorien Gibol
b7c49fa26a update nixpkgs unstable 2020-05-05 19:07:46 +00:00
Symphorien Gibol
8e95d4e456 tests/extern.nix: increase memory
got oom several time with rspamd 2.5 update
2020-05-05 19:07:46 +00:00
Symphorien Gibol
7ccf35cb5f tests/clamav.nix: relax expected error message
with rspamd 2.5 it only says Spam message rejected
2020-05-05 19:07:46 +00:00
Symphorien Gibol
9e772d166c rspamd: configure redis backend
The sqlite backed is deprecated, and the redis backend is the default
since rspamd 2.0.

Not having redis started results in such errors:
rspamd_redis_init: cannot init redis backend for BAYES_SPAM

To migrate the sqlite database, 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

The current module implements the recommended configuration that this
utility prints out.
2020-05-05 19:07:46 +00:00
Antoine Eiche
ac0f5c118f ci: build the branch 19.09 2020-05-05 21:03:10 +02:00
Antoine Eiche
899d68ac7a ci: better error message
The error message contains a link to the latest Hydra evaluation.
2020-04-23 00:23:17 +02:00
Antoine Eiche
b0647c95c9 Remove the GitLab runner related code 2020-04-22 23:35:48 +02:00
Antoine Eiche
b5263680a4 Check the Hydra master jobset as well 2020-04-22 23:35:48 +02:00
Antoine Eiche
493afb5f9a ci: check Hydra jobsets status 2020-04-19 15:08:32 +02:00
Antoine Eiche
1cac50dab5 .hydra: fix pull request jobset description 2020-04-19 14:59:01 +02:00
Antoine Eiche
2493056eed Setup an Hydra declarative project
This commit adds configuration files ingested an Hydra instance to
create a declarative project.
Currently, this Hydra instance builds
- the master branch
- all pull requests

We use the nix community provided Hydra instance
https://hydra.nix-community.org/project/simple-nixos-mailserver.
2020-04-19 10:01:57 +02:00
Antoine Eiche
09ca79801b tests: no ssl cert check for fetchmail
Since the certificate is autosigned, an SSL error was emitted by
dovecot.
2020-04-19 10:01:57 +02:00
Antoine Eiche
a53aa5ac9a Use Niv to pin nixpkgs releases
Before using Niv, we were following channels meaning we can not
reproduce CI jobs easily.

In this change, we use Niv to pin these dependencies. We are also
addding a tests/default.nix to be able to run these tests locally.

For instance, to run the test extern.nix on the nixpkgs-19.09 release:

    nix-build tests/default.nix -A extern.nixpkgs_19_09

Fixes #178
2020-04-19 10:01:57 +02:00
Galen Abell
6563abc1c4 Fix password hash file generation behavior
- Move the "create password hash file from hashed password" behavior to
  a separate variable, since having it in the default field of config
  would always cause the warning to trigger
- Change type of hashedPassword to `nullOr str`
2020-03-06 17:27:47 +00:00
Robin Raymond
7bda4c4f11 automatically update readme hash 2019-12-18 09:33:52 +01:00
Robin Raymond
5d1f5cb349 update readme for v2.3.0 2019-12-16 21:08:33 +01:00
Xavier Vello
289f71efe2 Retry tests twice on gitlab 2019-12-15 14:29:16 +01:00
Xavier Vello
d7b62bbb93 Update clamav test fixtures 2019-12-15 11:53:27 +01:00
Xavier Vello
9dae3d2cdc Run tests on 19.03 + 19.09 + unstable 2019-12-14 22:22:23 +01:00
Xavier Vello
5c6b6287d6 Fix extern test on 19.09
The openssl output format changed, breaking the
"dkim has user-specified size" test
2019-12-14 22:22:23 +01:00
Xavier Vello
43df84e1a0 Fix dovecot in 19.09+
Dovecot now requires at least 1024bits of dh params.
Test duration seems is not significantly increased.
2019-12-14 22:22:23 +01:00
Xavier Vello
5fb707e61a Update clamav test to use local fixtures
The current version fetches clamav-db-files from gitlab,
in the master branch. This is currently broken as the files
were removed, and does not allow to ensure a fixture update
does not break the test
2019-12-14 22:22:23 +01:00
Xavier Vello
81e4a49708 Restore tests and script deleted in b8661825 2019-12-14 22:22:10 +01:00
Maximilian Bosch
14cabd62e5
Trigger restart of postfix if passwords of mail accounts change 2019-10-18 21:21:01 +02:00
JosephTheEngineer
b866182532 Remove use of the deprecated string type 2019-09-22 13:32:37 +00:00
Martin Milata
ab33e87cea Delete leftover services.nix 2019-09-16 18:14:20 +02:00
Ruben Maher
b4f6d96365 fix: make surprising lmtp_save_to_detail_mailbox behaviour optional 2019-09-01 20:21:11 +09:00
Robin Raymond
4b480d1445 Merge branch 'metapensiero/nixos-mailserver-delimiter-master' 2019-08-13 19:57:31 +02:00
Robin Raymond
ee7bb07f25 Merge branch 'scintill/nixos-mailserver-dkim-bits' 2019-08-13 19:56:18 +02:00
Robin Raymond
0bf2bb0b54 Merge branch 'scintill/nixos-mailserver-fix-tests' 2019-08-13 19:51:16 +02:00
Alberto Berti
76922632ca Merge branch 'verbose-spam-header' into verbose-spam-header-master 2019-07-26 19:37:18 +02:00
Alberto Berti
6033364d0b Merge branch 'delimiter' into delimiter-master 2019-07-26 19:28:51 +02:00
Alberto Berti
05bb5518ad Let the milter add to headers the reason for tagging a message as spam 2019-07-26 19:01:54 +02:00
Alberto Berti
0ff81a9593 Make the delimiter configuration work 2019-07-26 19:00:32 +02:00
Alberto Berti
fad71d9948 Fix typo 2019-07-25 17:55:01 +02:00
Alberto Berti
253c8732b4 Add subaddresses configuration 2019-07-25 17:30:20 +02:00
Joey Hewitt
f789f7a80c add dkimKeyBits configuration 2019-07-09 21:59:28 -06:00
Joey Hewitt
7e718e0e33 dkim: transition to PermissionsStartOnly=false
That's how nixpkgs-unstable is now, so to be compatible with both we
have to force that setting. Use systemd tmpfiles to provision
directory with correct owner.
2019-07-07 21:47:09 -06:00
Joey Hewitt
93660eabcd fixes to tests
- restructure rspamd config. It's nicer now, and it was getting
overridden the old way.
- "scan_mime_parts = false" apparently must be used in rspamd for ClamAV
to work
- refactor the clamav test a bit for cleanliness
- wait for rspamd and clamd sockets to open, before testing
- use clamdscan for speed, and verify that the virus was found
- verify msmtp returns virus scan result
2019-07-07 21:47:09 -06:00
Joey Hewitt
0e6bb4e898 workaround GitLab CI KVM issue
Their CI environment currently doesn't have KVM.  This commit should be
reverted when/if they do, for much better CI speed.

You can still run tests locally on your KVM-enabled machine as documented
on the wiki.

Workaround on GitLab is several pieces (injected through .gitlab-ci.yml):
- Make a /dev/kvm file so that nix thinks we have "kvm" system feature
and proceeds with executing the tests.
- Inject a QEMU package that replaces qemu-kvm with a full emulator.
- Monkey-patch the test script to wait longer for the VM to boot, since
it's slow on full emulation. 1200 seconds, double the previous value.
The patch method is not bulletproof, but better than maintaining forks of
nixpkgs.
- Set systemd's DefaultTimeoutStartSec=15min, so nix's "backdoor" test
service doesn't time out on the slow boot.
2019-07-07 21:47:09 -06:00
Joey Hewitt
05d963e751 lower dhparam bits, for faster tests
Make a tests/lib/config.nix file that is imported into the configuration
of all testing VMs.
2019-07-06 21:08:26 -06:00
Oscar Carlsson
4e8fbac580 Disable TLSv1.0 and deprecated ciphers.
TLSv1.0 is as deprecated as the older SSL versions, and should not be
used. I've also disabled a slew of ciphers, and hopefully this will
make us less vulnerable to downgrade attacks and similar.
2019-06-21 11:09:30 +02:00
Robin Raymond
ba3336978e correct checksum 2019-05-03 17:35:33 +02:00
Robin Raymond
e35959b65f fix checksum 2019-05-03 17:33:39 +02:00
Robin Raymond
a658e7fc6c Advertise mailing list a little more 2019-05-03 14:38:41 +02:00
Robin Raymond
d127730f27 modify readme for v2.2.1 2019-05-03 14:21:18 +02:00
Robin Raymond
642a15fbf7 Merge branch 'master' of gitlab.com:simple-nixos-mailserver/nixos-mailserver 2019-05-03 14:13:42 +02:00
Robin Raymond
72e79e5c38 fix minimal test 2019-05-03 14:13:22 +02:00
Robin Raymond
e2702c8c8e update to nixos 19.03 2019-05-03 14:13:11 +02:00
Christian Kauhaus
bce95d0229 Use services.postfix.virtual option
SNM used to define virtual_alias_maps in extraConfig which collides with
the same parameter defined by the standard services.postfix.virtual
option. This led to *lots* of warnings during postfix startup like

```
May 02 18:29:58 nun postfix/master[24758]: warning: /etc/postfix/main.cf, line 47: overriding earlier entry: virtual_alias_maps=hash:/etc/postfix/virtual
```

Refraining from overriding virtual_alias_maps has the additional
advantage that virtual aliases defined by other modules dont' stop
working with SNM.
2019-05-03 11:25:23 +00:00
Robin Raymond
a485cb3719 fix nixops file 2019-05-03 13:11:47 +02:00
Christian Kauhaus
184975be76 Fix renamed rspamd_proxy option
Fixes #152
2019-05-03 10:54:15 +02:00
Tom
2c59de8dcb README: include sha256 of the expected release tarball
Advantages of including the SHA256:
(i) removes the impurity of the tarball contents being changed
(ii) if sha256 is not included, then each nixops deploy triggers a re-download of the tarball

Here's how to get the expected hash:

$ nix-prefetch-url --unpack 'https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.2.0/nixos-mailserver-v2.2.0.tar.gz'
unpacking...
[0.0 MiB DL]
path is '/nix/store/dwg8xlfnlw7mhr4cjk1viwmm0b249b74-nixos-mailserver-v2.2.0.tar.gz'
0gqzgy50hgb5zmdjiffaqp277a68564vflfpjvk1gv6079zahksc
2019-01-28 14:21:51 +00:00
Michishige Kaito
c2ca4d1bb0 postfix: allow configuring message_size_limit 2018-11-23 14:29:23 +00:00
Robin Raymond
ebf34930a7 udpate readme 2018-11-11 18:18:58 +01:00
Robin Raymond
8b7dde4b54 remove rspamd socket 2018-11-11 18:03:04 +01:00
Robin Raymond
1c1e301c11 Merge branch 'master' of gitlab.com:simple-nixos-mailserver/nixos-mailserver 2018-11-10 14:38:27 +01:00
Robin Raymond
91ce33f0e0 Merge branch 'eqyiel/nixos-mailserver-clamav-db-in-lfs' 2018-11-10 14:37:53 +01:00
Robin Raymond
e6069c276a Update .gitlab-ci.yml to 18.09 2018-11-10 13:33:31 +00:00
Robin Raymond
acd65c0803 New Feature >>rejectSender<<
Authored by tokudan
2018-11-10 14:29:16 +01:00
Robin Raymond
28cff2497a Merge branch 'plchldr/nixos-mailserver-master' 2018-11-10 14:09:26 +01:00
Robin Raymond
d624740db5 Update README.md to use gitlab ci badge 2018-11-10 12:37:38 +00:00
Ruben Maher
a4046a1227 Setup ci on gitlab 2018-11-10 12:35:09 +00:00
plchldr
fa0541b96b remove Diffie Hillman parameter creation as it is handled by the upstream dovecot2 module as of 18.09 2018-10-30 17:56:25 +01:00
Ruben Maher
9488b6fd43 ci: store Clamav DB in LFS
Fix #136 (stop pulling the files from @griff's poor server), also add a script
to update the files.

The fun thing about this is that due to sourcing the files from
`https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/raw/master/tests/clamav`
during the tests, updates to the `hashes.json` and `*.cvd` files will always
fail CI.  I guess this is a reasonable tradeoff as long as people are aware of
it.
2018-08-18 15:13:25 +09:30
Ruben Maher
817d84d36d remove ISSUE_TEMPLATE.md
This file is not used by GitLab.
2018-08-18 12:05:09 +09:30
Ruben Maher
3aecb1299d Merge branch 'pr-old-upstream-126' into 'master'
griff:my-changes

See merge request simple-nixos-mailserver/nixos-mailserver!132
2018-06-29 12:16:19 +00:00
Brian Olsen
88e292c5b7 postfix: Support setting options for policyd-spf 2018-06-29 21:36:34 +09:30
Brian Olsen
61df799036 dovecot: Add spam filter traning using imapsieve 2018-06-29 21:36:34 +09:30
Brian Olsen
616d779e1f Move from rmilter to rspamd #25 2018-06-29 21:36:34 +09:30
Brian Olsen
410c6c410b Use nixpkgs functions to check dovecot version 2018-06-29 21:36:34 +09:30
Brian Olsen
1c76e0a119 tests: Add ClamAV test and fix errors in virus scanning 2018-06-29 21:36:34 +09:30
Brian Olsen
e32a915489 postfix: Use pypolicyd-spf for SPF checking 2018-06-29 21:35:16 +09:30
Brian Olsen
f209fa3bf3 postfix: use masterConfig option instead of extraMasterConf
extraMasterConf is just a string while masterConfig is a nix module so
the options are more explicit and has help text.
2018-06-29 21:35:16 +09:30
Brian Olsen
7036371f75 Use OpenDKIM instead of rmilter for DKIM
As part of #61 this moves DKIM handling from rmilter to OpenDKIM.
2018-06-29 21:35:16 +09:30
Brian Olsen
0c883d8bcd tests/extern: Fix small errors
1. The lowquota test used the wrong fetchmail username and password
2. When running interactively repeatedly mkdir fails since dir exists
2018-06-29 21:35:16 +09:30
Brian Olsen
8a27b941bf Start dovecot before postfix and add target for certificates
It seemed weird to me that preStart on postfix was used to generate
files not needed directly by postfix and for the self-signed
certificate which is also needed by dovecot. nginx.service was also
used as a proxy for when ACME certificate generation was done.

So I have created mailserver-certificates.target for when certificates
are available for other services. For self-signed that means that a
new oneshot service called mailserver-selfsigned-certificate has been
run. And for ACME this means that the target
acme-selfsigned-certificates has been reached (which is when acme has
created the self-signed certificates used before the actual
certificates provided by LetsEncrypt are created). This setup has the
added bonus that if you want to run a service to provide your own
certificates you can set that to run before
mailserver-certificates.target.

DH Parameters are only needed by dovecot so generation of that file has
been moved to the dovecot2 preStart.

And lastly the only remaining reason to for dovecot to start before
postfix was that the auth and lmtp sockets where located in a directory
created by postfix. But since they could just as well be located in
/run/dovecot2 as long as postfix has access to them I have moved them
there.
2018-06-29 21:35:16 +09:30
Brian Olsen
0fbfbafb6e Make dovecot sockets use postfix user/group options 2018-06-29 21:35:16 +09:30
Robin Raymond
99f64355eb Merge branch 'master' into 'master'
Master

See merge request simple-nixos-mailserver/nixos-mailserver!131
2018-06-27 14:15:15 +00:00
nico202
18da60451f clean extra whitespaces 2018-06-27 14:48:15 +02:00
nico202
3541f76be5 Remove reference to github in sieveScript example 2018-06-27 14:48:15 +02:00
nico202
bb26860cf2 readme: remove references to github -> gitlab 2018-06-27 14:48:15 +02:00
Robin Raymond
ffc67fef46 remove cache 2018-06-09 15:21:21 +02:00
Robin Raymond
f016b9689a
Merge pull request #128 from Infinisil/fix-enable-conditions
fix conditions for enabling services
2018-06-09 15:18:46 +02:00
Robin Raymond
cfb8353f1a
Merge pull request #130 from phdoerfler/clamav-phishing
Disabled scanning of incoming mails for phishing attempts
2018-06-09 15:17:18 +02:00
Philipp Dörfler
92238c61f6 Disabled scanning of incoming mails for phishing attempts 2018-06-09 09:13:56 +00:00
Silvan Mosberger
845e06e61a
fix conditions for enabling services
Without this fix, kresd and others would get enabled even though the
main mailserver option is disabled.
2018-05-22 23:18:55 +02:00
Robin Raymond
68232ddf87
Merge pull request #116 from phdoerfler/post-upgrade-check
Added option for automatic reboot after a kernel upgrade.
2018-05-10 13:06:46 +02:00
Robin Raymond
6d3ab77a5d
Merge pull request #114 from geistesk/message-id
Fog user's hostname in the Message-ID
2018-05-10 13:05:32 +02:00
Robin Raymond
02b0e867d2
Merge pull request #124 from nlewo/pr-dh.pem
postfix: also create the dh.pem if it is empty
2018-05-10 13:04:35 +02:00
Robin Raymond
e0907f489b
Merge pull request #117 from tokudan/reject_recipients
Allow rejecting mails to selected local addresses from remote systems
2018-05-10 13:02:37 +02:00
Antoine Eiche
e9dea6cdb4 postfix: also create the dh.pem if it is empty
The dh.pem file is currently created by the postfix prestart
script. If the entropy of the system is to low, the postfix prestart
can timeout. In this case, an empty file is created.
If the user restarts the postfix service, the dh.pem is not created
because the file already exists (but is empty).
When a ssl is established with dovecot, it fails with this message:
imap-login: `Error:Failed to initialize SSL server context: Couldn't parse DH parameters:
error:0906D06C:PEM routines:PEM_read_bio:no start line: Expecting: DH
PARAMETERS`

With this patch, the postfix service creates the dh.pem if the dh.pem
doesn't exist or if it is empty. It doesn't fix the entropy or
timeout issue but at least, the user knows something is failing:/
2018-05-10 01:04:02 +02:00
Robin Raymond
31dae8a5f3
Merge pull request #121 from nlewo/pr-relaxed
dkim: set header canonicalization to relaxed
2018-05-06 11:32:37 +02:00
Antoine Eiche
0f75894b4f dkim: set header canonicalization to relaxed
Instead of simple canonicalization which is the default one.

Fixes #120
2018-05-04 19:55:25 +02:00
Daniel Frank
f613779999 Allow rejecting mails to selected local addresses from remote systems 2018-04-15 01:49:26 +02:00
Philipp Dörfler
610a4008dc Added option for automatic reboot after a kernel upgrade. 2018-04-12 10:31:49 +00:00
geistesk
386faf960c Fog user's hostname in the Message-ID 2018-04-09 22:14:17 +02:00
Robin Raymond
1dd394e63f
Allow longer cachings 2018-04-08 17:57:16 +02:00
Robin Raymond
ea2cc9fbfa simplify travis.yml 2018-04-08 17:05:25 +02:00
Robin Raymond
37ba2c656c test both 18.03 and unstable 2018-04-08 16:54:53 +02:00
Robin Raymond
52b4733f22 log failures 2018-04-08 16:26:15 +02:00
Robin Raymond
6bc15dd52c move travis ci to 18.03 2018-04-08 16:03:16 +02:00
Robin Raymond
b8314865fa version update 2018-04-08 15:59:48 +02:00
Robin Raymond
e4c6682eb9 Merge branch 'master' of github.com:r-raymond/nixos-mailserver 2018-04-08 15:28:58 +02:00
Robin Raymond
c28d7756c1
Merge pull request #101 from tokudan/mydestination
Avoid accepting mail to xyz@localhost from remote systems
2018-04-08 15:25:48 +02:00
Robin Raymond
319a6dd793
Merge pull request #112 from tokudan/catchAll_doc
Clarify distinction between aliases and catchAll
2018-04-08 15:25:22 +02:00
Daniel Frank
c0e51245bb Clarify distinction between aliases and catchAll 2018-04-04 19:20:53 +02:00
Robin Raymond
8e0984de9b
Merge pull request #109 from geistesk/multiple-aliases
Support for multiple extraVirtualAliases
2018-04-03 13:51:13 +02:00
geistesk
c0df22aaae Support for multiple extraVirtualAliases
Should fix #104 by introducing

```
extraVirtualAliases = {
  "single-alias@domain.foobar" = "user1@domain.foobar";
  "multi-alias@domain.foobar" = [
    "user1@domain.foobar" "user2@domain.foobar" ];
};
```
2018-04-03 11:52:03 +02:00
Robin Raymond
234f92f8a8
Merge pull request #106 from phdoerfler/mail_max_userip_connections
Added dovecot option for mail_max_userip_connections defaulting to 100
2018-03-14 15:26:37 +01:00
Philipp Dörfler
4f36b72dd6 Added dovecot option for mail_max_userip_connections defaulting to 100 2018-03-13 10:43:30 +00:00
Robin Raymond
e3a12093b7
Merge pull request #93 from phdoerfler/borgbackup
Added basic support for borgbackup
2018-03-10 18:20:19 +01:00
Daniel Frank
f283b6750b Avoid accepting mail to xyz@localhost. Local email should be aliased to another user anyway. 2018-03-10 17:13:30 +01:00
Robin Raymond
f69081226d
Merge pull request #98 from tokudan/fix_backscatter
Avoid backscattering on unknown recipients. Fixes #97
2018-03-06 16:00:54 +01:00
Robin Raymond
465da44b29
Merge pull request #96 from dotlambda/fix-94
Don't include identity twice in vaccounts
2018-03-06 15:42:56 +01:00
Daniel Frank
330cc73089 Avoid backscattering on unknown recipients. Fixes #97 2018-03-05 20:29:02 +01:00
Robert Schütz
f9820b55ab Don't include identity twice in vaccounts
fixes #94
2018-03-05 16:20:54 +01:00
Robin Raymond
9a68daea0f Merge branch 'master' of github.com:r-raymond/nixos-mailserver 2018-03-05 09:19:34 +01:00
Robert Schütz
23d06c9665 Add test for #94
Ensure that no warnings or errors are in the journal
2018-03-04 23:36:32 +01:00
Philipp Dörfler
b53364715d Added basic support for borgbackup 2018-03-04 14:36:42 +00:00
Robin Raymond
95dad50dcb
Move development to wiki page 2018-02-28 14:07:50 +01:00
Robin Raymond
6c2bfe55e8
Link to contributors 2018-02-28 14:03:31 +01:00
Robin Raymond
c3582e13cb
Remove outdated change log. 2018-02-28 14:01:07 +01:00
Robin Raymond
35fff89f11
Move backup section to wiki 2018-02-28 13:59:44 +01:00
Robin Raymond
6c50206165
move setup guide to wiki 2018-02-28 13:57:08 +01:00
Robin Raymond
c1c4706519 remove deprecated virtualAliases 2018-02-23 16:48:13 +01:00
Robin Raymond
e5e3e61f97
Update ISSUE_TEMPLATE.md 2018-02-23 16:29:22 +01:00
Robin Raymond
97e60971d4
Create ISSUE_TEMPLATE.md 2018-02-23 16:28:23 +01:00
Robin Raymond
0cdd1bd4e1 update readme for version 2.1.3 2018-02-23 15:39:56 +01:00
Robin Raymond
d72b975a45 Merge branch 'v2.1-bugfixes' 2018-02-23 15:37:09 +01:00
Robin Raymond
6bdfdca0e3 fix typo 2018-02-23 15:36:29 +01:00
Robin Raymond
7452c70a14 update readme for v2.1.2 2018-02-23 15:06:03 +01:00
Robin Raymond
22caa012d6 Merge branch 'v2.1-bugfixes' 2018-02-23 14:57:22 +01:00
Robin Raymond
5d169c3ef2 fixes #88 2018-02-23 14:56:28 +01:00
Robin Raymond
a3043b2242 fixes #87 2018-02-23 14:52:11 +01:00
Robin Raymond
ea20d60ec1 possible fix for #86 2018-02-23 14:51:15 +01:00
Robin Raymond
c252ecb869 possible fix for #86 2018-02-22 23:12:39 +01:00
Robin Raymond
2d5e5ac445 fix typo from merge 2018-02-22 23:08:52 +01:00
Robin Raymond
4b0bd61c49 Merge branch 'phdoerfler-rsnapshot' 2018-02-22 22:50:43 +01:00
Robin Raymond
df25233fd4 merge 'basic rsnapshot backup' 2018-02-22 22:49:58 +01:00
Robin Raymond
ca9680403e
Merge pull request #56 from phdoerfler/monitoring
Added monitoring of disk space via monit
2018-02-22 22:45:02 +01:00
Robin Raymond
981e9bda9f
Merge pull request #66 from phdoerfler/separator
Dovecot: Mailbox config + hierarchy separator.
2018-02-22 22:36:57 +01:00
Philipp Dörfler
29cb68a216 Added monitoring of disk space and more with monit. 2018-02-22 20:33:55 +00:00
Philipp Dörfler
59b1fafefc Added basic rsnapshot backup. 2018-02-22 20:33:27 +00:00
Philipp Dörfler
43d36d9b76 Dovecot: Mailbox config + hierarchy separator + FS layout.
- Factored mailbox config into its own option.
- Added hierarchy separator option.
- Added option for using FS layout.
2018-02-22 20:32:21 +00:00
Robin Raymond
82823a4085 v2.1.1 readme update 2018-02-22 13:39:13 +01:00
Robin Raymond
372c1a7033
Merge pull request #83 from eqyiel/avoid-timeout
ci: don't test on unstable to avoid timeout
2018-02-21 10:15:23 +01:00
Ruben Maher
a5684ffb53 ci: don't test on unstable to avoid timeout 2018-02-21 11:03:17 +10:30
Robin Raymond
2ef04b2d9c
Merge pull request #73 from eqyiel/fix-sieve-variables
mail-server/users.nix: don't expand variables in sieve script
2018-02-20 11:59:13 +01:00
Ruben Maher
789af710ed ci: increase cache timeout to 10 minutes
The default is 3, and this can lead to cache timeouts while saving the contents
of /nix/store.
2018-02-20 09:14:55 +10:30
Ruben Maher
929cac8f50 mail-server/users.nix: don't expand variables in sieve script 2018-02-19 09:32:40 +10:30
Robin Raymond
fd3754f43d fix tests 2018-02-18 12:57:06 +01:00
Robin Raymond
436cf0513b add vitual mail users 2018-02-18 12:17:32 +01:00
Robin Raymond
3c625fc191 update README 2018-02-18 11:49:55 +01:00
Robin Raymond
3f6a7d0e0a
Merge pull request #79 from griff/test-cleanup
tests/extern: cleanup and simplification
2018-02-18 11:43:00 +01:00
Robin Raymond
c221d02568
Merge pull request #81 from eqyiel/pin-nixpkgs-2
ci: test against stable and unstable channels
2018-02-18 11:38:25 +01:00
Ruben Maher
ef11f689ef ci: test against stable and unstable channels 2018-02-18 10:29:04 +10:30
Robin Raymond
49951d6ac4
Merge pull request #78 from r-raymond/dovecot23
Dovecot23
2018-02-17 18:43:46 +01:00
Robin Raymond
e2eaa48b40
Merge pull request #76 from eqyiel/detect-dovecot-version
dovecot: read dovecot version into nix variable
2018-02-17 16:03:58 +01:00
Brian Olsen
5a93bed70c
tests/extern: cleanup and simplification
The extern test used shell scripting for file writing and ip lookup
which Nix and make-test.nix can do. I have also replaced the 5 second
sleep with the check for completion of queue processing on server.
2018-02-17 15:10:30 +01:00
Ruben Maher
5b570ad5a0 dovecot: read dovecot version into nix variable
This allows determining whether it's OK to use particular configuration
variables that will throw errors when used in older versions.
2018-02-17 22:24:39 +10:30
Robin Raymond
f6546a1a8e fix dovecot 2.3 ssl_dh 2018-02-13 13:18:31 +01:00
Robin Raymond
b75575f02e
remove unbound 2018-02-09 15:02:28 +01:00
Robin Raymond
671f447015
Merge pull request #57 from phdoerfler/localnameserver
Added kresd as local nameserver so rspamd stops complaining
2018-02-09 15:00:09 +01:00
Robin Raymond
0f6de6ff57
remove clamav from packages, fixes #64 2018-02-01 09:14:21 +01:00
Robin Raymond
8268ab5f4b
Clarify spf records description, fixes #62 2018-01-31 09:50:54 +01:00
Robin Raymond
aca43875dc update copywright 2018-01-29 10:34:27 +01:00
Robin Raymond
c6f36916af update readme for new release candidate 2018-01-29 10:28:55 +01:00
Robin Raymond
ba4eaed61d related to #52 2018-01-29 10:24:53 +01:00
Philipp Dörfler
bc627f180a Added kresd as local nameserver so rspamd stops complaining 2018-01-13 01:39:21 +00:00
Robin Raymond
58896e39ec
Merge pull request #54 from erosennin/master
Add ManageSieve support
2018-01-08 14:01:22 +01:00
Andrey Golovizin
239cc771ec Update README.md 2018-01-07 14:05:16 +01:00
Andrey Golovizin
ee479ae683 Run spam.sieve after user scripts
Allows the user to override or disable it, if necessary.
2018-01-07 14:05:16 +01:00
Andrey Golovizin
aeedb25daf Use sieve_default option for sieveScript
https://wiki2.dovecot.org/Pigeonhole/Sieve/Configuration#Visible_Default_Script
2018-01-07 14:05:16 +01:00
Andrey Golovizin
a6d9604ea5 Fix Sieve script activation via ManageSieve 2018-01-07 14:05:16 +01:00
Andrey Golovizin
30e4f136fd Add enableManageSieve option to open port 4190 2018-01-07 14:05:12 +01:00
Robin Raymond
cd9e790f21
Fix typo in readme 2017-12-31 12:15:08 +01:00
Robin Raymond
3b9b7961d4 remove tests of old userdb; fixes #51 2017-12-25 14:11:39 +01:00
Robin Raymond
e2e7593725 readme update release candidate v2.1 2017-12-22 18:51:57 +01:00
Robin Raymond
6dd51d6e88 add quota test 2017-12-22 16:58:49 +01:00
Robin Raymond
eeb7fd64af implement qutoas 2017-12-22 16:58:35 +01:00
Robin Raymond
2d0648e0f4 move from real users to passwd file 2017-12-22 16:08:42 +01:00
65 changed files with 5079 additions and 1043 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 }}

13
.gitlab-ci.yml Normal file
View file

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

View file

@ -0,0 +1,55 @@
{ nixpkgs, pulls, ... }:
let
pkgs = import nixpkgs {};
prs = builtins.fromJSON (builtins.readFile pulls);
prJobsets = pkgs.lib.mapAttrs (num: info:
{ enabled = 1;
hidden = false;
description = "PR ${num}: ${info.title}";
checkinterval = 30;
schedulingshares = 20;
enableemail = false;
emailoverride = "";
keepnr = 1;
type = 1;
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
}
) 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 // {
"master" = mkFlakeJobset "master";
"nixos-23.11" = mkFlakeJobset "nixos-23.11";
"nixos-24.05" = mkFlakeJobset "nixos-24.05";
};
log = {
pulls = prs;
jobsets = desc;
};
in {
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
cat >$out <<EOF
${builtins.toJSON desc}
EOF
# This is to get nice .jobsets build logs on Hydra
cat >tmp <<EOF
${builtins.toJSON log}
EOF
${pkgs.jq}/bin/jq . tmp
'';
}

1
.hydra/default.nix Normal file
View file

@ -0,0 +1 @@
import ../tests

30
.hydra/spec.json Normal file
View file

@ -0,0 +1,30 @@
{
"enabled": 1,
"hidden": false,
"description": "Simple NixOS Mailserver",
"nixexprinput": "nixexpr",
"nixexprpath": ".hydra/declarative-jobsets.nix",
"checkinterval": 60,
"schedulingshares": 100,
"enableemail": false,
"emailoverride": "",
"keepnr": 3,
"type": 0,
"inputs": {
"nixexpr": {
"value": "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master",
"type": "git",
"emailresponsible": false
},
"nixpkgs": {
"value": "https://github.com/NixOS/nixpkgs 0f920b05cbcdb8c0f3c5c4a8ea29f1f0065c7033 ",
"type": "git",
"emailresponsible": false
},
"pulls": {
"type": "gitlabpulls",
"value": "https://gitlab.com 7219050",
"emailresponsible": false
}
}
}

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

View file

@ -1,8 +0,0 @@
language: nix
script:
- nix-build tests/intern.nix
- nix-build tests/extern.nix
cache:
directories:
- /nix/store

348
README.md
View file

@ -1,22 +1,29 @@
# ![Simple Nixos MailServer][logo]
![license](https://img.shields.io/badge/license-GPL3-brightgreen.svg)
![status](https://travis-ci.org/r-raymond/nixos-mailserver.svg?branch=master)
[![pipeline status](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/badges/master/pipeline.svg)](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
## Stable Releases
## Release branches
* [SNM v2.0.4](https://github.com/r-raymond/nixos-mailserver/releases/v2.0.4)
For each NixOS release, we publish a branch. You then have to use the
SNM branch corresponding to your NixOS version.
[Latest Release (Candidate)](https://github.com/r-raymond/nixos-mailserver/releases/latest)
* For NixOS 24.05
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05)
* For NixOS 23.11
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11)
* For NixOS unstable
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
This is a very low volume list where new releases of SNM are announced, so you
can stay up to date with bug fixes and updates. All announcements are signed by
the gpg key with fingerprint
can stay up to date with bug fixes and updates.
```
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
```
## Features
### v2.0
@ -24,12 +31,15 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
* [x] Multiple Domains
* Postfix MTA
- [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
* Dovecot
- [x] maildir folders
- [x] imap starttls on port 143
- [x] pop3 starttls on port 110
- [x] imap with tls on port 993
- [x] pop3 with tls on port 995
- [x] imap with starttls on port 143
- [x] pop3 with starttls on port 110
* Certificates
- [x] manual certificates
- [x] on the fly creation
@ -46,6 +56,7 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
* Sieves
- [x] A simple standard script that moves spam
- [x] Allow user defined sieve scripts
- [x] ManageSieve support
* User Aliases
- [x] Regular aliases
- [x] Catch all aliases
@ -55,316 +66,23 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
* DKIM Signing
- [ ] Allow a per domain selector
### Changelog
#### v1.0 -> v1.1
* Changed structure to Nix Modules
* Adds Sieve support
#### v1.1 -> v2.0
* rename domain to fqdn, seperate fqdn from domains
* multi domain support
### Quick Start
```nix
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/archive/v2.0.4.tar.gz")
];
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`.
### Get in touch
- 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
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 `mail-tester.com`.
What you need:
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
* A server with a public IP (referred to as `server-IP`)
* A Fully Qualified Domain Name (`FQDN`) where your server is 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 ]`.
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
### A) Setup server
## Development
The following describes a server setup that is fairly complete. Even though
there are more possible options (see `default.nix`), these should be the most
common ones.
```nix
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/archive/v2.0.4.tar.gz")
];
mailserver = {
enable = true;
fqdn = <server-FQDN>;
domains = [ <domains> ];
# A list of all login accounts. To create the password hashes, use
# mkpasswd -m sha-512 "super secret password"
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
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" = { ... };
};
# 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
# down nginx and opens port 80.
certificateScheme = 3;
# Enable IMAP and POP3
enableImap = true;
enablePop3 = true;
enableImapSsl = true;
enablePop3Ssl = 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;
};
}
```
After a `nixos-rebuild switch --upgrade` your server should be good to go. If
you want to use `nixops` to deploy the server, look in the subfolder `nixops`
for some inspiration.
### B) Setup everything else
#### Step 1: Set 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 resolved DNS equries 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 DNS
entries for the IP's you own. Add an entry resolving `server-IP` to
`server-FQDN`
You can test if your setting is correct by
```
host <server-IP>
<server-IP>.in-addr.arpa domain name pointer <server-FQDN>.
```
Note that it can take a while until a DNS entry is propagated.
#### Step 3: Set `MX` Records
For every `domain` in `domains` do:
* Add a `MX` record to the domain `domain`
| Name (Subdomain) | TTL | Type | Priority | Value |
| ---------------- | ----- | ---- | -------- | ----------------- |
| `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.
#### Step 4: Set `SPF` Records
For every `domain` in `domains` do:
* Add a `SPF` record to the domain `domain`
| 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 it can take a while until a DNS entry is propagated. If you want to
use multiple servers for your email handling, don't forget to add all server
IP's 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.
### C) Test your Setup
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. Maybe you want
to sign up for the [SNM Announcement List](https://www.freelists.org/list/snm)?
Besides that, you can send an email to [mail-tester.com](https://www.mail-tester.com/) and see how you score,
and let [mxtoolbox.com](http://mxtoolbox.com/) take a look at your setup, but if you followed
the steps closely then everything should be awesome!
## How to Backup
This is really easy. First off you should have a backup of your
`configuration.nix` file where you have the server config (but that is already
in a git repository right?)
Next you need to backup `/var/vmail` or whatever you have specified for the
option `mailDirectory`. This is where all the mails reside. Good options are a
cron job with `rsync` or `scp`. But really anything works, as it is simply a
folder with plenty of files in it. If your backup solution does not preserve the
owner of the files don't forget to `chown` them to `virtualMail:virtualMail` if you copy
them back (or whatever you specified as `vmailUserName`, and `vmailGoupName`).
Finally you can (optionally) make a backup of `/var/dkim` (or whatever you
specified as `dkimKeyDirectory`). If you should lose those don't worry, new ones
will be created on the fly. But you will need to repeat step `B)5` and correct
all the `dkim` keys.
## How to Test for Development
You can test the setup via `nixops`. After installation, do
```
nixops create nixops/single-server.nix nixops/vbox.nix -d mail
nixops deploy -d mail
nixops info -d mail
```
You can then test the server via e.g. `telnet`. To log into it, use
```
nixops ssh -d mail mailserver
```
To test imap manually use
```
openssl s_client -host mail.example.com -port 143 -starttls imap
```
## A Complete Mail Server Without Moving Parts
### Used Technologies
* Nixos
* Nixpkgs
* Dovecot
* Postfix
* Rmilter
* Rspamd
* Clamav
* Opendkim
* Pam
### Features
* unlimited domain
* unlimited mail accounts
* unlimited aliases for every mail account
* spam and virus checking
* dkim signing of outgoing emails
* imap (optionally pop3)
* startTLS
### Nonfeatures
* moving parts
* SQL databases
* configurations that need to be made after `nixos-rebuild switch`
* complicated storage schemes
* webclients / http-servers
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
## Contributors
* Special thanks to @Infinisil for the module rewrite
* Special thanks to @jbboehr for multidomain implementation
* @danbst
* @phdoerfler
* @eqyiel
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
### Alternative Implementations
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
@ -376,6 +94,4 @@ openssl s_client -host mail.example.com -port 143 -starttls imap
* Logo made with [Logomakr.com](https://logomakr.com)
[logo]: logo/logo.png
[logo]: docs/logo.png

File diff suppressed because it is too large Load diff

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)

55
docs/add-radicale.rst Normal file
View file

@ -0,0 +1,55 @@
Add Radicale
============
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
{ config, pkgs, lib, ... }:
with lib;
let
mailAccounts = config.mailserver.loginAccounts;
htpasswd = pkgs.writeText "radicale.users" (concatStrings
(flip mapAttrsToList mailAccounts (mail: user:
mail + ":" + user.hashedPassword + "\n"
))
);
in {
services.radicale = {
enable = true;
settings = {
auth = {
type = "htpasswd";
htpasswd_filename = "${htpasswd}";
htpasswd_encryption = "bcrypt";
};
};
};
services.nginx = {
enable = true;
virtualHosts = {
"cal.example.com" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://localhost:5232/";
extraConfig = ''
proxy_set_header X-Script-Name /;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass_header Authorization;
'';
};
};
};
};
networking.firewall.allowedTCPPorts = [ 80 443 ];
}

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.

20
docs/backup-guide.rst Normal file
View file

@ -0,0 +1,20 @@
Backup Guide
============
First off you should have a backup of your ``configuration.nix`` file
where you have the server config (but that is already in a git
repository right?)
Next you need to backup ``/var/vmail`` or whatever you have specified
for the option ``mailDirectory``. This is where all the mails reside.
Good options are a cron job with ``rsync`` or ``scp``. But really
anything works, as it is simply a folder with plenty of files in it. If
your backup solution does not preserve the owner of the files dont
forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
back (or whatever you specified as ``vmailUserName``, and
``vmailGoupName``).
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
you specified as ``dkimKeyDirectory``). If you should lose those dont
worry, new ones will be created on the fly. But you will need to repeat
step ``B)5`` and correct all the ``dkim`` keys.

61
docs/conf.py Normal file
View file

@ -0,0 +1,61 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
# -- Project information -----------------------------------------------------
project = 'NixOS Mailserver'
copyright = '2022, NixOS Mailserver Contributors'
author = 'NixOS Mailserver Contributors'
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'myst_parser'
]
myst_enable_extensions = [
'colon_fence',
'linkify',
]
smartquotes = False
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
master_doc = 'index'
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'sphinx_rtd_theme'
# 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,
# so a file named "default.css" will overwrite the builtin "default.css".
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.

72
docs/howto-develop.rst Normal file
View file

@ -0,0 +1,72 @@
Contribute or troubleshoot
==========================
To report an issue, please go to
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
You can also chat with us on the Libera IRC channel ``#nixos-mailserver``.
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 flake check -L
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
------
You can test the setup via ``nixops``. After installation, do
::
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
$ nixops deploy -d mail
$ nixops info -d mail
You can then test the server via e.g. \ ``telnet``. To log into it, use
::
$ nixops ssh -d mail mailserver
Imap
----
To test imap manually use
::
$ openssl s_client -host mail.example.com -port 143 -starttls imap

40
docs/index.rst Normal file
View file

@ -0,0 +1,40 @@
.. NixOS Mailserver documentation master file, created by
sphinx-quickstart on Thu Jul 2 20:50:36 2020.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to NixOS Mailserver's documentation!
============================================
.. image:: logo.png
:width: 400
:alt: SNM Logo
.. toctree::
:maxdepth: 2
setup-guide
howto-develop
faq
release-notes
options
.. toctree::
:maxdepth: 1
:caption: How-to
backup-guide
add-radicale
add-roundcube
rspamd-tuning
fts
flakes
autodiscovery
ldap
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

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

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:/";
};
};
};

227
docs/setup-guide.rst Normal file
View file

@ -0,0 +1,227 @@
Setup Guide
===========
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
`<https://mail-tester.com>`_.
What you need is:
- a server running NixOS with a public IP
- a domain name.
.. 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
though there are more possible options (see the `NixOS Mailserver
options documentation <options.html>`_), these should be the most
common ones.
.. code:: nix
{ config, pkgs, ... }: {
imports = [
(builtins.fetchTarball {
# Pick a release version you are interested in and set its hash, e.g.
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
# 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";
})
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
# A list of all login accounts. To create the password hashes, use
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
loginAccounts = {
"user1@example.com" = {
hashedPasswordFile = "/a/file/containing/a/hashed/password";
aliases = ["postmaster@example.com"];
};
"user2@example.com" = { ... };
};
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
# down nginx and opens port 80.
certificateScheme = "acme-nginx";
};
security.acme.acceptTerms = true;
security.acme.defaults.email = "security@example.com";
}
After a ``nixos-rebuild switch`` your server should be running all
mail components.
Setup all other DNS requirements
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Set rDNS (reverse DNS) entry for server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Wherever you have rented your server, you should be able to set reverse
DNS entries for the IPs you own. Add an entry resolving ``1.2.3.4``
to ``mail.example.com``.
.. 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
::
$ nix-shell -p bind --command "host 1.2.3.4"
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.
Set a ``MX`` record
^^^^^^^^^^^^^^^^^^^
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
::
$ nix-shell -p bind --command "host -t mx example.com"
example.com mail is handled by 10 mail.example.com.
Note that it can take a while until a DNS entry is propagated.
Set a ``SPF`` record
^^^^^^^^^^^^^^^^^^^^
Add a `SPF <https://en.wikipedia.org/wiki/Sender_Policy_Framework>`_
record to the domain ``example.com``.
================ ===== ==== ================================
Name (Subdomain) TTL Type Value
================ ===== ==== ================================
example.com 10800 TXT `v=spf1 a:mail.example.com -all`
================ ===== ==== ================================
You can check this with
::
$ 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.
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
long), and sign up for some of the finest newsletters the Internet has.
Maybe you want to sign up for the `SNM Announcement
List <https://www.freelists.org/list/snm>`__?
Besides that, you can send an email to
`mail-tester.com <https://www.mail-tester.com/>`__ and see how you
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
your setup, but if you followed the steps closely then everything should
be awesome!

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

@ -0,0 +1,78 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver.borgbackup;
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
autoFragment =
if cfg.compression.auto && cfg.compression.method == null
then throw "compression.method must be set when using auto."
else lib.optional cfg.compression.auto "auto";
levelFragment =
if cfg.compression.level != null && cfg.compression.method == null
then throw "compression.method must be set when using compression.level."
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
encryptionFragment = cfg.encryption.method;
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
else throw "passphraseFile must be set when using encryption.");
locations = lib.escapeShellArgs cfg.locations;
name = lib.escapeShellArg cfg.name;
repoLocation = lib.escapeShellArg cfg.repoLocation;
extraInitArgs = lib.escapeShellArgs cfg.extraArgumentsForInit;
extraCreateArgs = lib.escapeShellArgs cfg.extraArgumentsForCreate;
cmdPreexec = lib.optionalString (cfg.cmdPreexec != null) cfg.cmdPreexec;
cmdPostexec = lib.optionalString (cfg.cmdPostexec != null) cfg.cmdPostexec;
borgScript = ''
export BORG_REPO=${repoLocation}
${cmdPreexec}
${passphraseFragment} ${pkgs.borgbackup}/bin/borg init ${extraInitArgs} --encryption ${encryptionFragment} || true
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
${cmdPostexec}
'';
in {
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
environment.systemPackages = with pkgs; [
borgbackup
];
systemd.services.borgbackup = {
description = "borgbackup";
unitConfig.Documentation = "man:borgbackup";
script = borgScript;
serviceConfig = {
User = cfg.user;
Group = cfg.group;
CPUSchedulingPolicy = "idle";
IOSchedulingClass = "idle";
ProtectSystem = "full";
};
startAt = cfg.startAt;
};
};
}

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -14,15 +14,17 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{ config, pkgs, lib, options, ... }:
let
cfg = config.mailserver;
in
{
config = lib.mkIf cfg.virusScanning {
services.clamav.daemon.enable = true;
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
services.clamav.daemon = {
enable = true;
settings.PhishingScanURLs = "no";
};
services.clamav.updater.enable = true;
};
}

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -14,27 +14,57 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config }:
{ config, pkgs, lib }:
let
cfg = config.mailserver;
in
{
# cert :: PATH
certificatePath = if cfg.certificateScheme == 1
certificatePath = if cfg.certificateScheme == "manual"
then cfg.certificateFile
else if cfg.certificateScheme == 2
else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
else if cfg.certificateScheme == 3
then "/var/lib/acme/${cfg.fqdn}/fullchain.pem"
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
else throw "unknown certificate scheme";
# key :: PATH
keyPath = if cfg.certificateScheme == 1
keyPath = if cfg.certificateScheme == "manual"
then cfg.keyFile
else if cfg.certificateScheme == 2
else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else if cfg.certificateScheme == 3
then "/var/lib/acme/${cfg.fqdn}/key.pem"
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
else throw "unknown certificate scheme";
passwordFiles = let
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
in
lib.mapAttrs (name: value:
if value.hashedPasswordFile == null then
builtins.toString (mkHashFile name value.hashedPassword)
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

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -16,41 +16,210 @@
{ config, pkgs, lib, ... }:
with (import ./common.nix { inherit config; });
with (import ./common.nix { inherit config pkgs lib; });
let
cfg = config.mailserver;
passwdDir = "/run/dovecot2";
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";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
# maildir in format "/${domain}/${user}"
dovecot_maildir = "maildir:${cfg.mailDirectory}/%d/%n";
dovecotMaildir =
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n"
);
postfixCfg = config.services.postfix;
dovecot2Cfg = config.services.dovecot2;
stateDir = "/var/lib/dovecot";
pipeBin = pkgs.stdenv.mkDerivation {
name = "pipe_bin";
src = ./dovecot/pipe_bin;
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
buildCommand = ''
mkdir -p $out/pipe/bin
cp $src/* $out/pipe/bin/
chmod a+x $out/pipe/bin/*
patchShebangs $out/pipe/bin
for file in $out/pipe/bin/*; do
wrapProgram $file \
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
done
'';
};
ldapConfig = pkgs.writeTextFile {
name = "dovecot-ldap.conf.ext.template";
text = ''
ldap_version = 3
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
${lib.optionalString cfg.ldap.startTls ''
tls = yes
''}
tls_require_cert = hard
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
dn = ${cfg.ldap.bind.dn}
sasl_bind = no
auth_bind = yes
base = ${cfg.ldap.searchBase}
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
user_attrs = ${cfg.ldap.dovecot.userAttrs}
''}
user_filter = ${cfg.ldap.dovecot.userFilter}
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
''}
pass_filter = ${cfg.ldap.dovecot.passFilter}
'';
};
setPwdInLdapConfFile = appendLdapBindPwd {
name = "ldap-conf-file";
file = ldapConfig;
prefix = ''dnpass = "'';
suffix = ''"'';
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapConfFile;
};
genPasswdScript = pkgs.writeScript "generate-password-file" ''
#!${pkgs.stdenv.shell}
set -euo pipefail
if (! test -d "${passwdDir}"); then
mkdir "${passwdDir}"
chmod 755 "${passwdDir}"
fi
# Prevent world-readable password files, even temporarily.
umask 077
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!"
exit 1
fi
done
cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${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
then "userdb_quota_rule=*:storage=${value.quota}"
else "")
) cfg.loginAccounts)}
EOF
'';
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
{
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 = {
enable = true;
enableImap = enableImap;
enablePop3 = enablePop3;
enableImap = enableImap || enableImapSsl;
enablePop3 = enablePop3 || enablePop3Ssl;
enablePAM = false;
enableQuota = true;
mailGroup = vmailGroupName;
mailUser = vmailUserName;
mailLocation = dovecot_maildir;
mailLocation = dovecotMaildir;
sslServerCert = certificatePath;
sslServerKey = keyPath;
enableLmtp = true;
modules = [ pkgs.dovecot_pigeonhole ];
protocols = [ "sieve" ];
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
protocols = lib.optional cfg.enableManageSieve "sieve";
sieveScripts = {
before = builtins.toFile "spam.sieve" ''
pluginSettings = {
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";
if header :is "X-Spam" "Yes" {
fileinto "Junk";
fileinto "${junkMailboxName}";
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;
extraConfig = ''
#Extra Config
${lib.optionalString debug ''
@ -59,62 +228,172 @@ in
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 {
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
mail_plugins = $mail_plugins imap_sieve
}
protocol pop3 {
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
}
mail_access_groups = ${vmailGroupName}
ssl = required
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
service lmtp {
unix_listener /var/lib/postfix/queue/private/dovecot-lmtp {
group = postfix
unix_listener dovecot-lmtp {
group = ${postfixCfg.group}
mode = 0600
user = postfix # TODO: < make variable
user = ${postfixCfg.user}
}
}
recipient_delimiter = ${cfg.recipientDelimiter}
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
protocol lmtp {
mail_plugins = $mail_plugins sieve
}
passdb {
driver = passwd-file
args = ${passwdFile}
}
userdb {
driver = passwd-file
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 {
unix_listener /var/lib/postfix/queue/private/auth {
unix_listener auth {
mode = 0660
user = postfix # TODO: < make variable
group = postfix # TODO: < make variable
user = ${postfixCfg.user}
group = ${postfixCfg.group}
}
}
auth_mechanisms = plain login
namespace inbox {
separator = ${cfg.hierarchySeparator}
inbox = yes
mailbox "Trash" {
auto = no
special_use = \Trash
}
mailbox "Junk" {
auto = subscribe
special_use = \Junk
}
mailbox "Drafts" {
auto = subscribe
special_use = \Drafts
}
mailbox "Sent" {
auto = subscribe
special_use = \Sent
}
}
${lib.optionalString cfg.fullTextSearch.enable ''
plugin {
sieve = file:/var/sieve/%u.sieve
plugin = fts fts_xapian
fts = xapian
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
fts_enforced = ${cfg.fullTextSearch.enforced}
}
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
service indexer-worker {
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
}
''}
''}
lda_mailbox_autosubscribe = yes
lda_mailbox_autocreate = yes
'';
};
systemd.services.dovecot2 = {
preStart = ''
${genPasswdScript}
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
};
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

@ -0,0 +1,15 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.mailbox" "*" {
set "mailbox" "${1}";
}
if string "${mailbox}" "Trash" {
stop;
}
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-ham.sh" [ "${username}" ];

View file

@ -0,0 +1,7 @@
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
if environment :matches "imap.user" "*" {
set "username" "${1}";
}
pipe :copy "sa-learn-spam.sh" [ "${username}" ];

View file

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

View file

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

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -22,7 +22,7 @@ in
{
config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix clamav rspamd rmilter
] ++ (if certificateScheme == 2 then [ openssl ] else []);
dovecot opendkim openssh postfix rspamd
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
};
}

View file

@ -14,31 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver;
# cert :: PATH
cert = if cfg.certificateScheme == 1
then cfg.certificateFile
else if cfg.certificateScheme == 2
then "${cfg.certificateDirectory}/cert-${cfg.fqdn.pem"
else "";
# key :: PATH
key = if cfg.certificateScheme == 1
then cfg.keyFile
else if cfg.certificateScheme == 2
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else "";
in
{
imports = [
./rmilter.nix
./postfix.nix key
./dovecot.nix
];
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
services.kresd.enable = true;
};
}

32
mail-server/monit.nix Normal file
View file

@ -0,0 +1,32 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver;
in
{
config = lib.mkIf (cfg.enable && cfg.monitoring.enable) {
services.monit = {
enable = true;
config = ''
set alert ${cfg.monitoring.alertAddress}
${cfg.monitoring.config}
'';
};
};
}

View file

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

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -17,28 +17,26 @@
{ config, pkgs, lib, ... }:
with (import ./common.nix { inherit config; });
with (import ./common.nix { inherit config lib pkgs; });
let
cfg = config.mailserver;
acmeRoot = "/var/lib/acme/acme-challenge";
in
{
config = lib.mkIf (cfg.certificateScheme == 3) {
services.nginx = {
config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
enable = true;
virtualHosts."${cfg.fqdn}" = {
serverName = cfg.fqdn;
serverAliases = cfg.certificateDomains;
forceSSL = true;
enableACME = true;
acmeRoot = acmeRoot;
};
};
security.acme.certs."${cfg.fqdn}".postRun = ''
systemctl reload nginx
systemctl reload postfix
systemctl reload dovecot2
'';
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
"postfix.service"
"dovecot2.service"
];
};
}

89
mail-server/opendkim.nix Normal file
View file

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

View file

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

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -16,53 +16,82 @@
{ config, pkgs, lib, ... }:
with (import ./common.nix { inherit config; });
with (import ./common.nix { inherit config pkgs lib; });
let
inherit (lib.strings) concatStringsSep;
cfg = config.mailserver;
# valiases_postfix :: [ String ]
valiases_postfix = lib.flatten (lib.mapAttrsToList
# Merge several lookup tables. A lookup table is a attribute set where
# - the key is an address (user@example.com) or a domain (@example.com)
# - the value is a list of addresses
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
# valiases_postfix :: Map String [String]
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value:
let to = name;
in map (from: "${from} ${to}") (value.aliases ++ lib.singleton name))
cfg.loginAccounts);
# catchAllPostfix :: [ String ]
catchAllPostfix = lib.flatten (lib.mapAttrsToList
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
cfg.loginAccounts));
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value:
let to = name;
in map (from: "@${from} ${to}") value.catchAll)
cfg.loginAccounts);
in map (from: {"${from}" = to;}) value.aliasesRegexp)
cfg.loginAccounts));
# extra_valiases_postfix :: [ String ]
# TODO: Remove virtualAliases when deprecated -> removed
extra_valiases_postfix = (map
(from:
let to = cfg.virtualAliases.${from};
in "${from} ${to}")
(builtins.attrNames cfg.virtualAliases))
++
(map
(from:
let to = cfg.extraVirtualAliases.${from};
in "${from} ${to}")
(builtins.attrNames cfg.extraVirtualAliases));
# catchAllPostfix :: Map String [String]
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value:
let to = name;
in map (from: {"@${from}" = to;}) value.catchAll)
cfg.loginAccounts));
# all_valiases_postfix :: [ String ]
all_valiases_postfix = valiases_postfix ++ extra_valiases_postfix;
# all_valiases_postfix :: Map String [String]
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
# accountToIdentity :: User -> String
accountToIdentity = account: "${account.name} ${account.name}";
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
attrsToLookupTable = aliases: let
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
in mergeLookupTables lookupTables;
# vaccounts_identity :: [ String ]
vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts);
# 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 = builtins.toFile "valias"
(lib.concatStringsSep "\n" (all_valiases_postfix ++
catchAllPostfix));
valiases_file = let
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
in builtins.toFile "valias" content;
regex_valiases_file = let
content = lookupTableToString regex_valiases_postfix;
in builtins.toFile "regex_valias" content;
# denied_recipients_postfix :: [ String ]
denied_recipients_postfix = (map
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
reject_senders_postfix = (map
(sender:
"${sender} REJECT")
(cfg.rejectSender));
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
reject_recipients_postfix = (map
(recipient:
"${recipient} REJECT")
(cfg.rejectRecipients));
# rejectRecipients :: [ Path ]
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
# vhosts_file :: Path
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
@ -71,12 +100,12 @@ let
# see
# https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/
# for details on how this file looks. By using the same file as valiases,
# every alias is owned (uniquely) by its user. We have to add the users own
# address though
vaccounts_file = builtins.toFile "vaccounts" (lib.concatStringsSep "\n"
(vaccounts_identity ++ all_valiases_postfix));
# every alias is owned (uniquely) by its user.
# The user's own address is already in all_valiases_postfix.
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ''
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
# Removes sensitive headers from mails handed in via the submission port.
# See https://thomas-leister.de/mailserver-debian-stretch/
# Uses "pcre" style regex.
@ -86,84 +115,227 @@ let
/^X-Mailer:/ IGNORE
/^User-Agent:/ IGNORE
/^X-Enigmail:/ IGNORE
'';
in
{
config = with cfg; lib.mkIf enable {
'' + lib.optionalString cfg.rewriteMessageId ''
services.postfix = {
enable = true;
hostname = "${fqdn}";
networksStyle = "host";
mapFiles."valias" = valiases_file;
mapFiles."vaccounts" = vaccounts_file;
sslCert = certificatePath;
sslKey = keyPath;
enableSubmission = true;
# Replaces the user submitted hostname with the server's FQDN to hide the
# user's host or network.
extraConfig =
''
# Extra Config
mydestination = localhost
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
'');
smtpd_banner = ${fqdn} ESMTP NO UCE
disable_vrfy_command = yes
message_size_limit = 20971520
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
unixSocket = sock: "unix:${sock}";
# virtual mail system
virtual_uid_maps = static:5000
virtual_gid_maps = static:5000
virtual_mailbox_base = ${mailDirectory}
virtual_mailbox_domains = ${vhosts_file}
virtual_alias_maps = hash:/var/lib/postfix/conf/valias
virtual_transport = lmtp:unix:private/dovecot-lmtp
smtpdMilters =
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
# sasl with dovecot
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
# TLS settings, inspired by https://github.com/jeaye/nix-files
# Submission by mail clients is handled in submissionOptions
smtpd_tls_security_level = may
# strong might suffice and is computationally less expensive
smtpd_tls_eecdh_grade = ultra
# Disable predecessors to TLS
smtpd_tls_protocols = !SSLv2, !SSLv3
# Allowing AUTH on a non encrypted connection poses a security risk
smtpd_tls_auth_only = yes
# Log only a summary message on TLS handshake completion
smtpd_tls_loglevel = 1
# Disable weak ciphers as reported by https://ssl-tools.net
# https://serverfault.com/questions/744168/how-to-disable-rc4-on-postfix
smtpd_tls_exclude_ciphers = RC4, aNULL
smtp_tls_exclude_ciphers = RC4, aNULL
# Configure a non blocking source of randomness
tls_random_source = dev:/dev/urandom
'';
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 = "private/auth";
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_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";
};
extraMasterConf = ''
submission-header-cleanup unix n - n - 0 cleanup
-o header_checks=pcre:${submissionHeaderCleanupRules}
commonLdapConfig = ''
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
version = 3
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
tls_require_cert = yes
search_base = ${cfg.ldap.searchBase}
scope = ${cfg.ldap.searchScope}
bind = yes
bind_dn = ${cfg.ldap.bind.dn}
'';
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.mailAttribute}
'';
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
appendPwdInSenderLoginMap = appendLdapBindPwd {
name = "ldap-sender-login-map";
file = ldapSenderLoginMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapSenderLoginMapFile;
};
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.uidAttribute}
'';
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
name = "ldap-virtual-mailbox-map";
file = ldapVirtualMailboxMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapVirtualMailboxMapFile;
};
in
{
config = with cfg; lib.mkIf enable {
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = ''
${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap}
'';
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
};
services.postfix = {
enable = true;
hostname = "${sendingFqdn}";
networksStyle = "host";
mapFiles."valias" = valiases_file;
mapFiles."regex_valias" = regex_valiases_file;
mapFiles."vaccounts" = vaccounts_file;
mapFiles."regex_vaccounts" = regex_vaccounts_file;
mapFiles."denied_recipients" = denied_recipients_file;
mapFiles."reject_senders" = reject_senders_file;
mapFiles."reject_recipients" = reject_recipients_file;
sslCert = certificatePath;
sslKey = keyPath;
enableSubmission = cfg.enableSubmission;
enableSubmissions = cfg.enableSubmissionSsl;
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
config = {
# Extra Config
mydestination = "";
recipient_delimiter = cfg.recipientDelimiter;
smtpd_banner = "${fqdn} ESMTP NO UCE";
disable_vrfy_command = true;
message_size_limit = toString cfg.messageSizeLimit;
# virtual mail system
virtual_uid_maps = "static:5000";
virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory;
virtual_mailbox_domains = vhosts_file;
virtual_mailbox_maps = [
(mappedFile "valias")
] ++ lib.optionals (cfg.ldap.enable) [
"ldap:${ldapVirtualMailboxMapFile}"
] ++ lib.optionals (regex_valiases_postfix != {}) [
(mappedRegexFile "regex_valias")
];
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
(mappedRegexFile "regex_valias")
]);
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
# sasl with dovecot
smtpd_sasl_type = "dovecot";
smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_auth_enable = true;
smtpd_relay_restrictions = [
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
];
policy-spf_time_limit = "3600s";
# reject selected senders
smtpd_sender_restrictions = [
"check_sender_access ${mappedFile "reject_senders"}"
];
# quota and spf checking
smtpd_recipient_restrictions = [
"check_recipient_access ${mappedFile "denied_recipients"}"
"check_recipient_access ${mappedFile "reject_recipients"}"
"check_policy_service inet:localhost:12340"
"check_policy_service unix:private/policy-spf"
];
# TLS settings, inspired by https://github.com/jeaye/nix-files
# Submission by mail clients is handled in submissionOptions
smtpd_tls_security_level = "may";
# Disable obselete protocols
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtp_tls_ciphers = "high";
smtpd_tls_ciphers = "high";
smtp_tls_mandatory_ciphers = "high";
smtpd_tls_mandatory_ciphers = "high";
# Disable deprecated ciphers
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
tls_preempt_cipherlist = true;
# Allowing AUTH on a non encrypted connection poses a security risk
smtpd_tls_auth_only = true;
# Log only a summary message on TLS handshake completion
smtpd_tls_loglevel = "1";
# Configure a non blocking source of randomness
tls_random_source = "dev:/dev/urandom";
smtpd_milters = smtpdMilters;
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"];
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}";
# Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
};
submissionOptions = submissionOptions;
submissionsOptions = submissionOptions;
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" = {
type = "unix";
privileged = true;
chroot = false;
command = "spawn";
args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
};
"submission-header-cleanup" = {
type = "unix";
private = false;
chroot = false;
maxproc = 0;
command = "cleanup";
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
};
};
};
};
}

View file

@ -1,73 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver;
clamav = if cfg.virusScanning
then
''
clamav {
servers = /var/run/clamav/clamd.ctl;
};
''
else "";
dkim = if cfg.dkimSigning
# Note: domain = "*"; causes Rmilter to try to search key in the key path
# as keypath/domain.selector.key for any domain.
then
''
dkim {
domain {
key = "${cfg.dkimKeyDirectory}";
domain = "*";
selector = "${cfg.dkimSelector}";
};
sign_alg = sha256;
auth_only = yes;
}
''
else "";
in
{
config = with cfg; lib.mkIf enable {
services.rspamd = {
enable = true;
};
services.rmilter = {
inherit debug;
enable = true;
postfix.enable = true;
rspamd = {
enable = true;
extraConfig = "extended_spam_headers = yes;";
};
extraConfig =
''
use_redis = true;
max_size = 20M;
${clamav}
${dkim}
'';
};
};
}

59
mail-server/rsnapshot.nix Normal file
View file

@ -0,0 +1,59 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.mailserver;
preexecDefined = cfg.backup.cmdPreexec != null;
preexecWrapped = pkgs.writeScript "rsnapshot-preexec.sh" ''
#!${pkgs.stdenv.shell}
set -e
${cfg.backup.cmdPreexec}
'';
preexecString = optionalString preexecDefined "cmd_preexec ${preexecWrapped}";
postexecDefined = cfg.backup.cmdPostexec != null;
postexecWrapped = pkgs.writeScript "rsnapshot-postexec.sh" ''
#!${pkgs.stdenv.shell}
set -e
${cfg.backup.cmdPostexec}
'';
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
in {
config = mkIf (cfg.enable && cfg.backup.enable) {
services.rsnapshot = {
enable = true;
cronIntervals = cfg.backup.cronIntervals;
# rsnapshot expects intervals shortest first, e.g. hourly first, then daily.
# tabs must separate all elements
extraConfig = ''
${preexecString}
${postexecString}
snapshot_root ${cfg.backup.snapshotRoot}/
retain hourly ${toString cfg.backup.retain.hourly}
retain daily ${toString cfg.backup.retain.daily}
retain weekly ${toString cfg.backup.retain.weekly}
backup ${cfg.mailDirectory}/ localhost/
'';
};
};
}

180
mail-server/rspamd.nix Normal file
View file

@ -0,0 +1,180 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2018 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
let
cfg = config.mailserver;
postfixCfg = config.services.postfix;
rspamdCfg = config.services.rspamd;
rspamdSocket = "rspamd.service";
in
{
config = with cfg; lib.mkIf enable {
services.rspamd = {
enable = true;
inherit debug;
locals = {
"milter_headers.conf" = { text = ''
extended_spam_headers = true;
''; };
"redis.conf" = { text = ''
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
'' + (lib.optionalString (cfg.redis.password != null) ''
password = "${cfg.redis.password}";
''); };
"classifier-bayes.conf" = { text = ''
cache {
backend = "redis";
}
''; };
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
clamav {
action = "reject";
symbol = "CLAM_VIRUS";
type = "clamav";
log_clean = true;
servers = "/run/clamav/clamd.ctl";
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
}
''; };
"dkim_signing.conf" = { text = ''
# Disable outbound email signing, we use opendkim for this
enabled = false;
''; };
"dmarc.conf" = { text = ''
${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 = {
type = "rspamd_proxy";
bindSockets = [{
socket = "/run/rspamd/rspamd-milter.sock";
mode = "0664";
}];
count = 1; # Do not spawn too many processes of this type
extraConfig = ''
milter = yes; # Enable milter mode
timeout = 120s; # Needed for Milter usually
upstream "local" {
default = yes; # Self-scan upstreams are always default
self_scan = yes; # Enable self-scan
}
'';
};
workers.controller = {
type = "controller";
count = 1;
bindSockets = [{
socket = "/run/rspamd/worker-controller.sock";
mode = "0666";
}];
includes = [];
extraConfig = ''
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
'';
};
};
services.redis.servers.rspamd = {
enable = lib.mkDefault true;
port = lib.mkDefault 6380;
};
systemd.services.rspamd = {
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
};
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 = {
after = [ rspamdSocket ];
requires = [ rspamdSocket ];
};
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
};
}

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -18,81 +18,68 @@
let
cfg = config.mailserver;
create_certificate = if cfg.certificateScheme == 2 then
''
# Create certificates if they do not exist yet
dir="${cfg.certificateDirectory}"
fqdn="${cfg.fqdn}"
case $fqdn in /*) fqdn=$(cat "$fqdn");; esac
key="''${dir}/key-${cfg.fqdn}.pem";
cert="''${dir}/cert-${cfg.fqdn}.pem";
if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ]
then
mkdir -p "${cfg.certificateDirectory}"
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) &&
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \
-days 3650 -out "''${cert}"
fi
''
else "";
createDomainDkimCert = dom:
let
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
in
''
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \
--directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
fi
'';
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
create_dkim_cert =
''
# Create dkim dir
mkdir -p "${cfg.dkimKeyDirectory}"
chown rmilter:rmilter "${cfg.dkimKeyDirectory}"
${createAllCerts}
chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}"
'';
certificatesDeps =
if cfg.certificateScheme == "manual" then
[]
else if cfg.certificateScheme == "selfsigned" then
[ "mailserver-selfsigned-certificate.service" ]
else
[ "acme-finished-${cfg.fqdn}.target" ];
in
{
config = with cfg; lib.mkIf enable {
# Make sure postfix gets started first, so that the certificates are in place
systemd.services.dovecot2.after = [ "postfix.service" ];
# Create self signed certificate
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") {
after = [ "local-fs.target" ];
script = ''
# Create certificates if they do not exist yet
dir="${cfg.certificateDirectory}"
fqdn="${cfg.fqdn}"
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
key="$dir/key-${cfg.fqdn}.pem";
cert="$dir/cert-${cfg.fqdn}.pem";
# Create certificates and maildir folder
systemd.services.postfix = {
after = (if (certificateScheme == 3) then [ "nginx.service" ] else []);
preStart =
''
if [[ ! -f $key || ! -f $cert ]]; then
mkdir -p "${cfg.certificateDirectory}"
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
"${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
-days 3650 -out "$cert"
fi
'';
serviceConfig = {
Type = "oneshot";
PrivateTmp = true;
};
};
# Create maildir folder before dovecot startup
systemd.services.dovecot2 = {
wants = certificatesDeps;
after = certificatesDeps;
preStart = let
directories = lib.strings.escapeShellArgs (
[ mailDirectory ]
++ lib.optional (cfg.indexDir != null) cfg.indexDir
);
in ''
# Create mail directory and set permissions. See
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
mkdir -p "${mailDirectory}"
chgrp "${vmailGroupName}" "${mailDirectory}"
chmod 02770 "${mailDirectory}"
${create_certificate}
# Prevent world-readable paths, even temporarily.
umask 007
mkdir -p ${directories}
chgrp "${vmailGroupName}" ${directories}
chmod 02770 ${directories}
'';
};
# Create dkim certificates
systemd.services.rmilter = {
requires = [ "rmilter.socket" ];
after = [ "rmilter.socket" ];
preStart =
''
${create_dkim_cert}
'';
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = {
wants = certificatesDeps;
after = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service"
++ certificatesDeps;
requires = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "opendkim.service";
};
};
}

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -21,63 +21,74 @@ with config.mailserver;
let
vmail_user = {
name = vmailUserName;
isNormalUser = false;
isSystemUser = true;
uid = vmailUID;
home = mailDirectory;
createHome = true;
group = vmailGroupName;
};
# accountsToUser :: String -> UserRecord
accountsToUser = account: {
isNormalUser = false;
group = vmailGroupName;
inherit (account) hashedPassword name;
};
# mail_users :: { [String]: UserRecord }
mail_users = lib.foldl (prev: next: prev // { "${next.name}" = next; }) {}
(map accountsToUser (lib.attrValues loginAccounts));
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
#!${pkgs.stdenv.shell}
set -euo pipefail
# Prevent world-readable paths, even temporarily.
umask 007
# Create directory to store user sieve scripts if it doesn't exist
if (! test -d "/var/sieve"); then
mkdir "/var/sieve"
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve"
chmod 770 "/var/sieve"
if (! test -d "${sieveDirectory}"); then
mkdir "${sieveDirectory}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
chmod 770 "${sieveDirectory}"
fi
# Copy user's sieve script to the correct location (if it exists). If it
# is null, remove the file.
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
if lib.isString sieveScript then ''
cat << EOF > "/var/sieve/${name}.sieve"
if (! test -d "${sieveDirectory}/${name}"); then
mkdir -p "${sieveDirectory}/${name}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
chmod 770 "${sieveDirectory}/${name}"
fi
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
${sieveScript}
EOF
chown "${name}:${vmailGroupName}" "/var/sieve/${name}.sieve"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
'' else ''
if (test -f "/var/sieve/${name}.sieve"); then
rm "/var/sieve/${name}.sieve"
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
rm "${sieveDirectory}/${name}/default.sieve"
fi
if (test -f "/var/sieve/${name}.svbin"); then
rm "/var/sieve/${name}.svbin"
if (test -f "${sieveDirectory}/${name}.svbin"); then
rm "${sieveDirectory}/${name}/default.svbin"
fi
'') (map (user: { inherit (user) name sieveScript; })
(lib.attrValues loginAccounts))}
'';
in {
config = lib.mkIf enable {
# assert that all accounts provide a password
assertions = (map (acct: {
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
message = "${acct.name} must provide either a hashed password or a password hash file";
}) (lib.attrValues loginAccounts));
# warn for accounts that specify both password and file
warnings = (map
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
(lib.filter
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
(lib.attrValues loginAccounts)));
# set the vmail gid to a specific value
users.groups = {
"${vmailGroupName}" = { gid = vmailUID; };
};
# define all users
users.users = mail_users // {
users.users = {
"${vmail_user.name}" = lib.mkForce vmail_user;
};

View file

@ -5,7 +5,7 @@
{ config, pkgs, ... }:
{
imports = [
./../default.nix
../default.nix
];
mailserver = {
@ -17,7 +17,7 @@
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
};
};
virtualAliases = {
extraVirtualAliases = {
"info@example.com" = "user1@example.com";
"postmaster@example.com" = "user1@example.com";
"abuse@example.com" = "user1@example.com";

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)

10
shell.nix Normal file
View file

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

237
tests/clamav.nix Normal file
View file

@ -0,0 +1,237 @@
# 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> {}, blobs}:
pkgs.nixosTest {
name = "clamav";
nodes = {
server = { config, pkgs, lib, ... }:
{
imports = [
../default.nix
./lib/config.nix
];
virtualisation.memorySize = 1500;
services.rsyslogd = {
enable = true;
defaultConfig = ''
*.* /dev/console
'';
};
services.clamav.updater.enable = lib.mkForce false;
systemd.services.old-clam = {
before = [ "clamav-daemon.service" ];
requiredBy = [ "clamav-daemon.service" ];
description = "ClamAV virus database";
preStart = ''
mkdir -m 0755 -p /var/lib/clamav
chown clamav:clamav /var/lib/clamav
'';
script = ''
cp ${blobs}/clamav/main.cvd /var/lib/clamav/
cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
chown clamav:clamav /var/lib/clamav/*
'';
serviceConfig = {
Type = "oneshot";
PrivateTmp = "yes";
PrivateDevices = "yes";
};
};
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
virusScanning = true;
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ];
catchAll = [ "example.com" ];
};
"user@example2.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
};
};
enableImap = true;
};
environment.etc = {
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
};
};
client = { nodes, config, pkgs, ... }: let
serverIP = nodes.server.config.networking.primaryIPAddress;
clientIP = nodes.client.config.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
#!${pkgs.stdenv.shell}
echo grep '${clientIP}' "$@" >&2
exec grep '${clientIP}' "$@"
'';
in {
imports = [
./lib/config.nix
];
environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip
];
environment.etc = {
"root/.fetchmailrc" = {
text = ''
poll ${serverIP} with proto IMAP
user 'user1@example.com' there with password 'user1' is 'root' here
mda procmail
'';
mode = "0700";
};
"root/.procmailrc" = {
text = "DEFAULT=$HOME/mail";
};
"root/.msmtprc" = {
text = ''
defaults
tls on
tls_certcheck off
account user2
host ${serverIP}
port 587
from user@example2.com
auth on
user user@example2.com
password user2
'';
};
"root/virus-email".text = ''
From: User2 <user@example2.com>
Content-Type: multipart/mixed;
boundary="Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607"
Mime-Version: 1.0 (Mac OS X Mail 11.3 \(3445.6.18\))
Subject: Testy McTest
Message-Id: <94550DD9-1FF1-4ED1-9F09-8812FF2E59AA@example.com>
Date: Sat, 12 May 2018 14:15:44 +0200
To: User1 <user1@example.com>
X-Mailer: Apple Mail (2.3445.6.18)
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
Content-Transfer-Encoding: 7bit
Content-Type: text/plain;
charset=us-ascii
Hello
I have attached a dangerous virus.
Mfg.
User2
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
Content-Disposition: attachment;
filename=eicar.com.txt
Content-Type: text/plain;
x-unix-mode=0644;
name="eicar.com.txt"
Content-Transfer-Encoding: 7bit
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607--
'';
"root/safe-email".text = ''
From: User <user@example2.com>
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from user@example2.com to user1
Reply-To:
Hello User1,
how are you doing today?
XOXO User1
'';
};
};
};
testScript = { nodes, ... }:
''
start_all()
server.wait_for_unit("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.
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 ]"
)
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.succeed("mkdir -p ~/mail")
client.succeed("ls -la ~/ >&2")
client.succeed("cat ~/.fetchmailrc >&2")
client.succeed("cat ~/.procmailrc >&2")
client.succeed("cat ~/.msmtprc >&2")
# fetchmail returns EXIT_CODE 1 when no new mail
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
# Verify that mail can be sent and received before testing virus scanner
client.execute("rm ~/mail/*")
client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
client.execute("rm ~/mail/*")
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v >&2")
client.execute("rm ~/mail/*")
with subtest("virus scan file"):
server.succeed(
'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
)
with subtest("virus scan email"):
client.succeed(
'set +o pipefail; msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
)
server.succeed("journalctl -u rspamd | grep -i eicar")
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
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,238 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
import <nixpkgs/nixos/tests/make-test.nix> {
nodes =
{ server = { config, pkgs, ... }:
{
imports = [
./../default.nix
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ];
catchAll = [ "example.com" ];
};
"user2@example.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
aliases = [ "chuck@example.com" ];
};
"user@example2.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
};
};
enableImap = true;
};
};
client = { config, pkgs, ... }:
{
environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils
];
};
};
testScript =
let
fetchmailRc =
''
poll SERVER with proto IMAP
user 'user1\@example.com' there with password 'user1' is 'root' here
mda procmail
'';
procmailRc =
''
DEFAULT=\$HOME/mail
'';
msmtpRc =
''
account test
host SERVER
port 587
from user2\@example.com
user user2\@example.com
password user2
account test2
host SERVER
port 587
from user\@example2.com
user user\@example2.com
password user2
account test3
host SERVER
port 587
from chuck\@example.com
user user2\@example.com
password user2
account test4
host SERVER
port 587
from postmaster\@example.com
user user1\@example.com
password user1
'';
email1 =
''
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,
how are you doing today?
'';
email2 =
''
From: User <user\@example2.com>
To: User1 <user1\@example.com>
Cc:
Bcc:
Subject: This is a test Email from user\@example2.com to user1
Reply-To:
Hello User1,
how are you doing today?
XOXO User1
'';
email3 =
''
From: Postmaster <postmaster@example.com>
To: Chuck <chuck@example.com>
Cc:
Bcc:
Subject: This is a test Email from postmaster\@example.com to chuck
Reply-To:
Hello Chuck,
I think I may have misconfigured the mail server
XOXO Postmaster
'';
in
''
startAll;
$server->waitForUnit("multi-user.target");
$client->waitForUnit("multi-user.target");
subtest "imap retrieving mail", sub {
$client->succeed("mkdir ~/mail");
$client->succeed("echo '${fetchmailRc}' > ~/.fetchmailrc");
$client->succeed("echo '${procmailRc}' > ~/.procmailrc");
$client->succeed("sed -i s/SERVER/`getent hosts server | awk '{ print \$1 }'`/g ~/.fetchmailrc");
$client->succeed("chmod 0700 ~/.fetchmailrc");
$client->succeed("cat ~/.fetchmailrc >&2");
# fetchmail returns EXIT_CODE 1 when no new mail
$client->succeed("fetchmail -v || [ \$? -eq 1 ] >&2");
};
subtest "submission port send mail", sub {
$client->succeed("echo '${msmtpRc}' > ~/.msmtprc");
$client->succeed("sed -i s/SERVER/`getent hosts server | awk '{ print \$1 }'`/g ~/.msmtprc");
$client->succeed("cat ~/.msmtprc >&2");
$client->succeed("echo '${email1}' > mail.txt");
# send email from user2 to user1
$client->succeed("msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < mail.txt >&2");
};
subtest "imap retrieving mail 2", sub {
# give the mail server some time to process the mail
$client->succeed("sleep 5");
# fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail -v >&2");
};
subtest "remove sensitive information on submission port", sub {
$client->succeed("cat ~/mail/* >&2");
## make sure our IP is _not_ in the email header
$client->fail("grep `ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print \$2}' | cut -f1 -d'/'` ~/mail/*");
};
subtest "have correct fqdn as sender", sub {
$client->succeed("grep 'Received: from mail.example.com' ~/mail/*");
};
subtest "dkim singing, multiple domains", sub {
$client->succeed("rm ~/mail/*");
$client->succeed("rm mail.txt");
$client->succeed("echo '${email2}' > mail.txt");
# send email from user2 to user1
$client->succeed("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < mail.txt >&2");
$client->succeed("sleep 5");
# fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail -v");
$client->succeed("cat ~/mail/* >&2");
# make sure it is dkim signed
$client->succeed("grep DKIM ~/mail/*");
};
subtest "aliases", sub {
$client->succeed("rm ~/mail/*");
$client->succeed("rm mail.txt");
$client->succeed("echo '${email2}' > mail.txt");
# send email from chuck to postmaster
$client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < mail.txt >&2");
$client->succeed("sleep 5");
# fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail -v");
};
subtest "catchAlls", sub {
$client->succeed("rm ~/mail/*");
$client->succeed("rm mail.txt");
$client->succeed("echo '${email2}' > mail.txt");
# send email from chuck to non exsitent account
$client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < mail.txt >&2");
$client->succeed("sleep 5");
# fetchmail returns EXIT_CODE 0 when it retrieves mail
$client->succeed("fetchmail -v");
$client->succeed("rm ~/mail/*");
$client->succeed("rm mail.txt");
$client->succeed("echo '${email2}' > mail.txt");
# send email from user1 to chuck
$client->succeed("msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < mail.txt >&2");
$client->succeed("sleep 5");
# fetchmail returns EXIT_CODE 1 when no new mail
# if this succeeds, it means that user1 recieved the mail that was intended for chuck.
$client->fail("fetchmail -v");
};
'';
}

514
tests/external.nix Normal file
View file

@ -0,0 +1,514 @@
# 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> {}, ...}:
pkgs.nixosTest {
name = "external";
nodes = {
server = { config, pkgs, ... }:
{
imports = [
../default.nix
./lib/config.nix
];
virtualisation.memorySize = 1024;
services.rsyslogd = {
enable = true;
defaultConfig = ''
*.* /dev/console
'';
};
mailserver = {
enable = true;
debug = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
rewriteMessageId = true;
dkimKeyBits = 1535;
dmarcReporting = {
enable = true;
domain = "example.com";
organizationName = "ACME Corp";
};
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ];
catchAll = [ "example.com" ];
};
"user2@example.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
aliases = [ "chuck@example.com" ];
};
"user@example2.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
};
"lowquota@example.com" = {
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
quota = "1B";
};
};
extraVirtualAliases = {
"single-alias@example.com" = "user1@example.com";
"multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ];
};
enableImap = 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
serverIP = nodes.server.config.networking.primaryIPAddress;
clientIP = nodes.client.config.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
#!${pkgs.stdenv.shell}
echo grep '${clientIP}' "$@" >&2
exec grep '${clientIP}' "$@"
'';
check-mail-id = pkgs.writeScriptBin "check-mail-id" ''
#!${pkgs.stdenv.shell}
echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2
exec grep '^Message-ID:.*@mail.example.com>$' "$@"
'';
test-imap-spam = pkgs.writeScriptBin "imap-mark-spam" ''
#!${pkgs.python3.interpreter}
import imaplib
with imaplib.IMAP4_SSL('${serverIP}') as imap:
imap.login('user1@example.com', 'user1')
imap.select()
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.copy(','.join(msg_ids), 'Junk')
for num in msg_ids:
imap.store(num, '+FLAGS', '\\Deleted')
imap.expunge()
imap.select('Junk')
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.close()
'';
test-imap-ham = pkgs.writeScriptBin "imap-mark-ham" ''
#!${pkgs.python3.interpreter}
import imaplib
with imaplib.IMAP4_SSL('${serverIP}') as imap:
imap.login('user1@example.com', 'user1')
imap.select('Junk')
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
imap.copy(','.join(msg_ids), 'INBOX')
for num in msg_ids:
imap.store(num, '+FLAGS', '\\Deleted')
imap.expunge()
imap.select('INBOX')
status, [response] = imap.search(None, 'ALL')
msg_ids = response.decode("utf-8").split(' ')
print(msg_ids)
assert status == 'OK'
assert len(msg_ids) == 1
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 {
imports = [
./lib/config.nix
];
environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search
];
environment.etc = {
"root/.fetchmailrc" = {
text = ''
poll ${serverIP} with proto IMAP
user 'user1@example.com' there with password 'user1' is 'root' here
mda procmail
'';
mode = "0700";
};
"root/.fetchmailRcLowQuota" = {
text = ''
poll ${serverIP} with proto IMAP
user 'lowquota@example.com' there with password 'user2' is 'root' here
mda procmail
'';
mode = "0700";
};
"root/.procmailrc" = {
text = "DEFAULT=$HOME/mail";
};
"root/.msmtprc" = {
text = ''
account test
host ${serverIP}
port 587
from user2@example.com
user user2@example.com
password user2
account test2
host ${serverIP}
port 587
from user@example2.com
user user@example2.com
password user2
account test3
host ${serverIP}
port 587
from chuck@example.com
user user2@example.com
password user2
account test4
host ${serverIP}
port 587
from postmaster@example.com
user user1@example.com
password user1
account test5
host ${serverIP}
port 587
from single-alias@example.com
user user1@example.com
password user1
'';
};
"root/email1".text = ''
Message-ID: <12345qwerty@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,
how are you doing today?
'';
"root/email2".text = ''
Message-ID: <232323abc@host.local.network>
From: User <user@example2.com>
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from user@example2.com to user1
Reply-To:
Hello User1,
how are you doing today?
XOXO User1
'';
"root/email3".text = ''
Message-ID: <asdfghjkl42@host.local.network>
From: Postmaster <postmaster@example.com>
To: Chuck <chuck@example.com>
Cc:
Bcc:
Subject: This is a test Email from postmaster\@example.com to chuck
Reply-To:
Hello Chuck,
I think I may have misconfigured the mail server
XOXO Postmaster
'';
"root/email4".text = ''
Message-ID: <sdfsdf@host.local.network>
From: Single Alias <single-alias@example.com>
To: User1 <user1@example.com>
Cc:
Bcc:
Subject: This is a test Email from single-alias\@example.com to user1
Reply-To:
Hello User1,
how are you doing today?
XOXO User1 aka Single Alias
'';
"root/email5".text = ''
Message-ID: <789asdf@host.local.network>
From: User2 <user2@example.com>
To: Multi Alias <multi-alias@example.com>
Cc:
Bcc:
Subject: This is a test Email from user2\@example.com to multi-alias
Reply-To:
Hello Multi Alias,
how are we doing today?
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, ... }:
''
start_all()
server.wait_for_unit("multi-user.target")
client.wait_for_unit("multi-user.target")
# TODO put this blocking into the systemd units?
server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
client.execute("cp -p /etc/root/.* ~/")
client.succeed("mkdir -p ~/mail")
client.succeed("ls -la ~/ >&2")
client.succeed("cat ~/.fetchmailrc >&2")
client.succeed("cat ~/.procmailrc >&2")
client.succeed("cat ~/.msmtprc >&2")
with subtest("imap retrieving mail"):
# fetchmail returns EXIT_CODE 1 when no new mail
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
with subtest("submission port send mail"):
# send email from user2 to user1
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
)
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
with subtest("imap retrieving mail 2"):
client.execute("rm ~/mail/*")
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v >&2")
with subtest("remove sensitive information on submission port"):
client.succeed("cat ~/mail/* >&2")
## make sure our IP is _not_ in the email header
client.fail("grep-ip ~/mail/*")
client.succeed("check-mail-id ~/mail/*")
with subtest("have correct fqdn as sender"):
client.succeed("grep 'Received: from mail.example.com' ~/mail/*")
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'"
)
with subtest("dkim singing, multiple domains"):
client.execute("rm ~/mail/*")
# send email from user2 to user1
client.succeed(
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
client.succeed("cat ~/mail/* >&2")
# make sure it is dkim signed
client.succeed("grep DKIM ~/mail/*")
with subtest("aliases"):
client.execute("rm ~/mail/*")
# send email from chuck to postmaster
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
with subtest("catchAlls"):
client.execute("rm ~/mail/*")
# send email from chuck to non exsitent account
client.succeed(
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
client.execute("rm ~/mail/*")
# send email from user1 to chuck
client.succeed(
"msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < /etc/root/email2 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 1 when no new mail
# if this succeeds, it means that user1 recieved the mail that was intended for chuck.
client.fail("fetchmail --nosslcertck -v")
with subtest("extraVirtualAliases"):
client.execute("rm ~/mail/*")
# send email from single-alias to user1
client.succeed(
"msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email4 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
client.execute("rm ~/mail/*")
# send email from user1 to multi-alias (user{1,2}@example.com)
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias\@example.com < /etc/root/email5 >&2"
)
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
# fetchmail returns EXIT_CODE 0 when it retrieves mail
client.succeed("fetchmail --nosslcertck -v")
with subtest("quota"):
client.execute("rm ~/mail/*")
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
client.succeed(
"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
client.fail("fetchmail --nosslcertck -v")
with subtest("imap sieve junk trainer"):
# send email from user2 to user1
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
)
# give the mail server some time to process the mail
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
client.succeed("imap-mark-spam >&2")
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2")
client.succeed("imap-mark-ham >&2")
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2")
with subtest("full text search and indexation"):
# send 2 email from user2 to user1
client.succeed(
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email6 >&2"
)
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,60 +0,0 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
import <nixpkgs/nixos/tests/make-test.nix> {
machine =
{ config, pkgs, ... }:
{
imports = [
./../default.nix
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" ];
loginAccounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
};
};
vmailGroupName = "vmail";
vmailUID = 5000;
};
};
testScript =
''
$machine->start;
$machine->waitForUnit("multi-user.target");
subtest "user exists", sub {
$machine->succeed("cat /etc/shadow | grep 'user1\@example.com'");
};
subtest "password is set", sub {
$machine->succeed("cat /etc/shadow | grep 'user1\@example.com:\$6\$/z4n8AQl6K\$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/:1::::::'");
};
subtest "vmail gid is set correctly", sub {
$machine->succeed("getent group vmail | grep 5000");
};
'';
}

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"
]))
'';
}

3
tests/lib/config.nix Normal file
View file

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

View file

@ -1,5 +1,5 @@
# nixos-mailserver: a simple mail server
# Copyright (C) 2016-2017 Robin Raymond
# 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
@ -14,9 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
import ./../../nixpkgs/nixos/tests/make-test.nix {
import <nixpkgs/nixos/tests/make-test-python.nix> {
machine =
nodes.machine =
{ config, pkgs, ... }:
{
imports = [
@ -26,6 +26,6 @@ import ./../../nixpkgs/nixos/tests/make-test.nix {
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"
)
'';
}

View file

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