]>
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 DM |
8 | use Data::Dumper; |
9 | ||
10 | use PVE::Tools; | |
11 | use PVE::SafeSyslog; | |
12 | use PVE::RESTHandler; | |
13 | use PVE::JSONSchema qw(get_standard_option); | |
14 | ||
15 | use PMG::RESTEnvironment; | |
16 | ||
17 | use base qw(PVE::RESTHandler); | |
18 | ||
19 | my $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 | ||
30 | my $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 |
199 | my $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 | 407 | 1; |