]>
Commit | Line | Data |
---|---|---|
202c952a DM |
1 | package PMG::API2::MailTracker; |
2 | ||
3 | use strict; | |
4 | use warnings; | |
5 | use POSIX; | |
6 | use Digest::MD5; | |
6d26ff55 | 7 | use Time::Zone; |
202c952a | 8 | use Data::Dumper; |
db726d64 | 9 | use Encode; |
202c952a DM |
10 | |
11 | use PVE::Tools; | |
12 | use PVE::SafeSyslog; | |
13 | use PVE::RESTHandler; | |
14 | use PVE::JSONSchema qw(get_standard_option); | |
15 | ||
16 | use PMG::RESTEnvironment; | |
17 | ||
18 | use base qw(PVE::RESTHandler); | |
19 | ||
20 | my $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 | ||
31 | my $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 |
203 | my $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, |
7514a636 | 267 | permissions => { check => [ 'admin', 'audit' ] }, |
202c952a DM |
268 | parameters => { |
269 | additionalProperties => 0, | |
270 | properties => { | |
271 | node => get_standard_option('pve-node'), | |
272 | starttime => get_standard_option('pmg-starttime'), | |
273 | endtime => get_standard_option('pmg-endtime'), | |
607ab95f DM |
274 | xfilter => { |
275 | description => "Only include mails containing this filter string.", | |
276 | type => 'string', | |
277 | minLength => 1, | |
278 | maxLength => 256, | |
279 | optional => 1, | |
280 | }, | |
202c952a DM |
281 | from => { |
282 | description => "Sender email address filter.", | |
283 | type => 'string', | |
284 | optional => 1, | |
0f85a4ac | 285 | minLength => 1, |
202c952a DM |
286 | maxLength => 256, |
287 | }, | |
288 | target => { | |
289 | description => "Receiver email address filter.", | |
290 | type => 'string', | |
291 | optional => 1, | |
0f85a4ac | 292 | minLength => 1, |
202c952a DM |
293 | maxLength => 256, |
294 | }, | |
607ab95f DM |
295 | ndr => { |
296 | description => "Include NDRs (non delivery reports).", | |
297 | type => 'boolean', | |
298 | optional => 1, | |
299 | default => 0, | |
300 | }, | |
301 | greylist => { | |
302 | description => "Include Greylisted entries.", | |
303 | type => 'boolean', | |
304 | optional => 1, | |
305 | default => 0, | |
306 | }, | |
202c952a DM |
307 | }, |
308 | }, | |
309 | returns => { | |
310 | type => 'array', | |
311 | items => { | |
312 | type => "object", | |
23c3e8a1 | 313 | properties => $email_log_property_desc, |
202c952a DM |
314 | }, |
315 | links => [ { rel => 'child', href => "{id}" } ], | |
316 | }, | |
317 | code => sub { | |
318 | my ($param) = @_; | |
319 | ||
320 | my $restenv = PMG::RESTEnvironment->get(); | |
321 | ||
322 | my $args = []; | |
323 | ||
324 | my $start = $param->{starttime} // (time - 86400); | |
325 | my $end = $param->{endtime} // ($start + 86400); | |
326 | ||
327 | push @$args, '-s', $start; | |
328 | push @$args, '-e', $end; | |
329 | ||
607ab95f DM |
330 | push @$args, '-n' if !$param->{ndr}; |
331 | ||
332 | push @$args, '-g' if !$param->{greylist}; | |
333 | ||
334 | push @$args, '-x', $param->{xfilter} if defined($param->{xfilter}); | |
335 | ||
202c952a DM |
336 | if (defined($param->{from})) { |
337 | push @$args, '-f', $param->{from}; | |
338 | } | |
339 | if (defined($param->{target})) { | |
340 | push @$args, '-t', $param->{target}; | |
341 | } | |
342 | ||
235b71a3 | 343 | my ($list, $status) = $run_pmg_log_tracker->($args); |
202c952a DM |
344 | |
345 | my $res = []; | |
346 | foreach my $e (@$list) { | |
347 | push @$res, $e if !$e->{is_relay}; | |
348 | } | |
349 | ||
235b71a3 DM |
350 | # hack: return status message in 'changes' attribute |
351 | $restenv->set_result_attrib('changes', $status) if defined($status); | |
352 | ||
202c952a DM |
353 | return $res; |
354 | }}); | |
355 | ||
ef59543c DM |
356 | __PACKAGE__->register_method({ |
357 | name => 'maillog', | |
358 | path => '{id}', | |
359 | method => 'GET', | |
360 | description => "Get the detailed syslog entries for a specific mail ID.", | |
361 | proxyto => 'node', | |
39a46313 | 362 | protected => 1, |
7514a636 | 363 | permissions => { check => [ 'admin', 'audit' ] }, |
ef59543c DM |
364 | parameters => { |
365 | additionalProperties => 0, | |
366 | properties => { | |
367 | node => get_standard_option('pve-node'), | |
368 | starttime => get_standard_option('pmg-starttime'), | |
369 | endtime => get_standard_option('pmg-endtime'), | |
370 | id => { | |
371 | description => "Mail ID (as returend by the list API).", | |
372 | type => 'string', | |
373 | minLength => 3, | |
374 | maxLength => 64, | |
375 | }, | |
376 | }, | |
377 | }, | |
378 | returns => { | |
379 | type => "object", | |
380 | properties => { | |
23c3e8a1 DM |
381 | %$email_log_property_desc, |
382 | logs => { | |
383 | type => 'array', | |
384 | items => { type => "string" }, | |
385 | } | |
ef59543c DM |
386 | }, |
387 | }, | |
388 | code => sub { | |
389 | my ($param) = @_; | |
390 | ||
391 | my $restenv = PMG::RESTEnvironment->get(); | |
392 | ||
be78a520 | 393 | my $args = ['-v']; |
ef59543c DM |
394 | |
395 | my $start = $param->{starttime} // (time - 86400); | |
396 | my $end = $param->{endtime} // ($start + 86400); | |
397 | ||
398 | push @$args, '-s', $start; | |
399 | push @$args, '-e', $end; | |
400 | ||
401 | my $list = $run_pmg_log_tracker->($args, $param->{id}); | |
402 | ||
403 | my $res; | |
404 | foreach my $e (@$list) { | |
405 | $res = $e if $e->{id} eq $param->{id}; | |
406 | } | |
407 | ||
408 | die "entry '$param->{id}' not found\n" if !defined($res); | |
409 | ||
410 | return $res; | |
411 | }}); | |
412 | ||
202c952a | 413 | 1; |