1 package PMG
::CLI
::pmgqm
;
11 use POSIX
qw(strftime);
21 use PMG
::RESTEnvironment
;
27 use PMG
::ClusterConfig
;
28 use PMG
::API2
::Quarantine
;
30 use base
qw(PVE::CLIHandler);
32 sub setup_environment
{
33 PMG
::RESTEnvironment-
>setup_default_cli_env();
40 foreach my $d (@$domains) {
41 # skip domains with non-DNS name characters
42 next if $d =~ m/[^A-Za-z0-9\-\.]/;
43 if ($d =~ m/^\.(.*)$/) {
47 push @ra, "\.\*\\.$dom";
54 my $re = join ('|', @ra);
56 my $regex = qr/\@($re)$/i;
62 my ($data, $ref) = @_;
64 my @lines = split ('\n', $ref->{header
});
65 my $head = new Mail
::Header
(\
@lines);
69 $item->{id
} = sprintf("C%dR%d", $ref->{cid
}, $ref->{rid
});
71 $item->{subject
} = PMG
::Utils
::rfc1522_to_html
(
72 PVE
::Tools
::trim
($head->get('subject')) || 'No Subject');
74 my @fromarray = split('\s*,\s*', $head->get('from') || $ref->{sender
});
75 my $from = PMG
::Utils
::rfc1522_to_html
(PVE
::Tools
::trim
($fromarray[0]));
76 my $sender = PMG
::Utils
::rfc1522_to_html
(PVE
::Tools
::trim
($head->get('sender')));
79 $item->{sender
} = $sender;
80 $item->{from
} = sprintf ("%s on behalf of %s", $sender, $from);
82 $item->{from
} = $from;
85 $item->{pmail
} = $ref->{pmail
};
86 $item->{receiver
} = $ref->{receiver
} || $ref->{pmail
};
88 $item->{date
} = strftime
("%F", localtime($ref->{time}));
89 $item->{time} = strftime
("%H:%M:%S", localtime($ref->{time}));
91 $item->{bytes
} = $ref->{bytes
};
92 $item->{spamlevel
} = $ref->{spamlevel
};
93 $item->{spaminfo
} = $ref->{info
};
95 my $title = "Received: $item->{date} $item->{time}\n";
96 $title .= "From: $ref->{sender}\n";
97 $title .= "To: $ref->{receiver}\n" if $ref->{receiver
};
98 $title .= sprintf("Size: %d KB\n", int (($ref->{bytes
} + 1023) / 1024 ));
99 $title .= sprintf("Spam level: %d\n", $ref->{spamlevel
}) if $ref->{qtype
} eq 'S';
100 $title .= sprintf("Virus info: %s\n", encode_entities
($ref->{info
})) if $ref->{qtype
} eq 'V';
101 $title .= sprintf("File: %s", encode_entities
($ref->{file
}));
103 $item->{title
} = $title;
105 my $basehref = "https://$data->{fqdn}:$data->{port}/quarantine";
106 my $ticket = uri_escape
($data->{ticket
});
107 $item->{wlhref
} = "$basehref?ticket=$ticket&cselect=$item->{id}&action=whitelist";
108 $item->{blhref
} = "$basehref?ticket=$ticket&cselect=$item->{id}&action=blacklist";
109 $item->{deliverhref
} = "$basehref?ticket=$ticket&cselect=$item->{id}&action=deliver";
110 $item->{deletehref
} = "$basehref?ticket=$ticket&cselect=$item->{id}&action=delete";
115 sub finalize_report
{
116 my ($tt, $template, $data, $mailfrom, $receiver, $debug) = @_;
120 $tt->process($template, $data, \
$html) ||
121 die $tt->error() . "\n";
124 if ($html =~ m
|^\s
*<title
>(.*)</title
>|m
) {
127 die "unable to extract template title\n";
130 my $top = MIME
::Entity-
>build(
131 Type
=> "multipart/related",
132 To
=> $data->{pmail
},
134 Subject
=> PMG
::Utils
::bencode_header
(decode_entities
($title)));
139 Encoding
=> $debug ?
'binary' : 'quoted-printable');
145 # we use an empty envelope sender (we dont want to receive NDRs)
146 PMG
::Utils
::reinject_mail
($top, '', [$receiver], undef, $data->{fqdn
});
149 __PACKAGE__-
>register_method ({
153 description
=> "Generate and send spam report emails.",
155 additionalProperties
=> 0,
158 description
=> "Generate report for a single email address. If not specified, generate reports for all users.",
159 type
=> 'string', format
=> 'email',
163 description
=> "Select time span.",
165 enum
=> ['today', 'yesterday', 'week'],
170 description
=> "Spam report style. Value 'none' just prints the spam counts and does not send any emails. Default value is read from spam quarantine configuration.",
172 enum
=> ['none', 'short', 'verbose', 'custom'],
176 description
=> "Redirect spam report email to this address.",
177 type
=> 'string', format
=> 'email',
181 description
=> "Debug mode. Print raw email to stdout instead of sending them.",
188 returns
=> { type
=> 'null'},
192 my $cinfo = PMG
::ClusterConfig-
>new();
193 my $role = $cinfo->{local}->{type
} // '-';
195 if (!(($role eq '-') || ($role eq 'master'))) {
196 die "local node is not master - not sending spam report\n";
199 my $cfg = PMG
::Config-
>new();
201 my $reportstyle = $param->{style
} // $cfg->get('spamquar', 'reportstyle');
203 my $timespan = $param->{timespan
} // 'today';
205 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
206 my $daystart = timelocal
(0, 0, 0, $mday, $mon, $year);
211 if ($timespan eq 'today') {
213 $end = $start + 86400;
214 } elsif ($timespan eq 'yesterday') {
216 $start = $end - 86400;
217 } elsif ($timespan eq 'week') {
219 $start = $end - 7*86400;
221 die "internal error";
224 my $hostname = PVE
::INotify
::nodename
();
226 my $fqdn = $cfg->get('spamquar', 'hostname') //
227 PVE
::Tools
::get_fqdn
($hostname);
235 hostname
=> $hostname,
236 date
=> strftime
("%F", localtime($end - 1)),
237 timespan
=> $timespan,
241 my $mailfrom = $cfg->get ('spamquar', 'mailfrom') //
242 "Proxmox Mail Gateway <postmaster>";
244 my $dbh = PMG
::DBTools
::open_ruledb
();
246 my $target = $param->{receiver
};
247 my $redirect = $param->{redirect
};
249 if (defined($redirect) && !defined($target)) {
250 die "can't redirect mails for all users\n";
253 my $domains = PVE
::INotify
::read_file
('domains');
254 my $domainregex = domain_regex
([keys %$domains]);
258 if ($reportstyle ne 'none') {
260 $template = "spamreport-${reportstyle}.tt";
262 foreach my $path (@$PMG::Config
::tt_include_path
) {
263 if (-f
"$path/$template") { $found = 1; last; }
266 warn "unable to find template '$template' - using default\n";
267 $template = "spamreport-verbose.tt";
271 my $sth = $dbh->prepare(
272 "SELECT * FROM CMailStore, CMSReceivers " .
273 "WHERE time >= $start AND time < $end AND " .
274 ($target ?
"pmail = ? AND " : '') .
275 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
276 "AND Status = 'N' " .
277 "ORDER BY pmail, time, receiver");
280 $sth->execute($target);
290 my $tt = PMG
::Config
::get_template_toolkit
();
294 my $extern = ($domainregex && $creceiver !~ $domainregex);
298 $data->{mailcount
} = $mailcount;
299 my $sendto = $redirect ?
$redirect : $creceiver;
300 finalize_report
($tt, $template, $data, $mailfrom, $sendto, $param->{debug
});
303 my $hint = $extern ?
" (external address)" : "";
304 printf ("%-5d %s$hint\n", $mailcount, $creceiver);
308 while (my $ref = $sth->fetchrow_hashref()) {
309 if ($creceiver ne $ref->{pmail
}) {
311 $finalize->() if $data;
313 $data = clone
($global_data);
315 $creceiver = $ref->{pmail
};
318 $data->{pmail
} = $creceiver;
319 $data->{ticket
} = PMG
::Ticket
::assemble_quarantine_ticket
($data->{pmail
});
320 my $esc_ticket = uri_escape
($data->{ticket
});
321 $data->{managehref
} = "https://$fqdn:$port/quarantine?ticket=${esc_ticket}";
325 push @{$data->{items
}}, get_item_data
($data, $ref);
334 $finalize->() if $data;
336 if (defined($target) && !$mailcount) {
337 print STDERR
"no mails for '$target'\n";
343 sub find_stale_files
{
344 my ($path, $lifetime, $purge) = @_;
346 return if ! -d
$path;
348 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
349 my $daystart = timelocal
(0, 0, 0, $mday, $mon, $year);
350 my $expire = $daystart - $lifetime*86400;
353 my $name = $File::Find
::name
;
354 return if $name !~ m
|^($path/.*)$|;
355 $name = $1; # untaint
356 my $stat = stat($name);
358 return if $stat->mtime >= $expire;
361 print "removed: $name\n";
368 find
({ wanted
=> $wanted, no_chdir
=> 1 }, $path);
371 sub test_quarantine_files
{
372 my ($spamlifetime, $viruslifetime, $purge) = @_;
374 print STDERR
"searching for stale files\n" if !$purge;
376 my $spooldir = $PMG::MailQueue
::spooldir
;
378 find_stale_files
("$spooldir/spam", $spamlifetime, $purge);
379 foreach my $dir (<"/var/spool/pmg/cluster/*/spam">) {
380 next if $dir !~ m
|^(/var/spool/pmg/cluster
/\d+/spam)$|;
382 find_stale_files
($dir, $spamlifetime, $purge);
385 find_stale_files
("$spooldir/virus", $viruslifetime, $purge);
386 foreach my $dir (<"/var/spool/pmg/cluster/*/virus">) {
387 next if $dir !~ m
|^(/var/spool/pmg/cluster
/\d+/virus
)$|;
389 find_stale_files
($dir, $viruslifetime, $purge);
393 __PACKAGE__-
>register_method ({
397 description
=> "Cleanup Quarantine database. Remove entries older than configured quarantine lifetime.",
399 additionalProperties
=> 0,
402 description
=> "Only search for quarantine files older than configured quarantine lifetime. Just print found files, but do not remove them.",
409 returns
=> { type
=> 'null'},
413 my $cfg = PMG
::Config-
>new();
415 my $spamlifetime = $cfg->get('spamquar', 'lifetime');
416 my $viruslifetime = $cfg->get ('virusquar', 'lifetime');
418 my $purge = !$param->{check
};
421 print STDERR
"purging database\n";
423 my $dbh = PMG
::DBTools
::open_ruledb
();
425 if (my $count = PMG
::DBTools
::purge_quarantine_database
($dbh, 'S', $spamlifetime)) {
426 print STDERR
"removed $count spam quarantine files\n";
429 if (my $count = PMG
::DBTools
::purge_quarantine_database
($dbh, 'V', $viruslifetime)) {
430 print STDERR
"removed $count virus quarantine files\n";
434 test_quarantine_files
($spamlifetime, $viruslifetime, $purge);
441 'purge' => [ __PACKAGE__
, 'purge', []],
442 'send' => [ __PACKAGE__
, 'send', []],
443 'spam' => [ 'PMG::API2::Quarantine', 'spam', [], undef, sub {
445 print "Day Count AVG(Spam)\n";
446 foreach my $ref (@$res) {
447 print sprintf("%-12s %5d %10.2f\n",
448 strftime
("%F", localtime($ref->{day
})),
449 $ref->{count
}, $ref->{spamavg
});