use strict;
use warnings;
+use URI;
use Data::Dumper;
use PVE::Tools;
propertyList => {
type => { description => "Section type." },
section => {
- description => "Secion ID.",
+ description => "Section ID.",
type => 'string', format => 'pve-configid',
},
},
sub properties {
return {
+ advfilter => {
+ description => "Use advanced filters for statistic.",
+ type => 'boolean',
+ default => 1,
+ },
dailyreport => {
description => "Send daily reports.",
type => 'boolean',
type => 'string', format => 'email',
default => 'admin@domain.tld',
},
- proxyport => {
- description => "HTTP proxy port.",
- type => 'integer',
- minimum => 1,
- default => 8080,
- },
- proxyserver => {
- description => "HTTP proxy server address.",
+ http_proxy => {
+ description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
type => 'string',
+ pattern => "http://.*",
},
- proxyuser => {
- description => "HTTP proxy user name.",
- type => 'string',
+ avast => {
+ description => "Use Avast Virus Scanner (/bin/scan). You need to buy and install 'Avast Core Security' before you can enable this feature.",
+ type => 'boolean',
+ default => 0,
},
- proxypassword => {
- description => "HTTP proxy password.",
- type => 'string',
+ clamav => {
+ description => "Use ClamAV Virus Scanner. This is the default virus scanner and is enabled by default.",
+ type => 'boolean',
+ default => 1,
+ },
+ custom_check => {
+ description => "Use Custom Check Script. The script has to take the defined arguments and can return Virus findings or a Spamscore.",
+ type => 'boolean',
+ default => 0,
+ },
+ custom_check_path => {
+ description => "Absolute Path to the Custom Check Script",
+ type => 'string', pattern => '^/([^/\0]+\/)+[^/\0]+$',
+ default => '/usr/local/bin/pmg-custom-check',
},
};
}
sub options {
return {
+ advfilter => { optional => 1 },
+ avast => { optional => 1 },
+ clamav => { optional => 1 },
statlifetime => { optional => 1 },
dailyreport => { optional => 1 },
demo => { optional => 1 },
email => { optional => 1 },
- proxyport => { optional => 1 },
- proxyserver => { optional => 1 },
- proxyuser => { optional => 1 },
- proxypassword => { optional => 1 },
+ http_proxy => { optional => 1 },
+ custom_check => { optional => 1 },
+ custom_check_path => { optional => 1 },
};
}
description => "Whitelist legitimate bounce relays.",
type => 'string',
},
+ clamav_heuristic_score => {
+ description => "Score for ClamAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
+ type => 'integer',
+ minimum => 0,
+ maximum => 1000,
+ default => 3,
+ },
bounce_score => {
description => "Additional score for bounce mails.",
type => 'integer',
description => "Maximum size of spam messages in bytes.",
type => 'integer',
minimum => 64,
- default => 200*1024,
+ default => 256*1024,
},
};
}
wl_bounce_relays => { optional => 1 },
languages => { optional => 1 },
use_bayes => { optional => 1 },
+ clamav_heuristic_score => { optional => 1 },
bounce_score => { optional => 1 },
rbl_checks => { optional => 1 },
maxspamsize => { optional => 1 },
reportstyle => {
description => "Spam report style.",
type => 'string',
- enum => [qw(none short verbose outlook custom)],
+ enum => [qw(none short verbose custom)],
default => 'verbose',
},
viewimages => {
default => 1,
},
hostname => {
- description => "Quarantine Host. Usefule if you run a Cluster and want users to connect to a specific host.",
+ description => "Quarantine Host. Useful if you run a Cluster and want users to connect to a specific host.",
type => 'string', format => 'address',
},
+ port => {
+ description => "Quarantine Port. Useful if you have a reverse proxy or port forwarding for the webinterface. Only used for the generated Spam report.",
+ type => 'integer',
+ minimum => 1,
+ maximum => 65535,
+ default => 8006,
+ },
+ protocol => {
+ description => "Quarantine Webinterface Protocol. Useful if you have a reverse proxy for the webinterface. Only used for the generated Spam report.",
+ type => 'string',
+ enum => [qw(http https)],
+ default => 'https',
+ },
mailfrom => {
description => "Text for 'From' header in daily spam report mails.",
type => 'string',
reportstyle => { optional => 1 },
viewimages => { optional => 1 },
allowhrefs => { optional => 1 },
+ port => { optional => 1 },
+ protocol => { optional => 1 },
};
}
default => 'database.clamav.net',
},
archiveblockencrypted => {
- description => "Wether to block encrypted archives. Mark encrypted archives as viruses.",
+ description => "Whether to block encrypted archives. Mark encrypted archives as viruses.",
type => 'boolean',
default => 0,
},
type => 'integer',
minimum => 1,
maximum => 65535,
- default => 25,
+ default => 26,
},
ext_port => {
description => "SMTP port number for incoming mail (untrusted). This must be a different number than 'int_port'.",
type => 'integer',
minimum => 1,
maximum => 65535,
- default => 26,
+ default => 25,
},
relay => {
description => "The default mail delivery transport (incoming mails).",
description => "When set, all outgoing mails are deliverd to the specified smarthost.",
type => 'string', format => 'address',
},
+ smarthostport => {
+ description => "SMTP port number for smarthost.",
+ type => 'integer',
+ minimum => 1,
+ maximum => 65535,
+ default => 25,
+ },
banner => {
description => "ESMTP banner.",
type => 'string',
minimum => 0,
default => 4,
},
- use_rbl => {
- description => "Use Realtime Blacklists.",
- type => 'boolean',
- default => 1,
- },
tls => {
description => "Enable TLS.",
type => 'boolean',
},
dnsbl_sites => {
description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
- type => 'string',
+ 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).",
+ type => 'integer',
+ minimum => 0,
+ default => 1
},
};
}
int_port => { optional => 1 },
ext_port => { optional => 1 },
smarthost => { optional => 1 },
+ smarthostport => { optional => 1 },
relay => { optional => 1 },
relayport => { optional => 1 },
relaynomx => { optional => 1 },
max_smtpd_out => { optional => 1 },
greylist => { optional => 1 },
helotests => { optional => 1 },
- use_rbl => { optional => 1 },
tls => { optional => 1 },
tlslog => { optional => 1 },
tlsheader => { optional => 1 },
message_rate_limit => { optional => 1 },
verifyreceivers => { optional => 1 },
dnsbl_sites => { optional => 1 },
+ dnsbl_threshold => { optional => 1 },
};
}
+
package PMG::Config;
use strict;
use PVE::INotify;
use PVE::JSONSchema;
+use PMG::Cluster;
+
PMG::Config::Admin->register();
PMG::Config::Mail->register();
PMG::Config::SpamQuarantine->register();
PVE::JSONSchema::register_format(
'transport-domain', \&pmg_verify_transport_domain);
+
sub pmg_verify_transport_domain {
my ($name, $noerr) = @_;
return $name;
}
+PVE::JSONSchema::register_format(
+ 'transport-domain-or-email', \&pmg_verify_transport_domain_or_email);
+
+sub pmg_verify_transport_domain_or_email {
+ my ($name, $noerr) = @_;
+
+ my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
+
+ # email address
+ if ($name =~ m/^(?:[^\s\/\@]+\@)(${namere}\.)*${namere}$/) {
+ return $name;
+ }
+
+ # like dns-name, but can contain leading dot
+ if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
+ return undef if $noerr;
+ die "value does not look like a valid transport domain or email address\n";
+ }
+ return $name;
+}
+
+PVE::JSONSchema::register_format(
+ 'dnsbl-entry', \&pmg_verify_dnsbl_entry);
+
+sub pmg_verify_dnsbl_entry {
+ my ($name, $noerr) = @_;
+
+ # like dns-name, but can contain trailing filter and weight: 'domain=<FILTER>*<WEIGHT>'
+ # see http://www.postfix.org/postconf.5.html#postscreen_dnsbl_sites
+ # we don't implement the ';' separated numbers in pattern, because this
+ # breaks at PVE::JSONSchema::split_list
+ my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
+
+ my $dnsbloctet = qr/[0-9]+|\[(?:[0-9]+\.\.[0-9]+)\]/;
+ my $filterre = qr/=$dnsbloctet(:?\.$dnsbloctet){3}/;
+ if ($name !~ /^(${namere}\.)*${namere}(:?${filterre})?(?:\*\-?\d+)?$/) {
+ return undef if $noerr;
+ die "value '$name' does not look like a valid dnsbl entry\n";
+ }
+ return $name;
+}
+
sub new {
my ($type) = @_;
# get section value or default
sub get {
- my ($self, $section, $key) = @_;
+ my ($self, $section, $key, $nodefault) = @_;
my $pdata = PMG::Config::Base->private();
my $pdesc = $pdata->{propertyList}->{$key};
return $value;
}
+ return undef if $nodefault;
+
return $pdesc->{default};
}
my $mynetworks_filename = "/etc/pmg/mynetworks";
-sub postmap_pmg_mynetworks {
- PMG::Utils::run_postmap($mynetworks_filename);
-}
-
sub read_pmg_mynetworks {
my ($filename, $fh) = @_;
\&write_pmg_mynetworks,
undef, always_call_parser => 1);
+PVE::JSONSchema::register_format(
+ 'tls-policy', \&pmg_verify_tls_policy);
+
+# TODO: extend to parse attributes of the policy
+my $VALID_TLS_POLICY_RE = qr/none|may|encrypt|dane|dane-only|fingerprint|verify|secure/;
+sub pmg_verify_tls_policy {
+ my ($policy, $noerr) = @_;
+
+ if ($policy !~ /^$VALID_TLS_POLICY_RE\b/) {
+ return undef if $noerr;
+ die "value '$policy' does not look like a valid tls policy\n";
+ }
+ return $policy;
+}
+
+PVE::JSONSchema::register_format(
+ 'tls-policy-strict', \&pmg_verify_tls_policy_strict);
+
+sub pmg_verify_tls_policy_strict {
+ my ($policy, $noerr) = @_;
+
+ if ($policy !~ /^$VALID_TLS_POLICY_RE$/) {
+ return undef if $noerr;
+ die "value '$policy' does not look like a valid tls policy\n";
+ }
+ return $policy;
+}
+
+sub read_tls_policy {
+ my ($filename, $fh) = @_;
+
+ return {} if !defined($fh);
+
+ my $tls_policy = {};
+
+ while (defined(my $line = <$fh>)) {
+ chomp $line;
+ next if $line =~ m/^\s*$/;
+ next if $line =~ m/^#(.*)\s*$/;
+
+ my $parse_error = sub {
+ my ($err) = @_;
+ die "parse error in '$filename': $line - $err";
+ };
+
+ if ($line =~ m/^(\S+)\s+(.+)\s*$/) {
+ my ($domain, $policy) = ($1, $2);
+
+ eval {
+ pmg_verify_transport_domain($domain);
+ pmg_verify_tls_policy($policy);
+ };
+ if (my $err = $@) {
+ $parse_error->($err);
+ next;
+ }
+
+ $tls_policy->{$domain} = {
+ domain => $domain,
+ policy => $policy,
+ };
+ } else {
+ $parse_error->('wrong format');
+ }
+ }
+
+ return $tls_policy;
+}
+
+sub write_tls_policy {
+ my ($filename, $fh, $tls_policy) = @_;
+
+ return if !$tls_policy;
+
+ foreach my $domain (sort keys %$tls_policy) {
+ my $entry = $tls_policy->{$domain};
+ PVE::Tools::safe_print(
+ $filename, $fh, "$entry->{domain} $entry->{policy}\n");
+ }
+}
+
+my $tls_policy_map_filename = "/etc/pmg/tls_policy";
+PVE::INotify::register_file('tls_policy', $tls_policy_map_filename,
+ \&read_tls_policy,
+ \&write_tls_policy,
+ undef, always_call_parser => 1);
+
+sub postmap_tls_policy {
+ PMG::Utils::run_postmap($tls_policy_map_filename);
+}
+
my $transport_map_filename = "/etc/pmg/transport";
sub postmap_pmg_transport {
if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
my ($domain, $host, $port) = ($1, $2, $3);
- eval { pmg_verify_transport_domain($domain); };
+ eval { pmg_verify_transport_domain_or_email($domain); };
if (my $err = $@) {
$parse_error->($err);
next;
my $nodename = PVE::INotify::nodename();
my $int_ip = PMG::Cluster::remote_node_ip($nodename);
- my $int_net_cidr = PMG::Utils::find_local_network_for_ip($int_ip);
$vars->{ipconfig}->{int_ip} = $int_ip;
- # $vars->{ipconfig}->{int_net_cidr} = $int_net_cidr;
my $transportnets = [];
- my $tmap = PVE::INotify::read_file('transport');
- foreach my $domain (sort 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";
+ if (my $tmap = PVE::INotify::read_file('transport')) {
+ foreach my $domain (sort 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";
+ }
}
}
$vars->{postfix}->{transportnets} = join(' ', @$transportnets);
my $mynetworks = [ '127.0.0.0/8', '[::1]/128' ];
- push @$mynetworks, @$transportnets;
- push @$mynetworks, $int_net_cidr;
- push @$mynetworks, 'hash:/etc/pmg/mynetworks';
+
+ 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";
+ } else {
+ push @$mynetworks, "$int_ip/32";
+ }
+ }
my $netlist = PVE::INotify::read_file('mynetworks');
+ foreach my $cidr (sort keys %$netlist) {
+ if ($cidr =~ m/^($IPV6RE)\/(\d+)$/) {
+ push @$mynetworks, "[$1]/$2";
+ } else {
+ push @$mynetworks, $cidr;
+ }
+ }
+
+ push @$mynetworks, @$transportnets;
+
# add default relay to mynetworks
if (my $relay = $self->get('mail', 'relay')) {
if ($relay =~ m/^$IPV4RE$/) {
$vars->{postfix}->{mynetworks} = join(' ', @$mynetworks);
+ # normalize dnsbl_sites
+ my @dnsbl_sites = PVE::Tools::split_list($vars->{pmg}->{mail}->{dnsbl_sites});
+ if (scalar(@dnsbl_sites)) {
+ $vars->{postfix}->{dnsbl_sites} = join(',', @dnsbl_sites);
+ }
+
+ $vars->{postfix}->{dnsbl_threshold} = $self->get('mail', 'dnsbl_threshold');
+
my $usepolicy = 0;
$usepolicy = 1 if $self->get('mail', 'greylist') ||
- $self->get('mail', 'spf') || $self->get('mail', 'use_rbl');
+ $self->get('mail', 'spf');
$vars->{postfix}->{usepolicy} = $usepolicy;
+ if ($int_ip =~ m/^$IPV6RE$/) {
+ $vars->{postfix}->{int_ip} = "[$int_ip]";
+ } else {
+ $vars->{postfix}->{int_ip} = $int_ip;
+ }
+
my $resolv = PVE::INotify::read_file('resolvconf');
$vars->{dns}->{hostname} = $nodename;
- $vars->{dns}->{domain} = $resolv->{search};
+
+ my $domain = $resolv->{search} // 'localdomain';
+ $vars->{dns}->{domain} = $domain;
+
+ my $wlbr = "$nodename.$domain";
+ foreach my $r (PVE::Tools::split_list($vars->{pmg}->{spam}->{wl_bounce_relays})) {
+ $wlbr .= " $r"
+ }
+ $vars->{composed}->{wl_bounce_relays} = $wlbr;
+
+ if (my $proxy = $vars->{pmg}->{admin}->{http_proxy}) {
+ eval {
+ my $uri = URI->new($proxy);
+ my $host = $uri->host;
+ my $port = $uri->port // 8080;
+ if ($host) {
+ my $data = { host => $host, port => $port };
+ if (my $ui = $uri->userinfo) {
+ my ($username, $pw) = split(/:/, $ui, 2);
+ $data->{username} = $username;
+ $data->{password} = $pw if defined($pw);
+ }
+ $vars->{proxy} = $data;
+ }
+ };
+ warn "parse http_proxy failed - $@" if $@;
+ }
return $vars;
}
+# use one global TT cache
+our $tt_include_path = ['/etc/pmg/templates' ,'/var/lib/pmg/templates' ];
+
+my $template_toolkit;
+
+sub get_template_toolkit {
+
+ return $template_toolkit if $template_toolkit;
+
+ $template_toolkit = Template->new({ INCLUDE_PATH => $tt_include_path });
+
+ return $template_toolkit;
+}
+
# rewrite file from template
# return true if file has changed
sub rewrite_config_file {
my $demo = $self->get('admin', 'demo');
- my $srcfn = ($tmplname =~ m|^.?/|) ?
- $tmplname : "/var/lib/pmg/templates/$tmplname";
-
if ($demo) {
- my $demosrc = "$srcfn.demo";
- $srcfn = $demosrc if -f $demosrc;
+ my $demosrc = "$tmplname.demo";
+ $tmplname = $demosrc if -f "/var/lib/pmg/templates/$demosrc";
}
my ($perm, $uid, $gid);
- my $srcfd = IO::File->new ($srcfn, "r")
- || die "cant read template '$srcfn' - $!: ERROR";
-
- if ($dstfn eq '/etc/fetchmailrc') {
- (undef, undef, $uid, $gid) = getpwnam('fetchmail');
- $perm = 0600;
- } elsif ($dstfn eq '/etc/clamav/freshclam.conf') {
+ if ($dstfn eq '/etc/clamav/freshclam.conf') {
# needed if file contains a HTTPProxyPasswort
$uid = getpwnam('clamav');
$perm = 0600;
}
- my $template = Template->new({});
+ my $tt = get_template_toolkit();
my $vars = $self->get_template_vars();
my $output = '';
- $template->process($srcfd, $vars, \$output) ||
- die $template->error();
-
- $srcfd->close();
+ $tt->process($tmplname, $vars, \$output) ||
+ die $tt->error() . "\n";
my $old = PVE::Tools::file_get_contents($dstfn, 128*1024) if -f $dstfn;
return 1;
};
-my $rewrite_config_whitelist = sub {
+sub rewrite_postfix_whitelist {
my ($rulecache) = @_;
# see man page for regexp_table for postfix regex table format
my ($self, $rulecache) = @_;
# make sure we have required files (else postfix start fails)
- postmap_pmg_domains();
- postmap_pmg_transport();
- postmap_pmg_mynetworks();
-
IO::File->new($transport_map_filename, 'a', 0644);
my $changes = 0;
$changes = 1 if $self->rewrite_config_file(
'master.cf.in', '/etc/postfix/master.cf');
- $rewrite_config_whitelist->($rulecache);
+ # make sure we have required files (else postfix start fails)
+ # Note: postmap need a valid /etc/postfix/main.cf configuration
+ postmap_pmg_domains();
+ postmap_pmg_transport();
+ postmap_tls_policy();
- # fixme: rewrite_config_tls_policy ($class);
+ rewrite_postfix_whitelist($rulecache) if $rulecache;
# make sure aliases.db is up to date
system('/usr/bin/newaliases');
if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
$force_restart->{postfix}) {
- PMG::Utils::service_cmd('postfix', 'restart');
+ syslog ('info', 'configuration change detected - postfix');
+ PMG::Utils::service_cmd('postfix', 'reload');
}
if ($self->rewrite_dot_forward() && $restart_services) {
if (($self->rewrite_config_spam() && $restart_services) ||
$force_restart->{spam}) {
+ syslog ('info', 'configuration change detected - pmg-smtp-filter');
PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
}
if (($self->rewrite_config_clam() && $restart_services) ||
$force_restart->{clam}) {
+ syslog ('info', 'configuration change detected - clamav-daemon');
PMG::Utils::service_cmd('clamav-daemon', 'restart');
}
if (($self->rewrite_config_freshclam() && $restart_services) ||
$force_restart->{freshclam}) {
+ syslog ('info', 'configuration change detected - clamav-freshclam');
PMG::Utils::service_cmd('clamav-freshclam', 'restart');
}
}