]> git.proxmox.com Git - pmg-api.git/blob - PMG/API2/MailTracker.pm
add pmg report api call for the gui
[pmg-api.git] / PMG / API2 / MailTracker.pm
1 package PMG::API2::MailTracker;
2
3 use strict;
4 use warnings;
5 use POSIX;
6 use Digest::MD5;
7 use Time::Zone;
8 use Data::Dumper;
9 use Encode;
10
11 use PVE::Tools;
12 use PVE::SafeSyslog;
13 use PVE::RESTHandler;
14 use PVE::JSONSchema qw(get_standard_option);
15
16 use PMG::RESTEnvironment;
17
18 use base qw(PVE::RESTHandler);
19
20 my $statmap = {
21 2 => 'delivered',
22 4 => 'deferred',
23 5 => 'bounced',
24 N => 'rejected',
25 G => 'greylisted',
26 A => 'accepted',
27 B => 'blocked',
28 Q => 'quarantine',
29 };
30
31 my $run_pmg_log_tracker = sub {
32 my ($args, $includelog) = @_;
33
34 my $logids = {};
35
36 my $timezone = tz_local_offset();;
37
38 if (defined(my $id = $includelog)) {
39 if ($id =~ m/^Q([a-f0-9]+)R([a-f0-9]+)$/i) {
40 $logids->{$1} = 1;
41 $logids->{$2} = 1;
42 push @$args, '-q', $1, '-q', $2;
43 } else {
44 $logids->{$id} = 1;
45 push @$args, '-q', $id;
46 }
47 }
48
49 my $lookup_hash = {};
50 my $list = [];
51 my $state = 'start';
52 my $status;
53 my $entry;
54 my $logs = [];
55
56 my $parser = sub {
57 my ($line) = @_;
58
59 # assume syslog is UTF-8 encoded
60 $line = decode('UTF-8', $line);
61
62 if ($state eq 'start') {
63
64 return if $line =~ m/^\#/;
65 return if $line =~ m/^\s*$/;
66
67 if ($line =~ m/^STATUS: (.*)$/) {
68 $state = 'end';
69 $status = $1;
70 return;
71 }
72
73 if ($line =~ m/^SMTPD:\s+(T[0-9A-F]+L[0-9A-F]+)$/) {
74 $state = 'smtp';
75 $entry = { id => $1 };
76 return;
77 }
78
79 if ($line =~ m/^QENTRY:\s+([0-9A-F]+)$/) {
80 $state = 'qentry';
81 $entry = { qid => $1 };
82 return;
83 }
84
85 die "got unexpected data: $line";
86 } elsif ($state eq 'end') {
87 die "got unexpected data after status: $line";
88 } elsif ($state eq 'skiplogs') {
89 if ($line =~ m/^\s*$/) {
90 $entry = undef;
91 $state = 'start';
92 } else {
93 # skip
94 }
95 } elsif ($state eq 'logs') {
96 if ($line =~ m/^\s*$/) {
97 $entry = undef;
98 $state = 'start';
99 } elsif ($line =~ m/^(SMTP|FILTER|QMGR):/) {
100 # skip
101 } elsif ($line =~ m/^(L[A-F0-9]+)\s(.*)$/) {
102 push @$logs, { linenr => $1, text => $2 };
103 } else {
104 die "got unexpected data: $line";
105 }
106 } elsif ($state eq 'qentry') {
107 if ($line =~ m/^\s*$/) {
108 $entry = undef;
109 $state = 'start';
110 } elsif ($line =~ m/^SIZE:\s+(\d+)$/) {
111 $entry->{size} = $1;
112 } elsif ($line =~ m/^CLIENT:\s+(\S+)$/) {
113 $entry->{client} = $1;
114 } elsif ($line =~ m/^MSGID:\s+(\S+)$/) {
115 $entry->{msgid} = $1;
116 } elsif ($line =~ m/^CTIME:\s+([0-9A-F]+)$/) {
117 # ignore ?
118 } elsif ($line =~ m/^TO:([0-9A-F]+):([0-9A-F]+):([0-9A-Z]):\s+from <([^>]*)>\s+to\s+<([^>]+)>\s+\((\S+)\)$/) {
119 my $new = {};
120 $new->{size} = $entry->{size} // 0,
121 $new->{client} = $entry->{client} if defined($entry->{client});
122 $new->{msgid} = $entry->{msgid} if defined($entry->{msgid});
123 $new->{time} = hex($1) - $timezone;
124 $new->{qid} = $2;
125 $new->{dstatus} = $3;
126 $new->{from} = $4;
127 $new->{to} = $5;
128 $new->{relay} = $6;
129
130 push @$list, $new;
131 $lookup_hash->{$2}->{$5} = $new;
132 } elsif ($line =~ m/^(SMTP|FILTER|QMGR):/) {
133 if ($logids->{$entry->{qid}}) {
134 $state = 'logs';
135 } else {
136 $state = 'skiplogs';
137 }
138 } else {
139 die "got unexpected data: $line";
140 }
141 } elsif ($state eq 'smtp') {
142
143 if ($line =~ m/^\s*$/) {
144 $entry = undef;
145 $state = 'start';
146 } elsif ($line =~ m/^CLIENT:\s+(\S+)$/) {
147 $entry->{client} = $1;
148 } elsif ($line =~ m/^CTIME:\s+([0-9A-F]+)$/) {
149 # ignore ?
150 } elsif ($line =~ m/^TO:([0-9A-F]+):(T[0-9A-F]+L[0-9A-F]+):([0-9A-Z]):\s+from <([^>]*)>\s+to\s+<([^>]+)>$/) {
151 my $e = {};
152 $e->{client} = $entry->{client} if defined($entry->{client});
153 $e->{time} = hex($1) - $timezone;
154 $e->{id} = $2;
155 $e->{dstatus} = $3;
156 $e->{from} = $4;
157 $e->{to} = $5;
158 push @$list, $e;
159 } elsif ($line =~ m/^LOGS:$/) {
160 if ($logids->{$entry->{id}}) {
161 $state = 'logs';
162 } else {
163 $state = 'skiplogs';
164 }
165 } else {
166 die "got unexpected data: $line";
167 }
168 } else {
169 die "unknown state '$state'\n";
170 }
171 };
172
173 my $cmd = ['/usr/bin/pmg-log-tracker', '-v', '-l', 2000];
174
175 PVE::Tools::run_command([@$cmd, @$args], timeout => 25, outfunc => $parser);
176
177 my $sorted_logs = [];
178 foreach my $le (sort {$a->{linenr} cmp $b->{linenr}} @$logs) {
179 push @$sorted_logs, $le->{text};
180 }
181
182 foreach my $e (@$list) {
183 if (my $id = $e->{qid}) {
184 if (my $relay = $e->{relay}) {
185 if (my $ref = $lookup_hash->{$relay}->{$e->{to}}) {
186 $ref->{is_relay} = 1;
187 $id = 'Q' . $e->{qid} . 'R' . $e->{relay};
188 if ($e->{dstatus} eq 'A') {
189 $e->{rstatus} = $ref->{dstatus};
190 }
191 }
192 }
193 $e->{id} = $id;
194 }
195 if ($includelog && ($e->{id} eq $includelog)) {
196 $e->{logs} = $sorted_logs;
197 }
198 }
199
200 return wantarray ? ($list, $status) : $list;
201 };
202
203 my $email_log_property_desc = {
204 id => {
205 description => "Unique ID.",
206 type => 'string',
207 },
208 from => {
209 description => "Sender email address.",
210 type => 'string',
211 },
212 to => {
213 description => "Receiver email address.",
214 type => 'string',
215 },
216 qid => {
217 description => "Postfix qmgr ID.",
218 type => 'string',
219 optional => 1,
220 },
221 time => {
222 description => "Delivery timestamp.",
223 type => 'integer',
224 },
225 dstatus => {
226 description => "Delivery status.",
227 type => 'string',
228 minLength => 1,
229 maxLength => 1,
230 },
231 rstatus => {
232 description => "Delivery status of relayed mail.",
233 type => 'string',
234 minLength => 1,
235 maxLength => 1,
236 optional => 1,
237 },
238 relay => {
239 description => "ID of relayed mail.",
240 type => 'string',
241 optional => 1,
242 },
243 size => {
244 description => "The size of the raw email.",
245 type => 'number',
246 optional => 1,
247 },
248 client => {
249 description => "Client address",
250 type => 'string',
251 optional => 1,
252 },
253 msgid => {
254 description => "SMTP message ID.",
255 type => 'string',
256 optional => 1,
257 },
258 };
259
260 __PACKAGE__->register_method({
261 name => 'list_mails',
262 path => '',
263 method => 'GET',
264 description => "Read mail list.",
265 proxyto => 'node',
266 protected => 1,
267 permissions => { check => [ 'admin', 'audit' ] },
268 parameters => {
269 additionalProperties => 0,
270 properties => {
271 node => get_standard_option('pve-node'),
272 starttime => get_standard_option('pmg-starttime'),
273 endtime => get_standard_option('pmg-endtime'),
274 xfilter => {
275 description => "Only include mails containing this filter string.",
276 type => 'string',
277 minLength => 1,
278 maxLength => 256,
279 optional => 1,
280 },
281 from => {
282 description => "Sender email address filter.",
283 type => 'string',
284 optional => 1,
285 minLength => 1,
286 maxLength => 256,
287 },
288 target => {
289 description => "Receiver email address filter.",
290 type => 'string',
291 optional => 1,
292 minLength => 1,
293 maxLength => 256,
294 },
295 ndr => {
296 description => "Include NDRs (non delivery reports).",
297 type => 'boolean',
298 optional => 1,
299 default => 0,
300 },
301 greylist => {
302 description => "Include Greylisted entries.",
303 type => 'boolean',
304 optional => 1,
305 default => 0,
306 },
307 },
308 },
309 returns => {
310 type => 'array',
311 items => {
312 type => "object",
313 properties => $email_log_property_desc,
314 },
315 links => [ { rel => 'child', href => "{id}" } ],
316 },
317 code => sub {
318 my ($param) = @_;
319
320 my $restenv = PMG::RESTEnvironment->get();
321
322 my $args = [];
323
324 my $start = $param->{starttime} // (time - 86400);
325 my $end = $param->{endtime} // ($start + 86400);
326
327 push @$args, '-s', $start;
328 push @$args, '-e', $end;
329
330 push @$args, '-n' if !$param->{ndr};
331
332 push @$args, '-g' if !$param->{greylist};
333
334 push @$args, '-x', $param->{xfilter} if defined($param->{xfilter});
335
336 if (defined($param->{from})) {
337 push @$args, '-f', $param->{from};
338 }
339 if (defined($param->{target})) {
340 push @$args, '-t', $param->{target};
341 }
342
343 my ($list, $status) = $run_pmg_log_tracker->($args);
344
345 my $res = [];
346 foreach my $e (@$list) {
347 push @$res, $e if !$e->{is_relay};
348 }
349
350 # hack: return status message in 'changes' attribute
351 $restenv->set_result_attrib('changes', $status) if defined($status);
352
353 return $res;
354 }});
355
356 __PACKAGE__->register_method({
357 name => 'maillog',
358 path => '{id}',
359 method => 'GET',
360 description => "Get the detailed syslog entries for a specific mail ID.",
361 proxyto => 'node',
362 protected => 1,
363 permissions => { check => [ 'admin', 'audit' ] },
364 parameters => {
365 additionalProperties => 0,
366 properties => {
367 node => get_standard_option('pve-node'),
368 starttime => get_standard_option('pmg-starttime'),
369 endtime => get_standard_option('pmg-endtime'),
370 id => {
371 description => "Mail ID (as returend by the list API).",
372 type => 'string',
373 minLength => 3,
374 maxLength => 64,
375 },
376 },
377 },
378 returns => {
379 type => "object",
380 properties => {
381 %$email_log_property_desc,
382 logs => {
383 type => 'array',
384 items => { type => "string" },
385 }
386 },
387 },
388 code => sub {
389 my ($param) = @_;
390
391 my $restenv = PMG::RESTEnvironment->get();
392
393 my $args = ['-v'];
394
395 my $start = $param->{starttime} // (time - 86400);
396 my $end = $param->{endtime} // ($start + 86400);
397
398 push @$args, '-s', $start;
399 push @$args, '-e', $end;
400
401 my $list = $run_pmg_log_tracker->($args, $param->{id});
402
403 my $res;
404 foreach my $e (@$list) {
405 $res = $e if $e->{id} eq $param->{id};
406 }
407
408 die "entry '$param->{id}' not found\n" if !defined($res);
409
410 return $res;
411 }});
412
413 1;