File Coverage

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


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   1803 use strict;
  4         4  
  4         86  
37 4     4   13 use warnings;
  4         4  
  4         97  
38 4     4   15 use Scalar::Util qw(refaddr);
  4         4  
  4         183  
39             #use Mail::Log::Parse::Postfix;
40 4     4   359 use Mail::Log::Exceptions;
  4         9326  
  4         68  
41 4     4   11 use base qw(Mail::Log::Trace);
  4         4  
  4         563  
42 4     4   14 use constant EMPTY_STRING => qw{};
  4         4  
  4         217  
43              
44             BEGIN {
45 4     4   13 use Exporter ();
  4         4  
  4         65  
46 4     4   9 use vars qw($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
  4         4  
  4         181  
47 4     4   2371 $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   72 return qw(connection_id process_id status);
102             }
103              
104             sub _requested_cleared_parameters {
105 23     23   69 return qw(connection_id process_id status);
106             }
107              
108             sub _set_as_message_info {
109 23     23   96 return qw(connection_id process_id status);
110             };
111              
112             sub _requested_special_accessors {
113 5     5   3 return ( year => sub { my ($self, $year) = @_;
114 5 50       10 return '____INVALID__VALUE____' if $year < 1970;
115 5         10 my $maillog = $self->_get_log_parser();
116 5 100       7 if (defined($maillog)) {
117 1         4 $maillog->set_year($year);
118             }
119 5         182 return $year;
120             },
121 23     23   120 );
122             };
123              
124             sub find_message {
125 22     22 1 36 my ($self, $argref) = @_;
126              
127             # Parse the arguments, and get all the message info.
128 22         55 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         73 my $maillog = $self->_get_log_parser();
132 21 100       40 unless ( defined($maillog) ) {
133 18         54 my $parser_class = $self->_get_parser_class();
134 18 100       33 $parser_class = defined($parser_class) ? $parser_class : 'Mail::Log::Parse::Postfix';
135 18         1233 eval "require $parser_class;";
136            
137 18 100       24499 if ( defined($self->get_year()) ) {
138 1         54 $maillog = eval "$parser_class->new({log_file => \$self->get_log(), year => \$self->get_year()});";
139             }
140             else {
141 17         870 $maillog = eval "$parser_class->new({log_file => \$self->get_log(),});";
142             }
143 18         3649 $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       48 if ( $argref->{from_start} ) {
148 1         6 $maillog->go_to_beginning();
149             }
150              
151             # Look through the logfile one line at a time, until we've found it.
152 21         26 my $found_message = 0;
153 21   100     51 while ( (my $line_data = $maillog->next()) and !$found_message) {
154             #Check to see if this line matches.
155 6359 100       759137 if ( _line_matches($line_data, $msg_info) ) {
156             # Save anything we've matched that is new info.
157 17         37 $self->_read_data_from_line($line_data);
158              
159             # Also save the raw info, in case it is wanted.
160 17         63 $self->_set_message_raw_info($line_data);
161              
162             # Ok, we're done.
163 17         38 $found_message = 1;
164             }
165             }
166              
167             # Return whether we found anything.
168 21         364 return $found_message;
169             }
170              
171             sub find_message_info {
172 9     9 1 269 my ($self, $argref) = @_;
173              
174             # If we can't find it, we can't find info on it.
175 9 100       20 return undef unless $self->find_message($argref);
176              
177             # Get all the message info.
178 8         24 my $msg_info = $self->_parse_args($argref, 1);
179 8         23 my $maillog = $self->_get_log_parser();
180              
181             # So we can save something in it later.
182 8         11 my $begin_log_line;
183              
184             # Read backwards until we find the start of the connection
185 8         8 my $start_found = 0;
186 8   100     32 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     1042 if ( defined($line_data->{id}) and $line_data->{id} eq $msg_info->{connection_id} ) {
189 23         22 $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 66     231 if ( ($line_data->{pid} eq $msg_info->{process_id}) and $line_data->{connect} ) {
195 7         6 $start_found = 1;
196              
197             # Set the info we've just found.
198 7         33 $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         23 my $temp = $self->get_all_info();
203 7         9 foreach my $key ( keys %{$temp} ) {
  7         30  
204 154         124 $line_data->{$key} = $temp->{$key};
205             }
206 7         22 $self->_set_message_raw_info($line_data);
207              
208             # Save where we are: We'll go back here later.
209 7         19 $begin_log_line = $maillog->get_line_number();
210             }
211             }
212              
213             # Read through until we get all the info.
214 8         40 my $end_found = 0;
215 8   100     23 while ( !$end_found and (my $line_data = $maillog->next()) ) {
216             #Check to see if this line matches.
217 245 100 100     2058 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         117 $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         148 my $temp = $self->get_all_info();
224 108         69 my %temp_hash;
225 108 100       158 if (defined($line_data->{to}[0])) {
226 79         51 foreach my $element ( @{$line_data->{to}}, @{$temp->{to}}) {
  79         64  
  79         91  
227 1413         1052 $temp_hash{$element} = 1;
228             }
229 79         302 $temp->{to} = [(keys %temp_hash)];
230             }
231             # The rest doesn't need to be merged; it can be overwritten.
232 108         154 foreach my $key ( keys %{$line_data} ) {
  108         337  
233 2376 100 100     4501 if ( defined($line_data->{$key}) and $key ne 'to') {
234 1286         1061 $temp->{$key} = $line_data->{$key};
235             }
236             }
237 108         236 $self->_set_message_raw_info($temp);
238              
239             # Check to see if we're done.
240 108 100       283 if ( $line_data->{text} eq 'removed' ) {
241 7         9 $end_found = 1;
242             }
243             }
244              
245             # Check for disconnect.
246 245 100 66     656 if ( ($line_data->{pid} eq $msg_info->{process_id}) and $line_data->{disconnect} ) {
247 6         27 $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         26 $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       67 if ( !$start_found ) {
257 1         17 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       14 if ( !$end_found ) {
262 1         10 Mail::Log::Exceptions::Message::IncompleteLog->throw('Logfile ends before disconnection.');
263             }
264              
265 6         48 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   4254 my ( $line_data, $msg_info) = @_;
287              
288 6359         10163 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   17 no warnings qw(uninitialized);
  4         4  
  4         1161  
296 69949 100 100     214953 my @defined_data = grep { ($_ ne 'to_address') and ($_ ne 'from_start') and defined($msg_info->{$_}) }
297 6359         3541 keys %{$msg_info};
  6359         9421  
298              
299 6359         6501 my $matched_data = grep { ($msg_info->{$_} eq ${$line_data}{$line_data_map{$_}})
  5863         3773  
  5863         7926  
300             } @defined_data;
301              
302 6359         3920 my $unmatched_data = grep { !defined($line_data->{$line_data_map{$_}})
303 5863 100       12155 or ($msg_info->{$_} ne $line_data->{$line_data_map{$_}})
304             } @defined_data;
305              
306             # Check to addresses
307 2810         1843 my $to_count = grep { my $tmp = $_;
308 2810         1561 grep { $_ eq $tmp } @{$line_data->{to}};
  1089         1302  
  2810         2883  
309 6359         3668 } @{$msg_info->{to_address}};
  6359         5167  
310 6359 100       6311 if ( $to_count ) {
311 4         7 $matched_data = $matched_data + $to_count;
312             }
313             else {
314 6355 100       3490 if ( defined( ${$msg_info->{to_address}}[0]) ) {
  6355         8317  
315 2806         1624 $unmatched_data++;
316             }
317             }
318              
319 6359   100     21111 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   109 my ($self, $line_data) = @_;
335             # Set any info we've found.
336 125         79 $self->add_to_address($_) foreach (@{$line_data->{to}});
  125         365  
337 125 100       209 $self->set_from_address($line_data->{from}) unless defined($self->get_from_address());
338 125 100       203 $self->set_message_id($line_data->{msgid}) unless defined($self->get_message_id());
339 125 100       189 $self->set_relay($line_data->{relay}) unless defined($self->get_relay());
340 125 100       185 $self->set_status($line_data->{status}) unless defined($self->get_status());
341 125 100       187 $self->set_connection_id($line_data->{id}) unless defined($self->get_connection_id());
342 125 100       284 $self->_set_delay($line_data->{delay}) if defined($line_data->{delay});
343              
344             # Set times, if applicable.
345 125 100       201 $self->set_recieved_time($line_data->{timestamp}) if defined($line_data->{msgid});
346 125 100       265 $self->set_sent_time($line_data->{timestamp}) if defined($line_data->{to}->[0]);
347              
348 125         91 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