]> git.proxmox.com Git - pmg-api.git/blame - PMG/API2/Quarantine.pm
spam api: return details about matched spam tests
[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
30sub decode_spaminfo {
31 my ($info) = @_;
32
33 $spamdesc = PMG::Utils::load_sa_descriptions() if !$spamdesc;
34
35 my $res = [];
36
37 foreach my $test (split (',', $info)) {
38 my ($name, $score) = split (':', $test);
39
40 my $info = { name => $name, score => $score, desc => '-' };
41 if (my $si = $spamdesc->{$name}) {
42 $info->{desc} = $si->{desc};
43 $info->{url} = $si->{url} if defined($si->{url});
44 }
45 push @$res, $info;
46 }
47
48 return $res;
49}
b66faa68 50
dae021a8
DM
51my $parse_header_info = sub {
52 my ($ref) = @_;
53
54 my $res = { subject => '', from => '' };
55
56 my @lines = split('\n', $ref->{header});
57 my $head = Mail::Header->new(\@lines);
58
59 $res->{subject} = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('subject'))) // '';
60
61 my @fromarray = split('\s*,\s*', $head->get('from') || $ref->{sender});
62
63 $res->{from} = PMG::Utils::decode_rfc1522(PVE::Tools::trim ($fromarray[0])) // '';
64
65 my $sender = PMG::Utils::decode_rfc1522(PVE::Tools::trim($head->get('sender')));
1284c016 66 $res->{sender} = $sender if $sender && ($sender ne $res->{from});
dae021a8
DM
67
68 $res->{envelope_sender} = $ref->{sender};
69 $res->{receiver} = $ref->{receiver};
6e8886d4 70 $res->{id} = 'C' . $ref->{cid} . 'R' . $ref->{rid};
dae021a8
DM
71 $res->{time} = $ref->{time};
72 $res->{bytes} = $ref->{bytes};
73
1284c016
DM
74 my $qtype = $ref->{qtype};
75
76 if ($qtype eq 'V') {
77 $res->{virusname} = $ref->{info};
78 } elsif ($qtype eq 'S') {
79 $res->{spamlevel} = $ref->{spamlevel} // 0;
1284c016
DM
80 }
81
dae021a8
DM
82 return $res;
83};
84
6e8886d4 85
b66faa68
DM
86__PACKAGE__->register_method ({
87 name => 'index',
88 path => '',
89 method => 'GET',
90 permissions => { user => 'all' },
91 description => "Directory index.",
92 parameters => {
93 additionalProperties => 0,
94 properties => {},
95 },
96 returns => {
97 type => 'array',
98 items => {
99 type => "object",
100 properties => {},
101 },
102 links => [ { rel => 'child', href => "{name}" } ],
103 },
104 code => sub {
105 my ($param) = @_;
106
107 my $result = [
108 { name => 'deliver' },
6e8886d4 109 { name => 'content' },
b66faa68
DM
110 { name => 'spam' },
111 { name => 'virus' },
112 ];
113
114 return $result;
115 }});
116
117__PACKAGE__->register_method ({
118 name => 'spam',
119 path => 'spam',
120 method => 'GET',
121 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
122 description => "Show spam mails distribution (per day).",
123 parameters => {
124 additionalProperties => 0,
125 properties => {
126 starttime => {
127 description => "Only consider entries newer than 'startime' (unix epoch).",
128 type => 'integer',
129 minimum => 0,
130 optional => 1,
131 },
132 endtime => {
133 description => "Only consider entries older than 'endtime' (unix epoch).",
134 type => 'integer',
135 minimum => 1,
136 optional => 1,
137 },
138 pmail => {
ded33c7c 139 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.",
b66faa68
DM
140 type => 'string', format => 'email',
141 optional => 1,
142 },
143 },
144 },
145 returns => {
146 type => 'array',
147 items => {
148 type => "object",
149 properties => {
150 day => {
151 description => "Day (as unix epoch).",
152 type => 'integer',
153 },
bc1ebe25 154 count => {
b66faa68
DM
155 description => "Number of quarantine entries.",
156 type => 'integer',
157 },
158 spamavg => {
159 description => "Average spam level.",
160 type => 'number',
bc1ebe25 161 },
b66faa68
DM
162 },
163 },
ded33c7c 164 links => [ { rel => 'child', href => "{day}" } ],
b66faa68
DM
165 },
166 code => sub {
167 my ($param) = @_;
168
169 my $rpcenv = PMG::RESTEnvironment->get();
170 my $authuser = $rpcenv->get_user();
171 my $role = $rpcenv->get_role();
172
173 my $pmail = $param->{pmail};
174
175 if ($role eq 'quser') {
176 raise_param_exc({ pmail => "paramater not allwed with role '$role'"})
177 if defined($pmail);
178 $pmail = $authuser;
ded33c7c
DM
179 } else {
180 raise_param_exc({ pmail => "paramater required with role '$role'"})
181 if !defined($pmail);
b66faa68
DM
182 }
183
184 my $res = [];
bc1ebe25 185
b66faa68
DM
186 my $dbh = PMG::DBTools::open_ruledb();
187
ec7035c2 188 my $start = $param->{starttime};
b66faa68
DM
189 my $end = $param->{endtime};
190
191 my $timezone = tz_local_offset();
192
193 my $sth = $dbh->prepare(
194 "SELECT " .
195 "((time + $timezone) / 86400) * 86400 - $timezone as day, " .
196 "count (ID) as count, avg (Spamlevel) as spamavg " .
197 "FROM CMailStore, CMSReceivers WHERE " .
ec7035c2
DM
198 (defined($start) ? "time >= $start AND " : '') .
199 (defined($end) ? "time < $end AND " : '') .
ded33c7c 200 "pmail = ? AND " .
b66faa68
DM
201 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
202 "AND Status = 'N' " .
203 "GROUP BY day " .
204 "ORDER BY day DESC");
205
ded33c7c
DM
206 $sth->execute($pmail);
207
208 while (my $ref = $sth->fetchrow_hashref()) {
209 push @$res, $ref;
210 }
211
212 return $res;
213 }});
214
215__PACKAGE__->register_method ({
216 name => 'spamlist',
217 path => 'spam/{starttime}',
218 method => 'GET',
219 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
220 description => "Show spam mails distribution (per day).",
221 parameters => {
222 additionalProperties => 0,
223 properties => {
224 starttime => {
225 description => "Only consider entries newer than 'starttime' (unix epoch).",
226 type => 'integer',
227 minimum => 0,
228 },
229 endtime => {
230 description => "Only consider entries older than 'endtime' (unix epoch). This is set to '<start> + 1day' by default.",
231 type => 'integer',
232 minimum => 1,
233 optional => 1,
234 },
235 pmail => {
236 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.",
237 type => 'string', format => 'email',
238 optional => 1,
239 },
240 },
241 },
242 returns => {
243 type => 'array',
244 items => {
245 type => "object",
dae021a8
DM
246 properties => {
247 id => {
248 description => 'Unique ID',
249 type => 'string',
250 },
251 bytes => {
252 description => "Size of raw email.",
253 type => 'integer' ,
254 },
255 envelope_sender => {
256 description => "SMTP envelope sender.",
257 type => 'string',
258 },
259 from => {
260 description => "Header 'From' field.",
261 type => 'string',
262 },
263 sender => {
264 description => "Header 'Sender' field.",
265 type => 'string',
266 optional => 1,
267 },
268 receiver => {
269 description => "Receiver email address",
270 type => 'string',
271 },
272 subject => {
273 description => "Header 'Subject' field.",
274 type => 'string',
275 },
276 time => {
277 description => "Receive time stamp",
278 type => 'integer',
279 },
1284c016
DM
280 spamlevel => {
281 description => "Spam score.",
282 type => 'number',
283 },
dae021a8 284 },
ded33c7c
DM
285 },
286 },
287 code => sub {
288 my ($param) = @_;
289
290 my $rpcenv = PMG::RESTEnvironment->get();
291 my $authuser = $rpcenv->get_user();
292 my $role = $rpcenv->get_role();
293
294 my $pmail = $param->{pmail};
295
296 if ($role eq 'quser') {
297 raise_param_exc({ pmail => "paramater not allwed with role '$role'"})
298 if defined($pmail);
299 $pmail = $authuser;
b66faa68 300 } else {
ded33c7c
DM
301 raise_param_exc({ pmail => "paramater required with role '$role'"})
302 if !defined($pmail);
b66faa68
DM
303 }
304
ded33c7c
DM
305 my $res = [];
306
307 my $dbh = PMG::DBTools::open_ruledb();
308
309 my $start = $param->{starttime};
310 my $end = $param->{endtime} // ($start + 86400);
311
312 my $sth = $dbh->prepare(
313 "SELECT * " .
314 "FROM CMailStore, CMSReceivers WHERE " .
315 "pmail = ? AND time >= $start AND time < $end AND " .
316 "QType = 'S' AND CID = CMailStore_CID AND RID = CMailStore_RID " .
317 "AND Status = 'N' ORDER BY pmail, time, receiver");
318
319 $sth->execute($pmail);
320
b66faa68 321 while (my $ref = $sth->fetchrow_hashref()) {
dae021a8
DM
322 my $data = $parse_header_info->($ref);
323 push @$res, $data;
b66faa68
DM
324 }
325
326 return $res;
327 }});
328
6e8886d4
DM
329__PACKAGE__->register_method ({
330 name => 'content',
331 path => 'content',
332 method => 'GET',
333 permissions => { check => [ 'admin', 'qmanager', 'audit', 'quser'] },
34db0c3f 334 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
335 parameters => {
336 additionalProperties => 0,
337 properties => {
338 id => {
339 description => 'Unique ID',
340 type => 'string',
341 pattern => 'C\d+R\d+',
342 maxLength => 40,
343 },
34db0c3f
DM
344 raw => {
345 description => "Display 'raw' eml data. This is only used with the 'htmlmail' formatter.",
346 type => 'boolean',
347 optional => 1,
348 default => 0,
349 },
6e8886d4
DM
350 },
351 },
352 returns => {
353 type => "object",
cd31bb45
DM
354 properties => {
355 id => {
356 description => 'Unique ID',
357 type => 'string',
358 },
359 bytes => {
360 description => "Size of raw email.",
361 type => 'integer' ,
362 },
363 envelope_sender => {
364 description => "SMTP envelope sender.",
365 type => 'string',
366 },
367 from => {
368 description => "Header 'From' field.",
369 type => 'string',
370 },
371 sender => {
372 description => "Header 'Sender' field.",
373 type => 'string',
374 optional => 1,
375 },
376 receiver => {
377 description => "Receiver email address",
378 type => 'string',
379 },
380 subject => {
381 description => "Header 'Subject' field.",
382 type => 'string',
383 },
384 time => {
385 description => "Receive time stamp",
386 type => 'integer',
387 },
388 spamlevel => {
389 description => "Spam score.",
390 type => 'number',
391 },
392 spaminfo => {
393 description => "Information about matched spam tests (name, score, desc, url).",
394 type => 'array',
395 },
396 header => {
397 description => "Raw email header data.",
398 type => 'string',
399 },
400 content => {
401 description => "Raw email data (first 4096 bytes). Useful for preview. NOTE: The 'htmlmail' formatter displays the whole email.",
402 type => 'string',
403 }
404 },
6e8886d4
DM
405 },
406 code => sub {
407 my ($param) = @_;
408
409 my $rpcenv = PMG::RESTEnvironment->get();
410 my $authuser = $rpcenv->get_user();
411 my $role = $rpcenv->get_role();
34db0c3f 412 my $format = $rpcenv->get_format();
6e8886d4
DM
413
414 my ($cid, $rid) = $param->{id} =~ m/^C(\d+)R(\d+)$/;
415 $cid = int($cid);
416 $rid = int($rid);
417
418 my $dbh = PMG::DBTools::open_ruledb();
419
420 my $ref = PMG::DBTools::load_mail_data($dbh, $cid, $rid);
421
422 if ($role eq 'quser') {
423 raise_perm_exc("mail does not belong to user '$authuser'")
424 if $authuser ne $ref->{pmail};
425 }
426
427 my $res = $parse_header_info->($ref);
428
6e8886d4
DM
429
430 my $filename = $ref->{file};
431 my $spooldir = $PMG::MailQueue::spooldir;
432
433 my $path = "$spooldir/$filename";
434
34db0c3f
DM
435 if ($format eq 'htmlmail') {
436
437 my $cfg = PMG::Config->new();
438 my $viewimages = $cfg->get('spamquar', 'viewimages');
439 my $allowhref = $cfg->get('spamquar', 'allowhrefs');
440
441 $res->{header} = ''; # not required
442 $res->{content} = PMG::HTMLMail::email_to_html($path, $param->{raw}, $viewimages, $allowhref);
443
444 } else {
cd31bb45 445 # include additional details
34db0c3f 446
cd31bb45
DM
447 my ($header, $content) = PMG::HTMLMail::read_raw_email($path, 4096);
448
449 $res->{file} = $ref->{file};
450 $res->{spaminfo} = decode_spaminfo($ref->{info});
34db0c3f
DM
451 $res->{header} = $header;
452 $res->{content} = $content;
453 }
6e8886d4 454
6e8886d4
DM
455
456 return $res;
457
458 }});
459
34db0c3f
DM
460PVE::APIServer::Formatter::register_page_formatter(
461 'format' => 'htmlmail',
462 method => 'GET',
463 path => '/quarantine/content',
464 code => sub {
465 my ($res, $data, $param, $path, $auth, $config) = @_;
466
467 if(!HTTP::Status::is_success($res->{status})) {
468 return ("Error $res->{status}: $res->{message}", "text/plain");
469 }
470
471 my $ct = "text/html;charset=UTF-8";
472
473 my $raw = $data->{content};
474
475 return (encode('UTF-8', $raw), $ct, 1);
476});
477
b66faa68 4781;