From 315f2a345aa3ea0e375e3347a2366d715910affe Mon Sep 17 00:00:00 2001 From: the-djmaze <> Date: Wed, 21 Dec 2022 20:51:33 +0100 Subject: [PATCH] Bugfix: handle multiple DKIM signatures authentication results --- dev/Model/Email.js | 5 +- dev/Model/Message.js | 139 ++++-------------- dev/View/User/MailBox/MessageView.js | 4 +- .../app/libraries/MailSo/Mail/Message.php | 33 ++++- .../0.0.0/app/libraries/MailSo/Mime/Email.php | 25 +--- .../libraries/MailSo/Mime/EmailCollection.php | 10 -- .../MailSo/Mime/HeaderCollection.php | 91 ++++-------- 7 files changed, 90 insertions(+), 217 deletions(-) diff --git a/dev/Model/Email.js b/dev/Model/Email.js index 7b980a163..30c613ee9 100644 --- a/dev/Model/Email.js +++ b/dev/Model/Email.js @@ -259,14 +259,12 @@ export class EmailModel extends AbstractModel { * @param {string=} email = '' * @param {string=} name = '' * @param {string=} dkimStatus = 'none' - * @param {string=} dkimValue = '' */ - constructor(email = '', name = '', dkimStatus = 'none', dkimValue = '') { + constructor(email = '', name = '', dkimStatus = 'none') { super(); this.email = email; this.name = name; this.dkimStatus = dkimStatus; - this.dkimValue = dkimValue; this.clearDuplicateName(); } @@ -290,7 +288,6 @@ export class EmailModel extends AbstractModel { this.name = ''; this.dkimStatus = 'none'; - this.dkimValue = ''; } /** diff --git a/dev/Model/Message.js b/dev/Model/Message.js index bdbfd5926..798cfdb94 100644 --- a/dev/Model/Message.js +++ b/dev/Model/Message.js @@ -5,7 +5,7 @@ import { i18n } from 'Common/Translator'; import { doc, SettingsGet } from 'Common/Globals'; import { encodeHtml, plainToHtml, htmlToPlain, cleanHtml } from 'Common/Html'; -import { arrayLength, forEachObjectEntry } from 'Common/Utils'; +import { isFunction, forEachObjectEntry } from 'Common/Utils'; import { serverRequestRaw, proxy } from 'Common/Links'; import { addObservablesTo, addComputablesTo } from 'External/ko'; @@ -66,7 +66,24 @@ export class MessageModel extends AbstractModel { constructor() { super(); - this._reset(); + this.folder = ''; + this.uid = 0; + this.hash = ''; + this.requestHash = ''; + this.from = new EmailCollectionModel; + this.to = new EmailCollectionModel; + this.cc = new EmailCollectionModel; + this.bcc = new EmailCollectionModel; + this.replyTo = new EmailCollectionModel; + this.deliveredTo = new EmailCollectionModel; + this.body = null; + this.draftInfo = []; + this.dkim = []; + this.spf = []; + this.dmarc = []; + this.messageId = ''; + this.inReplyTo = ''; + this.references = ''; addObservablesTo(this, { subject: '', @@ -156,67 +173,6 @@ export class MessageModel extends AbstractModel { toggleTag(this, keyword); } - _reset() { - this.folder = ''; - this.uid = 0; - this.hash = ''; - this.requestHash = ''; - this.emails = []; - this.from = new EmailCollectionModel; - this.to = new EmailCollectionModel; - this.cc = new EmailCollectionModel; - this.bcc = new EmailCollectionModel; - this.replyTo = new EmailCollectionModel; - this.deliveredTo = new EmailCollectionModel; - this.body = null; - this.draftInfo = []; - this.messageId = ''; - this.inReplyTo = ''; - this.references = ''; - } - - clear() { - this._reset(); - this.subject(''); - this.html(''); - this.plain(''); - this.size(0); - this.spamScore(0); - this.spamResult(''); - this.isSpam(false); - this.hasVirus(null); - this.dateTimeStampInUTC(0); - this.priority(MessagePriority.Normal); - - this.senderEmailsString(''); - this.senderClearEmailsString(''); - - this.deleted(false); - - this.selected(false); - this.checked(false); - - this.isHtml(false); - this.hasImages(false); - this.hasExternals(false); - this.attachments(new AttachmentCollectionModel); - - this.pgpSigned(null); - this.pgpVerified(null); - - this.pgpEncrypted(null); - this.pgpDecrypted(false); - - this.priority(MessagePriority.Normal); - this.readReceipt(''); - - this.threads([]); - this.unsubsribeLinks([]); - - this.hasUnseenSubMessage(false); - this.hasFlaggedSubMessage(false); - } - spamStatus() { let spam = this.spamResult(); return spam ? i18n(this.isSpam() ? 'GLOBAL/SPAM' : 'GLOBAL/NOT_SPAM') + ': ' + spam : ''; @@ -276,18 +232,6 @@ export class MessageModel extends AbstractModel { return this.from.toString(friendlyView, wrapWithLink); } - /** - * @returns {string} - */ - fromDkimData() { - let result = ['none', '']; - if (1 === arrayLength(this.from) && this.from[0]?.dkimStatus) { - result = [this.from[0].dkimStatus, this.from[0].dkimValue || '']; - } - - return result; - } - /** * @param {boolean} friendlyView * @param {boolean=} wrapWithLink @@ -493,43 +437,14 @@ export class MessageModel extends AbstractModel { let self = new MessageModel(); if (message) { - self.folder = message.folder; - self.uid = message.uid; - self.hash = message.hash; - self.requestHash = message.requestHash; - self.subject(message.subject()); - self.plain(message.plain()); - self.html(message.html()); - - self.size(message.size()); - self.spamScore(message.spamScore()); - self.spamResult(message.spamResult()); - self.isSpam(message.isSpam()); - self.hasVirus(message.hasVirus()); - self.dateTimeStampInUTC(message.dateTimeStampInUTC()); - self.priority(message.priority()); - - self.hasExternals(message.hasExternals()); - - self.emails = message.emails; - - self.from = message.from; - self.to = message.to; - self.cc = message.cc; - self.bcc = message.bcc; - self.replyTo = message.replyTo; - self.deliveredTo = message.deliveredTo; - self.unsubsribeLinks(message.unsubsribeLinks); - - self.flags(message.flags()); - - self.priority(message.priority()); - - self.selected(message.selected()); - self.checked(message.checked()); - self.attachments(message.attachments()); - - self.threads(message.threads()); + // Clone message values + forEachObjectEntry(message, (key, value) => { + if (ko.isObservable(value)) { + ko.isComputed(value) || self[key](value()); + } else if (!isFunction(value)) { + self[key] = value; + } + }); } self.computeSenderEmail(); diff --git a/dev/View/User/MailBox/MessageView.js b/dev/View/User/MailBox/MessageView.js index 3bd14da25..47d9511fc 100644 --- a/dev/View/User/MailBox/MessageView.js +++ b/dev/View/User/MailBox/MessageView.js @@ -196,7 +196,9 @@ export class MailMessageView extends AbstractViewRight { this.viewHash = message.hash; // TODO: make first param a user setting #683 this.viewFromShort(message.fromToLine(false, true)); - this.viewFromDkimData(message.fromDkimData()); + let dkim = 1 === arrayLength(message.from) && message.dkim + && message.dkim.find(dkim => message.from[0].email.includes(dkim[1])); + this.viewFromDkimData(dkim ? [dkim[0], dkim[2]] : ['none', '']); this.viewToShort(message.toToLine(true, true)); } else { MessagelistUserStore.selectedMessage(null); 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 646d5e974..43929c7dd 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 @@ -63,6 +63,9 @@ class Message implements \JsonSerializable $bHasVirus = null; private array + $SPF = [], + $DKIM = [], + $DMARC = [], // $aFlags = [], $aFlagsLowerCase = [], $UnsubsribeLinks = [], @@ -138,9 +141,8 @@ class Message implements \JsonSerializable $sCharset = $oBodyStructure ? Utils::NormalizeCharset($oBodyStructure->SearchCharset()) : ''; $sHeaders = $oFetchResponse->GetHeaderFieldsValue(); - if (\strlen($sHeaders)) { - $oHeaders = new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset); - + $oHeaders = \strlen($sHeaders) ? new \MailSo\Mime\HeaderCollection($sHeaders, false, $sCharset) : null; + if ($oHeaders) { $sContentTypeCharset = $oHeaders->ParameterValue( \MailSo\Mime\Enumerations\Header::CONTENT_TYPE, \MailSo\Mime\Enumerations\Parameter::CHARSET @@ -165,10 +167,6 @@ class Message implements \JsonSerializable $oMessage->oCc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::CC, $bCharsetAutoDetect); $oMessage->oBcc = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::BCC, $bCharsetAutoDetect); - if ($oMessage->oFrom) { - $oHeaders->PopulateEmailColectionByDkim($oMessage->oFrom); - } - $oMessage->oSender = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::SENDER, $bCharsetAutoDetect); $oMessage->oReplyTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::REPLY_TO, $bCharsetAutoDetect); $oMessage->oDeliveredTo = $oHeaders->GetAsEmailCollection(\MailSo\Mime\Enumerations\Header::DELIVERED_TO, $bCharsetAutoDetect); @@ -291,6 +289,21 @@ class Message implements \JsonSerializable } } + $aAuth = $oHeaders->AuthStatuses(); + $oMessage->SPF = $aAuth['spf']; + $oMessage->DKIM = $aAuth['dkim']; + $oMessage->DMARC = $aAuth['dmarc']; + if ($aAuth['dkim'] && $oMessage->oFrom) { + foreach ($oMessage->oFrom as $oEmail) { + $sEmail = $oEmail->GetEmail(); + foreach ($aAuth['dkim'] as $aDkimData) { + if (\strpos($sEmail, $aDkimData[1])) { + $oEmail->SetDkimStatus($aDkimData[0]); + } + } + } + } + $oMessage->sAutocrypt = $oHeaders->ValueByName(\MailSo\Mime\Enumerations\Header::AUTOCRYPT); } else if ($oFetchResponse->GetEnvelope()) @@ -331,7 +344,7 @@ class Message implements \JsonSerializable // /?/Raw/&q[]=/0/View/&q[]=/... 'BodyPartId' => $oPart->SubParts()[0]->PartID(), 'SigPartId' => $oPgpSignaturePart->PartID(), - 'MicAlg' => (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') + 'MicAlg' => $oHeaders ? (string) $oHeaders->ParameterValue(\MailSo\Mime\Enumerations\Header::CONTENT_TYPE, 'micalg') : '' ]; /* // An empty section specification refers to the entire message, including the header. @@ -478,6 +491,10 @@ class Message implements \JsonSerializable 'Attachments' => $this->Attachments, + 'spf' => $this->SPF, + 'dkim' => $this->DKIM, + 'dmarc' => $this->DMARC, + 'Flags' => $aFlags, // https://datatracker.ietf.org/doc/html/rfc8621#section-4.1.1 diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php index eb7fa5a0b..8a9c84152 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/Email.php @@ -23,8 +23,6 @@ class Email implements \JsonSerializable private string $sDkimStatus = Enumerations\DkimStatus::NONE; - private string $sDkimValue = ''; - /** * @throws \InvalidArgumentException */ @@ -165,16 +163,6 @@ class Email implements \JsonSerializable return $this->sDisplayName; } - public function GetDkimStatus() : string - { - return $this->sDkimStatus; - } - - public function GetDkimValue() : string - { - return $this->sDkimValue; - } - public function GetAccountName() : string { return \MailSo\Base\Utils::GetAccountNameFromEmail($this->GetEmail(false)); @@ -185,17 +173,9 @@ class Email implements \JsonSerializable return \MailSo\Base\Utils::GetDomainFromEmail($this->GetEmail($bIdn)); } - public function SetDkimStatusAndValue(string $sDkimStatus, string $sDkimValue = '') + public function SetDkimStatus(string $sDkimStatus) { $this->sDkimStatus = Enumerations\DkimStatus::normalizeValue($sDkimStatus); - $this->sDkimValue = $sDkimValue; - } - - public function ToArray(bool $bIdn = false, bool $bDkim = true) : array - { - return $bDkim ? - array($this->sDisplayName, $this->GetEmail($bIdn), $this->sDkimStatus, $this->sDkimValue) : - array($this->sDisplayName, $this->GetEmail($bIdn)); } public function ToString(bool $bConvertSpecialsName = false, bool $bIdn = false) : string @@ -227,8 +207,7 @@ class Email implements \JsonSerializable '@Object' => 'Object/Email', 'Name' => \MailSo\Base\Utils::Utf8Clear($this->GetDisplayName()), 'Email' => \MailSo\Base\Utils::Utf8Clear($this->GetEmail(true)), - 'DkimStatus' => $this->GetDkimStatus(), - 'DkimValue' => $this->GetDkimValue() + 'DkimStatus' => $this->sDkimStatus ); } } diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php index f409dff6a..b909f318f 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/EmailCollection.php @@ -33,16 +33,6 @@ class EmailCollection extends \MailSo\Base\Collection parent::append($oEmail, $bToTop); } - public function ToArray() : array - { - $aReturn = array(); - foreach ($this as $oEmail) { - $aReturn[] = $oEmail->ToArray(); - } - - return $aReturn; - } - public function MergeWithOtherCollection(EmailCollection $oEmails) : self { foreach ($oEmails as $oEmail) { diff --git a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php index 48af760cb..893167385 100644 --- a/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php +++ b/snappymail/v/0.0.0/app/libraries/MailSo/Mime/HeaderCollection.php @@ -196,60 +196,46 @@ class HeaderCollection extends \MailSo\Base\Collection return $this; } - public function DkimStatuses() : array + /** + * https://www.rfc-editor.org/rfc/rfc8601 + * dkim=pass header.d=domain.tld header.s=s1 header.b=F2SfoZWw; + * spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld"; + * dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none) + */ + public function AuthStatuses() : array { - $aResult = array(); - + $aResult = [ + 'dkim' => [], + 'dmarc' => [], + 'spf' => [] + ]; $aHeaders = $this->ValuesByName(Enumerations\Header::AUTHENTICATION_RESULTS); if (\count($aHeaders)) { - foreach ($aHeaders as $sHeaderValue) { - $sStatus = ''; - $sHeader = ''; - $sDkimLine = ''; - - $aMatch = array(); - - $sHeaderValue = \preg_replace('/[\r\n\t\s]+/', ' ', $sHeaderValue); - - if (\preg_match('/dkim=.+/i', $sHeaderValue, $aMatch) && !empty($aMatch[0])) { - $sDkimLine = $aMatch[0]; - - $aMatch = array(); - if (\preg_match('/dkim=([a-zA-Z0-9]+)/i', $sDkimLine, $aMatch) && !empty($aMatch[1])) { - $sStatus = $aMatch[1]; - } - - $aMatch = array(); - if (\preg_match('/header\.(d|i|from)=([^\s;]+)/i', $sDkimLine, $aMatch) && !empty($aMatch[2])) { - $sHeader = \trim($aMatch[2]); - } - - if (!empty($sStatus) && !empty($sHeader)) { - $aResult[] = array($sStatus, $sHeader, $sDkimLine); - } + $aHeaders = \implode(';', $aHeaders); + $aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders); + $aHeaders = \explode(';', $aHeaders); + foreach ($aHeaders as $sLine) { + $aStatus = array(); + $aHeader = array(); + if (\preg_match("/(dkim|dmarc|spf)=([a-z0-9]+).*?(;|$)/Di", $sLine, $aStatus) + && \preg_match('/(?:header\\.(?:d|i|from)|smtp.mailfrom)="?([^\\s;"]+)/i', $sLine, $aHeader) + ) { + $sType = \strtolower($aStatus[1]); + $aResult[$sType][] = array(\strtolower($aStatus[2]), $aHeader[1], \trim($sLine)); } } - } else { + } + if (!\count($aResult['dkim'])) { // X-DKIM-Authentication-Results: signer="hostinger.com" status="pass" $aHeaders = $this->ValuesByName(Enumerations\Header::X_DKIM_AUTHENTICATION_RESULTS); foreach ($aHeaders as $sHeaderValue) { - $sStatus = ''; - $sHeader = ''; - - $aMatch = array(); - - $sHeaderValue = \preg_replace('/[\r\n\t\s]+/', ' ', $sHeaderValue); - - if (\preg_match('/status[\s]?=[\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aMatch) && !empty($aMatch[1])) { - $sStatus = $aMatch[1]; - } - - if (\preg_match('/signer[\s]?=[\s]?"([^";]+)"/i', $sHeaderValue, $aMatch) && !empty($aMatch[1])) { - $sHeader = \trim($aMatch[1]); - } - - if (!empty($sStatus) && !empty($sHeader)) { - $aResult[] = array($sStatus, $sHeader, $sHeaderValue); + $aStatus = array(); + $aHeader = array(); + $sHeaderValue = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $sHeaderValue); + if (\preg_match('/status[\\s]?=[\\s]?"([a-zA-Z0-9]+)"/i', $sHeaderValue, $aStatus) && !empty($aStatus[1]) + && \preg_match('/signer[\\s]?=[\\s]?"([^";]+)"/i', $sHeaderValue, $aHeader) && !empty($aHeader[1]) + ) { + $aResult['dkim'][] = array($aStatus[1], \trim($aHeader[1]), $sHeaderValue); } } } @@ -257,19 +243,6 @@ class HeaderCollection extends \MailSo\Base\Collection return $aResult; } - public function PopulateEmailColectionByDkim(EmailCollection $oEmails) : void - { - $aDkimStatuses = $this->DkimStatuses(); - foreach ($oEmails as $oEmail) { - $sEmail = $oEmail->GetEmail(); - foreach ($aDkimStatuses as $aDkimData) { - if (isset($aDkimData[0], $aDkimData[1]) && $aDkimData[1] === \strstr($sEmail, $aDkimData[1])) { - $oEmail->SetDkimStatusAndValue($aDkimData[0], empty($aDkimData[2]) ? '' : $aDkimData[2]); - } - } - } - } - public function __toString() : string { return \implode("\r\n", $this->getArrayCopy());