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