mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-09-04 15:07:02 +03:00
* Cleanup OpenPgpImportPopupView code * update polish translation * small fix * Added Import S/MIME certificate popup And much better handling of the sign and encrypt options * bugfix: store in Passphrases * Resolve #1448 * pre-verify S/MIME opaque signed messages so we have a body to view * Fix timestampToString() for future dates * Move php8.php to /app/libraries/polyfill/ * Improved Settings handling to prevent bugs in outer code * Changed AbstractProvider::IsActive() to be abstract * Example for #1449 * bugfix: previous IsActive() commit * OpenSSL required due to S/MIME * Use get_debug_type() instead of gettype() * update polish translation * Make all Enumerations classes abstract * Added search functionality in Admin -> Config And removed the unused ['capa']['quota'] * Cleanup Quota handling * OPEN_PGP should be OPENPGP as it is one word * Improve Capa handling * Resolve #1451 * Bugfix TypeError: b64Encode(...).match(...) is null * Small StorageType change * Bugfix: mailvelope editor failed * Bugfix: undefined getMailvelopePrivateKeyFor() * Bugfix: MIME parser RegExp didn't escape `boundary` which caused issues * Return detailed info on PgpImportKey * Show GnuPG verify error * Sort PGP keys by email and id * Sort S/MIME certificates on emailAddress else validTo * S/MIME import from signature use `BEGIN PKCS7` * Optionally use existing private key to generate S/MIME certificate * Chaned some error_log() to MailSo Logger() * Force reload of S/MIME certificates list on import * Make better use of SnappyMail\SensitiveString * Fix view PGP key button * Mask all POST data that has a key which contains `pass` * v2.35.1 * Resolve #1455 * Improved GnuPG error handling * Update pt/pt-PT translation * update Polish translation * Drop support for gnupg pecl extension as it fails with "no passphrase" issues * Resolve #1456 * Resolve #1458 * v2.35.2 * fix changelog * Resolve #1461 * Update pt/pt-PT translation * compact-composer plugin v1.0.0 * Resolve #1462 * Fix decrypt error message * `new Error()` to `Error()` * Resolve #1463 * Show url for #1466 * Simplify SignMe/Remember me code * Simplify language Notifications * Bugfix: SetPassword expects \SnappyMail\SensitiveString * https://github.com/the-djmaze/snappymail/issues/1450#issuecomment-1972147950 * improve: fire the 'squire2-toolbar' event after more props are added * improve: add dark theme support and use 'button' element as menu trigger for consistent styling * fix: use compact template in non-destructive way (do not replace the PopupsCompose template if a different wysiwyg is used) * Update admin.json * Update user.json * CSS rainloopErrorTip location * Improved error handling on PGP and S/MIME decrypt * KnockoutJS remove unused `beforeRemove` * KnockoutJS drop unused `as` * KnockoutJS simplify renderMode because only 1 option is used * KnoutJS cleanup templating.js a bit * KnockoutJS drop unused `bindingRewriteValidators` * KnockoutJS drop the twoWayBindings code * KnockoutJS simplify virtualElements binding check * KnockoutJS simplify applyBindingsToNodeInternal * KnockoutJS use Array.isArray * KnockoutJS drop alias `textinput` for `textInput` * KnockoutJS scramble `createChildContext` * KnockoutJS scramble `controlsDescendantBindings` * KnockoutJS scramble `exportDependencies` * KnockoutJS drop unused `throttleEvaluation` * KnockoutJS drop unused `valueAllowUnset` * KnockoutJS drop unused `templateNodes` * KnockoutJS drop unused `optionsCaption` * KnockoutJS drop unused `dontLimitMoves` * KnockoutJS drop unused `uniqueName` * KnockoutJS drop IE leftovers * KnockoutJS drop unused `preprocess` * KnockoutJS drop unused "disposeWhenNodeIsRemoved" and "disposeWhen" * KnockoutJS don't scramble exportDependencies. controlsDescendantBindings, createChildContext * KnockoutJS drop unused `$parentContext` and `$parents` * KnockoutJS drop unused `$rawData` * Knockoutjs built latest * KnockoutJS drop unused template options `nodes`, `if`, `ifnot` * KnockoutJS use more Array.isArray * KnockoutJS cleanup code a bit * KnockoutJS primitiveTypes can just be checked with Object() * KnockoutJS rebuilt * Verify S/MIME signed automatically and log Exception * Automatically verify PGP and S/MIME signed messages * `new Error` to `Error` * By default throw AccountNotAllowed as confused in #1478 * GPG use pinentries for decrypt, sign and export * Better GPG error handling * GPG show error on view/export * OpenPGP fix handling of importing keys * Make "verify signatures automatically" optional, as it requires more IMAP fetching * S/MIME don't post identity key and certificate, just fetch from server * Show error to old browsers, instead of crashing * Automatically verify S/MIME decrypted signed message --------- Co-authored-by: the-djmaze <> Co-authored-by: tinola <tinola@poczta.onet.pl> Co-authored-by: Maarten <3752035+the-djmaze@users.noreply.github.com> Co-authored-by: lmperfis <joint.striker@gmail.com> Co-authored-by: Sergey Mosin <sergey@srgdev.com> Co-authored-by: hguilbert <51283484+hguilbert@users.noreply.github.com>
577 lines
16 KiB
PHP
577 lines
16 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of MailSo.
|
|
*
|
|
* (c) 2014 Usenko Timur
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace MailSo\Imap;
|
|
|
|
use MailSo\Mime\Enumerations\ContentType;
|
|
|
|
/**
|
|
* @category MailSo
|
|
* @package Imap
|
|
*/
|
|
class BodyStructure implements \JsonSerializable
|
|
{
|
|
private string $sContentType;
|
|
|
|
private array $aContentTypeParams;
|
|
|
|
private string $sCharset;
|
|
|
|
private string $sContentID;
|
|
|
|
private string $sDescription;
|
|
|
|
private string $sMailEncodingName;
|
|
|
|
private string $sDisposition;
|
|
|
|
private string $sFileName;
|
|
|
|
private string $sLanguage = '';
|
|
|
|
private string $sLocation = '';
|
|
|
|
private int $iSize;
|
|
|
|
private string $sPartID;
|
|
|
|
/**
|
|
* \MailSo\Imap\BodyStructure[]
|
|
*/
|
|
private array $aSubParts;
|
|
|
|
public function MailEncodingName() : string
|
|
{
|
|
return $this->sMailEncodingName;
|
|
}
|
|
|
|
public function PartID() : string
|
|
{
|
|
return $this->sPartID;
|
|
}
|
|
|
|
public function FileName(bool $bCalculateOnEmpty = false) : string
|
|
{
|
|
$sFileName = \trim($this->sFileName);
|
|
if (\strlen($sFileName) || !$bCalculateOnEmpty) {
|
|
return $sFileName;
|
|
}
|
|
|
|
$sIdx = '-' . $this->PartID();
|
|
|
|
$sMimeType = $this->sContentType;
|
|
if ('message/rfc822' === $sMimeType) {
|
|
return "message{$sIdx}.eml";
|
|
}
|
|
if ('text/calendar' === $sMimeType) {
|
|
return "calendar{$sIdx}.ics";
|
|
}
|
|
if ('text/plain' === $sMimeType) {
|
|
return "part{$sIdx}.txt";
|
|
}
|
|
if (\preg_match('@text/(vcard|html|csv|xml|css|asp)@', $sMimeType, $aMatch)
|
|
|| \preg_match('@image/(png|jpeg|gif|bmp|cgm|ief|tiff|webp)@', $sMimeType, $aMatch)) {
|
|
return "part{$sIdx}.{$aMatch[1]}";
|
|
}
|
|
if (\strlen($sMimeType)) {
|
|
return \str_replace('/', $sIdx.'.', $sMimeType);
|
|
}
|
|
|
|
return ($this->isInline() ? 'inline' : 'part' ) . $sIdx;
|
|
}
|
|
|
|
public function ContentType() : string
|
|
{
|
|
return $this->sContentType;
|
|
}
|
|
|
|
public function EstimatedSize() : int
|
|
{
|
|
$fCoefficient = 1;
|
|
switch ($this->sMailEncodingName)
|
|
{
|
|
case 'base64':
|
|
$fCoefficient = 0.75;
|
|
break;
|
|
case 'quoted-printable':
|
|
$fCoefficient = 0.44;
|
|
break;
|
|
}
|
|
|
|
return (int) ($this->iSize * $fCoefficient);
|
|
}
|
|
|
|
public function Charset() : string
|
|
{
|
|
return $this->sCharset;
|
|
}
|
|
|
|
public function SubParts() : array
|
|
{
|
|
return $this->aSubParts;
|
|
}
|
|
|
|
public function isInline() : bool
|
|
{
|
|
return 'inline' === $this->sDisposition || \strlen($this->sContentID);
|
|
}
|
|
|
|
public function isText() : bool
|
|
{
|
|
return 'text/html' === $this->sContentType || 'text/plain' === $this->sContentType;
|
|
}
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc3156#section-4
|
|
public function isPgpEncrypted() : bool
|
|
{
|
|
return 'multipart/encrypted' === $this->sContentType
|
|
&& !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()
|
|
&& 'application/octet-stream' === $this->aSubParts[1]->ContentType();
|
|
// && 'Version: 1' === $this->aSubParts[0]->Body()
|
|
}
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc3156#section-5
|
|
public function isPgpSigned() : bool
|
|
{
|
|
return 'multipart/signed' === $this->sContentType
|
|
&& !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)
|
|
&& 'application/pgp-signature' === $this->aSubParts[1]->ContentType();
|
|
}
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc2633#section-3.3
|
|
public function isSMimeEncrypted() : bool
|
|
{
|
|
$type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? ''));
|
|
return ContentType::isPkcs7Mime($this->sContentType)
|
|
&& !empty($this->aContentTypeParams['smime-type'])
|
|
&& ('enveloped-data' === $type || 'authenveloped-data' === $type);
|
|
}
|
|
|
|
// https://www.rfc-editor.org/rfc/rfc8551.html#section-3.5
|
|
public function isSMimeSigned() : bool
|
|
{
|
|
return ('multipart/signed' === $this->sContentType
|
|
&& !empty($this->aContentTypeParams['protocol'])
|
|
&& ContentType::isPkcs7Signature(\strtolower(\trim($this->aContentTypeParams['protocol'])))
|
|
// The multipart/signed body MUST consist of exactly two parts.
|
|
&& 2 === \count($this->aSubParts)
|
|
&& ContentType::isPkcs7Signature($this->aSubParts[1]->ContentType())
|
|
) || (ContentType::isPkcs7Mime($this->sContentType)
|
|
&& !empty($this->aContentTypeParams['smime-type'])
|
|
&& 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type']))
|
|
);
|
|
}
|
|
|
|
public function IsAttachment() : bool
|
|
{
|
|
return 'application/pgp-encrypted' !== $this->sContentType
|
|
&& (
|
|
'attachment' === $this->sDisposition || (
|
|
!\str_starts_with($this->sContentType, 'multipart/')
|
|
&& !$this->isText()
|
|
)
|
|
);
|
|
}
|
|
|
|
public function IsFlowedFormat() : bool
|
|
{
|
|
return !empty($this->aContentTypeParams['format'])
|
|
&& 'flowed' === \strtolower(\trim($this->aContentTypeParams['format']))
|
|
&& !\in_array($this->sMailEncodingName, array('base64', 'quoted-printable'));
|
|
}
|
|
|
|
public function GetHtmlAndPlainParts() : array
|
|
{
|
|
$aParts = [];
|
|
|
|
$gParts = $this->SearchByCallback(function ($oItem) {
|
|
return $oItem->isText() && !$oItem->IsAttachment();
|
|
});
|
|
foreach ($gParts as $oPart) {
|
|
$aParts[] = $oPart;
|
|
}
|
|
|
|
/**
|
|
* No text found, is it encrypted?
|
|
* If so, just return that.
|
|
*/
|
|
if (!$aParts) {
|
|
$gEncryptedParts = $this->SearchByContentType('multipart/encrypted');
|
|
foreach ($gEncryptedParts as $oPart) {
|
|
if ($oPart->isPgpEncrypted()) {
|
|
return array($oPart->SubParts()[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Still no text found?
|
|
* Look in attachments as it could be an X-Mms-Message
|
|
* https://github.com/the-djmaze/snappymail/issues/1294
|
|
*/
|
|
if (!$aParts) {
|
|
$gParts = $this->SearchByCallback(fn($oItem) => $oItem->isText());
|
|
foreach ($gParts as $oPart) {
|
|
$aParts[] = $oPart;
|
|
}
|
|
}
|
|
|
|
return $aParts;
|
|
}
|
|
|
|
public function SearchCharset() : string
|
|
{
|
|
$gParts = $this->SearchByCallback(function ($oPart) {
|
|
return $oPart->Charset() && $oPart->isText() && !$oPart->IsAttachment();
|
|
});
|
|
|
|
if (!$gParts->valid()) {
|
|
$gParts = $this->SearchByCallback(function ($oPart) {
|
|
return $oPart->Charset() && $oPart->IsAttachment();
|
|
});
|
|
}
|
|
|
|
return $gParts->valid() ? $gParts->current()->Charset() : '';
|
|
}
|
|
|
|
public function SearchByCallback(callable $fCallback, /*BodyStructure*/ $parent = null) : iterable
|
|
{
|
|
if ($fCallback($this, $parent)) {
|
|
yield $this;
|
|
}
|
|
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) {
|
|
yield from $oSubPart->SearchByCallback($fCallback, $this);
|
|
}
|
|
}
|
|
|
|
public function SearchAttachmentsParts() : iterable
|
|
{
|
|
return $this->SearchByCallback(function ($oItem, $oParent) {
|
|
// return $oItem->IsAttachment();
|
|
return $oItem->IsAttachment() && (!$oParent || !$oParent->isPgpEncrypted());
|
|
});
|
|
}
|
|
|
|
public function SearchByContentType(string $sContentType) : iterable
|
|
{
|
|
$sContentType = \strtolower($sContentType);
|
|
return $this->SearchByCallback(function ($oItem) use ($sContentType) {
|
|
return $sContentType === $oItem->sContentType;
|
|
});
|
|
}
|
|
|
|
public function SearchByContentTypes(array $aContentTypes) : iterable
|
|
{
|
|
return $this->SearchByCallback(function ($oItem) use ($aContentTypes) {
|
|
return \in_array($oItem->sContentType, $aContentTypes);
|
|
});
|
|
}
|
|
|
|
public function GetPartByMimeIndex(string $sMimeIndex) : self
|
|
{
|
|
$oPart = null;
|
|
if (\strlen($sMimeIndex)) {
|
|
if ($sMimeIndex === $this->sPartID) {
|
|
$oPart = $this;
|
|
}
|
|
|
|
if (null === $oPart) {
|
|
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) {
|
|
$oPart = $oSubPart->GetPartByMimeIndex($sMimeIndex);
|
|
if (null !== $oPart) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $oPart;
|
|
}
|
|
|
|
private static function decodeAttrParameter(array $aParams, string $sParamName, string $sCharset) : string
|
|
{
|
|
$sResult = '';
|
|
if (isset($aParams[$sParamName])) {
|
|
$sResult = \MailSo\Base\Utils::DecodeHeaderValue($aParams[$sParamName], $sCharset);
|
|
} else if (isset($aParams[$sParamName.'*'])) {
|
|
$aValueParts = \explode("''", $aParams[$sParamName.'*'], 2);
|
|
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 {
|
|
$sResult = \urldecode($aParams[$sParamName.'*']);
|
|
}
|
|
} else {
|
|
$sCharset = '';
|
|
$sCharsetIndex = -1;
|
|
|
|
$aFileNames = array();
|
|
foreach ($aParams as $sName => $sValue) {
|
|
$aMatches = array();
|
|
if (\preg_match('/^'.\preg_quote($sParamName, '/').'\*([0-9]+)\*$/i', $sName, $aMatches)) {
|
|
$iIndex = (int) $aMatches[1];
|
|
if ($sCharsetIndex < $iIndex && false !== \strpos($sValue, "''")) {
|
|
$aValueParts = \explode("''", $sValue, 2);
|
|
if (2 === \count($aValueParts) && \strlen($aValueParts[0])) {
|
|
$sCharsetIndex = $iIndex;
|
|
$sCharset = $aValueParts[0];
|
|
$sValue = $aValueParts[1];
|
|
}
|
|
}
|
|
$aFileNames[$iIndex] = $sValue;
|
|
}
|
|
}
|
|
|
|
if (\count($aFileNames)) {
|
|
\ksort($aFileNames, SORT_NUMERIC);
|
|
$sResult = \implode(\array_values($aFileNames));
|
|
$sResult = \urldecode($sResult);
|
|
if (\strlen($sCharset)) {
|
|
$sResult = \MailSo\Base\Utils::ConvertEncoding($sResult,
|
|
$sCharset, \MailSo\Base\Enumerations\Charset::UTF_8);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $sResult;
|
|
}
|
|
|
|
public static function NewInstance(array $aBodyStructure, string $sPartID = '') : ?self
|
|
{
|
|
if (2 > \count($aBodyStructure)) {
|
|
return null;
|
|
}
|
|
|
|
$sContentTypeMain = '';
|
|
$sContentTypeSub = '';
|
|
$aSubParts = array();
|
|
$aContentTypeParams = array();
|
|
$sFileName = '';
|
|
$sCharset = ''; // \MailSo\Base\Enumerations\Charset::UTF_8 ?
|
|
$sContentID = '';
|
|
$sDescription = '';
|
|
$sMailEncodingName = '';
|
|
$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])) {
|
|
// Process multipart body structure
|
|
$sContentTypeMain = 'multipart';
|
|
$sContentTypeSub = 'mixed'; // primary default
|
|
$sSubPartIDPrefix = '';
|
|
if (!\strlen($sPartID) || '.' === $sPartID[\strlen($sPartID) - 1]) {
|
|
// This multi-part is root part of message.
|
|
$sSubPartIDPrefix = $sPartID;
|
|
$sPartID .= 'TEXT';
|
|
} else if (\strlen($sPartID)) {
|
|
// This multi-part is a part of another multi-part.
|
|
$sSubPartIDPrefix = $sPartID.'.';
|
|
}
|
|
|
|
$iIndex = 1;
|
|
|
|
/**
|
|
* First process the subparts, like:
|
|
("text" "plain" ("charset" "utf-8") …)
|
|
("text" "html" …)
|
|
*/
|
|
while ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) {
|
|
$oPart = self::NewInstance($aBodyStructure[$iExtraItemPos], $sSubPartIDPrefix.$iIndex);
|
|
if (!$oPart) {
|
|
return null;
|
|
}
|
|
|
|
// For multipart, we have no charset info in the part itself. Thus,
|
|
// obtain charset from nested parts.
|
|
if (!$sCharset) {
|
|
$sCharset = $oPart->Charset();
|
|
}
|
|
|
|
$aSubParts[] = $oPart;
|
|
++$iExtraItemPos;
|
|
++$iIndex;
|
|
}
|
|
|
|
/**
|
|
* Now process the subparts containter like:
|
|
"alternative" ("boundary" "--boundary_id") …
|
|
*/
|
|
if ($iExtraItemPos < \count($aBodyStructure)) {
|
|
if (!\is_string($aBodyStructure[$iExtraItemPos])) {
|
|
return null;
|
|
}
|
|
$sContentTypeSub = \strtolower($aBodyStructure[$iExtraItemPos]);
|
|
|
|
++$iExtraItemPos;
|
|
if ($iExtraItemPos < \count($aBodyStructure) && \is_array($aBodyStructure[$iExtraItemPos])) {
|
|
$aContentTypeParams = self::getKeyValueListFromArrayList($aBodyStructure[$iExtraItemPos]);
|
|
}
|
|
}
|
|
} else if (\is_string($aBodyStructure[0])) {
|
|
// Process simple (singlepart) body structure
|
|
if (7 > \count($aBodyStructure) || !\is_string($aBodyStructure[1])) {
|
|
return null;
|
|
}
|
|
|
|
$sContentTypeMain = \strtolower($aBodyStructure[0]);
|
|
$sContentTypeSub = \strtolower($aBodyStructure[1]);
|
|
|
|
$aBodyParamList = $aBodyStructure[2];
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (null !== $aBodyStructure[3]) {
|
|
if (!\is_string($aBodyStructure[3])) {
|
|
return null;
|
|
}
|
|
$sContentID = $aBodyStructure[3];
|
|
}
|
|
|
|
if (null !== $aBodyStructure[4]) {
|
|
if (!\is_string($aBodyStructure[4])) {
|
|
return null;
|
|
}
|
|
$sDescription = $aBodyStructure[4];
|
|
}
|
|
|
|
if (null !== $aBodyStructure[5]) {
|
|
if (!\is_string($aBodyStructure[5])) {
|
|
return null;
|
|
}
|
|
$sMailEncodingName = $aBodyStructure[5];
|
|
}
|
|
|
|
$iSize = \is_numeric($aBodyStructure[6]) ? (int) $aBodyStructure[6] : -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) {
|
|
/**
|
|
* 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) {
|
|
/**
|
|
* A body type of type MESSAGE and subtype RFC822 contains,
|
|
* immediately after the basic fields, the envelope structure,
|
|
* body structure, and size in text lines of the encapsulated message.
|
|
*/
|
|
$iExtraItemPos += 3;
|
|
}
|
|
} else {
|
|
return null;
|
|
}
|
|
|
|
// Skip body MD5 because most mail servers leave it NIL anyway
|
|
++$iExtraItemPos;
|
|
|
|
$sDisposition = '';
|
|
|
|
if ($iExtraItemPos < \count($aBodyStructure)) {
|
|
$aDispList = $aBodyStructure[$iExtraItemPos];
|
|
if (\is_array($aDispList) && 1 < \count($aDispList)) {
|
|
if (!\is_string($aDispList[0])) {
|
|
return null;
|
|
}
|
|
$sDisposition = $aDispList[0];
|
|
if (\is_array($aDispList[1])) {
|
|
$aDispositionParams = self::getKeyValueListFromArrayList($aDispList[1]);
|
|
$sFileName = self::decodeAttrParameter($aDispositionParams, 'filename', $sCharset);
|
|
}
|
|
}
|
|
++$iExtraItemPos;
|
|
}
|
|
|
|
$oStructure = new self;
|
|
$oStructure->sContentType = \strtolower(\trim($sContentTypeMain.'/'.$sContentTypeSub));
|
|
$oStructure->aContentTypeParams = $aContentTypeParams;
|
|
$oStructure->sCharset = $sCharset;
|
|
$oStructure->sContentID = \trim($sContentID);
|
|
$oStructure->sDescription = $sDescription;
|
|
$oStructure->sMailEncodingName = \strtolower($sMailEncodingName);
|
|
$oStructure->sDisposition = \strtolower($sDisposition);
|
|
$oStructure->sFileName = \MailSo\Base\Utils::Utf8Clear($sFileName);
|
|
$oStructure->iSize = $iSize;
|
|
$oStructure->sPartID = $sPartID;
|
|
$oStructure->aSubParts = $aSubParts;
|
|
|
|
if ($iExtraItemPos < \count($aBodyStructure)) {
|
|
if (\is_array($aBodyStructure[$iExtraItemPos])) {
|
|
$oStructure->sLanguage = \implode(',', $aBodyStructure[$iExtraItemPos]);
|
|
} else if (\is_string($aBodyStructure[$iExtraItemPos])) {
|
|
$oStructure->sLanguage = $aBodyStructure[$iExtraItemPos];
|
|
}
|
|
++$iExtraItemPos;
|
|
|
|
if ($iExtraItemPos < \count($aBodyStructure) && \is_string($aBodyStructure[$iExtraItemPos])) {
|
|
$oStructure->sLocation = $aBodyStructure[$iExtraItemPos];
|
|
}
|
|
}
|
|
|
|
return $oStructure;
|
|
}
|
|
|
|
/**
|
|
* Returns dict with key="charset" and value="US-ASCII" for array ("CHARSET" "US-ASCII").
|
|
* Keys are lowercased (StringDictionary itself does this), values are not altered.
|
|
*/
|
|
private static function getKeyValueListFromArrayList(array $aList) : array
|
|
{
|
|
$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])) {
|
|
$aDict[\strtolower($aList[$iIndex])] = $aList[$iIndex + 1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return $aDict;
|
|
}
|
|
|
|
#[\ReturnTypeWillChange]
|
|
public function jsonSerialize()
|
|
{
|
|
return array(
|
|
'mimeIndex' => $this->sPartID,
|
|
'mimeType' => $this->sContentType,
|
|
// 'mimeTypeParams' => $this->aContentTypeParams,
|
|
'fileName' => \MailSo\Base\Utils::SecureFileName($this->FileName(true)),
|
|
'estimatedSize' => $this->EstimatedSize(),
|
|
'cId' => $this->sContentID,
|
|
'contentLocation' => $this->sLocation,
|
|
'isInline' => $this->isInline()
|
|
);
|
|
}
|
|
}
|