]> git.proxmox.com Git - pmg-api.git/blobdiff - PMG/Config.pm
pmg config: fix avast scan executable path documentation
[pmg-api.git] / PMG / Config.pm
index d6d6570d78690e6091fc021d17e8dd047b88a6b6..2392db8eaefeff84eba5351280cfeea4c4d472ff 100755 (executable)
@@ -2,6 +2,7 @@ package PMG::Config::Base;
 
 use strict;
 use warnings;
+use URI;
 use Data::Dumper;
 
 use PVE::Tools;
@@ -14,7 +15,7 @@ my $defaultData = {
     propertyList => {
        type => { description => "Section type." },
        section => {
-           description => "Secion ID.",
+           description => "Section ID.",
            type => 'string', format => 'pve-configid',
        },
     },
@@ -60,11 +61,22 @@ sub type {
 
 sub properties {
     return {
+       advfilter => {
+           description => "Use advanced filters for statistic.",
+           type => 'boolean',
+           default => 1,
+       },
        dailyreport => {
            description => "Send daily reports.",
            type => 'boolean',
            default => 1,
        },
+       statlifetime => {
+           description => "User Statistics Lifetime (days)",
+           type => 'integer',
+           default => 7,
+           minimum => 1,
+       },
        demo => {
            description => "Demo mode - do not start SMTP filter.",
            type => 'boolean',
@@ -75,36 +87,46 @@ sub properties {
            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 (/usr/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 },
     };
 }
 
@@ -142,15 +164,17 @@ sub properties {
            type => 'boolean',
            default => 1,
        },
-       use_ocr => {
-           description => "Enable OCR to scan pictures.",
-           type => 'boolean',
-           default => 0,
-       },
        wl_bounce_relays => {
            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',
@@ -167,7 +191,7 @@ sub properties {
            description => "Maximum size of spam messages in bytes.",
            type => 'integer',
            minimum => 64,
-           default => 200*1024,
+           default => 256*1024,
        },
     };
 }
@@ -176,16 +200,118 @@ sub options {
     return {
        use_awl => { optional => 1 },
        use_razor => { optional => 1 },
-       use_ocr => { optional => 1 },
        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 },
     };
 }
 
+package PMG::Config::SpamQuarantine;
+
+use strict;
+use warnings;
+
+use base qw(PMG::Config::Base);
+
+sub type {
+    return 'spamquar';
+}
+
+sub properties {
+    return {
+       lifetime => {
+           description => "Quarantine life time (days)",
+           type => 'integer',
+           minimum => 1,
+           default => 7,
+       },
+       authmode => {
+           description => "Authentication mode to access the quarantine interface. Mode 'ticket' allows login using tickets sent with the daily spam report. Mode 'ldap' requires to login using an LDAP account. Finally, mode 'ldapticket' allows both ways.",
+           type => 'string',
+           enum => [qw(ticket ldap ldapticket)],
+           default => 'ticket',
+       },
+       reportstyle => {
+           description => "Spam report style.",
+           type => 'string',
+           enum => [qw(none short verbose custom)],
+           default => 'verbose',
+       },
+       viewimages => {
+           description => "Allow to view images.",
+           type => 'boolean',
+           default => 1,
+       },
+       allowhrefs => {
+           description => "Allow to view hyperlinks.",
+           type => 'boolean',
+           default => 1,
+       },
+       hostname => {
+           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',
+       },
+    };
+}
+
+sub options {
+    return {
+       mailfrom => { optional => 1 },
+       hostname => { optional => 1 },
+       lifetime => { optional => 1 },
+       authmode => { optional => 1 },
+       reportstyle => { optional => 1 },
+       viewimages => { optional => 1 },
+       allowhrefs => { optional => 1 },
+       port => { optional => 1 },
+       protocol => { optional => 1 },
+    };
+}
+
+package PMG::Config::VirusQuarantine;
+
+use strict;
+use warnings;
+
+use base qw(PMG::Config::Base);
+
+sub type {
+    return 'virusquar';
+}
+
+sub properties {
+    return {};
+}
+
+sub options {
+    return {
+       lifetime => { optional => 1 },
+       viewimages => { optional => 1 },
+       allowhrefs => { optional => 1 },
+    };
+}
+
 package PMG::Config::ClamAV;
 
 use strict;
@@ -205,7 +331,7 @@ sub properties {
            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,
        },
@@ -239,6 +365,11 @@ sub properties {
            minimum => 0,
            default => 0,
        },
+       safebrowsing => {
+           description => "Enables support for Google Safe Browsing.",
+           type => 'boolean',
+           default => 1
+       },
     };
 }
 
@@ -251,6 +382,7 @@ sub options {
        maxscansize  => { optional => 1 },
        dbmirror => { optional => 1 },
        maxcccount => { optional => 1 },
+       safebrowsing => { optional => 1 },
     };
 }
 
@@ -318,14 +450,14 @@ sub properties {
            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).",
@@ -347,6 +479,13 @@ sub properties {
            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',
@@ -416,13 +555,18 @@ sub properties {
            minimum => 0,
            default => 4,
        },
-       use_rbl => {
-           description => "Use Realtime Blacklists.",
+       tls => {
+           description => "Enable TLS.",
            type => 'boolean',
-           default => 1,
+           default => 0,
        },
-       tls => {
-           description => "Use TLS.",
+       tlslog => {
+           description => "Enable TLS Logging.",
+           type => 'boolean',
+           default => 0,
+       },
+       tlsheader => {
+           description => "Add TLS received header.",
            type => 'boolean',
            default => 0,
        },
@@ -458,7 +602,13 @@ sub properties {
        },
        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
        },
     };
 }
@@ -468,6 +618,7 @@ sub options {
        int_port => { optional => 1 },
        ext_port => { optional => 1 },
        smarthost => { optional => 1 },
+       smarthostport => { optional => 1 },
        relay => { optional => 1 },
        relayport => { optional => 1 },
        relaynomx => { optional => 1 },
@@ -476,8 +627,9 @@ sub options {
        max_smtpd_out => { optional => 1 },
        greylist => { optional => 1 },
        helotests => { optional => 1 },
-       use_rbl => { optional => 1 },
        tls => { optional => 1 },
+       tlslog => { optional => 1 },
+       tlsheader => { optional => 1 },
        spf => { optional => 1 },
        maxsize => { optional => 1 },
        banner => { optional => 1 },
@@ -491,8 +643,10 @@ sub options {
        message_rate_limit => { optional => 1 },
        verifyreceivers => { optional => 1 },
        dnsbl_sites => { optional => 1 },
+       dnsbl_threshold => { optional => 1 },
     };
 }
+
 package PMG::Config;
 
 use strict;
@@ -506,8 +660,12 @@ use PVE::Tools qw($IPV4RE $IPV6RE);
 use PVE::INotify;
 use PVE::JSONSchema;
 
+use PMG::Cluster;
+
 PMG::Config::Admin->register();
 PMG::Config::Mail->register();
+PMG::Config::SpamQuarantine->register();
+PMG::Config::VirusQuarantine->register();
 PMG::Config::Spam->register();
 PMG::Config::ClamAV->register();
 
@@ -516,6 +674,7 @@ PMG::Config::Base->init();
 
 PVE::JSONSchema::register_format(
     'transport-domain', \&pmg_verify_transport_domain);
+
 sub pmg_verify_transport_domain {
     my ($name, $noerr) = @_;
 
@@ -529,6 +688,48 @@ sub pmg_verify_transport_domain {
     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) = @_;
 
@@ -580,7 +781,7 @@ sub set {
 
 # 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};
@@ -593,6 +794,8 @@ sub get {
        return $value;
     }
 
+    return undef if $nodefault;
+
     return $pdesc->{default};
 }
 
@@ -713,6 +916,142 @@ PVE::INotify::register_file('domains', $domainsfilename,
                            \&write_pmg_domains,
                            undef, always_call_parser => 1);
 
+my $mynetworks_filename = "/etc/pmg/mynetworks";
+
+sub read_pmg_mynetworks {
+    my ($filename, $fh) = @_;
+
+    my $mynetworks = {};
+
+    my $comment = '';
+    if (defined($fh)) {
+       while (defined(my $line = <$fh>)) {
+           chomp $line;
+           next if $line =~ m/^\s*$/;
+           if ($line =~ m!^((?:$IPV4RE|$IPV6RE))/(\d+)\s*(?:#(.*)\s*)?$!) {
+               my ($network, $prefix_size, $comment) = ($1, $2, $3);
+               my $cidr = "$network/${prefix_size}";
+               $mynetworks->{$cidr} = {
+                   cidr => $cidr,
+                   network_address => $network,
+                   prefix_size => $prefix_size,
+                   comment => $comment // '',
+               };
+           } else {
+               warn "parse error in '$filename': $line\n";
+           }
+       }
+    }
+
+    return $mynetworks;
+}
+
+sub write_pmg_mynetworks {
+    my ($filename, $fh, $mynetworks) = @_;
+
+    foreach my $cidr (sort keys %$mynetworks) {
+       my $data = $mynetworks->{$cidr};
+       my $comment = $data->{comment} // '*';
+       PVE::Tools::safe_print($filename, $fh, "$cidr #$comment\n");
+    }
+}
+
+PVE::INotify::register_file('mynetworks', $mynetworks_filename,
+                           \&read_pmg_mynetworks,
+                           \&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 {
@@ -745,7 +1084,7 @@ sub read_transport_map {
        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;
@@ -818,28 +1157,50 @@ sub get_template_vars {
 
     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' ];
+
+    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;
-    push @$mynetworks, $int_net_cidr;
 
     # add default relay to mynetworks
     if (my $relay = $self->get('mail', 'relay')) {
@@ -854,18 +1215,72 @@ sub get_template_vars {
 
     $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 {
@@ -873,23 +1288,14 @@ 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');
@@ -897,16 +1303,14 @@ sub rewrite_config_file {
        $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;
 
@@ -970,7 +1374,7 @@ sub rewrite_config_spam {
                PVE::Tools::run_command(['razor-admin', '-register'], timeout => $timeout);
            };
            my $err = $@;
-           syslog('info', msgquote ("registering razor failed: $err")) if $err;
+           syslog('info', "registering razor failed: $err") if $err;
        }
     }
 
@@ -1032,14 +1436,84 @@ sub rewrite_dot_forward {
     return 1;
 }
 
+my $write_smtp_whitelist = sub {
+    my ($filename, $data, $action) = @_;
+
+    $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
+
+    PVE::Tools::file_set_contents($filename, $new);
+
+    PMG::Utils::run_postmap($filename);
+
+    return 1;
+};
+
+sub rewrite_postfix_whitelist {
+    my ($rulecache) = @_;
+
+    # see man page for regexp_table for postfix regex table format
+
+    # we use a hash to avoid duplicate entries in regex tables
+    my $tolist = {};
+    my $fromlist = {};
+    my $clientlist = {};
+
+    foreach my $obj (@{$rulecache->{"greylist:receiver"}}) {
+       my $oclass = ref($obj);
+       if ($oclass eq 'PMG::RuleDB::Receiver') {
+           my $addr = PMG::Utils::quote_regex($obj->{address});
+           $tolist->{"/^$addr\$/"} = 1;
+       } elsif ($oclass eq 'PMG::RuleDB::ReceiverDomain') {
+           my $addr = PMG::Utils::quote_regex($obj->{address});
+           $tolist->{"/^.+\@$addr\$/"} = 1;
+       } elsif ($oclass eq 'PMG::RuleDB::ReceiverRegex') {
+           my $addr = $obj->{address};
+           $addr =~ s|/|\\/|g;
+           $tolist->{"/^$addr\$/"} = 1;
+       }
+    }
+
+    foreach my $obj (@{$rulecache->{"greylist:sender"}}) {
+       my $oclass = ref($obj);
+       my $addr = PMG::Utils::quote_regex($obj->{address});
+       if ($oclass eq 'PMG::RuleDB::EMail') {
+           my $addr = PMG::Utils::quote_regex($obj->{address});
+           $fromlist->{"/^$addr\$/"} = 1;
+       } elsif ($oclass eq 'PMG::RuleDB::Domain') {
+           my $addr = PMG::Utils::quote_regex($obj->{address});
+           $fromlist->{"/^.+\@$addr\$/"} = 1;
+       } elsif ($oclass eq 'PMG::RuleDB::WhoRegex') {
+           my $addr = $obj->{address};
+           $addr =~ s|/|\\/|g;
+           $fromlist->{"/^$addr\$/"} = 1;
+       } elsif ($oclass eq 'PMG::RuleDB::IPAddress') {
+           $clientlist->{$obj->{address}} = 1;
+       } elsif ($oclass eq 'PMG::RuleDB::IPNet') {
+           $clientlist->{$obj->{address}} = 1;
+       }
+    }
+
+    $write_smtp_whitelist->("/etc/postfix/senderaccess", $fromlist);
+    $write_smtp_whitelist->("/etc/postfix/rcptaccess", $tolist);
+    $write_smtp_whitelist->("/etc/postfix/clientaccess", $clientlist);
+    $write_smtp_whitelist->("/etc/postfix/postscreen_access", $clientlist, 'permit');
+};
+
 # rewrite /etc/postfix/*
 sub rewrite_config_postfix {
-    my ($self) = @_;
+    my ($self, $rulecache) = @_;
 
     # make sure we have required files (else postfix start fails)
-    postmap_pmg_domains();
-    postmap_pmg_transport();
-
     IO::File->new($transport_map_filename, 'a', 0644);
 
     my $changes = 0;
@@ -1048,7 +1522,7 @@ sub rewrite_config_postfix {
        eval {
            PMG::Utils::gen_proxmox_tls_cert();
        };
-       syslog ('info', msgquote ("generating certificate failed: $@")) if $@;
+       syslog ('info', "generating certificate failed: $@") if $@;
     }
 
     $changes = 1 if $self->rewrite_config_file(
@@ -1057,9 +1531,13 @@ sub rewrite_config_postfix {
     $changes = 1 if $self->rewrite_config_file(
        'master.cf.in', '/etc/postfix/master.cf');
 
-    #rewrite_config_transports ($class);
-    #rewrite_config_whitelist ($class);
-    #rewrite_config_tls_policy ($class);
+    # 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();
+
+    rewrite_postfix_whitelist($rulecache) if $rulecache;
 
     # make sure aliases.db is up to date
     system('/usr/bin/newaliases');
@@ -1068,10 +1546,18 @@ sub rewrite_config_postfix {
 }
 
 sub rewrite_config {
-    my ($self, $restart_services) = @_;
+    my ($self, $rulecache, $restart_services, $force_restart) = @_;
 
-    if ($self->rewrite_config_postfix() && $restart_services) {
-       PMG::Utils::service_cmd('postfix', 'restart');
+    $force_restart = {} if ! $force_restart;
+
+    my $log_restart = sub {
+       syslog ('info', "configuration change detected for '$_[0]', restarting");
+    };
+
+    if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
+       $force_restart->{postfix}) {
+       $log_restart->('postfix');
+       PMG::Utils::service_cmd('postfix', 'reload');
     }
 
     if ($self->rewrite_dot_forward() && $restart_services) {
@@ -1083,18 +1569,23 @@ sub rewrite_config {
        # does not happen anyways, because config does not change.
     }
 
-    if ($self->rewrite_config_spam() && $restart_services) {
+    if (($self->rewrite_config_spam() && $restart_services) ||
+       $force_restart->{spam}) {
+       $log_restart->('pmg-smtp-filter');
        PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
     }
 
-    if ($self->rewrite_config_clam() && $restart_services) {
+    if (($self->rewrite_config_clam() && $restart_services) ||
+       $force_restart->{clam}) {
+       $log_restart->('clamav-daemon');
        PMG::Utils::service_cmd('clamav-daemon', 'restart');
     }
 
-    if ($self->rewrite_config_freshclam() && $restart_services) {
+    if (($self->rewrite_config_freshclam() && $restart_services) ||
+       $force_restart->{freshclam}) {
+       $log_restart->('clamav-freshclam');
        PMG::Utils::service_cmd('clamav-freshclam', 'restart');
     }
-
 }
 
 1;