Compare commits
No commits in common. "master" and "rename-tests" have entirely different histories.
master
...
rename-tes
65 changed files with 2055 additions and 3730 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
|
||||
.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:
|
||||
extends: .hydra-cli
|
||||
only:
|
||||
- merge_requests
|
||||
variables:
|
||||
jobset: $CI_MERGE_REQUEST_IID
|
||||
image: nixos/nix
|
||||
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}
|
||||
|
||||
hydra-master:
|
||||
extends: .hydra-cli
|
||||
only:
|
||||
- master
|
||||
variables:
|
||||
jobset: master
|
||||
image: nixos/nix
|
||||
script:
|
||||
- nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master
|
||||
|
|
|
@ -1,55 +1,98 @@
|
|||
{ nixpkgs, pulls, ... }:
|
||||
{ nixpkgs, declInput, pulls }:
|
||||
|
||||
let
|
||||
pkgs = import nixpkgs { };
|
||||
pkgs = import nixpkgs {};
|
||||
|
||||
prs = builtins.fromJSON (builtins.readFile pulls);
|
||||
prJobsets = pkgs.lib.mapAttrs (num: info: {
|
||||
enabled = 1;
|
||||
hidden = false;
|
||||
description = "PR ${num}: ${info.title}";
|
||||
checkinterval = 300;
|
||||
schedulingshares = 20;
|
||||
enableemail = false;
|
||||
emailoverride = "";
|
||||
keepnr = 1;
|
||||
type = 1;
|
||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
|
||||
}) prs;
|
||||
mkFlakeJobset = branch: {
|
||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
||||
checkinterval = 300;
|
||||
enabled = "1";
|
||||
schedulingshares = 100;
|
||||
enableemail = false;
|
||||
emailoverride = "";
|
||||
keepnr = 3;
|
||||
hidden = false;
|
||||
type = 1;
|
||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}";
|
||||
};
|
||||
prJobsets = pkgs.lib.mapAttrs (num: info:
|
||||
{ enabled = 1;
|
||||
hidden = false;
|
||||
description = "PR ${num}: ${info.title}";
|
||||
nixexprinput = "snm";
|
||||
nixexprpath = ".hydra/default.nix";
|
||||
checkinterval = 30;
|
||||
schedulingshares = 20;
|
||||
enableemail = false;
|
||||
emailoverride = "";
|
||||
keepnr = 1;
|
||||
type = 0;
|
||||
inputs = {
|
||||
snm = {
|
||||
type = "git";
|
||||
value = "${info.target_repo_url} merge-requests/${info.iid}/head";
|
||||
emailresponsible = false;
|
||||
};
|
||||
};
|
||||
}
|
||||
) prs;
|
||||
|
||||
desc = prJobsets // {
|
||||
"master" = mkFlakeJobset "master";
|
||||
"nixos-24.11" = mkFlakeJobset "nixos-24.11";
|
||||
"nixos-25.05" = mkFlakeJobset "nixos-25.05";
|
||||
master = {
|
||||
description = "Build master 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 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-20.09" = {
|
||||
description = "Build the nixos-20.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-20.09";
|
||||
type = "git";
|
||||
emailresponsible = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
log = {
|
||||
pulls = prs;
|
||||
jobsets = desc;
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
|
||||
in {
|
||||
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
|
||||
cat >$out <<EOF
|
||||
${builtins.toJSON desc}
|
||||
EOF
|
||||
# This is to get nice .jobsets build logs on Hydra
|
||||
cat >tmp <<EOF
|
||||
${builtins.toJSON log}
|
||||
EOF
|
||||
${pkgs.jq}/bin/jq . tmp
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -1,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
|
198
README.md
198
README.md
|
@ -1,106 +1,162 @@
|
|||
# ![Simple Nixos MailServer][logo]
|
||||
|
||||

|
||||
[](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
|
||||
|
||||
|
||||
## Release branches
|
||||
|
||||
For each NixOS release, we publish a branch. You then have to use the
|
||||
SNM branch corresponding to your NixOS version.
|
||||
|
||||
* For NixOS 25.05
|
||||
* Use the [SNM branch `nixos-25.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.05)
|
||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/)
|
||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05)
|
||||
* For NixOS 24.11
|
||||
* Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11)
|
||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/)
|
||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11)
|
||||
* For NixOS 20.09
|
||||
- Use the [SNM branch `nixos-20.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.09)
|
||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/)
|
||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/release-notes.html#nixos-20-09)
|
||||
* For NixOS 20.03
|
||||
- Use the [SNM branch `nixos-20.03`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.03)
|
||||
- [Release notes](#nixos-2003)
|
||||
* For NixOS unstable
|
||||
* Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
|
||||
- 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 supporting the NixOS release 20.09 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
|
||||
|
||||
* [x] Continous Integration Testing
|
||||
* [x] Multiple Domains
|
||||
* Postfix
|
||||
* [x] SMTP on port 25
|
||||
* [x] Submission TLS on port 465
|
||||
* [x] Submission StartTLS on port 587
|
||||
* [x] LMTP with Dovecot
|
||||
* Dovecot
|
||||
* [x] Maildir folders
|
||||
* [x] IMAP with TLS on port 993
|
||||
* [x] POP3 with TLS on port 995
|
||||
* [x] IMAP with StartTLS on port 143
|
||||
* [x] POP3 with StartTLS on port 110
|
||||
* Certificates
|
||||
* [x] ACME
|
||||
* [x] Custom certificates
|
||||
* Spam Filtering
|
||||
* [x] Via Rspamd
|
||||
* Virus Scanning
|
||||
* [x] Via ClamAV
|
||||
* DKIM Signing
|
||||
* [x] Via Rspamd
|
||||
* User Management
|
||||
* [x] Declarative user management
|
||||
* [x] Declarative password management
|
||||
* [x] LDAP users
|
||||
* Sieve
|
||||
* [x] Allow user defined sieve scripts
|
||||
* [x] Moving mails from/to junk trains the Bayes filter
|
||||
* [x] ManageSieve support
|
||||
* User Aliases
|
||||
* [x] Regular aliases
|
||||
* [x] Catch all aliases
|
||||
### v2.0
|
||||
* [x] Continous Integration Testing
|
||||
* [x] Multiple Domains
|
||||
* Postfix MTA
|
||||
- [x] smtp on port 25
|
||||
- [x] submission tls on port 465
|
||||
- [x] submission starttls on port 587
|
||||
- [x] lmtp with dovecot
|
||||
* Dovecot
|
||||
- [x] maildir folders
|
||||
- [x] imap with tls on port 993
|
||||
- [x] pop3 with tls on port 995
|
||||
- [x] imap with starttls on port 143
|
||||
- [x] pop3 with starttls on port 110
|
||||
* Certificates
|
||||
- [x] manual certificates
|
||||
- [x] on the fly creation
|
||||
- [x] Let's Encrypt
|
||||
* Spam Filtering
|
||||
- [x] via rspamd
|
||||
* Virus Scanning
|
||||
- [x] via clamav
|
||||
* DKIM Signing
|
||||
- [x] via opendkim
|
||||
* User Management
|
||||
- [x] declarative user management
|
||||
- [x] declarative password management
|
||||
* Sieves
|
||||
- [x] A simple standard script that moves spam
|
||||
- [x] Allow user defined sieve scripts
|
||||
- [x] ManageSieve support
|
||||
* User Aliases
|
||||
- [x] Regular aliases
|
||||
- [x] Catch all aliases
|
||||
|
||||
### In the future
|
||||
|
||||
* Automatic client configuration
|
||||
* [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)
|
||||
* [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019)
|
||||
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
|
||||
* DKIM Signing
|
||||
* [ ] Allow per domain selectors
|
||||
* [ ] Allow passing DKIM signing keys
|
||||
* Improve the Forwarding Experience
|
||||
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html)
|
||||
* [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd)
|
||||
* User management
|
||||
* [ ] Allow local and LDAP user to coexist
|
||||
* OpenID Connect
|
||||
* Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166)
|
||||
* DKIM Signing
|
||||
- [ ] Allow a per domain selector
|
||||
|
||||
### 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)
|
||||
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
||||
- Join the Freenode IRC channel `#nixos-mailserver`
|
||||
|
||||
### Quick Start
|
||||
|
||||
```nix
|
||||
{ config, pkgs, ... }:
|
||||
let release = "nixos-20.09";
|
||||
in {
|
||||
imports = [
|
||||
(builtins.fetchTarball {
|
||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz";
|
||||
# This hash needs to be updated
|
||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||
})
|
||||
];
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" "example2.com" ];
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 > /hashed/password/file/location
|
||||
hashedPasswordFile = "/hashed/password/file/location";
|
||||
|
||||
aliases = [
|
||||
"info@example.com"
|
||||
"postmaster@example.com"
|
||||
"postmaster@example2.com"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For a complete list of options, see `default.nix`.
|
||||
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
|
||||
|
||||
### Alternative Implementations
|
||||
|
||||
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
||||
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
||||
|
||||
### Credits
|
||||
|
||||
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
|
||||
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
|
||||
from [TheNounProject](https://thenounproject.com/) is licensed under
|
||||
[CC BY 3.0](http://creativecommons.org/~/3.0/)
|
||||
* Logo made with [Logomakr.com](https://logomakr.com)
|
||||
* Logo made with [Logomakr.com](https://logomakr.com)
|
||||
|
||||
[logo]: docs/logo.png
|
||||
|
||||
|
||||
|
||||
[logo]: logo/logo.png
|
||||
|
|
1015
default.nix
1015
default.nix
File diff suppressed because it is too large
Load diff
|
@ -4,8 +4,8 @@ Add Radicale
|
|||
Configuration by @dotlambda
|
||||
|
||||
Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional
|
||||
crypt passwords are no longer supported. Instead bcrypt passwords
|
||||
have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
||||
crypt passwords, as generated by `mkpasswd`, are no longer supported. Instead
|
||||
bcrypt passwords have to be used which can be generated using `htpasswd`.
|
||||
|
||||
.. code:: nix
|
||||
|
||||
|
@ -24,13 +24,12 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
|||
in {
|
||||
services.radicale = {
|
||||
enable = true;
|
||||
settings = {
|
||||
auth = {
|
||||
type = "htpasswd";
|
||||
htpasswd_filename = "${htpasswd}";
|
||||
htpasswd_encryption = "bcrypt";
|
||||
};
|
||||
};
|
||||
config = ''
|
||||
[auth]
|
||||
type = htpasswd
|
||||
htpasswd_filename = ${htpasswd}
|
||||
htpasswd_encryption = bcrypt
|
||||
'';
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
|
|
|
@ -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.
|
||||
|
|
@ -14,13 +14,6 @@ forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
|
|||
back (or whatever you specified as ``vmailUserName``, and
|
||||
``vmailGoupName``).
|
||||
|
||||
If you enabled ``enableManageSieve`` then you also may want to backup
|
||||
``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
|
||||
The same considerations regarding file ownership apply as for the
|
||||
Maildir.
|
||||
|
||||
To backup spam and ham training data, backup ``/var/lib/redis-rspamd``.
|
||||
|
||||
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
||||
you specified as ``dkimKeyDirectory``). If you should lose those don’t
|
||||
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 = "NixOS Mailserver"
|
||||
copyright = "2022, NixOS Mailserver Contributors"
|
||||
author = "NixOS Mailserver Contributors"
|
||||
project = 'NixOS Mailserver'
|
||||
copyright = '2020, NixOS Mailserver Contributors'
|
||||
author = 'NixOS Mailserver Contributors'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
@ -27,33 +27,27 @@ author = "NixOS Mailserver Contributors"
|
|||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["myst_parser"]
|
||||
|
||||
myst_enable_extensions = [
|
||||
"colon_fence",
|
||||
"linkify",
|
||||
extensions = [
|
||||
]
|
||||
|
||||
smartquotes = False
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
master_doc = "index"
|
||||
master_doc = 'index'
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = []
|
||||
html_static_path = ['_static']
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Nix Flakes
|
||||
==========
|
||||
|
||||
If you're using `flakes <https://wiki.nixos.org/wiki/Flakes>`__, you can use
|
||||
If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
|
||||
the following minimal ``flake.nix`` as an example:
|
||||
|
||||
.. code:: nix
|
||||
|
|
10
docs/fts.rst
10
docs/fts.rst
|
@ -4,7 +4,7 @@ Full text search
|
|||
By default, when your IMAP client searches for an email containing some
|
||||
text in its *body*, dovecot will read all your email sequentially. This
|
||||
is very slow and IO intensive. To speed body searches up, it is possible to
|
||||
*index* emails with a plugin to dovecot, ``fts_flatcurve``.
|
||||
*index* emails with a plugin to dovecot, ``fts_xapian``.
|
||||
|
||||
Enabling full text search
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -20,6 +20,8 @@ To enable indexing for full text search here is an example configuration.
|
|||
enable = true;
|
||||
# index new email as they arrive
|
||||
autoIndex = true;
|
||||
# this only applies to plain text attachments, binary attachments are never indexed
|
||||
indexAttachments = true;
|
||||
enforced = "body";
|
||||
};
|
||||
};
|
||||
|
@ -40,7 +42,7 @@ 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
|
||||
(``/var/lib/docecot/indices/%d/%n``) by using the option
|
||||
``mailserver.indexDir``.
|
||||
|
||||
.. warning::
|
||||
|
@ -59,8 +61,8 @@ Mitigating resources requirements
|
|||
|
||||
You can:
|
||||
|
||||
* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes``
|
||||
* disable expensive token normalisation in ``mailserver.fullTextSearch.filters``
|
||||
* disable indexation of attachements ``mailserver.fullTextSearch.indexAttachments = false``
|
||||
* reduce the size of ngrams to be indexed ``mailserver.fullTextSearch.minSize`` and ``maxSize``
|
||||
* disable automatic indexation for some folders with
|
||||
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by
|
||||
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard.
|
||||
|
|
|
@ -4,104 +4,60 @@ Contribute or troubleshoot
|
|||
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.
|
||||
You can also chat with us on the Freenode IRC channel ``#nixos-mailserver``.
|
||||
|
||||
Run NixOS tests
|
||||
---------------
|
||||
|
||||
To run the test suite, you need to enable `Nix Flakes
|
||||
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
||||
|
||||
You can then run the testsuite via
|
||||
You can run the testsuite via
|
||||
|
||||
::
|
||||
|
||||
$ nix flake check -L
|
||||
|
||||
Since Nix doesn't garantee your machine have enough resources to run
|
||||
all test VMs in parallel, some tests can fail. You would then haev to
|
||||
run tests manually. For instance:
|
||||
|
||||
::
|
||||
|
||||
$ nix build .#hydraJobs.x86_64-linux.external-unstable -L
|
||||
|
||||
$ nix-build tests -A external.nixpkgs_20_03
|
||||
$ nix-build tests -A internal.nixpkgs_unstable
|
||||
...
|
||||
|
||||
Contributing to the documentation
|
||||
---------------------------------
|
||||
|
||||
The documentation is written in RST (except option documentation which is in CommonMark),
|
||||
built with Sphinx and published by `Read the Docs <https://readthedocs.org/>`_.
|
||||
The documentation is written in RST, build with Sphinx and published
|
||||
by `Read the Docs <https://readthedocs.org/>`_.
|
||||
|
||||
For the syntax, see the `RST/Sphinx primer
|
||||
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
|
||||
|
||||
To build the documentation, you need to enable `Nix Flakes
|
||||
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
||||
For the syntax, see `RST/Sphinx Cheatsheet
|
||||
<https://sphinx-tutorial.readthedocs.io/cheatsheet/>`_.
|
||||
|
||||
The ``shell.nix`` provides all the tooling required to build the
|
||||
documentation:
|
||||
|
||||
::
|
||||
|
||||
$ nix build .#documentation
|
||||
$ xdg-open result/index.html
|
||||
$ nix-shell
|
||||
$ 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
|
||||
being asserted on in `mail-server/assertions.nix`. Then pick the next number
|
||||
and add a new assertion, write a good summary describing the issue and what
|
||||
remediation steps are necessary. Finally reference the URL to the specific
|
||||
section on the migration page in the documentation.
|
||||
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
|
||||
$ nixops deploy -d mail
|
||||
$ nixops info -d mail
|
||||
|
||||
.. code-block:: nix
|
||||
You can then test the server via e.g. \ ``telnet``. To log into it, use
|
||||
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = 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
|
||||
don't require any migration steps for new setups.
|
||||
Imap
|
||||
----
|
||||
|
||||
The migration documentation should paint a more complete picture about the steps
|
||||
that need to be carried out and why this has become necessary. Make sure to
|
||||
reference the correct anchor in the URL you put into the assertion message.
|
||||
To test imap manually use
|
||||
|
||||
::
|
||||
|
||||
$ openssl s_client -host mail.example.com -port 143 -starttls imap
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
Welcome to NixOS Mailserver's documentation!
|
||||
============================================
|
||||
|
||||
.. image:: logo.png
|
||||
.. image:: ../logo/logo.png
|
||||
:width: 400
|
||||
:alt: SNM Logo
|
||||
|
||||
|
@ -14,12 +14,9 @@ Welcome to NixOS Mailserver's documentation!
|
|||
:maxdepth: 2
|
||||
|
||||
setup-guide
|
||||
advanced-configurations
|
||||
howto-develop
|
||||
faq
|
||||
release-notes
|
||||
options
|
||||
migrations
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
@ -27,12 +24,9 @@ Welcome to NixOS Mailserver's documentation!
|
|||
|
||||
backup-guide
|
||||
add-radicale
|
||||
add-roundcube
|
||||
rspamd-tuning
|
||||
fts
|
||||
flakes
|
||||
autodiscovery
|
||||
ldap
|
||||
|
||||
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,45 +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
|
||||
-----------
|
||||
|
||||
#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;
|
||||
|
|
@ -1,99 +1,6 @@
|
|||
Release Notes
|
||||
=============
|
||||
|
||||
NixOS 25.05
|
||||
-----------
|
||||
|
||||
- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed`` canoncalizaliaton.
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/374>`__)
|
||||
- Rspamd now connects to Redis over its Unix Domain Socket by default
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/375>`__)
|
||||
|
||||
- If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``.
|
||||
- The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring.
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/380>`__)
|
||||
- Switch to the more efficient `fts-flatcurve` indexer for full text search
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/361>`__).
|
||||
|
||||
This makes use of a new index, which will be automatically re-generated the
|
||||
next time a folder is searched.
|
||||
The operation is now quick enough to be performed "just-in-time".
|
||||
Alternatively, all indices can be immediately re-generated for all users and
|
||||
folders by running
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
doveadm fts rescan -u '*' && doveadm index -u '*' -q '*'
|
||||
|
||||
The previous index (which is not automatically discarded to allow rollbacks)
|
||||
can be cleaned up by removing all the `xapian-indexes` directories within
|
||||
``mailserver.indexDir``.
|
||||
- Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``.
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/297>`__)
|
||||
- Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP.
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/313>`__)
|
||||
- Support for TLS 1.1 was disabled in accordance with `Mozilla's recommendations <https://ssl-config.mozilla.org/#server=postfix>`_.
|
||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/234>`__)
|
||||
|
||||
NixOS 24.11
|
||||
-----------
|
||||
|
||||
- No new feature, only bug fixes and documentation improvements
|
||||
|
||||
NixOS 24.05
|
||||
-----------
|
||||
|
||||
- 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
|
||||
-----------
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
sphinx ~= 5.3
|
||||
sphinx_rtd_theme ~= 1.1
|
||||
myst-parser ~= 0.18
|
||||
linkify-it-py ~= 2.0
|
||||
standard-imghdr
|
|
@ -24,14 +24,17 @@ You can run the training in a root shell as follows:
|
|||
|
||||
.. code:: bash
|
||||
|
||||
# Path to the controller socket
|
||||
export RSOCK="/var/run/rspamd/worker-controller.sock"
|
||||
|
||||
# Learn the Junk folder as spam
|
||||
rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
||||
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
||||
|
||||
# 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
|
||||
rspamc stat | grep learned
|
||||
rspamc -h $RSOCK stat | grep learned
|
||||
|
||||
Tune symbol weight
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -20,30 +20,25 @@ 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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Setup DNS A record for server
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Add DNS records to the domain ``example.com`` with the following
|
||||
Add a DNS record to the domain ``example.com`` with the following
|
||||
entries
|
||||
|
||||
==================== ===== ==== =============
|
||||
Name (Subdomain) TTL Type Value
|
||||
==================== ===== ==== =============
|
||||
``mail.example.com`` 10800 A ``1.2.3.4``
|
||||
``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
|
||||
$ ping mail.example.com
|
||||
64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms
|
||||
...
|
||||
|
||||
Note that it can take a while until a DNS entry is propagated. This
|
||||
DNS entry is required for the Let's Encrypt certificate generation
|
||||
|
@ -53,45 +48,41 @@ Setup the server
|
|||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The following describes a server setup that is fairly complete. Even
|
||||
though there are more possible options (see the `NixOS Mailserver
|
||||
options documentation <options.html>`_), these should be the most
|
||||
common ones.
|
||||
though there are more possible options (see the ``default.nix`` file),
|
||||
these should be the most common ones.
|
||||
|
||||
.. code:: nix
|
||||
|
||||
{ config, pkgs, ... }: {
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
(builtins.fetchTarball {
|
||||
# Pick a release version you are interested in and set its hash, e.g.
|
||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.05/nixos-mailserver-nixos-25.05.tar.gz";
|
||||
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
|
||||
# release="nixos-25.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
|
||||
# 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;
|
||||
stateVersion = 2;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" ];
|
||||
|
||||
# A list of all login accounts. To create the password hashes, use
|
||||
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
||||
# nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
hashedPasswordFile = "/a/file/containing/a/hashed/password";
|
||||
aliases = ["postmaster@example.com"];
|
||||
};
|
||||
"user2@example.com" = { ... };
|
||||
"user1@example.com" = {
|
||||
hashedPasswordFile = "/a/file/containing/a/hashed/password";
|
||||
aliases = ["postmaster@example.com"];
|
||||
};
|
||||
"user2@example.com" = { ... };
|
||||
};
|
||||
|
||||
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
|
||||
# down nginx and opens port 80.
|
||||
certificateScheme = "acme-nginx";
|
||||
certificateScheme = 3;
|
||||
};
|
||||
security.acme.acceptTerms = true;
|
||||
security.acme.defaults.email = "security@example.com";
|
||||
}
|
||||
|
||||
After a ``nixos-rebuild switch`` your server should be running all
|
||||
|
@ -104,18 +95,8 @@ Set rDNS (reverse DNS) entry for server
|
|||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Wherever you have rented your server, you should be able to set reverse
|
||||
DNS entries for the IP’s you own:
|
||||
|
||||
- Add an entry resolving IPv4 address ``1.2.3.4`` to ``mail.example.com``.
|
||||
- Add an entry resolving IPv6 ``2001::1`` to ``mail.example.com``. Again, this
|
||||
must be skipped if your server does not have an IPv6 address.
|
||||
|
||||
.. warning::
|
||||
|
||||
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.
|
||||
DNS entries for the IP’s you own. Add an entry resolving ``1.2.3.4``
|
||||
to ``mail.example.com``
|
||||
|
||||
You can check this with
|
||||
|
||||
|
@ -124,9 +105,6 @@ You can check this with
|
|||
$ nix-shell -p bind --command "host 1.2.3.4"
|
||||
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
|
||||
|
||||
$ nix-shell -p bind --command "host 2001::1"
|
||||
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.2.ip6.arpa domain name pointer mail.example.com.
|
||||
|
||||
Note that it can take a while until a DNS entry is propagated.
|
||||
|
||||
Set a ``MX`` record
|
||||
|
@ -153,7 +131,7 @@ Note that it can take a while until a DNS entry is propagated.
|
|||
Set a ``SPF`` record
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Add a `SPF <https://en.wikipedia.org/wiki/Sender_Policy_Framework>`_
|
||||
Add a `SPF <https://fr.wikipedia.org/wiki/Sender_Policy_Framework>`_
|
||||
record to the domain ``example.com``.
|
||||
|
||||
================ ===== ==== ================================
|
||||
|
@ -174,26 +152,25 @@ Note that it can take a while until a DNS entry is propagated.
|
|||
Set ``DKIM`` signature
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
On your server, the ``rspamd`` systemd service generated a file
|
||||
On your server, the ``opendkim`` systemd service generated a file
|
||||
containing your DKIM public key in the file
|
||||
``/var/dkim/example.com.mail.txt``. The content of this file looks
|
||||
like
|
||||
|
||||
::
|
||||
|
||||
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
|
||||
"p=<really-long-key>" ) ; ----- DKIM key mail for nixos.org
|
||||
mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=<really-long-key>" ; ----- DKIM mail for domain.tld
|
||||
|
||||
where ``really-long-key`` is your public key.
|
||||
|
||||
Based on the content of this file, we can add a ``DKIM`` record to the
|
||||
domain ``example.com``.
|
||||
|
||||
=========================== ===== ==== ================================================
|
||||
=========================== ===== ==== ==============================
|
||||
Name (Subdomain) TTL Type Value
|
||||
=========================== ===== ==== ================================================
|
||||
mail._domainkey.example.com 10800 TXT ``v=DKIM1; k=rsa; s=email; p=<really-long-key>``
|
||||
=========================== ===== ==== ================================================
|
||||
=========================== ===== ==== ==============================
|
||||
mail._domainkey.example.com 10800 TXT ``v=DKIM1; p=<really-long-key>``
|
||||
=========================== ===== ==== ==============================
|
||||
|
||||
You can check this with
|
||||
|
||||
|
@ -238,8 +215,3 @@ Besides that, you can send an email to
|
|||
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
||||
your setup, but if you followed the steps closely then everything should
|
||||
be awesome!
|
||||
|
||||
Next steps (optional)
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Take a look through our `Advanced Configurations <advanced-configurations.html>`_.
|
||||
|
|
124
flake.lock
generated
124
flake.lock
generated
|
@ -1,121 +1,39 @@
|
|||
{
|
||||
"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": 1749636823,
|
||||
"narHash": "sha256-WUaIlOlPLyPgz9be7fqWJA5iG6rHcGRtLERSCfUDne4=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "623c56286de5a3193aa38891a6991b28f9bab056",
|
||||
"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": 1749285348,
|
||||
"narHash": "sha256-frdhQvPbmDYaScPFiCnfdh3B/Vh81Uuoo0w5TkWmmjU=",
|
||||
"lastModified": 1607522989,
|
||||
"narHash": "sha256-o/jWhOSAlaK7y2M57OIriRt6whuVVocS/T0mG7fd1TI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "3e3afe5174c561dee0df6f2c2b2236990146329f",
|
||||
"rev": "e9158eca70ae59e73fae23be5d13d3fa0cfc78b4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"id": "nixpkgs",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-25_05": {
|
||||
"locked": {
|
||||
"lastModified": 1749727998,
|
||||
"narHash": "sha256-mHv/yeUbmL91/TvV95p+mBVahm9mdQMJoqaTVTALaFw=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fd487183437963a59ba763c0cc4f27e3447dd6dd",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"blobs": "blobs",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-25_05": "nixpkgs-25_05"
|
||||
"utils": "utils"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1605370193,
|
||||
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
232
flake.nix
232
flake.nix
|
@ -2,223 +2,25 @@
|
|||
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;
|
||||
};
|
||||
utils.url = "github:numtide/flake-utils";
|
||||
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
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};
|
||||
}
|
||||
outputs = { self, utils, nixpkgs }: {
|
||||
nixosModules.mailserver = import ./.;
|
||||
nixosModule = self.nixosModules.mailserver;
|
||||
} // utils.lib.eachDefaultSystem (system: let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShell = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
(python3.withPackages (p: with p; [
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
]))
|
||||
jq
|
||||
clamav
|
||||
];
|
||||
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"
|
||||
];
|
||||
dmarcReporting = {
|
||||
organizationName = "Example Corp";
|
||||
domain = "example.com";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
options = builtins.toFile "options.json" (
|
||||
builtins.toJSON (
|
||||
lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") (
|
||||
lib.optionAttrSetToDocList eval.options
|
||||
)
|
||||
)
|
||||
);
|
||||
in
|
||||
pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } ''
|
||||
echo "Generating options.md from ${options}"
|
||||
python ${./scripts/generate-options.py} ${options} > $out
|
||||
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,48 +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 >= 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.
|
||||
'';
|
||||
}
|
||||
]
|
||||
++ 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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver.borgbackup;
|
||||
|
||||
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
||||
autoFragment =
|
||||
if cfg.compression.auto && cfg.compression.method == null then
|
||||
throw "compression.method must be set when using auto."
|
||||
else
|
||||
lib.optional cfg.compression.auto "auto";
|
||||
if cfg.compression.auto && cfg.compression.method == null
|
||||
then throw "compression.method must be set when using auto."
|
||||
else lib.optional cfg.compression.auto "auto";
|
||||
levelFragment =
|
||||
if cfg.compression.level != null && cfg.compression.method == null then
|
||||
throw "compression.method must be set when using compression.level."
|
||||
else
|
||||
lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
||||
compressionFragment = lib.concatStringsSep "," (
|
||||
lib.flatten [
|
||||
autoFragment
|
||||
methodFragment
|
||||
levelFragment
|
||||
]
|
||||
);
|
||||
if cfg.compression.level != null && cfg.compression.method == null
|
||||
then throw "compression.method must be set when using compression.level."
|
||||
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
||||
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
|
||||
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
||||
|
||||
encryptionFragment = cfg.encryption.method;
|
||||
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none") (
|
||||
if cfg.encryption.passphraseFile != null then
|
||||
''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
||||
else
|
||||
throw "passphraseFile must be set when using encryption."
|
||||
);
|
||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
|
||||
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
||||
else throw "passphraseFile must be set when using encryption.");
|
||||
|
||||
locations = lib.escapeShellArgs cfg.locations;
|
||||
name = lib.escapeShellArg cfg.name;
|
||||
|
@ -71,8 +55,7 @@ let
|
|||
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
||||
${cmdPostexec}
|
||||
'';
|
||||
in
|
||||
{
|
||||
in {
|
||||
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
||||
environment.systemPackages = with pkgs; [
|
||||
borgbackup
|
||||
|
|
|
@ -14,17 +14,29 @@
|
|||
# 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, ... }:
|
||||
{ config, pkgs, lib, options, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
clamHasSettings = options.services.clamav.daemon ? settings;
|
||||
in
|
||||
with lib;
|
||||
{
|
||||
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
|
||||
|
||||
# Remove extraConfig and settings conditional after 20.09 support is removed
|
||||
|
||||
services.clamav.daemon = {
|
||||
enable = true;
|
||||
} // (if clamHasSettings then {
|
||||
settings.PhishingScanURLs = "no";
|
||||
};
|
||||
} else {
|
||||
extraConfig = ''
|
||||
PhishingScanURLs no
|
||||
'';
|
||||
});
|
||||
|
||||
services.clamav.updater.enable = true;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,76 +14,35 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
}:
|
||||
{ config, pkgs, lib }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
# cert :: PATH
|
||||
certificatePath =
|
||||
if cfg.certificateScheme == "manual" then
|
||||
cfg.certificateFile
|
||||
else if cfg.certificateScheme == "selfsigned" then
|
||||
"${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
||||
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
||||
else
|
||||
throw "unknown certificate scheme";
|
||||
certificatePath = if cfg.certificateScheme == 1
|
||||
then cfg.certificateFile
|
||||
else if cfg.certificateScheme == 2
|
||||
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == 3
|
||||
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
|
||||
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
||||
|
||||
# key :: PATH
|
||||
keyPath =
|
||||
if cfg.certificateScheme == "manual" then
|
||||
cfg.keyFile
|
||||
else if cfg.certificateScheme == "selfsigned" then
|
||||
"${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
||||
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
|
||||
else
|
||||
throw "unknown certificate scheme";
|
||||
|
||||
passwordFiles =
|
||||
let
|
||||
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
|
||||
in
|
||||
lib.mapAttrs (
|
||||
name: value:
|
||||
if value.hashedPasswordFile == null then
|
||||
builtins.toString (mkHashFile name value.hashedPassword)
|
||||
else
|
||||
value.hashedPasswordFile
|
||||
) cfg.loginAccounts;
|
||||
|
||||
# Appends the LDAP bind password to files to avoid writing this
|
||||
# password into the Nix store.
|
||||
appendLdapBindPwd =
|
||||
{
|
||||
name,
|
||||
file,
|
||||
prefix,
|
||||
suffix ? "",
|
||||
passwordFile,
|
||||
destination,
|
||||
}:
|
||||
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
set -euo pipefail
|
||||
|
||||
baseDir=$(dirname ${destination})
|
||||
if (! test -d "$baseDir"); then
|
||||
mkdir -p $baseDir
|
||||
chmod 755 $baseDir
|
||||
fi
|
||||
|
||||
cat ${file} > ${destination}
|
||||
echo -n '${prefix}' >> ${destination}
|
||||
cat ${passwordFile} | tr -d '\n' >> ${destination}
|
||||
echo -n '${suffix}' >> ${destination}
|
||||
chmod 600 ${destination}
|
||||
'';
|
||||
keyPath = if cfg.certificateScheme == 1
|
||||
then cfg.keyFile
|
||||
else if cfg.certificateScheme == 2
|
||||
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == 3
|
||||
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
|
||||
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
||||
|
||||
passwordFiles = let
|
||||
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
|
||||
in
|
||||
lib.mapAttrs (name: value:
|
||||
if value.hashedPasswordFile == null then
|
||||
builtins.toString (mkHashFile name value.hashedPassword)
|
||||
else value.hashedPasswordFile) cfg.loginAccounts;
|
||||
}
|
||||
|
|
4
mail-server/debug.nix
Normal file
4
mail-server/debug.nix
Normal file
|
@ -0,0 +1,4 @@
|
|||
{ config, lib, ... }:
|
||||
{
|
||||
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
|
||||
}
|
|
@ -14,12 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with (import ./common.nix { inherit config pkgs lib; });
|
||||
|
||||
|
@ -28,65 +23,40 @@ let
|
|||
|
||||
passwdDir = "/run/dovecot2";
|
||||
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
|
||||
);
|
||||
|
||||
bool2int = x: if x then "1" else "0";
|
||||
|
||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
||||
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
|
||||
|
||||
# maildir in format "/${domain}/${user}"
|
||||
dovecotMaildir =
|
||||
"maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
||||
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
|
||||
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
|
||||
+ (lib.optionalString (cfg.indexDir != null)
|
||||
":INDEX=${cfg.indexDir}/%d/%n"
|
||||
);
|
||||
|
||||
postfixCfg = config.services.postfix;
|
||||
dovecot2Cfg = config.services.dovecot2;
|
||||
|
||||
ldapConfig = pkgs.writeTextFile {
|
||||
name = "dovecot-ldap.conf.ext.template";
|
||||
text = ''
|
||||
ldap_version = 3
|
||||
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||
${lib.optionalString cfg.ldap.startTls ''
|
||||
tls = yes
|
||||
''}
|
||||
tls_require_cert = hard
|
||||
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
||||
dn = ${cfg.ldap.bind.dn}
|
||||
sasl_bind = no
|
||||
auth_bind = yes
|
||||
base = ${cfg.ldap.searchBase}
|
||||
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
||||
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
|
||||
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
||||
''}
|
||||
user_filter = ${cfg.ldap.dovecot.userFilter}
|
||||
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
||||
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
||||
''}
|
||||
pass_filter = ${cfg.ldap.dovecot.passFilter}
|
||||
stateDir = "/var/lib/dovecot";
|
||||
|
||||
pipeBin = pkgs.stdenv.mkDerivation {
|
||||
name = "pipe_bin";
|
||||
src = ./dovecot/pipe_bin;
|
||||
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
|
||||
buildCommand = ''
|
||||
mkdir -p $out/pipe/bin
|
||||
cp $src/* $out/pipe/bin/
|
||||
chmod a+x $out/pipe/bin/*
|
||||
patchShebangs $out/pipe/bin
|
||||
|
||||
for file in $out/pipe/bin/*; do
|
||||
wrapProgram $file \
|
||||
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
|
||||
done
|
||||
'';
|
||||
};
|
||||
|
||||
setPwdInLdapConfFile = appendLdapBindPwd {
|
||||
name = "ldap-conf-file";
|
||||
file = ldapConfig;
|
||||
prefix = ''dnpass = "'';
|
||||
suffix = ''"'';
|
||||
passwordFile = cfg.ldap.bind.passwordFile;
|
||||
destination = ldapConfFile;
|
||||
};
|
||||
|
||||
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
|
||||
|
@ -97,12 +67,7 @@ let
|
|||
chmod 755 "${passwdDir}"
|
||||
fi
|
||||
|
||||
# Prevent world-readable password files, even temporarily.
|
||||
umask 077
|
||||
|
||||
for f in ${
|
||||
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)
|
||||
}; do
|
||||
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "Expected password hash file $f does not exist!"
|
||||
exit 1
|
||||
|
@ -110,159 +75,51 @@ let
|
|||
done
|
||||
|
||||
cat <<EOF > ${passwdFile}
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
||||
) cfg.loginAccounts
|
||||
)}
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
||||
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
|
||||
+ (if lib.isString value.quota
|
||||
then "userdb_quota_rule=*:storage=${value.quota}"
|
||||
else "")
|
||||
) cfg.loginAccounts)}
|
||||
EOF
|
||||
|
||||
cat <<EOF > ${userdbFile}
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
"${name}:::::::"
|
||||
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
|
||||
) cfg.loginAccounts
|
||||
)}
|
||||
EOF
|
||||
chmod 600 ${passwdFile}
|
||||
'';
|
||||
|
||||
junkMailboxes = builtins.attrNames (
|
||||
lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes
|
||||
);
|
||||
junkMailboxNumber = builtins.length junkMailboxes;
|
||||
# The assertion garantees there is exactly one Junk mailbox.
|
||||
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
||||
|
||||
mkLdapSearchScope =
|
||||
scope:
|
||||
(
|
||||
if scope == "sub" then
|
||||
"subtree"
|
||||
else if scope == "one" then
|
||||
"onelevel"
|
||||
else
|
||||
scope
|
||||
);
|
||||
|
||||
ftsPluginSettings = {
|
||||
fts = "flatcurve";
|
||||
fts_languages = listToLine cfg.fullTextSearch.languages;
|
||||
fts_tokenizers = listToLine [
|
||||
"generic"
|
||||
"email-address"
|
||||
];
|
||||
fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian
|
||||
fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch;
|
||||
fts_filters = listToLine cfg.fullTextSearch.filters;
|
||||
fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes;
|
||||
fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex;
|
||||
fts_enforced = cfg.fullTextSearch.enforced;
|
||||
} // (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude);
|
||||
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.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";
|
||||
|
||||
config = with cfg; lib.mkIf enable {
|
||||
services.dovecot2 = {
|
||||
enable = true;
|
||||
enableImap = cfg.enableImap || cfg.enableImapSsl;
|
||||
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
|
||||
enableImap = enableImap || enableImapSsl;
|
||||
enablePop3 = enablePop3 || enablePop3Ssl;
|
||||
enablePAM = false;
|
||||
enableQuota = true;
|
||||
mailGroup = cfg.vmailGroupName;
|
||||
mailUser = cfg.vmailUserName;
|
||||
mailGroup = vmailGroupName;
|
||||
mailUser = vmailUserName;
|
||||
mailLocation = dovecotMaildir;
|
||||
sslServerCert = certificatePath;
|
||||
sslServerKey = keyPath;
|
||||
enableDHE = lib.mkDefault false;
|
||||
enableLmtp = true;
|
||||
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
|
||||
"fts"
|
||||
"fts_flatcurve"
|
||||
];
|
||||
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
|
||||
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
|
||||
protocols = lib.optional cfg.enableManageSieve "sieve";
|
||||
|
||||
pluginSettings = {
|
||||
sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve";
|
||||
sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve";
|
||||
sieve_default_name = "default";
|
||||
} // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
|
||||
|
||||
sieve = {
|
||||
extensions = [
|
||||
"fileinto"
|
||||
];
|
||||
|
||||
scripts.after = builtins.toFile "spam.sieve" ''
|
||||
sieveScripts = {
|
||||
after = builtins.toFile "spam.sieve" ''
|
||||
require "fileinto";
|
||||
|
||||
if header :is "X-Spam" "Yes" {
|
||||
fileinto "${junkMailboxName}";
|
||||
fileinto "Junk";
|
||||
stop;
|
||||
}
|
||||
'';
|
||||
|
||||
pipeBins = map lib.getExe [
|
||||
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
|
||||
(pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
|
||||
];
|
||||
};
|
||||
|
||||
imapsieve.mailbox = [
|
||||
{
|
||||
name = junkMailboxName;
|
||||
causes = [
|
||||
"COPY"
|
||||
"APPEND"
|
||||
];
|
||||
before = ./dovecot/imap_sieve/report-spam.sieve;
|
||||
}
|
||||
{
|
||||
name = "*";
|
||||
from = junkMailboxName;
|
||||
causes = [ "COPY" ];
|
||||
before = ./dovecot/imap_sieve/report-ham.sieve;
|
||||
}
|
||||
];
|
||||
|
||||
mailboxes = cfg.mailboxes;
|
||||
|
||||
extraConfig = ''
|
||||
#Extra Config
|
||||
${lib.optionalString cfg.debug ''
|
||||
${lib.optionalString debug ''
|
||||
mail_debug = yes
|
||||
auth_debug = yes
|
||||
verbose_ssl = yes
|
||||
|
@ -271,62 +128,42 @@ in
|
|||
${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
|
||||
''
|
||||
}
|
||||
${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
|
||||
''
|
||||
}
|
||||
${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
|
||||
''
|
||||
}
|
||||
${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
|
||||
''
|
||||
}
|
||||
${if cfg.enablePop3Ssl then ''
|
||||
port = 995
|
||||
ssl = yes
|
||||
'' else ''
|
||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||
port = 0
|
||||
''}
|
||||
}
|
||||
}
|
||||
''}
|
||||
|
@ -336,21 +173,14 @@ in
|
|||
mail_plugins = $mail_plugins imap_sieve
|
||||
}
|
||||
|
||||
service imap {
|
||||
vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB
|
||||
}
|
||||
|
||||
protocol pop3 {
|
||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
||||
}
|
||||
|
||||
mail_access_groups = ${cfg.vmailGroupName}
|
||||
|
||||
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
|
||||
mail_access_groups = ${vmailGroupName}
|
||||
ssl = required
|
||||
ssl_min_protocol = TLSv1.2
|
||||
ssl_prefer_server_ciphers = no
|
||||
ssl_curve_list = X25519:prime256v1:secp384r1
|
||||
ssl_prefer_server_ciphers = yes
|
||||
|
||||
service lmtp {
|
||||
unix_listener dovecot-lmtp {
|
||||
|
@ -358,17 +188,6 @@ in
|
|||
mode = 0600
|
||||
user = ${postfixCfg.user}
|
||||
}
|
||||
vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB
|
||||
}
|
||||
|
||||
service quota-status {
|
||||
inet_listener {
|
||||
port = 0
|
||||
}
|
||||
unix_listener quota-status {
|
||||
user = postfix
|
||||
}
|
||||
vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB
|
||||
}
|
||||
|
||||
recipient_delimiter = ${cfg.recipientDelimiter}
|
||||
|
@ -385,23 +204,9 @@ in
|
|||
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
args = ${userdbFile}
|
||||
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
|
||||
args = ${passwdFile}
|
||||
}
|
||||
|
||||
${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}
|
||||
}
|
||||
''}
|
||||
|
||||
service auth {
|
||||
unix_listener auth {
|
||||
mode = 0660
|
||||
|
@ -417,27 +222,90 @@ in
|
|||
inbox = yes
|
||||
}
|
||||
|
||||
service indexer-worker {
|
||||
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
|
||||
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
|
||||
''}
|
||||
plugin {
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
|
||||
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
|
||||
sieve_default_name = default
|
||||
|
||||
# From elsewhere to Spam folder
|
||||
imapsieve_mailbox1_name = 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
|
||||
}
|
||||
|
||||
${lib.optionalString (cfg.fullTextSearch.enable != null) ''
|
||||
plugin {
|
||||
plugin = fts fts_xapian
|
||||
fts = xapian
|
||||
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
|
||||
|
||||
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
|
||||
|
||||
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
|
||||
|
||||
fts_enforced = ${cfg.fullTextSearch.enforced}
|
||||
}
|
||||
|
||||
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
|
||||
service indexer-worker {
|
||||
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
|
||||
}
|
||||
''}
|
||||
''}
|
||||
|
||||
lda_mailbox_autosubscribe = yes
|
||||
lda_mailbox_autocreate = yes
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.dovecot2 = {
|
||||
preStart =
|
||||
''
|
||||
${genPasswdScript}
|
||||
''
|
||||
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
||||
preStart = ''
|
||||
${genPasswdScript}
|
||||
rm -rf '${stateDir}/imap_sieve'
|
||||
mkdir '${stateDir}/imap_sieve'
|
||||
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
|
||||
for k in "${stateDir}/imap_sieve"/*.sieve ; do
|
||||
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
|
||||
done
|
||||
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.postfix.restartTriggers = [
|
||||
genPasswdScript
|
||||
] ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
|
||||
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
|
||||
|
||||
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
||||
description = "Optimize dovecot indices for fts_xapian";
|
||||
requisite = [ "dovecot2.service" ];
|
||||
after = [ "dovecot2.service" ];
|
||||
startAt = cfg.fullTextSearch.maintenance.onCalendar;
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
|
||||
PrivateDevices = true;
|
||||
PrivateNetwork = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectSystem = true;
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
|
||||
timerConfig = {
|
||||
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -12,4 +12,4 @@ if environment :matches "imap.user" "*" {
|
|||
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}";
|
||||
}
|
||||
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages =
|
||||
with pkgs;
|
||||
[
|
||||
dovecot
|
||||
openssh
|
||||
postfix
|
||||
rspamd
|
||||
]
|
||||
++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]);
|
||||
config = with cfg; lib.mkIf enable {
|
||||
environment.systemPackages = with pkgs; [
|
||||
dovecot opendkim openssh postfix rspamd
|
||||
] ++ (if certificateScheme == 2 then [ openssl ] else []);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ config, lib, ... }:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
|
@ -22,5 +22,7 @@ in
|
|||
{
|
||||
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ config, lib, ... }:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
|
|
|
@ -14,25 +14,24 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ config, lib, ... }:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (cfg.enable && cfg.openFirewall) {
|
||||
config = with cfg; lib.mkIf (enable && openFirewall) {
|
||||
|
||||
networking.firewall = {
|
||||
allowedTCPPorts =
|
||||
[ 25 ]
|
||||
++ lib.optional cfg.enableSubmission 587
|
||||
++ lib.optional cfg.enableSubmissionSsl 465
|
||||
++ lib.optional cfg.enableImap 143
|
||||
++ lib.optional cfg.enableImapSsl 993
|
||||
++ lib.optional cfg.enablePop3 110
|
||||
++ lib.optional cfg.enablePop3Ssl 995
|
||||
++ lib.optional cfg.enableManageSieve 4190
|
||||
++ lib.optional (cfg.certificateScheme == "acme-nginx") 80;
|
||||
allowedTCPPorts = [ 25 ]
|
||||
++ lib.optional enableSubmission 587
|
||||
++ lib.optional enableSubmissionSsl 465
|
||||
++ lib.optional enableImap 143
|
||||
++ lib.optional enableImapSsl 993
|
||||
++ lib.optional enablePop3 110
|
||||
++ lib.optional enablePop3Ssl 995
|
||||
++ lib.optional enableManageSieve 4190
|
||||
++ lib.optional (certificateScheme == 3) 80;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,35 +14,31 @@
|
|||
# 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 (import ./common.nix { inherit config lib pkgs; });
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with (import ./common.nix { inherit config; });
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
acmeRoot = "/var/lib/acme/acme-challenge";
|
||||
in
|
||||
{
|
||||
config =
|
||||
lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"))
|
||||
{
|
||||
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.fqdn}" = {
|
||||
serverName = cfg.fqdn;
|
||||
serverAliases = cfg.certificateDomains;
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
};
|
||||
};
|
||||
|
||||
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
|
||||
"postfix.service"
|
||||
"dovecot2.service"
|
||||
];
|
||||
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.fqdn}" = {
|
||||
serverName = cfg.fqdn;
|
||||
forceSSL = true;
|
||||
enableACME = true;
|
||||
acmeRoot = acmeRoot;
|
||||
};
|
||||
};
|
||||
|
||||
security.acme.certs."${cfg.fqdn}".postRun = ''
|
||||
systemctl reload nginx
|
||||
systemctl reload postfix
|
||||
systemctl reload dovecot2
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
88
mail-server/opendkim.nix
Normal file
88
mail-server/opendkim.nix
Normal file
|
@ -0,0 +1,88 @@
|
|||
# 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;
|
||||
keyPath = cfg.dkimKeyDirectory;
|
||||
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,12 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with (import ./common.nix { inherit config pkgs lib; });
|
||||
|
||||
|
@ -30,58 +25,29 @@ let
|
|||
# Merge several lookup tables. A lookup table is a attribute set where
|
||||
# - the key is an address (user@example.com) or a domain (@example.com)
|
||||
# - the value is a list of addresses
|
||||
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
|
||||
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
|
||||
|
||||
# valiases_postfix :: Map String [String]
|
||||
valiases_postfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
|
||||
) cfg.loginAccounts
|
||||
)
|
||||
);
|
||||
regex_valiases_postfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "${from}" = to; }) value.aliasesRegexp
|
||||
) cfg.loginAccounts
|
||||
)
|
||||
);
|
||||
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||
(name: value:
|
||||
let to = name;
|
||||
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
||||
cfg.loginAccounts));
|
||||
|
||||
# catchAllPostfix :: Map String [String]
|
||||
catchAllPostfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "@${from}" = to; }) value.catchAll
|
||||
) cfg.loginAccounts
|
||||
)
|
||||
);
|
||||
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||
(name: value:
|
||||
let to = name;
|
||||
in map (from: {"@${from}" = to;}) value.catchAll)
|
||||
cfg.loginAccounts));
|
||||
|
||||
# all_valiases_postfix :: Map String [String]
|
||||
all_valiases_postfix = mergeLookupTables [
|
||||
valiases_postfix
|
||||
extra_valiases_postfix
|
||||
];
|
||||
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
|
||||
|
||||
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
|
||||
attrsToLookupTable =
|
||||
aliases:
|
||||
let
|
||||
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
|
||||
in
|
||||
mergeLookupTables lookupTables;
|
||||
attrsToLookupTable = aliases: let
|
||||
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
|
||||
in mergeLookupTables lookupTables;
|
||||
|
||||
# extra_valiases_postfix :: Map String [String]
|
||||
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
|
||||
|
@ -90,49 +56,33 @@ let
|
|||
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
|
||||
);
|
||||
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;
|
||||
valiases_file = let
|
||||
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
||||
in builtins.toFile "valias" content;
|
||||
|
||||
# denied_recipients_postfix :: [ String ]
|
||||
denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (
|
||||
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
|
||||
);
|
||||
denied_recipients_file = builtins.toFile "denied_recipients" (
|
||||
lib.concatStringsSep "\n" denied_recipients_postfix
|
||||
);
|
||||
denied_recipients_postfix = (map
|
||||
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
||||
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
|
||||
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
|
||||
|
||||
reject_senders_postfix = map (sender: "${sender} REJECT") cfg.rejectSender;
|
||||
reject_senders_file = builtins.toFile "reject_senders" (
|
||||
lib.concatStringsSep "\n" reject_senders_postfix
|
||||
);
|
||||
reject_senders_postfix = (map
|
||||
(sender:
|
||||
"${sender} REJECT")
|
||||
(cfg.rejectSender));
|
||||
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
|
||||
|
||||
reject_recipients_postfix = map (recipient: "${recipient} REJECT") cfg.rejectRecipients;
|
||||
reject_recipients_postfix = (map
|
||||
(recipient:
|
||||
"${recipient} REJECT")
|
||||
(cfg.rejectRecipients));
|
||||
# rejectRecipients :: [ Path ]
|
||||
reject_recipients_file = builtins.toFile "reject_recipients" (
|
||||
lib.concatStringsSep "\n" reject_recipients_postfix
|
||||
);
|
||||
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
|
||||
|
||||
# vhosts_file :: Path
|
||||
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
||||
|
@ -144,257 +94,159 @@ let
|
|||
# every alias is owned (uniquely) by its user.
|
||||
# The user's own address is already in all_valiases_postfix.
|
||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
||||
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (
|
||||
lookupTableToString regex_valiases_postfix
|
||||
);
|
||||
|
||||
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (
|
||||
''
|
||||
# Removes sensitive headers from mails handed in via the submission port.
|
||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||
# Uses "pcre" style regex.
|
||||
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
||||
# Removes sensitive headers from mails handed in via the submission port.
|
||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||
# Uses "pcre" style regex.
|
||||
|
||||
/^Received:/ IGNORE
|
||||
/^X-Originating-IP:/ IGNORE
|
||||
/^X-Mailer:/ IGNORE
|
||||
/^User-Agent:/ IGNORE
|
||||
/^X-Enigmail:/ IGNORE
|
||||
''
|
||||
+ lib.optionalString cfg.rewriteMessageId ''
|
||||
/^Received:/ IGNORE
|
||||
/^X-Originating-IP:/ IGNORE
|
||||
/^X-Mailer:/ IGNORE
|
||||
/^User-Agent:/ IGNORE
|
||||
/^X-Enigmail:/ IGNORE
|
||||
'' + lib.optionalString cfg.rewriteMessageId ''
|
||||
|
||||
# Replaces the user submitted hostname with the server's FQDN to hide the
|
||||
# user's host or network.
|
||||
# Replaces the user submitted hostname with the server's FQDN to hide the
|
||||
# user's host or network.
|
||||
|
||||
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
||||
''
|
||||
);
|
||||
/^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;
|
||||
|
||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
||||
|
||||
submissionOptions = {
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||
smtpd_sasl_security_options = "noanonymous";
|
||||
smtpd_sasl_local_domain = "$myhostname";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${
|
||||
lib.optionalString (regex_valiases_postfix != { }) ",pcre:/etc/postfix/regex_vaccounts"
|
||||
}";
|
||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||
cleanup_service_name = "submission-header-cleanup";
|
||||
};
|
||||
|
||||
commonLdapConfig = ''
|
||||
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
||||
version = 3
|
||||
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
||||
tls_require_cert = yes
|
||||
|
||||
search_base = ${cfg.ldap.searchBase}
|
||||
scope = ${cfg.ldap.searchScope}
|
||||
|
||||
bind = yes
|
||||
bind_dn = ${cfg.ldap.bind.dn}
|
||||
'';
|
||||
|
||||
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
|
||||
${commonLdapConfig}
|
||||
query_filter = ${cfg.ldap.postfix.filter}
|
||||
result_attribute = ${cfg.ldap.postfix.mailAttribute}
|
||||
'';
|
||||
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
||||
appendPwdInSenderLoginMap = appendLdapBindPwd {
|
||||
name = "ldap-sender-login-map";
|
||||
file = ldapSenderLoginMap;
|
||||
prefix = "bind_pw = ";
|
||||
passwordFile = cfg.ldap.bind.passwordFile;
|
||||
destination = ldapSenderLoginMapFile;
|
||||
};
|
||||
|
||||
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
|
||||
${commonLdapConfig}
|
||||
query_filter = ${cfg.ldap.postfix.filter}
|
||||
result_attribute = ${cfg.ldap.postfix.uidAttribute}
|
||||
'';
|
||||
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
|
||||
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
|
||||
name = "ldap-virtual-mailbox-map";
|
||||
file = ldapVirtualMailboxMap;
|
||||
prefix = "bind_pw = ";
|
||||
passwordFile = cfg.ldap.bind.passwordFile;
|
||||
destination = ldapVirtualMailboxMapFile;
|
||||
};
|
||||
submissionOptions =
|
||||
{
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||
smtpd_sasl_security_options = "noanonymous";
|
||||
smtpd_sasl_local_domain = "$myhostname";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
|
||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||
cleanup_service_name = "submission-header-cleanup";
|
||||
};
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
||||
preStart = ''
|
||||
${appendPwdInVirtualMailboxMap}
|
||||
${appendPwdInSenderLoginMap}
|
||||
'';
|
||||
restartTriggers = [
|
||||
appendPwdInVirtualMailboxMap
|
||||
appendPwdInSenderLoginMap
|
||||
];
|
||||
};
|
||||
config = with cfg; lib.mkIf enable {
|
||||
|
||||
services.postfix = {
|
||||
enable = true;
|
||||
hostname = "${sendingFqdn}";
|
||||
networksStyle = "host";
|
||||
mapFiles."valias" = valiases_file;
|
||||
mapFiles."regex_valias" = regex_valiases_file;
|
||||
mapFiles."vaccounts" = vaccounts_file;
|
||||
mapFiles."regex_vaccounts" = regex_vaccounts_file;
|
||||
mapFiles."denied_recipients" = denied_recipients_file;
|
||||
mapFiles."reject_senders" = reject_senders_file;
|
||||
mapFiles."reject_recipients" = reject_recipients_file;
|
||||
sslCert = certificatePath;
|
||||
sslKey = keyPath;
|
||||
enableSubmission = cfg.enableSubmission;
|
||||
enableSubmissions = cfg.enableSubmissionSsl;
|
||||
virtual = lookupTableToString (mergeLookupTables [
|
||||
all_valiases_postfix
|
||||
catchAllPostfix
|
||||
forwards
|
||||
]);
|
||||
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
|
||||
|
||||
config = {
|
||||
myhostname = cfg.sendingFqdn;
|
||||
mydestination = ""; # disable local mail delivery
|
||||
# Extra Config
|
||||
mydestination = "";
|
||||
recipient_delimiter = cfg.recipientDelimiter;
|
||||
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
|
||||
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 = cfg.mailDirectory;
|
||||
virtual_mailbox_base = mailDirectory;
|
||||
virtual_mailbox_domains = vhosts_file;
|
||||
virtual_mailbox_maps =
|
||||
[
|
||||
(mappedFile "valias")
|
||||
]
|
||||
++ lib.optionals cfg.ldap.enable [
|
||||
"ldap:${ldapVirtualMailboxMapFile}"
|
||||
]
|
||||
++ lib.optionals (regex_valiases_postfix != { }) [
|
||||
(mappedRegexFile "regex_valias")
|
||||
];
|
||||
virtual_alias_maps = lib.mkAfter (
|
||||
lib.optionals (regex_valiases_postfix != { }) [
|
||||
(mappedRegexFile "regex_valias")
|
||||
]
|
||||
);
|
||||
virtual_mailbox_maps = mappedFile "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"
|
||||
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
|
||||
];
|
||||
|
||||
policy-spf_time_limit = "3600s";
|
||||
|
||||
# reject selected senders
|
||||
smtpd_sender_restrictions = [
|
||||
"check_sender_access ${mappedFile "reject_senders"}"
|
||||
];
|
||||
|
||||
# quota and spf checking
|
||||
smtpd_recipient_restrictions = [
|
||||
# reject selected recipients
|
||||
"check_recipient_access ${mappedFile "denied_recipients"}"
|
||||
"check_recipient_access ${mappedFile "reject_recipients"}"
|
||||
# quota checking
|
||||
"check_policy_service unix:/run/dovecot2/quota-status"
|
||||
"check_policy_service inet:localhost:12340"
|
||||
"check_policy_service unix:private/policy-spf"
|
||||
];
|
||||
|
||||
# The X509 private key followed by the corresponding certificate
|
||||
smtpd_tls_chain_files = [
|
||||
"${keyPath}"
|
||||
"${certificatePath}"
|
||||
];
|
||||
|
||||
# TLS for incoming mail is optional
|
||||
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
||||
# Submission by mail clients is handled in submissionOptions
|
||||
smtpd_tls_security_level = "may";
|
||||
|
||||
# But required for authentication attempts
|
||||
smtpd_tls_auth_only = true;
|
||||
# strong might suffice and is computationally less expensive
|
||||
smtpd_tls_eecdh_grade = "ultra";
|
||||
|
||||
# TLS versions supported for the SMTP server
|
||||
smtpd_tls_protocols = ">=TLSv1.2";
|
||||
smtpd_tls_mandatory_protocols = ">=TLSv1.2";
|
||||
# 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";
|
||||
|
||||
# Require ciphersuites that OpenSSL classifies as "High"
|
||||
smtp_tls_ciphers = "high";
|
||||
smtpd_tls_ciphers = "high";
|
||||
smtp_tls_mandatory_ciphers = "high";
|
||||
smtpd_tls_mandatory_ciphers = "high";
|
||||
|
||||
# Exclude cipher suites with undesirable properties
|
||||
smtpd_tls_exclude_ciphers = "eNULL, aNULL";
|
||||
smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL";
|
||||
# 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";
|
||||
|
||||
# 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 = "eNULL, aNULL";
|
||||
smtp_tls_mandatory_exclude_ciphers = "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;
|
||||
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
|
||||
smtp_tls_loglevel = "1";
|
||||
smtpd_tls_loglevel = "1";
|
||||
|
||||
# Configure a non blocking source of randomness
|
||||
tls_random_source = "dev:/dev/urandom";
|
||||
|
||||
smtpd_milters = smtpdMilters;
|
||||
non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||
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_authen}";
|
||||
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
||||
|
||||
};
|
||||
|
||||
submissionOptions = submissionOptions;
|
||||
submissionsOptions = submissionOptions;
|
||||
|
||||
masterConfig = {
|
||||
"lmtp" = {
|
||||
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
||||
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
||||
args = [ "flags=O" ];
|
||||
"policy-spf" = {
|
||||
type = "unix";
|
||||
privileged = true;
|
||||
chroot = false;
|
||||
command = "spawn";
|
||||
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
|
||||
};
|
||||
"submission-header-cleanup" = {
|
||||
type = "unix";
|
||||
|
@ -402,10 +254,7 @@ in
|
|||
chroot = false;
|
||||
maxproc = 0;
|
||||
command = "cleanup";
|
||||
args = [
|
||||
"-o"
|
||||
"header_checks=pcre:${submissionHeaderCleanupRules}"
|
||||
];
|
||||
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,19 +14,11 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
optionalString
|
||||
mkIf
|
||||
;
|
||||
|
||||
cfg = config.mailserver;
|
||||
|
||||
preexecDefined = cfg.backup.cmdPreexec != null;
|
||||
|
@ -46,8 +38,7 @@ let
|
|||
${cfg.backup.cmdPostexec}
|
||||
'';
|
||||
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
||||
in
|
||||
{
|
||||
in {
|
||||
config = mkIf (cfg.enable && cfg.backup.enable) {
|
||||
services.rsnapshot = {
|
||||
enable = true;
|
||||
|
|
|
@ -14,12 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
|
@ -27,121 +22,56 @@ let
|
|||
postfixCfg = config.services.postfix;
|
||||
rspamdCfg = config.services.rspamd;
|
||||
rspamdSocket = "rspamd.service";
|
||||
|
||||
rspamdUser = config.services.rspamd.user;
|
||||
rspamdGroup = config.services.rspamd.group;
|
||||
|
||||
createDkimKeypair =
|
||||
domain:
|
||||
let
|
||||
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
|
||||
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
|
||||
in
|
||||
pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
||||
if [ ! -f "${privateKey}" ]
|
||||
then
|
||||
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
|
||||
--domain "${domain}" \
|
||||
--selector "${cfg.dkimSelector}" \
|
||||
--type "${cfg.dkimKeyType}" \
|
||||
--bits ${toString cfg.dkimKeyBits} \
|
||||
--privkey "${privateKey}" > "${publicKey}"
|
||||
chmod 0644 "${publicKey}"
|
||||
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = lib.mkBefore [
|
||||
(pkgs.runCommand "rspamc-wrapped"
|
||||
{
|
||||
nativeBuildInputs = with pkgs; [ makeWrapper ];
|
||||
}
|
||||
''
|
||||
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
|
||||
--add-flags "-h /run/rspamd/worker-controller.sock"
|
||||
''
|
||||
)
|
||||
];
|
||||
|
||||
config = with cfg; lib.mkIf enable {
|
||||
services.rspamd = {
|
||||
enable = true;
|
||||
inherit (cfg) debug;
|
||||
inherit debug;
|
||||
locals = {
|
||||
"milter_headers.conf" = { text = ''
|
||||
extended_spam_headers = yes;
|
||||
''; };
|
||||
"redis.conf" = { text = ''
|
||||
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
|
||||
'' + (lib.optionalString (cfg.redis.password != null) ''
|
||||
password = "${cfg.redis.password}";
|
||||
''); };
|
||||
"classifier-bayes.conf" = { text = ''
|
||||
cache {
|
||||
backend = "redis";
|
||||
}
|
||||
''; };
|
||||
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
|
||||
clamav {
|
||||
action = "reject";
|
||||
symbol = "CLAM_VIRUS";
|
||||
type = "clamav";
|
||||
log_clean = true;
|
||||
servers = "/run/clamav/clamd.ctl";
|
||||
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
||||
}
|
||||
''; };
|
||||
"dkim_signing.conf" = { text = ''
|
||||
# Disable outbound email signing, we use opendkim for this
|
||||
enabled = false;
|
||||
''; };
|
||||
};
|
||||
|
||||
overrides = {
|
||||
"milter_headers.conf" = {
|
||||
text = ''
|
||||
extended_spam_headers = true;
|
||||
'';
|
||||
};
|
||||
"redis.conf" = {
|
||||
text =
|
||||
''
|
||||
servers = "${
|
||||
if cfg.redis.port == null then
|
||||
cfg.redis.address
|
||||
else
|
||||
"${cfg.redis.address}:${toString cfg.redis.port}"
|
||||
}";
|
||||
''
|
||||
+ (lib.optionalString (cfg.redis.password != null) ''
|
||||
password = "${cfg.redis.password}";
|
||||
'');
|
||||
};
|
||||
"classifier-bayes.conf" = {
|
||||
text = ''
|
||||
cache {
|
||||
backend = "redis";
|
||||
}
|
||||
'';
|
||||
};
|
||||
"antivirus.conf" = lib.mkIf cfg.virusScanning {
|
||||
text = ''
|
||||
clamav {
|
||||
action = "reject";
|
||||
symbol = "CLAM_VIRUS";
|
||||
type = "clamav";
|
||||
log_clean = true;
|
||||
servers = "/run/clamav/clamd.ctl";
|
||||
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
||||
}
|
||||
'';
|
||||
};
|
||||
"dkim_signing.conf" = {
|
||||
text = ''
|
||||
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 = "${cfg.dmarcReporting.email}";
|
||||
domain = "${cfg.dmarcReporting.domain}";
|
||||
org_name = "${cfg.dmarcReporting.organizationName}";
|
||||
from_name = "${cfg.dmarcReporting.fromName}";
|
||||
msgid_from = "${cfg.dmarcReporting.domain}";
|
||||
${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
|
||||
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
|
||||
''}
|
||||
}''}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
workers.rspamd_proxy = {
|
||||
type = "rspamd_proxy";
|
||||
bindSockets = [
|
||||
{
|
||||
socket = "/run/rspamd/rspamd-milter.sock";
|
||||
mode = "0664";
|
||||
}
|
||||
];
|
||||
bindSockets = [{
|
||||
socket = "/run/rspamd/rspamd-milter.sock";
|
||||
mode = "0664";
|
||||
}];
|
||||
count = 1; # Do not spawn too many processes of this type
|
||||
extraConfig = ''
|
||||
milter = yes; # Enable milter mode
|
||||
|
@ -156,13 +86,11 @@ in
|
|||
workers.controller = {
|
||||
type = "controller";
|
||||
count = 1;
|
||||
bindSockets = [
|
||||
{
|
||||
socket = "/run/rspamd/worker-controller.sock";
|
||||
mode = "0666";
|
||||
}
|
||||
];
|
||||
includes = [ ];
|
||||
bindSockets = [{
|
||||
socket = "/run/rspamd/worker-controller.sock";
|
||||
mode = "0666";
|
||||
}];
|
||||
includes = [];
|
||||
extraConfig = ''
|
||||
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
||||
'';
|
||||
|
@ -170,96 +98,11 @@ in
|
|||
|
||||
};
|
||||
|
||||
services.redis.servers.rspamd.enable = lib.mkDefault true;
|
||||
|
||||
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
||||
"${cfg.dkimKeyDirectory}" = {
|
||||
d = {
|
||||
# Create /var/dkim owned by rspamd user/group
|
||||
user = rspamdUser;
|
||||
group = rspamdGroup;
|
||||
};
|
||||
Z = {
|
||||
# Recursively adjust permissions in /var/dkim
|
||||
user = rspamdUser;
|
||||
group = rspamdGroup;
|
||||
};
|
||||
};
|
||||
};
|
||||
services.redis.enable = true;
|
||||
|
||||
systemd.services.rspamd = {
|
||||
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||
serviceConfig = lib.mkMerge [
|
||||
{
|
||||
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
||||
}
|
||||
(lib.optionalAttrs cfg.dkimSigning {
|
||||
ExecStartPre = map createDkimKeypair cfg.domains;
|
||||
ReadWritePaths = [ cfg.dkimKeyDirectory ];
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
|
||||
# Explicitly select yesterday's date to work around broken
|
||||
# default behaviour when called without a date.
|
||||
# https://github.com/rspamd/rspamd/issues/4062
|
||||
script = ''
|
||||
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "${config.services.rspamd.user}";
|
||||
Group = "${config.services.rspamd.group}";
|
||||
|
||||
AmbientCapabilities = [ ];
|
||||
CapabilityBoundingSet = "";
|
||||
DevicePolicy = "closed";
|
||||
IPAddressAllow = "localhost";
|
||||
LockPersonality = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProcSubset = "pid";
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged"
|
||||
];
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
|
||||
description = "Daily delivery of aggregated DMARC reports";
|
||||
wantedBy = [
|
||||
"timers.target"
|
||||
];
|
||||
timerConfig = {
|
||||
OnCalendar = "daily";
|
||||
Persistent = true;
|
||||
RandomizedDelaySec = 86400;
|
||||
FixedRandomDelay = true;
|
||||
};
|
||||
requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||
after = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||
};
|
||||
|
||||
systemd.services.postfix = {
|
||||
|
@ -270,3 +113,4 @@ in
|
|||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -14,77 +14,70 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
certificatesDeps =
|
||||
if cfg.certificateScheme == "manual" then
|
||||
[ ]
|
||||
else if cfg.certificateScheme == "selfsigned" then
|
||||
if cfg.certificateScheme == 1 then
|
||||
[]
|
||||
else if cfg.certificateScheme == 2 then
|
||||
[ "mailserver-selfsigned-certificate.service" ]
|
||||
else
|
||||
[ "acme-finished-${cfg.fqdn}.target" ];
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
config = with cfg; lib.mkIf enable {
|
||||
# Create self signed certificate
|
||||
systemd.services.mailserver-selfsigned-certificate =
|
||||
lib.mkIf (cfg.certificateScheme == "selfsigned")
|
||||
{
|
||||
after = [ "local-fs.target" ];
|
||||
script = ''
|
||||
# Create certificates if they do not exist yet
|
||||
dir="${cfg.certificateDirectory}"
|
||||
fqdn="${cfg.fqdn}"
|
||||
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
|
||||
key="$dir/key-${cfg.fqdn}.pem";
|
||||
cert="$dir/cert-${cfg.fqdn}.pem";
|
||||
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
|
||||
after = [ "local-fs.target" ];
|
||||
script = ''
|
||||
# Create certificates if they do not exist yet
|
||||
dir="${cfg.certificateDirectory}"
|
||||
fqdn="${cfg.fqdn}"
|
||||
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
|
||||
key="$dir/key-${cfg.fqdn}.pem";
|
||||
cert="$dir/cert-${cfg.fqdn}.pem";
|
||||
|
||||
if [[ ! -f $key || ! -f $cert ]]; then
|
||||
mkdir -p "${cfg.certificateDirectory}"
|
||||
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
|
||||
"${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
|
||||
-days 3650 -out "$cert"
|
||||
fi
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
if [[ ! -f $key || ! -f $cert ]]; then
|
||||
mkdir -p "${cfg.certificateDirectory}"
|
||||
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
|
||||
"${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
|
||||
-days 3650 -out "$cert"
|
||||
fi
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Create maildir folder before dovecot startup
|
||||
systemd.services.dovecot2 = {
|
||||
wants = certificatesDeps;
|
||||
after = certificatesDeps;
|
||||
preStart =
|
||||
let
|
||||
directories = lib.strings.escapeShellArgs (
|
||||
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
||||
);
|
||||
in
|
||||
''
|
||||
# Create mail directory and set permissions. See
|
||||
# <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>.
|
||||
# Prevent world-readable paths, even temporarily.
|
||||
umask 007
|
||||
mkdir -p ${directories}
|
||||
chgrp "${cfg.vmailGroupName}" ${directories}
|
||||
chmod 02770 ${directories}
|
||||
'';
|
||||
preStart = let
|
||||
directories = lib.strings.escapeShellArgs (
|
||||
[ mailDirectory ]
|
||||
++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
||||
);
|
||||
in ''
|
||||
# Create mail directory and set permissions. See
|
||||
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
||||
mkdir -p ${directories}
|
||||
chgrp "${vmailGroupName}" ${directories}
|
||||
chmod 02770 ${directories}
|
||||
'';
|
||||
};
|
||||
|
||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
||||
systemd.services.postfix = {
|
||||
wants = certificatesDeps;
|
||||
after = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service" ++ certificatesDeps;
|
||||
requires = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
|
||||
after = [ "dovecot2.service" ]
|
||||
++ lib.optional cfg.dkimSigning "opendkim.service"
|
||||
++ certificatesDeps;
|
||||
requires = [ "dovecot2.service" ]
|
||||
++ lib.optional cfg.dkimSigning "opendkim.service";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,12 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with config.mailserver;
|
||||
|
||||
|
@ -33,14 +28,12 @@ let
|
|||
group = vmailGroupName;
|
||||
};
|
||||
|
||||
|
||||
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Prevent world-readable paths, even temporarily.
|
||||
umask 007
|
||||
|
||||
# Create directory to store user sieve scripts if it doesn't exist
|
||||
if (! test -d "${sieveDirectory}"); then
|
||||
mkdir "${sieveDirectory}"
|
||||
|
@ -50,54 +43,45 @@ let
|
|||
|
||||
# Copy user's sieve script to the correct location (if it exists). If it
|
||||
# is null, remove the file.
|
||||
${lib.concatMapStringsSep "\n" (
|
||||
{ name, sieveScript }:
|
||||
if lib.isString sieveScript then
|
||||
''
|
||||
if (! test -d "${sieveDirectory}/${name}"); then
|
||||
mkdir -p "${sieveDirectory}/${name}"
|
||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
||||
chmod 770 "${sieveDirectory}/${name}"
|
||||
fi
|
||||
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
|
||||
${sieveScript}
|
||||
EOF
|
||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
||||
''
|
||||
else
|
||||
''
|
||||
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
||||
rm "${sieveDirectory}/${name}/default.sieve"
|
||||
fi
|
||||
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
||||
rm "${sieveDirectory}/${name}/default.svbin"
|
||||
fi
|
||||
''
|
||||
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))}
|
||||
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
|
||||
if lib.isString sieveScript then ''
|
||||
if (! test -d "${sieveDirectory}/${name}"); then
|
||||
mkdir -p "${sieveDirectory}/${name}"
|
||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
||||
chmod 770 "${sieveDirectory}/${name}"
|
||||
fi
|
||||
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
|
||||
${sieveScript}
|
||||
EOF
|
||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
||||
'' else ''
|
||||
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
||||
rm "${sieveDirectory}/${name}/default.sieve"
|
||||
fi
|
||||
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
||||
rm "${sieveDirectory}/${name}/default.svbin"
|
||||
fi
|
||||
'') (map (user: { inherit (user) name sieveScript; })
|
||||
(lib.attrValues loginAccounts))}
|
||||
'';
|
||||
in
|
||||
{
|
||||
in {
|
||||
config = lib.mkIf enable {
|
||||
# assert that all accounts provide a password
|
||||
assertions = map (acct: {
|
||||
assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
|
||||
assertions = (map (acct: {
|
||||
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
|
||||
message = "${acct.name} must provide either a hashed password or a password hash file";
|
||||
}) (lib.attrValues loginAccounts);
|
||||
}) (lib.attrValues loginAccounts));
|
||||
|
||||
# warn for accounts that specify both password and file
|
||||
warnings =
|
||||
map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
||||
(
|
||||
lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
|
||||
lib.attrValues loginAccounts
|
||||
)
|
||||
);
|
||||
warnings = (map
|
||||
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
||||
(lib.filter
|
||||
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
|
||||
(lib.attrValues loginAccounts)));
|
||||
|
||||
# set the vmail gid to a specific value
|
||||
users.groups = {
|
||||
"${vmailGroupName}" = {
|
||||
gid = vmailUID;
|
||||
};
|
||||
"${vmailGroupName}" = { gid = vmailUID; };
|
||||
};
|
||||
|
||||
# define all users
|
||||
|
|
33
nix/sources.json
Normal file
33
nix/sources.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"blobs": {
|
||||
"sha256": "1g687x3b2r4ar5i4xyav5qzpy9fp1phx9wf70f4j3scwny0g7hn1",
|
||||
"type": "tarball",
|
||||
"url": "https://gitlab.com/simple-nixos-mailserver/blobs/-/archive/2cccdf1ca48316f2cfd1c9a0017e8de5a7156265/blobs-2cccdf1ca48316f2cfd1c9a0017e8de5a7156265.tar.gz",
|
||||
"url_template": "https://gitlab.com/simple-nixos-mailserver/blobs/-/archive/<version>/blobs-<version>.tar.gz",
|
||||
"version": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265"
|
||||
},
|
||||
"nixpkgs-20.09": {
|
||||
"branch": "release-20.09",
|
||||
"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",
|
||||
"rev": "b6eefa48d8e10491e43c0c6155ac12b463f6fed3",
|
||||
"sha256": "0hrp7gshy62bsj719xd6hk6z284pzr8ksw1vvxvyfrffq1f7d8k9",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/b6eefa48d8e10491e43c0c6155ac12b463f6fed3.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",
|
||||
"rev": "e5cc06a1e806070693add4f231060a62b962fc44",
|
||||
"sha256": "04543i332fx9m7jf6167ac825s4qb8is0d0x0pz39il979mlc87v",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/e5cc06a1e806070693add4f231060a62b962fc44.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,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,31 +1,26 @@
|
|||
import smtplib, sys
|
||||
import argparse
|
||||
import email
|
||||
import email.utils
|
||||
import imaplib
|
||||
import smtplib
|
||||
import time
|
||||
import os
|
||||
import uuid
|
||||
import imaplib
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
import email
|
||||
import time
|
||||
|
||||
RETRY = 100
|
||||
|
||||
def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, starttls):
|
||||
print("Sending mail with subject '{}'".format(subject))
|
||||
message = "\n".join([
|
||||
"From: {from_addr}",
|
||||
"To: {to_addr}",
|
||||
"Subject: {subject}",
|
||||
"",
|
||||
"This validates our mail server can send to Gmail :/"]).format(
|
||||
from_addr=from_addr,
|
||||
to_addr=to_addr,
|
||||
subject=subject)
|
||||
|
||||
def _send_mail(
|
||||
smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls
|
||||
):
|
||||
print(f"Sending mail with subject '{subject}'")
|
||||
message = "\n".join(
|
||||
[
|
||||
f"From: {from_addr}",
|
||||
f"To: {to_addr}",
|
||||
f"Subject: {subject}",
|
||||
f"Message-ID: {uuid.uuid4()}@mail-check.py",
|
||||
f"Date: {email.utils.formatdate()}",
|
||||
"",
|
||||
"This validates our mail server can send to Gmail :/",
|
||||
]
|
||||
)
|
||||
|
||||
retry = RETRY
|
||||
while True:
|
||||
|
@ -35,16 +30,14 @@ def _send_mail(
|
|||
if starttls:
|
||||
smtp.starttls()
|
||||
if from_pwd is not None:
|
||||
smtp.login(smtp_username or from_addr, from_pwd)
|
||||
smtp.login(from_addr, from_pwd)
|
||||
|
||||
smtp.sendmail(from_addr, [to_addr], message)
|
||||
return
|
||||
except smtplib.SMTPResponseException as e:
|
||||
if e.smtp_code == 451: # service unavailable error
|
||||
print(e)
|
||||
elif (
|
||||
e.smtp_code == 454
|
||||
): # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
|
||||
elif e.smtp_code == 454: # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
|
||||
print(e)
|
||||
else:
|
||||
raise
|
||||
|
@ -62,18 +55,16 @@ def _send_mail(
|
|||
print("Retry attempts exhausted")
|
||||
exit(5)
|
||||
|
||||
|
||||
def _read_mail(
|
||||
imap_host,
|
||||
imap_port,
|
||||
imap_username,
|
||||
to_pwd,
|
||||
subject,
|
||||
ignore_dkim_spf,
|
||||
show_body=False,
|
||||
delete=True,
|
||||
):
|
||||
print("Reading mail from {imap_username}")
|
||||
imap_host,
|
||||
imap_port,
|
||||
imap_username,
|
||||
to_pwd,
|
||||
subject,
|
||||
ignore_dkim_spf,
|
||||
show_body=False,
|
||||
delete=True):
|
||||
print("Reading mail from %s" % imap_username)
|
||||
|
||||
message = None
|
||||
|
||||
|
@ -83,62 +74,49 @@ def _read_mail(
|
|||
|
||||
today = datetime.today()
|
||||
cutoff = today - timedelta(days=1)
|
||||
dt = cutoff.strftime("%d-%b-%Y")
|
||||
dt = cutoff.strftime('%d-%b-%Y')
|
||||
for _ in range(0, RETRY):
|
||||
print("Retrying")
|
||||
obj.select()
|
||||
_, data = obj.search(None, f'(SINCE {dt}) (SUBJECT "{subject}")')
|
||||
if data == [b""]:
|
||||
typ, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")'%(dt, subject))
|
||||
if data == [b'']:
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
uids = data[0].decode("utf-8").split(" ")
|
||||
if len(uids) != 1:
|
||||
print(
|
||||
f"Warning: {len(uids)} messages have been found with subject containing {subject}"
|
||||
)
|
||||
print("Warning: %d messages have been found with subject containing %s " % (len(uids), subject))
|
||||
|
||||
# FIXME: we only consider the first matching message...
|
||||
uid = uids[0]
|
||||
_, raw = obj.fetch(uid, "(RFC822)")
|
||||
_, raw = obj.fetch(uid, '(RFC822)')
|
||||
if delete:
|
||||
obj.store(uid, "+FLAGS", "\\Deleted")
|
||||
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")
|
||||
message = email.message_from_bytes(raw[0][1])
|
||||
print("Message with subject '%s' has been found" % message['subject'])
|
||||
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}")
|
||||
for m in message.get_payload():
|
||||
if m.get_content_type() == 'text/plain':
|
||||
print("Body:\n%s" % m.get_payload(decode=True).decode('utf-8'))
|
||||
break
|
||||
|
||||
if message is None:
|
||||
print(
|
||||
f"Error: no message with subject '{subject}' has been found in INBOX of {imap_username}"
|
||||
)
|
||||
print("Error: no message with subject '%s' has been found in INBOX of %s" % (subject, imap_username))
|
||||
exit(1)
|
||||
|
||||
if ignore_dkim_spf:
|
||||
return
|
||||
|
||||
# gmail set this standardized header
|
||||
if "ARC-Authentication-Results" in message:
|
||||
if "dkim=pass" in message["ARC-Authentication-Results"]:
|
||||
if 'ARC-Authentication-Results' in message:
|
||||
if "dkim=pass" in message['ARC-Authentication-Results']:
|
||||
print("DKIM ok")
|
||||
else:
|
||||
print("Error: no DKIM validation found in message:")
|
||||
print(message.as_string())
|
||||
exit(2)
|
||||
if "spf=pass" in message["ARC-Authentication-Results"]:
|
||||
if "spf=pass" in message['ARC-Authentication-Results']:
|
||||
print("SPF ok")
|
||||
else:
|
||||
print("Error: no SPF validation found in message:")
|
||||
|
@ -148,108 +126,69 @@ def _read_mail(
|
|||
print("DKIM and SPF verification failed")
|
||||
exit(4)
|
||||
|
||||
|
||||
def send_and_read(args):
|
||||
src_pwd = None
|
||||
if args.src_password_file is not None:
|
||||
src_pwd = args.src_password_file.readline().rstrip()
|
||||
dst_pwd = args.dst_password_file.readline().rstrip()
|
||||
|
||||
if args.imap_username != "":
|
||||
if args.imap_username != '':
|
||||
imap_username = args.imap_username
|
||||
else:
|
||||
imap_username = args.to_addr
|
||||
|
||||
subject = f"{uuid.uuid4()}"
|
||||
subject = "{}".format(uuid.uuid4())
|
||||
|
||||
_send_mail(
|
||||
smtp_host=args.smtp_host,
|
||||
smtp_port=args.smtp_port,
|
||||
smtp_username=args.smtp_username,
|
||||
from_addr=args.from_addr,
|
||||
from_pwd=src_pwd,
|
||||
to_addr=args.to_addr,
|
||||
subject=subject,
|
||||
starttls=args.smtp_starttls,
|
||||
)
|
||||
|
||||
_read_mail(
|
||||
imap_host=args.imap_host,
|
||||
imap_port=args.imap_port,
|
||||
imap_username=imap_username,
|
||||
to_pwd=dst_pwd,
|
||||
subject=subject,
|
||||
ignore_dkim_spf=args.ignore_dkim_spf,
|
||||
)
|
||||
_send_mail(smtp_host=args.smtp_host,
|
||||
smtp_port=args.smtp_port,
|
||||
from_addr=args.from_addr,
|
||||
from_pwd=src_pwd,
|
||||
to_addr=args.to_addr,
|
||||
subject=subject,
|
||||
starttls=args.smtp_starttls)
|
||||
|
||||
_read_mail(imap_host=args.imap_host,
|
||||
imap_port=args.imap_port,
|
||||
imap_username=imap_username,
|
||||
to_pwd=dst_pwd,
|
||||
subject=subject,
|
||||
ignore_dkim_spf=args.ignore_dkim_spf)
|
||||
|
||||
def read(args):
|
||||
_read_mail(
|
||||
imap_host=args.imap_host,
|
||||
imap_port=args.imap_port,
|
||||
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,
|
||||
)
|
||||
|
||||
_read_mail(imap_host=args.imap_host,
|
||||
imap_port=args.imap_port,
|
||||
to_addr=args.imap_username,
|
||||
to_pwd=args.imap_password,
|
||||
subject=args.subject,
|
||||
ignore_dkim_spf=args.ignore_dkim_spf,
|
||||
show_body=args.show_body,
|
||||
delete=False)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers()
|
||||
|
||||
parser_send_and_read = subparsers.add_parser(
|
||||
"send-and-read",
|
||||
description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.",
|
||||
)
|
||||
parser_send_and_read.add_argument("--smtp-host", type=str)
|
||||
parser_send_and_read.add_argument("--smtp-port", type=str, default=25)
|
||||
parser_send_and_read.add_argument("--smtp-starttls", action="store_true")
|
||||
parser_send_and_read.add_argument(
|
||||
"--smtp-username",
|
||||
type=str,
|
||||
default="",
|
||||
help="username used for smtp login. If not specified, the from-addr value is used",
|
||||
)
|
||||
parser_send_and_read.add_argument("--from-addr", type=str)
|
||||
parser_send_and_read.add_argument("--imap-host", required=True, type=str)
|
||||
parser_send_and_read.add_argument("--imap-port", type=str, default=993)
|
||||
parser_send_and_read.add_argument("--to-addr", type=str, required=True)
|
||||
parser_send_and_read.add_argument(
|
||||
"--imap-username",
|
||||
type=str,
|
||||
default="",
|
||||
help="username used for imap login. If not specified, the to-addr value is used",
|
||||
)
|
||||
parser_send_and_read.add_argument("--src-password-file", type=argparse.FileType("r"))
|
||||
parser_send_and_read.add_argument(
|
||||
"--dst-password-file", required=True, type=argparse.FileType("r")
|
||||
)
|
||||
parser_send_and_read.add_argument(
|
||||
"--ignore-dkim-spf",
|
||||
action="store_true",
|
||||
help="to ignore the dkim and spf verification on the read mail",
|
||||
)
|
||||
parser_send_and_read = subparsers.add_parser('send-and-read', description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.")
|
||||
parser_send_and_read.add_argument('--smtp-host', type=str)
|
||||
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
||||
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
|
||||
parser_send_and_read.add_argument('--from-addr', type=str)
|
||||
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
||||
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
||||
parser_send_and_read.add_argument('--to-addr', type=str, required=True)
|
||||
parser_send_and_read.add_argument('--imap-username', type=str, default='', help="username used for imap login. If not specified, the to-addr value is used")
|
||||
parser_send_and_read.add_argument('--src-password-file', type=argparse.FileType('r'))
|
||||
parser_send_and_read.add_argument('--dst-password-file', required=True, type=argparse.FileType('r'))
|
||||
parser_send_and_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
|
||||
parser_send_and_read.set_defaults(func=send_and_read)
|
||||
|
||||
parser_read = subparsers.add_parser(
|
||||
"read",
|
||||
description="Search for an email with a subject containing 'subject' in the INBOX.",
|
||||
)
|
||||
parser_read.add_argument("--imap-host", type=str, default="localhost")
|
||||
parser_read.add_argument("--imap-port", type=str, default=993)
|
||||
parser_read.add_argument("--imap-username", required=True, type=str)
|
||||
parser_read.add_argument("--imap-password", required=True, type=str)
|
||||
parser_read.add_argument(
|
||||
"--ignore-dkim-spf",
|
||||
action="store_true",
|
||||
help="to ignore the dkim and spf verification on the read mail",
|
||||
)
|
||||
parser_read.add_argument(
|
||||
"--show-body", action="store_true", help="print mail text/plain payload"
|
||||
)
|
||||
parser_read.add_argument("subject", type=str)
|
||||
parser_read = subparsers.add_parser('read', description="Search for an email with a subject containing 'subject' in the INBOX.")
|
||||
parser_read.add_argument('--imap-host', type=str, default="localhost")
|
||||
parser_read.add_argument('--imap-port', type=str, default=993)
|
||||
parser_read.add_argument('--imap-username', required=True, type=str)
|
||||
parser_read.add_argument('--imap-password', required=True, type=str)
|
||||
parser_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
|
||||
parser_read.add_argument('--show-body', action='store_true', help="print mail text/plain payload")
|
||||
parser_read.add_argument('subject', type=str)
|
||||
parser_read.set_defaults(func=read)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
|
10
shell.nix
10
shell.nix
|
@ -1,9 +1 @@
|
|||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
) { src = ./.; }).shellNix
|
||||
(import (builtins.fetchGit "https://github.com/edolstra/flake-compat") { src = ./.; }).shellNix
|
||||
|
|
266
tests/clamav.nix
266
tests/clamav.nix
|
@ -14,115 +14,104 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
lib,
|
||||
blobs,
|
||||
...
|
||||
}:
|
||||
{ pkgs ? import <nixpkgs> {}}:
|
||||
|
||||
{
|
||||
pkgs.nixosTest {
|
||||
name = "clamav";
|
||||
|
||||
nodes = {
|
||||
server =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
|
||||
virtualisation.memorySize = 1500;
|
||||
|
||||
environment.systemPackages = with pkgs; [ netcat ];
|
||||
|
||||
services.rsyslogd = {
|
||||
enable = true;
|
||||
defaultConfig = ''
|
||||
*.* /dev/console
|
||||
'';
|
||||
};
|
||||
|
||||
services.clamav.updater.enable = lib.mkForce false;
|
||||
systemd.services.old-clam = {
|
||||
before = [ "clamav-daemon.service" ];
|
||||
requiredBy = [ "clamav-daemon.service" ];
|
||||
description = "ClamAV virus database";
|
||||
|
||||
preStart = ''
|
||||
mkdir -m 0755 -p /var/lib/clamav
|
||||
chown clamav:clamav /var/lib/clamav
|
||||
'';
|
||||
|
||||
script = ''
|
||||
cp ${blobs}/clamav/main.cvd /var/lib/clamav/
|
||||
cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
|
||||
cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
|
||||
chown clamav:clamav /var/lib/clamav/*
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = "yes";
|
||||
PrivateDevices = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [
|
||||
"example.com"
|
||||
"example2.com"
|
||||
];
|
||||
virusScanning = true;
|
||||
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||
aliases = [ "postmaster@example.com" ];
|
||||
catchAll = [ "example.com" ];
|
||||
};
|
||||
"user@example2.com" = {
|
||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||
};
|
||||
};
|
||||
enableImap = true;
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
||||
};
|
||||
};
|
||||
client =
|
||||
{ nodes, pkgs, ... }:
|
||||
server = { config, pkgs, lib, ... }:
|
||||
let
|
||||
serverIP = nodes.server.networking.primaryIPAddress;
|
||||
clientIP = nodes.client.networking.primaryIPAddress;
|
||||
sources = import ../nix/sources.nix;
|
||||
blobs = pkgs.fetchzip {
|
||||
url = sources.blobs.url;
|
||||
sha256 = sources.blobs.sha256;
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
|
||||
virtualisation.memorySize = 1500;
|
||||
|
||||
services.rsyslogd = {
|
||||
enable = true;
|
||||
defaultConfig = ''
|
||||
*.* /dev/console
|
||||
'';
|
||||
};
|
||||
|
||||
services.clamav.updater.enable = lib.mkForce false;
|
||||
systemd.services.old-clam = {
|
||||
before = [ "clamav-daemon.service" ];
|
||||
requiredBy = [ "clamav-daemon.service" ];
|
||||
description = "ClamAV virus database";
|
||||
|
||||
preStart = ''
|
||||
mkdir -m 0755 -p /var/lib/clamav
|
||||
chown clamav:clamav /var/lib/clamav
|
||||
'';
|
||||
|
||||
script = ''
|
||||
cp ${blobs}/clamav/main.cvd /var/lib/clamav/
|
||||
cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
|
||||
cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
|
||||
chown clamav:clamav /var/lib/clamav/*
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = "yes";
|
||||
PrivateDevices = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" "example2.com" ];
|
||||
virusScanning = true;
|
||||
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||
aliases = [ "postmaster@example.com" ];
|
||||
catchAll = [ "example.com" ];
|
||||
};
|
||||
"user@example2.com" = {
|
||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||
};
|
||||
};
|
||||
enableImap = true;
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
||||
};
|
||||
};
|
||||
client = { nodes, config, pkgs, ... }: let
|
||||
serverIP = nodes.server.config.networking.primaryIPAddress;
|
||||
clientIP = nodes.client.config.networking.primaryIPAddress;
|
||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
echo grep '${clientIP}' "$@" >&2
|
||||
exec grep '${clientIP}' "$@"
|
||||
'';
|
||||
in
|
||||
{
|
||||
in {
|
||||
imports = [
|
||||
./lib/config.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
fetchmail
|
||||
msmtp
|
||||
procmail
|
||||
findutils
|
||||
grep-ip
|
||||
fetchmail msmtp procmail findutils grep-ip
|
||||
];
|
||||
environment.etc = {
|
||||
"root/.fetchmailrc" = {
|
||||
text = ''
|
||||
poll ${serverIP} with proto IMAP
|
||||
user 'user1@example.com' there with password 'user1' is 'root' here
|
||||
mda procmail
|
||||
poll ${serverIP} with proto IMAP
|
||||
user 'user1@example.com' there with password 'user1' is 'root' here
|
||||
mda procmail
|
||||
'';
|
||||
mode = "0700";
|
||||
};
|
||||
|
@ -196,59 +185,60 @@
|
|||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
testScript = { nodes, ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
server.wait_for_unit("multi-user.target")
|
||||
client.wait_for_unit("multi-user.target")
|
||||
server.wait_for_unit("multi-user.target")
|
||||
client.wait_for_unit("multi-user.target")
|
||||
|
||||
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
||||
server.wait_until_succeeds(
|
||||
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||
)
|
||||
server.wait_until_succeeds(
|
||||
"set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
||||
)
|
||||
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
||||
server.wait_until_succeeds(
|
||||
"timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||
)
|
||||
server.wait_until_succeeds(
|
||||
"timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
||||
)
|
||||
|
||||
client.execute("cp -p /etc/root/.* ~/")
|
||||
client.succeed("mkdir -p ~/mail")
|
||||
client.succeed("ls -la ~/ >&2")
|
||||
client.succeed("cat ~/.fetchmailrc >&2")
|
||||
client.succeed("cat ~/.procmailrc >&2")
|
||||
client.succeed("cat ~/.msmtprc >&2")
|
||||
client.execute("cp -p /etc/root/.* ~/")
|
||||
client.succeed("mkdir -p ~/mail")
|
||||
client.succeed("ls -la ~/ >&2")
|
||||
client.succeed("cat ~/.fetchmailrc >&2")
|
||||
client.succeed("cat ~/.procmailrc >&2")
|
||||
client.succeed("cat ~/.msmtprc >&2")
|
||||
|
||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
|
||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
|
||||
|
||||
# Verify that mail can be sent and received before testing virus scanner
|
||||
client.execute("rm ~/mail/*")
|
||||
client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
client.execute("rm ~/mail/*")
|
||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||
client.succeed("fetchmail --nosslcertck -v >&2")
|
||||
client.execute("rm ~/mail/*")
|
||||
# Verify that mail can be sent and received before testing virus scanner
|
||||
client.execute("rm ~/mail/*")
|
||||
client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
client.execute("rm ~/mail/*")
|
||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||
client.succeed("fetchmail --nosslcertck -v >&2")
|
||||
client.execute("rm ~/mail/*")
|
||||
|
||||
with subtest("virus scan file"):
|
||||
server.succeed(
|
||||
'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
|
||||
)
|
||||
with subtest("virus scan file"):
|
||||
server.succeed(
|
||||
'clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
|
||||
)
|
||||
|
||||
with subtest("virus scan email"):
|
||||
client.succeed(
|
||||
'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
|
||||
)
|
||||
server.succeed("journalctl -u rspamd | grep -i eicar")
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
with subtest("virus scan email"):
|
||||
client.succeed(
|
||||
'msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
|
||||
)
|
||||
server.succeed("journalctl -u rspamd | grep -i eicar")
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
|
||||
with subtest("no warnings or errors"):
|
||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
||||
server.fail("journalctl -u dovecot2 | grep -i error >&2")
|
||||
server.fail("journalctl -u dovecot2 | grep -i warning >&2")
|
||||
'';
|
||||
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")
|
||||
'';
|
||||
}
|
||||
|
|
47
tests/default.nix
Normal file
47
tests/default.nix
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Generate an attribute sets containing all tests for all releaeses
|
||||
# It looks like:
|
||||
# - external.nixpkgs_20.03
|
||||
# - external.nixpkgs_unstable
|
||||
# - internal.nixpkgs_20.03
|
||||
# - internal.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;
|
||||
};
|
||||
|
||||
releaseNames = [
|
||||
"nixpkgs-unstable"
|
||||
"nixpkgs-20.09"
|
||||
];
|
||||
|
||||
testNames = [
|
||||
"internal"
|
||||
"external"
|
||||
"clamav"
|
||||
"multiple"
|
||||
];
|
||||
|
||||
# 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)
|
|
@ -14,90 +14,76 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
{ pkgs ? import <nixpkgs> {}}:
|
||||
|
||||
pkgs.nixosTest {
|
||||
name = "external";
|
||||
|
||||
nodes = {
|
||||
server =
|
||||
{ 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 = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [
|
||||
"example.com"
|
||||
"example2.com"
|
||||
];
|
||||
rewriteMessageId = true;
|
||||
dkimKeyBits = 1535;
|
||||
dmarcReporting = {
|
||||
enable = true;
|
||||
domain = "example.com";
|
||||
organizationName = "ACME Corp";
|
||||
};
|
||||
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||
aliases = [ "postmaster@example.com" ];
|
||||
catchAll = [ "example.com" ];
|
||||
};
|
||||
"user2@example.com" = {
|
||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||
aliases = [ "chuck@example.com" ];
|
||||
};
|
||||
"user@example2.com" = {
|
||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||
};
|
||||
"lowquota@example.com" = {
|
||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||
quota = "1B";
|
||||
};
|
||||
};
|
||||
|
||||
extraVirtualAliases = {
|
||||
"single-alias@example.com" = "user1@example.com";
|
||||
"multi-alias@example.com" = [
|
||||
"user1@example.com"
|
||||
"user2@example.com"
|
||||
server = { config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
};
|
||||
|
||||
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";
|
||||
};
|
||||
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;
|
||||
fullTextSearch = {
|
||||
enable = true;
|
||||
autoIndex = true;
|
||||
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
|
||||
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ];
|
||||
enforced = "yes";
|
||||
# fts-xapian warns when memory is low, which makes the test fail
|
||||
memoryLimit = 100000;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
client =
|
||||
{ nodes, pkgs, ... }:
|
||||
let
|
||||
serverIP = nodes.server.networking.primaryIPAddress;
|
||||
clientIP = nodes.client.networking.primaryIPAddress;
|
||||
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
|
||||
|
@ -182,36 +168,27 @@
|
|||
assert needle in repr(response)
|
||||
imap.close()
|
||||
'';
|
||||
in
|
||||
{
|
||||
in {
|
||||
imports = [
|
||||
./lib/config.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
environment.systemPackages = with pkgs; [
|
||||
fetchmail
|
||||
msmtp
|
||||
procmail
|
||||
findutils
|
||||
grep-ip
|
||||
check-mail-id
|
||||
test-imap-spam
|
||||
test-imap-ham
|
||||
search
|
||||
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search
|
||||
];
|
||||
environment.etc = {
|
||||
"root/.fetchmailrc" = {
|
||||
text = ''
|
||||
poll ${serverIP} with proto IMAP
|
||||
user 'user1@example.com' there with password 'user1' is 'root' here
|
||||
mda procmail
|
||||
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
|
||||
poll ${serverIP} with proto IMAP
|
||||
user 'lowquota@example.com' there with password 'user2' is 'root' here
|
||||
mda procmail
|
||||
'';
|
||||
mode = "0700";
|
||||
};
|
||||
|
@ -290,7 +267,7 @@
|
|||
To: Chuck <chuck@example.com>
|
||||
Cc:
|
||||
Bcc:
|
||||
Subject: This is a test Email from postmaster@example.com to chuck
|
||||
Subject: This is a test Email from postmaster\@example.com to chuck
|
||||
Reply-To:
|
||||
|
||||
Hello Chuck,
|
||||
|
@ -304,7 +281,7 @@
|
|||
To: User1 <user1@example.com>
|
||||
Cc:
|
||||
Bcc:
|
||||
Subject: This is a test Email from single-alias@example.com to user1
|
||||
Subject: This is a test Email from single-alias\@example.com to user1
|
||||
Reply-To:
|
||||
|
||||
Hello User1,
|
||||
|
@ -319,7 +296,7 @@
|
|||
To: Multi Alias <multi-alias@example.com>
|
||||
Cc:
|
||||
Bcc:
|
||||
Subject: This is a test Email from user2@example.com to multi-alias
|
||||
Subject: This is a test Email from user2\@example.com to multi-alias
|
||||
Reply-To:
|
||||
|
||||
Hello Multi Alias,
|
||||
|
@ -357,176 +334,175 @@
|
|||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
start_all()
|
||||
testScript = { nodes, ... }:
|
||||
''
|
||||
start_all()
|
||||
|
||||
server.wait_for_unit("multi-user.target")
|
||||
client.wait_for_unit("multi-user.target")
|
||||
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 ]"
|
||||
)
|
||||
# TODO put this blocking into the systemd units?
|
||||
server.wait_until_succeeds(
|
||||
"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")
|
||||
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("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("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("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("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("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 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("dkim singing, multiple domains"):
|
||||
client.execute("rm ~/mail/*")
|
||||
# send email from user2 to user1
|
||||
client.succeed(
|
||||
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"
|
||||
)
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||
client.succeed("fetchmail --nosslcertck -v")
|
||||
client.succeed("cat ~/mail/* >&2")
|
||||
# make sure it is dkim signed
|
||||
client.succeed("grep DKIM ~/mail/*")
|
||||
|
||||
with subtest("aliases"):
|
||||
client.execute("rm ~/mail/*")
|
||||
# send email from chuck to postmaster
|
||||
client.succeed(
|
||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2"
|
||||
)
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||
client.succeed("fetchmail --nosslcertck -v")
|
||||
with subtest("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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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" ]')
|
||||
with subtest("imap sieve junk trainer"):
|
||||
# send email from user2 to user1
|
||||
client.succeed(
|
||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
|
||||
)
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
|
||||
client.succeed("imap-mark-spam >&2")
|
||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-spam.sh >&2")
|
||||
client.succeed("imap-mark-ham >&2")
|
||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-ham.sh >&2")
|
||||
client.succeed("imap-mark-spam >&2")
|
||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2")
|
||||
client.succeed("imap-mark-ham >&2")
|
||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2")
|
||||
|
||||
with subtest("full text search and indexation"):
|
||||
# send 2 email from user2 to user1
|
||||
client.succeed(
|
||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2"
|
||||
)
|
||||
client.succeed(
|
||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2"
|
||||
)
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
with subtest("full text search and indexation"):
|
||||
# send 2 email from user2 to user1
|
||||
client.succeed(
|
||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email6 >&2"
|
||||
)
|
||||
client.succeed(
|
||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email7 >&2"
|
||||
)
|
||||
# give the mail server some time to process the mail
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
|
||||
# should find exactly one email containing this
|
||||
client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2")
|
||||
# should fail because this folder is not indexed
|
||||
client.fail("search Junk a >&2")
|
||||
# check that search really goes through the indexer
|
||||
server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
|
||||
# check that Junk is not indexed
|
||||
server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
|
||||
# should find exactly one email containing this
|
||||
client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2")
|
||||
# should fail because this folder is not indexed
|
||||
client.fail("search Junk a >&2")
|
||||
# check that search really goes through the indexer
|
||||
server.succeed(
|
||||
"journalctl -u dovecot2 | grep -E 'indexer-worker.*Indexed . messages in INBOX' >&2"
|
||||
)
|
||||
# check that Junk is not indexed
|
||||
server.fail(
|
||||
"journalctl -u dovecot2 | grep -E 'indexer-worker.*Indexed . messages in Junk' >&2"
|
||||
)
|
||||
|
||||
with subtest("dmarc reporting"):
|
||||
server.systemctl("start rspamd-dmarc-reporter.service")
|
||||
|
||||
with subtest("no warnings or errors"):
|
||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
||||
server.fail("journalctl -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2")
|
||||
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
|
||||
server.fail(
|
||||
"journalctl -u dovecot2 | \
|
||||
grep -v 'Expunged message reappeared, giving a new UID' | \
|
||||
grep -v 'Time moved forwards' | \
|
||||
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")
|
||||
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
|
||||
server.fail(
|
||||
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -i warning >&2"
|
||||
)
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -14,10 +14,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{ pkgs ? import <nixpkgs> {}}:
|
||||
|
||||
let
|
||||
sendMail = pkgs.writeTextFile {
|
||||
|
@ -30,80 +27,61 @@ let
|
|||
'';
|
||||
};
|
||||
|
||||
hashPassword =
|
||||
password:
|
||||
pkgs.runCommand "password-${password}-hashed"
|
||||
{
|
||||
buildInputs = [ pkgs.mkpasswd ];
|
||||
inherit password;
|
||||
}
|
||||
''
|
||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||
'';
|
||||
hashPassword = password: pkgs.runCommand
|
||||
"password-${password}-hashed"
|
||||
{ buildInputs = [ pkgs.apacheHttpd ]; } ''
|
||||
htpasswd -nbB "" "${password}" | cut -d: -f2 > $out
|
||||
'';
|
||||
|
||||
hashedPasswordFile = hashPassword "my-password";
|
||||
passwordFile = pkgs.writeText "password" "my-password";
|
||||
in
|
||||
{
|
||||
pkgs.nixosTest {
|
||||
name = "internal";
|
||||
|
||||
nodes = {
|
||||
machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
./../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
machine = { config, pkgs, ... }: {
|
||||
imports = [
|
||||
./../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
|
||||
virtualisation.memorySize = 1024;
|
||||
virtualisation.memorySize = 1024;
|
||||
|
||||
environment.systemPackages =
|
||||
[
|
||||
(pkgs.writeScriptBin "mail-check" ''
|
||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||
'')
|
||||
]
|
||||
++ (with pkgs; [
|
||||
curl
|
||||
openssl
|
||||
netcat
|
||||
]);
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeScriptBin "mail-check" ''
|
||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||
'')];
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [
|
||||
"example.com"
|
||||
"domain.com"
|
||||
];
|
||||
localDnsResolver = false;
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.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;
|
||||
};
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
hashedPasswordFile = hashedPasswordFile;
|
||||
};
|
||||
forwards = {
|
||||
# user2@example.com is a local account and its mails are
|
||||
# also forwarded to user1@example.com
|
||||
"user2@example.com" = "user1@example.com";
|
||||
"user2@example.com" = {
|
||||
hashedPasswordFile = hashedPasswordFile;
|
||||
};
|
||||
"send-only@example.com" = {
|
||||
hashedPasswordFile = hashPassword "send-only";
|
||||
sendOnly = true;
|
||||
};
|
||||
|
||||
vmailGroupName = "vmail";
|
||||
vmailUID = 5000;
|
||||
|
||||
enableImap = false;
|
||||
};
|
||||
forwards = {
|
||||
# user2@example.com is a local account and its mails are
|
||||
# also forwarded to user1@example.com
|
||||
"user2@example.com" = "user1@example.com";
|
||||
};
|
||||
|
||||
vmailGroupName = "vmail";
|
||||
vmailUID = 5000;
|
||||
|
||||
enableImap = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
machine.start()
|
||||
|
@ -148,46 +126,6 @@ in
|
|||
)
|
||||
)
|
||||
|
||||
with subtest("regex email alias are received"):
|
||||
# A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox
|
||||
machine.succeed(
|
||||
" ".join(
|
||||
[
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-host localhost",
|
||||
"--imap-host localhost",
|
||||
"--imap-username user2@example.com",
|
||||
"--from-addr user1@example.com",
|
||||
"--to-addr user2-regex-alias@domain.com",
|
||||
"--src-password-file ${passwordFile}",
|
||||
"--dst-password-file ${passwordFile}",
|
||||
"--ignore-dkim-spf",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
with subtest("user can send from regex email alias"):
|
||||
# A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received
|
||||
machine.succeed(
|
||||
" ".join(
|
||||
[
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-host localhost",
|
||||
"--imap-host localhost",
|
||||
"--smtp-username user2@example.com",
|
||||
"--from-addr user2-regex-alias@domain.com",
|
||||
"--to-addr user1@example.com",
|
||||
"--src-password-file ${passwordFile}",
|
||||
"--dst-password-file ${passwordFile}",
|
||||
"--ignore-dkim-spf",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
with subtest("vmail gid is set correctly"):
|
||||
machine.succeed("getent group vmail | grep 5000")
|
||||
|
||||
|
@ -195,22 +133,22 @@ in
|
|||
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 ]"
|
||||
"timeout 1 ${pkgs.netcat}/bin/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'"
|
||||
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'"
|
||||
)
|
||||
|
||||
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>'"
|
||||
"${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
|
||||
)
|
||||
|
||||
with subtest("imap port 143 is closed and imaps is serving SSL"):
|
||||
machine.wait_for_closed_port(143)
|
||||
machine.wait_for_open_port(993)
|
||||
machine.succeed(
|
||||
"echo | openssl s_client -connect localhost:993 | grep 'New, TLS'"
|
||||
"echo | ${pkgs.openssl}/bin/openssl s_client -connect localhost:993 | grep 'New, TLS'"
|
||||
)
|
||||
'';
|
||||
}
|
||||
|
|
221
tests/ldap.nix
221
tests/ldap.nix
|
@ -1,221 +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;
|
||||
|
||||
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 = ''
|
||||
import sys
|
||||
import re
|
||||
|
||||
machine.start()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# This function retrieves the ldap table file from a postconf
|
||||
# command.
|
||||
# A key lookup is achived and the returned value is compared
|
||||
# to the expected value.
|
||||
def test_lookup(postconf_cmdline, key, expected):
|
||||
conf = machine.succeed(postconf_cmdline).rstrip()
|
||||
ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1)
|
||||
value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip()
|
||||
try:
|
||||
assert value == expected
|
||||
except AssertionError:
|
||||
print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr)
|
||||
raise
|
||||
|
||||
with subtest("Test postmap lookups"):
|
||||
test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com")
|
||||
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com")
|
||||
|
||||
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com")
|
||||
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com")
|
||||
|
||||
with subtest("Test doveadm lookups"):
|
||||
machine.succeed("doveadm user -u alice@example.com")
|
||||
machine.succeed("doveadm user -u bob@example.com")
|
||||
|
||||
with subtest("Files containing secrets are only readable by root"):
|
||||
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
|
||||
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
|
||||
|
||||
with subtest("Test account/mail address binding"):
|
||||
machine.fail(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-host localhost",
|
||||
"--smtp-username alice@example.com",
|
||||
"--imap-host localhost",
|
||||
"--imap-username bob@example.com",
|
||||
"--from-addr bob@example.com",
|
||||
"--to-addr aliceb@example.com",
|
||||
"--src-password-file <(echo '${alicePassword}')",
|
||||
"--dst-password-file <(echo '${bobPassword}')",
|
||||
"--ignore-dkim-spf"
|
||||
]))
|
||||
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
||||
|
||||
with subtest("Test mail delivery"):
|
||||
machine.succeed(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-host localhost",
|
||||
"--smtp-username alice@example.com",
|
||||
"--imap-host localhost",
|
||||
"--imap-username bob@example.com",
|
||||
"--from-addr alice@example.com",
|
||||
"--to-addr bob@example.com",
|
||||
"--src-password-file <(echo '${alicePassword}')",
|
||||
"--dst-password-file <(echo '${bobPassword}')",
|
||||
"--ignore-dkim-spf"
|
||||
]))
|
||||
|
||||
with subtest("Test mail forwarding works"):
|
||||
machine.succeed(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-host localhost",
|
||||
"--smtp-username alice@example.com",
|
||||
"--imap-host localhost",
|
||||
"--imap-username bob@example.com",
|
||||
"--from-addr alice@example.com",
|
||||
"--to-addr bob_fw@example.com",
|
||||
"--src-password-file <(echo '${alicePassword}')",
|
||||
"--dst-password-file <(echo '${bobPassword}')",
|
||||
"--ignore-dkim-spf"
|
||||
]))
|
||||
|
||||
with subtest("Test cannot send mail from forwarded address"):
|
||||
machine.fail(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-host localhost",
|
||||
"--smtp-username bob@example.com",
|
||||
"--imap-host localhost",
|
||||
"--imap-username alice@example.com",
|
||||
"--from-addr bob_fw@example.com",
|
||||
"--to-addr alice@example.com",
|
||||
"--src-password-file <(echo '${bobPassword}')",
|
||||
"--dst-password-file <(echo '${alicePassword}')",
|
||||
"--ignore-dkim-spf"
|
||||
]))
|
||||
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'")
|
||||
|
||||
'';
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
{
|
||||
# Testing eval failures that result from stateVersion assertion is out of scope
|
||||
mailserver.stateVersion = 999;
|
||||
security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot
|
||||
}
|
||||
|
|
|
@ -14,17 +14,18 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{
|
||||
name = "minimal";
|
||||
import <nixpkgs/nixos/tests/make-test.nix> {
|
||||
|
||||
nodes.machine = {
|
||||
imports = [
|
||||
../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
};
|
||||
machine =
|
||||
{ config, pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
./../default.nix
|
||||
];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target");
|
||||
'';
|
||||
testScript =
|
||||
''
|
||||
$machine->waitForUnit("multi-user.target");
|
||||
'';
|
||||
}
|
||||
|
|
|
@ -1,33 +1,19 @@
|
|||
# This tests is used to test features requiring several mail domains.
|
||||
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{ pkgs ? import <nixpkgs> {}}:
|
||||
|
||||
let
|
||||
hashPassword =
|
||||
password:
|
||||
pkgs.runCommand "password-${password}-hashed"
|
||||
{
|
||||
buildInputs = [ pkgs.mkpasswd ];
|
||||
inherit password;
|
||||
}
|
||||
hashPassword = password: pkgs.runCommand
|
||||
"password-${password}-hashed"
|
||||
{ buildInputs = [ pkgs.apacheHttpd ]; }
|
||||
''
|
||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||
htpasswd -nbB "" "${password}" | cut -d: -f2 > $out
|
||||
'';
|
||||
|
||||
password = pkgs.writeText "password" "password";
|
||||
password = pkgs.writeText "password" "password";
|
||||
|
||||
domainGenerator =
|
||||
domain:
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
environment.systemPackages = with pkgs; [ netcat ];
|
||||
domainGenerator = domain: { config, pkgs, ... }: {
|
||||
imports = [../default.nix];
|
||||
virtualisation.memorySize = 1024;
|
||||
mailserver = {
|
||||
enable = true;
|
||||
|
@ -44,47 +30,35 @@ let
|
|||
};
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings.mx-host = [
|
||||
"domain1.com,domain1,10"
|
||||
"domain2.com,domain2,10"
|
||||
];
|
||||
extraConfig = ''
|
||||
mx-host=domain1.com,domain1,10
|
||||
mx-host=domain2.com,domain2,10
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
pkgs.nixosTest {
|
||||
name = "multiple";
|
||||
|
||||
nodes = {
|
||||
domain1 =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
(domainGenerator "domain1.com")
|
||||
];
|
||||
mailserver.forwards = {
|
||||
"non-local@domain1.com" = [
|
||||
"user@domain2.com"
|
||||
"user@domain1.com"
|
||||
];
|
||||
"non@domain1.com" = [
|
||||
"user@domain2.com"
|
||||
"user@domain1.com"
|
||||
];
|
||||
};
|
||||
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} $@
|
||||
'')
|
||||
];
|
||||
};
|
||||
client = { config, pkgs, ... }: {
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeScriptBin "mail-check" ''
|
||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||
'')];
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
start_all()
|
||||
|
@ -94,10 +68,10 @@ in
|
|||
|
||||
# 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 ]"
|
||||
"timeout 1 ${pkgs.netcat}/bin/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 ]"
|
||||
"timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||
)
|
||||
|
||||
# user@domain1.com sends a mail to user@domain2.com
|
||||
|
|
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