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