You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
650 lines
20 KiB
650 lines
20 KiB
<?php
|
|
/*********************************************************************
|
|
class.tnefparse.php
|
|
|
|
Parser library and data objects for Microsoft TNEF (Transport Neutral
|
|
Encapsulation Format) encoded email attachments.
|
|
|
|
Jared Hancock <jared@osticket.com>
|
|
Peter Rotich <peter@osticket.com>
|
|
Copyright (c) 2006-2014 osTicket
|
|
http://www.osticket.com
|
|
|
|
This algorithm based on a similar project; however the original code did
|
|
not process the HTML body of the message, nor did it properly handle the
|
|
Microsoft Unicode encoding found in the attributes.
|
|
|
|
* The Horde's class allows MS-TNEF data to be displayed.
|
|
*
|
|
* The TNEF rendering is based on code by:
|
|
* Graham Norbury <gnorbury@bondcar.com>
|
|
* Original design by:
|
|
* Thomas Boll <tb@boll.ch>, Mark Simpson <damned@world.std.com>
|
|
*
|
|
* Copyright 2002-2010 The Horde Project (http://www.horde.org/)
|
|
*
|
|
* See the enclosed file COPYING for license information (LGPL). If you
|
|
* did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
|
|
*
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @author Michael Slusarz <slusarz@horde.org>
|
|
* @package Horde_Compress
|
|
|
|
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
|
See LICENSE.TXT for details.
|
|
|
|
vim: expandtab sw=4 ts=4 sts=4:
|
|
**********************************************************************/
|
|
|
|
class TnefException extends Exception {}
|
|
|
|
/**
|
|
* References:
|
|
* http://download.microsoft.com/download/1/D/0/1D0C13E1-2961-4170-874E-FADD796200D9/%5BMS-OXTNEF%5D.pdf
|
|
* http://msdn.microsoft.com/en-us/library/ee160597(v=exchg.80).aspx
|
|
* http://sourceforge.net/apps/trac/mingw-w64/browser/trunk/mingw-w64-headers/include/tnef.h?rev=3952
|
|
*/
|
|
class TnefStreamReader implements Iterator {
|
|
const SIGNATURE = 0x223e9f78;
|
|
|
|
var $pos = 0;
|
|
var $length = 0;
|
|
var $streams = array();
|
|
var $current = true;
|
|
|
|
var $options = array(
|
|
'checksum' => true,
|
|
);
|
|
|
|
function __construct($stream, $options=array()) {
|
|
if (is_array($options))
|
|
$this->options += $options;
|
|
|
|
$this->push($stream);
|
|
|
|
// Read header
|
|
if (self::SIGNATURE != ($S = $this->_geti(32)))
|
|
throw new TnefException(sprintf("%08x: Invalid signature magic", $S));
|
|
|
|
$this->_geti(16); // Attach key
|
|
|
|
$this->next(); // Process first block
|
|
}
|
|
|
|
protected function push(&$stream) {
|
|
$this->streams[] = array($this->stream, $this->pos, $this->length);
|
|
$this->stream = &$stream;
|
|
$this->pos = 0;
|
|
$this->length = strlen($stream);
|
|
}
|
|
|
|
protected function pop() {
|
|
list($this->stream, $this->pos, $this->length) = array_pop($this->streams);
|
|
}
|
|
|
|
protected function _geti($bits) {
|
|
$bytes = $bits / 8;
|
|
|
|
switch($bytes) {
|
|
case 1:
|
|
$value = ord($this->stream[$this->pos]);
|
|
break;
|
|
case 2:
|
|
$value = unpack('vval', substr($this->stream, $this->pos, 2));
|
|
$value = $value['val'];
|
|
break;
|
|
case 4:
|
|
$value = unpack('Vval', substr($this->stream, $this->pos, 4));
|
|
$value = $value['val'];
|
|
break;
|
|
}
|
|
$this->pos += $bytes;
|
|
return $value;
|
|
}
|
|
|
|
protected function _getx($bytes) {
|
|
$value = substr($this->stream, $this->pos, $bytes);
|
|
$this->pos += $bytes;
|
|
|
|
return $value;
|
|
}
|
|
|
|
protected function skip($bytes) {
|
|
$this->pos += $bytes;
|
|
}
|
|
|
|
function check($block) {
|
|
$sum = 0; $bytes = strlen($block['data']); $bs = 1024;
|
|
for ($i=0; $i < $bytes; $i+=$bs) {
|
|
$b = unpack('C*', substr($block['data'], $i, min($bs, $bytes-$i)));
|
|
$sum += array_sum($b);
|
|
$sum = $sum % 65536;
|
|
}
|
|
if ($block['checksum'] != $sum)
|
|
throw new TnefException(sprintf('Corrupted block. %04x: Invalid checksum, expected %04x',
|
|
$sum, $block['checksum']));
|
|
}
|
|
|
|
function next() {
|
|
if ($this->length - $this->pos < 11) {
|
|
$this->current = false;
|
|
return;
|
|
}
|
|
|
|
$this->current = array(
|
|
'level' => $this->_geti(8),
|
|
'type' => $this->_geti(32),
|
|
'length' => $length = $this->_geti(32),
|
|
'data' => $this->_getx($length),
|
|
'checksum' => $this->_geti(16)
|
|
);
|
|
|
|
if ($this->options['checksum'])
|
|
$this->check($this->current);
|
|
}
|
|
|
|
function current() {
|
|
return $this->current;
|
|
}
|
|
|
|
function key() {
|
|
return $this->current['type'];
|
|
}
|
|
|
|
function valid() {
|
|
return (bool) $this->current;
|
|
}
|
|
|
|
function rewind() {
|
|
// Skip signature and attach-key
|
|
$this->pos = 6;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* References:
|
|
* http://msdn.microsoft.com/en-us/library/ee179447(v=exchg.80).aspx
|
|
*/
|
|
class TnefAttribute {
|
|
// Encapsulated Attributes
|
|
const AlternateRecipientAllowed = 0x0002;
|
|
const AutoForwarded = 0x0005;
|
|
const Importance = 0x0017;
|
|
const MessageClass = 0x001a;
|
|
const OriginatorDeliveryReportRequested = 0x0023;
|
|
const Priority = 0x0026;
|
|
const ReadReceiptRequested = 0x0029;
|
|
const Sensitivity = 0x0036;
|
|
const ClientSubmitTime = 0x0039;
|
|
const ReceivedByEntryId = 0x003f;
|
|
const ReceivedByName = 0x0040;
|
|
const ReceivedRepresentingEntryId = 0x0043;
|
|
const RecevedRepresentingName = 0x0044;
|
|
const MessageSubmissionId = 0x0047;
|
|
const ReceivedBySearchKey = 0x0051;
|
|
const ReceivedRepresentingSearchKey = 0x0052;
|
|
const MessageToMe = 0x0057;
|
|
const MessgeCcMe = 0x0058;
|
|
const ConversionTopic = 0x0070;
|
|
const ConversationIndex = 0x0071;
|
|
const ReceivedByAddressType = 0x0075;
|
|
const ReceivedByEmailAddress = 0x0076;
|
|
const TnefCorrelationKey = 0x007f;
|
|
const SenderName = 0x0c1a;
|
|
const HasAttachments = 0x0e1b;
|
|
const NormalizedSubject = 0x0e1d;
|
|
const AttachSize = 0x0e20;
|
|
const AttachNumber = 0x0e21;
|
|
const Body = 0x1000;
|
|
const RtfSyncBodyCrc = 0x1006;
|
|
const RtfSyncBodyCount = 0x1007;
|
|
const RtfSyncBodyTag = 0x1008;
|
|
const RtfCompressed = 0x1009;
|
|
const RtfSyncPrefixCount = 0x1010;
|
|
const RtfSyncTrailingCount = 0x1011;
|
|
const BodyHtml = 0x1013;
|
|
const BodyContentId = 0x1014;
|
|
const NativeBody = 0x1016;
|
|
const InternetMessageId = 0x1035;
|
|
const IconIndex = 0x1080;
|
|
const ImapCachedMsgsize = 0x10f0;
|
|
const UrlCompName = 0x10f3;
|
|
const AttributeHidden = 0x10f4;
|
|
const AttributeSystem = 0x10f5;
|
|
const AttributeReadOnly = 0x10f6;
|
|
const CreationTime = 0x3007;
|
|
const LastModificationTime = 0x3008;
|
|
const AttachDataBinary = 0x3701;
|
|
const AttachEncoding = 0x3702;
|
|
const AttachExtension = 0x3703;
|
|
const AttachFilename = 0x3704;
|
|
const AttachLongFilename = 0x3707;
|
|
const AttachPathname = 0x3708;
|
|
const AttachTransportName = 0x370c;
|
|
const AttachMimeTag = 0x370e; # Mime content-type
|
|
const AttachContentId = 0x3712;
|
|
const AttachmentCharset = 0x371b;
|
|
const InternetCodepage = 0x3fde;
|
|
const MessageLocaleId = 0x3ff1;
|
|
const CreatorName = 0x3ff8;
|
|
const CreatorEntryId = 0x3ff9;
|
|
const LastModifierName = 0x3ffa;
|
|
const LastModifierEntryId = 0x3ffb;
|
|
const MessageCodepage = 0x3ffd;
|
|
const SenderFlags = 0x4019;
|
|
const SentRepresentingFlags = 0x401a;
|
|
const ReceivedByFlags = 0x401b;
|
|
const ReceivedRepresentingFlags = 0x401c;
|
|
const SenderSimpleDisplayName = 0x4030;
|
|
const SentRepresentingSimpleDisplayName = 0x4031;
|
|
# NOTE: The M$ specification gives ambiguous values for this property
|
|
const ReceivedRepresentingSimpleDisplayName = 0x4034;
|
|
const CreatorSimpleDisplayName = 0x4038;
|
|
const LastModifierSimpleDisplayName = 0x4039;
|
|
const ContentFilterSpamConfidenceLevel = 0x4076;
|
|
const MessageEditorFormat = 0x5909;
|
|
|
|
static function getName($code) {
|
|
static $prop_codes = false;
|
|
if (!$prop_codes) {
|
|
$R = new ReflectionClass(get_class());
|
|
$prop_codes = array_flip($R->getConstants());
|
|
}
|
|
return $prop_codes[$code];
|
|
}
|
|
}
|
|
|
|
class TnefAttributeStreamReader extends TnefStreamReader {
|
|
var $count = 0;
|
|
|
|
const TypeUnspecified = 0x0;
|
|
const TypeNull = 0x0001;
|
|
const TypeInt16 = 0x0002;
|
|
const TypeInt32 = 0x0003;
|
|
const TypeFlt32 = 0x0004;
|
|
const TypeFlt64 = 0x0005;
|
|
const TypeCurency = 0x0006;
|
|
const TypeAppTime = 0x0007;
|
|
const TypeError = 0x000a;
|
|
const TypeBoolean = 0x000b;
|
|
const TypeObject = 0x000d;
|
|
const TypeInt64 = 0x0014;
|
|
const TypeString8 = 0x001e;
|
|
const TypeUnicode = 0x001f;
|
|
const TypeSystime = 0x0040;
|
|
const TypeCLSID = 0x0048;
|
|
const TypeBinary = 0x0102;
|
|
|
|
const MAPI_NAMED_TYPE_ID = 0x0000;
|
|
const MAPI_NAMED_TYPE_STRING = 0x0001;
|
|
const MAPI_MV_FLAG = 0x1000;
|
|
|
|
function __construct($stream) {
|
|
$this->push($stream);
|
|
/* Number of attributes. */
|
|
$this->count = $this->_geti(32);
|
|
}
|
|
|
|
function valid() {
|
|
return $this->count && $this->current;
|
|
}
|
|
|
|
function rewind() {
|
|
$this->pos = 4;
|
|
}
|
|
|
|
/**
|
|
* Read a single typed value from the current input stream. The type is
|
|
* a 16-bit number receved as an argument which should be one of the
|
|
* Type* constants defined in this class.
|
|
*
|
|
* According to the TNEF spec, all types regardless of their actual
|
|
* size, must be rounded up in size to the next multiple of four (4)
|
|
* bytes. Therefore 16-bit values and a strings will need to have extra
|
|
* padding consumed to keep the stream on track.
|
|
*/
|
|
protected function readPhpValue($type) {
|
|
switch ($type) {
|
|
case self::TypeUnspecified:
|
|
case self::TypeNull:
|
|
case self::TypeError:
|
|
return null;
|
|
|
|
case self::TypeInt16:
|
|
// Signed 16-bit value = INT16.
|
|
$int16 = unpack('v', $this->_getx(4));
|
|
$sign = $int16 & 0x8000;
|
|
if ($sign)
|
|
// Use two's compliment
|
|
$int16 = - ((~$int16 & 0xFFFF) + 1);
|
|
return $int16;
|
|
|
|
case self::TypeInt32:
|
|
// Signed 32-bit value = INT32.
|
|
// FIXME: Convert to signed value
|
|
return $this->_geti(32);
|
|
|
|
case self::TypeBoolean:
|
|
// 16-bit Boolean (non-zero = TRUE)
|
|
list($bool) = unpack('v', $this->_getx(4));
|
|
return 0 != $bool;
|
|
|
|
case self::TypeFlt32:
|
|
// Signed 32-bit floating point= FLOAT.
|
|
list($f) = unpack('f', $this->_getx(4));
|
|
return $f;
|
|
|
|
case self::TypeFlt64:
|
|
// 64-bit floating point= DOUBLE.
|
|
list($d) = unpack('d', $this->_getx(8));
|
|
return $d;
|
|
|
|
case self::TypeCurency:
|
|
// Signed 64-bit int = OLE CURRENCY type.
|
|
// FIXME: Convert to PHP double
|
|
return $this->_getx(8);
|
|
|
|
case self::TypeInt64:
|
|
// 8-byte signed integer= INT64.
|
|
$x = $this->_getx(8);
|
|
if (phpversion() >= '5.6.3')
|
|
list($x) = unpack('P', $x);
|
|
return $x;
|
|
|
|
case self::TypeAppTime:
|
|
list($d) = unpack('d', $this->_getx(8));
|
|
// Application time= OLE DATE type.
|
|
// Thanks, http://stackoverflow.com/a/10443946/1025836
|
|
// Convert to UNIX timestamp, UTC timezone is assumed
|
|
return ($d - 25569) * 86400;
|
|
|
|
case self::TypeSystime:
|
|
$a = unpack('Vl/Vh', $this->_getx(8));
|
|
// return FileTimeToU64(f) / 10000000 - 11644473600
|
|
$ft = ($a['l'] / 10000000.0) + ($a['h'] * 429.4967296);
|
|
return $ft - 11644473600;
|
|
|
|
case self::TypeString8:
|
|
case self::TypeUnicode:
|
|
case self::TypeBinary:
|
|
$length = $this->_geti(32);
|
|
|
|
/* Pad to next 4 byte boundary. */
|
|
$datalen = $length + ((4 - ($length % 4)) % 4);
|
|
|
|
// Chomp null terminator
|
|
if ($type == self::TypeString8)
|
|
--$length;
|
|
elseif ($type == self::TypeUnicode)
|
|
$length -= 2;
|
|
|
|
/* Read and truncate to length. */
|
|
$text = substr($this->_getx($datalen), 0, $length);
|
|
if ($type == self::TypeUnicode) {
|
|
// TNEF spec says encoding is UTF-16LE
|
|
$text = Charset::utf8($text, 'UTF-16LE');
|
|
}
|
|
|
|
return $text;
|
|
|
|
case self::TypeObject:
|
|
$length = $this->_geti(32);
|
|
$oid = $this->_getx(16);
|
|
if (bin2hex($text) == "0703020000000000c000000000000046") {
|
|
// TODO: Create stream parser for embedded TNEF stream
|
|
}
|
|
$this->skip($length - 16);
|
|
$this->skip((4 - ($length % 4)) % 4);
|
|
return null;
|
|
|
|
case self::TypeCLSID:
|
|
return $this->_getx(16);
|
|
|
|
default:
|
|
throw new TnefException(sprintf('0x%04x: Bad data type', $type));
|
|
}
|
|
}
|
|
|
|
function next() {
|
|
if ($this->count <= 0) {
|
|
return $this->current = false;
|
|
}
|
|
|
|
$this->count--;
|
|
|
|
$have_mval = false;
|
|
$named_id = $value = null;
|
|
$attr_type = $this->_geti(16);
|
|
$attr_name = $this->_geti(16);
|
|
$data_type = $attr_type & ~self::MAPI_MV_FLAG;
|
|
|
|
if (($attr_type & self::MAPI_MV_FLAG) != 0
|
|
// These are a "special case of multi-value attributes with
|
|
// num_values=1
|
|
|| in_array($attr_type, array(
|
|
self::TypeUnicode, self::TypeString8, self::TypeBinary,
|
|
self::TypeObject))
|
|
) {
|
|
$have_mval = true;
|
|
}
|
|
|
|
if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) {
|
|
$this->_getx(16);
|
|
$named_type = $this->_geti(32);
|
|
|
|
switch ($named_type) {
|
|
case self::MAPI_NAMED_TYPE_ID:
|
|
$named_id = $this->_geti(32);
|
|
break;
|
|
|
|
case self::MAPI_NAMED_TYPE_STRING:
|
|
$attr_name = 0x9999;
|
|
$named_id = $this->readPhpValue(self::TypeUnicode);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$have_mval) {
|
|
$value = $this->readPhpValue($data_type);
|
|
} else {
|
|
$value = array();
|
|
$k = $this->_geti(32);
|
|
for ($i=0; $i < $k; $i++)
|
|
$value[] = $this->readPhpValue($data_type);
|
|
}
|
|
|
|
if (is_array($value) && ($attr_type & self::MAPI_MV_FLAG) == 0)
|
|
$value = $value[0];
|
|
|
|
$this->current = array(
|
|
'type' => $attr_type,
|
|
'name' => $attr_name,
|
|
'named_id' => $named_id,
|
|
'value' => $value,
|
|
);
|
|
}
|
|
|
|
function key() {
|
|
return $this->current['name'];
|
|
}
|
|
}
|
|
|
|
class TnefStreamParser {
|
|
const LVL_MESSAGE = 0x01;
|
|
const LVL_ATTACHMENT = 0x02;
|
|
|
|
const attTnefVersion = 0x89006;
|
|
const attAttachData = 0x6800f;
|
|
const attAttachTransportFilename = 0x18010;
|
|
const attAttachRendData = 0x69002;
|
|
const attAttachment = 0x69005;
|
|
const attMsgProps = 0x69003;
|
|
const attRecipTable = 0x69004;
|
|
const attOemCodepage = 0x69007;
|
|
|
|
// Message-level attributes
|
|
const idMessageClass = 0x78008;
|
|
const idSenderEntryId = 0x8000; # From
|
|
const idSubject = 0x18004; # Subject
|
|
const idClientSubmitTime = 0x38004;
|
|
const idMessageDeliveryTime = 0x38005;
|
|
const idMessageStatus = 0x68007;
|
|
const idMessageID = 0x18009; # Message-Id
|
|
const idConversationID = 0x1800b;
|
|
const idBody = 0x2800c; # Body
|
|
const idImportance = 0x4800d; # Priority
|
|
const idLastModificationTime = 0x38020;
|
|
const idOriginalMessageClass = 0x70600;
|
|
const idReceivedRepresentingEmailAddress = 0x60000;
|
|
const idSentRepresentingEmailAddress = 0x60001;
|
|
const idStartDate = 0x030006;
|
|
const idEndDate = 0x30007;
|
|
const idOwnerAppointmentId = 0x50008;
|
|
const idResponseRequested = 0x40009;
|
|
|
|
function __construct($stream) {
|
|
$this->stream = new TnefStreamReader($stream);
|
|
}
|
|
|
|
function getMessage() {
|
|
$msg = new TnefMessage();
|
|
foreach ($this->stream as $type=>$info) {
|
|
switch($type) {
|
|
case self::attTnefVersion:
|
|
// Ignored (for now)
|
|
break;
|
|
|
|
case self::attOemCodepage:
|
|
$cp = unpack("Vpri/Vsec", $info['data']);
|
|
$msg->_set('OemCodepage', $cp['pri']);
|
|
break;
|
|
|
|
// Message level attributes
|
|
case self::idMessageClass:
|
|
$msg->_set('MessageClass', $info['data']);
|
|
break;
|
|
case self::idMessageID:
|
|
$msg->_set('MessageId', $info['data']);
|
|
break;
|
|
case self::idSubject:
|
|
$msg->_set('Subject', $info['data']);
|
|
break;
|
|
|
|
case self::attMsgProps:
|
|
// Message properties (includig body)
|
|
$msg->_setProperties(
|
|
new TnefAttributeStreamReader($info['data']));
|
|
break;
|
|
|
|
// Attachments
|
|
case self::attAttachRendData:
|
|
// Marks the start of an attachment
|
|
$attach = $msg->pushAttachment();
|
|
//$attach->_setRenderingData();
|
|
break;
|
|
case self::attAttachment:
|
|
$attach->_setProperties(
|
|
new TnefAttributeStreamReader($info['data']));
|
|
break;
|
|
case self::attAttachTransportFilename:
|
|
$attach->_setFilename(rtrim($info['data'], "\x00"));
|
|
break;
|
|
case self::attAttachData:
|
|
$attach->_setData($info['data']);
|
|
$attach->_setDataSize($info['length']);
|
|
break;
|
|
}
|
|
}
|
|
return $msg;
|
|
}
|
|
}
|
|
|
|
abstract class AbstractTnefObject {
|
|
function _setProperties($propReader) {
|
|
foreach ($propReader as $prop=>$info) {
|
|
if ($tag = TnefAttribute::getName($prop))
|
|
$this->{$tag} = $info['value'];
|
|
elseif ($prop == 0x9999)
|
|
// Extended, "named" attribute
|
|
$this->{$info['named_id']} = $info['value'];
|
|
}
|
|
}
|
|
|
|
function _set($prop, $value) {
|
|
$this->{$prop} = $value;
|
|
}
|
|
}
|
|
|
|
class TnefMessage extends AbstractTnefObject {
|
|
var $attachments = array();
|
|
|
|
function pushAttachment() {
|
|
$new = new TnefAttachment();
|
|
$this->attachments[] = $new;
|
|
return $new;
|
|
}
|
|
|
|
function getBody($type='text/html', $encoding=false) {
|
|
// First select the body
|
|
switch ($type) {
|
|
case 'text/html':
|
|
$body = $this->BodyHtml;
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
// Figure out the source encoding (§5.1.2)
|
|
$charset = false;
|
|
if (@$this->OemCodepage)
|
|
$charset = 'cp'.$this->OemCodepage;
|
|
elseif (@$this->InternetCodepage)
|
|
$charset = 'cp'.$this->InternetCodepage;
|
|
|
|
// Transcode it
|
|
if ($encoding && $charset)
|
|
$body = Charset::transcode($body, $charset, $encoding);
|
|
|
|
return $body;
|
|
}
|
|
}
|
|
|
|
class TnefAttachment extends AbstractTnefObject {
|
|
function _setFilename($data) {
|
|
$this->TransportFilename = $data;
|
|
}
|
|
|
|
function _setData($data) {
|
|
$this->Data = $data;
|
|
}
|
|
|
|
function _setDataSize($size) {
|
|
$this->DataSize = $size;
|
|
}
|
|
|
|
function getData() {
|
|
if (isset($this->Data))
|
|
return $this->Data;
|
|
elseif (isset($this->AttachDataBinary))
|
|
return $this->AttachDataBinary;
|
|
}
|
|
|
|
function _setRenderingData($data) {
|
|
// Pass
|
|
}
|
|
|
|
function getType() {
|
|
return $this->AttachMimeTag;
|
|
}
|
|
|
|
function getName() {
|
|
if (isset($this->AttachLongFilename))
|
|
return basename($this->AttachLongFilename);
|
|
elseif (isset($this->AttachFilename))
|
|
return $this->AttachFilename;
|
|
elseif (isset($this->AttachTransportName))
|
|
return $this->AttachTransportName;
|
|
else
|
|
return $this->TransportFilename;
|
|
}
|
|
}
|