
There exist multiple issues with these options, for example they are not introspectable, since the values are configured in the config part of the module. Also the keypair is always configured for both server and client usage, which is really surprising. The postfix docs even advise against setting up client certificates, if they aren't required. [1] The replacements are the `smtpd_tls_chain_files` for server usage and `smtp_tls_chain_files` for client usage, which are the prefered way to configure keys and certificates since Postfix 3.4.0. [2] [1] https://www.postfix.org/postconf.5.html#smtp_tls_cert_file [2] https://www.postfix.org/postconf.5.html#smtpd_tls_cert_file
1195 lines
37 KiB
Nix
1195 lines
37 KiB
Nix
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}:
|
|
let
|
|
inherit (lib)
|
|
mkOption
|
|
types
|
|
;
|
|
|
|
cfg = config.services.postfix;
|
|
user = cfg.user;
|
|
group = cfg.group;
|
|
setgidGroup = cfg.setgidGroup;
|
|
|
|
haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" || cfg.extraAliases != "";
|
|
haveCanonical = cfg.canonical != "";
|
|
haveTransport = cfg.transport != "";
|
|
haveVirtual = cfg.virtual != "";
|
|
haveLocalRecipients = cfg.localRecipients != null;
|
|
|
|
clientAccess = lib.optional (
|
|
cfg.dnsBlacklistOverrides != ""
|
|
) "check_client_access hash:/etc/postfix/client_access";
|
|
|
|
dnsBl = lib.optionals (cfg.dnsBlacklists != [ ]) (
|
|
map (s: "reject_rbl_client " + s) cfg.dnsBlacklists
|
|
);
|
|
|
|
clientRestrictions = lib.concatStringsSep ", " (clientAccess ++ dnsBl);
|
|
|
|
mainCf =
|
|
let
|
|
escape = lib.replaceStrings [ "$" ] [ "$$" ];
|
|
mkList = items: "\n " + lib.concatStringsSep ",\n " items;
|
|
mkVal =
|
|
value:
|
|
if lib.isList value then
|
|
mkList value
|
|
else
|
|
" "
|
|
+ (
|
|
if value == true then
|
|
"yes"
|
|
else if value == false then
|
|
"no"
|
|
else
|
|
toString value
|
|
);
|
|
mkEntry = name: value: "${escape name} =${mkVal value}";
|
|
in
|
|
lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList mkEntry (lib.filterAttrsRecursive (_: value: value != null) cfg.config)
|
|
)
|
|
+ "\n"
|
|
+ cfg.extraConfig;
|
|
|
|
masterCfOptions =
|
|
{
|
|
options,
|
|
config,
|
|
name,
|
|
...
|
|
}:
|
|
{
|
|
options = {
|
|
name = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = name;
|
|
example = "smtp";
|
|
description = ''
|
|
The name of the service to run. Defaults to the attribute set key.
|
|
'';
|
|
};
|
|
|
|
type = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"inet"
|
|
"unix"
|
|
"unix-dgram"
|
|
"fifo"
|
|
"pass"
|
|
];
|
|
default = "unix";
|
|
example = "inet";
|
|
description = "The type of the service";
|
|
};
|
|
|
|
private = lib.mkOption {
|
|
type = lib.types.bool;
|
|
example = false;
|
|
description = ''
|
|
Whether the service's sockets and storage directory is restricted to
|
|
be only available via the mail system. If `null` is
|
|
given it uses the postfix default `true`.
|
|
'';
|
|
};
|
|
|
|
privileged = lib.mkOption {
|
|
type = lib.types.bool;
|
|
example = true;
|
|
description = "";
|
|
};
|
|
|
|
chroot = lib.mkOption {
|
|
type = lib.types.bool;
|
|
example = true;
|
|
description = ''
|
|
Whether the service is chrooted to have only access to the
|
|
{option}`services.postfix.queueDir` and the closure of
|
|
store paths specified by the {option}`program` option.
|
|
'';
|
|
};
|
|
|
|
wakeup = lib.mkOption {
|
|
type = lib.types.int;
|
|
example = 60;
|
|
description = ''
|
|
Automatically wake up the service after the specified number of
|
|
seconds. If `0` is given, never wake the service
|
|
up.
|
|
'';
|
|
};
|
|
|
|
wakeupUnusedComponent = lib.mkOption {
|
|
type = lib.types.bool;
|
|
example = false;
|
|
description = ''
|
|
If set to `false` the component will only be woken
|
|
up if it is used. This is equivalent to postfix' notion of adding a
|
|
question mark behind the wakeup time in
|
|
{file}`master.cf`
|
|
'';
|
|
};
|
|
|
|
maxproc = lib.mkOption {
|
|
type = lib.types.int;
|
|
example = 1;
|
|
description = ''
|
|
The maximum number of processes to spawn for this service. If the
|
|
value is `0` it doesn't have any limit. If
|
|
`null` is given it uses the postfix default of
|
|
`100`.
|
|
'';
|
|
};
|
|
|
|
command = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = name;
|
|
example = "smtpd";
|
|
description = ''
|
|
A program name specifying a Postfix service/daemon process.
|
|
By default it's the attribute {option}`name`.
|
|
'';
|
|
};
|
|
|
|
args = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
example = [
|
|
"-o"
|
|
"smtp_helo_timeout=5"
|
|
];
|
|
description = ''
|
|
Arguments to pass to the {option}`command`. There is no shell
|
|
processing involved and shell syntax is passed verbatim to the
|
|
process.
|
|
'';
|
|
};
|
|
|
|
rawEntry = lib.mkOption {
|
|
type = lib.types.listOf lib.types.str;
|
|
default = [ ];
|
|
internal = true;
|
|
description = ''
|
|
The raw configuration line for the {file}`master.cf`.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config.rawEntry =
|
|
let
|
|
mkBool = bool: if bool then "y" else "n";
|
|
mkArg = arg: "${lib.optionalString (lib.hasPrefix "-" arg) "\n "}${arg}";
|
|
|
|
maybeOption = fun: option: if options.${option}.isDefined then fun config.${option} else "-";
|
|
|
|
# This is special, because we have two options for this value.
|
|
wakeup =
|
|
let
|
|
wakeupDefined = options.wakeup.isDefined;
|
|
wakeupUCDefined = options.wakeupUnusedComponent.isDefined;
|
|
finalValue =
|
|
toString config.wakeup + lib.optionalString (wakeupUCDefined && !config.wakeupUnusedComponent) "?";
|
|
in
|
|
if wakeupDefined then finalValue else "-";
|
|
|
|
in
|
|
[
|
|
config.name
|
|
config.type
|
|
(maybeOption mkBool "private")
|
|
(maybeOption (b: mkBool (!b)) "privileged")
|
|
(maybeOption mkBool "chroot")
|
|
wakeup
|
|
(maybeOption toString "maxproc")
|
|
(config.command + " " + lib.concatMapStringsSep " " mkArg config.args)
|
|
];
|
|
};
|
|
|
|
masterCfContent =
|
|
let
|
|
|
|
labels = [
|
|
"# service"
|
|
"type"
|
|
"private"
|
|
"unpriv"
|
|
"chroot"
|
|
"wakeup"
|
|
"maxproc"
|
|
"command + args"
|
|
];
|
|
|
|
labelDefaults = [
|
|
"# "
|
|
""
|
|
"(yes)"
|
|
"(yes)"
|
|
"(no)"
|
|
"(never)"
|
|
"(100)"
|
|
""
|
|
""
|
|
];
|
|
|
|
masterCf = lib.mapAttrsToList (lib.const (lib.getAttr "rawEntry")) cfg.masterConfig;
|
|
|
|
# A list of the maximum width of the columns across all lines and labels
|
|
maxWidths =
|
|
let
|
|
foldLine =
|
|
line: acc:
|
|
let
|
|
columnLengths = map lib.stringLength line;
|
|
in
|
|
lib.zipListsWith lib.max acc columnLengths;
|
|
# We need to handle the last column specially here, because it's
|
|
# open-ended (command + args).
|
|
lines = [
|
|
labels
|
|
labelDefaults
|
|
] ++ (map (l: lib.init l ++ [ "" ]) masterCf);
|
|
in
|
|
lib.foldr foldLine (lib.genList (lib.const 0) (lib.length labels)) lines;
|
|
|
|
# Pad a string with spaces from the right (opposite of fixedWidthString).
|
|
pad =
|
|
width: str:
|
|
let
|
|
padWidth = width - lib.stringLength str;
|
|
padding = lib.concatStrings (lib.genList (lib.const " ") padWidth);
|
|
in
|
|
str + lib.optionalString (padWidth > 0) padding;
|
|
|
|
# It's + 2 here, because that's the amount of spacing between columns.
|
|
fullWidth = lib.foldr (width: acc: acc + width + 2) 0 maxWidths;
|
|
|
|
formatLine = line: lib.concatStringsSep " " (lib.zipListsWith pad maxWidths line);
|
|
|
|
formattedLabels =
|
|
let
|
|
sep = "# " + lib.concatStrings (lib.genList (lib.const "=") (fullWidth + 5));
|
|
lines = [
|
|
sep
|
|
(formatLine labels)
|
|
(formatLine labelDefaults)
|
|
sep
|
|
];
|
|
in
|
|
lib.concatStringsSep "\n" lines;
|
|
|
|
in
|
|
formattedLabels
|
|
+ "\n"
|
|
+ lib.concatMapStringsSep "\n" formatLine masterCf
|
|
+ "\n"
|
|
+ cfg.extraMasterConf;
|
|
|
|
headerCheckOptions =
|
|
{ ... }:
|
|
{
|
|
options = {
|
|
pattern = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "/^.*/";
|
|
example = "/^X-Mailer:/";
|
|
description = "A regexp pattern matching the header";
|
|
};
|
|
action = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "DUNNO";
|
|
example = "BCC mail@example.com";
|
|
description = "The action to be executed when the pattern is matched";
|
|
};
|
|
};
|
|
};
|
|
|
|
headerChecks =
|
|
lib.concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks)
|
|
+ cfg.extraHeaderChecks;
|
|
|
|
aliases =
|
|
let
|
|
separator = lib.optionalString (cfg.aliasMapType == "hash") ":";
|
|
in
|
|
lib.optionalString (cfg.postmasterAlias != "") ''
|
|
postmaster${separator} ${cfg.postmasterAlias}
|
|
''
|
|
+ lib.optionalString (cfg.rootAlias != "") ''
|
|
root${separator} ${cfg.rootAlias}
|
|
''
|
|
+ cfg.extraAliases;
|
|
|
|
aliasesFile = pkgs.writeText "postfix-aliases" aliases;
|
|
canonicalFile = pkgs.writeText "postfix-canonical" cfg.canonical;
|
|
virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
|
|
localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" (
|
|
lib.concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients
|
|
);
|
|
checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
|
|
mainCfFile = pkgs.writeText "postfix-main.cf" mainCf;
|
|
masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent;
|
|
transportFile = pkgs.writeText "postfix-transport" cfg.transport;
|
|
headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks;
|
|
|
|
in
|
|
|
|
{
|
|
|
|
###### interface
|
|
|
|
options = {
|
|
|
|
services.postfix = {
|
|
|
|
enable = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Whether to run the Postfix mail server.";
|
|
};
|
|
|
|
enableSmtp = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to enable smtp in master.cf.";
|
|
};
|
|
|
|
enableSubmission = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Whether to enable smtp submission.";
|
|
};
|
|
|
|
enableSubmissions = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether to enable smtp submission via smtps.
|
|
|
|
According to RFC 8314 this should be preferred
|
|
over STARTTLS for submission of messages by end user clients.
|
|
'';
|
|
};
|
|
|
|
submissionOptions = lib.mkOption {
|
|
type = with lib.types; attrsOf str;
|
|
default = {
|
|
smtpd_tls_security_level = "encrypt";
|
|
smtpd_sasl_auth_enable = "yes";
|
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
|
milter_macro_daemon_name = "ORIGINATING";
|
|
};
|
|
example = {
|
|
smtpd_tls_security_level = "encrypt";
|
|
smtpd_sasl_auth_enable = "yes";
|
|
smtpd_sasl_type = "dovecot";
|
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
|
milter_macro_daemon_name = "ORIGINATING";
|
|
};
|
|
description = "Options for the submission config in master.cf";
|
|
};
|
|
|
|
submissionsOptions = lib.mkOption {
|
|
type = with lib.types; attrsOf str;
|
|
default = {
|
|
smtpd_sasl_auth_enable = "yes";
|
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
|
milter_macro_daemon_name = "ORIGINATING";
|
|
};
|
|
example = {
|
|
smtpd_sasl_auth_enable = "yes";
|
|
smtpd_sasl_type = "dovecot";
|
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
|
milter_macro_daemon_name = "ORIGINATING";
|
|
};
|
|
description = ''
|
|
Options for the submission config via smtps in master.cf.
|
|
|
|
smtpd_tls_security_level will be set to encrypt, if it is missing
|
|
or has one of the values "may" or "none".
|
|
|
|
smtpd_tls_wrappermode with value "yes" will be added automatically.
|
|
'';
|
|
};
|
|
|
|
setSendmail = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = true;
|
|
description = "Whether to set the system sendmail to postfix's.";
|
|
};
|
|
|
|
user = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "postfix";
|
|
description = "What to call the Postfix user (must be used only for postfix).";
|
|
};
|
|
|
|
group = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "postfix";
|
|
description = "What to call the Postfix group (must be used only for postfix).";
|
|
};
|
|
|
|
setgidGroup = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "postdrop";
|
|
description = ''
|
|
How to call postfix setgid group (for postdrop). Should
|
|
be uniquely used group.
|
|
'';
|
|
};
|
|
|
|
networks = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
example = [ "192.168.0.1/24" ];
|
|
description = ''
|
|
Net masks for trusted - allowed to relay mail to third parties -
|
|
hosts. Leave empty to use mynetworks_style configuration or use
|
|
default (localhost-only).
|
|
'';
|
|
};
|
|
|
|
networksStyle = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Name of standard way of trusted network specification to use,
|
|
leave blank if you specify it explicitly or if you want to use
|
|
default (localhost-only).
|
|
'';
|
|
};
|
|
|
|
hostname = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Hostname to use. Leave blank to use just the hostname of machine.
|
|
It should be FQDN.
|
|
'';
|
|
};
|
|
|
|
domain = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Domain to use. Leave blank to use hostname minus first component.
|
|
'';
|
|
};
|
|
|
|
origin = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Origin to use in outgoing e-mail. Leave blank to use hostname.
|
|
'';
|
|
};
|
|
|
|
destination = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
example = [ "localhost" ];
|
|
description = ''
|
|
Full (!) list of domains we deliver locally. Leave blank for
|
|
acceptable Postfix default.
|
|
'';
|
|
};
|
|
|
|
relayDomains = lib.mkOption {
|
|
type = lib.types.nullOr (lib.types.listOf lib.types.str);
|
|
default = null;
|
|
example = [ "localdomain" ];
|
|
description = ''
|
|
List of domains we agree to relay to. Default is empty.
|
|
'';
|
|
};
|
|
|
|
relayHost = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Mail relay for outbound mail.
|
|
'';
|
|
};
|
|
|
|
relayPort = lib.mkOption {
|
|
type = lib.types.int;
|
|
default = 25;
|
|
description = ''
|
|
SMTP port for relay mail relay.
|
|
'';
|
|
};
|
|
|
|
lookupMX = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = ''
|
|
Whether relay specified is just domain whose MX must be used.
|
|
'';
|
|
};
|
|
|
|
postmasterAlias = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "root";
|
|
description = ''
|
|
Who should receive postmaster e-mail. Multiple values can be added by
|
|
separating values with comma.
|
|
'';
|
|
};
|
|
|
|
rootAlias = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
description = ''
|
|
Who should receive root e-mail. Blank for no redirection.
|
|
Multiple values can be added by separating values with comma.
|
|
'';
|
|
};
|
|
|
|
extraAliases = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
description = ''
|
|
Additional entries to put verbatim into aliases file, cf. man-page {manpage}`aliases(8)`.
|
|
'';
|
|
};
|
|
|
|
aliasMapType = lib.mkOption {
|
|
type =
|
|
with lib.types;
|
|
enum [
|
|
"hash"
|
|
"regexp"
|
|
"pcre"
|
|
];
|
|
default = "hash";
|
|
example = "regexp";
|
|
description = "The format the alias map should have. Use regexp if you want to use regular expressions.";
|
|
};
|
|
|
|
config = lib.mkOption {
|
|
type = lib.types.submodule {
|
|
freeformType =
|
|
with types;
|
|
attrsOf (
|
|
nullOr (oneOf [
|
|
bool
|
|
int
|
|
str
|
|
(listOf str)
|
|
])
|
|
);
|
|
options = {
|
|
smtpd_tls_chain_files = mkOption {
|
|
type = with types; listOf path;
|
|
default = [ ];
|
|
example = [
|
|
"/var/lib/acme/mail.example.com/privkey.pem"
|
|
"/var/lib/acme/mail.example.com/fullchain.pem"
|
|
];
|
|
description = ''
|
|
List of paths to the server private keys and certificates.
|
|
|
|
::: {.caution}
|
|
The order of items matters and a private key must always be followed by the corresponding certificate.
|
|
:::
|
|
|
|
<https://www.postfix.org/postconf.5.html#smtpd_tls_chain_files>
|
|
'';
|
|
};
|
|
|
|
smtpd_tls_security_level = mkOption {
|
|
type = types.enum [
|
|
"none"
|
|
"may"
|
|
"encrypt"
|
|
];
|
|
default = if config.services.postfix.config.smtpd_tls_chain_files != [ ] then "may" else "none";
|
|
defaultText = lib.literalExpression ''
|
|
if config.services.postfix.config.smtpd_tls_chain_files != [ ] then "may" else "none"
|
|
'';
|
|
example = "may";
|
|
description = ''
|
|
The server TLS security level. Enable TLS by configuring at least `may`.
|
|
|
|
<https://www.postfix.org/postconf.5.html#smtpd_tls_security_level>
|
|
'';
|
|
};
|
|
};
|
|
};
|
|
|
|
description = ''
|
|
The main.cf configuration file as key value set.
|
|
|
|
Null values will not be rendered.
|
|
'';
|
|
example = {
|
|
mail_owner = "postfix";
|
|
smtp_tls_security_level = "may";
|
|
};
|
|
};
|
|
|
|
extraConfig = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
description = ''
|
|
Extra lines to be added verbatim to the main.cf configuration file.
|
|
'';
|
|
};
|
|
|
|
tlsTrustedAuthorities = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = config.security.pki.caBundle;
|
|
defaultText = lib.literalExpression "config.security.pki.caBundle";
|
|
example = lib.literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"'';
|
|
description = ''
|
|
File containing trusted certification authorities (CA) to verify certificates of mailservers contacted for mail delivery. This sets [smtp_tls_CAfile](https://www.postfix.org/postconf.5.html#smtp_tls_CAfile). Defaults to system trusted certificates (see `security.pki.*` options).
|
|
'';
|
|
};
|
|
|
|
recipientDelimiter = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "";
|
|
example = "+";
|
|
description = ''
|
|
Delimiter for address extension: so mail to user+test can be handled by ~user/.forward+test
|
|
'';
|
|
};
|
|
|
|
canonical = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
description = ''
|
|
Entries for the {manpage}`canonical(5)` table.
|
|
'';
|
|
};
|
|
|
|
virtual = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
description = ''
|
|
Entries for the virtual alias map, cf. man-page {manpage}`virtual(5)`.
|
|
'';
|
|
};
|
|
|
|
virtualMapType = lib.mkOption {
|
|
type = lib.types.enum [
|
|
"hash"
|
|
"regexp"
|
|
"pcre"
|
|
];
|
|
default = "hash";
|
|
description = ''
|
|
What type of virtual alias map file to use. Use `"regexp"` for regular expressions.
|
|
'';
|
|
};
|
|
|
|
localRecipients = lib.mkOption {
|
|
type = with lib.types; nullOr (listOf str);
|
|
default = null;
|
|
description = ''
|
|
List of accepted local users. Specify a bare username, an
|
|
`"@domain.tld"` wild-card, or a complete
|
|
`"user@domain.tld"` address. If set, these names end
|
|
up in the local recipient map -- see the {manpage}`local(8)` man-page -- and
|
|
effectively replace the system user database lookup that's otherwise
|
|
used by default.
|
|
'';
|
|
};
|
|
|
|
transport = lib.mkOption {
|
|
default = "";
|
|
type = lib.types.lines;
|
|
description = ''
|
|
Entries for the transport map, cf. man-page {manpage}`transport(8)`.
|
|
'';
|
|
};
|
|
|
|
dnsBlacklists = lib.mkOption {
|
|
default = [ ];
|
|
type = with lib.types; listOf str;
|
|
description = "dns blacklist servers to use with smtpd_client_restrictions";
|
|
};
|
|
|
|
dnsBlacklistOverrides = lib.mkOption {
|
|
default = "";
|
|
type = lib.types.lines;
|
|
description = "contents of check_client_access for overriding dnsBlacklists";
|
|
};
|
|
|
|
masterConfig = lib.mkOption {
|
|
type = lib.types.attrsOf (lib.types.submodule masterCfOptions);
|
|
default = { };
|
|
example = {
|
|
submission = {
|
|
type = "inet";
|
|
args = [
|
|
"-o"
|
|
"smtpd_tls_security_level=encrypt"
|
|
];
|
|
};
|
|
};
|
|
description = ''
|
|
An attribute set of service options, which correspond to the service
|
|
definitions usually done within the Postfix
|
|
{file}`master.cf` file.
|
|
'';
|
|
};
|
|
|
|
extraMasterConf = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
example = "submission inet n - n - - smtpd";
|
|
description = "Extra lines to append to the generated master.cf file.";
|
|
};
|
|
|
|
enableHeaderChecks = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
example = true;
|
|
description = "Whether to enable postfix header checks";
|
|
};
|
|
|
|
headerChecks = lib.mkOption {
|
|
type = lib.types.listOf (lib.types.submodule headerCheckOptions);
|
|
default = [ ];
|
|
example = [
|
|
{
|
|
pattern = "/^X-Spam-Flag:/";
|
|
action = "REDIRECT spam@example.com";
|
|
}
|
|
];
|
|
description = "Postfix header checks.";
|
|
};
|
|
|
|
extraHeaderChecks = lib.mkOption {
|
|
type = lib.types.lines;
|
|
default = "";
|
|
example = "/^X-Spam-Flag:/ REDIRECT spam@example.com";
|
|
description = "Extra lines to /etc/postfix/header_checks file.";
|
|
};
|
|
|
|
aliasFiles = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.path;
|
|
default = { };
|
|
description = "Aliases' tables to be compiled and placed into /var/lib/postfix/conf.";
|
|
};
|
|
|
|
mapFiles = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.path;
|
|
default = { };
|
|
description = "Maps to be compiled and placed into /var/lib/postfix/conf.";
|
|
};
|
|
|
|
useSrs = lib.mkOption {
|
|
type = lib.types.bool;
|
|
default = false;
|
|
description = "Whether to enable sender rewriting scheme";
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
###### implementation
|
|
|
|
config = lib.mkIf config.services.postfix.enable (
|
|
lib.mkMerge [
|
|
{
|
|
|
|
environment = {
|
|
etc.postfix.source = "/var/lib/postfix/conf";
|
|
|
|
# This makes it comfortable to run 'postqueue/postdrop' for example.
|
|
systemPackages = [ pkgs.postfix ];
|
|
};
|
|
|
|
services.pfix-srsd.enable = config.services.postfix.useSrs;
|
|
|
|
services.mail.sendmailSetuidWrapper = lib.mkIf config.services.postfix.setSendmail {
|
|
program = "sendmail";
|
|
source = "${pkgs.postfix}/bin/sendmail";
|
|
owner = "root";
|
|
group = setgidGroup;
|
|
setuid = false;
|
|
setgid = true;
|
|
};
|
|
|
|
security.wrappers.mailq = {
|
|
program = "mailq";
|
|
source = "${pkgs.postfix}/bin/mailq";
|
|
owner = "root";
|
|
group = setgidGroup;
|
|
setuid = false;
|
|
setgid = true;
|
|
};
|
|
|
|
security.wrappers.postqueue = {
|
|
program = "postqueue";
|
|
source = "${pkgs.postfix}/bin/postqueue";
|
|
owner = "root";
|
|
group = setgidGroup;
|
|
setuid = false;
|
|
setgid = true;
|
|
};
|
|
|
|
security.wrappers.postdrop = {
|
|
program = "postdrop";
|
|
source = "${pkgs.postfix}/bin/postdrop";
|
|
owner = "root";
|
|
group = setgidGroup;
|
|
setuid = false;
|
|
setgid = true;
|
|
};
|
|
|
|
users.users = lib.optionalAttrs (user == "postfix") {
|
|
postfix = {
|
|
description = "Postfix mail server user";
|
|
uid = config.ids.uids.postfix;
|
|
group = group;
|
|
};
|
|
};
|
|
|
|
users.groups =
|
|
lib.optionalAttrs (group == "postfix") {
|
|
${group}.gid = config.ids.gids.postfix;
|
|
}
|
|
// lib.optionalAttrs (setgidGroup == "postdrop") {
|
|
${setgidGroup}.gid = config.ids.gids.postdrop;
|
|
};
|
|
|
|
systemd.services.postfix-setup = {
|
|
description = "Setup for Postfix mail server";
|
|
serviceConfig.RemainAfterExit = true;
|
|
serviceConfig.Type = "oneshot";
|
|
script = ''
|
|
# Backwards compatibility
|
|
if [ ! -d /var/lib/postfix ] && [ -d /var/postfix ]; then
|
|
mkdir -p /var/lib
|
|
mv /var/postfix /var/lib/postfix
|
|
fi
|
|
|
|
# All permissions set according ${pkgs.postfix}/etc/postfix/postfix-files script
|
|
mkdir -p /var/lib/postfix /var/lib/postfix/queue/{pid,public,maildrop}
|
|
chmod 0755 /var/lib/postfix
|
|
chown root:root /var/lib/postfix
|
|
|
|
rm -rf /var/lib/postfix/conf
|
|
mkdir -p /var/lib/postfix/conf
|
|
chmod 0755 /var/lib/postfix/conf
|
|
ln -sf ${pkgs.postfix}/etc/postfix/postfix-files /var/lib/postfix/conf/postfix-files
|
|
ln -sf ${mainCfFile} /var/lib/postfix/conf/main.cf
|
|
ln -sf ${masterCfFile} /var/lib/postfix/conf/master.cf
|
|
|
|
${lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList (to: from: ''
|
|
ln -sf ${from} /var/lib/postfix/conf/${to}
|
|
${pkgs.postfix}/bin/postalias -o -p /var/lib/postfix/conf/${to}
|
|
'') cfg.aliasFiles
|
|
)}
|
|
${lib.concatStringsSep "\n" (
|
|
lib.mapAttrsToList (to: from: ''
|
|
ln -sf ${from} /var/lib/postfix/conf/${to}
|
|
${pkgs.postfix}/bin/postmap /var/lib/postfix/conf/${to}
|
|
'') cfg.mapFiles
|
|
)}
|
|
|
|
mkdir -p /var/spool/mail
|
|
chown root:root /var/spool/mail
|
|
chmod a+rwxt /var/spool/mail
|
|
ln -sf /var/spool/mail /var/
|
|
|
|
#Finally delegate to postfix checking remain directories in /var/lib/postfix and set permissions on them
|
|
${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf
|
|
'';
|
|
};
|
|
|
|
systemd.services.postfix = {
|
|
description = "Postfix mail server";
|
|
|
|
documentation = [ "man:postfix(1)" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [
|
|
"network.target"
|
|
"postfix-setup.service"
|
|
];
|
|
requires = [ "postfix-setup.service" ];
|
|
path = [ pkgs.postfix ];
|
|
|
|
serviceConfig = {
|
|
Type = "forking";
|
|
Restart = "always";
|
|
PIDFile = "/var/lib/postfix/queue/pid/master.pid";
|
|
ExecStart = "${pkgs.postfix}/bin/postfix start";
|
|
ExecStop = "${pkgs.postfix}/bin/postfix stop";
|
|
ExecReload = "${pkgs.postfix}/bin/postfix reload";
|
|
|
|
# Hardening
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
ProtectSystem = "full";
|
|
CapabilityBoundingSet = [ "~CAP_NET_ADMIN CAP_SYS_ADMIN CAP_SYS_BOOT CAP_SYS_MODULE" ];
|
|
MemoryDenyWriteExecute = true;
|
|
ProtectKernelModules = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectControlGroups = true;
|
|
RestrictAddressFamilies = [
|
|
"AF_INET"
|
|
"AF_INET6"
|
|
"AF_NETLINK"
|
|
"AF_UNIX"
|
|
];
|
|
RestrictNamespaces = true;
|
|
RestrictRealtime = true;
|
|
};
|
|
};
|
|
|
|
services.postfix.config =
|
|
(lib.mapAttrs (_: v: lib.mkDefault v) {
|
|
compatibility_level = pkgs.postfix.version;
|
|
mail_owner = cfg.user;
|
|
default_privs = "nobody";
|
|
|
|
# NixOS specific locations
|
|
data_directory = "/var/lib/postfix/data";
|
|
queue_directory = "/var/lib/postfix/queue";
|
|
|
|
# Default location of everything in package
|
|
meta_directory = "${pkgs.postfix}/etc/postfix";
|
|
command_directory = "${pkgs.postfix}/bin";
|
|
sample_directory = "/etc/postfix";
|
|
newaliases_path = "${pkgs.postfix}/bin/newaliases";
|
|
mailq_path = "${pkgs.postfix}/bin/mailq";
|
|
readme_directory = false;
|
|
sendmail_path = "${pkgs.postfix}/bin/sendmail";
|
|
daemon_directory = "${pkgs.postfix}/libexec/postfix";
|
|
manpage_directory = "${pkgs.postfix}/share/man";
|
|
html_directory = "${pkgs.postfix}/share/postfix/doc/html";
|
|
shlib_directory = false;
|
|
mail_spool_directory = "/var/spool/mail/";
|
|
setgid_group = cfg.setgidGroup;
|
|
})
|
|
// lib.optionalAttrs (cfg.relayHost != "") {
|
|
relayhost =
|
|
if cfg.lookupMX then
|
|
"${cfg.relayHost}:${toString cfg.relayPort}"
|
|
else
|
|
"[${cfg.relayHost}]:${toString cfg.relayPort}";
|
|
}
|
|
// lib.optionalAttrs (!config.networking.enableIPv6) { inet_protocols = lib.mkDefault "ipv4"; }
|
|
// lib.optionalAttrs (cfg.networks != null) { mynetworks = cfg.networks; }
|
|
// lib.optionalAttrs (cfg.networksStyle != "") { mynetworks_style = cfg.networksStyle; }
|
|
// lib.optionalAttrs (cfg.hostname != "") { myhostname = cfg.hostname; }
|
|
// lib.optionalAttrs (cfg.domain != "") { mydomain = cfg.domain; }
|
|
// lib.optionalAttrs (cfg.origin != "") { myorigin = cfg.origin; }
|
|
// lib.optionalAttrs (cfg.destination != null) { mydestination = cfg.destination; }
|
|
// lib.optionalAttrs (cfg.relayDomains != null) { relay_domains = cfg.relayDomains; }
|
|
// lib.optionalAttrs (cfg.recipientDelimiter != "") {
|
|
recipient_delimiter = cfg.recipientDelimiter;
|
|
}
|
|
// lib.optionalAttrs haveAliases { alias_maps = [ "${cfg.aliasMapType}:/etc/postfix/aliases" ]; }
|
|
// lib.optionalAttrs haveTransport { transport_maps = [ "hash:/etc/postfix/transport" ]; }
|
|
// lib.optionalAttrs haveVirtual {
|
|
virtual_alias_maps = [ "${cfg.virtualMapType}:/etc/postfix/virtual" ];
|
|
}
|
|
// lib.optionalAttrs haveLocalRecipients {
|
|
local_recipient_maps = [
|
|
"hash:/etc/postfix/local_recipients"
|
|
] ++ lib.optional haveAliases "$alias_maps";
|
|
}
|
|
// lib.optionalAttrs (cfg.dnsBlacklists != [ ]) { smtpd_client_restrictions = clientRestrictions; }
|
|
// lib.optionalAttrs cfg.useSrs {
|
|
sender_canonical_maps = [ "tcp:127.0.0.1:10001" ];
|
|
sender_canonical_classes = [ "envelope_sender" ];
|
|
recipient_canonical_maps = [ "tcp:127.0.0.1:10002" ];
|
|
recipient_canonical_classes = [ "envelope_recipient" ];
|
|
}
|
|
// lib.optionalAttrs cfg.enableHeaderChecks {
|
|
header_checks = [ "regexp:/etc/postfix/header_checks" ];
|
|
}
|
|
// lib.optionalAttrs (cfg.tlsTrustedAuthorities != "") {
|
|
smtp_tls_CAfile = cfg.tlsTrustedAuthorities;
|
|
smtp_tls_security_level = lib.mkDefault "may";
|
|
};
|
|
|
|
services.postfix.masterConfig =
|
|
{
|
|
pickup = {
|
|
private = false;
|
|
wakeup = 60;
|
|
maxproc = 1;
|
|
};
|
|
cleanup = {
|
|
private = false;
|
|
maxproc = 0;
|
|
};
|
|
qmgr = {
|
|
private = false;
|
|
wakeup = 300;
|
|
maxproc = 1;
|
|
};
|
|
tlsmgr = {
|
|
wakeup = 1000;
|
|
wakeupUnusedComponent = false;
|
|
maxproc = 1;
|
|
};
|
|
rewrite = {
|
|
command = "trivial-rewrite";
|
|
};
|
|
bounce = {
|
|
maxproc = 0;
|
|
};
|
|
defer = {
|
|
maxproc = 0;
|
|
command = "bounce";
|
|
};
|
|
trace = {
|
|
maxproc = 0;
|
|
command = "bounce";
|
|
};
|
|
verify = {
|
|
maxproc = 1;
|
|
};
|
|
flush = {
|
|
private = false;
|
|
wakeup = 1000;
|
|
wakeupUnusedComponent = false;
|
|
maxproc = 0;
|
|
};
|
|
proxymap = {
|
|
command = "proxymap";
|
|
};
|
|
proxywrite = {
|
|
maxproc = 1;
|
|
command = "proxymap";
|
|
};
|
|
showq = {
|
|
private = false;
|
|
};
|
|
error = { };
|
|
retry = {
|
|
command = "error";
|
|
};
|
|
discard = { };
|
|
local = {
|
|
privileged = true;
|
|
};
|
|
virtual = {
|
|
privileged = true;
|
|
};
|
|
lmtp = {
|
|
};
|
|
anvil = {
|
|
maxproc = 1;
|
|
};
|
|
scache = {
|
|
maxproc = 1;
|
|
};
|
|
}
|
|
// lib.optionalAttrs cfg.enableSubmission {
|
|
submission = {
|
|
type = "inet";
|
|
private = false;
|
|
command = "smtpd";
|
|
args =
|
|
let
|
|
mkKeyVal = opt: val: [
|
|
"-o"
|
|
(opt + "=" + val)
|
|
];
|
|
in
|
|
lib.concatLists (lib.mapAttrsToList mkKeyVal cfg.submissionOptions);
|
|
};
|
|
}
|
|
// lib.optionalAttrs cfg.enableSmtp {
|
|
smtp_inet = {
|
|
name = "smtp";
|
|
type = "inet";
|
|
private = false;
|
|
command = "smtpd";
|
|
};
|
|
smtp = { };
|
|
relay = {
|
|
command = "smtp";
|
|
args = [
|
|
"-o"
|
|
"smtp_fallback_relay="
|
|
];
|
|
};
|
|
}
|
|
// lib.optionalAttrs cfg.enableSubmissions {
|
|
submissions = {
|
|
type = "inet";
|
|
private = false;
|
|
command = "smtpd";
|
|
args =
|
|
let
|
|
mkKeyVal = opt: val: [
|
|
"-o"
|
|
(opt + "=" + val)
|
|
];
|
|
adjustSmtpTlsSecurityLevel =
|
|
!(cfg.submissionsOptions ? smtpd_tls_security_level)
|
|
|| cfg.submissionsOptions.smtpd_tls_security_level == "none"
|
|
|| cfg.submissionsOptions.smtpd_tls_security_level == "may";
|
|
submissionsOptions =
|
|
cfg.submissionsOptions
|
|
// {
|
|
smtpd_tls_wrappermode = "yes";
|
|
}
|
|
// lib.optionalAttrs adjustSmtpTlsSecurityLevel {
|
|
smtpd_tls_security_level = "encrypt";
|
|
};
|
|
in
|
|
lib.concatLists (lib.mapAttrsToList mkKeyVal submissionsOptions);
|
|
};
|
|
};
|
|
}
|
|
|
|
(lib.mkIf haveAliases {
|
|
services.postfix.aliasFiles.aliases = aliasesFile;
|
|
})
|
|
(lib.mkIf haveCanonical {
|
|
services.postfix.mapFiles.canonical = canonicalFile;
|
|
})
|
|
(lib.mkIf haveTransport {
|
|
services.postfix.mapFiles.transport = transportFile;
|
|
})
|
|
(lib.mkIf haveVirtual {
|
|
services.postfix.mapFiles.virtual = virtualFile;
|
|
})
|
|
(lib.mkIf haveLocalRecipients {
|
|
services.postfix.mapFiles.local_recipients = localRecipientMapFile;
|
|
})
|
|
(lib.mkIf cfg.enableHeaderChecks {
|
|
services.postfix.mapFiles.header_checks = headerChecksFile;
|
|
})
|
|
(lib.mkIf (cfg.dnsBlacklists != [ ]) {
|
|
services.postfix.mapFiles.client_access = checkClientAccessFile;
|
|
})
|
|
]
|
|
);
|
|
|
|
imports = [
|
|
(lib.mkRemovedOptionModule [ "services" "postfix" "sslCACert" ]
|
|
"services.postfix.sslCACert was replaced by services.postfix.tlsTrustedAuthorities. In case you intend that your server should validate requested client certificates use services.postfix.extraConfig."
|
|
)
|
|
(lib.mkRemovedOptionModule [ "services" "postfix" "sslCert" ]
|
|
"services.postfix.sslCert was removed. Use services.postfix.config.smtpd_tls_chain_files for the server certificate, or services.postfix.config.smtp_tls_chain_files for the client certificate."
|
|
)
|
|
(lib.mkRemovedOptionModule [ "services" "postfix" "sslKey" ]
|
|
"services.postfix.sslKey was removed. Use services.postfix.config.smtpd_tls_chain_files for server private key, or services.postfix.config.smtp_tls_chain_files for the client private key."
|
|
)
|
|
|
|
(lib.mkChangedOptionModule
|
|
[ "services" "postfix" "useDane" ]
|
|
[ "services" "postfix" "config" "smtp_tls_security_level" ]
|
|
(config: lib.mkIf config.services.postfix.useDane "dane")
|
|
)
|
|
];
|
|
}
|