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