File Coverage

blib/lib/Mail/Log/Trace/Postfix.pm
Criterion Covered Total %
statement 137 137 100.0
branch 57 58 98.2
condition 30 30 100.0
subroutine 19 19 100.0
pod 2 2 100.0
total 245 246 99.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             Mail::Log::Trace::Postfix - Trace an email through Postfix logs.
6              
7             =head1 SYNOPSIS
8              
9             use Mail::Log::Trace::Postfix;
10            
11             my $tracer = Mail::Log::Trace::Postfix->new({log_file => 'path/to/log'});
12             $tracer->set_message_id('message_id');
13             $tracer->find_message();
14             my $from_address = $tracer->get_from_address();
15            
16             etc.
17              
18             =head1 DESCRIPTION
19              
20             A subclass for L that handles Postfix logs. See the
21             documentation for the root class for more. This doc will just deal with the
22             additions to the base class.
23              
24             =head1 USAGE
25              
26             An object-oriented module: See the base class for most of the meathods.
27              
28             Additions are:
29              
30             =head2 SETTERS
31              
32             =cut
33              
34             package Mail::Log::Trace::Postfix;
35             {
36 4     4   4310 use strict;
  4         10  
  4         178  
37 4     4   23 use warnings;
  4         9  
  4         146  
38 4     4   25 use Scalar::Util qw(refaddr);
  4         10  
  4         344  
39             #use Mail::Log::Parse::Postfix;
40 4     4   1058 use Mail::Log::Exceptions;
  4         13370  
  4         115  
41 4     4   23 use base qw(Mail::Log::Trace);
  4         7  
  4         8608  
42 4     4   27 use constant EMPTY_STRING => qw{};
  4         7  
  4         331  
43              
44             BEGIN {
45 4     4   21 use Exporter ();
  4         7  
  4         95  
46 4     4   20 use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
  4         8  
  4         488  
47 4     4   4223 $VERSION = '1.0001';
48             }
49              
50             #
51             # Define class variables. Note that they are hashes...
52             #
53              
54             =head3 set_connection_id
55              
56             Sets the connection id of the message we are looking for.
57              
58             =head3 set_process_id
59              
60             Sets the process id of the message we are looking for. (Note that pids are
61             often reused, and Postfix will use several processes for a specific message.)
62              
63             =head3 set_status
64              
65             Sets the status id of the message we are looking for.
66              
67             Currently this is the B status, not just the numeric code.
68              
69             =head3 set_year
70              
71             Sets the year the logfile was written in, since Postfix doesn't log that.
72              
73             Assumes the current year if not set. (See L.)
74              
75             =head2 GETTERS
76              
77             =head3 get_connection_id
78              
79             Returns the connection id of the message we are looking for/have found.
80              
81             =head3 get_process_id
82              
83             Returns the process id of the message we are looking for/have found.
84              
85             This will be the process id of the first part of the message found, which may
86             or may not be the first entry of the message in the log.
87              
88             =head3 get_status
89              
90             Returns the status of the message we are looking for/have found.
91              
92             Currently this is the B status, not just the numeric code.
93              
94             =cut
95              
96             #
97             # Overridden methods.
98             #
99              
100             sub _requested_public_accessors {
101 23     23   158 return qw(connection_id process_id status);
102             }
103              
104             sub _requested_cleared_parameters {
105 23     23   126 return qw(connection_id process_id status);
106             }
107              
108             sub _set_as_message_info {
109 23     23   197 return qw(connection_id process_id status);
110             };
111              
112             sub _requested_special_accessors {
113 5     5   9 return ( year => sub { my ($self, $year) = @_;
114 5 50       16 return '____INVALID__VALUE____' if $year < 1970;
115 5         18 my $maillog = $self->_get_log_parser();
116 5 100       13 if (defined($maillog)) {
117 1         9 $maillog->set_year($year);
118             }
119 5         552 return $year;
120             },
121 23     23   204 );
122             };
123              
124             sub find_message {
125 22     22 1 91 my ($self, $argref) = @_;
126              
127             # Parse the arguments, and get all the message info.
128 22         141 my $msg_info = $self->_parse_args($argref, 1); # The '1' means throw an error if we don't have any info.
129              
130             # Open the log file. (Unless we've already opened it.)
131 21         128 my $maillog = $self->_get_log_parser();
132 21 100       62 unless ( defined($maillog) ) {
133 18         91 my $parser_class = $self->_get_parser_class();
134 18 100       78 $parser_class = defined($parser_class) ? $parser_class : 'Mail::Log::Parse::Postfix';
135 18         2632 eval "require $parser_class;";
136            
137 18 100       62239 if ( defined($self->get_year()) ) {
138 1         74 $maillog = eval "$parser_class->new({log_file => \$self->get_log(), year => \$self->get_year()});";
139             }
140             else {
141 17         1324 $maillog = eval "$parser_class->new({log_file => \$self->get_log(),});";
142             }
143 18         6247 $self->_set_log_parser($maillog);
144             }
145              
146             # Normally we start where we left off, but we can start at the beginning.
147 21 100       91 if ( $argref->{from_start} ) {
148 1         14 $maillog->go_to_beginning();
149             }
150              
151             # Look through the logfile one line at a time, until we've found it.
152 21         41 my $found_message = 0;
153 21   100     132 while ( (my $line_data = $maillog->next()) and !$found_message) {
154             #Check to see if this line matches.
155 6359 100       1656255 if ( _line_matches($line_data, $msg_info) ) {
156             # Save anything we've matched that is new info.
157 17         92 $self->_read_data_from_line($line_data);
158              
159             # Also save the raw info, in case it is wanted.
160 17         151 $self->_set_message_raw_info($line_data);
161              
162             # Ok, we're done.
163 17         84 $found_message = 1;
164             }
165             }
166              
167             # Return whether we found anything.
168 21         636 return $found_message;
169             }
170              
171             sub find_message_info {
172 9     9 1 468 my ($self, $argref) = @_;
173              
174             # If we can't find it, we can't find info on it.
175 9 100       31 return undef unless $self->find_message($argref);
176              
177             # Get all the message info.
178 8         45 my $msg_info = $self->_parse_args($argref, 1);
179 8         43 my $maillog = $self->_get_log_parser();
180              
181             # So we can save something in it later.
182 8         12 my $begin_log_line;
183              
184             # Read backwards until we find the start of the connection
185 8         14 my $start_found = 0;
186 8   100     102 while ( !$start_found and (my $line_data = $maillog->previous()) ) {
187             # Reset process ID's if we find earlier ones. (We trust the connection ID.)
188 83 100 100     2087 if ( defined($line_data->{id}) and $line_data->{id} eq $msg_info->{connection_id} ) {
189 23         64 $msg_info->{process_id} = $line_data->{pid};
190             }
191              
192             # The connection doesn't list the connection ID, but it's process
193             # ID will match a later line that does...
194 83 100 100     569 if ( ($line_data->{pid} eq $msg_info->{process_id}) and $line_data->{connect} ) {
195 7         13 $start_found = 1;
196              
197             # Set the info we've just found.
198 7         42 $self->_set_connect_time($line_data->{timestamp});
199            
200             # Add in new info to the 'raw info'.
201             # We'll overwrite what is already there.
202 7         46 my $temp = $self->get_all_info();
203 7         11 foreach my $key ( keys %{$temp} ) {
  7         72  
204 154         394 $line_data->{$key} = $temp->{$key};
205             }
206 7         38 $self->_set_message_raw_info($line_data);
207              
208             # Save where we are: We'll go back here later.
209 7         45 $begin_log_line = $maillog->get_line_number();
210             }
211             }
212              
213             # Read through until we get all the info.
214 8         88 my $end_found = 0;
215 8   100     53 while ( !$end_found and (my $line_data = $maillog->next()) ) {
216             #Check to see if this line matches.
217 245 100 100     4276 if ( defined($line_data->{id}) and $line_data->{id} eq $msg_info->{connection_id} ) {
218             # Save anything we've matched that is new info.
219 108         222 $self->_read_data_from_line($line_data);
220              
221             # Add in new 'raw_info'.
222             # Now we need to _merge_ what is already there...
223 108         279 my $temp = $self->get_all_info();
224 108         142 my %temp_hash;
225 108 100       283 if (defined($line_data->{to}[0])) {
226 79         89 foreach my $element ( @{$line_data->{to}}, @{$temp->{to}}) {
  79         120  
  79         196  
227 1413         2040 $temp_hash{$element} = 1;
228             }
229 79         607 $temp->{to} = [(keys %temp_hash)];
230             }
231             # The rest doesn't need to be merged; it can be overwritten.
232 108         315 foreach my $key ( keys %{$line_data} ) {
  108         690  
233 2376 100 100     8223 if ( defined($line_data->{$key}) and $key ne 'to') {
234 1286         2386 $temp->{$key} = $line_data->{$key};
235             }
236             }
237 108         476 $self->_set_message_raw_info($temp);
238              
239             # Check to see if we're done.
240 108 100       498 if ( $line_data->{text} eq 'removed' ) {
241 7         18 $end_found = 1;
242             }
243             }
244              
245             # Check for disconnect.
246 245 100 100     1516 if ( ($line_data->{pid} eq $msg_info->{process_id}) and $line_data->{disconnect} ) {
247 6         33 $self->_set_disconnect_time($line_data->{timestamp});
248             }
249             }
250              
251             # We're going to go back to where we found the beginning of the connection:
252             # It's polite and useful.
253 8         58 $maillog->go_to_line_number($begin_log_line);
254              
255             # Check to see if we found it, and throw an error if we didn't.
256 8 100       125 if ( !$start_found ) {
257 1         25 Mail::Log::Exceptions::Message::IncompleteLog->throw('Connection start predates logfile.');
258             }
259              
260             # Check to see if we found it, and throw an error if we didn't.
261 7 100       23 if ( !$end_found ) {
262 1         24 Mail::Log::Exceptions::Message::IncompleteLog->throw('Logfile ends before disconnection.');
263             }
264              
265 6         95 return 1;
266             }
267              
268             ####
269             # Private Functions.
270             ####
271              
272             #
273             # line_matches: Finds whether a line matches the given info. Function.
274             #
275             # Takes a hashref to match against (as returned from Mail::Log::Parse::Postfix)
276             # and a hashref of data (internal format, see code.) Checks to see if the
277             # two hashes match on all that exists in both. (But _only_ in both: Either can
278             # have data that the other doesn't, as long as the other has 'undef' for
279             # that key.)
280             #
281             # Arguments: Positional, the hashref from the parser, and the internal hashref.
282             #
283             # Return Value: True if they match, False if they do not.
284             #
285             sub _line_matches ($$) {
286 6359     6359   11216 my ( $line_data, $msg_info) = @_;
287              
288 6359         25153 my %line_data_map = ( from_address => 'from'
289             ,message_id => 'msgid'
290             ,relay => 'relay'
291             ,connection_id => 'id'
292             ,status => 'status'
293             );
294              
295 4     4   34 no warnings qw(uninitialized);
  4         7  
  4         2136  
296 69949 100 100     443860 my @defined_data = grep { ($_ ne 'to_address') and ($_ ne 'from_start') and defined($msg_info->{$_}) }
  6359         21173  
297 6359         8339 keys %{$msg_info};
298              
299 6359         34667 my $matched_data = grep { ($msg_info->{$_} eq ${$line_data}{$line_data_map{$_}})
  5863         7656  
  5863         35886  
300             } @defined_data;
301              
302 6359 100       8409 my $unmatched_data = grep { !defined($line_data->{$line_data_map{$_}})
  5863         29215  
303             or ($msg_info->{$_} ne $line_data->{$line_data_map{$_}})
304             } @defined_data;
305              
306             # Check to addresses
307 2810         3234 my $to_count = grep { my $tmp = $_;
  6359         12714  
308 2810         2726 grep { $_ eq $tmp } @{$line_data->{to}};
  1089         3110  
  2810         8821  
309 6359         7399 } @{$msg_info->{to_address}};
310 6359 100       12977 if ( $to_count ) {
311 4         25 $matched_data = $matched_data + $to_count;
312             }
313             else {
314 6355 100       7750 if ( defined( ${$msg_info->{to_address}}[0]) ) {
  6355         27903  
315 2806         3374 $unmatched_data++;
316             }
317             }
318              
319 6359   100     48198 return ( ($matched_data > 0) and ($unmatched_data == 0) );
320             }
321              
322             #
323             # read_data_from_line: Reads data from a parsed line. Function.
324             #
325             # Takes a hashref of data from a Mail:::Log::Parse::Postfix, and sets the values
326             # in self for all the data we capture, skiping data we already have.
327             #
328             # Arguments: Postional, the hashref from the parser.
329             #
330             # Return Value: None.
331             #
332              
333             sub _read_data_from_line {
334 125     125   210 my ($self, $line_data) = @_;
335             # Set any info we've found.
336 125         135 $self->add_to_address($_) foreach (@{$line_data->{to}});
  125         539  
337 125 100       1049 $self->set_from_address($line_data->{from}) unless defined($self->get_from_address());
338 125 100       359 $self->set_message_id($line_data->{msgid}) unless defined($self->get_message_id());
339 125 100       365 $self->set_relay($line_data->{relay}) unless defined($self->get_relay());
340 125 100       371 $self->set_status($line_data->{status}) unless defined($self->get_status());
341 125 100       363 $self->set_connection_id($line_data->{id}) unless defined($self->get_connection_id());
342 125 100       528 $self->_set_delay($line_data->{delay}) if defined($line_data->{delay});
343              
344             # Set times, if applicable.
345 125 100       337 $self->set_recieved_time($line_data->{timestamp}) if defined($line_data->{msgid});
346 125 100       463 $self->set_sent_time($line_data->{timestamp}) if defined($line_data->{to}->[0]);
347              
348 125         184 return;
349             }
350              
351             =head1 BUGS
352              
353             Tracing a message works, but is slow. The statuses should probably be smart
354             about what they take/return, so we can say 'find all rejected messages' or
355             something of the sort...
356              
357             =head1 REQUIRES
358              
359             L, L, L
360              
361             Something that can pretend it is L. (The actual class
362             B required, but it is the default. Another parser class can be set at
363             runtime. However, it is assumed to behave exactly like Mail::Log::Parse::Postfix.)
364              
365             =head1 HISTORY
366              
367             1.0.1 Dec 5, 2008 - Licence clarification.
368              
369             1.0 Nov 28, 2008.
370             - original version.
371              
372             =head1 AUTHOR
373              
374             Daniel T. Staal
375             CPAN ID: DSTAAL
376             dstaal@usa.net
377              
378             =head1 COPYRIGHT
379              
380             This program is free software; you can redistribute
381             it and/or modify it under the same terms as Perl itself.
382              
383             This copyright will expire in 30 years, or five years after the author's death,
384             whichever occurs last, at which time the code will be released to the public domain.
385              
386             =head1 SEE ALSO
387              
388             L
389              
390             =cut
391              
392             #################### main pod documentation end ###################
393              
394             }
395             1;
396             # The preceding line will help the module return a true value
397