use PVE::Tools;
use PVE::JSONSchema qw(get_standard_option);
use PVE::SectionConfig;
+use PVE::Network;
use base qw(PVE::SectionConfig);
sub properties {
return {
advfilter => {
- description => "Use advanced filters for statistic.",
+ description => "Enable advanced filters for statistic.",
+ verbose_description => <<EODESC,
+Enable advanced filters for statistic.
+
+If this is enabled, the receiver statistic are limited to active ones
+(receivers which also sent out mail in the 90 days before), and the contact
+statistic will not contain these active receivers.
+EODESC
type => 'boolean',
- default => 1,
+ default => 0,
},
dailyreport => {
description => "Send daily reports.",
use_bayes => {
description => "Whether to use the naive-Bayesian-style classifier.",
type => 'boolean',
- default => 1,
+ default => 0,
},
use_awl => {
description => "Use the Auto-Whitelist plugin.",
type => 'boolean',
- default => 1,
+ default => 0,
},
use_razor => {
description => "Whether to use Razor2, if it is available.",
type => 'string',
},
clamav_heuristic_score => {
- description => "Score for ClamAV heuristics (Encrypted Archives/Documents, Google Safe Browsing database, PhishingScanURLs, ...).",
+ description => "Score for ClamAV heuristics (Encrypted Archives/Documents, PhishingScanURLs, ...).",
type => 'integer',
minimum => 0,
maximum => 1000,
minimum => 64,
default => 256*1024,
},
+ extract_text => {
+ description => "Extract text from attachments (doc, pdf, rtf, images) and scan for spam.",
+ type => 'boolean',
+ default => 0,
+ },
};
}
bounce_score => { optional => 1 },
rbl_checks => { optional => 1 },
maxspamsize => { optional => 1 },
+ extract_text => { optional => 1 },
};
}
description => "Text for 'From' header in daily spam report mails.",
type => 'string',
},
+ quarantinelink => {
+ description => "Enables user self-service for Quarantine Links. Caution: this is accessible without authentication",
+ type => 'boolean',
+ default => 0,
+ },
};
}
allowhrefs => { optional => 1 },
port => { optional => 1 },
protocol => { optional => 1 },
+ quarantinelink => { optional => 1 },
};
}
minimum => 0,
default => 0,
},
+ # FIXME: remove for PMG 8.0 - https://blog.clamav.net/2021/04/are-you-still-attempting-to-download.html
safebrowsing => {
- description => "Enables support for Google Safe Browsing.",
+ description => "Enables support for Google Safe Browsing. (deprecated option, will be ignored)",
+ type => 'boolean',
+ default => 0
+ },
+ scriptedupdates => {
+ description => "Enables ScriptedUpdates (incremental download of signatures)",
type => 'boolean',
default => 1
},
maxscansize => { optional => 1 },
dbmirror => { optional => 1 },
maxcccount => { optional => 1 },
- safebrowsing => { optional => 1 },
+ safebrowsing => { optional => 1 }, # FIXME: remove for PMG 8.0
+ scriptedupdates => { optional => 1},
};
}
my $max_servers = 5;
my $servermem = 120;
+ my $base;
my $memory = physical_memory();
- my $add_servers = int(($memory - 512)/$servermem);
+ if ($memory < 3840) {
+ warn "low amount of system memory installed, recommended is 4+ GB\n"
+ ."to prevent OOM kills, it is better to set max_filters manually\n";
+ $base = $memory > 1536 ? 1024 : 512;
+ } else {
+ $base = 2816;
+ $servermem = 150;
+ }
+ my $add_servers = int(($memory - $base)/$servermem);
$max_servers += $add_servers if $add_servers > 0;
$max_servers = 40 if $max_servers > 40;
default => 0,
},
smarthost => {
- description => "When set, all outgoing mails are deliverd to the specified smarthost.",
+ description => "When set, all outgoing mails are deliverd to the specified smarthost."
+ ." (postfix option `default_transport`)",
type => 'string', format => 'address',
},
smarthostport => {
- description => "SMTP port number for smarthost.",
+ description => "SMTP port number for smarthost. (postfix option `default_transport`)",
type => 'integer',
minimum => 1,
maximum => 65535,
default => 0,
},
maxsize => {
- description => "Maximum email size. Larger mails are rejected.",
+ description => "Maximum email size. Larger mails are rejected. (postfix option `message_size_limit`)",
type => 'integer',
minimum => 1024,
default => 1024*1024*10,
},
dwarning => {
- description => "SMTP delay warning time (in hours).",
+ description => "SMTP delay warning time (in hours). (postfix option `delay_warning_time`)",
type => 'integer',
minimum => 0,
default => 4,
default => 1,
},
greylist => {
- description => "Use Greylisting.",
+ description => "Use Greylisting for IPv4.",
type => 'boolean',
default => 1,
},
+ greylistmask4 => {
+ description => "Netmask to apply for greylisting IPv4 hosts",
+ type => 'integer',
+ minimum => 0,
+ maximum => 32,
+ default => 24,
+ },
+ greylist6 => {
+ description => "Use Greylisting for IPv6.",
+ type => 'boolean',
+ default => 0,
+ },
+ greylistmask6 => {
+ description => "Netmask to apply for greylisting IPv6 hosts",
+ type => 'integer',
+ minimum => 0,
+ maximum => 128,
+ default => 64,
+ },
helotests => {
- description => "Use SMTP HELO tests.",
+ description => "Use SMTP HELO tests. (postfix option `smtpd_helo_restrictions`)",
type => 'boolean',
default => 0,
},
rejectunknown => {
- description => "Reject unknown clients.",
+ description => "Reject unknown clients. (postfix option `reject_unknown_client_hostname`)",
type => 'boolean',
default => 0,
},
rejectunknownsender => {
- description => "Reject unknown senders.",
+ description => "Reject unknown senders. (postfix option `reject_unknown_sender_domain`)",
type => 'boolean',
default => 0,
},
verifyreceivers => {
- description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
+ description => "Enable receiver verification. The value spefifies the numerical reply"
+ ." code when the Postfix SMTP server rejects a recipient address."
+ ." (postfix options `reject_unknown_recipient_domain`, `reject_unverified_recipient`,"
+ ." and `unverified_recipient_reject_code`)",
type => 'string',
enum => ['450', '550'],
},
dnsbl_sites => {
- description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
+ description => "Optional list of DNS white/blacklist domains (postfix option `postscreen_dnsbl_sites`).",
type => 'string', format => 'dnsbl-entry-list',
},
dnsbl_threshold => {
- description => "The inclusive lower bound for blocking a remote SMTP client, based on its combined DNSBL score (see postscreen_dnsbl_threshold parameter).",
+ description => "The inclusive lower bound for blocking a remote SMTP client, based on"
+ ." its combined DNSBL score (postfix option `postscreen_dnsbl_threshold`).",
type => 'integer',
minimum => 0,
default => 1
type => 'boolean',
default => 0
},
+ smtputf8 => {
+ description => "Enable SMTPUTF8 support in Postfix and detection for locally generated mail (postfix option `smtputf8_enable`)",
+ type => 'boolean',
+ default => 1
+ },
};
}
max_smtpd_in => { optional => 1 },
max_smtpd_out => { optional => 1 },
greylist => { optional => 1 },
+ greylistmask4 => { optional => 1 },
+ greylist6 => { optional => 1 },
+ greylistmask6 => { optional => 1 },
helotests => { optional => 1 },
tls => { optional => 1 },
tlslog => { optional => 1 },
dnsbl_threshold => { optional => 1 },
before_queue_filtering => { optional => 1 },
ndr_on_block => { optional => 1 },
+ smtputf8 => { optional => 1 },
};
}
sub read_pmg_conf {
my ($filename, $fh) = @_;
- local $/ = undef; # slurp mode
-
- my $raw = <$fh> if defined($fh);
+ my $raw;
+ $raw = do { local $/ = undef; <$fh> } if defined($fh);
return PMG::Config::Base->parse_config($filename, $raw);
}
if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
my ($network, $prefix_size, $comment) = ($1, $2, $3);
my $cidr = "$network/${prefix_size}";
+ # FIXME: Drop unused `network_address` and `prefix_size` with PMG 8.0
$mynetworks->{$cidr} = {
cidr => $cidr,
network_address => $network,
return $policy;
}
+PVE::JSONSchema::register_format(
+ 'transport-domain-or-nexthop', \&pmg_verify_transport_domain_or_nexthop);
+
+sub pmg_verify_transport_domain_or_nexthop {
+ my ($name, $noerr) = @_;
+
+ if (pmg_verify_transport_domain($name, 1)) {
+ return $name;
+ } elsif ($name =~ m/^(\S+)(?::\d+)?$/) {
+ my $nexthop = $1;
+ if ($nexthop =~ m/^\[(.*)\]$/) {
+ $nexthop = $1;
+ }
+ return $name if pmg_verify_transport_address($nexthop, 1);
+ } else {
+ return undef if $noerr;
+ die "value does not look like a valid domain or next-hop\n";
+ }
+}
+
sub read_tls_policy {
my ($filename, $fh) = @_;
my $parse_error = sub {
my ($err) = @_;
- die "parse error in '$filename': $line - $err";
+ warn "parse error in '$filename': $line - $err\n";
};
if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
- my ($domain, $policy) = ($1, $2);
+ my ($destination, $policy) = ($1, $2);
eval {
- pmg_verify_transport_domain($domain);
+ pmg_verify_transport_domain_or_nexthop($destination);
pmg_verify_tls_policy($policy);
};
if (my $err = $@) {
next;
}
- $tls_policy->{$domain} = {
- domain => $domain,
+ $tls_policy->{$destination} = {
+ destination => $destination,
policy => $policy,
};
} else {
return if !$tls_policy;
- foreach my $domain (sort keys %$tls_policy) {
- my $entry = $tls_policy->{$domain};
+ foreach my $destination (sort keys %$tls_policy) {
+ my $entry = $tls_policy->{$destination};
PVE::Tools::safe_print(
- $filename, $fh, "$entry->{domain} $entry->{policy}\n");
+ $filename, $fh, "$entry->{destination} $entry->{policy}\n");
}
}
PMG::Utils::run_postmap($tls_policy_map_filename);
}
+sub read_tls_inbound_domains {
+ my ($filename, $fh) = @_;
+
+ return {} if !defined($fh);
+
+ my $domains = {};
+
+ while (defined(my $line = <$fh>)) {
+ chomp $line;
+ next if $line =~ m/^\s*$/;
+ next if $line =~ m/^#(.*)\s*$/;
+
+ my $parse_error = sub {
+ my ($err) = @_;
+ warn "parse error in '$filename': $line - $err\n";
+ };
+
+ if ($line =~ m/^(\S+) reject_plaintext_session$/) {
+ my $domain = $1;
+
+ eval { pmg_verify_transport_domain($domain) };
+ if (my $err = $@) {
+ $parse_error->($err);
+ next;
+ }
+
+ $domains->{$domain} = 1;
+ } else {
+ $parse_error->('wrong format');
+ }
+ }
+
+ return $domains;
+}
+
+sub write_tls_inbound_domains {
+ my ($filename, $fh, $domains) = @_;
+
+ return if !$domains;
+
+ foreach my $domain (sort keys %$domains) {
+ PVE::Tools::safe_print($filename, $fh, "$domain reject_plaintext_session\n");
+ }
+}
+
+my $tls_inbound_domains_map_filename = "/etc/pmg/tls_inbound_domains";
+PVE::INotify::register_file('tls_inbound_domains', $tls_inbound_domains_map_filename,
+ \&read_tls_inbound_domains,
+ \&write_tls_inbound_domains,
+ undef, always_call_parser => 1);
+
+sub postmap_tls_inbound_domains {
+ PMG::Utils::run_postmap($tls_inbound_domains_map_filename);
+}
+
my $transport_map_filename = "/etc/pmg/transport";
sub postmap_pmg_transport {
PMG::Utils::run_postmap($transport_map_filename);
}
+PVE::JSONSchema::register_format(
+ 'transport-address', \&pmg_verify_transport_address);
+
+sub pmg_verify_transport_address {
+ my ($name, $noerr) = @_;
+
+ if ($name =~ m/^ipv6:($IPV6RE)$/i) {
+ return $name;
+ } elsif (PVE::JSONSchema::pve_verify_address($name, 1)) {
+ return $name;
+ } else {
+ return undef if $noerr;
+ die "value does not look like a valid address\n";
+ }
+}
+
sub read_transport_map {
my ($filename, $fh) = @_;
$host = $1;
$use_mx = 0;
}
- $use_mx = 0 if ($protocol eq "lmtp");
+ $use_mx = 0 if ($protocol eq "lmtp");
- eval { PVE::JSONSchema::pve_verify_address($host); };
+ eval { pmg_verify_transport_address($host); };
if (my $err = $@) {
$parse_error->($err);
next;
PVE::Tools::safe_print($filename, $fh, "#$comment\n")
if defined($comment) && $comment !~ m/^\s*$/;
- my $use_mx = $data->{use_mx};
- $use_mx = 0 if $data->{host} =~ m/^(?:$IPV4RE|$IPV6RE)$/;
-
- my $is_lmtp = 0;
- $is_lmtp = 1 if $data->{protocol} eq "lmtp";
+ my $bracket_host = !$data->{use_mx};
- if ($is_lmtp) {
- $data->{protocol} = "lmtp:inet";
+ if ($data->{protocol} eq 'lmtp') {
+ $bracket_host = 0;
+ $data->{protocol} .= ":inet";
}
+ $bracket_host = 1 if $data->{host} =~ m/^(?:$IPV4RE|(?:ipv6:)?$IPV6RE)$/i;
+ my $host = $bracket_host ? "[$data->{host}]" : $data->{host};
- if ($use_mx or $is_lmtp) {
- PVE::Tools::safe_print(
- $filename, $fh, "$data->{domain} $data->{protocol}:$data->{host}:$data->{port}\n");
- } else {
- PVE::Tools::safe_print(
- $filename, $fh, "$data->{domain} $data->{protocol}:[$data->{host}]:$data->{port}\n");
- }
+ PVE::Tools::safe_print($filename, $fh, "$data->{domain} $data->{protocol}:$host:$data->{port}\n");
}
}
my $resolv = PVE::INotify::read_file('resolvconf');
my $domain = $resolv->{search} // 'localdomain';
+ # postfix will not parse a hostname with trailing '.'
+ $domain =~ s/^(.*)\.$/$1/;
$dnsinfo->{domain} = $domain;
$dnsinfo->{fqdn} = "$nodename.$domain";
my $int_ip = PMG::Cluster::remote_node_ip($dnsinfo->{hostname});
$vars->{ipconfig}->{int_ip} = $int_ip;
- my $transportnets = [];
+ my $transportnets = {};
+ my $mynetworks = {
+ '127.0.0.0/8' => 1,
+ '[::1]/128', => 1,
+ };
if (my $tmap = PVE::INotify::read_file('transport')) {
- foreach my $domain (sort keys %$tmap) {
+ foreach my $domain (keys %$tmap) {
my $data = $tmap->{$domain};
my $host = $data->{host};
if ($host =~ m/^$IPV4RE$/) {
- push @$transportnets, "$host/32";
- } elsif ($host =~ m/^$IPV6RE$/) {
- push @$transportnets, "[$host]/128";
+ $transportnets->{"$host/32"} = 1;
+ $mynetworks->{"$host/32"} = 1;
+ } elsif ($host =~ m/^(?:ipv6:)?($IPV6RE)$/i) {
+ $transportnets->{"[$1]/128"} = 1;
+ $mynetworks->{"[$1]/128"} = 1;
}
}
}
- $vars->{postfix}->{transportnets} = join(' ', @$transportnets);
-
- my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
+ $vars->{postfix}->{transportnets} = join(' ', sort keys %$transportnets);
- if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
- if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
- push @$mynetworks, "[$1]/$2";
- } else {
- push @$mynetworks, $int_net_cidr;
- }
- } else {
- if ($int_ip =~ m/^$IPV6RE$/) {
- push @$mynetworks, "[$int_ip]/128";
+ if (defined($int_ip)) { # we cannot really do anything and the loopback nets are already added
+ if (my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip, 1)) {
+ if ($int_net_cidr =~ m/^($IPV6RE)\/(\d+)$/) {
+ $mynetworks->{"[$1]/$2"} = 1;
+ } else {
+ $mynetworks->{$int_net_cidr} = 1;
+ }
} else {
- push @$mynetworks, "$int_ip/32";
+ if ($int_ip =~ m/^$IPV6RE$/) {
+ $mynetworks->{"[$int_ip]/128"} = 1;
+ } else {
+ $mynetworks->{"$int_ip/32"} = 1;
+ }
}
}
my $netlist = PVE::INotify::read_file('mynetworks');
- foreach my $cidr (sort keys %$netlist) {
- if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
- push @$mynetworks, "[$1]/$2";
+ foreach my $cidr (keys %$netlist) {
+ my $ip = PVE::Network::IP_from_cidr($cidr);
+
+ if (!$ip) {
+ warn "failed to parse mynetworks entry '$cidr', ignoring\n";
+ } elsif ($ip->version() == 4) {
+ $mynetworks->{$ip->prefix()} = 1;
} else {
- push @$mynetworks, $cidr;
+ my $address = '[' . $ip->short() . ']/' . $ip->prefixlen();
+ $mynetworks->{$address} = 1;
}
}
- push @$mynetworks, @$transportnets;
-
# add default relay to mynetworks
if (my $relay = $self->get('mail', 'relay')) {
if ($relay =~ m/^$IPV4RE$/) {
- push @$mynetworks, "$relay/32";
+ $mynetworks->{"$relay/32"} = 1;
} elsif ($relay =~ m/^$IPV6RE$/) {
- push @$mynetworks, "[$relay]/128";
+ $mynetworks->{"[$relay]/128"} = 1;
} else {
# DNS name - do nothing ?
}
}
- $vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
+ $vars->{postfix}->{mynetworks} = join(' ', sort keys %$mynetworks);
# normalize dnsbl_sites
my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
my $usepolicy = 0;
$usepolicy = 1 if $self->get('mail', 'greylist') ||
- $self->get('mail', 'spf');
+ $self->get('mail', 'greylist6') || $self->get('mail', 'spf');
$vars->{postfix}->{usepolicy} = $usepolicy;
- if ($int_ip =~ m/^$IPV6RE$/) {
+ if (!defined($int_ip)) {
+ warn "could not get node IP, falling back to loopback '127.0.0.1'\n";
+ $vars->{postfix}->{int_ip} = '127.0.0.1';
+ } elsif ($int_ip =~ m/^$IPV6RE$/) {
$vars->{postfix}->{int_ip} = "[$int_ip]";
} else {
$vars->{postfix}->{int_ip} = $int_ip;
return $vars;
}
+# reads the $filename and checks if it's equal as the $cmp string passed
+my sub file_content_equals_str {
+ my ($filename, $cmp) = @_;
+
+ return if !-f $filename;
+ my $current = PVE::Tools::file_get_contents($filename, 128*1024);
+ return defined($current) && $current eq $cmp; # no change
+}
+
# use one global TT cache
our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
my $output = '';
- $tt->process($tmplname, $vars, \$output) ||
- die $tt->error() . "\n";
-
- my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
+ $tt->process($tmplname, $vars, \$output) || die $tt->error() . "\n";
- return 0 if defined($old) && ($old eq $output); # no change
+ return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
PVE::Tools::file_set_contents($dstfn, $output, $perm);
$changes = 1 if $self->rewrite_config_file(
'v320.pre.in', '/etc/mail/spamassassin/v320.pre');
+ $changes = 1 if $self->rewrite_config_file(
+ 'v342.pre.in', '/etc/mail/spamassassin/v342.pre');
+
+ $changes = 1 if $self->rewrite_config_file(
+ 'v400.pre.in', '/etc/mail/spamassassin/v400.pre');
+
if ($use_razor) {
mkdir "/root/.razor";
} else {
# empty .forward does not forward mails (see man local)
}
-
- my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
-
- return 0 if defined($old) && ($old eq $output); # no change
+ return 0 if file_content_equals_str($dstfn, $output); # no change -> nothing to do
PVE::Tools::file_set_contents($dstfn, $output);
$action = 'OK' if !$action;
- my $old = PVE::Tools::file_get_contents($filename, 1024*1024)
- if -f $filename;
-
my $new = '';
foreach my $k (sort keys %$data) {
$new .= "$k $action\n";
}
-
- return 0 if defined($old) && ($old eq $new); # no change
+ return 0 if file_content_equals_str($filename, $new); # no change -> nothing to do
PVE::Tools::file_set_contents($filename, $new);
postmap_pmg_domains();
postmap_pmg_transport();
postmap_tls_policy();
+ postmap_tls_inbound_domains();
rewrite_postfix_whitelist($rulecache) if $rulecache;
mail => {
hide_received => 1,
ndr_on_block => 1,
+ smtputf8 => 1,
},
admin => {
dkim_selector => 1,