Improved MIME handling for JSON

This commit is contained in:
the-djmaze 2023-01-05 10:55:59 +01:00
parent d4e1e90eef
commit a2ddd493ce
11 changed files with 134 additions and 143 deletions

View file

@ -11,6 +11,8 @@
namespace MailSo\Imap;
use MailSo\Mime\ParameterCollection;
/**
* @category MailSo
* @package Imap
@ -19,9 +21,9 @@ class BodyStructure
{
private string $sContentType;
private string $sCharset;
private array $aContentTypeParams;
private array $aBodyParams;
private string $sCharset;
private string $sContentID;
@ -63,6 +65,11 @@ class BodyStructure
return $this->sContentType;
}
public function ContentTypeParameters() : array
{
return $this->aContentTypeParams;
}
public function Size() : int
{
return $this->iSize;
@ -133,8 +140,8 @@ class BodyStructure
{
// https://datatracker.ietf.org/doc/html/rfc3156#section-4
return 'multipart/encrypted' === $this->sContentType
&& !empty($this->aBodyParams['protocol'])
&& 'application/pgp-encrypted' === \strtolower(\trim($this->aBodyParams['protocol']))
&& !empty($this->aContentTypeParams['protocol'])
&& 'application/pgp-encrypted' === \strtolower(\trim($this->aContentTypeParams['protocol']))
// The multipart/encrypted body MUST consist of exactly two parts.
&& 2 === \count($this->aSubParts)
&& 'application/pgp-encrypted' === $this->aSubParts[0]->ContentType()
@ -146,8 +153,8 @@ class BodyStructure
{
// https://datatracker.ietf.org/doc/html/rfc3156#section-5
return 'multipart/signed' === $this->sContentType
&& !empty($this->aBodyParams['protocol'])
&& 'application/pgp-signature' === \strtolower(\trim($this->aBodyParams['protocol']))
&& !empty($this->aContentTypeParams['protocol'])
&& 'application/pgp-signature' === \strtolower(\trim($this->aContentTypeParams['protocol']))
// The multipart/signed body MUST consist of exactly two parts.
&& 2 === \count($this->aSubParts)
&& $this->aSubParts[1]->IsPgpSignature();
@ -172,8 +179,8 @@ class BodyStructure
public function IsFlowedFormat() : bool
{
return !empty($this->aBodyParams['format'])
&& 'flowed' === \strtolower(\trim($this->aBodyParams['format']))
return !empty($this->aContentTypeParams['format'])
&& 'flowed' === \strtolower(\trim($this->aContentTypeParams['format']))
&& !\in_array($this->sMailEncodingName, array('base64', 'quoted-printable'));
}
@ -255,20 +262,15 @@ class BodyStructure
public function GetPartByMimeIndex(string $sMimeIndex) : self
{
$oPart = null;
if (\strlen($sMimeIndex))
{
if ($sMimeIndex === $this->sPartID)
{
if (\strlen($sMimeIndex)) {
if ($sMimeIndex === $this->sPartID) {
$oPart = $this;
}
if (null === $oPart)
{
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart)
{
if (null === $oPart) {
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) {
$oPart = $oSubPart->GetPartByMimeIndex($sMimeIndex);
if (null !== $oPart)
{
if (null !== $oPart) {
break;
}
}
@ -281,60 +283,43 @@ class BodyStructure
private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset) : string
{
$sResult = '';
if (isset($aParams[$sParamName]))
{
if (isset($aParams[$sParamName])) {
$sResult = \MailSo\Base\Utils::DecodeHeaderValue($aParams[$sParamName], $sCharset);
}
else if (isset($aParams[$sParamName.'*']))
{
} else if (isset($aParams[$sParamName.'*'])) {
$aValueParts = \explode("''", $aParams[$sParamName.'*'], 2);
if (2 === \count($aValueParts))
{
if (2 === \count($aValueParts)) {
$sCharset = isset($aValueParts[0]) ? $aValueParts[0] : \MailSo\Base\Enumerations\Charset::UTF_8;
$sResult = \MailSo\Base\Utils::ConvertEncoding(
\urldecode($aValueParts[1]), $sCharset, \MailSo\Base\Enumerations\Charset::UTF_8);
}
else
{
} else {
$sResult = \urldecode($aParams[$sParamName.'*']);
}
}
else
{
} else {
$sCharset = '';
$sCharsetIndex = -1;
$aFileNames = array();
foreach ($aParams as $sName => $sValue)
{
foreach ($aParams as $sName => $sValue) {
$aMatches = array();
if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches))
{
if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches)) {
$iIndex = (int) $aMatches[1];
if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''"))
{
if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''")) {
$aValueParts = \explode("''", $sValue, 2);
if (2 === \count($aValueParts) && \strlen($aValueParts[0]))
{
if (2 === \count($aValueParts) && \strlen($aValueParts[0])) {
$sCharsetIndex = $iIndex;
$sCharset = $aValueParts[0];
$sValue = $aValueParts[1];
}
}
$aFileNames[$iIndex] = $sValue;
}
}
if (\count($aFileNames))
{
if (\count($aFileNames)) {
\ksort($aFileNames, SORT_NUMERIC);
$sResult = \implode(\array_values($aFileNames));
$sResult = \urldecode($sResult);
if (\strlen($sCharset))
{
if (\strlen($sCharset)) {
$sResult = \MailSo\Base\Utils::ConvertEncoding($sResult,
$sCharset, \MailSo\Base\Enumerations\Charset::UTF_8);
}
@ -353,8 +338,8 @@ class BodyStructure
$sContentTypeMain = '';
$sContentTypeSub = '';
$aSubParts = array();
$aBodyParams = array();
$sName = '';
$aContentTypeParams = array();
$sFileName = '';
$sCharset = ''; // \MailSo\Base\Enumerations\Charset::UTF_8 ?
$sContentID = '';
$sDescription = '';
@ -362,20 +347,16 @@ class BodyStructure
$iSize = 0;
$iExtraItemPos = 0; // list index of items which have no well-established position (such as 0, 1, 5, etc).
if (\is_array($aBodyStructure[0]))
{
if (\is_array($aBodyStructure[0])) {
// Process multipart body structure
$sContentTypeMain = 'multipart';
$sContentTypeSub = 'mixed'; // primary default
$sSubPartIDPrefix = '';
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1])
{
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) {
// This multi-part is root part of message.
$sSubPartIDPrefix = $sPartID;
$sPartID .= 'TEXT';
}
else if (\strlen($sPartID))
{
} else if (\strlen($sPartID)) {
// This multi-part is a part of another multi-part.
$sSubPartIDPrefix = $sPartID.'.';
}
@ -387,18 +368,15 @@ class BodyStructure
("text" "plain" ("charset" "utf-8") )
("text" "html" )
*/
while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos]))
{
while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) {
$oPart = self::NewInstance($aBodyStructure[$iExtraItemPos], $sSubPartIDPrefix.$iIndex);
if (!$oPart)
{
if (!$oPart) {
return null;
}
// For multipart, we have no charset info in the part itself. Thus,
// obtain charset from nested parts.
if (!$sCharset)
{
if (!$sCharset) {
$sCharset = $oPart->Charset();
}
@ -411,26 +389,20 @@ class BodyStructure
* Now process the subparts containter like:
"alternative" ("boundary" "--boundary_id")
*/
if ($iExtraItemPos < \count($aBodyStructure))
{
if (!\is_string($aBodyStructure[$iExtraItemPos]))
{
if ($iExtraItemPos < \count($aBodyStructure)) {
if (!\is_string($aBodyStructure[$iExtraItemPos])) {
return null;
}
$sContentTypeSub = \strtolower($aBodyStructure[$iExtraItemPos]);
++$iExtraItemPos;
if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos]))
{
$aBodyParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]);
if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) {
$aContentTypeParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]);
}
}
}
else if (\is_string($aBodyStructure[0]))
{
} else if (\is_string($aBodyStructure[0])) {
// Process simple (singlepart) body structure
if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1]))
{
if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1])) {
return null;
}
@ -438,39 +410,33 @@ class BodyStructure
$sContentTypeSub = \strtolower($aBodyStructure[1]);
$aBodyParamList = $aBodyStructure[2];
if (\is_array($aBodyParamList))
{
$aBodyParams = self::getKeyValueListFromArrayList($aBodyParamList);
if (isset($aBodyParams['charset']))
{
$sCharset = $aBodyParams['charset'];
if (\is_array($aBodyParamList)) {
$aContentTypeParams = self::getKeyValueListFromArrayList($aBodyParamList);
if (isset($aContentTypeParams['charset'])) {
$sCharset = $aContentTypeParams['charset'];
}
$sFileName = self::decodeAttrParameter($aContentTypeParams, 'name', $sCharset);
if ($sFileName) {
$aContentTypeParams['name'] = $sFileName;
}
$sName = self::decodeAttrParameter($aBodyParams, 'name', $sCharset);
}
if (null !== $aBodyStructure[3])
{
if (!\is_string($aBodyStructure[3]))
{
if (null !== $aBodyStructure[3]) {
if (!\is_string($aBodyStructure[3])) {
return null;
}
$sContentID = $aBodyStructure[3];
}
if (null !== $aBodyStructure[4])
{
if (!\is_string($aBodyStructure[4]))
{
if (null !== $aBodyStructure[4]) {
if (!\is_string($aBodyStructure[4])) {
return null;
}
$sDescription = $aBodyStructure[4];
}
if (null !== $aBodyStructure[5])
{
if (!\is_string($aBodyStructure[5]))
{
if (null !== $aBodyStructure[5]) {
if (!\is_string($aBodyStructure[5])) {
return null;
}
$sMailEncodingName = $aBodyStructure[5];
@ -478,24 +444,20 @@ class BodyStructure
$iSize = \is_numeric($aBodyStructure[6]) ? (int) $aBodyStructure[6] : -1;
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1])
{
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) {
// This is the only sub-part of the message (otherwise, it would be
// one of sub-parts of a multi-part, and partID would already be fully set up).
$sPartID .= '1';
}
$iExtraItemPos = 7;
if ('text' === $sContentTypeMain)
{
if ('text' === $sContentTypeMain) {
/**
* A body type of type TEXT contains, immediately after the basic
* fields, the size of the body in text lines.
*/
++$iExtraItemPos;
}
else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub)
{
} else if ('message' === $sContentTypeMain && 'rfc822' === $sContentTypeSub) {
/**
* A body type of type MESSAGE and subtype RFC822 contains,
* immediately after the basic fields, the envelope structure,
@ -503,9 +465,7 @@ class BodyStructure
*/
$iExtraItemPos += 3;
}
}
else
{
} else {
return null;
}
@ -515,18 +475,14 @@ class BodyStructure
$sDisposition = '';
$sFileName = '';
if ($iExtraItemPos < \count($aBodyStructure))
{
if ($iExtraItemPos < \count($aBodyStructure)) {
$aDispList = $aBodyStructure[$iExtraItemPos];
if (\is_array($aDispList) && 1 < \count($aDispList))
{
if (!\is_string($aDispList[0]))
{
if (\is_array($aDispList) && 1 < \count($aDispList)) {
if (!\is_string($aDispList[0])) {
return null;
}
$sDisposition = $aDispList[0];
if (\is_array($aDispList[1]))
{
if (\is_array($aDispList[1])) {
$aDispositionParams = self::getKeyValueListFromArrayList($aDispList[1]);
$sFileName = self::decodeAttrParameter($aDispositionParams, 'filename', $sCharset);
}
@ -536,13 +492,13 @@ class BodyStructure
$oStructure = new self;
$oStructure->sContentType = \strtolower($sContentTypeMain.'/'.$sContentTypeSub);
$oStructure->aContentTypeParams = $aContentTypeParams;
$oStructure->sCharset = $sCharset;
$oStructure->aBodyParams = $aBodyParams;
$oStructure->sContentID = $sContentID;
$oStructure->sDescription = $sDescription;
$oStructure->sMailEncodingName = \strtolower($sMailEncodingName);
$oStructure->sDisposition = \strtolower($sDisposition);
$oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName ?: $sName);
$oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName);
$oStructure->iSize = $iSize;
$oStructure->sPartID = $sPartID;
$oStructure->aSubParts = $aSubParts;
@ -571,12 +527,9 @@ class BodyStructure
{
$aDict = array();
$iLen = \count($aList);
if (0 === ($iLen % 2))
{
for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2)
{
if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1]))
{
if (0 === ($iLen % 2)) {
for ($iIndex = 0; $iIndex < $iLen; $iIndex += 2) {
if (\is_string($aList[$iIndex]) && \is_string($aList[$iIndex + 1])) {
$aDict[\strtolower($aList[$iIndex])] = $aList[$iIndex + 1];
}
}

View file

@ -99,6 +99,7 @@ class Attachment implements \JsonSerializable
'Uid' => (string) $this->iUid,
'MimeIndex' => (string) $this->oBodyStructure->PartID(),
'MimeType' => $this->oBodyStructure->ContentType(),
'MimeTypeParams' => $this->oBodyStructure->ContentTypeParameters(),
'FileName' => \MailSo\Base\Utils::SecureFileName($this->oBodyStructure->FileName(true)),
'EstimatedSize' => $this->oBodyStructure->EstimatedSize(),
'Cid' => $this->oBodyStructure->ContentID(),

View file

@ -11,8 +11,8 @@
namespace MailSo\Mail;
use \MailSo\Base\Utils;
use \MailSo\Imap\Enumerations\FetchType;
use MailSo\Base\Utils;
use MailSo\Imap\Enumerations\FetchType;
/**
* @category MailSo

View file

@ -151,15 +151,15 @@ class Attachment
$oAttachmentPart->Headers->append(
new Header(Enumerations\Header::CONTENT_TYPE,
$this->ContentType().';'.
(($oContentTypeParameters) ? ' '.$oContentTypeParameters->ToString() : '')
$this->ContentType().
($oContentTypeParameters ? '; '.$oContentTypeParameters : '')
)
);
$oAttachmentPart->Headers->append(
new Header(Enumerations\Header::CONTENT_DISPOSITION,
($this->IsInline() ? 'inline' : 'attachment').';'.
(($oContentDispositionParameters) ? ' '.$oContentDispositionParameters->ToString() : '')
($this->IsInline() ? 'inline' : 'attachment').
($oContentDispositionParameters ? '; '.$oContentDispositionParameters : '')
)
);

View file

@ -200,6 +200,11 @@ class Email implements \JsonSerializable
return \trim($sReturn);
}
public function __toString() : string
{
return $this->ToString();
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{

View file

@ -66,6 +66,11 @@ class EmailCollection extends \MailSo\Base\Collection
return \implode(', ', $aReturn);
}
public function __toString() : string
{
return $this->ToString();
}
private function parseEmailAddresses(string $sRawEmails) : void
{
// $sRawEmails = \MailSo\Base\Utils::Trim($sRawEmails);

View file

@ -126,13 +126,6 @@ class Header
$this->oParameters->setParameter($sName, $sValue);
}
private function wordWrapHelper(string $sValue, string $sGlue = "\r\n ") : string
{
return \trim(\substr(\wordwrap($this->NameWithDelimitrom().$sValue,
74, $sGlue
), \strlen($this->NameWithDelimitrom())));
}
public function __toString() : string
{
$sResult = $this->sFullValue;
@ -159,11 +152,12 @@ class Header
{
$oEmailCollection = new EmailCollection($this->sFullValue);
if ($oEmailCollection && $oEmailCollection->count()) {
$sResult = $oEmailCollection->ToString(true, false);
$sResult = $oEmailCollection->ToString(true);
}
}
return $this->NameWithDelimitrom().$this->wordWrapHelper($sResult);
// https://www.rfc-editor.org/rfc/rfc2822#section-2.1.1
return \wordwrap($this->NameWithDelimitrom() . $sValue, 78, "\r\n ");
}
public function IsSubject() : bool

View file

@ -342,7 +342,7 @@ class Message extends Part
}
$oPart->Headers->append(
new Header(Enumerations\Header::CONTENT_TYPE,
$aAttachments[0]->ContentType().'; '.$oParameters->ToString())
$aAttachments[0]->ContentType().'; '.$oParameters)
);
if ($resource = $aAttachments[0]->Resource()) {
@ -427,11 +427,6 @@ class Message extends Part
foreach ($this->aHeadersValue as $sName => $mValue) {
if (!($bWithoutBcc && \strtolower(Enumerations\Header::BCC) === \strtolower($sName))) {
if (\is_object($mValue)) {
if ($mValue instanceof EmailCollection || $mValue instanceof Email || $mValue instanceof ParameterCollection) {
$mValue = $mValue->ToString();
}
}
$oRootPart->Headers->SetByName($sName, (string) $mValue);
}
}

View file

@ -15,7 +15,7 @@ namespace MailSo\Mime;
* @category MailSo
* @package Mime
*/
class Parameter
class Parameter implements \JsonSerializable
{
private string $sName;
@ -88,4 +88,18 @@ class Parameter
return $this->sName . '="' . $this->sValue . '"';
}
public function __toString() : string
{
return $this->ToString();
}
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array(
'name' => $this->sName,
'value' => $this->sValue
);
}
}

View file

@ -81,10 +81,29 @@ class ParameterCollection extends \MailSo\Base\Collection
$aResult[] = $sLine;
}
}
return \count($aResult) ? \implode('; ', $aResult) : '';
}
public function __toString() : string
{
return $this->ToString();
}
/*
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
$aResult = array();
foreach ($this as $oParam) {
$aResult[$oParam->Name()] = $oParam->Value();
}
return array(
'@Object' => 'Collection/ParameterCollection',
'@Collection' => $aResult
);
}
*/
private function reParseParameters() : void
{
$aDataToReParse = $this->getArrayCopy();

View file

@ -51,7 +51,12 @@ class Part
{
return \trim(\strtolower($this->Headers->ValueByName(Enumerations\Header::CONTENT_TYPE)));
}
/*
public function ContentTypeParameters() : ParameterCollection
{
return $this->Headers->ParametersByName(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE);
}
*/
public function ContentID() : string
{
return \trim($this->Headers->ValueByName(Enumerations\Header::CONTENT_ID));