]> git.proxmox.com Git - pmg-api.git/commitdiff
pmgspamreport: tool to send spam reports per email
authorDietmar Maurer <dietmar@proxmox.com>
Wed, 26 Apr 2017 11:22:54 +0000 (13:22 +0200)
committerDietmar Maurer <dietmar@proxmox.com>
Wed, 26 Apr 2017 11:30:14 +0000 (13:30 +0200)
Makefile
PMG/CLI/pmgspamreport.pm [new file with mode: 0755]
PMG/Utils.pm
bin/pmgspamreport [new file with mode: 0644]
templates/spamreport-verbose.tmpl [new file with mode: 0644]

index b8f032453e92b69c8f7ffccc64de2a11f9dcf410..4d6512a91d9477e27e1ce34b0ce95a45020a5397 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -18,7 +18,7 @@ BASHCOMPLDIR=${DESTDIR}/usr/share/bash-completion/completions/
 REPOID=`./repoid.pl .git`
 
 SERVICES = pmgdaemon pmgproxy pmgtunnel pmgmirror
-CLITOOLS = pmgdb pmgconfig pmgperf pmgcm
+CLITOOLS = pmgdb pmgconfig pmgperf pmgcm pmgspamreport
 CLISCRIPTS = pmg-smtp-filter pmgsh pmgpolicy
 CRONSCRIPTS = pmg-hourly pmg-daily
 
@@ -35,6 +35,7 @@ SERVICE_MANS = $(addsuffix .8, ${SERVICES}) pmg-smtp-filter.8 pmgpolicy.8
 CONF_MANS= pmg.conf.5 cluster.conf.5
 
 TEMPLATES =                            \
+       spamreport-verbose.tmpl         \
        main.cf.in                      \
        main.cf.in.demo                 \
        master.cf.in                    \
diff --git a/PMG/CLI/pmgspamreport.pm b/PMG/CLI/pmgspamreport.pm
new file mode 100755 (executable)
index 0000000..b503779
--- /dev/null
@@ -0,0 +1,421 @@
+package PMG::CLI::pmgspamreport;
+
+use strict;
+use Data::Dumper;
+use Template;
+use MIME::Entity;
+use HTML::Entities;
+use Time::Local;
+use Clone 'clone';
+use Mail::Header;
+use POSIX qw(strftime);
+
+use PVE::SafeSyslog;
+use PVE::Tools;
+use PVE::INotify;
+use PVE::CLIHandler;
+
+use PMG::RESTEnvironment;
+use PMG::Utils;
+use PMG::DBTools;
+use PMG::RuleDB;
+use PMG::Config;
+use PMG::ClusterConfig;
+
+use base qw(PVE::CLIHandler);
+
+sub setup_environment {
+    PMG::RESTEnvironment->setup_default_cli_env();
+}
+
+sub domain_regex {
+    my ($domains) = @_;
+
+    my @ra;
+    foreach my $d (@$domains) {
+       # skip domains with non-DNS name characters
+       next if $d =~ m/[^A-Za-z0-9\-\.]/;
+       if ($d =~ m/^\.(.*)$/) {
+           my $dom = $1;
+           $dom =~ s/\./\\\./g;
+           push @ra, $dom;
+           push @ra, "\.\*\\.$dom";
+       } else {
+           $d =~ s/\./\\\./g;
+           push @ra, $d;
+       }
+    }
+
+    my $re = join ('|', @ra);
+
+    my $regex = qr/\@($re)$/i;
+
+    return $regex;
+}
+
+sub get_item_data {
+    my ($ref) = @_;
+
+    my @lines = split ('\n', $ref->{header});
+    my $head = new Mail::Header(\@lines);
+
+    my $data = {};
+    
+    $data->{subject} = PMG::Utils::rfc1522_to_html(
+       PVE::Tools::trim($head->get('subject')) || 'No Subject');
+
+    my @fromarray = split('\s*,\s*', $head->get('from') || $ref->{sender});
+    my $from = PMG::Utils::rfc1522_to_html(PVE::Tools::trim($fromarray[0]));
+    my $sender = PMG::Utils::rfc1522_to_html(PVE::Tools::trim($head->get('sender')));
+
+    if ($sender) {
+       $data->{sender} = $sender;
+       $data->{from} = sprintf ("%s on behalf of %s", $sender, $from);
+    } else {
+       $data->{from} = $from;
+    }
+
+    $data->{pmail} = $ref->{pmail};
+    $data->{receiver} = $ref->{receiver} || $ref->{pmail};
+
+    $data->{date} = strftime("%F", localtime($ref->{time}));
+    $data->{time} = strftime("%H%M%S", localtime($ref->{time}));
+    # fixme: $data->{ticket} = Proxmox::Utils::create_ticket ($ref);
+    $data->{bytes} = $ref->{bytes};
+    $data->{spamlevel} = $ref->{spamlevel};
+    $data->{spaminfo} = $ref->{info};
+      
+    my $title = "Received: $data->{date} $data->{time}\n";
+    $title .= "From: $ref->{sender}\n";
+    $title .= "To: $ref->{receiver}\n" if $ref->{receiver};
+    $title .= sprintf("Size: %d KB\n", int (($ref->{bytes} + 1023) / 1024 ));
+    $title .= sprintf("Spam level: %d\n", $ref->{spamlevel}) if $ref->{qtype} eq 'S';
+    $title .= sprintf("Virus info: %s\n", encode_entities ($ref->{info})) if $ref->{qtype} eq 'V';
+    $title .= sprintf("File: %s", encode_entities($ref->{file}));
+
+    # fixme: urlencode?
+    $ref->{title} = $title;
+    
+    return $data;
+}
+
+sub finalize_report {
+    my ($tmpl_data, $data, $mailfrom, $receiver) = @_;
+
+    my $html = '';
+
+    my $template = Template->new({});
+
+    $template->process(\$tmpl_data, $data, \$html) ||
+       die $template->error();
+
+    my $title;
+    if ($html =~ m|^\s*<title>(.*)</title>|m) {
+       $title = $1;
+    } else {
+       die "unable to extract template title\n";
+    }
+
+    my $top = MIME::Entity->build(
+       Type    => "multipart/related",
+       To      => $data->{pmail},
+       From    => $mailfrom,
+       Subject => PMG::Utils::bencode_header(decode_entities($title)));
+
+    $top->attach(
+       Data     => $html,
+       Type     => "text/html",
+       Encoding => "quoted-printable");
+
+    # we use an empty envelope sender (we dont want to receive NDRs)
+    PMG::Utils::reinject_mail ($top, '', [$receiver], undef, $data->{fqdn});
+}
+
+__PACKAGE__->register_method ({
+    name => 'send',
+    path => 'send',
+    method => 'POST',
+    description => "Generate and send spam report emails.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           receiver => {
+               description => "Generate report for a single email address. If not specified, generate reports for all users.",
+               type => 'string', format => 'email',
+               optional => 1,          
+           },
+           timespan => {
+               description => "Select time span.",
+               type => 'string',
+               enum => ['today', 'yesterday', 'week'],
+               default => 'today',
+               optional => 1,
+           },
+           style => {
+               description => "Spam report style. Defaults to 'none', which just prints the spam counts and does not send any emails.",
+               type => 'string',
+               enum => ['none', 'short', 'verbose', 'outlook', 'custom'],
+               optional => 1,
+               default => 'none',
+           },
+           redirect => {
+               description => "Redirect spam report email to this address.",
+               type => 'string', format => 'email',
+               optional => 1,
+           },
+       },
+    },
+    returns => { type => 'null'},
+    code => sub {
+       my ($param) = @_;
+
+       my $cinfo = PMG::ClusterConfig->new();
+       my $role = $cinfo->{local}->{type} // '-';
+
+       if (!(($role eq '-') || ($role eq 'master'))) {
+          die "local node is not master - not sending spam report\n";
+       } 
+
+       my $cfg = PMG::Config->new();
+
+       my $reportstyle = $param->{style} // 'none';
+
+       my $timespan = $param->{timespan} // 'today';
+
+       my (undef, undef, undef, $mday, $mon, $year) = localtime(time());       
+       my $daystart = timelocal(0, 0, 0, $mday, $mon, $year);
+       
+       my $start;
+       my $end;
+       
+       if ($timespan eq 'today') {
+           $start = $daystart;
+           $end = $start + 86400;
+       } elsif ($timespan eq 'yesterday') {
+           $end = $daystart;
+           $start = $end - 86400;
+       } elsif ($timespan eq 'week') {
+           $end = $daystart;
+           $start = $end - 7*86400;
+       } else {
+           die "internal error";
+       }
+
+       my $hostname = PVE::INotify::nodename();
+       
+       my $fqdn = $cfg->get('spamquar', 'hostname') // 
+           PVE::Tools::get_fqdn($hostname);
+       
+       my $port = 8006;
+       
+       my $global_data = {
+           protocol => 'https',
+           port => $port,
+           fqdn => $fqdn,
+           hostname => $hostname,
+           actionhref => "https://$fqdn:$port/userquar/",
+           date => strftime("%F", localtime($end - 1)),
+           timespan => $timespan,
+           items => [],
+       };
+
+       my $mailfrom = $cfg->get ('spamquar', 'mailfrom') // 
+           "Proxmox Mail Gateway <postmaster>";
+       
+       my $dbh = PMG::DBTools::open_ruledb();
+
+       my $target = $param->{receiver};
+       my $redirect = $param->{redirect};
+       
+       if (defined($redirect)) {
+           die "can't redirect mails for all users\n";
+       }
+       
+       my $domains = PVE::INotify::read_file('domains');
+       my $domainregex = domain_regex([keys %$domains]);
+
+       my $tmpl_data;
+       
+       if ($reportstyle ne 'none') {
+
+           my $template_filename;
+
+           my $customtmpl =  "/etc/pmg/spamreport.tmpl";
+           if ($reportstyle eq 'verbose') {
+               $template_filename = "/var/lib/pmg/templates/spamreport-verbose.tmpl";
+           } elsif ($reportstyle eq 'outlook') {
+               $template_filename = "/var/lib/pmg/templates/spamreport-verbose-noform.tmpl";
+           } elsif (($reportstyle eq 'custom') && (-f $customtmpl)) {
+               $template_filename = $customtmpl;
+           } else {
+               $template_filename = "/var/lib/pmg/templates/spamreport-short.tmpl";
+           }
+
+           $tmpl_data = PVE::Tools::file_get_contents($template_filename);
+       }
+       
+           
+       my $sth = $dbh->prepare(
+           "SELECT * FROM CMailStore, CMSReceivers " . 
+           "WHERE time >= $start AND time < $end AND " . 
+           ($target ? "pmail = ? AND " : '') .
+           "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
+           "AND Status = 'N' " .
+           "ORDER BY pmail, time, receiver");
+           
+       if ($target) {
+           $sth->execute($target);
+       } else {
+           $sth->execute();
+       }
+
+       my $lastref;
+       my $mailcount = 0;
+       my $creceiver = '';
+       my $data;
+       
+       my $finalize = sub {
+           
+           my $extern = ($domainregex && $creceiver !~ $domainregex);
+               
+           if ($tmpl_data) {
+               if (!$extern) {
+                   # fixme: my $ticket = Proxmox::Utils::create_ticket ($lastref);
+                   my $ticket = "TEST";
+                   $data->{ticket} = $ticket;
+                   $data->{mailcount} = $mailcount;
+
+                   my $sendto = $redirect ? $redirect : $creceiver;
+                   finalize_report($tmpl_data, $data, $mailfrom, $sendto);
+               }
+           } else {
+               my $hint = $extern ? " (external address)" : "";
+               printf ("%-5d %s$hint\n", $mailcount, $creceiver);
+           }
+       };
+       
+       while (my $ref = $sth->fetchrow_hashref()) {
+           if ($creceiver ne $ref->{pmail}) {
+
+               $finalize->() if $data;
+
+               $data = clone($global_data);
+               
+               $creceiver = $ref->{pmail};
+               $mailcount = 0;
+
+               $data->{pmail} = $creceiver;
+           }
+
+           if ($tmpl_data) {
+               push @{$data->{items}}, get_item_data($ref);
+           }
+           
+           $mailcount++;
+           $lastref = $ref;
+       }
+
+       $sth->finish();
+
+       $finalize->() if $data;
+
+       if (defined($target) && !$mailcount) {
+           print STDERR "no mails for '$target'\n";
+       }
+
+       return undef;
+    }});
+
+sub find_stale_files {
+    my ($path, $lifetime, $purge) = @_;
+
+    my $cmd = ['find', $path, '-daystart', '-mtime', '+$lifetime',
+              '-type', 'f'];
+
+    if ($purge) {
+       push @$cmd, '-exec', 'rm', '-vf', '{}', ';';
+    } else {
+       push @$cmd, '-print';
+    }
+    
+    PVE::Tools::run_command($cmd);
+}
+
+sub test_quarantine_files {
+    my ($spamlifetime, $viruslifetime, $purge) = @_;
+    
+    print STDERR "searching for stale files\n" if !$purge; 
+
+    find_stale_files ("/var/spool/proxmox/spam", $spamlifetime, $purge);
+    find_stale_files ("/var/spool/proxmox/cluster/*/spam", $spamlifetime, $purge);
+
+    find_stale_files ("/var/spool/proxmox/virus", $viruslifetime, $purge);
+    find_stale_files ("/var/spool/proxmox/cluster/*/virus", $viruslifetime, $purge);
+}
+
+__PACKAGE__->register_method ({
+    name => 'purge',
+    path => 'purge',
+    method => 'POST',
+    description => "Cleanup Quarantine database. Remove entries older than configured quarantine lifetime.",
+    parameters => {
+       additionalProperties => 0,
+    },
+    returns => { type => 'null'},
+    code => sub {
+       my ($param) = @_;
+
+       my $cfg = PMG::Config->new();
+
+       my $spamlifetime = $cfg->get('spamquar', 'lifetime');
+       my $viruslifetime = $cfg->get ('virusquar', 'lifetime');
+
+       print STDERR "purging database\n"; 
+
+       my $dbh = PMG::DBTools::open_ruledb();
+
+       if (my $count = PMG::DBTools::purge_quarantine_database($dbh, 'S', $spamlifetime)) {
+           print STDERR "removed $count spam quarantine files\n"; 
+       }
+
+       if (my $count = PMG::DBTools::purge_quarantine_database($dbh, 'V', $viruslifetime)) {
+           print STDERR "removed $count virus quarantine files\n"; 
+       }
+
+       test_quarantine_files($spamlifetime, $viruslifetime, 1);
+
+       return undef;
+    }});
+
+__PACKAGE__->register_method ({
+    name => 'check',
+    path => 'check',
+    method => 'GET',
+    description => "Search Quarantine database for entries older than configured quarantine lifetime.",
+    parameters => {
+       additionalProperties => 0,
+    },
+    returns => { type => 'null'},
+    code => sub {
+       my ($param) = @_;
+
+       my $cfg = PMG::Config->new();
+
+       my $spamlifetime = $cfg->get('spamquar', 'lifetime');
+       my $viruslifetime = $cfg->get ('virusquar', 'lifetime');
+
+       test_quarantine_files($spamlifetime, $viruslifetime, 0);
+
+       return undef;
+    }});
+
+
+
+our $cmddef = {
+    'check' => [ __PACKAGE__, 'check', []],
+    'purge' => [ __PACKAGE__, 'purge', []],
+    'send' => [ __PACKAGE__, 'send', []],
+};
+
+1;
index 6028804d893db9849c1a47350f7235c2b9f348b2..8a7aaddb841f4c4ebf0a4402fcde2b47f6b417e6 100644 (file)
@@ -21,6 +21,7 @@ use Socket;
 use RRDs;
 use Filesys::Df;
 use Encode;
+use HTML::Entities;
 
 use PVE::ProcFSTools;
 use PVE::Network;
@@ -855,4 +856,29 @@ sub create_rrd_data {
     return $res;
 }
 
+sub rfc1522_to_html {
+    my ($enc) = @_;
+
+    my $res = '';
+
+    return '' if !$enc;
+
+    eval {
+       foreach my $r (MIME::Words::decode_mimewords($enc)) {
+           my ($d, $cs) = @$r;
+           if ($d) {
+               if ($cs) {
+                   $res .= encode_entities(decode($cs, $d));
+               } else {
+                   $res .= encode_entities($d);
+               }
+           }
+       }
+    };
+
+    $res = $enc if $@;
+
+    return $res;
+}
+
 1;
diff --git a/bin/pmgspamreport b/bin/pmgspamreport
new file mode 100644 (file)
index 0000000..0749375
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/perl -T
+
+use strict;
+use warnings;
+
+use PMG::CLI::pmgspamreport;
+
+PMG::CLI::pmgspamreport->run_cli_handler();
diff --git a/templates/spamreport-verbose.tmpl b/templates/spamreport-verbose.tmpl
new file mode 100644 (file)
index 0000000..838c4d2
--- /dev/null
@@ -0,0 +1,61 @@
+[%- IF timespan == 'week' -%]
+[%- SET title = "Weekly Spam Report for '${pmail}' - ${date}'" -%]
+[%- ELSE %]
+[%- SET title = "Daily Spam Report for '${pmail}' - ${date}" -%]
+[%- END -%]
+<html>
+<head>
+<title>[% title %]</title>
+</head>
+<body>
+
+<div align=center>
+
+<form action='[% actionhref %]' method='POST'>
+<input type=hidden name=ticket value='[% ticket %]'>
+
+<table width='100%'>
+  <tr>
+    <td colspan=3>
+      <div style='width:600px;'><h2>[% title %]</h2></div>
+    </td>
+  </tr>
+
+  <tr><td colspan=3><hr></td></tr>     
+
+  <!--start entries-->
+  [% FOREACH item IN items %]
+  <tr title='[% item.title %]'>
+    <td><input type=checkbox name=cselect value='[% item.ticket %]'></td>
+    <td>[% item.from %]</td>
+    <td align=right>[% item.data %] [% item.time %]</td>
+  </tr>
+  <tr title='[% item.title %]'>
+    <td></td>
+    <td colspan=2><a style='cursor:pointer;' href='[% item.href %]'>
+       <b>[% item.subject %]</b></a></td>
+  </tr>
+  <tr><td colspan=3><hr></td></tr>
+  [% END %]
+  <!--end entries-->
+
+  <tr>
+    <td colspan=3>
+      <div align=center>
+      <input type=submit name=whitelist>
+      <input type=submit name=blacklist>
+      <input type=submit name=deliver>
+      <input type=submit name=delete>
+      </div>
+    </td>
+  </tr>
+  
+</table>
+
+<p>Please use the <a href='[% manage %]?ticket=[% ticket %]'>web interface</a> to manage your spam quarantine.</p>
+<p>Powered by <a target=_blank href='http://www.proxmox.com'>Proxmox</a>.</p>
+</div>
+
+</form>
+</body>
+</html>