The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.
package Mail::Exchange::Message;

=head1 NAME

Mail::Exchange::Message - class to deal with .msg files, used by Microsoft
Exchange / MS Outlook.

=head1 SYNOPSIS

    use Mail::Exchange::Message;
    use Mail::Exchange::Message::MessageFlags;
    use Mail::Exchange::Recipient;
    use Mail::Exchange::Attachment;
    use Mail::Exchange::PidTagIDs;

    # modify an existing .msg file

    my $msg=Mail::Exchange::Message->new("my.msg");
    print "old Subject: ", $msg->get(PidTagSubject), "\n";
    $msg->setSubject('new subject');
    $msg->save("changed.msg");

    # create a .msg file from scratch, and send it to
    # the browser from a CGI script

    my $msg=Mail::Exchange::Message->new();
    $msg->setUnicode(1);
    $msg->setSubject('message subject');
    $msg->setBody('message body');
    $msg->set(PidTagMessageFlags, mfUnsent);
    $message->setDisplayTo('test@somewhere.com');

    my $recipient=Mail::Exchange::Recipient->new();
    $recipient->setEmailAddress('test@somewhere.com');
    $recipient->setDisplayName('John Tester');
    $recipient->setRecipientType('To');
    $message->addRecipient($recipient);

    my $attachment=Mail::Exchange::Message->new("attach.dat");
    $message->addAttachment($attachment);

    binmode(STDOUT);
    print STDOUT qq(Content-type: application/vnd.ms-outlook
    Content-Disposition: attachment; filename="newmessage.msg"

    );

    $message->save(\*STDOUT);

=head1 DESCRIPTION

Mail::Exchange::Message allows you to read and write binary message files that
Microsoft Outlook uses to store emails, to-dos, appointments and so on. It does
not need Windows, or Outlook, installed, and should be able to run on any
operating system that supports perl.

It might have been named "Outlook" instead of "Exchange", but the 
"Mail::Outlook" and "Email::Outlook" namespaces had both been taken at the
time of its implementation, and it contains some sub-modules that might be
helpful to implementations of more functionality with Microsoft Exchange,
with which it is intended to coexist.

=cut


use strict;
use warnings;
use 5.008;

use Exporter;
use Encode;
use Mail::Exchange::Time;
use Mail::Exchange::PidTagIDs;
use Mail::Exchange::PidTagDefs;
use Mail::Exchange::PropertyContainer;
use Mail::Exchange::NamedProperties;
use Mail::Exchange::Recipient;
use Mail::Exchange::Attachment;
use Mail::Exchange::Message::MessageFlags;
use OLE::Storage_Lite;

use vars qw($VERSION @ISA);
@ISA=qw(Mail::Exchange::PropertyContainer Exporter);

$VERSION = "0.04";

=head2 new()

$msg=Mail::Exchange::Message->new([$file])

Read a message from the .msg file C<$file>, or create a new, empty one,
if C<$file> isn't given.

=cut

sub new {
	my $class=shift;
	my $file=shift;

	my $self=Mail::Exchange::PropertyContainer->new();
	bless($self, $class);

	$self->{_recipients}=();
	$self->{_attachments}=();
	$self->{_namedProperties}=Mail::Exchange::NamedProperties->new();
	
	if ($file) {
		$self->parse($file);
	} else {
		# these are taken from [MS-OXCMSG] 3.2.5.2
		# PidTagMessageClass is NOT initialized, there are
		# subclasses for that.
		my $now=Mail::Exchange::Time->new(time());
		$self->set(PidTagImportance,		1);
		$self->set(PidTagSensitivity,		0);
		$self->set(PidTagDisplayBcc,		"");
		$self->set(PidTagDisplayCc,		"");
		$self->set(PidTagDisplayTo,		"");
		$self->set(PidTagMessageFlags,		9);
		$self->set(PidTagMessageSize,		1);
		$self->set(PidTagHasAttachments,	0);
		$self->set(PidTagTrustSender,		1);
		$self->set(PidTagAccess,		3);
		$self->set(PidTagAccessLevel,		3);
		$self->set(PidTagUrlCompName,		"No Subject.EML");
		$self->set(PidTagCreationTime,		$now->mstime());
		$self->set(PidTagLastModificationTime,	$now->mstime());
	}

	$self;
}

=head2 parse()

$msg->parse($file)

Read a message file into an internal structure. Called from new() if a
filename argument is given. C<$file> is expected to be a string, but may
be anything that is accepted by OLE::Storage_Lite.

=cut

sub parse {
	my $self=shift;
	my $file=shift;
	my $OLEFile = OLE::Storage_Lite->new($file);
	my $root=$OLEFile->getPpsTree(1);
	die "$file does not seem to be an OLE File" unless $root;

	my $nameid=Encode::encode("UCS2LE", "__nameid_version1.0");
	foreach my $entry (@{$root->{Child}}) {
		if ($entry->{Name} eq $nameid) {
			$self->_parsePropertyNames($entry);
		}
	}

	my $propid=Encode::encode("UCS2LE", "__properties_version1.0");
	foreach my $entry (@{$root->{Child}}) {
		if ($entry->{Name} eq $propid) {
			$self->_parseMessageProperties($entry, $root);
		}
	}

	foreach my $entry (@{$root->{Child}}) {
		# print Encode::decode("UCS2LE", $entry->{Name}), "\n";
		if (Encode::decode("UCS2LE", $entry->{Name})
				=~ /__recip_version1.0_#([0-9A-F]{8})/) {
			my $idx=hex($1);
			foreach my $subentry (@{$entry->{Child}}) {
				if ($subentry->{Name} eq $propid) {
					$self->{_recipients}[$idx]=Mail::Exchange::Recipient->new();
					$self->{_recipients}[$idx]->_parseRecipientProperties(
						$subentry, $entry, $self->{_namedProperties});
				}
			}
		}

		if (Encode::decode("UCS2LE", $entry->{Name})
				=~ /__attach_version1.0_#([0-9A-F]{8})/) {
			my $idx=hex($1);
			foreach my $subentry (@{$entry->{Child}}) {
				if ($subentry->{Name} eq $propid) {
					$self->{_attachments}[$idx]=Mail::Exchange::Attachment->new();
					$self->{_attachments}[$idx]->_parseAttachmentProperties(
						$subentry, $entry, $self->{_namedProperties});
				}
			}
		}
	}
}

sub _parsePropertyNames {
	my $self=shift;
	my $dir=shift;

	my $stringstreamdata;
	my $guidstreamdata;
	my $ssid=Encode::encode("UCS2LE", "__substg1.0_00040102");
	my $gsid=Encode::encode("UCS2LE", "__substg1.0_00020102");
	foreach my $item (@{$dir->{Child}}) {
		if ($item->{Name} eq $ssid) {
			$stringstreamdata=$item->{Data};
		}
		if ($item->{Name} eq $gsid) {
			$guidstreamdata=$item->{Data};
		}
	}

	my $psid=Encode::encode("UCS2LE", "__substg1.0_00030102");
	foreach my $item (@{$dir->{Child}}) {
		if ($item->{Name} eq $psid) {
			my $data=$item->{Data};
			while ($data ne "") {
				my ($niso, $iko)=unpack("VV", $data);
				my $pi=($iko>>16)&0xffff;
				my $gi=($iko>>1)&0x7fff;
				my $pk=$iko&1;
				my $guid;
				my $name;
				if ($gi==1) { $guid="PS_MAPI"; }
				if ($gi==2) { $guid="PS_PUBLIC_STRINGS"; }
				if ($gi>2)  { $guid=GUIDDecode(substr($guidstreamdata, 16*($gi-3), 16)); }

				# We don't know the type here, so we just
				# add the property with undef type. The type
				# will be set later when we actually read
				# the value from the properties stream.
				if ($pk==0) {
					$self->{_namedProperties}->namedPropertyIndex(
						$niso, undef, $guid);
				} else {
					my $len=unpack("V", substr($stringstreamdata, $niso, 4));
					$name=Encode::decode("UCS2LE", substr($stringstreamdata, $niso+4, $len));
					$self->{_namedProperties}->namedPropertyIndex($name, undef, $guid);
					# @@@ die if returncode != $pi ??
				}
				$data=substr($data, 8);
			}
		}
	}
}

sub _parseMessageProperties {
	my $self=shift;
	my $file=shift;
	my $dir=shift;

	$self->_parseProperties($file, $dir, 32, $self->{_namedProperties});
}

=head2 set()

$msg->set($tag, $value, [$flags,] [$type,] [$guid])

Set a property within a message. C<$tag> can be any numeric property defined in
Mail::Exchange::PidTagIDs.pm, a numeric named property defined in
Mail::Exchange::PidLidIDs.pm, or a string property. C<$value> is the value
the property is set to. 

C<$flags> is a bit-wise or of 1 (this property is mandatory and must not be
deleted), 2 (property is writable) and 4 (property is readable and may be
displayed to the user). Default is 6.

When a string named property is defined for the first time, its C<$type>
and C<$guid> must be given as well, as stated [MS-OXPROPS], section 1.3.2

=cut

sub set {
	my ($self, $tag, $value, $flags, $type, $guid) = @_;
	Mail::Exchange::PropertyContainer::set($self, $tag, $value,
		$flags, $type, $guid, $self->{_namedProperties});
}

sub get {
	my ($self, $tag) = @_;
	return Mail::Exchange::PropertyContainer::get($self, $tag,
		$self->{_namedProperties});
}

=head2 setSender()

$msg->setSender($address)

setSender is a shortcut for setting various properties that
descripe the sender of a message.

=cut

sub setSender {
	my $self=shift;
	my $sender=shift;

	$self->set(PidTagSentRepresentingAddressType, "SMTP");
	$self->set(PidTagSentRepresentingName, $sender);
	$self->set(PidTagSentRepresentingEmailAddress, $sender);

	$self->set(PidTagSenderAddressType, "SMTP");
	$self->set(PidTagSenderName, $sender);
	$self->set(PidTagSenderEmailAddress, $sender);
	$self->set(PidTagSenderSmtpAddress, $sender);
	# $self->set(0x5D02, $sender);
}

=head2 setDisplayTo()

$msg->setDisplayTo($text)

setDisplayTo sets the recipient list that is shown by outlook in the
"To:" address line.

=cut

sub setDisplayTo {
	my $self=shift;
	my $recipient=shift;

	$self->set(PidTagDisplayTo, $recipient);
}

=head2 setDisplayCc()

$msg->setDisplayCc($text)

setDisplayCc sets the recipient list that is shown by outlook in the
"Cc:" address line.

=cut

sub setDisplayCc {
	my $self=shift;
	my $recipient=shift;

	$self->set(PidTagDisplayCc, $recipient);
}

=head2 setDisplayBcc()

$msg->setDisplayBcc($text)

setDisplayBcc sets the recipient list that is shown by outlook in the
"Bcc:" address line.

=cut

sub setDisplayBcc {
	my $self=shift;
	my $recipient=shift;

	$self->set(PidTagDisplayBcc, $recipient);
}

=head2 setSubject()

$msg->setSubject($text)

setSubject sets the subject of the message by setting various internal
properties.

=cut

sub setSubject {
	my $self=shift;
	my $subject=shift;

	$self->set(PidTagSubject, $subject);
	$self->set(PidTagSubjectPrefix, "");
	$self->set(PidTagConversationTopic, $subject);
	$self->set(PidTagNormalizedSubject, $subject);
}

=head2 setBody()

$msg->setBody($text)

setBody sets the plain text body of the message.

=cut

sub setBody {
	my $self=shift;
	my $body=shift;

	$self->set(PidTagBody, $body);
}

=head2 setHTMLBody()

$msg->setHTMLBody($text)

setHTMLBody sets the html body of the message.

=cut

sub setHTMLBody {
	my $self=shift;
	my $body=shift;

	$self->set(PidTagHtml, $body);
}

=head2 setRtfBody()

setRtfBody($text[, $compress])

setRtfBody sets the rich text format body of the message.

The message file format allows for compressed or uncompressed
rtf storage. If C<$compress> is set, C<$text> will be compressed before being
stored. (As of now, C<$compress> is not implemented, and C<$text> will always
be stored in uncompressed form).

=cut

sub setRtfBody {
	my $self=shift;
	my $body=shift;

	# OXRTFCP says CRC MUST be 0 when uncompressed.

	my $header=pack("VVVV", length($body)+12, length($body), 0x414c454D, 0);
	$self->set(PidTagRtfCompressed, $header.$body);
	$self->set(PidTagRtfInSync, 1);
}

=head2 getRtfBody()

$msg->getRtfBody()

getRtfBody gets the RTF Body of a message, uncompressing it if neccesary.

=cut

sub getRtfBody {
	my $self=shift;
	my $rtf=$self->get(PidTagRtfCompressed);
	my ($compsize, $rawsize, $comptype, $crc)=unpack("VVVV", $rtf);
	$rtf=substr($rtf, 16);

	if ($comptype == 0x414c454D) {
		return $rtf;
	} elsif ($comptype != 0x75465a4c) {
		die(sprintf("rtf compression type %08x unknown", $comptype));
	}

	my $dictionary='{\rtf1\ansi\mac\deff0\deftab720{\fonttbl;}'.
			'{\f0\fnil \froman \fswiss \fmodern '.
			'\fscript \fdecor MS Sans SerifSymbolArialTimes'.
			' New RomanCourier{\colortbl\red0\green0\blue0'.
			"\r\n".'\par \pard\plain\f0\fs20\b\i\u\tab\tx';

	my $dpos=207;
	my $rpos=0;
	my $rlen=length $rtf;
	my $output='';
RTFTEXT:
	while ($rpos<$rlen) {
		my $control=unpack("C", substr($rtf, $rpos++, 1));
		for (my $i=0; $i<8 && $rpos<$rlen; $i++) {
			my $newbyte;
			my $ofs;
			my $len;
			if ($control & (1<<$i)) {
				my $ref=unpack("n", substr($rtf, $rpos));
				$rpos+=2;
				$ofs=$ref>>4;
				$len=($ref&0x0f)+2;
				if ($ofs==($dpos%4096)) {
					last RTFTEXT;
				}
				for (my $j=0; $j<$len; $j++) {
					$newbyte=substr($dictionary, ($ofs++%4096), 1);
					substr($dictionary, ($dpos++%4096), 1)=$newbyte;
					$output.=$newbyte;

				}
			} else {
				$newbyte=substr($rtf, $rpos++, 1);
				$output.=$newbyte;
				substr($dictionary, ($dpos++%4096), 1)=$newbyte;
			}
		}
	}
	return $output;
}

=head2 setHtmlBody()

$msg->setHtmlBody($htmltext)

setHtmlBody sets the html version of the message body. It is a shortcut
for C<$msg->set(PidTagHtml, $htmltext)>.

=cut

sub setHtmlBody {
	my $self=shift;
	my $body=shift;

	$self->set(PidTagHtml, $body);
}

=head2 setUnicode()

$msg->setUnicode($flag)

If C<$flag> is 0, all strings within the message will be stored one byte per
character. If C<$flag> is 1, strings will be stored in what's called unicode
in the documentation (actually, UCS2LE encoded strings, using 2 bytes per
character).

=cut

sub setUnicode {
	my $self=shift;
	my $flag=shift;
	my $mask=$self->get(PidTagStoreSupportMask);
	if ($flag) {
		$mask |= 0x40000;
	} else {
		$mask &= ~0x40000;
	}
	$self->set(PidTagStoreSupportMask, $mask);
}

=head2 save()

save($msgfile)

Saves a message object to a file. $msgfile may be a file, or anything else
that OLE::Storage_Lite accepts.

=cut

sub save {
	my $self=shift;
	my $output=shift;

	my @streams=();
	push(@streams, $self->{_namedProperties}->OleContainer());

	my $unicode=$self->get(PidTagStoreSupportMask)&0x40000;
	my $header=pack("V8",
		0, 0,
		$#{$self->{_recipients}}+1,
		$#{$self->{_attachments}}+1,
		$#{$self->{_recipients}}+1,
		$#{$self->{_attachments}}+1,
		0, 0);


	push(@streams, $self->_OlePropertyStreamlist($unicode, $header));

	foreach my $i (0..$#{$self->{_recipients}}) {
		push(@streams, $self->{_recipients}[$i]->OleContainer($i, $unicode));
	}

	foreach my $i (0..$#{$self->{_attachments}}) {
		push(@streams, $self->{_attachments}[$i]->OleContainer($i, $unicode));
	}

	my @ltime=localtime();
	my $root=OLE::Storage_Lite::PPS::Root->new(\@ltime, \@ltime, \@streams);
	$root->save($output);
}

=head2 addAttachment()

addAttachment($object)

Adds a Mail::Exchange::Attachment object to a message.

=cut

sub addAttachment($$) {
	my $self=shift;
	my $attachment=shift;

	push(@{$self->{_attachments}}, $attachment);
	$self->set(PidTagHasAttachments, 1);
	my $flags=$self->get(PidTagMessageFlags) || 0;
	$self->set(PidTagMessageFlags, $flags | mfHasAttach);
}

=head2 addRecipient()

addRecipient($object)

Adds a Mail::Exchange::Recipient object to a message.

=cut

sub addRecipient($$) {
	my $self=shift;
	my $recipient=shift;

	push(@{$self->{_recipients}}, $recipient);
}

__END__

=head1 WARNING

This Module is quite alpha. It reads most msg files ok, and writing some .msg
files works, but there are several features that aren't implemented yet, 
and using generated .msg files may have any adverse effects on Outlook,
including program crashes.

=head1 COPYRIGHT

The Mail::Exchange modules are Copyright (c) 2012 Guntram Blohm.
All rights reserverd.

You may distribute under the terms of either the GNU General Public
License or the Artistic License, as specified in the Perl README file.

The Name definitions in Mail::Message::PidTagIDs.pm,
Mail::Message::PidTagDefs.pm, Mail::Message::PidLidIDs.pm and
Mail::Message::PropertyTypes.pm are taken from [MS-OXPROPS], which states

Copyrights. This documentation is covered by Microsoft
copyrights. Regardless of any other terms that are contained in the terms
of use for the Microsoft website that hosts this documentation, you may
make copies of it in order to develop implementations of the technologies
described in the Open Specifications and may distribute portions of it
in your implementations using these technologies or your documentation as
necessary to properly document the implementation. You may also distribute
in your implementation, with or without modification, any schema,
IDL’s, or code samples that are included in the documentation. This
permission also applies to any documents that are referenced in the
Open Specifications.

The compression/decompression algorithm for RTF content is described in
[MS-OXRTFCP], which has the same copyright statement.

=head1 AUTHOR

Guntram Blohm gbl@bso2001.com

=head1 REFERENCES

[MS-OXPROPS] Exchange Server Protocols Master Property List, 
http://msdn.microsoft.com/en-us/library/cc433490(v=exchg.80).aspx
on Sep 30, 2012

[MS-OXRTFCP] Rich Text Format (RTF) Compression Algorithm
http://msdn.microsoft.com/en-us/library/cc463890(v=exchg.80).aspx
on Sep 30, 2012

[MS-OXMSG]: Outlook Item (.msg) File Format
http://msdn.microsoft.com/en-us/library/cc463912(v=exchg.80).aspx
on Sep 30, 2012