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