Improved TNEFDecoder

This commit is contained in:
the-djmaze 2024-03-10 18:02:38 +01:00
parent d498bf2b53
commit 913ed54668
11 changed files with 406 additions and 550 deletions

View file

@ -531,6 +531,10 @@ trait Messages
if (\in_array($oFile->type, ['application/rtf', 'text/rtf'])) { if (\in_array($oFile->type, ['application/rtf', 'text/rtf'])) {
$rtf = new \SnappyMail\Rtf\Document($oFile->content); $rtf = new \SnappyMail\Rtf\Document($oFile->content);
$oMessage->setHtml($rtf->toHTML()); $oMessage->setHtml($rtf->toHTML());
} else {
// List as attachment?
$oMapiAttachment = new \MailSo\Mail\Attachment($sFolder, $iUid, BodyStructure);
$oMessage->Attachments->append($oMapiAttachment);
} }
} }
break; break;

View file

@ -2,7 +2,6 @@
namespace TNEFDecoder; namespace TNEFDecoder;
require 'functions.php';
require 'constants.php'; require 'constants.php';
/** /**
@ -19,31 +18,37 @@ require 'constants.php';
* *
*/ */
function tnef_log($string)
{
echo $string . "\n";
// \error_log($string . "\n", 3, '/tmp/squirrelmail_tnef_decoder.log');
// \SnappyMail\Log::debug('TNEF', $string);
}
class TNEFAttachment class TNEFAttachment
{ {
var $validateChecksum; public bool $debug;
var $mailinfo; public bool $validateChecksum;
var $files; public TNEFMailinfo $mailinfo;
var $files_nested; public array
var $attachments; $files = [],
var $current_receiver; $attachments = [],
var $body; $body = [];
public ?array $files_nested = null;
public ?TNEFFile $current_receiver = null;
function __construct($validateChecksum = false) public function __construct(bool $debug = false, bool $validateChecksum = false)
{ {
$this->debug = $debug;
$this->validateChecksum = $validateChecksum; $this->validateChecksum = $validateChecksum;
$this->files = array();
$this->attachments = array();
$this->mailinfo = new TNEFMailinfo(); $this->mailinfo = new TNEFMailinfo();
$this->body = [];
} }
/** /**
* @return TNEFFileBase[] * @return TNEFFileBase[]
*/ */
function &getFiles() public function &getFiles(): array
{ {
return $this->files; return $this->files;
} }
@ -51,96 +56,69 @@ class TNEFAttachment
/** /**
* @return TNEFFileBase[] * @return TNEFFileBase[]
*/ */
function &getFilesNested() public function &getFilesNested(): array
{
if (!$this->files_nested)
{ {
if (null === $this->files_nested) {
$this->files_nested = array(); $this->files_nested = array();
$this->addFilesNested($this->files);
$num_attach = count($this->attachments); foreach ($this->attachments as $attachment) {
if ($num_attach > 0) $this->addFilesNested($attachment->getFilesNested());
{
for ($cnt = 0; $cnt < $num_attach; $cnt++)
{
$this->addFiles($this->files_nested, $this->files);
$this->addFiles($this->files_nested, $this->attachments[$cnt]->getFilesNested());
} }
} }
else
$this->addFiles($this->files_nested, $this->files);
}
return $this->files_nested; return $this->files_nested;
} }
function addFiles(&$add_to, &$add) private function addFilesNested(array &$add): void
{ {
global $tnef_minimum_rtf_size_to_decode; foreach ($add as $file) {
$num_files = count($add); $this->files_nested[] = &$file;
for ($cnt = 0; $cnt < $num_files; $cnt++) }
if ((strtolower(get_class($add[$cnt])) != "tneffilertf") || ($add[$cnt]->getSize() > $tnef_minimum_rtf_size_to_decode))
$add_to[] = &$add[$cnt];
} }
function addFilesCond(&$add_to, &$add) public function getAttachments(): array
{
global $tnef_minimum_rtf_size_to_decode;
$num_files = count($add);
for ($cnt = 0; $cnt < $num_files; $cnt++)
if ((strtolower(get_class($add[$cnt])) == "tneffilertf") && ($add[$cnt]->getSize() > $tnef_minimum_rtf_size_to_decode))
$add_to[] = &$add[$cnt];
}
function getAttachments()
{ {
return $this->attachments; return $this->attachments;
} }
/** public function getMailinfo(): TNEFMailinfo
* @return TNEFMailinfo
*/
function getMailinfo()
{ {
return $this->mailinfo; return $this->mailinfo;
} }
function getBodyElements() public function getBodyElements(): array
{ {
return $this->body; return $this->body;
} }
function decodeTnef($data) public function decodeTnef($data): void
{ {
$buffer = new TNEFBuffer($data); $buffer = new TNEFBuffer($data);
$tnef_signature = tnef_geti32($buffer); $tnef_signature = $buffer->geti32();
if ($tnef_signature == TNEF_SIGNATURE) { if (TNEF_SIGNATURE == $tnef_signature) {
$tnef_key = tnef_geti16($buffer); $tnef_key = $buffer->geti16();
tnef_log(sprintf("Signature: 0x%08x\nKey: 0x%04x\n", $tnef_signature, $tnef_key)); $this->debug && tnef_log(\sprintf("Signature: 0x%08x\nKey: 0x%04x\n", $tnef_signature, $tnef_key));
while ($buffer->getRemainingBytes() > 0) { while ($buffer->getRemainingBytes() > 0) {
$lvl_type = tnef_geti8($buffer); $lvl_type = $buffer->geti8();
switch ($lvl_type) { switch ($lvl_type) {
case TNEF_LVL_MESSAGE: case TNEF_LVL_MESSAGE:
$this->tnef_decode_attribute($buffer);
break;
case TNEF_LVL_ATTACHMENT: case TNEF_LVL_ATTACHMENT:
$this->tnef_decode_attribute($buffer); $this->tnef_decode_attribute($buffer);
break; break;
default: default:
if ($this->debug) {
$len = $buffer->getRemainingBytes(); $len = $buffer->getRemainingBytes();
if ($len > 0) if ($len)
tnef_log("Invalid file format! Unknown Level $lvl_type. Rest=$len"); tnef_log("Invalid file format! Unknown Level {$lvl_type}. Rest={$len}");
} }
break; break;
} }
} }
else } else {
{ throw new \RuntimeException("TNEF: Invalid file format! Wrong signature.");
tnef_log("Invalid file format! Wrong signature.");
} }
// propagate parent message's code page to child files if given // propagate parent message's code page to child files if given
@ -151,14 +129,14 @@ class TNEFAttachment
$this->files[$i]->setMessageCodePage($code_page); $this->files[$i]->setMessageCodePage($code_page);
} }
function tnef_decode_attribute(TNEFBuffer $buffer) private function tnef_decode_attribute(TNEFBuffer $buffer)
{ {
$attribute = tnef_geti32($buffer); // attribute if $attribute = $buffer->geti32(); // attribute if
$length = tnef_geti32($buffer); // length $length = $buffer->geti32(); // length
$value = tnef_getx($length, $buffer); // data $value = $buffer->getBytes($length); // data
$checksumAtt = tnef_geti16($buffer); // checksum $checksumAtt = $buffer->geti16(); // checksum
if ($value !== null && $this->validateChecksum) { if ($value !== null && $this->validateChecksum) {
$checksum = array_sum(unpack('C*', $value)) & 0xFFFF; $checksum = \array_sum(\unpack('C*', $value)) & 0xFFFF;
if ($checksum !== $checksumAtt) { if ($checksum !== $checksumAtt) {
throw new \Exception('Checksums do not match'); throw new \Exception('Checksums do not match');
} }
@ -167,27 +145,26 @@ class TNEFAttachment
switch ($attribute) switch ($attribute)
{ {
case TNEF_ARENDDATA: // marks start of new attachment case TNEF_ARENDDATA: // marks start of new attachment
tnef_log("Creating new File for Attachment"); $this->debug && tnef_log("Creating new File for Attachment");
$this->current_receiver = new TNEFFile(); $this->current_receiver = new TNEFFile($this->debug);
$this->files[] = $this->current_receiver; $this->files[] = $this->current_receiver;
break; break;
case TNEF_AMAPIATTRS: case TNEF_AMAPIATTRS:
tnef_log("mapi attrs"); $this->debug && tnef_log("mapi attrs");
$this->extract_mapi_attrs(new TNEFBuffer($value)); $this->extract_mapi_attrs(new TNEFBuffer($value));
break; break;
case TNEF_AMAPIPROPS: case TNEF_AMAPIPROPS:
tnef_log("mapi props"); $this->debug && tnef_log("mapi props");
$this->extract_mapi_attrs(new TNEFBuffer($value)); $this->extract_mapi_attrs(new TNEFBuffer($value));
break; break;
case TNEF_AMCLASS: case TNEF_AMCLASS:
$value = substr($value, 0, $length - 1); $value = substr($value, 0, $length - 1);
if ($value == 'IPM.Contact') if ($value == 'IPM.Contact') {
{ $this->debug && tnef_log("Creating vCard Attachment");
tnef_log("Creating vCard Attachment"); $this->current_receiver = new TNEFvCard($this->debug);
$this->current_receiver = new TNEFvCard();
$this->files[] = $this->current_receiver; $this->files[] = $this->current_receiver;
} }
break; break;
@ -200,63 +177,59 @@ class TNEFAttachment
} }
} }
function extract_mapi_attrs(TNEFBuffer $buffer) private function extract_mapi_attrs(TNEFBuffer $buffer): void
{ {
$number = tnef_geti32($buffer); // number of attributes $number = $buffer->geti32(); // number of attributes
$props = 0; $props = 0;
$ended = 0; $ended = 0;
while (($buffer->getRemainingBytes() > 0) && ($props < $number) && (!$ended)) while (($buffer->getRemainingBytes() > 0) && ($props++ < $number) && !$ended) {
{
$props++;
$value = ''; $value = '';
unset($named_id); unset($named_id);
$length = 0; $length = 0;
$have_multivalue = 0; $have_multivalue = false;
$num_multivalues = 1; $num_multivalues = 1;
$attr_type = tnef_geti16($buffer); $attr_type = $buffer->geti16();
$attr_name = tnef_geti16($buffer); $attr_name = $buffer->geti16();
if (($attr_type & TNEF_MAPI_MV_FLAG) != 0) if (($attr_type & TNEF_MAPI_MV_FLAG) != 0) {
{ $this->debug && tnef_log("Multivalue Attribute found.");
tnef_log("Multivalue Attribute found."); $have_multivalue = true;
$have_multivalue = 1;
$attr_type = $attr_type & ~TNEF_MAPI_MV_FLAG; $attr_type = $attr_type & ~TNEF_MAPI_MV_FLAG;
} }
if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) // Named Attribute // Named Attribute
{ if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) {
$guid = tnef_getx(16, $buffer); $guid = $buffer->getBytes(16);
$named_type = tnef_geti32($buffer); $named_type = $buffer->geti32();
switch ($named_type) switch ($named_type)
{ {
case TNEF_MAPI_NAMED_TYPE_ID: case TNEF_MAPI_NAMED_TYPE_ID:
$named_id = tnef_geti32($buffer); $named_id = $buffer->geti32();
$attr_name = $named_id; $attr_name = $named_id;
tnef_log(sprintf("Named Id='0x%04x'", $named_id)); $this->debug && tnef_log(sprintf("Named Id='0x%04x'", $named_id));
break; break;
case TNEF_MAPI_NAMED_TYPE_STRING: case TNEF_MAPI_NAMED_TYPE_STRING:
$attr_name = 0x9999; // dummy to identify strings $attr_name = 0x9999; // dummy to identify strings
$idlen = tnef_geti32($buffer); $idlen = $buffer->geti32();
tnef_log("idlen=$idlen"); $this->debug && tnef_log("idlen={$idlen}");
$buflen = $idlen + ((4 - ($idlen % 4)) % 4); // pad to next 4 byte boundary $buflen = $idlen + ((4 - ($idlen % 4)) % 4); // pad to next 4 byte boundary
tnef_log("buflen=$buflen"); $this->debug && tnef_log("buflen={$buflen}");
$named_id = substr(tnef_getx($buflen, $buffer), 0, $idlen ); // read and truncate to length $named_id = substr($buffer->getBytes($buflen), 0, $idlen ); // read and truncate to length
tnef_log("Named Id='$named_id'"); $this->debug && tnef_log("Named Id='{$named_id}'");
break; break;
default: default:
tnef_log(sprintf("Unknown Named Type 0x%04x found", $named_type)); $this->debug && tnef_log(\sprintf("Unknown Named Type 0x%04x found", $named_type));
break; break;
} }
} }
if ($have_multivalue) if ($have_multivalue) {
{ $num_multivalues = $buffer->geti32();
$num_multivalues = tnef_geti32($buffer); $this->debug && tnef_log("Number of multivalues={$num_multivalues}");
tnef_log("Number of multivalues=$num_multivalues");
} }
switch ($attr_type) switch ($attr_type)
@ -265,18 +238,18 @@ class TNEFAttachment
break; break;
case TNEF_MAPI_SHORT: case TNEF_MAPI_SHORT:
$value = tnef_geti16($buffer); $value = $buffer->geti16();
break; break;
case TNEF_MAPI_INT: case TNEF_MAPI_INT:
case TNEF_MAPI_BOOLEAN: case TNEF_MAPI_BOOLEAN:
for ($cnt = 0; $cnt < $num_multivalues; $cnt++) for ($cnt = 0; $cnt < $num_multivalues; $cnt++)
$value = tnef_geti32($buffer); $value = $buffer->geti32();
break; break;
case TNEF_MAPI_FLOAT: case TNEF_MAPI_FLOAT:
case TNEF_MAPI_ERROR: case TNEF_MAPI_ERROR:
$value = tnef_getx(4, $buffer); $value = $buffer->getBytes(4);
break; break;
case TNEF_MAPI_DOUBLE: case TNEF_MAPI_DOUBLE:
@ -284,59 +257,53 @@ class TNEFAttachment
case TNEF_MAPI_CURRENCY: case TNEF_MAPI_CURRENCY:
case TNEF_MAPI_INT8BYTE: case TNEF_MAPI_INT8BYTE:
case TNEF_MAPI_SYSTIME: case TNEF_MAPI_SYSTIME:
$value = tnef_getx(8, $buffer); $value = $buffer->getBytes(8);
break; break;
case TNEF_MAPI_CLSID: case TNEF_MAPI_CLSID:
tnef_log("What is a MAPI CLSID ????"); $this->debug && tnef_log("What is a MAPI CLSID ????");
break; break;
case TNEF_MAPI_STRING: case TNEF_MAPI_STRING:
case TNEF_MAPI_UNICODE_STRING: case TNEF_MAPI_UNICODE_STRING:
case TNEF_MAPI_BINARY: case TNEF_MAPI_BINARY:
case TNEF_MAPI_OBJECT: case TNEF_MAPI_OBJECT:
if ($have_multivalue) $num_vals = $have_multivalue ? $num_multivalues : $buffer->geti32();
$num_vals = $num_multivalues;
else
$num_vals = tnef_geti32($buffer);
if ($num_vals > 20) // A Sanity check. if ($num_vals > 20) {
{ // A Sanity check.
$ended = 1; $ended = 1;
tnef_log("Number of entries in String Attributes=$num_vals. Aborting Mapi parsing."); $this->debug && tnef_log("Number of entries in String Attributes={$num_vals}. Aborting Mapi parsing.");
} } else {
else for ($cnt = 0; $cnt < $num_vals; ++$cnt) {
{ $length = $buffer->geti32();
for ($cnt = 0; $cnt < $num_vals; $cnt++)
{
$length = tnef_geti32($buffer);
$buflen = $length + ((4 - ($length % 4)) % 4); // pad to next 4 byte boundary $buflen = $length + ((4 - ($length % 4)) % 4); // pad to next 4 byte boundary
if ($attr_type == TNEF_MAPI_STRING) if ($attr_type == TNEF_MAPI_STRING)
$length -= 1; $length -= 1;
$value = substr(tnef_getx($buflen, $buffer), 0, $length); // read and truncate to length $value = \substr($buffer->getBytes($buflen), 0, $length); // read and truncate to length
} }
} }
break; break;
default: default:
tnef_log("Unknown mapi attribute! $attr_type"); $this->debug && tnef_log("Unknown mapi attribute! {$attr_type}");
break; break;
} }
switch ($attr_name) switch ($attr_name)
{ {
case TNEF_MAPI_ATTACH_DATA: case TNEF_MAPI_ATTACH_DATA:
tnef_log("MAPI Found nested attachment. Processing new one."); $this->debug && tnef_log("MAPI Found nested attachment. Processing new one.");
$value = substr($value, 16); // skip the next 16 bytes (unknown data) $value = substr($value, 16); // skip the next 16 bytes (unknown data)
$att = new TNEFAttachment($this->validateChecksum); $att = new TNEFAttachment($this->debug, $this->validateChecksum);
$att->decodeTnef($value); $att->decodeTnef($value);
$this->attachments[] = $att; $this->attachments[] = $att;
tnef_log("MAPI Finished nested attachment. Continuing old one."); $this->debug && tnef_log("MAPI Finished nested attachment. Continuing old one.");
break; break;
case TNEF_MAPI_RTF_COMPRESSED: case TNEF_MAPI_RTF_COMPRESSED:
tnef_log("MAPI Found Compressed RTF Attachment."); $this->debug && tnef_log("MAPI Found Compressed RTF Attachment.");
$this->files[] = new TNEFFileRTF($value); $this->files[] = new TNEFFileRTF($this->debug, $value);
break; break;
case TNEF_MAPI_BODY: case TNEF_MAPI_BODY:
case TNEF_MAPI_BODY_HTML: case TNEF_MAPI_BODY_HTML:
@ -349,23 +316,18 @@ class TNEFAttachment
$this->body[] = $result; $this->body[] = $result;
break; break;
default: default:
$this->mailinfo->receiveMapiAttribute($attr_type, $attr_name, $value, $length, ($attr_type == TNEF_MAPI_UNICODE_STRING)); $this->mailinfo->receiveMapiAttribute($attr_type, $attr_name, $value, $length);
if ($this->current_receiver) if ($this->current_receiver)
$this->current_receiver->receiveMapiAttribute($attr_type, $attr_name, $value, $length, ($attr_type == TNEF_MAPI_UNICODE_STRING)); $this->current_receiver->receiveMapiAttribute($attr_type, $attr_name, $value, $length);
break; break;
} }
} }
if ($ended) if ($this->debug && $ended) {
{
$len = $buffer->getRemainingBytes(); $len = $buffer->getRemainingBytes();
for ($cnt = 0; $cnt < $len; $cnt++) for ($cnt = 0; $cnt < $len; ++$cnt) {
{ $ord = $buffer->geti8();
$ord = tnef_geti8($buffer); $char = $ord ? \chr($ord) : '';
if ($ord == 0) tnef_log(\sprintf("Char Nr. %6d = 0x%02x = '%s'", $cnt, $ord, $char));
$char = "";
else
$char = chr($ord);
tnef_log(sprintf("Char Nr. %6d = 0x%02x = '%s'", $cnt, $ord, $char));
} }
} }
} }

View file

@ -19,15 +19,14 @@ namespace TNEFDecoder;
class TNEFBuffer class TNEFBuffer
{ {
private string $data; private string $data;
private int $offset; private int $offset = 0;
function __construct(string $data) public function __construct(string $data)
{ {
$this->data = $data; $this->data = $data;
$this->offset = 0;
} }
function getBytes(int $numBytes): ?string public function getBytes(int $numBytes): ?string
{ {
if ($this->getRemainingBytes() < $numBytes) { if ($this->getRemainingBytes() < $numBytes) {
$this->offset = \strlen($this->data); $this->offset = \strlen($this->data);
@ -38,11 +37,30 @@ class TNEFBuffer
return \substr($this->data, $this->offset - $numBytes, $numBytes); return \substr($this->data, $this->offset - $numBytes, $numBytes);
} }
function getRemainingBytes(): int public function getRemainingBytes(): int
{ {
return \strlen($this->data) - $this->offset; return \strlen($this->data) - $this->offset;
} }
public function geti8(): ?int
{
$bytes = $this->getBytes(1);
return (null === $bytes) ? null : \ord($bytes[0]);
} }
public function geti16(): ?int
{
$bytes = $this->getBytes(2);
return (null === $bytes) ? null : \ord($bytes[0]) + (\ord($bytes[1]) << 8);
}
public function geti32(): ?int
{
$bytes = $this->getBytes(4);
return (null === $bytes) ? null
: \ord($bytes[0])
+ (\ord($bytes[1]) << 8)
+ (\ord($bytes[2]) << 16)
+ (\ord($bytes[3]) << 24);
}
}

View file

@ -14,31 +14,24 @@
* *
*/ */
class TNEFDate class TNEFDate extends \DateTime
{ {
public function __construct(string $datetime = "now", ?\DateTimeZone $timezone = null)
private ?int $year; {
private ?int $month; parent::__construct($datetime, new \DateTimeZone('UTC'));
private ?int $day; }
private ?int $hour;
private ?int $minute;
private ?int $second;
public function setTnefBuffer(TNEFBuffer $buffer) public function setTnefBuffer(TNEFBuffer $buffer)
{ {
$this->year = tnef_geti16($buffer); $this->setDate(
$this->month = tnef_geti16($buffer); $buffer->geti16(), // year
$this->day = tnef_geti16($buffer); $buffer->geti16(), // month
$this->hour = tnef_geti16($buffer); $buffer->geti16() // day
$this->minute = tnef_geti16($buffer); );
$this->second = tnef_geti16($buffer); $this->setTime(
$buffer->geti16(), // hour
$buffer->geti16(), // minute
$buffer->geti16() // second
);
} }
public function __toString(): string
{
return \sprintf("%04d-%02d-%02d %02d:%02d:%02d",
$this->year, $this->month, $this->day,
$this->hour, $this->minute, $this->second);
}
} }

View file

@ -1,4 +1,6 @@
<?php namespace TNEFDecoder; <?php
namespace TNEFDecoder;
/** /**
* SquirrelMail TNEF Decoder Plugin * SquirrelMail TNEF Decoder Plugin
@ -17,14 +19,14 @@
class TNEFFile extends TNEFFileBase class TNEFFile extends TNEFFileBase
{ {
var $metafile; public string $metafile;
function getMetafile() public function getMetafile()
{ {
return $this->metafile; return $this->metafile;
} }
function receiveTnefAttribute($attribute, $value, $length) public function receiveTnefAttribute(int $attribute, string $value, int $length): void
{ {
switch ($attribute) switch ($attribute)
{ {
@ -34,18 +36,18 @@ class TNEFFile extends TNEFFileBase
case TNEF_AFILENAME: case TNEF_AFILENAME:
// strip path // strip path
// //
if (($pos = strrpos($value, '/')) !== FALSE) if (($pos = \strrpos($value, '/')) !== FALSE)
$this->name = substr($value, $pos + 1); $this->name = \substr($value, $pos + 1);
else else
$this->name = $value; $this->name = $value;
// Strip trailing null bytes if present // Strip trailing null bytes if present
$this->name = trim($this->name); $this->name = \trim($this->name);
break; break;
// code page // code page
// //
case TNEF_AOEMCODEPAGE: case TNEF_AOEMCODEPAGE:
$this->code_page = tnef_geti16(new TNEFBuffer($value)); $this->code_page = (new TNEFBuffer($value))->geti16();
break; break;
// the attachment itself // the attachment itself
@ -71,7 +73,7 @@ class TNEFFile extends TNEFFileBase
} }
} }
function receiveMapiAttribute($attr_type, $attr_name, $value, $length, $is_unicode=FALSE) public function receiveMapiAttribute(int $attr_type, int $attr_name, string $value, int $length): void
{ {
switch ($attr_name) switch ($attr_name)
{ {
@ -81,26 +83,26 @@ class TNEFFile extends TNEFFileBase
case TNEF_MAPI_ATTACH_LONG_FILENAME: case TNEF_MAPI_ATTACH_LONG_FILENAME:
// strip path // strip path
// //
if (($pos = strrpos($value, '/')) !== FALSE) if (($pos = \strrpos($value, '/')) !== FALSE)
$this->name = substr($value, $pos + 1); $this->name = \substr($value, $pos + 1);
else else
$this->name = $value; $this->name = $value;
if ($is_unicode) $this->name_is_unicode = TRUE; $this->name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
break; break;
// Is this ever set, and what is format? // Is this ever set, and what is format?
// //
case TNEF_MAPI_ATTACH_MIME_TAG: case TNEF_MAPI_ATTACH_MIME_TAG:
$type0 = $type1 = ''; $type0 = $type1 = '';
$mime_type = explode('/', $value, 2); $mime_type = \explode('/', $value, 2);
if (!empty($mime_type[0])) if (!empty($mime_type[0]))
$type0 = $mime_type[0]; $type0 = $mime_type[0];
if (!empty($mime_type[1])) if (!empty($mime_type[1]))
$type1 = $mime_type[1]; $type1 = $mime_type[1];
$this->type = "$type0/$type1"; $this->type = "{$type0}/{$type1}";
if ($is_unicode) { if (TNEF_MAPI_UNICODE_STRING === $attr_type) {
$this->type = substr(mb_convert_encoding($this->type, "UTF-8" , "UTF-16LE"), 0, -1); $this->type = \substr(\mb_convert_encoding($this->type, "UTF-8" , "UTF-16LE"), 0, -1);
} }
break; break;

View file

@ -1,4 +1,6 @@
<?php namespace TNEFDecoder; <?php
namespace TNEFDecoder;
/** /**
* SquirrelMail TNEF Decoder Plugin * SquirrelMail TNEF Decoder Plugin
@ -17,36 +19,64 @@
class TNEFFileBase class TNEFFileBase
{ {
public bool $name_is_unicode = FALSE; public bool $name_is_unicode = FALSE;
public string $name = 'Untitled'; public string
public string $code_page = ''; $name = 'Untitled',
public string $message_code_page = ''; // parent message's code page (the whole TNEF file) $code_page = '',
public string $type = 'application/octet-stream'; $message_code_page = '', // parent message's code page (the whole TNEF file)
public string $content = ''; $type = 'application/octet-stream',
var $created; $content = '';
var $modified; public ?TNEFDate
$created = null,
$modified = null;
public bool $debug;
function setMessageCodePage(string $code_page) public function __construct(bool $debug)
{
$this->debug = $debug;
}
public function setMessageCodePage(string $code_page): void
{ {
$this->message_code_page = $code_page; $this->message_code_page = $code_page;
} }
function getCodePage() public function getCodePage(): string
{ {
return empty($this->code_page) return empty($this->code_page)
? $this->message_code_page ? $this->message_code_page
: $this->code_page; : $this->code_page;
} }
function getName() public function getName(): string
{ {
if ($this->name_is_unicode) { return $this->name_is_unicode
return \substr(\mb_convert_encoding($this->name, "UTF-8" , "UTF-16LE"), 0, -1); ? \substr(\mb_convert_encoding($this->name, "UTF-8" , "UTF-16LE"), 0, -1)
} : $this->name;
return $this->name;
} }
function getSize() public function getType(): string
{
return $this->type;
}
public function getSize(): int
{ {
return \strlen($this->content); return \strlen($this->content);
} }
public function getCreated(): ?TNEFDate
{
return $this->created;
}
public function getModified(): ?TNEFDate
{
return $this->modified;
}
public function getContent(): string
{
return $this->content;
}
} }

View file

@ -15,34 +15,33 @@
*/ */
class TNEFFileRTF extends TNEFFileBase class TNEFFileRTF extends TNEFFileBase
{ {
var $size; public string $name = 'EmbeddedRTF.rtf';
public string $type = 'application/rtf';
protected int $size = 0;
const MAX_DICT_SIZE = 4096; const MAX_DICT_SIZE = 4096;
const INIT_DICT_SIZE = 207; const INIT_DICT_SIZE = 207;
function __construct($data) public function __construct($debug, $data)
{ {
parent::__construct(); parent::__construct($debug);
$this->type = "application/rtf";
$this->name = "EmbeddedRTF.rtf";
$this->decode_crtf(new TNEFBuffer($data)); $this->decode_crtf(new TNEFBuffer($data));
} }
function getSize() public function getSize(): int
{ {
return $this->size; return $this->size;
} }
function decode_crtf(TNEFBuffer $buffer) public function decode_crtf(TNEFBuffer $buffer)
{ {
$size_compressed = tnef_geti32($buffer); $size_compressed = $buffer->geti32();
$this->size = tnef_geti32($buffer); $this->size = $buffer->geti32();
$magic = tnef_geti32($buffer); $magic = $buffer->geti32();
$crc32 = tnef_geti32($buffer); $crc32 = $buffer->geti32();
tnef_log("CRTF: size comp=$size_compressed, size=$this->size"); $this->debug && tnef_log("CRTF: size comp={$size_compressed}, size={$this->size}");
$data = tnef_getx($buffer->getRemainingBytes(), $buffer); $data = $buffer->getBytes($buffer->getRemainingBytes());
switch ($magic) { switch ($magic) {
case CRTF_COMPRESSED: case CRTF_COMPRESSED:
@ -54,33 +53,33 @@ class TNEFFileRTF extends TNEFFileBase
break; break;
default: default:
tnef_log("Unknown Compressed RTF Format"); $this->debug && tnef_log("Unknown Compressed RTF Format");
break; break;
} }
} }
function uncompress($data) public function uncompress($data)
{ {
$preload = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx"; $preload = "{\\rtf1\\ansi\\mac\\deff0\\deftab720{\\fonttbl;}{\\f0\\fnil \\froman \\fswiss \\fmodern \\fscript \\fdecor MS Sans SerifSymbolArialTimes New RomanCourier{\\colortbl\\red0\\green0\\blue0\n\r\\par \\pard\\plain\\f0\\fs20\\b\\i\\u\\tab\\tx";
$length_preload = strlen($preload); $length_preload = \strlen($preload);
$init_dict = []; $init_dict = [];
for ($cnt = 0; $cnt < $length_preload; $cnt++) { for ($cnt = 0; $cnt < $length_preload; ++$cnt) {
$init_dict[$cnt] = $preload[$cnt]; $init_dict[$cnt] = $preload[$cnt];
} }
$init_dict = array_merge($init_dict, array_fill(count($init_dict), self::MAX_DICT_SIZE - $length_preload, ' ')); $init_dict = \array_merge($init_dict, \array_fill(\count($init_dict), self::MAX_DICT_SIZE - $length_preload, ' '));
$write_offset = self::INIT_DICT_SIZE; $write_offset = self::INIT_DICT_SIZE;
$this->content = ''; $this->content = '';
$end = false; $end = false;
$in = 0; $in = 0;
$l = strlen($data); $l = \strlen($data);
while (!$end) { while (!$end) {
if ($in >= $l) { if ($in >= $l) {
break; break;
} }
$control = strrev(str_pad(decbin(ord($data[$in++])), 8, 0, STR_PAD_LEFT)); $control = \strrev(\str_pad(\decbin(\ord($data[$in++])), 8, 0, STR_PAD_LEFT));
for ($i = 0; $i < 8; $i++) { for ($i = 0; $i < 8; ++$i) {
if ($control[$i] == '1') { if ($control[$i] == '1') {
$token = unpack("n", $data[$in++] . $data[$in++])[1]; $token = \unpack("n", $data[$in++] . $data[$in++])[1];
$offset = ($token >> 4) & 0b111111111111; $offset = ($token >> 4) & 0b111111111111;
$length = $token & 0b1111; $length = $token & 0b1111;
if ($write_offset == $offset) { if ($write_offset == $offset) {
@ -88,7 +87,7 @@ class TNEFFileRTF extends TNEFFileBase
break; break;
} }
$actual_length = $length + 2; $actual_length = $length + 2;
for ($step = 0; $step < $actual_length; $step++) { for ($step = 0; $step < $actual_length; ++$step) {
$read_offset = ($offset + $step) % self::MAX_DICT_SIZE; $read_offset = ($offset + $step) % self::MAX_DICT_SIZE;
$char = $init_dict[$read_offset]; $char = $init_dict[$read_offset];
$this->content .= $char; $this->content .= $char;

View file

@ -17,91 +17,89 @@
class TNEFMailinfo class TNEFMailinfo
{ {
var $subject; public string
var $topic; $subject = '',
var $topic_is_unicode = FALSE; $topic = '',
var $from; $from = '',
var $from_is_unicode = FALSE; $from_name = '',
var $from_name; $code_page = '';
var $from_name_is_unicode = FALSE; public ?TNEFDate $date_sent = null;
var $date_sent; public bool
var $code_page = ''; $topic_is_unicode = FALSE,
$from_is_unicode = FALSE,
$from_name_is_unicode = FALSE;
public function getTopic(): string
function getTopic()
{ {
return $this->topic; return $this->topic;
} }
function getSubject() public function getSubject(): string
{ {
return $this->subject; return $this->subject;
} }
function getFrom() public function getFrom(): string
{ {
return $this->from; return $this->from;
} }
function getCodePage() public function getCodePage(): string
{ {
return $this->code_page; return $this->code_page;
} }
function getFromName() public function getFromName(): string
{ {
return $this->from_name; return $this->from_name;
} }
function getDateSent() public function getDateSent(): TNEFDate
{ {
return $this->date_sent; return $this->date_sent;
} }
function receiveTnefAttribute($attribute, $value, $length) public function receiveTnefAttribute(int $attribute, string $value, int $length): void
{ {
$value = new TNEFBuffer($value); $value = new TNEFBuffer($value);
switch ($attribute) switch ($attribute)
{ {
case TNEF_AOEMCODEPAGE: case TNEF_AOEMCODEPAGE:
$this->code_page = tnef_geti16($value); $this->code_page = $value->geti16();
break; break;
case TNEF_ASUBJECT: case TNEF_ASUBJECT:
$this->subject = tnef_getx($length - 1, $value); $this->subject = $value->getBytes($length - 1);
break; break;
case TNEF_ADATERECEIVED: case TNEF_ADATERECEIVED:
if (!$this->date_sent) if ($this->date_sent) {
{
$this->date_sent = new TNEFDate();
$this->date_sent->setTnefBuffer($value);
}
break; break;
}
case TNEF_ADATESENT: case TNEF_ADATESENT:
$this->date_sent = new TNEFDate(); $this->date_sent = new TNEFDate();
$this->date_sent->setTnefBuffer($value); $this->date_sent->setTnefBuffer($value);
} }
} }
function receiveMapiAttribute($attr_type, $attr_name, $value, $length, $is_unicode=FALSE) public function receiveMapiAttribute(int $attr_type, int $attr_name, string $value, int $length): void
{ {
switch ($attr_name) switch ($attr_name)
{ {
case TNEF_MAPI_CONVERSATION_TOPIC: case TNEF_MAPI_CONVERSATION_TOPIC:
$this->topic = $value; $this->topic = $value;
if ($is_unicode) $this->topic_is_unicode = TRUE; $this->topic_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
break; break;
case TNEF_MAPI_SENT_REP_EMAIL_ADDR: case TNEF_MAPI_SENT_REP_EMAIL_ADDR:
$this->from = $value; $this->from = $value;
if ($is_unicode) $this->from_is_unicode = TRUE; $this->from_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
break; break;
case TNEF_MAPI_SENT_REP_NAME: case TNEF_MAPI_SENT_REP_NAME:
$this->from_name = $value; $this->from_name = $value;
if ($is_unicode) $this->from_name_is_unicode = TRUE; $this->from_name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
break; break;
} }
} }

View file

@ -18,28 +18,26 @@ namespace TNEFDecoder;
class TNEFvCard extends TNEFFileBase class TNEFvCard extends TNEFFileBase
{ {
public bool $name_is_unicode = FALSE;
public string $name = 'Untitled';
public string $code_page = '';
public string $message_code_page = ''; // parent message's code page (the whole TNEF file)
public string $type = 'text/x-vcard'; public string $type = 'text/x-vcard';
public string $content = '';
var $metafile; public bool
var $surname; $surname_is_unicode = FALSE,
var $surname_is_unicode = FALSE; $given_name_is_unicode = FALSE,
var $given_name; $middle_name_is_unicode = FALSE,
var $given_name_is_unicode = FALSE; $nickname_is_unicode = FALSE,
var $middle_name; $company_is_unicode = FALSE;
var $middle_name_is_unicode = FALSE; public string
var $nickname; $surname,
var $nickname_is_unicode = FALSE; $given_name,
var $company; $middle_name,
var $company_is_unicode = FALSE; $nickname,
var $homepages; $company,
var $addresses; $metafile;
var $emails; public array
var $telefones; $homepages = [],
$addresses = [],
$emails = [],
$telefones = [];
private static private static
$address_mapping = array ( $address_mapping = array (
@ -97,206 +95,155 @@ class TNEFvCard extends TNEFFileBase
TNEF_MAPI_HOME_FAX_NUMBER => "Home Fax", TNEF_MAPI_HOME_FAX_NUMBER => "Home Fax",
); );
function __construct() public function getSurname(): string
{
$this->telefones = array();
$this->homepages = array();
$this->emails = array();
$this->addresses = array();
}
function getSurname()
{ {
return $this->surname; return $this->surname;
} }
function getGivenName() public function getGivenName(): string
{ {
return $this->given_name; return $this->given_name;
} }
function getMiddleName() public function getMiddleName(): string
{ {
return $this->middle_name; return $this->middle_name;
} }
function getNickname() public function getNickname(): string
{ {
return $this->nickname; return $this->nickname;
} }
function getCompany() public function getCompany(): string
{ {
return $this->company; return $this->company;
} }
function getAddresses() public function getAddresses(): array
{ {
return $this->addresses; return $this->addresses;
} }
function getMetafile() public function getMetafile()
{ {
return $this->metafile; return $this->metafile;
} }
function getTelefones() public function getTelefones(): array
{ {
return $this->telefones; return $this->telefones;
} }
function getHomepages() public function getHomepages(): array
{ {
return $this->homepages; return $this->homepages;
} }
function getEmails() public function getEmails(): array
{ {
return $this->emails; return $this->emails;
} }
function receiveTnefAttribute($attribute, $value, $length) public function receiveTnefAttribute(int $attribute, string $value, int $length): void
{ {
switch ($attribute) switch ($attribute)
{ {
// code page // code page
// //
case TNEF_AOEMCODEPAGE: case TNEF_AOEMCODEPAGE:
$this->code_page = tnef_geti16(new TNEFBuffer($value)); $this->code_page = (new TNEFBuffer($value))->geti16();
break; break;
} }
} }
function receiveMapiAttribute($attr_type, $attr_name, $value, $length, $is_unicode=FALSE) public function receiveMapiAttribute(int $attr_type, int $attr_name, string $value, int $length)
{ {
switch ($attr_name) switch ($attr_name)
{ {
case TNEF_MAPI_DISPLAY_NAME: case TNEF_MAPI_DISPLAY_NAME:
$this->name = $value; $this->name = $value;
$this->name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
if ($is_unicode) $this->name_is_unicode = TRUE;
break; break;
case TNEF_MAPI_SURNAME: case TNEF_MAPI_SURNAME:
$this->surname = $value; $this->surname = $value;
$this->surname_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
if ($is_unicode) $this->surname_is_unicode = TRUE;
break; break;
case TNEF_MAPI_GIVEN_NAME: case TNEF_MAPI_GIVEN_NAME:
$this->given_name = $value; $this->given_name = $value;
$this->given_name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
if ($is_unicode) $this->given_name_is_unicode = TRUE;
break; break;
case TNEF_MAPI_MIDDLE_NAME: case TNEF_MAPI_MIDDLE_NAME:
$this->middle_name = $value; $this->middle_name = $value;
$this->middle_name_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
if ($is_unicode) $this->middle_name_is_unicode = TRUE;
break; break;
case TNEF_MAPI_NICKNAME: case TNEF_MAPI_NICKNAME:
$this->nickname = $value; $this->nickname = $value;
$this->nickname_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
if ($is_unicode) $this->nickname_is_unicode = TRUE;
break; break;
case TNEF_MAPI_COMPANY_NAME: case TNEF_MAPI_COMPANY_NAME:
$this->company = $value; $this->company = $value;
$this->company_is_unicode = TNEF_MAPI_UNICODE_STRING === $attr_type;
if ($is_unicode) $this->company_is_unicode = TRUE;
break; break;
default: default:
$rc = $this->evaluateTelefoneAttribute($attr_type, $attr_name, $value, $length); $this->evaluateTelefoneAttribute($attr_type, $attr_name, $value, $length)
if (!$rc) || $this->evaluateEmailAttribute($attr_type, $attr_name, $value, $length)
$rc = $this->evaluateEmailAttribute($attr_type, $attr_name, $value, $length); || $this->evaluateAddressAttribute($attr_type, $attr_name, $value, $length)
if (!$rc) || $this->evaluateHomepageAttribute($attr_type, $attr_name, $value, $length);
$rc = $this->evaluateAddressAttribute($attr_type, $attr_name, $value, $length);
if (!$rc)
$rc = $this->evaluateHomepageAttribute($attr_type, $attr_name, $value, $length);
break; break;
} }
} }
function evaluateTelefoneAttribute($attr_type, $attr_name, $value, $length) private function evaluateTelefoneAttribute(int $attr_type, int $attr_name, string $value, int $length): bool
{
$rc = 0;
if ($length > 0)
{
if (array_key_exists($attr_name, static::$telefone_mapping))
{ {
if ($length && \array_key_exists($attr_name, static::$telefone_mapping)) {
$telefone_key = static::$telefone_mapping[$attr_name]; $telefone_key = static::$telefone_mapping[$attr_name];
$this->telefones[$telefone_key] = $value; $this->telefones[$telefone_key] = $value;
$rc = 1; $this->debug && tnef_log("Setting telefone '{$telefone_key}' to value '{$value}'");
tnef_log("Setting telefone '$telefone_key' to value '$value'"); return true;
} }
return false;
} }
return $rc; private function evaluateEmailAttribute(int $attr_type, int $attr_name, string $value, int $length): bool
}
function evaluateEmailAttribute($attr_type, $attr_name, $value, $length)
{
$rc = 0;
if ($length > 0)
{
if (array_key_exists($attr_name, static::$email_mapping))
{ {
if ($length && \array_key_exists($attr_name, static::$email_mapping)) {
$email_key = static::$email_mapping[$attr_name]; $email_key = static::$email_mapping[$attr_name];
if (!array_key_exists($email_key[0], $this->emails)) if (!\array_key_exists($email_key[0], $this->emails))
$this->emails[$email_key[0]] = array(EMAIL_DISPLAY => "", EMAIL_TRANSPORT => "", EMAIL_EMAIL => "", EMAIL_EMAIL2 => ""); $this->emails[$email_key[0]] = array(EMAIL_DISPLAY => "", EMAIL_TRANSPORT => "", EMAIL_EMAIL => "", EMAIL_EMAIL2 => "");
$this->emails[$email_key[0]][$email_key[1]] = $value; $this->emails[$email_key[0]][$email_key[1]] = $value;
return true;
} }
return false;
} }
return $rc; private function evaluateAddressAttribute(int $attr_type, int $attr_name, string $value, int $length): bool
}
function evaluateAddressAttribute($attr_type, $attr_name, $value, $length)
{
$rc = 0;
if ($length > 0)
{
if (array_key_exists($attr_name, static::$address_mapping))
{ {
if ($length && \array_key_exists($attr_name, static::$address_mapping)) {
$address_key = static::$address_mapping[$attr_name]; $address_key = static::$address_mapping[$attr_name];
if (!array_key_exists($address_key[0], $this->addresses)) if (!\array_key_exists($address_key[0], $this->addresses))
$this->addresses[$address_key[0]] = array(); $this->addresses[$address_key[0]] = array();
$this->addresses[$address_key[0]][$address_key[1]] = $value; $this->addresses[$address_key[0]][$address_key[1]] = $value;
return true;
} }
return false;
} }
return $rc; private function evaluateHomepageAttribute(int $attr_type, int $attr_name, string $value, int $length): bool
}
function evaluateHomepageAttribute($attr_type, $attr_name, $value, $length)
{
$rc = 0;
if ($length > 0)
{
if (array_key_exists($attr_name, static::$homepage_mapping))
{ {
if ($length && \array_key_exists($attr_name, static::$homepage_mapping)) {
$homepage_key = static::$homepage_mapping[$attr_name]; $homepage_key = static::$homepage_mapping[$attr_name];
$this->homepages[$homepage_key] = $value; $this->homepages[$homepage_key] = $value;
$rc = 1; $this->debug && tnef_log("Setting homepage '{$homepage_key}' to value '{$value}'");
tnef_log("Setting homepage '$homepage_key' to value '$value'"); return true;
} }
} return false;
return $rc;
} }
} }

View file

@ -194,7 +194,6 @@ define ("EMAIL_DISPLAY", 1);
define ("EMAIL_TRANSPORT", 2); define ("EMAIL_TRANSPORT", 2);
define ("EMAIL_EMAIL", 3); define ("EMAIL_EMAIL", 3);
define ("EMAIL_EMAIL2", 4); define ("EMAIL_EMAIL2", 4);
define ("ADDRESS_STREET", "Street"); define ("ADDRESS_STREET", "Street");
define ("ADDRESS_ZIP", "Zip"); define ("ADDRESS_ZIP", "Zip");
define ("ADDRESS_CITY", "City"); define ("ADDRESS_CITY", "City");

View file

@ -1,96 +0,0 @@
<?php namespace TNEFDecoder;
/**
* SquirrelMail TNEF Decoder Plugin
*
* Copyright (c) 2010- Paul Lesniewski <paul@squirrelmail.org>
* Copyright (c) 2003 Bernd Wiegmann <bernd@wib-software.de>
* Copyright (c) 2002 Graham Norburys <gnorbury@bondcar.com>
*
* Licensed under the GNU GPL. For full terms see the file COPYING.
*
* @package plugins
* @subpackage tnef_decoder
*
*/
/**
* Debugging output --> to a file (/tmp/squirrelmail_tnef_decoder.log)
*
* Note this assumes a world-writable /tmp directory
*
* @param string $string The text to be logged
*
* @return boolean TRUE on success, FALSE when a problem occurred
*
*/
function tnef_log($string)
{
\SnappyMail\Log::debug('TNEF', $string);
}
/**
* TNEF decoding helper function
*
*/
function tnef_getx($size, TNEFBuffer $buf)
{
return $buf->getBytes($size);
}
/**
* TNEF decoding helper function
*
*/
function tnef_geti8(TNEFBuffer $buf)
{
$bytes = $buf->getBytes(1, $buf);
if ($bytes === null) {
return null;
}
return ord($bytes[0]);
}
/**
* TNEF decoding helper function
*
*/
function tnef_geti16(TNEFBuffer $buf): ?int
{
$bytes = $buf->getBytes(2, $buf);
if ($bytes === null) {
return null;
}
return ord($bytes[0])
+ (ord($bytes[1]) << 8);
}
/**
* TNEF decoding helper function
*
*/
function tnef_geti32(TNEFBuffer $buf): ?int
{
$bytes = $buf->getBytes(4, $buf);
if ($bytes === null) {
return null;
}
return ord($bytes[0])
+ (ord($bytes[1]) << 8)
+ (ord($bytes[2]) << 16)
+ (ord($bytes[3]) << 24);
}