]> git.proxmox.com Git - pmg-api.git/blame - PMG/API2/MailTracker.pm
get_template_vars: compute wl_bounce_relays, always include local fqdn
[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 8use Data::Dumper;
db726d64 9use Encode;
202c952a
DM
10
11use PVE::Tools;
12use PVE::SafeSyslog;
13use PVE::RESTHandler;
14use PVE::JSONSchema qw(get_standard_option);
15
16use PMG::RESTEnvironment;
17
18use base qw(PVE::RESTHandler);
19
20my $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
31my $run_pmg_log_tracker = sub {
32 my ($args, $includelog) = @_;
33
34 my $logids = {};
35
6d26ff55
DM
36 my $timezone = tz_local_offset();;
37
202c952a
DM
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;
be78a520 42 push @$args, '-q', $1, '-q', $2;
202c952a
DM
43 } else {
44 $logids->{$id} = 1;
be78a520 45 push @$args, '-q', $id;
202c952a
DM
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
f2f5730f 59 # assume syslog is UTF-8 encoded
db726d64
DM
60 $line = decode('UTF-8', $line);
61
202c952a
DM
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
39a46313 73 if ($line =~ m/^SMTPD:\s+(T[0-9A-F]+L[0-9A-F]+)$/) {
202c952a 74 $state = 'smtp';
39a46313 75 $entry = { id => $1 };
202c952a
DM
76 return;
77 }
78
79 if ($line =~ m/^QENTRY:\s+([0-9A-F]+)$/) {
80 $state = 'qentry';
39a46313 81 $entry = { qid => $1 };
202c952a
DM
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
ef59543c
DM
101 } elsif ($line =~ m/^(L[A-F0-9]+)\s(.*)$/) {
102 push @$logs, { linenr => $1, text => $2 };
202c952a
DM
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 ?
be78a520 118 } elsif ($line =~ m/^TO:([0-9A-F]+):([0-9A-F]+):([0-9A-Z]):\s+from <([^>]*)>\s+to\s+<([^>]+)>\s+\((\S+)\)$/) {
202c952a
DM
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});
6d26ff55 123 $new->{time} = hex($1) - $timezone;
202c952a
DM
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';
23c3e8a1
DM
146 } elsif ($line =~ m/^CLIENT:\s+(\S+)$/) {
147 $entry->{client} = $1;
202c952a
DM
148 } elsif ($line =~ m/^CTIME:\s+([0-9A-F]+)$/) {
149 # ignore ?
be78a520 150 } elsif ($line =~ m/^TO:([0-9A-F]+):(T[0-9A-F]+L[0-9A-F]+):([0-9A-Z]):\s+from <([^>]*)>\s+to\s+<([^>]+)>$/) {
202c952a 151 my $e = {};
23c3e8a1 152 $e->{client} = $entry->{client} if defined($entry->{client});
6d26ff55 153 $e->{time} = hex($1) - $timezone;
39a46313 154 $e->{id} = $2;
be78a520
DM
155 $e->{dstatus} = $3;
156 $e->{from} = $4;
157 $e->{to} = $5;
202c952a
DM
158 push @$list, $e;
159 } elsif ($line =~ m/^LOGS:$/) {
be78a520 160 if ($logids->{$entry->{id}}) {
202c952a
DM
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
235b71a3 173 my $cmd = ['/usr/bin/pmg-log-tracker', '-v', '-l', 2000];
202c952a 174
235b71a3 175 PVE::Tools::run_command([@$cmd, @$args], timeout => 25, outfunc => $parser);
202c952a
DM
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;
202c952a
DM
194 }
195 if ($includelog && ($e->{id} eq $includelog)) {
196 $e->{logs} = $sorted_logs;
197 }
198 }
199
235b71a3 200 return wantarray ? ($list, $status) : $list;
202c952a
DM
201};
202
23c3e8a1
DM
203my $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
202c952a
DM
260__PACKAGE__->register_method({
261 name => 'list_mails',
262 path => '',
263 method => 'GET',
264 description => "Read mail list.",
265 proxyto => 'node',
39a46313 266 protected => 1,
202c952a
DM
267 parameters => {
268 additionalProperties => 0,
269 properties => {
270 node => get_standard_option('pve-node'),
271 starttime => get_standard_option('pmg-starttime'),
272 endtime => get_standard_option('pmg-endtime'),
607ab95f
DM
273 xfilter => {
274 description => "Only include mails containing this filter string.",
275 type => 'string',
276 minLength => 1,
277 maxLength => 256,
278 optional => 1,
279 },
202c952a
DM
280 from => {
281 description => "Sender email address filter.",
282 type => 'string',
283 optional => 1,
0f85a4ac 284 minLength => 1,
202c952a
DM
285 maxLength => 256,
286 },
287 target => {
288 description => "Receiver email address filter.",
289 type => 'string',
290 optional => 1,
0f85a4ac 291 minLength => 1,
202c952a
DM
292 maxLength => 256,
293 },
607ab95f
DM
294 ndr => {
295 description => "Include NDRs (non delivery reports).",
296 type => 'boolean',
297 optional => 1,
298 default => 0,
299 },
300 greylist => {
301 description => "Include Greylisted entries.",
302 type => 'boolean',
303 optional => 1,
304 default => 0,
305 },
202c952a
DM
306 },
307 },
308 returns => {
309 type => 'array',
310 items => {
311 type => "object",
23c3e8a1 312 properties => $email_log_property_desc,
202c952a
DM
313 },
314 links => [ { rel => 'child', href => "{id}" } ],
315 },
316 code => sub {
317 my ($param) = @_;
318
319 my $restenv = PMG::RESTEnvironment->get();
320
321 my $args = [];
322
323 my $start = $param->{starttime} // (time - 86400);
324 my $end = $param->{endtime} // ($start + 86400);
325
326 push @$args, '-s', $start;
327 push @$args, '-e', $end;
328
607ab95f
DM
329 push @$args, '-n' if !$param->{ndr};
330
331 push @$args, '-g' if !$param->{greylist};
332
333 push @$args, '-x', $param->{xfilter} if defined($param->{xfilter});
334
202c952a
DM
335 if (defined($param->{from})) {
336 push @$args, '-f', $param->{from};
337 }
338 if (defined($param->{target})) {
339 push @$args, '-t', $param->{target};
340 }
341
235b71a3 342 my ($list, $status) = $run_pmg_log_tracker->($args);
202c952a
DM
343
344 my $res = [];
345 foreach my $e (@$list) {
346 push @$res, $e if !$e->{is_relay};
347 }
348
235b71a3
DM
349 # hack: return status message in 'changes' attribute
350 $restenv->set_result_attrib('changes', $status) if defined($status);
351
202c952a
DM
352 return $res;
353 }});
354
ef59543c
DM
355__PACKAGE__->register_method({
356 name => 'maillog',
357 path => '{id}',
358 method => 'GET',
359 description => "Get the detailed syslog entries for a specific mail ID.",
360 proxyto => 'node',
39a46313 361 protected => 1,
ef59543c
DM
362 parameters => {
363 additionalProperties => 0,
364 properties => {
365 node => get_standard_option('pve-node'),
366 starttime => get_standard_option('pmg-starttime'),
367 endtime => get_standard_option('pmg-endtime'),
368 id => {
369 description => "Mail ID (as returend by the list API).",
370 type => 'string',
371 minLength => 3,
372 maxLength => 64,
373 },
374 },
375 },
376 returns => {
377 type => "object",
378 properties => {
23c3e8a1
DM
379 %$email_log_property_desc,
380 logs => {
381 type => 'array',
382 items => { type => "string" },
383 }
ef59543c
DM
384 },
385 },
386 code => sub {
387 my ($param) = @_;
388
389 my $restenv = PMG::RESTEnvironment->get();
390
be78a520 391 my $args = ['-v'];
ef59543c
DM
392
393 my $start = $param->{starttime} // (time - 86400);
394 my $end = $param->{endtime} // ($start + 86400);
395
396 push @$args, '-s', $start;
397 push @$args, '-e', $end;
398
399 my $list = $run_pmg_log_tracker->($args, $param->{id});
400
401 my $res;
402 foreach my $e (@$list) {
403 $res = $e if $e->{id} eq $param->{id};
404 }
405
406 die "entry '$param->{id}' not found\n" if !defined($res);
407
408 return $res;
409 }});
410
202c952a 4111;