]> git.proxmox.com Git - pmg-api.git/blame - PMG/API2/Quarantine.pm
PMG::DBTools::get_quarantine_count - new helper
[pmg-api.git] / PMG / API2 / Quarantine.pm
CommitLineData
b66faa68
DM
1package PMG::API2::Quarantine;
2
3use strict;
4use warnings;
5use Time::Local;
6use Time::Zone;
7use Data::Dumper;
34db0c3f 8use Encode;
b66faa68 9
34db0c3f 10use Mail::Header;
dae021a8 11
b66faa68 12use PVE::SafeSyslog;
6e8886d4 13use PVE::Exception qw(raise_param_exc raise_perm_exc);
b66faa68
DM
14use PVE::Tools qw(extract_param);
15use PVE::JSONSchema qw(get_standard_option);
16use PVE::RESTHandler;
17use PVE::INotify;
34db0c3f 18use PVE::APIServer::Formatter;
b66faa68 19
dae021a8 20use PMG::Utils;
b66faa68 21use PMG::AccessControl;
34db0c3f 22use PMG::Config;
b66faa68 23use PMG::DBTools;
34db0c3f 24use PMG::HTMLMail;
e84bf942 25use PMG::Quarantine;
b66faa68
DM
26
27use base qw(PVE::RESTHandler);
28
1284c016
DM
29my $spamdesc;
30
157a946b
DM
31my $verify_optional_pmail = sub {
32 my ($authuser, $role, $pmail) = @_;
33
34 if ($role eq 'quser') {
5182cea0 35 raise_param_exc({ pmail => "parameter not allwed with role '$role'"})
157a946b
DM
36 if defined($pmail);
37 $pmail = $authuser;
38 } else {
5182cea0 39 raise_param_exc({ pmail => "parameter required with role '$role'"})
157a946b
DM
40 if !defined($pmail);
41 }
42 return $pmail;
43};
44
1284c016
DM
45sub decode_spaminfo {
46 my ($info) = @_;
47
48 $spamdesc = PMG::Utils::load_sa_descriptions() if !$spamdesc;
49
50 my $res = [];
51
52 foreach my $test (split (',', $info)) {
53 my ($name, $score) = split (':', $test);
54
55 my $info = { name => $name, score => $score, desc => '-' };
56 if (my $si = $spamdesc->{$name}) {
57 $info->{desc} = $si->{desc};
58 $info->{url} = $si->{url} if defined($si->{url});
59 }
60 push @$res, $info;
61 }
62
63 return $res;
64}
b66faa68 65
157a946b
DM
66my $extract_email = sub {
67 my $data = shift;
68
69 return $data if !$data;
70
71 if ($data =~ m/^.*\s(\S+)\s*$/) {
72 $data = $1;
73 }
74
75 if ($data =~ m/^<([^<>\s]+)>$/) {
76 $data = $1;
77 }
78
79 if ($data !~ m/[\s><]/ && $data =~ m/^(.+\@[^\.]+\..*[^\.]+)$/) {
80 $data = $1;
81 } else {
82 $data = undef;
83 }
84
85 return $data;
86};
87
88my $get_real_sender = sub {
89 my ($ref) = @_;
90
91 my @lines = split('\n', $ref->{header});
92 my $head = Mail::Header->new(\@lines);
93
94 my @fromarray = split ('\s*,\s*', $head->get ('from') || $ref->{sender});
95 my $from = $extract_email->($fromarray[0]) || $ref->{sender};;
96 my $sender = $extract_email->($head->get ('sender'));
97
98 return $sender if $sender;
99
100 return $from;
101};
102
dae021a8
DM
103my $parse_header_info = sub {
104 my ($ref) = @_;
105
106 my $res = { subject => '', from => '' };
107
108 my @lines = split('\n', $ref->{header});
109 my $head = Mail::Header->new(\@lines);
110
111 $res->{subject} = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('subject'))) // '';
112
113 my @fromarray = split('\s*,\s*', $head->get('from') || $ref->{sender});
114
115 $res->{from} = PMG::Utils::decode_rfc1522(PVE::Tools::trim ($fromarray[0])) // '';
116
117 my $sender = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('sender')));
1284c016 118 $res->{sender} = $sender if $sender && ($sender ne $res->{from});
dae021a8
DM
119
120 $res->{envelope_sender} = $ref->{sender};
e84bf942 121 $res->{receiver} = $ref->{receiver} // $ref->{pmail};
6e8886d4 122 $res->{id} = 'C' . $ref->{cid} . 'R' . $ref->{rid};
dae021a8
DM
123 $res->{time} = $ref->{time};
124 $res->{bytes} = $ref->{bytes};
125
1284c016
DM
126 my $qtype = $ref->{qtype};
127
128 if ($qtype eq 'V') {
129 $res->{virusname} = $ref->{info};
130 } elsif ($qtype eq 'S') {
131 $res->{spamlevel} = $ref->{spamlevel} // 0;
1284c016
DM
132 }
133
dae021a8
DM
134 return $res;
135};
136
157a946b
DM
137my $pmail_param_type = {
138 description => "List entries for the user with this primary email address. Quarantine users cannot speficy this parameter, but it is required for all other roles.",
139 type => 'string', format => 'email',
140 optional => 1,
141};
6e8886d4 142
b66faa68
DM
143__PACKAGE__->register_method ({
144 name => 'index',
145 path => '',
146 method => 'GET',
147 permissions => { user => 'all' },
148 description => "Directory index.",
149 parameters => {
150 additionalProperties => 0,
151 properties => {},
152 },
153 returns => {
154 type => 'array',
155 items => {
156 type => "object",
157 properties => {},
158 },
159 links => [ { rel => 'child', href => "{name}" } ],
160 },
161 code => sub {
162 my ($param) = @_;
163
164 my $result = [
157a946b
DM
165 { name => 'whitelist' },
166 { name => 'blacklist' },
6e8886d4 167 { name => 'content' },
b66faa68 168 { name => 'spam' },
4f41cebc 169 { name => 'spamusers' },
b66faa68
DM
170 { name => 'virus' },
171 ];
172
173 return $result;
174 }});
175
157a946b 176
767657cb
DM
177my $read_or_modify_user_bw_list = sub {
178 my ($listname, $param, $addrs, $delete) = @_;
157a946b
DM
179
180 my $rpcenv = PMG::RESTEnvironment->get();
181 my $authuser = $rpcenv->get_user();
182 my $role = $rpcenv->get_role();
183
184 my $pmail = $verify_optional_pmail->($authuser, $role, $param->{pmail});
185
186 my $dbh = PMG::DBTools::open_ruledb();
187
767657cb
DM
188 my $list = PMG::Quarantine::add_to_blackwhite(
189 $dbh, $pmail, $listname, $addrs, $delete);
157a946b
DM
190
191 my $res = [];
192 foreach my $a (@$list) { push @$res, { address => $a }; }
193 return $res;
194};
195
767657cb
DM
196my $address_pattern = '[a-zA-Z0-9\+\-\_\*\.\@]+';
197
157a946b
DM
198__PACKAGE__->register_method ({
199 name => 'whitelist',
200 path => 'whitelist',
201 method => 'GET',
202 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
203 description => "Show user whitelist.",
204 parameters => {
205 additionalProperties => 0,
206 properties => {
207 pmail => $pmail_param_type,
208 },
209 },
210 returns => {
211 type => 'array',
212 items => {
213 type => "object",
214 properties => {
215 address => {
216 type => "string",
217 },
218 },
219 },
220 },
221 code => sub {
222 my ($param) = @_;
223
767657cb
DM
224 return $read_or_modify_user_bw_list->('WL', $param);
225 }});
226
227__PACKAGE__->register_method ({
228 name => 'whitelist_add',
229 path => 'whitelist',
230 method => 'POST',
231 description => "Add user whitelist entries.",
232 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
233 protected => 1,
234 parameters => {
235 additionalProperties => 0,
236 properties => {
237 pmail => $pmail_param_type,
238 address => {
239 description => "The address you want to add.",
240 type => "string",
241 pattern => $address_pattern,
242 maxLength => 512,
243 },
244 },
245 },
246 returns => { type => 'null' },
247 code => sub {
248 my ($param) = @_;
249
250 $read_or_modify_user_bw_list->('WL', $param, [ $param->{address} ]);
251
252 return undef;
253 }});
254
255__PACKAGE__->register_method ({
256 name => 'whitelist_delete',
257 path => 'whitelist/{address}',
258 method => 'DELETE',
259 description => "Delete user whitelist entries.",
260 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
261 protected => 1,
262 parameters => {
263 additionalProperties => 0,
264 properties => {
265 pmail => $pmail_param_type,
266 address => {
267 description => "The address you want to remove.",
268 type => "string",
269 pattern => $address_pattern,
270 maxLength => 512,
271 },
272 },
273 },
274 returns => { type => 'null' },
275 code => sub {
276 my ($param) = @_;
277
278 $read_or_modify_user_bw_list->('WL', $param, [ $param->{address} ], 1);
279
280 return undef;
157a946b
DM
281 }});
282
283__PACKAGE__->register_method ({
284 name => 'blacklist',
285 path => 'blacklist',
286 method => 'GET',
287 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
288 description => "Show user blacklist.",
289 parameters => {
290 additionalProperties => 0,
291 properties => {
292 pmail => $pmail_param_type,
293 },
294 },
295 returns => {
296 type => 'array',
297 items => {
298 type => "object",
299 properties => {
300 address => {
301 type => "string",
302 },
303 },
304 },
305 },
306 code => sub {
307 my ($param) = @_;
308
767657cb
DM
309 return $read_or_modify_user_bw_list->('BL', $param);
310 }});
311
312__PACKAGE__->register_method ({
313 name => 'blacklist_add',
314 path => 'blacklist',
315 method => 'POST',
316 description => "Add user blacklist entries.",
317 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
318 protected => 1,
319 parameters => {
320 additionalProperties => 0,
321 properties => {
322 pmail => $pmail_param_type,
323 address => {
324 description => "The address you want to add.",
325 type => "string",
326 pattern => $address_pattern,
327 maxLength => 512,
328 },
329 },
330 },
331 returns => { type => 'null' },
332 code => sub {
333 my ($param) = @_;
334
335 $read_or_modify_user_bw_list->('BL', $param, [ $param->{address} ]);
336
337 return undef;
338 }});
339
340__PACKAGE__->register_method ({
341 name => 'blacklist_delete',
342 path => 'blacklist/{address}',
343 method => 'DELETE',
344 description => "Delete user blacklist entries.",
345 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
346 protected => 1,
347 parameters => {
348 additionalProperties => 0,
349 properties => {
350 pmail => $pmail_param_type,
351 address => {
352 description => "The address you want to remove.",
353 type => "string",
354 pattern => $address_pattern,
355 maxLength => 512,
356 },
357 },
358 },
359 returns => { type => 'null' },
360 code => sub {
361 my ($param) = @_;
362
363 $read_or_modify_user_bw_list->('BL', $param, [ $param->{address} ], 1);
364
365 return undef;
157a946b
DM
366 }});
367
4f41cebc
DC
368__PACKAGE__->register_method ({
369 name => 'spamusers',
370 path => 'spamusers',
371 method => 'GET',
372 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
373 description => "Get a list of receivers of spam in the given timespan (Default the last 24 hours).",
374 parameters => {
375 additionalProperties => 0,
376 properties => {
377 starttime => {
378 description => "Only consider entries newer than 'starttime' (unix epoch). Default is 'now - 1day'.",
379 type => 'integer',
380 minimum => 0,
381 optional => 1,
382 },
383 endtime => {
384 description => "Only consider entries older than 'endtime' (unix epoch). This is set to '<start> + 1day' by default.",
385 type => 'integer',
386 minimum => 1,
387 optional => 1,
388 },
389 },
390 },
391 returns => {
392 type => 'array',
393 items => {
394 type => "object",
395 properties => {
396 mail => {
397 description => 'the receiving email',
398 type => 'string',
399 },
400 },
401 },
402 },
403 code => sub {
404 my ($param) = @_;
405
406 my $rpcenv = PMG::RESTEnvironment->get();
407 my $authuser = $rpcenv->get_user();
408 my $role = $rpcenv->get_role();
409
410 my $res = [];
411
412 my $dbh = PMG::DBTools::open_ruledb();
413
414 my $start = $param->{starttime} // (time - 86400);
415 my $end = $param->{endtime} // ($start + 86400);
416
417 my $sth = $dbh->prepare(
418 "SELECT DISTINCT pmail " .
419 "FROM CMailStore, CMSReceivers WHERE " .
420 "time >= $start AND time < $end AND " .
421 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
422 "AND Status = 'N' ORDER BY pmail");
423
424 $sth->execute();
425
426 while (my $ref = $sth->fetchrow_hashref()) {
427 push @$res, { mail => $ref->{pmail} };
428 }
429
430 return $res;
431 }});
432
ded33c7c 433__PACKAGE__->register_method ({
83ce499f
DC
434 name => 'spam',
435 path => 'spam',
ded33c7c
DM
436 method => 'GET',
437 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
83ce499f 438 description => "Get a list of quarantined spam mails in the given timeframe (default the last 24 hours) for the given user.",
ded33c7c
DM
439 parameters => {
440 additionalProperties => 0,
441 properties => {
442 starttime => {
83ce499f 443 description => "Only consider entries newer than 'starttime' (unix epoch). This is set to 'now - 1day' by default.",
ded33c7c
DM
444 type => 'integer',
445 minimum => 0,
83ce499f 446 optional => 1,
ded33c7c
DM
447 },
448 endtime => {
449 description => "Only consider entries older than 'endtime' (unix epoch). This is set to '<start> + 1day' by default.",
450 type => 'integer',
451 minimum => 1,
452 optional => 1,
453 },
157a946b 454 pmail => $pmail_param_type,
ded33c7c
DM
455 },
456 },
457 returns => {
458 type => 'array',
459 items => {
460 type => "object",
dae021a8
DM
461 properties => {
462 id => {
463 description => 'Unique ID',
464 type => 'string',
465 },
466 bytes => {
467 description => "Size of raw email.",
468 type => 'integer' ,
469 },
470 envelope_sender => {
471 description => "SMTP envelope sender.",
472 type => 'string',
473 },
474 from => {
475 description => "Header 'From' field.",
476 type => 'string',
477 },
478 sender => {
479 description => "Header 'Sender' field.",
480 type => 'string',
481 optional => 1,
482 },
483 receiver => {
484 description => "Receiver email address",
485 type => 'string',
486 },
487 subject => {
488 description => "Header 'Subject' field.",
489 type => 'string',
490 },
491 time => {
492 description => "Receive time stamp",
493 type => 'integer',
494 },
1284c016
DM
495 spamlevel => {
496 description => "Spam score.",
497 type => 'number',
498 },
dae021a8 499 },
ded33c7c
DM
500 },
501 },
502 code => sub {
503 my ($param) = @_;
504
505 my $rpcenv = PMG::RESTEnvironment->get();
506 my $authuser = $rpcenv->get_user();
507 my $role = $rpcenv->get_role();
508
157a946b 509 my $pmail = $verify_optional_pmail->($authuser, $role, $param->{pmail});
b66faa68 510
ded33c7c
DM
511 my $res = [];
512
513 my $dbh = PMG::DBTools::open_ruledb();
514
83ce499f 515 my $start = $param->{starttime} // (time - 86400);
ded33c7c
DM
516 my $end = $param->{endtime} // ($start + 86400);
517
518 my $sth = $dbh->prepare(
519 "SELECT * " .
520 "FROM CMailStore, CMSReceivers WHERE " .
521 "pmail = ? AND time >= $start AND time < $end AND " .
522 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
523 "AND Status = 'N' ORDER BY pmail, time, receiver");
524
525 $sth->execute($pmail);
526
b66faa68 527 while (my $ref = $sth->fetchrow_hashref()) {
dae021a8
DM
528 my $data = $parse_header_info->($ref);
529 push @$res, $data;
b66faa68
DM
530 }
531
532 return $res;
533 }});
534
6e8886d4
DM
535__PACKAGE__->register_method ({
536 name => 'content',
537 path => 'content',
538 method => 'GET',
539 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
34db0c3f 540 description => "Get email data. There is a special formatter called 'htmlmail' to get sanitized html view of the mail content (use the '/api2/htmlmail/quarantine/content' url).",
6e8886d4
DM
541 parameters => {
542 additionalProperties => 0,
543 properties => {
544 id => {
545 description => 'Unique ID',
546 type => 'string',
547 pattern => 'C\d+R\d+',
548 maxLength => 40,
549 },
34db0c3f
DM
550 raw => {
551 description => "Display 'raw' eml data. This is only used with the 'htmlmail' formatter.",
552 type => 'boolean',
553 optional => 1,
554 default => 0,
555 },
6e8886d4
DM
556 },
557 },
558 returns => {
559 type => "object",
cd31bb45
DM
560 properties => {
561 id => {
562 description => 'Unique ID',
563 type => 'string',
564 },
565 bytes => {
566 description => "Size of raw email.",
567 type => 'integer' ,
568 },
569 envelope_sender => {
570 description => "SMTP envelope sender.",
571 type => 'string',
572 },
573 from => {
574 description => "Header 'From' field.",
575 type => 'string',
576 },
577 sender => {
578 description => "Header 'Sender' field.",
579 type => 'string',
580 optional => 1,
581 },
582 receiver => {
583 description => "Receiver email address",
584 type => 'string',
585 },
586 subject => {
587 description => "Header 'Subject' field.",
588 type => 'string',
589 },
590 time => {
591 description => "Receive time stamp",
592 type => 'integer',
593 },
594 spamlevel => {
595 description => "Spam score.",
596 type => 'number',
597 },
598 spaminfo => {
599 description => "Information about matched spam tests (name, score, desc, url).",
600 type => 'array',
601 },
602 header => {
603 description => "Raw email header data.",
604 type => 'string',
605 },
606 content => {
607 description => "Raw email data (first 4096 bytes). Useful for preview. NOTE: The 'htmlmail' formatter displays the whole email.",
608 type => 'string',
6eac8473 609 },
cd31bb45 610 },
6e8886d4
DM
611 },
612 code => sub {
613 my ($param) = @_;
614
615 my $rpcenv = PMG::RESTEnvironment->get();
616 my $authuser = $rpcenv->get_user();
617 my $role = $rpcenv->get_role();
34db0c3f 618 my $format = $rpcenv->get_format();
6e8886d4
DM
619
620 my ($cid, $rid) = $param->{id} =~ m/^C(\d+)R(\d+)$/;
621 $cid = int($cid);
622 $rid = int($rid);
623
624 my $dbh = PMG::DBTools::open_ruledb();
625
626 my $ref = PMG::DBTools::load_mail_data($dbh, $cid, $rid);
627
628 if ($role eq 'quser') {
629 raise_perm_exc("mail does not belong to user '$authuser'")
630 if $authuser ne $ref->{pmail};
631 }
632
633 my $res = $parse_header_info->($ref);
634
6e8886d4
DM
635
636 my $filename = $ref->{file};
637 my $spooldir = $PMG::MailQueue::spooldir;
638
639 my $path = "$spooldir/$filename";
640
34db0c3f
DM
641 if ($format eq 'htmlmail') {
642
643 my $cfg = PMG::Config->new();
644 my $viewimages = $cfg->get('spamquar', 'viewimages');
645 my $allowhref = $cfg->get('spamquar', 'allowhrefs');
646
34db0c3f
DM
647 $res->{content} = PMG::HTMLMail::email_to_html($path, $param->{raw}, $viewimages, $allowhref);
648
157a946b
DM
649 # to make result verification happy
650 $res->{file} = '';
651 $res->{header} = '';
652 $res->{spaminfo} = [];
34db0c3f 653 } else {
cd31bb45 654 # include additional details
34db0c3f 655
cd31bb45 656 my ($header, $content) = PMG::HTMLMail::read_raw_email($path, 4096);
157a946b 657
cd31bb45
DM
658 $res->{file} = $ref->{file};
659 $res->{spaminfo} = decode_spaminfo($ref->{info});
34db0c3f
DM
660 $res->{header} = $header;
661 $res->{content} = $content;
662 }
6e8886d4 663
6e8886d4
DM
664
665 return $res;
666
667 }});
668
34db0c3f
DM
669PVE::APIServer::Formatter::register_page_formatter(
670 'format' => 'htmlmail',
671 method => 'GET',
672 path => '/quarantine/content',
673 code => sub {
674 my ($res, $data, $param, $path, $auth, $config) = @_;
675
676 if(!HTTP::Status::is_success($res->{status})) {
677 return ("Error $res->{status}: $res->{message}", "text/plain");
678 }
679
680 my $ct = "text/html;charset=UTF-8";
681
682 my $raw = $data->{content};
683
684 return (encode('UTF-8', $raw), $ct, 1);
685});
686
157a946b
DM
687__PACKAGE__->register_method ({
688 name =>'action',
689 path => 'content',
690 method => 'POST',
691 description => "Execute quarantine actions.",
692 permissions => { check => [ 'admin', 'qmanager', 'quser'] },
693 protected => 1,
694 parameters => {
695 additionalProperties => 0,
696 properties => {
697 id => {
698 description => 'Unique ID',
699 type => 'string',
700 pattern => 'C\d+R\d+',
701 maxLength => 40,
702 },
703 action => {
704 description => 'Action - specify what you want to do with the mail.',
705 type => 'string',
706 enum => ['whitelist', 'blacklist', 'deliver', 'delete'],
707 },
708 },
709 },
710 returns => { type => "null" },
711 code => sub {
712 my ($param) = @_;
713
714 my $rpcenv = PMG::RESTEnvironment->get();
715 my $authuser = $rpcenv->get_user();
716 my $role = $rpcenv->get_role();
717 my $action = $param->{action};
718
719 my ($cid, $rid) = $param->{id} =~ m/^C(\d+)R(\d+)$/;
720 $cid = int($cid);
721 $rid = int($rid);
722
723 my $dbh = PMG::DBTools::open_ruledb();
724
725 my $ref = PMG::DBTools::load_mail_data($dbh, $cid, $rid);
726
727 if ($role eq 'quser') {
728 raise_perm_exc("mail does not belong to user '$authuser'")
729 if $authuser ne $ref->{pmail};
730 }
731
732 my $sender = $get_real_sender->($ref);
733 my $username = $ref->{pmail};
734
735 if ($action eq 'whitelist') {
e84bf942 736 PMG::Quarantine::add_to_blackwhite($dbh, $username, 'WL', [ $sender ]);
157a946b 737 } elsif ($action eq 'blacklist') {
e84bf942 738 PMG::Quarantine::add_to_blackwhite($dbh, $username, 'BL', [ $sender ]);
157a946b 739 } elsif ($action eq 'deliver') {
e84bf942
DM
740 my $targets = [ $ref->{pmail} ];
741 PMG::Quarantine::deliver_quarantined_mail($dbh, $ref, $targets);
157a946b 742 } elsif ($action eq 'delete') {
e84bf942
DM
743 PMG::Quarantine::delete_quarantined_mail($dbh, $ref);
744 } else {
745 die "internal error"; # should not be reached
157a946b
DM
746 }
747
748 return undef;
749 }});
e84bf942 750
b66faa68 7511;