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