From 7ea7ab39df010c01e7280b1e35b41c794a5c677b Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Thu, 21 Sep 2017 16:32:01 +0200 Subject: [PATCH 01/38] add build status --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c3b11a6..7c3393d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # ![Simple Nixos MailServer][logo] ![license](https://img.shields.io/badge/license-GPL3-brightgreen.svg) +![status](https://travis-ci.org/r-raymond/nixos-mailserver.svg?branch=master) ## Stable Releases From b06775cef7ef3487033d971a6bd579041bb67549 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Fri, 22 Sep 2017 18:57:14 +0200 Subject: [PATCH 02/38] add vmail user name again - otherwise postfix errors on startup --- mail-server/users.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail-server/users.nix b/mail-server/users.nix index 873059e..a8cbcc9 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -20,12 +20,12 @@ with config.mailserver; let vmail_user = [{ - #name = vmailUserName; + name = vmailUserName; isNormalUser = false; uid = vmailUIDStart; home = mailDirectory; createHome = true; - #group = vmailGroupName; + group = vmailGroupName; }]; # accountsToUser :: String -> UserRecord From 613f9c8ce07e0269ae3629ca61631829c7ba686c Mon Sep 17 00:00:00 2001 From: Matthew Bauer Date: Fri, 22 Sep 2017 12:37:03 -0500 Subject: [PATCH 03/38] Switch to nixpkgs-unstable channel. see travis-ci/travis-ci#8483 --- .travis.yml | 1 - tests/extern.nix | 2 +- tests/intern.nix | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fe9e2d1..22e0e56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,4 @@ language: nix script: - - git clone --depth 1 https://github.com/NixOS/nixpkgs ../nixpkgs - nix-build tests/intern.nix - nix-build tests/extern.nix diff --git a/tests/extern.nix b/tests/extern.nix index 147d218..f98f10e 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -import ./../../nixpkgs/nixos/tests/make-test.nix { +import { nodes = { server = { config, pkgs, ... }: diff --git a/tests/intern.nix b/tests/intern.nix index 5002e24..d066c24 100644 --- a/tests/intern.nix +++ b/tests/intern.nix @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -import ./../../nixpkgs/nixos/tests/make-test.nix { +import { machine = { config, pkgs, ... }: From 2f7e3a9f0ce314d03e0e6371a0cbf3085ce83cd5 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 23 Sep 2017 09:56:09 +0200 Subject: [PATCH 04/38] initial acme support; needs testing --- default.nix | 5 ++--- mail-server/common.nix | 8 ++++++-- mail-server/nginx.nix | 43 +++++++++++++++++++++++++++++++++++++++++ mail-server/postfix.nix | 6 +++--- mail-server/systemd.nix | 1 + 5 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 mail-server/nginx.nix diff --git a/default.nix b/default.nix index 74c47c2..2a36ae7 100644 --- a/default.nix +++ b/default.nix @@ -138,7 +138,7 @@ in }; certificateScheme = mkOption { - type = types.enum [ 1 2 ]; + type = types.enum [ 1 2 3 ]; default = 2; description = '' Certificate Files. There are three options for these. @@ -149,8 +149,6 @@ in 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. TODO: Explain more details - - TODO: Only certificate scheme 1) and 2) work as of yet. ''; }; @@ -256,5 +254,6 @@ in ./mail-server/dovecot.nix ./mail-server/postfix.nix ./mail-server/rmilter.nix + ./mail-server/nginx.nix ]; } diff --git a/mail-server/common.nix b/mail-server/common.nix index f32d898..0d15ce7 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -25,12 +25,16 @@ in then cfg.certificateFile else if cfg.certificateScheme == 2 then "${cfg.certificateDirectory}/cert-${cfg.domain}.pem" - else ""; + else if cfg.certificateScheme == 3 + then "/var/lib/acme/acme-challenge/${cfg.hostPrefix}.${cfg.domain}/fullchain.pem" + else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; # key :: PATH keyPath = if cfg.certificateScheme == 1 then cfg.keyFile else if cfg.certificateScheme == 2 then "${cfg.certificateDirectory}/key-${cfg.domain}.pem" - else ""; + else if cfg.certificateScheme == 3 + then "/var/lib/acme/acme-challenge/${cfg.hostPrefix}.${cfg.domain}/privkey.pem" + else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix new file mode 100644 index 0000000..15bb596 --- /dev/null +++ b/mail-server/nginx.nix @@ -0,0 +1,43 @@ +# 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 + cfg = config.mailserver; +in +{ + config = with cfg; lib.mkIf (certificateScheme == 3) { + + services.nginx = { + enable = true; + virtualHosts = { + domain = { + serverName = "${hostPrefix}.${domain}"; + forceSSL = true; + enableACME = true; + locations."/" = { + root = "/var/www"; + }; + acmeRoot = "/var/lib/acme/acme-challenge"; + }; + }; + }; + }; +} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index ebcfb0c..fedb53c 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -49,9 +49,9 @@ let vaccounts_file = builtins.toFile "vaccounts" (lib.concatStringsSep "\n" (vaccounts_identity ++ 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. + # 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 diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 0c82b16..48f5a5e 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -63,6 +63,7 @@ in # Create certificates and maildir folder systemd.services.postfix = { + after = (if (certificateScheme == 3) then [ "nginx.service" ] else []); preStart = '' # Create mail directory and set permissions. See From 6ee342d96352eb10fd9615ca92085cc4a305db6f Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 23 Sep 2017 09:58:10 +0200 Subject: [PATCH 05/38] Update Readme (acme) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c3393d..88c8d87 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ None so far. ### v1.2 * Certificates - - [ ] Let's Encrypt + - [x] Let's Encrypt * Sieves - [ ] Allow user defined sieve scripts * User Aliases From e1ff6fdec43c32e1d2440ec84c95a59f394bc001 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Tue, 17 Oct 2017 11:52:47 +0200 Subject: [PATCH 06/38] Update README.md --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 88c8d87..1b4f6c5 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,34 @@ None so far. * Changed structure to Nix Modules * Adds Sieve support +### How to Deploy + +``` +{ config, pkgs, ... }: +{ + imports = [ + (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/releases/tag/v1.1-rc2") + ]; + + mailserver = { + enable = true; + domain = "example.com"; + login_accounts = { + user1 = { + name = "test"; + hashedPassword = "$6$Mmmx1U68$Twd8acMxqHoqFyfz3SPz1pzjY/D36gayAdpUTFMvfrHQUwObF3acuLz2GYAGFzsjHLEK/dPIv3pCwj3kZ5T2u."; + }; + }; + valiases = { + admin = "user1"; + }; + }; +} +``` + +For a complete list of options, see `default.nix`. + + ### How to Test You can test the setup via `nixops`. After installation, do From 2262f7a67ebaa943a2c2156a0c6bcd9a9d786eaf Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Tue, 17 Oct 2017 11:53:53 +0200 Subject: [PATCH 07/38] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b4f6c5..086746e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ None so far. ### How to Deploy -``` +```nix { config, pkgs, ... }: { imports = [ From d8832b9cf82fd044eb8648f81fb5d80408809e37 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Tue, 17 Oct 2017 17:29:07 +0200 Subject: [PATCH 08/38] Fix error in example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 086746e..cfdc99b 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ None so far. hashedPassword = "$6$Mmmx1U68$Twd8acMxqHoqFyfz3SPz1pzjY/D36gayAdpUTFMvfrHQUwObF3acuLz2GYAGFzsjHLEK/dPIv3pCwj3kZ5T2u."; }; }; - valiases = { + virtualAliases = { admin = "user1"; }; }; From 9f40c38bc69e9cd1835c79bd09cc8fa7ce8443ea Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Wed, 18 Oct 2017 09:09:04 +0200 Subject: [PATCH 09/38] remove variables from vmail user --- mail-server/users.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail-server/users.nix b/mail-server/users.nix index a8cbcc9..2d98d84 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -20,12 +20,12 @@ with config.mailserver; let vmail_user = [{ - name = vmailUserName; + name = "vmail"; isNormalUser = false; uid = vmailUIDStart; home = mailDirectory; createHome = true; - group = vmailGroupName; + group = "vmail"; }]; # accountsToUser :: String -> UserRecord From 6ac36a1092c578661467c08302ee4ca8526eb3c3 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Wed, 18 Oct 2017 09:10:51 +0200 Subject: [PATCH 10/38] changing names --- mail-server/users.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail-server/users.nix b/mail-server/users.nix index 2d98d84..a519985 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -20,12 +20,12 @@ with config.mailserver; let vmail_user = [{ - name = "vmail"; + name = "vmail2"; isNormalUser = false; uid = vmailUIDStart; home = mailDirectory; createHome = true; - group = "vmail"; + group = "vmail2"; }]; # accountsToUser :: String -> UserRecord From bbdcdfc0a7673a70a525946c7cb4f4ce816cfe7a Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Wed, 18 Oct 2017 09:20:44 +0200 Subject: [PATCH 11/38] fix vmail bug --- default.nix | 4 ++-- mail-server/users.nix | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 2a36ae7..6a07f7e 100644 --- a/default.nix +++ b/default.nix @@ -113,7 +113,7 @@ in vmailUserName = mkOption { type = types.str; - default = "vmail"; + default = "virtualMail"; description = '' The user name and group name of the user that owns the directory where all the mail is stored. @@ -122,7 +122,7 @@ in vmailGroupName = mkOption { type = types.str; - default = "vmail"; + default = "virtualMail"; description = '' The user name and group name of the user that owns the directory where all the mail is stored. diff --git a/mail-server/users.nix b/mail-server/users.nix index a519985..a8cbcc9 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -20,12 +20,12 @@ with config.mailserver; let vmail_user = [{ - name = "vmail2"; + name = vmailUserName; isNormalUser = false; uid = vmailUIDStart; home = mailDirectory; createHome = true; - group = "vmail2"; + group = vmailGroupName; }]; # accountsToUser :: String -> UserRecord From e91d237d8109b27214378044c61fcab4f4ee8eeb Mon Sep 17 00:00:00 2001 From: Ruben Maher Date: Sun, 5 Nov 2017 19:12:39 +1030 Subject: [PATCH 12/38] Fix r-raymond/nixos-mailserver#18 --- mail-server/users.nix | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mail-server/users.nix b/mail-server/users.nix index a8cbcc9..e3fad42 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -19,14 +19,14 @@ with config.mailserver; let - vmail_user = [{ + vmail_user = { name = vmailUserName; isNormalUser = false; uid = vmailUIDStart; home = mailDirectory; createHome = true; group = vmailGroupName; - }]; + }; # accountsToUser :: String -> UserRecord accountsToUser = account: { @@ -36,8 +36,9 @@ let inherit (account) hashedPassword; }; - # mail_user :: [ UserRecord ] - mail_user = map accountsToUser (lib.attrValues loginAccounts); + # mail_users :: { [String]: UserRecord } + mail_users = lib.foldl (prev: next: prev // { "${next.name}" = next; }) {} + (map accountsToUser (lib.attrValues loginAccounts)); in { @@ -49,6 +50,8 @@ in }; # define all users - users.extraUsers = vmail_user ++ mail_user; + users.users = mail_users // { + "${vmail_user.name}" = lib.mkForce vmail_user; + }; }; } From 8372b85369d8a8c650086bf662436bc31296fe01 Mon Sep 17 00:00:00 2001 From: Ruben Maher Date: Sun, 5 Nov 2017 19:15:56 +1030 Subject: [PATCH 13/38] users.nix: ensure the group getting its gid set is vmailGroupName --- mail-server/users.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/users.nix b/mail-server/users.nix index a8cbcc9..64c9c8a 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -45,7 +45,7 @@ in config = lib.mkIf enable { # set the vmail gid to a specific value users.groups = { - vmail = { gid = vmailUIDStart; }; + "${vmailGroupName}" = { gid = vmailUIDStart; }; }; # define all users From 8d9881215ba2e2a6e3f0b2a33b7906faafca56fa Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sun, 5 Nov 2017 10:57:26 +0100 Subject: [PATCH 14/38] update contributer --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cfdc99b..33c0132 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ None so far. imports = [ (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/releases/tag/v1.1-rc2") ]; - + mailserver = { enable = true; domain = "example.com"; @@ -135,6 +135,7 @@ openssl s_client -host mail.example.com -port 143 -starttls imap * Special thanks to @Infinisil for the module rewrite * @danbst * @phdoerfler + * @eqyiel ### Credits From 695779926a60453002d6a2fb40ca764a237ec919 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sun, 5 Nov 2017 11:14:39 +0100 Subject: [PATCH 15/38] add test for vmail gid --- tests/intern.nix | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/intern.nix b/tests/intern.nix index d066c24..58c4b75 100644 --- a/tests/intern.nix +++ b/tests/intern.nix @@ -33,6 +33,9 @@ import { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; }; }; + + vmailGroupName = "vmail"; + vmailUIDStart = 5000; }; }; @@ -49,5 +52,9 @@ import { $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"); + }; + ''; } From e4852151efa29583dff78400226084eb59b1662e Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sun, 5 Nov 2017 11:22:58 +0100 Subject: [PATCH 16/38] update README for new release candidate --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33c0132..1b1e6c6 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ None so far. { config, pkgs, ... }: { imports = [ - (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/releases/tag/v1.1-rc2") + (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/releases/tag/v1.1-rc3") ]; mailserver = { From 3d2f41dedca389355d2c4a8c125a4d440c4484f7 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Thu, 9 Nov 2017 08:23:13 +0100 Subject: [PATCH 17/38] jbboehr's fix for #21 --- mail-server/common.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail-server/common.nix b/mail-server/common.nix index 0d15ce7..12d7b96 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -26,7 +26,7 @@ in else if cfg.certificateScheme == 2 then "${cfg.certificateDirectory}/cert-${cfg.domain}.pem" else if cfg.certificateScheme == 3 - then "/var/lib/acme/acme-challenge/${cfg.hostPrefix}.${cfg.domain}/fullchain.pem" + then "/var/lib/acme/${cfg.hostPrefix}.${cfg.domain}/fullchain.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; # key :: PATH @@ -35,6 +35,6 @@ in else if cfg.certificateScheme == 2 then "${cfg.certificateDirectory}/key-${cfg.domain}.pem" else if cfg.certificateScheme == 3 - then "/var/lib/acme/acme-challenge/${cfg.hostPrefix}.${cfg.domain}/privkey.pem" + then "/var/lib/acme/${cfg.hostPrefix}.${cfg.domain}/key.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; } From ebd0f656eda09cb55ca78ee01ce1273f8b953483 Mon Sep 17 00:00:00 2001 From: John Boehr Date: Thu, 9 Nov 2017 13:13:27 -0800 Subject: [PATCH 18/38] Preliminary multi-domain support --- default.nix | 10 ++++++++-- mail-server/nginx.nix | 35 +++++++++++++++++++++++------------ mail-server/postfix.nix | 16 +++++++++------- mail-server/systemd.nix | 2 +- mail-server/users.nix | 2 +- nixops/single-server.nix | 13 +++++++++---- 6 files changed, 51 insertions(+), 27 deletions(-) diff --git a/default.nix b/default.nix index 6a07f7e..f9303a7 100644 --- a/default.nix +++ b/default.nix @@ -28,8 +28,14 @@ in domain = mkOption { type = types.str; - example = "example.com"; - description = "The domain that this mail server serves. So far only one domain is supported"; + example = "[ example.com ]"; + description = "The primary domain that this mail server serves."; + }; + + extraDomains = mkOption { + type = types.listOf types.str; + example = "[ example.com ]"; + description = "Extra domains that this mail server serves."; }; hostPrefix = mkOption { diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 15bb596..71f6c28 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -21,23 +21,34 @@ with (import ./common.nix { inherit config; }); let cfg = config.mailserver; + allDomains = [ cfg.domain ] ++ cfg.extraDomains; + acmeRoot = "/var/lib/acme/acme-challenge"; in { config = with cfg; lib.mkIf (certificateScheme == 3) { - services.nginx = { enable = true; - virtualHosts = { - domain = { - serverName = "${hostPrefix}.${domain}"; - forceSSL = true; - enableACME = true; - locations."/" = { - root = "/var/www"; - }; - acmeRoot = "/var/lib/acme/acme-challenge"; - }; - }; + virtualHosts = genAttrs allDomains (domain: { + serverName = "${hostPrefix}.${domain}"; + forceSSL = true; + enableACME = true; + locations."/" = { + root = "/var/www"; + }; + acmeRoot = acmeRoot; + }); + }; + security.acme.certs."${hostPrefix}.${domain}" = { + # @todo what user/group should this run as? + user = "postfix"; # cfg.user; + group = "postfix"; # lib.mkDefault cfg.group; + domain = "${hostPrefix}.${domain}"; + extraDomains = map (domain: "${hostPrefix}.${domain}") extraDomains; + webroot = acmeRoot; + # @todo should we reload postfix here? + postRun = '' + systemctl reload nginx + ''; }; }; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index fedb53c..ee91da9 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -19,17 +19,19 @@ with (import ./common.nix { inherit config; }); let + inherit (lib.strings) concatStringsSep; cfg = config.mailserver; + allDomains = [ cfg.domain ] ++ cfg.extraDomains; # valiases_postfix :: [ String ] valiases_postfix = map (from: let to = cfg.virtualAliases.${from}; - in "${from}@${cfg.domain} ${to}@${cfg.domain}") + in "${from} ${to}") (builtins.attrNames cfg.virtualAliases); # accountToIdentity :: User -> String - accountToIdentity = account: "${account.name}@${cfg.domain} ${account.name}@${cfg.domain}"; + accountToIdentity = account: "${account.name} ${account.name}"; # vaccounts_identity :: [ String ] vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts); @@ -38,7 +40,7 @@ let valiases_file = builtins.toFile "valias" (lib.concatStringsSep "\n" valiases_postfix); # vhosts_file :: Path - vhosts_file = builtins.toFile "vhosts" cfg.domain; + vhosts_file = builtins.toFile "vhosts" (concatStringsSep ", " allDomains); # vaccounts_file :: Path # see @@ -47,7 +49,7 @@ let # 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 ++ 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/ @@ -67,12 +69,12 @@ in enable = true; networksStyle = "host"; mapFiles."valias" = valiases_file; - mapFiles."vaccounts" = vaccounts_file; + mapFiles."vaccounts" = vaccounts_file; sslCert = certificatePath; sslKey = keyPath; enableSubmission = true; - extraConfig = + extraConfig = '' # Extra Config @@ -116,7 +118,7 @@ in ''; submissionOptions = - { + { smtpd_tls_security_level = "encrypt"; smtpd_sasl_auth_enable = "yes"; smtpd_sasl_type = "dovecot"; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 48f5a5e..b6556a8 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -64,7 +64,7 @@ in # Create certificates and maildir folder systemd.services.postfix = { after = (if (certificateScheme == 3) then [ "nginx.service" ] else []); - preStart = + preStart = '' # Create mail directory and set permissions. See # . diff --git a/mail-server/users.nix b/mail-server/users.nix index c55375c..f49be1f 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -30,7 +30,7 @@ let # accountsToUser :: String -> UserRecord accountsToUser = account: { - name = account.name + "@" + domain; + name = account.name; isNormalUser = false; group = vmailGroupName; inherit (account) hashedPassword; diff --git a/nixops/single-server.nix b/nixops/single-server.nix index 15e9e5e..8072233 100644 --- a/nixops/single-server.nix +++ b/nixops/single-server.nix @@ -11,17 +11,22 @@ mailserver = { enable = true; domain = "example.com"; + extraDomains = [ "example2.com" ]; hostPrefix = "mail"; loginAccounts = { - user1 = { + "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; }; }; virtualAliases = { - info = "user1"; - postmaster = "user1"; - abuse = "user1"; + "user1@example2.com" = "user1@example.com"; + "info@example.com" = "user1@example.com"; + "postmaster@example.com" = "user1@example.com"; + "abuse@example.com" = "user1@example.com"; + "info@example2.com" = "user1@example.com"; + "postmaster@example2.com" = "user1@example.com"; + "abuse@example2.com" = "user1@example.com"; }; }; }; From bbca0bd6783700a7aeefc05d3860a585c7037e06 Mon Sep 17 00:00:00 2001 From: John Boehr Date: Thu, 9 Nov 2017 13:16:06 -0800 Subject: [PATCH 19/38] Fix a few issues with ACME certs --- mail-server/common.nix | 4 ++-- mail-server/nginx.nix | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/mail-server/common.nix b/mail-server/common.nix index 12d7b96..42d0180 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -26,7 +26,7 @@ in else if cfg.certificateScheme == 2 then "${cfg.certificateDirectory}/cert-${cfg.domain}.pem" else if cfg.certificateScheme == 3 - then "/var/lib/acme/${cfg.hostPrefix}.${cfg.domain}/fullchain.pem" + then "/var/lib/acme/mailserver/fullchain.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; # key :: PATH @@ -35,6 +35,6 @@ in else if cfg.certificateScheme == 2 then "${cfg.certificateDirectory}/key-${cfg.domain}.pem" else if cfg.certificateScheme == 3 - then "/var/lib/acme/${cfg.hostPrefix}.${cfg.domain}/key.pem" + then "/var/lib/acme/mailserver/key.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; } diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 71f6c28..52a0bbb 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -20,6 +20,7 @@ with (import ./common.nix { inherit config; }); let + inherit (lib.attrsets) genAttrs; cfg = config.mailserver; allDomains = [ cfg.domain ] ++ cfg.extraDomains; acmeRoot = "/var/lib/acme/acme-challenge"; @@ -38,7 +39,7 @@ in acmeRoot = acmeRoot; }); }; - security.acme.certs."${hostPrefix}.${domain}" = { + security.acme.certs."mailserver" = { # @todo what user/group should this run as? user = "postfix"; # cfg.user; group = "postfix"; # lib.mkDefault cfg.group; From f3727540528b00aa7a1af818441ec41f2dfdb086 Mon Sep 17 00:00:00 2001 From: John Boehr Date: Thu, 9 Nov 2017 14:17:03 -0800 Subject: [PATCH 20/38] Qualify user names --- default.nix | 1 + mail-server/common.nix | 9 ++++++++- mail-server/dovecot.nix | 2 +- mail-server/nginx.nix | 13 +++++-------- mail-server/postfix.nix | 6 +++--- mail-server/users.nix | 6 ++++-- nixops/single-server.nix | 16 ++++++++-------- 7 files changed, 30 insertions(+), 23 deletions(-) diff --git a/default.nix b/default.nix index f9303a7..381a0d1 100644 --- a/default.nix +++ b/default.nix @@ -35,6 +35,7 @@ in extraDomains = mkOption { type = types.listOf types.str; example = "[ example.com ]"; + default = []; description = "Extra domains that this mail server serves."; }; diff --git a/mail-server/common.nix b/mail-server/common.nix index 42d0180..f491911 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -14,10 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config }: +{ config, lib }: let cfg = config.mailserver; + inherit (lib.strings) stringToCharacters; in { # cert :: PATH @@ -37,4 +38,10 @@ in else if cfg.certificateScheme == 3 then "/var/lib/acme/mailserver/key.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; + + # appends cfg.domain to argument if it does not contain "@" + qualifyUser = user: ( + if (builtins.any (c: c == "@") (stringToCharacters user)) + then user + else "${user}@${cfg.domain}"); } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 7ccaab1..fb8330b 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -16,7 +16,7 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config; }); +with (import ./common.nix { inherit config lib; }); let cfg = config.mailserver; diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 52a0bbb..9eeace4 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -26,11 +26,11 @@ let acmeRoot = "/var/lib/acme/acme-challenge"; in { - config = with cfg; lib.mkIf (certificateScheme == 3) { + config = lib.mkIf (cfg.certificateScheme == 3) { services.nginx = { enable = true; - virtualHosts = genAttrs allDomains (domain: { - serverName = "${hostPrefix}.${domain}"; + virtualHosts = genAttrs (map (domain: "${cfg.hostPrefix}.${domain}") allDomains) (domain: { + serverName = "${domain}"; forceSSL = true; enableACME = true; locations."/" = { @@ -40,11 +40,8 @@ in }); }; security.acme.certs."mailserver" = { - # @todo what user/group should this run as? - user = "postfix"; # cfg.user; - group = "postfix"; # lib.mkDefault cfg.group; - domain = "${hostPrefix}.${domain}"; - extraDomains = map (domain: "${hostPrefix}.${domain}") extraDomains; + domain = "${cfg.hostPrefix}.${cfg.domain}"; + extraDomains = genAttrs (map (domain: "${cfg.hostPrefix}.${domain}") cfg.extraDomains) (domain: null); webroot = acmeRoot; # @todo should we reload postfix here? postRun = '' diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index ee91da9..a03e366 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -16,7 +16,7 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config; }); +with (import ./common.nix { inherit config lib; }); let inherit (lib.strings) concatStringsSep; @@ -27,11 +27,11 @@ let valiases_postfix = map (from: let to = cfg.virtualAliases.${from}; - in "${from} ${to}") + in "${qualifyUser from} ${qualifyUser to}") (builtins.attrNames cfg.virtualAliases); # accountToIdentity :: User -> String - accountToIdentity = account: "${account.name} ${account.name}"; + accountToIdentity = account: "${qualifyUser account.name} ${qualifyUser account.name}"; # vaccounts_identity :: [ String ] vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts); diff --git a/mail-server/users.nix b/mail-server/users.nix index f49be1f..d813101 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -19,6 +19,8 @@ with config.mailserver; let + qualifyUser = (import ./common.nix { inherit config lib; }).qualifyUser; + vmail_user = { name = vmailUserName; isNormalUser = false; @@ -30,14 +32,14 @@ let # accountsToUser :: String -> UserRecord accountsToUser = account: { - name = account.name; + name = (qualifyUser account.name); isNormalUser = false; group = vmailGroupName; inherit (account) hashedPassword; }; # mail_users :: { [String]: UserRecord } - mail_users = lib.foldl (prev: next: prev // { "${next.name}" = next; }) {} + mail_users = lib.foldl (prev: next: prev // { "${qualifyUser next.name}" = next; }) {} (map accountsToUser (lib.attrValues loginAccounts)); in diff --git a/nixops/single-server.nix b/nixops/single-server.nix index 8072233..af909d1 100644 --- a/nixops/single-server.nix +++ b/nixops/single-server.nix @@ -15,18 +15,18 @@ hostPrefix = "mail"; loginAccounts = { - "user1@example.com" = { + "user1" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; }; }; virtualAliases = { - "user1@example2.com" = "user1@example.com"; - "info@example.com" = "user1@example.com"; - "postmaster@example.com" = "user1@example.com"; - "abuse@example.com" = "user1@example.com"; - "info@example2.com" = "user1@example.com"; - "postmaster@example2.com" = "user1@example.com"; - "abuse@example2.com" = "user1@example.com"; + "info" = "user1"; + "postmaster" = "user1"; + "abuse" = "user1"; + "user1@example2.com" = "user1"; + "info@example2.com" = "user1"; + "postmaster@example2.com" = "user1"; + "abuse@example2.com" = "user1"; }; }; }; From f79080d2d9f05ae71899dd0cc8a229efcbc0480e Mon Sep 17 00:00:00 2001 From: John Boehr Date: Thu, 9 Nov 2017 14:17:18 -0800 Subject: [PATCH 21/38] Add .editorconfig --- .editorconfig | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2d46cdd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[Makefile] +indent_style = tab +indent_size = 8 From a745abaa8ebcbaeb225243e54487a4816eeccdf8 Mon Sep 17 00:00:00 2001 From: John Boehr Date: Thu, 9 Nov 2017 14:32:33 -0800 Subject: [PATCH 22/38] Reload postfix and dovecot2 --- mail-server/nginx.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index 9eeace4..f487d7a 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -46,6 +46,8 @@ in # @todo should we reload postfix here? postRun = '' systemctl reload nginx + systemctl reload postfix + systemctl reload dovecot2 ''; }; }; From 276450ff64254bbc8290b7a47a8a47efb7180061 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Fri, 10 Nov 2017 16:57:11 +0100 Subject: [PATCH 23/38] begin to write a small guide on how to set up a mail server --- README.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/README.md b/README.md index 1b1e6c6..2f00565 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,40 @@ openssl s_client -host mail.example.com -port 143 -starttls imap ``` +## How to Set Up a 10/10 Mail Server +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`. + +### Fully Qualified Domain Name +No matter how many domains you want to serve on your mail server, you need to +settle on 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`. + +After you settled on a FQDN (we will assume `mx.example.com` henceforth) you +need to + * Set a DNS entry on your domain to point to the IP of the server. For this + add a DNS record such as + + | Name (Subdomain) | TTL | Type | Priority | Value | + | ---------------- | ----- | ---- | -------- | ----------------- | + | mx.example.com | 10800 | A | | `xxx.xxx.xxx.xxx` | + + to your domain, where `xxx.xxx.xxx.xxx` is the IP of your server. + + * Set a `rDNS` (reverse DNS) entry for your FQDN. You need to do so wherever + you have rented your server. Make sure that `xxx.xxx.xxx.xxx` resolves to + `mx.example.com`. + + +### Spf record + +TODO + +### DKIM signature + +TODO + ## A Complete Mail Server Without Moving Parts ### Used Technologies From 4a97d9448a9907659f1e575d58960369a9c4e8c4 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Fri, 10 Nov 2017 16:58:52 +0100 Subject: [PATCH 24/38] add mx recorde to todo list --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 2f00565..671b5b7 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,10 @@ need to `mx.example.com`. +### MX Record + +TODO + ### Spf record TODO From 64ca3391d6254eedb9948738c1c84290e2220388 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Fri, 10 Nov 2017 17:16:21 +0100 Subject: [PATCH 25/38] give a rough overview of the steps --- README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 671b5b7..d5109e5 100644 --- a/README.md +++ b/README.md @@ -130,15 +130,23 @@ need to ### MX Record -TODO + | Name (Subdomain) | TTL | Type | Priority | Value | + | ---------------- | ----- | ---- | -------- | ----------------- | + | domain1.com | | MX | 10 | mx.exmaple.com | ### Spf record -TODO + | Name (Subdomain) | TTL | Type | Priority | Value | + | ---------------- | ----- | ---- | -------- | ----------------- | + | domain1.com | 10800 | TXT | | `v=spf1 ip4:xxx.xxx.xxx.xxx -all` | ### DKIM signature -TODO + | Name (Subdomain) | TTL | Type | Priority | Value | + | ---------------- | ----- | ---- | -------- | ----------------- | + | dkim._domainkey.domain1.com | 10800 | TXT | | `v=DKIM1; p=yyyyyyyyyyyy` | + +where `yyyyyyyyyyyy` is the `DKIM` signature ## A Complete Mail Server Without Moving Parts From ddfb2e621077378f11e45b2b81e6e0e91974d47c Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Fri, 10 Nov 2017 17:17:53 +0100 Subject: [PATCH 26/38] fix tables --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d5109e5..fe3b31d 100644 --- a/README.md +++ b/README.md @@ -130,21 +130,21 @@ need to ### MX Record - | Name (Subdomain) | TTL | Type | Priority | Value | - | ---------------- | ----- | ---- | -------- | ----------------- | - | domain1.com | | MX | 10 | mx.exmaple.com | +| Name (Subdomain) | TTL | Type | Priority | Value | +| ---------------- | ----- | ---- | -------- | ----------------- | +| domain1.com | | MX | 10 | mx.exmaple.com | ### Spf record - | Name (Subdomain) | TTL | Type | Priority | Value | - | ---------------- | ----- | ---- | -------- | ----------------- | - | domain1.com | 10800 | TXT | | `v=spf1 ip4:xxx.xxx.xxx.xxx -all` | +| Name (Subdomain) | TTL | Type | Priority | Value | +| ---------------- | ----- | ---- | -------- | ----------------- | +| domain1.com | 10800 | TXT | | `v=spf1 ip4:xxx.xxx.xxx.xxx -all` | ### DKIM signature - | Name (Subdomain) | TTL | Type | Priority | Value | - | ---------------- | ----- | ---- | -------- | ----------------- | - | dkim._domainkey.domain1.com | 10800 | TXT | | `v=DKIM1; p=yyyyyyyyyyyy` | +| Name (Subdomain) | TTL | Type | Priority | Value | +| ---------------- | ----- | ---- | -------- | ----------------- | +| dkim._domainkey.domain1.com | 10800 | TXT | | `v=DKIM1; p=yyyyyyyyyyyy` | where `yyyyyyyyyyyy` is the `DKIM` signature From 16fb41de0167face01d63ae02f86d96768b6d493 Mon Sep 17 00:00:00 2001 From: John Boehr Date: Sat, 11 Nov 2017 09:44:45 +0000 Subject: [PATCH 27/38] Change domain to fqdn and extraDomains to domains --- default.nix | 17 ++++------------- mail-server/common.nix | 17 +++++------------ mail-server/dovecot.nix | 2 +- mail-server/nginx.nix | 30 ++++++++++++------------------ mail-server/postfix.nix | 9 ++++----- mail-server/services.nix | 4 ++-- mail-server/systemd.nix | 8 ++++---- mail-server/users.nix | 6 ++---- nixops/single-server.nix | 22 ++++++++++------------ tests/extern.nix | 8 ++++---- tests/intern.nix | 6 +++--- 11 files changed, 51 insertions(+), 78 deletions(-) diff --git a/default.nix b/default.nix index 381a0d1..9b7f6fe 100644 --- a/default.nix +++ b/default.nix @@ -26,26 +26,17 @@ in options.mailserver = { enable = mkEnableOption "nixos-mailserver"; - domain = mkOption { + fqdn = mkOption { type = types.str; example = "[ example.com ]"; - description = "The primary domain that this mail server serves."; + description = "The fully qualified domain name of the mail server."; }; - extraDomains = mkOption { + domains = mkOption { type = types.listOf types.str; example = "[ example.com ]"; default = []; - description = "Extra domains that this mail server serves."; - }; - - hostPrefix = mkOption { - type = types.str; - default = "mail"; - description = '' - The prefix of the FQDN of the server. In this example the FQDN of the server - is given by 'mail.example.com' - ''; + description = "The domains that this mail server serves."; }; loginAccounts = mkOption { diff --git a/mail-server/common.nix b/mail-server/common.nix index f491911..910b5c2 100644 --- a/mail-server/common.nix +++ b/mail-server/common.nix @@ -14,34 +14,27 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see -{ config, lib }: +{ config }: let cfg = config.mailserver; - inherit (lib.strings) stringToCharacters; in { # cert :: PATH certificatePath = if cfg.certificateScheme == 1 then cfg.certificateFile else if cfg.certificateScheme == 2 - then "${cfg.certificateDirectory}/cert-${cfg.domain}.pem" + then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" else if cfg.certificateScheme == 3 - then "/var/lib/acme/mailserver/fullchain.pem" + 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 == 1 then cfg.keyFile else if cfg.certificateScheme == 2 - then "${cfg.certificateDirectory}/key-${cfg.domain}.pem" + then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" else if cfg.certificateScheme == 3 - then "/var/lib/acme/mailserver/key.pem" + then "/var/lib/acme/${cfg.fqdn}/key.pem" else throw "Error: Certificate Scheme must be in { 1, 2, 3 }"; - - # appends cfg.domain to argument if it does not contain "@" - qualifyUser = user: ( - if (builtins.any (c: c == "@") (stringToCharacters user)) - then user - else "${user}@${cfg.domain}"); } diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index fb8330b..7ccaab1 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -16,7 +16,7 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config lib; }); +with (import ./common.nix { inherit config; }); let cfg = config.mailserver; diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix index f487d7a..0ba4a54 100644 --- a/mail-server/nginx.nix +++ b/mail-server/nginx.nix @@ -20,35 +20,29 @@ with (import ./common.nix { inherit config; }); let - inherit (lib.attrsets) genAttrs; cfg = config.mailserver; - allDomains = [ cfg.domain ] ++ cfg.extraDomains; acmeRoot = "/var/lib/acme/acme-challenge"; in { config = lib.mkIf (cfg.certificateScheme == 3) { services.nginx = { enable = true; - virtualHosts = genAttrs (map (domain: "${cfg.hostPrefix}.${domain}") allDomains) (domain: { - serverName = "${domain}"; - forceSSL = true; - enableACME = true; - locations."/" = { - root = "/var/www"; - }; - acmeRoot = acmeRoot; - }); + virtualHosts."${cfg.fqdn}" = { + serverName = cfg.fqdn; + forceSSL = true; + enableACME = true; + acmeRoot = acmeRoot; + }; }; - security.acme.certs."mailserver" = { - domain = "${cfg.hostPrefix}.${cfg.domain}"; - extraDomains = genAttrs (map (domain: "${cfg.hostPrefix}.${domain}") cfg.extraDomains) (domain: null); - webroot = acmeRoot; - # @todo should we reload postfix here? - postRun = '' + security.acme.certs."${cfg.fqdn}".postRun = #{ + # domain = "${cfg.fqdn}"; +# webroot = acmeRoot; +# postRun = + '' systemctl reload nginx systemctl reload postfix systemctl reload dovecot2 ''; - }; +# }; }; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index a03e366..a57e63d 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -16,22 +16,21 @@ { config, pkgs, lib, ... }: -with (import ./common.nix { inherit config lib; }); +with (import ./common.nix { inherit config; }); let inherit (lib.strings) concatStringsSep; cfg = config.mailserver; - allDomains = [ cfg.domain ] ++ cfg.extraDomains; # valiases_postfix :: [ String ] valiases_postfix = map (from: let to = cfg.virtualAliases.${from}; - in "${qualifyUser from} ${qualifyUser to}") + in "${from} ${to}") (builtins.attrNames cfg.virtualAliases); # accountToIdentity :: User -> String - accountToIdentity = account: "${qualifyUser account.name} ${qualifyUser account.name}"; + accountToIdentity = account: "${account.name} ${account.name}"; # vaccounts_identity :: [ String ] vaccounts_identity = map accountToIdentity (lib.attrValues cfg.loginAccounts); @@ -40,7 +39,7 @@ let valiases_file = builtins.toFile "valias" (lib.concatStringsSep "\n" valiases_postfix); # vhosts_file :: Path - vhosts_file = builtins.toFile "vhosts" (concatStringsSep ", " allDomains); + vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains); # vaccounts_file :: Path # see diff --git a/mail-server/services.nix b/mail-server/services.nix index 2cebdaf..41d2bb3 100644 --- a/mail-server/services.nix +++ b/mail-server/services.nix @@ -24,14 +24,14 @@ let cert = if cfg.certificateScheme == 1 then cfg.certificateFile else if cfg.certificateScheme == 2 - then "${cfg.certificateDirectory}/cert-${cfg.domain}.pem" + 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.domain}.pem" + then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" else ""; in { diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index b6556a8..ecfbbde 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -23,10 +23,10 @@ let '' # Create certificates if they do not exist yet dir="${cfg.certificateDirectory}" - fqdn="${cfg.hostPrefix}.${cfg.domain}" + fqdn="${cfg.fqdn}" case $fqdn in /*) fqdn=$(cat "$fqdn");; esac - key="''${dir}/key-${cfg.domain}.pem"; - cert="''${dir}/cert-${cfg.domain}.pem"; + key="''${dir}/key-${cfg.fqdn}.pem"; + cert="''${dir}/cert-${cfg.fqdn}.pem"; if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ] then @@ -50,7 +50,7 @@ let then ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ - -d ${cfg.domain} \ + -d ${cfg.fqdn} \ --directory="${cfg.dkimKeyDirectory}" chown rmilter:rmilter "${dkim_key}" fi diff --git a/mail-server/users.nix b/mail-server/users.nix index d813101..f49be1f 100644 --- a/mail-server/users.nix +++ b/mail-server/users.nix @@ -19,8 +19,6 @@ with config.mailserver; let - qualifyUser = (import ./common.nix { inherit config lib; }).qualifyUser; - vmail_user = { name = vmailUserName; isNormalUser = false; @@ -32,14 +30,14 @@ let # accountsToUser :: String -> UserRecord accountsToUser = account: { - name = (qualifyUser account.name); + name = account.name; isNormalUser = false; group = vmailGroupName; inherit (account) hashedPassword; }; # mail_users :: { [String]: UserRecord } - mail_users = lib.foldl (prev: next: prev // { "${qualifyUser next.name}" = next; }) {} + mail_users = lib.foldl (prev: next: prev // { "${next.name}" = next; }) {} (map accountsToUser (lib.attrValues loginAccounts)); in diff --git a/nixops/single-server.nix b/nixops/single-server.nix index af909d1..abcd671 100644 --- a/nixops/single-server.nix +++ b/nixops/single-server.nix @@ -10,23 +10,21 @@ mailserver = { enable = true; - domain = "example.com"; - extraDomains = [ "example2.com" ]; - - hostPrefix = "mail"; + fqdn = "mail.example.com"; + domains = [ "example.com", "example2.com" ]; loginAccounts = { - "user1" = { + "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; }; }; virtualAliases = { - "info" = "user1"; - "postmaster" = "user1"; - "abuse" = "user1"; - "user1@example2.com" = "user1"; - "info@example2.com" = "user1"; - "postmaster@example2.com" = "user1"; - "abuse@example2.com" = "user1"; + "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/tests/extern.nix b/tests/extern.nix index f98f10e..03c53c6 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -25,14 +25,14 @@ import { mailserver = { enable = true; - domain = "example.com"; + fqdn = "mail.example.com"; + domains = [ "example.com" ]; - hostPrefix = "mail"; loginAccounts = { - user1 = { + "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; }; - user2 = { + "user2@example.com" = { hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; }; }; diff --git a/tests/intern.nix b/tests/intern.nix index 58c4b75..bcfce2a 100644 --- a/tests/intern.nix +++ b/tests/intern.nix @@ -25,11 +25,11 @@ import { mailserver = { enable = true; - domain = "example.com"; + fqdn = "mail.example.com"; + domains = [ "example.com" ]; - hostPrefix = "mail"; loginAccounts = { - user1 = { + "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; }; }; From d249edc13524efc2ae00b1f24f5e27671dd4721a Mon Sep 17 00:00:00 2001 From: John Boehr Date: Sat, 11 Nov 2017 09:47:25 +0000 Subject: [PATCH 28/38] Remove makefile section from editorconfig --- .editorconfig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2d46cdd..86a63dc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,7 +7,3 @@ indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true - -[Makefile] -indent_style = tab -indent_size = 8 From 76fc3d67ae855de80fc9523add5bc5a41ec8bc5d Mon Sep 17 00:00:00 2001 From: John Boehr Date: Sat, 11 Nov 2017 09:53:35 +0000 Subject: [PATCH 29/38] Fix syntax error in sample nixops config --- nixops/single-server.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nixops/single-server.nix b/nixops/single-server.nix index abcd671..1976809 100644 --- a/nixops/single-server.nix +++ b/nixops/single-server.nix @@ -11,7 +11,7 @@ mailserver = { enable = true; fqdn = "mail.example.com"; - domains = [ "example.com", "example2.com" ]; + domains = [ "example.com" "example2.com" ]; loginAccounts = { "user1@example.com" = { hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; From f8309d5b7636c6fb2d7b538335d8c169105ba1d9 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 11 Nov 2017 14:12:16 +0100 Subject: [PATCH 30/38] add second domain to test --- tests/extern.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/extern.nix b/tests/extern.nix index 03c53c6..6b9feae 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -26,7 +26,7 @@ import { mailserver = { enable = true; fqdn = "mail.example.com"; - domains = [ "example.com" ]; + domains = [ "example.com" "example2.com" ]; loginAccounts = { "user1@example.com" = { From b89d6e7b272ecb1f279d90786bda09c43de707b2 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 11 Nov 2017 14:19:05 +0100 Subject: [PATCH 31/38] fix fqdn in smtp banner --- mail-server/postfix.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index a57e63d..4b1cf45 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -77,7 +77,7 @@ in '' # Extra Config - smtpd_banner = $myhostname ESMTP NO UCE + smtpd_banner = ${fqdn} ESMTP NO UCE disable_vrfy_command = yes message_size_limit = 20971520 From d905be86d5b98949e08b96b2fde38f00464e5bb1 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 11 Nov 2017 16:05:49 +0100 Subject: [PATCH 32/38] fix multidomain dkim signing fixes #24 --- mail-server/rmilter.nix | 2 ++ mail-server/systemd.nix | 27 ++++++++++++++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/mail-server/rmilter.nix b/mail-server/rmilter.nix index da3df2c..91a49fa 100644 --- a/mail-server/rmilter.nix +++ b/mail-server/rmilter.nix @@ -28,6 +28,8 @@ let '' 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 { diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index ecfbbde..0f98b7d 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -38,22 +38,31 @@ let '' else ""; - dkim_key = "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private"; - dkim_txt = "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt"; + 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}" - if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] - then + ${createAllCerts} - ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ - -d ${cfg.fqdn} \ - --directory="${cfg.dkimKeyDirectory}" - chown rmilter:rmilter "${dkim_key}" - fi + chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}" ''; in { From db3812c3290586eb7b2836f880a8213911922bc1 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 11 Nov 2017 16:07:03 +0100 Subject: [PATCH 33/38] add test for multidomain dkim --- tests/extern.nix | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/extern.nix b/tests/extern.nix index 6b9feae..7820df4 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -35,6 +35,9 @@ import { "user2@example.com" = { hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; }; + "user@example2.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + }; }; enableImap = true; @@ -68,6 +71,13 @@ import { 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 ''; email1 = '' @@ -82,6 +92,21 @@ import { 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 + ''; in '' startAll; @@ -121,5 +146,19 @@ import { # 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 "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 >&2"); + $client->succeed("cat ~/mail/* >&2"); + # make sure it is dkim signed + $client->succeed("grep DKIM ~/mail/*"); + }; ''; } From 4708654ef906234526fe47ecd9f9e9bcc77d8ed8 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 11 Nov 2017 16:07:48 +0100 Subject: [PATCH 34/38] remove output from fetchmail in test --- tests/extern.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/extern.nix b/tests/extern.nix index 7820df4..0b928d7 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -155,7 +155,7 @@ import { $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 >&2"); + $client->succeed("fetchmail -v"); $client->succeed("cat ~/mail/* >&2"); # make sure it is dkim signed $client->succeed("grep DKIM ~/mail/*"); From dcd73f59ee7aab28d0714b4fb59321c4e8f15511 Mon Sep 17 00:00:00 2001 From: Robin Raymond Date: Sat, 11 Nov 2017 16:15:30 +0100 Subject: [PATCH 35/38] update readme for v2.0 --- README.md | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index fe3b31d..be26b02 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ None so far. [Latest Release Candidate](https://github.com/r-raymond/nixos-mailserver/releases/latest) ## Features -### v1.1 +### v2.0 + * [x] Multiple Domains * Postfix MTA - [x] smtp on port 25 - [x] submission port 587 @@ -22,6 +23,7 @@ None so far. * Certificates - [x] manual certificates - [x] on the fly creation + - [x] Let's Encrypt * Spam Filtering - [x] via rspamd - [x] hard coded sieve script to move spam into Junk folder @@ -33,17 +35,13 @@ None so far. - [x] declarative user management - [x] declarative password management - -### v1.2 - * Certificates - - [x] Let's Encrypt +### In the future * Sieves - [ ] Allow user defined sieve scripts * User Aliases - [ ] More complete alias support - -### v2.0 - * [ ] Multiple Domains + * DKIM Signing + - [ ] Allow a per domain selector ### Changelog @@ -51,26 +49,37 @@ None so far. * Changed structure to Nix Modules * Adds Sieve support +#### v1.1 -> v2.0 + * rename domain to fqdn, seperate fqdn from domains + * multi domain support + ### How to Deploy ```nix { config, pkgs, ... }: { imports = [ - (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/releases/tag/v1.1-rc3") + (builtins.fetchTarball "https://github.com/r-raymond/nixos-mailserver/releases/tag/v2.0-rc1") ]; mailserver = { enable = true; - domain = "example.com"; - login_accounts = { - user1 = { - name = "test"; - hashedPassword = "$6$Mmmx1U68$Twd8acMxqHoqFyfz3SPz1pzjY/D36gayAdpUTFMvfrHQUwObF3acuLz2GYAGFzsjHLEK/dPIv3pCwj3kZ5T2u."; - }; + fqdn = "mail.example.com"; + domains = [ "example.com" "example2.com" ]; + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + }; }; virtualAliases = { - admin = "user1"; + # address = forward address; + "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"; }; }; } @@ -162,7 +171,7 @@ where `yyyyyyyyyyyy` is the `DKIM` signature * Pam ### Features - * one domain + * unlimited domain * unlimited mail accounts * unlimited aliases for every mail account * spam and virus checking @@ -179,6 +188,7 @@ where `yyyyyyyyyyyy` is the `DKIM` signature ## Contributors * Special thanks to @Infinisil for the module rewrite + * Special thanks to @jbboehr for multidomain implementation * @danbst * @phdoerfler * @eqyiel From f9d59450324db04e90863b729601d9767adc7ddc Mon Sep 17 00:00:00 2001 From: Ruben Maher Date: Sun, 12 Nov 2017 12:29:53 +1030 Subject: [PATCH 36/38] nixos-mailserver/default.nix: fix examples --- default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 9b7f6fe..d5cb669 100644 --- a/default.nix +++ b/default.nix @@ -28,13 +28,13 @@ in fqdn = mkOption { type = types.str; - example = "[ example.com ]"; + example = "example.com"; description = "The fully qualified domain name of the mail server."; }; domains = mkOption { type = types.listOf types.str; - example = "[ example.com ]"; + example = [ "example.com" ]; default = []; description = "The domains that this mail server serves."; }; From 5047c2982f07dd581b78261fbfe792b83c71da7d Mon Sep 17 00:00:00 2001 From: Ruben Maher Date: Mon, 13 Nov 2017 09:29:29 +1030 Subject: [PATCH 37/38] default.nix: add options to open ports 993 (IMAPS) and 995 (POP3S) Dovecot is already configured to serve IMAPS on port 993 and POP3S on port 995. --- default.nix | 25 ++++++++++++++++++++----- mail-server/networking.nix | 4 +++- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/default.nix b/default.nix index d5cb669..4de34f5 100644 --- a/default.nix +++ b/default.nix @@ -184,7 +184,7 @@ in default = true; description = '' Whether to enable imap / pop3. Both variants are only supported in the - (sane) startTLS configuration. (TODO: Allow SSL ports). The ports are + (sane) startTLS configuration. The ports are 110 - Pop3 143 - IMAP @@ -192,12 +192,21 @@ in ''; }; + enableImapSsl = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable IMAPS, setting this option to true will open port 993 + in the firewall. + ''; + }; + enablePop3 = mkOption { type = types.bool; default = false; description = '' - Whether to enable POP3. Both variants are only supported in the - (sane) startTLS configuration. (TODO: Allow SSL ports). The ports are + Whether to enable POP3. Both variants are only supported in the (sane) + startTLS configuration. The ports are 110 - Pop3 143 - IMAP @@ -205,8 +214,14 @@ in ''; }; - # imapSsl = mkOption {} #< TODO - # pop3Ssl = mkOption {} #< TODO + enablePop3Ssl = mkOption { + type = types.bool; + default = false; + description = '' + Whether to enable POP3S, setting this option to true will open port 995 + in the firewall. + ''; + }; virusScanning = mkOption { type = types.bool; diff --git a/mail-server/networking.nix b/mail-server/networking.nix index 4a685f5..851a1f0 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -25,7 +25,9 @@ in networking.firewall = { allowedTCPPorts = [ 25 587 ] ++ (if enableImap then [ 143 ] else []) - ++ (if enablePop3 then [ 110 ] else []); + ++ (if enableImapSsl then [ 993 ] else []) + ++ (if enablePop3 then [ 110 ] else []) + ++ (if enablePop3Ssl then [ 995 ] else []); }; }; } From 7b3e33c49cb543d6a21cf846aae7103ef118d2d8 Mon Sep 17 00:00:00 2001 From: Ruben Maher Date: Mon, 13 Nov 2017 20:03:19 +1030 Subject: [PATCH 38/38] mail-server/networking.nix: make use of use lib.optional --- mail-server/networking.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mail-server/networking.nix b/mail-server/networking.nix index 851a1f0..f9b3336 100644 --- a/mail-server/networking.nix +++ b/mail-server/networking.nix @@ -24,10 +24,10 @@ in networking.firewall = { allowedTCPPorts = [ 25 587 ] - ++ (if enableImap then [ 143 ] else []) - ++ (if enableImapSsl then [ 993 ] else []) - ++ (if enablePop3 then [ 110 ] else []) - ++ (if enablePop3Ssl then [ 995 ] else []); + ++ lib.optional enableImap 143 + ++ lib.optional enableImapSsl 993 + ++ lib.optional enablePop3 110 + ++ lib.optional enablePop3Ssl 995; }; }; }