Improved detection of PGP/MIME encrypted messages instead of showing them as attachments

This commit is contained in:
the-djmaze 2022-01-25 17:02:29 +01:00
parent 48febdb414
commit 0ed7f000d6
4 changed files with 60 additions and 45 deletions

View file

@ -168,6 +168,19 @@ class BodyStructure
return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName); return 'doc' === \MailSo\Base\Utils::ContentTypeType($this->sContentType, $this->sFileName);
} }
public function IsPgpEncrypted() : bool
{
// 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']))
// 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()
}
public function IsPgpSigned() : bool public function IsPgpSigned() : bool
{ {
// https://datatracker.ietf.org/doc/html/rfc3156#section-5 // https://datatracker.ietf.org/doc/html/rfc3156#section-5
@ -187,44 +200,21 @@ class BodyStructure
public function IsAttachBodyPart() : bool public function IsAttachBodyPart() : bool
{ {
return 'attachment' === $this->sDisposition return 'application/pgp-encrypted' !== $this->sContentType
|| ( && (
'attachment' === $this->sDisposition || (
!\str_starts_with($this->sContentType, 'multipart/') !\str_starts_with($this->sContentType, 'multipart/')
&& 'text/html' !== $this->sContentType && 'text/html' !== $this->sContentType
&& 'text/plain' !== $this->sContentType && 'text/plain' !== $this->sContentType
)
); );
return $bResult;
} }
public function IsFlowedFormat() : bool public function IsFlowedFormat() : bool
{ {
$bResult = !empty($this->aBodyParams['format']) && return !empty($this->aBodyParams['format'])
'flowed' === \strtolower(\trim($this->aBodyParams['format'])); && 'flowed' === \strtolower(\trim($this->aBodyParams['format']))
&& !\in_array($this->sMailEncodingName, array('base64', 'quoted-printable'));
if ($bResult && \in_array($this->sMailEncodingName, array('base64', 'quoted-printable')))
{
$bResult = false;
}
return $bResult;
}
public function SearchInlineEncryptedPart() : ?self
{
if ('multipart/encrypted' === \strtolower($this->sContentType))
{
$aSearchParts = \iterator_to_array($this->SearchByCallback(function ($oItem) {
return $oItem->IsInline();
}));
if (1 === \count($aSearchParts) && isset($aSearchParts[0]))
{
return $aSearchParts[0];
}
}
return null;
} }
public function GetHtmlAndPlainParts() : array public function GetHtmlAndPlainParts() : array
@ -238,9 +228,15 @@ class BodyStructure
return \array_merge([$aParts->current()], \iterator_to_array($aParts)); return \array_merge([$aParts->current()], \iterator_to_array($aParts));
} }
$oPart = $this->SearchInlineEncryptedPart(); /**
if ($oPart instanceof self) { * No text found, is it encrypted?
return array($oPart); * If so, just return that.
*/
$gEncryptedParts = $this->SearchByContentType('multipart/encrypted');
foreach ($gEncryptedParts as $oPart) {
if ($oPart->IsPgpEncrypted() && $oPart->SubParts()[1]->IsInline()) {
return array($oPart->SubParts()[1]);
}
} }
return []; return [];
@ -267,20 +263,21 @@ class BodyStructure
* @param mixed $fCallback * @param mixed $fCallback
*/ */
// public function SearchByCallback($fCallback) : \Generator // public function SearchByCallback($fCallback) : \Generator
public function SearchByCallback($fCallback) : iterable public function SearchByCallback($fCallback, $parent = null) : iterable
{ {
if ($fCallback($this)) { if ($fCallback($this, $parent)) {
yield $this; yield $this;
} }
foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) { foreach ($this->aSubParts as /* @var $oSubPart \MailSo\Imap\BodyStructure */ $oSubPart) {
yield from $oSubPart->SearchByCallback($fCallback); yield from $oSubPart->SearchByCallback($fCallback, $this);
} }
} }
public function SearchAttachmentsParts() : iterable public function SearchAttachmentsParts() : iterable
{ {
return $this->SearchByCallback(function ($oItem) { return $this->SearchByCallback(function ($oItem, $oParent) {
return $oItem->IsAttachBodyPart(); // return $oItem->IsAttachBodyPart();
return $oItem->IsAttachBodyPart() && (!$oParent || !$oParent->IsPgpEncrypted());
}); });
} }

View file

@ -490,9 +490,8 @@ class MailClient
if (!\in_array(\strtolower(MessageFlag::SEEN), $aFlags)) if (!\in_array(\strtolower(MessageFlag::SEEN), $aFlags))
{ {
$iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID); $iUid = (int) $oFetchResponse->GetFetchValue(FetchType::UID);
$sHeaders = $oFetchResponse->GetHeaderFieldsValue();
$oHeaders = new \MailSo\Mime\HeaderCollection($sHeaders); $oHeaders = new \MailSo\Mime\HeaderCollection($oFetchResponse->GetHeaderFieldsValue());
$sContentTypeCharset = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::CHARSET); $sContentTypeCharset = $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, MimeParameter::CHARSET);

View file

@ -78,6 +78,7 @@ class Message implements \JsonSerializable
$aThreads = array(), $aThreads = array(),
$aPgpSigned = null, $aPgpSigned = null,
$aPgpEncrypted = null,
$bPgpEncrypted = false; $bPgpEncrypted = false;
function __construct() function __construct()
@ -100,9 +101,14 @@ class Message implements \JsonSerializable
return $this->aPgpSigned; return $this->aPgpSigned;
} }
public function PgpEncrypted() : ?array
{
return $this->aPgpEncrypted;
}
public function isPgpEncrypted() : bool public function isPgpEncrypted() : bool
{ {
return $this->bPgpEncrypted; return $this->bPgpEncrypted || $this->aPgpEncrypted;
} }
public function Folder() : string public function Folder() : string
@ -491,16 +497,28 @@ class Message implements \JsonSerializable
if ($oBodyStructure) if ($oBodyStructure)
{ {
$gEncryptedParts = $oBodyStructure->SearchByContentType('multipart/encrypted');
foreach ($gEncryptedParts as $oPart) {
if ($oPart->IsPgpEncrypted()) {
if (!$oMessage->aPgpEncrypted) {
$oMessage->aPgpEncrypted = [];
}
$oMessage->aPgpEncrypted = [
'PartId' => $oPart->SubParts()[1]->PartID()
];
}
}
$gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed');
foreach ($gSignatureParts as $oPart) { foreach ($gSignatureParts as $oPart) {
if (!$oPart->IsPgpSigned()) { if (!$oPart->IsPgpSigned()) {
continue; continue;
} }
$oPgpSignaturePart = $oBodyStructure->SubParts()[1]; $oPgpSignaturePart = $oPart->SubParts()[1];
$oMessage->aPgpSigned = [ $oMessage->aPgpSigned = [
// /?/Raw/&q[]=/0/Download/&q[]=/... // /?/Raw/&q[]=/0/Download/&q[]=/...
// /?/Raw/&q[]=/0/View/&q[]=/... // /?/Raw/&q[]=/0/View/&q[]=/...
'BodyPartId' => $oBodyStructure->SubParts()[0]->PartID(), 'BodyPartId' => $oPart->SubParts()[0]->PartID(),
'SigPartId' => $oPgpSignaturePart->PartID(), 'SigPartId' => $oPgpSignaturePart->PartID(),
'MicAlg' => (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') 'MicAlg' => (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg')
]; ];
@ -517,7 +535,7 @@ class Message implements \JsonSerializable
} }
$sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['SigPartId'].']'); $sPgpSignatureText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oMessage->aPgpSigned['SigPartId'].']');
if ($sPgpSignatureText && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) { if ($sPgpSignatureText && 0 < \strpos($sPgpSignatureText, 'BEGIN PGP SIGNATURE')) {
$oMessage->aPgpSigned['Signature'] = $oBodyStructure->SubParts()[0]->PartID(); $oMessage->aPgpSigned['Signature'] = $oPart->SubParts()[0]->PartID();
} }
*/ */
break; break;

View file

@ -257,6 +257,7 @@ trait Response
// $this->GetCapa(false, Capa::OPEN_PGP) || $this->GetCapa(false, Capa::GNUPG) // $this->GetCapa(false, Capa::OPEN_PGP) || $this->GetCapa(false, Capa::GNUPG)
$mResult['isPgpEncrypted'] = $mResponse->isPgpEncrypted(); $mResult['isPgpEncrypted'] = $mResponse->isPgpEncrypted();
$mResult['PgpSigned'] = $mResponse->PgpSigned(); $mResult['PgpSigned'] = $mResponse->PgpSigned();
$mResult['PgpEncrypted'] = $mResponse->PgpEncrypted();
$mResult['HasExternals'] = $bHasExternals; $mResult['HasExternals'] = $bHasExternals;
$mResult['HasInternals'] = \count($aFoundCIDs) || \count($aFoundContentLocationUrls); $mResult['HasInternals'] = \count($aFoundCIDs) || \count($aFoundContentLocationUrls);