File Coverage

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