]> git.proxmox.com Git - pmg-api.git/blob - src/PMG/API2/Quarantine.pm
api/quarantine: add deprecation fixme for old delete b/w-list entries endpoints
[pmg-api.git] / src / PMG / API2 / Quarantine.pm
1 package PMG::API2::Quarantine;
2
3 use strict;
4 use warnings;
5 use Time::Local;
6 use Time::Zone;
7 use Data::Dumper;
8 use Encode;
9 use File::Path;
10 use IO::File;
11
12 use Mail::Header;
13 use Mail::SpamAssassin;
14
15 use PVE::SafeSyslog;
16 use PVE::Exception qw(raise_param_exc raise_perm_exc);
17 use PVE::Tools qw(extract_param);
18 use PVE::JSONSchema qw(get_standard_option);
19 use PVE::RESTHandler;
20 use PVE::INotify;
21 use PVE::APIServer::Formatter;
22
23 use PMG::Utils;
24 use PMG::AccessControl;
25 use PMG::Config;
26 use PMG::DBTools;
27 use PMG::HTMLMail;
28 use PMG::Quarantine;
29 use PMG::MailQueue;
30 use PMG::MIMEUtils;
31
32 use base qw(PVE::RESTHandler);
33
34 my $spamdesc;
35
36 my $extract_pmail = sub {
37 my ($authuser, $role) = @_;
38
39 if ($authuser =~ m/^(.+)\@quarantine$/) {
40 return $1;
41 }
42 raise_param_exc({ pmail => "got unexpected authuser '$authuser' with role '$role'"});
43 };
44
45 my $verify_optional_pmail = sub {
46 my ($authuser, $role, $pmail_param) = @_;
47
48 my $pmail;
49 if ($role eq 'quser') {
50 $pmail = $extract_pmail->($authuser, $role);
51 raise_param_exc({ pmail => "parameter not allwed with role '$role'"})
52 if defined($pmail_param) && ($pmail ne $pmail_param);
53 } else {
54 raise_param_exc({ pmail => "parameter required with role '$role'"})
55 if !defined($pmail_param);
56 $pmail = $pmail_param;
57 }
58 return $pmail;
59 };
60
61 sub decode_spaminfo {
62 my ($info) = @_;
63
64 my $res = [];
65 return $res if !defined($info);
66
67 my $saversion = Mail::SpamAssassin->VERSION;
68
69 my $salocaldir = "/var/lib/spamassassin/$saversion/updates_spamassassin_org";
70
71 $spamdesc = PMG::Utils::load_sa_descriptions([$salocaldir]) if !$spamdesc;
72
73 foreach my $test (split (',', $info)) {
74 my ($name, $score) = split (':', $test);
75
76 my $info = { name => $name, score => $score + 0, desc => '-' };
77 if (my $si = $spamdesc->{$name}) {
78 $info->{desc} = $si->{desc};
79 $info->{url} = $si->{url} if defined($si->{url});
80 }
81 push @$res, $info;
82 }
83
84 return $res;
85 }
86
87 my $extract_email = sub {
88 my $data = shift;
89
90 return $data if !$data;
91
92 if ($data =~ m/^.*\s(\S+)\s*$/) {
93 $data = $1;
94 }
95
96 if ($data =~ m/^<([^<>\s]+)>$/) {
97 $data = $1;
98 }
99
100 if ($data !~ m/[\s><]/ && $data =~ m/^(.+\@[^\.]+\..*[^\.]+)$/) {
101 $data = $1;
102 } else {
103 $data = undef;
104 }
105
106 return $data;
107 };
108
109 my $get_real_sender = sub {
110 my ($ref) = @_;
111
112 my @lines = split('\n', $ref->{header});
113 my $head = Mail::Header->new(\@lines);
114
115 my @fromarray = split ('\s*,\s*', $head->get ('from') || $ref->{sender});
116 my $from = $extract_email->($fromarray[0]) || $ref->{sender};;
117 my $sender = $extract_email->($head->get ('sender'));
118
119 return $sender if $sender;
120
121 return $from;
122 };
123
124 my $parse_header_info = sub {
125 my ($ref) = @_;
126
127 my $res = { subject => '', from => '' };
128
129 my @lines = split('\n', $ref->{header});
130 my $head = Mail::Header->new(\@lines);
131
132 $res->{subject} = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('subject'))) // '';
133
134 my @fromarray = split('\s*,\s*', $head->get('from') || $ref->{sender});
135
136 $res->{from} = PMG::Utils::decode_rfc1522(PVE::Tools::trim ($fromarray[0])) // '';
137
138 my $sender = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('sender')));
139 $res->{sender} = $sender if $sender && ($sender ne $res->{from});
140
141 $res->{envelope_sender} = $ref->{sender};
142 $res->{receiver} = $ref->{receiver} // $ref->{pmail};
143 $res->{id} = 'C' . $ref->{cid} . 'R' . $ref->{rid} . 'T' . $ref->{ticketid};
144 $res->{time} = $ref->{time};
145 $res->{bytes} = $ref->{bytes};
146
147 my $qtype = $ref->{qtype};
148
149 if ($qtype eq 'V') {
150 $res->{virusname} = $ref->{info};
151 $res->{spamlevel} = 0;
152 } elsif ($qtype eq 'S') {
153 $res->{spamlevel} = $ref->{spamlevel} // 0;
154 }
155
156 return $res;
157 };
158
159 my $pmail_param_type = get_standard_option('pmg-email-address', {
160 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.",
161 optional => 1,
162 });
163
164 __PACKAGE__->register_method ({
165 name => 'index',
166 path => '',
167 method => 'GET',
168 permissions => { user => 'all' },
169 description => "Directory index.",
170 parameters => {
171 additionalProperties => 0,
172 properties => {},
173 },
174 returns => {
175 type => 'array',
176 items => {
177 type => "object",
178 properties => {},
179 },
180 links => [ { rel => 'child', href => "{name}" } ],
181 },
182 code => sub {
183 my ($param) = @_;
184
185 my $result = [
186 { name => 'whitelist' },
187 { name => 'blacklist' },
188 { name => 'content' },
189 { name => 'spam' },
190 { name => 'spamusers' },
191 { name => 'spamstatus' },
192 { name => 'virus' },
193 { name => 'virusstatus' },
194 { name => 'quarusers' },
195 { name => 'attachment' },
196 { name => 'listattachments' },
197 { name => 'download' },
198 ];
199
200 return $result;
201 }});
202
203
204 my $read_or_modify_user_bw_list = sub {
205 my ($listname, $param, $addrs, $delete) = @_;
206
207 my $rpcenv = PMG::RESTEnvironment->get();
208 my $authuser = $rpcenv->get_user();
209 my $role = $rpcenv->get_role();
210
211 my $pmail = $verify_optional_pmail->($authuser, $role, $param->{pmail});
212
213 my $dbh = PMG::DBTools::open_ruledb();
214
215 my $list = PMG::Quarantine::add_to_blackwhite(
216 $dbh, $pmail, $listname, $addrs, $delete);
217
218 my $res = [];
219 foreach my $a (@$list) { push @$res, { address => $a }; }
220 return $res;
221 };
222
223 __PACKAGE__->register_method ({
224 name => 'whitelist',
225 path => 'whitelist',
226 method => 'GET',
227 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
228 description => "Show user whitelist.",
229 parameters => {
230 additionalProperties => 0,
231 properties => {
232 pmail => $pmail_param_type,
233 },
234 },
235 returns => {
236 type => 'array',
237 items => {
238 type => "object",
239 properties => {
240 address => {
241 type => "string",
242 },
243 },
244 },
245 },
246 code => sub {
247 my ($param) = @_;
248
249 return $read_or_modify_user_bw_list->('WL', $param);
250 }});
251
252 __PACKAGE__->register_method ({
253 name => 'whitelist_add',
254 path => 'whitelist',
255 method => 'POST',
256 description => "Add user whitelist entries.",
257 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
258 protected => 1,
259 parameters => {
260 additionalProperties => 0,
261 properties => {
262 pmail => $pmail_param_type,
263 address => get_standard_option('pmg-whiteblacklist-entry-list', {
264 description => "The address you want to add.",
265 }),
266 },
267 },
268 returns => { type => 'null' },
269 code => sub {
270 my ($param) = @_;
271
272 my $addresses = [split(',', $param->{address})];
273 $read_or_modify_user_bw_list->('WL', $param, $addresses);
274
275 return undef;
276 }});
277
278 __PACKAGE__->register_method ({
279 name => 'whitelist_delete_base',
280 path => 'whitelist',
281 method => 'DELETE',
282 description => "Delete user whitelist entries.",
283 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
284 protected => 1,
285 parameters => {
286 additionalProperties => 0,
287 properties => {
288 pmail => $pmail_param_type,
289 address => get_standard_option('pmg-whiteblacklist-entry-list', {
290 pattern => '',
291 description => "The address, or comma-separated list of addresses, you want to remove.",
292 }),
293 },
294 },
295 returns => { type => 'null' },
296 code => sub {
297 my ($param) = @_;
298
299 my $addresses = [split(',', $param->{address})];
300 $read_or_modify_user_bw_list->('WL', $param, $addresses, 1);
301
302 return undef;
303 }});
304
305 # FIXME: remove for PMG 7.0 - addresses can contain stuff like '/' which breaks
306 # API path resolution, thus we replaced it by above "un-templated" call
307 __PACKAGE__->register_method ({
308 name => 'whitelist_delete',
309 path => 'whitelist/{address}',
310 method => 'DELETE',
311 description => "Delete user whitelist entries.",
312 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
313 protected => 1,
314 parameters => {
315 additionalProperties => 0,
316 properties => {
317 pmail => $pmail_param_type,
318 address => get_standard_option('pmg-whiteblacklist-entry-list', {
319 description => "The address you want to remove.",
320 }),
321 },
322 },
323 returns => { type => 'null' },
324 code => sub {
325 my ($param) = @_;
326
327 my $addresses = [split(',', $param->{address})];
328 $read_or_modify_user_bw_list->('WL', $param, $addresses, 1);
329
330 return undef;
331 }});
332
333 __PACKAGE__->register_method ({
334 name => 'blacklist',
335 path => 'blacklist',
336 method => 'GET',
337 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
338 description => "Show user blacklist.",
339 parameters => {
340 additionalProperties => 0,
341 properties => {
342 pmail => $pmail_param_type,
343 },
344 },
345 returns => {
346 type => 'array',
347 items => {
348 type => "object",
349 properties => {
350 address => {
351 type => "string",
352 },
353 },
354 },
355 },
356 code => sub {
357 my ($param) = @_;
358
359 return $read_or_modify_user_bw_list->('BL', $param);
360 }});
361
362 __PACKAGE__->register_method ({
363 name => 'blacklist_add',
364 path => 'blacklist',
365 method => 'POST',
366 description => "Add user blacklist entries.",
367 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
368 protected => 1,
369 parameters => {
370 additionalProperties => 0,
371 properties => {
372 pmail => $pmail_param_type,
373 address => get_standard_option('pmg-whiteblacklist-entry-list', {
374 description => "The address you want to add.",
375 }),
376 },
377 },
378 returns => { type => 'null' },
379 code => sub {
380 my ($param) = @_;
381
382 my $addresses = [split(',', $param->{address})];
383 $read_or_modify_user_bw_list->('BL', $param, $addresses);
384
385 return undef;
386 }});
387
388 __PACKAGE__->register_method ({
389 name => 'blacklist_delete_base',
390 path => 'blacklist',
391 method => 'DELETE',
392 description => "Delete user blacklist entries.",
393 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
394 protected => 1,
395 parameters => {
396 additionalProperties => 0,
397 properties => {
398 pmail => $pmail_param_type,
399 address => get_standard_option('pmg-whiteblacklist-entry-list', {
400 pattern => '',
401 description => "The address, or comma-separated list of addresses, you want to remove.",
402 }),
403 },
404 },
405 returns => { type => 'null' },
406 code => sub {
407 my ($param) = @_;
408
409 my $addresses = [split(',', $param->{address})];
410 $read_or_modify_user_bw_list->('BL', $param, $addresses, 1);
411
412 return undef;
413 }});
414
415 # FIXME: remove for PMG 7.0 - addresses can contain stuff like '/' which breaks
416 # API path resolution, thus we replaced it by above "un-templated" call
417 __PACKAGE__->register_method ({
418 name => 'blacklist_delete',
419 path => 'blacklist/{address}',
420 method => 'DELETE',
421 description => "Delete user blacklist entries.",
422 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
423 protected => 1,
424 parameters => {
425 additionalProperties => 0,
426 properties => {
427 pmail => $pmail_param_type,
428 address => get_standard_option('pmg-whiteblacklist-entry-list', {
429 description => "The address you want to remove.",
430 }),
431 },
432 },
433 returns => { type => 'null' },
434 code => sub {
435 my ($param) = @_;
436
437 my $addresses = [split(',', $param->{address})];
438 $read_or_modify_user_bw_list->('BL', $param, $addresses, 1);
439
440 return undef;
441 }});
442
443 __PACKAGE__->register_method ({
444 name => 'spamusers',
445 path => 'spamusers',
446 method => 'GET',
447 permissions => { check => [ 'admin', 'qmanager', 'audit'] },
448 description => "Get a list of receivers of spam in the given timespan (Default the last 24 hours).",
449 parameters => {
450 additionalProperties => 0,
451 properties => {
452 starttime => get_standard_option('pmg-starttime'),
453 endtime => get_standard_option('pmg-endtime'),
454 },
455 },
456 returns => {
457 type => 'array',
458 items => {
459 type => "object",
460 properties => {
461 mail => {
462 description => 'the receiving email',
463 type => 'string',
464 },
465 },
466 },
467 },
468 code => sub {
469 my ($param) = @_;
470
471 my $rpcenv = PMG::RESTEnvironment->get();
472 my $authuser = $rpcenv->get_user();
473
474 my $res = [];
475
476 my $dbh = PMG::DBTools::open_ruledb();
477
478 my $start = $param->{starttime} // (time - 86400);
479 my $end = $param->{endtime} // ($start + 86400);
480
481 my $sth = $dbh->prepare(
482 "SELECT DISTINCT pmail " .
483 "FROM CMailStore, CMSReceivers WHERE " .
484 "time >= $start AND time < $end AND " .
485 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
486 "AND Status = 'N' ORDER BY pmail");
487
488 $sth->execute();
489
490 while (my $ref = $sth->fetchrow_hashref()) {
491 push @$res, { mail => $ref->{pmail} };
492 }
493
494 return $res;
495 }});
496
497 __PACKAGE__->register_method ({
498 name => 'spamstatus',
499 path => 'spamstatus',
500 method => 'GET',
501 permissions => { check => [ 'admin', 'qmanager', 'audit'] },
502 description => "Get Spam Quarantine Status",
503 parameters => {
504 additionalProperties => 0,
505 properties => {},
506 },
507 returns => {
508 type => "object",
509 properties => {
510 count => {
511 description => 'Number of stored mails.',
512 type => 'integer',
513 },
514 mbytes => {
515 description => "Estimated disk space usage in MByte.",
516 type => 'number',
517 },
518 avgbytes => {
519 description => "Average size of stored mails in bytes.",
520 type => 'number',
521 },
522 avgspam => {
523 description => "Average spam level.",
524 type => 'number',
525 },
526 },
527 },
528 code => sub {
529 my ($param) = @_;
530
531 my $dbh = PMG::DBTools::open_ruledb();
532 my $ref = PMG::DBTools::get_quarantine_count($dbh, 'S');
533
534 return $ref;
535 }});
536
537 __PACKAGE__->register_method ({
538 name => 'quarusers',
539 path => 'quarusers',
540 method => 'GET',
541 permissions => { check => [ 'admin', 'qmanager', 'audit'] },
542 description => "Get a list of users with whitelist/blacklist setttings.",
543 parameters => {
544 additionalProperties => 0,
545 properties => {
546 list => {
547 type => 'string',
548 description => 'If set, limits the result to the given list.',
549 enum => ['BL', 'WL'],
550 optional => 1,
551 },
552 },
553 },
554 returns => {
555 type => 'array',
556 items => {
557 type => "object",
558 properties => {
559 mail => {
560 description => 'the receiving email',
561 type => 'string',
562 },
563 },
564 },
565 },
566 code => sub {
567 my ($param) = @_;
568
569 my $rpcenv = PMG::RESTEnvironment->get();
570 my $authuser = $rpcenv->get_user();
571
572 my $res = [];
573
574 my $dbh = PMG::DBTools::open_ruledb();
575
576 my $sth;
577 if ($param->{list}) {
578 $sth = $dbh->prepare("SELECT DISTINCT pmail FROM UserPrefs WHERE name = ? ORDER BY pmail");
579 $sth->execute($param->{list});
580 } else {
581 $sth = $dbh->prepare("SELECT DISTINCT pmail FROM UserPrefs ORDER BY pmail");
582 $sth->execute();
583 }
584
585 while (my $ref = $sth->fetchrow_hashref()) {
586 push @$res, { mail => $ref->{pmail} };
587 }
588
589 return $res;
590 }});
591
592 my $quarantine_api = sub {
593 my ($param, $quartype, $check_pmail) = @_;
594
595 my $rpcenv = PMG::RESTEnvironment->get();
596 my $authuser = $rpcenv->get_user();
597
598 my $start = $param->{starttime} // (time - 86400);
599 my $end = $param->{endtime} // ($start + 86400);
600
601 my $select;
602 my $pmail;
603 if ($check_pmail) {
604 my $role = $rpcenv->get_role();
605 $pmail = $verify_optional_pmail->($authuser, $role, $param->{pmail});
606 $select = "SELECT * " .
607 "FROM CMailStore, CMSReceivers WHERE " .
608 "pmail = ? AND time >= $start AND time < $end AND " .
609 "QType = '$quartype' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
610 "AND Status = 'N' ORDER BY pmail, time, receiver";
611 } else {
612 $select = "SELECT * " .
613 "FROM CMailStore, CMSReceivers WHERE " .
614 "time >= $start AND time < $end AND " .
615 "QType = '$quartype' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
616 "AND Status = 'N' ORDER BY time, receiver";
617 }
618
619 my $res = [];
620
621 my $dbh = PMG::DBTools::open_ruledb();
622
623 my $sth = $dbh->prepare($select);
624
625 if ($check_pmail) {
626 $sth->execute($pmail);
627 } else {
628 $sth->execute();
629 }
630
631 while (my $ref = $sth->fetchrow_hashref()) {
632 my $data = $parse_header_info->($ref);
633 push @$res, $data;
634 }
635
636 return $res;
637 };
638
639 __PACKAGE__->register_method ({
640 name => 'spam',
641 path => 'spam',
642 method => 'GET',
643 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
644 description => "Get a list of quarantined spam mails in the given timeframe (default the last 24 hours) for the given user.",
645 parameters => {
646 additionalProperties => 0,
647 properties => {
648 starttime => get_standard_option('pmg-starttime'),
649 endtime => get_standard_option('pmg-endtime'),
650 pmail => $pmail_param_type,
651 },
652 },
653 returns => {
654 type => 'array',
655 items => {
656 type => "object",
657 properties => {
658 id => {
659 description => 'Unique ID',
660 type => 'string',
661 },
662 bytes => {
663 description => "Size of raw email.",
664 type => 'integer' ,
665 },
666 envelope_sender => {
667 description => "SMTP envelope sender.",
668 type => 'string',
669 },
670 from => {
671 description => "Header 'From' field.",
672 type => 'string',
673 },
674 sender => {
675 description => "Header 'Sender' field.",
676 type => 'string',
677 optional => 1,
678 },
679 receiver => {
680 description => "Receiver email address",
681 type => 'string',
682 },
683 subject => {
684 description => "Header 'Subject' field.",
685 type => 'string',
686 },
687 time => {
688 description => "Receive time stamp",
689 type => 'integer',
690 },
691 spamlevel => {
692 description => "Spam score.",
693 type => 'number',
694 },
695 },
696 },
697 },
698 code => sub {
699 my ($param) = @_;
700 return $quarantine_api->($param, 'S', 1);
701 }});
702
703 __PACKAGE__->register_method ({
704 name => 'virus',
705 path => 'virus',
706 method => 'GET',
707 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
708 description => "Get a list of quarantined virus mails in the given timeframe (default the last 24 hours).",
709 parameters => {
710 additionalProperties => 0,
711 properties => {
712 starttime => get_standard_option('pmg-starttime'),
713 endtime => get_standard_option('pmg-endtime'),
714 },
715 },
716 returns => {
717 type => 'array',
718 items => {
719 type => "object",
720 properties => {
721 id => {
722 description => 'Unique ID',
723 type => 'string',
724 },
725 bytes => {
726 description => "Size of raw email.",
727 type => 'integer' ,
728 },
729 envelope_sender => {
730 description => "SMTP envelope sender.",
731 type => 'string',
732 },
733 from => {
734 description => "Header 'From' field.",
735 type => 'string',
736 },
737 sender => {
738 description => "Header 'Sender' field.",
739 type => 'string',
740 optional => 1,
741 },
742 receiver => {
743 description => "Receiver email address",
744 type => 'string',
745 },
746 subject => {
747 description => "Header 'Subject' field.",
748 type => 'string',
749 },
750 time => {
751 description => "Receive time stamp",
752 type => 'integer',
753 },
754 virusname => {
755 description => "Virus name.",
756 type => 'string',
757 },
758 },
759 },
760 },
761 code => sub {
762 my ($param) = @_;
763 return $quarantine_api->($param, 'V');
764 }});
765
766 __PACKAGE__->register_method ({
767 name => 'attachment',
768 path => 'attachment',
769 method => 'GET',
770 permissions => { check => [ 'admin', 'qmanager', 'audit' ] },
771 description => "Get a list of quarantined attachment mails in the given timeframe (default the last 24 hours).",
772 parameters => {
773 additionalProperties => 0,
774 properties => {
775 starttime => get_standard_option('pmg-starttime'),
776 endtime => get_standard_option('pmg-endtime'),
777 },
778 },
779 returns => {
780 type => 'array',
781 items => {
782 type => "object",
783 properties => {
784 id => {
785 description => 'Unique ID',
786 type => 'string',
787 },
788 bytes => {
789 description => "Size of raw email.",
790 type => 'integer' ,
791 },
792 envelope_sender => {
793 description => "SMTP envelope sender.",
794 type => 'string',
795 },
796 from => {
797 description => "Header 'From' field.",
798 type => 'string',
799 },
800 sender => {
801 description => "Header 'Sender' field.",
802 type => 'string',
803 optional => 1,
804 },
805 receiver => {
806 description => "Receiver email address",
807 type => 'string',
808 },
809 subject => {
810 description => "Header 'Subject' field.",
811 type => 'string',
812 },
813 time => {
814 description => "Receive time stamp",
815 type => 'integer',
816 },
817 },
818 },
819 },
820 code => sub {
821 my ($param) = @_;
822 return $quarantine_api->($param, 'A');
823 }});
824
825 __PACKAGE__->register_method ({
826 name => 'virusstatus',
827 path => 'virusstatus',
828 method => 'GET',
829 permissions => { check => [ 'admin', 'qmanager', 'audit'] },
830 description => "Get Virus Quarantine Status",
831 parameters => {
832 additionalProperties => 0,
833 properties => {},
834 },
835 returns => {
836 type => "object",
837 properties => {
838 count => {
839 description => 'Number of stored mails.',
840 type => 'integer',
841 },
842 mbytes => {
843 description => "Estimated disk space usage in MByte.",
844 type => 'number',
845 },
846 avgbytes => {
847 description => "Average size of stored mails in bytes.",
848 type => 'number',
849 },
850 },
851 },
852 code => sub {
853 my ($param) = @_;
854
855 my $dbh = PMG::DBTools::open_ruledb();
856 my $ref = PMG::DBTools::get_quarantine_count($dbh, 'V');
857
858 delete $ref->{avgspam};
859
860 return $ref;
861 }});
862
863 my $get_and_check_mail = sub {
864 my ($id, $rpcenv, $dbh) = @_;
865
866 my ($cid, $rid, $tid) = $id =~ m/^C(\d+)R(\d+)T(\d+)$/;
867 $cid = int($cid);
868 $rid = int($rid);
869 $tid = int($tid);
870
871 if (!$dbh) {
872 $dbh = PMG::DBTools::open_ruledb();
873 }
874
875 my $ref = PMG::DBTools::load_mail_data($dbh, $cid, $rid, $tid);
876
877 my $authuser = $rpcenv->get_user();
878 my $role = $rpcenv->get_role();
879
880 if ($role eq 'quser') {
881 my $quar_username = $ref->{pmail} . '@quarantine';
882 raise_perm_exc("mail does not belong to user '$authuser' ($ref->{pmail})")
883 if $authuser ne $quar_username;
884 }
885
886 return $ref;
887 };
888
889 __PACKAGE__->register_method ({
890 name => 'content',
891 path => 'content',
892 method => 'GET',
893 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
894 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).",
895 parameters => {
896 additionalProperties => 0,
897 properties => {
898 id => {
899 description => 'Unique ID',
900 type => 'string',
901 pattern => 'C\d+R\d+T\d+',
902 maxLength => 60,
903 },
904 raw => {
905 description => "Display 'raw' eml data. Deactivates size limit.",
906 type => 'boolean',
907 optional => 1,
908 default => 0,
909 },
910 },
911 },
912 returns => {
913 type => "object",
914 properties => {
915 id => {
916 description => 'Unique ID',
917 type => 'string',
918 },
919 bytes => {
920 description => "Size of raw email.",
921 type => 'integer' ,
922 },
923 envelope_sender => {
924 description => "SMTP envelope sender.",
925 type => 'string',
926 },
927 from => {
928 description => "Header 'From' field.",
929 type => 'string',
930 },
931 sender => {
932 description => "Header 'Sender' field.",
933 type => 'string',
934 optional => 1,
935 },
936 receiver => {
937 description => "Receiver email address",
938 type => 'string',
939 },
940 subject => {
941 description => "Header 'Subject' field.",
942 type => 'string',
943 },
944 time => {
945 description => "Receive time stamp",
946 type => 'integer',
947 },
948 spamlevel => {
949 description => "Spam score.",
950 type => 'number',
951 },
952 spaminfo => {
953 description => "Information about matched spam tests (name, score, desc, url).",
954 type => 'array',
955 },
956 header => {
957 description => "Raw email header data.",
958 type => 'string',
959 },
960 content => {
961 description => "Raw email data (first 4096 bytes). Useful for preview. NOTE: The 'htmlmail' formatter displays the whole email.",
962 type => 'string',
963 },
964 },
965 },
966 code => sub {
967 my ($param) = @_;
968
969 my $rpcenv = PMG::RESTEnvironment->get();
970 my $format = $rpcenv->get_format();
971
972 my $raw = $param->{raw} // 0;
973
974 my $ref = $get_and_check_mail->($param->{id}, $rpcenv);
975
976 my $res = $parse_header_info->($ref);
977
978 my $filename = $ref->{file};
979 my $spooldir = $PMG::MailQueue::spooldir;
980
981 my $path = "$spooldir/$filename";
982
983 if ($format eq 'htmlmail') {
984
985 my $cfg = PMG::Config->new();
986 my $viewimages = $cfg->get('spamquar', 'viewimages');
987 my $allowhref = $cfg->get('spamquar', 'allowhrefs');
988
989 $res->{content} = PMG::HTMLMail::email_to_html($path, $raw, $viewimages, $allowhref) // 'unable to parse mail';
990
991 # to make result verification happy
992 $res->{file} = '';
993 $res->{header} = '';
994 $res->{spamlevel} = 0;
995 $res->{spaminfo} = [];
996 } else {
997 # include additional details
998
999 # we want to get the whole email in raw mode
1000 my $maxbytes = (!$raw)? 4096 : undef;
1001
1002 my ($header, $content) = PMG::HTMLMail::read_raw_email($path, $maxbytes);
1003
1004 $res->{file} = $ref->{file};
1005 $res->{spaminfo} = decode_spaminfo($ref->{info});
1006 $res->{header} = $header;
1007 $res->{content} = $content;
1008 }
1009
1010 return $res;
1011
1012 }});
1013
1014 my $get_attachments = sub {
1015 my ($mailid, $dumpdir, $with_path) = @_;
1016
1017 my $rpcenv = PMG::RESTEnvironment->get();
1018
1019 my $ref = $get_and_check_mail->($mailid, $rpcenv);
1020
1021 my $filename = $ref->{file};
1022 my $spooldir = $PMG::MailQueue::spooldir;
1023
1024 my $parser = PMG::MIMEUtils::new_mime_parser({
1025 nested => 1,
1026 decode_bodies => 0,
1027 extract_uuencode => 0,
1028 dumpdir => $dumpdir,
1029 });
1030
1031 my $entity = $parser->parse_open("$spooldir/$filename");
1032 PMG::MIMEUtils::fixup_multipart($entity);
1033 PMG::MailQueue::decode_entities($parser, 'attachmentquarantine', $entity);
1034
1035 my $res = [];
1036 my $id = 0;
1037
1038 PMG::MIMEUtils::traverse_mime_parts($entity, sub {
1039 my ($part) = @_;
1040 my $name = PMG::Utils::extract_filename($part->head) || "part-$id";
1041 my $attachment_path = $part->{PMX_decoded_path};
1042 return if !$attachment_path || ! -f $attachment_path;
1043 my $size = -s $attachment_path // 0;
1044 my $entry = {
1045 id => $id,
1046 name => $name,
1047 size => $size,
1048 'content-type' => $part->head->mime_attr('content-type'),
1049 };
1050 $entry->{path} = $attachment_path if $with_path;
1051 push @$res, $entry;
1052 $id++;
1053 });
1054
1055 return $res;
1056 };
1057
1058 __PACKAGE__->register_method ({
1059 name => 'listattachments',
1060 path => 'listattachments',
1061 method => 'GET',
1062 permissions => { check => [ 'admin', 'qmanager', 'audit'] },
1063 description => "Get Attachments for E-Mail in Quarantine.",
1064 parameters => {
1065 additionalProperties => 0,
1066 properties => {
1067 id => {
1068 description => 'Unique ID',
1069 type => 'string',
1070 pattern => 'C\d+R\d+T\d+',
1071 maxLength => 60,
1072 },
1073 },
1074 },
1075 returns => {
1076 type => "array",
1077 items => {
1078 type => "object",
1079 properties => {
1080 id => {
1081 description => 'Attachment ID',
1082 type => 'integer',
1083 },
1084 size => {
1085 description => "Size of raw attachment in bytes.",
1086 type => 'integer' ,
1087 },
1088 name => {
1089 description => "Raw email header data.",
1090 type => 'string',
1091 },
1092 'content-type' => {
1093 description => "Raw email header data.",
1094 type => 'string',
1095 },
1096 },
1097 },
1098 },
1099 code => sub {
1100 my ($param) = @_;
1101
1102 my $dumpdir = "/run/pmgproxy/pmg-$param->{id}-$$";
1103 my $res = $get_attachments->($param->{id}, $dumpdir);
1104 rmtree $dumpdir;
1105
1106 return $res;
1107
1108 }});
1109
1110 __PACKAGE__->register_method ({
1111 name => 'download',
1112 path => 'download',
1113 method => 'GET',
1114 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
1115 description => "Download E-Mail or Attachment from Quarantine.",
1116 download => 1,
1117 parameters => {
1118 additionalProperties => 0,
1119 properties => {
1120 mailid => {
1121 description => 'Unique ID',
1122 type => 'string',
1123 pattern => 'C\d+R\d+T\d+',
1124 maxLength => 60,
1125 },
1126 attachmentid => {
1127 description => "The Attachment ID for the mail.",
1128 type => 'integer',
1129 optional => 1,
1130 },
1131 },
1132 },
1133 returns => {
1134 type => "object",
1135 },
1136 code => sub {
1137 my ($param) = @_;
1138
1139 my $mailid = $param->{mailid};
1140 my $attachmentid = $param->{attachmentid};
1141
1142 my $dumpdir = "/run/pmgproxy/pmg-$mailid-$$/";
1143 my $res;
1144
1145 if ($attachmentid) {
1146 my $attachments = $get_attachments->($mailid, $dumpdir, 1);
1147 $res = $attachments->[$attachmentid];
1148 if (!$res) {
1149 raise_param_exc({ attachmentid => "Invalid Attachment ID for Mail."});
1150 }
1151 } else {
1152 my $rpcenv = PMG::RESTEnvironment->get();
1153 my $ref = $get_and_check_mail->($mailid, $rpcenv);
1154 my $spooldir = $PMG::MailQueue::spooldir;
1155
1156 $res = {
1157 'content-type' => 'message/rfc822',
1158 path => "$spooldir/$ref->{file}",
1159 };
1160 }
1161
1162 $res->{fh} = IO::File->new($res->{path}, '<') ||
1163 die "unable to open file '$res->{path}' - $!\n";
1164
1165 rmtree $dumpdir if -e $dumpdir;
1166
1167 return $res;
1168
1169 }});
1170
1171 PVE::APIServer::Formatter::register_page_formatter(
1172 'format' => 'htmlmail',
1173 method => 'GET',
1174 path => '/quarantine/content',
1175 code => sub {
1176 my ($res, $data, $param, $path, $auth, $config) = @_;
1177
1178 if(!HTTP::Status::is_success($res->{status})) {
1179 return ("Error $res->{status}: $res->{message}", "text/plain");
1180 }
1181
1182 my $ct = "text/html;charset=UTF-8";
1183
1184 my $raw = $data->{content};
1185
1186 return (encode('UTF-8', $raw), $ct, 1);
1187 });
1188
1189 __PACKAGE__->register_method ({
1190 name =>'action',
1191 path => 'content',
1192 method => 'POST',
1193 description => "Execute quarantine actions.",
1194 permissions => { check => [ 'admin', 'qmanager', 'quser'] },
1195 protected => 1,
1196 parameters => {
1197 additionalProperties => 0,
1198 properties => {
1199 id => {
1200 description => 'Unique IDs, seperate with ;',
1201 type => 'string',
1202 pattern => 'C\d+R\d+T\d+(;C\d+R\d+T\d+)*',
1203 },
1204 action => {
1205 description => 'Action - specify what you want to do with the mail.',
1206 type => 'string',
1207 enum => ['whitelist', 'blacklist', 'deliver', 'delete'],
1208 },
1209 },
1210 },
1211 returns => { type => "null" },
1212 code => sub {
1213 my ($param) = @_;
1214
1215 my $rpcenv = PMG::RESTEnvironment->get();
1216 my $action = $param->{action};
1217 my @idlist = split(';', $param->{id});
1218
1219 my $dbh = PMG::DBTools::open_ruledb();
1220
1221 for my $id (@idlist) {
1222
1223 my $ref = $get_and_check_mail->($id, $rpcenv, $dbh);
1224 my $sender = $get_real_sender->($ref);
1225
1226 if ($action eq 'whitelist') {
1227 PMG::Quarantine::add_to_blackwhite($dbh, $ref->{pmail}, 'WL', [ $sender ]);
1228 } elsif ($action eq 'blacklist') {
1229 PMG::Quarantine::add_to_blackwhite($dbh, $ref->{pmail}, 'BL', [ $sender ]);
1230 } elsif ($action eq 'deliver') {
1231 PMG::Quarantine::deliver_quarantined_mail($dbh, $ref, $ref->{receiver} // $ref->{pmail});
1232 } elsif ($action eq 'delete') {
1233 PMG::Quarantine::delete_quarantined_mail($dbh, $ref);
1234 } else {
1235 die "internal error"; # should not be reached
1236 }
1237 }
1238
1239 return undef;
1240 }});
1241
1242 1;