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