Self-host your own digital island

unvendored nixos-mailserver

+136 -1
flake.lock
···
{
"nodes": {
+
"blobs": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1604995301,
+
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
+
"owner": "simple-nixos-mailserver",
+
"repo": "blobs",
+
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
+
"type": "gitlab"
+
},
+
"original": {
+
"owner": "simple-nixos-mailserver",
+
"repo": "blobs",
+
"type": "gitlab"
+
}
+
},
+
"flake-compat": {
+
"flake": false,
+
"locked": {
+
"lastModified": 1696426674,
+
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+
"type": "github"
+
},
+
"original": {
+
"owner": "edolstra",
+
"repo": "flake-compat",
+
"type": "github"
+
}
+
},
+
"nixos-mailserver": {
+
"inputs": {
+
"blobs": "blobs",
+
"flake-compat": "flake-compat",
+
"nixpkgs": "nixpkgs",
+
"nixpkgs-23_05": "nixpkgs-23_05",
+
"nixpkgs-23_11": "nixpkgs-23_11",
+
"utils": "utils"
+
},
+
"locked": {
+
"lastModified": 1711069052,
+
"narHash": "sha256-QScBLiWRDmrOhG4/jCrdZTNe8zQdf6gzSZocAYOcG3U=",
+
"owner": "RyanGibb",
+
"repo": "nixos-mailserver",
+
"rev": "435c3a167c52ebe443f6ed1ba3412331ae566d05",
+
"type": "github"
+
},
+
"original": {
+
"owner": "RyanGibb",
+
"ref": "fork-23.11",
+
"repo": "nixos-mailserver",
+
"type": "github"
+
}
+
},
"nixpkgs": {
"locked": {
+
"lastModified": 1709703039,
+
"narHash": "sha256-6hqgQ8OK6gsMu1VtcGKBxKQInRLHtzulDo9Z5jxHEFY=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "9df3e30ce24fd28c7b3e2de0d986769db5d6225d",
+
"type": "github"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"ref": "nixos-unstable",
+
"type": "indirect"
+
}
+
},
+
"nixpkgs-23_05": {
+
"locked": {
+
"lastModified": 1704290814,
+
"narHash": "sha256-LWvKHp7kGxk/GEtlrGYV68qIvPHkU9iToomNFGagixU=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "70bdadeb94ffc8806c0570eb5c2695ad29f0e421",
+
"type": "github"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"ref": "nixos-23.05",
+
"type": "indirect"
+
}
+
},
+
"nixpkgs-23_11": {
+
"locked": {
+
"lastModified": 1710951922,
+
"narHash": "sha256-FOOBJ3DQenLpTNdxMHR2CpGZmYuctb92gF0lpiirZ30=",
+
"owner": "NixOS",
+
"repo": "nixpkgs",
+
"rev": "f091af045dff8347d66d186a62d42aceff159456",
+
"type": "github"
+
},
+
"original": {
+
"id": "nixpkgs",
+
"ref": "nixos-23.11",
+
"type": "indirect"
+
}
+
},
+
"nixpkgs_2": {
+
"locked": {
"lastModified": 1710272261,
"narHash": "sha256-g0bDwXFmTE7uGDOs9HcJsfLFhH7fOsASbAuOzDC+fhQ=",
"owner": "nixos",
···
},
"root": {
"inputs": {
-
"nixpkgs": "nixpkgs"
+
"nixos-mailserver": "nixos-mailserver",
+
"nixpkgs": "nixpkgs_2"
+
}
+
},
+
"systems": {
+
"locked": {
+
"lastModified": 1681028828,
+
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+
"owner": "nix-systems",
+
"repo": "default",
+
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+
"type": "github"
+
},
+
"original": {
+
"owner": "nix-systems",
+
"repo": "default",
+
"type": "github"
+
}
+
},
+
"utils": {
+
"inputs": {
+
"systems": "systems"
+
},
+
"locked": {
+
"lastModified": 1709126324,
+
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
+
"type": "github"
+
},
+
"original": {
+
"owner": "numtide",
+
"repo": "flake-utils",
+
"type": "github"
}
}
},
+7 -3
flake.nix
···
{
-
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+
inputs = {
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
+
nixos-mailserver.url = "github:RyanGibb/nixos-mailserver/fork-23.11";
+
};
-
outputs = { self, nixpkgs, ... }: rec {
+
outputs = { self, nixpkgs, nixos-mailserver, ... }: rec {
packages = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
-
manpage = import ./man { inherit pkgs system; };
+
manpage = import ./man { inherit pkgs system nixos-mailserver; };
});
nixosModules.default = {
imports = [
./modules/default.nix
+
nixos-mailserver.nixosModule
({ pkgs, config, ... }: {
nixpkgs.overlays = [ (final: prev: {
mautrix-meta = (prev.callPackage ./pkgs/mautrix-meta.nix { });
+2 -1
man/default.nix
···
-
{ pkgs, system, ... }:
+
{ pkgs, system, nixos-mailserver, ... }:
with pkgs;
let
···
inherit system;
modules = [
../modules/default.nix
+
nixos-mailserver
];
};
in pkgs.nixosOptionsDoc {
+1 -3
modules/default.nix
···
{
imports = [
./services/dns/default.nix
-
./mailserver/default.nix
./mastodon.nix
./mailserver.nix
./gitea.nix
···
};
config = {
-
# install manpage
+
# TODO install manpage
environment.systemPackages = [
-
(import ../man/default.nix { inherit pkgs; system = config.nixpkgs.hostPlatform.system; })
];
security.acme.defaults.email = "${config.eilean.username}@${config.networking.domain}";
networking.firewall.allowedTCPPorts = mkIf config.services.nginx.enable [
+8 -1
modules/mailserver.nix
···
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
# down nginx and opens port 80.
-
certificateScheme = 3;
+
certificateScheme = "acme-nginx";
localDnsResolver = false;
};
···
services.nginx.virtualHosts."${config.mailserver.fqdn}".extraConfig = ''
return 301 $scheme://${domain}$request_uri;
'';
+
+
services.postfix.config = {
+
smtpd_tls_protocols = mkForce "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtp_tls_protocols = mkForce "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtpd_tls_mandatory_protocols = mkForce "TLSv1.3, !TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
smtp_tls_mandatory_protocols = mkForce "TLSv1.3, !TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
+
};
eilean.dns.enable = true;
eilean.services.dns.zones.${config.networking.domain}.records = [
-78
modules/mailserver/borgbackup.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver.borgbackup;
-
-
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
-
autoFragment =
-
if cfg.compression.auto && cfg.compression.method == null
-
then throw "compression.method must be set when using auto."
-
else lib.optional cfg.compression.auto "auto";
-
levelFragment =
-
if cfg.compression.level != null && cfg.compression.method == null
-
then throw "compression.method must be set when using compression.level."
-
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
-
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
-
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
-
-
encryptionFragment = cfg.encryption.method;
-
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
-
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
-
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
-
else throw "passphraseFile must be set when using encryption.");
-
-
locations = lib.escapeShellArgs cfg.locations;
-
name = lib.escapeShellArg cfg.name;
-
-
repoLocation = lib.escapeShellArg cfg.repoLocation;
-
-
extraInitArgs = lib.escapeShellArgs cfg.extraArgumentsForInit;
-
extraCreateArgs = lib.escapeShellArgs cfg.extraArgumentsForCreate;
-
-
cmdPreexec = lib.optionalString (cfg.cmdPreexec != null) cfg.cmdPreexec;
-
cmdPostexec = lib.optionalString (cfg.cmdPostexec != null) cfg.cmdPostexec;
-
-
borgScript = ''
-
export BORG_REPO=${repoLocation}
-
${cmdPreexec}
-
${passphraseFragment} ${pkgs.borgbackup}/bin/borg init ${extraInitArgs} --encryption ${encryptionFragment} || true
-
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
-
${cmdPostexec}
-
'';
-
in {
-
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
-
environment.systemPackages = with pkgs; [
-
borgbackup
-
];
-
-
systemd.services.borgbackup = {
-
description = "borgbackup";
-
unitConfig.Documentation = "man:borgbackup";
-
script = borgScript;
-
serviceConfig = {
-
User = cfg.user;
-
Group = cfg.group;
-
CPUSchedulingPolicy = "idle";
-
IOSchedulingClass = "idle";
-
ProtectSystem = "full";
-
};
-
startAt = cfg.startAt;
-
};
-
};
-
}
-30
modules/mailserver/clamav.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, options, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.virusScanning) {
-
services.clamav.daemon = {
-
enable = true;
-
settings.PhishingScanURLs = "no";
-
};
-
services.clamav.updater.enable = true;
-
};
-
}
-48
modules/mailserver/common.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
# cert :: PATH
-
certificatePath = if cfg.certificateScheme == 1
-
then cfg.certificateFile
-
else if cfg.certificateScheme == 2
-
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
-
else if cfg.certificateScheme == 3
-
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
-
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
-
-
# key :: PATH
-
keyPath = if cfg.certificateScheme == 1
-
then cfg.keyFile
-
else if cfg.certificateScheme == 2
-
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
-
else if cfg.certificateScheme == 3
-
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
-
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
-
-
passwordFiles = let
-
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password" hash;
-
in
-
lib.mapAttrs (name: value:
-
if value.passwordFile == null then
-
builtins.toString (mkHashFile name value.password)
-
else value.passwordFile) cfg.loginAccounts;
-
}
-4
modules/mailserver/debug.nix
···
-
{ config, lib, ... }:
-
{
-
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
-
}
-1035
modules/mailserver/default.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, lib, pkgs, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
options.mailserver = {
-
enable = lib.mkEnableOption "nixos-mailserver";
-
-
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.";
-
};
-
-
domains = mkOption {
-
type = types.listOf types.str;
-
example = [ "example.com" ];
-
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 = "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";
-
};
-
-
password = mkOption {
-
type = with types; nullOr str;
-
default = null;
-
example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
-
description = ''
-
The password.
-
-
Warning: this is stored in plaintext in the Nix store!
-
Use `passwordFile` instead.
-
'';
-
};
-
-
passwordFile = mkOption {
-
type = with types; nullOr path;
-
default = null;
-
example = "/run/keys/user1-passwordhash";
-
description = ''
-
A file containing the user's password.
-
'';
-
};
-
-
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.
-
'';
-
};
-
-
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;
-
}));
-
example = {
-
user1 = {
-
password = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
-
};
-
user2 = {
-
password = "$6$oE0ZNv2n7Vk9gOf$9xcZWCCLGdMflIfuA0vR1Q1Xblw6RZqPrP94mEit2/81/7AKj2bqUai5yPyWE.QYPyv6wLMHZvjw3Rlg7yTCD/";
-
};
-
};
-
description = ''
-
The login account of the domain. Every account is mapped to a unix user,
-
e.g. `user1@example.com`. To generate the passwords use `htpasswd` as
-
follows
-
-
```
-
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
-
```
-
'';
-
default = {};
-
};
-
-
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 the fullTextSearch option is enable 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/configuration_manual/mail_location/#variables
-
for details.
-
'';
-
example = "/var/lib/dovecot/indices";
-
};
-
-
fullTextSearch = {
-
enable = lib.mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
-
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.
-
'';
-
};
-
-
indexAttachments = mkOption {
-
type = types.bool;
-
default = false;
-
description = "Also index text-only attachements. Binary attachements are never indexed.";
-
};
-
-
enforced = mkOption {
-
type = types.enum [ "yes" "no" "body" ];
-
default = "no";
-
description = ''
-
Fail searches when no index is available. If set to
-
<literal>body</literal>, then only body searches (as opposed to
-
header) are affected. If set to <literal>no</literal>, searches may
-
fall back to a very slow brute force search.
-
'';
-
};
-
-
minSize = mkOption {
-
type = types.int;
-
default = 2;
-
description = "Size of the smallest n-gram to index.";
-
};
-
maxSize = mkOption {
-
type = types.int;
-
default = 20;
-
description = "Size of the largest n-gram to index.";
-
};
-
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.";
-
};
-
-
maintenance = {
-
enable = mkOption {
-
type = types.bool;
-
default = true;
-
description = "Regularly optmize indices, as recommended by upstream.";
-
};
-
-
onCalendar = mkOption {
-
type = types.str;
-
default = "daily";
-
description = "When to run the maintenance job. See systemd.time(7) for more information about the format.";
-
};
-
-
randomizedDelaySec = mkOption {
-
type = types.int;
-
default = 1000;
-
description = "Run the maintenance job not exactly at the time specified with <literal>onCalendar</literal>, but plus or minus this many seconds.";
-
};
-
};
-
};
-
-
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".
-
'';
-
};
-
-
extraVirtualAliases = mkOption {
-
type = let
-
loginAccount = mkOptionType {
-
name = "Login Account";
-
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
-
};
-
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
-
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
-
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`.
-
'';
-
default = {};
-
};
-
-
forwards = mkOption {
-
type = with types; attrsOf (either (listOf str) str);
-
example = {
-
"user@example.com" = "user@elsewhere.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
-
`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.
-
'';
-
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 = [];
-
};
-
-
vmailUID = mkOption {
-
type = types.int;
-
default = 5000;
-
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.
-
'';
-
};
-
-
vmailUserName = mkOption {
-
type = types.str;
-
default = "virtualMail";
-
description = ''
-
The user name and group name of the user that owns the directory where all
-
the mail is stored.
-
'';
-
};
-
-
vmailGroupName = mkOption {
-
type = types.str;
-
default = "virtualMail";
-
description = ''
-
The user name and group name of the user that owns the directory where all
-
the mail is stored.
-
'';
-
};
-
-
mailDirectory = mkOption {
-
type = types.path;
-
default = "/var/vmail";
-
description = ''
-
Where to store the mail.
-
'';
-
};
-
-
useFsLayout = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Sets whether dovecot should organize mail in subdirectories:
-
-
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
-
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
-
-
See https://wiki2.dovecot.org/MailboxFormat/Maildir for details.
-
'';
-
};
-
-
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://wiki.dovecot.org/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 = mkOption {
-
type = types.enum [ 1 2 3 ];
-
default = 2;
-
description = ''
-
Certificate Files. There are three options for these.
-
-
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.
-
'';
-
};
-
-
certificateFile = mkOption {
-
type = types.path;
-
example = "/root/mail-server.crt";
-
description = ''
-
Scheme 1)
-
Location of the certificate
-
'';
-
};
-
-
keyFile = mkOption {
-
type = types.path;
-
example = "/root/mail-server.key";
-
description = ''
-
Scheme 1)
-
Location of the key file
-
'';
-
};
-
-
certificateDirectory = mkOption {
-
type = types.path;
-
default = "/var/certs";
-
description = ''
-
Scheme 2)
-
This is the folder where the certificate will be created. The name is
-
hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the
-
certificate is valid for 10 years.
-
'';
-
};
-
-
enableImap = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to enable IMAP with STARTTLS on port 143.
-
'';
-
};
-
-
enableImapSsl = mkOption {
-
type = types.bool;
-
default = true;
-
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.
-
'';
-
};
-
-
enablePop3 = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to enable POP3 with STARTTLS on port on port 110.
-
'';
-
};
-
-
enablePop3Ssl = mkOption {
-
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.
-
'';
-
};
-
-
virusScanning = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
Whether to activate virus scanning. Note that virus scanning is _very_
-
expensive memory wise.
-
'';
-
};
-
-
dkimSigning = mkOption {
-
type = types.bool;
-
default = true;
-
description = ''
-
Whether to activate dkim signing.
-
'';
-
};
-
-
dkimSelector = mkOption {
-
type = types.str;
-
default = "mail";
-
description = ''
-
-
'';
-
};
-
-
dkimKeyDirectory = mkOption {
-
type = types.path;
-
default = "/var/dkim";
-
description = ''
-
-
'';
-
};
-
-
dkimKeyBits = mkOption {
-
type = types.int;
-
default = 1024;
-
description = ''
-
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
-
-
If you have already deployed a key with a different number of bits than specified
-
here, then you should use a different selector (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.
-
'';
-
};
-
-
dkimHeaderCanonicalization = mkOption {
-
type = types.enum ["relaxed" "simple"];
-
default = "relaxed";
-
description = ''
-
DKIM canonicalization algorithm for message headers.
-
-
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
-
'';
-
};
-
-
dkimBodyCanonicalization = mkOption {
-
type = types.enum ["relaxed" "simple"];
-
default = "relaxed";
-
description = ''
-
DKIM canonicalization algorithm for message bodies.
-
-
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
-
'';
-
};
-
-
debug = mkOption {
-
type = types.bool;
-
default = false;
-
description = ''
-
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.
-
'';
-
};
-
-
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 = {
-
address = mkOption {
-
type = types.str;
-
# read the default from nixos' redis module
-
default = let
-
cf = config.services.redis.servers.rspamd.bind;
-
cfdefault = if cf == null then "127.0.0.1" else cf;
-
ips = lib.strings.splitString " " cfdefault;
-
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
-
isIpv6 = ip: lib.lists.elem ":" (lib.stringToCharacters ip);
-
in
-
if (ip == "0.0.0.0" || ip == "::")
-
then "127.0.0.1"
-
else if isIpv6 ip then "[${ip}]" else ip;
-
defaultText = lib.literalMD "computed from <option>config.services.redis.servers.rspamd.bind</option>";
-
description = ''
-
Address that rspamd should use to contact redis.
-
'';
-
};
-
-
port = mkOption {
-
type = types.port;
-
default = config.services.redis.servers.rspamd.port;
-
defaultText = lib.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 = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
-
description = ''
-
Password that rspamd should use to contact redis, or null if not required.
-
'';
-
};
-
};
-
-
rewriteMessageId = 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.
-
'';
-
};
-
-
sendingFqdn = mkOption {
-
type = types.str;
-
default = cfg.fqdn;
-
defaultText = "config.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
-
`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.
-
'';
-
};
-
-
policydSPFExtraConfig = mkOption {
-
type = types.lines;
-
default = "";
-
example = ''
-
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
-
'';
-
description = ''
-
Extra configuration options for policyd-spf. This can be use to among
-
other things skip spf checking for some IP addresses.
-
'';
-
};
-
-
monitoring = {
-
enable = lib.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 = lib.literalMD "see source";
-
description = ''
-
The configuration used for monitoring via monit.
-
Use a mail address that you actively check and set it via 'set alert ...'.
-
'';
-
};
-
};
-
-
borgbackup = {
-
enable = lib.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 cmdPreexec and 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];
-
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.
-
'';
-
};
-
-
};
-
-
rebootAfterKernelUpgrade = {
-
enable = mkOption {
-
type = types.bool;
-
default = false;
-
example = true;
-
description = ''
-
Whether to enable automatic reboot after kernel upgrades.
-
This is to be used in conjunction with system.autoUpgrade.enable = true"
-
'';
-
};
-
method = mkOption {
-
type = types.enum [ "reboot" "systemctl kexec" ];
-
default = "reboot";
-
description = ''
-
Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot.
-
It is recommended to use the default value because the quicker kexec reboot has a number of problems.
-
Also if your server is running in a virtual machine the regular reboot will already be very quick.
-
'';
-
};
-
};
-
-
backup = {
-
enable = lib.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 = [
-
./borgbackup.nix
-
./debug.nix
-
./rsnapshot.nix
-
./clamav.nix
-
./monit.nix
-
./users.nix
-
./environment.nix
-
./networking.nix
-
./systemd.nix
-
./dovecot.nix
-
./opendkim.nix
-
./postfix.nix
-
./rspamd.nix
-
./nginx.nix
-
./kresd.nix
-
./post-upgrade-check.nix
-
];
-
}
-319
modules/mailserver/dovecot.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with (import ./common.nix { inherit config pkgs lib; });
-
-
let
-
cfg = config.mailserver;
-
-
passwdDir = "/run/dovecot2";
-
passwdFile = "${passwdDir}/passwd";
-
-
bool2int = x: if x then "1" else "0";
-
-
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
-
-
# maildir in format "/${domain}/${user}"
-
dovecotMaildir =
-
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
-
+ (lib.optionalString (cfg.indexDir != null)
-
":INDEX=${cfg.indexDir}/%d/%n"
-
);
-
-
postfixCfg = config.services.postfix;
-
dovecot2Cfg = config.services.dovecot2;
-
-
stateDir = "/var/lib/dovecot";
-
-
pipeBin = pkgs.stdenv.mkDerivation {
-
name = "pipe_bin";
-
src = ./dovecot/pipe_bin;
-
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
-
buildCommand = ''
-
mkdir -p $out/pipe/bin
-
cp $src/* $out/pipe/bin/
-
chmod a+x $out/pipe/bin/*
-
patchShebangs $out/pipe/bin
-
-
for file in $out/pipe/bin/*; do
-
wrapProgram $file \
-
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
-
done
-
'';
-
};
-
-
genPasswdScript = pkgs.writeScript "generate-password-file" ''
-
#!${pkgs.stdenv.shell}
-
-
set -euo pipefail
-
-
if (! test -d "${passwdDir}"); then
-
mkdir "${passwdDir}"
-
chmod 755 "${passwdDir}"
-
fi
-
-
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
-
if [ ! -f "$f" ]; then
-
echo "Expected password hash file $f does not exist!"
-
exit 1
-
fi
-
done
-
-
cat <<EOF > ${passwdFile}
-
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
-
let passwordHash = ''$(${pkgs.dovecot}/bin/doveadm pw -p "$(head -n 1 ${passwordFiles."${name}"})")''; in
-
"${name}:${passwordHash}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
-
+ (if lib.isString value.quota
-
then "userdb_quota_rule=*:storage=${value.quota}"
-
else "")
-
) cfg.loginAccounts)}
-
EOF
-
-
chmod 600 ${passwdFile}
-
'';
-
-
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
-
junkMailboxNumber = builtins.length junkMailboxes;
-
# The assertion garantees there is exactly one Junk mailbox.
-
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
-
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
-
services.dovecot2 = {
-
enable = true;
-
enableImap = enableImap || enableImapSsl;
-
enablePop3 = enablePop3 || enablePop3Ssl;
-
enablePAM = false;
-
enableQuota = true;
-
mailGroup = vmailGroupName;
-
mailUser = vmailUserName;
-
mailLocation = dovecotMaildir;
-
sslServerCert = certificatePath;
-
sslServerKey = keyPath;
-
enableLmtp = true;
-
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
-
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
-
protocols = lib.optional cfg.enableManageSieve "sieve";
-
-
sieveScripts = {
-
after = builtins.toFile "spam.sieve" ''
-
require "fileinto";
-
-
if header :is "X-Spam" "Yes" {
-
fileinto "${junkMailboxName}";
-
stop;
-
}
-
'';
-
};
-
-
mailboxes = cfg.mailboxes;
-
-
extraConfig = ''
-
#Extra Config
-
${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
-
}
-
-
protocol pop3 {
-
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
-
}
-
-
mail_access_groups = ${vmailGroupName}
-
ssl = required
-
ssl_min_protocol = TLSv1.2
-
ssl_prefer_server_ciphers = yes
-
-
service lmtp {
-
unix_listener dovecot-lmtp {
-
group = ${postfixCfg.group}
-
mode = 0600
-
user = ${postfixCfg.user}
-
}
-
}
-
-
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 = ${passwdFile}
-
}
-
-
service auth {
-
unix_listener auth {
-
mode = 0660
-
user = ${postfixCfg.user}
-
group = ${postfixCfg.group}
-
}
-
}
-
-
auth_mechanisms = plain login
-
-
namespace inbox {
-
separator = ${cfg.hierarchySeparator}
-
inbox = yes
-
}
-
-
plugin {
-
sieve_plugins = sieve_imapsieve sieve_extprograms
-
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
-
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
-
sieve_default_name = default
-
-
# From elsewhere to Spam folder
-
imapsieve_mailbox1_name = ${junkMailboxName}
-
imapsieve_mailbox1_causes = COPY
-
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
-
-
# From Spam folder to elsewhere
-
imapsieve_mailbox2_name = *
-
imapsieve_mailbox2_from = ${junkMailboxName}
-
imapsieve_mailbox2_causes = COPY
-
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
-
-
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
-
-
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
-
}
-
-
${lib.optionalString cfg.fullTextSearch.enable ''
-
plugin {
-
plugin = fts fts_xapian
-
fts = xapian
-
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
-
-
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
-
-
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
-
-
fts_enforced = ${cfg.fullTextSearch.enforced}
-
}
-
-
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
-
service indexer-worker {
-
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
-
}
-
''}
-
''}
-
-
lda_mailbox_autosubscribe = yes
-
lda_mailbox_autocreate = yes
-
'';
-
};
-
-
systemd.services.dovecot2 = {
-
preStart = ''
-
${genPasswdScript}
-
rm -rf '${stateDir}/imap_sieve'
-
mkdir '${stateDir}/imap_sieve'
-
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
-
for k in "${stateDir}/imap_sieve"/*.sieve ; do
-
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
-
done
-
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
-
'';
-
};
-
-
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
-
-
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
-
description = "Optimize dovecot indices for fts_xapian";
-
requisite = [ "dovecot2.service" ];
-
after = [ "dovecot2.service" ];
-
startAt = cfg.fullTextSearch.maintenance.onCalendar;
-
serviceConfig = {
-
Type = "oneshot";
-
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
-
PrivateDevices = true;
-
PrivateNetwork = true;
-
ProtectKernelTunables = true;
-
ProtectKernelModules = true;
-
ProtectControlGroups = true;
-
ProtectHome = true;
-
ProtectSystem = true;
-
PrivateTmp = true;
-
};
-
};
-
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
-
timerConfig = {
-
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
-
};
-
};
-
};
-
}
-15
modules/mailserver/dovecot/imap_sieve/report-ham.sieve
···
-
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
-
-
if environment :matches "imap.mailbox" "*" {
-
set "mailbox" "${1}";
-
}
-
-
if string "${mailbox}" "Trash" {
-
stop;
-
}
-
-
if environment :matches "imap.user" "*" {
-
set "username" "${1}";
-
}
-
-
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
-7
modules/mailserver/dovecot/imap_sieve/report-spam.sieve
···
-
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
-
-
if environment :matches "imap.user" "*" {
-
set "username" "${1}";
-
}
-
-
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
-3
modules/mailserver/dovecot/pipe_bin/sa-learn-ham.sh
···
-
#!/bin/bash
-
set -o errexit
-
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
-3
modules/mailserver/dovecot/pipe_bin/sa-learn-spam.sh
···
-
#!/bin/bash
-
set -o errexit
-
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
-28
modules/mailserver/environment.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
environment.systemPackages = with pkgs; [
-
dovecot opendkim openssh postfix rspamd
-
] ++ (if certificateScheme == 2 then [ openssl ] else []);
-
};
-
}
-27
modules/mailserver/kresd.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2017 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
-
services.kresd.enable = true;
-
};
-
}
-
-32
modules/mailserver/monit.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.monitoring.enable) {
-
services.monit = {
-
enable = true;
-
config = ''
-
set alert ${cfg.monitoring.alertAddress}
-
${cfg.monitoring.config}
-
'';
-
};
-
};
-
}
-37
modules/mailserver/networking.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = with cfg; lib.mkIf (enable && openFirewall) {
-
-
networking.firewall = {
-
allowedTCPPorts = [ 25 ]
-
++ lib.optional enableSubmission 587
-
++ lib.optional enableSubmissionSsl 465
-
++ lib.optional enableImap 143
-
++ lib.optional enableImapSsl 993
-
++ lib.optional enablePop3 110
-
++ lib.optional enablePop3Ssl 995
-
++ lib.optional enableManageSieve 4190
-
++ lib.optional (certificateScheme == 3) 80;
-
};
-
};
-
}
-44
modules/mailserver/nginx.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
-
{ config, pkgs, lib, ... }:
-
-
with (import ./common.nix { inherit config; });
-
-
let
-
cfg = config.mailserver;
-
acmeRoot = "/var/lib/acme/acme-challenge";
-
in
-
{
-
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) {
-
services.nginx = {
-
enable = true;
-
virtualHosts."${cfg.fqdn}" = {
-
serverName = cfg.fqdn;
-
serverAliases = cfg.certificateDomains;
-
forceSSL = true;
-
enableACME = true;
-
acmeRoot = acmeRoot;
-
};
-
};
-
-
security.acme.certs."${cfg.fqdn}".reloadServices = [
-
"postfix.service"
-
"dovecot2.service"
-
];
-
};
-
}
-88
modules/mailserver/opendkim.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2017 Brian Olsen
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
{ config, lib, pkgs, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
-
dkimUser = config.services.opendkim.user;
-
dkimGroup = config.services.opendkim.group;
-
-
createDomainDkimCert = dom:
-
let
-
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
-
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
-
in
-
''
-
if [ ! -f "${dkim_key}" ]
-
then
-
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-
-d "${dom}" \
-
--bits="${toString cfg.dkimKeyBits}" \
-
--directory="${cfg.dkimKeyDirectory}"
-
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
-
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
-
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
-
fi
-
'';
-
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
-
-
keyTable = pkgs.writeText "opendkim-KeyTable"
-
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
-
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
-
signingTable = pkgs.writeText "opendkim-SigningTable"
-
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
-
-
dkim = config.services.opendkim;
-
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
-
in
-
{
-
config = mkIf (cfg.dkimSigning && cfg.enable) {
-
services.opendkim = {
-
enable = true;
-
selector = cfg.dkimSelector;
-
keyPath = cfg.dkimKeyDirectory;
-
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
-
configFile = pkgs.writeText "opendkim.conf" (''
-
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
-
UMask 0002
-
Socket ${dkim.socket}
-
KeyTable file:${keyTable}
-
SigningTable file:${signingTable}
-
'' + (lib.optionalString cfg.debug ''
-
Syslog yes
-
SyslogSuccess yes
-
LogWhy yes
-
''));
-
};
-
-
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
-
postfix.extraGroups = [ "${dkimGroup}" ];
-
};
-
systemd.services.opendkim = {
-
preStart = lib.mkForce createAllCerts;
-
serviceConfig = {
-
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
-
PermissionsStartOnly = lib.mkForce false;
-
};
-
};
-
systemd.tmpfiles.rules = [
-
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
-
];
-
};
-
}
-46
modules/mailserver/post-upgrade-check.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
in
-
{
-
config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) {
-
systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" ''
-
#!${pkgs.stdenv.shell}
-
-
# Checks whether the "current" kernel is different from the booted kernel
-
# and then triggers a reboot so that the "current" kernel will be the booted one.
-
# This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos.
-
-
current=$(readlink -f /run/current-system/kernel)
-
booted=$(readlink -f /run/booted-system/kernel)
-
-
if [ "$current" == "$booted" ]; then
-
echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check";
-
else
-
echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check"
-
echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
-
echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
-
${cfg.rebootAfterKernelUpgrade.method}
-
fi
-
'';
-
};
-
}
-269
modules/mailserver/postfix.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with (import ./common.nix { inherit config pkgs lib; });
-
-
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 (n: v: lib.flatten v) tables;
-
-
# valiases_postfix :: Map String [String]
-
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
-
(name: value:
-
let to = name;
-
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
-
cfg.loginAccounts));
-
-
# catchAllPostfix :: Map String [String]
-
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
-
(name: value:
-
let to = name;
-
in map (from: {"@${from}" = to;}) value.catchAll)
-
cfg.loginAccounts));
-
-
# all_valiases_postfix :: Map String [String]
-
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
-
-
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
-
attrsToLookupTable = aliases: let
-
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
-
in mergeLookupTables lookupTables;
-
-
# extra_valiases_postfix :: Map String [String]
-
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
-
-
# forwards :: Map String [String]
-
forwards = attrsToLookupTable cfg.forwards;
-
-
# lookupTableToString :: Map String [String] -> String
-
lookupTableToString = attrs: let
-
valueToString = value: lib.concatStringsSep ", " value;
-
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
-
-
# valiases_file :: Path
-
valiases_file = let
-
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
-
in builtins.toFile "valias" content;
-
-
# denied_recipients_postfix :: [ String ]
-
denied_recipients_postfix = (map
-
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
-
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
-
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
-
-
reject_senders_postfix = (map
-
(sender:
-
"${sender} REJECT")
-
(cfg.rejectSender));
-
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
-
-
reject_recipients_postfix = (map
-
(recipient:
-
"${recipient} REJECT")
-
(cfg.rejectRecipients));
-
# rejectRecipients :: [ Path ]
-
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
-
-
# vhosts_file :: Path
-
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
-
-
# 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.
-
# The user's own address is already in all_valiases_postfix.
-
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString 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
-
'' + lib.optionalString cfg.rewriteMessageId ''
-
-
# Replaces the user submitted hostname with the server's FQDN to hide the
-
# user's host or network.
-
-
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
-
'');
-
-
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
-
unixSocket = sock: "unix:${sock}";
-
-
smtpdMilters =
-
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
-
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
-
-
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
-
-
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
-
-
submissionOptions =
-
{
-
smtpd_tls_security_level = "encrypt";
-
smtpd_sasl_auth_enable = "yes";
-
smtpd_sasl_type = "dovecot";
-
smtpd_sasl_path = "/run/dovecot2/auth";
-
smtpd_sasl_security_options = "noanonymous";
-
smtpd_sasl_local_domain = "$myhostname";
-
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
-
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
-
smtpd_sender_restrictions = "reject_sender_login_mismatch";
-
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
-
cleanup_service_name = "submission-header-cleanup";
-
};
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
-
services.postfix = {
-
enable = true;
-
hostname = "${sendingFqdn}";
-
networksStyle = "host";
-
mapFiles."valias" = valiases_file;
-
mapFiles."vaccounts" = vaccounts_file;
-
mapFiles."denied_recipients" = denied_recipients_file;
-
mapFiles."reject_senders" = reject_senders_file;
-
mapFiles."reject_recipients" = reject_recipients_file;
-
sslCert = certificatePath;
-
sslKey = keyPath;
-
enableSubmission = cfg.enableSubmission;
-
enableSubmissions = cfg.enableSubmissionSsl;
-
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
-
-
config = {
-
# Extra Config
-
mydestination = "";
-
recipient_delimiter = cfg.recipientDelimiter;
-
smtpd_banner = "${fqdn} ESMTP NO UCE";
-
disable_vrfy_command = true;
-
message_size_limit = toString cfg.messageSizeLimit;
-
-
# virtual mail system
-
virtual_uid_maps = "static:5000";
-
virtual_gid_maps = "static:5000";
-
virtual_mailbox_base = mailDirectory;
-
virtual_mailbox_domains = vhosts_file;
-
virtual_mailbox_maps = mappedFile "valias";
-
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
-
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
-
lmtp_destination_recipient_limit = "1";
-
-
# sasl with dovecot
-
smtpd_sasl_type = "dovecot";
-
smtpd_sasl_path = "/run/dovecot2/auth";
-
smtpd_sasl_auth_enable = true;
-
smtpd_relay_restrictions = [
-
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
-
];
-
-
policy-spf_time_limit = "3600s";
-
-
# reject selected senders
-
smtpd_sender_restrictions = [
-
"check_sender_access ${mappedFile "reject_senders"}"
-
];
-
-
# quota and spf checking
-
smtpd_recipient_restrictions = [
-
"check_recipient_access ${mappedFile "denied_recipients"}"
-
"check_recipient_access ${mappedFile "reject_recipients"}"
-
"check_policy_service inet:localhost:12340"
-
"check_policy_service unix:private/policy-spf"
-
];
-
-
# TLS settings, inspired by https://github.com/jeaye/nix-files
-
# Submission by mail clients is handled in submissionOptions
-
smtpd_tls_security_level = "may";
-
-
# strong might suffice and is computationally less expensive
-
smtpd_tls_eecdh_grade = "ultra";
-
-
# Disable obselete protocols
-
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
smtp_tls_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, !TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
-
-
smtp_tls_ciphers = "high";
-
smtpd_tls_ciphers = "high";
-
smtp_tls_mandatory_ciphers = "high";
-
smtpd_tls_mandatory_ciphers = "high";
-
-
# Disable deprecated ciphers
-
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
-
-
tls_preempt_cipherlist = true;
-
-
# Allowing AUTH on a non encrypted connection poses a security risk
-
smtpd_tls_auth_only = true;
-
# Log only a summary message on TLS handshake completion
-
smtpd_tls_loglevel = "1";
-
-
# Configure a non blocking source of randomness
-
tls_random_source = "dev:/dev/urandom";
-
-
smtpd_milters = smtpdMilters;
-
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"];
-
milter_protocol = "6";
-
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
-
-
};
-
-
submissionOptions = submissionOptions;
-
submissionsOptions = submissionOptions;
-
-
masterConfig = {
-
"lmtp" = {
-
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
-
# D => Delivered-To, O => X-Original-To, R => Return-Path
-
args = [ "flags=O" ];
-
};
-
"policy-spf" = {
-
type = "unix";
-
privileged = true;
-
chroot = false;
-
command = "spawn";
-
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
-
};
-
"submission-header-cleanup" = {
-
type = "unix";
-
private = false;
-
chroot = false;
-
maxproc = 0;
-
command = "cleanup";
-
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
-
};
-
};
-
};
-
};
-
}
-59
modules/mailserver/rsnapshot.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with lib;
-
-
let
-
cfg = config.mailserver;
-
-
preexecDefined = cfg.backup.cmdPreexec != null;
-
preexecWrapped = pkgs.writeScript "rsnapshot-preexec.sh" ''
-
#!${pkgs.stdenv.shell}
-
set -e
-
-
${cfg.backup.cmdPreexec}
-
'';
-
preexecString = optionalString preexecDefined "cmd_preexec ${preexecWrapped}";
-
-
postexecDefined = cfg.backup.cmdPostexec != null;
-
postexecWrapped = pkgs.writeScript "rsnapshot-postexec.sh" ''
-
#!${pkgs.stdenv.shell}
-
set -e
-
-
${cfg.backup.cmdPostexec}
-
'';
-
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
-
in {
-
config = mkIf (cfg.enable && cfg.backup.enable) {
-
services.rsnapshot = {
-
enable = true;
-
cronIntervals = cfg.backup.cronIntervals;
-
# rsnapshot expects intervals shortest first, e.g. hourly first, then daily.
-
# tabs must separate all elements
-
extraConfig = ''
-
${preexecString}
-
${postexecString}
-
snapshot_root ${cfg.backup.snapshotRoot}/
-
retain hourly ${toString cfg.backup.retain.hourly}
-
retain daily ${toString cfg.backup.retain.daily}
-
retain weekly ${toString cfg.backup.retain.weekly}
-
backup ${cfg.mailDirectory}/ localhost/
-
'';
-
};
-
};
-
}
-119
modules/mailserver/rspamd.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
-
postfixCfg = config.services.postfix;
-
rspamdCfg = config.services.rspamd;
-
rspamdSocket = "rspamd.service";
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
services.rspamd = {
-
enable = true;
-
inherit debug;
-
locals = {
-
"milter_headers.conf" = { text = ''
-
extended_spam_headers = yes;
-
''; };
-
"redis.conf" = { text = ''
-
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
-
'' + (lib.optionalString (cfg.redis.password != null) ''
-
password = "${cfg.redis.password}";
-
''); };
-
"classifier-bayes.conf" = { text = ''
-
cache {
-
backend = "redis";
-
}
-
''; };
-
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
-
clamav {
-
action = "reject";
-
symbol = "CLAM_VIRUS";
-
type = "clamav";
-
log_clean = true;
-
servers = "/run/clamav/clamd.ctl";
-
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
-
}
-
''; };
-
"dkim_signing.conf" = { text = ''
-
# Disable outbound email signing, we use opendkim for this
-
enabled = false;
-
''; };
-
};
-
-
overrides = {
-
"milter_headers.conf" = {
-
text = ''
-
extended_spam_headers = true;
-
'';
-
};
-
};
-
-
workers.rspamd_proxy = {
-
type = "rspamd_proxy";
-
bindSockets = [{
-
socket = "/run/rspamd/rspamd-milter.sock";
-
mode = "0664";
-
}];
-
count = 1; # Do not spawn too many processes of this type
-
extraConfig = ''
-
milter = yes; # Enable milter mode
-
timeout = 120s; # Needed for Milter usually
-
-
upstream "local" {
-
default = yes; # Self-scan upstreams are always default
-
self_scan = yes; # Enable self-scan
-
}
-
'';
-
};
-
workers.controller = {
-
type = "controller";
-
count = 1;
-
bindSockets = [{
-
socket = "/run/rspamd/worker-controller.sock";
-
mode = "0666";
-
}];
-
includes = [];
-
extraConfig = ''
-
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
-
'';
-
};
-
-
};
-
-
services.redis.servers.rspamd = {
-
enable = lib.mkDefault true;
-
port = lib.mkDefault 6380;
-
};
-
-
systemd.services.rspamd = {
-
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
-
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
-
};
-
-
systemd.services.postfix = {
-
after = [ rspamdSocket ];
-
requires = [ rspamdSocket ];
-
};
-
-
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
-
};
-
}
-
-83
modules/mailserver/systemd.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
let
-
cfg = config.mailserver;
-
certificatesDeps =
-
if cfg.certificateScheme == 1 then
-
[]
-
else if cfg.certificateScheme == 2 then
-
[ "mailserver-selfsigned-certificate.service" ]
-
else
-
[ "acme-finished-${cfg.fqdn}.target" ];
-
in
-
{
-
config = with cfg; lib.mkIf enable {
-
# Create self signed certificate
-
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
-
after = [ "local-fs.target" ];
-
script = ''
-
# Create certificates if they do not exist yet
-
dir="${cfg.certificateDirectory}"
-
fqdn="${cfg.fqdn}"
-
[[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
-
key="$dir/key-${cfg.fqdn}.pem";
-
cert="$dir/cert-${cfg.fqdn}.pem";
-
-
if [[ ! -f $key || ! -f $cert ]]; then
-
mkdir -p "${cfg.certificateDirectory}"
-
(umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
-
"${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
-
-days 3650 -out "$cert"
-
fi
-
'';
-
serviceConfig = {
-
Type = "oneshot";
-
PrivateTmp = true;
-
};
-
};
-
-
# Create maildir folder before dovecot startup
-
systemd.services.dovecot2 = {
-
wants = certificatesDeps;
-
after = certificatesDeps;
-
preStart = let
-
directories = lib.strings.escapeShellArgs (
-
[ mailDirectory ]
-
++ lib.optional (cfg.indexDir != null) cfg.indexDir
-
);
-
in ''
-
# Create mail directory and set permissions. See
-
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
-
mkdir -p ${directories}
-
chgrp "${vmailGroupName}" ${directories}
-
chmod 02770 ${directories}
-
'';
-
};
-
-
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
-
systemd.services.postfix = {
-
wants = certificatesDeps;
-
after = [ "dovecot2.service" ]
-
++ lib.optional cfg.dkimSigning "opendkim.service"
-
++ certificatesDeps;
-
requires = [ "dovecot2.service" ]
-
++ lib.optional cfg.dkimSigning "opendkim.service";
-
};
-
};
-
}
-97
modules/mailserver/users.nix
···
-
# nixos-mailserver: a simple mail server
-
# Copyright (C) 2016-2018 Robin Raymond
-
#
-
# This program is free software: you can redistribute it and/or modify
-
# it under the terms of the GNU General Public License as published by
-
# the Free Software Foundation, either version 3 of the License, or
-
# (at your option) any later version.
-
#
-
# This program is distributed in the hope that it will be useful,
-
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-
# GNU General Public License for more details.
-
#
-
# You should have received a copy of the GNU General Public License
-
# along with this program. If not, see <http://www.gnu.org/licenses/>
-
-
{ config, pkgs, lib, ... }:
-
-
with config.mailserver;
-
-
let
-
vmail_user = {
-
name = vmailUserName;
-
isSystemUser = true;
-
uid = vmailUID;
-
home = mailDirectory;
-
createHome = true;
-
group = vmailGroupName;
-
};
-
-
-
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
-
#!${pkgs.stdenv.shell}
-
-
set -euo pipefail
-
-
# 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}"
-
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))}
-
'';
-
in {
-
config = lib.mkIf enable {
-
# assert that all accounts provide a password
-
-
# warn for accounts that specify both password and file
-
warnings = (map
-
(acct: "${acct.name} specifies both a password and password file; password file will be used")
-
(lib.filter
-
(acct: (acct.password != null && acct.passwordFile != null))
-
(lib.attrValues loginAccounts)));
-
-
# set the vmail gid to a specific value
-
users.groups = {
-
"${vmailGroupName}" = { gid = vmailUID; };
-
};
-
-
# define all users
-
users.users = {
-
"${vmail_user.name}" = lib.mkForce vmail_user;
-
};
-
-
systemd.services.activate-virtual-mail-users = {
-
wantedBy = [ "multi-user.target" ];
-
before = [ "dovecot2.service" ];
-
serviceConfig = {
-
ExecStart = virtualMailUsersActivationScript;
-
};
-
enable = true;
-
};
-
};
-
}