Bugfix: handle multiple DKIM signatures authentication results

This commit is contained in:
the-djmaze 2022-12-21 20:51:33 +01:00
parent 219b155ede
commit 315f2a345a
7 changed files with 90 additions and 217 deletions

View file

@ -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 = '';
}
/**

View file

@ -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();

View file

@ -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);

View file

@ -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

View file

@ -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
);
}
}

View file

@ -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) {

View file

@ -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());