Helpdesk da PluGzOne, baseado no osTicket
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

<?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;
}
}