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