]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/CLI/pmgqm.pm
fix 4006: do not split from header on ', ' for spamreport mails
[pmg-api.git] / src / PMG / CLI / pmgqm.pm
1 package PMG::CLI::pmgqm;
2
3 use strict;
4 use Data::Dumper;
5 use Template;
6 use MIME::Entity;
7 use HTML::Entities;
8 use Time::Local;
9 use Clone 'clone';
10 use Mail::Header;
11 use POSIX qw(strftime);
12 use File::Find;
13 use File::stat;
14 use URI::Escape;
15
16 use PVE::SafeSyslog;
17 use PVE::Tools;
18 use PVE::INotify;
19 use PVE::CLIHandler;
20
21 use PMG::RESTEnvironment;
22 use PMG::Utils;
23 use PMG::Ticket;
24 use PMG::DBTools;
25 use PMG::RuleDB;
26 use PMG::Config;
27 use PMG::ClusterConfig;
28 use PMG::API2::Quarantine;
29
30 use base qw(PVE::CLIHandler);
31
32 sub setup_environment {
33 PMG::RESTEnvironment->setup_default_cli_env();
34 }
35
36 sub get_item_data {
37 my ($data, $ref) = @_;
38
39 my @lines = split ('\n', $ref->{header});
40 my $head = new Mail::Header(\@lines);
41
42 my $item = {};
43
44 $item->{id} = sprintf("C%dR%dT%d", $ref->{cid}, $ref->{rid}, $ref->{ticketid});
45
46 $item->{subject} = PMG::Utils::rfc1522_to_html(
47 PVE::Tools::trim($head->get('subject')) || 'No Subject');
48
49 my $from = PMG::Utils::rfc1522_to_html(PVE::Tools::trim($head->get('from') // $ref->{sender}));
50 my $sender = PMG::Utils::rfc1522_to_html(PVE::Tools::trim($head->get('sender')));
51
52 if ($sender) {
53 $item->{sender} = $sender;
54 $item->{from} = sprintf ("%s on behalf of %s", $sender, $from);
55 } else {
56 $item->{from} = $from;
57 }
58
59 $item->{envelope_sender} = $ref->{sender};
60 $item->{pmail} = $ref->{pmail};
61 $item->{receiver} = $ref->{receiver} || $ref->{pmail};
62
63 $item->{date} = strftime("%F", localtime($ref->{time}));
64 $item->{time} = strftime("%H:%M:%S", localtime($ref->{time}));
65
66 $item->{bytes} = $ref->{bytes};
67 $item->{spamlevel} = $ref->{spamlevel};
68 $item->{spaminfo} = $ref->{info};
69 $item->{file} = $ref->{file};
70
71 my $basehref = "$data->{protocol_fqdn_port}/quarantine";
72 if ($data->{authmode} ne 'ldap') {
73 my $ticket = uri_escape($data->{ticket});
74 $item->{href} = "$basehref?ticket=$ticket&cselect=$item->{id}&date=$item->{date}";
75 } else {
76 $item->{href} = "$basehref?cselect=$item->{id}&date=$item->{date}";
77 }
78
79 return $item;
80 }
81
82 __PACKAGE__->register_method ({
83 name => 'status',
84 path => 'status',
85 method => 'POST',
86 description => "Print quarantine status (mails per user) for specified time span.",
87 parameters => {
88 additionalProperties => 0,
89 properties => {
90 timespan => {
91 description => "Select time span.",
92 type => 'string',
93 enum => ['today', 'yesterday', 'week'],
94 default => 'today',
95 optional => 1,
96 },
97 },
98 },
99 returns => { type => 'null'},
100 code => sub {
101 my ($param) = @_;
102
103 my $cinfo = PMG::ClusterConfig->new();
104 my $role = $cinfo->{local}->{type} // '-';
105
106 if (!(($role eq '-') || ($role eq 'master'))) {
107 warn "local node is not master\n";
108 return;
109 }
110
111 my $cfg = PMG::Config->new();
112
113 my $timespan = $param->{timespan} // 'today';
114
115 my ($start, $end) = PMG::Utils::lookup_timespan($timespan);
116
117 my $hostname = PVE::INotify::nodename();
118
119 my $fqdn = $cfg->get('spamquar', 'hostname') //
120 PVE::Tools::get_fqdn($hostname);
121
122
123 my $dbh = PMG::DBTools::open_ruledb();
124
125 my $domains = PVE::INotify::read_file('domains');
126 my $domainregex = PMG::Utils::domain_regex([keys %$domains]);
127
128 my $sth = $dbh->prepare(
129 "SELECT pmail, AVG(spamlevel) as spamlevel, count(*) FROM CMailStore, CMSReceivers " .
130 "WHERE time >= $start AND time < $end AND " .
131 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
132 "AND Status = 'N' " .
133 "GROUP BY pmail " .
134 "ORDER BY pmail");
135
136 $sth->execute();
137
138 print "Count Spamlevel Mail\n";
139 my $res = [];
140 while (my $ref = $sth->fetchrow_hashref()) {
141 push @$res, $ref;
142 my $extern = ($domainregex && $ref->{pmail} !~ $domainregex);
143 my $hint = $extern ? " (external address)" : "";
144 printf ("%-5d %10.2f %s$hint\n", $ref->{count}, $ref->{spamlevel}, $ref->{pmail});
145 }
146
147 $sth->finish();
148
149 return undef;
150 }});
151
152 __PACKAGE__->register_method ({
153 name => 'send',
154 path => 'send',
155 method => 'POST',
156 description => "Generate and send spam report emails.",
157 parameters => {
158 additionalProperties => 0,
159 properties => {
160 receiver => {
161 description => "Generate report for a single email address. If not specified, generate reports for all users.",
162 type => 'string', format => 'email',
163 optional => 1,
164 },
165 timespan => {
166 description => "Select time span.",
167 type => 'string',
168 enum => ['today', 'yesterday', 'week'],
169 default => 'today',
170 optional => 1,
171 },
172 style => {
173 description => "Spam report style. Default value is read from spam quarantine configuration.",
174 type => 'string',
175 enum => ['short', 'verbose', 'custom'],
176 optional => 1,
177 },
178 redirect => {
179 description => "Redirect spam report email to this address.",
180 type => 'string', format => 'email',
181 optional => 1,
182 },
183 debug => {
184 description => "Debug mode. Print raw email to stdout instead of sending them.",
185 type => 'boolean',
186 optional => 1,
187 default => 0,
188 }
189 },
190 },
191 returns => { type => 'null'},
192 code => sub {
193 my ($param) = @_;
194
195 my $cinfo = PMG::ClusterConfig->new();
196 my $role = $cinfo->{local}->{type} // '-';
197
198 if (!(($role eq '-') || ($role eq 'master'))) {
199 warn "local node is not master - not sending spam report\n";
200 return;
201 }
202
203 my $cfg = PMG::Config->new();
204
205 my $reportstyle = $param->{style} // $cfg->get('spamquar', 'reportstyle');
206
207 # overwrite report style none when:
208 # - explicit receiver specified
209 # - when debug flag enabled
210 if ($reportstyle eq 'none') {
211 $reportstyle = 'verbose' if $param->{debug} || defined($param->{receiver});
212 }
213
214 return if $reportstyle eq 'none'; # do nothing
215
216 my $timespan = $param->{timespan} // 'today';
217
218 my ($start, $end) = PMG::Utils::lookup_timespan($timespan);
219
220 my $hostname = PVE::INotify::nodename();
221
222 my $fqdn = $cfg->get('spamquar', 'hostname') //
223 PVE::Tools::get_fqdn($hostname);
224
225 my $port = $cfg->get('spamquar', 'port') // 8006;
226
227 my $protocol = $cfg->get('spamquar', 'protocol') // 'https';
228
229 my $protocol_fqdn_port = "$protocol://$fqdn";
230 if (($protocol eq 'https' && $port != 443) ||
231 ($protocol eq 'http' && $port != 80)) {
232 $protocol_fqdn_port .= ":$port";
233 }
234
235 my $authmode = $cfg->get ('spamquar', 'authmode') // 'ticket';
236
237 my $global_data = {
238 protocol => $protocol,
239 port => $port,
240 fqdn => $fqdn,
241 hostname => $hostname,
242 date => strftime("%F", localtime($end - 1)),
243 timespan => $timespan,
244 items => [],
245 protocol_fqdn_port => $protocol_fqdn_port,
246 authmode => $authmode,
247 };
248
249 my $mailfrom = $cfg->get ('spamquar', 'mailfrom') //
250 "Proxmox Mail Gateway <postmaster>";
251
252 my $dbh = PMG::DBTools::open_ruledb();
253
254 my $target = $param->{receiver};
255 my $redirect = $param->{redirect};
256
257 if (defined($redirect) && !defined($target)) {
258 die "can't redirect mails for all users\n";
259 }
260
261 my $domains = PVE::INotify::read_file('domains');
262 my $domainregex = PMG::Utils::domain_regex([keys %$domains]);
263
264 my $template = "spamreport-${reportstyle}.tt";
265 my $found = 0;
266 foreach my $path (@$PMG::Config::tt_include_path) {
267 if (-f "$path/$template") { $found = 1; last; }
268 }
269 if (!$found) {
270 warn "unable to find template '$template' - using default\n";
271 $template = "spamreport-verbose.tt";
272 }
273
274 my $sth = $dbh->prepare(
275 "SELECT * FROM CMailStore, CMSReceivers " .
276 "WHERE time >= $start AND time < $end AND " .
277 ($target ? "pmail = ? AND " : '') .
278 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
279 "AND Status = 'N' " .
280 "ORDER BY pmail, time, receiver");
281
282 if ($target) {
283 $sth->execute($target);
284 } else {
285 $sth->execute();
286 }
287
288 my $mailcount = 0;
289 my $creceiver = '';
290 my $data;
291
292 my $tt = PMG::Config::get_template_toolkit();
293
294 my $finalize = sub {
295
296 my $extern = ($domainregex && $creceiver !~ $domainregex);
297 if (!$extern) {
298 $data->{mailcount} = $mailcount;
299 my $sendto = $redirect ? $redirect : $creceiver;
300 PMG::Utils::finalize_report($tt, $template, $data, $mailfrom, $sendto, $param->{debug});
301 }
302 };
303
304 while (my $ref = $sth->fetchrow_hashref()) {
305 if ($creceiver ne $ref->{pmail}) {
306
307 $finalize->() if $data;
308
309 $data = clone($global_data);
310
311 $creceiver = $ref->{pmail};
312 $mailcount = 0;
313
314 $data->{pmail} = $creceiver;
315 $data->{managehref} = "$protocol_fqdn_port/quarantine";
316 if ($data->{authmode} ne 'ldap') {
317 $data->{ticket} = PMG::Ticket::assemble_quarantine_ticket($data->{pmail});
318 my $esc_ticket = uri_escape($data->{ticket});
319 $data->{managehref} .= "?ticket=${esc_ticket}";
320 }
321
322 }
323
324 push @{$data->{items}}, get_item_data($data, $ref);
325
326 $mailcount++;
327 }
328
329 $sth->finish();
330
331 $finalize->() if $data;
332
333 if (defined($target) && !$mailcount) {
334 print STDERR "no mails for '$target'\n";
335 }
336
337 return undef;
338 }});
339
340 sub find_stale_files {
341 my ($path, $lifetime, $purge) = @_;
342
343 return if ! -d $path;
344
345 my (undef, undef, undef, $mday, $mon, $year) = localtime(time());
346 my $daystart = timelocal(0, 0, 0, $mday, $mon, $year);
347 my $expire = $daystart - $lifetime*86400;
348
349 my $wanted = sub {
350 my $name = $File::Find::name;
351 return if $name !~ m|^($path/.*)$|;
352 $name = $1; # untaint
353 my $stat = stat($name);
354 return if ! -f _;
355 return if $stat->mtime >= $expire;
356 if ($purge) {
357 if (unlink($name)) {
358 print "removed: $name\n";
359 }
360 } else {
361 print "$name\n";
362 }
363 };
364
365 find({ wanted => $wanted, no_chdir => 1 }, $path);
366 }
367
368 sub test_quarantine_files {
369 my ($spamlifetime, $viruslifetime, $purge) = @_;
370
371 print STDERR "searching for stale files\n" if !$purge;
372
373 my $spooldir = $PMG::MailQueue::spooldir;
374
375 find_stale_files ("$spooldir/spam", $spamlifetime, $purge);
376 foreach my $dir (<"/var/spool/pmg/cluster/*/spam">) {
377 next if $dir !~ m|^(/var/spool/pmg/cluster/\d+/spam)$|;
378 $dir = $1; # untaint
379 find_stale_files ($dir, $spamlifetime, $purge);
380 }
381
382 find_stale_files ("$spooldir/virus", $viruslifetime, $purge);
383 foreach my $dir (<"/var/spool/pmg/cluster/*/virus">) {
384 next if $dir !~ m|^(/var/spool/pmg/cluster/\d+/virus)$|;
385 $dir = $1; # untaint
386 find_stale_files ($dir, $viruslifetime, $purge);
387 }
388 }
389
390 __PACKAGE__->register_method ({
391 name => 'purge',
392 path => 'purge',
393 method => 'POST',
394 description => "Cleanup Quarantine database. Remove entries older than configured quarantine lifetime.",
395 parameters => {
396 additionalProperties => 0,
397 properties => {
398 check => {
399 description => "Only search for quarantine files older than configured quarantine lifetime. Just print found files, but do not remove them.",
400 type => 'boolean',
401 optional => 1,
402 default => 0,
403 }
404 }
405 },
406 returns => { type => 'null'},
407 code => sub {
408 my ($param) = @_;
409
410 my $cfg = PMG::Config->new();
411
412 my $spamlifetime = $cfg->get('spamquar', 'lifetime');
413 my $viruslifetime = $cfg->get ('virusquar', 'lifetime');
414
415 my $purge = !$param->{check};
416
417 if ($purge) {
418 print STDERR "purging database\n";
419
420 my $dbh = PMG::DBTools::open_ruledb();
421
422 if (my $count = PMG::DBTools::purge_quarantine_database($dbh, 'S', $spamlifetime)) {
423 print STDERR "removed $count spam quarantine files\n";
424 }
425
426 if (my $count = PMG::DBTools::purge_quarantine_database($dbh, 'V', $viruslifetime)) {
427 print STDERR "removed $count virus quarantine files\n";
428 }
429
430 if (my $count = PMG::DBTools::purge_quarantine_database($dbh, 'A', $spamlifetime)) {
431 print STDERR "removed $count attachment quarantine files\n";
432 }
433 }
434
435 test_quarantine_files($spamlifetime, $viruslifetime, $purge);
436
437 return undef;
438 }});
439
440
441 our $cmddef = {
442 'purge' => [ __PACKAGE__, 'purge', []],
443 'send' => [ __PACKAGE__, 'send', []],
444 'status' => [ __PACKAGE__, 'status', []],
445 };
446
447 1;