File Coverage

blib/lib/Mail/Exim/MainLogParser.pm
Criterion Covered Total %
statement 82 90 91.1
branch 39 48 81.2
condition 20 40 50.0
subroutine 12 14 85.7
pod 5 5 100.0
total 158 197 80.2


line stmt bran cond sub pod time code
1             package Mail::Exim::MainLogParser;
2 2     2   138047 use strict;
  2         13  
  2         61  
3 2     2   15 use warnings;
  2         4  
  2         66  
4              
5             BEGIN {
6 2     2   11 use Exporter ();
  2         3  
  2         54  
7 2     2   13 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  2         2  
  2         257  
8 2     2   8 $VERSION = '0.02';
9 2         50 @ISA = qw(Exporter);
10 2         7 @EXPORT = qw();
11 2         5 @EXPORT_OK = qw(&EximMainLoglineParse EximMainLoglineCompose);
12 2         57 %EXPORT_TAGS = ();
13             }
14              
15             BEGIN {
16 2     2   12 use vars qw(%EXIM_FLAGS %EXIM_FIELD_IDENFIERS);
  2         13  
  2         333  
17              
18             # As of 2017-06-08
19             # Source: http://www.exim.org/exim-html-current/doc/html/spec_html/ch-log_files.html
20 2     2   16 %EXIM_FLAGS = (
21             '<=' => 'message arrival',
22             '(=' => 'message fakereject',
23             '=>' => 'normal message delivery',
24             '->' => 'additional address in same delivery',
25             '>>' => 'cutthrough message delivery',
26             '*>' => 'delivery suppressed by -N',
27             '**' => 'delivery failed; address bounced',
28             '==' => 'delivery deferred; temporary problem',
29             );
30 2         2585 %EXIM_FIELD_IDENFIERS = (
31             A => 'authenticator name (and optional id and sender)',
32             C => ('SMTP confirmation on delivery'. '; '.
33             'command list for "no mail in SMTP session”'),
34             CV => 'certificate verification status',
35             D => 'duration of "no mail in SMTP session”',
36             DN => 'distinguished name from peer certificate',
37             DS => 'DNSSEC secured lookups',
38             DT => 'on => lines: time taken for a delivery',
39             F => 'sender address (on delivery lines)',
40             H => 'host name and IP address',
41             I => 'local interface used',
42             K => 'CHUNKING extension used',
43             id => 'message id for incoming message',
44             P => ('on <= lines: protocol used'. '; '.
45             'on => and ** lines: return path'),
46             PRDR => 'PRDR extension used',
47             PRX => 'on <= and => lines: proxy address',
48             Q => 'alternate queue name',
49             QT => ('on => lines: time spent on queue so far'. '; '.
50             'on "Completed” lines: time spent on queue'),
51             R => ('on <= lines: reference for local bounce'. '; '.
52             'on => >> ** and == lines: router name'),
53             S => 'size of message in bytes',
54             SNI => 'server name indication from TLS client hello',
55             ST => 'shadow transport name',
56             T => ('on <= lines: message subject (topic)'. '; '.
57             'on => ** and == lines: transport name'),
58             U => 'local user or RFC 1413 identity',
59             X => 'TLS cipher suite'
60             );
61             }
62              
63              
64             sub new
65             {
66 2     2 1 172 my ($class, %parameters) = @_;
67              
68 2   33     17 my $self = bless ({}, ref ($class) || $class);
69              
70 2         8 return $self;
71             }
72              
73             sub _exim_log_main__parse($) {
74 15   50 15   62 my $line = shift || return undef;
75 15 50       104 if ($line !~ m/^\d{4}\-\d{2}-\d{2}\s\d{2}\:\d{2}\:\d{2}( [\-\+]\d{4})?/o) {
76 0         0 warn __PACKAGE__,": Exim log line not in expected format.";
77 0         0 return undef;
78             }
79             # Split the line by spaces, and examine each element
80 15         152 my @line = split(/\s/,$line);
81             # To pass this simple filter the string must have a minimum of three elements
82             #
83 15 50       65 return undef unless scalar @line >= 3;
84 15         73 my $l = {
85             'date' => shift @line,
86             'time' => shift @line
87             };
88             # detect if a time zone is provided # Exim: log_timezone=true
89             # 2003-04-25 11:17:07 +0100 Start queue run: pid=12762
90 15 100       74 if ($line[0] =~ /\+\d{4}/) {
91 1         5 $l->{'timezone'} = shift @line;
92             }
93              
94             # detect is the Exim process Id is provided # Exim: log_selector = +pid
95             # The parser should not be modifying data, only understanding and separating it
96             # For this reason: 'pid': "[]" , as given in the log line
97 15 100       55 if ($line[0] =~ /\[\d+\]/) {
98 4         15 $l->{'pid'} = shift @line;
99             }
100              
101             # Exim ID Format: 1dIyz2-0002mc-5x
102 15 100       67 if ($line[0] =~ /[a-zA-Z0-9]{6}\-[a-zA-Z0-9]{6}\-[a-zA-Z0-9]{2}/) {
103 13         95 $l->{'eximid'} = shift @line;
104             } else {
105             # $l->{'eximid'} = undef;
106             }
107              
108             # Exim log line flag
109 15 100       83 if ($line[0] =~ /\<\=|\(\=|\=\>|\-\>|\>\>|\*\>|\*\*|\=\=/) {
110 7         21 $l->{'flag'} = shift @line;
111             } else {
112             # mail rejected or completed
113             # $l->{'flag'} = undef;
114             }
115              
116 15 50       38 return $l if ! scalar @line >= 1;
117              
118             # If the flag is undefined or delivery failure, then detect either the message or the field identifiers
119             # 2020-06-07 18:10:24 1ji4Qp-0002jY-UD Completed
120             # 2020-06-07 18:12:18 1ji4Sf-0002jl-Vk H=ugso.tenet.odessa.ua (ugso.odessa.ua) [195.138.65.238] F= rejected after DATA: Your message scored 17.4 SpamAssassin point. Report follows:
121 15 100 66     88 if ( ((!exists $l->{'flag'}) || (!defined $l->{'flag'})) || ($l->{'flag'} eq "**") ) {
      100        
122              
123 9 100 33     54 if ( ($line[0] !~ /^[A-Zid]{1,4}\=.*/) || (($line[0] =~ /^([A-Zid]{1,4})\=.*/) && (!exists $EXIM_FIELD_IDENFIERS{$1})) ) {
      66        
124             # If the element does not start with a known field identifier, we assume the whole line is a message
125 7         17 while (scalar @line >= 1) {
126 25 100       52 $l->{'message'} .= (" ") if defined $l->{'message'};
127 25         61 $l->{'message'} .= shift @line;
128             }
129             }
130              
131             # If the flag is defined and not a delivery failure, then we expect a mail destination (e.g. email, system pipe, system file, etc)
132             } else {
133              
134             # Exim Address could be email address, pipe, file, and a string combination of several of these elements
135             # 2020-06-07 23:44:57 1ji9ea-0003oh-Tt => :blackhole: R=pipe_to_useraddress
136             # 2020-06-07 21:49:23 1ji7qj-0003Ud-Ss => |/usr/bin/listmgr-queue listmgr R=pipe_to_listmgr T=address_pipe
137             # Skip email detection if element is AAAA=somevalue or id=somevalue, a simple field identifier matcher (where A is alpha)
138             # For deferred deliveries (i.e. == flag) email could be: some-identified-realperson=realdomain@some-domain
139 6   66     27 while ( (scalar @line >= 1)
      33        
140             && ( ($line[0] !~ /^[A-Zid]{1,4}\=.*/)
141             || (($line[0] =~ /^([A-Zid]{1,4})\=.*/) && (!exists $EXIM_FIELD_IDENFIERS{$1}))
142             )
143             ) {
144             # The element is appended to address until matching a known field identifier
145 6 50       13 $l->{'address'} .= (" ") if defined $l->{'address'};
146 6         79 $l->{'address'} .= shift @line;
147             }
148             }
149              
150 15 100       48 return $l if ! scalar @line >= 1;
151              
152             # Exim field identifiers and identifier messages
153 8         24 $l->{'args'} = [];
154 8         19 while (scalar @line >= 1) {
155             # Matching anything that looks like a field identifier, rather than looking each up in the $EXIM_FIELD_IDENFIERS hash
156 30 50 33     168 if ( ($line[0] =~ /^([A-Zid]{1,4})\=(.*)/) && (exists $EXIM_FIELD_IDENFIERS{$1}) ) {
157 30         68 my $this_arg = $1;
158 30         60 my $this_val = $2;
159 30         48 shift @line;
160 30   66     144 while ( (scalar @line >= 1) && (($line[0] !~ /^([A-Zid]{1,4})\=(.*)/) || (!exists $EXIM_FIELD_IDENFIERS{$1})) ) {
      100        
161 44         186 $this_val .= (" " . shift @line);
162             }
163 30         55 push(@{$l->{'args'}},{$this_arg => $this_val});
  30         122  
164             } else {
165             # If the field identifier is not detected, fall back to message
166             # This should only happen if the text in the element does not match any field identifiers
167 0         0 $l->{'message'} = shift @line;
168 0   0     0 while ( (scalar @line >= 1) && (($line[0] !~ /^([A-Zid]{1,4})\=(.*)/) || (!exists $EXIM_FIELD_IDENFIERS{$1})) ) {
      0        
169 0         0 $l->{'message'} .= (" " . shift @line);
170             }
171             }
172             }
173              
174 8 50       16 if (scalar @line >= 1) {
175 0         0 warn ("Error Parsing Line: $line\n"."Unparsed log line data: ".join("; ",@line)."\n");
176             }
177              
178 8         23 return $l;
179             }
180              
181             sub _exim_log_main__compose ($) {
182 15   50 15   34 my $parsed = shift || return undef;
183 15 50       34 return undef unless ref $parsed eq "HASH";
184 15         27 my @s_args;
185 15         22 foreach my $arg (@{$parsed->{'args'}}) {
  15         40  
186 30         89 push(@s_args, map{qq{$_=$arg->{$_}}} keys %$arg)
  30         120  
187             }
188 15         26 my @s_line;
189 15 50       37 push(@s_line,$parsed->{'date'}) if exists $parsed->{'date'};
190 15 50       34 push(@s_line,$parsed->{'time'}) if exists $parsed->{'time'};
191 15 100       30 push(@s_line,$parsed->{'timezone'}) if exists $parsed->{'timezone'};
192 15 100       31 push(@s_line,$parsed->{'pid'}) if exists $parsed->{'pid'};
193 15 100       29 push(@s_line,$parsed->{'eximid'}) if exists $parsed->{'eximid'};
194 15 100       30 push(@s_line,$parsed->{'flag'}) if exists $parsed->{'flag'};
195 15 100       28 push(@s_line,$parsed->{'address'}) if exists $parsed->{'address'};
196 15 100       51 push(@s_line, join(" ", @s_args)) if @s_args >= 1;
197 15 100       32 push(@s_line,$parsed->{'message'}) if exists $parsed->{'message'};
198 15         61 return(join(" ", @s_line));
199             }
200              
201             sub EximMainLoglineParse($) {
202 0     0 1 0 return _exim_log_main__parse($_[0]);
203             }
204              
205             sub EximMainLoglineCompose($) {
206 0     0 1 0 return _exim_log_main__compose($_[0]);
207             }
208              
209             sub parse($) {
210 15     15 1 8547 my $self = shift;
211 15         63 return _exim_log_main__parse($_[0]);
212             }
213              
214             sub compose($) {
215 15     15 1 405 my $self = shift;
216 15         30 return _exim_log_main__compose($_[0]);
217             }
218              
219             1;
220             __END__