]> git.proxmox.com Git - pmg-api.git/blobdiff - PMG/Config.pm
PMG/API2/Cluster.pm - use PMG::MailQueue::create_spooldirs
[pmg-api.git] / PMG / Config.pm
index 7a6fa536eeeb251ce6db6daa0939cd4dac116cd5..22450811c2f0b1abe38bf8c873e280c4272776d0 100755 (executable)
@@ -2,6 +2,7 @@ package PMG::Config::Base;
 
 use strict;
 use warnings;
+use URI;
 use Data::Dumper;
 
 use PVE::Tools;
@@ -27,27 +28,22 @@ sub private {
 sub format_section_header {
     my ($class, $type, $sectionId) = @_;
 
-    if ($type eq 'ldap') {
-       $sectionId =~ s/^ldap_//;
-       return "$type: $sectionId\n";
-    } else {
-       return "section: $type\n";
-    }
+    die "internal error ($type ne $sectionId)" if $type ne $sectionId;
+
+    return "section: $type\n";
 }
 
 
 sub parse_section_header {
     my ($class, $line) = @_;
 
-    if ($line =~ m/^(ldap|section):\s*(\S+)\s*$/) {
-       my ($raw_type, $raw_id) = (lc($1), $2);
-       my $type = $raw_type eq 'section' ? $raw_id : $raw_type;
-       my $section_id =  "${raw_type}_${raw_id}";
+    if ($line =~ m/^section:\s*(\S+)\s*$/) {
+       my $section = $1;
        my $errmsg = undef; # set if you want to skip whole section
-       eval { PVE::JSONSchema::pve_verify_configid($raw_id); };
+       eval { PVE::JSONSchema::pve_verify_configid($section); };
        $errmsg = $@ if $@;
        my $config = {}; # to return additional attributes
-       return ($type, $section_id, $errmsg, $config);
+       return ($section, $section, $errmsg, $config);
     }
     return undef;
 }
@@ -65,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',
@@ -80,35 +87,22 @@ 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.",
-           type => 'string',
-       },
-       proxyuser => {
-           description => "HTTP proxy user name.",
-           type => 'string',
-       },
-       proxypassword => {
-           description => "HTTP proxy password.",
+       http_proxy => {
+           description => "Specify external http proxy which is used for downloads (example: 'http://username:password\@host:port/')",
            type => 'string',
+           pattern => "http://.*",
        },
     };
 }
 
 sub options {
     return {
+       advfilter => { optional => 1 },
+       statlifetime => { optional => 1 },
        dailyreport => { optional => 1 },
        demo => { optional => 1 },
-       proxyport => { optional => 1 },
-       proxyserver => { optional => 1 },
-       proxyuser => { optional => 1 },
-       proxypassword => { optional => 1 },
+       email => { optional => 1 },
+       http_proxy => { optional => 1 },
     };
 }
 
@@ -146,11 +140,6 @@ 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',
@@ -180,7 +169,6 @@ 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 },
@@ -190,6 +178,93 @@ sub options {
     };
 }
 
+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. Usefule if you run a Cluster and want users to connect to a specific host.",
+           type => 'string', format => 'address',
+       },
+       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 },
+    };
+}
+
+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;
@@ -243,6 +318,11 @@ sub properties {
            minimum => 0,
            default => 0,
        },
+       safebrowsing => {
+           description => "Enables support for Google Safe Browsing.",
+           type => 'boolean',
+           default => 1
+       },
     };
 }
 
@@ -255,34 +335,7 @@ sub options {
        maxscansize  => { optional => 1 },
        dbmirror => { optional => 1 },
        maxcccount => { optional => 1 },
-    };
-}
-
-package PMG::Config::LDAP;
-
-use strict;
-use warnings;
-
-use base qw(PMG::Config::Base);
-
-sub type {
-    return 'ldap';
-}
-
-sub properties {
-    return {
-       mode => {
-           description => "LDAP protocol mode ('ldap' or 'ldaps').",
-           type => 'string',
-           enum => ['ldap', 'ldaps'],
-           default => 'ldap',
-       },
-    };
-}
-
-sub options {
-    return {
-       mode => { optional => 1 },
+       safebrowsing => { optional => 1 },
     };
 }
 
@@ -335,12 +388,33 @@ sub get_max_smtpd {
     return $max_servers;
 }
 
+sub get_max_policy {
+    # estimate optimal number of proxpolicy servers
+    my $max_servers = 2;
+    my $memory = physical_memory();
+    $max_servers = 5 if  $memory >= 500;
+    return $max_servers;
+}
 
 sub properties {
     return {
+       int_port => {
+           description => "SMTP port number for outgoing mail (trusted).",
+           type => 'integer',
+           minimum => 1,
+           maximum => 65535,
+           default => 25,
+       },
+       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,
+       },
        relay => {
            description => "The default mail delivery transport (incoming mails).",
-           type => 'string',
+           type => 'string', format => 'address',
        },
        relayport => {
            description => "SMTP port number for relay host.",
@@ -356,7 +430,7 @@ sub properties {
        },
        smarthost => {
            description => "When set, all outgoing mails are deliverd to the specified smarthost.",
-           type => 'string',
+           type => 'string', format => 'address',
        },
        banner => {
            description => "ESMTP banner.",
@@ -365,12 +439,19 @@ sub properties {
            default => 'ESMTP Proxmox',
        },
        max_filters => {
-           description => "Maximum number of filter processes.",
+           description => "Maximum number of pmg-smtp-filter processes.",
            type => 'integer',
            minimum => 3,
            maximum => 40,
            default => get_max_filters(),
        },
+       max_policy => {
+           description => "Maximum number of pmgpolicy processes.",
+           type => 'integer',
+           minimum => 2,
+           maximum => 10,
+           default => get_max_policy(),
+       },
        max_smtpd_in => {
            description => "Maximum number of SMTP daemon processes (in).",
            type => 'integer',
@@ -426,7 +507,17 @@ sub properties {
            default => 1,
        },
        tls => {
-           description => "Use TLS.",
+           description => "Enable TLS.",
+           type => 'boolean',
+           default => 0,
+       },
+       tlslog => {
+           description => "Enable TLS Logging.",
+           type => 'boolean',
+           default => 0,
+       },
+       tlsheader => {
+           description => "Add TLS received header.",
            type => 'boolean',
            default => 0,
        },
@@ -456,11 +547,9 @@ sub properties {
            default => 0,
        },
        verifyreceivers => {
-           description => "Enable receiver verification. The value (if greater than 0) spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address (450 or 550).",
-           type => 'integer',
-           minimum => 0,
-           maximum => 599,
-           default => 0,
+           description => "Enable receiver verification. The value spefifies the numerical reply code when the Postfix SMTP server rejects a recipient address.",
+           type => 'string',
+           enum => ['450', '550'],
        },
        dnsbl_sites => {
            description => "Optional list of DNS white/blacklist domains (see postscreen_dnsbl_sites parameter).",
@@ -471,6 +560,9 @@ sub properties {
 
 sub options {
     return {
+       int_port => { optional => 1 },
+       ext_port => { optional => 1 },
+       smarthost => { optional => 1 },
        relay => { optional => 1 },
        relayport => { optional => 1 },
        relaynomx => { optional => 1 },
@@ -481,10 +573,13 @@ sub options {
        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 },
        max_filters => { optional => 1 },
+       max_policy => { optional => 1 },
        hide_received => { optional => 1 },
        rejectunknown => { optional => 1 },
        rejectunknownsender => { optional => 1 },
@@ -495,6 +590,7 @@ sub options {
        dnsbl_sites => { optional => 1 },
     };
 }
+
 package PMG::Config;
 
 use strict;
@@ -504,18 +600,36 @@ use Data::Dumper;
 use Template;
 
 use PVE::SafeSyslog;
-use PVE::Tools;
+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::LDAP->register();
 PMG::Config::ClamAV->register();
 
 # initialize all plugins
 PMG::Config::Base->init();
 
+PVE::JSONSchema::register_format(
+    'transport-domain', \&pmg_verify_transport_domain);
+sub pmg_verify_transport_domain {
+    my ($name, $noerr) = @_;
+
+    # like dns-name, but can contain leading dot
+    my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
+
+    if ($name !~ /^\.?(${namere}\.)*${namere}$/) {
+          return undef if $noerr;
+          die "value does not look like a valid transport domain\n";
+    }
+    return $name;
+}
 
 sub new {
     my ($type) = @_;
@@ -545,26 +659,21 @@ sub lock_config {
 }
 
 # set section values
-# this does not work for ldap entries
 sub set {
     my ($self, $section, $key, $value) = @_;
 
     my $pdata = PMG::Config::Base->private();
 
-    die "internal error" if $section eq 'ldap';
-
     my $plugin = $pdata->{plugins}->{$section};
     die "no such section '$section'" if !$plugin;
 
-    my $configid = "section_$section";
     if (defined($value)) {
        my $tmp = PMG::Config::Base->check_value($section, $key, $value, $section, 0);
-       print Dumper($self->{ids});
-       $self->{ids}->{$configid} = { type => $section } if !defined($self->{ids}->{$configid});
-       $self->{ids}->{$configid}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
+       $self->{ids}->{$section} = { type => $section } if !defined($self->{ids}->{$section});
+       $self->{ids}->{$section}->{$key} = PMG::Config::Base->decode_value($section, $key, $tmp);
     } else {
-       if (defined($self->{ids}->{$configid})) {
-           delete $self->{ids}->{$configid}->{$key};
+       if (defined($self->{ids}->{$section})) {
+           delete $self->{ids}->{$section}->{$key};
        }
     }
 
@@ -572,27 +681,26 @@ sub set {
 }
 
 # get section value or default
-# this does not work for ldap entries
 sub get {
-    my ($self, $section, $key) = @_;
+    my ($self, $section, $key, $nodefault) = @_;
 
     my $pdata = PMG::Config::Base->private();
-    return undef if !defined($pdata->{options}->{$section});
-    return undef if !defined($pdata->{options}->{$section}->{$key});
     my $pdesc = $pdata->{propertyList}->{$key};
-    return undef if !defined($pdesc);
+    die "no such property '$section/$key'\n"
+       if !(defined($pdesc) && defined($pdata->{options}->{$section}) &&
+            defined($pdata->{options}->{$section}->{$key}));
 
-    my $configid = "section_$section";
-    if (defined($self->{ids}->{$configid}) &&
-       defined(my $value = $self->{ids}->{$configid}->{$key})) {
+    if (defined($self->{ids}->{$section}) &&
+       defined(my $value = $self->{ids}->{$section}->{$key})) {
        return $value;
     }
 
+    return undef if $nodefault;
+
     return $pdesc->{default};
 }
 
 # get a whole section with default value
-# this does not work for ldap entries
 sub get_section {
     my ($self, $section) = @_;
 
@@ -605,9 +713,8 @@ sub get_section {
 
        my $pdesc = $pdata->{propertyList}->{$key};
 
-       my $configid = "section_$section";
-       if (defined($self->{ids}->{$configid}) &&
-           defined(my $value = $self->{ids}->{$configid}->{$key})) {
+       if (defined($self->{ids}->{$section}) &&
+           defined(my $value = $self->{ids}->{$section}->{$key})) {
            $res->{$key} = $value;
            next;
        }
@@ -618,7 +725,6 @@ sub get_section {
 }
 
 # get a whole config with default values
-# this does not work for ldap entries
 sub get_config {
     my ($self) = @_;
 
@@ -627,7 +733,6 @@ sub get_config {
     my $res = {};
 
     foreach my $type (keys %{$pdata->{plugins}}) {
-       next if $type eq 'ldap';
        my $plugin = $pdata->{plugins}->{$type};
        $res->{$type} = $self->get_section($type);
     }
@@ -640,7 +745,7 @@ sub read_pmg_conf {
 
     local $/ = undef; # slurp mode
 
-    my $raw = <$fh>;
+    my $raw = <$fh> if defined($fh);
 
     return  PMG::Config::Base->parse_config($filename, $raw);
 }
@@ -653,24 +758,41 @@ sub write_pmg_conf {
     PVE::Tools::safe_print($filename, $fh, $raw);
 }
 
-PVE::INotify::register_file('pmg.conf', "/etc/proxmox/pmg.conf",
+PVE::INotify::register_file('pmg.conf', "/etc/pmg/pmg.conf",
                            \&read_pmg_conf,
-                           \&write_pmg_conf);
+                           \&write_pmg_conf,
+                           undef, always_call_parser => 1);
 
 # parsers/writers for other files
 
-my $domainsfilename = "/etc/proxmox/domains";
+my $domainsfilename = "/etc/pmg/domains";
+
+sub postmap_pmg_domains {
+    PMG::Utils::run_postmap($domainsfilename);
+}
 
 sub read_pmg_domains {
     my ($filename, $fh) = @_;
 
-    my $domains = [];
+    my $domains = {};
 
+    my $comment = '';
     if (defined($fh)) {
        while (defined(my $line = <$fh>)) {
-           if ($line =~ m/^\s*(\S+)\s*$/) {
+           chomp $line;
+           next if $line =~ m/^\s*$/;
+           if ($line =~ m/^#(.*)\s*$/) {
+               $comment = $1;
+               next;
+           }
+           if ($line =~ m/^(\S+)\s.*$/) {
                my $domain = $1;
-               push @$domains, $domain;
+               $domains->{$domain} = {
+                   domain => $domain, comment => $comment };
+               $comment = '';
+           } else {
+               warn "parse error in '$filename': $line\n";
+               $comment = '';
            }
        }
     }
@@ -679,10 +801,14 @@ sub read_pmg_domains {
 }
 
 sub write_pmg_domains {
-    my ($filename, $fh, $domain) = @_;
+    my ($filename, $fh, $domains) = @_;
 
-    foreach my $domain (sort @$domain) {
-       PVE::Tools::safe_print($filename, $fh, "$domain\n");
+    foreach my $domain (sort keys %$domains) {
+       my $comment = $domains->{$domain}->{comment};
+       PVE::Tools::safe_print($filename, $fh, "#$comment\n")
+           if defined($comment) && $comment !~ m/^\s*$/;
+
+       PVE::Tools::safe_print($filename, $fh, "$domain 1\n");
     }
 }
 
@@ -691,7 +817,60 @@ PVE::INotify::register_file('domains', $domainsfilename,
                            \&write_pmg_domains,
                            undef, always_call_parser => 1);
 
-my $transport_map_filename = "/etc/postfix/transport";
+my $mynetworks_filename = "/etc/pmg/mynetworks";
+
+sub postmap_pmg_mynetworks {
+    PMG::Utils::run_postmap($mynetworks_filename);
+}
+
+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);
+
+my $transport_map_filename = "/etc/pmg/transport";
+
+sub postmap_pmg_transport {
+    PMG::Utils::run_postmap($transport_map_filename);
+}
 
 sub read_transport_map {
     my ($filename, $fh) = @_;
@@ -700,63 +879,87 @@ sub read_transport_map {
 
     my $res = {};
 
+    my $comment = '';
+
     while (defined(my $line = <$fh>)) {
        chomp $line;
        next if $line =~ m/^\s*$/;
-       next if $line =~ m/^\s*\#/;
+       if ($line =~ m/^#(.*)\s*$/) {
+           $comment = $1;
+           next;
+       }
+
+       my $parse_error = sub {
+           my ($err) = @_;
+           warn "parse error in '$filename': $line - $err";
+           $comment = '';
+       };
 
-       if ($line =~ m/^(\S+)\s+smtp:([^\s:]+):(\d+)\s*$/) {
-           my $domain = $1;
-           my $host = $2;
-           my $port =$3;
-           my $nomx;
+       if ($line =~ m/^(\S+)\s+smtp:(\S+):(\d+)\s*$/) {
+           my ($domain, $host, $port) = ($1, $2, $3);
 
+           eval { pmg_verify_transport_domain($domain); };
+           if (my $err = $@) {
+               $parse_error->($err);
+               next;
+           }
+           my $use_mx = 1;
            if ($host =~ m/^\[(.*)\]$/) {
                $host = $1;
-               $nomx = 1;
+               $use_mx = 0;
            }
 
-           my $key = "$host:$port";
-
-           $res->{$key}->{nomx} = $nomx;
-           $res->{$key}->{host} = $host;
-           $res->{$key}->{port} = $port;
-           $res->{$key}->{transport} = $key;
+           eval { PVE::JSONSchema::pve_verify_address($host); };
+           if (my $err = $@) {
+               $parse_error->($err);
+               next;
+           }
 
-           push @{$res->{$key}->{domains}}, $domain;
+           my $data = {
+               domain => $domain,
+               host => $host,
+               port => $port,
+               use_mx => $use_mx,
+               comment => $comment,
+           };
+           $res->{$domain} = $data;
+           $comment = '';
+       } else {
+           $parse_error->('wrong format');
        }
     }
 
-    my $ta = [];
-
-    foreach my $t (sort keys %$res) {
-       push @$ta, $res->{$t};
-    }
-
-    return $ta;
+    return $res;
 }
 
-sub write_ransport_map {
+sub write_transport_map {
     my ($filename, $fh, $tmap) = @_;
 
     return if !$tmap;
 
-    foreach my $t (sort { $a->{transport} cmp $b->{transport} } @$tmap) {
-       my $domains = $t->{domains};
+    foreach my $domain (sort keys %$tmap) {
+       my $data = $tmap->{$domain};
 
-       foreach my $d (sort @$domains) {
-           if ($t->{nomx}) {
-               PVE::Tools::safe_print($filename, $fh, "$d smtp:[$t->{host}]:$t->{port}\n");
-           } else {
-               PVE::Tools::safe_print($filename, $fh, "$d smtp:$t->{host}:$t->{port}\n");
-           }
+       my $comment = $data->{comment};
+       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)$/;
+
+       if ($use_mx) {
+           PVE::Tools::safe_print(
+               $filename, $fh, "$data->{domain} smtp:$data->{host}:$data->{port}\n");
+       } else {
+           PVE::Tools::safe_print(
+               $filename, $fh, "$data->{domain} smtp:[$data->{host}]:$data->{port}\n");
        }
     }
 }
 
 PVE::INotify::register_file('transport', $transport_map_filename,
                            \&read_transport_map,
-                           \&write_ransport_map,
+                           \&write_transport_map,
                            undef, always_call_parser => 1);
 
 # config file generation using templates
@@ -769,27 +972,38 @@ 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;
-    $vars->{ipconfig}->{int_port} = 26;
-    $vars->{ipconfig}->{ext_port} = 25;
 
-    my $transportnets = []; # fixme
+    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";
+       }
+    }
+
     $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';
 
+    my $netlist = PVE::INotify::read_file('mynetworks');
     # add default relay to mynetworks
     if (my $relay = $self->get('mail', 'relay')) {
-       if (Net::IP::ip_is_ipv4($relay)) {
+       if ($relay =~ m/^$IPV4RE$/) {
            push @$mynetworks, "$relay/32";
-       } elsif (Net::IP::ip_is_ipv6($relay)) {
+       } elsif ($relay =~ m/^$IPV6RE$/) {
            push @$mynetworks, "[$relay]/128";
        } else {
-           warn "unable to detect IP version of relay '$relay'";
+           # DNS name - do nothing ?
        }
     }
 
@@ -804,9 +1018,47 @@ sub get_template_vars {
     $vars->{dns}->{hostname} = $nodename;
     $vars->{dns}->{domain} = $resolv->{search};
 
+    my $wlbr = "$nodename.$resolv->{search}";
+    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 {
@@ -814,23 +1066,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');
@@ -838,16 +1081,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;
 
@@ -911,7 +1152,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;
        }
     }
 
@@ -955,7 +1196,7 @@ sub rewrite_dot_forward {
 
     my $dstfn = '/root/.forward';
 
-    my $email = $self->get('administration', 'email');
+    my $email = $self->get('admin', 'email');
 
     my $output = '';
     if ($email && $email =~ m/\s*(\S+)\s*/) {
@@ -973,26 +1214,97 @@ 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;
+};
+
+my $rewrite_config_whitelist = sub {
+    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)
-    IO::File->new($domainsfilename, 'a', 0644);
+    postmap_pmg_domains();
+    postmap_pmg_transport();
+    postmap_pmg_mynetworks();
+
     IO::File->new($transport_map_filename, 'a', 0644);
 
     my $changes = 0;
 
     if ($self->get('mail', 'tls')) {
        eval {
-           my $resolv = PVE::INotify::read_file('resolvconf');
-           my $domain = $resolv->{search};
-
-           my $company = $domain; # what else ?
-           my $cn = "*.$domain";
-           PMG::Utils::gen_proxmox_tls_cert(0, $company, $cn);
+           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(
@@ -1001,9 +1313,9 @@ 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);
+    $rewrite_config_whitelist->($rulecache) if $rulecache;
+
+    # fixme: rewrite_config_tls_policy ($class);
 
     # make sure aliases.db is up to date
     system('/usr/bin/newaliases');
@@ -1012,9 +1324,12 @@ sub rewrite_config_postfix {
 }
 
 sub rewrite_config {
-    my ($self, $restart_services) = @_;
+    my ($self, $rulecache, $restart_services, $force_restart) = @_;
+
+    $force_restart = {} if ! $force_restart;
 
-    if ($self->rewrite_config_postfix() && $restart_services) {
+    if (($self->rewrite_config_postfix($rulecache) && $restart_services) ||
+       $force_restart->{postfix}) {
        PMG::Utils::service_cmd('postfix', 'restart');
     }
 
@@ -1027,18 +1342,20 @@ 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}) {
        PMG::Utils::service_cmd('pmg-smtp-filter', 'restart');
     }
 
-    if ($self->rewrite_config_clam() && $restart_services) {
-       PMG::Utils::service_cmd('clamd', 'restart');
+    if (($self->rewrite_config_clam() && $restart_services) ||
+       $force_restart->{clam}) {
+       PMG::Utils::service_cmd('clamav-daemon', 'restart');
     }
 
-    if ($self->rewrite_config_freshclam() && $restart_services) {
-       PMG::Utils::service_cmd('freshclam', 'restart');
+    if (($self->rewrite_config_freshclam() && $restart_services) ||
+       $force_restart->{freshclam}) {
+       PMG::Utils::service_cmd('clamav-freshclam', 'restart');
     }
-
 }
 
 1;