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