File Coverage

blib/lib/Convert/TNEF.pm
Criterion Covered Total %
statement 167 238 70.1
branch 65 138 47.1
condition 24 58 41.3
subroutine 20 25 80.0
pod 0 6 0.0
total 276 465 59.3


line stmt bran cond sub pod time code
1             # Convert::TNEF.pm
2             #
3             # Copyright (c) 1999 Douglas Wilson . All rights reserved.
4             # This program is free software; you can redistribute it and/or
5             # modify it under the same terms as Perl itself.
6              
7             package Convert::TNEF;
8              
9 1     1   801 use strict;
  1         2  
  1         38  
10 1     1   956 use integer;
  1         9  
  1         5  
11 1         133 use vars qw(
12             $VERSION
13             $TNEF_SIGNATURE
14             $TNEF_PURE
15             $LVL_MESSAGE
16             $LVL_ATTACHMENT
17             $errstr
18             $g_file_cnt
19             %dflts
20             %atp
21             %att
22             %att_name
23 1     1   33 );
  1         4  
24              
25 1     1   5 use Carp;
  1         1  
  1         79  
26 1     1   761 use IO::Wrap;
  1         13601  
  1         45  
27 1     1   9 use File::Spec;
  1         1  
  1         18  
28 1     1   861 use MIME::Body;
  1         979127  
  1         3330  
29              
30             $VERSION = '0.18';
31              
32             # Set some TNEF constants. Everything turned
33             # out to be in little endian order, so I just added
34             # 'reverse' everywhere that I needed to
35             # instead of reversing the hex codes.
36             $TNEF_SIGNATURE = reverse pack( 'H*', '223E9F78' );
37             $TNEF_PURE = reverse pack( 'H*', '00010000' );
38              
39             $LVL_MESSAGE = pack( 'H*', '01' );
40             $LVL_ATTACHMENT = pack( 'H*', '02' );
41              
42             %atp = (
43             Triples => pack( 'H*', '0000' ),
44             String => pack( 'H*', '0001' ),
45             Text => pack( 'H*', '0002' ),
46             Date => pack( 'H*', '0003' ),
47             Short => pack( 'H*', '0004' ),
48             Long => pack( 'H*', '0005' ),
49             Byte => pack( 'H*', '0006' ),
50             Word => pack( 'H*', '0007' ),
51             Dword => pack( 'H*', '0008' ),
52             Max => pack( 'H*', '0009' ),
53             );
54              
55             for ( keys %atp ) {
56             $atp{$_} = reverse $atp{$_};
57             }
58              
59             sub _ATT {
60 33     33   38 my ( $att, $id ) = @_;
61 33         130 return reverse($id) . $att;
62             }
63              
64             # The side comments are 'MAPI' equivalents
65             %att = (
66             Null => _ATT( pack( 'H*', '0000' ), pack( 'H4', '0000' ) ),
67             # PR_ORIGINATOR_RETURN_ADDRESS
68             From => _ATT( $atp{Triples}, pack( 'H*', '8000' ) ),
69             # PR_SUBJECT
70             Subject => _ATT( $atp{String}, pack( 'H*', '8004' ) ),
71             # PR_CLIENT_SUBMIT_TIME
72             DateSent => _ATT( $atp{Date}, pack( 'H*', '8005' ) ),
73             # PR_MESSAGE_DELIVERY_TIME
74             DateRecd => _ATT( $atp{Date}, pack( 'H*', '8006' ) ),
75             # PR_MESSAGE_FLAGS
76             MessageStatus => _ATT( $atp{Byte}, pack( 'H*', '8007' ) ),
77             # PR_MESSAGE_CLASS
78             MessageClass => _ATT( $atp{Word}, pack( 'H*', '8008' ) ),
79             # PR_MESSAGE_ID
80             MessageID => _ATT( $atp{String}, pack( 'H*', '8009' ) ),
81             # PR_PARENT_ID
82             ParentID => _ATT( $atp{String}, pack( 'H*', '800A' ) ),
83             # PR_CONVERSATION_ID
84             ConversationID => _ATT( $atp{String}, pack( 'H*', '800B' ) ),
85             Body => _ATT( $atp{Text}, pack( 'H*', '800C' ) ), # PR_BODY
86             # PR_IMPORTANCE
87             Priority => _ATT( $atp{Short}, pack( 'H*', '800D' ) ),
88             # PR_ATTACH_DATA_xxx
89             AttachData => _ATT( $atp{Byte}, pack( 'H*', '800F' ) ),
90             # PR_ATTACH_FILENAME
91             AttachTitle => _ATT( $atp{String}, pack( 'H*', '8010' ) ),
92             # PR_ATTACH_RENDERING
93             AttachMetaFile => _ATT( $atp{Byte}, pack( 'H*', '8011' ) ),
94             # PR_CREATION_TIME
95             AttachCreateDate => _ATT( $atp{Date}, pack( 'H*', '8012' ) ),
96             # PR_LAST_MODIFICATION_TIME
97             AttachModifyDate => _ATT( $atp{Date}, pack( 'H*', '8013' ) ),
98             # PR_LAST_MODIFICATION_TIME
99             DateModified => _ATT( $atp{Date}, pack( 'H*', '8020' ) ),
100             #PR_ATTACH_TRANSPORT_NAME
101             AttachTransportFilename => _ATT( $atp{Byte}, pack( 'H*', '9001' ) ),
102             AttachRenddata => _ATT( $atp{Byte}, pack( 'H*', '9002' ) ),
103             MAPIProps => _ATT( $atp{Byte}, pack( 'H*', '9003' ) ),
104             # PR_MESSAGE_RECIPIENTS
105             RecipTable => _ATT( $atp{Byte}, pack( 'H*', '9004' ) ),
106             Attachment => _ATT( $atp{Byte}, pack( 'H*', '9005' ) ),
107             TnefVersion => _ATT( $atp{Dword}, pack( 'H*', '9006' ) ),
108             OemCodepage => _ATT( $atp{Byte}, pack( 'H*', '9007' ) ),
109             # PR_ORIG_MESSAGE_CLASS
110             OriginalMessageClass => _ATT( $atp{Word}, pack( 'H*', '0006' ) ),
111              
112             # PR_RCVD_REPRESENTING_xxx or PR_SENT_REPRESENTING_xxx
113             Owner => _ATT( $atp{Byte}, pack( 'H*', '0000' ) ),
114             # PR_SENT_REPRESENTING_xxx
115             SentFor => _ATT( $atp{Byte}, pack( 'H*', '0001' ) ),
116             # PR_RCVD_REPRESENTING_xxx
117             Delegate => _ATT( $atp{Byte}, pack( 'H*', '0002' ) ),
118             # PR_DATE_START
119             DateStart => _ATT( $atp{Date}, pack( 'H*', '0006' ) ),
120             DateEnd => _ATT( $atp{Date}, pack( 'H*', '0007' ) ), # PR_DATE_END
121             # PR_OWNER_APPT_ID
122             AidOwner => _ATT( $atp{Long}, pack( 'H*', '0008' ) ),
123             # PR_RESPONSE_REQUESTED
124             RequestRes => _ATT( $atp{Short}, pack( 'H*', '0009' ) ),
125             );
126              
127             # Create reverse lookup table
128             %att_name = reverse %att;
129              
130             # Global counter for creating file names
131             $g_file_cnt = 0;
132              
133             # Set some package global defaults for new objects
134             # which can be overridden for any individual object.
135             %dflts = (
136             debug => 0,
137             debug_max_display => 1024,
138             debug_max_line_size => 64,
139             ignore_checksum => 0,
140             display_after_err => 32,
141             output_to_core => 4096,
142             output_dir => File::Spec->curdir,
143             output_prefix => "tnef",
144             buffer_size => 1024,
145             );
146              
147             # Make a file name
148             sub _mk_fname {
149 0     0   0 my $parms = shift;
150 0         0 File::Spec->catfile( $parms->{output_dir},
151             $parms->{output_prefix} . "-" . $$ . "-"
152             . ++$g_file_cnt . ".doc" );
153             }
154              
155             sub _rtn_err {
156 1     1   3 my ( $errmsg, $fh, $parms ) = @_;
157 1         2 $errstr = $errmsg;
158 1 50       5 if ( $parms->{debug} ) {
159 0   0     0 my $read_size = $parms->{display_after_err} || 32;
160 0         0 my $data;
161 0         0 $fh->read( $data, $read_size );
162 0         0 print "Error: $errstr\n";
163 0         0 print "Data:\n";
164 0         0 print $1, "\n" while $data =~
165             /([^\r\n]{0,$parms->{debug_max_line_size}})\r?\n?/g;
166 0         0 print "HData:\n";
167 0         0 my $hdata = unpack( "H*", $data );
168 0         0 print $1, "\n"
169             while $hdata =~ /(.{0,$parms->{debug_max_line_size}})/g;
170             }
171 1         34 return undef;
172             }
173              
174             sub _read_err {
175 0     0   0 my ( $bytes, $fh, $errmsg ) = @_;
176 0 0       0 $errstr =
177             ( defined $bytes ) ? "Premature EOF" : "Read Error:" . $errmsg;
178 0         0 return undef;
179             }
180              
181             sub read_ent {
182 0 0 0 0 0 0 croak "Usage: Convert::TNEF->read_ent(entity, parameters) "
183             unless @_ == 2 or @_ == 3;
184 0         0 my $self = shift;
185 0         0 my ( $ent, $parms ) = @_;
186 0 0       0 my $io = $ent->open("r") or do {
187 0         0 $errstr = "Can't open entity: $!";
188 0         0 return undef;
189             };
190 0         0 my $tnef = $self->read( $io, $parms );
191 0 0       0 $io->close or do {
192 0         0 $errstr = "Error closing handle: $!";
193 0         0 return undef;
194             };
195 0         0 return $tnef;
196             }
197              
198             sub read_in {
199 2 50 66 2 0 86 croak "Usage: Convert::TNEF->read_in(filename, parameters) "
200             unless @_ == 2 or @_ == 3;
201 2         5 my $self = shift;
202 2         4 my ( $fname, $parms ) = @_;
203 2 50       92 open( INFILE, "<$fname" ) or do {
204 0         0 $errstr = "Can't open $fname: $!";
205 0         0 return undef;
206             };
207 2         7 binmode INFILE;
208 2         11 my $tnef = $self->read( \*INFILE, $parms );
209 2 50       39 close INFILE or do {
210 0         0 $errstr = "Error closing $fname: $!";
211 0         0 return undef;
212             };
213 2         11 return $tnef;
214             }
215              
216             sub read {
217 2 50 33 2 0 17 croak "Usage: Convert::TNEF->read(fh, parameters) "
218             unless @_ == 2 or @_ == 3;
219 2         5 my $self = shift;
220 2   33     13 my $class = ref($self) || $self;
221 2         5 $self = {};
222 2         6 bless $self, $class;
223 2         6 my ( $fd, $parms ) = @_;
224 2         21 $fd = wraphandle($fd);
225              
226 2         62 my %parms = %dflts;
227 2 100       15 @parms{ keys %$parms } = values %$parms if defined $parms;
228 2         5 $parms = \%parms;
229 2         5 my $debug = $parms{debug};
230 2         6 my $ignore_checksum = $parms{ignore_checksum};
231              
232             # Start of TNEF stream
233 2         4 my $data;
234 2         11 my $num_bytes = $fd->read( $data, 4 );
235 2 50       63 return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 4;
236 2 50       8 print "TNEF start: ", unpack( "H*", $data ), "\n" if $debug;
237 2 50       6 return _rtn_err( "Not TNEF-encapsulated", $fd, $parms )
238             unless $data eq $TNEF_SIGNATURE;
239              
240             # Key
241 2         9 $num_bytes = $fd->read( $data, 2 );
242 2 50       16 return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 2;
243 2 50       6 print "TNEF key: ", unpack( "H*", $data ), "\n" if $debug;
244              
245             # Start of First Object
246 2         9 $num_bytes = $fd->read( $data, 1 );
247 2 50       14 return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 1;
248              
249 2         5 my $msg_att = "";
250              
251 2         6 my $is_msg = ( $data eq $LVL_MESSAGE );
252 2         5 my $is_att = ( $data eq $LVL_ATTACHMENT );
253 2 50       7 print "TNEF object start: ", unpack( "H*", $data ), "\n" if $debug;
254 2 50 33     9 return _rtn_err( "Neither a message nor an attachment", $fd,
255             $parms )
256             unless $is_msg or $is_att;
257              
258 2         13 my $msg = Convert::TNEF::Data->new;
259 2         5 my @atts;
260              
261             # Current message or attachment in loop
262 2         4 my $ent = $msg;
263              
264             # Read message and attachments
265 24 100       46 LOOP: {
266 2         4 my $type = $is_msg ? 'message' : 'attachment';
267 24 50       48 print "Reading $type attribute\n" if $debug;
268 24         63 $num_bytes = $fd->read( $data, 4 );
269 24 50       123 return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 4;
270 24         28 my $att_id = $data;
271 24         48 my $att_name = $att_name{$att_id};
272              
273 24 50       42 print "TNEF $type attribute: ", unpack( "H*", $data ), "\n"
274             if $debug;
275 24 50       52 return _rtn_err( "Bad Attribute found in $type", $fd, $parms )
276             unless $att_name{$att_id};
277 24 100       89 if ( $att_id eq $att{TnefVersion} ) {
    100          
    100          
278 2 50       8 return _rtn_err( "Version attribute found in attachment", $fd,
279             $parms )
280             if $is_att;
281             } elsif ( $att_id eq $att{MessageClass} ) {
282 2 50       5 return _rtn_err( "MessageClass attribute found in attachment",
283             $fd, $parms )
284             if $is_att;
285             } elsif ( $att_id eq $att{AttachRenddata} ) {
286 1 50       4 return _rtn_err( "AttachRenddata attribute found in message",
287             $fd, $parms )
288             if $is_msg;
289 1         8 push @atts, ( $ent = Convert::TNEF::Data->new );
290             } else {
291 19 50 66     60 return _rtn_err( "AttachRenddata must be first attribute", $fd,
      33        
292             $parms )
293             if $is_att
294             and !@atts
295             and $att_name ne "AttachRenddata";
296             }
297 24 50       44 print "Got attribute:$att_name{$att_id}\n" if $debug;
298              
299 24         64 $num_bytes = $fd->read( $data, 4 );
300 24 50       132 return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 4;
301              
302 24 50       45 print "HLength:", unpack( "H8", $data ), "\n" if $debug;
303 24         70 my $length = unpack( "V", $data );
304 24 50       46 print "Length: $length\n" if $debug;
305              
306             # Get the attribute data (returns an object since data may
307             # actually end up in a file)
308 24         44 my $calc_chksum;
309 24 50       49 $data = _build_data( $fd, $length, \$calc_chksum, $parms )
310             or return undef;
311 24 50       52 _debug_print( $length, $att_id, $data, $parms ) if $debug;
312 24         52 $ent->datahandle( $att_name, $data, $length );
313              
314 24         66 $num_bytes = $fd->read( $data, 2 );
315 24 50       142 return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 2;
316 24         26 my $file_chksum = $data;
317 24 50       44 if ($debug) {
318 0         0 print "Calc Chksum:", unpack( "H*", $calc_chksum ), "\n";
319 0         0 print "File Chksum:", unpack( "H*", $file_chksum ), "\n";
320             }
321 24 100 100     67 return _rtn_err( "Bad Checksum", $fd, $parms )
322             unless $calc_chksum eq $file_chksum
323             or $ignore_checksum;
324              
325 23         58 my $num_bytes = $fd->read( $data, 1 );
326              
327             # EOF (0 bytes) is ok
328 23 50       122 return _read_err( $num_bytes, $fd, $! ) unless defined $num_bytes;
329 23 100       42 last LOOP if $num_bytes < 1;
330 22 50       35 print "Next token:", unpack( "H2", $data ), "\n" if $debug;
331 22         33 $is_msg = ( $data eq $LVL_MESSAGE );
332 22 50 66     74 return _rtn_err( "Found message data in attachment", $fd, $parms )
333             if $is_msg and $is_att;
334 22         28 $is_att = ( $data eq $LVL_ATTACHMENT );
335 22 50 66     75 redo LOOP if $is_msg or $is_att;
336 0         0 return _rtn_err( "Not a TNEF $type", $fd, $parms );
337             }
338              
339 1 50       5 print "EOF\n" if $debug;
340              
341 1         8 $self->{TN_Message} = $msg;
342 1         3 $self->{TN_Attachments} = \@atts;
343 1         17 return $self;
344             }
345              
346             sub _debug_print {
347 0     0   0 my ( $length, $att_id, $data, $parms ) = @_;
348 0 0       0 if ( $length < $parms->{debug_max_display} ) {
349 0         0 $data = $data->data;
350 0 0 0     0 if ( $att_id eq $att{TnefVersion} ) {
    0 0        
    0          
351 0         0 $data = unpack( "L", $data );
352 0         0 print "Version: $data\n";
353             } elsif ( substr( $att_id, 2 ) eq $atp{Date} and $length == 14 ) {
354 0         0 my ( $yr, $mo, $day, $hr, $min, $sec, $dow ) =
355             unpack( "vvvvvvv", $data );
356 0         0 my $date = join ":", $yr, $mo, $day, $hr, $min, $sec, $dow;
357 0         0 print "Date: $date\n";
358 0         0 print "HDate:", unpack( "H*", $data ), "\n";
359             } elsif ( $att_id eq $att{AttachRenddata} and $length == 14 ) {
360 0         0 my ( $atyp, $ulPosition, $dxWidth, $dyHeight, $dwFlags ) =
361             unpack( "vVvvV", $data );
362 0         0 $data = join ":", $atyp, $ulPosition, $dxWidth, $dyHeight,
363             $dwFlags;
364 0         0 print "AttachRendData: $data\n";
365             } else {
366 0         0 print "Data:\n";
367 0         0 print $1, "\n" while $data =~
368             /([^\r\n]{0,$parms->{debug_max_line_size}})\r?\n?/g;
369 0         0 print "HData:\n";
370 0         0 my $hdata = unpack( "H*", $data );
371 0         0 print $1, "\n"
372             while $hdata =~ /(.{0,$parms->{debug_max_line_size}})/g;
373             }
374             } else {
375 0 0       0 my $io = $data->open("r")
376             or croak "Error opening attachment data handle: $!";
377 0         0 my $buffer;
378 0         0 $io->read( $buffer, $parms->{debug_max_display} );
379 0 0       0 $io->close or croak "Error closing attachment data handle: $!";
380 0         0 print "Data:\n";
381 0         0 print $1, "\n" while $buffer =~
382             /([^\r\n]{0,$parms->{debug_max_line_size}})\r?\n?/sg;
383 0         0 print "HData:\n";
384 0         0 my $hdata = unpack( "H*", $buffer );
385 0         0 print $1, "\n"
386             while $hdata =~ /(.{0,$parms->{debug_max_line_size}})/g;
387             }
388             }
389              
390             sub _build_data {
391 24     24   37 my ( $fd, $length, $chksumref, $parms ) = @_;
392 24         32 my $cutoff = $parms->{output_to_core};
393 24         24 my $incore = do {
394 24 50       75 if ( $cutoff eq 'NONE' ) { 0 } #Everything to files
  0 50       0  
    50          
395 0         0 elsif ( $cutoff eq 'ALL' ) { 1 } #Everything in memory
396 0         0 elsif ( $cutoff < $length ) { 0 } #Large items in files
397 24         37 else { 1 } #Everything else in memory
398             };
399              
400             # Just borrow some other objects for the attachment attribute data
401 24 50       138 my $body =
402             ($incore)
403             ? new MIME::Body::Scalar
404             : new MIME::Body::File _mk_fname($parms);
405 24         355 $body->binmode(1);
406 24         249 my $io = $body->open("w");
407 24         3950 my $bufsiz = $parms->{buffer_size};
408 24 100       62 $bufsiz = $length if $length < $bufsiz;
409 24         25 my $buffer;
410 24         25 my $chksum = 0;
411              
412 24         47 while ( $length > 0 ) {
413 26         81 my $num_bytes = $fd->read( $buffer, $bufsiz );
414 26 50       140 return _read_err( $num_bytes, $fd, $! )
415             unless $num_bytes == $bufsiz;
416 26         71 $io->print($buffer);
417 26         215 $chksum += unpack( "%16C*", $buffer );
418 26         30 $chksum %= 65536;
419 26         27 $length -= $bufsiz;
420 26 50       86 $bufsiz = $length if $length < $bufsiz;
421             }
422 24         49 $$chksumref = pack( "v", $chksum );
423 24         61 $io->close;
424 24         198 return $body;
425             }
426              
427             sub purge {
428 0     0 0 0 my $self = shift;
429 0         0 my $msg = $self->{TN_Message};
430 0         0 my @atts = $self->attachments;
431 0         0 for ( keys %$msg ) {
432 0 0       0 $msg->{$_}->purge if exists $att{$_};
433             }
434 0         0 for my $attch (@atts) {
435 0         0 for ( keys %$attch ) {
436 0 0       0 $attch->{$_}->purge if exists $att{$_};
437             }
438             }
439             }
440              
441             sub message {
442 1     1 0 373 my $self = shift;
443 1         4 $self->{TN_Message};
444             }
445              
446             sub attachments {
447 1     1 0 461 my $self = shift;
448 1 50       12 return @{ $self->{TN_Attachments} } if wantarray;
  1         6  
449 0         0 $self->{TN_Attachments};
450             }
451              
452             # This is for Messages or Attachments
453             # since they are essentially the same thing except
454             # for the leading attribute code
455             package Convert::TNEF::Data;
456              
457             sub new {
458 3     3   8 my $proto = shift;
459 3   33     27 my $class = ref($proto) || $proto;
460 3         7 my $self = {};
461 3         9 $self->{TN_Size} = {};
462 3         8 bless $self, $class;
463             }
464              
465             sub data {
466 3     3   14 my $self = shift;
467 3   100     12 my $attr = shift || 'AttachData';
468 3   33     19 return $self->{$attr} && $self->{$attr}->as_string;
469             }
470              
471             sub name {
472 1     1   8 my $self = shift;
473 1   50     8 my $attr = shift || 'AttachTitle';
474 1   33     14 my $name = $self->{$attr} && $self->{$attr}->data;
475 1 50       168 $name =~ s/\x00+$// if $name;
476 1         5 return $name;
477             }
478              
479             # Try to get the long filename out of the
480             # 'Attachment' attribute.
481             sub longname {
482 1     1   11 my $self = shift;
483              
484 1         5 my $data = $self->data("Attachment");
485 1 50       8 return unless $data;
486 1         5 my $pos = index( $data, pack( "H*", "1e00013001" ) );
487 1 50       5 $pos = index( $data, pack( "H*", "1e00073701" ) ) if ($pos < 0);
488 1 50       6 return $self->name unless $pos >= 0;
489 1         5 my $len = unpack( "V", substr( $data, $pos + 8, 4 ) );
490 1         3 my $longname = substr( $data, $pos + 12, $len );
491 1 50       6 $longname =~ s/\x00+$// if $longname;
492 1   33     7 return $longname || $self->name;
493             }
494              
495             sub datahandle {
496 24     24   27 my $self = shift;
497 24   50     47 my $attr = shift || 'AttachData';
498 24 50       79 $self->{$attr} = shift if @_;
499 24 50       70 $self->size( $attr, shift ) if @_;
500 24         39 return $self->{$attr};
501             }
502              
503             sub size {
504 24     24   26 my $self = shift;
505 24   50     45 my $attr = shift || 'AttachData';
506 24 50       66 $self->{TN_Size}->{$attr} = shift if @_;
507 24         47 return $self->{TN_Size}->{$attr};
508             }
509              
510             # Autoload methods go after =cut, and are processed by the autosplit program.
511              
512             1;
513             __END__