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