File Coverage

blib/lib/Mail/Exchange/Message.pm
Criterion Covered Total %
statement 146 251 58.1
branch 27 40 67.5
condition 2 5 40.0
subroutine 22 35 62.8
pod 17 18 94.4
total 214 349 61.3


line stmt bran cond sub pod time code
1             package Mail::Exchange::Message;
2              
3             =head1 NAME
4              
5             Mail::Exchange::Message - class to deal with .msg files, used by Microsoft
6             Exchange / MS Outlook.
7              
8             =head1 SYNOPSIS
9              
10             use Mail::Exchange::Message;
11             use Mail::Exchange::Message::MessageFlags;
12             use Mail::Exchange::Recipient;
13             use Mail::Exchange::Attachment;
14             use Mail::Exchange::PidTagIDs;
15              
16             # modify an existing .msg file
17              
18             my $msg=Mail::Exchange::Message->new("my.msg");
19             print "old Subject: ", $msg->get(PidTagSubject), "\n";
20             $msg->setSubject('new subject');
21             $msg->save("changed.msg");
22              
23             # create a .msg file from scratch, and send it to
24             # the browser from a CGI script
25              
26             my $msg=Mail::Exchange::Message->new();
27             $msg->setUnicode(1);
28             $msg->setSubject('message subject');
29             $msg->setBody('message body');
30             $msg->set(PidTagMessageFlags, mfUnsent);
31             $message->setDisplayTo('test@somewhere.com');
32              
33             my $recipient=Mail::Exchange::Recipient->new();
34             $recipient->setEmailAddress('test@somewhere.com');
35             $recipient->setDisplayName('John Tester');
36             $recipient->setRecipientType('To');
37             $message->addRecipient($recipient);
38              
39             my $attachment=Mail::Exchange::Message->new("attach.dat");
40             $message->addAttachment($attachment);
41              
42             binmode(STDOUT);
43             print STDOUT qq(Content-type: application/vnd.ms-outlook
44             Content-Disposition: attachment; filename="newmessage.msg"
45              
46             );
47              
48             $message->save(\*STDOUT);
49              
50             =head1 DESCRIPTION
51              
52             Mail::Exchange::Message allows you to read and write binary message files that
53             Microsoft Outlook uses to store emails, to-dos, appointments and so on. It does
54             not need Windows, or Outlook, installed, and should be able to run on any
55             operating system that supports perl.
56              
57             It might have been named "Outlook" instead of "Exchange", but the
58             "Mail::Outlook" and "Email::Outlook" namespaces had both been taken at the
59             time of its implementation, and it contains some sub-modules that might be
60             helpful to implementations of more functionality with Microsoft Exchange,
61             with which it is intended to coexist.
62              
63             =cut
64              
65              
66 5     5   224816 use strict;
  5         14  
  5         206  
67 5     5   28 use warnings;
  5         10  
  5         201  
68 5     5   141 use 5.008;
  5         23  
  5         1017  
69              
70 5     5   30 use Exporter;
  5         9  
  5         293  
71 5     5   13617 use Encode;
  5         79369  
  5         602  
72 5     5   3538 use Mail::Exchange::Time;
  5         13  
  5         229  
73 5     5   4631 use Mail::Exchange::PidTagIDs;
  5         18  
  5         14000  
74 5     5   5542 use Mail::Exchange::PidTagDefs;
  5         20  
  5         1051  
75 5     5   3529 use Mail::Exchange::PropertyContainer;
  5         110  
  5         282  
76 5     5   3450 use Mail::Exchange::NamedProperties;
  5         21  
  5         363  
77 5     5   3296 use Mail::Exchange::Recipient;
  5         15  
  5         295  
78 5     5   3076 use Mail::Exchange::Attachment;
  5         15  
  5         247  
79 5     5   3347 use Mail::Exchange::Message::MessageFlags;
  5         19  
  5         505  
80 5     5   34 use OLE::Storage_Lite;
  5         11  
  5         191  
81              
82 5     5   33 use vars qw($VERSION @ISA);
  5         7  
  5         13921  
83             @ISA=qw(Mail::Exchange::PropertyContainer Exporter);
84              
85             $VERSION = "0.04";
86              
87             =head2 new()
88              
89             $msg=Mail::Exchange::Message->new([$file])
90              
91             Read a message from the .msg file C<$file>, or create a new, empty one,
92             if C<$file> isn't given.
93              
94             =cut
95              
96             sub new {
97 1     1 1 728 my $class=shift;
98 1         2 my $file=shift;
99              
100 1         13 my $self=Mail::Exchange::PropertyContainer->new();
101 1         3 bless($self, $class);
102              
103 1         10 $self->{_recipients}=();
104 1         3 $self->{_attachments}=();
105 1         10 $self->{_namedProperties}=Mail::Exchange::NamedProperties->new();
106            
107 1 50       4 if ($file) {
108 1         5 $self->parse($file);
109             } else {
110             # these are taken from [MS-OXCMSG] 3.2.5.2
111             # PidTagMessageClass is NOT initialized, there are
112             # subclasses for that.
113 0         0 my $now=Mail::Exchange::Time->new(time());
114 0         0 $self->set(PidTagImportance, 1);
115 0         0 $self->set(PidTagSensitivity, 0);
116 0         0 $self->set(PidTagDisplayBcc, "");
117 0         0 $self->set(PidTagDisplayCc, "");
118 0         0 $self->set(PidTagDisplayTo, "");
119 0         0 $self->set(PidTagMessageFlags, 9);
120 0         0 $self->set(PidTagMessageSize, 1);
121 0         0 $self->set(PidTagHasAttachments, 0);
122 0         0 $self->set(PidTagTrustSender, 1);
123 0         0 $self->set(PidTagAccess, 3);
124 0         0 $self->set(PidTagAccessLevel, 3);
125 0         0 $self->set(PidTagUrlCompName, "No Subject.EML");
126 0         0 $self->set(PidTagCreationTime, $now->mstime());
127 0         0 $self->set(PidTagLastModificationTime, $now->mstime());
128             }
129              
130 1         217 $self;
131             }
132              
133             =head2 parse()
134              
135             $msg->parse($file)
136              
137             Read a message file into an internal structure. Called from new() if a
138             filename argument is given. C<$file> is expected to be a string, but may
139             be anything that is accepted by OLE::Storage_Lite.
140              
141             =cut
142              
143             sub parse {
144 1     1 1 2 my $self=shift;
145 1         3 my $file=shift;
146 1         9 my $OLEFile = OLE::Storage_Lite->new($file);
147 1         12 my $root=$OLEFile->getPpsTree(1);
148 1 50       27340 die "$file does not seem to be an OLE File" unless $root;
149              
150 1         9 my $nameid=Encode::encode("UCS2LE", "__nameid_version1.0");
151 1         5422 foreach my $entry (@{$root->{Child}}) {
  1         7  
152 22 100       60 if ($entry->{Name} eq $nameid) {
153 1         7 $self->_parsePropertyNames($entry);
154             }
155             }
156              
157 1         6 my $propid=Encode::encode("UCS2LE", "__properties_version1.0");
158 1         54 foreach my $entry (@{$root->{Child}}) {
  1         4  
159 22 100       46 if ($entry->{Name} eq $propid) {
160 1         5 $self->_parseMessageProperties($entry, $root);
161             }
162             }
163              
164 1         3 foreach my $entry (@{$root->{Child}}) {
  1         5  
165             # print Encode::decode("UCS2LE", $entry->{Name}), "\n";
166 22 100       605 if (Encode::decode("UCS2LE", $entry->{Name})
167             =~ /__recip_version1.0_#([0-9A-F]{8})/) {
168 1         42 my $idx=hex($1);
169 1         3 foreach my $subentry (@{$entry->{Child}}) {
  1         4  
170 10 100       26 if ($subentry->{Name} eq $propid) {
171 1         15 $self->{_recipients}[$idx]=Mail::Exchange::Recipient->new();
172 1         8 $self->{_recipients}[$idx]->_parseRecipientProperties(
173             $subentry, $entry, $self->{_namedProperties});
174             }
175             }
176             }
177              
178 22 50       600 if (Encode::decode("UCS2LE", $entry->{Name})
179             =~ /__attach_version1.0_#([0-9A-F]{8})/) {
180 0         0 my $idx=hex($1);
181 0         0 foreach my $subentry (@{$entry->{Child}}) {
  0         0  
182 0 0       0 if ($subentry->{Name} eq $propid) {
183 0         0 $self->{_attachments}[$idx]=Mail::Exchange::Attachment->new();
184 0         0 $self->{_attachments}[$idx]->_parseAttachmentProperties(
185             $subentry, $entry, $self->{_namedProperties});
186             }
187             }
188             }
189             }
190             }
191              
192             sub _parsePropertyNames {
193 1     1   3 my $self=shift;
194 1         3 my $dir=shift;
195              
196 1         2 my $stringstreamdata;
197             my $guidstreamdata;
198 1         6 my $ssid=Encode::encode("UCS2LE", "__substg1.0_00040102");
199 1         45 my $gsid=Encode::encode("UCS2LE", "__substg1.0_00020102");
200 1         28 foreach my $item (@{$dir->{Child}}) {
  1         4  
201 24 100       69 if ($item->{Name} eq $ssid) {
202 1         4 $stringstreamdata=$item->{Data};
203             }
204 24 100       43 if ($item->{Name} eq $gsid) {
205 1         3 $guidstreamdata=$item->{Data};
206             }
207             }
208              
209 1         5 my $psid=Encode::encode("UCS2LE", "__substg1.0_00030102");
210 1         32 foreach my $item (@{$dir->{Child}}) {
  1         3  
211 24 100       66 if ($item->{Name} eq $psid) {
212 1         5 my $data=$item->{Data};
213 1         4 while ($data ne "") {
214 27         55 my ($niso, $iko)=unpack("VV", $data);
215 27         36 my $pi=($iko>>16)&0xffff;
216 27         34 my $gi=($iko>>1)&0x7fff;
217 27         27 my $pk=$iko&1;
218 27         27 my $guid;
219             my $name;
220 27 50       45 if ($gi==1) { $guid="PS_MAPI"; }
  0         0  
221 27 50       45 if ($gi==2) { $guid="PS_PUBLIC_STRINGS"; }
  0         0  
222 27 50       51 if ($gi>2) { $guid=GUIDDecode(substr($guidstreamdata, 16*($gi-3), 16)); }
  27         92  
223              
224             # We don't know the type here, so we just
225             # add the property with undef type. The type
226             # will be set later when we actually read
227             # the value from the properties stream.
228 27 50       117 if ($pk==0) {
229 27         87 $self->{_namedProperties}->namedPropertyIndex(
230             $niso, undef, $guid);
231             } else {
232 0         0 my $len=unpack("V", substr($stringstreamdata, $niso, 4));
233 0         0 $name=Encode::decode("UCS2LE", substr($stringstreamdata, $niso+4, $len));
234 0         0 $self->{_namedProperties}->namedPropertyIndex($name, undef, $guid);
235             # @@@ die if returncode != $pi ??
236             }
237 27         87 $data=substr($data, 8);
238             }
239             }
240             }
241             }
242              
243             sub _parseMessageProperties {
244 1     1   3 my $self=shift;
245 1         2 my $file=shift;
246 1         4 my $dir=shift;
247              
248 1         25 $self->_parseProperties($file, $dir, 32, $self->{_namedProperties});
249             }
250              
251             =head2 set()
252              
253             $msg->set($tag, $value, [$flags,] [$type,] [$guid])
254              
255             Set a property within a message. C<$tag> can be any numeric property defined in
256             Mail::Exchange::PidTagIDs.pm, a numeric named property defined in
257             Mail::Exchange::PidLidIDs.pm, or a string property. C<$value> is the value
258             the property is set to.
259              
260             C<$flags> is a bit-wise or of 1 (this property is mandatory and must not be
261             deleted), 2 (property is writable) and 4 (property is readable and may be
262             displayed to the user). Default is 6.
263              
264             When a string named property is defined for the first time, its C<$type>
265             and C<$guid> must be given as well, as stated [MS-OXPROPS], section 1.3.2
266              
267             =cut
268              
269             sub set {
270 59     59 1 103 my ($self, $tag, $value, $flags, $type, $guid) = @_;
271 59         179 Mail::Exchange::PropertyContainer::set($self, $tag, $value,
272             $flags, $type, $guid, $self->{_namedProperties});
273             }
274              
275             sub get {
276 10     10 0 20 my ($self, $tag) = @_;
277 10         53 return Mail::Exchange::PropertyContainer::get($self, $tag,
278             $self->{_namedProperties});
279             }
280              
281             =head2 setSender()
282              
283             $msg->setSender($address)
284              
285             setSender is a shortcut for setting various properties that
286             descripe the sender of a message.
287              
288             =cut
289              
290             sub setSender {
291 0     0 1 0 my $self=shift;
292 0         0 my $sender=shift;
293              
294 0         0 $self->set(PidTagSentRepresentingAddressType, "SMTP");
295 0         0 $self->set(PidTagSentRepresentingName, $sender);
296 0         0 $self->set(PidTagSentRepresentingEmailAddress, $sender);
297              
298 0         0 $self->set(PidTagSenderAddressType, "SMTP");
299 0         0 $self->set(PidTagSenderName, $sender);
300 0         0 $self->set(PidTagSenderEmailAddress, $sender);
301 0         0 $self->set(PidTagSenderSmtpAddress, $sender);
302             # $self->set(0x5D02, $sender);
303             }
304              
305             =head2 setDisplayTo()
306              
307             $msg->setDisplayTo($text)
308              
309             setDisplayTo sets the recipient list that is shown by outlook in the
310             "To:" address line.
311              
312             =cut
313              
314             sub setDisplayTo {
315 0     0 1 0 my $self=shift;
316 0         0 my $recipient=shift;
317              
318 0         0 $self->set(PidTagDisplayTo, $recipient);
319             }
320              
321             =head2 setDisplayCc()
322              
323             $msg->setDisplayCc($text)
324              
325             setDisplayCc sets the recipient list that is shown by outlook in the
326             "Cc:" address line.
327              
328             =cut
329              
330             sub setDisplayCc {
331 0     0 1 0 my $self=shift;
332 0         0 my $recipient=shift;
333              
334 0         0 $self->set(PidTagDisplayCc, $recipient);
335             }
336              
337             =head2 setDisplayBcc()
338              
339             $msg->setDisplayBcc($text)
340              
341             setDisplayBcc sets the recipient list that is shown by outlook in the
342             "Bcc:" address line.
343              
344             =cut
345              
346             sub setDisplayBcc {
347 0     0 1 0 my $self=shift;
348 0         0 my $recipient=shift;
349              
350 0         0 $self->set(PidTagDisplayBcc, $recipient);
351             }
352              
353             =head2 setSubject()
354              
355             $msg->setSubject($text)
356              
357             setSubject sets the subject of the message by setting various internal
358             properties.
359              
360             =cut
361              
362             sub setSubject {
363 0     0 1 0 my $self=shift;
364 0         0 my $subject=shift;
365              
366 0         0 $self->set(PidTagSubject, $subject);
367 0         0 $self->set(PidTagSubjectPrefix, "");
368 0         0 $self->set(PidTagConversationTopic, $subject);
369 0         0 $self->set(PidTagNormalizedSubject, $subject);
370             }
371              
372             =head2 setBody()
373              
374             $msg->setBody($text)
375              
376             setBody sets the plain text body of the message.
377              
378             =cut
379              
380             sub setBody {
381 0     0 1 0 my $self=shift;
382 0         0 my $body=shift;
383              
384 0         0 $self->set(PidTagBody, $body);
385             }
386              
387             =head2 setHTMLBody()
388              
389             $msg->setHTMLBody($text)
390              
391             setHTMLBody sets the html body of the message.
392              
393             =cut
394              
395             sub setHTMLBody {
396 0     0 1 0 my $self=shift;
397 0         0 my $body=shift;
398              
399 0         0 $self->set(PidTagHtml, $body);
400             }
401              
402             =head2 setRtfBody()
403              
404             setRtfBody($text[, $compress])
405              
406             setRtfBody sets the rich text format body of the message.
407              
408             The message file format allows for compressed or uncompressed
409             rtf storage. If C<$compress> is set, C<$text> will be compressed before being
410             stored. (As of now, C<$compress> is not implemented, and C<$text> will always
411             be stored in uncompressed form).
412              
413             =cut
414              
415             sub setRtfBody {
416 0     0 1 0 my $self=shift;
417 0         0 my $body=shift;
418              
419             # OXRTFCP says CRC MUST be 0 when uncompressed.
420              
421 0         0 my $header=pack("VVVV", length($body)+12, length($body), 0x414c454D, 0);
422 0         0 $self->set(PidTagRtfCompressed, $header.$body);
423 0         0 $self->set(PidTagRtfInSync, 1);
424             }
425              
426             =head2 getRtfBody()
427              
428             $msg->getRtfBody()
429              
430             getRtfBody gets the RTF Body of a message, uncompressing it if neccesary.
431              
432             =cut
433              
434             sub getRtfBody {
435 1     1 1 3 my $self=shift;
436 1         9 my $rtf=$self->get(PidTagRtfCompressed);
437 1         22 my ($compsize, $rawsize, $comptype, $crc)=unpack("VVVV", $rtf);
438 1         15 $rtf=substr($rtf, 16);
439              
440 1 50       9 if ($comptype == 0x414c454D) {
    50          
441 0         0 return $rtf;
442             } elsif ($comptype != 0x75465a4c) {
443 0         0 die(sprintf("rtf compression type %08x unknown", $comptype));
444             }
445              
446 1         4 my $dictionary='{\rtf1\ansi\mac\deff0\deftab720{\fonttbl;}'.
447             '{\f0\fnil \froman \fswiss \fmodern '.
448             '\fscript \fdecor MS Sans SerifSymbolArialTimes'.
449             ' New RomanCourier{\colortbl\red0\green0\blue0'.
450             "\r\n".'\par \pard\plain\f0\fs20\b\i\u\tab\tx';
451              
452 1         3 my $dpos=207;
453 1         2 my $rpos=0;
454 1         3 my $rlen=length $rtf;
455 1         3 my $output='';
456             RTFTEXT:
457 1         5 while ($rpos<$rlen) {
458 372         1577 my $control=unpack("C", substr($rtf, $rpos++, 1));
459 372   66     1660 for (my $i=0; $i<8 && $rpos<$rlen; $i++) {
460 2975         3499 my $newbyte;
461             my $ofs;
462 0         0 my $len;
463 2975 100       4916 if ($control & (1<<$i)) {
464 2184         6337 my $ref=unpack("n", substr($rtf, $rpos));
465 2184         2960 $rpos+=2;
466 2184         2547 $ofs=$ref>>4;
467 2184         4526 $len=($ref&0x0f)+2;
468 2184 100       4860 if ($ofs==($dpos%4096)) {
469 1         8 last RTFTEXT;
470             }
471 2183         4908 for (my $j=0; $j<$len; $j++) {
472 23289         58403 $newbyte=substr($dictionary, ($ofs++%4096), 1);
473 23289         42235 substr($dictionary, ($dpos++%4096), 1)=$newbyte;
474 23289         70541 $output.=$newbyte;
475              
476             }
477             } else {
478 791         1217 $newbyte=substr($rtf, $rpos++, 1);
479 791         825 $output.=$newbyte;
480 791         3502 substr($dictionary, ($dpos++%4096), 1)=$newbyte;
481             }
482             }
483             }
484 1         64 return $output;
485             }
486              
487             =head2 setHtmlBody()
488              
489             $msg->setHtmlBody($htmltext)
490              
491             setHtmlBody sets the html version of the message body. It is a shortcut
492             for C<$msg->set(PidTagHtml, $htmltext)>.
493              
494             =cut
495              
496             sub setHtmlBody {
497 0     0 1   my $self=shift;
498 0           my $body=shift;
499              
500 0           $self->set(PidTagHtml, $body);
501             }
502              
503             =head2 setUnicode()
504              
505             $msg->setUnicode($flag)
506              
507             If C<$flag> is 0, all strings within the message will be stored one byte per
508             character. If C<$flag> is 1, strings will be stored in what's called unicode
509             in the documentation (actually, UCS2LE encoded strings, using 2 bytes per
510             character).
511              
512             =cut
513              
514             sub setUnicode {
515 0     0 1   my $self=shift;
516 0           my $flag=shift;
517 0           my $mask=$self->get(PidTagStoreSupportMask);
518 0 0         if ($flag) {
519 0           $mask |= 0x40000;
520             } else {
521 0           $mask &= ~0x40000;
522             }
523 0           $self->set(PidTagStoreSupportMask, $mask);
524             }
525              
526             =head2 save()
527              
528             save($msgfile)
529              
530             Saves a message object to a file. $msgfile may be a file, or anything else
531             that OLE::Storage_Lite accepts.
532              
533             =cut
534              
535             sub save {
536 0     0 1   my $self=shift;
537 0           my $output=shift;
538              
539 0           my @streams=();
540 0           push(@streams, $self->{_namedProperties}->OleContainer());
541              
542 0           my $unicode=$self->get(PidTagStoreSupportMask)&0x40000;
543 0           my $header=pack("V8",
544             0, 0,
545 0           $#{$self->{_recipients}}+1,
546 0           $#{$self->{_attachments}}+1,
547 0           $#{$self->{_recipients}}+1,
548 0           $#{$self->{_attachments}}+1,
549             0, 0);
550              
551              
552 0           push(@streams, $self->_OlePropertyStreamlist($unicode, $header));
553              
554 0           foreach my $i (0..$#{$self->{_recipients}}) {
  0            
555 0           push(@streams, $self->{_recipients}[$i]->OleContainer($i, $unicode));
556             }
557              
558 0           foreach my $i (0..$#{$self->{_attachments}}) {
  0            
559 0           push(@streams, $self->{_attachments}[$i]->OleContainer($i, $unicode));
560             }
561              
562 0           my @ltime=localtime();
563 0           my $root=OLE::Storage_Lite::PPS::Root->new(\@ltime, \@ltime, \@streams);
564 0           $root->save($output);
565             }
566              
567             =head2 addAttachment()
568              
569             addAttachment($object)
570              
571             Adds a Mail::Exchange::Attachment object to a message.
572              
573             =cut
574              
575             sub addAttachment($$) {
576 0     0 1   my $self=shift;
577 0           my $attachment=shift;
578              
579 0           push(@{$self->{_attachments}}, $attachment);
  0            
580 0           $self->set(PidTagHasAttachments, 1);
581 0   0       my $flags=$self->get(PidTagMessageFlags) || 0;
582 0           $self->set(PidTagMessageFlags, $flags | mfHasAttach);
583             }
584              
585             =head2 addRecipient()
586              
587             addRecipient($object)
588              
589             Adds a Mail::Exchange::Recipient object to a message.
590              
591             =cut
592              
593             sub addRecipient($$) {
594 0     0 1   my $self=shift;
595 0           my $recipient=shift;
596              
597 0           push(@{$self->{_recipients}}, $recipient);
  0            
598             }
599              
600             __END__