From b8d898bc8aac1b443c592d5df8910d7a101a5ae5 Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Tue, 18 Jan 2022 14:51:08 +0100 Subject: [PATCH] #89 Detect and verify PGP cleartext/clearsigned messages --- .../app/libraries/MailSo/Mail/Message.php | 43 +++++++----------- .../libraries/RainLoop/Actions/Messages.php | 45 +++++++++++++------ 2 files changed, 46 insertions(+), 42 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php index 5a65d33fb..b6eb063da 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/Message.php @@ -77,8 +77,6 @@ class Message implements \JsonSerializable $aThreads = array(), - $bTextPartIsTrimmed = false, - $aPgpSigned = null, $bPgpEncrypted = false; @@ -538,27 +536,28 @@ class Message implements \JsonSerializable $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']'); if (null === $sText) { + // TextPartIsTrimmed ? $sText = $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$oPart->PartID().']<0>'); - if (\is_string($sText) && \strlen($sText)) - { - $oMessage->bTextPartIsTrimmed = true; - } } if (\is_string($sText) && \strlen($sText)) { - $sTextCharset = $oPart->Charset(); - if (empty($sTextCharset)) - { - $sTextCharset = $sCharset; - } - - $sTextCharset = Utils::NormalizeCharset($sTextCharset, true); - $sText = Utils::DecodeEncodingValue($sText, $oPart->MailEncodingName()); - $sText = Utils::ConvertEncoding($sText, $sTextCharset, \MailSo\Base\Enumerations\Charset::UTF_8); + $sText = Utils::ConvertEncoding($sText, + Utils::NormalizeCharset($oPart->Charset() ?: $sCharset, true), + \MailSo\Base\Enumerations\Charset::UTF_8 + ); $sText = Utils::Utf8Clear($sText); + // https://datatracker.ietf.org/doc/html/rfc4880#section-7 + // Cleartext Signature + if (!$oMessage->aPgpSigned && \str_contains($sText, '-----BEGIN PGP SIGNED MESSAGE-----')) + { + $oMessage->aPgpSigned = [ + 'BodyPartId' => $oPart->PartID() + ]; + } + if ('text/html' === $oPart->ContentType()) { $aHtmlParts[] = $sText; @@ -578,19 +577,6 @@ class Message implements \JsonSerializable $oMessage->sHtml = \implode('
', $aHtmlParts); $oMessage->sPlain = \trim(\implode("\n", $aPlainParts)); - $aMatch = array(); - if (!$oMessage->aPgpSigned && \preg_match('/-----BEGIN PGP SIGNATURE-----.+?-----END PGP SIGNATURE-----/ism', $oMessage->sPlain, $aMatch)) - { - $oMessage->aPgpSigned = [ - // /?/Raw/&q[]=/0/Download/&q[]=/... - // /?/Raw/&q[]=/0/View/&q[]=/... - 'BodyPartId' => 0, - 'SigPartId' => 0, - 'MicAlg' => '', - 'Signature' => \trim($aMatch[0]) - ]; - } - $oMessage->bPgpEncrypted = !$oMessage->bPgpEncrypted && false !== \stripos($oMessage->sPlain, '-----BEGIN PGP MESSAGE-----'); unset($aHtmlParts, $aPlainParts, $aMatch); @@ -602,6 +588,7 @@ class Message implements \JsonSerializable $oMessage->oAttachments = new AttachmentCollection; foreach ($gAttachmentsParts as /* @var $oAttachmentItem \MailSo\Imap\BodyStructure */ $oAttachmentItem) { +// if ('application/pgp-keys' === $oAttachmentItem->ContentType()) import ??? $oMessage->oAttachments->append( Attachment::NewBodyStructureInstance($oMessage->sFolder, $oMessage->iUid, $oAttachmentItem) ); diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php index d7eae6010..b353efb31 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Messages.php @@ -652,6 +652,9 @@ trait Messages return $this->DefaultResponse(__FUNCTION__, $mResult); } + /** + * https://datatracker.ietf.org/doc/html/rfc3156#section-5 + */ public function DoMessagePgpVerify() : array { $sFolderName = $this->GetActionParam('Folder', ''); @@ -665,23 +668,37 @@ trait Messages $oImapClient = $this->MailClient()->ImapClient(); $oImapClient->FolderExamine($sFolderName); - $oFetchResponse = $oImapClient->Fetch([ + $aParts = [ + FetchType::BODY_PEEK.'['.$sBodyPartId.']' + ]; + if ($sSigPartId) { // An empty section specification refers to the entire message, including the header. // But Dovecot does not return it with BODY.PEEK[1], so we also use BODY.PEEK[1.MIME]. - FetchType::BODY_PEEK.'['.$sBodyPartId.'.MIME]', - FetchType::BODY_PEEK.'['.$sBodyPartId.']', - FetchType::BODY_PEEK.'['.$sSigPartId.']' - ], $iUid, true)[0]; + $aParts[] = FetchType::BODY_PEEK.'['.$sBodyPartId.'.MIME]'; + $aParts[] = FetchType::BODY_PEEK.'['.$sSigPartId.']'; + } - $result = [ - 'Text' => \preg_replace('/\\R/s', "\r\n", - $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.'.MIME]') - . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.']') - ), - 'Signature' => preg_replace('/[^\x00-\x7F]/', '', - $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']') - ) - ]; + $oFetchResponse = $oImapClient->Fetch($aParts, $iUid, true)[0]; + + if ($sSigPartId) { + $result = [ + 'Text' => \preg_replace('/\\R/s', "\r\n", + $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.'.MIME]') + . $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.']') + ), + 'Signature' => preg_replace('/[^\x00-\x7F]/', '', + $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sSigPartId.']') + ) + ]; + } else { + // clearsigned text + $result = [ + 'Text' => \preg_replace('/\\R/s', "\r\n", + $oFetchResponse->GetFetchValue(FetchType::BODY.'['.$sBodyPartId.']') + ), + 'Signature' => false + ]; + } if (\class_exists('gnupg')) { $info = $this->GnuPG()->verify($result['Text'], $result['Signature'])[0];