File Coverage

blib/lib/TheBat/Read_TBB.pm
Criterion Covered Total %
statement 121 145 83.4
branch 27 42 64.2
condition 4 6 66.6
subroutine 7 8 87.5
pod 2 3 66.6
total 161 204 78.9


line stmt bran cond sub pod time code
1             package TheBat::Read_TBB;
2              
3 1     1   32942 use 5.00001;
  1         4  
4 1     1   6 use strict;
  1         2  
  1         27  
5 1     1   5 use warnings;
  1         6  
  1         38  
6 1     1   904 use bytes;
  1         10  
  1         5  
7 1     1   26 use Digest::MD5;
  1         3  
  1         1877  
8              
9             require Exporter;
10              
11             =head1 NAME
12              
13             TheBat::Read_TBB - Read individual email messages out of TheBat! .tbb messages files
14              
15             =head1 SYNOPSIS
16              
17             use TheBat::Read_TBB;
18             while(&Read_TBB("messages.tbb",\%ref) {
19             print "$ref{'sender'}\n";
20             }
21              
22             =head1 DESCRIPTION
23              
24             Reads the TheBat! binary messages flags (status codes like "deleted" or "replied" or "flagged" etc)
25             as well as the email headers and body, and returns md5 checksums of some parts as well as flags
26             and headers etc.
27              
28             Call repeatedly in a loop; returns 0 when there's no more to read.
29              
30             =head2 EXPORT
31              
32             Read_TBB
33              
34             =head2 EXAMPLE
35              
36             use TheBat::Read_TBB;
37             my %ref;
38             while(&Read_TBB("t/messages.tbb",\%ref)) {
39             foreach my $k (keys %ref) {
40             print "$k:\t" . $ref{$k} . "\n";
41             }
42             }
43              
44             =head1 SEE ALSO
45              
46             http://www.felix-schwarz.name/files/thebat/tbb.txt
47              
48              
49             =head1 AUTHOR
50              
51             Chris Drake, Ecdrake@cpan.orgE
52              
53             =head1 COPYRIGHT AND LICENSE
54              
55             Copyright (C) 2013 by Chris Drake
56              
57             This library is free software; you can redistribute it and/or modify
58             it under the same terms as Perl itself, either Perl version 5.10.1 or,
59             at your option, any later version of Perl 5 you may have available.
60              
61              
62             =cut
63              
64              
65              
66             our @ISA = qw(Exporter);
67              
68             # Items to export into callers namespace by default. Note: do not export
69             # names by default without a very good reason. Use EXPORT_OK instead.
70             # Do not simply export all your public functions/methods/constants.
71              
72             # This allows declaration use TheBat::Read_TBB ':all';
73             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
74             # will save memory.
75             our %EXPORT_TAGS = ( 'all' => [ qw(
76            
77             ) ] );
78              
79             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
80              
81             our @EXPORT = qw(
82             Read_TBB
83             );
84              
85             our $VERSION = '0.01';
86              
87              
88              
89             # Call repeatedly; returns 1 email at a time
90             sub Read_TBB {
91 2     2 0 459 my($tbb,$ref)=@_; # Supply input .TBB filename, and a ref to keep status info
92              
93 2         3 my($readbuf)=536870911; # How big shall our message-sysread chunks be?
94 2         4 my $flagcodes='DoaPAxFft.c................m....';
95 2 100       3 my($buf,$bytes_read)=(undef,0); my %persist=qw(tbb 1 more 1 total 1 msgs 1 deld 1 msgno 1 seek 1); foreach my $k(keys %{$ref}){$ref->{$k}=undef unless($persist{$k})}
  2         13  
  2         3  
  2         10  
  37         94  
96 2 100       9 if(!$ref->{'tbb'}) { # Not opened yet
97 1 50       30 if(open(TBB,'<',$tbb)) {
98 1         3 $ref->{'tbb'}=$tbb;
99 1         4 binmode(TBB);
100 1         11 $bytes_read=sysread(TBB,$buf,6); # Get magic number and file header size
101 1         3 $ref->{'seek'}=$bytes_read;
102 1         5 ($ref->{'file_magic'},$ref->{'head_size'})=unpack("VS",$buf);
103 1         3 $ref->{'head_size'}-=6; # remove the size of the header(magic+size) itself from the header
104              
105 1 50       5 if($ref->{'file_magic'} != 427361824) { # if(unpack('H*',substr($buf,0,6)) ne '20067919080c');
106 0         0 &write_log("Possible input file corruption: magic number mismatch: " . $ref->{'file_magic'} . " != 427361824 " . unpack('H*',$buf));
107 0         0 close(TBB);
108 0         0 return 0;
109             }
110              
111 1         8 $bytes_read=sysread(TBB,$buf,$ref->{'head_size'}); # Get the TBB file header
112 1         2 $ref->{'seek'}+=$bytes_read;
113 1 50       4 &write_log("Possible input file corruption: $bytes_read != ". $ref->{'head_size'} ." - too short") if($bytes_read != $ref->{'head_size'});
114              
115 1         14 $ref->{'more'}=1;
116 1         3 $ref->{'total'}=0;
117 1         1 $ref->{'msgs'}=0;
118 1         4 $ref->{'deld'}=0;
119 1         2 $ref->{'msgno'}=0;
120              
121             } else {
122 0         0 &write_log("Skipping: " . $ref->{'tbb'} . " - $!");
123 0         0 return 0;
124             }
125             } else {
126 1 50       4 goto NOMORE unless($ref->{'more'});
127             }
128              
129 2         9 $bytes_read=sysread(TBB,$buf,6); # Get the message header's magic and size
130 2         3 $ref->{'seek'}+=$bytes_read;
131 2 100       7 if($bytes_read<6) {
132 1         8 NOMORE:
133             close(TBB);
134             # &write_log("End of " . $ref->{'tbb'} . " - Total messages:".$ref->{'total'} ." (." . $ref->{'msgs'}." messages, ".$ref->{'deld'}." deleted): run=".$ref->{'grandtot'});# if($switch{'debug'});
135 1         2 undef $ref; # works
136 1         4 return 0;
137             }
138              
139              
140 1         3 my($msg_magic,$msghead_size)=unpack("VS",$buf); # V is a little-endian 32bit int, S is an unsigned little-ending 16 bit short)
141 1         2 $msghead_size-=6; # remove the size of the header(magic+size) itself from the header
142 1 50       4 if($msg_magic != 426772769) {
143 0         0 die "Possible input file corruption: message magic number mismatch: $msg_magic != 426772769 / 21 09 70 19 != " . unpack('H*',$buf);
144             }
145              
146 1 50       4 $msghead_size=0 if($msghead_size<0);
147             # &write_log($ref->{'tbb'} . " message header size is $msghead_size"); # debug
148 1 50       8 &write_log("Possible input file corruption: message header size unusual") if($msghead_size != 42);
149            
150 1         3 $ref->{'tell'}=$ref->{'seek'}; # remember where this message started
151 1         4 $bytes_read=sysread(TBB,$buf,$msghead_size); # Get the tbb message header
152 1         2 $ref->{'seek'}+=$bytes_read;
153 1 50       4 if($bytes_read != $msghead_size) {
154 0         0 &write_log("Possible input file corruption: message header too short");
155 0         0 goto NOMORE;
156             }
157              
158 1         2 $ref->{'total'}++; $ref->{'grandtot'}++;
  1         2  
159 1         2 $ref->{'msgno'}++;
160            
161             #( 0 ,3441210366,1038086103,1830,0 ,146 ,0 ,0 ,1 ,78594 ,0 ,0)
162 1         8 my( $z1,$unknown ,$time ,$id ,$z2,$status,$z4,$col,$pri,$msg_size,$msg_sizeB,$z5)=unpack(
163             'v V V v v b32 V V V V V V',$buf);
164 1 100       2 my $flags='';for(my $i=0; $i{"flag_$f"}++;} # DoaPAxFft.c................m....
  1 100       6  
  32         35  
  32         53  
  32         61  
  32         67  
  32         41  
  32         80  
165 1         18 $ref->{'flags'}=$flags;
166              
167              
168 1         2 my($read)=0;
169 1         2 my($head)=1;
170 1         2 my($towrite)=1;
171 1         2 my($full)='';
172 1         3 $ref->{'sender'}='<>';
173            
174 1 50       4 if(substr($status,0,1)) {
175 0         0 $ref->{'deld'}++;
176             } else {
177 1         2 $ref->{'msgs'}++;
178             }
179              
180 1         18 $ref->{'md5'} = Digest::MD5->new;
181 1         5 $ref->{'md5h'} = Digest::MD5->new;
182 1         4 $ref->{'md5b'} = Digest::MD5->new;
183 1         2 my %hdr; my($more)=1;
  1         2  
184 1         2 $ref->{'size'} = 0;
185              
186 1   66     12 while(($more)&&($read<$msg_size)&&($msg_size>0)) { # Loop over sensible sized reads of the message
      66        
187 1 50       2 if(($msg_size-$read)<$readbuf) {
188 1         6 $bytes_read=sysread(TBB,$buf,($msg_size-$read)); # Get the rest of the message
189 1         3 $ref->{'seek'}+=$bytes_read;
190 1         2 $ref->{'size'}+=$bytes_read;
191 1 50       3 if($bytes_read != ($msg_size-$read)) {
192 0         0 $more=0; $ref->{'more'}=0;
  0         0  
193 0         0 &write_log("Possible input file corruption: file ends before message $ref->{'total'} does");
194             }
195 1         3 $read=$msg_size; # Got the lot now
196             } else {
197 0         0 $bytes_read=sysread(TBB,$buf,$readbuf); # Get 32k of the message
198 0         0 $ref->{'seek'}+=$bytes_read;
199 0         0 $ref->{'size'}+=$bytes_read;
200 0 0       0 if($bytes_read != $readbuf) {
201 0         0 $more=0; $ref->{'more'}=0;
  0         0  
202 0         0 &write_log("Possible input file corruption: file ends before message $ref->{'total'} does");
203             }
204 0         0 $read+=$readbuf;
205             }
206              
207              
208 1 50       3 if($head) { # We're in the header
209 1         11 $head=0;
210 1         18 ($ref->{'header'},$buf)=split(/(?:\r\r|\n\n|\r\n\r\n|\n\r\n\r)/,$buf,2);
211 1         15 $ref->{'header'}=~s/\r\n/\n/gsm;
212 1         5 chop($ref->{'header'}) while(substr($ref->{'header'},-1) eq "\r");
213 1         5 &ParseHead(\$ref->{'header'},\%hdr);
214              
215 1         14 ($ref->{'sender'})=($ref->{'header'}=~/[\s<]([^\s<>]+\@[a-z0-9\.\-]+)/i);
216              
217 1 50       5 if(substr($ref->{'header'},0,5) ne 'From ') {
218 1         5 $ref->{'header'}=~s/^From /X-From:/gsm; # Don't allow another "^From " in the headers
219 1         5 $ref->{'header'}="From $ref->{'sender'}\n" . $ref->{'header'};
220             }
221              
222 1         15 $ref->{'md5h'}->add($ref->{'header'});
223             $ref->{'md5b'}->add("From:" . $hdr{'from'} .
224             "\tTo:" . $hdr{'to'} .
225             "\tDate:" . $hdr{'date'} .
226             "\tId:" . $hdr{'message-id'} .
227 1         9 "\tSubject:" . $hdr{'subject'} );
228              
229 1         4 foreach my $k (keys %hdr){$ref->{"h_$k"}=$hdr{$k};}
  10         25  
230             }
231              
232 1         13 $ref->{'md5'}->add($buf);
233              
234             #$full.=$buf if($switch{'uniq'});
235              
236             } # loop over 1 email
237              
238 1         3 foreach my $h (qw(md5 md5h md5b)) {
239 3         18 $ref->{"b$h"}=$ref->{$h}->digest;
240 3         12 $ref->{$h.'hex'}=unpack("H*",$ref->{"b$h"}); # $ref->{'bmd5'} / $ref->{'md5hex'} / etc
241             }
242              
243 1         7 return 1;
244             } # bat_read
245              
246              
247              
248             #######################################################################
249              
250             =head2 ParseHead
251              
252             Extract fields from header
253              
254             =cut
255             #######################################################################
256              
257              
258             sub ParseHead {
259 1     1 1 2 my($phdrc,$phdr)=@_;
260 1         34 my @hdr=split(/(?:\r\n|\n\r|\r|\n)/,$$phdrc);
261 1         2 push @hdr,''; # So the loop below also does the last element properly (the $i++ bit)
262 1         2 my $i=0; while($i<($#hdr)) {
  1         4  
263             #print "h$i =$hdr[$i]\n";
264 20 100       58 if($hdr[1+$i]=~/^\s(.*)/) {
265 4         12 $hdr[$i].=" $1"; splice(@hdr,$i+1,1);
  4         13  
266             } else {
267 16 100       50 my($f,$b)=split(':',$hdr[$i],2); if(!defined $phdr->{lc($f)}){$phdr->{lc($f)}=$b}else{$phdr->{lc($f)}.=" ".$b}
  16         42  
  10         25  
  6         21  
268 16         36 $i++;
269             }
270             #print "h$i'=$hdr[$i]\n";
271             }
272 1         4 pop @hdr; # @hdr now has headers, all one-lined.
273              
274             } # ParseHead
275              
276              
277              
278              
279              
280              
281             #######################################################################
282              
283             =head2 write_log
284              
285             Outputs debugging and informational letters to /var/log file.
286              
287             =cut
288             #######################################################################
289             sub write_log {
290 0     0 1   my($message)=@_;
291 0           my($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
292 0           my($date)=sprintf("%02d/%02d %02d:%02d.%02d",$mon+1,$mday,$hour,$min,$sec);
293 0           print STDERR "$date $$ Read_TBB $message\n";
294             }
295              
296              
297              
298              
299             __END__