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