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