diff --git a/.envrc b/.envrc deleted file mode 100644 index 069abc3..0000000 --- a/.envrc +++ /dev/null @@ -1,3 +0,0 @@ -# shellcheck shell=bash - -use flake diff --git a/.forgejo/workflows/build.yaml b/.forgejo/workflows/build.yaml deleted file mode 100644 index 7a76f6c..0000000 --- a/.forgejo/workflows/build.yaml +++ /dev/null @@ -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 }} diff --git a/.gitignore b/.gitignore index 0d3fe25..b2be92b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1 @@ result -.direnv -.pre-commit-config.yaml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 8901bb5..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,18 +0,0 @@ -.hydra-cli: - image: docker.nix-community.org/nixpkgs/nix-flakes - script: - - nix run --inputs-from ./. nixpkgs#hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver "${jobset}" - -hydra-pr: - extends: .hydra-cli - only: - - merge_requests - variables: - jobset: $CI_MERGE_REQUEST_IID - -hydra-master: - extends: .hydra-cli - only: - - master - variables: - jobset: master diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix deleted file mode 100644 index 6877235..0000000 --- a/.hydra/declarative-jobsets.nix +++ /dev/null @@ -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 = 300; - schedulingshares = 20; - enableemail = false; - emailoverride = ""; - keepnr = 1; - type = 1; - flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; - }) prs; - mkFlakeJobset = branch: { - description = "Build ${branch} branch of Simple NixOS MailServer"; - checkinterval = 300; - enabled = "1"; - schedulingshares = 100; - enableemail = false; - emailoverride = ""; - keepnr = 3; - hidden = false; - type = 1; - flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}"; - }; - - desc = prJobsets // { - "master" = mkFlakeJobset "master"; - "nixos-24.11" = mkFlakeJobset "nixos-24.11"; - "nixos-25.05" = mkFlakeJobset "nixos-25.05"; - }; - - log = { - pulls = prs; - jobsets = desc; - }; - -in -{ - jobsets = pkgs.runCommand "spec-jobsets.json" { } '' - cat >$out <tmp < ~/.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 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c46adf4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: nix +script: + - nix-build tests/intern.nix + - nix-build tests/extern.nix + +cache: + directories: + - /nix/store diff --git a/README.md b/README.md index ef3042a..1b0cff8 100644 --- a/README.md +++ b/README.md @@ -1,106 +1,381 @@ # ![Simple Nixos MailServer][logo] - ![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 -For each NixOS release, we publish a branch. You then have to use the -SNM branch corresponding to your NixOS version. +## Stable Releases -* For NixOS 25.05 - * Use the [SNM branch `nixos-25.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.05) - * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/) - * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05) -* For NixOS 24.11 - * Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11) - * [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/) - * [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11) -* For NixOS unstable - * Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - * [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) +* [SNM v2.0.4](https://github.com/r-raymond/nixos-mailserver/releases/v2.0.4) + +[Latest Release (Candidate)](https://github.com/r-raymond/nixos-mailserver/releases/latest) + +[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm) +This is a very low volume list where new releases of SNM are announced, so you +can stay up to date with bug fixes and updates. All announcements are signed by +the gpg key with fingerprint + +``` +D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A +``` ## Features - -* [x] Continous Integration Testing -* [x] Multiple Domains -* Postfix - * [x] SMTP on port 25 - * [x] Submission TLS on port 465 - * [x] Submission StartTLS on port 587 - * [x] LMTP with Dovecot -* Dovecot - * [x] Maildir folders - * [x] IMAP with TLS on port 993 - * [x] POP3 with TLS on port 995 - * [x] IMAP with StartTLS on port 143 - * [x] POP3 with StartTLS on port 110 -* Certificates - * [x] ACME - * [x] Custom certificates -* Spam Filtering - * [x] Via Rspamd -* Virus Scanning - * [x] Via ClamAV -* DKIM Signing - * [x] Via Rspamd -* User Management - * [x] Declarative user management - * [x] Declarative password management - * [x] LDAP users -* Sieve - * [x] Allow user defined sieve scripts - * [x] Moving mails from/to junk trains the Bayes filter - * [x] ManageSieve support -* User Aliases - * [x] Regular aliases - * [x] Catch all aliases +### v2.0 + * [x] Continous Integration Testing + * [x] Multiple Domains + * Postfix MTA + - [x] smtp on port 25 + - [x] submission port 587 + - [x] lmtp with dovecot + * Dovecot + - [x] maildir folders + - [x] imap starttls on port 143 + - [x] pop3 starttls on port 110 + * Certificates + - [x] manual certificates + - [x] on the fly creation + - [x] Let's Encrypt + * Spam Filtering + - [x] via rspamd + * Virus Scanning + - [x] via clamav + * DKIM Signing + - [x] via opendkim + * User Management + - [x] declarative user management + - [x] declarative password management + * Sieves + - [x] A simple standard script that moves spam + - [x] Allow user defined sieve scripts + * User Aliases + - [x] Regular aliases + - [x] Catch all aliases ### In the future -* Automatic client configuration - * [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration) - * [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019) - * [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac) -* DKIM Signing - * [ ] Allow per domain selectors - * [ ] Allow passing DKIM signing keys -* Improve the Forwarding Experience - * [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html) - * [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd) -* User management - * [ ] Allow local and LDAP user to coexist -* OpenID Connect - * Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166) + * DKIM Signing + - [ ] Allow a per domain selector + +### Changelog + +#### v1.0 -> v1.1 + * Changed structure to Nix Modules + * Adds Sieve support + +#### v1.1 -> v2.0 + * rename domain to fqdn, seperate fqdn from domains + * multi domain support + +### Quick Start + +```nix +{ config, pkgs, ... }: +{ + imports = [ + (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/archive/v2.0.4.tar.gz") + ]; + + mailserver = { + enable = true; + fqdn = "mail.example.com"; + domains = [ "example.com" "example2.com" ]; + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + + aliases = [ + "info@example.com" + "postmaster@example.com" + "postmaster@example2.com" + ]; + }; + }; + }; +} +``` + +For a complete list of options, see `default.nix`. -### Get in touch -* Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org) -* IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect) ## How to Set Up a 10/10 Mail Server Guide +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 = ; + 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 +64 bytes from (): 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 +.in-addr.arpa domain name pointer . +``` + +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 + +... +;; ANSWER SECTION: + 10800 IN MX 10 +... +``` + +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: -all` | + +You can check this with `dig -t TXT ` 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=" ; ----- 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=` | + + +You can check this with `dig -t TXT mail._domainkey.` 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 - -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 - -* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices) + * [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices) ### Credits - -* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao) + * send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao) from [TheNounProject](https://thenounproject.com/) is licensed under [CC BY 3.0](http://creativecommons.org/~/3.0/) -* Logo made with [Logomakr.com](https://logomakr.com) + * Logo made with [Logomakr.com](https://logomakr.com) -[logo]: docs/logo.png + + + +[logo]: logo/logo.png diff --git a/default.nix b/default.nix index 9d97410..7958d74 100644 --- a/default.nix +++ b/default.nix @@ -1,5 +1,6 @@ + # 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 # it under the terms of the GNU General Public License as published by @@ -14,254 +15,95 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - lib, - pkgs, - ... -}: +{ config, lib, pkgs, ... }: + +with lib; let - inherit (lib) - literalExpression - literalMD - mkDefault - mkEnableOption - mkOption - mkOptionType - mkRemovedOptionModule - mkRenamedOptionModule - types - warn - ; - cfg = config.mailserver; in { options.mailserver = { enable = mkEnableOption "nixos-mailserver"; - stateVersion = mkOption { - type = types.nullOr types.ints.positive; - default = null; - description = '' - Tracking stateful version changes as an incrementing number. - - When a new release comes out we may require manual migration steps to - be completed, before the new version can be put into production. - - If your `stateVersion` is too low one or multiple assertions may - trigger to give you instructions on what migrations steps are required - to continue. Increase the `stateVersion` as instructed by the assertion - message. - ''; - }; - - openFirewall = mkOption { - type = types.bool; - default = true; - description = "Automatically open ports in the firewall."; - }; - fqdn = mkOption { type = types.str; example = "mx.example.com"; description = "The fully qualified domain name of the mail server."; }; - systemName = mkOption { - type = types.str; - default = "${cfg.systemDomain} mail system"; - defaultText = literalExpression "\${config.mailserver.systemDomain} mail system"; - example = "ACME Corp."; - description = '' - The sender name given in automated reports. - ''; - }; - - systemDomain = mkOption { - type = types.str; - default = - if (config.networking.domain != null && lib.elem config.networking.domain cfg.domains) then - config.networking.domain - else - lib.head cfg.domains; - defaultText = literalExpression '' - if config.networking.domain != null && lib.elem config.networking.domain cfg.domains then - config.networking.domain - else - lib.head cfg.domains - ''; - example = literalExpression "config.networking.domain"; - description = '' - The primary domain used for sending automated reports. - ''; - }; - domains = mkOption { type = types.listOf types.str; example = [ "example.com" ]; - default = [ ]; + default = []; description = "The domains that this mail server serves."; }; - certificateDomains = mkOption { - type = types.listOf types.str; - example = [ - "imap.example.com" - "pop3.example.com" - ]; - default = [ ]; - description = '' - ({option}`mailserver.certificateScheme` == `acme-nginx`) - - Secondary domains and subdomains for which it is necessary to generate a certificate. - ''; - }; - - messageSizeLimit = mkOption { - type = types.int; - example = 52428800; - default = 20971520; - description = "Message size limit enforced by Postfix."; - }; - loginAccounts = mkOption { - type = types.attrsOf ( - types.submodule ( - { name, ... }: - { - options = { - name = mkOption { - type = types.str; - example = "user1@example.com"; - description = "Username"; - }; + type = types.loaOf (types.submodule ({ name, ... }: { + options = { + name = mkOption { + type = types.str; + example = "user1@example.com"; + description = "Username"; + }; - hashedPassword = mkOption { - type = with types; nullOr str; - default = null; - example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; - description = '' - The user's hashed password. Use `mkpasswd` as follows + hashedPassword = mkOption { + type = types.str; + example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; + description = '' + Hashed password. Use `mkpasswd` as follows - ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' - ``` + ``` + mkpasswd -m sha-512 "super secret password" + ``` + ''; + }; - Warning: this is stored in plaintext in the Nix store! - Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead. - ''; - }; + aliases = mkOption { + type = with types; listOf types.str; + example = ["abuse@example.com" "postmaster@example.com"]; + default = []; + description = '' + A list of aliases of this login account. + ''; + }; - hashedPasswordFile = mkOption { - type = with types; nullOr path; - default = null; - example = "/run/keys/user1-passwordhash"; - description = '' - A file containing the user's hashed password. Use `mkpasswd` as follows + catchAll = mkOption { + type = with types; listOf (enum cfg.domains); + example = ["example.com" "example2.com"]; + default = []; + description = '' + For which domains should this account act as a catch all? + ''; + }; - ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' - ``` - ''; - }; + sieveScript = mkOption { + type = with types; nullOr lines; + default = null; + example = '' + require ["fileinto", "mailbox"]; - aliases = mkOption { - type = with types; listOf types.str; - example = [ - "abuse@example.com" - "postmaster@example.com" - ]; - default = [ ]; - description = '' - A list of aliases of this login account. - Note: Use list entries like "@example.com" to create a catchAll - that allows sending from all email addresses in these domain. - ''; - }; + if address :is "from" "notifications@github.com" { + fileinto :create "GitHub"; + stop; + } - aliasesRegexp = mkOption { - type = with types; listOf types.str; - example = [ ''/^tom\..*@domain\.com$/'' ]; - default = [ ]; - description = '' - Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). - ''; - }; + # This must be the last rule, it will check if list-id is set, and + # file the message into the Lists folder for further investigation + elsif header :matches "list-id" "" { + fileinto :create "Lists"; + stop; + } + ''; + description = '' + Per-user sieve script. + ''; + }; + }; - catchAll = mkOption { - type = with types; listOf (enum cfg.domains); - example = [ - "example.com" - "example2.com" - ]; - default = [ ]; - description = '' - For which domains should this account act as a catch all? - Note: Does not allow sending from all addresses of these domains. - ''; - }; - - quota = mkOption { - type = with types; nullOr types.str; - default = null; - example = "2G"; - description = '' - Per user quota rules. Accepted sizes are `xx k/M/G/T` with the - obvious meaning. Leave blank for the standard quota `100G`. - ''; - }; - - sieveScript = mkOption { - type = with types; nullOr lines; - default = null; - example = '' - require ["fileinto", "mailbox"]; - - if address :is "from" "gitlab@mg.gitlab.com" { - fileinto :create "GitLab"; - stop; - } - - # This must be the last rule, it will check if list-id is set, and - # file the message into the Lists folder for further investigation - elsif header :matches "list-id" "" { - fileinto :create "Lists"; - stop; - } - ''; - description = '' - Per-user sieve script. - ''; - }; - - sendOnly = mkOption { - type = types.bool; - default = false; - description = '' - Specifies if the account should be a send-only account. - Emails sent to send-only accounts will be rejected from - unauthorized senders with the `sendOnlyRejectMessage` - stating the reason. - ''; - }; - - sendOnlyRejectMessage = mkOption { - type = types.str; - default = "This account cannot receive emails."; - description = '' - The message that will be returned to the sender when an email is - sent to a send-only account. Only used if the account is marked - as send-only. - ''; - }; - }; - - config.name = mkDefault name; - } - ) - ); + config.name = mkDefault name; + })); example = { user1 = { hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; @@ -276,394 +118,41 @@ in follows ``` - nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' + mkpasswd -m sha-512 "super secret password" ``` ''; - default = { }; - }; - - ldap = { - enable = mkEnableOption "LDAP support"; - - uris = mkOption { - type = types.listOf types.str; - example = literalExpression '' - [ - "ldaps://ldap1.example.com" - "ldaps://ldap2.example.com" - ] - ''; - description = '' - URIs where your LDAP server can be reached - ''; - }; - - startTls = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable StartTLS upon connection to the server. - ''; - }; - - tlsCAFile = mkOption { - type = types.path; - default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; - defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; - description = '' - Certifificate trust anchors used to verify the LDAP server certificate. - ''; - }; - - bind = { - dn = mkOption { - type = types.str; - example = "cn=mail,ou=accounts,dc=example,dc=com"; - description = '' - Distinguished name used by the mail server to do lookups - against the LDAP servers. - ''; - }; - - passwordFile = mkOption { - type = types.str; - example = "/run/my-secret"; - description = '' - A file containing the password required to authenticate against the LDAP servers. - ''; - }; - }; - - searchBase = mkOption { - type = types.str; - example = "ou=people,ou=accounts,dc=example,dc=com"; - description = '' - Base DN at below which to search for users accounts. - ''; - }; - - searchScope = mkOption { - type = types.enum [ - "sub" - "base" - "one" - ]; - default = "sub"; - description = '' - Search scope below which users accounts are looked for. - ''; - }; - - dovecot = { - userAttrs = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - LDAP attributes to be retrieved during userdb lookups. - - See the users_attrs reference at - https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-attrs - in the Dovecot manual. - ''; - }; - - userFilter = mkOption { - type = types.str; - default = "mail=%{user}"; - example = "(&(objectClass=inetOrgPerson)(mail=%{user}))"; - description = '' - Filter for user lookups in Dovecot. - - See the user_filter reference at - https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-filter - in the Dovecot manual. - ''; - }; - - passAttrs = mkOption { - type = types.str; - default = "userPassword=password"; - description = '' - LDAP attributes to be retrieved during passdb lookups. - - See the pass_attrs reference at - https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#pass-attrs - in the Dovecot manual. - ''; - }; - - passFilter = mkOption { - type = types.nullOr types.str; - default = "mail=%{user}"; - example = "(&(objectClass=inetOrgPerson)(mail=%{user}))"; - description = '' - Filter for password lookups in Dovecot. - - See the pass_filter reference for - https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#pass-filter - in the Dovecot manual. - ''; - }; - }; - - postfix = { - filter = mkOption { - type = types.str; - default = "mail=%s"; - example = "(&(objectClass=inetOrgPerson)(mail=%s))"; - description = '' - LDAP filter used to search for an account by mail, where - `%s` is a substitute for the address in - question. - ''; - }; - - uidAttribute = mkOption { - type = types.str; - default = "mail"; - example = "uid"; - description = '' - The LDAP attribute referencing the account name for a user. - ''; - }; - - mailAttribute = mkOption { - type = types.str; - default = "mail"; - description = '' - The LDAP attribute holding mail addresses for a user. - ''; - }; - }; - }; - - indexDir = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - Folder to store search indices. If null, indices are stored - along with email, which could not necessarily be desirable, - especially when {option}`mailserver.fullTextSearch.enable` is `true` since - indices it creates are voluminous and do not need to be backed - up. - - Be careful when changing this option value since all indices - would be recreated at the new location (and clients would need - to resynchronize). - - Note the some variables can be used in the file path. See - https://doc.dovecot.org/2.3/configuration_manual/mail_location/#variables - for details. - ''; - example = "/var/lib/dovecot/indices"; - }; - - fullTextSearch = { - enable = mkEnableOption '' - Full text search indexing with Xapian through the fts_flatcurve plugin. - This has significant performance and disk space cost. - ''; - memoryLimit = mkOption { - type = types.nullOr types.int; - default = null; - example = 2000; - description = '' - Memory limit for the indexer process, in MiB. - If null, leaves the default (which is rather low), - and if 0, no limit. - ''; - }; - - autoIndex = mkOption { - type = types.bool; - default = true; - description = "Enable automatic indexing of messages as they are received or modified."; - }; - autoIndexExclude = mkOption { - type = types.listOf types.str; - default = [ ]; - example = [ - "\\Trash" - "SomeFolder" - "Other/*" - ]; - description = '' - Mailboxes to exclude from automatic indexing. - ''; - }; - - enforced = mkOption { - type = types.enum [ - "yes" - "no" - "body" - ]; - default = "no"; - description = '' - Fail searches when no index is available. If set to - `body`, then only body searches (as opposed to - header) are affected. If set to `no`, searches may - fall back to a very slow brute force search. - ''; - }; - - languages = mkOption { - type = types.nonEmptyListOf types.str; - default = [ "en" ]; - example = [ - "en" - "de" - ]; - description = '' - A list of languages that the full text search should detect. - At least one language must be specified. - The language listed first is the default and is used when language recognition fails. - See . - ''; - }; - - substringSearch = mkOption { - type = types.bool; - default = false; - description = '' - If enabled, allows substring searches. - See . - - Enabling this requires significant additional storage space. - ''; - }; - - headerExcludes = mkOption { - type = types.listOf types.str; - default = [ - "Received" - "DKIM-*" - "X-*" - "Comments" - ]; - description = '' - The list of headers to exclude. - See . - ''; - }; - - filters = mkOption { - type = types.listOf types.str; - default = [ - "normalizer-icu" - "snowball" - "stopwords" - ]; - description = '' - The list of filters to apply. - . - ''; - }; - }; - - lmtpSaveToDetailMailbox = mkOption { - type = types.enum [ - "yes" - "no" - ]; - default = "yes"; - description = '' - If an email address is delimited by a "+", should it be filed into a - mailbox matching the string after the "+"? For example, - user1+test@example.com would be filed into the mailbox "test". - ''; - }; - - lmtpMemoryLimit = mkOption { - type = types.int; - default = 256; - description = '' - The memory limit for the LMTP service, in megabytes. - ''; - }; - - quotaStatusMemoryLimit = mkOption { - type = types.int; - default = 256; - description = '' - The memory limit for the quota-status service, in megabytes. - ''; + default = {}; }; extraVirtualAliases = mkOption { - type = - let - loginAccount = mkOptionType { - name = "Login Account"; - }; - in - with types; - attrsOf (either loginAccount (nonEmptyListOf loginAccount)); + type = types.attrsOf (types.enum (builtins.attrNames cfg.loginAccounts)); example = { "info@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com"; - "multi@example.com" = [ - "user1@example.com" - "user2@example.com" - ]; }; description = '' - Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that - all mail to `info@example.com` is forwarded to `user1@example.com`. Note + Virtual Aliases. A virtual alias `"info@example2.com" = "user1@example.com"` means that + all mail to `info@example2.com` is forwarded to `user1@example.com`. Note that it is expected that `postmaster@example.com` and `abuse@example.com` is forwarded to some valid email address. (Alternatively you can create login accounts for `postmaster` and (or) `abuse`). Furthermore, it also allows - the user `user1@example.com` to send emails as `info@example.com`. - It's also possible to create an alias for multiple accounts. In this - example all mails for `multi@example.com` will be forwarded to both - `user1@example.com` and `user2@example.com`. + the user `user1@example.com` to send emails as `info@example2.com`. ''; - default = { }; + default = {}; }; - forwards = mkOption { - type = with types; attrsOf (either (listOf str) str); + virtualAliases = mkOption { + type = types.attrsOf (types.enum (builtins.attrNames cfg.loginAccounts)); example = { - "user@example.com" = "user@elsewhere.com"; + "info@example.com" = "user1@example.com"; + "postmaster@example.com" = "user1@example.com"; + "abuse@example.com" = "user1@example.com"; }; description = '' - To forward mails to an external address. For instance, - the value {`"user@example.com" = "user@elsewhere.com";}` - means that mails to `user@example.com` are forwarded to - `user@elsewhere.com`. The difference with the - {option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com` - can't send mail as `user@example.com`. Also, this option - allows to forward mails to external addresses. + Alias for extraVirtualAliases. Deprecated. ''; - default = { }; - }; - - rejectSender = mkOption { - type = types.listOf types.str; - example = [ - "example.com" - "spammer@example.net" - ]; - description = '' - Reject emails from these addresses from unauthorized senders. - Use if a spammer is using the same domain or the same sender over and over. - ''; - default = [ ]; - }; - - rejectRecipients = mkOption { - type = types.listOf types.str; - example = [ - "sales@example.com" - "info@example.com" - ]; - description = '' - Reject emails addressed to these local addresses from unauthorized senders. - Use if a spammer has found email addresses in a catchall domain but you do - not want to disable the catchall. - ''; - default = [ ]; + default = {}; }; vmailUID = mkOption { @@ -672,13 +161,13 @@ in description = '' The unix UID of the virtual mail user. Be mindful that if this is changed, you will need to manually adjust the permissions of - `mailDirectory`. + mailDirectory. ''; }; vmailUserName = mkOption { type = types.str; - default = "virtualMail"; + default = "vmail"; description = '' The user name and group name of the user that owns the directory where all the mail is stored. @@ -687,7 +176,7 @@ in vmailGroupName = mkOption { type = types.str; - default = "virtualMail"; + default = "vmail"; description = '' The user name and group name of the user that owns the directory where all the mail is stored. @@ -702,113 +191,28 @@ in ''; }; - useFsLayout = mkOption { - type = types.bool; - default = false; + certificateScheme = mkOption { + type = types.enum [ 1 2 3 ]; + default = 2; description = '' - Sets whether dovecot should organize mail in subdirectories: + Certificate Files. There are three options for these. - - /var/vmail/example.com/user/.folder.subfolder/ (default layout) - - /var/vmail/example.com/user/folder/subfolder/ (FS layout) - - See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details. + 1) You specify locations and manually copy certificates there. + 2) You let the server create new (self signed) certificates on the fly. + 3) You let the server create a certificate via `Let's Encrypt`. Note that + this implies that a stripped down webserver has to be started. This also + implies that the FQDN must be set as an `A` record to point to the IP of + the server. In particular port 80 on the server will be opened. For details + on how to set up the domain records, see the guide in the readme. ''; }; - useUTF8FolderNames = mkOption { - type = types.bool; - default = false; - description = '' - Store mailbox names on disk using UTF-8 instead of modified UTF-7 (mUTF-7). - ''; - }; - - hierarchySeparator = mkOption { - type = types.str; - default = "."; - description = '' - The hierarchy separator for mailboxes used by dovecot for the namespace 'inbox'. - Dovecot defaults to "." but recommends "/". - This affects how mailboxes appear to mail clients and sieve scripts. - For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example". - This does not determine the way your mails are stored on disk. - See https://doc.dovecot.org/main/core/config/namespaces.html#namespaces for details. - ''; - }; - - mailboxes = mkOption { - description = '' - The mailboxes for dovecot. - Depending on the mail client used it might be necessary to change some mailbox's name. - ''; - default = { - Trash = { - auto = "no"; - specialUse = "Trash"; - }; - Junk = { - auto = "subscribe"; - specialUse = "Junk"; - }; - Drafts = { - auto = "subscribe"; - specialUse = "Drafts"; - }; - Sent = { - auto = "subscribe"; - specialUse = "Sent"; - }; - }; - }; - - certificateScheme = - let - schemes = [ - "manual" - "selfsigned" - "acme-nginx" - "acme" - ]; - translate = - i: - warn - "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${ - (builtins.elemAt schemes (i - 1)) - }\"'." - (builtins.elemAt schemes (i - 1)); - in - mkOption { - type = - with types; - coercedTo (enum [ - 1 - 2 - 3 - ]) translate (enum schemes); - default = "selfsigned"; - description = '' - The scheme to use for managing TLS certificates: - - 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and - {option}`mailserver.keyFile` and manually copy certificates there. - 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly. - 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org) - via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for - {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly - configured to point to your server (see the [setup guide](setup-guide.rst) for more information). - 4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled - Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded - when the certificate is renewed. - ''; - }; - certificateFile = mkOption { type = types.path; example = "/root/mail-server.crt"; description = '' - ({option}`mailserver.certificateScheme` == `manual`) - - Location of the certificate. + Scheme 1) + Location of the certificate ''; }; @@ -816,9 +220,8 @@ in type = types.path; example = "/root/mail-server.key"; description = '' - ({option}`mailserver.certificateScheme` == `manual`) - - Location of the key file. + Scheme 1) + Location of the key file ''; }; @@ -826,65 +229,32 @@ in type = types.path; default = "/var/certs"; description = '' - ({option}`mailserver.certificateScheme` == `selfsigned`) - - This is the folder where the self-signed certificate will be created. The name is - hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the + Sceme 2) + This is the folder where the certificate will be created. The name is + hardcoded to "cert-.pem" and "key-.pem" and the certificate is valid for 10 years. ''; }; - acmeCertificateName = mkOption { - type = types.str; - default = cfg.fqdn; - defaultText = literalExpression "config.mailserver.fqdn"; - example = "example.com"; - description = '' - ({option}`mailserver.certificateScheme` == `acme`) - - When the `acme` `certificateScheme` is selected, you can use this option - to override the default certificate name. This is useful if you've - generated a wildcard certificate, for example. - ''; - }; - enableImap = mkOption { type = types.bool; default = true; description = '' - Whether to enable IMAP with STARTTLS on port 143. - ''; - }; + Whether to enable imap / pop3. Both variants are only supported in the + (sane) startTLS configuration. The ports are - imapMemoryLimit = mkOption { - type = types.int; - default = 256; - description = '' - The memory limit for the imap service, in megabytes. + 110 - Pop3 + 143 - IMAP + 587 - SMTP with login ''; }; enableImapSsl = mkOption { type = types.bool; - default = true; + default = false; description = '' - Whether to enable IMAP with TLS in wrapper-mode on port 993. - ''; - }; - - enableSubmission = mkOption { - type = types.bool; - default = true; - description = '' - Whether to enable SMTP with STARTTLS on port 587. - ''; - }; - - enableSubmissionSsl = mkOption { - type = types.bool; - default = true; - description = '' - Whether to enable SMTP with TLS in wrapper-mode on port 465. + Whether to enable IMAPS, setting this option to true will open port 993 + in the firewall. ''; }; @@ -892,7 +262,12 @@ in type = types.bool; default = false; description = '' - Whether to enable POP3 with STARTTLS on port on port 110. + Whether to enable POP3. Both variants are only supported in the (sane) + startTLS configuration. The ports are + + 110 - Pop3 + 143 - IMAP + 587 - SMTP with login ''; }; @@ -900,27 +275,8 @@ in type = types.bool; default = false; description = '' - Whether to enable POP3 with TLS in wrapper-mode on port 995. - ''; - }; - - enableManageSieve = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable ManageSieve, setting this option to true will open - port 4190 in the firewall. - - The ManageSieve protocol allows users to manage their Sieve scripts on - a remote server with a supported client, including Thunderbird. - ''; - }; - - sieveDirectory = mkOption { - type = types.path; - default = "/var/sieve"; - description = '' - Where to store the sieve scripts. + Whether to enable POP3S, setting this option to true will open port 995 + in the firewall. ''; }; @@ -945,7 +301,7 @@ in type = types.str; default = "mail"; description = '' - The DKIM selector. + ''; }; @@ -953,521 +309,38 @@ in type = types.path; default = "/var/dkim"; description = '' - The DKIM directory. + ''; }; - dkimKeyType = mkOption { - type = types.enum [ - "rsa" - "ed25519" - ]; - default = "rsa"; - description = '' - The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018). - - If you have already deployed a key with a different type than specified - here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get - this package to generate a key with the new type, you will either have to - change the selector or delete the old key file. - ''; - }; - - dkimKeyBits = mkOption { - type = types.int; - default = 2048; - description = '' - How many bits in generated DKIM keys. RFC8301 suggests a minimum RSA key length of 2048 bit. - - If you have already deployed a key with a different number of bits than specified - here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get - this package to generate a key with the new number of bits, you will either have to - change the selector or delete the old key file. - ''; - }; - - dmarcReporting = { - enable = mkOption { - type = types.bool; - default = false; - description = '' - Whether to send out aggregated, daily DMARC reports in response to incoming - mail, when the sender domain defines a DMARC policy including the RUA tag. - - This is helpful for the mail ecosystem, because it allows third parties to - get notified about SPF/DKIM violations originating from their sender domains. - - See https://rspamd.com/doc/modules/dmarc.html#reporting - ''; - }; - - excludeDomains = mkOption { - type = types.listOf types.str; - default = [ ]; - description = '' - List of domains or eSLDs to be excluded from DMARC reports. - ''; - }; - }; - - debug = { - all = mkOption { - type = types.bool; - default = false; - description = '' - Whether to enable verbose logging for all mailserver related services. - This intended be used for development purposes only, you probably - don't want to enable this unless you're hacking on nixos-mailserver. - ''; - }; - - dovecot = mkOption { - type = types.bool; - default = cfg.debug.all; - defaultText = lib.literalExpression "config.mailserver.debug.all"; - description = '' - Whether to enable verbose logging for Dovecot. - ''; - }; - - rspamd = mkOption { - type = types.bool; - default = cfg.debug.all; - defaultText = lib.literalExpression "config.mailserver.debug.all"; - description = '' - Whether to enable verbose logging for Rspamd. - ''; - }; - }; - - maxConnectionsPerUser = mkOption { - type = types.int; - default = 100; - description = '' - Maximum number of IMAP/POP3 connections allowed for a user from each IP address. - E.g. a value of 50 allows for 50 IMAP and 50 POP3 connections at the same - time for a single user. - ''; - }; - - localDnsResolver = mkOption { - type = types.bool; - default = true; - description = '' - Runs a local DNS resolver (kresd) as recommended when running rspamd. This prevents your log file from filling up with rspamd_monitored_dns_mon entries. - ''; - }; - - recipientDelimiter = mkOption { - type = types.str; - default = "+"; - description = '' - Configure the recipient delimiter. - ''; - }; - - redis = { - configureLocally = mkOption { - type = types.bool; - default = true; - description = '' - Whether to provision a local Redis instance. - ''; - }; - - address = mkOption { - type = types.str; - # read the default from nixos' redis module - default = config.services.redis.servers.rspamd.unixSocket; - defaultText = literalExpression "config.services.redis.servers.rspamd.unixSocket"; - description = '' - Path, IP address or hostname that Rspamd should use to contact Redis. - ''; - }; - - port = mkOption { - type = with types; nullOr port; - default = null; - example = literalExpression "config.services.redis.servers.rspamd.port"; - description = '' - Port that Rspamd should use to contact Redis. - ''; - }; - - password = mkOption { - type = types.nullOr types.str; - default = config.services.redis.servers.rspamd.requirePass; - defaultText = literalExpression "config.services.redis.servers.rspamd.requirePass"; - description = '' - Password that rspamd should use to contact redis, or null if not required. - ''; - }; - }; - - rewriteMessageId = mkOption { + debug = mkOption { type = types.bool; default = false; description = '' - Rewrites the Message-ID's hostname-part of outgoing emails to the FQDN. - Please be aware that this may cause problems with some mail clients - relying on the original Message-ID. + Whether to enable verbose logging for mailserver related services. This + intended be used for development purposes only, you probably don't want + to enable this unless you're hacking on nixos-mailserver. ''; }; - - sendingFqdn = mkOption { - type = types.str; - default = cfg.fqdn; - defaultText = literalMD "{option}`mailserver.fqdn`"; - example = "myserver.example.com"; - description = '' - The fully qualified domain name of the mail server used to - identify with remote servers. - - If this server's IP serves purposes other than a mail server, - it may be desirable for the server to have a name other than - that to which the user will connect. For example, the user - might connect to mx.example.com, but the server's IP has - reverse DNS that resolves to myserver.example.com; in this - scenario, some mail servers may reject or penalize the - message. - - This setting allows the server to identify as - myserver.example.com when forwarding mail, independently of - {option}`mailserver.fqdn` (which, for SSL reasons, should generally be the name - to which the user connects). - - Set this to the name to which the sending IP's reverse DNS - resolves. - ''; - }; - - monitoring = { - enable = mkEnableOption "monitoring via monit"; - - alertAddress = mkOption { - type = types.str; - description = '' - The email address to send alerts to. - ''; - }; - - config = mkOption { - type = types.str; - default = '' - set daemon 120 with start delay 60 - set mailserver - localhost - - set httpd port 2812 and use address localhost - allow localhost - allow admin:obwjoawijerfoijsiwfj29jf2f2jd - - check filesystem root with path / - if space usage > 80% then alert - if inode usage > 80% then alert - - check system $HOST - if cpu usage > 95% for 10 cycles then alert - if memory usage > 75% for 5 cycles then alert - if swap usage > 20% for 10 cycles then alert - if loadavg (1min) > 90 for 15 cycles then alert - if loadavg (5min) > 80 for 10 cycles then alert - if loadavg (15min) > 70 for 8 cycles then alert - - check process sshd with pidfile /var/run/sshd.pid - start program "${pkgs.systemd}/bin/systemctl start sshd" - stop program "${pkgs.systemd}/bin/systemctl stop sshd" - if failed port 22 protocol ssh for 2 cycles then restart - - check process postfix with pidfile /var/lib/postfix/queue/pid/master.pid - start program = "${pkgs.systemd}/bin/systemctl start postfix" - stop program = "${pkgs.systemd}/bin/systemctl stop postfix" - if failed port 25 protocol smtp for 5 cycles then restart - - check process dovecot with pidfile /var/run/dovecot2/master.pid - start program = "${pkgs.systemd}/bin/systemctl start dovecot2" - stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2" - if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart - - check process rspamd with matching "rspamd: main process" - start program = "${pkgs.systemd}/bin/systemctl start rspamd" - stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" - ''; - defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)"; - description = '' - The configuration used for monitoring via monit. - Use a mail address that you actively check and set it via 'set alert ...'. - ''; - }; - }; - - borgbackup = { - enable = mkEnableOption "backup via borgbackup"; - - repoLocation = mkOption { - type = types.str; - default = "/var/borgbackup"; - description = '' - The location where borg saves the backups. - This can be a local path or a remote location such as user@host:/path/to/repo. - It is exported and thus available as an environment variable to - {option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`. - ''; - }; - - startAt = mkOption { - type = types.str; - default = "hourly"; - description = "When or how often the backup should run. Must be in the format described in systemd.time 7."; - }; - - user = mkOption { - type = types.str; - default = "virtualMail"; - description = "The user borg and its launch script is run as."; - }; - - group = mkOption { - type = types.str; - default = "virtualMail"; - description = "The group borg and its launch script is run as."; - }; - - compression = { - method = mkOption { - type = types.nullOr ( - types.enum [ - "none" - "lz4" - "zstd" - "zlib" - "lzma" - ] - ); - default = null; - description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4."; - }; - - level = mkOption { - type = types.nullOr types.int; - default = null; - description = '' - Denotes the level of compression used by borg. - Most methods accept levels from 0 to 9 but zstd which accepts values from 1 to 22. - If null the decision is left up to borg. - ''; - }; - - auto = mkOption { - type = types.bool; - default = false; - description = "Leaves it to borg to determine whether an individual file should be compressed."; - }; - }; - - encryption = { - method = mkOption { - type = types.enum [ - "none" - "authenticated" - "authenticated-blake2" - "repokey" - "keyfile" - "repokey-blake2" - "keyfile-blake2" - ]; - default = "none"; - description = '' - The backup can be encrypted by choosing any other value than 'none'. - When using encryption the password/passphrase must be provided in `passphraseFile`. - ''; - }; - - passphraseFile = mkOption { - type = types.nullOr types.path; - default = null; - description = "Path to a file containing the encryption password or passphrase."; - }; - }; - - name = mkOption { - type = types.str; - default = "{hostname}-{user}-{now}"; - description = '' - The name of the individual backups as used by borg. - Certain placeholders will be replaced by borg. - ''; - }; - - locations = mkOption { - type = types.listOf types.path; - default = [ cfg.mailDirectory ]; - defaultText = literalExpression "[ config.mailserver.mailDirectory ]"; - description = "The locations that are to be backed up by borg."; - }; - - extraArgumentsForInit = mkOption { - type = types.listOf types.str; - default = [ "--critical" ]; - description = "Additional arguments to add to the borg init command line."; - }; - - extraArgumentsForCreate = mkOption { - type = types.listOf types.str; - default = [ ]; - description = "Additional arguments to add to the borg create command line e.g. '--stats'."; - }; - - cmdPreexec = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - The command to be executed before each backup operation. - This is called prior to borg init in the same script that runs borg init and create and `cmdPostexec`. - ''; - example = '' - export BORG_RSH="ssh -i /path/to/private/key" - ''; - }; - - cmdPostexec = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - The command to be executed after each backup operation. - This is called after borg create completed successfully and in the same script that runs - `cmdPreexec`, borg init and create. - ''; - }; - - }; - - backup = { - enable = mkEnableOption "backup via rsnapshot"; - - snapshotRoot = mkOption { - type = types.path; - default = "/var/rsnapshot"; - description = '' - The directory where rsnapshot stores the backup. - ''; - }; - - cmdPreexec = mkOption { - type = types.nullOr types.str; - default = null; - description = '' - The command to be executed before each backup operation. This is wrapped in a shell script to be called by rsnapshot. - ''; - }; - - cmdPostexec = mkOption { - type = types.nullOr types.str; - default = null; - description = "The command to be executed after each backup operation. This is wrapped in a shell script to be called by rsnapshot."; - }; - - retain = { - hourly = mkOption { - type = types.int; - default = 24; - description = "How many hourly snapshots are retained."; - }; - daily = mkOption { - type = types.int; - default = 7; - description = "How many daily snapshots are retained."; - }; - weekly = mkOption { - type = types.int; - default = 54; - description = "How many weekly snapshots are retained."; - }; - }; - - cronIntervals = mkOption { - type = types.attrsOf types.str; - default = { - # minute, hour, day-in-month, month, weekday (0 = sunday) - hourly = " 0 * * * *"; # Every full hour - daily = "30 3 * * *"; # Every day at 3:30 - weekly = " 0 5 * * 0"; # Every sunday at 5:00 AM - }; - description = '' - Periodicity at which intervals should be run by cron. - Note that the intervals also have to exist in configuration - as retain options. - ''; - }; - }; }; imports = [ - (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] '' - This option is not needed for fts-flatcurve - '') - (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] '' - This option is not needed for fts-flatcurve - '') - (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] '' - This option is not needed for fts-flatcurve - '') - (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] '' - This option is not supported by fts-flatcurve - '') - (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] '' - This option is not needed since fts-xapian 1.8.3 - '') - (mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] '' - Text attachments are always indexed since fts-xapian 1.4.8 - '') - (mkRenamedOptionModule - [ "mailserver" "rebootAfterKernelUpgrade" "enable" ] - [ "system" "autoUpgrade" "allowReboot" ] - ) - (mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] '' - Use `system.autoUpgrade` instead. - '') - ./mail-server/assertions.nix - ./mail-server/borgbackup.nix - ./mail-server/rsnapshot.nix ./mail-server/clamav.nix - ./mail-server/monit.nix ./mail-server/users.nix ./mail-server/environment.nix ./mail-server/networking.nix ./mail-server/systemd.nix ./mail-server/dovecot.nix - ./mail-server/postfix.nix - ./mail-server/rspamd.nix + ./mail-server/opensmtpd.nix + # ./mail-server/rmilter.nix ./mail-server/nginx.nix - ./mail-server/kresd.nix - (mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] '' - SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings. - It may be that they are redundant and are already configured in rspamd like for skip_addresses. - '') - (mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] '' - DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. - '') - (mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] '' - DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization. - '') - (mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] '' - The workaround for the SMTP Smuggling attack is default enabled in Postfix >3.9. Use `services.postfix.config.smtpd_forbid_bare_newline` if you need to deviate from its default. - '') - (mkRenamedOptionModule [ "mailserver" "dmarcReporting" "domain" ] [ "mailserver" "systemDomain" ]) - (mkRenamedOptionModule - [ "mailserver" "dmarcReporting" "organizationName" ] - [ "mailserver" "systemName" ] - ) - (mkRemovedOptionModule [ "mailserver" "dmarcReporting" "localpart" ] '' - The localpart is now fixed at `noreply-dmarc` to simplify the configuration. - '') - (mkRemovedOptionModule [ "mailserver" "dmarcReporting" "email" ] '' - The address is now fixed at `noreply-dmarc@''${config.mailserver.systemDomain}` to simplify the configuration. - '') - (mkRemovedOptionModule [ "mailserver" "dmarcReporting" "fromName" ] '' - The name in the `FROM` field for DMARC report now uses the `mailserver.systemName`. - '') ]; + + config = lib.mkIf config.mailserver.enable { + warnings = if (config.mailserver.virtualAliases != {}) then [ '' + virtualAliases had been derprecated. Use extraVirtualAliases instead or + use the `aliases` field of the loginAccount attribute set + ''] + else []; + }; } diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d4bb2cb..0000000 --- a/docs/Makefile +++ /dev/null @@ -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) diff --git a/docs/add-radicale.rst b/docs/add-radicale.rst deleted file mode 100644 index cf98333..0000000 --- a/docs/add-radicale.rst +++ /dev/null @@ -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 ]; - } diff --git a/docs/add-roundcube.rst b/docs/add-roundcube.rst deleted file mode 100644 index 6b10d5b..0000000 --- a/docs/add-roundcube.rst +++ /dev/null @@ -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 ]; - } diff --git a/docs/advanced-configurations.rst b/docs/advanced-configurations.rst deleted file mode 100644 index e2b7837..0000000 --- a/docs/advanced-configurations.rst +++ /dev/null @@ -1,14 +0,0 @@ -Advanced Configurations -======================= - -Congratulations on completing the `Setup Guide `_! - -If you're an experienced mailserver admin, then you probably know what you want -to do next. Our How-to guides (accessible in the navigation sidebar) -might help you accomplish your goals. If not, consider contributing a guide! - -If this is your first mailserver, consider the following: - -- Set up `backups `_. -- Enable `DMARC reporting `_ to be a - good citizen in the mail ecosystem. diff --git a/docs/autodiscovery.rst b/docs/autodiscovery.rst deleted file mode 100644 index a630850..0000000 --- a/docs/autodiscovery.rst +++ /dev/null @@ -1,18 +0,0 @@ -Autodiscovery -============= - -`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 `_ can be used instead. - diff --git a/docs/backup-guide.rst b/docs/backup-guide.rst deleted file mode 100644 index 67d08d0..0000000 --- a/docs/backup-guide.rst +++ /dev/null @@ -1,27 +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``). - -If you enabled ``enableManageSieve`` then you also may want to backup -``/var/sieve`` or whatever you have specified as ``sieveDirectory``. -The same considerations regarding file ownership apply as for the -Maildir. - -To backup spam and ham training data, backup ``/var/lib/redis-rspamd``. - -Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever -you specified as ``dkimKeyDirectory``). If you should lose those don’t -worry, new ones will be created on the fly. But you will need to repeat -step ``B)5`` and correct all the ``dkim`` keys. diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 7bc771b..0000000 --- a/docs/conf.py +++ /dev/null @@ -1,59 +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 = [] diff --git a/docs/faq.rst b/docs/faq.rst deleted file mode 100644 index ef306de..0000000 --- a/docs/faq.rst +++ /dev/null @@ -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 `__ for details. diff --git a/docs/flakes.rst b/docs/flakes.rst deleted file mode 100644 index f56ec96..0000000 --- a/docs/flakes.rst +++ /dev/null @@ -1,30 +0,0 @@ -Nix Flakes -========== - -If you're using `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; - # ... - }; - } - ]; - }; - }; - }; - } diff --git a/docs/fts.rst b/docs/fts.rst deleted file mode 100644 index bb2fe88..0000000 --- a/docs/fts.rst +++ /dev/null @@ -1,67 +0,0 @@ -Full text search -========================== - -By default, when your IMAP client searches for an email containing some -text in its *body*, dovecot will read all your email sequentially. This -is very slow and IO intensive. To speed body searches up, it is possible to -*index* emails with a plugin to dovecot, ``fts_flatcurve``. - -Enabling full text search -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To enable indexing for full text search here is an example configuration. - -.. code:: nix - - { - mailserver = { - # ... - fullTextSearch = { - enable = true; - # index new email as they arrive - autoIndex = true; - enforced = "body"; - }; - }; - } - - -The ``enforced`` parameter tells dovecot to fail any body search query that cannot -use an index. This prevents dovecot to fall back to the IO-intensive brute -force search. - -If you set ``autoIndex`` to ``false``, indices will be created when the IMAP client -issues a search query, so latency will be high. - -Resource requirements -~~~~~~~~~~~~~~~~~~~~~~~~ - -Indices created by the full text search feature can take more disk -space than the emails themselves. By default, they are kept in the -emails location. When enabling the full text search feature, it is -recommended to move indices in a different location, such as -(``/var/lib/dovecot/indices``) by using the option -``mailserver.indexDir``. - -.. warning:: - - When the value of the ``indexDir`` option is changed, all dovecot - indices needs to be recreated: clients would need to resynchronize. - -Indexation itself is rather resouces intensive, in CPU, and for emails with -large headers, in memory as well. Initial indexation of existing emails can take -hours. If the indexer worker is killed or segfaults during indexation, it can -be that it tried to allocate more memory than allowed. You can increase the memory -limit by eg ``mailserver.fullTextSearch.memoryLimit = 2000`` (in MiB). - -Mitigating resources requirements -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can: - -* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes`` -* disable expensive token normalisation in ``mailserver.fullTextSearch.filters`` -* disable automatic indexation for some folders with - ``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by - name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard. - diff --git a/docs/howto-develop.rst b/docs/howto-develop.rst deleted file mode 100644 index 700f9d0..0000000 --- a/docs/howto-develop.rst +++ /dev/null @@ -1,107 +0,0 @@ -Contribute or troubleshoot -========================== - -To report an issue, please go to -``_. - -If you have questions, feel free to reach out: - -* Matrix: `#nixos-mailserver:nixos.org `__ -* IRC: `#nixos-mailserver `__ on `Libera Chat `__ - -All our workflows rely on Nix being configured with `Flakes `__. - -Development Shell ------------------ - -We provide a `flake.nix` devshell that automatically sets up pre-commit hooks, -which allows for fast feedback cycles when making changes to the repository. - - -:: - - $ nix develop - - -We recommend setting up `direnv `__ to automatically -attach to the development environment when entering the project directories. - -Run NixOS tests ---------------- - -To run the test suite, you need to enable `Nix 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 `_. - -For the syntax, see the `RST/Sphinx primer -`_. - -To build the documentation, you need to enable `Nix Flakes -`__. - - -:: - - $ nix build .#documentation - $ xdg-open result/index.html - - -Manual migrations ------------------ - -We need to take great care around providing a migration story around breaking -changes. If manual intervention becomes necessary we provide the `stateVersion` -option to notify the user that they need to complete a migration before -they can deploy an update. - -If that is the case for your change, find the highest `stateVersion` that is -being asserted on in `mail-server/assertions.nix`. Then pick the next number -and add a new assertion, write a good summary describing the issue and what -remediation steps are necessary. Finally reference the URL to the specific -section on the migration page in the documentation. - -.. code-block:: nix - - { - assertions = [ - { - assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 1; - message = '' - Problem: The home directory for the foobar service is snafu. - Remediation: - - Stop the `foobar.service` - - Rename `/var/lib/foobaz` to `/var/lib/foobar` - - Increase the `mailserver.stateVersion` to 1. - - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#specific-anchor-here for further details. - ''; - } - ]; - } - -The setup guide should always reference the latest `stateVersion`, since we -don't require any migration steps for new setups. - -The migration documentation should paint a more complete picture about the steps -that need to be carried out and why this has become necessary. Make sure to -reference the correct anchor in the URL you put into the assertion message. diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 0536c3c..0000000 --- a/docs/index.rst +++ /dev/null @@ -1,42 +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 - advanced-configurations - howto-develop - faq - release-notes - options - migrations - -.. 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` diff --git a/docs/ldap.rst b/docs/ldap.rst deleted file mode 100644 index efd975d..0000000 --- a/docs/ldap.rst +++ /dev/null @@ -1,14 +0,0 @@ -LDAP Support -============ - -It is possible to manage mail user accounts with LDAP rather than with -the option `loginAccounts `_. - -All related LDAP options are described in the `LDAP options section -`_ and the `LDAP test -`_ -provides a getting started example. - -.. note:: - The LDAP support can not be enabled if some accounts are also defined with ``mailserver.loginAccounts``. - diff --git a/docs/migrations.rst b/docs/migrations.rst deleted file mode 100644 index 6c186b0..0000000 --- a/docs/migrations.rst +++ /dev/null @@ -1,114 +0,0 @@ -Migrations -========== - -With mail server configuration best practices changing over time we might need -to make changes that require you to complete manual migration steps before you -can deploy a new version of NixOS mailserver. - -The initial `mailserver.stateVersion` value should be copied from the setup -guide that you used to initially set up your mail server. If in doubt you can -always initialize it at `1` and walk through all assertions, that might apply -to your setup. - -NixOS 25.11 ------------ - -#3 Dovecot mail directory migration -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The way the Dovecot home directory for login accounts were previously set up -resulted in shared home directories for all those users. This is not a -supported Dovecot configuration. - -To resolve this we migrated the home directory into the individual -`domain/localpart` subdirectory below the `mailserver.mailDirectory`. - -But since this now overlaps with the location of the Maildir, it must be -migrated into the `mail/` directory below the home directory. -And while the LDAP home directory is not affected we use this migration to -keep the Maildir configurations of LDAP users in sync with those of local -accounts. - -This is a big step forward, since we can now more cleanly colocate other -data directories, like sieve in the home directory, which in turn simplifies -backups. - -This migration is required for every configuration. - -For remediating this issue the following steps are required: - -1. Copy the `migration script `_ script to your mailserver - and make it executable: - - .. code-block:: bash - - wget https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/raw/master/migrations/nixos-mailserver-migration-03.py - chmod +x nixos-mailserver-migration-03.py - -2. Stop the ``dovecot2.service``. - - .. code-block:: bash - - systemctl stop dovecot2.service - -3. Create a backup or snapshot of your ``mailserver.mailDirectory``, so you can restore - should anything go wrong. - -4. Run the migration script under your virtual mail user with the following arguments: - - - ``--layout default`` unless ``useFSLayout`` is enabled, then ``--layout folder`` - - The value of ``mailserver.mailDirectory``, which defaults to ``/var/vmail`` - - The script will not modify your data unless called with ``--execute``. - - Example: - - .. code-block:: bash - - sudo -u virtualMail ./nixos-mailserver-migration-03.py --layout default /var/vmail - -5. Review the commands. They should be - - - create a ``mail`` directory for each accounnt, - - move maildir contents from the parent directory into it, - - suggest removal of files that do not belong to the maildir - - - their removal is not mandatory and the script **will not** remove them when called with ``--execute`` - - review these items carefully if you want to remove them yourself - - - remove obsolete files from the old home directory location - -6. Rerun the command with ``--execute`` or run the commands manually. - -7. Update the ``mailserver.stateVersion`` to ``3``. - -#2 Dovecot LDAP home directory migration -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The Dovecot configuration for LDAP home directories previously did not respect -the ``mailserver.mailDirectory`` setting. - -This means that home directories were unconditionally located at -``/var/vmail/ldap/%{user}``. - -This migration is required if you both: - -* enabled the LDAP integration (``mailserver.ldap.enable``) -* and customized the default mail directory (``mailserver.mailDirectory != "/var/vmail"``) - -For remediating this issue the following steps are required: - -1. Stop ``dovecot2.service``. -2. Move ``/var/vmail/ldap`` below your ``m̀ailserver.mailDirectory``. -3. Update the ``mailserver.stateVersion`` to ``2``. - -#1 Initialization -^^^^^^^^^^^^^^^^^ - -This option was introduced in the NixOS 25.11 release cycle, in which case you -can safely initialize its value at `1`. - -.. code-block:: nix - - mailserver.stateVersion = 1; - diff --git a/docs/release-notes.rst b/docs/release-notes.rst deleted file mode 100644 index 7a48dd4..0000000 --- a/docs/release-notes.rst +++ /dev/null @@ -1,123 +0,0 @@ -Release Notes -============= - -NixOS 25.11 ------------ - -- The ``systemName`` and ``systemDomain`` options have been introduced to have - reusable configurations for automated reports (DMARC, TLSRPT). They come with - reasonable defaults, but it is suggested to check and change them as needed. -- The default key length for new DKIM RSA keys was increased to 2048 bits as - recommended in `RFC 8301 3.2`_. - We recommend rotating existing keys, as the RFC advises that signatures from - 1024 bit keys should not be considered valid any longer. -- DMARC reports are now sent with the ``noreply-dmarc`` localpart from the - system domain. - -.. _RFC 8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2 - -NixOS 25.05 ------------ - -- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed`` canoncalizaliaton. - (`merge request `__) -- Rspamd now connects to Redis over its Unix Domain Socket by default - (`merge request `__) - - - If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``. -- The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring. - (`merge request `__) -- Switch to the more efficient `fts-flatcurve` indexer for full text search - (`merge request `__). - - This makes use of a new index, which will be automatically re-generated the - next time a folder is searched. - The operation is now quick enough to be performed "just-in-time". - Alternatively, all indices can be immediately re-generated for all users and - folders by running - - .. code-block:: bash - - doveadm fts rescan -u '*' && doveadm index -u '*' -q '*' - - The previous index (which is not automatically discarded to allow rollbacks) - can be cleaned up by removing all the `xapian-indexes` directories within - ``mailserver.indexDir``. -- Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``. - (`merge request `__) -- Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP. - (`merge request `__) -- Support for TLS 1.1 was disabled in accordance with `Mozilla's recommendations `_. - (`merge request `__) - -NixOS 24.11 ------------ - -- No new feature, only bug fixes and documentation improvements - -NixOS 24.05 ------------ - -- Add new option ``acmeCertificateName`` which can be used to support - wildcard certificates - -NixOS 23.11 ------------ - -- Add basic support for LDAP users -- Add support for regex (PCRE) aliases - -NixOS 23.05 ------------ - -- Existing ACME certificates can be reused without configuring NGINX -- Certificate scheme is no longer a number, but a meaningful string instead - -NixOS 22.11 ------------ - -- Allow Rspamd to send DMARC reporting - (`merge request `__) - -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 `__) -- Ensure locally-delivered mails have the X-Original-To header - (`merge request `__) -- NixOS Mailserver options are detailed in the `documentation - `__ -- 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 `__) -- Flake support - (`Merge Request `__) -- 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 `__) -- New `sendingFqdn` option to specify the fqdn of the machine sending - email (`Merge Request `__) -- Move the Gitlab wiki to `ReadTheDocs - `_ diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index c77dd1e..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -sphinx ~= 5.3 -sphinx_rtd_theme ~= 1.1 -myst-parser ~= 0.18 -linkify-it-py ~= 2.0 -standard-imghdr diff --git a/docs/rspamd-tuning.rst b/docs/rspamd-tuning.rst deleted file mode 100644 index 3ba8133..0000000 --- a/docs/rspamd-tuning.rst +++ /dev/null @@ -1,110 +0,0 @@ -Tune spam filtering -=================== - -SNM comes with the `rspamd spam filtering system `_ -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 `_ -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 - - # Learn the Junk folder as spam - rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/ - - # Learn the INBOX as ham - rspamc learn_ham /var/vmail/$DOMAIN/$USER/cur/ - - # Check that training was successful - rspamc stat | grep learned - -Tune symbol weight -~~~~~~~~~~~~~~~~~~ - -The ``X-Spamd-Result`` header is automatically added to your emails, detailing -the scoring decisions. The `modules documentation `_ -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 `_ 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 `_: - -.. 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:/"; - }; - }; - }; diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst deleted file mode 100644 index 4312373..0000000 --- a/docs/setup-guide.rst +++ /dev/null @@ -1,245 +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 -``_. - -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/AAAA records for server -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Add DNS records to the domain ``example.com`` with the following -entries - -==================== ===== ==== ============= -Name (Subdomain) TTL Type Value -==================== ===== ==== ============= -``mail.example.com`` 10800 A ``1.2.3.4`` -``mail.example.com`` 10800 AAAA ``2001::1`` -==================== ===== ==== ============= - -If your server does not have an IPv6 address, you must skip the `AAAA` record. - -You can check this with - -:: - - $ nix-shell -p bind --command "host -t A mail.example.com" - mail.example.com has address 1.2.3.4 - - $ nix-shell -p bind --command "host -t AAAA mail.example.com" - mail.example.com has address 2001::1 - -Note that it can take a while until a DNS entry is propagated. This -DNS entry is required for the Let's Encrypt certificate generation -(which is used in the below configuration example). - -Setup the server -~~~~~~~~~~~~~~~~ - -The following describes a server setup that is fairly complete. Even -though there are more possible options (see the `NixOS Mailserver -options documentation `_), 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-25.05/nixos-mailserver-nixos-25.05.tar.gz"; - # To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: - # release="nixos-25.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack - sha256 = "0000000000000000000000000000000000000000000000000000"; - }) - ]; - - mailserver = { - enable = true; - stateVersion = 3; - 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 IPv4 address ``1.2.3.4`` to ``mail.example.com``. -- Add an entry resolving IPv6 ``2001::1`` to ``mail.example.com``. Again, this - must be skipped if your server does not have an IPv6 address. - -.. warning:: - - We don't recommend setting up a mail server if you are not able to - set a reverse DNS on your public IP because sent emails would be - mostly marked as spam. Note that many residential ISP providers - don't allow you to set a reverse DNS entry. - -You can check this with - -:: - - $ nix-shell -p bind --command "host 1.2.3.4" - 4.3.2.1.in-addr.arpa domain name pointer mail.example.com. - - $ nix-shell -p bind --command "host 2001::1" - 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.2.ip6.arpa domain name pointer mail.example.com. - -Note that it can take a while until a DNS entry is propagated. - -Set a ``MX`` record -^^^^^^^^^^^^^^^^^^^ - - -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 `_ -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 ``rspamd`` systemd service generated a file -containing your DKIM public key in the file -``/var/dkim/example.com.mail.txt``. The content of this file looks -like - -:: - - mail._domainkey IN TXT ( "v=DKIM1; k=rsa; " - "p=" ) ; ----- DKIM key mail for nixos.org - -where ``really-long-key`` is your public key. - -Based on the content of this file, we can add a ``DKIM`` record to the -domain ``example.com``. - -=========================== ===== ==== ================================================ -Name (Subdomain) TTL Type Value -=========================== ===== ==== ================================================ -mail._domainkey.example.com 10800 TXT ``v=DKIM1; k=rsa; s=email; p=`` -=========================== ===== ==== ================================================ - -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=" - -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 `__? - -Besides that, you can send an email to -`mail-tester.com `__ and see how you -score, and let `mxtoolbox.com `__ take a look at -your setup, but if you followed the steps closely then everything should -be awesome! - -Next steps (optional) -~~~~~~~~~~~~~~~~~~~~~ - -Take a look through our `Advanced Configurations `_. diff --git a/flake.lock b/flake.lock deleted file mode 100644 index 74df222..0000000 --- a/flake.lock +++ /dev/null @@ -1,124 +0,0 @@ -{ - "nodes": { - "blobs": { - "flake": false, - "locked": { - "lastModified": 1604995301, - "narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=", - "owner": "simple-nixos-mailserver", - "repo": "blobs", - "rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265", - "type": "gitlab" - }, - "original": { - "owner": "simple-nixos-mailserver", - "repo": "blobs", - "type": "gitlab" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1747046372, - "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": [ - "flake-compat" - ], - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1750779888, - "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=", - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1753939845, - "narHash": "sha256-K2ViRJfdVGE8tpJejs8Qpvvejks1+A4GQej/lBk5y7I=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "94def634a20494ee057c76998843c015909d6311", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-25_05": { - "locked": { - "lastModified": 1753749649, - "narHash": "sha256-+jkEZxs7bfOKfBIk430K+tK9IvXlwzqQQnppC2ZKFj4=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "1f08a4df998e21f4e8be8fb6fbf61d11a1a5076a", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-25.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "blobs": "blobs", - "flake-compat": "flake-compat", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "nixpkgs-25_05": "nixpkgs-25_05" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index da8ae84..0000000 --- a/flake.nix +++ /dev/null @@ -1,220 +0,0 @@ -{ - description = "A complete and Simple Nixos Mailserver"; - - inputs = { - flake-compat = { - # for shell.nix compat - url = "github:edolstra/flake-compat"; - flake = false; - }; - git-hooks = { - url = "github:cachix/git-hooks.nix"; - inputs.flake-compat.follows = "flake-compat"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - nixpkgs-25_05.url = "github:NixOS/nixpkgs/nixos-25.05"; - blobs = { - url = "gitlab:simple-nixos-mailserver/blobs"; - flake = false; - }; - }; - - outputs = - { - self, - blobs, - git-hooks, - nixpkgs, - nixpkgs-25_05, - ... - }: - let - lib = nixpkgs.lib; - system = "x86_64-linux"; - pkgs = nixpkgs.legacyPackages.${system}; - releases = [ - { - name = "unstable"; - nixpkgs = nixpkgs; - pkgs = nixpkgs.legacyPackages.${system}; - } - { - name = "25.05"; - nixpkgs = nixpkgs-25_05; - pkgs = nixpkgs-25_05.legacyPackages.${system}; - } - ]; - testNames = [ - "clamav" - "external" - "internal" - "ldap" - "multiple" - ]; - - genTest = - testName: release: - let - pkgs = release.pkgs; - nixos-lib = import (release.nixpkgs + "/nixos/lib") { - inherit (pkgs) lib; - }; - in - { - name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}"; - value = nixos-lib.runTest { - hostPkgs = pkgs; - imports = [ ./tests/${testName}.nix ]; - _module.args = { inherit blobs; }; - extraBaseModules.imports = [ ./default.nix ]; - }; - }; - - # Generate an attribute set such as - # { - # external-unstable = ; - # external-21_05 = ; - # ... - # } - allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames)); - - mailserverModule = import ./.; - - # Generate a MarkDown file describing the options of the NixOS mailserver module - optionsDoc = - let - eval = lib.evalModules { - modules = [ - mailserverModule - { - _module.check = false; - mailserver = { - fqdn = "mx.example.com"; - domains = [ - "example.com" - ]; - }; - } - ]; - }; - options = builtins.toFile "options.json" ( - builtins.toJSON ( - lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") ( - lib.optionAttrSetToDocList eval.options - ) - ) - ); - in - pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } '' - echo "Generating options.md from ${options}" - python ${./scripts/generate-options.py} ${options} > $out - echo $out - ''; - - documentation = pkgs.stdenv.mkDerivation { - name = "documentation"; - src = lib.sourceByRegex ./docs [ - "logo\\.png" - "conf\\.py" - "Makefile" - ".*\\.rst" - ]; - buildInputs = [ - (pkgs.python3.withPackages ( - p: with p; [ - sphinx - sphinx_rtd_theme - myst-parser - linkify-it-py - ] - )) - ]; - buildPhase = '' - cp ${optionsDoc} options.md - # Workaround for https://github.com/sphinx-doc/sphinx/issues/3451 - unset SOURCE_DATE_EPOCH - make html - ''; - installPhase = '' - cp -Tr _build/html $out - ''; - }; - - in - { - nixosModules = rec { - mailserver = mailserverModule; - default = mailserver; - }; - nixosModule = self.nixosModules.default; # compatibility - hydraJobs.${system} = allTests // { - inherit documentation; - inherit (self.checks.${system}) pre-commit; - }; - checks.${system} = allTests // { - pre-commit = git-hooks.lib.${system}.run { - src = ./.; - hooks = { - # docs - markdownlint = { - enable = true; - settings.configuration = { - # Max line length, doesn't seem to correclty account for lines containing links - # https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md - MD013 = false; - }; - }; - rstcheck = { - enable = true; - package = pkgs.rstcheckWithSphinx; - entry = lib.getExe pkgs.rstcheckWithSphinx; - files = "\\.rst$"; - }; - - # nix - deadnix.enable = true; - nixfmt-rfc-style.enable = true; - - # python - pyright.enable = true; - ruff = { - enable = true; - args = [ - "--extend-select" - "I" - ]; - }; - ruff-format.enable = true; - - # scripts - shellcheck.enable = true; - - # sieve - check-sieve = { - enable = true; - package = pkgs.check-sieve; - entry = lib.getExe pkgs.check-sieve; - files = "\\.sieve$"; - }; - }; - }; - }; - packages.${system} = { - inherit optionsDoc documentation; - }; - devShells.${system}.default = pkgs.mkShellNoCC { - inputsFrom = [ documentation ]; - packages = - with pkgs; - [ - glab - ] - ++ self.checks.${system}.pre-commit.enabledPackages; - shellHook = self.checks.${system}.pre-commit.shellHook; - }; - devShell.${system} = self.devShells.${system}.default; # compatibility - - formatter.${system} = pkgs.nixfmt-tree; - }; -} diff --git a/docs/logo.png b/logo/logo.png similarity index 100% rename from docs/logo.png rename to logo/logo.png diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix deleted file mode 100644 index e0dab19..0000000 --- a/mail-server/assertions.nix +++ /dev/null @@ -1,58 +0,0 @@ -{ - config, - lib, - ... -}: -{ - # We guard all assertions by requiring mailserver to be actually enabled - assertions = lib.optionals config.mailserver.enable ( - [ - { - assertion = config.mailserver.stateVersion != null; - message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at."; - } - ] - ++ lib.optionals config.mailserver.ldap.enable [ - { - assertion = config.mailserver.loginAccounts == { }; - message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts"; - } -# { -# assertion = config.mailserver.extraVirtualAliases == { }; -# message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases"; -# } - ] - ++ - lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail") - [ - { - assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2; - message = '' - Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`. - Remediation: - - Stop the `dovecot2.service` - - Move `/var/vmail/ldap` below your `mailserver.mailDirectory` - - Increase the `stateVersion` to 2. - - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information. - ''; - } - ] - ++ [ - { - assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 3; - message = '' - Issue: The dovecot mail location for all users has changed and need to be migrated. - - Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration for the required remediation steps. - ''; - } - ] - ++ lib.optionals (config.mailserver.certificateScheme != "acme") [ - { - assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; - message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName"; - } - ] - ); -} diff --git a/mail-server/borgbackup.nix b/mail-server/borgbackup.nix deleted file mode 100644 index 51ae986..0000000 --- a/mail-server/borgbackup.nix +++ /dev/null @@ -1,95 +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 - -{ - 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; - }; - }; -} diff --git a/mail-server/clamav.nix b/mail-server/clamav.nix index 0dafd4f..5542a95 100644 --- a/mail-server/clamav.nix +++ b/mail-server/clamav.nix @@ -1,5 +1,5 @@ # 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 # 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 # along with this program. If not, see -{ config, lib, ... }: +{ config, pkgs, lib, ... }: let cfg = config.mailserver; in { - config = lib.mkIf (cfg.enable && cfg.virusScanning) { - services.clamav.daemon = { - enable = true; - settings.PhishingScanURLs = "no"; - }; + config = lib.mkIf cfg.virusScanning { + services.clamav.daemon.enable = true; services.clamav.updater.enable = true; }; } + diff --git a/mail-server/common.nix b/mail-server/common.nix index 4247360..910b5c2 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,79 +14,27 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - options, - pkgs, - lib, -}: +{ config }: let cfg = config.mailserver; in { # cert :: PATH - certificatePath = - if cfg.certificateScheme == "manual" then - cfg.certificateFile - else if cfg.certificateScheme == "selfsigned" then - "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" - else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then - "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem" - else - throw "unknown certificate scheme"; + certificatePath = if cfg.certificateScheme == 1 + then cfg.certificateFile + else if cfg.certificateScheme == 2 + then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" + else if cfg.certificateScheme == 3 + then "/var/lib/acme/${cfg.fqdn}/fullchain.pem" + else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; # key :: PATH - keyPath = - if cfg.certificateScheme == "manual" then - cfg.keyFile - else if cfg.certificateScheme == "selfsigned" then - "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" - else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then - "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem" - else - throw "unknown certificate scheme"; - - passwordFiles = - let - mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; - in - lib.mapAttrs ( - name: value: - if value.hashedPasswordFile == null then - builtins.toString (mkHashFile name value.hashedPassword) - else - value.hashedPasswordFile - ) cfg.loginAccounts; - - # Appends the LDAP bind password to files to avoid writing this - # password into the Nix store. - appendLdapBindPwd = - { - name, - file, - prefix, - suffix ? "", - passwordFile, - destination, - }: - pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' - #!${pkgs.stdenv.shell} - set -euo pipefail - - baseDir=$(dirname ${destination}) - if (! test -d "$baseDir"); then - mkdir -p $baseDir - chmod 755 $baseDir - fi - - cat ${file} > ${destination} - echo -n '${prefix}' >> ${destination} - cat ${passwordFile} | tr -d '\n' >> ${destination} - echo -n '${suffix}' >> ${destination} - chmod 600 ${destination} - ''; - - dovecotUnitName = if options.services.dovecot2 ? hasNewUnitName then "dovecot" else "dovecot2"; - + keyPath = if cfg.certificateScheme == 1 + then cfg.keyFile + else if cfg.certificateScheme == 2 + then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" + else if cfg.certificateScheme == 3 + then "/var/lib/acme/${cfg.fqdn}/key.pem" + else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 69d2b6b..0b51600 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,452 +14,99 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - options, - pkgs, - lib, - ... -}: +{ config, pkgs, lib, ... }: -with (import ./common.nix { - inherit - config - options - pkgs - lib - ; -}); +with (import ./common.nix { inherit config; }); let cfg = config.mailserver; - passwdDir = "/run/dovecot2"; - passwdFile = "${passwdDir}/passwd"; - userdbFile = "${passwdDir}/userdb"; - # This file contains the ldap bind password - ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; - boolToYesNo = x: if x then "yes" else "no"; - listToLine = lib.concatStringsSep " "; - listToMultiAttrs = - keyPrefix: attrs: - lib.listToAttrs ( - lib.imap1 (n: x: { - name = "${keyPrefix}${if n == 1 then "" else toString n}"; - value = x; - }) attrs - ); - - maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; - maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; - - # https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory - # Mail directory below the home directory - dovecotMaildir = - "maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}" - + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}"); - - postfixCfg = config.services.postfix; - - 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: _: passwordFiles."${name}") cfg.loginAccounts) - }; do - if [ ! -f "$f" ]; then - echo "Expected password hash file $f does not exist!" - exit 1 - fi - done - - cat < ${passwdFile} - ${lib.concatStringsSep "\n" ( - lib.mapAttrsToList ( - name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" - ) cfg.loginAccounts - )} - EOF - - cat < ${userdbFile} - ${lib.concatStringsSep "\n" ( - lib.mapAttrsToList ( - name: value: - "${name}:::::::" - + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" - ) cfg.loginAccounts - )} - EOF - ''; - - junkMailboxes = builtins.attrNames ( - lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes - ); - junkMailboxNumber = builtins.length junkMailboxes; - # The assertion garantees there is exactly one Junk mailbox. - junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else ""; - - mkLdapSearchScope = - scope: - ( - if scope == "sub" then - "subtree" - else if scope == "one" then - "onelevel" - else - scope - ); - - ftsPluginSettings = { - fts = "flatcurve"; - fts_languages = listToLine cfg.fullTextSearch.languages; - fts_tokenizers = listToLine [ - "generic" - "email-address" - ]; - fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian - fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch; - fts_filters = listToLine cfg.fullTextSearch.filters; - fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes; - fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex; - fts_enforced = cfg.fullTextSearch.enforced; - } - // (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude); + # maildir in format "/${domain}/${user}" + dovecot_maildir = "maildir:${cfg.mailDirectory}/%d/%n"; in { - config = lib.mkIf cfg.enable { - assertions = [ - { - assertion = junkMailboxNumber == 1; - message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)"; - } - ]; - - warnings = - lib.optional - ( - (builtins.length cfg.fullTextSearch.languages > 1) - && (builtins.elem "stopwords" cfg.fullTextSearch.filters) - ) - '' - Using stopwords in `mailserver.fullTextSearch.filters` with multiple - languages in `mailserver.fullTextSearch.languages` configured WILL - cause some searches to fail. - - The recommended solution is to NOT use the stopword filter when - multiple languages are present in the configuration. - ''; - - # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads - # the global config and tries to open shared libraries configured in there, - # which are usually not compatible. - environment.systemPackages = [ - pkgs.dovecot_pigeonhole - ] - ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve; - - # For compatibility with python imaplib - environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules"; - + config = with cfg; lib.mkIf enable { services.dovecot2 = { enable = true; - enableImap = cfg.enableImap || cfg.enableImapSsl; - enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl; - enablePAM = false; - enableQuota = true; - mailGroup = cfg.vmailGroupName; - mailUser = cfg.vmailUserName; - mailLocation = dovecotMaildir; + enableImap = enableImap; + enablePop3 = enablePop3; + mailGroup = vmailGroupName; + mailUser = vmailUserName; + mailLocation = dovecot_maildir; sslServerCert = certificatePath; sslServerKey = keyPath; - enableDHE = lib.mkDefault false; enableLmtp = true; - mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ - "fts" - "fts_flatcurve" - ]; - protocols = lib.optional cfg.enableManageSieve "sieve"; + modules = [ pkgs.dovecot_pigeonhole ]; + protocols = [ "sieve" ]; - pluginSettings = { - sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; - sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; - sieve_default_name = "default"; - } - // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings); - - sieve = { - extensions = [ - "fileinto" - ]; - - scripts.after = builtins.toFile "spam.sieve" '' + sieveScripts = { + before = builtins.toFile "spam.sieve" '' require "fileinto"; if header :is "X-Spam" "Yes" { - fileinto "${junkMailboxName}"; + fileinto "Junk"; stop; } ''; - - pipeBins = map lib.getExe [ - (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham") - (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam") - ]; }; - imapsieve.mailbox = [ - { - name = junkMailboxName; - causes = [ - "COPY" - "APPEND" - ]; - before = ./dovecot/imap_sieve/report-spam.sieve; - } - { - name = "*"; - from = junkMailboxName; - causes = [ "COPY" ]; - before = ./dovecot/imap_sieve/report-ham.sieve; - } - ]; - - mailboxes = cfg.mailboxes; - extraConfig = '' #Extra Config - ${lib.optionalString cfg.debug.dovecot '' + ${lib.optionalString debug '' mail_debug = yes auth_debug = 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 - } - - service imap { - vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB - } - - protocol pop3 { - mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} - } - - mail_access_groups = ${cfg.vmailGroupName} - - # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7 + mail_access_groups = ${vmailGroupName} ssl = required - ssl_min_protocol = TLSv1.2 - ssl_prefer_server_ciphers = no - ssl_curve_list = X25519:prime256v1:secp384r1 service lmtp { - unix_listener dovecot-lmtp { - group = ${postfixCfg.group} + unix_listener /run/dovecot/lmtp { + group = ${vmailGroupName} mode = 0600 - user = ${postfixCfg.user} + user = ${vmailUserName} } - vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB } - service quota-status { - inet_listener { - port = 0 - } - unix_listener quota-status { - user = postfix - } - vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB - } - - recipient_delimiter = ${cfg.recipientDelimiter} - lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} - protocol lmtp { mail_plugins = $mail_plugins sieve } - passdb { - driver = passwd-file - args = ${passwdFile} - } - - userdb { - driver = passwd-file - args = ${userdbFile} - default_fields = \ - home=${cfg.mailDirectory}/%{domain}/%{username} \ - uid=${builtins.toString cfg.vmailUID} \ - gid=${builtins.toString cfg.vmailUID} - } - - ${lib.optionalString cfg.ldap.enable '' - passdb { - driver = ldap - args = ${ldapConfFile} - } - - userdb { - driver = ldap - args = ${ldapConfFile} - default_fields = \ - home=${cfg.mailDirectory}/ldap/%{user} \ - uid=${toString cfg.vmailUID} \ - gid=${toString cfg.vmailUID} \ - mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${ - lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}" - } - - } - ''} - - service auth { - unix_listener auth { - mode = 0660 - user = ${postfixCfg.user} - group = ${postfixCfg.group} - } - } - auth_mechanisms = plain login namespace inbox { - separator = ${cfg.hierarchySeparator} inbox = yes + + mailbox "Trash" { + auto = no + special_use = \Trash + } + + mailbox "Junk" { + auto = subscribe + special_use = \Junk + } + + mailbox "Drafts" { + auto = subscribe + special_use = \Drafts + } + + mailbox "Sent" { + auto = subscribe + special_use = \Sent + } } - service indexer-worker { - ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' - vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)} - ''} + plugin { + sieve = file:/var/sieve/%u.sieve } lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes ''; }; - - systemd.services.${dovecotUnitName} = { - preStart = '' - ${genPasswdScript} - '' - + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile); - }; - - systemd.services.postfix.restartTriggers = [ - genPasswdScript - ] - ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]); }; } diff --git a/mail-server/dovecot/imap_sieve/report-ham.sieve b/mail-server/dovecot/imap_sieve/report-ham.sieve deleted file mode 100644 index 720be7a..0000000 --- a/mail-server/dovecot/imap_sieve/report-ham.sieve +++ /dev/null @@ -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 "rspamd-learn-ham.sh" [ "${username}" ]; diff --git a/mail-server/dovecot/imap_sieve/report-spam.sieve b/mail-server/dovecot/imap_sieve/report-spam.sieve deleted file mode 100644 index 4681aac..0000000 --- a/mail-server/dovecot/imap_sieve/report-spam.sieve +++ /dev/null @@ -1,7 +0,0 @@ -require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; - -if environment :matches "imap.user" "*" { - set "username" "${1}"; -} - -pipe :copy "rspamd-learn-spam.sh" [ "${username}" ]; diff --git a/mail-server/environment.nix b/mail-server/environment.nix index 462cb05..48e68c7 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,26 +14,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - pkgs, - lib, - ... -}: +{ config, pkgs, lib, ... }: let cfg = config.mailserver; in { - config = lib.mkIf cfg.enable { - environment.systemPackages = - with pkgs; - [ - dovecot - openssh - postfix - rspamd - ] - ++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]); + config = with cfg; lib.mkIf enable { + environment.systemPackages = with pkgs; [ + dovecot opendkim openssh postfix clamav rspamd rmilter + ] ++ (if certificateScheme == 2 then [ openssl ] else []); }; } diff --git a/mail-server/networking.nix b/mail-server/networking.nix index a79aa37..c224b0a 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,26 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, lib, ... }: +{ config, pkgs, lib, ... }: let cfg = config.mailserver; in { - config = lib.mkIf (cfg.enable && cfg.openFirewall) { + config = with cfg; lib.mkIf enable { networking.firewall = { - allowedTCPPorts = [ - 25 - ] - ++ lib.optional cfg.enableSubmission 587 - ++ lib.optional cfg.enableSubmissionSsl 465 - ++ lib.optional cfg.enableImap 143 - ++ lib.optional cfg.enableImapSsl 993 - ++ lib.optional cfg.enablePop3 110 - ++ lib.optional cfg.enablePop3Ssl 995 - ++ lib.optional cfg.enableManageSieve 4190 - ++ lib.optional (cfg.certificateScheme == "acme-nginx") 80; + allowedTCPPorts = [ 25 587 ] + ++ lib.optional enableImap 143 + ++ lib.optional enableImapSsl 993 + ++ lib.optional enablePop3 110 + ++ lib.optional enablePop3Ssl 995 + ++ lib.optional (certificateScheme == 3) 80; }; }; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 75ebc4c..d6ca87c 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,43 +14,31 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - options, - pkgs, - lib, - ... -}: -with (import ./common.nix { - inherit - config - options - lib - pkgs - ; -}); +{ config, pkgs, lib, ... }: + +with (import ./common.nix { inherit config; }); let cfg = config.mailserver; + acmeRoot = "/var/lib/acme/acme-challenge"; in { - config = - lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) - { - services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") { - enable = true; - virtualHosts."${cfg.fqdn}" = { - serverName = cfg.fqdn; - serverAliases = cfg.certificateDomains; - forceSSL = true; - enableACME = true; - }; - }; - - security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [ - "postfix.service" - "${dovecotUnitName}.service" - ]; + config = lib.mkIf (cfg.certificateScheme == 3) { + services.nginx = { + enable = true; + virtualHosts."${cfg.fqdn}" = { + serverName = cfg.fqdn; + forceSSL = true; + enableACME = true; + acmeRoot = acmeRoot; }; + }; + + security.acme.certs."${cfg.fqdn}".postRun = '' + systemctl reload nginx + systemctl reload postfix + systemctl reload dovecot2 + ''; + }; } diff --git a/mail-server/opensmtpd.nix b/mail-server/opensmtpd.nix new file mode 100644 index 0000000..497eb0b --- /dev/null +++ b/mail-server/opensmtpd.nix @@ -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 + +{ 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 received-auth + + # allow local messages + accept from any for domain virtual 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 + ''; + }; + }; +} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 8c0bccd..b36accd 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,133 +14,55 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - options, - pkgs, - lib, - ... -}: +{ config, pkgs, lib, ... }: -with (import ./common.nix { - inherit - config - options - lib - pkgs - ; -}); +with (import ./common.nix { inherit config; }); let inherit (lib.strings) concatStringsSep; cfg = config.mailserver; - # Merge several lookup tables. A lookup table is a attribute set where - # - the key is an address (user@example.com) or a domain (@example.com) - # - the value is a list of addresses - mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables; + # valiases_postfix :: [ String ] + valiases_postfix = lib.flatten (lib.mapAttrsToList + (name: value: + let to = name; + in map (from: "${from} ${to}") (value.aliases ++ lib.singleton name)) + cfg.loginAccounts); - # valiases_postfix :: Map String [String] - valiases_postfix = mergeLookupTables ( - lib.flatten ( - lib.mapAttrsToList ( - name: value: - let - to = name; - in - map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name) - ) cfg.loginAccounts - ) - ); - regex_valiases_postfix = mergeLookupTables ( - lib.flatten ( - lib.mapAttrsToList ( - name: value: - let - to = name; - in - map (from: { "${from}" = to; }) value.aliasesRegexp - ) cfg.loginAccounts - ) - ); + # catchAllPostfix :: [ String ] + catchAllPostfix = lib.flatten (lib.mapAttrsToList + (name: value: + let to = name; + in map (from: "@${from} ${to}") value.catchAll) + cfg.loginAccounts); - # catchAllPostfix :: Map String [String] - catchAllPostfix = mergeLookupTables ( - lib.flatten ( - lib.mapAttrsToList ( - name: value: - let - to = name; - in - map (from: { "@${from}" = to; }) value.catchAll - ) cfg.loginAccounts - ) - ); + # 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 :: Map String [String] - all_valiases_postfix = mergeLookupTables [ - valiases_postfix - extra_valiases_postfix - ]; + # all_valiases_postfix :: [ String ] + all_valiases_postfix = valiases_postfix ++ extra_valiases_postfix; - # attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String] - attrsToLookupTable = - aliases: - let - lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases; - in - mergeLookupTables lookupTables; + # accountToIdentity :: User -> String + accountToIdentity = account: "${account.name} ${account.name}"; - # extra_valiases_postfix :: Map String [String] - extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases; - - # forwards :: Map String [String] - forwards = attrsToLookupTable cfg.forwards; - - # lookupTableToString :: Map String [String] -> String - lookupTableToString = - attrs: - let - valueToString = value: lib.concatStringsSep ", " value; - in - lib.concatStringsSep "\n" ( - lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs - ); + # vaccounts_identity :: [ String ] + vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts); # valiases_file :: Path - valiases_file = - let - content = lookupTableToString (mergeLookupTables [ - all_valiases_postfix - catchAllPostfix - ]); - in - builtins.toFile "valias" content; - - regex_valiases_file = - let - content = lookupTableToString regex_valiases_postfix; - in - builtins.toFile "regex_valias" content; - - # denied_recipients_postfix :: [ String ] - denied_recipients_postfix = 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 - ); + valiases_file = builtins.toFile "valias" + (lib.concatStringsSep "\n" (all_valiases_postfix ++ + catchAllPostfix)); # vhosts_file :: Path vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains); @@ -149,272 +71,99 @@ let # see # https://blog.grimneko.de/2011/12/24/a-bunch-of-tips-for-improving-your-postfix-setup/ # for details on how this file looks. By using the same file as valiases, - # every alias is owned (uniquely) by its user. - # The user's own address is already in all_valiases_postfix. - vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); - regex_vaccounts_file = builtins.toFile "regex_vaccounts" ( - lookupTableToString regex_valiases_postfix - ); + # 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. + submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" '' + # Removes sensitive headers from mails handed in via the submission port. + # See https://thomas-leister.de/mailserver-debian-stretch/ + # Uses "pcre" style regex. - /^Received:/ IGNORE - /^X-Originating-IP:/ IGNORE - /^X-Mailer:/ IGNORE - /^User-Agent:/ IGNORE - /^X-Enigmail:/ IGNORE - '' - + lib.optionalString cfg.rewriteMessageId '' - - # 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}> - '' - ); - - smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; - - 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} + /^Received:/ IGNORE + /^X-Originating-IP:/ IGNORE + /^X-Mailer:/ IGNORE + /^User-Agent:/ IGNORE + /^X-Enigmail:/ IGNORE ''; - - ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" '' - ${commonLdapConfig} - query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.mailAttribute} - ''; - ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; - appendPwdInSenderLoginMap = appendLdapBindPwd { - name = "ldap-sender-login-map"; - file = ldapSenderLoginMap; - prefix = "bind_pw = "; - passwordFile = cfg.ldap.bind.passwordFile; - destination = ldapSenderLoginMapFile; - }; - - ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" '' - ${commonLdapConfig} - query_filter = ${cfg.ldap.postfix.filter} - result_attribute = ${cfg.ldap.postfix.uidAttribute} - ''; - ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf"; - appendPwdInVirtualMailboxMap = appendLdapBindPwd { - name = "ldap-virtual-mailbox-map"; - file = ldapVirtualMailboxMap; - prefix = "bind_pw = "; - passwordFile = cfg.ldap.bind.passwordFile; - destination = ldapVirtualMailboxMapFile; - }; in { - config = lib.mkIf cfg.enable { - - systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { - preStart = '' - ${appendPwdInVirtualMailboxMap} - ${appendPwdInSenderLoginMap} - ''; - restartTriggers = [ - appendPwdInVirtualMailboxMap - appendPwdInSenderLoginMap - ]; - }; + config = with cfg; lib.mkIf enable { services.postfix = { enable = true; + hostname = "${fqdn}"; + networksStyle = "host"; mapFiles."valias" = valiases_file; - mapFiles."regex_valias" = regex_valiases_file; mapFiles."vaccounts" = vaccounts_file; - mapFiles."regex_vaccounts" = regex_vaccounts_file; - mapFiles."denied_recipients" = denied_recipients_file; - mapFiles."reject_senders" = reject_senders_file; - mapFiles."reject_recipients" = reject_recipients_file; - enableSubmission = cfg.enableSubmission; - enableSubmissions = cfg.enableSubmissionSsl; - virtual = lookupTableToString (mergeLookupTables [ - all_valiases_postfix - catchAllPostfix - forwards - ]); + sslCert = certificatePath; + sslKey = keyPath; + enableSubmission = true; - config = { - myhostname = cfg.sendingFqdn; - mydestination = ""; # disable local mail delivery - recipient_delimiter = cfg.recipientDelimiter; - smtpd_banner = "${cfg.fqdn} ESMTP NO UCE"; - disable_vrfy_command = true; - message_size_limit = cfg.messageSizeLimit; + extraConfig = + '' + # Extra Config + mydestination = localhost + + smtpd_banner = ${fqdn} ESMTP NO UCE + disable_vrfy_command = yes + message_size_limit = 20971520 # virtual mail system - virtual_uid_maps = "static:5000"; - virtual_gid_maps = "static:5000"; - virtual_mailbox_base = cfg.mailDirectory; - virtual_mailbox_domains = vhosts_file; - virtual_mailbox_maps = [ - (mappedFile "valias") - ] - ++ lib.optionals cfg.ldap.enable [ - "ldap:${ldapVirtualMailboxMapFile}" - ] - ++ lib.optionals (regex_valiases_postfix != { }) [ - (mappedRegexFile "regex_valias") - ]; - virtual_alias_maps = lib.mkAfter ( - lib.optionals (regex_valiases_postfix != { }) [ - (mappedRegexFile "regex_valias") - ] - ); - virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; - - # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients - lmtp_destination_recipient_limit = "1"; + virtual_uid_maps = static:5000 + virtual_gid_maps = static:5000 + virtual_mailbox_base = ${mailDirectory} + virtual_mailbox_domains = ${vhosts_file} + virtual_alias_maps = hash:/var/lib/postfix/conf/valias + virtual_transport = lmtp:unix:private/dovecot-lmtp # sasl with dovecot - smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "/run/dovecot2/auth"; - smtpd_sasl_auth_enable = true; - smtpd_relay_restrictions = [ - "permit_mynetworks" - "permit_sasl_authenticated" - "reject_unauth_destination" - ]; - - # reject selected senders - smtpd_sender_restrictions = [ - "check_sender_access ${mappedFile "reject_senders"}" - ]; - - smtpd_recipient_restrictions = [ - # reject selected recipients - "check_recipient_access ${mappedFile "denied_recipients"}" - "check_recipient_access ${mappedFile "reject_recipients"}" - # quota checking - "check_policy_service unix:/run/dovecot2/quota-status" - ]; - - # The X509 private key followed by the corresponding certificate - smtpd_tls_chain_files = [ - "${keyPath}" - "${certificatePath}" - ]; - - # TLS for incoming mail is optional - smtpd_tls_security_level = "may"; - - # But required for authentication attempts - smtpd_tls_auth_only = true; - - # TLS versions supported for the SMTP server - smtpd_tls_protocols = ">=TLSv1.2"; - smtpd_tls_mandatory_protocols = ">=TLSv1.2"; - - # Require ciphersuites that OpenSSL classifies as "High" - smtpd_tls_ciphers = "high"; - smtpd_tls_mandatory_ciphers = "high"; - - # Exclude cipher suites with undesirable properties - smtpd_tls_exclude_ciphers = "SHA1, eNULL, aNULL"; - smtpd_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL"; - - # Opportunistic DANE support when delivering mail to other servers - # https://www.postfix.org/postconf.5.html#smtp_tls_security_level - smtp_dns_support_level = "dnssec"; - smtp_tls_security_level = "dane"; - - # TLS versions supported for the SMTP client - smtp_tls_protocols = ">=TLSv1.2"; - smtp_tls_mandatory_protocols = ">=TLSv1.2"; - - # Require ciphersuites that OpenSSL classifies as "High" - smtp_tls_ciphers = "high"; - smtp_tls_mandatory_ciphers = "high"; - - # Exclude ciphersuites with undesirable properties - smtp_tls_exclude_ciphers = "SHA1, eNULL, aNULL"; - smtp_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL"; - - # Restrict and prioritize the following curves in the given order - # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly. - # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves - # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7 - tls_eecdh_auto_curves = [ - "X25519" - "prime256v1" - "secp384r1" - ]; - - # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves - # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups - tls_ffdhe_auto_groups = [ ]; - - # As long as all cipher suites are considered safe, let the client use its preferred cipher - tls_preempt_cipherlist = false; + smtpd_sasl_type = dovecot + smtpd_sasl_path = private/auth + smtpd_sasl_auth_enable = yes + smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination + # TLS settings, inspired by https://github.com/jeaye/nix-files + # Submission by mail clients is handled in submissionOptions + smtpd_tls_security_level = may + # strong might suffice and is computationally less expensive + smtpd_tls_eecdh_grade = ultra + # Disable predecessors to TLS + smtpd_tls_protocols = !SSLv2, !SSLv3 + # Allowing AUTH on a non encrypted connection poses a security risk + smtpd_tls_auth_only = yes # Log only a summary message on TLS handshake completion - smtp_tls_loglevel = "1"; - smtpd_tls_loglevel = "1"; + smtpd_tls_loglevel = 1 - smtpd_milters = smtpdMilters; - non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ]; - milter_protocol = "6"; - milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}"; + # Disable weak ciphers as reported by https://ssl-tools.net + # https://serverfault.com/questions/744168/how-to-disable-rc4-on-postfix + smtpd_tls_exclude_ciphers = RC4, aNULL + smtp_tls_exclude_ciphers = RC4, aNULL + + # Configure a non blocking source of randomness + tls_random_source = dev:/dev/urandom + ''; + + submissionOptions = + { + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_sasl_type = "dovecot"; + smtpd_sasl_path = "private/auth"; + smtpd_sasl_security_options = "noanonymous"; + smtpd_sasl_local_domain = "$myhostname"; + smtpd_client_restrictions = "permit_sasl_authenticated,reject"; + smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts"; + smtpd_sender_restrictions = "reject_sender_login_mismatch"; + smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; + cleanup_service_name = "submission-header-cleanup"; }; - submissionOptions = submissionOptions; - submissionsOptions = submissionOptions; - - masterConfig = { - "lmtp" = { - # Add headers when delivering, see http://www.postfix.org/smtp.8.html - # D => Delivered-To, O => X-Original-To, R => Return-Path - args = [ "flags=O" ]; - }; - "submission-header-cleanup" = { - type = "unix"; - private = false; - chroot = false; - maxproc = 0; - command = "cleanup"; - args = [ - "-o" - "header_checks=pcre:${submissionHeaderCleanupRules}" - ]; - }; - }; + extraMasterConf = '' + submission-header-cleanup unix n - n - 0 cleanup + -o header_checks=pcre:${submissionHeaderCleanupRules} + ''; }; }; } diff --git a/mail-server/rmilter.nix b/mail-server/rmilter.nix new file mode 100644 index 0000000..5693a9a --- /dev/null +++ b/mail-server/rmilter.nix @@ -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 + +{ 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} + ''; + }; + }; +} + diff --git a/mail-server/rsnapshot.nix b/mail-server/rsnapshot.nix deleted file mode 100644 index f01ff8d..0000000 --- a/mail-server/rsnapshot.nix +++ /dev/null @@ -1,68 +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 - -{ - config, - pkgs, - lib, - ... -}: - -let - inherit (lib) - optionalString - mkIf - ; - - 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/ - ''; - }; - }; -} diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix deleted file mode 100644 index ab46750..0000000 --- a/mail-server/rspamd.nix +++ /dev/null @@ -1,284 +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 - -{ - config, - pkgs, - lib, - ... -}: - -let - cfg = config.mailserver; - - postfixCfg = config.services.postfix; - rspamdCfg = config.services.rspamd; - rspamdSocket = "rspamd.service"; - - rspamdUser = config.services.rspamd.user; - rspamdGroup = config.services.rspamd.group; - - createDkimKeypair = - domain: - let - privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key"; - publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt"; - in - pkgs.writeShellScript "dkim-keygen-${domain}" '' - if [ ! -f "${privateKey}" ] - then - ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \ - --domain "${domain}" \ - --selector "${cfg.dkimSelector}" \ - --type "${cfg.dkimKeyType}" \ - --bits ${toString cfg.dkimKeyBits} \ - --privkey "${privateKey}" > "${publicKey}" - chmod 0644 "${publicKey}" - echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}" - fi - ''; -in -{ - config = lib.mkIf cfg.enable { - environment.systemPackages = lib.mkBefore [ - (pkgs.runCommand "rspamc-wrapped" - { - nativeBuildInputs = with pkgs; [ makeWrapper ]; - } - '' - makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ - --add-flags "-h /run/rspamd/worker-controller.sock" - '' - ) - ]; - - services.rspamd = { - enable = true; - debug = cfg.debug.rspamd; - locals = { - "milter_headers.conf" = { - text = '' - extended_spam_headers = true; - ''; - }; - "redis.conf" = { - text = '' - servers = "${ - if cfg.redis.port == null then - cfg.redis.address - else - "${cfg.redis.address}:${toString cfg.redis.port}" - }"; - '' - + (lib.optionalString (cfg.redis.password != null) '' - password = "${cfg.redis.password}"; - ''); - }; - "classifier-bayes.conf" = { - text = '' - cache { - backend = "redis"; - } - ''; - }; - "antivirus.conf" = lib.mkIf cfg.virusScanning { - text = '' - clamav { - action = "reject"; - symbol = "CLAM_VIRUS"; - type = "clamav"; - log_clean = true; - servers = "/run/clamav/clamd.ctl"; - scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all - } - ''; - }; - "dkim_signing.conf" = { - text = '' - enabled = ${lib.boolToString cfg.dkimSigning}; - path = "${cfg.dkimKeyDirectory}/$domain.$selector.key"; - selector = "${cfg.dkimSelector}"; - # Allow for usernames w/o domain part - allow_username_mismatch = true - ''; - }; - "dmarc.conf" = { - text = '' - ${lib.optionalString cfg.dmarcReporting.enable '' - reporting { - enabled = true; - email = "noreply-dmarc@${cfg.systemDomain}"; - domain = "${cfg.systemDomain}"; - org_name = "${cfg.systemName}"; - from_name = "${cfg.systemName}"; - msgid_from = "${cfg.systemDomain}"; - ${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) '' - exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; - ''} - }''} - ''; - }; - }; - overrides = { - "options.inc" = { - text = '' - local_addrs = [::1/128, 127.0.0.0/8] - ''; - }; - }; - - 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 cfg.redis.configureLocally; - - systemd.tmpfiles.settings."10-rspamd.conf" = { - "${cfg.dkimKeyDirectory}" = { - d = { - # Create /var/dkim owned by rspamd user/group - user = rspamdUser; - group = rspamdGroup; - }; - Z = { - # Recursively adjust permissions in /var/dkim - user = rspamdUser; - group = rspamdGroup; - }; - }; - }; - - systemd.services.rspamd = { - requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - serviceConfig = lib.mkMerge [ - { - SupplementaryGroups = [ config.services.redis.servers.rspamd.group ]; - } - (lib.optionalAttrs cfg.dkimSigning { - ExecStartPre = map createDkimKeypair cfg.domains; - ReadWritePaths = [ cfg.dkimKeyDirectory ]; - }) - ]; - }; - - systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - # Explicitly select yesterday's date to work around broken - # default behaviour when called without a date. - # https://github.com/rspamd/rspamd/issues/4062 - script = toString [ - (lib.getExe' pkgs.rspamd "rspamadm") - "dmarc_report" - "$(date -d 'yesterday' '+%Y%m%d')" - ]; - serviceConfig = { - User = "${config.services.rspamd.user}"; - Group = "${config.services.rspamd.group}"; - - AmbientCapabilities = [ ]; - CapabilityBoundingSet = ""; - DevicePolicy = "closed"; - IPAddressAllow = "localhost"; - LockPersonality = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateMounts = true; - PrivateTmp = true; - PrivateUsers = true; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProcSubset = "pid"; - ProtectSystem = "strict"; - RemoveIPC = true; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - "AF_UNIX" - ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SupplementaryGroups = lib.optionals cfg.redis.configureLocally [ - config.services.redis.servers.rspamd.group - ]; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - "~@privileged" - ]; - UMask = "0077"; - }; - }; - - systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable { - description = "Daily delivery of aggregated DMARC reports"; - wantedBy = [ - "timers.target" - ]; - timerConfig = { - OnCalendar = "daily"; - Persistent = true; - RandomizedDelaySec = 86400; - FixedRandomDelay = true; - }; - }; - - systemd.services.postfix = { - after = [ rspamdSocket ]; - requires = [ rspamdSocket ]; - }; - - users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; - }; -} diff --git a/mail-server/monit.nix b/mail-server/services.nix similarity index 55% rename from mail-server/monit.nix rename to mail-server/services.nix index c3f8760..41d2bb3 100644 --- a/mail-server/monit.nix +++ b/mail-server/services.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,19 +14,31 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, lib, ... }: + +{ config, pkgs, lib, ... }: let cfg = config.mailserver; + + # cert :: PATH + cert = if cfg.certificateScheme == 1 + then cfg.certificateFile + else if cfg.certificateScheme == 2 + then "${cfg.certificateDirectory}/cert-${cfg.fqdn.pem" + else ""; + + # key :: PATH + key = if cfg.certificateScheme == 1 + then cfg.keyFile + else if cfg.certificateScheme == 2 + then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" + else ""; in { - config = lib.mkIf (cfg.enable && cfg.monitoring.enable) { - services.monit = { - enable = true; - config = '' - set alert ${cfg.monitoring.alertAddress} - ${cfg.monitoring.config} - ''; - }; - }; + + imports = [ + ./rmilter.nix + ./postfix.nix key + ./dovecot.nix + ]; } diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index fb11a2d..e13029a 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,92 +14,93 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - options, - pkgs, - lib, - ... -}: - -with (import ./common.nix { - inherit - config - options - lib - pkgs - ; -}); +{ config, pkgs, lib, ... }: let cfg = config.mailserver; - certificatesDeps = - if cfg.certificateScheme == "manual" then - [ ] - else if cfg.certificateScheme == "selfsigned" then - [ "mailserver-selfsigned-certificate.service" ] - else - [ "acme-finished-${cfg.fqdn}.target" ]; + create_certificate = if cfg.certificateScheme == 2 then + '' + # Create certificates if they do not exist yet + dir="${cfg.certificateDirectory}" + fqdn="${cfg.fqdn}" + case $fqdn in /*) fqdn=$(cat "$fqdn");; esac + key="''${dir}/key-${cfg.fqdn}.pem"; + cert="''${dir}/cert-${cfg.fqdn}.pem"; + + if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ] + then + mkdir -p "${cfg.certificateDirectory}" + (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) && + "${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \ + -days 3650 -out "''${cert}" + fi + '' + else ""; + + createDomainDkimCert = dom: + let + dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"; + dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; + in + '' + if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] + then + ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ + -d "${dom}" \ + --directory="${cfg.dkimKeyDirectory}" + mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" + mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" + fi + ''; + createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains); + create_dkim_cert = + '' + # Create dkim dir + mkdir -p "${cfg.dkimKeyDirectory}" + chown rmilter:rmilter "${cfg.dkimKeyDirectory}" + + ${createAllCerts} + + chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}" + ''; in { - config = lib.mkIf cfg.enable { - # Create self signed certificate - systemd.services.mailserver-selfsigned-certificate = - lib.mkIf (cfg.certificateScheme == "selfsigned") - { - after = [ "local-fs.target" ]; - script = '' - # Create certificates if they do not exist yet - dir="${cfg.certificateDirectory}" - fqdn="${cfg.fqdn}" - [[ $fqdn == /* ]] && fqdn=$(< "$fqdn") - key="$dir/key-${cfg.fqdn}.pem"; - cert="$dir/cert-${cfg.fqdn}.pem"; - - 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.${dovecotUnitName} = { - wants = certificatesDeps; - after = certificatesDeps; - preStart = - let - directories = lib.strings.escapeShellArgs ( - [ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir - ); - in + config = with cfg; lib.mkIf enable { + # Make sure postfix gets started first, so that the certificates are in place + systemd.services.dovecot2 = { + after = [ "postfix.service" ]; + preStart = '' - # Create mail directory and set permissions. See - # . - # Prevent world-readable paths, even temporarily. - umask 007 - mkdir -p ${directories} - chgrp "${cfg.vmailGroupName}" ${directories} - chmod 02770 ${directories} + mkdir -p '/run/dovecot/' + chown 'dovecot2:dovecot2' '/run/dovecot' ''; }; - # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work - systemd.services.postfix = { - wants = certificatesDeps; - after = [ - "${dovecotUnitName}.service" - ] - ++ lib.optional cfg.dkimSigning "rspamd.service" - ++ certificatesDeps; - requires = [ "${dovecotUnitName}.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service"; + # Create certificates and maildir folder + systemd.services.opensmtpd = { + after = (if (certificateScheme == 3) then [ "nginx.service" ] else []); + preStart = + '' + mkdir -p /var/empty + # Create mail directory and set permissions. See + # . + mkdir -p "${mailDirectory}" + chgrp "${vmailGroupName}" "${mailDirectory}" + chmod 02770 "${mailDirectory}" + + ${create_certificate} + ''; + }; + + # Create dkim certificates + systemd.services.rmilter = { + requires = [ "rmilter.socket" ]; + after = [ "rmilter.socket" ]; + preStart = + '' + ${create_dkim_cert} + ''; }; }; } diff --git a/mail-server/users.nix b/mail-server/users.nix index b08e3b5..9484882 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -1,5 +1,5 @@ # 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 # it under the terms of the GNU General Public License as published by @@ -14,110 +14,76 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ - config, - options, - pkgs, - lib, - ... -}: - -with (import ./common.nix { - inherit - config - options - lib - pkgs - ; -}); +{ config, pkgs, lib, ... }: with config.mailserver; let vmail_user = { name = vmailUserName; - isSystemUser = true; + isNormalUser = false; uid = vmailUID; home = mailDirectory; createHome = true; group = vmailGroupName; }; + # accountsToUser :: String -> UserRecord + accountsToUser = account: { + isNormalUser = false; + group = vmailGroupName; + inherit (account) hashedPassword name; + }; + + # mail_users :: { [String]: UserRecord } + mail_users = lib.foldl (prev: next: prev // { "${next.name}" = next; }) {} + (map accountsToUser (lib.attrValues loginAccounts)); + virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" '' #!${pkgs.stdenv.shell} set -euo pipefail - # Prevent world-readable paths, even temporarily. - umask 007 - # Create directory to store user sieve scripts if it doesn't exist - if (! test -d "${sieveDirectory}"); then - mkdir "${sieveDirectory}" - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}" - chmod 770 "${sieveDirectory}" + if (! test -d "/var/sieve"); then + mkdir "/var/sieve" + chown "${vmailUserName}:${vmailGroupName}" "/var/sieve" + chmod 770 "/var/sieve" fi # Copy user's sieve script to the correct location (if it exists). If it # is null, remove the file. - ${lib.concatMapStringsSep "\n" ( - { name, sieveScript }: - if lib.isString sieveScript then - '' - if (! test -d "${sieveDirectory}/${name}"); then - mkdir -p "${sieveDirectory}/${name}" - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}" - chmod 770 "${sieveDirectory}/${name}" - fi - cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve" - ${sieveScript} - EOF - chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve" - '' - else - '' - if (test -f "${sieveDirectory}/${name}/default.sieve"); then - rm "${sieveDirectory}/${name}/default.sieve" - fi - if (test -f "${sieveDirectory}/${name}.svbin"); then - rm "${sieveDirectory}/${name}/default.svbin" - fi - '' - ) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))} + ${lib.concatMapStringsSep "\n" ({ name, sieveScript }: + if lib.isString sieveScript then '' + cat << EOF > "/var/sieve/${name}.sieve" + ${sieveScript} + EOF + chown "${name}:${vmailGroupName}" "/var/sieve/${name}.sieve" + '' else '' + if (test -f "/var/sieve/${name}.sieve"); then + rm "/var/sieve/${name}.sieve" + fi + if (test -f "/var/sieve/${name}.svbin"); then + rm "/var/sieve/${name}.svbin" + fi + '') (map (user: { inherit (user) name sieveScript; }) + (lib.attrValues loginAccounts))} ''; -in -{ +in { config = lib.mkIf enable { - # assert that all accounts provide a password - assertions = map (acct: { - assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null; - message = "${acct.name} must provide either a hashed password or a password hash file"; - }) (lib.attrValues loginAccounts); - - # warn for accounts that specify both password and file - warnings = - map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used") - ( - lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) ( - lib.attrValues loginAccounts - ) - ); - # set the vmail gid to a specific value users.groups = { - "${vmailGroupName}" = { - gid = vmailUID; - }; + "${vmailGroupName}" = { gid = vmailUID; }; }; # define all users - users.users = { + users.users = mail_users // { "${vmail_user.name}" = lib.mkForce vmail_user; }; systemd.services.activate-virtual-mail-users = { wantedBy = [ "multi-user.target" ]; - before = [ "${dovecotUnitName}.service" ]; + before = [ "dovecot2.service" ]; serviceConfig = { ExecStart = virtualMailUsersActivationScript; }; diff --git a/migrations/nixos-mailserver-migration-03.py b/migrations/nixos-mailserver-migration-03.py deleted file mode 100644 index ead6df1..0000000 --- a/migrations/nixos-mailserver-migration-03.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env nix-shell -#!nix-shell -i python3 -p python3 - -import argparse -import os -import shutil -import sys -from enum import Enum -from pathlib import Path -from pwd import getpwnam - - -class FolderLayout(Enum): - Default = 1 - Folder = 2 - - -def check_user(vmail_root: Path): - owner = vmail_root.owner() - owner_uid = getpwnam(owner).pw_uid - - if os.geteuid() == owner_uid: - return - - try: - print( - f"Trying to switch effective user id to {owner_uid} ({owner})", - file=sys.stderr, - ) - os.seteuid(owner_uid) - return - except PermissionError: - print( - f"Failed switching to virtual mail user. Please run this script under it, for example by using `sudo -u {owner}`)", - file=sys.stderr, - ) - sys.exit(1) - - -def is_maildir_related(path: Path, layout: FolderLayout) -> bool: - if path.name in [ - "subscriptions", - # https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-uid-mapping - "dovecot-uidlist", - # https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-keywords - "dovecot-keywords", - ]: - return True - if not path.is_dir(): - return False - if path.name in ["cur", "new", "tmp"]: - return True - if layout is FolderLayout.Default and path.name.startswith("."): - return True - if layout is FolderLayout.Folder: - if path.name in ["mail"]: - return False - return True - - return False - - -def mkdir(dst: Path, dry_run: bool = True): - print(f'mkdir "{dst}"') - if not dry_run: - # u+rwx, setgid - dst.mkdir(mode=0o2700) - - -def move(src: Path, dst: Path, dry_run: bool = True): - print(f'mv "{src}" "{dst}"') - if not dry_run: - src.rename(dst) - - -def delete(dst: Path, dry_run: bool = True): - if not dst.exists(): - return - - if dst.is_dir(): - print(f'rm --recursive "{dst}"') - if not dry_run: - shutil.rmtree(dst) - else: - print(f'rm "{dst}"') - if not dry_run: - dst.unlink() - - -def main(vmail_root: Path, layout: FolderLayout, dry_run: bool = True): - maildirs = {path.parent for path in vmail_root.glob("*/*/cur")} - maybe_delete = [] - - # The old maildir will be the new home directory - for homedir in maildirs: - maildir = homedir / "mail" - mkdir(maildir, dry_run) - - for path in homedir.iterdir(): - if is_maildir_related(path, layout): - move(path, maildir / path.name, dry_run) - else: - maybe_delete.append(path) - - # Files that are part of the previous home directory, but now obsolete - for path in [ - vmail_root / ".dovecot.lda-dupes", - vmail_root / ".dovecot.lda-dupes.locks", - ]: - delete(path, dry_run) - - # The remaining files are likely obsolete, but should still be checked with care - for path in maybe_delete: - print(f"# rm {str(path)}") - - -if __name__ == "__main__": - parser = argparse.ArgumentParser( - description=""" - NixOS Mailserver Migration #3: Dovecot mail directory migration - (https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration) - """ - ) - parser.add_argument( - "vmail_root", type=Path, help="Path to the `mailserver.mailDirectory`" - ) - parser.add_argument( - "--layout", - choices=["default", "folder"], - required=True, - help="Folder layout: 'default' unless `mailserver.useFsLayout` was enabled, then'folder'", - ) - parser.add_argument( - "--execute", action="store_true", help="Actually perform changes" - ) - - args = parser.parse_args() - - layout = FolderLayout.Default if args.layout == "default" else FolderLayout.Folder - - check_user(args.vmail_root) - main(args.vmail_root, layout, not args.execute) diff --git a/nixops/single-server.nix b/nixops/single-server.nix new file mode 100644 index 0000000..1976809 --- /dev/null +++ b/nixops/single-server.nix @@ -0,0 +1,31 @@ +{ + network.description = "mail server"; + + mailserver = + { config, pkgs, ... }: + { + imports = [ + ./../default.nix + ]; + + mailserver = { + enable = true; + fqdn = "mail.example.com"; + domains = [ "example.com" "example2.com" ]; + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + }; + }; + virtualAliases = { + "info@example.com" = "user1@example.com"; + "postmaster@example.com" = "user1@example.com"; + "abuse@example.com" = "user1@example.com"; + "user1@example2.com" = "user1@example.com"; + "info@example2.com" = "user1@example.com"; + "postmaster@example2.com" = "user1@example.com"; + "abuse@example2.com" = "user1@example.com"; + }; + }; + }; +} diff --git a/nixops/vbox.nix b/nixops/vbox.nix new file mode 100644 index 0000000..2af7518 --- /dev/null +++ b/nixops/vbox.nix @@ -0,0 +1,9 @@ +{ + mailserver = + { config, pkgs, ... }: + { deployment.targetEnv = "virtualbox"; + deployment.virtualbox.memorySize = 1024; # megabytes + deployment.virtualbox.vcpu = 2; # number of cpus + deployment.virtualbox.headless = true; + }; +} diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index f290152..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,5 +0,0 @@ -[tool.ruff.lint] -extend-select = ["ISC"] - -[tool.ruff.lint.flake8-implicit-str-concat] -allow-multiline = false diff --git a/scripts/generate-options.py b/scripts/generate-options.py deleted file mode 100644 index e78e262..0000000 --- a/scripts/generate-options.py +++ /dev/null @@ -1,109 +0,0 @@ -import json -import sys -from textwrap import indent -from typing import Any, Mapping - -header = """ -# Mailserver options - -## `mailserver` - -""" - -template = """ -`````{{option}} {key} -{description} - -{type} -{default} -{example} -````` -""" - -f = open(sys.argv[1]) -options = json.load(f) - -groups = [ - "mailserver.loginAccounts", - "mailserver.certificate", - "mailserver.dkim", - "mailserver.dmarcReporting", - "mailserver.fullTextSearch", - "mailserver.redis", - "mailserver.ldap", - "mailserver.monitoring", - "mailserver.backup", - "mailserver.borgbackup", -] - - -def md_literal(value: str) -> str: - return f"`{value}`" - - -def md_codefence(value: str, language: str = "nix") -> str: - return indent( - f"\n```{language}\n{value}\n```", - prefix=2 * " ", - ) - - -def render_option_value(option: Mapping[str, Any], key: str) -> str: - if key not in option: - return "" - - if isinstance(option[key], dict) and "_type" in option[key]: - if option[key]["_type"] == "literalExpression": - # multi-line codeblock - if "\n" in option[key]["text"]: - text = option[key]["text"].rstrip("\n") - value = md_codefence(text) - # inline codeblock - else: - value = md_literal(option[key]["text"]) - # literal markdown - elif option[key]["_type"] == "literalMD": - value = option[key]["text"] - else: - assert RuntimeError(f"Unhandled option type {option[key]['_type']}") - else: - text = str(option[key]) - if text == "": - value = md_literal('""') - elif "\n" in text: - value = md_codefence(text.rstrip("\n")) - else: - value = md_literal(text) - - return f"- {key}: {value}" # type: ignore - - -def print_option(option): - if ( - isinstance(option["description"], dict) and "_type" in option["description"] - ): # mdDoc - description = option["description"]["text"] - else: - description = option["description"] - print( - template.format( - key=option["name"], - description=description or "", - type=f"- type: {md_literal(option['type'])}", - default=render_option_value(option, "default"), - example=render_option_value(option, "example"), - ) - ) - - -print(header) -for opt in options: - if any([opt["name"].startswith(c) for c in groups]): - continue - print_option(opt) - -for c in groups: - print(f"## `{c}`\n") - for opt in options: - if opt["name"].startswith(c): - print_option(opt) diff --git a/scripts/mail-check.py b/scripts/mail-check.py deleted file mode 100644 index b0f65ff..0000000 --- a/scripts/mail-check.py +++ /dev/null @@ -1,267 +0,0 @@ -import argparse -import email -import email.utils -import imaplib -import smtplib -import time -import uuid -from datetime import datetime, timedelta -from typing import cast - -RETRY = 100 - - -def _send_mail( - smtp_host, - smtp_port, - smtp_username, - from_addr, - from_pwd, - to_addr, - subject, - starttls, - ssl, -): - print(f"Sending mail with subject '{subject}'") - message = "\n".join( - [ - f"From: {from_addr}", - f"To: {to_addr}", - f"Subject: {subject}", - f"Message-ID: {uuid.uuid4()}@mail-check.py", - f"Date: {email.utils.formatdate()}", - "", - "This validates our mail server can send to Gmail :/", - ] - ) - - retry = RETRY - smtp_class = smtplib.SMTP_SSL if ssl else smtplib.SMTP - while True: - try: - with smtp_class(smtp_host, port=smtp_port) as smtp: - try: - if starttls: - smtp.starttls() - if from_pwd is not None: - smtp.login(smtp_username or from_addr, from_pwd) - - smtp.sendmail(from_addr, [to_addr], message) - return - except smtplib.SMTPResponseException as e: - if e.smtp_code == 451: # service unavailable error - print(e) - elif ( - e.smtp_code == 454 - ): # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later') - print(e) - else: - raise - except OSError as e: - if e.errno in [16, -2]: - print("OSError exception message: ", e) - else: - raise - - if retry > 0: - retry = retry - 1 - time.sleep(1) - print("Retrying") - else: - print("Retry attempts exhausted") - exit(5) - - -def _read_mail( - imap_host, - imap_port, - imap_username, - to_pwd, - subject, - ignore_dkim_spf, - show_body=False, - delete=True, -): - print(f"Reading mail from {imap_username}") - - message = None - - obj = imaplib.IMAP4_SSL(imap_host, imap_port) - obj.login(imap_username, to_pwd) - obj.select() - - today = datetime.today() - cutoff = today - timedelta(days=1) - dt = cutoff.strftime("%d-%b-%Y") - for _ in range(0, RETRY): - print("Retrying") - obj.select() - _, data = obj.search(None, f'(SINCE {dt}) (SUBJECT "{subject}")') - if data == [b""]: - time.sleep(1) - continue - - uids = data[0].decode("utf-8").split(" ") - if len(uids) != 1: - print( - f"Warning: {len(uids)} messages have been found with subject containing {subject}" - ) - - # FIXME: we only consider the first matching message... - uid = uids[0] - _, raw = obj.fetch(uid, "(RFC822)") - if delete: - obj.store(uid, "+FLAGS", "\\Deleted") - obj.expunge() - assert raw[0] and raw[0][1] - message = email.message_from_bytes(cast(bytes, raw[0][1])) - print(f"Message with subject '{message['subject']}' has been found") - if show_body: - if message.is_multipart(): - for part in message.walk(): - ctype = part.get_content_type() - if ctype == "text/plain": - body = cast(bytes, part.get_payload(decode=True)).decode() - print(f"Body:\n{body}") - else: - print(f"Body with content type {ctype} not printed") - else: - body = cast(bytes, message.get_payload(decode=True)).decode() - print(f"Body:\n{body}") - break - - if message is None: - print( - f"Error: no message with subject '{subject}' has been found in INBOX of {imap_username}" - ) - exit(1) - - if ignore_dkim_spf: - return - - # gmail set this standardized header - if "ARC-Authentication-Results" in message: - if "dkim=pass" in message["ARC-Authentication-Results"]: - print("DKIM ok") - else: - print("Error: no DKIM validation found in message:") - print(message.as_string()) - exit(2) - if "spf=pass" in message["ARC-Authentication-Results"]: - print("SPF ok") - else: - print("Error: no SPF validation found in message:") - print(message.as_string()) - exit(3) - else: - print("DKIM and SPF verification failed") - exit(4) - - -def send_and_read(args): - src_pwd = None - if args.src_password_file is not None: - src_pwd = args.src_password_file.readline().rstrip() - dst_pwd = args.dst_password_file.readline().rstrip() - - if args.imap_username != "": - imap_username = args.imap_username - else: - imap_username = args.to_addr - - subject = f"{uuid.uuid4()}" - - _send_mail( - smtp_host=args.smtp_host, - smtp_port=args.smtp_port, - smtp_username=args.smtp_username, - from_addr=args.from_addr, - from_pwd=src_pwd, - to_addr=args.to_addr, - subject=subject, - starttls=args.smtp_starttls, - ssl=args.smtp_ssl, - ) - - _read_mail( - imap_host=args.imap_host, - imap_port=args.imap_port, - imap_username=imap_username, - to_pwd=dst_pwd, - subject=subject, - ignore_dkim_spf=args.ignore_dkim_spf, - ) - - -def read(args): - _read_mail( - imap_host=args.imap_host, - imap_port=args.imap_port, - imap_username=args.imap_username, - to_pwd=args.imap_password, - subject=args.subject, - ignore_dkim_spf=args.ignore_dkim_spf, - show_body=args.show_body, - delete=False, - ) - - -parser = argparse.ArgumentParser() -subparsers = parser.add_subparsers() - -parser_send_and_read = subparsers.add_parser( - "send-and-read", - description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.", -) -parser_send_and_read.add_argument("--smtp-host", type=str) -parser_send_and_read.add_argument("--smtp-port", type=str, default=25) -parser_send_and_read.add_argument("--smtp-starttls", action="store_true") -parser_send_and_read.add_argument("--smtp-ssl", action="store_true") -parser_send_and_read.add_argument( - "--smtp-username", - type=str, - default="", - help="username used for smtp login. If not specified, the from-addr value is used", -) -parser_send_and_read.add_argument("--from-addr", type=str) -parser_send_and_read.add_argument("--imap-host", required=True, type=str) -parser_send_and_read.add_argument("--imap-port", type=str, default=993) -parser_send_and_read.add_argument("--to-addr", type=str, required=True) -parser_send_and_read.add_argument( - "--imap-username", - type=str, - default="", - help="username used for imap login. If not specified, the to-addr value is used", -) -parser_send_and_read.add_argument("--src-password-file", type=argparse.FileType("r")) -parser_send_and_read.add_argument( - "--dst-password-file", required=True, type=argparse.FileType("r") -) -parser_send_and_read.add_argument( - "--ignore-dkim-spf", - action="store_true", - help="to ignore the dkim and spf verification on the read mail", -) -parser_send_and_read.set_defaults(func=send_and_read) - -parser_read = subparsers.add_parser( - "read", - description="Search for an email with a subject containing 'subject' in the INBOX.", -) -parser_read.add_argument("--imap-host", type=str, default="localhost") -parser_read.add_argument("--imap-port", type=str, default=993) -parser_read.add_argument("--imap-username", required=True, type=str) -parser_read.add_argument("--imap-password", required=True, type=str) -parser_read.add_argument( - "--ignore-dkim-spf", - action="store_true", - help="to ignore the dkim and spf verification on the read mail", -) -parser_read.add_argument( - "--show-body", action="store_true", help="print mail text/plain payload" -) -parser_read.add_argument("subject", type=str) -parser_read.set_defaults(func=read) - -args = parser.parse_args() -args.func(args) diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 493783d..0000000 --- a/shell.nix +++ /dev/null @@ -1,9 +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 diff --git a/tests/clamav.nix b/tests/clamav.nix deleted file mode 100644 index 209e91e..0000000 --- a/tests/clamav.nix +++ /dev/null @@ -1,254 +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 - -{ - lib, - blobs, - ... -}: - -{ - name = "clamav"; - - nodes = { - server = - { pkgs, ... }: - { - imports = [ - ../default.nix - ./lib/config.nix - ]; - - virtualisation.memorySize = 1500; - - environment.systemPackages = with pkgs; [ netcat ]; - - services.rsyslogd = { - enable = true; - defaultConfig = '' - *.* /dev/console - ''; - }; - - services.clamav.updater.enable = lib.mkForce false; - systemd.services.old-clam = { - before = [ "clamav-daemon.service" ]; - requiredBy = [ "clamav-daemon.service" ]; - description = "ClamAV virus database"; - - preStart = '' - mkdir -m 0755 -p /var/lib/clamav - chown clamav:clamav /var/lib/clamav - ''; - - script = '' - cp ${blobs}/clamav/main.cvd /var/lib/clamav/ - cp ${blobs}/clamav/daily.cvd /var/lib/clamav/ - cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/ - chown clamav:clamav /var/lib/clamav/* - ''; - - serviceConfig = { - Type = "oneshot"; - PrivateTmp = "yes"; - PrivateDevices = "yes"; - }; - }; - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ - "example.com" - "example2.com" - ]; - virusScanning = true; - - loginAccounts = { - "user1@example.com" = { - hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; - aliases = [ "postmaster@example.com" ]; - catchAll = [ "example.com" ]; - }; - "user@example2.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - }; - }; - enableImap = true; - }; - - environment.etc = { - "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; - }; - }; - client = - { nodes, pkgs, ... }: - let - serverIP = nodes.server.networking.primaryIPAddress; - clientIP = nodes.client.networking.primaryIPAddress; - grep-ip = pkgs.writeScriptBin "grep-ip" '' - #!${pkgs.stdenv.shell} - echo grep '${clientIP}' "$@" >&2 - exec grep '${clientIP}' "$@" - ''; - 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 - 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 - 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 - To: User1 - 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 = '' - 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 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - server.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" - ) - - 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") - ''; -} diff --git a/tests/extern.nix b/tests/extern.nix new file mode 100644 index 0000000..28fff3b --- /dev/null +++ b/tests/extern.nix @@ -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 + +import { + + 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 + To: User1 + Cc: + Bcc: + Subject: This is a test Email from user2 to user1 + Reply-To: + + Hello User1, + + how are you doing today? + ''; + email2 = + '' + From: User + To: User1 + 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 + To: Chuck + 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"); + }; + + + ''; + + +} diff --git a/tests/external.nix b/tests/external.nix deleted file mode 100644 index 0f47acb..0000000 --- a/tests/external.nix +++ /dev/null @@ -1,528 +0,0 @@ -# nixos-mailserver: a simple mail server -# Copyright (C) 2016-2018 Robin Raymond -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see - -{ - name = "external"; - - nodes = { - server = - { pkgs, ... }: - { - imports = [ - ../default.nix - ./lib/config.nix - ]; - - environment.systemPackages = with pkgs; [ netcat ]; - - virtualisation.memorySize = 1024; - - services.rsyslogd = { - enable = true; - defaultConfig = '' - *.* /dev/console - ''; - }; - - mailserver = { - enable = true; - debug.dovecot = true; # enabled for sieve script logging - fqdn = "mail.example.com"; - domains = [ - "example.com" - "example2.com" - ]; - rewriteMessageId = true; - dkimKeyBits = 1535; - dmarcReporting.enable = true; - - loginAccounts = { - "user1@example.com" = { - hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; - aliases = [ "postmaster@example.com" ]; - catchAll = [ "example.com" ]; - }; - "user2@example.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - aliases = [ "chuck@example.com" ]; - }; - "user@example2.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - }; - "lowquota@example.com" = { - hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; - quota = "1B"; - }; - }; - - extraVirtualAliases = { - "single-alias@example.com" = "user1@example.com"; - "multi-alias@example.com" = [ - "user1@example.com" - "user2@example.com" - ]; - }; - - enableImap = true; - enableImapSsl = true; - fullTextSearch = { - enable = true; - autoIndex = true; - # special use depends on https://github.com/NixOS/nixpkgs/pull/93201 - autoIndexExclude = [ - (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") - ]; - enforced = "yes"; - }; - }; - }; - client = - { nodes, pkgs, ... }: - let - serverIP = nodes.server.networking.primaryIPAddress; - clientIP = nodes.client.networking.primaryIPAddress; - grep-ip = pkgs.writeScriptBin "grep-ip" '' - #!${pkgs.stdenv.shell} - echo grep '${clientIP}' "$@" >&2 - exec grep '${clientIP}' "$@" - ''; - check-mail-id = pkgs.writeScriptBin "check-mail-id" '' - #!${pkgs.stdenv.shell} - echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2 - exec grep '^Message-ID:.*@mail.example.com>$' "$@" - ''; - test-imap-spam = pkgs.writeScriptBin "imap-mark-spam" '' - #!${pkgs.python3.interpreter} - import imaplib - - with imaplib.IMAP4_SSL('${serverIP}') as imap: - imap.login('user1@example.com', 'user1') - imap.select() - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.copy(','.join(msg_ids), 'Junk') - for num in msg_ids: - imap.store(num, '+FLAGS', '\\Deleted') - imap.expunge() - - imap.select('Junk') - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.close() - ''; - test-imap-ham = pkgs.writeScriptBin "imap-mark-ham" '' - #!${pkgs.python3.interpreter} - import imaplib - - with imaplib.IMAP4_SSL('${serverIP}') as imap: - imap.login('user1@example.com', 'user1') - imap.select('Junk') - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.copy(','.join(msg_ids), 'INBOX') - for num in msg_ids: - imap.store(num, '+FLAGS', '\\Deleted') - imap.expunge() - - imap.select('INBOX') - status, [response] = imap.search(None, 'ALL') - msg_ids = response.decode("utf-8").split(' ') - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - - imap.close() - ''; - search = pkgs.writeScriptBin "search" '' - #!${pkgs.python3.interpreter} - import imaplib - import sys - - [_, mailbox, needle] = sys.argv - - with imaplib.IMAP4_SSL('${serverIP}') as imap: - imap.login('user1@example.com', 'user1') - imap.select(mailbox) - status, [response] = imap.search(None, 'BODY', repr(needle)) - msg_ids = [ i for i in response.decode("utf-8").split(' ') if i ] - print(msg_ids) - assert status == 'OK' - assert len(msg_ids) == 1 - status, response = imap.fetch(msg_ids[0], '(RFC822)') - assert status == "OK" - assert needle in repr(response) - imap.close() - ''; - in - { - imports = [ - ./lib/config.nix - ]; - environment.systemPackages = with pkgs; [ - fetchmail - msmtp - procmail - findutils - grep-ip - check-mail-id - test-imap-spam - test-imap-ham - search - ]; - environment.etc = { - "root/.fetchmailrc" = { - text = '' - poll ${serverIP} with proto IMAP - user 'user1@example.com' there with password 'user1' is 'root' here - mda procmail - ''; - mode = "0700"; - }; - "root/.fetchmailRcLowQuota" = { - text = '' - poll ${serverIP} with proto IMAP - user 'lowquota@example.com' there with password 'user2' is 'root' here - mda procmail - ''; - mode = "0700"; - }; - "root/.procmailrc" = { - text = "DEFAULT=$HOME/mail"; - }; - "root/.msmtprc" = { - text = '' - account test - host ${serverIP} - port 587 - from user2@example.com - user user2@example.com - password user2 - - account test2 - host ${serverIP} - port 587 - from user@example2.com - user user@example2.com - password user2 - - account test3 - host ${serverIP} - port 587 - from chuck@example.com - user user2@example.com - password user2 - - account test4 - host ${serverIP} - port 587 - from postmaster@example.com - user user1@example.com - password user1 - - account test5 - host ${serverIP} - port 587 - from single-alias@example.com - user user1@example.com - password user1 - ''; - }; - "root/email1".text = '' - Message-ID: <12345qwerty@host.local.network> - From: User2 - To: User1 - 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 - To: User1 - 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: - From: Postmaster - To: Chuck - 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: - From: Single Alias - To: User1 - 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 - To: Multi Alias - 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 - To: User1 - 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 - To: User1 - Cc: - Bcc: - Subject: This is a test Email from user2 to user1 - Reply-To: - - Hello User1, - - this email does not contain the needle :( - ''; - }; - }; - }; - - testScript = '' - start_all() - - server.wait_for_unit("multi-user.target") - client.wait_for_unit("multi-user.target") - - # TODO put this blocking into the systemd units? - server.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - - client.execute("cp -p /etc/root/.* ~/") - client.succeed("mkdir -p ~/mail") - client.succeed("ls -la ~/ >&2") - client.succeed("cat ~/.fetchmailrc >&2") - client.succeed("cat ~/.procmailrc >&2") - client.succeed("cat ~/.msmtprc >&2") - - with subtest("imap retrieving mail"): - # fetchmail returns EXIT_CODE 1 when no new mail - client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2") - - with subtest("submission port send mail"): - # send email from user2 to user1 - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2" - ) - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - - with subtest("imap retrieving mail 2"): - client.execute("rm ~/mail/*") - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v >&2") - - with subtest("remove sensitive information on submission port"): - client.succeed("cat ~/mail/* >&2") - ## make sure our IP is _not_ in the email header - client.fail("grep-ip ~/mail/*") - client.succeed("check-mail-id ~/mail/*") - - with subtest("have correct fqdn as sender"): - client.succeed("grep 'Received: from mail.example.com' ~/mail/*") - - with subtest("dkim has user-specified size"): - server.succeed( - "openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'" - ) - - with subtest("dkim singing, multiple domains"): - client.execute("rm ~/mail/*") - # send email from user2 to user1 - client.succeed( - "msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") - client.succeed("cat ~/mail/* >&2") - # make sure it is dkim signed - client.succeed("grep DKIM-Signature: ~/mail/*") - - with subtest("aliases"): - client.execute("rm ~/mail/*") - # send email from chuck to postmaster - client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") - - with subtest("catchAlls"): - client.execute("rm ~/mail/*") - # send email from chuck to non exsitent account - client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") - - client.execute("rm ~/mail/*") - # send email from user1 to chuck - client.succeed( - "msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 1 when no new mail - # if this succeeds, it means that user1 recieved the mail that was intended for chuck. - client.fail("fetchmail --nosslcertck -v") - - with subtest("extraVirtualAliases"): - client.execute("rm ~/mail/*") - # send email from single-alias to user1 - client.succeed( - "msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") - - client.execute("rm ~/mail/*") - # send email from user1 to multi-alias (user{1,2}@example.com) - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.succeed("fetchmail --nosslcertck -v") - - with subtest("quota"): - client.execute("rm ~/mail/*") - client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc") - - client.succeed( - "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2" - ) - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - # fetchmail returns EXIT_CODE 0 when it retrieves mail - client.fail("fetchmail --nosslcertck -v") - - with subtest("imap sieve junk trainer"): - # send email from user2 to user1 - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2" - ) - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - - client.succeed("imap-mark-spam >&2") - server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-spam.sh >&2") - client.succeed("imap-mark-ham >&2") - server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-ham.sh >&2") - - with subtest("full text search and indexation"): - # send 2 email from user2 to user1 - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2" - ) - client.succeed( - "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2" - ) - # give the mail server some time to process the mail - server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]') - - # should find exactly one email containing this - client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2") - # should fail because this folder is not indexed - client.fail("search Junk a >&2") - # check that search really goes through the indexer - server.succeed("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2") - # check that Junk is not indexed - server.fail("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2") - - with subtest("dmarc reporting"): - server.systemctl("start rspamd-dmarc-reporter.service") - - with subtest("no warnings or errors"): - server.fail("journalctl -u postfix | grep -i error >&2") - server.fail("journalctl -u postfix | grep -i warning >&2") - server.fail("journalctl -u dovecot -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2") - # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html - server.fail( - "journalctl -u dovecot -u dovecot2 | \ - grep -v 'Expunged message reappeared, giving a new UID' | \ - grep -v 'Time moved forwards' | \ - grep -i warning >&2" - ) - ''; -} diff --git a/tests/intern.nix b/tests/intern.nix new file mode 100644 index 0000000..3bdacb8 --- /dev/null +++ b/tests/intern.nix @@ -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 + +import { + + 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"); + }; + + ''; +} diff --git a/tests/internal.nix b/tests/internal.nix deleted file mode 100644 index 29d0880..0000000 --- a/tests/internal.nix +++ /dev/null @@ -1,227 +0,0 @@ -# nixos-mailserver: a simple mail server -# Copyright (C) 2016-2018 Robin Raymond -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see - -{ - pkgs, - ... -}: - -let - sendMail = pkgs.writeTextFile { - "name" = "send-mail-to-send-only-account"; - "text" = '' - EHLO mail.example.com - MAIL FROM: none@example.com - RCPT TO: send-only@example.com - QUIT - ''; - }; - - hashPassword = - password: - pkgs.runCommand "password-${password}-hashed" - { - buildInputs = [ pkgs.mkpasswd ]; - inherit password; - } - '' - mkpasswd -sm bcrypt <<<"$password" > $out - ''; - - hashedPasswordFile = hashPassword "my-password"; - passwordFile = pkgs.writeText "password" "my-password"; -in -{ - name = "internal"; - - nodes = { - machine = - { pkgs, ... }: - { - imports = [ - ./../default.nix - ./lib/config.nix - ]; - - virtualisation.memorySize = 1024; - - environment.systemPackages = [ - (pkgs.writeScriptBin "mail-check" '' - ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '') - ] - ++ (with pkgs; [ - curl - openssl - netcat - ]); - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ - "example.com" - "domain.com" - ]; - localDnsResolver = false; - - loginAccounts = { - "user1@example.com" = { - hashedPasswordFile = hashedPasswordFile; - }; - "user2@example.com" = { - hashedPasswordFile = hashedPasswordFile; - aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ]; - }; - "send-only@example.com" = { - hashedPasswordFile = hashPassword "send-only"; - sendOnly = true; - }; - }; - forwards = { - # user2@example.com is a local account and its mails are - # also forwarded to user1@example.com - "user2@example.com" = "user1@example.com"; - }; - - vmailGroupName = "vmail"; - vmailUID = 5000; - indexDir = "/var/lib/dovecot/indices"; - - enableImap = false; - }; - }; - }; - testScript = - { - nodes, - ... - }: - '' - machine.start() - machine.wait_for_unit("multi-user.target") - - # Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205 - with subtest("mail forwarded can are locally kept"): - # A mail sent to user2@example.com via explicit TLS is in the user1@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--imap-host localhost", - "--imap-username user1@example.com", - "--from-addr user1@example.com", - "--to-addr user2@example.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) - # A mail sent to user2@example.com via implicit TLS is in the user2@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 465", - "--smtp-ssl", - "--smtp-host localhost", - "--imap-host localhost", - "--imap-username user2@example.com", - "--from-addr user1@example.com", - "--to-addr user2@example.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) - - with subtest("regex email alias are received"): - # A mail sent to user2-regex-alias@domain.com via explicit TLS is in the user2@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--imap-host localhost", - "--imap-username user2@example.com", - "--from-addr user1@example.com", - "--to-addr user2-regex-alias@domain.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) - - with subtest("user can send from regex email alias"): - # A mail sent to user1@example.com from user2-regex-alias@domain.com by - # user2@example.com via implicit TLS is in the user1@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 465", - "--smtp-ssl", - "--smtp-host localhost", - "--imap-host localhost", - "--smtp-username user2@example.com", - "--from-addr user2-regex-alias@domain.com", - "--to-addr user1@example.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) - - with subtest("vmail gid is set correctly"): - machine.succeed("getent group vmail | grep 5000") - - with subtest("Check dovecot maildir and index locations"): - # If these paths change we need a migration - machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.mailDirectory}/example.com/user1") - machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'") - - with subtest("mail to send only accounts is rejected"): - machine.wait_for_open_port(25) - # TODO put this blocking into the systemd units - machine.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - machine.succeed( - "cat ${sendMail} | nc localhost 25 | grep -q '554 5.5.0 Error'" - ) - - with subtest("rspamd controller serves web ui"): - machine.succeed( - "set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" - ) - - with subtest("imap port 143 is closed and imaps is serving SSL"): - machine.wait_for_closed_port(143) - machine.wait_for_open_port(993) - machine.succeed( - "echo | openssl s_client -connect localhost:993 | grep 'New, TLS'" - ) - ''; -} diff --git a/tests/ldap.nix b/tests/ldap.nix deleted file mode 100644 index 4d0675a..0000000 --- a/tests/ldap.nix +++ /dev/null @@ -1,231 +0,0 @@ -let - bindPassword = "unsafegibberish"; - alicePassword = "testalice"; - bobPassword = "testbob"; -in -{ - name = "ldap"; - - nodes = { - machine = - { pkgs, ... }: - { - imports = [ - ./../default.nix - ./lib/config.nix - ]; - - virtualisation.memorySize = 1024; - - services.openssh = { - enable = true; - settings.PermitRootLogin = "yes"; - }; - - environment.systemPackages = [ - (pkgs.writeScriptBin "mail-check" '' - ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '') - ]; - - environment.etc.bind-password.text = bindPassword; - - services.openldap = { - enable = true; - settings = { - children = { - "cn=schema".includes = [ - "${pkgs.openldap}/etc/schema/core.ldif" - "${pkgs.openldap}/etc/schema/cosine.ldif" - "${pkgs.openldap}/etc/schema/inetorgperson.ldif" - "${pkgs.openldap}/etc/schema/nis.ldif" - ]; - "olcDatabase={1}mdb" = { - attrs = { - objectClass = [ - "olcDatabaseConfig" - "olcMdbConfig" - ]; - olcDatabase = "{1}mdb"; - olcDbDirectory = "/var/lib/openldap/example"; - olcSuffix = "dc=example"; - }; - }; - }; - }; - declarativeContents."dc=example" = '' - dn: dc=example - objectClass: domain - dc: example - - dn: cn=mail,dc=example - objectClass: organizationalRole - objectClass: simpleSecurityObject - objectClass: top - cn: mail - userPassword: ${bindPassword} - - dn: ou=users,dc=example - objectClass: organizationalUnit - ou: users - - dn: cn=alice,ou=users,dc=example - objectClass: inetOrgPerson - cn: alice - sn: Foo - mail: alice@example.com - userPassword: ${alicePassword} - - dn: cn=bob,ou=users,dc=example - objectClass: inetOrgPerson - cn: bob - sn: Bar - mail: bob@example.com - userPassword: ${bobPassword} - ''; - }; - - mailserver = { - enable = true; - fqdn = "mail.example.com"; - domains = [ "example.com" ]; - localDnsResolver = false; - indexDir = "/var/lib/dovecot/indices"; - - ldap = { - enable = true; - uris = [ - "ldap://" - ]; - bind = { - dn = "cn=mail,dc=example"; - passwordFile = "/etc/bind-password"; - }; - searchBase = "ou=users,dc=example"; - searchScope = "sub"; - }; - - forwards = { - "bob_fw@example.com" = "bob@example.com"; - }; - - vmailGroupName = "vmail"; - vmailUID = 5000; - - enableImap = false; - }; - }; - }; - testScript = - { - nodes, - ... - }: - '' - import sys - import re - - machine.start() - machine.wait_for_unit("multi-user.target") - - # This function retrieves the ldap table file from a postconf - # command. - # A key lookup is achived and the returned value is compared - # to the expected value. - def test_lookup(postconf_cmdline, key, expected): - conf = machine.succeed(postconf_cmdline).rstrip() - ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1) - value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip() - try: - assert value == expected - except AssertionError: - print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr) - raise - - with subtest("Test postmap lookups"): - test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com") - test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com") - - test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com") - test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com") - - with subtest("Test doveadm lookups"): - machine.succeed("doveadm user -u alice@example.com") - machine.succeed("doveadm user -u bob@example.com") - - with subtest("Files containing secrets are only readable by root"): - machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'") - machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'") - - with subtest("Test account/mail address binding via explicit TLS"): - machine.fail(" ".join([ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--smtp-username alice@example.com", - "--imap-host localhost", - "--imap-username bob@example.com", - "--from-addr bob@example.com", - "--to-addr aliceb@example.com", - "--src-password-file <(echo '${alicePassword}')", - "--dst-password-file <(echo '${bobPassword}')", - "--ignore-dkim-spf" - ])) - machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'") - - with subtest("Test mail delivery via implicit TLS"): - machine.succeed(" ".join([ - "mail-check send-and-read", - "--smtp-port 465", - "--smtp-ssl", - "--smtp-host localhost", - "--smtp-username alice@example.com", - "--imap-host localhost", - "--imap-username bob@example.com", - "--from-addr alice@example.com", - "--to-addr bob@example.com", - "--src-password-file <(echo '${alicePassword}')", - "--dst-password-file <(echo '${bobPassword}')", - "--ignore-dkim-spf" - ])) - - with subtest("Test mail forwarding via explicit TLS works"): - machine.succeed(" ".join([ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--smtp-username alice@example.com", - "--imap-host localhost", - "--imap-username bob@example.com", - "--from-addr alice@example.com", - "--to-addr bob_fw@example.com", - "--src-password-file <(echo '${alicePassword}')", - "--dst-password-file <(echo '${bobPassword}')", - "--ignore-dkim-spf" - ])) - - with subtest("Test cannot send mail via implicit TLS from forwarded address"): - machine.fail(" ".join([ - "mail-check send-and-read", - "--smtp-port 465", - "--smtp-ssl", - "--smtp-host localhost", - "--smtp-username bob@example.com", - "--imap-host localhost", - "--imap-username alice@example.com", - "--from-addr bob_fw@example.com", - "--to-addr alice@example.com", - "--src-password-file <(echo '${bobPassword}')", - "--dst-password-file <(echo '${alicePassword}')", - "--ignore-dkim-spf" - ])) - machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'") - - with subtest("Check dovecot mail and index locations"): - # If these paths change we need a migration - machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/bob@example.com") - machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/bob@example.com'") - ''; -} diff --git a/tests/lib/config.nix b/tests/lib/config.nix deleted file mode 100644 index 199e1b8..0000000 --- a/tests/lib/config.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ - lib, - ... -}: - -{ - # Testing eval failures that result from stateVersion assertion is out of scope - mailserver.stateVersion = 999; - - # Enable second CPU core - virtualisation.cores = lib.mkDefault 2; - - services.rspamd = { - # Don't make tests block on DNS requests that will never succeed - locals."options.inc".text = '' - dns { - nameservers = ["127.0.0.1"]; - timeout = 0.0s; - retransmits = 0; - } - ''; - # Relax `local_addrs` definition to default for tests, so mail doesn't get flagged as spam - overrides."options.inc".enable = false; - }; -} diff --git a/mail-server/kresd.nix b/tests/minimal.nix similarity index 75% rename from mail-server/kresd.nix rename to tests/minimal.nix index 3920534..79e070f 100644 --- a/mail-server/kresd.nix +++ b/tests/minimal.nix @@ -14,13 +14,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, lib, ... }: +import ./../../nixpkgs/nixos/tests/make-test.nix { -let - cfg = config.mailserver; -in -{ - config = lib.mkIf (cfg.enable && cfg.localDnsResolver) { - services.kresd.enable = true; - }; + machine = + { config, pkgs, ... }: + { + imports = [ + ./../default.nix + ]; + }; + + testScript = + '' + $machine->waitForUnit("multi-user.target"); + ''; } diff --git a/tests/multiple.nix b/tests/multiple.nix deleted file mode 100644 index 8ba2920..0000000 --- a/tests/multiple.nix +++ /dev/null @@ -1,113 +0,0 @@ -# This tests is used to test features requiring several mail domains. - -{ - pkgs, - ... -}: - -let - hashPassword = - password: - pkgs.runCommand "password-${password}-hashed" - { - buildInputs = [ pkgs.mkpasswd ]; - inherit password; - } - '' - mkpasswd -sm bcrypt <<<"$password" > $out - ''; - - password = pkgs.writeText "password" "password"; - - domainGenerator = - domain: - { pkgs, ... }: - { - imports = [ - ../default.nix - ./lib/config.nix - ]; - environment.systemPackages = with pkgs; [ netcat ]; - virtualisation.memorySize = 1024; - mailserver = { - enable = true; - fqdn = "mail.${domain}"; - domains = [ domain ]; - localDnsResolver = false; - loginAccounts = { - "user@${domain}" = { - hashedPasswordFile = hashPassword "password"; - }; - }; - enableImap = true; - enableImapSsl = true; - }; - services.dnsmasq = { - enable = true; - settings.mx-host = [ - "domain1.com,domain1,10" - "domain2.com,domain2,10" - ]; - }; - }; - -in - -{ - name = "multiple"; - - nodes = { - domain1 = - { ... }: - { - imports = [ - ../default.nix - (domainGenerator "domain1.com") - ]; - mailserver.forwards = { - "non-local@domain1.com" = [ - "user@domain2.com" - "user@domain1.com" - ]; - "non@domain1.com" = [ - "user@domain2.com" - "user@domain1.com" - ]; - }; - }; - domain2 = domainGenerator "domain2.com"; - client = - { pkgs, ... }: - { - environment.systemPackages = [ - (pkgs.writeScriptBin "mail-check" '' - ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ - '') - ]; - }; - }; - testScript = '' - start_all() - - domain1.wait_for_unit("multi-user.target") - domain2.wait_for_unit("multi-user.target") - - # TODO put this blocking into the systemd units? - domain1.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - domain2.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - - # user@domain1.com sends a mail to user@domain2.com via explicit TLS - client.succeed( - "mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf" - ) - - # Send a mail to the address forwarded via implicit TLS and check it is in the recipient mailbox - client.succeed( - "mail-check send-and-read --smtp-port 465 --smtp-ssl --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf" - ) - ''; -} diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..ff38a9c --- /dev/null +++ b/update.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +sed -i -e "s/v[0-9]\+\.[0-9]\+\.[0-9]\+/$1/g" README.md