Compare commits
2 commits
Author | SHA1 | Date | |
---|---|---|---|
|
d6e8a2aa47 | ||
|
a1aff6b61c |
66 changed files with 1198 additions and 5087 deletions
|
@ -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 }}
|
|
|
@ -1,13 +0,0 @@
|
||||||
hydra-pr:
|
|
||||||
only:
|
|
||||||
- merge_requests
|
|
||||||
image: nixos/nix
|
|
||||||
script:
|
|
||||||
- nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}'
|
|
||||||
|
|
||||||
hydra-master:
|
|
||||||
only:
|
|
||||||
- master
|
|
||||||
image: nixos/nix
|
|
||||||
script:
|
|
||||||
- nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master'
|
|
|
@ -1,55 +0,0 @@
|
||||||
{ nixpkgs, pulls, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
pkgs = import nixpkgs {};
|
|
||||||
|
|
||||||
prs = builtins.fromJSON (builtins.readFile pulls);
|
|
||||||
prJobsets = pkgs.lib.mapAttrs (num: info:
|
|
||||||
{ enabled = 1;
|
|
||||||
hidden = false;
|
|
||||||
description = "PR ${num}: ${info.title}";
|
|
||||||
checkinterval = 30;
|
|
||||||
schedulingshares = 20;
|
|
||||||
enableemail = false;
|
|
||||||
emailoverride = "";
|
|
||||||
keepnr = 1;
|
|
||||||
type = 1;
|
|
||||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
|
|
||||||
}
|
|
||||||
) prs;
|
|
||||||
mkFlakeJobset = branch: {
|
|
||||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
|
||||||
checkinterval = "60";
|
|
||||||
enabled = "1";
|
|
||||||
schedulingshares = 100;
|
|
||||||
enableemail = false;
|
|
||||||
emailoverride = "";
|
|
||||||
keepnr = 3;
|
|
||||||
hidden = false;
|
|
||||||
type = 1;
|
|
||||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}";
|
|
||||||
};
|
|
||||||
|
|
||||||
desc = prJobsets // {
|
|
||||||
"master" = mkFlakeJobset "master";
|
|
||||||
"nixos-23.11" = mkFlakeJobset "nixos-23.11";
|
|
||||||
"nixos-24.05" = mkFlakeJobset "nixos-24.05";
|
|
||||||
};
|
|
||||||
|
|
||||||
log = {
|
|
||||||
pulls = prs;
|
|
||||||
jobsets = desc;
|
|
||||||
};
|
|
||||||
|
|
||||||
in {
|
|
||||||
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
|
|
||||||
cat >$out <<EOF
|
|
||||||
${builtins.toJSON desc}
|
|
||||||
EOF
|
|
||||||
# This is to get nice .jobsets build logs on Hydra
|
|
||||||
cat >tmp <<EOF
|
|
||||||
${builtins.toJSON log}
|
|
||||||
EOF
|
|
||||||
${pkgs.jq}/bin/jq . tmp
|
|
||||||
'';
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
import ../tests
|
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"enabled": 1,
|
|
||||||
"hidden": false,
|
|
||||||
"description": "Simple NixOS Mailserver",
|
|
||||||
"nixexprinput": "nixexpr",
|
|
||||||
"nixexprpath": ".hydra/declarative-jobsets.nix",
|
|
||||||
"checkinterval": 60,
|
|
||||||
"schedulingshares": 100,
|
|
||||||
"enableemail": false,
|
|
||||||
"emailoverride": "",
|
|
||||||
"keepnr": 3,
|
|
||||||
"type": 0,
|
|
||||||
"inputs": {
|
|
||||||
"nixexpr": {
|
|
||||||
"value": "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master",
|
|
||||||
"type": "git",
|
|
||||||
"emailresponsible": false
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"value": "https://github.com/NixOS/nixpkgs 0f920b05cbcdb8c0f3c5c4a8ea29f1f0065c7033 ",
|
|
||||||
"type": "git",
|
|
||||||
"emailresponsible": false
|
|
||||||
},
|
|
||||||
"pulls": {
|
|
||||||
"type": "gitlabpulls",
|
|
||||||
"value": "https://gitlab.com 7219050",
|
|
||||||
"emailresponsible": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
8
.travis.yml
Normal file
8
.travis.yml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
language: nix
|
||||||
|
script:
|
||||||
|
- nix-build tests/intern.nix
|
||||||
|
- nix-build tests/extern.nix
|
||||||
|
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- /nix/store
|
348
README.md
348
README.md
|
@ -1,29 +1,22 @@
|
||||||
# ![Simple Nixos MailServer][logo]
|
# ![Simple Nixos MailServer][logo]
|
||||||
![license](https://img.shields.io/badge/license-GPL3-brightgreen.svg)
|
![license](https://img.shields.io/badge/license-GPL3-brightgreen.svg)
|
||||||
[![pipeline status](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/badges/master/pipeline.svg)](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
|
![status](https://travis-ci.org/r-raymond/nixos-mailserver.svg?branch=master)
|
||||||
|
|
||||||
|
|
||||||
## Release branches
|
## Stable Releases
|
||||||
|
|
||||||
For each NixOS release, we publish a branch. You then have to use the
|
* [SNM v2.0.4](https://github.com/r-raymond/nixos-mailserver/releases/v2.0.4)
|
||||||
SNM branch corresponding to your NixOS version.
|
|
||||||
|
|
||||||
* For NixOS 24.05
|
[Latest Release (Candidate)](https://github.com/r-raymond/nixos-mailserver/releases/latest)
|
||||||
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05)
|
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/)
|
|
||||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05)
|
|
||||||
* For NixOS 23.11
|
|
||||||
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11)
|
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/)
|
|
||||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11)
|
|
||||||
* For NixOS unstable
|
|
||||||
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
|
|
||||||
|
|
||||||
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
|
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
|
||||||
This is a very low volume list where new releases of SNM are announced, so you
|
This is a very low volume list where new releases of SNM are announced, so you
|
||||||
can stay up to date with bug fixes and updates.
|
can stay up to date with bug fixes and updates. All announcements are signed by
|
||||||
|
the gpg key with fingerprint
|
||||||
|
|
||||||
|
```
|
||||||
|
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
### v2.0
|
### v2.0
|
||||||
|
@ -31,15 +24,12 @@ can stay up to date with bug fixes and updates.
|
||||||
* [x] Multiple Domains
|
* [x] Multiple Domains
|
||||||
* Postfix MTA
|
* Postfix MTA
|
||||||
- [x] smtp on port 25
|
- [x] smtp on port 25
|
||||||
- [x] submission tls on port 465
|
- [x] submission port 587
|
||||||
- [x] submission starttls on port 587
|
|
||||||
- [x] lmtp with dovecot
|
- [x] lmtp with dovecot
|
||||||
* Dovecot
|
* Dovecot
|
||||||
- [x] maildir folders
|
- [x] maildir folders
|
||||||
- [x] imap with tls on port 993
|
- [x] imap starttls on port 143
|
||||||
- [x] pop3 with tls on port 995
|
- [x] pop3 starttls on port 110
|
||||||
- [x] imap with starttls on port 143
|
|
||||||
- [x] pop3 with starttls on port 110
|
|
||||||
* Certificates
|
* Certificates
|
||||||
- [x] manual certificates
|
- [x] manual certificates
|
||||||
- [x] on the fly creation
|
- [x] on the fly creation
|
||||||
|
@ -56,7 +46,6 @@ can stay up to date with bug fixes and updates.
|
||||||
* Sieves
|
* Sieves
|
||||||
- [x] A simple standard script that moves spam
|
- [x] A simple standard script that moves spam
|
||||||
- [x] Allow user defined sieve scripts
|
- [x] Allow user defined sieve scripts
|
||||||
- [x] ManageSieve support
|
|
||||||
* User Aliases
|
* User Aliases
|
||||||
- [x] Regular aliases
|
- [x] Regular aliases
|
||||||
- [x] Catch all aliases
|
- [x] Catch all aliases
|
||||||
|
@ -66,23 +55,316 @@ can stay up to date with bug fixes and updates.
|
||||||
* DKIM Signing
|
* DKIM Signing
|
||||||
- [ ] Allow a per domain selector
|
- [ ] Allow a per domain selector
|
||||||
|
|
||||||
### Get in touch
|
### Changelog
|
||||||
|
|
||||||
|
#### v1.0 -> v1.1
|
||||||
|
* Changed structure to Nix Modules
|
||||||
|
* Adds Sieve support
|
||||||
|
|
||||||
|
#### v1.1 -> v2.0
|
||||||
|
* rename domain to fqdn, seperate fqdn from domains
|
||||||
|
* multi domain support
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/archive/v2.0.4.tar.gz")
|
||||||
|
];
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" "example2.com" ];
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
|
||||||
|
aliases = [
|
||||||
|
"info@example.com"
|
||||||
|
"postmaster@example.com"
|
||||||
|
"postmaster@example2.com"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For a complete list of options, see `default.nix`.
|
||||||
|
|
||||||
|
|
||||||
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
|
||||||
- Join the Libera Chat IRC channel `#nixos-mailserver`
|
|
||||||
|
|
||||||
## How to Set Up a 10/10 Mail Server Guide
|
## How to Set Up a 10/10 Mail Server Guide
|
||||||
|
Mail servers can be a tricky thing to set up. This guide is supposed to run you
|
||||||
|
through the most important steps to achieve a 10/10 score on `mail-tester.com`.
|
||||||
|
|
||||||
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
|
What you need:
|
||||||
|
|
||||||
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
|
* A server with a public IP (referred to as `server-IP`)
|
||||||
|
* A Fully Qualified Domain Name (`FQDN`) where your server is reachable,
|
||||||
|
so that other servers can find yours. Common FQDN include `mx.example.com`
|
||||||
|
(where `example.com` is a domain you own) or `mail.example.com`. The domain
|
||||||
|
is referred to as `server-domain` (`example.com` in the above example) and
|
||||||
|
the `FQDN` is referred to by `server-FQDN` (`mx.example.com` above).
|
||||||
|
* A list of domains you want to your email server to serve. (Note that this
|
||||||
|
does not have to include `server-domain`, but may of course). These will be
|
||||||
|
referred to as `domains`. As an example, `domains = [ example1.com,
|
||||||
|
example2.com ]`.
|
||||||
|
|
||||||
## Development
|
### A) Setup server
|
||||||
|
|
||||||
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
|
The following describes a server setup that is fairly complete. Even though
|
||||||
|
there are more possible options (see `default.nix`), these should be the most
|
||||||
|
common ones.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
(builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/archive/v2.0.4.tar.gz")
|
||||||
|
];
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = <server-FQDN>;
|
||||||
|
domains = [ <domains> ];
|
||||||
|
|
||||||
|
# A list of all login accounts. To create the password hashes, use
|
||||||
|
# mkpasswd -m sha-512 "super secret password"
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
|
||||||
|
aliases = [
|
||||||
|
"postmaster@example.com"
|
||||||
|
"postmaster@example2.com"
|
||||||
|
];
|
||||||
|
|
||||||
|
# Make this user the catchAll address for domains example.com and
|
||||||
|
# example2.com
|
||||||
|
catchAll = [
|
||||||
|
"example.com"
|
||||||
|
"example2.com"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
"user2@example.com" = { ... };
|
||||||
|
};
|
||||||
|
|
||||||
|
# Extra virtual aliases. These are email addresses that are forwarded to
|
||||||
|
# loginAccounts addresses.
|
||||||
|
extraVirtualAliases = {
|
||||||
|
# address = forward address;
|
||||||
|
"abuse@example.com" = "user1@example.com";
|
||||||
|
};
|
||||||
|
|
||||||
|
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
|
||||||
|
# down nginx and opens port 80.
|
||||||
|
certificateScheme = 3;
|
||||||
|
|
||||||
|
# Enable IMAP and POP3
|
||||||
|
enableImap = true;
|
||||||
|
enablePop3 = true;
|
||||||
|
enableImapSsl = true;
|
||||||
|
enablePop3Ssl = true;
|
||||||
|
|
||||||
|
# whether to scan inbound emails for viruses (note that this requires at least
|
||||||
|
# 1 Gb RAM for the server. Without virus scanning 256 MB RAM should be plenty)
|
||||||
|
virusScanning = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
After a `nixos-rebuild switch --upgrade` your server should be good to go. If
|
||||||
|
you want to use `nixops` to deploy the server, look in the subfolder `nixops`
|
||||||
|
for some inspiration.
|
||||||
|
|
||||||
|
|
||||||
|
### B) Setup everything else
|
||||||
|
|
||||||
|
#### Step 1: Set DNS entry for server
|
||||||
|
|
||||||
|
Add a DNS record to the domain `server-domain` with the following entries
|
||||||
|
|
||||||
|
| Name (Subdomain) | TTL | Type | Priority | Value |
|
||||||
|
| ---------------- | ----- | ---- | -------- | ----------------- |
|
||||||
|
| `server-FQDN` | 10800 | A | | `server-IP` |
|
||||||
|
|
||||||
|
This resolved DNS equries for `server-FQDN` to `server-IP`. You can test if your
|
||||||
|
setting is correct by
|
||||||
|
|
||||||
|
```
|
||||||
|
ping <server-FQDN>
|
||||||
|
64 bytes from <server-FQDN> (<server-IP>): icmp_seq=1 ttl=46 time=21.3 ms
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
|
#### Step 2: Set rDNS (reverse DNS) entry for server
|
||||||
|
Wherever you have rented your server, you should be able to set reverse DNS
|
||||||
|
entries for the IP's you own. Add an entry resolving `server-IP` to
|
||||||
|
`server-FQDN`
|
||||||
|
|
||||||
|
You can test if your setting is correct by
|
||||||
|
|
||||||
|
```
|
||||||
|
host <server-IP>
|
||||||
|
<server-IP>.in-addr.arpa domain name pointer <server-FQDN>.
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
|
#### Step 3: Set `MX` Records
|
||||||
|
|
||||||
|
For every `domain` in `domains` do:
|
||||||
|
* Add a `MX` record to the domain `domain`
|
||||||
|
|
||||||
|
| Name (Subdomain) | TTL | Type | Priority | Value |
|
||||||
|
| ---------------- | ----- | ---- | -------- | ----------------- |
|
||||||
|
| `domain` | | MX | 10 | `server-FQDN` |
|
||||||
|
|
||||||
|
You can test this via
|
||||||
|
```
|
||||||
|
dig -t MX <domain>
|
||||||
|
|
||||||
|
...
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
<domain> 10800 IN MX 10 <server-FQDN>
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
|
#### Step 4: Set `SPF` Records
|
||||||
|
|
||||||
|
For every `domain` in `domains` do:
|
||||||
|
* Add a `SPF` record to the domain `domain`
|
||||||
|
|
||||||
|
| Name (Subdomain) | TTL | Type | Priority | Value |
|
||||||
|
| ---------------- | ----- | ---- | -------- | ----------------- |
|
||||||
|
| `domain` | 10800 | TXT | | `v=spf1 ip4:<server-IP> -all` |
|
||||||
|
|
||||||
|
You can check this with `dig -t TXT <domain>` similar to the last section.
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated. If you want to
|
||||||
|
use multiple servers for your email handling, don't forget to add all server
|
||||||
|
IP's to this list.
|
||||||
|
|
||||||
|
#### Step 5: Set `DKIM` signature
|
||||||
|
|
||||||
|
In this section we assume that your `dkimSelector` is set to `mail`. If you have a different selector, replace
|
||||||
|
all `mail`'s below accordingly.
|
||||||
|
|
||||||
|
For every `domain` in `domains` do:
|
||||||
|
* Go to your server and navigate to the dkim key directory (by default
|
||||||
|
`/var/dkim`). There you will find a public key for any domain in the
|
||||||
|
`domain.txt` file. It will look like
|
||||||
|
```
|
||||||
|
mail._domainkey IN TXT "v=DKIM1; r=postmaster; g=*; k=rsa; p=<really-long-key>" ; ----- DKIM mail for domain.tld
|
||||||
|
```
|
||||||
|
* Add a `DKIM` record to the domain `domain`
|
||||||
|
|
||||||
|
| Name (Subdomain) | TTL | Type | Priority | Value |
|
||||||
|
| ---------------- | ----- | ---- | -------- | ----------------- |
|
||||||
|
| mail._domainkey.`domain` | 10800 | TXT | | `v=DKIM1; p=<really-long-key>` |
|
||||||
|
|
||||||
|
|
||||||
|
You can check this with `dig -t TXT mail._domainkey.<domain>` similar to the last section.
|
||||||
|
|
||||||
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
|
|
||||||
|
### C) Test your Setup
|
||||||
|
|
||||||
|
Write an email to your aunt (who has been waiting for your reply far too long),
|
||||||
|
and sign up for some of the finest newsletters the Internet has. Maybe you want
|
||||||
|
to sign up for the [SNM Announcement List](https://www.freelists.org/list/snm)?
|
||||||
|
|
||||||
|
Besides that, you can send an email to [mail-tester.com](https://www.mail-tester.com/) and see how you score,
|
||||||
|
and let [mxtoolbox.com](http://mxtoolbox.com/) take a look at your setup, but if you followed
|
||||||
|
the steps closely then everything should be awesome!
|
||||||
|
|
||||||
|
|
||||||
|
## How to Backup
|
||||||
|
|
||||||
|
This is really easy. First off you should have a backup of your
|
||||||
|
`configuration.nix` file where you have the server config (but that is already
|
||||||
|
in a git repository right?)
|
||||||
|
|
||||||
|
Next you need to backup `/var/vmail` or whatever you have specified for the
|
||||||
|
option `mailDirectory`. This is where all the mails reside. Good options are a
|
||||||
|
cron job with `rsync` or `scp`. But really anything works, as it is simply a
|
||||||
|
folder with plenty of files in it. If your backup solution does not preserve the
|
||||||
|
owner of the files don't forget to `chown` them to `virtualMail:virtualMail` if you copy
|
||||||
|
them back (or whatever you specified as `vmailUserName`, and `vmailGoupName`).
|
||||||
|
|
||||||
|
Finally you can (optionally) make a backup of `/var/dkim` (or whatever you
|
||||||
|
specified as `dkimKeyDirectory`). If you should lose those don't worry, new ones
|
||||||
|
will be created on the fly. But you will need to repeat step `B)5` and correct
|
||||||
|
all the `dkim` keys.
|
||||||
|
|
||||||
|
## How to Test for Development
|
||||||
|
|
||||||
|
You can test the setup via `nixops`. After installation, do
|
||||||
|
|
||||||
|
```
|
||||||
|
nixops create nixops/single-server.nix nixops/vbox.nix -d mail
|
||||||
|
nixops deploy -d mail
|
||||||
|
nixops info -d mail
|
||||||
|
```
|
||||||
|
|
||||||
|
You can then test the server via e.g. `telnet`. To log into it, use
|
||||||
|
|
||||||
|
```
|
||||||
|
nixops ssh -d mail mailserver
|
||||||
|
```
|
||||||
|
|
||||||
|
To test imap manually use
|
||||||
|
|
||||||
|
```
|
||||||
|
openssl s_client -host mail.example.com -port 143 -starttls imap
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## A Complete Mail Server Without Moving Parts
|
||||||
|
|
||||||
|
### Used Technologies
|
||||||
|
* Nixos
|
||||||
|
* Nixpkgs
|
||||||
|
* Dovecot
|
||||||
|
* Postfix
|
||||||
|
* Rmilter
|
||||||
|
* Rspamd
|
||||||
|
* Clamav
|
||||||
|
* Opendkim
|
||||||
|
* Pam
|
||||||
|
|
||||||
|
### Features
|
||||||
|
* unlimited domain
|
||||||
|
* unlimited mail accounts
|
||||||
|
* unlimited aliases for every mail account
|
||||||
|
* spam and virus checking
|
||||||
|
* dkim signing of outgoing emails
|
||||||
|
* imap (optionally pop3)
|
||||||
|
* startTLS
|
||||||
|
|
||||||
|
### Nonfeatures
|
||||||
|
* moving parts
|
||||||
|
* SQL databases
|
||||||
|
* configurations that need to be made after `nixos-rebuild switch`
|
||||||
|
* complicated storage schemes
|
||||||
|
* webclients / http-servers
|
||||||
|
|
||||||
## Contributors
|
## Contributors
|
||||||
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
|
* Special thanks to @Infinisil for the module rewrite
|
||||||
|
* Special thanks to @jbboehr for multidomain implementation
|
||||||
|
* @danbst
|
||||||
|
* @phdoerfler
|
||||||
|
* @eqyiel
|
||||||
|
|
||||||
### Alternative Implementations
|
### Alternative Implementations
|
||||||
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
||||||
|
@ -94,4 +376,6 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails
|
||||||
* Logo made with [Logomakr.com](https://logomakr.com)
|
* Logo made with [Logomakr.com](https://logomakr.com)
|
||||||
|
|
||||||
|
|
||||||
[logo]: docs/logo.png
|
|
||||||
|
|
||||||
|
[logo]: logo/logo.png
|
||||||
|
|
1111
default.nix
1111
default.nix
File diff suppressed because it is too large
Load diff
|
@ -1,20 +0,0 @@
|
||||||
# Minimal makefile for Sphinx documentation
|
|
||||||
#
|
|
||||||
|
|
||||||
# You can set these variables from the command line, and also
|
|
||||||
# from the environment for the first two.
|
|
||||||
SPHINXOPTS ?=
|
|
||||||
SPHINXBUILD ?= sphinx-build
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
# Put it first so that "make" without argument is like "make help".
|
|
||||||
help:
|
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
|
|
||||||
.PHONY: help Makefile
|
|
||||||
|
|
||||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
|
||||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
|
@ -1,55 +0,0 @@
|
||||||
Add Radicale
|
|
||||||
============
|
|
||||||
|
|
||||||
Configuration by @dotlambda
|
|
||||||
|
|
||||||
Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional
|
|
||||||
crypt passwords are no longer supported. Instead bcrypt passwords
|
|
||||||
have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
mailAccounts = config.mailserver.loginAccounts;
|
|
||||||
htpasswd = pkgs.writeText "radicale.users" (concatStrings
|
|
||||||
(flip mapAttrsToList mailAccounts (mail: user:
|
|
||||||
mail + ":" + user.hashedPassword + "\n"
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
in {
|
|
||||||
services.radicale = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
auth = {
|
|
||||||
type = "htpasswd";
|
|
||||||
htpasswd_filename = "${htpasswd}";
|
|
||||||
htpasswd_encryption = "bcrypt";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
services.nginx = {
|
|
||||||
enable = true;
|
|
||||||
virtualHosts = {
|
|
||||||
"cal.example.com" = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
locations."/" = {
|
|
||||||
proxyPass = "http://localhost:5232/";
|
|
||||||
extraConfig = ''
|
|
||||||
proxy_set_header X-Script-Name /;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_pass_header Authorization;
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
|
||||||
}
|
|
|
@ -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,18 +0,0 @@
|
||||||
Autodiscovery
|
|
||||||
=============
|
|
||||||
|
|
||||||
`RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
|
|
||||||
of the mailserver. For that, the following records are required:
|
|
||||||
|
|
||||||
================= ==== ==== ======== ====== ==== =================
|
|
||||||
Record TTL Type Priority Weight Port Value
|
|
||||||
================= ==== ==== ======== ====== ==== =================
|
|
||||||
_submission._tcp 3600 SRV 5 0 587 mail.example.com.
|
|
||||||
_submissions._tcp 3600 SRV 5 0 465 mail.example.com.
|
|
||||||
_imap._tcp 3600 SRV 5 0 143 mail.example.com.
|
|
||||||
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
|
|
||||||
================= ==== ==== ======== ====== ==== =================
|
|
||||||
|
|
||||||
Please note that only a few MUAs currently implement this. For vendor-specific
|
|
||||||
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
Backup Guide
|
|
||||||
============
|
|
||||||
|
|
||||||
First off you should have a backup of your ``configuration.nix`` file
|
|
||||||
where you have the server config (but that is already in a git
|
|
||||||
repository right?)
|
|
||||||
|
|
||||||
Next you need to backup ``/var/vmail`` or whatever you have specified
|
|
||||||
for the option ``mailDirectory``. This is where all the mails reside.
|
|
||||||
Good options are a cron job with ``rsync`` or ``scp``. But really
|
|
||||||
anything works, as it is simply a folder with plenty of files in it. If
|
|
||||||
your backup solution does not preserve the owner of the files don’t
|
|
||||||
forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
|
|
||||||
back (or whatever you specified as ``vmailUserName``, and
|
|
||||||
``vmailGoupName``).
|
|
||||||
|
|
||||||
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
|
||||||
you specified as ``dkimKeyDirectory``). If you should lose those don’t
|
|
||||||
worry, new ones will be created on the fly. But you will need to repeat
|
|
||||||
step ``B)5`` and correct all the ``dkim`` keys.
|
|
61
docs/conf.py
61
docs/conf.py
|
@ -1,61 +0,0 @@
|
||||||
# Configuration file for the Sphinx documentation builder.
|
|
||||||
#
|
|
||||||
# This file only contains a selection of the most common options. For a full
|
|
||||||
# list see the documentation:
|
|
||||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
|
||||||
|
|
||||||
# -- Path setup --------------------------------------------------------------
|
|
||||||
|
|
||||||
# If extensions (or modules to document with autodoc) are in another directory,
|
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
|
||||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
|
||||||
#
|
|
||||||
# import os
|
|
||||||
# import sys
|
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
|
||||||
|
|
||||||
project = 'NixOS Mailserver'
|
|
||||||
copyright = '2022, NixOS Mailserver Contributors'
|
|
||||||
author = 'NixOS Mailserver Contributors'
|
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
|
||||||
# ones.
|
|
||||||
extensions = [
|
|
||||||
'myst_parser'
|
|
||||||
]
|
|
||||||
|
|
||||||
myst_enable_extensions = [
|
|
||||||
'colon_fence',
|
|
||||||
'linkify',
|
|
||||||
]
|
|
||||||
|
|
||||||
smartquotes = False
|
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
|
||||||
templates_path = ['_templates']
|
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
|
||||||
# directories to ignore when looking for source files.
|
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
|
||||||
# a list of builtin themes.
|
|
||||||
#
|
|
||||||
html_theme = 'sphinx_rtd_theme'
|
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
|
||||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
|
||||||
html_static_path = []
|
|
22
docs/faq.rst
22
docs/faq.rst
|
@ -1,22 +0,0 @@
|
||||||
FAQ
|
|
||||||
===
|
|
||||||
|
|
||||||
``catchAll`` users can't send email as user other than themself
|
|
||||||
---------------------------------------------------------------
|
|
||||||
|
|
||||||
To allow a ``catchAll`` user to send mail with the address used as
|
|
||||||
recipient, the option ``aliases`` has to be used instead of ``catchAll``.
|
|
||||||
|
|
||||||
For instance, to allow ``user@example.com`` to catch all mails to the
|
|
||||||
domain ``example.com`` and send mails with any address of this domain:
|
|
||||||
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
mailserver.loginAccounts = {
|
|
||||||
"user@example.com" = {
|
|
||||||
aliases = [ "@example.com" ];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
See also `this discussion <https://github.com/r-raymond/nixos-mailserver/issues/49>`__ for details.
|
|
|
@ -1,30 +0,0 @@
|
||||||
Nix Flakes
|
|
||||||
==========
|
|
||||||
|
|
||||||
If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
|
|
||||||
the following minimal ``flake.nix`` as an example:
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
{
|
|
||||||
description = "NixOS configuration";
|
|
||||||
|
|
||||||
inputs.simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-20.09";
|
|
||||||
|
|
||||||
outputs = { self, nixpkgs, simple-nixos-mailserver }: {
|
|
||||||
nixosConfigurations = {
|
|
||||||
hostname = nixpkgs.lib.nixosSystem {
|
|
||||||
system = "x86_64-linux";
|
|
||||||
modules = [
|
|
||||||
simple-nixos-mailserver.nixosModule
|
|
||||||
{
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
# ...
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
69
docs/fts.rst
69
docs/fts.rst
|
@ -1,69 +0,0 @@
|
||||||
Full text search
|
|
||||||
==========================
|
|
||||||
|
|
||||||
By default, when your IMAP client searches for an email containing some
|
|
||||||
text in its *body*, dovecot will read all your email sequentially. This
|
|
||||||
is very slow and IO intensive. To speed body searches up, it is possible to
|
|
||||||
*index* emails with a plugin to dovecot, ``fts_xapian``.
|
|
||||||
|
|
||||||
Enabling full text search
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
To enable indexing for full text search here is an example configuration.
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
{
|
|
||||||
mailserver = {
|
|
||||||
# ...
|
|
||||||
fullTextSearch = {
|
|
||||||
enable = true;
|
|
||||||
# index new email as they arrive
|
|
||||||
autoIndex = true;
|
|
||||||
# this only applies to plain text attachments, binary attachments are never indexed
|
|
||||||
indexAttachments = true;
|
|
||||||
enforced = "body";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
The ``enforced`` parameter tells dovecot to fail any body search query that cannot
|
|
||||||
use an index. This prevents dovecot to fall back to the IO-intensive brute
|
|
||||||
force search.
|
|
||||||
|
|
||||||
If you set ``autoIndex`` to ``false``, indices will be created when the IMAP client
|
|
||||||
issues a search query, so latency will be high.
|
|
||||||
|
|
||||||
Resource requirements
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Indices created by the full text search feature can take more disk
|
|
||||||
space than the emails themselves. By default, they are kept in the
|
|
||||||
emails location. When enabling the full text search feature, it is
|
|
||||||
recommended to move indices in a different location, such as
|
|
||||||
(``/var/lib/dovecot/indices``) by using the option
|
|
||||||
``mailserver.indexDir``.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
When the value of the ``indexDir`` option is changed, all dovecot
|
|
||||||
indices needs to be recreated: clients would need to resynchronize.
|
|
||||||
|
|
||||||
Indexation itself is rather resouces intensive, in CPU, and for emails with
|
|
||||||
large headers, in memory as well. Initial indexation of existing emails can take
|
|
||||||
hours. If the indexer worker is killed or segfaults during indexation, it can
|
|
||||||
be that it tried to allocate more memory than allowed. You can increase the memory
|
|
||||||
limit by eg ``mailserver.fullTextSearch.memoryLimit = 2000`` (in MiB).
|
|
||||||
|
|
||||||
Mitigating resources requirements
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
You can:
|
|
||||||
|
|
||||||
* disable indexation of attachements ``mailserver.fullTextSearch.indexAttachments = false``
|
|
||||||
* reduce the size of ngrams to be indexed ``mailserver.fullTextSearch.minSize`` and ``maxSize``
|
|
||||||
* disable automatic indexation for some folders with
|
|
||||||
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by
|
|
||||||
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard.
|
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
Contribute or troubleshoot
|
|
||||||
==========================
|
|
||||||
|
|
||||||
To report an issue, please go to
|
|
||||||
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
|
|
||||||
|
|
||||||
You can also chat with us on the Libera IRC channel ``#nixos-mailserver``.
|
|
||||||
|
|
||||||
Run NixOS tests
|
|
||||||
---------------
|
|
||||||
|
|
||||||
To run the test suite, you need to enable `Nix Flakes
|
|
||||||
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
|
|
||||||
|
|
||||||
You can then run the testsuite via
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix flake check -L
|
|
||||||
|
|
||||||
Since Nix doesn't garantee your machine have enough resources to run
|
|
||||||
all test VMs in parallel, some tests can fail. You would then haev to
|
|
||||||
run tests manually. For instance:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix build .#hydraJobs.x86_64-linux.external-unstable -L
|
|
||||||
|
|
||||||
|
|
||||||
Contributing to the documentation
|
|
||||||
---------------------------------
|
|
||||||
|
|
||||||
The documentation is written in RST (except option documentation which is in CommonMark),
|
|
||||||
built with Sphinx and published by `Read the Docs <https://readthedocs.org/>`_.
|
|
||||||
|
|
||||||
For the syntax, see the `RST/Sphinx primer
|
|
||||||
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
|
|
||||||
|
|
||||||
To build the documentation, you need to enable `Nix Flakes
|
|
||||||
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
|
|
||||||
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix build .#documentation
|
|
||||||
$ xdg-open result/index.html
|
|
||||||
|
|
||||||
Nixops
|
|
||||||
------
|
|
||||||
|
|
||||||
You can test the setup via ``nixops``. After installation, do
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
|
|
||||||
$ nixops deploy -d mail
|
|
||||||
$ nixops info -d mail
|
|
||||||
|
|
||||||
You can then test the server via e.g. \ ``telnet``. To log into it, use
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nixops ssh -d mail mailserver
|
|
||||||
|
|
||||||
Imap
|
|
||||||
----
|
|
||||||
|
|
||||||
To test imap manually use
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ openssl s_client -host mail.example.com -port 143 -starttls imap
|
|
|
@ -1,40 +0,0 @@
|
||||||
.. NixOS Mailserver documentation master file, created by
|
|
||||||
sphinx-quickstart on Thu Jul 2 20:50:36 2020.
|
|
||||||
You can adapt this file completely to your liking, but it should at least
|
|
||||||
contain the root `toctree` directive.
|
|
||||||
|
|
||||||
Welcome to NixOS Mailserver's documentation!
|
|
||||||
============================================
|
|
||||||
|
|
||||||
.. image:: logo.png
|
|
||||||
:width: 400
|
|
||||||
:alt: SNM Logo
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
setup-guide
|
|
||||||
howto-develop
|
|
||||||
faq
|
|
||||||
release-notes
|
|
||||||
options
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 1
|
|
||||||
:caption: How-to
|
|
||||||
|
|
||||||
backup-guide
|
|
||||||
add-radicale
|
|
||||||
add-roundcube
|
|
||||||
rspamd-tuning
|
|
||||||
fts
|
|
||||||
flakes
|
|
||||||
autodiscovery
|
|
||||||
ldap
|
|
||||||
|
|
||||||
Indices and tables
|
|
||||||
==================
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
* :ref:`search`
|
|
|
@ -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,70 +0,0 @@
|
||||||
Release Notes
|
|
||||||
=============
|
|
||||||
|
|
||||||
NixOS 24.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Add new option ``acmeCertificateName`` which can be used to support
|
|
||||||
wildcard certificates
|
|
||||||
|
|
||||||
NixOS 23.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Add basic support for LDAP users
|
|
||||||
- Add support for regex (PCRE) aliases
|
|
||||||
|
|
||||||
NixOS 23.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Existing ACME certificates can be reused without configuring NGINX
|
|
||||||
- Certificate scheme is no longer a number, but a meaningful string instead
|
|
||||||
|
|
||||||
NixOS 22.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Allow Rspamd to send DMARC reporting
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
|
|
||||||
|
|
||||||
NixOS 22.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Make NixOS Mailserver options discoverable from search.nixos.org
|
|
||||||
- Add a roundcube setup guide in the documentation
|
|
||||||
|
|
||||||
NixOS 21.11
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- Switch default DKIM body policy from simple to relaxed
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/247>`__)
|
|
||||||
- Ensure locally-delivered mails have the X-Original-To header
|
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/243>`__)
|
|
||||||
- NixOS Mailserver options are detailed in the `documentation
|
|
||||||
<https://nixos-mailserver.readthedocs.io/en/latest/options.html>`__
|
|
||||||
- New options ``dkimBodyCanonicalization`` and
|
|
||||||
``dkimHeaderCanonicalization``
|
|
||||||
- New option ``certificateDomains`` to generate certificate for
|
|
||||||
additional domains (such as ``imap.example.com``)
|
|
||||||
|
|
||||||
|
|
||||||
NixOS 21.05
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- New `fullTextSearch` option to search in messages (based on Xapian)
|
|
||||||
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/212>`__)
|
|
||||||
- Flake support
|
|
||||||
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/200>`__)
|
|
||||||
- New `openFirewall` option defaulting to `true`
|
|
||||||
- We moved from Freenode to Libera Chat
|
|
||||||
|
|
||||||
NixOS 20.09
|
|
||||||
-----------
|
|
||||||
|
|
||||||
- IMAP and Submission with TLS wrapped-mode are now enabled by default
|
|
||||||
on ports 993 and 465 respectively
|
|
||||||
- OpenDKIM is now sandboxed with Systemd
|
|
||||||
- New `forwards` option to forwards emails to external addresses
|
|
||||||
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/193>`__)
|
|
||||||
- New `sendingFqdn` option to specify the fqdn of the machine sending
|
|
||||||
email (`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/187>`__)
|
|
||||||
- Move the Gitlab wiki to `ReadTheDocs
|
|
||||||
<https://nixos-mailserver.readthedocs.io/en/latest/>`_
|
|
|
@ -1,4 +0,0 @@
|
||||||
sphinx ~= 5.3
|
|
||||||
sphinx_rtd_theme ~= 1.1
|
|
||||||
myst-parser ~= 0.18
|
|
||||||
linkify-it-py ~= 2.0
|
|
|
@ -1,113 +0,0 @@
|
||||||
Tune spam filtering
|
|
||||||
===================
|
|
||||||
|
|
||||||
SNM comes with the `rspamd spam filtering system <https://rspamd.com/>`_
|
|
||||||
enabled by default. Although its out-of-the-box performance is good, you
|
|
||||||
can increase its efficiency by tuning its behaviour.
|
|
||||||
|
|
||||||
Auto-learning
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Moving spam email to the Junk folder (and false-positives out of it) will
|
|
||||||
trigger an automatic training of the Bayesian filters, improving filtering
|
|
||||||
of future emails.
|
|
||||||
|
|
||||||
Train from existing folders
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
If you kept previous spam, you can train the filter from it. Note that the
|
|
||||||
`rspamd FAQ <https://rspamd.com/doc/faq.html#how-can-i-learn-messages>`_
|
|
||||||
indicates that *you should always learn both classes with almost equal
|
|
||||||
amount of messages to increase performance of the statistical engine.*
|
|
||||||
|
|
||||||
You can run the training in a root shell as follows:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
# Path to the controller socket
|
|
||||||
export RSOCK="/var/run/rspamd/worker-controller.sock"
|
|
||||||
|
|
||||||
# Learn the Junk folder as spam
|
|
||||||
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
|
||||||
|
|
||||||
# Learn the INBOX as ham
|
|
||||||
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/
|
|
||||||
|
|
||||||
# Check that training was successful
|
|
||||||
rspamc -h $RSOCK stat | grep learned
|
|
||||||
|
|
||||||
Tune symbol weight
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The ``X-Spamd-Result`` header is automatically added to your emails, detailing
|
|
||||||
the scoring decisions. The `modules documentation <https://rspamd.com/doc/modules/>`_
|
|
||||||
details the meaning of each symbol. You can tune the weight if a symbol if needed.
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
services.rspamd.locals = {
|
|
||||||
"groups.conf".text = ''
|
|
||||||
symbols {
|
|
||||||
"FORGED_RECIPIENTS" { weight = 0; }
|
|
||||||
}'';
|
|
||||||
};
|
|
||||||
|
|
||||||
Tune action thresholds
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
After scoring the message, rspamd decides on an action based on configurable thresholds.
|
|
||||||
By default, rspamd will tell postfix to reject any message with a score higher than 15.
|
|
||||||
If you experience issues in scoring or want to stay on the safe side, you can disable
|
|
||||||
this behaviour by tuning the configuration. For example:
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
services.rspamd.extraConfig = ''
|
|
||||||
actions {
|
|
||||||
reject = null; # Disable rejects, default is 15
|
|
||||||
add_header = 6; # Add header when reaching this score
|
|
||||||
greylist = 4; # Apply greylisting when reaching this score
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
|
|
||||||
|
|
||||||
Access the rspamd web UI
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Rspamd comes with `a web interface <https://rspamd.com/webui/>`_ that displays statistics
|
|
||||||
and history of past scans. **We do NOT recommend using it to change the configuration**
|
|
||||||
as doing so will override values from the configuration set in the previous sections.
|
|
||||||
|
|
||||||
The UI is served on the ``/var/run/rspamd/worker-controller.sock`` Unix socket. Here are
|
|
||||||
two ways to access it from your browser.
|
|
||||||
|
|
||||||
With ssh forwarding
|
|
||||||
^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
For occasional access, the simplest way is to forward the socket to localhost and open
|
|
||||||
http://localhost:3333 in your browser.
|
|
||||||
|
|
||||||
.. code:: shell
|
|
||||||
|
|
||||||
ssh -L 3333:/run/rspamd/worker-controller.sock $HOSTNAME
|
|
||||||
|
|
||||||
With an nginx reverse-proxy
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
If you have a secured nginx reverse proxy set on the host, you can use it to expose the socket.
|
|
||||||
**Keep in mind the UI is unsecured by default, you need to setup an authentication scheme**, for
|
|
||||||
exemple with `basic auth <https://docs.nginx.com/nginx/admin-guide/security-controls/configuring-http-basic-authentication/>`_:
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
services.nginx.virtualHosts.rspamd = {
|
|
||||||
forceSSL = true;
|
|
||||||
enableACME = true;
|
|
||||||
basicAuthFile = "/basic/auth/hashes/file";
|
|
||||||
serverName = "rspamd.example.com";
|
|
||||||
locations = {
|
|
||||||
"/" = {
|
|
||||||
proxyPass = "http://unix:/run/rspamd/worker-controller.sock:/";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,227 +0,0 @@
|
||||||
Setup Guide
|
|
||||||
===========
|
|
||||||
|
|
||||||
Mail servers can be a tricky thing to set up. This guide is supposed to
|
|
||||||
run you through the most important steps to achieve a 10/10 score on
|
|
||||||
`<https://mail-tester.com>`_.
|
|
||||||
|
|
||||||
What you need is:
|
|
||||||
|
|
||||||
- a server running NixOS with a public IP
|
|
||||||
- a domain name.
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
In the following, we consider a server with the public IP ``1.2.3.4``
|
|
||||||
and the domain ``example.com``.
|
|
||||||
|
|
||||||
First, we will set the minimum DNS configuration to be able to deploy
|
|
||||||
an up and running mail server. Once the server is deployed, we could
|
|
||||||
then set all DNS entries required to send and receive mails on this
|
|
||||||
server.
|
|
||||||
|
|
||||||
Setup DNS A record for server
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Add a DNS record to the domain ``example.com`` with the following
|
|
||||||
entries
|
|
||||||
|
|
||||||
==================== ===== ==== =============
|
|
||||||
Name (Subdomain) TTL Type Value
|
|
||||||
==================== ===== ==== =============
|
|
||||||
``mail.example.com`` 10800 A ``1.2.3.4``
|
|
||||||
==================== ===== ==== =============
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ ping mail.example.com
|
|
||||||
64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms
|
|
||||||
...
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated. This
|
|
||||||
DNS entry is required for the Let's Encrypt certificate generation
|
|
||||||
(which is used in the below configuration example).
|
|
||||||
|
|
||||||
Setup the server
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The following describes a server setup that is fairly complete. Even
|
|
||||||
though there are more possible options (see the `NixOS Mailserver
|
|
||||||
options documentation <options.html>`_), these should be the most
|
|
||||||
common ones.
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
{ config, pkgs, ... }: {
|
|
||||||
imports = [
|
|
||||||
(builtins.fetchTarball {
|
|
||||||
# Pick a release version you are interested in and set its hash, e.g.
|
|
||||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
|
|
||||||
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
|
|
||||||
# release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
|
|
||||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" ];
|
|
||||||
|
|
||||||
# A list of all login accounts. To create the password hashes, use
|
|
||||||
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
|
||||||
loginAccounts = {
|
|
||||||
"user1@example.com" = {
|
|
||||||
hashedPasswordFile = "/a/file/containing/a/hashed/password";
|
|
||||||
aliases = ["postmaster@example.com"];
|
|
||||||
};
|
|
||||||
"user2@example.com" = { ... };
|
|
||||||
};
|
|
||||||
|
|
||||||
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
|
|
||||||
# down nginx and opens port 80.
|
|
||||||
certificateScheme = "acme-nginx";
|
|
||||||
};
|
|
||||||
security.acme.acceptTerms = true;
|
|
||||||
security.acme.defaults.email = "security@example.com";
|
|
||||||
}
|
|
||||||
|
|
||||||
After a ``nixos-rebuild switch`` your server should be running all
|
|
||||||
mail components.
|
|
||||||
|
|
||||||
Setup all other DNS requirements
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Set rDNS (reverse DNS) entry for server
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Wherever you have rented your server, you should be able to set reverse
|
|
||||||
DNS entries for the IP’s you own. Add an entry resolving ``1.2.3.4``
|
|
||||||
to ``mail.example.com``.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
We don't recommend setting up a mail server if you are not able to
|
|
||||||
set a reverse DNS on your public IP because sent emails would be
|
|
||||||
mostly marked as spam. Note that many residential ISP providers
|
|
||||||
don't allow you to set a reverse DNS entry.
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host 1.2.3.4"
|
|
||||||
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
|
||||||
|
|
||||||
Set a ``MX`` record
|
|
||||||
^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
|
|
||||||
Add a ``MX`` record to the domain ``example.com``.
|
|
||||||
|
|
||||||
================ ==== ======== =================
|
|
||||||
Name (Subdomain) Type Priority Value
|
|
||||||
================ ==== ======== =================
|
|
||||||
example.com MX 10 mail.example.com
|
|
||||||
================ ==== ======== =================
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t mx example.com"
|
|
||||||
example.com mail is handled by 10 mail.example.com.
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
|
||||||
|
|
||||||
Set a ``SPF`` record
|
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Add a `SPF <https://en.wikipedia.org/wiki/Sender_Policy_Framework>`_
|
|
||||||
record to the domain ``example.com``.
|
|
||||||
|
|
||||||
================ ===== ==== ================================
|
|
||||||
Name (Subdomain) TTL Type Value
|
|
||||||
================ ===== ==== ================================
|
|
||||||
example.com 10800 TXT `v=spf1 a:mail.example.com -all`
|
|
||||||
================ ===== ==== ================================
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t TXT example.com"
|
|
||||||
example.com descriptive text "v=spf1 a:mail.example.com -all"
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
|
||||||
|
|
||||||
Set ``DKIM`` signature
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
On your server, the ``opendkim`` systemd service generated a file
|
|
||||||
containing your DKIM public key in the file
|
|
||||||
``/var/dkim/example.com.mail.txt``. The content of this file looks
|
|
||||||
like
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=<really-long-key>" ; ----- DKIM mail for domain.tld
|
|
||||||
|
|
||||||
where ``really-long-key`` is your public key.
|
|
||||||
|
|
||||||
Based on the content of this file, we can add a ``DKIM`` record to the
|
|
||||||
domain ``example.com``.
|
|
||||||
|
|
||||||
=========================== ===== ==== ==============================
|
|
||||||
Name (Subdomain) TTL Type Value
|
|
||||||
=========================== ===== ==== ==============================
|
|
||||||
mail._domainkey.example.com 10800 TXT ``v=DKIM1; p=<really-long-key>``
|
|
||||||
=========================== ===== ==== ==============================
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t txt mail._domainkey.example.com"
|
|
||||||
mail._domainkey.example.com descriptive text "v=DKIM1;p=<really-long-key>"
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
|
||||||
|
|
||||||
Set a ``DMARC`` record
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Add a ``DMARC`` record to the domain ``example.com``.
|
|
||||||
|
|
||||||
======================== ===== ==== ====================
|
|
||||||
Name (Subdomain) TTL Type Value
|
|
||||||
======================== ===== ==== ====================
|
|
||||||
_dmarc.example.com 10800 TXT ``v=DMARC1; p=none``
|
|
||||||
======================== ===== ==== ====================
|
|
||||||
|
|
||||||
You can check this with
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell -p bind --command "host -t TXT _dmarc.example.com"
|
|
||||||
_dmarc.example.com descriptive text "v=DMARC1; p=none"
|
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
|
||||||
|
|
||||||
|
|
||||||
Test your Setup
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Write an email to your aunt (who has been waiting for your reply far too
|
|
||||||
long), and sign up for some of the finest newsletters the Internet has.
|
|
||||||
Maybe you want to sign up for the `SNM Announcement
|
|
||||||
List <https://www.freelists.org/list/snm>`__?
|
|
||||||
|
|
||||||
Besides that, you can send an email to
|
|
||||||
`mail-tester.com <https://www.mail-tester.com/>`__ and see how you
|
|
||||||
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
|
||||||
your setup, but if you followed the steps closely then everything should
|
|
||||||
be awesome!
|
|
76
flake.lock
76
flake.lock
|
@ -1,76 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"blobs": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1604995301,
|
|
||||||
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
|
|
||||||
"owner": "simple-nixos-mailserver",
|
|
||||||
"repo": "blobs",
|
|
||||||
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
|
|
||||||
"type": "gitlab"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "simple-nixos-mailserver",
|
|
||||||
"repo": "blobs",
|
|
||||||
"type": "gitlab"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"flake-compat": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1696426674,
|
|
||||||
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
|
||||||
"owner": "edolstra",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "edolstra",
|
|
||||||
"repo": "flake-compat",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1717602782,
|
|
||||||
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-24_05": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1717144377,
|
|
||||||
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "805a384895c696f802a9bf5bf4720f37385df547",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"ref": "nixos-24.05",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"blobs": "blobs",
|
|
||||||
"flake-compat": "flake-compat",
|
|
||||||
"nixpkgs": "nixpkgs",
|
|
||||||
"nixpkgs-24_05": "nixpkgs-24_05"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
127
flake.nix
127
flake.nix
|
@ -1,127 +0,0 @@
|
||||||
{
|
|
||||||
description = "A complete and Simple Nixos Mailserver";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
flake-compat = {
|
|
||||||
url = "github:edolstra/flake-compat";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
|
|
||||||
nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05";
|
|
||||||
blobs = {
|
|
||||||
url = "gitlab:simple-nixos-mailserver/blobs";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let
|
|
||||||
lib = nixpkgs.lib;
|
|
||||||
system = "x86_64-linux";
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
releases = [
|
|
||||||
{
|
|
||||||
name = "unstable";
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "24.05";
|
|
||||||
pkgs = nixpkgs-24_05.legacyPackages.${system};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
testNames = [
|
|
||||||
"internal"
|
|
||||||
"external"
|
|
||||||
"clamav"
|
|
||||||
"multiple"
|
|
||||||
"ldap"
|
|
||||||
];
|
|
||||||
genTest = testName: release: {
|
|
||||||
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
|
|
||||||
"value"= import (./tests/. + "/${testName}.nix") {
|
|
||||||
pkgs = release.pkgs;
|
|
||||||
inherit blobs;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
# Generate an attribute set such as
|
|
||||||
# {
|
|
||||||
# external-unstable = <derivation>;
|
|
||||||
# external-21_05 = <derivation>;
|
|
||||||
# ...
|
|
||||||
# }
|
|
||||||
allTests = lib.listToAttrs (
|
|
||||||
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
|
|
||||||
|
|
||||||
mailserverModule = import ./.;
|
|
||||||
|
|
||||||
# Generate a MarkDown file describing the options of the NixOS mailserver module
|
|
||||||
optionsDoc = let
|
|
||||||
eval = lib.evalModules {
|
|
||||||
modules = [
|
|
||||||
mailserverModule
|
|
||||||
{
|
|
||||||
_module.check = false;
|
|
||||||
mailserver = {
|
|
||||||
fqdn = "mx.example.com";
|
|
||||||
domains = [
|
|
||||||
"example.com"
|
|
||||||
];
|
|
||||||
dmarcReporting = {
|
|
||||||
organizationName = "Example Corp";
|
|
||||||
domain = "example.com";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
];
|
|
||||||
};
|
|
||||||
options = builtins.toFile "options.json" (builtins.toJSON
|
|
||||||
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
|
|
||||||
(lib.optionAttrSetToDocList eval.options)));
|
|
||||||
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
|
|
||||||
echo "Generating options.md from ${options}"
|
|
||||||
python ${./scripts/generate-options.py} ${options} > $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
documentation = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "documentation";
|
|
||||||
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
|
|
||||||
buildInputs = [(
|
|
||||||
pkgs.python3.withPackages (p: with p; [
|
|
||||||
sphinx
|
|
||||||
sphinx_rtd_theme
|
|
||||||
myst-parser
|
|
||||||
linkify-it-py
|
|
||||||
])
|
|
||||||
)];
|
|
||||||
buildPhase = ''
|
|
||||||
cp ${optionsDoc} options.md
|
|
||||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
|
|
||||||
unset SOURCE_DATE_EPOCH
|
|
||||||
make html
|
|
||||||
'';
|
|
||||||
installPhase = ''
|
|
||||||
cp -Tr _build/html $out
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
in {
|
|
||||||
nixosModules = rec {
|
|
||||||
mailserver = mailserverModule;
|
|
||||||
default = mailserver;
|
|
||||||
};
|
|
||||||
nixosModule = self.nixosModules.default; # compatibility
|
|
||||||
hydraJobs.${system} = allTests // {
|
|
||||||
inherit documentation;
|
|
||||||
};
|
|
||||||
checks.${system} = allTests;
|
|
||||||
packages.${system} = {
|
|
||||||
inherit optionsDoc documentation;
|
|
||||||
};
|
|
||||||
devShells.${system}.default = pkgs.mkShell {
|
|
||||||
inputsFrom = [ documentation ];
|
|
||||||
packages = with pkgs; [
|
|
||||||
clamav
|
|
||||||
];
|
|
||||||
};
|
|
||||||
devShell.${system} = self.devShells.${system}.default; # compatibility
|
|
||||||
};
|
|
||||||
}
|
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -1,18 +0,0 @@
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
{
|
|
||||||
assertions = lib.optionals config.mailserver.ldap.enable [
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.loginAccounts == {};
|
|
||||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.forwards == {};
|
|
||||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards";
|
|
||||||
}
|
|
||||||
] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [
|
|
||||||
{
|
|
||||||
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;
|
|
||||||
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver.borgbackup;
|
|
||||||
|
|
||||||
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
|
||||||
autoFragment =
|
|
||||||
if cfg.compression.auto && cfg.compression.method == null
|
|
||||||
then throw "compression.method must be set when using auto."
|
|
||||||
else lib.optional cfg.compression.auto "auto";
|
|
||||||
levelFragment =
|
|
||||||
if cfg.compression.level != null && cfg.compression.method == null
|
|
||||||
then throw "compression.method must be set when using compression.level."
|
|
||||||
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
|
||||||
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
|
|
||||||
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
|
||||||
|
|
||||||
encryptionFragment = cfg.encryption.method;
|
|
||||||
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
|
||||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
|
|
||||||
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
|
||||||
else throw "passphraseFile must be set when using encryption.");
|
|
||||||
|
|
||||||
locations = lib.escapeShellArgs cfg.locations;
|
|
||||||
name = lib.escapeShellArg cfg.name;
|
|
||||||
|
|
||||||
repoLocation = lib.escapeShellArg cfg.repoLocation;
|
|
||||||
|
|
||||||
extraInitArgs = lib.escapeShellArgs cfg.extraArgumentsForInit;
|
|
||||||
extraCreateArgs = lib.escapeShellArgs cfg.extraArgumentsForCreate;
|
|
||||||
|
|
||||||
cmdPreexec = lib.optionalString (cfg.cmdPreexec != null) cfg.cmdPreexec;
|
|
||||||
cmdPostexec = lib.optionalString (cfg.cmdPostexec != null) cfg.cmdPostexec;
|
|
||||||
|
|
||||||
borgScript = ''
|
|
||||||
export BORG_REPO=${repoLocation}
|
|
||||||
${cmdPreexec}
|
|
||||||
${passphraseFragment} ${pkgs.borgbackup}/bin/borg init ${extraInitArgs} --encryption ${encryptionFragment} || true
|
|
||||||
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
|
||||||
${cmdPostexec}
|
|
||||||
'';
|
|
||||||
in {
|
|
||||||
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
borgbackup
|
|
||||||
];
|
|
||||||
|
|
||||||
systemd.services.borgbackup = {
|
|
||||||
description = "borgbackup";
|
|
||||||
unitConfig.Documentation = "man:borgbackup";
|
|
||||||
script = borgScript;
|
|
||||||
serviceConfig = {
|
|
||||||
User = cfg.user;
|
|
||||||
Group = cfg.group;
|
|
||||||
CPUSchedulingPolicy = "idle";
|
|
||||||
IOSchedulingClass = "idle";
|
|
||||||
ProtectSystem = "full";
|
|
||||||
};
|
|
||||||
startAt = cfg.startAt;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -14,17 +14,15 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, pkgs, lib, options, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
|
config = lib.mkIf cfg.virusScanning {
|
||||||
services.clamav.daemon = {
|
services.clamav.daemon.enable = true;
|
||||||
enable = true;
|
|
||||||
settings.PhishingScanURLs = "no";
|
|
||||||
};
|
|
||||||
services.clamav.updater.enable = true;
|
services.clamav.updater.enable = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -14,57 +14,27 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, pkgs, lib }:
|
{ config }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# cert :: PATH
|
# cert :: PATH
|
||||||
certificatePath = if cfg.certificateScheme == "manual"
|
certificatePath = if cfg.certificateScheme == 1
|
||||||
then cfg.certificateFile
|
then cfg.certificateFile
|
||||||
else if cfg.certificateScheme == "selfsigned"
|
else if cfg.certificateScheme == 2
|
||||||
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
else if cfg.certificateScheme == 3
|
||||||
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
then "/var/lib/acme/${cfg.fqdn}/fullchain.pem"
|
||||||
else throw "unknown certificate scheme";
|
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
||||||
|
|
||||||
# key :: PATH
|
# key :: PATH
|
||||||
keyPath = if cfg.certificateScheme == "manual"
|
keyPath = if cfg.certificateScheme == 1
|
||||||
then cfg.keyFile
|
then cfg.keyFile
|
||||||
else if cfg.certificateScheme == "selfsigned"
|
else if cfg.certificateScheme == 2
|
||||||
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
else if cfg.certificateScheme == 3
|
||||||
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
|
then "/var/lib/acme/${cfg.fqdn}/key.pem"
|
||||||
else throw "unknown certificate scheme";
|
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;
|
|
||||||
|
|
||||||
# Appends the LDAP bind password to files to avoid writing this
|
|
||||||
# password into the Nix store.
|
|
||||||
appendLdapBindPwd = {
|
|
||||||
name, file, prefix, suffix ? "", passwordFile, destination
|
|
||||||
}: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
baseDir=$(dirname ${destination})
|
|
||||||
if (! test -d "$baseDir"); then
|
|
||||||
mkdir -p $baseDir
|
|
||||||
chmod 755 $baseDir
|
|
||||||
fi
|
|
||||||
|
|
||||||
cat ${file} > ${destination}
|
|
||||||
echo -n '${prefix}' >> ${destination}
|
|
||||||
cat ${passwordFile} >> ${destination}
|
|
||||||
echo -n '${suffix}' >> ${destination}
|
|
||||||
chmod 600 ${destination}
|
|
||||||
'';
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
{ config, lib, ... }:
|
|
||||||
{
|
|
||||||
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,210 +16,41 @@
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with (import ./common.nix { inherit config pkgs lib; });
|
with (import ./common.nix { inherit config; });
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
passwdDir = "/run/dovecot2";
|
|
||||||
passwdFile = "${passwdDir}/passwd";
|
|
||||||
userdbFile = "${passwdDir}/userdb";
|
|
||||||
# This file contains the ldap bind password
|
|
||||||
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
|
|
||||||
bool2int = x: if x then "1" else "0";
|
|
||||||
|
|
||||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
|
||||||
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
|
|
||||||
|
|
||||||
# maildir in format "/${domain}/${user}"
|
# maildir in format "/${domain}/${user}"
|
||||||
dovecotMaildir =
|
dovecot_maildir = "maildir:${cfg.mailDirectory}/%d/%n";
|
||||||
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
|
||||||
+ (lib.optionalString (cfg.indexDir != null)
|
|
||||||
":INDEX=${cfg.indexDir}/%d/%n"
|
|
||||||
);
|
|
||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
|
||||||
dovecot2Cfg = config.services.dovecot2;
|
|
||||||
|
|
||||||
stateDir = "/var/lib/dovecot";
|
|
||||||
|
|
||||||
pipeBin = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "pipe_bin";
|
|
||||||
src = ./dovecot/pipe_bin;
|
|
||||||
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
|
|
||||||
buildCommand = ''
|
|
||||||
mkdir -p $out/pipe/bin
|
|
||||||
cp $src/* $out/pipe/bin/
|
|
||||||
chmod a+x $out/pipe/bin/*
|
|
||||||
patchShebangs $out/pipe/bin
|
|
||||||
|
|
||||||
for file in $out/pipe/bin/*; do
|
|
||||||
wrapProgram $file \
|
|
||||||
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
ldapConfig = pkgs.writeTextFile {
|
|
||||||
name = "dovecot-ldap.conf.ext.template";
|
|
||||||
text = ''
|
|
||||||
ldap_version = 3
|
|
||||||
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
|
|
||||||
${lib.optionalString cfg.ldap.startTls ''
|
|
||||||
tls = yes
|
|
||||||
''}
|
|
||||||
tls_require_cert = hard
|
|
||||||
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
|
||||||
dn = ${cfg.ldap.bind.dn}
|
|
||||||
sasl_bind = no
|
|
||||||
auth_bind = yes
|
|
||||||
base = ${cfg.ldap.searchBase}
|
|
||||||
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
|
||||||
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
|
|
||||||
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
|
||||||
''}
|
|
||||||
user_filter = ${cfg.ldap.dovecot.userFilter}
|
|
||||||
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
|
||||||
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
|
||||||
''}
|
|
||||||
pass_filter = ${cfg.ldap.dovecot.passFilter}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
setPwdInLdapConfFile = appendLdapBindPwd {
|
|
||||||
name = "ldap-conf-file";
|
|
||||||
file = ldapConfig;
|
|
||||||
prefix = ''dnpass = "'';
|
|
||||||
suffix = ''"'';
|
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
|
||||||
destination = ldapConfFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if (! test -d "${passwdDir}"); then
|
|
||||||
mkdir "${passwdDir}"
|
|
||||||
chmod 755 "${passwdDir}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prevent world-readable password files, even temporarily.
|
|
||||||
umask 077
|
|
||||||
|
|
||||||
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
|
||||||
if [ ! -f "$f" ]; then
|
|
||||||
echo "Expected password hash file $f does not exist!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
cat <<EOF > ${passwdFile}
|
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
|
||||||
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
|
||||||
) cfg.loginAccounts)}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat <<EOF > ${userdbFile}
|
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
|
||||||
"${name}:::::::"
|
|
||||||
+ (if lib.isString value.quota
|
|
||||||
then "userdb_quota_rule=*:storage=${value.quota}"
|
|
||||||
else "")
|
|
||||||
) cfg.loginAccounts)}
|
|
||||||
EOF
|
|
||||||
'';
|
|
||||||
|
|
||||||
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
|
|
||||||
junkMailboxNumber = builtins.length junkMailboxes;
|
|
||||||
# The assertion garantees there is exactly one Junk mailbox.
|
|
||||||
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
|
||||||
|
|
||||||
mkLdapSearchScope = scope: (
|
|
||||||
if scope == "sub" then "subtree"
|
|
||||||
else if scope == "one" then "onelevel"
|
|
||||||
else scope
|
|
||||||
);
|
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
assertions = [
|
|
||||||
{
|
|
||||||
assertion = junkMailboxNumber == 1;
|
|
||||||
message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
|
|
||||||
# the global config and tries to open shared libraries configured in there,
|
|
||||||
# which are usually not compatible.
|
|
||||||
environment.systemPackages = [
|
|
||||||
pkgs.dovecot_pigeonhole
|
|
||||||
];
|
|
||||||
|
|
||||||
services.dovecot2 = {
|
services.dovecot2 = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableImap = enableImap || enableImapSsl;
|
enableImap = enableImap;
|
||||||
enablePop3 = enablePop3 || enablePop3Ssl;
|
enablePop3 = enablePop3;
|
||||||
enablePAM = false;
|
|
||||||
enableQuota = true;
|
|
||||||
mailGroup = vmailGroupName;
|
mailGroup = vmailGroupName;
|
||||||
mailUser = vmailUserName;
|
mailUser = vmailUserName;
|
||||||
mailLocation = dovecotMaildir;
|
mailLocation = dovecot_maildir;
|
||||||
sslServerCert = certificatePath;
|
sslServerCert = certificatePath;
|
||||||
sslServerKey = keyPath;
|
sslServerKey = keyPath;
|
||||||
enableLmtp = true;
|
enableLmtp = true;
|
||||||
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
|
modules = [ pkgs.dovecot_pigeonhole ];
|
||||||
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
|
protocols = [ "sieve" ];
|
||||||
protocols = lib.optional cfg.enableManageSieve "sieve";
|
|
||||||
|
|
||||||
pluginSettings = {
|
sieveScripts = {
|
||||||
sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve";
|
before = builtins.toFile "spam.sieve" ''
|
||||||
sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve";
|
|
||||||
sieve_default_name = "default";
|
|
||||||
};
|
|
||||||
|
|
||||||
sieve = {
|
|
||||||
extensions = [
|
|
||||||
"fileinto"
|
|
||||||
];
|
|
||||||
|
|
||||||
scripts.after = builtins.toFile "spam.sieve" ''
|
|
||||||
require "fileinto";
|
require "fileinto";
|
||||||
|
|
||||||
if header :is "X-Spam" "Yes" {
|
if header :is "X-Spam" "Yes" {
|
||||||
fileinto "${junkMailboxName}";
|
fileinto "Junk";
|
||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
pipeBins = map lib.getExe [
|
|
||||||
(pkgs.writeShellScriptBin "sa-learn-ham.sh"
|
|
||||||
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
|
|
||||||
(pkgs.writeShellScriptBin "sa-learn-spam.sh"
|
|
||||||
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
imapsieve.mailbox = [
|
|
||||||
{
|
|
||||||
name = junkMailboxName;
|
|
||||||
causes = [ "COPY" "APPEND" ];
|
|
||||||
before = ./dovecot/imap_sieve/report-spam.sieve;
|
|
||||||
}
|
|
||||||
{
|
|
||||||
name = "*";
|
|
||||||
from = junkMailboxName;
|
|
||||||
causes = [ "COPY" ];
|
|
||||||
before = ./dovecot/imap_sieve/report-ham.sieve;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
mailboxes = cfg.mailboxes;
|
|
||||||
|
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
#Extra Config
|
#Extra Config
|
||||||
${lib.optionalString debug ''
|
${lib.optionalString debug ''
|
||||||
|
@ -228,172 +59,54 @@ in
|
||||||
verbose_ssl = yes
|
verbose_ssl = yes
|
||||||
''}
|
''}
|
||||||
|
|
||||||
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
|
|
||||||
service imap-login {
|
|
||||||
inet_listener imap {
|
|
||||||
${if cfg.enableImap then ''
|
|
||||||
port = 143
|
|
||||||
'' else ''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''}
|
|
||||||
}
|
|
||||||
inet_listener imaps {
|
|
||||||
${if cfg.enableImapSsl then ''
|
|
||||||
port = 993
|
|
||||||
ssl = yes
|
|
||||||
'' else ''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''}
|
|
||||||
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
|
|
||||||
service pop3-login {
|
|
||||||
inet_listener pop3 {
|
|
||||||
${if cfg.enablePop3 then ''
|
|
||||||
port = 110
|
|
||||||
'' else ''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''}
|
|
||||||
}
|
|
||||||
inet_listener pop3s {
|
|
||||||
${if cfg.enablePop3Ssl then ''
|
|
||||||
port = 995
|
|
||||||
ssl = yes
|
|
||||||
'' else ''
|
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
|
||||||
port = 0
|
|
||||||
''}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''}
|
|
||||||
|
|
||||||
protocol imap {
|
|
||||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
|
||||||
mail_plugins = $mail_plugins imap_sieve
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol pop3 {
|
|
||||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
|
||||||
}
|
|
||||||
|
|
||||||
mail_access_groups = ${vmailGroupName}
|
mail_access_groups = ${vmailGroupName}
|
||||||
ssl = required
|
ssl = required
|
||||||
ssl_min_protocol = TLSv1.2
|
|
||||||
ssl_prefer_server_ciphers = yes
|
|
||||||
|
|
||||||
service lmtp {
|
service lmtp {
|
||||||
unix_listener dovecot-lmtp {
|
unix_listener /run/dovecot/lmtp {
|
||||||
group = ${postfixCfg.group}
|
group = ${vmailGroupName}
|
||||||
mode = 0600
|
mode = 0600
|
||||||
user = ${postfixCfg.user}
|
user = ${vmailUserName}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recipient_delimiter = ${cfg.recipientDelimiter}
|
|
||||||
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
|
|
||||||
|
|
||||||
protocol lmtp {
|
protocol lmtp {
|
||||||
mail_plugins = $mail_plugins sieve
|
mail_plugins = $mail_plugins sieve
|
||||||
}
|
}
|
||||||
|
|
||||||
passdb {
|
|
||||||
driver = passwd-file
|
|
||||||
args = ${passwdFile}
|
|
||||||
}
|
|
||||||
|
|
||||||
userdb {
|
|
||||||
driver = passwd-file
|
|
||||||
args = ${userdbFile}
|
|
||||||
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
|
|
||||||
}
|
|
||||||
|
|
||||||
${lib.optionalString cfg.ldap.enable ''
|
|
||||||
passdb {
|
|
||||||
driver = ldap
|
|
||||||
args = ${ldapConfFile}
|
|
||||||
}
|
|
||||||
|
|
||||||
userdb {
|
|
||||||
driver = ldap
|
|
||||||
args = ${ldapConfFile}
|
|
||||||
default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
|
|
||||||
}
|
|
||||||
''}
|
|
||||||
|
|
||||||
service auth {
|
|
||||||
unix_listener auth {
|
|
||||||
mode = 0660
|
|
||||||
user = ${postfixCfg.user}
|
|
||||||
group = ${postfixCfg.group}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auth_mechanisms = plain login
|
auth_mechanisms = plain login
|
||||||
|
|
||||||
namespace inbox {
|
namespace inbox {
|
||||||
separator = ${cfg.hierarchySeparator}
|
|
||||||
inbox = yes
|
inbox = yes
|
||||||
|
|
||||||
|
mailbox "Trash" {
|
||||||
|
auto = no
|
||||||
|
special_use = \Trash
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox "Junk" {
|
||||||
|
auto = subscribe
|
||||||
|
special_use = \Junk
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox "Drafts" {
|
||||||
|
auto = subscribe
|
||||||
|
special_use = \Drafts
|
||||||
|
}
|
||||||
|
|
||||||
|
mailbox "Sent" {
|
||||||
|
auto = subscribe
|
||||||
|
special_use = \Sent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
${lib.optionalString cfg.fullTextSearch.enable ''
|
|
||||||
plugin {
|
plugin {
|
||||||
plugin = fts fts_xapian
|
sieve = file:/var/sieve/%u.sieve
|
||||||
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_autosubscribe = yes
|
||||||
lda_mailbox_autocreate = yes
|
lda_mailbox_autocreate = yes
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.dovecot2 = {
|
|
||||||
preStart = ''
|
|
||||||
${genPasswdScript}
|
|
||||||
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
|
|
||||||
|
|
||||||
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
|
||||||
description = "Optimize dovecot indices for fts_xapian";
|
|
||||||
requisite = [ "dovecot2.service" ];
|
|
||||||
after = [ "dovecot2.service" ];
|
|
||||||
startAt = cfg.fullTextSearch.maintenance.onCalendar;
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
|
|
||||||
PrivateDevices = true;
|
|
||||||
PrivateNetwork = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectSystem = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
|
|
||||||
timerConfig = {
|
|
||||||
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
|
|
||||||
|
|
||||||
if environment :matches "imap.mailbox" "*" {
|
|
||||||
set "mailbox" "${1}";
|
|
||||||
}
|
|
||||||
|
|
||||||
if string "${mailbox}" "Trash" {
|
|
||||||
stop;
|
|
||||||
}
|
|
||||||
|
|
||||||
if environment :matches "imap.user" "*" {
|
|
||||||
set "username" "${1}";
|
|
||||||
}
|
|
||||||
|
|
||||||
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
|
|
|
@ -1,7 +0,0 @@
|
||||||
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
|
|
||||||
|
|
||||||
if environment :matches "imap.user" "*" {
|
|
||||||
set "username" "${1}";
|
|
||||||
}
|
|
||||||
|
|
||||||
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -o errexit
|
|
||||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
set -o errexit
|
|
||||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -22,7 +22,7 @@ in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
dovecot opendkim openssh postfix rspamd
|
dovecot opendkim openssh postfix clamav rspamd rmilter
|
||||||
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
|
] ++ (if certificateScheme == 2 then [ openssl ] else []);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = lib.mkIf (cfg.enable && cfg.monitoring.enable) {
|
|
||||||
services.monit = {
|
|
||||||
enable = true;
|
|
||||||
config = ''
|
|
||||||
set alert ${cfg.monitoring.alertAddress}
|
|
||||||
${cfg.monitoring.config}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -14,24 +14,21 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf (enable && openFirewall) {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
allowedTCPPorts = [ 25 ]
|
allowedTCPPorts = [ 25 587 ]
|
||||||
++ lib.optional enableSubmission 587
|
|
||||||
++ lib.optional enableSubmissionSsl 465
|
|
||||||
++ lib.optional enableImap 143
|
++ lib.optional enableImap 143
|
||||||
++ lib.optional enableImapSsl 993
|
++ lib.optional enableImapSsl 993
|
||||||
++ lib.optional enablePop3 110
|
++ lib.optional enablePop3 110
|
||||||
++ lib.optional enablePop3Ssl 995
|
++ lib.optional enablePop3Ssl 995
|
||||||
++ lib.optional enableManageSieve 4190
|
++ lib.optional (certificateScheme == 3) 80;
|
||||||
++ lib.optional (certificateScheme == "acme-nginx") 80;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -17,26 +17,28 @@
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with (import ./common.nix { inherit config lib pkgs; });
|
with (import ./common.nix { inherit config; });
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
acmeRoot = "/var/lib/acme/acme-challenge";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
|
config = lib.mkIf (cfg.certificateScheme == 3) {
|
||||||
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
|
services.nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
virtualHosts."${cfg.fqdn}" = {
|
virtualHosts."${cfg.fqdn}" = {
|
||||||
serverName = cfg.fqdn;
|
serverName = cfg.fqdn;
|
||||||
serverAliases = cfg.certificateDomains;
|
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
|
acmeRoot = acmeRoot;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
|
security.acme.certs."${cfg.fqdn}".postRun = ''
|
||||||
"postfix.service"
|
systemctl reload nginx
|
||||||
"dovecot2.service"
|
systemctl reload postfix
|
||||||
];
|
systemctl reload dovecot2
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2017 Brian Olsen
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
|
|
||||||
dkimUser = config.services.opendkim.user;
|
|
||||||
dkimGroup = config.services.opendkim.group;
|
|
||||||
|
|
||||||
createDomainDkimCert = dom:
|
|
||||||
let
|
|
||||||
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
|
|
||||||
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
|
|
||||||
in
|
|
||||||
''
|
|
||||||
if [ ! -f "${dkim_key}" ]
|
|
||||||
then
|
|
||||||
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
|
||||||
-d "${dom}" \
|
|
||||||
--bits="${toString cfg.dkimKeyBits}" \
|
|
||||||
--directory="${cfg.dkimKeyDirectory}"
|
|
||||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
|
|
||||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
|
|
||||||
chmod 644 "${dkim_txt}"
|
|
||||||
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
|
|
||||||
|
|
||||||
keyTable = pkgs.writeText "opendkim-KeyTable"
|
|
||||||
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
|
|
||||||
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
|
|
||||||
signingTable = pkgs.writeText "opendkim-SigningTable"
|
|
||||||
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
|
|
||||||
|
|
||||||
dkim = config.services.opendkim;
|
|
||||||
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = mkIf (cfg.dkimSigning && cfg.enable) {
|
|
||||||
services.opendkim = {
|
|
||||||
enable = true;
|
|
||||||
selector = cfg.dkimSelector;
|
|
||||||
keyPath = cfg.dkimKeyDirectory;
|
|
||||||
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
|
|
||||||
configFile = pkgs.writeText "opendkim.conf" (''
|
|
||||||
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
|
|
||||||
UMask 0002
|
|
||||||
Socket ${dkim.socket}
|
|
||||||
KeyTable file:${keyTable}
|
|
||||||
SigningTable file:${signingTable}
|
|
||||||
'' + (lib.optionalString cfg.debug ''
|
|
||||||
Syslog yes
|
|
||||||
SyslogSuccess yes
|
|
||||||
LogWhy yes
|
|
||||||
''));
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
|
|
||||||
postfix.extraGroups = [ "${dkimGroup}" ];
|
|
||||||
};
|
|
||||||
systemd.services.opendkim = {
|
|
||||||
preStart = lib.mkForce createAllCerts;
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
|
|
||||||
PermissionsStartOnly = lib.mkForce false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
146
mail-server/opensmtpd.nix
Normal file
146
mail-server/opensmtpd.nix
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
# nixos-mailserver: a simple mail server
|
||||||
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with (import ./common.nix { inherit config; });
|
||||||
|
|
||||||
|
let
|
||||||
|
inherit (lib.strings) concatStringsSep;
|
||||||
|
cfg = config.mailserver;
|
||||||
|
|
||||||
|
# valiases_postfix :: [ String ]
|
||||||
|
valiases_postfix = lib.flatten (lib.mapAttrsToList
|
||||||
|
(name: value:
|
||||||
|
let to = name;
|
||||||
|
in map (from: "${from} ${to}") value.aliases)
|
||||||
|
cfg.loginAccounts);
|
||||||
|
|
||||||
|
vmailMaps = lib.flatten (lib.mapAttrsToList
|
||||||
|
(name: value: "${name} ${cfg.vmailUserName}") cfg.loginAccounts);
|
||||||
|
|
||||||
|
# catchAllPostfix :: [ String ]
|
||||||
|
catchAllPostfix = lib.flatten (lib.mapAttrsToList
|
||||||
|
(name: value:
|
||||||
|
let to = name;
|
||||||
|
in map (from: "@${from} ${to}") value.catchAll)
|
||||||
|
cfg.loginAccounts);
|
||||||
|
|
||||||
|
# extra_valiases_postfix :: [ String ]
|
||||||
|
# TODO: Remove virtualAliases when deprecated -> removed
|
||||||
|
extra_valiases_postfix = (map
|
||||||
|
(from:
|
||||||
|
let to = cfg.virtualAliases.${from};
|
||||||
|
in "${from} ${to}")
|
||||||
|
(builtins.attrNames cfg.virtualAliases))
|
||||||
|
++
|
||||||
|
(map
|
||||||
|
(from:
|
||||||
|
let to = cfg.extraVirtualAliases.${from};
|
||||||
|
in "${from} ${to}")
|
||||||
|
(builtins.attrNames cfg.extraVirtualAliases));
|
||||||
|
|
||||||
|
# all_valiases_postfix :: [ String ]
|
||||||
|
all_valiases_postfix = valiases_postfix ++ extra_valiases_postfix ++ catchAllPostfix ++ vmailMaps;
|
||||||
|
|
||||||
|
# accountToIdentity :: User -> String
|
||||||
|
accountToIdentity = account: "${account.name} ${account.name}";
|
||||||
|
|
||||||
|
# vaccounts_identity :: [ String ]
|
||||||
|
vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts);
|
||||||
|
|
||||||
|
# valiases_file :: Path
|
||||||
|
valiases_file = builtins.toFile "valias"
|
||||||
|
(lib.concatStringsSep "\n" all_valiases_postfix);
|
||||||
|
|
||||||
|
# vhosts_file :: Path
|
||||||
|
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
||||||
|
|
||||||
|
passwdList = lib.flatten (lib.mapAttrsToList (name : value:
|
||||||
|
"${name}:${value.hashedPassword}:5000:5000::/var/vmail/:/run/current-system/sw/bin/nologin")
|
||||||
|
cfg.loginAccounts);
|
||||||
|
passwd = lib.concatStringsSep "\n" passwdList;
|
||||||
|
|
||||||
|
|
||||||
|
example =
|
||||||
|
''
|
||||||
|
user1@example.com:$6$IsXn9Xe2kUTPETVl$Z.gkkqpwi95/ZsL/FXZaAjMjdv03m5jae6v8Pv7aaNnzdzNd01nbgt3HtKnaS10hZTbXgumqdQyTU0m1wkr76.:5000:5000::/var/vmail:/run/current-system/sw/bin/nologin
|
||||||
|
'';
|
||||||
|
|
||||||
|
passwd_file = builtins.toFile "passwd" passwd;
|
||||||
|
|
||||||
|
# vaccounts_file :: Path
|
||||||
|
# see
|
||||||
|
# https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/
|
||||||
|
# for details on how this file looks. By using the same file as valiases,
|
||||||
|
# every alias is owned (uniquely) by its user. We have to add the users own
|
||||||
|
# address though
|
||||||
|
vaccounts_file = builtins.toFile "vaccounts" (lib.concatStringsSep "\n"
|
||||||
|
(vaccounts_identity ++ all_valiases_postfix));
|
||||||
|
|
||||||
|
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
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
|
services.opensmtpd = {
|
||||||
|
enable = true;
|
||||||
|
procPackages = [ pkgs.opensmtpd-extras ];
|
||||||
|
extraServerArgs = [ "-v" ];
|
||||||
|
serverConfiguration =
|
||||||
|
''
|
||||||
|
# pki setup
|
||||||
|
pki ${fqdn} certificate "${certificatePath}"
|
||||||
|
pki ${fqdn} key "${keyPath}"
|
||||||
|
|
||||||
|
# tables setup
|
||||||
|
# table aliases file:/etc/mail/aliases
|
||||||
|
table domains file:${vhosts_file}
|
||||||
|
table passwd passwd:${passwd_file}
|
||||||
|
table virtuals file:${valiases_file}
|
||||||
|
|
||||||
|
# # listen ports setup
|
||||||
|
listen on 0.0.0.0 port 25 tls pki ${fqdn}
|
||||||
|
listen on 0.0.0.0 port 587 tls-require pki ${fqdn} auth <passwd> received-auth
|
||||||
|
|
||||||
|
# allow local messages
|
||||||
|
accept from any for domain <domains> virtual <virtuals> deliver to lmtp "/run/dovecot/lmtp" rcpt-to
|
||||||
|
|
||||||
|
# DKIM
|
||||||
|
listen on lo hostname ${fqdn}
|
||||||
|
listen on lo port 10028 tag DKIM hostname ${fqdn}
|
||||||
|
|
||||||
|
accept tagged DKIM \
|
||||||
|
for any \
|
||||||
|
relay \
|
||||||
|
hostname ${fqdn}
|
||||||
|
accept from local \
|
||||||
|
for any \
|
||||||
|
relay via smtp://127.0.0.1:10027
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,46 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) {
|
|
||||||
systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
|
|
||||||
# Checks whether the "current" kernel is different from the booted kernel
|
|
||||||
# and then triggers a reboot so that the "current" kernel will be the booted one.
|
|
||||||
# This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos.
|
|
||||||
|
|
||||||
current=$(readlink -f /run/current-system/kernel)
|
|
||||||
booted=$(readlink -f /run/booted-system/kernel)
|
|
||||||
|
|
||||||
if [ "$current" == "$booted" ]; then
|
|
||||||
echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check";
|
|
||||||
else
|
|
||||||
echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check"
|
|
||||||
echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
|
|
||||||
echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
|
|
||||||
${cfg.rebootAfterKernelUpgrade.method}
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,82 +16,53 @@
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with (import ./common.nix { inherit config pkgs lib; });
|
with (import ./common.nix { inherit config; });
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib.strings) concatStringsSep;
|
inherit (lib.strings) concatStringsSep;
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
# Merge several lookup tables. A lookup table is a attribute set where
|
# valiases_postfix :: [ String ]
|
||||||
# - the key is an address (user@example.com) or a domain (@example.com)
|
valiases_postfix = lib.flatten (lib.mapAttrsToList
|
||||||
# - the value is a list of addresses
|
|
||||||
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
|
|
||||||
|
|
||||||
# valiases_postfix :: Map String [String]
|
|
||||||
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
|
||||||
(name: value:
|
(name: value:
|
||||||
let to = name;
|
let to = name;
|
||||||
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
in map (from: "${from} ${to}") (value.aliases ++ lib.singleton name))
|
||||||
cfg.loginAccounts));
|
cfg.loginAccounts);
|
||||||
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
|
||||||
|
# catchAllPostfix :: [ String ]
|
||||||
|
catchAllPostfix = lib.flatten (lib.mapAttrsToList
|
||||||
(name: value:
|
(name: value:
|
||||||
let to = name;
|
let to = name;
|
||||||
in map (from: {"${from}" = to;}) value.aliasesRegexp)
|
in map (from: "@${from} ${to}") value.catchAll)
|
||||||
cfg.loginAccounts));
|
cfg.loginAccounts);
|
||||||
|
|
||||||
# catchAllPostfix :: Map String [String]
|
# extra_valiases_postfix :: [ String ]
|
||||||
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
# TODO: Remove virtualAliases when deprecated -> removed
|
||||||
(name: value:
|
extra_valiases_postfix = (map
|
||||||
let to = name;
|
(from:
|
||||||
in map (from: {"@${from}" = to;}) value.catchAll)
|
let to = cfg.virtualAliases.${from};
|
||||||
cfg.loginAccounts));
|
in "${from} ${to}")
|
||||||
|
(builtins.attrNames cfg.virtualAliases))
|
||||||
|
++
|
||||||
|
(map
|
||||||
|
(from:
|
||||||
|
let to = cfg.extraVirtualAliases.${from};
|
||||||
|
in "${from} ${to}")
|
||||||
|
(builtins.attrNames cfg.extraVirtualAliases));
|
||||||
|
|
||||||
# all_valiases_postfix :: Map String [String]
|
# all_valiases_postfix :: [ String ]
|
||||||
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
|
all_valiases_postfix = valiases_postfix ++ extra_valiases_postfix;
|
||||||
|
|
||||||
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
|
# accountToIdentity :: User -> String
|
||||||
attrsToLookupTable = aliases: let
|
accountToIdentity = account: "${account.name} ${account.name}";
|
||||||
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
|
|
||||||
in mergeLookupTables lookupTables;
|
|
||||||
|
|
||||||
# extra_valiases_postfix :: Map String [String]
|
# vaccounts_identity :: [ String ]
|
||||||
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
|
vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts);
|
||||||
|
|
||||||
# forwards :: Map String [String]
|
|
||||||
forwards = attrsToLookupTable cfg.forwards;
|
|
||||||
|
|
||||||
# lookupTableToString :: Map String [String] -> String
|
|
||||||
lookupTableToString = attrs: let
|
|
||||||
valueToString = value: lib.concatStringsSep ", " value;
|
|
||||||
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
|
|
||||||
|
|
||||||
# valiases_file :: Path
|
# valiases_file :: Path
|
||||||
valiases_file = let
|
valiases_file = builtins.toFile "valias"
|
||||||
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
(lib.concatStringsSep "\n" (all_valiases_postfix ++
|
||||||
in builtins.toFile "valias" content;
|
catchAllPostfix));
|
||||||
|
|
||||||
regex_valiases_file = let
|
|
||||||
content = lookupTableToString regex_valiases_postfix;
|
|
||||||
in builtins.toFile "regex_valias" content;
|
|
||||||
|
|
||||||
# denied_recipients_postfix :: [ String ]
|
|
||||||
denied_recipients_postfix = (map
|
|
||||||
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
|
||||||
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
|
|
||||||
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
|
|
||||||
|
|
||||||
reject_senders_postfix = (map
|
|
||||||
(sender:
|
|
||||||
"${sender} REJECT")
|
|
||||||
(cfg.rejectSender));
|
|
||||||
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
|
|
||||||
|
|
||||||
reject_recipients_postfix = (map
|
|
||||||
(recipient:
|
|
||||||
"${recipient} REJECT")
|
|
||||||
(cfg.rejectRecipients));
|
|
||||||
# rejectRecipients :: [ Path ]
|
|
||||||
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
|
|
||||||
|
|
||||||
# vhosts_file :: Path
|
# vhosts_file :: Path
|
||||||
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
||||||
|
@ -100,12 +71,12 @@ let
|
||||||
# see
|
# see
|
||||||
# https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/
|
# https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/
|
||||||
# for details on how this file looks. By using the same file as valiases,
|
# for details on how this file looks. By using the same file as valiases,
|
||||||
# every alias is owned (uniquely) by its user.
|
# every alias is owned (uniquely) by its user. We have to add the users own
|
||||||
# The user's own address is already in all_valiases_postfix.
|
# address though
|
||||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
vaccounts_file = builtins.toFile "vaccounts" (lib.concatStringsSep "\n"
|
||||||
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
|
(vaccounts_identity ++ all_valiases_postfix));
|
||||||
|
|
||||||
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ''
|
||||||
# Removes sensitive headers from mails handed in via the submission port.
|
# Removes sensitive headers from mails handed in via the submission port.
|
||||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||||
# Uses "pcre" style regex.
|
# Uses "pcre" style regex.
|
||||||
|
@ -115,227 +86,84 @@ let
|
||||||
/^X-Mailer:/ IGNORE
|
/^X-Mailer:/ IGNORE
|
||||||
/^User-Agent:/ IGNORE
|
/^User-Agent:/ IGNORE
|
||||||
/^X-Enigmail:/ 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.
|
|
||||||
|
|
||||||
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
|
||||||
'');
|
|
||||||
|
|
||||||
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
|
|
||||||
unixSocket = sock: "unix:${sock}";
|
|
||||||
|
|
||||||
smtpdMilters =
|
|
||||||
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
|
|
||||||
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
|
||||||
|
|
||||||
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
|
|
||||||
|
|
||||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
|
||||||
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
|
||||||
|
|
||||||
submissionOptions =
|
|
||||||
{
|
|
||||||
smtpd_tls_security_level = "encrypt";
|
|
||||||
smtpd_sasl_auth_enable = "yes";
|
|
||||||
smtpd_sasl_type = "dovecot";
|
|
||||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
|
||||||
smtpd_sasl_security_options = "noanonymous";
|
|
||||||
smtpd_sasl_local_domain = "$myhostname";
|
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
|
||||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}";
|
|
||||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
|
||||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
|
||||||
cleanup_service_name = "submission-header-cleanup";
|
|
||||||
};
|
|
||||||
|
|
||||||
commonLdapConfig = ''
|
|
||||||
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
|
||||||
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
|
||||||
version = 3
|
|
||||||
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
|
||||||
tls_require_cert = yes
|
|
||||||
|
|
||||||
search_base = ${cfg.ldap.searchBase}
|
|
||||||
scope = ${cfg.ldap.searchScope}
|
|
||||||
|
|
||||||
bind = yes
|
|
||||||
bind_dn = ${cfg.ldap.bind.dn}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
|
|
||||||
${commonLdapConfig}
|
|
||||||
query_filter = ${cfg.ldap.postfix.filter}
|
|
||||||
result_attribute = ${cfg.ldap.postfix.mailAttribute}
|
|
||||||
'';
|
|
||||||
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
|
||||||
appendPwdInSenderLoginMap = appendLdapBindPwd {
|
|
||||||
name = "ldap-sender-login-map";
|
|
||||||
file = ldapSenderLoginMap;
|
|
||||||
prefix = "bind_pw = ";
|
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
|
||||||
destination = ldapSenderLoginMapFile;
|
|
||||||
};
|
|
||||||
|
|
||||||
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
|
|
||||||
${commonLdapConfig}
|
|
||||||
query_filter = ${cfg.ldap.postfix.filter}
|
|
||||||
result_attribute = ${cfg.ldap.postfix.uidAttribute}
|
|
||||||
'';
|
|
||||||
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
|
|
||||||
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
|
|
||||||
name = "ldap-virtual-mailbox-map";
|
|
||||||
file = ldapVirtualMailboxMap;
|
|
||||||
prefix = "bind_pw = ";
|
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
|
||||||
destination = ldapVirtualMailboxMapFile;
|
|
||||||
};
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
|
||||||
preStart = ''
|
|
||||||
${appendPwdInVirtualMailboxMap}
|
|
||||||
${appendPwdInSenderLoginMap}
|
|
||||||
'';
|
|
||||||
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
|
|
||||||
};
|
|
||||||
|
|
||||||
services.postfix = {
|
services.postfix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostname = "${sendingFqdn}";
|
hostname = "${fqdn}";
|
||||||
networksStyle = "host";
|
networksStyle = "host";
|
||||||
mapFiles."valias" = valiases_file;
|
mapFiles."valias" = valiases_file;
|
||||||
mapFiles."regex_valias" = regex_valiases_file;
|
|
||||||
mapFiles."vaccounts" = vaccounts_file;
|
mapFiles."vaccounts" = vaccounts_file;
|
||||||
mapFiles."regex_vaccounts" = regex_vaccounts_file;
|
|
||||||
mapFiles."denied_recipients" = denied_recipients_file;
|
|
||||||
mapFiles."reject_senders" = reject_senders_file;
|
|
||||||
mapFiles."reject_recipients" = reject_recipients_file;
|
|
||||||
sslCert = certificatePath;
|
sslCert = certificatePath;
|
||||||
sslKey = keyPath;
|
sslKey = keyPath;
|
||||||
enableSubmission = cfg.enableSubmission;
|
enableSubmission = true;
|
||||||
enableSubmissions = cfg.enableSubmissionSsl;
|
|
||||||
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
|
|
||||||
|
|
||||||
config = {
|
extraConfig =
|
||||||
|
''
|
||||||
# Extra Config
|
# Extra Config
|
||||||
mydestination = "";
|
mydestination = localhost
|
||||||
recipient_delimiter = cfg.recipientDelimiter;
|
|
||||||
smtpd_banner = "${fqdn} ESMTP NO UCE";
|
smtpd_banner = ${fqdn} ESMTP NO UCE
|
||||||
disable_vrfy_command = true;
|
disable_vrfy_command = yes
|
||||||
message_size_limit = toString cfg.messageSizeLimit;
|
message_size_limit = 20971520
|
||||||
|
|
||||||
# virtual mail system
|
# virtual mail system
|
||||||
virtual_uid_maps = "static:5000";
|
virtual_uid_maps = static:5000
|
||||||
virtual_gid_maps = "static:5000";
|
virtual_gid_maps = static:5000
|
||||||
virtual_mailbox_base = mailDirectory;
|
virtual_mailbox_base = ${mailDirectory}
|
||||||
virtual_mailbox_domains = vhosts_file;
|
virtual_mailbox_domains = ${vhosts_file}
|
||||||
virtual_mailbox_maps = [
|
virtual_alias_maps = hash:/var/lib/postfix/conf/valias
|
||||||
(mappedFile "valias")
|
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||||
] ++ lib.optionals (cfg.ldap.enable) [
|
|
||||||
"ldap:${ldapVirtualMailboxMapFile}"
|
|
||||||
] ++ lib.optionals (regex_valiases_postfix != {}) [
|
|
||||||
(mappedRegexFile "regex_valias")
|
|
||||||
];
|
|
||||||
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
|
|
||||||
(mappedRegexFile "regex_valias")
|
|
||||||
]);
|
|
||||||
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
|
||||||
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
|
|
||||||
lmtp_destination_recipient_limit = "1";
|
|
||||||
|
|
||||||
# sasl with dovecot
|
# sasl with dovecot
|
||||||
smtpd_sasl_type = "dovecot";
|
smtpd_sasl_type = dovecot
|
||||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
smtpd_sasl_path = private/auth
|
||||||
smtpd_sasl_auth_enable = true;
|
smtpd_sasl_auth_enable = yes
|
||||||
smtpd_relay_restrictions = [
|
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 = [
|
|
||||||
"check_recipient_access ${mappedFile "denied_recipients"}"
|
|
||||||
"check_recipient_access ${mappedFile "reject_recipients"}"
|
|
||||||
"check_policy_service inet:localhost:12340"
|
|
||||||
"check_policy_service unix:private/policy-spf"
|
|
||||||
];
|
|
||||||
|
|
||||||
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
||||||
# Submission by mail clients is handled in submissionOptions
|
# Submission by mail clients is handled in submissionOptions
|
||||||
smtpd_tls_security_level = "may";
|
smtpd_tls_security_level = may
|
||||||
|
# strong might suffice and is computationally less expensive
|
||||||
# Disable obselete protocols
|
smtpd_tls_eecdh_grade = ultra
|
||||||
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
# Disable predecessors to TLS
|
||||||
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
smtpd_tls_protocols = !SSLv2, !SSLv3
|
||||||
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
|
||||||
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
|
||||||
|
|
||||||
smtp_tls_ciphers = "high";
|
|
||||||
smtpd_tls_ciphers = "high";
|
|
||||||
smtp_tls_mandatory_ciphers = "high";
|
|
||||||
smtpd_tls_mandatory_ciphers = "high";
|
|
||||||
|
|
||||||
# Disable deprecated ciphers
|
|
||||||
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
|
|
||||||
tls_preempt_cipherlist = true;
|
|
||||||
|
|
||||||
# Allowing AUTH on a non encrypted connection poses a security risk
|
# Allowing AUTH on a non encrypted connection poses a security risk
|
||||||
smtpd_tls_auth_only = true;
|
smtpd_tls_auth_only = yes
|
||||||
# Log only a summary message on TLS handshake completion
|
# Log only a summary message on TLS handshake completion
|
||||||
smtpd_tls_loglevel = "1";
|
smtpd_tls_loglevel = 1
|
||||||
|
|
||||||
|
# Disable weak ciphers as reported by https://ssl-tools.net
|
||||||
|
# https://serverfault.com/questions/744168/how-to-disable-rc4-on-postfix
|
||||||
|
smtpd_tls_exclude_ciphers = RC4, aNULL
|
||||||
|
smtp_tls_exclude_ciphers = RC4, aNULL
|
||||||
|
|
||||||
# Configure a non blocking source of randomness
|
# Configure a non blocking source of randomness
|
||||||
tls_random_source = "dev:/dev/urandom";
|
tls_random_source = dev:/dev/urandom
|
||||||
|
'';
|
||||||
|
|
||||||
smtpd_milters = smtpdMilters;
|
submissionOptions =
|
||||||
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"];
|
{
|
||||||
milter_protocol = "6";
|
smtpd_tls_security_level = "encrypt";
|
||||||
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
smtpd_sasl_auth_enable = "yes";
|
||||||
|
smtpd_sasl_type = "dovecot";
|
||||||
# Fix for https://www.postfix.org/smtp-smuggling.html
|
smtpd_sasl_path = "private/auth";
|
||||||
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
|
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";
|
||||||
};
|
};
|
||||||
|
|
||||||
submissionOptions = submissionOptions;
|
extraMasterConf = ''
|
||||||
submissionsOptions = submissionOptions;
|
submission-header-cleanup unix n - n - 0 cleanup
|
||||||
|
-o header_checks=pcre:${submissionHeaderCleanupRules}
|
||||||
masterConfig = {
|
'';
|
||||||
"lmtp" = {
|
|
||||||
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
|
||||||
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
|
||||||
args = [ "flags=O" ];
|
|
||||||
};
|
|
||||||
"policy-spf" = {
|
|
||||||
type = "unix";
|
|
||||||
privileged = true;
|
|
||||||
chroot = false;
|
|
||||||
command = "spawn";
|
|
||||||
args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
|
|
||||||
};
|
|
||||||
"submission-header-cleanup" = {
|
|
||||||
type = "unix";
|
|
||||||
private = false;
|
|
||||||
chroot = false;
|
|
||||||
maxproc = 0;
|
|
||||||
command = "cleanup";
|
|
||||||
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
73
mail-server/rmilter.nix
Normal file
73
mail-server/rmilter.nix
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
# nixos-mailserver: a simple mail server
|
||||||
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.mailserver;
|
||||||
|
|
||||||
|
clamav = if cfg.virusScanning
|
||||||
|
then
|
||||||
|
''
|
||||||
|
clamav {
|
||||||
|
servers = /var/run/clamav/clamd.ctl;
|
||||||
|
};
|
||||||
|
''
|
||||||
|
else "";
|
||||||
|
dkim = if cfg.dkimSigning
|
||||||
|
# Note: domain = "*"; causes Rmilter to try to search key in the key path
|
||||||
|
# as keypath/domain.selector.key for any domain.
|
||||||
|
then
|
||||||
|
''
|
||||||
|
dkim {
|
||||||
|
domain {
|
||||||
|
key = "${cfg.dkimKeyDirectory}";
|
||||||
|
domain = "*";
|
||||||
|
selector = "${cfg.dkimSelector}";
|
||||||
|
};
|
||||||
|
sign_alg = sha256;
|
||||||
|
auth_only = yes;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
else "";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
config = with cfg; lib.mkIf enable {
|
||||||
|
services.rspamd = {
|
||||||
|
enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.rmilter = {
|
||||||
|
inherit debug;
|
||||||
|
enable = true;
|
||||||
|
postfix.enable = true;
|
||||||
|
rspamd = {
|
||||||
|
enable = true;
|
||||||
|
extraConfig = "extended_spam_headers = yes;";
|
||||||
|
};
|
||||||
|
extraConfig =
|
||||||
|
''
|
||||||
|
use_redis = true;
|
||||||
|
max_size = 20M;
|
||||||
|
|
||||||
|
${clamav}
|
||||||
|
|
||||||
|
${dkim}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
|
|
||||||
preexecDefined = cfg.backup.cmdPreexec != null;
|
|
||||||
preexecWrapped = pkgs.writeScript "rsnapshot-preexec.sh" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
set -e
|
|
||||||
|
|
||||||
${cfg.backup.cmdPreexec}
|
|
||||||
'';
|
|
||||||
preexecString = optionalString preexecDefined "cmd_preexec ${preexecWrapped}";
|
|
||||||
|
|
||||||
postexecDefined = cfg.backup.cmdPostexec != null;
|
|
||||||
postexecWrapped = pkgs.writeScript "rsnapshot-postexec.sh" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
set -e
|
|
||||||
|
|
||||||
${cfg.backup.cmdPostexec}
|
|
||||||
'';
|
|
||||||
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
|
||||||
in {
|
|
||||||
config = mkIf (cfg.enable && cfg.backup.enable) {
|
|
||||||
services.rsnapshot = {
|
|
||||||
enable = true;
|
|
||||||
cronIntervals = cfg.backup.cronIntervals;
|
|
||||||
# rsnapshot expects intervals shortest first, e.g. hourly first, then daily.
|
|
||||||
# tabs must separate all elements
|
|
||||||
extraConfig = ''
|
|
||||||
${preexecString}
|
|
||||||
${postexecString}
|
|
||||||
snapshot_root ${cfg.backup.snapshotRoot}/
|
|
||||||
retain hourly ${toString cfg.backup.retain.hourly}
|
|
||||||
retain daily ${toString cfg.backup.retain.daily}
|
|
||||||
retain weekly ${toString cfg.backup.retain.weekly}
|
|
||||||
backup ${cfg.mailDirectory}/ localhost/
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,180 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
|
||||||
rspamdCfg = config.services.rspamd;
|
|
||||||
rspamdSocket = "rspamd.service";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = with cfg; lib.mkIf enable {
|
|
||||||
services.rspamd = {
|
|
||||||
enable = true;
|
|
||||||
inherit debug;
|
|
||||||
locals = {
|
|
||||||
"milter_headers.conf" = { text = ''
|
|
||||||
extended_spam_headers = true;
|
|
||||||
''; };
|
|
||||||
"redis.conf" = { text = ''
|
|
||||||
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
|
|
||||||
'' + (lib.optionalString (cfg.redis.password != null) ''
|
|
||||||
password = "${cfg.redis.password}";
|
|
||||||
''); };
|
|
||||||
"classifier-bayes.conf" = { text = ''
|
|
||||||
cache {
|
|
||||||
backend = "redis";
|
|
||||||
}
|
|
||||||
''; };
|
|
||||||
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
|
|
||||||
clamav {
|
|
||||||
action = "reject";
|
|
||||||
symbol = "CLAM_VIRUS";
|
|
||||||
type = "clamav";
|
|
||||||
log_clean = true;
|
|
||||||
servers = "/run/clamav/clamd.ctl";
|
|
||||||
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
|
||||||
}
|
|
||||||
''; };
|
|
||||||
"dkim_signing.conf" = { text = ''
|
|
||||||
# Disable outbound email signing, we use opendkim for this
|
|
||||||
enabled = false;
|
|
||||||
''; };
|
|
||||||
"dmarc.conf" = { text = ''
|
|
||||||
${lib.optionalString cfg.dmarcReporting.enable ''
|
|
||||||
reporting {
|
|
||||||
enabled = true;
|
|
||||||
email = "${cfg.dmarcReporting.email}";
|
|
||||||
domain = "${cfg.dmarcReporting.domain}";
|
|
||||||
org_name = "${cfg.dmarcReporting.organizationName}";
|
|
||||||
from_name = "${cfg.dmarcReporting.fromName}";
|
|
||||||
msgid_from = "dmarc-rua";
|
|
||||||
}''}
|
|
||||||
''; };
|
|
||||||
};
|
|
||||||
|
|
||||||
workers.rspamd_proxy = {
|
|
||||||
type = "rspamd_proxy";
|
|
||||||
bindSockets = [{
|
|
||||||
socket = "/run/rspamd/rspamd-milter.sock";
|
|
||||||
mode = "0664";
|
|
||||||
}];
|
|
||||||
count = 1; # Do not spawn too many processes of this type
|
|
||||||
extraConfig = ''
|
|
||||||
milter = yes; # Enable milter mode
|
|
||||||
timeout = 120s; # Needed for Milter usually
|
|
||||||
|
|
||||||
upstream "local" {
|
|
||||||
default = yes; # Self-scan upstreams are always default
|
|
||||||
self_scan = yes; # Enable self-scan
|
|
||||||
}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
workers.controller = {
|
|
||||||
type = "controller";
|
|
||||||
count = 1;
|
|
||||||
bindSockets = [{
|
|
||||||
socket = "/run/rspamd/worker-controller.sock";
|
|
||||||
mode = "0666";
|
|
||||||
}];
|
|
||||||
includes = [];
|
|
||||||
extraConfig = ''
|
|
||||||
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
services.redis.servers.rspamd = {
|
|
||||||
enable = lib.mkDefault true;
|
|
||||||
port = lib.mkDefault 6380;
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.rspamd = {
|
|
||||||
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
|
||||||
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
|
|
||||||
# Explicitly select yesterday's date to work around broken
|
|
||||||
# default behaviour when called without a date.
|
|
||||||
# https://github.com/rspamd/rspamd/issues/4062
|
|
||||||
script = ''
|
|
||||||
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
|
|
||||||
'';
|
|
||||||
serviceConfig = {
|
|
||||||
User = "${config.services.rspamd.user}";
|
|
||||||
Group = "${config.services.rspamd.group}";
|
|
||||||
|
|
||||||
AmbientCapabilities = [];
|
|
||||||
CapabilityBoundingSet = "";
|
|
||||||
DevicePolicy = "closed";
|
|
||||||
IPAddressAllow = "localhost";
|
|
||||||
LockPersonality = true;
|
|
||||||
NoNewPrivileges = true;
|
|
||||||
PrivateDevices = true;
|
|
||||||
PrivateMounts = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
PrivateUsers = true;
|
|
||||||
ProtectClock = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectHostname = true;
|
|
||||||
ProtectKernelLogs = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectProc = "invisible";
|
|
||||||
ProcSubset = "pid";
|
|
||||||
ProtectSystem = "strict";
|
|
||||||
RemoveIPC = true;
|
|
||||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
|
||||||
RestrictNamespaces = true;
|
|
||||||
RestrictRealtime = true;
|
|
||||||
RestrictSUIDSGID = true;
|
|
||||||
SystemCallArchitectures = "native";
|
|
||||||
SystemCallFilter = [
|
|
||||||
"@system-service"
|
|
||||||
"~@privileged"
|
|
||||||
];
|
|
||||||
UMask = "0077";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
|
|
||||||
description = "Daily delivery of aggregated DMARC reports";
|
|
||||||
wantedBy = [
|
|
||||||
"timers.target"
|
|
||||||
];
|
|
||||||
timerConfig = {
|
|
||||||
OnCalendar = "daily";
|
|
||||||
Persistent = true;
|
|
||||||
RandomizedDelaySec = 86400;
|
|
||||||
FixedRandomDelay = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.services.postfix = {
|
|
||||||
after = [ rspamdSocket ];
|
|
||||||
requires = [ rspamdSocket ];
|
|
||||||
};
|
|
||||||
|
|
||||||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,14 +14,31 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
|
# cert :: PATH
|
||||||
|
cert = if cfg.certificateScheme == 1
|
||||||
|
then cfg.certificateFile
|
||||||
|
else if cfg.certificateScheme == 2
|
||||||
|
then "${cfg.certificateDirectory}/cert-${cfg.fqdn.pem"
|
||||||
|
else "";
|
||||||
|
|
||||||
|
# key :: PATH
|
||||||
|
key = if cfg.certificateScheme == 1
|
||||||
|
then cfg.keyFile
|
||||||
|
else if cfg.certificateScheme == 2
|
||||||
|
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||||
|
else "";
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
|
|
||||||
services.kresd.enable = true;
|
imports = [
|
||||||
};
|
./rmilter.nix
|
||||||
|
./postfix.nix key
|
||||||
|
./dovecot.nix
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -18,68 +18,89 @@
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
certificatesDeps =
|
|
||||||
if cfg.certificateScheme == "manual" then
|
create_certificate = if cfg.certificateScheme == 2 then
|
||||||
[]
|
''
|
||||||
else if cfg.certificateScheme == "selfsigned" then
|
# Create certificates if they do not exist yet
|
||||||
[ "mailserver-selfsigned-certificate.service" ]
|
dir="${cfg.certificateDirectory}"
|
||||||
else
|
fqdn="${cfg.fqdn}"
|
||||||
[ "acme-finished-${cfg.fqdn}.target" ];
|
case $fqdn in /*) fqdn=$(cat "$fqdn");; esac
|
||||||
|
key="''${dir}/key-${cfg.fqdn}.pem";
|
||||||
|
cert="''${dir}/cert-${cfg.fqdn}.pem";
|
||||||
|
|
||||||
|
if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ]
|
||||||
|
then
|
||||||
|
mkdir -p "${cfg.certificateDirectory}"
|
||||||
|
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) &&
|
||||||
|
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \
|
||||||
|
-days 3650 -out "''${cert}"
|
||||||
|
fi
|
||||||
|
''
|
||||||
|
else "";
|
||||||
|
|
||||||
|
createDomainDkimCert = dom:
|
||||||
|
let
|
||||||
|
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
|
||||||
|
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
|
||||||
|
in
|
||||||
|
''
|
||||||
|
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
|
||||||
|
then
|
||||||
|
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
||||||
|
-d "${dom}" \
|
||||||
|
--directory="${cfg.dkimKeyDirectory}"
|
||||||
|
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
|
||||||
|
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
|
||||||
|
create_dkim_cert =
|
||||||
|
''
|
||||||
|
# Create dkim dir
|
||||||
|
mkdir -p "${cfg.dkimKeyDirectory}"
|
||||||
|
chown rmilter:rmilter "${cfg.dkimKeyDirectory}"
|
||||||
|
|
||||||
|
${createAllCerts}
|
||||||
|
|
||||||
|
chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}"
|
||||||
|
'';
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
# Create self signed certificate
|
# Make sure postfix gets started first, so that the certificates are in place
|
||||||
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";
|
|
||||||
|
|
||||||
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 = {
|
systemd.services.dovecot2 = {
|
||||||
wants = certificatesDeps;
|
after = [ "postfix.service" ];
|
||||||
after = certificatesDeps;
|
preStart =
|
||||||
preStart = let
|
''
|
||||||
directories = lib.strings.escapeShellArgs (
|
mkdir -p '/run/dovecot/'
|
||||||
[ mailDirectory ]
|
chown 'dovecot2:dovecot2' '/run/dovecot'
|
||||||
++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
'';
|
||||||
);
|
};
|
||||||
in ''
|
|
||||||
# Create mail directory and set permissions. See
|
# Create certificates and maildir folder
|
||||||
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
systemd.services.opensmtpd = {
|
||||||
# Prevent world-readable paths, even temporarily.
|
after = (if (certificateScheme == 3) then [ "nginx.service" ] else []);
|
||||||
umask 007
|
preStart =
|
||||||
mkdir -p ${directories}
|
''
|
||||||
chgrp "${vmailGroupName}" ${directories}
|
mkdir -p /var/empty
|
||||||
chmod 02770 ${directories}
|
# Create mail directory and set permissions. See
|
||||||
|
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
||||||
|
mkdir -p "${mailDirectory}"
|
||||||
|
chgrp "${vmailGroupName}" "${mailDirectory}"
|
||||||
|
chmod 02770 "${mailDirectory}"
|
||||||
|
|
||||||
|
${create_certificate}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
# Create dkim certificates
|
||||||
systemd.services.postfix = {
|
systemd.services.rmilter = {
|
||||||
wants = certificatesDeps;
|
requires = [ "rmilter.socket" ];
|
||||||
after = [ "dovecot2.service" ]
|
after = [ "rmilter.socket" ];
|
||||||
++ lib.optional cfg.dkimSigning "opendkim.service"
|
preStart =
|
||||||
++ certificatesDeps;
|
''
|
||||||
requires = [ "dovecot2.service" ]
|
${create_dkim_cert}
|
||||||
++ lib.optional cfg.dkimSigning "opendkim.service";
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -21,74 +21,63 @@ with config.mailserver;
|
||||||
let
|
let
|
||||||
vmail_user = {
|
vmail_user = {
|
||||||
name = vmailUserName;
|
name = vmailUserName;
|
||||||
isSystemUser = true;
|
isNormalUser = false;
|
||||||
uid = vmailUID;
|
uid = vmailUID;
|
||||||
home = mailDirectory;
|
home = mailDirectory;
|
||||||
createHome = true;
|
createHome = true;
|
||||||
group = vmailGroupName;
|
group = vmailGroupName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# accountsToUser :: String -> UserRecord
|
||||||
|
accountsToUser = account: {
|
||||||
|
isNormalUser = false;
|
||||||
|
group = vmailGroupName;
|
||||||
|
inherit (account) hashedPassword name;
|
||||||
|
};
|
||||||
|
|
||||||
|
# mail_users :: { [String]: UserRecord }
|
||||||
|
mail_users = lib.foldl (prev: next: prev // { "${next.name}" = next; }) {}
|
||||||
|
(map accountsToUser (lib.attrValues loginAccounts));
|
||||||
|
|
||||||
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Prevent world-readable paths, even temporarily.
|
|
||||||
umask 007
|
|
||||||
|
|
||||||
# Create directory to store user sieve scripts if it doesn't exist
|
# Create directory to store user sieve scripts if it doesn't exist
|
||||||
if (! test -d "${sieveDirectory}"); then
|
if (! test -d "/var/sieve"); then
|
||||||
mkdir "${sieveDirectory}"
|
mkdir "/var/sieve"
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
|
chown "${vmailUserName}:${vmailGroupName}" "/var/sieve"
|
||||||
chmod 770 "${sieveDirectory}"
|
chmod 770 "/var/sieve"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy user's sieve script to the correct location (if it exists). If it
|
# Copy user's sieve script to the correct location (if it exists). If it
|
||||||
# is null, remove the file.
|
# is null, remove the file.
|
||||||
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
|
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
|
||||||
if lib.isString sieveScript then ''
|
if lib.isString sieveScript then ''
|
||||||
if (! test -d "${sieveDirectory}/${name}"); then
|
cat << EOF > "/var/sieve/${name}.sieve"
|
||||||
mkdir -p "${sieveDirectory}/${name}"
|
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
|
||||||
chmod 770 "${sieveDirectory}/${name}"
|
|
||||||
fi
|
|
||||||
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
|
|
||||||
${sieveScript}
|
${sieveScript}
|
||||||
EOF
|
EOF
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
chown "${name}:${vmailGroupName}" "/var/sieve/${name}.sieve"
|
||||||
'' else ''
|
'' else ''
|
||||||
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
if (test -f "/var/sieve/${name}.sieve"); then
|
||||||
rm "${sieveDirectory}/${name}/default.sieve"
|
rm "/var/sieve/${name}.sieve"
|
||||||
fi
|
fi
|
||||||
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
if (test -f "/var/sieve/${name}.svbin"); then
|
||||||
rm "${sieveDirectory}/${name}/default.svbin"
|
rm "/var/sieve/${name}.svbin"
|
||||||
fi
|
fi
|
||||||
'') (map (user: { inherit (user) name sieveScript; })
|
'') (map (user: { inherit (user) name sieveScript; })
|
||||||
(lib.attrValues loginAccounts))}
|
(lib.attrValues loginAccounts))}
|
||||||
'';
|
'';
|
||||||
in {
|
in {
|
||||||
config = lib.mkIf enable {
|
config = lib.mkIf enable {
|
||||||
# assert that all accounts provide a password
|
|
||||||
assertions = (map (acct: {
|
|
||||||
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
|
|
||||||
message = "${acct.name} must provide either a hashed password or a password hash file";
|
|
||||||
}) (lib.attrValues loginAccounts));
|
|
||||||
|
|
||||||
# warn for accounts that specify both password and file
|
|
||||||
warnings = (map
|
|
||||||
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
|
||||||
(lib.filter
|
|
||||||
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
|
|
||||||
(lib.attrValues loginAccounts)));
|
|
||||||
|
|
||||||
# set the vmail gid to a specific value
|
# set the vmail gid to a specific value
|
||||||
users.groups = {
|
users.groups = {
|
||||||
"${vmailGroupName}" = { gid = vmailUID; };
|
"${vmailGroupName}" = { gid = vmailUID; };
|
||||||
};
|
};
|
||||||
|
|
||||||
# define all users
|
# define all users
|
||||||
users.users = {
|
users.users = mail_users // {
|
||||||
"${vmail_user.name}" = lib.mkForce vmail_user;
|
"${vmail_user.name}" = lib.mkForce vmail_user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../default.nix
|
./../default.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
mailserver = {
|
mailserver = {
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
extraVirtualAliases = {
|
virtualAliases = {
|
||||||
"info@example.com" = "user1@example.com";
|
"info@example.com" = "user1@example.com";
|
||||||
"postmaster@example.com" = "user1@example.com";
|
"postmaster@example.com" = "user1@example.com";
|
||||||
"abuse@example.com" = "user1@example.com";
|
"abuse@example.com" = "user1@example.com";
|
||||||
|
|
|
@ -1,82 +0,0 @@
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
header = """
|
|
||||||
# Mailserver options
|
|
||||||
|
|
||||||
## `mailserver`
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = """
|
|
||||||
`````{{option}} {key}
|
|
||||||
{description}
|
|
||||||
|
|
||||||
{type}
|
|
||||||
{default}
|
|
||||||
{example}
|
|
||||||
`````
|
|
||||||
"""
|
|
||||||
|
|
||||||
f = open(sys.argv[1])
|
|
||||||
options = json.load(f)
|
|
||||||
|
|
||||||
groups = ["mailserver.loginAccounts",
|
|
||||||
"mailserver.certificate",
|
|
||||||
"mailserver.dkim",
|
|
||||||
"mailserver.dmarcReporting",
|
|
||||||
"mailserver.fullTextSearch",
|
|
||||||
"mailserver.redis",
|
|
||||||
"mailserver.ldap",
|
|
||||||
"mailserver.monitoring",
|
|
||||||
"mailserver.backup",
|
|
||||||
"mailserver.borgbackup"]
|
|
||||||
|
|
||||||
def render_option_value(opt, attr):
|
|
||||||
if attr in opt:
|
|
||||||
if isinstance(opt[attr], dict) and '_type' in opt[attr]:
|
|
||||||
if opt[attr]['_type'] == 'literalExpression':
|
|
||||||
if '\n' in opt[attr]['text']:
|
|
||||||
res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```'
|
|
||||||
else:
|
|
||||||
res = '```{}```'.format(opt[attr]['text'])
|
|
||||||
elif opt[attr]['_type'] == 'literalMD':
|
|
||||||
res = opt[attr]['text']
|
|
||||||
else:
|
|
||||||
s = str(opt[attr])
|
|
||||||
if s == "":
|
|
||||||
res = '`""`'
|
|
||||||
elif '\n' in s:
|
|
||||||
res = '\n```\n' + s.rstrip('\n') + '\n```'
|
|
||||||
else:
|
|
||||||
res = '```{}```'.format(s)
|
|
||||||
res = '- ' + attr + ': ' + res
|
|
||||||
else:
|
|
||||||
res = ""
|
|
||||||
return res
|
|
||||||
|
|
||||||
def print_option(opt):
|
|
||||||
if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc
|
|
||||||
description = opt['description']['text']
|
|
||||||
else:
|
|
||||||
description = opt['description']
|
|
||||||
print(template.format(
|
|
||||||
key=opt['name'],
|
|
||||||
description=description or "",
|
|
||||||
type="- type: ```{}```".format(opt['type']),
|
|
||||||
default=render_option_value(opt, 'default'),
|
|
||||||
example=render_option_value(opt, 'example')))
|
|
||||||
|
|
||||||
|
|
||||||
print(header)
|
|
||||||
for opt in options:
|
|
||||||
if any([opt['name'].startswith(c) for c in groups]):
|
|
||||||
continue
|
|
||||||
print_option(opt)
|
|
||||||
|
|
||||||
for c in groups:
|
|
||||||
print('## `{}`'.format(c))
|
|
||||||
print()
|
|
||||||
for opt in options:
|
|
||||||
if opt['name'].startswith(c):
|
|
||||||
print_option(opt)
|
|
|
@ -1,197 +0,0 @@
|
||||||
import smtplib, sys
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import imaplib
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import email
|
|
||||||
import time
|
|
||||||
|
|
||||||
RETRY = 100
|
|
||||||
|
|
||||||
def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls):
|
|
||||||
print("Sending mail with subject '{}'".format(subject))
|
|
||||||
message = "\n".join([
|
|
||||||
"From: {from_addr}",
|
|
||||||
"To: {to_addr}",
|
|
||||||
"Subject: {subject}",
|
|
||||||
"",
|
|
||||||
"This validates our mail server can send to Gmail :/"]).format(
|
|
||||||
from_addr=from_addr,
|
|
||||||
to_addr=to_addr,
|
|
||||||
subject=subject)
|
|
||||||
|
|
||||||
|
|
||||||
retry = RETRY
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
with smtplib.SMTP(smtp_host, port=smtp_port) as smtp:
|
|
||||||
try:
|
|
||||||
if starttls:
|
|
||||||
smtp.starttls()
|
|
||||||
if from_pwd is not None:
|
|
||||||
smtp.login(smtp_username or from_addr, from_pwd)
|
|
||||||
|
|
||||||
smtp.sendmail(from_addr, [to_addr], message)
|
|
||||||
return
|
|
||||||
except smtplib.SMTPResponseException as e:
|
|
||||||
if e.smtp_code == 451: # service unavailable error
|
|
||||||
print(e)
|
|
||||||
elif e.smtp_code == 454: # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
|
|
||||||
print(e)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
except OSError as e:
|
|
||||||
if e.errno in [16, -2]:
|
|
||||||
print("OSError exception message: ", e)
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
|
|
||||||
if retry > 0:
|
|
||||||
retry = retry - 1
|
|
||||||
time.sleep(1)
|
|
||||||
print("Retrying")
|
|
||||||
else:
|
|
||||||
print("Retry attempts exhausted")
|
|
||||||
exit(5)
|
|
||||||
|
|
||||||
def _read_mail(
|
|
||||||
imap_host,
|
|
||||||
imap_port,
|
|
||||||
imap_username,
|
|
||||||
to_pwd,
|
|
||||||
subject,
|
|
||||||
ignore_dkim_spf,
|
|
||||||
show_body=False,
|
|
||||||
delete=True):
|
|
||||||
print("Reading mail from %s" % imap_username)
|
|
||||||
|
|
||||||
message = None
|
|
||||||
|
|
||||||
obj = imaplib.IMAP4_SSL(imap_host, imap_port)
|
|
||||||
obj.login(imap_username, to_pwd)
|
|
||||||
obj.select()
|
|
||||||
|
|
||||||
today = datetime.today()
|
|
||||||
cutoff = today - timedelta(days=1)
|
|
||||||
dt = cutoff.strftime('%d-%b-%Y')
|
|
||||||
for _ in range(0, RETRY):
|
|
||||||
print("Retrying")
|
|
||||||
obj.select()
|
|
||||||
typ, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")'%(dt, subject))
|
|
||||||
if data == [b'']:
|
|
||||||
time.sleep(1)
|
|
||||||
continue
|
|
||||||
|
|
||||||
uids = data[0].decode("utf-8").split(" ")
|
|
||||||
if len(uids) != 1:
|
|
||||||
print("Warning: %d messages have been found with subject containing %s " % (len(uids), subject))
|
|
||||||
|
|
||||||
# FIXME: we only consider the first matching message...
|
|
||||||
uid = uids[0]
|
|
||||||
_, raw = obj.fetch(uid, '(RFC822)')
|
|
||||||
if delete:
|
|
||||||
obj.store(uid, '+FLAGS', '\\Deleted')
|
|
||||||
obj.expunge()
|
|
||||||
message = email.message_from_bytes(raw[0][1])
|
|
||||||
print("Message with subject '%s' has been found" % message['subject'])
|
|
||||||
if show_body:
|
|
||||||
for m in message.get_payload():
|
|
||||||
if m.get_content_type() == 'text/plain':
|
|
||||||
print("Body:\n%s" % m.get_payload(decode=True).decode('utf-8'))
|
|
||||||
break
|
|
||||||
|
|
||||||
if message is None:
|
|
||||||
print("Error: no message with subject '%s' has been found in INBOX of %s" % (subject, imap_username))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
if ignore_dkim_spf:
|
|
||||||
return
|
|
||||||
|
|
||||||
# gmail set this standardized header
|
|
||||||
if 'ARC-Authentication-Results' in message:
|
|
||||||
if "dkim=pass" in message['ARC-Authentication-Results']:
|
|
||||||
print("DKIM ok")
|
|
||||||
else:
|
|
||||||
print("Error: no DKIM validation found in message:")
|
|
||||||
print(message.as_string())
|
|
||||||
exit(2)
|
|
||||||
if "spf=pass" in message['ARC-Authentication-Results']:
|
|
||||||
print("SPF ok")
|
|
||||||
else:
|
|
||||||
print("Error: no SPF validation found in message:")
|
|
||||||
print(message.as_string())
|
|
||||||
exit(3)
|
|
||||||
else:
|
|
||||||
print("DKIM and SPF verification failed")
|
|
||||||
exit(4)
|
|
||||||
|
|
||||||
def send_and_read(args):
|
|
||||||
src_pwd = None
|
|
||||||
if args.src_password_file is not None:
|
|
||||||
src_pwd = args.src_password_file.readline().rstrip()
|
|
||||||
dst_pwd = args.dst_password_file.readline().rstrip()
|
|
||||||
|
|
||||||
if args.imap_username != '':
|
|
||||||
imap_username = args.imap_username
|
|
||||||
else:
|
|
||||||
imap_username = args.to_addr
|
|
||||||
|
|
||||||
subject = "{}".format(uuid.uuid4())
|
|
||||||
|
|
||||||
_send_mail(smtp_host=args.smtp_host,
|
|
||||||
smtp_port=args.smtp_port,
|
|
||||||
smtp_username=args.smtp_username,
|
|
||||||
from_addr=args.from_addr,
|
|
||||||
from_pwd=src_pwd,
|
|
||||||
to_addr=args.to_addr,
|
|
||||||
subject=subject,
|
|
||||||
starttls=args.smtp_starttls)
|
|
||||||
|
|
||||||
_read_mail(imap_host=args.imap_host,
|
|
||||||
imap_port=args.imap_port,
|
|
||||||
imap_username=imap_username,
|
|
||||||
to_pwd=dst_pwd,
|
|
||||||
subject=subject,
|
|
||||||
ignore_dkim_spf=args.ignore_dkim_spf)
|
|
||||||
|
|
||||||
def read(args):
|
|
||||||
_read_mail(imap_host=args.imap_host,
|
|
||||||
imap_port=args.imap_port,
|
|
||||||
to_addr=args.imap_username,
|
|
||||||
to_pwd=args.imap_password,
|
|
||||||
subject=args.subject,
|
|
||||||
ignore_dkim_spf=args.ignore_dkim_spf,
|
|
||||||
show_body=args.show_body,
|
|
||||||
delete=False)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
subparsers = parser.add_subparsers()
|
|
||||||
|
|
||||||
parser_send_and_read = subparsers.add_parser('send-and-read', description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.")
|
|
||||||
parser_send_and_read.add_argument('--smtp-host', type=str)
|
|
||||||
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
|
||||||
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
|
|
||||||
parser_send_and_read.add_argument('--smtp-username', type=str, default='', help="username used for smtp login. If not specified, the from-addr value is used")
|
|
||||||
parser_send_and_read.add_argument('--from-addr', type=str)
|
|
||||||
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
|
||||||
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
|
||||||
parser_send_and_read.add_argument('--to-addr', type=str, required=True)
|
|
||||||
parser_send_and_read.add_argument('--imap-username', type=str, default='', help="username used for imap login. If not specified, the to-addr value is used")
|
|
||||||
parser_send_and_read.add_argument('--src-password-file', type=argparse.FileType('r'))
|
|
||||||
parser_send_and_read.add_argument('--dst-password-file', required=True, type=argparse.FileType('r'))
|
|
||||||
parser_send_and_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
|
|
||||||
parser_send_and_read.set_defaults(func=send_and_read)
|
|
||||||
|
|
||||||
parser_read = subparsers.add_parser('read', description="Search for an email with a subject containing 'subject' in the INBOX.")
|
|
||||||
parser_read.add_argument('--imap-host', type=str, default="localhost")
|
|
||||||
parser_read.add_argument('--imap-port', type=str, default=993)
|
|
||||||
parser_read.add_argument('--imap-username', required=True, type=str)
|
|
||||||
parser_read.add_argument('--imap-password', required=True, type=str)
|
|
||||||
parser_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
|
|
||||||
parser_read.add_argument('--show-body', action='store_true', help="print mail text/plain payload")
|
|
||||||
parser_read.add_argument('subject', type=str)
|
|
||||||
parser_read.set_defaults(func=read)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
args.func(args)
|
|
10
shell.nix
10
shell.nix
|
@ -1,10 +0,0 @@
|
||||||
(import
|
|
||||||
(
|
|
||||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
|
||||||
fetchTarball {
|
|
||||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
|
||||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
|
||||||
}
|
|
||||||
)
|
|
||||||
{ src = ./.; }
|
|
||||||
).shellNix
|
|
237
tests/clamav.nix
237
tests/clamav.nix
|
@ -1,237 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, blobs}:
|
|
||||||
|
|
||||||
pkgs.nixosTest {
|
|
||||||
name = "clamav";
|
|
||||||
nodes = {
|
|
||||||
server = { config, pkgs, lib, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1500;
|
|
||||||
|
|
||||||
services.rsyslogd = {
|
|
||||||
enable = true;
|
|
||||||
defaultConfig = ''
|
|
||||||
*.* /dev/console
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
services.clamav.updater.enable = lib.mkForce false;
|
|
||||||
systemd.services.old-clam = {
|
|
||||||
before = [ "clamav-daemon.service" ];
|
|
||||||
requiredBy = [ "clamav-daemon.service" ];
|
|
||||||
description = "ClamAV virus database";
|
|
||||||
|
|
||||||
preStart = ''
|
|
||||||
mkdir -m 0755 -p /var/lib/clamav
|
|
||||||
chown clamav:clamav /var/lib/clamav
|
|
||||||
'';
|
|
||||||
|
|
||||||
script = ''
|
|
||||||
cp ${blobs}/clamav/main.cvd /var/lib/clamav/
|
|
||||||
cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
|
|
||||||
cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
|
|
||||||
chown clamav:clamav /var/lib/clamav/*
|
|
||||||
'';
|
|
||||||
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
PrivateTmp = "yes";
|
|
||||||
PrivateDevices = "yes";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" "example2.com" ];
|
|
||||||
virusScanning = true;
|
|
||||||
|
|
||||||
loginAccounts = {
|
|
||||||
"user1@example.com" = {
|
|
||||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
|
||||||
aliases = [ "postmaster@example.com" ];
|
|
||||||
catchAll = [ "example.com" ];
|
|
||||||
};
|
|
||||||
"user@example2.com" = {
|
|
||||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
enableImap = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.etc = {
|
|
||||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
client = { nodes, config, pkgs, ... }: let
|
|
||||||
serverIP = nodes.server.config.networking.primaryIPAddress;
|
|
||||||
clientIP = nodes.client.config.networking.primaryIPAddress;
|
|
||||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
echo grep '${clientIP}' "$@" >&2
|
|
||||||
exec grep '${clientIP}' "$@"
|
|
||||||
'';
|
|
||||||
in {
|
|
||||||
imports = [
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
fetchmail msmtp procmail findutils grep-ip
|
|
||||||
];
|
|
||||||
environment.etc = {
|
|
||||||
"root/.fetchmailrc" = {
|
|
||||||
text = ''
|
|
||||||
poll ${serverIP} with proto IMAP
|
|
||||||
user 'user1@example.com' there with password 'user1' is 'root' here
|
|
||||||
mda procmail
|
|
||||||
'';
|
|
||||||
mode = "0700";
|
|
||||||
};
|
|
||||||
"root/.procmailrc" = {
|
|
||||||
text = "DEFAULT=$HOME/mail";
|
|
||||||
};
|
|
||||||
"root/.msmtprc" = {
|
|
||||||
text = ''
|
|
||||||
defaults
|
|
||||||
tls on
|
|
||||||
tls_certcheck off
|
|
||||||
|
|
||||||
account user2
|
|
||||||
host ${serverIP}
|
|
||||||
port 587
|
|
||||||
from user@example2.com
|
|
||||||
auth on
|
|
||||||
user user@example2.com
|
|
||||||
password user2
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"root/virus-email".text = ''
|
|
||||||
From: User2 <user@example2.com>
|
|
||||||
Content-Type: multipart/mixed;
|
|
||||||
boundary="Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607"
|
|
||||||
Mime-Version: 1.0 (Mac OS X Mail 11.3 \(3445.6.18\))
|
|
||||||
Subject: Testy McTest
|
|
||||||
Message-Id: <94550DD9-1FF1-4ED1-9F09-8812FF2E59AA@example.com>
|
|
||||||
Date: Sat, 12 May 2018 14:15:44 +0200
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
X-Mailer: Apple Mail (2.3445.6.18)
|
|
||||||
|
|
||||||
|
|
||||||
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
Content-Type: text/plain;
|
|
||||||
charset=us-ascii
|
|
||||||
|
|
||||||
Hello
|
|
||||||
|
|
||||||
I have attached a dangerous virus.
|
|
||||||
|
|
||||||
Mfg.
|
|
||||||
User2
|
|
||||||
|
|
||||||
|
|
||||||
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
|
|
||||||
Content-Disposition: attachment;
|
|
||||||
filename=eicar.com.txt
|
|
||||||
Content-Type: text/plain;
|
|
||||||
x-unix-mode=0644;
|
|
||||||
name="eicar.com.txt"
|
|
||||||
Content-Transfer-Encoding: 7bit
|
|
||||||
|
|
||||||
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
|
|
||||||
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607--
|
|
||||||
'';
|
|
||||||
"root/safe-email".text = ''
|
|
||||||
From: User <user@example2.com>
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from user@example2.com to user1
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello User1,
|
|
||||||
|
|
||||||
how are you doing today?
|
|
||||||
|
|
||||||
XOXO User1
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = { nodes, ... }:
|
|
||||||
''
|
|
||||||
start_all()
|
|
||||||
|
|
||||||
server.wait_for_unit("multi-user.target")
|
|
||||||
client.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
|
||||||
server.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
server.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
|
|
||||||
client.execute("cp -p /etc/root/.* ~/")
|
|
||||||
client.succeed("mkdir -p ~/mail")
|
|
||||||
client.succeed("ls -la ~/ >&2")
|
|
||||||
client.succeed("cat ~/.fetchmailrc >&2")
|
|
||||||
client.succeed("cat ~/.procmailrc >&2")
|
|
||||||
client.succeed("cat ~/.msmtprc >&2")
|
|
||||||
|
|
||||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
|
|
||||||
|
|
||||||
# Verify that mail can be sent and received before testing virus scanner
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
|
|
||||||
# give the mail server some time to process the mail
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v >&2")
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
|
|
||||||
with subtest("virus scan file"):
|
|
||||||
server.succeed(
|
|
||||||
'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("virus scan email"):
|
|
||||||
client.succeed(
|
|
||||||
'set +o pipefail; msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
|
|
||||||
)
|
|
||||||
server.succeed("journalctl -u rspamd | grep -i eicar")
|
|
||||||
# give the mail server some time to process the mail
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
|
|
||||||
with subtest("no warnings or errors"):
|
|
||||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
|
||||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
|
||||||
server.fail("journalctl -u dovecot2 | grep -i error >&2")
|
|
||||||
server.fail("journalctl -u dovecot2 | grep -i warning >&2")
|
|
||||||
'';
|
|
||||||
}
|
|
238
tests/extern.nix
Normal file
238
tests/extern.nix
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
# nixos-mailserver: a simple mail server
|
||||||
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
import <nixpkgs/nixos/tests/make-test.nix> {
|
||||||
|
|
||||||
|
nodes =
|
||||||
|
{ server = { config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" "example2.com" ];
|
||||||
|
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
aliases = [ "postmaster@example.com" ];
|
||||||
|
catchAll = [ "example.com" ];
|
||||||
|
};
|
||||||
|
"user2@example.com" = {
|
||||||
|
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||||
|
aliases = [ "chuck@example.com" ];
|
||||||
|
};
|
||||||
|
"user@example2.com" = {
|
||||||
|
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
enableImap = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
client = { config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
fetchmail msmtp procmail findutils
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript =
|
||||||
|
let
|
||||||
|
fetchmailRc =
|
||||||
|
''
|
||||||
|
poll SERVER with proto IMAP
|
||||||
|
user 'user1\@example.com' there with password 'user1' is 'root' here
|
||||||
|
mda procmail
|
||||||
|
'';
|
||||||
|
|
||||||
|
procmailRc =
|
||||||
|
''
|
||||||
|
DEFAULT=\$HOME/mail
|
||||||
|
'';
|
||||||
|
|
||||||
|
msmtpRc =
|
||||||
|
''
|
||||||
|
account test
|
||||||
|
host SERVER
|
||||||
|
port 587
|
||||||
|
from user2\@example.com
|
||||||
|
user user2\@example.com
|
||||||
|
password user2
|
||||||
|
|
||||||
|
account test2
|
||||||
|
host SERVER
|
||||||
|
port 587
|
||||||
|
from user\@example2.com
|
||||||
|
user user\@example2.com
|
||||||
|
password user2
|
||||||
|
|
||||||
|
account test3
|
||||||
|
host SERVER
|
||||||
|
port 587
|
||||||
|
from chuck\@example.com
|
||||||
|
user user2\@example.com
|
||||||
|
password user2
|
||||||
|
|
||||||
|
account test4
|
||||||
|
host SERVER
|
||||||
|
port 587
|
||||||
|
from postmaster\@example.com
|
||||||
|
user user1\@example.com
|
||||||
|
password user1
|
||||||
|
'';
|
||||||
|
email1 =
|
||||||
|
''
|
||||||
|
From: User2 <user2\@example.com>
|
||||||
|
To: User1 <user1\@example.com>
|
||||||
|
Cc:
|
||||||
|
Bcc:
|
||||||
|
Subject: This is a test Email from user2 to user1
|
||||||
|
Reply-To:
|
||||||
|
|
||||||
|
Hello User1,
|
||||||
|
|
||||||
|
how are you doing today?
|
||||||
|
'';
|
||||||
|
email2 =
|
||||||
|
''
|
||||||
|
From: User <user\@example2.com>
|
||||||
|
To: User1 <user1\@example.com>
|
||||||
|
Cc:
|
||||||
|
Bcc:
|
||||||
|
Subject: This is a test Email from user\@example2.com to user1
|
||||||
|
Reply-To:
|
||||||
|
|
||||||
|
Hello User1,
|
||||||
|
|
||||||
|
how are you doing today?
|
||||||
|
|
||||||
|
XOXO User1
|
||||||
|
'';
|
||||||
|
email3 =
|
||||||
|
''
|
||||||
|
From: Postmaster <postmaster@example.com>
|
||||||
|
To: Chuck <chuck@example.com>
|
||||||
|
Cc:
|
||||||
|
Bcc:
|
||||||
|
Subject: This is a test Email from postmaster\@example.com to chuck
|
||||||
|
Reply-To:
|
||||||
|
|
||||||
|
Hello Chuck,
|
||||||
|
|
||||||
|
I think I may have misconfigured the mail server
|
||||||
|
XOXO Postmaster
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
''
|
||||||
|
startAll;
|
||||||
|
|
||||||
|
$server->waitForUnit("multi-user.target");
|
||||||
|
$client->waitForUnit("multi-user.target");
|
||||||
|
|
||||||
|
subtest "imap retrieving mail", sub {
|
||||||
|
$client->succeed("mkdir ~/mail");
|
||||||
|
$client->succeed("echo '${fetchmailRc}' > ~/.fetchmailrc");
|
||||||
|
$client->succeed("echo '${procmailRc}' > ~/.procmailrc");
|
||||||
|
$client->succeed("sed -i s/SERVER/`getent hosts server | awk '{ print \$1 }'`/g ~/.fetchmailrc");
|
||||||
|
$client->succeed("chmod 0700 ~/.fetchmailrc");
|
||||||
|
$client->succeed("cat ~/.fetchmailrc >&2");
|
||||||
|
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||||
|
$client->succeed("fetchmail -v || [ \$? -eq 1 ] >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "submission port send mail", sub {
|
||||||
|
$client->succeed("echo '${msmtpRc}' > ~/.msmtprc");
|
||||||
|
$client->succeed("sed -i s/SERVER/`getent hosts server | awk '{ print \$1 }'`/g ~/.msmtprc");
|
||||||
|
$client->succeed("cat ~/.msmtprc >&2");
|
||||||
|
$client->succeed("echo '${email1}' > mail.txt");
|
||||||
|
# send email from user2 to user1
|
||||||
|
$client->succeed("msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < mail.txt >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "imap retrieving mail 2", sub {
|
||||||
|
# give the mail server some time to process the mail
|
||||||
|
$client->succeed("sleep 5");
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail -v >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "remove sensitive information on submission port", sub {
|
||||||
|
$client->succeed("cat ~/mail/* >&2");
|
||||||
|
## make sure our IP is _not_ in the email header
|
||||||
|
$client->fail("grep `ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print \$2}' | cut -f1 -d'/'` ~/mail/*");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "have correct fqdn as sender", sub {
|
||||||
|
$client->succeed("grep 'Received: from mail.example.com' ~/mail/*");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "dkim singing, multiple domains", sub {
|
||||||
|
$client->succeed("rm ~/mail/*");
|
||||||
|
$client->succeed("rm mail.txt");
|
||||||
|
$client->succeed("echo '${email2}' > mail.txt");
|
||||||
|
# send email from user2 to user1
|
||||||
|
$client->succeed("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < mail.txt >&2");
|
||||||
|
$client->succeed("sleep 5");
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail -v");
|
||||||
|
$client->succeed("cat ~/mail/* >&2");
|
||||||
|
# make sure it is dkim signed
|
||||||
|
$client->succeed("grep DKIM ~/mail/*");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "aliases", sub {
|
||||||
|
$client->succeed("rm ~/mail/*");
|
||||||
|
$client->succeed("rm mail.txt");
|
||||||
|
$client->succeed("echo '${email2}' > mail.txt");
|
||||||
|
# send email from chuck to postmaster
|
||||||
|
$client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < mail.txt >&2");
|
||||||
|
$client->succeed("sleep 5");
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail -v");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
subtest "catchAlls", sub {
|
||||||
|
$client->succeed("rm ~/mail/*");
|
||||||
|
$client->succeed("rm mail.txt");
|
||||||
|
$client->succeed("echo '${email2}' > mail.txt");
|
||||||
|
# send email from chuck to non exsitent account
|
||||||
|
$client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < mail.txt >&2");
|
||||||
|
$client->succeed("sleep 5");
|
||||||
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
|
$client->succeed("fetchmail -v");
|
||||||
|
|
||||||
|
$client->succeed("rm ~/mail/*");
|
||||||
|
$client->succeed("rm mail.txt");
|
||||||
|
$client->succeed("echo '${email2}' > mail.txt");
|
||||||
|
# send email from user1 to chuck
|
||||||
|
$client->succeed("msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < mail.txt >&2");
|
||||||
|
$client->succeed("sleep 5");
|
||||||
|
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||||
|
# if this succeeds, it means that user1 recieved the mail that was intended for chuck.
|
||||||
|
$client->fail("fetchmail -v");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
'';
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,514 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, ...}:
|
|
||||||
|
|
||||||
pkgs.nixosTest {
|
|
||||||
name = "external";
|
|
||||||
nodes = {
|
|
||||||
server = { config, pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
|
|
||||||
services.rsyslogd = {
|
|
||||||
enable = true;
|
|
||||||
defaultConfig = ''
|
|
||||||
*.* /dev/console
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
debug = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" "example2.com" ];
|
|
||||||
rewriteMessageId = true;
|
|
||||||
dkimKeyBits = 1535;
|
|
||||||
dmarcReporting = {
|
|
||||||
enable = true;
|
|
||||||
domain = "example.com";
|
|
||||||
organizationName = "ACME Corp";
|
|
||||||
};
|
|
||||||
|
|
||||||
loginAccounts = {
|
|
||||||
"user1@example.com" = {
|
|
||||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
|
||||||
aliases = [ "postmaster@example.com" ];
|
|
||||||
catchAll = [ "example.com" ];
|
|
||||||
};
|
|
||||||
"user2@example.com" = {
|
|
||||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
|
||||||
aliases = [ "chuck@example.com" ];
|
|
||||||
};
|
|
||||||
"user@example2.com" = {
|
|
||||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
|
||||||
};
|
|
||||||
"lowquota@example.com" = {
|
|
||||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
|
||||||
quota = "1B";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
extraVirtualAliases = {
|
|
||||||
"single-alias@example.com" = "user1@example.com";
|
|
||||||
"multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
enableImap = true;
|
|
||||||
enableImapSsl = true;
|
|
||||||
fullTextSearch = {
|
|
||||||
enable = true;
|
|
||||||
autoIndex = true;
|
|
||||||
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
|
|
||||||
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ];
|
|
||||||
enforced = "yes";
|
|
||||||
# fts-xapian warns when memory is low, which makes the test fail
|
|
||||||
memoryLimit = 100000;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
client = { nodes, config, pkgs, ... }: let
|
|
||||||
serverIP = nodes.server.config.networking.primaryIPAddress;
|
|
||||||
clientIP = nodes.client.config.networking.primaryIPAddress;
|
|
||||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
echo grep '${clientIP}' "$@" >&2
|
|
||||||
exec grep '${clientIP}' "$@"
|
|
||||||
'';
|
|
||||||
check-mail-id = pkgs.writeScriptBin "check-mail-id" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2
|
|
||||||
exec grep '^Message-ID:.*@mail.example.com>$' "$@"
|
|
||||||
'';
|
|
||||||
test-imap-spam = pkgs.writeScriptBin "imap-mark-spam" ''
|
|
||||||
#!${pkgs.python3.interpreter}
|
|
||||||
import imaplib
|
|
||||||
|
|
||||||
with imaplib.IMAP4_SSL('${serverIP}') as imap:
|
|
||||||
imap.login('user1@example.com', 'user1')
|
|
||||||
imap.select()
|
|
||||||
status, [response] = imap.search(None, 'ALL')
|
|
||||||
msg_ids = response.decode("utf-8").split(' ')
|
|
||||||
print(msg_ids)
|
|
||||||
assert status == 'OK'
|
|
||||||
assert len(msg_ids) == 1
|
|
||||||
|
|
||||||
imap.copy(','.join(msg_ids), 'Junk')
|
|
||||||
for num in msg_ids:
|
|
||||||
imap.store(num, '+FLAGS', '\\Deleted')
|
|
||||||
imap.expunge()
|
|
||||||
|
|
||||||
imap.select('Junk')
|
|
||||||
status, [response] = imap.search(None, 'ALL')
|
|
||||||
msg_ids = response.decode("utf-8").split(' ')
|
|
||||||
print(msg_ids)
|
|
||||||
assert status == 'OK'
|
|
||||||
assert len(msg_ids) == 1
|
|
||||||
|
|
||||||
imap.close()
|
|
||||||
'';
|
|
||||||
test-imap-ham = pkgs.writeScriptBin "imap-mark-ham" ''
|
|
||||||
#!${pkgs.python3.interpreter}
|
|
||||||
import imaplib
|
|
||||||
|
|
||||||
with imaplib.IMAP4_SSL('${serverIP}') as imap:
|
|
||||||
imap.login('user1@example.com', 'user1')
|
|
||||||
imap.select('Junk')
|
|
||||||
status, [response] = imap.search(None, 'ALL')
|
|
||||||
msg_ids = response.decode("utf-8").split(' ')
|
|
||||||
print(msg_ids)
|
|
||||||
assert status == 'OK'
|
|
||||||
assert len(msg_ids) == 1
|
|
||||||
|
|
||||||
imap.copy(','.join(msg_ids), 'INBOX')
|
|
||||||
for num in msg_ids:
|
|
||||||
imap.store(num, '+FLAGS', '\\Deleted')
|
|
||||||
imap.expunge()
|
|
||||||
|
|
||||||
imap.select('INBOX')
|
|
||||||
status, [response] = imap.search(None, 'ALL')
|
|
||||||
msg_ids = response.decode("utf-8").split(' ')
|
|
||||||
print(msg_ids)
|
|
||||||
assert status == 'OK'
|
|
||||||
assert len(msg_ids) == 1
|
|
||||||
|
|
||||||
imap.close()
|
|
||||||
'';
|
|
||||||
search = pkgs.writeScriptBin "search" ''
|
|
||||||
#!${pkgs.python3.interpreter}
|
|
||||||
import imaplib
|
|
||||||
import sys
|
|
||||||
|
|
||||||
[_, mailbox, needle] = sys.argv
|
|
||||||
|
|
||||||
with imaplib.IMAP4_SSL('${serverIP}') as imap:
|
|
||||||
imap.login('user1@example.com', 'user1')
|
|
||||||
imap.select(mailbox)
|
|
||||||
status, [response] = imap.search(None, 'BODY', repr(needle))
|
|
||||||
msg_ids = [ i for i in response.decode("utf-8").split(' ') if i ]
|
|
||||||
print(msg_ids)
|
|
||||||
assert status == 'OK'
|
|
||||||
assert len(msg_ids) == 1
|
|
||||||
status, response = imap.fetch(msg_ids[0], '(RFC822)')
|
|
||||||
assert status == "OK"
|
|
||||||
assert needle in repr(response)
|
|
||||||
imap.close()
|
|
||||||
'';
|
|
||||||
in {
|
|
||||||
imports = [
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search
|
|
||||||
];
|
|
||||||
environment.etc = {
|
|
||||||
"root/.fetchmailrc" = {
|
|
||||||
text = ''
|
|
||||||
poll ${serverIP} with proto IMAP
|
|
||||||
user 'user1@example.com' there with password 'user1' is 'root' here
|
|
||||||
mda procmail
|
|
||||||
'';
|
|
||||||
mode = "0700";
|
|
||||||
};
|
|
||||||
"root/.fetchmailRcLowQuota" = {
|
|
||||||
text = ''
|
|
||||||
poll ${serverIP} with proto IMAP
|
|
||||||
user 'lowquota@example.com' there with password 'user2' is 'root' here
|
|
||||||
mda procmail
|
|
||||||
'';
|
|
||||||
mode = "0700";
|
|
||||||
};
|
|
||||||
"root/.procmailrc" = {
|
|
||||||
text = "DEFAULT=$HOME/mail";
|
|
||||||
};
|
|
||||||
"root/.msmtprc" = {
|
|
||||||
text = ''
|
|
||||||
account test
|
|
||||||
host ${serverIP}
|
|
||||||
port 587
|
|
||||||
from user2@example.com
|
|
||||||
user user2@example.com
|
|
||||||
password user2
|
|
||||||
|
|
||||||
account test2
|
|
||||||
host ${serverIP}
|
|
||||||
port 587
|
|
||||||
from user@example2.com
|
|
||||||
user user@example2.com
|
|
||||||
password user2
|
|
||||||
|
|
||||||
account test3
|
|
||||||
host ${serverIP}
|
|
||||||
port 587
|
|
||||||
from chuck@example.com
|
|
||||||
user user2@example.com
|
|
||||||
password user2
|
|
||||||
|
|
||||||
account test4
|
|
||||||
host ${serverIP}
|
|
||||||
port 587
|
|
||||||
from postmaster@example.com
|
|
||||||
user user1@example.com
|
|
||||||
password user1
|
|
||||||
|
|
||||||
account test5
|
|
||||||
host ${serverIP}
|
|
||||||
port 587
|
|
||||||
from single-alias@example.com
|
|
||||||
user user1@example.com
|
|
||||||
password user1
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
"root/email1".text = ''
|
|
||||||
Message-ID: <12345qwerty@host.local.network>
|
|
||||||
From: User2 <user2@example.com>
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from user2 to user1
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello User1,
|
|
||||||
|
|
||||||
how are you doing today?
|
|
||||||
'';
|
|
||||||
"root/email2".text = ''
|
|
||||||
Message-ID: <232323abc@host.local.network>
|
|
||||||
From: User <user@example2.com>
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from user@example2.com to user1
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello User1,
|
|
||||||
|
|
||||||
how are you doing today?
|
|
||||||
|
|
||||||
XOXO User1
|
|
||||||
'';
|
|
||||||
"root/email3".text = ''
|
|
||||||
Message-ID: <asdfghjkl42@host.local.network>
|
|
||||||
From: Postmaster <postmaster@example.com>
|
|
||||||
To: Chuck <chuck@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from postmaster\@example.com to chuck
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello Chuck,
|
|
||||||
|
|
||||||
I think I may have misconfigured the mail server
|
|
||||||
XOXO Postmaster
|
|
||||||
'';
|
|
||||||
"root/email4".text = ''
|
|
||||||
Message-ID: <sdfsdf@host.local.network>
|
|
||||||
From: Single Alias <single-alias@example.com>
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from single-alias\@example.com to user1
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello User1,
|
|
||||||
|
|
||||||
how are you doing today?
|
|
||||||
|
|
||||||
XOXO User1 aka Single Alias
|
|
||||||
'';
|
|
||||||
"root/email5".text = ''
|
|
||||||
Message-ID: <789asdf@host.local.network>
|
|
||||||
From: User2 <user2@example.com>
|
|
||||||
To: Multi Alias <multi-alias@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from user2\@example.com to multi-alias
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello Multi Alias,
|
|
||||||
|
|
||||||
how are we doing today?
|
|
||||||
|
|
||||||
XOXO User1
|
|
||||||
'';
|
|
||||||
"root/email6".text = ''
|
|
||||||
Message-ID: <123457qwerty@host.local.network>
|
|
||||||
From: User2 <user2@example.com>
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from user2 to user1
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello User1,
|
|
||||||
|
|
||||||
this email contains the needle:
|
|
||||||
576a4565b70f5a4c1a0925cabdb587a6
|
|
||||||
'';
|
|
||||||
"root/email7".text = ''
|
|
||||||
Message-ID: <1234578qwerty@host.local.network>
|
|
||||||
From: User2 <user2@example.com>
|
|
||||||
To: User1 <user1@example.com>
|
|
||||||
Cc:
|
|
||||||
Bcc:
|
|
||||||
Subject: This is a test Email from user2 to user1
|
|
||||||
Reply-To:
|
|
||||||
|
|
||||||
Hello User1,
|
|
||||||
|
|
||||||
this email does not contain the needle :(
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript = { nodes, ... }:
|
|
||||||
''
|
|
||||||
start_all()
|
|
||||||
|
|
||||||
server.wait_for_unit("multi-user.target")
|
|
||||||
client.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# TODO put this blocking into the systemd units?
|
|
||||||
server.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
|
|
||||||
client.execute("cp -p /etc/root/.* ~/")
|
|
||||||
client.succeed("mkdir -p ~/mail")
|
|
||||||
client.succeed("ls -la ~/ >&2")
|
|
||||||
client.succeed("cat ~/.fetchmailrc >&2")
|
|
||||||
client.succeed("cat ~/.procmailrc >&2")
|
|
||||||
client.succeed("cat ~/.msmtprc >&2")
|
|
||||||
|
|
||||||
with subtest("imap retrieving mail"):
|
|
||||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
|
|
||||||
|
|
||||||
with subtest("submission port send mail"):
|
|
||||||
# send email from user2 to user1
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
|
|
||||||
)
|
|
||||||
# give the mail server some time to process the mail
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
|
|
||||||
with subtest("imap retrieving mail 2"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v >&2")
|
|
||||||
|
|
||||||
with subtest("remove sensitive information on submission port"):
|
|
||||||
client.succeed("cat ~/mail/* >&2")
|
|
||||||
## make sure our IP is _not_ in the email header
|
|
||||||
client.fail("grep-ip ~/mail/*")
|
|
||||||
client.succeed("check-mail-id ~/mail/*")
|
|
||||||
|
|
||||||
with subtest("have correct fqdn as sender"):
|
|
||||||
client.succeed("grep 'Received: from mail.example.com' ~/mail/*")
|
|
||||||
|
|
||||||
with subtest("dkim has user-specified size"):
|
|
||||||
server.succeed(
|
|
||||||
"openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'"
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("dkim singing, multiple domains"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from user2 to user1
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v")
|
|
||||||
client.succeed("cat ~/mail/* >&2")
|
|
||||||
# make sure it is dkim signed
|
|
||||||
client.succeed("grep DKIM ~/mail/*")
|
|
||||||
|
|
||||||
with subtest("aliases"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from chuck to postmaster
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < /etc/root/email2 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v")
|
|
||||||
|
|
||||||
with subtest("catchAlls"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from chuck to non exsitent account
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < /etc/root/email2 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v")
|
|
||||||
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from user1 to chuck
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < /etc/root/email2 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
|
||||||
# if this succeeds, it means that user1 recieved the mail that was intended for chuck.
|
|
||||||
client.fail("fetchmail --nosslcertck -v")
|
|
||||||
|
|
||||||
with subtest("extraVirtualAliases"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from single-alias to user1
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email4 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v")
|
|
||||||
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
# send email from user1 to multi-alias (user{1,2}@example.com)
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias\@example.com < /etc/root/email5 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.succeed("fetchmail --nosslcertck -v")
|
|
||||||
|
|
||||||
with subtest("quota"):
|
|
||||||
client.execute("rm ~/mail/*")
|
|
||||||
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
|
|
||||||
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota\@example.com < /etc/root/email2 >&2"
|
|
||||||
)
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
|
||||||
client.fail("fetchmail --nosslcertck -v")
|
|
||||||
|
|
||||||
with subtest("imap sieve junk trainer"):
|
|
||||||
# send email from user2 to user1
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
|
|
||||||
)
|
|
||||||
# give the mail server some time to process the mail
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
|
|
||||||
client.succeed("imap-mark-spam >&2")
|
|
||||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2")
|
|
||||||
client.succeed("imap-mark-ham >&2")
|
|
||||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2")
|
|
||||||
|
|
||||||
with subtest("full text search and indexation"):
|
|
||||||
# send 2 email from user2 to user1
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email6 >&2"
|
|
||||||
)
|
|
||||||
client.succeed(
|
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email7 >&2"
|
|
||||||
)
|
|
||||||
# give the mail server some time to process the mail
|
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
|
||||||
|
|
||||||
# should find exactly one email containing this
|
|
||||||
client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2")
|
|
||||||
# should fail because this folder is not indexed
|
|
||||||
client.fail("search Junk a >&2")
|
|
||||||
# check that search really goes through the indexer
|
|
||||||
server.succeed(
|
|
||||||
"journalctl -u dovecot2 | grep -E 'indexer-worker.* Done indexing .INBOX.' >&2"
|
|
||||||
)
|
|
||||||
# check that Junk is not indexed
|
|
||||||
server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2")
|
|
||||||
|
|
||||||
with subtest("dmarc reporting"):
|
|
||||||
server.systemctl("start rspamd-dmarc-reporter.service")
|
|
||||||
|
|
||||||
with subtest("no warnings or errors"):
|
|
||||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
|
||||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
|
||||||
server.fail("journalctl -u dovecot2 | grep -i error >&2")
|
|
||||||
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
|
|
||||||
server.fail(
|
|
||||||
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -v 'FTS Xapian: Box is empty' | grep -i warning >&2"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
}
|
|
61
tests/intern.nix
Normal file
61
tests/intern.nix
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# nixos-mailserver: a simple mail server
|
||||||
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
|
import <nixpkgs/nixos/tests/make-test.nix> {
|
||||||
|
|
||||||
|
machine =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
|
||||||
|
loginAccounts = {
|
||||||
|
"user1@example.com" = {
|
||||||
|
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
vmailGroupName = "vmail";
|
||||||
|
vmailUID = 5000;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
testScript =
|
||||||
|
''
|
||||||
|
$machine->start;
|
||||||
|
$machine->waitForUnit("multi-user.target");
|
||||||
|
|
||||||
|
subtest "user exists", sub {
|
||||||
|
$machine->succeed("cat /etc/shadow | grep 'user1\@example.com'");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "password is set", sub {
|
||||||
|
$machine->succeed("cat /etc/shadow | grep 'user1\@example.com:\$6\$/z4n8AQl6K\$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/:1::::::'");
|
||||||
|
};
|
||||||
|
|
||||||
|
subtest "vmail gid is set correctly", sub {
|
||||||
|
$machine->succeed("getent group vmail | grep 5000");
|
||||||
|
$machine->succeed("systemctl status opensmtpd.service -l >&2");
|
||||||
|
};
|
||||||
|
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,195 +0,0 @@
|
||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, ...}:
|
|
||||||
|
|
||||||
let
|
|
||||||
sendMail = pkgs.writeTextFile {
|
|
||||||
"name" = "send-mail-to-send-only-account";
|
|
||||||
"text" = ''
|
|
||||||
EHLO mail.example.com
|
|
||||||
MAIL FROM: none@example.com
|
|
||||||
RCPT TO: send-only@example.com
|
|
||||||
QUIT
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
hashPassword = password: pkgs.runCommand
|
|
||||||
"password-${password}-hashed"
|
|
||||||
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
|
|
||||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
hashedPasswordFile = hashPassword "my-password";
|
|
||||||
passwordFile = pkgs.writeText "password" "my-password";
|
|
||||||
in
|
|
||||||
pkgs.nixosTest {
|
|
||||||
name = "internal";
|
|
||||||
nodes = {
|
|
||||||
machine = { config, pkgs, ... }: {
|
|
||||||
imports = [
|
|
||||||
./../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
|
||||||
'')];
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" "domain.com" ];
|
|
||||||
localDnsResolver = false;
|
|
||||||
|
|
||||||
loginAccounts = {
|
|
||||||
"user1@example.com" = {
|
|
||||||
hashedPasswordFile = hashedPasswordFile;
|
|
||||||
};
|
|
||||||
"user2@example.com" = {
|
|
||||||
hashedPasswordFile = hashedPasswordFile;
|
|
||||||
aliasesRegexp = [''/^user2.*@domain\.com$/''];
|
|
||||||
};
|
|
||||||
"send-only@example.com" = {
|
|
||||||
hashedPasswordFile = hashPassword "send-only";
|
|
||||||
sendOnly = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
forwards = {
|
|
||||||
# user2@example.com is a local account and its mails are
|
|
||||||
# also forwarded to user1@example.com
|
|
||||||
"user2@example.com" = "user1@example.com";
|
|
||||||
};
|
|
||||||
|
|
||||||
vmailGroupName = "vmail";
|
|
||||||
vmailUID = 5000;
|
|
||||||
|
|
||||||
enableImap = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
testScript = ''
|
|
||||||
machine.start()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
|
|
||||||
with subtest("mail forwarded can are locally kept"):
|
|
||||||
# A mail sent to user2@example.com is in the user1@example.com mailbox
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username user1@example.com",
|
|
||||||
"--from-addr user1@example.com",
|
|
||||||
"--to-addr user2@example.com",
|
|
||||||
"--src-password-file ${passwordFile}",
|
|
||||||
"--dst-password-file ${passwordFile}",
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# A mail sent to user2@example.com is in the user2@example.com mailbox
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username user2@example.com",
|
|
||||||
"--from-addr user1@example.com",
|
|
||||||
"--to-addr user2@example.com",
|
|
||||||
"--src-password-file ${passwordFile}",
|
|
||||||
"--dst-password-file ${passwordFile}",
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("regex email alias are received"):
|
|
||||||
# A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username user2@example.com",
|
|
||||||
"--from-addr user1@example.com",
|
|
||||||
"--to-addr user2-regex-alias@domain.com",
|
|
||||||
"--src-password-file ${passwordFile}",
|
|
||||||
"--dst-password-file ${passwordFile}",
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("user can send from regex email alias"):
|
|
||||||
# A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received
|
|
||||||
machine.succeed(
|
|
||||||
" ".join(
|
|
||||||
[
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--smtp-username user2@example.com",
|
|
||||||
"--from-addr user2-regex-alias@domain.com",
|
|
||||||
"--to-addr user1@example.com",
|
|
||||||
"--src-password-file ${passwordFile}",
|
|
||||||
"--dst-password-file ${passwordFile}",
|
|
||||||
"--ignore-dkim-spf",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("vmail gid is set correctly"):
|
|
||||||
machine.succeed("getent group vmail | grep 5000")
|
|
||||||
|
|
||||||
with subtest("mail to send only accounts is rejected"):
|
|
||||||
machine.wait_for_open_port(25)
|
|
||||||
# TODO put this blocking into the systemd units
|
|
||||||
machine.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
machine.succeed(
|
|
||||||
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q '554 5.5.0 Error'"
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("rspamd controller serves web ui"):
|
|
||||||
machine.succeed(
|
|
||||||
"set +o pipefail; ${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
|
|
||||||
)
|
|
||||||
|
|
||||||
with subtest("imap port 143 is closed and imaps is serving SSL"):
|
|
||||||
machine.wait_for_closed_port(143)
|
|
||||||
machine.wait_for_open_port(993)
|
|
||||||
machine.succeed(
|
|
||||||
"echo | ${pkgs.openssl}/bin/openssl s_client -connect localhost:993 | grep 'New, TLS'"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
}
|
|
183
tests/ldap.nix
183
tests/ldap.nix
|
@ -1,183 +0,0 @@
|
||||||
{ pkgs ? import <nixpkgs> {}
|
|
||||||
, ...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
|
||||||
bindPassword = "unsafegibberish";
|
|
||||||
alicePassword = "testalice";
|
|
||||||
bobPassword = "testbob";
|
|
||||||
in
|
|
||||||
pkgs.nixosTest {
|
|
||||||
name = "ldap";
|
|
||||||
nodes = {
|
|
||||||
machine = { config, pkgs, ... }: {
|
|
||||||
imports = [
|
|
||||||
./../default.nix
|
|
||||||
./lib/config.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
|
|
||||||
services.openssh = {
|
|
||||||
enable = true;
|
|
||||||
permitRootLogin = "yes";
|
|
||||||
};
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
|
||||||
'')];
|
|
||||||
|
|
||||||
environment.etc.bind-password.text = bindPassword;
|
|
||||||
|
|
||||||
services.openldap = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
children = {
|
|
||||||
"cn=schema".includes = [
|
|
||||||
"${pkgs.openldap}/etc/schema/core.ldif"
|
|
||||||
"${pkgs.openldap}/etc/schema/cosine.ldif"
|
|
||||||
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
|
|
||||||
"${pkgs.openldap}/etc/schema/nis.ldif"
|
|
||||||
];
|
|
||||||
"olcDatabase={1}mdb" = {
|
|
||||||
attrs = {
|
|
||||||
objectClass = [
|
|
||||||
"olcDatabaseConfig"
|
|
||||||
"olcMdbConfig"
|
|
||||||
];
|
|
||||||
olcDatabase = "{1}mdb";
|
|
||||||
olcDbDirectory = "/var/lib/openldap/example";
|
|
||||||
olcSuffix = "dc=example";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
declarativeContents."dc=example" = ''
|
|
||||||
dn: dc=example
|
|
||||||
objectClass: domain
|
|
||||||
dc: example
|
|
||||||
|
|
||||||
dn: cn=mail,dc=example
|
|
||||||
objectClass: organizationalRole
|
|
||||||
objectClass: simpleSecurityObject
|
|
||||||
objectClass: top
|
|
||||||
cn: mail
|
|
||||||
userPassword: ${bindPassword}
|
|
||||||
|
|
||||||
dn: ou=users,dc=example
|
|
||||||
objectClass: organizationalUnit
|
|
||||||
ou: users
|
|
||||||
|
|
||||||
dn: cn=alice,ou=users,dc=example
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
cn: alice
|
|
||||||
sn: Foo
|
|
||||||
mail: alice@example.com
|
|
||||||
userPassword: ${alicePassword}
|
|
||||||
|
|
||||||
dn: cn=bob,ou=users,dc=example
|
|
||||||
objectClass: inetOrgPerson
|
|
||||||
cn: bob
|
|
||||||
sn: Bar
|
|
||||||
mail: bob@example.com
|
|
||||||
userPassword: ${bobPassword}
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" ];
|
|
||||||
localDnsResolver = false;
|
|
||||||
|
|
||||||
ldap = {
|
|
||||||
enable = true;
|
|
||||||
uris = [
|
|
||||||
"ldap://"
|
|
||||||
];
|
|
||||||
bind = {
|
|
||||||
dn = "cn=mail,dc=example";
|
|
||||||
passwordFile = "/etc/bind-password";
|
|
||||||
};
|
|
||||||
searchBase = "ou=users,dc=example";
|
|
||||||
searchScope = "sub";
|
|
||||||
};
|
|
||||||
|
|
||||||
vmailGroupName = "vmail";
|
|
||||||
vmailUID = 5000;
|
|
||||||
|
|
||||||
enableImap = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
testScript = ''
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
machine.start()
|
|
||||||
machine.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# This function retrieves the ldap table file from a postconf
|
|
||||||
# command.
|
|
||||||
# A key lookup is achived and the returned value is compared
|
|
||||||
# to the expected value.
|
|
||||||
def test_lookup(postconf_cmdline, key, expected):
|
|
||||||
conf = machine.succeed(postconf_cmdline).rstrip()
|
|
||||||
ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1)
|
|
||||||
value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip()
|
|
||||||
try:
|
|
||||||
assert value == expected
|
|
||||||
except AssertionError:
|
|
||||||
print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr)
|
|
||||||
raise
|
|
||||||
|
|
||||||
with subtest("Test postmap lookups"):
|
|
||||||
test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com")
|
|
||||||
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com")
|
|
||||||
|
|
||||||
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com")
|
|
||||||
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com")
|
|
||||||
|
|
||||||
with subtest("Test doveadm lookups"):
|
|
||||||
machine.succeed("doveadm user -u alice@example.com")
|
|
||||||
machine.succeed("doveadm user -u bob@example.com")
|
|
||||||
|
|
||||||
with subtest("Files containing secrets are only readable by root"):
|
|
||||||
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
|
|
||||||
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
|
|
||||||
|
|
||||||
with subtest("Test account/mail address binding"):
|
|
||||||
machine.fail(" ".join([
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--smtp-username alice@example.com",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username bob@example.com",
|
|
||||||
"--from-addr bob@example.com",
|
|
||||||
"--to-addr aliceb@example.com",
|
|
||||||
"--src-password-file <(echo '${alicePassword}')",
|
|
||||||
"--dst-password-file <(echo '${bobPassword}')",
|
|
||||||
"--ignore-dkim-spf"
|
|
||||||
]))
|
|
||||||
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
|
||||||
|
|
||||||
with subtest("Test mail delivery"):
|
|
||||||
machine.succeed(" ".join([
|
|
||||||
"mail-check send-and-read",
|
|
||||||
"--smtp-port 587",
|
|
||||||
"--smtp-starttls",
|
|
||||||
"--smtp-host localhost",
|
|
||||||
"--smtp-username alice@example.com",
|
|
||||||
"--imap-host localhost",
|
|
||||||
"--imap-username bob@example.com",
|
|
||||||
"--from-addr alice@example.com",
|
|
||||||
"--to-addr bob@example.com",
|
|
||||||
"--src-password-file <(echo '${alicePassword}')",
|
|
||||||
"--dst-password-file <(echo '${bobPassword}')",
|
|
||||||
"--ignore-dkim-spf"
|
|
||||||
]))
|
|
||||||
'';
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
# nixos-mailserver: a simple mail server
|
# nixos-mailserver: a simple mail server
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
# Copyright (C) 2016-2017 Robin Raymond
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
@ -14,9 +14,9 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
import <nixpkgs/nixos/tests/make-test-python.nix> {
|
import ./../../nixpkgs/nixos/tests/make-test.nix {
|
||||||
|
|
||||||
nodes.machine =
|
machine =
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
@ -26,6 +26,6 @@ import <nixpkgs/nixos/tests/make-test-python.nix> {
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
''
|
''
|
||||||
machine.wait_for_unit("multi-user.target");
|
$machine->waitForUnit("multi-user.target");
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
# This tests is used to test features requiring several mail domains.
|
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, ...}:
|
|
||||||
|
|
||||||
let
|
|
||||||
hashPassword = password: pkgs.runCommand
|
|
||||||
"password-${password}-hashed"
|
|
||||||
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; }
|
|
||||||
''
|
|
||||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
|
||||||
'';
|
|
||||||
|
|
||||||
password = pkgs.writeText "password" "password";
|
|
||||||
|
|
||||||
domainGenerator = domain: { config, pkgs, ... }: {
|
|
||||||
imports = [../default.nix];
|
|
||||||
virtualisation.memorySize = 1024;
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.${domain}";
|
|
||||||
domains = [ domain ];
|
|
||||||
localDnsResolver = false;
|
|
||||||
loginAccounts = {
|
|
||||||
"user@${domain}" = {
|
|
||||||
hashedPasswordFile = hashPassword "password";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
enableImap = true;
|
|
||||||
enableImapSsl = true;
|
|
||||||
};
|
|
||||||
services.dnsmasq = {
|
|
||||||
enable = true;
|
|
||||||
# Fixme: once nixos-22.11 has been removed, could be replaced by
|
|
||||||
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
|
|
||||||
extraConfig = ''
|
|
||||||
mx-host=domain1.com,domain1,10
|
|
||||||
mx-host=domain2.com,domain2,10
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
in
|
|
||||||
|
|
||||||
pkgs.nixosTest {
|
|
||||||
name = "multiple";
|
|
||||||
nodes = {
|
|
||||||
domain1 = {...}: {
|
|
||||||
imports = [
|
|
||||||
../default.nix
|
|
||||||
(domainGenerator "domain1.com")
|
|
||||||
];
|
|
||||||
mailserver.forwards = {
|
|
||||||
"non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"];
|
|
||||||
"non@domain1.com" = ["user@domain2.com" "user@domain1.com"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
domain2 = domainGenerator "domain2.com";
|
|
||||||
client = { config, pkgs, ... }: {
|
|
||||||
environment.systemPackages = [
|
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
|
||||||
'')];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
testScript = ''
|
|
||||||
start_all()
|
|
||||||
|
|
||||||
domain1.wait_for_unit("multi-user.target")
|
|
||||||
domain2.wait_for_unit("multi-user.target")
|
|
||||||
|
|
||||||
# TODO put this blocking into the systemd units?
|
|
||||||
domain1.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
domain2.wait_until_succeeds(
|
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
|
||||||
)
|
|
||||||
|
|
||||||
# user@domain1.com sends a mail to user@domain2.com
|
|
||||||
client.succeed(
|
|
||||||
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Send a mail to the address forwarded and check it is in the recipient mailbox
|
|
||||||
client.succeed(
|
|
||||||
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
|
||||||
)
|
|
||||||
'';
|
|
||||||
}
|
|
|
@ -1,7 +1,3 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
sed -i -e "s/v[0-9]\+\.[0-9]\+\.[0-9]\+/$1/g" README.md
|
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…
Reference in a new issue