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