File Coverage

blib/lib/Email/Outlook/Message/Base.pm
Criterion Covered Total %
statement 121 156 77.5
branch 28 52 53.8
condition 3 5 60.0
subroutine 24 26 92.3
pod 5 5 100.0
total 181 244 74.1


line stmt bran cond sub pod time code
1             package Email::Outlook::Message::Base;
2             =head1 NAME
3              
4             Email::Outlook::Message::Base - Base parser for .msg files.
5              
6             =head1 DESCRIPTION
7              
8             This is an internal module of Email::Outlook::Message.
9              
10             =head1 METHODS
11              
12             =over 8
13              
14             =item B
15              
16             =item B
17              
18             =item B
19              
20             =item B
21              
22             =item B
23              
24             =back
25              
26             =head1 AUTHOR
27              
28             Matijs van Zuijlen, C
29              
30             =head1 COPYRIGHT AND LICENSE
31              
32             Copyright 2002--2014 by Matijs van Zuijlen
33              
34             This module is free software; you can redistribute it and/or modify
35             it under the same terms as Perl itself.
36              
37             =cut
38 7     7   46 use strict;
  7         14  
  7         204  
39 7     7   32 use warnings;
  7         11  
  7         141  
40 7     7   29 use Encode;
  7         12  
  7         414  
41 7     7   1811 use IO::String;
  7         19418  
  7         197  
42 7     7   2021 use POSIX;
  7         33488  
  7         36  
43 7     7   16295 use Carp;
  7         18  
  7         358  
44 7     7   3060 use OLE::Storage_Lite;
  7         101592  
  7         11440  
45              
46             my $DIR_TYPE = 1;
47             my $FILE_TYPE = 2;
48              
49             # Variable encodings
50             my $ENCODING_UNICODE = '001F';
51             my $ENCODING_ASCII = '001E';
52             my $ENCODING_BINARY = '0102';
53             my $ENCODING_DIRECTORY = '000D';
54              
55             our $VARIABLE_ENCODINGS = {
56             '000D' => 'Directory',
57             '001F' => 'Unicode',
58             '001E' => 'Ascii?',
59             '0102' => 'Binary',
60             };
61              
62             # Fixed encodings
63             my $ENCODING_INTEGER16 = '0002';
64             my $ENCODING_INTEGER32 = '0003';
65             my $ENCODING_BOOLEAN = '000B';
66             my $ENCODING_DATE = '0040';
67              
68             #
69             # Descriptions partially based on mapitags.h
70             #
71             our $skipproperties = {
72             # Envelope properties
73             '0002' => "Alternate Recipient Allowed",
74             '000B' => "Conversation Key",
75             '0017' => "Importance", #TODO: Use this.
76             '001A' => "Message Class",
77             '0023' => "Originator Delivery Report Requested",
78             '0026' => "Priority", #TODO: Use this.
79             '0029' => "Read Receipt Requested", #TODO: Use this.
80             '0036' => "Sensitivity", # As assessed by the Sender
81             '003B' => "Sent Representing Search Key",
82             '003D' => "Subject Prefix",
83             '003F' => "Received By EntryId",
84             '0040' => "Received By Name",
85             # TODO: These two fields are part of the Sender field.
86             '0041' => "Sent Representing EntryId",
87             '0042' => "Sent Representing Name",
88             '0043' => "Received Representing EntryId",
89             '0044' => "Received Representing Name",
90             '0046' => "Read Receipt EntryId",
91             '0051' => "Received By Search Key",
92             '0052' => "Received Representing Search Key",
93             '0053' => "Read Receipt Search Key",
94             # TODO: These two fields are part of the Sender field.
95             '0064' => "Sent Representing Address Type",
96             '0065' => "Sent Representing Email Address",
97             '0070' => "Conversation Topic",
98             '0071' => "Conversation Index",
99             '0075' => "Received By Address Type",
100             '0076' => "Received By Email Address",
101             '0077' => "Received Representing Address Type",
102             '0078' => "Received Representing Email Address",
103             '007F' => "TNEF Correlation Key",
104             # Recipient properties
105             '0C15' => "Recipient Type",
106             # Sender properties
107             '0C19' => "Sender Entry Id",
108             '0C1D' => "Sender Search Key",
109             '0C1E' => "Sender Address Type",
110             # Non-transmittable properties
111             '0E02' => "Display Bcc",
112             '0E06' => "Message Delivery Time",
113             '0E07' => "Message Flags",
114             '0E0A' => "Sent Mail EntryId",
115             '0E0F' => "Responsibility",
116             '0E1B' => "Has Attachments",
117             '0E1D' => "Normalized Subject",
118             '0E1F' => "RTF In Sync",
119             '0E20' => "Attachment Size",
120             '0E21' => "Attachment Number",
121             '0E23' => "Internet Article Number",
122             '0E27' => "Security Descriptor",
123             '0E79' => "Trust Sender",
124             '0FF4' => "Access",
125             '0FF6' => "Instance Key",
126             '0FF7' => "Access Level",
127             '0FF9' => "Record Key",
128             '0FFE' => "Object Type",
129             '0FFF' => "EntryId",
130             # Content properties
131             '1006' => "RTF Sync Body CRC",
132             '1007' => "RTF Sync Body Count",
133             '1008' => "RTF Sync Body Tag",
134             '1010' => "RTF Sync Prefix Count",
135             '1011' => "RTF Sync Trailing Count",
136             '1046' => "Original Message ID",
137             '1080' => "Icon Index",
138             '1081' => "Last Verb Executed",
139             '1082' => "Last Verb Execution Time",
140             '10F3' => "URL Component Name",
141             '10F4' => "Attribute Hidden",
142             '10F5' => "Attribute System",
143             '10F6' => "Attribute Read Only",
144             # 'Common property'
145             '3000' => "Row Id",
146             '3001' => "Display Name",
147             '3002' => "Address Type",
148             '3007' => "Creation Time",
149             '3008' => "Last Modification Time",
150             '300B' => "Search Key",
151             # Message store info
152             '340D' => "Store Support Mask",
153             '3414' => "Message Store Provider",
154             # Attachment properties
155             '3702' => "Attachment Encoding",
156             '3703' => "Attachment Extension",
157             # TODO: Use the following to distinguish between nested msg and other OLE
158             # stores.
159             '3705' => "Attachment Method",
160             '3709' => "Attachment Rendering", # Icon as WMF
161             '370A' => "Tag identifying application that supplied the attachment",
162             '370B' => "Attachment Rendering Position",
163             '3713' => "Attachment Content Location", #TODO: Use this?
164             # 3900 -- 39FF: 'Address book'
165             '3900' => "Address Book Display Type",
166             '39FF' => "Address Book 7 Bit Display Name",
167             # Mail User Object
168             '3A00' => "Account",
169             '3A20' => "Transmittable Display Name",
170             '3A40' => "Send Rich Info",
171             '3FDE' => "Internet Code Page", # TODO: Perhaps use this.
172             # 'Display table properties'
173             '3FF8' => "Creator Name",
174             '3FF9' => "Creator EntryId",
175             '3FFA' => "Last Modifier Name",
176             '3FFB' => "Last Modifier EntryId",
177             '3FFD' => "Message Code Page",
178             # 'Transport-defined envelope property'
179             '4019' => "Sender Flags",
180             '401A' => "Sent Representing Flags",
181             '401B' => "Received By Flags",
182             '401C' => "Received Representing Flags",
183             '4029' => "Read Receipt Address Type",
184             '402A' => "Read Receipt Email Address",
185             '402B' => "Read Receipt Name",
186             '5FF6' => "Recipient Display Name",
187             '5FF7' => "Recipient EntryId",
188             '5FFD' => "Recipient Flags",
189             '5FFF' => "Recipient Track Status",
190             # 'Provider-defined internal non-transmittable property'
191             '664A' => "Has Named Properties",
192             '6740' => "Sent Mail Server EntryId",
193             };
194              
195             sub new {
196 7     7 1 19 my ($class, $pps, $verbose) = @_;
197 7         26 my $self = bless {
198             _pps_file_entries => {},
199             _pps => $pps
200             }, $class;
201 7         34 $self->_set_verbosity($verbose);
202 7         156 $self->_process_pps($pps);
203 7         25 return $self;
204             }
205              
206             sub mapi_property_names {
207 12     12 1 29 my $self = shift;
208 12         22 return keys %{$self->{_pps_file_entries}};
  12         80  
209             }
210              
211             sub get_mapi_property {
212 89     89 1 545 my ($self, $code) = @_;
213 89         184 return $self->{_pps_file_entries}->{$code};
214             }
215              
216             sub set_mapi_property {
217 303     303 1 459 my ($self, $code, $data) = @_;
218 303         520 $self->{_pps_file_entries}->{$code} = $data;
219 303         446 return;
220             }
221              
222             sub property {
223 25     25 1 45 my ($self, $name) = @_;
224 25         54 my $map = $self->_property_map;
225             # TODO: Prepare reverse map instead of doing dumb lookup.
226 25         37 foreach my $code (keys %{$map}) {
  25         64  
227 98         130 my $key = $map->{$code};
228 98 100       169 next unless $key eq $name;
229 25         40 my $prop = $self->get_mapi_property($code);
230 25 100       57 if ($prop) {
231 20         28 my ($encoding, $data) = @{$prop};
  20         39  
232 20         46 return $self->_decode_mapi_property($encoding, $data);
233             } else {
234 5         27 return;
235             }
236             }
237 0         0 return;
238             }
239              
240             sub _decode_mapi_property {
241 83     83   129 my ($self, $encoding, $data) = @_;
242              
243 83 100 100     225 if ($encoding eq $ENCODING_ASCII or $encoding eq $ENCODING_UNICODE) {
244 69 100       114 if ($encoding eq $ENCODING_UNICODE) {
245 22         51 $data = decode("UTF-16LE", $data);
246             }
247 69         5296 $data =~ s/ \000 $ //sgx;
248 69         191 return $data;
249             }
250              
251 14 100       31 if ($encoding eq $ENCODING_BINARY) {
252 7         18 return $data;
253             }
254              
255 7 50       26 if ($encoding eq $ENCODING_DATE) {
256 7         20 my @a = OLE::Storage_Lite::OLEDate2Local $data;
257 7         113 return $self->_format_date(\@a);
258             }
259              
260 0 0       0 if ($encoding eq $ENCODING_INTEGER16) {
261 0         0 return unpack("v", substr($data, 0, 2));
262             }
263              
264 0 0       0 if ($encoding eq $ENCODING_INTEGER32) {
265 0         0 return unpack("V", substr($data, 0, 4));
266             }
267              
268 0 0       0 if ($encoding eq $ENCODING_BOOLEAN) {
269 0         0 return unpack("C", substr($data, 0, 1));
270             }
271              
272 0         0 warn "Unhandled encoding $encoding\n";
273 0         0 return $data;
274             }
275              
276             sub _process_pps {
277 12     12   26 my ($self, $pps) = @_;
278 12         22 foreach my $child (@{$pps->{Child}}) {
  12         73  
279 200 100       415 if ($child->{Type} == $DIR_TYPE) {
    50          
280 12         36 $self->_process_subdirectory($child);
281             } elsif ($child->{Type} == $FILE_TYPE) {
282 188         321 $self->_process_pps_file_entry($child);
283             } else {
284 0         0 carp "Unknown entry type: $child->{Type}";
285             }
286             }
287 12         40 $self->_check_pps_file_entries($self->_property_map);
288 12         20 return;
289             }
290              
291             sub _get_pps_name {
292 205     205   275 my ($self, $pps) = @_;
293 205         359 my $name = OLE::Storage_Lite::Ucs2Asc($pps->{Name});
294 205         3030 $name =~ s/ \W / /gx;
295 205         386 return $name;
296             }
297              
298             sub _parse_item_name {
299 188     188   290 my ($self, $name) = @_;
300              
301 188 100       477 if ($name =~ / ^ __substg1 [ ] 0_ (....) (....) $ /x) {
302 176         380 my ($property, $encoding) = ($1, $2);
303 176         401 return ($property, $encoding);
304             } else {
305 12         28 return (undef, undef);
306             }
307             }
308              
309             sub _warn_about_unknown_directory {
310 5     5   13 my ($self, $pps) = @_;
311              
312 5         14 my $name = $self->_get_pps_name($pps);
313 5 50       17 if ($name eq '__nameid_version1 0') {
314             # TODO: Use this data to access so-called named properties.
315             $self->{VERBOSE}
316 5 50       19 and warn "Skipping DIR entry $name (Introductory stuff)\n";
317             } else {
318 0         0 warn "Unknown DIR entry $name\n";
319             }
320 5         11 return;
321             }
322              
323             sub _warn_about_unknown_file {
324 0     0   0 my ($self, $pps) = @_;
325              
326 0         0 my $name = $self->_get_pps_name($pps);
327              
328 0 0       0 if ($name eq 'Olk10SideProps_0001') {
329             $self->{VERBOSE}
330 0 0       0 and warn "Skipping FILE entry $name (Properties)\n";
331             } else {
332 0         0 warn "Unknown FILE entry $name\n";
333             }
334 0         0 return;
335             }
336              
337             #
338             # Generic processor for a file entry: Inserts the entry's data into the
339             # $self's mapi property list.
340             #
341             sub _process_pps_file_entry {
342 188     188   252 my ($self, $pps) = @_;
343 188         284 my $name = $self->_get_pps_name($pps);
344 188         345 my ($property, $encoding) = $self->_parse_item_name($name);
345              
346 188 100       324 if (defined $property) {
    50          
347 176         422 $self->set_mapi_property($property, [$encoding, $pps->{Data}]);
348             } elsif ($name eq '__properties_version1 0') {
349 12         58 $self->_process_property_stream ($pps->{Data});
350             } else {
351 0         0 $self->_warn_about_unknown_file($pps);
352             }
353 188         291 return;
354             }
355              
356             sub _process_property_stream {
357 12     12   25 my ($self, $data) = @_;
358 12         42 my ($n, $len) = ($self->_property_stream_header_length, length $data) ;
359              
360 12         35 while ($n + 16 <= $len) {
361 303         529 my @f = unpack "v4", substr $data, $n, 8;
362              
363 303         540 my $encoding = sprintf("%04X", $f[0]);
364              
365 303 100       539 unless ($VARIABLE_ENCODINGS->{$encoding}) {
366 127         197 my $property = sprintf("%04X", $f[1]);
367 127         221 my $propdata = substr $data, $n+8, 8;
368 127         254 $self->set_mapi_property($property, [$encoding, $propdata]);
369             }
370             } continue {
371 303         517 $n += 16 ;
372             }
373 12         19 return;
374             }
375              
376             sub _check_pps_file_entries {
377 12     12   32 my ($self, $map) = @_;
378              
379 12         41 foreach my $property ($self->mapi_property_names) {
380 303 100       517 if (my $key = $map->{$property}) {
381 63         123 $self->_use_property($key, $property);
382             } else {
383 240         357 $self->_warn_about_skipped_property($property);
384             }
385             }
386 12         29 return;
387             }
388              
389             sub _use_property {
390 63     63   101 my ($self, $key, $property) = @_;
391 63         85 my ($encoding, $data) = @{$self->get_mapi_property($property)};
  63         124  
392 63         117 $self->{$key} = $self->_decode_mapi_property($encoding, $data);
393              
394             $self->{VERBOSE}
395 63 50       125 and $self->_log_property("Using ", $property, $key);
396 63         104 return;
397             }
398              
399             sub _warn_about_skipped_property {
400 240     240   331 my ($self, $property) = @_;
401              
402 240 50       434 return unless $self->{VERBOSE};
403              
404 0   0     0 my $meaning = $skipproperties->{$property} || "UNKNOWN";
405              
406 0         0 $self->_log_property("Skipping", $property, $meaning);
407 0         0 return;
408             }
409              
410             sub _log_property {
411 0     0   0 my ($self, $message, $property, $meaning) = @_;
412              
413 0         0 my ($encoding, $data) = @{$self->get_mapi_property($property)};
  0         0  
414 0         0 my $value = $self->_decode_mapi_property($encoding, $data);
415 0         0 $value = substr($value, 0, 50);
416              
417 0 0       0 if ($encoding eq $ENCODING_BINARY) {
418 0 0       0 if ($value =~ / [[:print:]] /x) {
419 0         0 $value =~ s/ [^[:print:]] /./gx;
420             } else {
421 0         0 $value =~ s/ . / sprintf("%02x ", ord($&)) /sgex;
  0         0  
422             }
423             }
424              
425 0 0       0 if (length($value) > 45) {
426 0         0 $value = substr($value, 0, 41) . " ...";
427             }
428              
429 0         0 warn "$message property $encoding:$property ($meaning): $value\n";
430 0         0 return;
431             }
432              
433             sub _set_verbosity {
434 12     12   31 my ($self, $verbosity) = @_;
435 12 50       55 $self->{VERBOSE} = $verbosity ? 1 : 0;
436 12         24 return;
437             }
438              
439             #
440             # Format a gmt date according to RFC822
441             #
442             sub _format_date {
443 17     17   33 my ($self, $datearr) = @_;
444 17         26 my $day = qw(Sun Mon Tue Wed Thu Fri Sat)[strftime("%w", @{$datearr})];
  17         517  
445 17         45 my $month = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec)[strftime("%m", @{$datearr}) - 1];
  17         192  
446 17         58 return strftime("$day, %d $month %Y %H:%M:%S +0000", @{$datearr});
  17         211  
447             }
448              
449             1;