]> git.proxmox.com Git - pmg-api.git/blobdiff - PMG/Config.pm
PMG/API2/MyNetworks.pm: fix links attribute
[pmg-api.git] / PMG / Config.pm
index ab468df68c057b911d7402603e07177ece2c873c..c387d691160426969741eea5e5e8051a2c2d2424 100755 (executable)
@@ -2,6 +2,7 @@ package PMG::Config::Base;
 
 use strict;
 use warnings;
+use URI;
 use Data::Dumper;
 
 use PVE::Tools;
@@ -60,6 +61,11 @@ sub type {
 
 sub properties {
     return {
+       advfilter => {
+           description => "Use advanced filters for statistic.",
+           type => 'boolean',
+           default => 1,
+       },
        dailyreport => {
            description => "Send daily reports.",
            type => 'boolean',
@@ -81,37 +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 },
        email => { optional => 1 },
-       proxyport => { optional => 1 },
-       proxyserver => { optional => 1 },
-       proxyuser => { optional => 1 },
-       proxypassword => { optional => 1 },
+       http_proxy => { optional => 1 },
     };
 }
 
@@ -153,6 +144,13 @@ sub properties {
            description => "Whitelist legitimate bounce relays.",
            type => 'string',
        },
+       clamav_heuristic_score => {
+           description => "Score for ClamaAV heuristics (Google Safe Browsing database, PhishingScanURLs, ...).",
+           type => 'integer',
+           minimum => 0,
+           maximum => 1000,
+           default => 3,
+       },
        bounce_score => {
            description => "Additional score for bounce mails.",
            type => 'integer',
@@ -169,7 +167,7 @@ sub properties {
            description => "Maximum size of spam messages in bytes.",
            type => 'integer',
            minimum => 64,
-           default => 200*1024,
+           default => 256*1024,
        },
     };
 }
@@ -181,6 +179,7 @@ sub options {
        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 },
@@ -215,7 +214,7 @@ sub properties {
        reportstyle => {
            description => "Spam report style.",
            type => 'string',
-           enum => [qw(none short verbose outlook custom)],
+           enum => [qw(none short verbose custom)],
            default => 'verbose',
        },
        viewimages => {
@@ -412,14 +411,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).",
@@ -510,11 +509,6 @@ sub properties {
            minimum => 0,
            default => 4,
        },
-       use_rbl => {
-           description => "Use Realtime Blacklists.",
-           type => 'boolean',
-           default => 1,
-       },
        tls => {
            description => "Enable TLS.",
            type => 'boolean',
@@ -562,7 +556,7 @@ 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',
        },
     };
 }
@@ -580,7 +574,6 @@ 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 },
@@ -599,6 +592,7 @@ sub options {
        dnsbl_sites => { optional => 1 },
     };
 }
+
 package PMG::Config;
 
 use strict;
@@ -612,6 +606,8 @@ 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();
@@ -624,6 +620,7 @@ PMG::Config::Base->init();
 
 PVE::JSONSchema::register_format(
     'transport-domain', \&pmg_verify_transport_domain);
+
 sub pmg_verify_transport_domain {
     my ($name, $noerr) = @_;
 
@@ -637,6 +634,22 @@ sub pmg_verify_transport_domain {
     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 weight: 'domain*<WEIGHT>'
+    my $namere = "([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?)";
+
+    if ($name !~ /^(${namere}\.)*${namere}(\*\-?\d+)?$/) {
+          return undef if $noerr;
+          die "value '$name' does not look like a valid dnsbl entry\n";
+    }
+    return $name;
+}
+
 sub new {
     my ($type) = @_;
 
@@ -688,7 +701,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};
@@ -701,6 +714,8 @@ sub get {
        return $value;
     }
 
+    return undef if $nodefault;
+
     return $pdesc->{default};
 }
 
@@ -823,10 +838,6 @@ PVE::INotify::register_file('domains', $domainsfilename,
 
 my $mynetworks_filename = "/etc/pmg/mynetworks";
 
-sub postmap_pmg_mynetworks {
-    PMG::Utils::run_postmap($mynetworks_filename);
-}
-
 sub read_pmg_mynetworks {
     my ($filename, $fh) = @_;
 
@@ -870,6 +881,12 @@ PVE::INotify::register_file('mynetworks', $mynetworks_filename,
                            \&write_pmg_mynetworks,
                            undef, always_call_parser => 1);
 
+my $tls_policy_map_filename = "/etc/pmg/tls_policy";
+
+sub postmap_tls_policy {
+    PMG::Utils::run_postmap($tls_policy_map_filename);
+}
+
 my $transport_map_filename = "/etc/pmg/transport";
 
 sub postmap_pmg_transport {
@@ -981,25 +998,28 @@ sub get_template_vars {
 
     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';
 
     my $netlist = PVE::INotify::read_file('mynetworks');
+    push @$mynetworks, keys %$netlist;
+
+    push @$mynetworks, @$transportnets;
+
     # add default relay to mynetworks
     if (my $relay = $self->get('mail', 'relay')) {
        if ($relay =~ m/^$IPV4RE$/) {
@@ -1013,18 +1033,64 @@ 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);
+    }
+
     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;
 
     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 {
@@ -1032,23 +1098,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');
@@ -1056,16 +1113,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;
 
@@ -1191,15 +1246,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();
-    postmap_pmg_mynetworks();
-
     IO::File->new($transport_map_filename, 'a', 0644);
 
     my $changes = 0;
@@ -1217,9 +1341,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');
@@ -1228,11 +1356,11 @@ sub rewrite_config_postfix {
 }
 
 sub rewrite_config {
-    my ($self, $restart_services, $force_restart) = @_;
+    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');
     }