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