Compare commits
1 commit
master
...
nixpkgs-up
Author | SHA1 | Date | |
---|---|---|---|
|
4d845db172 |
79 changed files with 2279 additions and 5189 deletions
3
.envrc
3
.envrc
|
@ -1,3 +0,0 @@
|
||||||
# shellcheck shell=bash
|
|
||||||
|
|
||||||
use flake
|
|
|
@ -1,17 +0,0 @@
|
||||||
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 }}
|
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,3 +1 @@
|
||||||
result
|
result
|
||||||
.direnv
|
|
||||||
.pre-commit-config.yaml
|
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
.hydra-cli:
|
|
||||||
image: docker.nix-community.org/nixpkgs/nix-flakes
|
|
||||||
script:
|
|
||||||
- nix run --inputs-from ./. nixpkgs#hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver "${jobset}"
|
|
||||||
|
|
||||||
hydra-pr:
|
hydra-pr:
|
||||||
extends: .hydra-cli
|
|
||||||
only:
|
only:
|
||||||
- merge_requests
|
- merge_requests
|
||||||
variables:
|
image: nixos/nix
|
||||||
jobset: $CI_MERGE_REQUEST_IID
|
script:
|
||||||
|
- nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID} || (echo Go to https://hydra.nix-community.org/jobset/simple-nixos-mailserver/${CI_MERGE_REQUEST_IID}/latest-eval for details; exit 1)
|
||||||
|
|
||||||
hydra-master:
|
hydra-master:
|
||||||
extends: .hydra-cli
|
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
variables:
|
image: nixos/nix
|
||||||
jobset: master
|
script:
|
||||||
|
- nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master || (echo Go to https://hydra.nix-community.org/jobset/simple-nixos-mailserver/master/latest-eval for details; exit 1)
|
||||||
|
|
|
@ -1,55 +1,98 @@
|
||||||
{ nixpkgs, pulls, ... }:
|
{ nixpkgs, declInput, pulls }:
|
||||||
|
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { };
|
pkgs = import nixpkgs {};
|
||||||
|
|
||||||
prs = builtins.fromJSON (builtins.readFile pulls);
|
prs = builtins.fromJSON (builtins.readFile pulls);
|
||||||
prJobsets = pkgs.lib.mapAttrs (num: info: {
|
prJobsets = pkgs.lib.mapAttrs (num: info:
|
||||||
enabled = 1;
|
{ enabled = 1;
|
||||||
hidden = false;
|
hidden = false;
|
||||||
description = "PR ${num}: ${info.title}";
|
description = "PR ${num}: ${info.title}";
|
||||||
checkinterval = 300;
|
nixexprinput = "snm";
|
||||||
|
nixexprpath = ".hydra/default.nix";
|
||||||
|
checkinterval = 30;
|
||||||
schedulingshares = 20;
|
schedulingshares = 20;
|
||||||
enableemail = false;
|
enableemail = false;
|
||||||
emailoverride = "";
|
emailoverride = "";
|
||||||
keepnr = 1;
|
keepnr = 1;
|
||||||
type = 1;
|
type = 0;
|
||||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
|
inputs = {
|
||||||
}) prs;
|
snm = {
|
||||||
mkFlakeJobset = branch: {
|
type = "git";
|
||||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
value = "${info.target_repo_url} merge-requests/${info.iid}/head";
|
||||||
checkinterval = 300;
|
emailresponsible = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
) prs;
|
||||||
|
|
||||||
|
desc = prJobsets // {
|
||||||
|
master = {
|
||||||
|
description = "Build master branch of Simple NixOS MailServer";
|
||||||
|
checkinterval = "60";
|
||||||
enabled = "1";
|
enabled = "1";
|
||||||
|
nixexprinput = "snm";
|
||||||
|
nixexprpath = ".hydra/default.nix";
|
||||||
schedulingshares = 100;
|
schedulingshares = 100;
|
||||||
enableemail = false;
|
enableemail = false;
|
||||||
emailoverride = "";
|
emailoverride = "";
|
||||||
keepnr = 3;
|
keepnr = 3;
|
||||||
hidden = false;
|
hidden = false;
|
||||||
type = 1;
|
type = 0;
|
||||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}";
|
inputs = {
|
||||||
|
snm = {
|
||||||
|
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master";
|
||||||
|
type = "git";
|
||||||
|
emailresponsible = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"nixos-20.03" = {
|
||||||
|
description = "Build the nixos-20.03 branch of Simple NixOS MailServer";
|
||||||
|
checkinterval = "60";
|
||||||
|
enabled = "1";
|
||||||
|
nixexprinput = "snm";
|
||||||
|
nixexprpath = ".hydra/default.nix";
|
||||||
|
schedulingshares = 100;
|
||||||
|
enableemail = false;
|
||||||
|
emailoverride = "";
|
||||||
|
keepnr = 3;
|
||||||
|
hidden = false;
|
||||||
|
type = 0;
|
||||||
|
inputs = {
|
||||||
|
snm = {
|
||||||
|
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.03";
|
||||||
|
type = "git";
|
||||||
|
emailresponsible = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"nixos-19.09" = {
|
||||||
|
description = "Build the nixos-19.09 branch of Simple NixOS MailServer";
|
||||||
|
checkinterval = "60";
|
||||||
|
enabled = "1";
|
||||||
|
nixexprinput = "snm";
|
||||||
|
nixexprpath = ".hydra/default.nix";
|
||||||
|
schedulingshares = 100;
|
||||||
|
enableemail = false;
|
||||||
|
emailoverride = "";
|
||||||
|
keepnr = 3;
|
||||||
|
hidden = false;
|
||||||
|
type = 0;
|
||||||
|
inputs = {
|
||||||
|
snm = {
|
||||||
|
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-19.09";
|
||||||
|
type = "git";
|
||||||
|
emailresponsible = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
desc = prJobsets // {
|
in {
|
||||||
"master" = mkFlakeJobset "master";
|
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
|
||||||
"nixos-24.11" = mkFlakeJobset "nixos-24.11";
|
|
||||||
"nixos-25.05" = mkFlakeJobset "nixos-25.05";
|
|
||||||
};
|
|
||||||
|
|
||||||
log = {
|
|
||||||
pulls = prs;
|
|
||||||
jobsets = desc;
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
{
|
|
||||||
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
|
|
||||||
cat >$out <<EOF
|
cat >$out <<EOF
|
||||||
${builtins.toJSON desc}
|
${builtins.toJSON desc}
|
||||||
EOF
|
EOF
|
||||||
# This is to get nice .jobsets build logs on Hydra
|
|
||||||
cat >tmp <<EOF
|
|
||||||
${builtins.toJSON log}
|
|
||||||
EOF
|
|
||||||
${pkgs.jq}/bin/jq . tmp
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
# 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
|
|
192
README.md
192
README.md
|
@ -1,106 +1,154 @@
|
||||||
# ![Simple Nixos MailServer][logo]
|
# ![Simple Nixos MailServer][logo]
|
||||||
|
|
||||||

|

|
||||||
[](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
|
[](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
|
||||||
|
|
||||||
|
|
||||||
## Release branches
|
## Release branches
|
||||||
|
|
||||||
For each NixOS release, we publish a branch. You then have to use the
|
For each NixOS release, we publish a branch. You then have to use the
|
||||||
SNM branch corresponding to your NixOS version.
|
SNM branch corresponding to your NixOS version.
|
||||||
|
|
||||||
* For NixOS 25.05
|
* For NixOS 20.03
|
||||||
* Use the [SNM branch `nixos-25.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.05)
|
- Use the [SNM branch `nixos-20.03`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.03)
|
||||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/)
|
- [Release notes](#nixos-2003)
|
||||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05)
|
* For NixOS 19.09
|
||||||
* For NixOS 24.11
|
- Use the [SNM branch `nixos-19.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-19.09)
|
||||||
* Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11)
|
|
||||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/)
|
|
||||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11)
|
|
||||||
* For NixOS unstable
|
* For NixOS unstable
|
||||||
* Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
||||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
|
- This branch is currently still supporting the NixOS release 20.03
|
||||||
|
but we could remove this support on any NixOS unstable breaking
|
||||||
|
change.
|
||||||
|
|
||||||
|
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
|
||||||
|
This is a very low volume list where new releases of SNM are announced, so you
|
||||||
|
can stay up to date with bug fixes and updates. All announcements are signed by
|
||||||
|
the gpg key with fingerprint
|
||||||
|
|
||||||
|
```
|
||||||
|
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
### v2.0
|
||||||
* [x] Continous Integration Testing
|
* [x] Continous Integration Testing
|
||||||
* [x] Multiple Domains
|
* [x] Multiple Domains
|
||||||
* Postfix
|
* Postfix MTA
|
||||||
* [x] SMTP on port 25
|
- [x] smtp on port 25
|
||||||
* [x] Submission TLS on port 465
|
- [x] submission port 587
|
||||||
* [x] Submission StartTLS on port 587
|
- [x] lmtp with dovecot
|
||||||
* [x] LMTP with Dovecot
|
* Dovecot
|
||||||
* Dovecot
|
- [x] maildir folders
|
||||||
* [x] Maildir folders
|
- [x] imap starttls on port 143
|
||||||
* [x] IMAP with TLS on port 993
|
- [x] pop3 starttls on port 110
|
||||||
* [x] POP3 with TLS on port 995
|
* Certificates
|
||||||
* [x] IMAP with StartTLS on port 143
|
- [x] manual certificates
|
||||||
* [x] POP3 with StartTLS on port 110
|
- [x] on the fly creation
|
||||||
* Certificates
|
- [x] Let's Encrypt
|
||||||
* [x] ACME
|
* Spam Filtering
|
||||||
* [x] Custom certificates
|
- [x] via rspamd
|
||||||
* Spam Filtering
|
* Virus Scanning
|
||||||
* [x] Via Rspamd
|
- [x] via clamav
|
||||||
* Virus Scanning
|
* DKIM Signing
|
||||||
* [x] Via ClamAV
|
- [x] via opendkim
|
||||||
* DKIM Signing
|
* User Management
|
||||||
* [x] Via Rspamd
|
- [x] declarative user management
|
||||||
* User Management
|
- [x] declarative password management
|
||||||
* [x] Declarative user management
|
* Sieves
|
||||||
* [x] Declarative password management
|
- [x] A simple standard script that moves spam
|
||||||
* [x] LDAP users
|
- [x] Allow user defined sieve scripts
|
||||||
* Sieve
|
- [x] ManageSieve support
|
||||||
* [x] Allow user defined sieve scripts
|
* User Aliases
|
||||||
* [x] Moving mails from/to junk trains the Bayes filter
|
- [x] Regular aliases
|
||||||
* [x] ManageSieve support
|
- [x] Catch all aliases
|
||||||
* User Aliases
|
|
||||||
* [x] Regular aliases
|
|
||||||
* [x] Catch all aliases
|
|
||||||
|
|
||||||
### In the future
|
### In the future
|
||||||
|
|
||||||
* Automatic client configuration
|
* DKIM Signing
|
||||||
* [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)
|
- [ ] Allow a per domain selector
|
||||||
* [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019)
|
|
||||||
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
|
### Changelog and How to Stay Up-to-Date
|
||||||
* DKIM Signing
|
|
||||||
* [ ] Allow per domain selectors
|
See the [mailing list archive](https://www.freelists.org/archive/snm/)
|
||||||
* [ ] Allow passing DKIM signing keys
|
|
||||||
* Improve the Forwarding Experience
|
### Quick Start
|
||||||
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html)
|
|
||||||
* [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd)
|
```nix
|
||||||
* User management
|
{ config, pkgs, ... }:
|
||||||
* [ ] Allow local and LDAP user to coexist
|
{
|
||||||
* OpenID Connect
|
imports = [
|
||||||
* Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166)
|
(builtins.fetchTarball {
|
||||||
|
# Pick a commit from the branch you are interested in
|
||||||
|
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
|
||||||
|
# And set its hash
|
||||||
|
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" "example2.com" ];
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
|
||||||
|
aliases = [
|
||||||
|
"info@example.com"
|
||||||
|
"postmaster@example.com"
|
||||||
|
"postmaster@example2.com"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of options, see `default.nix`.
|
||||||
|
|
||||||
### Get in touch
|
|
||||||
|
|
||||||
* Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org)
|
|
||||||
* IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect)
|
|
||||||
|
|
||||||
## How to Set Up a 10/10 Mail Server Guide
|
## How to Set Up a 10/10 Mail Server Guide
|
||||||
|
Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
|
||||||
|
|
||||||
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
|
## How to Backup
|
||||||
|
|
||||||
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
|
Checkout the [Complete Backup Guide](https://nixos-mailserver.readthedocs.io/en/latest/backup-guide.html). Backups are easy with `SNM`.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
|
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) wiki page.
|
||||||
|
|
||||||
|
## Release notes
|
||||||
|
|
||||||
|
### nixos-20.03
|
||||||
|
|
||||||
|
- Rspamd is upgraded to 2.0 which deprecates the SQLite Bayes
|
||||||
|
backend. We then moved to the Redis backend (the default since
|
||||||
|
Rspamd 2.0). If you don't want to relearn the Redis backend from the
|
||||||
|
scratch, we could manually run
|
||||||
|
|
||||||
|
rspamadm statconvert --spam-db /var/lib/rspamd/bayes.spam.sqlite --ham-db /var/lib/rspamd/bayes.ham.sqlite -h 127.0.0.1:6379 --symbol-ham BAYES_HAM --symbol-spam BAYES_SPAM
|
||||||
|
|
||||||
|
See the [Rspamd migration
|
||||||
|
notes](https://rspamd.com/doc/migration.html#migration-to-rspamd-20)
|
||||||
|
and [this SNM Merge
|
||||||
|
Request](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/164)
|
||||||
|
for details.
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
|
|
||||||
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
|
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
|
||||||
|
|
||||||
### Alternative Implementations
|
### Alternative Implementations
|
||||||
|
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
||||||
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
|
||||||
|
|
||||||
### Credits
|
### Credits
|
||||||
|
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
|
||||||
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
|
|
||||||
from [TheNounProject](https://thenounproject.com/) is licensed under
|
from [TheNounProject](https://thenounproject.com/) is licensed under
|
||||||
[CC BY 3.0](http://creativecommons.org/~/3.0/)
|
[CC BY 3.0](http://creativecommons.org/~/3.0/)
|
||||||
* Logo made with [Logomakr.com](https://logomakr.com)
|
* Logo made with [Logomakr.com](https://logomakr.com)
|
||||||
|
|
||||||
[logo]: docs/logo.png
|
|
||||||
|
|
||||||
|
|
||||||
|
[logo]: logo/logo.png
|
||||||
|
|
921
default.nix
921
default.nix
File diff suppressed because it is too large
Load diff
|
@ -1,20 +0,0 @@
|
||||||
# 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)
|
|
|
@ -1,32 +0,0 @@
|
||||||
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 ];
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
Advanced Configurations
|
|
||||||
=======================
|
|
||||||
|
|
||||||
Congratulations on completing the `Setup Guide <setup-guide.html>`_!
|
|
||||||
|
|
||||||
If you're an experienced mailserver admin, then you probably know what you want
|
|
||||||
to do next. Our How-to guides (accessible in the navigation sidebar)
|
|
||||||
might help you accomplish your goals. If not, consider contributing a guide!
|
|
||||||
|
|
||||||
If this is your first mailserver, consider the following:
|
|
||||||
|
|
||||||
- Set up `backups <backup-guide.html>`_.
|
|
||||||
- Enable `DMARC reporting <options.html#mailserver-dmarcreporting>`_ to be a
|
|
||||||
good citizen in the mail ecosystem.
|
|
|
@ -1,18 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
Backup Guide
|
A Complete Backup Guide
|
||||||
============
|
=======================
|
||||||
|
|
||||||
First off you should have a backup of your ``configuration.nix`` file
|
This is really easy. First off you should have a backup of your
|
||||||
where you have the server config (but that is already in a git
|
``configuration.nix`` file where you have the server config (but that is
|
||||||
repository right?)
|
already in a git repository right?)
|
||||||
|
|
||||||
Next you need to backup ``/var/vmail`` or whatever you have specified
|
Next you need to backup ``/var/vmail`` or whatever you have specified
|
||||||
for the option ``mailDirectory``. This is where all the mails reside.
|
for the option ``mailDirectory``. This is where all the mails reside.
|
||||||
|
@ -14,13 +14,6 @@ forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
|
||||||
back (or whatever you specified as ``vmailUserName``, and
|
back (or whatever you specified as ``vmailUserName``, and
|
||||||
``vmailGoupName``).
|
``vmailGoupName``).
|
||||||
|
|
||||||
If you enabled ``enableManageSieve`` then you also may want to backup
|
|
||||||
``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
|
|
||||||
The same considerations regarding file ownership apply as for the
|
|
||||||
Maildir.
|
|
||||||
|
|
||||||
To backup spam and ham training data, backup ``/var/lib/redis-rspamd``.
|
|
||||||
|
|
||||||
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
||||||
you specified as ``dkimKeyDirectory``). If you should lose those don’t
|
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
|
worry, new ones will be created on the fly. But you will need to repeat
|
||||||
|
|
24
docs/conf.py
24
docs/conf.py
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = "NixOS Mailserver"
|
project = 'NixOS Mailserver'
|
||||||
copyright = "2022, NixOS Mailserver Contributors"
|
copyright = '2020, NixOS Mailserver Contributors'
|
||||||
author = "NixOS Mailserver Contributors"
|
author = 'NixOS Mailserver Contributors'
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
|
@ -27,33 +27,27 @@ author = "NixOS Mailserver Contributors"
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
extensions = ["myst_parser"]
|
extensions = [
|
||||||
|
|
||||||
myst_enable_extensions = [
|
|
||||||
"colon_fence",
|
|
||||||
"linkify",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
smartquotes = False
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
templates_path = ["_templates"]
|
templates_path = ['_templates']
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
master_doc = "index"
|
master_doc = 'index'
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
html_theme = "sphinx_rtd_theme"
|
html_theme = 'sphinx_rtd_theme'
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
html_static_path = []
|
html_static_path = ['_static']
|
||||||
|
|
22
docs/faq.rst
22
docs/faq.rst
|
@ -1,22 +0,0 @@
|
||||||
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.
|
|
|
@ -1,30 +0,0 @@
|
||||||
Nix Flakes
|
|
||||||
==========
|
|
||||||
|
|
||||||
If you're using `flakes <https://wiki.nixos.org/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;
|
|
||||||
# ...
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
67
docs/fts.rst
67
docs/fts.rst
|
@ -1,67 +0,0 @@
|
||||||
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_flatcurve``.
|
|
||||||
|
|
||||||
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;
|
|
||||||
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:
|
|
||||||
|
|
||||||
* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes``
|
|
||||||
* disable expensive token normalisation in ``mailserver.fullTextSearch.filters``
|
|
||||||
* disable automatic indexation for some folders with
|
|
||||||
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by
|
|
||||||
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard.
|
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
Add Radicale
|
How to Add Radicale to SNM
|
||||||
============
|
==========================
|
||||||
|
|
||||||
Configuration by @dotlambda
|
Configuration by @dotlambda
|
||||||
|
|
||||||
Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional
|
|
||||||
crypt passwords are no longer supported. Instead bcrypt passwords
|
|
||||||
have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
|
||||||
|
|
||||||
.. code:: nix
|
.. code:: nix
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
@ -24,13 +20,12 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
||||||
in {
|
in {
|
||||||
services.radicale = {
|
services.radicale = {
|
||||||
enable = true;
|
enable = true;
|
||||||
settings = {
|
config = ''
|
||||||
auth = {
|
[auth]
|
||||||
type = "htpasswd";
|
type = htpasswd
|
||||||
htpasswd_filename = "${htpasswd}";
|
htpasswd_filename = ${htpasswd}
|
||||||
htpasswd_encryption = "bcrypt";
|
htpasswd_encryption = crypt
|
||||||
};
|
'';
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
|
@ -1,107 +1,58 @@
|
||||||
Contribute or troubleshoot
|
How to Develop SNM
|
||||||
==========================
|
==================
|
||||||
|
|
||||||
To report an issue, please go to
|
|
||||||
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
|
|
||||||
|
|
||||||
If you have questions, feel free to reach out:
|
|
||||||
|
|
||||||
* Matrix: `#nixos-mailserver:nixos.org <https://matrix.to/#/#nixos-mailserver:nixos.org>`__
|
|
||||||
* IRC: `#nixos-mailserver <ircs://irc.libera.chat/nixos-mailserver>`__ on `Libera Chat <https://libera.chat/guides/connect>`__
|
|
||||||
|
|
||||||
All our workflows rely on Nix being configured with `Flakes <https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
|
||||||
|
|
||||||
Development Shell
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
We provide a `flake.nix` devshell that automatically sets up pre-commit hooks,
|
|
||||||
which allows for fast feedback cycles when making changes to the repository.
|
|
||||||
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix develop
|
|
||||||
|
|
||||||
|
|
||||||
We recommend setting up `direnv <https://direnv.net/>`__ to automatically
|
|
||||||
attach to the development environment when entering the project directories.
|
|
||||||
|
|
||||||
Run NixOS tests
|
Run NixOS tests
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To run the test suite, you need to enable `Nix Flakes
|
You can run the testsuite via
|
||||||
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
|
||||||
|
|
||||||
You can then run the testsuite via
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
$ nix flake check -L
|
$ nix-build tests -A extern.nixpkgs_20_03
|
||||||
|
$ nix-build tests -A intern.nixpkgs_unstable
|
||||||
Since Nix doesn't garantee your machine have enough resources to run
|
...
|
||||||
all test VMs in parallel, some tests can fail. You would then haev to
|
|
||||||
run tests manually. For instance:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix build .#hydraJobs.x86_64-linux.external-unstable -L
|
|
||||||
|
|
||||||
|
|
||||||
Contributing to the documentation
|
Contributing to the documentation
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
The documentation is written in RST (except option documentation which is in CommonMark),
|
The documentation is written in RST, build with Sphinx and published
|
||||||
built with Sphinx and published by `Read the Docs <https://readthedocs.org/>`_.
|
by `Read the Docs <https://readthedocs.org/>`_.
|
||||||
|
|
||||||
For the syntax, see the `RST/Sphinx primer
|
For the syntax, see `RST/Sphinx Cheatsheet
|
||||||
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
|
<https://sphinx-tutorial.readthedocs.io/cheatsheet/>`_.
|
||||||
|
|
||||||
To build the documentation, you need to enable `Nix Flakes
|
|
||||||
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
|
||||||
|
|
||||||
|
The ``shell.nix`` provides all the tooling required to build the
|
||||||
|
documentation:
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
$ nix build .#documentation
|
$ nix-shell
|
||||||
$ xdg-open result/index.html
|
$ cd docs
|
||||||
|
$ make html
|
||||||
|
$ firefox ./_build/html/index.html
|
||||||
|
|
||||||
|
Nixops
|
||||||
|
------
|
||||||
|
|
||||||
Manual migrations
|
You can test the setup via ``nixops``. After installation, do
|
||||||
-----------------
|
|
||||||
|
|
||||||
We need to take great care around providing a migration story around breaking
|
::
|
||||||
changes. If manual intervention becomes necessary we provide the `stateVersion`
|
|
||||||
option to notify the user that they need to complete a migration before
|
|
||||||
they can deploy an update.
|
|
||||||
|
|
||||||
If that is the case for your change, find the highest `stateVersion` that is
|
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
|
||||||
being asserted on in `mail-server/assertions.nix`. Then pick the next number
|
$ nixops deploy -d mail
|
||||||
and add a new assertion, write a good summary describing the issue and what
|
$ nixops info -d mail
|
||||||
remediation steps are necessary. Finally reference the URL to the specific
|
|
||||||
section on the migration page in the documentation.
|
|
||||||
|
|
||||||
.. code-block:: nix
|
You can then test the server via e.g. \ ``telnet``. To log into it, use
|
||||||
|
|
||||||
{
|
::
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 1;
|
|
||||||
message = ''
|
|
||||||
Problem: The home directory for the foobar service is snafu.
|
|
||||||
Remediation:
|
|
||||||
- Stop the `foobar.service`
|
|
||||||
- Rename `/var/lib/foobaz` to `/var/lib/foobar`
|
|
||||||
- Increase the `mailserver.stateVersion` to 1.
|
|
||||||
|
|
||||||
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#specific-anchor-here for further details.
|
$ nixops ssh -d mail mailserver
|
||||||
'';
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
The setup guide should always reference the latest `stateVersion`, since we
|
Imap
|
||||||
don't require any migration steps for new setups.
|
----
|
||||||
|
|
||||||
The migration documentation should paint a more complete picture about the steps
|
To test imap manually use
|
||||||
that need to be carried out and why this has become necessary. Make sure to
|
|
||||||
reference the correct anchor in the URL you put into the assertion message.
|
::
|
||||||
|
|
||||||
|
$ openssl s_client -host mail.example.com -port 143 -starttls imap
|
||||||
|
|
|
@ -6,33 +6,19 @@
|
||||||
Welcome to NixOS Mailserver's documentation!
|
Welcome to NixOS Mailserver's documentation!
|
||||||
============================================
|
============================================
|
||||||
|
|
||||||
.. image:: logo.png
|
.. image:: ../logo/logo.png
|
||||||
:width: 400
|
:width: 400
|
||||||
:alt: SNM Logo
|
:alt: SNM Logo
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
quick-start
|
||||||
setup-guide
|
setup-guide
|
||||||
advanced-configurations
|
|
||||||
howto-develop
|
howto-develop
|
||||||
faq
|
|
||||||
release-notes
|
|
||||||
options
|
|
||||||
migrations
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
:caption: How-to
|
|
||||||
|
|
||||||
backup-guide
|
backup-guide
|
||||||
add-radicale
|
howto-add-radicale
|
||||||
add-roundcube
|
|
||||||
rspamd-tuning
|
rspamd-tuning
|
||||||
fts
|
|
||||||
flakes
|
|
||||||
autodiscovery
|
|
||||||
ldap
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
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``.
|
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
Migrations
|
|
||||||
==========
|
|
||||||
|
|
||||||
With mail server configuration best practices changing over time we might need
|
|
||||||
to make changes that require you to complete manual migration steps before you
|
|
||||||
can deploy a new version of NixOS mailserver.
|
|
||||||
|
|
||||||
The initial `mailserver.stateVersion` value should be copied from the setup
|
|
||||||
guide that you used to initially set up your mail server. If in doubt you can
|
|
||||||
always initialize it at `1` and walk through all assertions, that might apply
|
|
||||||
to your setup.
|
|
||||||
|
|
||||||
NixOS 25.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
#3 Dovecot mail directory migration
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
The way the Dovecot home directory for login accounts were previously set up
|
|
||||||
resulted in shared home directories for all those users. This is not a
|
|
||||||
supported Dovecot configuration.
|
|
||||||
|
|
||||||
To resolve this we migrated the home directory into the individual
|
|
||||||
`domain/localpart` subdirectory below the `mailserver.mailDirectory`.
|
|
||||||
|
|
||||||
But since this now overlaps with the location of the Maildir, it must be
|
|
||||||
migrated into the `mail/` directory below the home directory.
|
|
||||||
And while the LDAP home directory is not affected we use this migration to
|
|
||||||
keep the Maildir configurations of LDAP users in sync with those of local
|
|
||||||
accounts.
|
|
||||||
|
|
||||||
This is a big step forward, since we can now more cleanly colocate other
|
|
||||||
data directories, like sieve in the home directory, which in turn simplifies
|
|
||||||
backups.
|
|
||||||
|
|
||||||
This migration is required for every configuration.
|
|
||||||
|
|
||||||
For remediating this issue the following steps are required:
|
|
||||||
|
|
||||||
1. Copy the `migration script <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/migrations/nixos-mailserver-migration-03.py>`_ script to your mailserver
|
|
||||||
and make it executable:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
wget https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/raw/master/migrations/nixos-mailserver-migration-03.py
|
|
||||||
chmod +x nixos-mailserver-migration-03.py
|
|
||||||
|
|
||||||
2. Stop the ``dovecot2.service``.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
systemctl stop dovecot2.service
|
|
||||||
|
|
||||||
3. Create a backup or snapshot of your ``mailserver.mailDirectory``, so you can restore
|
|
||||||
should anything go wrong.
|
|
||||||
|
|
||||||
4. Run the migration script under your virtual mail user with the following arguments:
|
|
||||||
|
|
||||||
- ``--layout default`` unless ``useFSLayout`` is enabled, then ``--layout folder``
|
|
||||||
- The value of ``mailserver.mailDirectory``, which defaults to ``/var/vmail``
|
|
||||||
|
|
||||||
The script will not modify your data unless called with ``--execute``.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
sudo -u virtualMail ./nixos-mailserver-migration-03.py --layout default /var/vmail
|
|
||||||
|
|
||||||
5. Review the commands. They should be
|
|
||||||
|
|
||||||
- create a ``mail`` directory for each accounnt,
|
|
||||||
- move maildir contents from the parent directory into it,
|
|
||||||
- suggest removal of files that do not belong to the maildir
|
|
||||||
|
|
||||||
- their removal is not mandatory and the script **will not** remove them when called with ``--execute``
|
|
||||||
- review these items carefully if you want to remove them yourself
|
|
||||||
|
|
||||||
- remove obsolete files from the old home directory location
|
|
||||||
|
|
||||||
6. Rerun the command with ``--execute`` or run the commands manually.
|
|
||||||
|
|
||||||
7. Update the ``mailserver.stateVersion`` to ``3``.
|
|
||||||
|
|
||||||
#2 Dovecot LDAP home directory migration
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
The Dovecot configuration for LDAP home directories previously did not respect
|
|
||||||
the ``mailserver.mailDirectory`` setting.
|
|
||||||
|
|
||||||
This means that home directories were unconditionally located at
|
|
||||||
``/var/vmail/ldap/%{user}``.
|
|
||||||
|
|
||||||
This migration is required if you both:
|
|
||||||
|
|
||||||
* enabled the LDAP integration (``mailserver.ldap.enable``)
|
|
||||||
* and customized the default mail directory (``mailserver.mailDirectory != "/var/vmail"``)
|
|
||||||
|
|
||||||
For remediating this issue the following steps are required:
|
|
||||||
|
|
||||||
1. Stop ``dovecot2.service``.
|
|
||||||
2. Move ``/var/vmail/ldap`` below your ``m̀ailserver.mailDirectory``.
|
|
||||||
3. Update the ``mailserver.stateVersion`` to ``2``.
|
|
||||||
|
|
||||||
#1 Initialization
|
|
||||||
^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
This option was introduced in the NixOS 25.11 release cycle, in which case you
|
|
||||||
can safely initialize its value at `1`.
|
|
||||||
|
|
||||||
.. code-block:: nix
|
|
||||||
|
|
||||||
mailserver.stateVersion = 1;
|
|
||||||
|
|
32
docs/quick-start.rst
Normal file
32
docs/quick-start.rst
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
Quick Start
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. code:: nix
|
||||||
|
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(builtins.fetchTarball {
|
||||||
|
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.2.1/nixos-mailserver-v2.2.1.tar.gz";
|
||||||
|
sha256 = "03d49v8qnid9g9rha0wg2z6vic06mhp0b049s3whccn1axvs2zzx";
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" "example2.com" ];
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
|
||||||
|
aliases = [
|
||||||
|
"info@example.com"
|
||||||
|
"postmaster@example.com"
|
||||||
|
"postmaster@example2.com"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,123 +0,0 @@
|
||||||
Release Notes
|
|
||||||
=============
|
|
||||||
|
|
||||||
NixOS 25.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- The ``systemName`` and ``systemDomain`` options have been introduced to have
|
|
||||||
reusable configurations for automated reports (DMARC, TLSRPT). They come with
|
|
||||||
reasonable defaults, but it is suggested to check and change them as needed.
|
|
||||||
- The default key length for new DKIM RSA keys was increased to 2048 bits as
|
|
||||||
recommended in `RFC 8301 3.2`_.
|
|
||||||
We recommend rotating existing keys, as the RFC advises that signatures from
|
|
||||||
1024 bit keys should not be considered valid any longer.
|
|
||||||
- DMARC reports are now sent with the ``noreply-dmarc`` localpart from the
|
|
||||||
system domain.
|
|
||||||
|
|
||||||
.. _RFC 8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2
|
|
||||||
|
|
||||||
NixOS 25.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed`` canoncalizaliaton.
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/374>`__)
|
|
||||||
- Rspamd now connects to Redis over its Unix Domain Socket by default
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/375>`__)
|
|
||||||
|
|
||||||
- If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``.
|
|
||||||
- The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring.
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/380>`__)
|
|
||||||
- Switch to the more efficient `fts-flatcurve` indexer for full text search
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/361>`__).
|
|
||||||
|
|
||||||
This makes use of a new index, which will be automatically re-generated the
|
|
||||||
next time a folder is searched.
|
|
||||||
The operation is now quick enough to be performed "just-in-time".
|
|
||||||
Alternatively, all indices can be immediately re-generated for all users and
|
|
||||||
folders by running
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
doveadm fts rescan -u '*' && doveadm index -u '*' -q '*'
|
|
||||||
|
|
||||||
The previous index (which is not automatically discarded to allow rollbacks)
|
|
||||||
can be cleaned up by removing all the `xapian-indexes` directories within
|
|
||||||
``mailserver.indexDir``.
|
|
||||||
- Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``.
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/297>`__)
|
|
||||||
- Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP.
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/313>`__)
|
|
||||||
- Support for TLS 1.1 was disabled in accordance with `Mozilla's recommendations <https://ssl-config.mozilla.org/#server=postfix>`_.
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/234>`__)
|
|
||||||
|
|
||||||
NixOS 24.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- No new feature, only bug fixes and documentation improvements
|
|
||||||
|
|
||||||
NixOS 24.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Add new option ``acmeCertificateName`` which can be used to support
|
|
||||||
wildcard certificates
|
|
||||||
|
|
||||||
NixOS 23.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Add basic support for LDAP users
|
|
||||||
- Add support for regex (PCRE) aliases
|
|
||||||
|
|
||||||
NixOS 23.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Existing ACME certificates can be reused without configuring NGINX
|
|
||||||
- Certificate scheme is no longer a number, but a meaningful string instead
|
|
||||||
|
|
||||||
NixOS 22.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Allow Rspamd to send DMARC reporting
|
|
||||||
(`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/>`_
|
|
|
@ -1,5 +0,0 @@
|
||||||
sphinx ~= 5.3
|
|
||||||
sphinx_rtd_theme ~= 1.1
|
|
||||||
myst-parser ~= 0.18
|
|
||||||
linkify-it-py ~= 2.0
|
|
||||||
standard-imghdr
|
|
|
@ -1,19 +1,19 @@
|
||||||
Tune spam filtering
|
How to tune spam filtering
|
||||||
===================
|
==========================
|
||||||
|
|
||||||
SNM comes with the `rspamd spam filtering system <https://rspamd.com/>`_
|
SNM comes with the `rspamd spam filtering system <https://rspamd.com/>`_
|
||||||
enabled by default. Although its out-of-the-box performance is good, you
|
enabled by default. Although its out-of-the-box performance is good, you
|
||||||
can increase its efficiency by tuning its behaviour.
|
can increase its efficiency by tuning its behaviour.
|
||||||
|
|
||||||
Auto-learning
|
A) Auto-learning
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Moving spam email to the Junk folder (and false-positives out of it) will
|
Moving spam email to the Junk folder (and false-positives out of it) will
|
||||||
trigger an automatic training of the Bayesian filters, improving filtering
|
trigger an automatic training of the Bayesian filters, improving filtering
|
||||||
of future emails.
|
of future emails.
|
||||||
|
|
||||||
Train from existing folders
|
B) Train from existing folders
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If you kept previous spam, you can train the filter from it. Note that the
|
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>`_
|
`rspamd FAQ <https://rspamd.com/doc/faq.html#how-can-i-learn-messages>`_
|
||||||
|
@ -24,17 +24,20 @@ You can run the training in a root shell as follows:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
|
# Path to the controller socket
|
||||||
|
export RSOCK="/var/run/rspamd/worker-controller.sock"
|
||||||
|
|
||||||
# Learn the Junk folder as spam
|
# Learn the Junk folder as spam
|
||||||
rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
||||||
|
|
||||||
# Learn the INBOX as ham
|
# Learn the INBOX as ham
|
||||||
rspamc learn_ham /var/vmail/$DOMAIN/$USER/cur/
|
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/
|
||||||
|
|
||||||
# Check that training was successful
|
# Check that training was successful
|
||||||
rspamc stat | grep learned
|
rspamc -h $RSOCK stat | grep learned
|
||||||
|
|
||||||
Tune symbol weight
|
C) Tune symbol weight
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The ``X-Spamd-Result`` header is automatically added to your emails, detailing
|
The ``X-Spamd-Result`` header is automatically added to your emails, detailing
|
||||||
the scoring decisions. The `modules documentation <https://rspamd.com/doc/modules/>`_
|
the scoring decisions. The `modules documentation <https://rspamd.com/doc/modules/>`_
|
||||||
|
@ -49,8 +52,8 @@ details the meaning of each symbol. You can tune the weight if a symbol if neede
|
||||||
}'';
|
}'';
|
||||||
};
|
};
|
||||||
|
|
||||||
Tune action thresholds
|
D) Tune action thresholds
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
After scoring the message, rspamd decides on an action based on configurable 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.
|
By default, rspamd will tell postfix to reject any message with a score higher than 15.
|
||||||
|
@ -68,8 +71,8 @@ this behaviour by tuning the configuration. For example:
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
||||||
Access the rspamd web UI
|
E) Access the rspamd web UI
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Rspamd comes with `a web interface <https://rspamd.com/webui/>`_ that displays statistics
|
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**
|
and history of past scans. **We do NOT recommend using it to change the configuration**
|
||||||
|
|
|
@ -1,232 +1,234 @@
|
||||||
Setup Guide
|
A Complete Setup Guide
|
||||||
===========
|
======================
|
||||||
|
|
||||||
Mail servers can be a tricky thing to set up. This guide is supposed to
|
Mail servers can be a tricky thing to set up. This guide is supposed to
|
||||||
run you through the most important steps to achieve a 10/10 score on
|
run you through the most important steps to achieve a 10/10 score on
|
||||||
`<https://mail-tester.com>`_.
|
``mail-tester.com``.
|
||||||
|
|
||||||
What you need is:
|
What you need:
|
||||||
|
|
||||||
- a server running NixOS with a public IP
|
- A server with a public IP (referred to as ``server-IP``)
|
||||||
- a domain name.
|
- 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 ]``.
|
||||||
|
|
||||||
.. note::
|
A) Setup server
|
||||||
|
~~~~~~~~~~~~~~~
|
||||||
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/AAAA records for server
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Add DNS records to the domain ``example.com`` with the following
|
|
||||||
entries
|
|
||||||
|
|
||||||
==================== ===== ==== =============
|
|
||||||
Name (Subdomain) TTL Type Value
|
|
||||||
==================== ===== ==== =============
|
|
||||||
``mail.example.com`` 10800 A ``1.2.3.4``
|
|
||||||
``mail.example.com`` 10800 AAAA ``2001::1``
|
|
||||||
==================== ===== ==== =============
|
|
||||||
|
|
||||||
If your server does not have an IPv6 address, you must skip the `AAAA` record.
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t A mail.example.com"
|
|
||||||
mail.example.com has address 1.2.3.4
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t AAAA mail.example.com"
|
|
||||||
mail.example.com has address 2001::1
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated. This
|
|
||||||
DNS entry is required for the Let's Encrypt certificate generation
|
|
||||||
(which is used in the below configuration example).
|
|
||||||
|
|
||||||
Setup the server
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The following describes a server setup that is fairly complete. Even
|
The following describes a server setup that is fairly complete. Even
|
||||||
though there are more possible options (see the `NixOS Mailserver
|
though there are more possible options (see ``default.nix``), these
|
||||||
options documentation <options.html>`_), these should be the most
|
should be the most common ones.
|
||||||
common ones.
|
|
||||||
|
|
||||||
.. code:: nix
|
.. code:: nix
|
||||||
|
|
||||||
{ config, pkgs, ... }: {
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
(builtins.fetchTarball {
|
(builtins.fetchTarball {
|
||||||
# Pick a release version you are interested in and set its hash, e.g.
|
# Pick a commit from the branch you are interested in
|
||||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.05/nixos-mailserver-nixos-25.05.tar.gz";
|
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
|
||||||
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
|
# And set its hash
|
||||||
# release="nixos-25.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
|
|
||||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
stateVersion = 3;
|
fqdn = <server-FQDN>;
|
||||||
fqdn = "mail.example.com";
|
domains = [ <domains> ];
|
||||||
domains = [ "example.com" ];
|
|
||||||
|
|
||||||
# A list of all login accounts. To create the password hashes, use
|
# A list of all login accounts. To create the password hashes, use
|
||||||
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
# mkpasswd -m sha-512 "super secret password"
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
"user1@example.com" = {
|
"user1@example.com" = {
|
||||||
hashedPasswordFile = "/a/file/containing/a/hashed/password";
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
aliases = ["postmaster@example.com"];
|
|
||||||
|
aliases = [
|
||||||
|
"postmaster@example.com"
|
||||||
|
"postmaster@example2.com"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Make this user the catchAll address for domains example.com and
|
||||||
|
# example2.com
|
||||||
|
catchAll = [
|
||||||
|
"example.com"
|
||||||
|
"example2.com"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
"user2@example.com" = { ... };
|
"user2@example.com" = { ... };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# Extra virtual aliases. These are email addresses that are forwarded to
|
||||||
|
# loginAccounts addresses.
|
||||||
|
extraVirtualAliases = {
|
||||||
|
# address = forward address;
|
||||||
|
"abuse@example.com" = "user1@example.com";
|
||||||
|
};
|
||||||
|
|
||||||
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
|
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
|
||||||
# down nginx and opens port 80.
|
# down nginx and opens port 80.
|
||||||
certificateScheme = "acme-nginx";
|
certificateScheme = 3;
|
||||||
|
|
||||||
|
# Enable IMAP and POP3
|
||||||
|
enableImap = true;
|
||||||
|
enablePop3 = true;
|
||||||
|
enableImapSsl = true;
|
||||||
|
enablePop3Ssl = true;
|
||||||
|
|
||||||
|
# Enable the ManageSieve protocol
|
||||||
|
enableManageSieve = true;
|
||||||
|
|
||||||
|
# whether to scan inbound emails for viruses (note that this requires at least
|
||||||
|
# 1 Gb RAM for the server. Without virus scanning 256 MB RAM should be plenty)
|
||||||
|
virusScanning = false;
|
||||||
};
|
};
|
||||||
security.acme.acceptTerms = true;
|
|
||||||
security.acme.defaults.email = "security@example.com";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
After a ``nixos-rebuild switch`` your server should be running all
|
After a ``nixos-rebuild switch --upgrade`` your server should be good to
|
||||||
mail components.
|
go. If you want to use ``nixops`` to deploy the server, look in the
|
||||||
|
subfolder ``nixops`` for some inspiration.
|
||||||
|
|
||||||
Setup all other DNS requirements
|
B) Setup everything else
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Set rDNS (reverse DNS) entry for server
|
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 resolves DNS queries for ``server-FQDN`` to ``server-IP``. You can
|
||||||
|
test if your setting is correct by
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
ping <server-FQDN>
|
||||||
|
64 bytes from <server-FQDN> (<server-IP>): icmp_seq=1 ttl=46 time=21.3 ms
|
||||||
|
...
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
|
Step 2: Set rDNS (reverse DNS) entry for server
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Wherever you have rented your server, you should be able to set reverse
|
Wherever you have rented your server, you should be able to set reverse
|
||||||
DNS entries for the IP’s you own:
|
DNS entries for the IP’s you own. Add an entry resolving ``server-IP``
|
||||||
|
to ``server-FQDN``
|
||||||
|
|
||||||
- Add an entry resolving IPv4 address ``1.2.3.4`` to ``mail.example.com``.
|
You can test if your setting is correct by
|
||||||
- Add an entry resolving IPv6 ``2001::1`` to ``mail.example.com``. Again, this
|
|
||||||
must be skipped if your server does not have an IPv6 address.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
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"
|
host <server-IP>
|
||||||
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
|
<server-IP>.in-addr.arpa domain name pointer <server-FQDN>.
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host 2001::1"
|
|
||||||
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.2.ip6.arpa domain name pointer mail.example.com.
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
Set a ``MX`` record
|
Step 3: Set ``MX`` Records
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
For every ``domain`` in ``domains`` do: \* Add a ``MX`` record to the
|
||||||
Add a ``MX`` record to the domain ``example.com``.
|
domain ``domain``
|
||||||
|
|
||||||
================ ==== ======== =================
|
|
||||||
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"
|
| Name (Subdomain) | TTL | Type | Priority | Value |
|
||||||
example.com mail is handled by 10 mail.example.com.
|
| ---------------- | ----- | ---- | -------- | ----------------- |
|
||||||
|
| `domain` | | MX | 10 | `server-FQDN` |
|
||||||
|
|
||||||
|
You can test this via
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
dig -t MX <domain>
|
||||||
|
|
||||||
|
...
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
<domain> 10800 IN MX 10 <server-FQDN>
|
||||||
|
...
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
Set a ``SPF`` record
|
Step 4: Set ``SPF`` Records
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Add a `SPF <https://en.wikipedia.org/wiki/Sender_Policy_Framework>`_
|
For every ``domain`` in ``domains`` do: \* Add a ``SPF`` record to the
|
||||||
record to the domain ``example.com``.
|
domain ``domain``
|
||||||
|
|
||||||
================ ===== ==== ================================
|
|
||||||
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"
|
| Name (Subdomain) | TTL | Type | Priority | Value |
|
||||||
example.com descriptive text "v=spf1 a:mail.example.com -all"
|
| ---------------- | ----- | ---- | -------- | ----------------- |
|
||||||
|
| `domain` | 10800 | TXT | | `v=spf1 ip4:<server-IP> -all` |
|
||||||
|
|
||||||
|
You can check this with ``dig -t TXT <domain>`` similar to the last
|
||||||
|
section. Note that ``SPF`` records are set as ``TXT`` records since
|
||||||
|
RFC1035.
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated. If you
|
||||||
|
want to use multiple servers for your email handling, 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.
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
Set ``DKIM`` signature
|
Step 6: Set ``DMARC`` record
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
On your server, the ``rspamd`` systemd service generated a file
|
For every ``domain`` in ``domains`` do:
|
||||||
containing your DKIM public key in the file
|
|
||||||
``/var/dkim/example.com.mail.txt``. The content of this file looks
|
|
||||||
like
|
|
||||||
|
|
||||||
::
|
- Add a ``DMARC`` record to the domain ``domain``
|
||||||
|
|
||||||
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
|
==================== ===== ==== ======== ====================
|
||||||
"p=<really-long-key>" ) ; ----- DKIM key mail for nixos.org
|
Name (Subdomain) TTL Type Priority Value
|
||||||
|
==================== ===== ==== ======== ====================
|
||||||
|
\_dmarc.\ ``domain`` 10800 TXT ``v=DMARC1; p=none``
|
||||||
|
==================== ===== ==== ======== ====================
|
||||||
|
|
||||||
where ``really-long-key`` is your public key.
|
You can check this with ``dig -t TXT _dmarc.<domain>`` similar to the
|
||||||
|
last section.
|
||||||
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; k=rsa; s=email; 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.
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
Set a ``DMARC`` record
|
C) Test your Setup
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Add a ``DMARC`` record to the domain ``example.com``.
|
|
||||||
|
|
||||||
======================== ===== ==== ====================
|
|
||||||
Name (Subdomain) TTL Type Value
|
|
||||||
======================== ===== ==== ====================
|
|
||||||
_dmarc.example.com 10800 TXT ``v=DMARC1; p=none``
|
|
||||||
======================== ===== ==== ====================
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t TXT _dmarc.example.com"
|
|
||||||
_dmarc.example.com descriptive text "v=DMARC1; p=none"
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
|
||||||
|
|
||||||
|
|
||||||
Test your Setup
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Write an email to your aunt (who has been waiting for your reply far too
|
Write an email to your aunt (who has been waiting for your reply far too
|
||||||
long), and sign up for some of the finest newsletters the Internet has.
|
long), and sign up for some of the finest newsletters the Internet has.
|
||||||
|
@ -238,8 +240,3 @@ Besides that, you can send an email to
|
||||||
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
||||||
your setup, but if you followed the steps closely then everything should
|
your setup, but if you followed the steps closely then everything should
|
||||||
be awesome!
|
be awesome!
|
||||||
|
|
||||||
Next steps (optional)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Take a look through our `Advanced Configurations <advanced-configurations.html>`_.
|
|
||||||
|
|
124
flake.lock
generated
124
flake.lock
generated
|
@ -1,124 +0,0 @@
|
||||||
{
|
|
||||||
"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": 1747046372,
|
|
||||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
|
||||||
"owner": "edolstra",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "edolstra",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"git-hooks": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-compat": [
|
|
||||||
"flake-compat"
|
|
||||||
],
|
|
||||||
"gitignore": "gitignore",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1750779888,
|
|
||||||
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "cachix",
|
|
||||||
"repo": "git-hooks.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitignore": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"git-hooks",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1709087332,
|
|
||||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "gitignore.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1753939845,
|
|
||||||
"narHash": "sha256-K2ViRJfdVGE8tpJejs8Qpvvejks1+A4GQej/lBk5y7I=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "94def634a20494ee057c76998843c015909d6311",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-25_05": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1753749649,
|
|
||||||
"narHash": "sha256-+jkEZxs7bfOKfBIk430K+tK9IvXlwzqQQnppC2ZKFj4=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "NixOS",
|
|
||||||
"ref": "nixos-25.05",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"blobs": "blobs",
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"git-hooks": "git-hooks",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"nixpkgs-25_05": "nixpkgs-25_05"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
220
flake.nix
220
flake.nix
|
@ -1,220 +0,0 @@
|
||||||
{
|
|
||||||
description = "A complete and Simple Nixos Mailserver";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
flake-compat = {
|
|
||||||
# for shell.nix compat
|
|
||||||
url = "github:edolstra/flake-compat";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
git-hooks = {
|
|
||||||
url = "github:cachix/git-hooks.nix";
|
|
||||||
inputs.flake-compat.follows = "flake-compat";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
|
||||||
nixpkgs-25_05.url = "github:NixOS/nixpkgs/nixos-25.05";
|
|
||||||
blobs = {
|
|
||||||
url = "gitlab:simple-nixos-mailserver/blobs";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{
|
|
||||||
self,
|
|
||||||
blobs,
|
|
||||||
git-hooks,
|
|
||||||
nixpkgs,
|
|
||||||
nixpkgs-25_05,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
lib = nixpkgs.lib;
|
|
||||||
system = "x86_64-linux";
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
releases = [
|
|
||||||
{
|
|
||||||
name = "unstable";
|
|
||||||
nixpkgs = nixpkgs;
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "25.05";
|
|
||||||
nixpkgs = nixpkgs-25_05;
|
|
||||||
pkgs = nixpkgs-25_05.legacyPackages.${system};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
testNames = [
|
|
||||||
"clamav"
|
|
||||||
"external"
|
|
||||||
"internal"
|
|
||||||
"ldap"
|
|
||||||
"multiple"
|
|
||||||
];
|
|
||||||
|
|
||||||
genTest =
|
|
||||||
testName: release:
|
|
||||||
let
|
|
||||||
pkgs = release.pkgs;
|
|
||||||
nixos-lib = import (release.nixpkgs + "/nixos/lib") {
|
|
||||||
inherit (pkgs) lib;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}";
|
|
||||||
value = nixos-lib.runTest {
|
|
||||||
hostPkgs = pkgs;
|
|
||||||
imports = [ ./tests/${testName}.nix ];
|
|
||||||
_module.args = { inherit blobs; };
|
|
||||||
extraBaseModules.imports = [ ./default.nix ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
# Generate an attribute set such as
|
|
||||||
# {
|
|
||||||
# external-unstable = <derivation>;
|
|
||||||
# external-21_05 = <derivation>;
|
|
||||||
# ...
|
|
||||||
# }
|
|
||||||
allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames));
|
|
||||||
|
|
||||||
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"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
options = builtins.toFile "options.json" (
|
|
||||||
builtins.toJSON (
|
|
||||||
lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") (
|
|
||||||
lib.optionAttrSetToDocList eval.options
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
in
|
|
||||||
pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } ''
|
|
||||||
echo "Generating options.md from ${options}"
|
|
||||||
python ${./scripts/generate-options.py} ${options} > $out
|
|
||||||
echo $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
documentation = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "documentation";
|
|
||||||
src = lib.sourceByRegex ./docs [
|
|
||||||
"logo\\.png"
|
|
||||||
"conf\\.py"
|
|
||||||
"Makefile"
|
|
||||||
".*\\.rst"
|
|
||||||
];
|
|
||||||
buildInputs = [
|
|
||||||
(pkgs.python3.withPackages (
|
|
||||||
p: with p; [
|
|
||||||
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;
|
|
||||||
inherit (self.checks.${system}) pre-commit;
|
|
||||||
};
|
|
||||||
checks.${system} = allTests // {
|
|
||||||
pre-commit = git-hooks.lib.${system}.run {
|
|
||||||
src = ./.;
|
|
||||||
hooks = {
|
|
||||||
# docs
|
|
||||||
markdownlint = {
|
|
||||||
enable = true;
|
|
||||||
settings.configuration = {
|
|
||||||
# Max line length, doesn't seem to correclty account for lines containing links
|
|
||||||
# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
|
|
||||||
MD013 = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
rstcheck = {
|
|
||||||
enable = true;
|
|
||||||
package = pkgs.rstcheckWithSphinx;
|
|
||||||
entry = lib.getExe pkgs.rstcheckWithSphinx;
|
|
||||||
files = "\\.rst$";
|
|
||||||
};
|
|
||||||
|
|
||||||
# nix
|
|
||||||
deadnix.enable = true;
|
|
||||||
nixfmt-rfc-style.enable = true;
|
|
||||||
|
|
||||||
# python
|
|
||||||
pyright.enable = true;
|
|
||||||
ruff = {
|
|
||||||
enable = true;
|
|
||||||
args = [
|
|
||||||
"--extend-select"
|
|
||||||
"I"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
ruff-format.enable = true;
|
|
||||||
|
|
||||||
# scripts
|
|
||||||
shellcheck.enable = true;
|
|
||||||
|
|
||||||
# sieve
|
|
||||||
check-sieve = {
|
|
||||||
enable = true;
|
|
||||||
package = pkgs.check-sieve;
|
|
||||||
entry = lib.getExe pkgs.check-sieve;
|
|
||||||
files = "\\.sieve$";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
packages.${system} = {
|
|
||||||
inherit optionsDoc documentation;
|
|
||||||
};
|
|
||||||
devShells.${system}.default = pkgs.mkShellNoCC {
|
|
||||||
inputsFrom = [ documentation ];
|
|
||||||
packages =
|
|
||||||
with pkgs;
|
|
||||||
[
|
|
||||||
glab
|
|
||||||
]
|
|
||||||
++ self.checks.${system}.pre-commit.enabledPackages;
|
|
||||||
shellHook = self.checks.${system}.pre-commit.shellHook;
|
|
||||||
};
|
|
||||||
devShell.${system} = self.devShells.${system}.default; # compatibility
|
|
||||||
|
|
||||||
formatter.${system} = pkgs.nixfmt-tree;
|
|
||||||
};
|
|
||||||
}
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -1,58 +0,0 @@
|
||||||
{
|
|
||||||
config,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
{
|
|
||||||
# We guard all assertions by requiring mailserver to be actually enabled
|
|
||||||
assertions = lib.optionals config.mailserver.enable (
|
|
||||||
[
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.stateVersion != null;
|
|
||||||
message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at.";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
++ lib.optionals config.mailserver.ldap.enable [
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.loginAccounts == { };
|
|
||||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
|
||||||
}
|
|
||||||
# {
|
|
||||||
# assertion = config.mailserver.extraVirtualAliases == { };
|
|
||||||
# message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases";
|
|
||||||
# }
|
|
||||||
]
|
|
||||||
++
|
|
||||||
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
|
|
||||||
[
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
|
|
||||||
message = ''
|
|
||||||
Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`.
|
|
||||||
Remediation:
|
|
||||||
- Stop the `dovecot2.service`
|
|
||||||
- Move `/var/vmail/ldap` below your `mailserver.mailDirectory`
|
|
||||||
- Increase the `stateVersion` to 2.
|
|
||||||
|
|
||||||
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
]
|
|
||||||
++ [
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 3;
|
|
||||||
message = ''
|
|
||||||
Issue: The dovecot mail location for all users has changed and need to be migrated.
|
|
||||||
|
|
||||||
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration for the required remediation steps.
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
]
|
|
||||||
++ lib.optionals (config.mailserver.certificateScheme != "acme") [
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;
|
|
||||||
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
|
|
||||||
}
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -14,44 +14,28 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver.borgbackup;
|
cfg = config.mailserver.borgbackup;
|
||||||
|
|
||||||
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
||||||
autoFragment =
|
autoFragment =
|
||||||
if cfg.compression.auto && cfg.compression.method == null then
|
if cfg.compression.auto && cfg.compression.method == null
|
||||||
throw "compression.method must be set when using auto."
|
then throw "compression.method must be set when using auto."
|
||||||
else
|
else lib.optional cfg.compression.auto "auto";
|
||||||
lib.optional cfg.compression.auto "auto";
|
|
||||||
levelFragment =
|
levelFragment =
|
||||||
if cfg.compression.level != null && cfg.compression.method == null then
|
if cfg.compression.level != null && cfg.compression.method == null
|
||||||
throw "compression.method must be set when using compression.level."
|
then throw "compression.method must be set when using compression.level."
|
||||||
else
|
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
||||||
lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
|
||||||
compressionFragment = lib.concatStringsSep "," (
|
|
||||||
lib.flatten [
|
|
||||||
autoFragment
|
|
||||||
methodFragment
|
|
||||||
levelFragment
|
|
||||||
]
|
|
||||||
);
|
|
||||||
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
||||||
|
|
||||||
encryptionFragment = cfg.encryption.method;
|
encryptionFragment = cfg.encryption.method;
|
||||||
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
||||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none") (
|
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
|
||||||
if cfg.encryption.passphraseFile != null then
|
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
||||||
''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
else throw "passphraseFile must be set when using encryption.");
|
||||||
else
|
|
||||||
throw "passphraseFile must be set when using encryption."
|
|
||||||
);
|
|
||||||
|
|
||||||
locations = lib.escapeShellArgs cfg.locations;
|
locations = lib.escapeShellArgs cfg.locations;
|
||||||
name = lib.escapeShellArg cfg.name;
|
name = lib.escapeShellArg cfg.name;
|
||||||
|
@ -71,8 +55,7 @@ let
|
||||||
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
||||||
${cmdPostexec}
|
${cmdPostexec}
|
||||||
'';
|
'';
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
borgbackup
|
borgbackup
|
||||||
|
|
|
@ -14,17 +14,19 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
|
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
|
||||||
services.clamav.daemon = {
|
services.clamav.daemon.enable = true;
|
||||||
enable = true;
|
|
||||||
settings.PhishingScanURLs = "no";
|
|
||||||
};
|
|
||||||
services.clamav.updater.enable = true;
|
services.clamav.updater.enable = true;
|
||||||
|
|
||||||
|
services.clamav.daemon.extraConfig = ''
|
||||||
|
PhishingScanURLs no
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,79 +14,35 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib }:
|
||||||
config,
|
|
||||||
options,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# cert :: PATH
|
# cert :: PATH
|
||||||
certificatePath =
|
certificatePath = if cfg.certificateScheme == 1
|
||||||
if cfg.certificateScheme == "manual" then
|
then cfg.certificateFile
|
||||||
cfg.certificateFile
|
else if cfg.certificateScheme == 2
|
||||||
else if cfg.certificateScheme == "selfsigned" then
|
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||||
"${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
else if cfg.certificateScheme == 3
|
||||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
then "/var/lib/acme/${cfg.fqdn}/fullchain.pem"
|
||||||
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
||||||
else
|
|
||||||
throw "unknown certificate scheme";
|
|
||||||
|
|
||||||
# key :: PATH
|
# key :: PATH
|
||||||
keyPath =
|
keyPath = if cfg.certificateScheme == 1
|
||||||
if cfg.certificateScheme == "manual" then
|
then cfg.keyFile
|
||||||
cfg.keyFile
|
else if cfg.certificateScheme == 2
|
||||||
else if cfg.certificateScheme == "selfsigned" then
|
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||||
"${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
else if cfg.certificateScheme == 3
|
||||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
then "/var/lib/acme/${cfg.fqdn}/key.pem"
|
||||||
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
|
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
||||||
else
|
|
||||||
throw "unknown certificate scheme";
|
|
||||||
|
|
||||||
passwordFiles =
|
passwordFiles = let
|
||||||
let
|
|
||||||
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
|
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
|
||||||
in
|
in
|
||||||
lib.mapAttrs (
|
lib.mapAttrs (name: value:
|
||||||
name: value:
|
|
||||||
if value.hashedPasswordFile == null then
|
if value.hashedPasswordFile == null then
|
||||||
builtins.toString (mkHashFile name value.hashedPassword)
|
builtins.toString (mkHashFile name value.hashedPassword)
|
||||||
else
|
else value.hashedPasswordFile) cfg.loginAccounts;
|
||||||
value.hashedPasswordFile
|
|
||||||
) cfg.loginAccounts;
|
|
||||||
|
|
||||||
# Appends the LDAP bind password to files to avoid writing this
|
|
||||||
# password into the Nix store.
|
|
||||||
appendLdapBindPwd =
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
file,
|
|
||||||
prefix,
|
|
||||||
suffix ? "",
|
|
||||||
passwordFile,
|
|
||||||
destination,
|
|
||||||
}:
|
|
||||||
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
baseDir=$(dirname ${destination})
|
|
||||||
if (! test -d "$baseDir"); then
|
|
||||||
mkdir -p $baseDir
|
|
||||||
chmod 755 $baseDir
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat ${file} > ${destination}
|
|
||||||
echo -n '${prefix}' >> ${destination}
|
|
||||||
cat ${passwordFile} | tr -d '\n' >> ${destination}
|
|
||||||
echo -n '${suffix}' >> ${destination}
|
|
||||||
chmod 600 ${destination}
|
|
||||||
'';
|
|
||||||
|
|
||||||
dovecotUnitName = if options.services.dovecot2 ? hasNewUnitName then "dovecot" else "dovecot2";
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,88 +14,43 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
options,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
with (import ./common.nix {
|
with (import ./common.nix { inherit config pkgs lib; });
|
||||||
inherit
|
|
||||||
config
|
|
||||||
options
|
|
||||||
pkgs
|
|
||||||
lib
|
|
||||||
;
|
|
||||||
});
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
passwdDir = "/run/dovecot2";
|
passwdDir = "/run/dovecot2";
|
||||||
passwdFile = "${passwdDir}/passwd";
|
passwdFile = "${passwdDir}/passwd";
|
||||||
userdbFile = "${passwdDir}/userdb";
|
|
||||||
# This file contains the ldap bind password
|
|
||||||
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
|
|
||||||
boolToYesNo = x: if x then "yes" else "no";
|
|
||||||
listToLine = lib.concatStringsSep " ";
|
|
||||||
listToMultiAttrs =
|
|
||||||
keyPrefix: attrs:
|
|
||||||
lib.listToAttrs (
|
|
||||||
lib.imap1 (n: x: {
|
|
||||||
name = "${keyPrefix}${if n == 1 then "" else toString n}";
|
|
||||||
value = x;
|
|
||||||
}) attrs
|
|
||||||
);
|
|
||||||
|
|
||||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
||||||
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
|
|
||||||
|
|
||||||
# https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory
|
# maildir in format "/${domain}/${user}"
|
||||||
# Mail directory below the home directory
|
dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}";
|
||||||
dovecotMaildir =
|
|
||||||
"maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
|
||||||
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
|
|
||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
postfixCfg = config.services.postfix;
|
||||||
|
dovecot2Cfg = config.services.dovecot2;
|
||||||
|
|
||||||
ldapConfig = pkgs.writeTextFile {
|
stateDir = "/var/lib/dovecot";
|
||||||
name = "dovecot-ldap.conf.ext.template";
|
|
||||||
text = ''
|
pipeBin = pkgs.stdenv.mkDerivation {
|
||||||
ldap_version = 3
|
name = "pipe_bin";
|
||||||
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
|
src = ./dovecot/pipe_bin;
|
||||||
${lib.optionalString cfg.ldap.startTls ''
|
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
|
||||||
tls = yes
|
buildCommand = ''
|
||||||
''}
|
mkdir -p $out/pipe/bin
|
||||||
tls_require_cert = hard
|
cp $src/* $out/pipe/bin/
|
||||||
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
chmod a+x $out/pipe/bin/*
|
||||||
dn = ${cfg.ldap.bind.dn}
|
patchShebangs $out/pipe/bin
|
||||||
sasl_bind = no
|
|
||||||
auth_bind = yes
|
for file in $out/pipe/bin/*; do
|
||||||
base = ${cfg.ldap.searchBase}
|
wrapProgram $file \
|
||||||
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
|
||||||
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
|
done
|
||||||
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
|
||||||
''}
|
|
||||||
user_filter = ${cfg.ldap.dovecot.userFilter}
|
|
||||||
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
|
||||||
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
|
||||||
''}
|
|
||||||
pass_filter = ${cfg.ldap.dovecot.passFilter}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
setPwdInLdapConfFile = appendLdapBindPwd {
|
|
||||||
name = "ldap-conf-file";
|
|
||||||
file = ldapConfig;
|
|
||||||
prefix = ''dnpass = "'';
|
|
||||||
suffix = ''"'';
|
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
|
||||||
destination = ldapConfFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
|
@ -106,12 +61,7 @@ let
|
||||||
chmod 755 "${passwdDir}"
|
chmod 755 "${passwdDir}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Prevent world-readable password files, even temporarily.
|
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
||||||
umask 077
|
|
||||||
|
|
||||||
for f in ${
|
|
||||||
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)
|
|
||||||
}; do
|
|
||||||
if [ ! -f "$f" ]; then
|
if [ ! -f "$f" ]; then
|
||||||
echo "Expected password hash file $f does not exist!"
|
echo "Expected password hash file $f does not exist!"
|
||||||
exit 1
|
exit 1
|
||||||
|
@ -119,250 +69,68 @@ let
|
||||||
done
|
done
|
||||||
|
|
||||||
cat <<EOF > ${passwdFile}
|
cat <<EOF > ${passwdFile}
|
||||||
${lib.concatStringsSep "\n" (
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
||||||
lib.mapAttrsToList (
|
"${name}:${"$(cat ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
|
||||||
name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
+ (if lib.isString value.quota
|
||||||
) cfg.loginAccounts
|
then "userdb_quota_rule=*:storage=${value.quota}"
|
||||||
)}
|
else "")
|
||||||
|
) cfg.loginAccounts)}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat <<EOF > ${userdbFile}
|
chmod 600 ${passwdFile}
|
||||||
${lib.concatStringsSep "\n" (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
name: value:
|
|
||||||
"${name}:::::::"
|
|
||||||
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
|
|
||||||
) cfg.loginAccounts
|
|
||||||
)}
|
|
||||||
EOF
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
junkMailboxes = builtins.attrNames (
|
|
||||||
lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes
|
|
||||||
);
|
|
||||||
junkMailboxNumber = builtins.length junkMailboxes;
|
|
||||||
# The assertion garantees there is exactly one Junk mailbox.
|
|
||||||
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
|
||||||
|
|
||||||
mkLdapSearchScope =
|
|
||||||
scope:
|
|
||||||
(
|
|
||||||
if scope == "sub" then
|
|
||||||
"subtree"
|
|
||||||
else if scope == "one" then
|
|
||||||
"onelevel"
|
|
||||||
else
|
|
||||||
scope
|
|
||||||
);
|
|
||||||
|
|
||||||
ftsPluginSettings = {
|
|
||||||
fts = "flatcurve";
|
|
||||||
fts_languages = listToLine cfg.fullTextSearch.languages;
|
|
||||||
fts_tokenizers = listToLine [
|
|
||||||
"generic"
|
|
||||||
"email-address"
|
|
||||||
];
|
|
||||||
fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian
|
|
||||||
fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch;
|
|
||||||
fts_filters = listToLine cfg.fullTextSearch.filters;
|
|
||||||
fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes;
|
|
||||||
fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex;
|
|
||||||
fts_enforced = cfg.fullTextSearch.enforced;
|
|
||||||
}
|
|
||||||
// (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = junkMailboxNumber == 1;
|
|
||||||
message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
warnings =
|
|
||||||
lib.optional
|
|
||||||
(
|
|
||||||
(builtins.length cfg.fullTextSearch.languages > 1)
|
|
||||||
&& (builtins.elem "stopwords" cfg.fullTextSearch.filters)
|
|
||||||
)
|
|
||||||
''
|
|
||||||
Using stopwords in `mailserver.fullTextSearch.filters` with multiple
|
|
||||||
languages in `mailserver.fullTextSearch.languages` configured WILL
|
|
||||||
cause some searches to fail.
|
|
||||||
|
|
||||||
The recommended solution is to NOT use the stopword filter when
|
|
||||||
multiple languages are present in the configuration.
|
|
||||||
'';
|
|
||||||
|
|
||||||
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
|
|
||||||
# the global config and tries to open shared libraries configured in there,
|
|
||||||
# which are usually not compatible.
|
|
||||||
environment.systemPackages = [
|
|
||||||
pkgs.dovecot_pigeonhole
|
|
||||||
]
|
|
||||||
++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
|
|
||||||
|
|
||||||
# For compatibility with python imaplib
|
|
||||||
environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
|
|
||||||
|
|
||||||
services.dovecot2 = {
|
services.dovecot2 = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableImap = cfg.enableImap || cfg.enableImapSsl;
|
enableImap = enableImap;
|
||||||
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
|
enablePop3 = enablePop3;
|
||||||
enablePAM = false;
|
enablePAM = false;
|
||||||
enableQuota = true;
|
enableQuota = true;
|
||||||
mailGroup = cfg.vmailGroupName;
|
mailGroup = vmailGroupName;
|
||||||
mailUser = cfg.vmailUserName;
|
mailUser = vmailUserName;
|
||||||
mailLocation = dovecotMaildir;
|
mailLocation = dovecotMaildir;
|
||||||
sslServerCert = certificatePath;
|
sslServerCert = certificatePath;
|
||||||
sslServerKey = keyPath;
|
sslServerKey = keyPath;
|
||||||
enableDHE = lib.mkDefault false;
|
|
||||||
enableLmtp = true;
|
enableLmtp = true;
|
||||||
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
|
modules = [ pkgs.dovecot_pigeonhole ];
|
||||||
"fts"
|
protocols = [ "sieve" ];
|
||||||
"fts_flatcurve"
|
|
||||||
];
|
|
||||||
protocols = lib.optional cfg.enableManageSieve "sieve";
|
|
||||||
|
|
||||||
pluginSettings = {
|
sieveScripts = {
|
||||||
sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve";
|
after = builtins.toFile "spam.sieve" ''
|
||||||
sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve";
|
|
||||||
sieve_default_name = "default";
|
|
||||||
}
|
|
||||||
// (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
|
|
||||||
|
|
||||||
sieve = {
|
|
||||||
extensions = [
|
|
||||||
"fileinto"
|
|
||||||
];
|
|
||||||
|
|
||||||
scripts.after = builtins.toFile "spam.sieve" ''
|
|
||||||
require "fileinto";
|
require "fileinto";
|
||||||
|
|
||||||
if header :is "X-Spam" "Yes" {
|
if header :is "X-Spam" "Yes" {
|
||||||
fileinto "${junkMailboxName}";
|
fileinto "Junk";
|
||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
pipeBins = map lib.getExe [
|
|
||||||
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
|
|
||||||
(pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
imapsieve.mailbox = [
|
|
||||||
{
|
|
||||||
name = junkMailboxName;
|
|
||||||
causes = [
|
|
||||||
"COPY"
|
|
||||||
"APPEND"
|
|
||||||
];
|
|
||||||
before = ./dovecot/imap_sieve/report-spam.sieve;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "*";
|
|
||||||
from = junkMailboxName;
|
|
||||||
causes = [ "COPY" ];
|
|
||||||
before = ./dovecot/imap_sieve/report-ham.sieve;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
mailboxes = cfg.mailboxes;
|
mailboxes = cfg.mailboxes;
|
||||||
|
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
#Extra Config
|
#Extra Config
|
||||||
${lib.optionalString cfg.debug.dovecot ''
|
${lib.optionalString debug ''
|
||||||
mail_debug = yes
|
mail_debug = yes
|
||||||
auth_debug = yes
|
auth_debug = yes
|
||||||
verbose_ssl = yes
|
verbose_ssl = yes
|
||||||
''}
|
''}
|
||||||
|
|
||||||
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
|
|
||||||
service imap-login {
|
|
||||||
inet_listener imap {
|
|
||||||
${
|
|
||||||
if cfg.enableImap then
|
|
||||||
''
|
|
||||||
port = 143
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inet_listener imaps {
|
|
||||||
${
|
|
||||||
if cfg.enableImapSsl then
|
|
||||||
''
|
|
||||||
port = 993
|
|
||||||
ssl = yes
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''}
|
|
||||||
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
|
|
||||||
service pop3-login {
|
|
||||||
inet_listener pop3 {
|
|
||||||
${
|
|
||||||
if cfg.enablePop3 then
|
|
||||||
''
|
|
||||||
port = 110
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
inet_listener pop3s {
|
|
||||||
${
|
|
||||||
if cfg.enablePop3Ssl then
|
|
||||||
''
|
|
||||||
port = 995
|
|
||||||
ssl = yes
|
|
||||||
''
|
|
||||||
else
|
|
||||||
''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''}
|
|
||||||
|
|
||||||
protocol imap {
|
protocol imap {
|
||||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
||||||
mail_plugins = $mail_plugins imap_sieve
|
mail_plugins = $mail_plugins imap_sieve
|
||||||
}
|
}
|
||||||
|
|
||||||
service imap {
|
|
||||||
vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol pop3 {
|
protocol pop3 {
|
||||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
||||||
}
|
}
|
||||||
|
|
||||||
mail_access_groups = ${cfg.vmailGroupName}
|
mail_access_groups = ${vmailGroupName}
|
||||||
|
|
||||||
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
|
|
||||||
ssl = required
|
ssl = required
|
||||||
ssl_min_protocol = TLSv1.2
|
ssl_min_protocol = TLSv1.2
|
||||||
ssl_prefer_server_ciphers = no
|
ssl_prefer_server_ciphers = yes
|
||||||
ssl_curve_list = X25519:prime256v1:secp384r1
|
|
||||||
|
|
||||||
service lmtp {
|
service lmtp {
|
||||||
unix_listener dovecot-lmtp {
|
unix_listener dovecot-lmtp {
|
||||||
|
@ -370,20 +138,9 @@ in
|
||||||
mode = 0600
|
mode = 0600
|
||||||
user = ${postfixCfg.user}
|
user = ${postfixCfg.user}
|
||||||
}
|
}
|
||||||
vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
service quota-status {
|
recipient_delimiter = +
|
||||||
inet_listener {
|
|
||||||
port = 0
|
|
||||||
}
|
|
||||||
unix_listener quota-status {
|
|
||||||
user = postfix
|
|
||||||
}
|
|
||||||
vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB
|
|
||||||
}
|
|
||||||
|
|
||||||
recipient_delimiter = ${cfg.recipientDelimiter}
|
|
||||||
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
|
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
|
||||||
|
|
||||||
protocol lmtp {
|
protocol lmtp {
|
||||||
|
@ -397,33 +154,9 @@ in
|
||||||
|
|
||||||
userdb {
|
userdb {
|
||||||
driver = passwd-file
|
driver = passwd-file
|
||||||
args = ${userdbFile}
|
args = ${passwdFile}
|
||||||
default_fields = \
|
|
||||||
home=${cfg.mailDirectory}/%{domain}/%{username} \
|
|
||||||
uid=${builtins.toString cfg.vmailUID} \
|
|
||||||
gid=${builtins.toString cfg.vmailUID}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
${lib.optionalString cfg.ldap.enable ''
|
|
||||||
passdb {
|
|
||||||
driver = ldap
|
|
||||||
args = ${ldapConfFile}
|
|
||||||
}
|
|
||||||
|
|
||||||
userdb {
|
|
||||||
driver = ldap
|
|
||||||
args = ${ldapConfFile}
|
|
||||||
default_fields = \
|
|
||||||
home=${cfg.mailDirectory}/ldap/%{user} \
|
|
||||||
uid=${toString cfg.vmailUID} \
|
|
||||||
gid=${toString cfg.vmailUID} \
|
|
||||||
mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
|
|
||||||
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
''}
|
|
||||||
|
|
||||||
service auth {
|
service auth {
|
||||||
unix_listener auth {
|
unix_listener auth {
|
||||||
mode = 0660
|
mode = 0660
|
||||||
|
@ -439,10 +172,26 @@ in
|
||||||
inbox = yes
|
inbox = yes
|
||||||
}
|
}
|
||||||
|
|
||||||
service indexer-worker {
|
plugin {
|
||||||
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
|
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||||
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
|
sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve
|
||||||
''}
|
sieve_default = file:/var/sieve/%u/default.sieve
|
||||||
|
sieve_default_name = default
|
||||||
|
|
||||||
|
# From elsewhere to Spam folder
|
||||||
|
imapsieve_mailbox1_name = Junk
|
||||||
|
imapsieve_mailbox1_causes = COPY
|
||||||
|
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
|
||||||
|
|
||||||
|
# From Spam folder to elsewhere
|
||||||
|
imapsieve_mailbox2_name = *
|
||||||
|
imapsieve_mailbox2_from = Junk
|
||||||
|
imapsieve_mailbox2_causes = COPY
|
||||||
|
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
|
||||||
|
|
||||||
|
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
|
||||||
|
|
||||||
|
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
|
||||||
}
|
}
|
||||||
|
|
||||||
lda_mailbox_autosubscribe = yes
|
lda_mailbox_autosubscribe = yes
|
||||||
|
@ -450,16 +199,19 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.${dovecotUnitName} = {
|
systemd.services.dovecot2 = {
|
||||||
preStart = ''
|
preStart = ''
|
||||||
${genPasswdScript}
|
${genPasswdScript}
|
||||||
''
|
rm -rf '${stateDir}/imap_sieve'
|
||||||
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
mkdir '${stateDir}/imap_sieve'
|
||||||
|
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
|
||||||
|
for k in "${stateDir}/imap_sieve"/*.sieve ; do
|
||||||
|
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
|
||||||
|
done
|
||||||
|
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.postfix.restartTriggers = [
|
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
|
||||||
genPasswdScript
|
|
||||||
]
|
|
||||||
++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,4 +12,4 @@ if environment :matches "imap.user" "*" {
|
||||||
set "username" "${1}";
|
set "username" "${1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pipe :copy "rspamd-learn-ham.sh" [ "${username}" ];
|
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
|
||||||
|
|
|
@ -4,4 +4,4 @@ if environment :matches "imap.user" "*" {
|
||||||
set "username" "${1}";
|
set "username" "${1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pipe :copy "rspamd-learn-spam.sh" [ "${username}" ];
|
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
|
3
mail-server/dovecot/pipe_bin/sa-learn-ham.sh
Executable file
3
mail-server/dovecot/pipe_bin/sa-learn-ham.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -o errexit
|
||||||
|
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
3
mail-server/dovecot/pipe_bin/sa-learn-spam.sh
Executable file
3
mail-server/dovecot/pipe_bin/sa-learn-spam.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -o errexit
|
||||||
|
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
|
|
@ -14,26 +14,15 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
environment.systemPackages =
|
environment.systemPackages = with pkgs; [
|
||||||
with pkgs;
|
dovecot opendkim openssh postfix rspamd
|
||||||
[
|
] ++ (if certificateScheme == 2 then [ openssl ] else []);
|
||||||
dovecot
|
|
||||||
openssh
|
|
||||||
postfix
|
|
||||||
rspamd
|
|
||||||
]
|
|
||||||
++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
@ -22,5 +22,7 @@ in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
|
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
|
||||||
services.kresd.enable = true;
|
services.kresd.enable = true;
|
||||||
|
networking.nameservers = [ "127.0.0.1" ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
|
@ -14,26 +14,22 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && cfg.openFirewall) {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
allowedTCPPorts = [
|
allowedTCPPorts = [ 25 587 ]
|
||||||
25
|
++ lib.optional enableImap 143
|
||||||
]
|
++ lib.optional enableImapSsl 993
|
||||||
++ lib.optional cfg.enableSubmission 587
|
++ lib.optional enablePop3 110
|
||||||
++ lib.optional cfg.enableSubmissionSsl 465
|
++ lib.optional enablePop3Ssl 995
|
||||||
++ lib.optional cfg.enableImap 143
|
++ lib.optional enableManageSieve 4190
|
||||||
++ lib.optional cfg.enableImapSsl 993
|
++ lib.optional (certificateScheme == 3) 80;
|
||||||
++ lib.optional cfg.enablePop3 110
|
|
||||||
++ lib.optional cfg.enablePop3Ssl 995
|
|
||||||
++ lib.optional cfg.enableManageSieve 4190
|
|
||||||
++ lib.optional (cfg.certificateScheme == "acme-nginx") 80;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,43 +14,31 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
|
||||||
config,
|
|
||||||
options,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
with (import ./common.nix {
|
{ config, pkgs, lib, ... }:
|
||||||
inherit
|
|
||||||
config
|
with (import ./common.nix { inherit config; });
|
||||||
options
|
|
||||||
lib
|
|
||||||
pkgs
|
|
||||||
;
|
|
||||||
});
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
acmeRoot = "/var/lib/acme/acme-challenge";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config =
|
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) {
|
||||||
lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"))
|
services.nginx = {
|
||||||
{
|
|
||||||
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
|
|
||||||
enable = true;
|
enable = true;
|
||||||
virtualHosts."${cfg.fqdn}" = {
|
virtualHosts."${cfg.fqdn}" = {
|
||||||
serverName = cfg.fqdn;
|
serverName = cfg.fqdn;
|
||||||
serverAliases = cfg.certificateDomains;
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
|
acmeRoot = acmeRoot;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
|
security.acme.certs."${cfg.fqdn}".postRun = ''
|
||||||
"postfix.service"
|
systemctl reload nginx
|
||||||
"${dovecotUnitName}.service"
|
systemctl reload postfix
|
||||||
];
|
systemctl reload dovecot2
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
87
mail-server/opendkim.nix
Normal file
87
mail-server/opendkim.nix
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# 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}" ] || [ ! -f "${dkim_txt}" ]
|
||||||
|
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}"
|
||||||
|
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;
|
||||||
|
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
|
||||||
|
configFile = pkgs.writeText "opendkim.conf" (''
|
||||||
|
Canonicalization relaxed/simple
|
||||||
|
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} - -"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
46
mail-server/post-upgrade-check.nix
Normal file
46
mail-server/post-upgrade-check.nix
Normal 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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
|
@ -14,133 +14,67 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
options,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
with (import ./common.nix {
|
with (import ./common.nix { inherit config pkgs lib; });
|
||||||
inherit
|
|
||||||
config
|
|
||||||
options
|
|
||||||
lib
|
|
||||||
pkgs
|
|
||||||
;
|
|
||||||
});
|
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib.strings) concatStringsSep;
|
inherit (lib.strings) concatStringsSep;
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
# Merge several lookup tables. A lookup table is a attribute set where
|
# valiases_postfix :: [ String ]
|
||||||
# - the key is an address (user@example.com) or a domain (@example.com)
|
valiases_postfix = lib.flatten (lib.mapAttrsToList
|
||||||
# - the value is a list of addresses
|
(name: value:
|
||||||
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
|
let to = name;
|
||||||
|
in map (from: "${from} ${to}") (value.aliases ++ lib.singleton name))
|
||||||
|
cfg.loginAccounts);
|
||||||
|
|
||||||
# valiases_postfix :: Map String [String]
|
# catchAllPostfix :: [ String ]
|
||||||
valiases_postfix = mergeLookupTables (
|
catchAllPostfix = lib.flatten (lib.mapAttrsToList
|
||||||
lib.flatten (
|
(name: value:
|
||||||
lib.mapAttrsToList (
|
let to = name;
|
||||||
name: value:
|
in map (from: "@${from} ${to}") value.catchAll)
|
||||||
let
|
cfg.loginAccounts);
|
||||||
to = name;
|
|
||||||
in
|
|
||||||
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
|
|
||||||
) cfg.loginAccounts
|
|
||||||
)
|
|
||||||
);
|
|
||||||
regex_valiases_postfix = mergeLookupTables (
|
|
||||||
lib.flatten (
|
|
||||||
lib.mapAttrsToList (
|
|
||||||
name: value:
|
|
||||||
let
|
|
||||||
to = name;
|
|
||||||
in
|
|
||||||
map (from: { "${from}" = to; }) value.aliasesRegexp
|
|
||||||
) cfg.loginAccounts
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
# catchAllPostfix :: Map String [String]
|
# extra_valiases_postfix :: [ String ]
|
||||||
catchAllPostfix = mergeLookupTables (
|
extra_valiases_postfix =
|
||||||
lib.flatten (
|
(map
|
||||||
lib.mapAttrsToList (
|
(from:
|
||||||
name: value:
|
let to = cfg.extraVirtualAliases.${from};
|
||||||
let
|
aliasList = (l: let aliasStr = builtins.foldl' (x: y: x + y + ", ") "" l;
|
||||||
to = name;
|
in builtins.substring 0 (builtins.stringLength aliasStr - 2) aliasStr);
|
||||||
in
|
in if (builtins.isList to) then "${from} " + (aliasList to)
|
||||||
map (from: { "@${from}" = to; }) value.catchAll
|
else "${from} ${to}")
|
||||||
) cfg.loginAccounts
|
(builtins.attrNames cfg.extraVirtualAliases));
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
# all_valiases_postfix :: Map String [String]
|
# all_valiases_postfix :: [ String ]
|
||||||
all_valiases_postfix = mergeLookupTables [
|
all_valiases_postfix = valiases_postfix ++ extra_valiases_postfix;
|
||||||
valiases_postfix
|
|
||||||
extra_valiases_postfix
|
|
||||||
];
|
|
||||||
|
|
||||||
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
|
|
||||||
attrsToLookupTable =
|
|
||||||
aliases:
|
|
||||||
let
|
|
||||||
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
|
|
||||||
in
|
|
||||||
mergeLookupTables lookupTables;
|
|
||||||
|
|
||||||
# extra_valiases_postfix :: Map String [String]
|
|
||||||
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
|
|
||||||
|
|
||||||
# forwards :: Map String [String]
|
|
||||||
forwards = attrsToLookupTable cfg.forwards;
|
|
||||||
|
|
||||||
# lookupTableToString :: Map String [String] -> String
|
|
||||||
lookupTableToString =
|
|
||||||
attrs:
|
|
||||||
let
|
|
||||||
valueToString = value: lib.concatStringsSep ", " value;
|
|
||||||
in
|
|
||||||
lib.concatStringsSep "\n" (
|
|
||||||
lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs
|
|
||||||
);
|
|
||||||
|
|
||||||
# valiases_file :: Path
|
|
||||||
valiases_file =
|
|
||||||
let
|
|
||||||
content = lookupTableToString (mergeLookupTables [
|
|
||||||
all_valiases_postfix
|
|
||||||
catchAllPostfix
|
|
||||||
]);
|
|
||||||
in
|
|
||||||
builtins.toFile "valias" content;
|
|
||||||
|
|
||||||
regex_valiases_file =
|
|
||||||
let
|
|
||||||
content = lookupTableToString regex_valiases_postfix;
|
|
||||||
in
|
|
||||||
builtins.toFile "regex_valias" content;
|
|
||||||
|
|
||||||
# denied_recipients_postfix :: [ String ]
|
# denied_recipients_postfix :: [ String ]
|
||||||
denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (
|
denied_recipients_postfix = (map
|
||||||
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
|
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
||||||
);
|
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
|
||||||
denied_recipients_file = builtins.toFile "denied_recipients" (
|
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
|
||||||
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;
|
# valiases_file :: Path
|
||||||
|
valiases_file = builtins.toFile "valias"
|
||||||
|
(lib.concatStringsSep "\n" (all_valiases_postfix ++
|
||||||
|
catchAllPostfix));
|
||||||
|
|
||||||
|
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 ]
|
# rejectRecipients :: [ Path ]
|
||||||
reject_recipients_file = builtins.toFile "reject_recipients" (
|
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
|
||||||
lib.concatStringsSep "\n" reject_recipients_postfix
|
|
||||||
);
|
|
||||||
|
|
||||||
# vhosts_file :: Path
|
# vhosts_file :: Path
|
||||||
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
||||||
|
@ -151,13 +85,9 @@ let
|
||||||
# for details on how this file looks. By using the same file as valiases,
|
# for details on how this file looks. By using the same file as valiases,
|
||||||
# every alias is owned (uniquely) by its user.
|
# every alias is owned (uniquely) by its user.
|
||||||
# The user's own address is already in all_valiases_postfix.
|
# The user's own address is already in all_valiases_postfix.
|
||||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
vaccounts_file = builtins.toFile "vaccounts" (lib.concatStringsSep "\n" all_valiases_postfix);
|
||||||
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (
|
|
||||||
lookupTableToString regex_valiases_postfix
|
|
||||||
);
|
|
||||||
|
|
||||||
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (
|
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
||||||
''
|
|
||||||
# Removes sensitive headers from mails handed in via the submission port.
|
# Removes sensitive headers from mails handed in via the submission port.
|
||||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||||
# Uses "pcre" style regex.
|
# Uses "pcre" style regex.
|
||||||
|
@ -167,22 +97,128 @@ let
|
||||||
/^X-Mailer:/ IGNORE
|
/^X-Mailer:/ IGNORE
|
||||||
/^User-Agent:/ IGNORE
|
/^User-Agent:/ IGNORE
|
||||||
/^X-Enigmail:/ IGNORE
|
/^X-Enigmail:/ IGNORE
|
||||||
''
|
'' + lib.optionalString cfg.rewriteMessageId ''
|
||||||
+ lib.optionalString cfg.rewriteMessageId ''
|
|
||||||
|
|
||||||
# Replaces the user submitted hostname with the server's FQDN to hide the
|
# Replaces the user submitted hostname with the server's FQDN to hide the
|
||||||
# user's host or network.
|
# user's host or network.
|
||||||
|
|
||||||
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
||||||
''
|
'');
|
||||||
);
|
|
||||||
|
|
||||||
smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
|
||||||
|
unixSocket = sock: "unix:${sock}";
|
||||||
|
|
||||||
|
smtpdMilters =
|
||||||
|
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
|
||||||
|
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||||
|
|
||||||
|
policyd-spf = pkgs.writeText "policyd-spf.conf" (
|
||||||
|
cfg.policydSPFExtraConfig
|
||||||
|
+ (lib.optionalString cfg.debug ''
|
||||||
|
debugLevel = 4
|
||||||
|
''));
|
||||||
|
|
||||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||||
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
in
|
||||||
|
{
|
||||||
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
submissionOptions = {
|
services.postfix = {
|
||||||
|
enable = true;
|
||||||
|
hostname = "${fqdn}";
|
||||||
|
networksStyle = "host";
|
||||||
|
mapFiles."valias" = valiases_file;
|
||||||
|
mapFiles."vaccounts" = 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 = true;
|
||||||
|
virtual =
|
||||||
|
(lib.concatStringsSep "\n" (all_valiases_postfix ++ catchAllPostfix));
|
||||||
|
|
||||||
|
config = {
|
||||||
|
# Extra Config
|
||||||
|
mydestination = "";
|
||||||
|
recipient_delimiter = "+";
|
||||||
|
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";
|
||||||
|
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||||
|
|
||||||
|
# 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";
|
||||||
|
|
||||||
|
# strong might suffice and is computationally less expensive
|
||||||
|
smtpd_tls_eecdh_grade = "ultra";
|
||||||
|
|
||||||
|
# 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}";
|
||||||
|
|
||||||
|
};
|
||||||
|
submissionOptions =
|
||||||
|
{
|
||||||
smtpd_tls_security_level = "encrypt";
|
smtpd_tls_security_level = "encrypt";
|
||||||
smtpd_sasl_auth_enable = "yes";
|
smtpd_sasl_auth_enable = "yes";
|
||||||
smtpd_sasl_type = "dovecot";
|
smtpd_sasl_type = "dovecot";
|
||||||
|
@ -190,218 +226,18 @@ let
|
||||||
smtpd_sasl_security_options = "noanonymous";
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
smtpd_sasl_local_domain = "$myhostname";
|
smtpd_sasl_local_domain = "$myhostname";
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${
|
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
|
||||||
lib.optionalString (regex_valiases_postfix != { }) ",pcre:/etc/postfix/regex_vaccounts"
|
|
||||||
}";
|
|
||||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||||
cleanup_service_name = "submission-header-cleanup";
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
};
|
};
|
||||||
|
|
||||||
commonLdapConfig = ''
|
|
||||||
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
|
||||||
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
|
||||||
version = 3
|
|
||||||
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
|
||||||
tls_require_cert = yes
|
|
||||||
|
|
||||||
search_base = ${cfg.ldap.searchBase}
|
|
||||||
scope = ${cfg.ldap.searchScope}
|
|
||||||
|
|
||||||
bind = yes
|
|
||||||
bind_dn = ${cfg.ldap.bind.dn}
|
|
||||||
'';
|
|
||||||
|
|
||||||
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
|
|
||||||
${commonLdapConfig}
|
|
||||||
query_filter = ${cfg.ldap.postfix.filter}
|
|
||||||
result_attribute = ${cfg.ldap.postfix.mailAttribute}
|
|
||||||
'';
|
|
||||||
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
|
||||||
appendPwdInSenderLoginMap = appendLdapBindPwd {
|
|
||||||
name = "ldap-sender-login-map";
|
|
||||||
file = ldapSenderLoginMap;
|
|
||||||
prefix = "bind_pw = ";
|
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
|
||||||
destination = ldapSenderLoginMapFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
|
|
||||||
${commonLdapConfig}
|
|
||||||
query_filter = ${cfg.ldap.postfix.filter}
|
|
||||||
result_attribute = ${cfg.ldap.postfix.uidAttribute}
|
|
||||||
'';
|
|
||||||
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
|
|
||||||
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
|
|
||||||
name = "ldap-virtual-mailbox-map";
|
|
||||||
file = ldapVirtualMailboxMap;
|
|
||||||
prefix = "bind_pw = ";
|
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
|
||||||
destination = ldapVirtualMailboxMapFile;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf cfg.enable {
|
|
||||||
|
|
||||||
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
|
||||||
preStart = ''
|
|
||||||
${appendPwdInVirtualMailboxMap}
|
|
||||||
${appendPwdInSenderLoginMap}
|
|
||||||
'';
|
|
||||||
restartTriggers = [
|
|
||||||
appendPwdInVirtualMailboxMap
|
|
||||||
appendPwdInSenderLoginMap
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postfix = {
|
|
||||||
enable = true;
|
|
||||||
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;
|
|
||||||
enableSubmission = cfg.enableSubmission;
|
|
||||||
enableSubmissions = cfg.enableSubmissionSsl;
|
|
||||||
virtual = lookupTableToString (mergeLookupTables [
|
|
||||||
all_valiases_postfix
|
|
||||||
catchAllPostfix
|
|
||||||
forwards
|
|
||||||
]);
|
|
||||||
|
|
||||||
config = {
|
|
||||||
myhostname = cfg.sendingFqdn;
|
|
||||||
mydestination = ""; # disable local mail delivery
|
|
||||||
recipient_delimiter = cfg.recipientDelimiter;
|
|
||||||
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
|
|
||||||
disable_vrfy_command = true;
|
|
||||||
message_size_limit = cfg.messageSizeLimit;
|
|
||||||
|
|
||||||
# virtual mail system
|
|
||||||
virtual_uid_maps = "static:5000";
|
|
||||||
virtual_gid_maps = "static:5000";
|
|
||||||
virtual_mailbox_base = cfg.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"
|
|
||||||
];
|
|
||||||
|
|
||||||
# reject selected senders
|
|
||||||
smtpd_sender_restrictions = [
|
|
||||||
"check_sender_access ${mappedFile "reject_senders"}"
|
|
||||||
];
|
|
||||||
|
|
||||||
smtpd_recipient_restrictions = [
|
|
||||||
# reject selected recipients
|
|
||||||
"check_recipient_access ${mappedFile "denied_recipients"}"
|
|
||||||
"check_recipient_access ${mappedFile "reject_recipients"}"
|
|
||||||
# quota checking
|
|
||||||
"check_policy_service unix:/run/dovecot2/quota-status"
|
|
||||||
];
|
|
||||||
|
|
||||||
# The X509 private key followed by the corresponding certificate
|
|
||||||
smtpd_tls_chain_files = [
|
|
||||||
"${keyPath}"
|
|
||||||
"${certificatePath}"
|
|
||||||
];
|
|
||||||
|
|
||||||
# TLS for incoming mail is optional
|
|
||||||
smtpd_tls_security_level = "may";
|
|
||||||
|
|
||||||
# But required for authentication attempts
|
|
||||||
smtpd_tls_auth_only = true;
|
|
||||||
|
|
||||||
# TLS versions supported for the SMTP server
|
|
||||||
smtpd_tls_protocols = ">=TLSv1.2";
|
|
||||||
smtpd_tls_mandatory_protocols = ">=TLSv1.2";
|
|
||||||
|
|
||||||
# Require ciphersuites that OpenSSL classifies as "High"
|
|
||||||
smtpd_tls_ciphers = "high";
|
|
||||||
smtpd_tls_mandatory_ciphers = "high";
|
|
||||||
|
|
||||||
# Exclude cipher suites with undesirable properties
|
|
||||||
smtpd_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
|
|
||||||
smtpd_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL";
|
|
||||||
|
|
||||||
# Opportunistic DANE support when delivering mail to other servers
|
|
||||||
# https://www.postfix.org/postconf.5.html#smtp_tls_security_level
|
|
||||||
smtp_dns_support_level = "dnssec";
|
|
||||||
smtp_tls_security_level = "dane";
|
|
||||||
|
|
||||||
# TLS versions supported for the SMTP client
|
|
||||||
smtp_tls_protocols = ">=TLSv1.2";
|
|
||||||
smtp_tls_mandatory_protocols = ">=TLSv1.2";
|
|
||||||
|
|
||||||
# Require ciphersuites that OpenSSL classifies as "High"
|
|
||||||
smtp_tls_ciphers = "high";
|
|
||||||
smtp_tls_mandatory_ciphers = "high";
|
|
||||||
|
|
||||||
# Exclude ciphersuites with undesirable properties
|
|
||||||
smtp_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
|
|
||||||
smtp_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL";
|
|
||||||
|
|
||||||
# Restrict and prioritize the following curves in the given order
|
|
||||||
# Excludes curves that have no widespread support, so we don't bloat the handshake needlessly.
|
|
||||||
# https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves
|
|
||||||
# https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7
|
|
||||||
tls_eecdh_auto_curves = [
|
|
||||||
"X25519"
|
|
||||||
"prime256v1"
|
|
||||||
"secp384r1"
|
|
||||||
];
|
|
||||||
|
|
||||||
# Disable FFDHE on TLSv1.3 because it is slower than elliptic curves
|
|
||||||
# https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups
|
|
||||||
tls_ffdhe_auto_groups = [ ];
|
|
||||||
|
|
||||||
# As long as all cipher suites are considered safe, let the client use its preferred cipher
|
|
||||||
tls_preempt_cipherlist = false;
|
|
||||||
|
|
||||||
# Log only a summary message on TLS handshake completion
|
|
||||||
smtp_tls_loglevel = "1";
|
|
||||||
smtpd_tls_loglevel = "1";
|
|
||||||
|
|
||||||
smtpd_milters = smtpdMilters;
|
|
||||||
non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
|
||||||
milter_protocol = "6";
|
|
||||||
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
|
|
||||||
};
|
|
||||||
|
|
||||||
submissionOptions = submissionOptions;
|
|
||||||
submissionsOptions = submissionOptions;
|
|
||||||
|
|
||||||
masterConfig = {
|
masterConfig = {
|
||||||
"lmtp" = {
|
"policy-spf" = {
|
||||||
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
type = "unix";
|
||||||
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
privileged = true;
|
||||||
args = [ "flags=O" ];
|
chroot = false;
|
||||||
|
command = "spawn";
|
||||||
|
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
|
||||||
};
|
};
|
||||||
"submission-header-cleanup" = {
|
"submission-header-cleanup" = {
|
||||||
type = "unix";
|
type = "unix";
|
||||||
|
@ -409,10 +245,7 @@ in
|
||||||
chroot = false;
|
chroot = false;
|
||||||
maxproc = 0;
|
maxproc = 0;
|
||||||
command = "cleanup";
|
command = "cleanup";
|
||||||
args = [
|
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
|
||||||
"-o"
|
|
||||||
"header_checks=pcre:${submissionHeaderCleanupRules}"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,19 +14,11 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
pkgs,
|
with lib;
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib)
|
|
||||||
optionalString
|
|
||||||
mkIf
|
|
||||||
;
|
|
||||||
|
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
preexecDefined = cfg.backup.cmdPreexec != null;
|
preexecDefined = cfg.backup.cmdPreexec != null;
|
||||||
|
@ -46,8 +38,7 @@ let
|
||||||
${cfg.backup.cmdPostexec}
|
${cfg.backup.cmdPostexec}
|
||||||
'';
|
'';
|
||||||
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
config = mkIf (cfg.enable && cfg.backup.enable) {
|
config = mkIf (cfg.enable && cfg.backup.enable) {
|
||||||
services.rsnapshot = {
|
services.rsnapshot = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|
|
@ -14,12 +14,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
@ -27,75 +22,27 @@ let
|
||||||
postfixCfg = config.services.postfix;
|
postfixCfg = config.services.postfix;
|
||||||
rspamdCfg = config.services.rspamd;
|
rspamdCfg = config.services.rspamd;
|
||||||
rspamdSocket = "rspamd.service";
|
rspamdSocket = "rspamd.service";
|
||||||
|
|
||||||
rspamdUser = config.services.rspamd.user;
|
|
||||||
rspamdGroup = config.services.rspamd.group;
|
|
||||||
|
|
||||||
createDkimKeypair =
|
|
||||||
domain:
|
|
||||||
let
|
|
||||||
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
|
|
||||||
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
|
|
||||||
in
|
|
||||||
pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
|
||||||
if [ ! -f "${privateKey}" ]
|
|
||||||
then
|
|
||||||
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
|
|
||||||
--domain "${domain}" \
|
|
||||||
--selector "${cfg.dkimSelector}" \
|
|
||||||
--type "${cfg.dkimKeyType}" \
|
|
||||||
--bits ${toString cfg.dkimKeyBits} \
|
|
||||||
--privkey "${privateKey}" > "${publicKey}"
|
|
||||||
chmod 0644 "${publicKey}"
|
|
||||||
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
environment.systemPackages = lib.mkBefore [
|
|
||||||
(pkgs.runCommand "rspamc-wrapped"
|
|
||||||
{
|
|
||||||
nativeBuildInputs = with pkgs; [ makeWrapper ];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
|
|
||||||
--add-flags "-h /run/rspamd/worker-controller.sock"
|
|
||||||
''
|
|
||||||
)
|
|
||||||
];
|
|
||||||
|
|
||||||
services.rspamd = {
|
services.rspamd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
debug = cfg.debug.rspamd;
|
inherit debug;
|
||||||
locals = {
|
locals = {
|
||||||
"milter_headers.conf" = {
|
"milter_headers.conf" = { text = ''
|
||||||
text = ''
|
extended_spam_headers = yes;
|
||||||
extended_spam_headers = true;
|
''; };
|
||||||
'';
|
"redis.conf" = { text = ''
|
||||||
};
|
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
|
||||||
"redis.conf" = {
|
'' + (lib.optionalString (cfg.redis.password != null) ''
|
||||||
text = ''
|
|
||||||
servers = "${
|
|
||||||
if cfg.redis.port == null then
|
|
||||||
cfg.redis.address
|
|
||||||
else
|
|
||||||
"${cfg.redis.address}:${toString cfg.redis.port}"
|
|
||||||
}";
|
|
||||||
''
|
|
||||||
+ (lib.optionalString (cfg.redis.password != null) ''
|
|
||||||
password = "${cfg.redis.password}";
|
password = "${cfg.redis.password}";
|
||||||
'');
|
''); };
|
||||||
};
|
"classifier-bayes.conf" = { text = ''
|
||||||
"classifier-bayes.conf" = {
|
|
||||||
text = ''
|
|
||||||
cache {
|
cache {
|
||||||
backend = "redis";
|
backend = "redis";
|
||||||
}
|
}
|
||||||
'';
|
''; };
|
||||||
};
|
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
|
||||||
"antivirus.conf" = lib.mkIf cfg.virusScanning {
|
|
||||||
text = ''
|
|
||||||
clamav {
|
clamav {
|
||||||
action = "reject";
|
action = "reject";
|
||||||
symbol = "CLAM_VIRUS";
|
symbol = "CLAM_VIRUS";
|
||||||
|
@ -104,50 +51,23 @@ in
|
||||||
servers = "/run/clamav/clamd.ctl";
|
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
|
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
||||||
}
|
}
|
||||||
'';
|
''; };
|
||||||
};
|
|
||||||
"dkim_signing.conf" = {
|
|
||||||
text = ''
|
|
||||||
enabled = ${lib.boolToString cfg.dkimSigning};
|
|
||||||
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
|
|
||||||
selector = "${cfg.dkimSelector}";
|
|
||||||
# Allow for usernames w/o domain part
|
|
||||||
allow_username_mismatch = true
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"dmarc.conf" = {
|
|
||||||
text = ''
|
|
||||||
${lib.optionalString cfg.dmarcReporting.enable ''
|
|
||||||
reporting {
|
|
||||||
enabled = true;
|
|
||||||
email = "noreply-dmarc@${cfg.systemDomain}";
|
|
||||||
domain = "${cfg.systemDomain}";
|
|
||||||
org_name = "${cfg.systemName}";
|
|
||||||
from_name = "${cfg.systemName}";
|
|
||||||
msgid_from = "${cfg.systemDomain}";
|
|
||||||
${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
|
|
||||||
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
|
|
||||||
''}
|
|
||||||
}''}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
overrides = {
|
overrides = {
|
||||||
"options.inc" = {
|
"milter_headers.conf" = {
|
||||||
text = ''
|
text = ''
|
||||||
local_addrs = [::1/128, 127.0.0.0/8]
|
extended_spam_headers = true;
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
workers.rspamd_proxy = {
|
workers.rspamd_proxy = {
|
||||||
type = "rspamd_proxy";
|
type = "rspamd_proxy";
|
||||||
bindSockets = [
|
bindSockets = [{
|
||||||
{
|
|
||||||
socket = "/run/rspamd/rspamd-milter.sock";
|
socket = "/run/rspamd/rspamd-milter.sock";
|
||||||
mode = "0664";
|
mode = "0664";
|
||||||
}
|
}];
|
||||||
];
|
|
||||||
count = 1; # Do not spawn too many processes of this type
|
count = 1; # Do not spawn too many processes of this type
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
milter = yes; # Enable milter mode
|
milter = yes; # Enable milter mode
|
||||||
|
@ -162,13 +82,11 @@ in
|
||||||
workers.controller = {
|
workers.controller = {
|
||||||
type = "controller";
|
type = "controller";
|
||||||
count = 1;
|
count = 1;
|
||||||
bindSockets = [
|
bindSockets = [{
|
||||||
{
|
|
||||||
socket = "/run/rspamd/worker-controller.sock";
|
socket = "/run/rspamd/worker-controller.sock";
|
||||||
mode = "0666";
|
mode = "0666";
|
||||||
}
|
}];
|
||||||
];
|
includes = [];
|
||||||
includes = [ ];
|
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
||||||
'';
|
'';
|
||||||
|
@ -176,102 +94,11 @@ in
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
|
services.redis.enable = true;
|
||||||
|
|
||||||
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
|
||||||
"${cfg.dkimKeyDirectory}" = {
|
|
||||||
d = {
|
|
||||||
# Create /var/dkim owned by rspamd user/group
|
|
||||||
user = rspamdUser;
|
|
||||||
group = rspamdGroup;
|
|
||||||
};
|
|
||||||
Z = {
|
|
||||||
# Recursively adjust permissions in /var/dkim
|
|
||||||
user = rspamdUser;
|
|
||||||
group = rspamdGroup;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.rspamd = {
|
systemd.services.rspamd = {
|
||||||
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||||
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
after = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||||
serviceConfig = lib.mkMerge [
|
|
||||||
{
|
|
||||||
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
|
||||||
}
|
|
||||||
(lib.optionalAttrs cfg.dkimSigning {
|
|
||||||
ExecStartPre = map createDkimKeypair cfg.domains;
|
|
||||||
ReadWritePaths = [ cfg.dkimKeyDirectory ];
|
|
||||||
})
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
|
|
||||||
# Explicitly select yesterday's date to work around broken
|
|
||||||
# default behaviour when called without a date.
|
|
||||||
# https://github.com/rspamd/rspamd/issues/4062
|
|
||||||
script = toString [
|
|
||||||
(lib.getExe' pkgs.rspamd "rspamadm")
|
|
||||||
"dmarc_report"
|
|
||||||
"$(date -d 'yesterday' '+%Y%m%d')"
|
|
||||||
];
|
|
||||||
serviceConfig = {
|
|
||||||
User = "${config.services.rspamd.user}";
|
|
||||||
Group = "${config.services.rspamd.group}";
|
|
||||||
|
|
||||||
AmbientCapabilities = [ ];
|
|
||||||
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"
|
|
||||||
"AF_UNIX"
|
|
||||||
];
|
|
||||||
RestrictNamespaces = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
SupplementaryGroups = lib.optionals cfg.redis.configureLocally [
|
|
||||||
config.services.redis.servers.rspamd.group
|
|
||||||
];
|
|
||||||
SystemCallArchitectures = "native";
|
|
||||||
SystemCallFilter = [
|
|
||||||
"@system-service"
|
|
||||||
"~@privileged"
|
|
||||||
];
|
|
||||||
UMask = "0077";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
|
|
||||||
description = "Daily delivery of aggregated DMARC reports";
|
|
||||||
wantedBy = [
|
|
||||||
"timers.target"
|
|
||||||
];
|
|
||||||
timerConfig = {
|
|
||||||
OnCalendar = "daily";
|
|
||||||
Persistent = true;
|
|
||||||
RandomizedDelaySec = 86400;
|
|
||||||
FixedRandomDelay = true;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.postfix = {
|
systemd.services.postfix = {
|
||||||
|
@ -282,3 +109,4 @@ in
|
||||||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,54 +14,44 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
options,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
with (import ./common.nix {
|
|
||||||
inherit
|
|
||||||
config
|
|
||||||
options
|
|
||||||
lib
|
|
||||||
pkgs
|
|
||||||
;
|
|
||||||
});
|
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
certificatesDeps =
|
preliminarySelfsigned = config.security.acme.preliminarySelfsigned;
|
||||||
if cfg.certificateScheme == "manual" then
|
acmeWantsTarget = [ "acme-certificates.target" ]
|
||||||
[ ]
|
++ (lib.optional preliminarySelfsigned "acme-selfsigned-certificates.target");
|
||||||
else if cfg.certificateScheme == "selfsigned" then
|
acmeAfterTarget = if preliminarySelfsigned
|
||||||
[ "mailserver-selfsigned-certificate.service" ]
|
then [ "acme-selfsigned-certificates.target" ]
|
||||||
else
|
else [ "acme-certificates.target" ];
|
||||||
[ "acme-finished-${cfg.fqdn}.target" ];
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf cfg.enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
# Add target for when certificates are available
|
||||||
|
systemd.targets."mailserver-certificates" = {
|
||||||
|
wants = lib.mkIf (cfg.certificateScheme == 3) acmeWantsTarget;
|
||||||
|
after = lib.mkIf (cfg.certificateScheme == 3) acmeAfterTarget;
|
||||||
|
};
|
||||||
|
|
||||||
# Create self signed certificate
|
# Create self signed certificate
|
||||||
systemd.services.mailserver-selfsigned-certificate =
|
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
|
||||||
lib.mkIf (cfg.certificateScheme == "selfsigned")
|
wantedBy = [ "mailserver-certificates.target" ];
|
||||||
{
|
|
||||||
after = [ "local-fs.target" ];
|
after = [ "local-fs.target" ];
|
||||||
|
before = [ "mailserver-certificates.target" ];
|
||||||
script = ''
|
script = ''
|
||||||
# Create certificates if they do not exist yet
|
# Create certificates if they do not exist yet
|
||||||
dir="${cfg.certificateDirectory}"
|
dir="${cfg.certificateDirectory}"
|
||||||
fqdn="${cfg.fqdn}"
|
fqdn="${cfg.fqdn}"
|
||||||
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
|
case $fqdn in /*) fqdn=$(cat "$fqdn");; esac
|
||||||
key="$dir/key-${cfg.fqdn}.pem";
|
key="''${dir}/key-${cfg.fqdn}.pem";
|
||||||
cert="$dir/cert-${cfg.fqdn}.pem";
|
cert="''${dir}/cert-${cfg.fqdn}.pem";
|
||||||
|
|
||||||
if [[ ! -f $key || ! -f $cert ]]; then
|
if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ]
|
||||||
|
then
|
||||||
mkdir -p "${cfg.certificateDirectory}"
|
mkdir -p "${cfg.certificateDirectory}"
|
||||||
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
|
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) &&
|
||||||
"${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
|
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \
|
||||||
-days 3650 -out "$cert"
|
-days 3650 -out "''${cert}"
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
|
@ -71,35 +61,25 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
# Create maildir folder before dovecot startup
|
# Create maildir folder before dovecot startup
|
||||||
systemd.services.${dovecotUnitName} = {
|
systemd.services.dovecot2 = {
|
||||||
wants = certificatesDeps;
|
after = [ "mailserver-certificates.target" ];
|
||||||
after = certificatesDeps;
|
wants = [ "mailserver-certificates.target" ];
|
||||||
preStart =
|
preStart = ''
|
||||||
let
|
|
||||||
directories = lib.strings.escapeShellArgs (
|
|
||||||
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
|
||||||
);
|
|
||||||
in
|
|
||||||
''
|
|
||||||
# Create mail directory and set permissions. See
|
# Create mail directory and set permissions. See
|
||||||
# <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>.
|
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
||||||
# Prevent world-readable paths, even temporarily.
|
mkdir -p "${mailDirectory}"
|
||||||
umask 007
|
chgrp "${vmailGroupName}" "${mailDirectory}"
|
||||||
mkdir -p ${directories}
|
chmod 02770 "${mailDirectory}"
|
||||||
chgrp "${cfg.vmailGroupName}" ${directories}
|
|
||||||
chmod 02770 ${directories}
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
||||||
systemd.services.postfix = {
|
systemd.services.postfix = {
|
||||||
wants = certificatesDeps;
|
after = [ "dovecot2.service" "mailserver-certificates.target" ]
|
||||||
after = [
|
++ (lib.optional cfg.dkimSigning "opendkim.service");
|
||||||
"${dovecotUnitName}.service"
|
wants = [ "mailserver-certificates.target" ];
|
||||||
]
|
requires = [ "dovecot2.service" ]
|
||||||
++ lib.optional cfg.dkimSigning "rspamd.service"
|
++ (lib.optional cfg.dkimSigning "opendkim.service");
|
||||||
++ certificatesDeps;
|
|
||||||
requires = [ "${dovecotUnitName}.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,100 +14,74 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ config, pkgs, lib, ... }:
|
||||||
config,
|
|
||||||
options,
|
|
||||||
pkgs,
|
|
||||||
lib,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
with (import ./common.nix {
|
|
||||||
inherit
|
|
||||||
config
|
|
||||||
options
|
|
||||||
lib
|
|
||||||
pkgs
|
|
||||||
;
|
|
||||||
});
|
|
||||||
|
|
||||||
with config.mailserver;
|
with config.mailserver;
|
||||||
|
|
||||||
let
|
let
|
||||||
vmail_user = {
|
vmail_user = {
|
||||||
name = vmailUserName;
|
name = vmailUserName;
|
||||||
isSystemUser = true;
|
isNormalUser = false;
|
||||||
uid = vmailUID;
|
uid = vmailUID;
|
||||||
home = mailDirectory;
|
home = mailDirectory;
|
||||||
createHome = true;
|
createHome = true;
|
||||||
group = vmailGroupName;
|
group = vmailGroupName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Prevent world-readable paths, even temporarily.
|
|
||||||
umask 007
|
|
||||||
|
|
||||||
# Create directory to store user sieve scripts if it doesn't exist
|
# Create directory to store user sieve scripts if it doesn't exist
|
||||||
if (! test -d "${sieveDirectory}"); then
|
if (! test -d "/var/sieve"); then
|
||||||
mkdir "${sieveDirectory}"
|
mkdir "/var/sieve"
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
|
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve"
|
||||||
chmod 770 "${sieveDirectory}"
|
chmod 770 "/var/sieve"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy user's sieve script to the correct location (if it exists). If it
|
# Copy user's sieve script to the correct location (if it exists). If it
|
||||||
# is null, remove the file.
|
# is null, remove the file.
|
||||||
${lib.concatMapStringsSep "\n" (
|
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
|
||||||
{ name, sieveScript }:
|
if lib.isString sieveScript then ''
|
||||||
if lib.isString sieveScript then
|
if (! test -d "/var/sieve/${name}"); then
|
||||||
''
|
mkdir -p "/var/sieve/${name}"
|
||||||
if (! test -d "${sieveDirectory}/${name}"); then
|
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve/${name}"
|
||||||
mkdir -p "${sieveDirectory}/${name}"
|
chmod 770 "/var/sieve/${name}"
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
|
||||||
chmod 770 "${sieveDirectory}/${name}"
|
|
||||||
fi
|
fi
|
||||||
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
|
cat << 'EOF' > "/var/sieve/${name}/default.sieve"
|
||||||
${sieveScript}
|
${sieveScript}
|
||||||
EOF
|
EOF
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve/${name}/default.sieve"
|
||||||
''
|
'' else ''
|
||||||
else
|
if (test -f "/var/sieve/${name}/default.sieve"); then
|
||||||
''
|
rm "/var/sieve/${name}/default.sieve"
|
||||||
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
|
||||||
rm "${sieveDirectory}/${name}/default.sieve"
|
|
||||||
fi
|
fi
|
||||||
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
if (test -f "/var/sieve/${name}.svbin"); then
|
||||||
rm "${sieveDirectory}/${name}/default.svbin"
|
rm "/var/sieve/${name}/default.svbin"
|
||||||
fi
|
fi
|
||||||
''
|
'') (map (user: { inherit (user) name sieveScript; })
|
||||||
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))}
|
(lib.attrValues loginAccounts))}
|
||||||
'';
|
'';
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
config = lib.mkIf enable {
|
config = lib.mkIf enable {
|
||||||
# assert that all accounts provide a password
|
# assert that all accounts provide a password
|
||||||
assertions = map (acct: {
|
assertions = (map (acct: {
|
||||||
assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
|
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
|
||||||
message = "${acct.name} must provide either a hashed password or a password hash file";
|
message = "${acct.name} must provide either a hashed password or a password hash file";
|
||||||
}) (lib.attrValues loginAccounts);
|
}) (lib.attrValues loginAccounts));
|
||||||
|
|
||||||
# warn for accounts that specify both password and file
|
# warn for accounts that specify both password and file
|
||||||
warnings =
|
warnings = (map
|
||||||
map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
||||||
(
|
(lib.filter
|
||||||
lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
|
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
|
||||||
lib.attrValues loginAccounts
|
(lib.attrValues loginAccounts)));
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
# set the vmail gid to a specific value
|
# set the vmail gid to a specific value
|
||||||
users.groups = {
|
users.groups = {
|
||||||
"${vmailGroupName}" = {
|
"${vmailGroupName}" = { gid = vmailUID; };
|
||||||
gid = vmailUID;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
# define all users
|
# define all users
|
||||||
|
@ -117,7 +91,7 @@ in
|
||||||
|
|
||||||
systemd.services.activate-virtual-mail-users = {
|
systemd.services.activate-virtual-mail-users = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
before = [ "${dovecotUnitName}.service" ];
|
before = [ "dovecot2.service" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = virtualMailUsersActivationScript;
|
ExecStart = virtualMailUsersActivationScript;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,142 +0,0 @@
|
||||||
#!/usr/bin/env nix-shell
|
|
||||||
#!nix-shell -i python3 -p python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import sys
|
|
||||||
from enum import Enum
|
|
||||||
from pathlib import Path
|
|
||||||
from pwd import getpwnam
|
|
||||||
|
|
||||||
|
|
||||||
class FolderLayout(Enum):
|
|
||||||
Default = 1
|
|
||||||
Folder = 2
|
|
||||||
|
|
||||||
|
|
||||||
def check_user(vmail_root: Path):
|
|
||||||
owner = vmail_root.owner()
|
|
||||||
owner_uid = getpwnam(owner).pw_uid
|
|
||||||
|
|
||||||
if os.geteuid() == owner_uid:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
print(
|
|
||||||
f"Trying to switch effective user id to {owner_uid} ({owner})",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
os.seteuid(owner_uid)
|
|
||||||
return
|
|
||||||
except PermissionError:
|
|
||||||
print(
|
|
||||||
f"Failed switching to virtual mail user. Please run this script under it, for example by using `sudo -u {owner}`)",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def is_maildir_related(path: Path, layout: FolderLayout) -> bool:
|
|
||||||
if path.name in [
|
|
||||||
"subscriptions",
|
|
||||||
# https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-uid-mapping
|
|
||||||
"dovecot-uidlist",
|
|
||||||
# https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-keywords
|
|
||||||
"dovecot-keywords",
|
|
||||||
]:
|
|
||||||
return True
|
|
||||||
if not path.is_dir():
|
|
||||||
return False
|
|
||||||
if path.name in ["cur", "new", "tmp"]:
|
|
||||||
return True
|
|
||||||
if layout is FolderLayout.Default and path.name.startswith("."):
|
|
||||||
return True
|
|
||||||
if layout is FolderLayout.Folder:
|
|
||||||
if path.name in ["mail"]:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def mkdir(dst: Path, dry_run: bool = True):
|
|
||||||
print(f'mkdir "{dst}"')
|
|
||||||
if not dry_run:
|
|
||||||
# u+rwx, setgid
|
|
||||||
dst.mkdir(mode=0o2700)
|
|
||||||
|
|
||||||
|
|
||||||
def move(src: Path, dst: Path, dry_run: bool = True):
|
|
||||||
print(f'mv "{src}" "{dst}"')
|
|
||||||
if not dry_run:
|
|
||||||
src.rename(dst)
|
|
||||||
|
|
||||||
|
|
||||||
def delete(dst: Path, dry_run: bool = True):
|
|
||||||
if not dst.exists():
|
|
||||||
return
|
|
||||||
|
|
||||||
if dst.is_dir():
|
|
||||||
print(f'rm --recursive "{dst}"')
|
|
||||||
if not dry_run:
|
|
||||||
shutil.rmtree(dst)
|
|
||||||
else:
|
|
||||||
print(f'rm "{dst}"')
|
|
||||||
if not dry_run:
|
|
||||||
dst.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def main(vmail_root: Path, layout: FolderLayout, dry_run: bool = True):
|
|
||||||
maildirs = {path.parent for path in vmail_root.glob("*/*/cur")}
|
|
||||||
maybe_delete = []
|
|
||||||
|
|
||||||
# The old maildir will be the new home directory
|
|
||||||
for homedir in maildirs:
|
|
||||||
maildir = homedir / "mail"
|
|
||||||
mkdir(maildir, dry_run)
|
|
||||||
|
|
||||||
for path in homedir.iterdir():
|
|
||||||
if is_maildir_related(path, layout):
|
|
||||||
move(path, maildir / path.name, dry_run)
|
|
||||||
else:
|
|
||||||
maybe_delete.append(path)
|
|
||||||
|
|
||||||
# Files that are part of the previous home directory, but now obsolete
|
|
||||||
for path in [
|
|
||||||
vmail_root / ".dovecot.lda-dupes",
|
|
||||||
vmail_root / ".dovecot.lda-dupes.locks",
|
|
||||||
]:
|
|
||||||
delete(path, dry_run)
|
|
||||||
|
|
||||||
# The remaining files are likely obsolete, but should still be checked with care
|
|
||||||
for path in maybe_delete:
|
|
||||||
print(f"# rm {str(path)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="""
|
|
||||||
NixOS Mailserver Migration #3: Dovecot mail directory migration
|
|
||||||
(https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration)
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"vmail_root", type=Path, help="Path to the `mailserver.mailDirectory`"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--layout",
|
|
||||||
choices=["default", "folder"],
|
|
||||||
required=True,
|
|
||||||
help="Folder layout: 'default' unless `mailserver.useFsLayout` was enabled, then'folder'",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--execute", action="store_true", help="Actually perform changes"
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
layout = FolderLayout.Default if args.layout == "default" else FolderLayout.Folder
|
|
||||||
|
|
||||||
check_user(args.vmail_root)
|
|
||||||
main(args.vmail_root, layout, not args.execute)
|
|
26
nix/sources.json
Normal file
26
nix/sources.json
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"nixpkgs-20.03": {
|
||||||
|
"branch": "nixos-20.03",
|
||||||
|
"description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to",
|
||||||
|
"homepage": "https://github.com/NixOS/nixpkgs",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs-channels",
|
||||||
|
"rev": "aaa66d8d887c73f643246ac1a684fcb1521543b8",
|
||||||
|
"sha256": "0qvrhc7hv8h4yqa4jh64y6v5j3nza53ivkbq6j72g434c3yp2h50",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://github.com/NixOS/nixpkgs-channels/archive/aaa66d8d887c73f643246ac1a684fcb1521543b8.tar.gz",
|
||||||
|
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||||
|
},
|
||||||
|
"nixpkgs-unstable": {
|
||||||
|
"branch": "nixos-unstable",
|
||||||
|
"description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to",
|
||||||
|
"homepage": "https://github.com/NixOS/nixpkgs",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs-channels",
|
||||||
|
"rev": "c71518e75bf067fb639d44264fdd8cf80f53d75a",
|
||||||
|
"sha256": "0hwa79prsqgvfwd3ah54nl0wh73q215z7np4k6y0pd6zr3m17vxs",
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://github.com/NixOS/nixpkgs-channels/archive/c71518e75bf067fb639d44264fdd8cf80f53d75a.tar.gz",
|
||||||
|
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||||
|
}
|
||||||
|
}
|
134
nix/sources.nix
Normal file
134
nix/sources.nix
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
# This file has been generated by Niv.
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
#
|
||||||
|
# The fetchers. fetch_<type> fetches specs of type <type>.
|
||||||
|
#
|
||||||
|
|
||||||
|
fetch_file = pkgs: spec:
|
||||||
|
if spec.builtin or true then
|
||||||
|
builtins_fetchurl { inherit (spec) url sha256; }
|
||||||
|
else
|
||||||
|
pkgs.fetchurl { inherit (spec) url sha256; };
|
||||||
|
|
||||||
|
fetch_tarball = pkgs: spec:
|
||||||
|
if spec.builtin or true then
|
||||||
|
builtins_fetchTarball { inherit (spec) url sha256; }
|
||||||
|
else
|
||||||
|
pkgs.fetchzip { inherit (spec) url sha256; };
|
||||||
|
|
||||||
|
fetch_git = spec:
|
||||||
|
builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
|
||||||
|
|
||||||
|
fetch_builtin-tarball = spec:
|
||||||
|
builtins.trace
|
||||||
|
''
|
||||||
|
WARNING:
|
||||||
|
The niv type "builtin-tarball" will soon be deprecated. You should
|
||||||
|
instead use `builtin = true`.
|
||||||
|
|
||||||
|
$ niv modify <package> -a type=tarball -a builtin=true
|
||||||
|
''
|
||||||
|
builtins_fetchTarball { inherit (spec) url sha256; };
|
||||||
|
|
||||||
|
fetch_builtin-url = spec:
|
||||||
|
builtins.trace
|
||||||
|
''
|
||||||
|
WARNING:
|
||||||
|
The niv type "builtin-url" will soon be deprecated. You should
|
||||||
|
instead use `builtin = true`.
|
||||||
|
|
||||||
|
$ niv modify <package> -a type=file -a builtin=true
|
||||||
|
''
|
||||||
|
(builtins_fetchurl { inherit (spec) url sha256; });
|
||||||
|
|
||||||
|
#
|
||||||
|
# Various helpers
|
||||||
|
#
|
||||||
|
|
||||||
|
# The set of packages used when specs are fetched using non-builtins.
|
||||||
|
mkPkgs = sources:
|
||||||
|
let
|
||||||
|
sourcesNixpkgs =
|
||||||
|
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {};
|
||||||
|
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
|
||||||
|
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
|
||||||
|
in
|
||||||
|
if builtins.hasAttr "nixpkgs" sources
|
||||||
|
then sourcesNixpkgs
|
||||||
|
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
|
||||||
|
import <nixpkgs> {}
|
||||||
|
else
|
||||||
|
abort
|
||||||
|
''
|
||||||
|
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
|
||||||
|
add a package called "nixpkgs" to your sources.json.
|
||||||
|
'';
|
||||||
|
|
||||||
|
# The actual fetching function.
|
||||||
|
fetch = pkgs: name: spec:
|
||||||
|
|
||||||
|
if ! builtins.hasAttr "type" spec then
|
||||||
|
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
|
||||||
|
else if spec.type == "file" then fetch_file pkgs spec
|
||||||
|
else if spec.type == "tarball" then fetch_tarball pkgs spec
|
||||||
|
else if spec.type == "git" then fetch_git spec
|
||||||
|
else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec
|
||||||
|
else if spec.type == "builtin-url" then fetch_builtin-url spec
|
||||||
|
else
|
||||||
|
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
|
||||||
|
|
||||||
|
# Ports of functions for older nix versions
|
||||||
|
|
||||||
|
# a Nix version of mapAttrs if the built-in doesn't exist
|
||||||
|
mapAttrs = builtins.mapAttrs or (
|
||||||
|
f: set: with builtins;
|
||||||
|
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
|
||||||
|
);
|
||||||
|
|
||||||
|
# fetchTarball version that is compatible between all the versions of Nix
|
||||||
|
builtins_fetchTarball = { url, sha256 }@attrs:
|
||||||
|
let
|
||||||
|
inherit (builtins) lessThan nixVersion fetchTarball;
|
||||||
|
in
|
||||||
|
if lessThan nixVersion "1.12" then
|
||||||
|
fetchTarball { inherit url; }
|
||||||
|
else
|
||||||
|
fetchTarball attrs;
|
||||||
|
|
||||||
|
# fetchurl version that is compatible between all the versions of Nix
|
||||||
|
builtins_fetchurl = { url, sha256 }@attrs:
|
||||||
|
let
|
||||||
|
inherit (builtins) lessThan nixVersion fetchurl;
|
||||||
|
in
|
||||||
|
if lessThan nixVersion "1.12" then
|
||||||
|
fetchurl { inherit url; }
|
||||||
|
else
|
||||||
|
fetchurl attrs;
|
||||||
|
|
||||||
|
# Create the final "sources" from the config
|
||||||
|
mkSources = config:
|
||||||
|
mapAttrs (
|
||||||
|
name: spec:
|
||||||
|
if builtins.hasAttr "outPath" spec
|
||||||
|
then abort
|
||||||
|
"The values in sources.json should not have an 'outPath' attribute"
|
||||||
|
else
|
||||||
|
spec // { outPath = fetch config.pkgs name spec; }
|
||||||
|
) config.sources;
|
||||||
|
|
||||||
|
# The "config" used by the fetchers
|
||||||
|
mkConfig =
|
||||||
|
{ sourcesFile ? ./sources.json
|
||||||
|
, sources ? builtins.fromJSON (builtins.readFile sourcesFile)
|
||||||
|
, pkgs ? mkPkgs sources
|
||||||
|
}: rec {
|
||||||
|
# The sources, i.e. the attribute set of spec name to spec
|
||||||
|
inherit sources;
|
||||||
|
|
||||||
|
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
|
||||||
|
inherit pkgs;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }
|
31
nixops/single-server.nix
Normal file
31
nixops/single-server.nix
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
network.description = "mail server";
|
||||||
|
|
||||||
|
mailserver =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" "example2.com" ];
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraVirtualAliases = {
|
||||||
|
"info@example.com" = "user1@example.com";
|
||||||
|
"postmaster@example.com" = "user1@example.com";
|
||||||
|
"abuse@example.com" = "user1@example.com";
|
||||||
|
"user1@example2.com" = "user1@example.com";
|
||||||
|
"info@example2.com" = "user1@example.com";
|
||||||
|
"postmaster@example2.com" = "user1@example.com";
|
||||||
|
"abuse@example2.com" = "user1@example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
9
nixops/vbox.nix
Normal file
9
nixops/vbox.nix
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
mailserver =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{ deployment.targetEnv = "virtualbox";
|
||||||
|
deployment.virtualbox.memorySize = 1024; # megabytes
|
||||||
|
deployment.virtualbox.vcpu = 2; # number of cpus
|
||||||
|
deployment.virtualbox.headless = true;
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
[tool.ruff.lint]
|
|
||||||
extend-select = ["ISC"]
|
|
||||||
|
|
||||||
[tool.ruff.lint.flake8-implicit-str-concat]
|
|
||||||
allow-multiline = false
|
|
|
@ -1,109 +0,0 @@
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
from textwrap import indent
|
|
||||||
from typing import Any, Mapping
|
|
||||||
|
|
||||||
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 md_literal(value: str) -> str:
|
|
||||||
return f"`{value}`"
|
|
||||||
|
|
||||||
|
|
||||||
def md_codefence(value: str, language: str = "nix") -> str:
|
|
||||||
return indent(
|
|
||||||
f"\n```{language}\n{value}\n```",
|
|
||||||
prefix=2 * " ",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def render_option_value(option: Mapping[str, Any], key: str) -> str:
|
|
||||||
if key not in option:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
if isinstance(option[key], dict) and "_type" in option[key]:
|
|
||||||
if option[key]["_type"] == "literalExpression":
|
|
||||||
# multi-line codeblock
|
|
||||||
if "\n" in option[key]["text"]:
|
|
||||||
text = option[key]["text"].rstrip("\n")
|
|
||||||
value = md_codefence(text)
|
|
||||||
# inline codeblock
|
|
||||||
else:
|
|
||||||
value = md_literal(option[key]["text"])
|
|
||||||
# literal markdown
|
|
||||||
elif option[key]["_type"] == "literalMD":
|
|
||||||
value = option[key]["text"]
|
|
||||||
else:
|
|
||||||
assert RuntimeError(f"Unhandled option type {option[key]['_type']}")
|
|
||||||
else:
|
|
||||||
text = str(option[key])
|
|
||||||
if text == "":
|
|
||||||
value = md_literal('""')
|
|
||||||
elif "\n" in text:
|
|
||||||
value = md_codefence(text.rstrip("\n"))
|
|
||||||
else:
|
|
||||||
value = md_literal(text)
|
|
||||||
|
|
||||||
return f"- {key}: {value}" # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def print_option(option):
|
|
||||||
if (
|
|
||||||
isinstance(option["description"], dict) and "_type" in option["description"]
|
|
||||||
): # mdDoc
|
|
||||||
description = option["description"]["text"]
|
|
||||||
else:
|
|
||||||
description = option["description"]
|
|
||||||
print(
|
|
||||||
template.format(
|
|
||||||
key=option["name"],
|
|
||||||
description=description or "",
|
|
||||||
type=f"- type: {md_literal(option['type'])}",
|
|
||||||
default=render_option_value(option, "default"),
|
|
||||||
example=render_option_value(option, "example"),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
print(header)
|
|
||||||
for opt in options:
|
|
||||||
if any([opt["name"].startswith(c) for c in groups]):
|
|
||||||
continue
|
|
||||||
print_option(opt)
|
|
||||||
|
|
||||||
for c in groups:
|
|
||||||
print(f"## `{c}`\n")
|
|
||||||
for opt in options:
|
|
||||||
if opt["name"].startswith(c):
|
|
||||||
print_option(opt)
|
|
|
@ -1,267 +0,0 @@
|
||||||
import argparse
|
|
||||||
import email
|
|
||||||
import email.utils
|
|
||||||
import imaplib
|
|
||||||
import smtplib
|
|
||||||
import time
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
RETRY = 100
|
|
||||||
|
|
||||||
|
|
||||||
def _send_mail(
|
|
||||||
smtp_host,
|
|
||||||
smtp_port,
|
|
||||||
smtp_username,
|
|
||||||
from_addr,
|
|
||||||
from_pwd,
|
|
||||||
to_addr,
|
|
||||||
subject,
|
|
||||||
starttls,
|
|
||||||
ssl,
|
|
||||||
):
|
|
||||||
print(f"Sending mail with subject '{subject}'")
|
|
||||||
message = "\n".join(
|
|
||||||
[
|
|
||||||
f"From: {from_addr}",
|
|
||||||
f"To: {to_addr}",
|
|
||||||
f"Subject: {subject}",
|
|
||||||
f"Message-ID: {uuid.uuid4()}@mail-check.py",
|
|
||||||
f"Date: {email.utils.formatdate()}",
|
|
||||||
"",
|
|
||||||
"This validates our mail server can send to Gmail :/",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
retry = RETRY
|
|
||||||
smtp_class = smtplib.SMTP_SSL if ssl else smtplib.SMTP
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
with smtp_class(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(f"Reading mail from {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()
|
|
||||||
_, data = obj.search(None, f'(SINCE {dt}) (SUBJECT "{subject}")')
|
|
||||||
if data == [b""]:
|
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
uids = data[0].decode("utf-8").split(" ")
|
|
||||||
if len(uids) != 1:
|
|
||||||
print(
|
|
||||||
f"Warning: {len(uids)} messages have been found with subject containing {subject}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# FIXME: we only consider the first matching message...
|
|
||||||
uid = uids[0]
|
|
||||||
_, raw = obj.fetch(uid, "(RFC822)")
|
|
||||||
if delete:
|
|
||||||
obj.store(uid, "+FLAGS", "\\Deleted")
|
|
||||||
obj.expunge()
|
|
||||||
assert raw[0] and raw[0][1]
|
|
||||||
message = email.message_from_bytes(cast(bytes, raw[0][1]))
|
|
||||||
print(f"Message with subject '{message['subject']}' has been found")
|
|
||||||
if show_body:
|
|
||||||
if message.is_multipart():
|
|
||||||
for part in message.walk():
|
|
||||||
ctype = part.get_content_type()
|
|
||||||
if ctype == "text/plain":
|
|
||||||
body = cast(bytes, part.get_payload(decode=True)).decode()
|
|
||||||
print(f"Body:\n{body}")
|
|
||||||
else:
|
|
||||||
print(f"Body with content type {ctype} not printed")
|
|
||||||
else:
|
|
||||||
body = cast(bytes, message.get_payload(decode=True)).decode()
|
|
||||||
print(f"Body:\n{body}")
|
|
||||||
break
|
|
||||||
|
|
||||||
if message is None:
|
|
||||||
print(
|
|
||||||
f"Error: no message with subject '{subject}' has been found in INBOX of {imap_username}"
|
|
||||||
)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if ignore_dkim_spf:
|
|
||||||
return
|
|
||||||
|
|
||||||
# gmail set this standardized header
|
|
||||||
if "ARC-Authentication-Results" in message:
|
|
||||||
if "dkim=pass" in message["ARC-Authentication-Results"]:
|
|
||||||
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 = f"{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,
|
|
||||||
ssl=args.smtp_ssl,
|
|
||||||
)
|
|
||||||
|
|
||||||
_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,
|
|
||||||
imap_username=args.imap_username,
|
|
||||||
to_pwd=args.imap_password,
|
|
||||||
subject=args.subject,
|
|
||||||
ignore_dkim_spf=args.ignore_dkim_spf,
|
|
||||||
show_body=args.show_body,
|
|
||||||
delete=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
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-ssl", action="store_true")
|
|
||||||
parser_send_and_read.add_argument(
|
|
||||||
"--smtp-username",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help="username used for smtp login. If not specified, the from-addr value is used",
|
|
||||||
)
|
|
||||||
parser_send_and_read.add_argument("--from-addr", type=str)
|
|
||||||
parser_send_and_read.add_argument("--imap-host", required=True, type=str)
|
|
||||||
parser_send_and_read.add_argument("--imap-port", type=str, default=993)
|
|
||||||
parser_send_and_read.add_argument("--to-addr", type=str, required=True)
|
|
||||||
parser_send_and_read.add_argument(
|
|
||||||
"--imap-username",
|
|
||||||
type=str,
|
|
||||||
default="",
|
|
||||||
help="username used for imap login. If not specified, the to-addr value is used",
|
|
||||||
)
|
|
||||||
parser_send_and_read.add_argument("--src-password-file", type=argparse.FileType("r"))
|
|
||||||
parser_send_and_read.add_argument(
|
|
||||||
"--dst-password-file", required=True, type=argparse.FileType("r")
|
|
||||||
)
|
|
||||||
parser_send_and_read.add_argument(
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
action="store_true",
|
|
||||||
help="to ignore the dkim and spf verification on the read mail",
|
|
||||||
)
|
|
||||||
parser_send_and_read.set_defaults(func=send_and_read)
|
|
||||||
|
|
||||||
parser_read = subparsers.add_parser(
|
|
||||||
"read",
|
|
||||||
description="Search for an email with a subject containing 'subject' in the INBOX.",
|
|
||||||
)
|
|
||||||
parser_read.add_argument("--imap-host", type=str, default="localhost")
|
|
||||||
parser_read.add_argument("--imap-port", type=str, default=993)
|
|
||||||
parser_read.add_argument("--imap-username", required=True, type=str)
|
|
||||||
parser_read.add_argument("--imap-password", required=True, type=str)
|
|
||||||
parser_read.add_argument(
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
action="store_true",
|
|
||||||
help="to ignore the dkim and spf verification on the read mail",
|
|
||||||
)
|
|
||||||
parser_read.add_argument(
|
|
||||||
"--show-body", action="store_true", help="print mail text/plain payload"
|
|
||||||
)
|
|
||||||
parser_read.add_argument("subject", type=str)
|
|
||||||
parser_read.set_defaults(func=read)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
args.func(args)
|
|
20
shell.nix
20
shell.nix
|
@ -1,9 +1,11 @@
|
||||||
(import (
|
let
|
||||||
let
|
nixpkgs = (import ./nix/sources.nix).nixpkgs-unstable;
|
||||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
pkgs = import nixpkgs {};
|
||||||
in
|
in
|
||||||
fetchTarball {
|
pkgs.mkShell {
|
||||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
buildInputs = with pkgs; [
|
||||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
(python3.withPackages(p: [p.sphinx p.sphinx_rtd_theme]))
|
||||||
}
|
niv
|
||||||
) { src = ./.; }).shellNix
|
jq clamav
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
127
tests/clamav.nix
127
tests/clamav.nix
|
@ -14,18 +14,23 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{
|
{ pkgs ? import <nixpkgs> {}}:
|
||||||
lib,
|
|
||||||
blobs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
{
|
import (pkgs.path + "/nixos/tests/make-test.nix") {
|
||||||
name = "clamav";
|
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
server =
|
server = { config, pkgs, lib, ... }:
|
||||||
{ pkgs, ... }:
|
let
|
||||||
|
clamav-db-files = pkgs.stdenv.mkDerivation rec {
|
||||||
|
name = "clamav-db-files";
|
||||||
|
src = lib.cleanSource ./clamav;
|
||||||
|
dontUnpack = true;
|
||||||
|
installPhase = ''
|
||||||
|
mkdir $out
|
||||||
|
cp -R $src/*.cvd $out/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../default.nix
|
../default.nix
|
||||||
|
@ -34,8 +39,6 @@
|
||||||
|
|
||||||
virtualisation.memorySize = 1500;
|
virtualisation.memorySize = 1500;
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [ netcat ];
|
|
||||||
|
|
||||||
services.rsyslogd = {
|
services.rsyslogd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
defaultConfig = ''
|
defaultConfig = ''
|
||||||
|
@ -55,9 +58,9 @@
|
||||||
'';
|
'';
|
||||||
|
|
||||||
script = ''
|
script = ''
|
||||||
cp ${blobs}/clamav/main.cvd /var/lib/clamav/
|
cp ${clamav-db-files}/main.cvd /var/lib/clamav/
|
||||||
cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
|
cp ${clamav-db-files}/daily.cvd /var/lib/clamav/
|
||||||
cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
|
cp ${clamav-db-files}/bytecode.cvd /var/lib/clamav/
|
||||||
chown clamav:clamav /var/lib/clamav/*
|
chown clamav:clamav /var/lib/clamav/*
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
@ -70,11 +73,9 @@
|
||||||
|
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
debug = true;
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [
|
domains = [ "example.com" "example2.com" ];
|
||||||
"example.com"
|
|
||||||
"example2.com"
|
|
||||||
];
|
|
||||||
virusScanning = true;
|
virusScanning = true;
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
|
@ -94,28 +95,21 @@
|
||||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
client =
|
client = { nodes, config, pkgs, ... }: let
|
||||||
{ nodes, pkgs, ... }:
|
serverIP = nodes.server.config.networking.primaryIPAddress;
|
||||||
let
|
clientIP = nodes.client.config.networking.primaryIPAddress;
|
||||||
serverIP = nodes.server.networking.primaryIPAddress;
|
|
||||||
clientIP = nodes.client.networking.primaryIPAddress;
|
|
||||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
echo grep '${clientIP}' "$@" >&2
|
echo grep '${clientIP}' "$@" >&2
|
||||||
exec grep '${clientIP}' "$@"
|
exec grep '${clientIP}' "$@"
|
||||||
'';
|
'';
|
||||||
in
|
in {
|
||||||
{
|
|
||||||
imports = [
|
imports = [
|
||||||
./lib/config.nix
|
./lib/config.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
fetchmail
|
fetchmail msmtp procmail findutils grep-ip
|
||||||
msmtp
|
|
||||||
procmail
|
|
||||||
findutils
|
|
||||||
grep-ip
|
|
||||||
];
|
];
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
"root/.fetchmailrc" = {
|
"root/.fetchmailrc" = {
|
||||||
|
@ -198,57 +192,54 @@
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = ''
|
testScript = { nodes, ... }:
|
||||||
start_all()
|
''
|
||||||
|
startAll;
|
||||||
|
|
||||||
server.wait_for_unit("multi-user.target")
|
$server->waitForUnit("multi-user.target");
|
||||||
client.wait_for_unit("multi-user.target")
|
$client->waitForUnit("multi-user.target");
|
||||||
|
|
||||||
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
||||||
server.wait_until_succeeds(
|
$server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ \$? -eq 124 ]");
|
||||||
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
$server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ \$? -eq 124 ]");
|
||||||
)
|
|
||||||
server.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
|
|
||||||
client.execute("cp -p /etc/root/.* ~/")
|
$client->execute("cp -p /etc/root/.* ~/");
|
||||||
client.succeed("mkdir -p ~/mail")
|
$client->succeed("mkdir -p ~/mail");
|
||||||
client.succeed("ls -la ~/ >&2")
|
$client->succeed("ls -la ~/ >&2");
|
||||||
client.succeed("cat ~/.fetchmailrc >&2")
|
$client->succeed("cat ~/.fetchmailrc >&2");
|
||||||
client.succeed("cat ~/.procmailrc >&2")
|
$client->succeed("cat ~/.procmailrc >&2");
|
||||||
client.succeed("cat ~/.msmtprc >&2")
|
$client->succeed("cat ~/.msmtprc >&2");
|
||||||
|
|
||||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||||
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
|
$client->succeed("fetchmail --nosslcertck -v || [ \$? -eq 1 ] >&2");
|
||||||
|
|
||||||
# Verify that mail can be sent and received before testing virus scanner
|
# Verify that mail can be sent and received before testing virus scanner
|
||||||
client.execute("rm ~/mail/*")
|
$client->execute("rm ~/mail/*");
|
||||||
client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
|
$client->succeed("msmtp -a user2 user1\@example.com < /etc/root/safe-email >&2");
|
||||||
# give the mail server some time to process the mail
|
# give the mail server some time to process the mail
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
client.execute("rm ~/mail/*")
|
$client->execute("rm ~/mail/*");
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
client.succeed("fetchmail --nosslcertck -v >&2")
|
$client->succeed("fetchmail --nosslcertck -v >&2");
|
||||||
client.execute("rm ~/mail/*")
|
$client->execute("rm ~/mail/*");
|
||||||
|
|
||||||
with subtest("virus scan file"):
|
subtest "virus scan file", sub {
|
||||||
server.succeed(
|
$server->succeed("clamdscan \$(readlink -f /etc/root/eicar.com.txt) | grep \"Txt\\.Malware\\.Agent-1787597 FOUND\" >&2");
|
||||||
'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
|
};
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("virus scan email"):
|
subtest "virus scan email", sub {
|
||||||
client.succeed(
|
$client->succeed("msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep \"server message: 554 5\\.7\\.1\" >&2");
|
||||||
'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");
|
||||||
)
|
|
||||||
server.succeed("journalctl -u rspamd | grep -i eicar")
|
|
||||||
# give the mail server some time to process the mail
|
# give the mail server some time to process the mail
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "no warnings or errors", sub {
|
||||||
|
$server->fail("journalctl -u postfix | grep -i error >&2");
|
||||||
|
$server->fail("journalctl -u postfix | grep -i warning >&2");
|
||||||
|
$server->fail("journalctl -u dovecot2 | grep -i error >&2");
|
||||||
|
$server->fail("journalctl -u dovecot2 | grep -i warning >&2");
|
||||||
|
};
|
||||||
|
|
||||||
with subtest("no warnings or errors"):
|
|
||||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
|
||||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
|
||||||
server.fail("journalctl -u dovecot2 | grep -i error >&2")
|
|
||||||
server.fail("journalctl -u dovecot2 | grep -i warning >&2")
|
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
1
tests/clamav/.gitattributes
vendored
Normal file
1
tests/clamav/.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*cvd filter=lfs diff=lfs merge=lfs -text
|
1
tests/clamav/.gitignore
vendored
Normal file
1
tests/clamav/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
mirrors.dat
|
BIN
tests/clamav/bytecode.cvd
Normal file
BIN
tests/clamav/bytecode.cvd
Normal file
Binary file not shown.
BIN
tests/clamav/daily.cvd
Normal file
BIN
tests/clamav/daily.cvd
Normal file
Binary file not shown.
1
tests/clamav/freshclam.conf
Normal file
1
tests/clamav/freshclam.conf
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DatabaseMirror database.clamav.net
|
5
tests/clamav/hashes.json
Normal file
5
tests/clamav/hashes.json
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"bytecode.cvd": "633d4f0a2054249e23df12db5a9e76bcaac23cadaef5ee8f644986f600d8d81e",
|
||||||
|
"daily.cvd": "0b6798b54e490be168b873d39ebda41ff4a027720aed855f879779b88982838f",
|
||||||
|
"main.cvd": "9694933f37148ec39c1f2ef7b97211ded9b03b140bb48a5eeb27270120844b24"
|
||||||
|
}
|
BIN
tests/clamav/main.cvd
Normal file
BIN
tests/clamav/main.cvd
Normal file
Binary file not shown.
15
tests/clamav/update-clamav-database.sh
Executable file
15
tests/clamav/update-clamav-database.sh
Executable file
|
@ -0,0 +1,15 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "${0}")"
|
||||||
|
|
||||||
|
rm ./*.cvd hashes.json || :
|
||||||
|
|
||||||
|
freshclam --datadir=. --config-file=freshclam.conf
|
||||||
|
(for i in ./*.cvd;
|
||||||
|
do echo '{}' |
|
||||||
|
jq --arg path "$(basename "${i}")" \
|
||||||
|
--arg sha256sum "$(sha256sum "${i}" | awk '{ print $1; }')" \
|
||||||
|
'.[$path] = $sha256sum'; done) |
|
||||||
|
jq -s add > hashes.json
|
46
tests/default.nix
Normal file
46
tests/default.nix
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
# Generate an attribute sets containing all tests for all releaeses
|
||||||
|
# It looks like:
|
||||||
|
# - extern.nixpkgs_20.03
|
||||||
|
# - extern.nixpkgs_unstable
|
||||||
|
# - intern.nixpkgs_20.03
|
||||||
|
# - intern.nixpkgs_unstable
|
||||||
|
|
||||||
|
with builtins;
|
||||||
|
|
||||||
|
let
|
||||||
|
sources = import ../nix/sources.nix;
|
||||||
|
|
||||||
|
releases = listToAttrs (map genRelease releaseNames);
|
||||||
|
|
||||||
|
genRelease = name: {
|
||||||
|
name = name;
|
||||||
|
value = import sources."${name}" {};
|
||||||
|
};
|
||||||
|
|
||||||
|
genTest = testName: release:
|
||||||
|
let
|
||||||
|
pkgs = releases."${release}";
|
||||||
|
test = pkgs.callPackage (./. + "/${testName}.nix") { };
|
||||||
|
in {
|
||||||
|
"name"= builtins.replaceStrings ["." "-"] ["_" "_"] release;
|
||||||
|
"value"= test { inherit pkgs; };
|
||||||
|
};
|
||||||
|
|
||||||
|
releaseNames = [
|
||||||
|
"nixpkgs-20.03"
|
||||||
|
"nixpkgs-unstable"
|
||||||
|
];
|
||||||
|
|
||||||
|
testNames = [
|
||||||
|
"intern"
|
||||||
|
"extern"
|
||||||
|
"clamav"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Generate an attribute set containing one test per releases
|
||||||
|
genTests = testName: {
|
||||||
|
name = testName;
|
||||||
|
value = listToAttrs (map (genTest testName) (builtins.attrNames releases));
|
||||||
|
};
|
||||||
|
|
||||||
|
in listToAttrs (map genTests testNames)
|
418
tests/extern.nix
Normal file
418
tests/extern.nix
Normal file
|
@ -0,0 +1,418 @@
|
||||||
|
# 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> {}}:
|
||||||
|
|
||||||
|
import (pkgs.path + "/nixos/tests/make-test.nix") {
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
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()
|
||||||
|
'';
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
./lib/config.nix
|
||||||
|
];
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham
|
||||||
|
];
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript = { nodes, ... }:
|
||||||
|
''
|
||||||
|
startAll;
|
||||||
|
|
||||||
|
$server->waitForUnit("multi-user.target");
|
||||||
|
$client->waitForUnit("multi-user.target");
|
||||||
|
|
||||||
|
# TODO put this blocking into the systemd units?
|
||||||
|
$server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ \$? -eq 124 ]");
|
||||||
|
|
||||||
|
$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");
|
||||||
|
|
||||||
|
subtest "imap retrieving mail", sub {
|
||||||
|
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||||
|
$client->succeed("fetchmail --nosslcertck -v || [ \$? -eq 1 ] >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "submission port send mail", sub {
|
||||||
|
# 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->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "imap retrieving mail 2", sub {
|
||||||
|
$client->execute("rm ~/mail/*");
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail --nosslcertck -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 ~/mail/*");
|
||||||
|
$client->succeed("check-mail-id ~/mail/*");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "have correct fqdn as sender", sub {
|
||||||
|
$client->succeed("grep 'Received: from mail.example.com' ~/mail/*");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "dkim has user-specified size", sub {
|
||||||
|
$server->succeed("openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "dkim singing, multiple domains", sub {
|
||||||
|
$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->waitUntilFails('[ "$(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/*");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "aliases", sub {
|
||||||
|
$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->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail --nosslcertck -v");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "catchAlls", sub {
|
||||||
|
$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->waitUntilFails('[ "$(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->waitUntilFails('[ "$(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");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "extraVirtualAliases", sub {
|
||||||
|
$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->waitUntilFails('[ "$(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->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail --nosslcertck -v");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "quota", sub {
|
||||||
|
$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->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->fail("fetchmail --nosslcertck -v");
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "imap sieve junk trainer", sub {
|
||||||
|
# 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->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||||
|
|
||||||
|
$client->succeed("imap-mark-spam >&2");
|
||||||
|
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2");
|
||||||
|
$client->succeed("imap-mark-ham >&2");
|
||||||
|
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "no warnings or errors", sub {
|
||||||
|
$server->fail("journalctl -u postfix | grep -i error >&2");
|
||||||
|
$server->fail("journalctl -u postfix | grep -i warning >&2");
|
||||||
|
$server->fail("journalctl -u dovecot2 | grep -i error >&2");
|
||||||
|
$server->fail("journalctl -u dovecot2 | grep -i warning >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,528 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{
|
|
||||||
name = "external";
|
|
||||||
|
|
||||||
nodes = {
|
|
||||||
server =
|
|
||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [ netcat ];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
|
|
||||||
services.rsyslogd = {
|
|
||||||
enable = true;
|
|
||||||
defaultConfig = ''
|
|
||||||
*.* /dev/console
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
debug.dovecot = true; # enabled for sieve script logging
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [
|
|
||||||
"example.com"
|
|
||||||
"example2.com"
|
|
||||||
];
|
|
||||||
rewriteMessageId = true;
|
|
||||||
dkimKeyBits = 1535;
|
|
||||||
dmarcReporting.enable = true;
|
|
||||||
|
|
||||||
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";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
client =
|
|
||||||
{ nodes, pkgs, ... }:
|
|
||||||
let
|
|
||||||
serverIP = nodes.server.networking.primaryIPAddress;
|
|
||||||
clientIP = nodes.client.networking.primaryIPAddress;
|
|
||||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
echo grep '${clientIP}' "$@" >&2
|
|
||||||
exec grep '${clientIP}' "$@"
|
|
||||||
'';
|
|
||||||
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 = ''
|
|
||||||
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 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-Signature: ~/mail/*")
|
|
||||||
|
|
||||||
with subtest("aliases"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from chuck to postmaster
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2"
|
|
||||||
)
|
|
||||||
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 dovecot -u dovecot2 | grep -i rspamd-learn-spam.sh >&2")
|
|
||||||
client.succeed("imap-mark-ham >&2")
|
|
||||||
server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-ham.sh >&2")
|
|
||||||
|
|
||||||
with subtest("full text search and indexation"):
|
|
||||||
# send 2 email from user2 to user1
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2"
|
|
||||||
)
|
|
||||||
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 dovecot -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
|
|
||||||
# check that Junk is not indexed
|
|
||||||
server.fail("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&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 dovecot -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2")
|
|
||||||
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
|
|
||||||
server.fail(
|
|
||||||
"journalctl -u dovecot -u dovecot2 | \
|
|
||||||
grep -v 'Expunged message reappeared, giving a new UID' | \
|
|
||||||
grep -v 'Time moved forwards' | \
|
|
||||||
grep -i warning >&2"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
}
|
|
89
tests/intern.nix
Normal file
89
tests/intern.nix
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
# nixos-mailserver: a simple mail server
|
||||||
|
# Copyright (C) 2016-2018 Robin Raymond
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
{ pkgs ? import <nixpkgs> {}}:
|
||||||
|
|
||||||
|
let
|
||||||
|
sendMail = pkgs.writeTextFile {
|
||||||
|
"name" = "send-mail-to-send-only-account";
|
||||||
|
"text" = ''
|
||||||
|
EHLO mail.example.com
|
||||||
|
MAIL FROM: none@example.com
|
||||||
|
RCPT TO: send-only@example.com
|
||||||
|
QUIT
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
hashPassword = password: pkgs.runCommand
|
||||||
|
"password-${password}-hashed"
|
||||||
|
{ buildInputs = [ pkgs.mkpasswd ]; } ''
|
||||||
|
mkpasswd -m sha-512 ${password} > $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
import (pkgs.path + "/nixos/tests/make-test.nix") {
|
||||||
|
|
||||||
|
machine =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
./lib/config.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
virtualisation.memorySize = 1024;
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
};
|
||||||
|
"send-only@example.com" = {
|
||||||
|
hashedPasswordFile = hashPassword "send-only";
|
||||||
|
sendOnly = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
vmailGroupName = "vmail";
|
||||||
|
vmailUID = 5000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript =
|
||||||
|
''
|
||||||
|
$machine->start;
|
||||||
|
$machine->waitForUnit("multi-user.target");
|
||||||
|
|
||||||
|
subtest "vmail gid is set correctly", sub {
|
||||||
|
$machine->succeed("getent group vmail | grep 5000");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "mail to send only accounts is rejected", sub {
|
||||||
|
$machine->waitForOpenPort(25);
|
||||||
|
# TODO put this blocking into the systemd units?
|
||||||
|
$machine->waitUntilSucceeds("timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ \$? -eq 124 ]");
|
||||||
|
$machine->succeed("cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'" );
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "rspamd controller serves web ui", sub {
|
||||||
|
$machine->succeed("${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'" );
|
||||||
|
};
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,227 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
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
|
|
||||||
{
|
|
||||||
name = "internal";
|
|
||||||
|
|
||||||
nodes = {
|
|
||||||
machine =
|
|
||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
|
||||||
'')
|
|
||||||
]
|
|
||||||
++ (with pkgs; [
|
|
||||||
curl
|
|
||||||
openssl
|
|
||||||
netcat
|
|
||||||
]);
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [
|
|
||||||
"example.com"
|
|
||||||
"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;
|
|
||||||
indexDir = "/var/lib/dovecot/indices";
|
|
||||||
|
|
||||||
enableImap = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
testScript =
|
|
||||||
{
|
|
||||||
nodes,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
''
|
|
||||||
machine.start()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
|
|
||||||
with subtest("mail forwarded can are locally kept"):
|
|
||||||
# A mail sent to user2@example.com via explicit TLS 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 via implicit TLS is in the user2@example.com mailbox
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 465",
|
|
||||||
"--smtp-ssl",
|
|
||||||
"--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 via explicit TLS is in the user2@example.com mailbox
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username user2@example.com",
|
|
||||||
"--from-addr user1@example.com",
|
|
||||||
"--to-addr user2-regex-alias@domain.com",
|
|
||||||
"--src-password-file ${passwordFile}",
|
|
||||||
"--dst-password-file ${passwordFile}",
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("user can send from regex email alias"):
|
|
||||||
# A mail sent to user1@example.com from user2-regex-alias@domain.com by
|
|
||||||
# user2@example.com via implicit TLS is in the user1@example.com mailbox
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 465",
|
|
||||||
"--smtp-ssl",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--smtp-username user2@example.com",
|
|
||||||
"--from-addr user2-regex-alias@domain.com",
|
|
||||||
"--to-addr user1@example.com",
|
|
||||||
"--src-password-file ${passwordFile}",
|
|
||||||
"--dst-password-file ${passwordFile}",
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("vmail gid is set correctly"):
|
|
||||||
machine.succeed("getent group vmail | grep 5000")
|
|
||||||
|
|
||||||
with subtest("Check dovecot maildir and index locations"):
|
|
||||||
# If these paths change we need a migration
|
|
||||||
machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.mailDirectory}/example.com/user1")
|
|
||||||
machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'")
|
|
||||||
|
|
||||||
with subtest("mail to send only accounts is rejected"):
|
|
||||||
machine.wait_for_open_port(25)
|
|
||||||
# TODO put this blocking into the systemd units
|
|
||||||
machine.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
machine.succeed(
|
|
||||||
"cat ${sendMail} | nc localhost 25 | grep -q '554 5.5.0 Error'"
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("rspamd controller serves web ui"):
|
|
||||||
machine.succeed(
|
|
||||||
"set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("imap port 143 is closed and imaps is serving SSL"):
|
|
||||||
machine.wait_for_closed_port(143)
|
|
||||||
machine.wait_for_open_port(993)
|
|
||||||
machine.succeed(
|
|
||||||
"echo | openssl s_client -connect localhost:993 | grep 'New, TLS'"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
}
|
|
231
tests/ldap.nix
231
tests/ldap.nix
|
@ -1,231 +0,0 @@
|
||||||
let
|
|
||||||
bindPassword = "unsafegibberish";
|
|
||||||
alicePassword = "testalice";
|
|
||||||
bobPassword = "testbob";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
name = "ldap";
|
|
||||||
|
|
||||||
nodes = {
|
|
||||||
machine =
|
|
||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
|
|
||||||
services.openssh = {
|
|
||||||
enable = true;
|
|
||||||
settings.PermitRootLogin = "yes";
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.etc.bind-password.text = bindPassword;
|
|
||||||
|
|
||||||
services.openldap = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
children = {
|
|
||||||
"cn=schema".includes = [
|
|
||||||
"${pkgs.openldap}/etc/schema/core.ldif"
|
|
||||||
"${pkgs.openldap}/etc/schema/cosine.ldif"
|
|
||||||
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
|
|
||||||
"${pkgs.openldap}/etc/schema/nis.ldif"
|
|
||||||
];
|
|
||||||
"olcDatabase={1}mdb" = {
|
|
||||||
attrs = {
|
|
||||||
objectClass = [
|
|
||||||
"olcDatabaseConfig"
|
|
||||||
"olcMdbConfig"
|
|
||||||
];
|
|
||||||
olcDatabase = "{1}mdb";
|
|
||||||
olcDbDirectory = "/var/lib/openldap/example";
|
|
||||||
olcSuffix = "dc=example";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
declarativeContents."dc=example" = ''
|
|
||||||
dn: dc=example
|
|
||||||
objectClass: domain
|
|
||||||
dc: example
|
|
||||||
|
|
||||||
dn: cn=mail,dc=example
|
|
||||||
objectClass: organizationalRole
|
|
||||||
objectClass: simpleSecurityObject
|
|
||||||
objectClass: top
|
|
||||||
cn: mail
|
|
||||||
userPassword: ${bindPassword}
|
|
||||||
|
|
||||||
dn: ou=users,dc=example
|
|
||||||
objectClass: organizationalUnit
|
|
||||||
ou: users
|
|
||||||
|
|
||||||
dn: cn=alice,ou=users,dc=example
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
cn: alice
|
|
||||||
sn: Foo
|
|
||||||
mail: alice@example.com
|
|
||||||
userPassword: ${alicePassword}
|
|
||||||
|
|
||||||
dn: cn=bob,ou=users,dc=example
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
cn: bob
|
|
||||||
sn: Bar
|
|
||||||
mail: bob@example.com
|
|
||||||
userPassword: ${bobPassword}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" ];
|
|
||||||
localDnsResolver = false;
|
|
||||||
indexDir = "/var/lib/dovecot/indices";
|
|
||||||
|
|
||||||
ldap = {
|
|
||||||
enable = true;
|
|
||||||
uris = [
|
|
||||||
"ldap://"
|
|
||||||
];
|
|
||||||
bind = {
|
|
||||||
dn = "cn=mail,dc=example";
|
|
||||||
passwordFile = "/etc/bind-password";
|
|
||||||
};
|
|
||||||
searchBase = "ou=users,dc=example";
|
|
||||||
searchScope = "sub";
|
|
||||||
};
|
|
||||||
|
|
||||||
forwards = {
|
|
||||||
"bob_fw@example.com" = "bob@example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
vmailGroupName = "vmail";
|
|
||||||
vmailUID = 5000;
|
|
||||||
|
|
||||||
enableImap = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
testScript =
|
|
||||||
{
|
|
||||||
nodes,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
''
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
machine.start()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# This function retrieves the ldap table file from a postconf
|
|
||||||
# command.
|
|
||||||
# A key lookup is achived and the returned value is compared
|
|
||||||
# to the expected value.
|
|
||||||
def test_lookup(postconf_cmdline, key, expected):
|
|
||||||
conf = machine.succeed(postconf_cmdline).rstrip()
|
|
||||||
ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1)
|
|
||||||
value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip()
|
|
||||||
try:
|
|
||||||
assert value == expected
|
|
||||||
except AssertionError:
|
|
||||||
print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
with subtest("Test postmap lookups"):
|
|
||||||
test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com")
|
|
||||||
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com")
|
|
||||||
|
|
||||||
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com")
|
|
||||||
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com")
|
|
||||||
|
|
||||||
with subtest("Test doveadm lookups"):
|
|
||||||
machine.succeed("doveadm user -u alice@example.com")
|
|
||||||
machine.succeed("doveadm user -u bob@example.com")
|
|
||||||
|
|
||||||
with subtest("Files containing secrets are only readable by root"):
|
|
||||||
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
|
|
||||||
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
|
|
||||||
|
|
||||||
with subtest("Test account/mail address binding via explicit TLS"):
|
|
||||||
machine.fail(" ".join([
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--smtp-username alice@example.com",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username bob@example.com",
|
|
||||||
"--from-addr bob@example.com",
|
|
||||||
"--to-addr aliceb@example.com",
|
|
||||||
"--src-password-file <(echo '${alicePassword}')",
|
|
||||||
"--dst-password-file <(echo '${bobPassword}')",
|
|
||||||
"--ignore-dkim-spf"
|
|
||||||
]))
|
|
||||||
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
|
||||||
|
|
||||||
with subtest("Test mail delivery via implicit TLS"):
|
|
||||||
machine.succeed(" ".join([
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 465",
|
|
||||||
"--smtp-ssl",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--smtp-username alice@example.com",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username bob@example.com",
|
|
||||||
"--from-addr alice@example.com",
|
|
||||||
"--to-addr bob@example.com",
|
|
||||||
"--src-password-file <(echo '${alicePassword}')",
|
|
||||||
"--dst-password-file <(echo '${bobPassword}')",
|
|
||||||
"--ignore-dkim-spf"
|
|
||||||
]))
|
|
||||||
|
|
||||||
with subtest("Test mail forwarding via explicit TLS works"):
|
|
||||||
machine.succeed(" ".join([
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--smtp-username alice@example.com",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username bob@example.com",
|
|
||||||
"--from-addr alice@example.com",
|
|
||||||
"--to-addr bob_fw@example.com",
|
|
||||||
"--src-password-file <(echo '${alicePassword}')",
|
|
||||||
"--dst-password-file <(echo '${bobPassword}')",
|
|
||||||
"--ignore-dkim-spf"
|
|
||||||
]))
|
|
||||||
|
|
||||||
with subtest("Test cannot send mail via implicit TLS from forwarded address"):
|
|
||||||
machine.fail(" ".join([
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 465",
|
|
||||||
"--smtp-ssl",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--smtp-username bob@example.com",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username alice@example.com",
|
|
||||||
"--from-addr bob_fw@example.com",
|
|
||||||
"--to-addr alice@example.com",
|
|
||||||
"--src-password-file <(echo '${bobPassword}')",
|
|
||||||
"--dst-password-file <(echo '${alicePassword}')",
|
|
||||||
"--ignore-dkim-spf"
|
|
||||||
]))
|
|
||||||
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'")
|
|
||||||
|
|
||||||
with subtest("Check dovecot mail and index locations"):
|
|
||||||
# If these paths change we need a migration
|
|
||||||
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/bob@example.com")
|
|
||||||
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/bob@example.com'")
|
|
||||||
'';
|
|
||||||
}
|
|
|
@ -1,25 +1,3 @@
|
||||||
{
|
{
|
||||||
lib,
|
security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
{
|
|
||||||
# Testing eval failures that result from stateVersion assertion is out of scope
|
|
||||||
mailserver.stateVersion = 999;
|
|
||||||
|
|
||||||
# Enable second CPU core
|
|
||||||
virtualisation.cores = lib.mkDefault 2;
|
|
||||||
|
|
||||||
services.rspamd = {
|
|
||||||
# Don't make tests block on DNS requests that will never succeed
|
|
||||||
locals."options.inc".text = ''
|
|
||||||
dns {
|
|
||||||
nameservers = ["127.0.0.1"];
|
|
||||||
timeout = 0.0s;
|
|
||||||
retransmits = 0;
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
# Relax `local_addrs` definition to default for tests, so mail doesn't get flagged as spam
|
|
||||||
overrides."options.inc".enable = false;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
31
tests/minimal.nix
Normal file
31
tests/minimal.nix
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# nixos-mailserver: a simple mail server
|
||||||
|
# Copyright (C) 2016-2018 Robin Raymond
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
import <nixpkgs/nixos/tests/make-test.nix> {
|
||||||
|
|
||||||
|
machine =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript =
|
||||||
|
''
|
||||||
|
$machine->waitForUnit("multi-user.target");
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,113 +0,0 @@
|
||||||
# This tests is used to test features requiring several mail domains.
|
|
||||||
|
|
||||||
{
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}:
|
|
||||||
|
|
||||||
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:
|
|
||||||
{ pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
environment.systemPackages = with pkgs; [ netcat ];
|
|
||||||
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;
|
|
||||||
settings.mx-host = [
|
|
||||||
"domain1.com,domain1,10"
|
|
||||||
"domain2.com,domain2,10"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
|
|
||||||
{
|
|
||||||
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 =
|
|
||||||
{ 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 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
domain2.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
|
|
||||||
# user@domain1.com sends a mail to user@domain2.com via explicit TLS
|
|
||||||
client.succeed(
|
|
||||||
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send a mail to the address forwarded via implicit TLS and check it is in the recipient mailbox
|
|
||||||
client.succeed(
|
|
||||||
"mail-check send-and-read --smtp-port 465 --smtp-ssl --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
}
|
|
7
update.sh
Executable file
7
update.sh
Executable file
|
@ -0,0 +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
|
Loading…
Add table
Add a link
Reference in a new issue