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 36ac413c6..1c945bae3 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 @@ -291,6 +291,7 @@ class Message implements \JsonSerializable } $aAuth = $oHeaders->AuthStatuses(); + $aAuth = $oHeaders->AuthStatuses(\RainLoop\Api::Config()->Get('security', 'trusted_authserv_ids', '')); $oMessage->SPF = $aAuth['spf']; $oMessage->DKIM = $aAuth['dkim']; $oMessage->DMARC = $aAuth['dmarc']; 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 1bb807bab..84283e097 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 @@ -180,7 +180,7 @@ class HeaderCollection extends \MailSo\Base\Collection * 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 + public function AuthStatuses(string $sTrustedIds = ''): array { $aResult = [ 'dkim' => [], @@ -188,6 +188,27 @@ class HeaderCollection extends \MailSo\Base\Collection 'spf' => [] ]; $aHeaders = $this->ValuesByName(Enumerations\Header::AUTHENTICATION_RESULTS); + + /** + * A sender can add this header itself, so a result is only worth reading when the + * server that wrote it handled the message. $sTrustedIds names those servers by + * authserv-id; with none given, the authserv-id of the topmost header is taken as + * the local one, which keeps the separate headers an opendkim and an opendmarc + * each add. See RFC 8601 section 7.1. + */ + $aTrusted = \array_filter(\preg_split('/[\\s,;]+/', \strtolower(\trim($sTrustedIds)))); + $bConfigured = !!\count($aTrusted); + if (\count($aHeaders)) { + // The authserv-id is the first field, before the first ";". A version number + // may follow it, and a comment may sit anywhere. + $fAuthservId = function (string $sValue) : string { + $sId = \preg_replace('/\\([^)]*\\)/', ' ', \explode(';', $sValue, 2)[0]); + return \strtolower(\trim(\preg_replace('/\\s+\\d+$/', '', \trim($sId)), " \t\"")); + }; + $aTrusted = $aTrusted ?: array($fAuthservId($aHeaders[0])); + $aHeaders = \array_filter($aHeaders, fn($sValue) => \in_array($fAuthservId($sValue), $aTrusted, true)); + } + if (\count($aHeaders)) { $aHeaders = \implode(';', $aHeaders); $aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders); @@ -204,8 +225,10 @@ class HeaderCollection extends \MailSo\Base\Collection } } } - if (!\count($aResult['dkim'])) { + if (!\count($aResult['dkim']) && !$bConfigured) { // X-DKIM-Authentication-Results: signer="hostinger.com" status="pass" + // This one carries no authserv-id, so there is nothing to tie it to: only the + // topmost is read, and an install that names its trusted ids skips it. $aHeaders = $this->ValuesByName(Enumerations\Header::X_DKIM_AUTHENTICATION_RESULTS); foreach ($aHeaders as $sHeaderValue) { $aStatus = array(); @@ -216,6 +239,7 @@ class HeaderCollection extends \MailSo\Base\Collection ) { $aResult['dkim'][] = array($aStatus[1], \trim($aHeader[1]), $sHeaderValue); } + break; } } diff --git a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php index fe8c0082a..87478571c 100644 --- a/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php +++ b/snappymail/v/0.0.0/app/libraries/RainLoop/Config/Application.php @@ -226,6 +226,14 @@ Warning: only enable when server does not do this, else double compression error 'openpgp' => array(true), 'auto_verify_signatures' => array(false), + 'trusted_authserv_ids' => array('', 'Which servers\' DKIM, SPF and DMARC results to believe, +by authserv-id (the first field of an Authentication-Results header), separated by +spaces or commas. A sender can add that header itself, so a result not tied to a +server that handled the message says nothing. +Empty: the authserv-id of the topmost header is taken as the local one. That covers +the usual case, but a server which adds no results of its own leaves the sender\'s +header as the topmost, so name your own id here if these results are shown.'), + 'allow_admin_panel' => array(true, 'Access settings'), 'admin_login' => array('admin', 'Login and password for web admin panel'), 'admin_password' => array(''),