From 7b3102a6b3ac60fc71c6d6122c5a49033e7dd6bf Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 14 Feb 2024 02:43:54 +0100 Subject: [PATCH] Added isSMimeEncrypted() and isSMimeSigned() #259 --- .../libraries/MailSo/Imap/BodyStructure.php | 37 ++++++++++++---- .../app/libraries/MailSo/Mail/MailClient.php | 2 +- .../app/libraries/MailSo/Mail/Message.php | 44 ++++++++++++++----- .../0.0.0/app/libraries/MailSo/Mime/Part.php | 19 +++++--- .../app/libraries/RainLoop/Actions/Pgp.php | 2 +- 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php index 4dad44737..dd912746b 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Imap/BodyStructure.php @@ -127,9 +127,9 @@ class BodyStructure implements \JsonSerializable return 'text/html' === $this->sContentType || 'text/plain' === $this->sContentType; } - public function IsPgpEncrypted() : bool + // https://datatracker.ietf.org/doc/html/rfc3156#section-4 + public function isPgpEncrypted() : bool { - // https://datatracker.ietf.org/doc/html/rfc3156#section-4 return 'multipart/encrypted' === $this->sContentType && !empty($this->aContentTypeParams['protocol']) && 'application/pgp-encrypted' === \strtolower(\trim($this->aContentTypeParams['protocol'])) @@ -140,20 +140,39 @@ class BodyStructure implements \JsonSerializable // && 'Version: 1' === $this->aSubParts[0]->Body() } - public function IsPgpSigned() : bool + // https://datatracker.ietf.org/doc/html/rfc3156#section-5 + public function isPgpSigned() : bool { - // https://datatracker.ietf.org/doc/html/rfc3156#section-5 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) - && $this->aSubParts[1]->IsPgpSignature(); + && 'application/pgp-signature' === $this->aSubParts[1]->ContentType(); } - public function IsPgpSignature() : bool + // https://datatracker.ietf.org/doc/html/rfc2633#section-3.3 + public function isSMimeEncrypted() : bool { - return \in_array($this->sContentType, ['application/pgp-signature', 'application/pkcs7-signature']); + $type = \strtolower(\trim($this->aContentTypeParams['smime-type'] ?? '')); + return 'application/pkcs7-mime' === $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']) + && 'application/pkcs7-signature' === \strtolower(\trim($this->aContentTypeParams['protocol'])) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->aSubParts) + && 'application/pkcs7-signature' === $this->aSubParts[1]->ContentType() + ) || ('application/pkcs7-mime' === $this->sContentType + && !empty($this->aContentTypeParams['smime-type']) + && 'signed-data' === \strtolower(\trim($this->aContentTypeParams['smime-type'])) + ); } public function IsAttachment() : bool @@ -192,7 +211,7 @@ class BodyStructure implements \JsonSerializable if (!$aParts) { $gEncryptedParts = $this->SearchByContentType('multipart/encrypted'); foreach ($gEncryptedParts as $oPart) { - if ($oPart->IsPgpEncrypted() && $oPart->SubParts()[1]->isInline()) { + if ($oPart->isPgpEncrypted() && $oPart->SubParts()[1]->isInline()) { return array($oPart->SubParts()[1]); } } @@ -242,7 +261,7 @@ class BodyStructure implements \JsonSerializable { return $this->SearchByCallback(function ($oItem, $oParent) { // return $oItem->IsAttachment(); - return $oItem->IsAttachment() && (!$oParent || !$oParent->IsPgpEncrypted()); + return $oItem->IsAttachment() && (!$oParent || !$oParent->isPgpEncrypted()); }); } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php index 958e5b818..38ee6d9e0 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mail/MailClient.php @@ -152,7 +152,7 @@ class MailClient /* $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); foreach ($gSignatureParts as $oPart) { - if ($oPart->IsPgpSigned()) { + if ($oPart->isPgpSigned()) { // 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]. $aFetchItems[] = FetchType::BODY_PEEK.'['.$oPart->SubParts()[0]->PartID().'.MIME]'; 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 0ca6e7b11..25046ed40 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 @@ -65,9 +65,13 @@ class Message implements \JsonSerializable $aThreadUnseenUIDs = []; private ?array $DraftInfo = null; + private ?array $pgpSigned = null; private ?array $pgpEncrypted = null; + private ?array $smimeSigned = null; + private ?array $smimeEncrypted = null; + private ?\MailSo\Mime\EmailCollection $oFrom = null, $oSender = null, @@ -309,26 +313,44 @@ class Message implements \JsonSerializable if ($oBodyStructure) { $gEncryptedParts = $oBodyStructure->SearchByContentType('multipart/encrypted'); foreach ($gEncryptedParts as $oPart) { - if ($oPart->IsPgpEncrypted()) { + if ($oPart->isPgpEncrypted()) { $oMessage->pgpEncrypted = [ 'partId' => $oPart->SubParts()[1]->PartID() ]; } } + $gEncryptedParts = $oBodyStructure->SearchByContentType('application/pkcs7-mime'); + foreach ($gEncryptedParts as $oPart) { + if ($oPart->isSMimeEncrypted()) { + $oMessage->smimeEncrypted = [ + 'partId' => $oPart->PartID() + ]; + } else if ($oPart->isSMimeSigned()) { + $oMessage->smimeSigned = [ + 'sigPartId' => $oPart->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '' + ]; + } + } + $gSignatureParts = $oBodyStructure->SearchByContentType('multipart/signed'); foreach ($gSignatureParts as $oPart) { - if (!$oPart->IsPgpSigned()) { - continue; + if ($oPart->isPgpSigned()) { + $oMessage->pgpSigned = [ + // /?/Raw/&q[]=/0/Download/&q[]=/... + // /?/Raw/&q[]=/0/View/&q[]=/... + 'bodyPartId' => $oPart->SubParts()[0]->PartID(), + 'sigPartId' => $oPart->SubParts()[1]->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '' + ]; + } else if ($oPart->isSMimeSigned()) { + $oMessage->smimeSigned = [ + 'bodyPartId' => $oPart->SubParts()[0]->PartID(), + 'sigPartId' => $oPart->SubParts()[1]->PartID(), + 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '' + ]; } - $oPgpSignaturePart = $oPart->SubParts()[1]; - $oMessage->pgpSigned = [ - // /?/Raw/&q[]=/0/Download/&q[]=/... - // /?/Raw/&q[]=/0/View/&q[]=/... - 'bodyPartId' => $oPart->SubParts()[0]->PartID(), - 'sigPartId' => $oPgpSignaturePart->PartID(), - 'micAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(MimeHeader::CONTENT_TYPE, 'micalg') : '' - ]; /* // 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]. diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php index ceb2e2692..2c93649fe 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Part.php @@ -80,20 +80,29 @@ class Part return $sResult; } - public function IsPgpSigned() : bool + // https://datatracker.ietf.org/doc/html/rfc3156#section-5 + public function isPgpSigned() : bool { - // https://datatracker.ietf.org/doc/html/rfc3156#section-5 $header = $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE); return $header && \preg_match('#multipart/signed.+protocol=["\']?application/pgp-signature#si', $header->FullValue()) // The multipart/signed body MUST consist of exactly two parts. && 2 === \count($this->SubParts) - && $this->SubParts[1]->IsPgpSignature(); + && 'application/pgp-signature' === $this->SubParts[1]->ContentType(); } - public function IsPgpSignature() : bool + // https://www.rfc-editor.org/rfc/rfc8551.html#section-3.5 + public function isSMimeSigned() : bool { - return \in_array($this->ContentType(), array('application/pgp-signature', 'application/pkcs7-signature')); + $header = $this->Headers->GetByName(Enumerations\Header::CONTENT_TYPE); + return ($header + && \preg_match('#multipart/signed.+protocol=["\']?application/pkcs7-signature#si', $header->FullValue()) + // The multipart/signed body MUST consist of exactly two parts. + && 2 === \count($this->SubParts) + && 'application/pkcs7-signature' === $this->SubParts[1]->ContentType() + ) || ($header + && \preg_match('#application/pkcs7-mime.+smime-type=["\']?signed-data#si', $header->FullValue()) + ); } public static function FromFile(string $sFileName) : ?self diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php index 634e5fe4f..0751d1401 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Actions/Pgp.php @@ -146,7 +146,7 @@ trait Pgp ); } - if ($oPart && $oPart->IsPgpSigned()) { + if ($oPart && $oPart->isPgpSigned()) { // $GPG->verifyStream($oPart->SubParts[0]->Body, \stream_get_contents($oPart->SubParts[1]->Body)); // $result['signatures'] = $oPart->SubParts[0]; }