File Coverage

blib/lib/Matts/Message/Parser.pm
Criterion Covered Total %
statement 47 643 7.3
branch 0 278 0.0
condition 0 96 0.0
subroutine 16 65 24.6
pod 0 16 0.0
total 63 1098 5.7


line stmt bran cond sub pod time code
1              
2             package Matts::Message::Parser;
3 1     1   1008 use strict;
  1         2  
  1         48  
4 1     1   6 use vars qw(@Try_Encodings $VERSION);
  1         2  
  1         63  
5              
6             $VERSION = '1.0';
7              
8             # MIME Message parser, for email and nntp engines.
9              
10 1     1   594 use Matts::Message;
  1         3  
  1         31  
11 1     1   7 use MIME::Base64;
  1         2  
  1         43  
12 1     1   1039 use MIME::QuotedPrint;
  1         263  
  1         48  
13 1     1   6 use Carp;
  1         2  
  1         64  
14 1     1   999 use UNIVERSAL;
  1         14  
  1         6  
15              
16             @Try_Encodings = qw(euc-cn euc-jp shiftjis euc-kr big5-eten iso-8859-15 );
17              
18             sub debug {
19 0 0   0 0 0 return unless $ENV{DEBUG};
20 0         0 warn((caller)[2], @_);
21             }
22              
23             sub mkbinmode {
24 0 0   0 0 0 if ($] > 5.007) {
25 0         0 binmode($_[0], ':utf8');
26             }
27             else {
28 0         0 binmode($_[0]);
29             }
30             }
31              
32             =head1 NAME
33              
34             Matts::Message::Parser - a MIME message parser for email and nttp
35              
36             =head1 SYNOPSIS
37              
38             use Matts::Message::Parser;
39             open(my $fh, "foo.eml");
40             my $msg = Matts::Message::Parser->parse($fh);
41              
42             =head1 DESCRIPTION
43              
44             This is an email parser I originally wrote when I ran my own business that tries
45             quite hard to decode the various parts of an email correctly and down to unicode
46             so that all strings can be treated the same in perl.
47              
48             DO NOT USE THIS MODULE
49              
50             I urge you, please don't. It's not a very good API. I'm just uploading it to
51             CPAN because it's better for my purposes than most of the Email::* and Mail::*
52             classes I can find, and it's fast, and doesn't use any memory when parsing very
53             large emails, which is a huge bonus for me. But I have no intention of documenting
54             this module any more than I have to.
55              
56             =head1 AUTHOR
57              
58             Matt Sergeant,
59              
60             =head1 LICENSE
61              
62             This is free software. You may use it and redistribute it under the same terms
63             as perl itself.
64              
65             =head1 HACKING NOTES
66              
67             =head2 This is how mail messages can come in:
68              
69             =over 4
70              
71             =item 1. Plain text
72              
73             Plain text messages come in with a content-type of text/plain. They
74             may contain attachments as UU Encoded strings.
75              
76             =item 2. HTML text
77              
78             Straight HTML messages come in with a content-type of text/html. They
79             may not contain attachments as far as I'm aware.
80              
81             =item 3. Mixed text, html and maybe other.
82              
83             These messages come in as MIME messages with the content-type of
84             multipart/alternative (alternate means you get to pick which view of the
85             message to display, as all must contain the same basic information).
86              
87             There may not be attachments this way as far as I'm aware.
88              
89             =item 4. Plain text with attachments
90              
91             Here the content-type is multipart/mixed. The first part of the multipart
92             message is the the plain text message (after the preamble, that is), with
93             a content type of text/plain. The remaining parts are attachments.
94              
95             =item 5. HTML text with attachments
96              
97             Again, the content-type is multipart/mixed. The first part of the multipart
98             message is the html message, with a content-type of text/html. The
99             remaining parts are attachments.
100              
101             =item 6. Mixed text, html with attachments
102              
103             Here the main part of the message has a content-type of multipart/mixed. The
104             first part has a content-type of multipart/alternative, and is identical to
105             item 3 above. The remaining parts are the attachments.
106              
107             =item 7. Report.
108              
109             This is a delivery status report. It comes with the main part of the message
110             having a content-type of multipart/report, the first one or two parts of which
111             may be textual content of some sort, and the last seems to be of type
112             message/rfc822.
113              
114             =back
115              
116             Overall this is a fairly naive way to view email messages, as the
117             attachments can be email messages themselves, and thus it gets very
118             recursive. But this should be enough for us to deal with right now.
119              
120             =cut
121              
122             # constructor
123             sub parse {
124 0     0 0 0 my $class = shift;
125            
126 0         0 my $ioref;
127            
128 0 0 0     0 if (ref($_[0]) and UNIVERSAL::isa($_[0], 'IO::Handle')) {
129 0         0 $ioref = $_[0];
130             }
131             else {
132 0         0 eval {
133 0 0       0 if (defined(*{$_[0]})) {
  0         0  
134             # throw an exception if not a FH
135 0         0 $ioref = *{$_[0]}{IO};
  0         0  
136             # if no exception thrown, just use the real FH
137 0 0       0 $ioref = $_[0] if defined($ioref);
138             }
139             };
140             }
141            
142 0 0       0 if (!defined $ioref) {
143 0         0 $ioref = $class->new_tmpfile();
144 0         0 print $ioref $_[0];
145 0         0 seek($ioref, 0, 0);
146             }
147            
148 0         0 shift; # lose $_[0] now
149 0         0 my %opts = @_;
150            
151 0         0 binmode($ioref);
152            
153 0         0 my $msg = Matts::Message->new();
154              
155 0         0 $msg->size((stat($ioref))[7]);
156 0         0 $msg->mtime((stat($ioref))[9]);
157            
158 0         0 my $header = <$ioref>;
159 0         0 $header =~ s/\r\n/\n/;
160             # $header =~ s/\s+$//;
161            
162 0 0       0 if ($header =~ /^from\s+([^:].*)$/) {
163 0         0 $msg->header('Envelope-From', $1);
164 0         0 $header = <$ioref>;
165 0         0 $header =~ s/\r\n/\n/;
166             # $header =~ s/\s+$//;
167             }
168            
169 0         0 local $_; # protect from abuse
170            
171             HEADER:
172 0         0 while (my $last = <$ioref>) {
173 0         0 $last =~ s/\r\n/\n/;
174             # chomp($last);
175             # $last =~ s/\s+$//;
176 0 0       0 if ($last =~ /^\s+\S/) { # if its a continuation
177 0         0 $header .= $last; # fold continuations
178 0         0 next HEADER;
179             }
180            
181             # not a continuation...
182 0         0 my ($key, $value) = split( /:\s*/, $header, 2);
183 0 0       0 if ($value =~ /[\x80-\xff]/) {
184             # header contains high 8bit chars
185 0         0 $msg->binary_header($key, $value);
186             }
187             else {
188 0         0 $value = $class->decode_header($key, $value);
189             # debug("Got header: $key: $value\n");
190 0         0 $msg->header($key, $value);
191             }
192            
193 0         0 $header = $last;
194            
195 0 0       0 last HEADER if ($last =~ /^$/m);
196             }
197            
198 0 0       0 if ($opts{header_only}) {
199 0 0       0 if ($msg->binary_headers) {
200 0         0 debug("Binary headers found - trying to decode them without body hints\n");
201 0         0 my $conv = $class->converter('ASCII');
202 0         0 foreach my $header ($msg->binary_headers()) {
203 0         0 debug("Trying to fix up $header\n");
204 0         0 $msg->header($header, $conv->convert($msg->binary_header($header)));
205             }
206             }
207 0         0 return $msg;
208             }
209            
210 0         0 my $body = $class->new_tmpfile();
211 0         0 binmode($body);
212 0         0 my $lines = 0;
213            
214 0         0 while (my $line = <$ioref>) {
215 0         0 $line =~ s/\r\n/\n/;
216 0         0 print $body $line;
217 0         0 $lines++;
218             }
219            
220 0         0 seek($body, 0, 0);
221            
222 0         0 $class->parse_body($msg, $msg, $body);
223              
224             # warn("Fixup binary headers\n");
225              
226 0 0       0 return $msg unless $msg->binary_headers;
227              
228 0         0 my $binenc = '';
229 0         0 my @bodies = $msg->bodies;
230 0         0 my $id = 0;
231 0         0 while (@bodies) {
232 0         0 my ($type, $fh) = splice(@bodies, 0, 2);
233 0         0 my $enc = $msg->body_enc($id);
234 0 0       0 if ($enc ne 'null') {
235 0         0 $binenc = $enc;
236 0         0 last;
237             }
238 0         0 $id++;
239             }
240            
241 0         0 debug("Fixup binary headers. Got binenc: $binenc\n");
242            
243 0 0       0 if (!$binenc) {
244 0         0 my $ct = $msg->header('x-original-content-type');
245 0         0 debug("binenc was blank. Trying content-type: $ct\n");
246 0 0 0     0 if ($ct and $ct =~ /charset="?([^\";]+)"?;?/i) {
247 0         0 $binenc = $1;
248             }
249             }
250            
251 0   0     0 $binenc ||= 'ASCII';
252            
253 0         0 my $conv = $class->converter($binenc);
254 0         0 foreach my $header ($msg->binary_headers()) {
255 0         0 debug("Fixing up $header to be $binenc\n");
256 0         0 $msg->header($header,
257             $conv->convert($msg->binary_header($header)));
258             }
259            
260 0 0       0 unless ($msg->header('message-id')) {
261 0         0 $msg->header('message-id', '<' . time . '@unknown>');
262             }
263            
264 0 0       0 unless ($msg->header('subject')) {
265 0         0 $msg->header('subject', "No Topic");
266             }
267            
268 0         0 return $msg;
269             }
270              
271             sub parse_body {
272 0     0 0 0 my $class = shift;
273 0         0 my ($msg, $_msg, $body) = @_;
274            
275 0   0     0 my $type = $_msg->header('Content-Type') || 'text/plain';
276            
277 0         0 debug("Parsing message of type: $type\n");
278            
279 0 0       0 if ($type =~ /^text\/html/i) {
    0          
    0          
    0          
    0          
280 0         0 debug("Parse text/html\n");
281 0         0 $class->parse_normal($msg, $_msg, $body);
282             }
283             elsif ($type =~ /^text/i) {
284 0         0 debug("Parse text/plain\n");
285 0         0 $class->parse_normal($msg, $_msg, $body);
286             #$class->process_uue($msg);
287             }
288             elsif ($type =~ /ms-tnef/i) {
289 0         0 debug("Parse ms-tnef\n");
290 0         0 eval {
291 0         0 $class->parse_tnef($msg, $_msg, $body);
292             };
293 0 0       0 if ($@) {
294 0         0 warn("parse_tnef failed: $@\n");
295             }
296             }
297             elsif ($type =~ /^multipart\/alternative/i) {
298 0         0 debug("Parse multipart/alternative\n");
299 0         0 $class->parse_multipart_alternate($msg, $_msg, $body);
300             }
301             elsif ($type =~ /^multipart\//i) {
302 0         0 debug("Parse $type\n");
303 0         0 $class->parse_multipart_mixed($msg, $_msg, $body);
304             }
305             else {
306 0         0 debug("Regular attachment\n");
307 0         0 $class->decode_attachment($msg, $_msg, $body);
308             }
309            
310 0 0       0 if (!$msg->body()) {
311 0         0 debug("No message body found. Reparsing\n");
312 0         0 my $part_fh = $class->new_tmpfile();
313 0         0 my $part_msg = Matts::Message->new();
314 0         0 $class->decode_body($msg, $part_msg, $part_fh);
315             }
316            
317 0         0 $class->process_uue($msg);
318             }
319              
320             sub parse_multipart_alternate {
321 0     0 0 0 my $class = shift;
322 0         0 my ($msg, $_msg, $body) = @_;
323            
324 0         0 my ($boundary) = $_msg->header('content-type') =~ /boundary\s*=\s*["']?([^"';]+)["']?/i;
325            
326 0         0 my $preamble = '';
327              
328 0         0 debug("m/a got boundary: $boundary\n");
329            
330             # extract preamble (normally contains "This message is in Multipart/MIME format")
331 0         0 while(my $line = <$body>) {
332 0         0 $line =~ s/\r\n/\n/;
333 0 0       0 last if $line =~ /^\-\-\Q$boundary\E$/;
334 0         0 $preamble .= $line;
335             }
336              
337 0         0 debug("preamble: [[$preamble]]\n");
338            
339 0         0 my $part_fh = $class->new_tmpfile();
340 0         0 my $part_msg = Matts::Message->new();
341 0         0 my $in_body = 0;
342            
343 0         0 my $header;
344              
345 0         0 while(<$body>) {
346 0         0 s/\r\n/\n/;
347             # debug($_);
348 0 0 0     0 if (/^\-\-\Q$boundary\E/ || eof($body)) {
349 0         0 debug("m/a got end of section\n");
350             # end of part
351 0         0 seek($part_fh, 0, 0);
352 0         0 my $line = $_;
353             # assume body part if it's text
354 0 0       0 if ($part_msg->header('content-type') =~ /^text/i) {
355 0         0 $class->decode_body($msg, $part_msg, $part_fh);
356             }
357             else {
358 0         0 debug("Likely virus?\n");
359 0         0 $class->decode_attachment($msg, $part_msg, $part_fh);
360             }
361 0 0       0 last if $line =~ /^\-\-\Q$boundary\E\-\-$/;
362 0         0 $in_body = 0;
363 0         0 $part_msg = Matts::Message->new();
364 0         0 $part_fh = $class->new_tmpfile();
365 0         0 next;
366             }
367            
368 0 0       0 if ($in_body) {
369 0         0 print $part_fh $_;
370             }
371             else {
372             # chomp($_);
373 0         0 s/\s+$//;
374 0 0       0 if (m/^\S/) {
    0          
375 0 0       0 if ($header) {
376 0         0 my ($key, $value) = split( /:\s*/, $header, 2);
377 0         0 $part_msg->header($key, $value);
378             }
379 0         0 $header = $_;
380             }
381             elsif (/^$/) {
382 0 0       0 if ($header) {
383 0         0 my ($key, $value) = split( /:\s*/, $header, 2);
384 0         0 $part_msg->header($key, $value);
385             }
386 0         0 $in_body = 1;
387             }
388             else {
389 0         0 $_ =~ s/^\s*//;
390 0         0 $header .= $_;
391             }
392             }
393             }
394            
395             }
396              
397             sub parse_multipart_mixed {
398 0     0 0 0 my $class = shift;
399 0         0 my ($msg, $_msg, $body) = @_;
400            
401 0         0 my ($boundary) = $_msg->header('content-type') =~ /boundary\s*=\s*["']?([^"';]+)["']?/i;
402            
403 0         0 debug("m/m Got boundary: $boundary\n");
404 0         0 my $preamble = '';
405            
406             # extract preamble (normally contains "This message is in Multipart/MIME format")
407 0         0 while(my $line = <$body>) {
408 0         0 $line =~ s/\r\n/\n/;
409 0 0       0 last if $line =~ /^\-\-\Q$boundary\E$/;
410 0         0 $preamble .= $line;
411             }
412              
413 0         0 debug("Extracted preamble: [[$preamble]]\n");
414            
415 0         0 my $part_fh = $class->new_tmpfile();
416 0         0 my $part_msg = Matts::Message->new(); # just used for headers storage
417 0         0 my $in_body = 0;
418            
419 0         0 my $header;
420              
421 0         0 while(<$body>) {
422 0         0 s/\r\n/\n/;
423             # debug($_);
424 0 0 0     0 if (/^\-\-\Q$boundary\E/ || eof($body)) {
425             # end of part
426 0         0 debug("Got end of MIME section: $_\n");
427 0         0 my $line = $_;
428 0         0 seek($part_fh, 0, 0);
429 0         0 $class->parse_body($msg, $part_msg, $part_fh);
430            
431 0 0       0 last if $line =~ /^\-\-\Q${boundary}\E\-\-$/;
432 0         0 $in_body = 0;
433 0         0 $part_msg = Matts::Message->new();
434 0         0 $part_fh = $class->new_tmpfile();
435 0         0 next;
436             }
437            
438 0 0       0 if ($in_body) {
439 0         0 print $part_fh $_;
440             }
441             else {
442             # chomp($_);
443 0         0 s/\s+$//;
444 0 0       0 if (m/^\S/) {
    0          
445 0 0       0 if ($header) {
446 0         0 my ($key, $value) = split( /:\s*/, $header, 2);
447 0         0 $part_msg->header($key, $value);
448             }
449 0         0 $header = $_;
450             }
451             elsif (/^$/) {
452 0 0       0 if ($header) {
453 0         0 my ($key, $value) = split( /:\s*/, $header, 2);
454 0         0 $part_msg->header($key, $value);
455             }
456 0         0 $in_body = 1;
457             }
458             else {
459 0         0 $_ =~ s/^\s*//;
460 0         0 $header .= $_;
461             }
462             }
463             }
464            
465             }
466              
467             sub parse_normal {
468 0     0 0 0 my $class = shift;
469 0         0 my ($msg, $_msg, $body) = @_;
470            
471             # extract body, store it in $msg
472 0         0 $class->decode_body($msg, $_msg, $body);
473             }
474              
475 1     1   2660 use File::Path qw(rmtree);
  1         3  
  1         1681  
476              
477             sub parse_tnef {
478 0     0 0 0 my $class = shift;
479 0         0 my ($msg, $_msg, $body) = @_;
480            
481 0         0 my ($type, $main) = $class->decode($_msg, $body);
482 0         0 debug("got tnef: $type\n");
483            
484 0         0 my $dir = $class->new_tmpdir();
485            
486             # Create a tnef object
487 0 0       0 my $tnef = Matts::Message::TNEF->read($main, { output_dir => $dir })
488             or die $Matts::Message::TNEF::errstr;
489              
490 0         0 my $body_part = $tnef->message();
491 0 0       0 if (my $data = $body_part->data) {
492 0         0 my $fh = $class->new_tmpfile();
493 0         0 debug("Got tnef body part: $data\n");
494 0         0 print $fh $data;
495 0         0 seek($fh, 0, 0);
496             # Make possibly invalid assumption that it's text/plain
497 0         0 debug("Adding tnef body part\n");
498 0         0 $msg->add_body_part("text/plain", $fh);
499             }
500            
501 0         0 for my $part ($tnef->attachments) {
502 0         0 my $fh = $class->new_tmpfile();
503 0         0 print $fh $part->data;
504 0         0 seek($fh, 0, 0);
505 0         0 my $filename = $part->longname;
506 0         0 debug("Got tnef attachment part $filename\n");
507 0         0 debug("Adding tnef attachment: $filename\n");
508 0         0 $msg->add_attachment("application/octet-stream", $fh, $filename);
509             }
510            
511 0         0 rmtree($dir);
512             }
513              
514             sub process_uue {
515 0     0 0 0 my $class = shift;
516 0         0 my ($msg) = @_;
517             }
518              
519             sub _decode_header {
520 0     0   0 my ($encoding, $cte, $data) = @_;
521            
522 0         0 my $converter = __PACKAGE__->converter($encoding);
523 0 0       0 my $decoder = $cte eq 'B' ? \&MIME::Base64::decode_base64 :
    0          
524             $cte eq 'Q' ? \&MIME::QuotedPrint::decode_qp :
525             die "Unknown encoding type '$cte' in RFC2047 header";
526            
527 0         0 return $converter->convert($decoder->($data));
528             }
529              
530             # decode according to RFC2047
531             sub decode_header {
532 0     0 0 0 my $class = shift;
533 0         0 my ($key, $header) = @_;
534              
535 0 0       0 return '' unless $header;
536 0 0 0     0 return $header unless ($header =~ /=\?/ or $header =~ /[\x80-\xff]/);
537              
538 0         0 $header =~ s/=\?([\w_-]+)\?([bqBQ])\?(.*?)\?=/_decode_header($1, uc($2), $3)/ge;
  0         0  
539 0         0 return $header;
540             }
541              
542             sub decode_body {
543 0     0 0 0 my $class = shift;
544 0         0 my ($msg, $part_msg, $body) = @_;
545            
546 0         0 my ($type, $main) = $class->decode($part_msg, $body);
547              
548 0         0 debug("got body: $type\n");
549            
550 0         0 $msg->add_body_part($type, $main);
551             }
552              
553             sub decode_attachment {
554 0     0 0 0 my $class = shift;
555 0         0 my ($msg, $part_msg, $fh) = @_;
556            
557 0         0 debug("decoding attachment\n");
558            
559 0         0 my ($type, $content, $filename) = $class->decode($part_msg, $fh);
560            
561 0         0 $msg->add_attachment($type, $content, $filename);
562             }
563              
564             sub decode ($$;$) {
565 0     0 0 0 my $class = shift;
566 0         0 my ($msg, $body) = @_;
567            
568 0         0 my $fh = $class->new_tmpfile();
569 0         0 binmode($fh);
570            
571 0         0 my $hibit;
572            
573 0 0       0 if (lc($msg->header('content-transfer-encoding')) eq 'quoted-printable') {
    0          
574 0         0 debug("decoding QP file\n");
575 0         0 while(<$body>) {
576 0         0 $_ = MIME::QuotedPrint::decode_qp($_);
577 0         0 print $fh $_;
578 0 0       0 $hibit++ if /[\x80-\xff]/;
579             }
580             }
581             elsif (lc($msg->header('content-transfer-encoding')) eq 'base64') {
582 0         0 debug("decoding B64 file\n");
583 0         0 my $output = '';
584 0         0 local $/ = "\n";
585 0         0 my $not_really_base64 = 0;
586 0         0 while(<$body>) {
587             # check to see if its really base64 encoded data
588 0 0       0 $not_really_base64++ if /[<\.,]/;
589 0         0 chomp;
590 0 0       0 if ($not_really_base64) {
591 0         0 print $fh $_;
592 0 0       0 $hibit++ if /[\x80-\xff]/;
593             }
594             else {
595             # pad with = chars - stops MIME::Base64 outputting warnings
596 0 0       0 if (my $len = (length($_) % 4)) {
597 0         0 $_ .= "=" x (4 - $len);
598             }
599 0         0 $_ = MIME::Base64::decode_base64($_);
600 0         0 print $fh $_;
601 0 0       0 $hibit++ if /[\x80-\xff]/;
602             }
603             }
604             }
605             else {
606 0         0 debug("decoding other encoding\n");
607             # Encoding is one of 7bit, 8bit, binary or x-something - just save.
608 0         0 my $buf;
609 0         0 while($buf = <$body>) {
610             # debug("BODY: $buf");
611 0         0 print $fh $buf;
612 0 0       0 $hibit++ if $buf =~ /[\x80-\xff]/;
613             }
614             }
615            
616 0         0 seek($fh, 0, 0);
617            
618 0         0 my $type = $msg->header('content-type');
619 0         0 local $^W;
620 0         0 my ($filename) = ($msg->header('content-disposition') =~ /name="?([^\";]+)"?/i);
621 0 0       0 if (!$filename) {
622 0         0 ($filename) = ($msg->header('content-type') =~ /name="?([^\";]+)"?/i);
623             }
624            
625 0         0 debug("Body type was: $type\n");
626            
627 0         0 my $binenc;
628 0 0       0 if ($type =~ /html/i) {
629 0         0 while (<$fh>) {
630 0 0       0 if (/]*charset="?([\w-]*)[^>]*>/i) {
631 0         0 $binenc = $1;
632 0         0 $type = "text/html";
633 0         0 last;
634             }
635             }
636 0         0 seek($fh, 0, 0);
637             }
638            
639 0         0 my $converter;
640 0 0 0     0 if ($binenc) {
    0          
641 0         0 $converter = $class->converter($binenc);
642 0         0 $msg->header('content-type', 'text/html');
643             }
644             elsif ($type && ($type =~ /^text\b/i)) {
645             # text type - might need to translate to UTF8
646 0         0 debug("Trying to get charset from content-type: $type\n");
647             # remember to strip charset portion - we can always add it later.
648 0         0 $msg->header('X-Original-Content-Type', $type);
649 0 0       0 if ($type =~ s/charset="?([^\";]+)"?;?//i) {
650 0         0 $binenc = $1;
651 0         0 $converter = $class->converter($binenc);
652 0         0 $msg->header('content-type', $type);
653             }
654             }
655            
656 0 0       0 if ($hibit) {
657             # we say ascii here, but in reality the converter should skip to the next converter
658 0   0     0 $converter ||= $class->converter('ascii');
659             }
660            
661 0 0       0 if ($converter) {
662 0         0 my $decoded_fh = $class->new_tmpfile();
663 0         0 my $data = '';
664 0         0 while (<$fh>) {
665 0         0 $data .= $_;
666             }
667 0         0 $data =~ s/]*charset="?([\w-]*)[^>]*>//i;
668 0         0 print $decoded_fh $converter->convert($data);
669 0         0 seek($decoded_fh, 0, 0);
670             # warn("Decoded body. Returning type = $type; charset=$binenc\n");
671 0         0 return "$type; charset=$binenc", $decoded_fh, $filename;
672             }
673            
674 0         0 return $type, $fh, $filename;
675             }
676              
677 1     1   1306 use File::Temp qw(tempfile tempdir);
  1         20881  
  1         778  
678              
679             sub new_tmpfile {
680 0     0 0 0 my $class = shift;
681 0   0     0 my $tmpfile = tempfile() || croak "new_tmpfile failed : $!";
682 0         0 mkbinmode($tmpfile);
683 0         0 return $tmpfile;
684             }
685              
686             sub new_tmpdir {
687 0     0 0 0 my $class = shift;
688 0   0     0 return tempdir(CLEANUP => 1) || croak "new_tmpdir failed: $!";
689             }
690              
691             sub converter {
692 0     0 0 0 my $class = shift;
693 0         0 my ($charset) = @_;
694            
695             # some broken mailers say they're us-ascii, but include higher chars
696             # $charset = 'ISO-8859-15' if $charset =~ /us-?ascii/i;
697              
698 0   0     0 my $converter = eval { EncodeConverter->new($charset) }
699             || NullConverter->new();
700              
701 0         0 return $converter;
702             }
703              
704             package EncodeConverter;
705              
706             *debug = *Matts::Message::Parser::debug;
707              
708             my $loaded = 0;
709             sub load_encode {
710 0 0   0   0 return if $loaded;
711 0         0 require Encode;
712 0         0 require Encode::Alias;
713 0         0 foreach my $enc (Encode->encodings(':all')) {
714 0 0       0 $enc =~ /^cp(\d+)$/ or next;
715             # warn("Defining alias: windows-$1 => $enc\n");
716 0         0 eval { Encode::Alias::define_alias("windows-$1" => $enc) };
  0         0  
717 0 0       0 warn($@) if $@;
718             }
719 0         0 eval { Encode::Alias::define_alias("big5" => 'big5-eten') };
  0         0  
720 0         0 eval { Encode::Alias::define_alias("iso-8859-8-i" => 'iso-8859-8') };
  0         0  
721 0 0       0 warn($@) if $@;
722 0         0 $loaded++;
723             }
724              
725             sub new {
726 0     0   0 my $class = shift;
727 0         0 my ($from) = @_;
728 0         0 load_encode();
729 0         0 return bless { from => $from }, $class;
730             }
731              
732             sub try_decode {
733 0     0   0 my $self = shift;
734 0         0 my $data = shift;
735 0         0 foreach my $enc ($self->{from}, @Matts::Message::Parser::Try_Encodings) {
736 0 0       0 next if $enc =~ /ascii/i;
737 0         0 debug("Trying: $enc\n");
738 0         0 my $d = $data;
739 0         0 my $results = eval {
740 0         0 Encode::decode($enc, $d, Encode::FB_CROAK());
741             };
742 0 0       0 if (!$@) {
743             # debug("Success!: $enc => $results\n");
744 0 0       0 if ($self->{from} ne $enc) {
745 0         0 $self->{from} = $enc;
746             }
747 0 0       0 return wantarray ? ($results, $enc) : $results;
748             }
749 0         0 debug("$enc failed: $@\n");
750             }
751 0 0       0 return wantarray ? ($data, 'UTF-8') : $data;
752             }
753              
754             sub convert {
755 0     0   0 my $self = shift;
756 0         0 return scalar $self->try_decode($_[0]);
757             }
758              
759             package NullConverter;
760              
761             sub new {
762 0     0   0 my $class = shift;
763 0         0 bless {}, $class;
764             }
765              
766             sub try_decode {
767 0     0   0 my $self = shift;
768 0         0 return $_[0];
769             }
770              
771             sub convert {
772 0     0   0 my $self = shift;
773 0         0 return $_[0];
774             }
775              
776             # Taken from Convert::TNEF.pm
777             #
778             # Copyright (c) 1999 Douglas Wilson . All rights reserved.
779             # This program is free software; you can redistribute it and/or
780             # modify it under the same terms as Perl itself.
781              
782             package Matts::Message::TNEF;
783              
784 1     1   9 use strict;
  1         2  
  1         32  
785 1     1   964 use integer;
  1         10  
  1         5  
786 1         120 use vars qw(
787             $TNEF_SIGNATURE
788             $TNEF_PURE
789             $LVL_MESSAGE
790             $LVL_ATTACHMENT
791             $errstr
792             $g_file_cnt
793             %dflts
794             %atp
795             %att
796             %att_name
797 1     1   38 );
  1         2  
798              
799 1     1   6 use Carp;
  1         2  
  1         68  
800 1     1   6 use File::Spec;
  1         1  
  1         8  
801              
802             # Set some TNEF constants. Everything turned
803             # out to be in little endian order, so I just added
804             # 'reverse' everywhere that I needed to
805             # instead of reversing the hex codes.
806             $TNEF_SIGNATURE = reverse pack( 'H*', '223E9F78' );
807             $TNEF_PURE = reverse pack( 'H*', '00010000' );
808              
809             $LVL_MESSAGE = pack( 'H*', '01' );
810             $LVL_ATTACHMENT = pack( 'H*', '02' );
811              
812             %atp = (
813             Triples => pack( 'H*', '0000' ),
814             String => pack( 'H*', '0001' ),
815             Text => pack( 'H*', '0002' ),
816             Date => pack( 'H*', '0003' ),
817             Short => pack( 'H*', '0004' ),
818             Long => pack( 'H*', '0005' ),
819             Byte => pack( 'H*', '0006' ),
820             Word => pack( 'H*', '0007' ),
821             Dword => pack( 'H*', '0008' ),
822             Max => pack( 'H*', '0009' ),
823             );
824              
825             for ( keys %atp ) {
826             $atp{$_} = reverse $atp{$_};
827             }
828              
829             sub _ATT {
830 33     33   43 my ( $att, $id ) = @_;
831 33         131 return reverse($id) . $att;
832             }
833              
834             # The side comments are 'MAPI' equivalents
835             %att = (
836             Null => _ATT( pack( 'H*', '0000' ), pack( 'H4', '0000' ) ),
837             # PR_ORIGINATOR_RETURN_ADDRESS
838             From => _ATT( $atp{Triples}, pack( 'H*', '8000' ) ),
839             # PR_SUBJECT
840             Subject => _ATT( $atp{String}, pack( 'H*', '8004' ) ),
841             # PR_CLIENT_SUBMIT_TIME
842             DateSent => _ATT( $atp{Date}, pack( 'H*', '8005' ) ),
843             # PR_MESSAGE_DELIVERY_TIME
844             DateRecd => _ATT( $atp{Date}, pack( 'H*', '8006' ) ),
845             # PR_MESSAGE_FLAGS
846             MessageStatus => _ATT( $atp{Byte}, pack( 'H*', '8007' ) ),
847             # PR_MESSAGE_CLASS
848             MessageClass => _ATT( $atp{Word}, pack( 'H*', '8008' ) ),
849             # PR_MESSAGE_ID
850             MessageID => _ATT( $atp{String}, pack( 'H*', '8009' ) ),
851             # PR_PARENT_ID
852             ParentID => _ATT( $atp{String}, pack( 'H*', '800A' ) ),
853             # PR_CONVERSATION_ID
854             ConversationID => _ATT( $atp{String}, pack( 'H*', '800B' ) ),
855             Body => _ATT( $atp{Text}, pack( 'H*', '800C' ) ), # PR_BODY
856             # PR_IMPORTANCE
857             Priority => _ATT( $atp{Short}, pack( 'H*', '800D' ) ),
858             # PR_ATTACH_DATA_xxx
859             AttachData => _ATT( $atp{Byte}, pack( 'H*', '800F' ) ),
860             # PR_ATTACH_FILENAME
861             AttachTitle => _ATT( $atp{String}, pack( 'H*', '8010' ) ),
862             # PR_ATTACH_RENDERING
863             AttachMetaFile => _ATT( $atp{Byte}, pack( 'H*', '8011' ) ),
864             # PR_CREATION_TIME
865             AttachCreateDate => _ATT( $atp{Date}, pack( 'H*', '8012' ) ),
866             # PR_LAST_MODIFICATION_TIME
867             AttachModifyDate => _ATT( $atp{Date}, pack( 'H*', '8013' ) ),
868             # PR_LAST_MODIFICATION_TIME
869             DateModified => _ATT( $atp{Date}, pack( 'H*', '8020' ) ),
870             #PR_ATTACH_TRANSPORT_NAME
871             AttachTransportFilename => _ATT( $atp{Byte}, pack( 'H*', '9001' ) ),
872             AttachRenddata => _ATT( $atp{Byte}, pack( 'H*', '9002' ) ),
873             MAPIProps => _ATT( $atp{Byte}, pack( 'H*', '9003' ) ),
874             # PR_MESSAGE_RECIPIENTS
875             RecipTable => _ATT( $atp{Byte}, pack( 'H*', '9004' ) ),
876             Attachment => _ATT( $atp{Byte}, pack( 'H*', '9005' ) ),
877             TnefVersion => _ATT( $atp{Dword}, pack( 'H*', '9006' ) ),
878             OemCodepage => _ATT( $atp{Byte}, pack( 'H*', '9007' ) ),
879             # PR_ORIG_MESSAGE_CLASS
880             OriginalMessageClass => _ATT( $atp{Word}, pack( 'H*', '0006' ) ),
881              
882             # PR_RCVD_REPRESENTING_xxx or PR_SENT_REPRESENTING_xxx
883             Owner => _ATT( $atp{Byte}, pack( 'H*', '0000' ) ),
884             # PR_SENT_REPRESENTING_xxx
885             SentFor => _ATT( $atp{Byte}, pack( 'H*', '0001' ) ),
886             # PR_RCVD_REPRESENTING_xxx
887             Delegate => _ATT( $atp{Byte}, pack( 'H*', '0002' ) ),
888             # PR_DATE_START
889             DateStart => _ATT( $atp{Date}, pack( 'H*', '0006' ) ),
890             DateEnd => _ATT( $atp{Date}, pack( 'H*', '0007' ) ), # PR_DATE_END
891             # PR_OWNER_APPT_ID
892             AidOwner => _ATT( $atp{Long}, pack( 'H*', '0008' ) ),
893             # PR_RESPONSE_REQUESTED
894             RequestRes => _ATT( $atp{Short}, pack( 'H*', '0009' ) ),
895             );
896              
897             # Create reverse lookup table
898             %att_name = reverse %att;
899              
900             # Global counter for creating file names
901             $g_file_cnt = 0;
902              
903             # Set some package global defaults for new objects
904             # which can be overridden for any individual object.
905             %dflts = (
906             debug => 0,
907             debug_max_display => 1024,
908             debug_max_line_size => 64,
909             ignore_checksum => 0,
910             display_after_err => 32,
911             output_to_core => 4096,
912             output_dir => File::Spec->curdir,
913             output_prefix => "tnef",
914             buffer_size => 1024,
915             );
916              
917             # Make a file name
918             sub _mk_fname {
919 0     0     my $parms = shift;
920 0           File::Spec->catfile( $parms->{output_dir},
921             $parms->{output_prefix} . "-" . $$ . "-"
922             . ++$g_file_cnt . ".doc" );
923             }
924              
925             sub _rtn_err {
926 0     0     my ( $errmsg, $fh, $parms ) = @_;
927 0           $errstr = $errmsg;
928 0 0         if ( $parms->{debug} ) {
929 0   0       my $read_size = $parms->{display_after_err} || 32;
930 0           my $data;
931 0           read($fh, $data, $read_size );
932 0           print "Error: $errstr\n";
933 0           print "Data:\n";
934 0           print $1, "\n" while $data =~
935             /([^\r\n]{0,$parms->{debug_max_line_size}})\r?\n?/g;
936 0           print "HData:\n";
937 0           my $hdata = unpack( "H*", $data );
938 0           print $1, "\n"
939             while $hdata =~ /(.{0,$parms->{debug_max_line_size}})/g;
940             }
941 0           return undef;
942             }
943              
944             sub _read_err {
945 0     0     my ( $bytes, $fh, $errmsg ) = @_;
946 0 0         $errstr =
947             ( defined $bytes ) ? "Premature EOF" : "Read Error:" . $errmsg;
948 0           return undef;
949             }
950              
951             sub read {
952 0 0 0 0     croak "Usage: Matts::Message::TNEF->read(fh, parameters) "
953             unless @_ == 2 or @_ == 3;
954 0           my $self = shift;
955 0   0       my $class = ref($self) || $self;
956 0           $self = {};
957 0           bless $self, $class;
958 0           my ( $fd, $parms ) = @_;
959            
960 0           my %parms = %dflts;
961 0 0         @parms{ keys %$parms } = values %$parms if defined $parms;
962 0           $parms = \%parms;
963 0           my $debug = $parms{debug};
964 0           my $ignore_checksum = $parms{ignore_checksum};
965              
966             # Start of TNEF stream
967 0           my $data;
968 0           my $num_bytes = read($fd, $data, 4 );
969 0 0         return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 4;
970 0 0         print "TNEF start: ", unpack( "H*", $data ), "\n" if $debug;
971 0 0         return _rtn_err( "Not TNEF-encapsulated", $fd, $parms )
972             unless $data eq $TNEF_SIGNATURE;
973              
974             # Key
975 0           $num_bytes = read($fd, $data, 2 );
976 0 0         return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 2;
977 0 0         print "TNEF key: ", unpack( "H*", $data ), "\n" if $debug;
978              
979             # Start of First Object
980 0           $num_bytes = read($fd, $data, 1 );
981 0 0         return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 1;
982              
983 0           my $msg_att = "";
984              
985 0           my $is_msg = ( $data eq $LVL_MESSAGE );
986 0           my $is_att = ( $data eq $LVL_ATTACHMENT );
987 0 0         print "TNEF object start: ", unpack( "H*", $data ), "\n" if $debug;
988 0 0 0       return _rtn_err( "Neither a message nor an attachment", $fd,
989             $parms )
990             unless $is_msg or $is_att;
991              
992 0           my $msg = Matts::Message::TNEF::Data->new;
993 0           my @atts;
994              
995             # Current message or attachment in loop
996 0           my $ent = $msg;
997              
998             # Read message and attachments
999 0 0         LOOP: {
1000 0           my $type = $is_msg ? 'message' : 'attachment';
1001 0 0         print "Reading $type attribute\n" if $debug;
1002 0           $num_bytes = read($fd, $data, 4 );
1003 0 0         return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 4;
1004 0           my $att_id = $data;
1005 0           my $att_name = $att_name{$att_id};
1006              
1007 0 0         print "TNEF $type attribute: ", unpack( "H*", $data ), "\n"
1008             if $debug;
1009 0 0         return _rtn_err( "Bad Attribute found in $type", $fd, $parms )
1010             unless $att_name{$att_id};
1011 0 0         if ( $att_id eq $att{TnefVersion} ) {
    0          
    0          
1012 0 0         return _rtn_err( "Version attribute found in attachment", $fd,
1013             $parms )
1014             if $is_att;
1015             } elsif ( $att_id eq $att{MessageClass} ) {
1016 0 0         return _rtn_err( "MessageClass attribute found in attachment",
1017             $fd, $parms )
1018             if $is_att;
1019             } elsif ( $att_id eq $att{AttachRenddata} ) {
1020 0 0         return _rtn_err( "AttachRenddata attribute found in message",
1021             $fd, $parms )
1022             if $is_msg;
1023 0           push @atts, ( $ent = Matts::Message::TNEF::Data->new );
1024             } else {
1025 0 0 0       return _rtn_err( "AttachRenddata must be first attribute", $fd,
      0        
1026             $parms )
1027             if $is_att
1028             and !@atts
1029             and $att_name ne "AttachRenddata";
1030             }
1031 0 0         print "Got attribute:$att_name{$att_id}\n" if $debug;
1032              
1033 0           $num_bytes = read($fd, $data, 4 );
1034 0 0         return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 4;
1035              
1036 0 0         print "HLength:", unpack( "H8", $data ), "\n" if $debug;
1037 0           my $length = unpack( "V", $data );
1038 0 0         print "Length: $length\n" if $debug;
1039              
1040             # Get the attribute data (returns an object since data may
1041             # actually end up in a file)
1042 0           my $calc_chksum;
1043 0 0         $data = _build_data( $fd, $length, \$calc_chksum, $parms )
1044             or return undef;
1045 0 0         _debug_print( $length, $att_id, $data, $parms ) if $debug;
1046 0           $ent->datahandle( $att_name, $data, $length );
1047              
1048 0           $num_bytes = read($fd, $data, 2 );
1049 0 0         return _read_err( $num_bytes, $fd, $! ) unless $num_bytes == 2;
1050 0           my $file_chksum = $data;
1051 0 0         if ($debug) {
1052 0           print "Calc Chksum:", unpack( "H*", $calc_chksum ), "\n";
1053 0           print "File Chksum:", unpack( "H*", $file_chksum ), "\n";
1054             }
1055 0 0 0       return _rtn_err( "Bad Checksum", $fd, $parms )
1056             unless $calc_chksum eq $file_chksum
1057             or $ignore_checksum;
1058              
1059 0           my $num_bytes = read($fd, $data, 1 );
1060              
1061             # EOF (0 bytes) is ok
1062 0 0         return _read_err( $num_bytes, $fd, $! ) unless defined $num_bytes;
1063 0 0         last LOOP if $num_bytes < 1;
1064 0 0         print "Next token:", unpack( "H2", $data ), "\n" if $debug;
1065 0           $is_msg = ( $data eq $LVL_MESSAGE );
1066 0 0 0       return _rtn_err( "Found message data in attachment", $fd, $parms )
1067             if $is_msg and $is_att;
1068 0           $is_att = ( $data eq $LVL_ATTACHMENT );
1069 0 0 0       redo LOOP if $is_msg or $is_att;
1070 0           return _rtn_err( "Not a TNEF $type", $fd, $parms );
1071             }
1072              
1073 0 0         print "EOF\n" if $debug;
1074              
1075 0           $self->{TN_Message} = $msg;
1076 0           $self->{TN_Attachments} = \@atts;
1077 0           return $self;
1078             }
1079              
1080             sub _debug_print {
1081 0     0     my ( $length, $att_id, $data, $parms ) = @_;
1082 0 0         if ( $length < $parms->{debug_max_display} ) {
1083 0           $data = $data->data;
1084 0 0 0       if ( $att_id eq $att{TnefVersion} ) {
    0 0        
    0          
1085 0           $data = unpack( "L", $data );
1086 0           print "Version: $data\n";
1087             } elsif ( substr( $att_id, 2 ) eq $atp{Date} and $length == 14 ) {
1088 0           my ( $yr, $mo, $day, $hr, $min, $sec, $dow ) =
1089             unpack( "vvvvvvv", $data );
1090 0           my $date = join ":", $yr, $mo, $day, $hr, $min, $sec, $dow;
1091 0           print "Date: $date\n";
1092 0           print "HDate:", unpack( "H*", $data ), "\n";
1093             } elsif ( $att_id eq $att{AttachRenddata} and $length == 14 ) {
1094 0           my ( $atyp, $ulPosition, $dxWidth, $dyHeight, $dwFlags ) =
1095             unpack( "vVvvV", $data );
1096 0           $data = join ":", $atyp, $ulPosition, $dxWidth, $dyHeight,
1097             $dwFlags;
1098 0           print "AttachRendData: $data\n";
1099             } else {
1100 0           print "Data:\n";
1101 0           print $1, "\n" while $data =~
1102             /([^\r\n]{0,$parms->{debug_max_line_size}})\r?\n?/g;
1103 0           print "HData:\n";
1104 0           my $hdata = unpack( "H*", $data );
1105 0           print $1, "\n"
1106             while $hdata =~ /(.{0,$parms->{debug_max_line_size}})/g;
1107             }
1108             } else {
1109 0 0         my $io = $data->open("r")
1110             or croak "Error opening attachment data handle: $!";
1111 0           my $buffer;
1112 0           CORE::read($io, $buffer, $parms->{debug_max_display} );
1113 0 0         close($io) or croak "Error closing attachment data handle: $!";
1114 0           print "Data:\n";
1115 0           print $1, "\n" while $buffer =~
1116             /([^\r\n]{0,$parms->{debug_max_line_size}})\r?\n?/sg;
1117 0           print "HData:\n";
1118 0           my $hdata = unpack( "H*", $buffer );
1119 0           print $1, "\n"
1120             while $hdata =~ /(.{0,$parms->{debug_max_line_size}})/g;
1121             }
1122             }
1123              
1124             sub _build_data {
1125 0     0     my ( $fd, $length, $chksumref, $parms ) = @_;
1126              
1127             # Just borrow some other objects for the attachment attribute data
1128 0           my $body = new Matts::Message::TNEF::Body _mk_fname($parms);;
1129 0           $body->binmode(1);
1130 0           my $io = $body->open("w");
1131 0           my $bufsiz = $parms->{buffer_size};
1132 0 0         $bufsiz = $length if $length < $bufsiz;
1133 0           my $buffer;
1134 0           my $chksum = 0;
1135              
1136 0           while ( $length > 0 ) {
1137 0           my $num_bytes = CORE::read($fd, $buffer, $bufsiz );
1138 0 0         return _read_err( $num_bytes, $fd, $! )
1139             unless $num_bytes == $bufsiz;
1140 0           $io->print($buffer);
1141 0           $chksum += unpack( "%16C*", $buffer );
1142 0           $chksum %= 65536;
1143 0           $length -= $bufsiz;
1144 0 0         $bufsiz = $length if $length < $bufsiz;
1145             }
1146 0           $$chksumref = pack( "v", $chksum );
1147 0           $io->close;
1148 0           return $body;
1149             }
1150              
1151             sub purge {
1152 0     0     my $self = shift;
1153 0           my $msg = $self->{TN_Message};
1154 0           my @atts = $self->attachments;
1155 0           for ( keys %$msg ) {
1156 0 0         $msg->{$_}->purge if exists $att{$_};
1157             }
1158 0           for my $attch (@atts) {
1159 0           for ( keys %$attch ) {
1160 0 0         $attch->{$_}->purge if exists $att{$_};
1161             }
1162             }
1163             }
1164              
1165             sub message {
1166 0     0     my $self = shift;
1167 0           $self->{TN_Message};
1168             }
1169              
1170             sub attachments {
1171 0     0     my $self = shift;
1172 0 0         return @{ $self->{TN_Attachments} } if wantarray;
  0            
1173 0           $self->{TN_Attachments};
1174             }
1175              
1176             # This is for Messages or Attachments
1177             # since they are essentially the same thing except
1178             # for the leading attribute code
1179             package Matts::Message::TNEF::Data;
1180              
1181             sub new {
1182 0     0     my $proto = shift;
1183 0   0       my $class = ref($proto) || $proto;
1184 0           my $self = {};
1185 0           $self->{TN_Size} = {};
1186 0           bless $self, $class;
1187             }
1188              
1189             sub data {
1190 0     0     my $self = shift;
1191 0   0       my $attr = shift || 'AttachData';
1192 0   0       return $self->{$attr} && $self->{$attr}->as_string;
1193             }
1194              
1195             sub name {
1196 0     0     my $self = shift;
1197 0   0       my $attr = shift || 'AttachTitle';
1198 0   0       my $name = $self->{$attr} && $self->{$attr}->data;
1199 0 0         $name =~ s/\x00+$// if $name;
1200 0           return $name;
1201             }
1202              
1203             # Try to get the long filename out of the
1204             # 'Attachment' attribute.
1205             sub longname {
1206 0     0     my $self = shift;
1207              
1208 0           my $data = $self->data("Attachment");
1209 0 0         return unless $data;
1210 0           my $pos = index( $data, pack( "H*", "1e00013001" ) );
1211 0 0         return $self->name unless $pos >= 0;
1212 0           my $len = unpack( "V", substr( $data, $pos + 8, 4 ) );
1213 0           my $longname = substr( $data, $pos + 12, $len );
1214 0 0         $longname =~ s/\x00+$// if $longname;
1215 0   0       return $longname || $self->name;
1216             }
1217              
1218             sub datahandle {
1219 0     0     my $self = shift;
1220 0   0       my $attr = shift || 'AttachData';
1221 0 0         $self->{$attr} = shift if @_;
1222 0 0         $self->size( $attr, shift ) if @_;
1223 0           return $self->{$attr};
1224             }
1225              
1226             sub size {
1227 0     0     my $self = shift;
1228 0   0       my $attr = shift || 'AttachData';
1229 0 0         $self->{TN_Size}->{$attr} = shift if @_;
1230 0           return $self->{TN_Size}->{$attr};
1231             }
1232              
1233             package Matts::Message::TNEF::Body;
1234              
1235             sub new {
1236 0     0     my $self = bless {}, shift;
1237 0           $self->init(@_);
1238 0           $self;
1239             }
1240              
1241             sub as_lines {
1242 0     0     my $self = shift;
1243 0           my @lines;
1244 0   0       my $io = $self->open("r") || return ();
1245 0           push @lines, $_ while (defined($_ = $io->getline()));
1246 0           $io->close;
1247 0           @lines;
1248             }
1249              
1250             sub as_string {
1251 0     0     my $self = shift;
1252 0           my $str = '';
1253 0           my $buf = '';
1254 0   0       my $io = $self->open("r") || return undef;
1255 0           my $nread = 0;
1256 0           $str .= $buf while ($nread = read($io, $buf, 2048));
1257 0           $io->close;
1258 0           return $str;
1259             }
1260             *data = \&as_string; ### silenty invoke preferred usage
1261              
1262             sub binmode {
1263 0     0     my ($self, $onoff) = @_;
1264 0 0         $self->{MB_Binmode} = $onoff if (@_ > 1);
1265 0           $self->{MB_Binmode};
1266             }
1267              
1268             sub dup {
1269 0     0     my $self = shift;
1270 0           bless { %$self }, ref($self); ### shallow copy ok for ::File and ::Scalar
1271             }
1272              
1273             sub path {
1274 0     0     my $self = shift;
1275 0 0         $self->{MB_Path} = shift if @_;
1276 0           $self->{MB_Path};
1277             }
1278              
1279             sub print {
1280 0     0     my ($self, $fh) = @_;
1281 0           my $nread;
1282              
1283             ### Write it:
1284 0           my $buf = '';
1285 0   0       my $io = $self->open("r") || return undef;
1286 0           $fh->print($buf) while ($nread = read($io, $buf, 2048));
1287 0           $io->close;
1288 0           return defined($nread); ### how'd we do?
1289             }
1290              
1291             sub init {
1292 0     0     my ($self, $path) = @_;
1293 0           $self->path($path); ### use it as-is
1294 0           $self;
1295             }
1296              
1297 1     1   4382 use FileHandle;
  1         2764  
  1         5  
1298              
1299             sub open {
1300 0     0     my ($self, $mode) = @_;
1301 0           my $IO;
1302 0           my $path = $self->path;
1303 0 0         if ($mode eq 'w') { ### writing
    0          
1304 0   0       $IO = FileHandle->new(">$path") || die "write-open $path: $!";
1305             }
1306             elsif ($mode eq 'r') { ### reading
1307 0   0       $IO = FileHandle->new("<$path") || die "read-open $path: $!";
1308             }
1309             else {
1310 0           die "bad mode: '$mode'";
1311             }
1312 0 0         CORE::binmode($IO) if $self->binmode; ### set binary read/write mode?
1313 0           return $IO;
1314             }
1315              
1316             sub purge {
1317 0     0     my $self = shift;
1318 0 0         if (defined($self->path)) {
1319 0 0         unlink $self->path or die("couldn't unlink ".$self->path.": $!");
1320 0           $self->path(undef);
1321             }
1322 0           1;
1323             }
1324              
1325             1;
1326             __END__