Merge branch 'pr-old-upstream-126' into 'master'
griff:my-changes See merge request simple-nixos-mailserver/nixos-mailserver!132
This commit is contained in:
commit
3aecb1299d
17 changed files with 696 additions and 193 deletions
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
*.cvd filter=lfs diff=lfs merge=lfs -text
|
|
@ -5,3 +5,4 @@ env:
|
|||
script:
|
||||
- nix-build tests/intern.nix
|
||||
- nix-build tests/extern.nix
|
||||
- nix-build tests/clamav.nix
|
||||
|
|
15
default.nix
15
default.nix
|
@ -454,6 +454,18 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
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 = mkEnableOption "monitoring via monit";
|
||||
|
||||
|
@ -733,8 +745,9 @@ in
|
|||
./mail-server/networking.nix
|
||||
./mail-server/systemd.nix
|
||||
./mail-server/dovecot.nix
|
||||
./mail-server/opendkim.nix
|
||||
./mail-server/postfix.nix
|
||||
./mail-server/rmilter.nix
|
||||
./mail-server/rspamd.nix
|
||||
./mail-server/nginx.nix
|
||||
./mail-server/kresd.nix
|
||||
./mail-server/post-upgrade-check.nix
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
{ dovecot, gawk, gnused, jq, runCommand }:
|
||||
|
||||
runCommand "dovecot-version" {
|
||||
buildInputs = [dovecot gnused jq];
|
||||
} ''
|
||||
jq -n \
|
||||
--arg dovecot_version "$(dovecot --version |
|
||||
sed 's/\([0-9.]*\).*/\1/' |
|
||||
awk -F '.' '{ print $1"."$2"."$3 }')" \
|
||||
'[$dovecot_version | split("."), ["major", "minor", "patch"]]
|
||||
| transpose | map( { (.[1]): .[0] | tonumber }) | add' > $out
|
||||
''
|
|
@ -23,11 +23,30 @@ let
|
|||
|
||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
||||
|
||||
dovecotVersion = builtins.fromJSON
|
||||
(builtins.readFile (pkgs.callPackage ./dovecot-version.nix {}));
|
||||
|
||||
# maildir in format "/${domain}/${user}"
|
||||
dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}";
|
||||
|
||||
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
|
||||
'';
|
||||
};
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
|
@ -69,6 +88,7 @@ in
|
|||
|
||||
protocol imap {
|
||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
||||
mail_plugins = $mail_plugins imap_sieve
|
||||
}
|
||||
|
||||
protocol pop3 {
|
||||
|
@ -77,15 +97,15 @@ in
|
|||
|
||||
mail_access_groups = ${vmailGroupName}
|
||||
ssl = required
|
||||
${lib.optionalString (dovecotVersion.major == 2 && dovecotVersion.minor >= 3) ''
|
||||
${lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.3") ''
|
||||
ssl_dh = <${certificateDirectory}/dh.pem
|
||||
''}
|
||||
|
||||
service lmtp {
|
||||
unix_listener /var/lib/postfix/queue/private/dovecot-lmtp {
|
||||
group = postfix
|
||||
unix_listener dovecot-lmtp {
|
||||
group = ${postfixCfg.group}
|
||||
mode = 0600
|
||||
user = postfix # TODO: < make variable
|
||||
user = ${postfixCfg.user}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,10 +124,10 @@ in
|
|||
}
|
||||
|
||||
service auth {
|
||||
unix_listener /var/lib/postfix/queue/private/auth {
|
||||
unix_listener auth {
|
||||
mode = 0660
|
||||
user = postfix # TODO: < make variable
|
||||
group = postfix # TODO: < make variable
|
||||
user = ${postfixCfg.user}
|
||||
group = ${postfixCfg.group}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,14 +139,40 @@ in
|
|||
}
|
||||
|
||||
plugin {
|
||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
||||
sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve
|
||||
sieve_default = file:/var/sieve/%u/default.sieve
|
||||
sieve_default_name = default
|
||||
|
||||
# From elsewhere to Spam folder
|
||||
imapsieve_mailbox1_name = Junk
|
||||
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 = Junk
|
||||
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
|
||||
}
|
||||
|
||||
lda_mailbox_autosubscribe = yes
|
||||
lda_mailbox_autocreate = yes
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.dovecot2.preStart = ''
|
||||
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'
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
15
mail-server/dovecot/imap_sieve/report-ham.sieve
Normal file
15
mail-server/dovecot/imap_sieve/report-ham.sieve
Normal file
|
@ -0,0 +1,15 @@
|
|||
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
mail-server/dovecot/imap_sieve/report-spam.sieve
Normal file
7
mail-server/dovecot/imap_sieve/report-spam.sieve
Normal file
|
@ -0,0 +1,7 @@
|
|||
require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
|
||||
|
||||
if environment :matches "imap.user" "*" {
|
||||
set "username" "${1}";
|
||||
}
|
||||
|
||||
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
|
3
mail-server/dovecot/pipe_bin/sa-learn-ham.sh
Executable file
3
mail-server/dovecot/pipe_bin/sa-learn-ham.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
set -o errexit
|
||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
3
mail-server/dovecot/pipe_bin/sa-learn-spam.sh
Executable file
3
mail-server/dovecot/pipe_bin/sa-learn-spam.sh
Executable file
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
set -o errexit
|
||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
|
|
@ -22,7 +22,7 @@ in
|
|||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
environment.systemPackages = with pkgs; [
|
||||
dovecot opendkim openssh postfix rspamd rmilter
|
||||
dovecot opendkim openssh postfix rspamd
|
||||
] ++ (if certificateScheme == 2 then [ openssl ] else []);
|
||||
};
|
||||
}
|
||||
|
|
90
mail-server/opendkim.nix
Normal file
90
mail-server/opendkim.nix
Normal file
|
@ -0,0 +1,90 @@
|
|||
# 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}" ] || [ ! -f "${dkim_txt}" ]
|
||||
then
|
||||
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
||||
-d "${dom}" \
|
||||
--directory="${cfg.dkimKeyDirectory}"
|
||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
|
||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
|
||||
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
|
||||
fi
|
||||
'';
|
||||
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
|
||||
create_dkim_cert =
|
||||
''
|
||||
# Create dkim dir
|
||||
mkdir -p "${cfg.dkimKeyDirectory}"
|
||||
chown ${dkimUser}:${dkimGroup} "${cfg.dkimKeyDirectory}"
|
||||
|
||||
${createAllCerts}
|
||||
|
||||
chown -R ${dkimUser}:${dkimGroup} "${cfg.dkimKeyDirectory}"
|
||||
'';
|
||||
|
||||
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;
|
||||
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
|
||||
configFile = pkgs.writeText "opendkim.conf" (''
|
||||
Canonicalization relaxed/simple
|
||||
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 = [ "${config.services.opendkim.group}" ];
|
||||
};
|
||||
systemd.services.opendkim = {
|
||||
preStart = create_dkim_cert;
|
||||
serviceConfig.ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
|
||||
};
|
||||
};
|
||||
}
|
|
@ -90,6 +90,19 @@ let
|
|||
|
||||
/^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
|
||||
+ (lib.optionalString cfg.debug ''
|
||||
debugLevel = 4
|
||||
''));
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
|
@ -121,16 +134,21 @@ in
|
|||
virtual_mailbox_domains = ${vhosts_file}
|
||||
virtual_mailbox_maps = hash:/var/lib/postfix/conf/valias
|
||||
virtual_alias_maps = hash:/var/lib/postfix/conf/valias
|
||||
virtual_transport = lmtp:unix:private/dovecot-lmtp
|
||||
virtual_transport = lmtp:unix:/run/dovecot2/dovecot-lmtp
|
||||
|
||||
# sasl with dovecot
|
||||
smtpd_sasl_type = dovecot
|
||||
smtpd_sasl_path = private/auth
|
||||
smtpd_sasl_path = /run/dovecot2/auth
|
||||
smtpd_sasl_auth_enable = yes
|
||||
smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination
|
||||
|
||||
# reject selected recipients, quota
|
||||
smtpd_recipient_restrictions = check_recipient_access hash:/var/lib/postfix/conf/reject_recipients, check_policy_service inet:localhost:12340
|
||||
policy-spf_time_limit = 3600s
|
||||
|
||||
# quota and spf checking
|
||||
smtpd_recipient_restrictions =
|
||||
check_recipient_access hash:/var/lib/postfix/conf/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
|
||||
|
@ -151,6 +169,11 @@ in
|
|||
|
||||
# Configure a non blocking source of randomness
|
||||
tls_random_source = dev:/dev/urandom
|
||||
|
||||
smtpd_milters = ${lib.concatStringsSep "," smtpdMilters}
|
||||
${lib.optionalString cfg.dkimSigning "non_smtpd_milters = 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 =
|
||||
|
@ -158,7 +181,7 @@ in
|
|||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_sasl_path = "private/auth";
|
||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||
smtpd_sasl_security_options = "noanonymous";
|
||||
smtpd_sasl_local_domain = "$myhostname";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
|
@ -167,11 +190,23 @@ in
|
|||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||
cleanup_service_name = "submission-header-cleanup";
|
||||
};
|
||||
|
||||
extraMasterConf = ''
|
||||
submission-header-cleanup unix n - n - 0 cleanup
|
||||
-o header_checks=pcre:${submissionHeaderCleanupRules}
|
||||
'';
|
||||
masterConfig = {
|
||||
"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}"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,74 +0,0 @@
|
|||
# nixos-mailserver: a simple mail server
|
||||
# Copyright (C) 2016-2018 Robin Raymond
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
|
||||
clamav = if cfg.virusScanning
|
||||
then
|
||||
''
|
||||
clamav {
|
||||
servers = /var/run/clamav/clamd.ctl;
|
||||
};
|
||||
''
|
||||
else "";
|
||||
dkim = if cfg.dkimSigning
|
||||
# Note: domain = "*"; causes Rmilter to try to search key in the key path
|
||||
# as keypath/domain.selector.key for any domain.
|
||||
then
|
||||
''
|
||||
dkim {
|
||||
domain {
|
||||
key = "${cfg.dkimKeyDirectory}";
|
||||
domain = "*";
|
||||
selector = "${cfg.dkimSelector}";
|
||||
};
|
||||
sign_alg = sha256;
|
||||
auth_only = yes;
|
||||
header_canon = relaxed;
|
||||
}
|
||||
''
|
||||
else "";
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
services.rspamd = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.rmilter = {
|
||||
inherit debug;
|
||||
enable = true;
|
||||
postfix.enable = true;
|
||||
rspamd = {
|
||||
enable = true;
|
||||
extraConfig = "extended_spam_headers = yes;";
|
||||
};
|
||||
extraConfig =
|
||||
''
|
||||
use_redis = true;
|
||||
max_size = 20M;
|
||||
|
||||
${clamav}
|
||||
|
||||
${dkim}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
88
mail-server/rspamd.nix
Normal file
88
mail-server/rspamd.nix
Normal file
|
@ -0,0 +1,88 @@
|
|||
# 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 = if rspamdCfg.socketActivation
|
||||
then "rspamd-rspamd_proxy-1.socket"
|
||||
else "rspamd.service";
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
services.rspamd = {
|
||||
enable = true;
|
||||
socketActivation = false;
|
||||
extraConfig = ''
|
||||
extended_spam_headers = yes;
|
||||
'' + (lib.optionalString cfg.virusScanning ''
|
||||
antivirus {
|
||||
clamav {
|
||||
action = "reject";
|
||||
symbol = "CLAM_VIRUS";
|
||||
type = "clamav";
|
||||
log_clean = true;
|
||||
servers = "/run/clamav/clamd.ctl";
|
||||
}
|
||||
}
|
||||
'');
|
||||
|
||||
workers.rspamd_proxy = {
|
||||
type = "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 = [];
|
||||
};
|
||||
|
||||
};
|
||||
systemd.services.rspamd = {
|
||||
requires = (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||
after = (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||
};
|
||||
|
||||
systemd.services.postfix = {
|
||||
after = [ rspamdSocket ];
|
||||
requires = [ rspamdSocket ];
|
||||
};
|
||||
|
||||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
||||
};
|
||||
}
|
||||
|
|
@ -19,8 +19,40 @@
|
|||
let
|
||||
cfg = config.mailserver;
|
||||
|
||||
create_certificate = if cfg.certificateScheme == 2 then
|
||||
createDhParameterFile =
|
||||
lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.3")
|
||||
''
|
||||
# Create a dh parameter file
|
||||
if [ ! -s "${cfg.certificateDirectory}/dh.pem" ]
|
||||
then
|
||||
mkdir -p "${cfg.certificateDirectory}"
|
||||
${pkgs.openssl}/bin/openssl \
|
||||
dhparam ${builtins.toString cfg.dhParamBitLength} \
|
||||
> "${cfg.certificateDirectory}/dh.pem"
|
||||
fi
|
||||
'';
|
||||
|
||||
preliminarySelfsigned = config.security.acme.preliminarySelfsigned;
|
||||
acmeWantsTarget = [ "acme-certificates.target" ]
|
||||
++ (lib.optional preliminarySelfsigned "acme-selfsigned-certificates.target");
|
||||
acmeAfterTarget = if preliminarySelfsigned
|
||||
then [ "acme-selfsigned-certificates.target" ]
|
||||
else [ "acme-certificates.target" ];
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
# Add target for when certificates are available
|
||||
systemd.targets."mailserver-certificates" = {
|
||||
wants = lib.mkIf (cfg.certificateScheme == 3) acmeWantsTarget;
|
||||
after = lib.mkIf (cfg.certificateScheme == 3) acmeAfterTarget;
|
||||
};
|
||||
|
||||
# Create self signed certificate
|
||||
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
|
||||
wantedBy = [ "mailserver-certificates.target" ];
|
||||
after = [ "local-fs.target" ];
|
||||
before = [ "mailserver-certificates.target" ];
|
||||
script = ''
|
||||
# Create certificates if they do not exist yet
|
||||
dir="${cfg.certificateDirectory}"
|
||||
fqdn="${cfg.fqdn}"
|
||||
|
@ -35,83 +67,35 @@ let
|
|||
"${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \
|
||||
-days 3650 -out "''${cert}"
|
||||
fi
|
||||
''
|
||||
else "";
|
||||
|
||||
createDhParameterFile =
|
||||
''
|
||||
# Create a dh parameter file
|
||||
if [ ! -s "${cfg.certificateDirectory}/dh.pem" ]
|
||||
then
|
||||
mkdir -p "${cfg.certificateDirectory}"
|
||||
${pkgs.openssl}/bin/openssl \
|
||||
dhparam ${builtins.toString cfg.dhParamBitLength} \
|
||||
> "${cfg.certificateDirectory}/dh.pem"
|
||||
fi
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = true;
|
||||
};
|
||||
};
|
||||
|
||||
createDomainDkimCert = dom:
|
||||
let
|
||||
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
|
||||
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
|
||||
in
|
||||
''
|
||||
if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
|
||||
then
|
||||
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
||||
-d "${dom}" \
|
||||
--directory="${cfg.dkimKeyDirectory}"
|
||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
|
||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
|
||||
fi
|
||||
'';
|
||||
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
|
||||
create_dkim_cert =
|
||||
''
|
||||
# Create dkim dir
|
||||
mkdir -p "${cfg.dkimKeyDirectory}"
|
||||
chown rmilter:rmilter "${cfg.dkimKeyDirectory}"
|
||||
|
||||
${createAllCerts}
|
||||
|
||||
chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}"
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
# Make sure postfix gets started first, so that the certificates are in place
|
||||
systemd.services.dovecot2.after = [ "postfix.service" ];
|
||||
|
||||
# Create certificates and maildir folder
|
||||
systemd.services.postfix = {
|
||||
after = (if (certificateScheme == 3) then [ "nginx.service" ] else []);
|
||||
preStart =
|
||||
''
|
||||
# Create maildir folder and dh parameters before dovecot startup
|
||||
systemd.services.dovecot2 = {
|
||||
after = [ "mailserver-certificates.target" ];
|
||||
wants = [ "mailserver-certificates.target" ];
|
||||
preStart = ''
|
||||
# Create mail directory and set permissions. See
|
||||
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
||||
mkdir -p "${mailDirectory}"
|
||||
chgrp "${vmailGroupName}" "${mailDirectory}"
|
||||
chmod 02770 "${mailDirectory}"
|
||||
|
||||
${create_certificate}
|
||||
|
||||
${let
|
||||
dovecotVersion = builtins.fromJSON
|
||||
(builtins.readFile (pkgs.callPackage ./dovecot-version.nix {}));
|
||||
in lib.optionalString
|
||||
(dovecotVersion.major == 2 && dovecotVersion.minor >= 3)
|
||||
createDhParameterFile}
|
||||
${createDhParameterFile}
|
||||
'';
|
||||
};
|
||||
|
||||
# Create dkim certificates
|
||||
systemd.services.rmilter = {
|
||||
requires = [ "rmilter.socket" ];
|
||||
after = [ "rmilter.socket" ];
|
||||
preStart =
|
||||
''
|
||||
${create_dkim_cert}
|
||||
'';
|
||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
||||
systemd.services.postfix = {
|
||||
after = [ "dovecot2.service" "mailserver-certificates.target" ]
|
||||
++ (lib.optional cfg.dkimSigning "opendkim.service");
|
||||
wants = [ "mailserver-certificates.target" ];
|
||||
requires = [ "dovecot2.service" ]
|
||||
++ (lib.optional cfg.dkimSigning "opendkim.service");
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
228
tests/clamav.nix
Normal file
228
tests/clamav.nix
Normal file
|
@ -0,0 +1,228 @@
|
|||
# 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/>
|
||||
|
||||
import <nixpkgs/nixos/tests/make-test.nix> {
|
||||
|
||||
nodes = {
|
||||
server = { config, pkgs, lib, ... }:
|
||||
let
|
||||
clamav-db = pkgs.srcOnly {
|
||||
name = "ClamAV-db";
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://files.griff.name/ClamAV-db.tar";
|
||||
sha256 = "eecad99f4c071d216bd91565f84c0d90a1f93e5e3e22d8f3087686ba3bd219e7";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
];
|
||||
|
||||
virtualisation.memorySize = 1500;
|
||||
|
||||
services.rsyslogd = {
|
||||
enable = true;
|
||||
defaultConfig = ''
|
||||
*.* /dev/console
|
||||
'';
|
||||
};
|
||||
|
||||
services.clamav.updater.enable = lib.mkForce false;
|
||||
systemd.services.old-clam = {
|
||||
before = [ "clamav-daemon.service" ];
|
||||
requiredBy = [ "clamav-daemon.service" ];
|
||||
description = "ClamAV virus database";
|
||||
|
||||
preStart = ''
|
||||
mkdir -m 0755 -p /var/lib/clamav
|
||||
chown clamav:clamav /var/lib/clamav
|
||||
'';
|
||||
|
||||
script = ''
|
||||
cp ${clamav-db}/bytecode.cvd /var/lib/clamav/
|
||||
cp ${clamav-db}/main.cvd /var/lib/clamav/
|
||||
cp ${clamav-db}/daily.cvd /var/lib/clamav/
|
||||
chown clamav:clamav /var/lib/clamav/*
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
PrivateTmp = "yes";
|
||||
PrivateDevices = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
debug = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" "example2.com" ];
|
||||
dhParamBitLength = 512;
|
||||
virusScanning = true;
|
||||
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
||||
aliases = [ "postmaster@example.com" ];
|
||||
catchAll = [ "example.com" ];
|
||||
};
|
||||
"user@example2.com" = {
|
||||
hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
|
||||
};
|
||||
};
|
||||
enableImap = true;
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
||||
};
|
||||
};
|
||||
client = { nodes, config, pkgs, ... }: let
|
||||
serverIP = nodes.server.config.networking.primaryIPAddress;
|
||||
clientIP = nodes.client.config.networking.primaryIPAddress;
|
||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
echo grep '${clientIP}' "$@" >&2
|
||||
exec grep '${clientIP}' "$@"
|
||||
'';
|
||||
in {
|
||||
environment.systemPackages = with pkgs; [
|
||||
fetchmail msmtp procmail findutils grep-ip
|
||||
];
|
||||
environment.etc = {
|
||||
"root/.fetchmailrc" = {
|
||||
text = ''
|
||||
poll ${serverIP} with proto IMAP
|
||||
user 'user1@example.com' there with password 'user1' is 'root' here
|
||||
mda procmail
|
||||
'';
|
||||
mode = "0700";
|
||||
};
|
||||
"root/.procmailrc" = {
|
||||
text = "DEFAULT=$HOME/mail";
|
||||
};
|
||||
"root/.msmtprc" = {
|
||||
text = ''
|
||||
account test2
|
||||
host ${serverIP}
|
||||
port 587
|
||||
from user@example2.com
|
||||
user user@example2.com
|
||||
password user2
|
||||
'';
|
||||
};
|
||||
"root/virus-email".text = ''
|
||||
From: User2 <user@example2.com>
|
||||
Content-Type: multipart/mixed;
|
||||
boundary="Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607"
|
||||
Mime-Version: 1.0 (Mac OS X Mail 11.3 \(3445.6.18\))
|
||||
Subject: Testy McTest
|
||||
Message-Id: <94550DD9-1FF1-4ED1-9F09-8812FF2E59AA@example.com>
|
||||
Date: Sat, 12 May 2018 14:15:44 +0200
|
||||
To: User1 <user1@example.com>
|
||||
X-Mailer: Apple Mail (2.3445.6.18)
|
||||
|
||||
|
||||
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
|
||||
Content-Transfer-Encoding: 7bit
|
||||
Content-Type: text/plain;
|
||||
charset=us-ascii
|
||||
|
||||
Hello
|
||||
|
||||
I have attached a dangerous virus.
|
||||
|
||||
Mfg.
|
||||
User2
|
||||
|
||||
|
||||
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607
|
||||
Content-Disposition: attachment;
|
||||
filename=eicar.com.txt
|
||||
Content-Type: text/plain;
|
||||
x-unix-mode=0644;
|
||||
name="eicar.com.txt"
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*
|
||||
--Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607--
|
||||
'';
|
||||
"root/email2".text = ''
|
||||
From: User <user@example2.com>
|
||||
To: User1 <user1@example.com>
|
||||
Cc:
|
||||
Bcc:
|
||||
Subject: This is a test Email from user@example2.com to user1
|
||||
Reply-To:
|
||||
|
||||
Hello User1,
|
||||
|
||||
how are you doing today?
|
||||
|
||||
XOXO User1
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript =
|
||||
''
|
||||
startAll;
|
||||
|
||||
$server->waitForUnit("multi-user.target");
|
||||
$client->waitForUnit("multi-user.target");
|
||||
|
||||
$client->execute("cp -p /etc/root/.* ~/");
|
||||
$client->succeed("mkdir -p ~/mail");
|
||||
$client->succeed("ls -la ~/ >&2");
|
||||
$client->succeed("cat ~/.fetchmailrc >&2");
|
||||
$client->succeed("cat ~/.procmailrc >&2");
|
||||
$client->succeed("cat ~/.msmtprc >&2");
|
||||
|
||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||
$client->succeed("fetchmail -v || [ \$? -eq 1 ] >&2");
|
||||
|
||||
# Verify that mail can be sent and received before testing virus scanner
|
||||
$client->execute("rm ~/mail/*");
|
||||
$client->succeed("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2");
|
||||
# give the mail server some time to process the mail
|
||||
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||
$client->execute("rm ~/mail/*");
|
||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||
$client->succeed("fetchmail -v >&2");
|
||||
$client->execute("rm ~/mail/*");
|
||||
|
||||
|
||||
subtest "virus scan file", sub {
|
||||
$server->fail("clamscan --follow-file-symlinks=2 -r /etc/root/ >&2");
|
||||
};
|
||||
|
||||
subtest "virus scanner", sub {
|
||||
$client->fail("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/virus-email >&2");
|
||||
# give the mail server some time to process the mail
|
||||
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||
};
|
||||
|
||||
subtest "no warnings or errors", sub {
|
||||
$server->fail("journalctl -u postfix | grep -i error >&2");
|
||||
$server->fail("journalctl -u postfix | grep -i warning >&2");
|
||||
$server->fail("journalctl -u dovecot2 | grep -i error >&2");
|
||||
$server->fail("journalctl -u dovecot2 | grep -i warning >&2");
|
||||
};
|
||||
|
||||
'';
|
||||
}
|
|
@ -23,6 +23,14 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
|||
../default.nix
|
||||
];
|
||||
|
||||
services.rsyslogd = {
|
||||
enable = true;
|
||||
defaultConfig = ''
|
||||
*.* /dev/console
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
debug = true;
|
||||
|
@ -56,6 +64,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
|||
};
|
||||
|
||||
enableImap = true;
|
||||
enableImapSsl = true;
|
||||
};
|
||||
};
|
||||
client = { nodes, config, pkgs, ... }: let
|
||||
|
@ -71,9 +80,63 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
|||
echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2
|
||||
exec grep '^Message-ID:.*@mail.example.com>$' "$@"
|
||||
'';
|
||||
test-imap-spam = pkgs.writeScriptBin "imap-mark-spam" ''
|
||||
#!${pkgs.python3.interpreter}
|
||||
import imaplib
|
||||
|
||||
with imaplib.IMAP4_SSL('${serverIP}') as imap:
|
||||
imap.login('user1@example.com', 'user1')
|
||||
imap.select()
|
||||
status, [response] = imap.search(None, 'ALL')
|
||||
msg_ids = response.decode("utf-8").split(' ')
|
||||
print(msg_ids)
|
||||
assert status == 'OK'
|
||||
assert len(msg_ids) == 1
|
||||
|
||||
imap.copy(','.join(msg_ids), 'Junk')
|
||||
for num in msg_ids:
|
||||
imap.store(num, '+FLAGS', '\\Deleted')
|
||||
imap.expunge()
|
||||
|
||||
imap.select('Junk')
|
||||
status, [response] = imap.search(None, 'ALL')
|
||||
msg_ids = response.decode("utf-8").split(' ')
|
||||
print(msg_ids)
|
||||
assert status == 'OK'
|
||||
assert len(msg_ids) == 1
|
||||
|
||||
imap.close()
|
||||
'';
|
||||
test-imap-ham = pkgs.writeScriptBin "imap-mark-ham" ''
|
||||
#!${pkgs.python3.interpreter}
|
||||
import imaplib
|
||||
|
||||
with imaplib.IMAP4_SSL('${serverIP}') as imap:
|
||||
imap.login('user1@example.com', 'user1')
|
||||
imap.select('Junk')
|
||||
status, [response] = imap.search(None, 'ALL')
|
||||
msg_ids = response.decode("utf-8").split(' ')
|
||||
print(msg_ids)
|
||||
assert status == 'OK'
|
||||
assert len(msg_ids) == 1
|
||||
|
||||
imap.copy(','.join(msg_ids), 'INBOX')
|
||||
for num in msg_ids:
|
||||
imap.store(num, '+FLAGS', '\\Deleted')
|
||||
imap.expunge()
|
||||
|
||||
imap.select('INBOX')
|
||||
status, [response] = imap.search(None, 'ALL')
|
||||
msg_ids = response.decode("utf-8").split(' ')
|
||||
print(msg_ids)
|
||||
assert status == 'OK'
|
||||
assert len(msg_ids) == 1
|
||||
|
||||
imap.close()
|
||||
'';
|
||||
in {
|
||||
environment.systemPackages = with pkgs; [
|
||||
fetchmail msmtp procmail findutils grep-ip check-mail-id
|
||||
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham
|
||||
];
|
||||
environment.etc = {
|
||||
"root/.fetchmailrc" = {
|
||||
|
@ -87,7 +150,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
|||
"root/.fetchmailRcLowQuota" = {
|
||||
text = ''
|
||||
poll ${serverIP} with proto IMAP
|
||||
user 'lowquota\@example.com' there with password 'user1' is 'root' here
|
||||
user 'lowquota@example.com' there with password 'user2' is 'root' here
|
||||
mda procmail
|
||||
'';
|
||||
mode = "0700";
|
||||
|
@ -217,7 +280,7 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
|||
$client->waitForUnit("multi-user.target");
|
||||
|
||||
$client->execute("cp -p /etc/root/.* ~/");
|
||||
$client->succeed("mkdir ~/mail");
|
||||
$client->succeed("mkdir -p ~/mail");
|
||||
$client->succeed("ls -la ~/ >&2");
|
||||
$client->succeed("cat ~/.fetchmailrc >&2");
|
||||
$client->succeed("cat ~/.procmailrc >&2");
|
||||
|
@ -317,6 +380,18 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
|||
|
||||
};
|
||||
|
||||
subtest "imap sieve junk trainer", sub {
|
||||
# send email from user2 to user1
|
||||
$client->succeed("msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2");
|
||||
# give the mail server some time to process the mail
|
||||
$server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]');
|
||||
|
||||
$client->succeed("imap-mark-spam >&2");
|
||||
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2");
|
||||
$client->succeed("imap-mark-ham >&2");
|
||||
$server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2");
|
||||
};
|
||||
|
||||
subtest "no warnings or errors", sub {
|
||||
$server->fail("journalctl -u postfix | grep -i error >&2");
|
||||
$server->fail("journalctl -u postfix | grep -i warning >&2");
|
||||
|
|
Loading…
Reference in a new issue