README*, config: Removed the version number from these files.
[yaala.git] / lib / Yaala / Parser / Sendmail.pm
1 package Yaala::Parser;
2
3 =head1 NAME
4
5 Yaala::Parser::Sendmail
6
7 =head1 DESCRIPTION
8
9 Parser for sendmail log.
10
11 Note:
12
13 Each message in sendmail log forms several records (lines):
14 one record with 'from=' field, and one or several with 'to=' field.
15
16 Parser joins each 'from'-part with 'to'-part by message log id.
17 However, if there're several recipients, result will be several records for the same message:
18 one per recipient.
19 When message is first time countd, datafield 'uniq' is set to 1.
20 This is usefull to calculate total count/traffic or traffic by type.
21 But if you count total by recipients, using this key (as WHERE uniq=='1')
22 will make yaala ignore all recipients of a message, but the first.
23
24 Grand total (extra) is counted unique and only with stat=/Sent.*/
25
26 =head1 CONFIG OPTIONS
27
28  sendmail_aliases     - aliases file used to resolve adresses
29  sendmail_localdomain - local domain to remove from adresses
30  sendmail_localrelay  - IP regexp to determine incoming/outgoing/local traffic, egg '192.168.1.\d+'
31
32 =head1 DATA FIELDS
33
34 =head2 Key-fields
35
36 =over 4
37
38 =item id
39
40 =item from
41
42 =item class
43
44 =item msgid
45
46 =item bodytype
47
48 =item proto
49
50 =item daemon
51
52 =item relay
53
54 =item to
55
56 =item delay
57
58 =item xdelay
59
60 =item mailer
61
62 =item pri
63
64 =item dsn
65
66 =item stat
67
68 =item rrelay
69
70 =item date
71
72 =item hour
73
74 =item uniq
75
76 =item type
77
78 =back
79
80 =head2 Aggregation-Fields
81
82 =over 4
83
84 =item size (bytes)
85
86 =item nrcpts
87
88 =item count
89
90 =back
91
92 =head2 Additional Notes
93
94 timedate is splited to I<date> and I<hour>, as usual, year is taken from
95 current date.  I<rrelay> is relay field from to-part
96
97 I<uniq> is set to 1 when message first time counted.
98
99 I<type> = "I","O","L","R" for incoming, outgoing, local and relay traffic. It
100 is determined using fields 'mailer' and 'relay'. (Thus, only applied to
101 sent/recieved messages)
102
103 =head1 TODO
104
105 =over 4
106
107 =item Properly resolve multiple aliases.
108
109 =item Split non-local multiple recipients
110
111 =back
112
113 =head1 AUTHOR
114
115 qMax E<lt>qmax-at-mediasoft.ruE<gt>
116
117 =cut
118
119 use strict;
120 use warnings;
121 use vars qw(%DATAFIELDS);
122
123 use Exporter;
124 use Yaala::Parser::WebserverTools qw(%MONTH_NUMBERS);
125 use Yaala::Data::Persistent qw#init#;
126 use Yaala::Config qw#get_config#;
127
128 @Yaala::Parser::EXPORT_OK = qw(parse extra %DATAFIELDS);
129 @Yaala::Parser::ISA = ('Exporter');
130
131 our $EXTRA = init ('$EXTRA', 'hash');
132
133 my %COUNTED = ();
134
135 if (!defined ($EXTRA->{'totalcount'}))  { $EXTRA->{'totalcount'} = {I=>0, O=>0, L=>0}; }
136 if (!defined ($EXTRA->{'totalamount'})) { $EXTRA->{'totalamount'} = {I=>0, O=>0, L=>0}; }
137 if (!defined ($EXTRA->{'start'}  ))     { $EXTRA->{'start'} = undef; }
138 if (!defined ($EXTRA->{'end'} ))        { $EXTRA->{'end'} = undef; }
139
140 %DATAFIELDS = (
141         # log message id
142         id      => 'key',
143         # 'from' part
144         from    => 'key',
145         size    => 'agg:bytes',
146         class   => 'key',
147         nrcpts  => 'agg',
148         msgid   => 'key',
149         bodytype=> 'key',
150         proto   => 'key',
151         daemon  => 'key',
152         relay   => 'key',
153         # 'to' part
154         to      => 'key',
155         delay   => 'key',
156         xdelay  => 'key',
157         mailer  => 'key',
158         pri     => 'key',
159         dsn     => 'key',
160         'stat'  => 'key',
161         rrelay  => 'key',
162         # additional
163         date    => 'key',
164         hour    => 'key',
165         uniq    => 'key',
166         type    => 'key',
167         count   => 'agg'
168 );
169
170 # This needs to be done at runtime, since Data uses Setup which relies on
171 # %DATAFIELDS to be defined  -octo
172 require Yaala::Data::Core;
173 import Yaala::Data::Core qw#store#;
174
175 my $VERSION = 'v 1.1$';
176 print STDERR $/, __FILE__, ": $VERSION" if ($::DEBUG);
177
178 our %ALIASES;
179 my $aliasfile = get_config("sendmail_aliases");
180 if( $aliasfile ) {
181     print STDERR $/, __FILE__, ": Loaded aliases from $aliasfile" if ($::DEBUG);
182     load_aliases($aliasfile);
183 }
184
185 our $localdomain = get_config("sendmail_localdomain");
186 our $localrelay = get_config("sendmail_localrelay");
187 print STDERR $/, __FILE__, ": Local relay: $localrelay" if ($::DEBUG);
188 $localrelay = qr/\[$localrelay\]/;
189
190 our %RECF = (); # all pending from-parts
191 our %RECT = (); # all pending to-parts
192
193 return (1);
194
195 sub parse
196 {
197         my $line = shift or return undef;
198         if ( $line =~ s/^(...\s*\d+ \d\d:\d\d:\d\d) [\w-]+ sm-mta.*\[\d+\]: ([a-zA-Z0-9]{14}): // ) 
199         {
200                 my $datetime = $1;
201                 my $id    = $2;
202                 if( $line =~ /^from=/ ) {
203                         my $rec = parseline($line);
204                         $rec->{'from'} = resolve_alias($rec->{'from'});
205                         $RECF{$id} = { datetime=>$datetime, %$rec };
206                         checkpair($id);
207                         }
208                 elsif( $line =~ /^to=/ ) {
209                         my $rec = parseline($line);
210                         $rec->{'to'} = resolve_alias($rec->{'to'});
211                         $RECT{$id} = { datetime=>$datetime, %$rec };
212                         checkpair($id);
213                         }
214                 else {
215                         # some heaers mangling or mail filters log lines
216                   }
217         }
218 }
219
220 sub parseline
221 {
222         my $line = shift;
223         my %rec=();
224         foreach (split(/,\s+/,$line)) {
225                 if( m/(.*?)=(.*)/ ) {
226                         $rec{$1}=$2 if exists $DATAFIELDS{$1};
227                         }
228                 }
229         return \%rec;
230 }
231
232 sub checkpair
233 {
234         my $id = shift;
235         return unless ( $RECF{$id} and $RECT{$id} );
236         # rename relay in TO-part into rrelay
237         $RECT{$id}->{'rrelay'} = $RECT{$id}->{'relay'} if $RECT{$id}->{'relay'};
238         delete $RECT{$id}->{'relay'};
239
240         my %rec = ( %{$RECF{$id}}, %{$RECT{$id}} );
241
242         #print STDERR "\nRECT K: ",join("; ", keys   %{$RECT{$id}});
243         #print STDERR "\nRECT V: ",join("; ", values %{$RECT{$id}});
244         #print STDERR "\nRECF K: ",join("; ", keys   %{$RECF{$id}});
245         #print STDERR "\nRECF V: ",join("; ", values %{$RECF{$id}});
246         
247         $rec{'datetime'} =~ /(\w\w\w)\s*(\d+) (\d\d):\d\d:\d\d/;
248         my ($month,$day,$hour) = ($1,$2,$3);
249         $month = $MONTH_NUMBERS{$month};
250         my $year = [localtime(time)]->[5]+1900; # current year
251         my $date = sprintf("%04u-%02u-%02u", $year, $month, $day);
252
253         my %combined = %rec;
254         $combined{'date'}=$date;
255         $combined{'hour'}=$hour;
256         $combined{'count'}=1;
257         $combined{'uniq'} = (exists($COUNTED{$id}) ? 0 : 1);
258         $combined{'to'} =~ s/\</\&lt;/g;
259         $combined{'to'} =~ s/\>/\&gt;/g;
260         $combined{'from'} =~ s/\</\&lt;/g;
261         $combined{'from'} =~ s/\>/\&gt;/g;
262         $combined{'stat'} =~ s/^((\w+)(\s+\w+)*).*$/$1/;
263
264         my $type="UNDEF";
265         if( $localrelay and $combined{'relay'} and $combined{'mailer'}) {
266             #
267             # L: l/l O: l/-
268             # I: -/l R: -/-
269             #
270             $type =
271                 ( $combined{'relay'} =~ $localrelay ) ? 
272                     ( ( $combined{'mailer'} eq 'local' ) ? 'L' : 'O' ) :
273                     ( ( $combined{'mailer'} eq 'local' ) ? 'I' : 'R' ) ;
274
275             }
276
277         $combined{'type'} = $type;
278         
279         unless( $COUNTED{$id} ) {
280                 $COUNTED{$id} = 1;
281                 $EXTRA->{'totalcount'}->{$type}++;
282                 $EXTRA->{'totalamount'}->{$type}+=$combined{'size'};
283         }
284
285         if( not defined $EXTRA->{'start'} or $month < $EXTRA->{'start'}->{'m'} or $day < $EXTRA->{'start'}->{'d'} ) {
286                 $EXTRA->{'start'}->{'m'} = $month;
287                 $EXTRA->{'start'}->{'d'} = $day;
288                 }
289         if( not defined $EXTRA->{'end'} or $month > $EXTRA->{'end'}->{'m'} or $day > $EXTRA->{'end'}->{'d'} ) {
290                 $EXTRA->{'end'}->{'m'} = $month;
291                 $EXTRA->{'end'}->{'d'} = $day;
292                 }
293         
294         #print STDERR "\nParsed $id: ",join(";",map("$_=".$combined{$_}, sort keys %combined));
295         store (\%combined);
296         
297         delete $RECF{$id};
298         delete $RECT{$id};
299
300 }
301
302 sub load_aliases
303 {
304         my $file = shift;
305         if( open(F,"<$file") ) {
306                 while(<F>) {
307                         chomp();
308                         next unless( /^([^ ]*)\s*:\s*([^, ]*)\s*$/ );
309                         $ALIASES{lc($1)}=lc($2);
310                 }
311                 close(F);
312         }
313 }
314
315 sub resolve_alias
316 {
317         my ($alias) = @_;
318         my $addr = lc($alias);
319         $addr =~ s/[<>]//g;
320         $addr =~ s/\@$localdomain// if( $localdomain );
321         if( $ALIASES{$addr} ) {
322                 $addr = $ALIASES{$addr};
323                 }
324         return $addr;
325 }
326
327 sub extra
328 {
329         $::EXTRA->{'0. Begin/End date'} = sprintf "%02d/%02d - %02d/%02d",
330                 $EXTRA->{'start'}->{'d'},
331                 $EXTRA->{'start'}->{'m'},
332                 $EXTRA->{'end'}->{'d'},
333                 $EXTRA->{'end'}->{'m'};
334         
335         if ($EXTRA->{'totalcount'}->{'I'})
336         {
337                 $::EXTRA->{'1. Incoming mail'} = sprintf ("%5d / %12d Bytes",
338                         $EXTRA->{'totalcount'}->{'I'}, $EXTRA->{'totalamount'}->{'I'});
339         }
340         if ($EXTRA->{'totalcount'}->{'O'})
341         {
342                 $::EXTRA->{'2. Outgoing mail'} = sprintf ("%5d / %12d Bytes",
343                         $EXTRA->{'totalcount'}->{'O'}, $EXTRA->{'totalamount'}->{'O'});
344                 }
345         if ($EXTRA->{'totalcount'}->{'I'})
346         {
347                 $::EXTRA->{'3. Local mail'} = sprintf ("%5d / %12d Bytes",
348                         $EXTRA->{'totalcount'}->{'L'}, $EXTRA->{'totalamount'}->{'L'});
349                 }
350         if ($EXTRA->{'totalcount'}->{'R'})
351         {
352                 $::EXTRA->{'4. Relayed mail'} = sprintf ("%5d / %12d Bytes",
353                         $EXTRA->{'totalcount'}->{'R'}, $EXTRA->{'totalamount'}->{'R'});
354         }
355         if ($EXTRA->{'totalcount'}->{'UNDEF'})
356         {
357                 $::EXTRA->{'5. Unknown mail'} = sprintf ("%5d / %12d Bytes",
358                         $EXTRA->{'totalcount'}->{'UNDEF'}, $EXTRA->{'totalamount'}->{'UNDEF'});
359         }
360 }