Skip to content

Commit

Permalink
Add support for BIMI (#242)
Browse files Browse the repository at this point in the history
  • Loading branch information
lieser committed Oct 29, 2023
1 parent 093cefb commit 2381e68
Show file tree
Hide file tree
Showing 10 changed files with 932 additions and 11 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@

# Disable end-of-line normalization for the following files
data/favicon/** -text
test/data/original/** -text
thirdparty/** -text
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file.

### Enhancements

- Added support for using the Brand Indicators for Message Identification (BIMI)
when showing favicons is enabled (#242).
- Added the possibility to show a favicon for a specific From address or AUID (#107).
- Don't save DKIM results that contain a temporary error.
- Show proper error message if parsing of a message failed.
Expand Down
5 changes: 5 additions & 0 deletions modules/arhParser.mjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ function checkResultKeyword(method, resultKeyword) {
allowedKeywords = ["none", "pass", "fail", "temperror", "permerror"];
}

// BIMI (https://datatracker.ietf.org/doc/draft-brand-indicators-for-message-identification/04/ section 7.7.)
if (method === "bimi") {
allowedKeywords = ["pass", "none", "fail", "temperror", "declined", "skipped"];
}

// Note: Both the ARH RFC and the IANA registry contain keywords for more than the above methods.
// As we don't really care about them, for simplicity we treat them the same as unknown methods,
// And don't restrict the keyword.
Expand Down
28 changes: 21 additions & 7 deletions modules/authVerifier.mjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@

export const moduleVersion = "2.0.0";

import { addrIsInDomain2, domainIsInDomain, getDomainFromAddr } from "./utils.mjs.js";
import { addrIsInDomain, addrIsInDomain2, domainIsInDomain, getDomainFromAddr } from "./utils.mjs.js";
import ArhParser from "./arhParser.mjs.js";
import DMARC from "./dkim/dmarc.mjs.js";
import ExtensionUtils from "./extensionUtils.mjs.js";
import Logging from "./logging.mjs.js";
import MsgParser from "./msgParser.mjs.js";
import SignRules from "./dkim/signRules.mjs.js";
import Verifier from "./dkim/verifier.mjs.js";
import { getBimiIndicator } from "./bimi.mjs.js";
import { getFavicon } from "./dkim/favicon.mjs.js";
import prefs from "./preferences.mjs.js";

Expand All @@ -50,11 +51,12 @@ const log = Logging.getLogger("AuthVerifier");

/**
* @typedef {object} SavedAuthResultV3
* @property {string} version Result version ("3.0").
* @property {string} version Result version ("3.1").
* @property {dkimSigResultV2[]} dkim
* @property {ArhResInfo[]|undefined} [spf]
* @property {ArhResInfo[]|undefined} [dmarc]
* @property {{dkim?: dkimSigResultV2[]}|undefined} [arh]
* @property {string|undefined} [bimiIndicator] Since version 3.1
*/
/**
* @typedef {SavedAuthResultV3} SavedAuthResult
Expand Down Expand Up @@ -179,13 +181,14 @@ export default class AuthVerifier {
savedAuthResult = arhResult;
} else {
savedAuthResult = {
version: "3.0",
version: "3.1",
dkim: [],
spf: arhResult.spf,
dmarc: arhResult.dmarc,
arh: {
dkim: arhResult.dkim
},
bimiIndicator: arhResult.bimiIndicator,
};
}
} else {
Expand Down Expand Up @@ -253,6 +256,8 @@ async function getARHResult(message, headers, from, listId, account, dmarc) {
let arhSPF = [];
/** @type {ArhResInfo[]} */
let arhDMARC = [];
/** @type {ArhResInfo[]} */
let arhBIMI = [];
for (const header of arHeaders) {
/** @type {import("./arhParser.mjs.js").ArhHeader} */
let arh;
Expand Down Expand Up @@ -289,6 +294,9 @@ async function getARHResult(message, headers, from, listId, account, dmarc) {
arhDMARC = arhDMARC.concat(arh.resinfo.filter((element) => {
return element.method === "dmarc";
}));
arhBIMI = arhBIMI.concat(arh.resinfo.filter((element) => {
return element.method === "bimi";
}));
}

// convert DKIM results
Expand Down Expand Up @@ -331,10 +339,11 @@ async function getARHResult(message, headers, from, listId, account, dmarc) {
sortSignatures(dkimSigResults, from, listId);

const savedAuthResult = {
version: "3.0",
version: "3.1",
dkim: dkimSigResults,
spf: arhSPF,
dmarc: arhDMARC,
bimiIndicator: getBimiIndicator(headers, arhBIMI) ?? undefined,
};
log.debug("ARH result:", savedAuthResult);
return savedAuthResult;
Expand Down Expand Up @@ -875,7 +884,7 @@ function SavedAuthResult_to_AuthResult(savedAuthResult, from) {
dkimSigResultV2_to_AuthResultDKIM)
};
}
return addFavicons(authResult, from);
return addFavicons(authResult, from, savedAuthResult.bimiIndicator);
}

/**
Expand Down Expand Up @@ -904,9 +913,10 @@ function AuthResultDKIMV2_to_dkimSigResultV2(authResultDKIM) {
*
* @param {AuthResult} authResult
* @param {string?} from
* @param {string|undefined} bimiIndicator
* @returns {Promise<AuthResult>} authResult
*/
async function addFavicons(authResult, from) {
async function addFavicons(authResult, from, bimiIndicator) {
if (!prefs["display.favicon.show"]) {
return authResult;
}
Expand All @@ -915,7 +925,11 @@ async function addFavicons(authResult, from) {
}
for (const dkim of authResult.dkim) {
if (dkim.sdid) {
dkim.favicon = await getFavicon(dkim.sdid, dkim.auid, from);
if (bimiIndicator && from && addrIsInDomain(from, dkim.sdid)) {
dkim.favicon = `data:image/svg+xml;base64,${bimiIndicator}`;
} else {
dkim.favicon = await getFavicon(dkim.sdid, dkim.auid, from);
}
}
}
return authResult;
Expand Down
72 changes: 72 additions & 0 deletions modules/bimi.mjs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Brand Indicators for Message Identification (BIMI).
* https://datatracker.ietf.org/doc/draft-brand-indicators-for-message-identification/04/
*
* BIMI implementation for a Mail User Agent (MUA).
* Gets the BIMI Indicator based on the information the receiving
* Mail Transfer Agent (MTA) writes into the headers of the message.
*
* This is not a complete implementation of BIMI.
*
* Copyright (c) 2023 Philippe Lieser
*
* This software is licensed under the terms of the MIT License.
*
* The above copyright and license notice shall be
* included in all copies or substantial portions of the Software.
*/

// @ts-check

import Logging from "./logging.mjs.js";
import RfcParser from "./rfcParser.mjs.js";

const log = Logging.getLogger("BIMI");


/**
* Try to get the BIMI Indicator if available.
*
* @param {Map<string, string[]>} headers
* @param {import("./arhParser.mjs.js").ArhResInfo[]} arhBIMI - Trusted ARHs containing a BIMI result.
* @returns {string|null}
*/
export function getBimiIndicator(headers, arhBIMI) {
// Assuming:
// 1. We only get ARHs that can be trusted (i.e. from the receiving MTA).
// 2. If the receiving MTA does not supports BIMI,
// we will not see an ARH with a BIMI result (because of 1)
// 3. If the receiving MTA supports BIMI,
// it will make sure we only see his BIMI-Indicator headers (as required by the RFC).
//
// Given the above, it should be safe to trust the BIMI indicator from the BIMI-Indicator header
// if we have a passing BIMI result there the MTA claims to have checked the Authority Evidence.
const hasAuthorityPassBIMI = arhBIMI.some(
arh => arh.method === "bimi" &&
arh.result === "pass" &&
arh.propertys.policy.authority === "pass"
);
if (!hasAuthorityPassBIMI) {
return null;
}

const bimiIndicators = headers.get("bimi-indicator") ?? [];
if (bimiIndicators.length > 1) {
log.warn("Message contains more than one BIMI-Indicator header");
return null;
}
let bimiIndicator = bimiIndicators[0];
if (!bimiIndicator) {
log.warn("Message contains an ARH with passing BIMI but does not have a BIMI-Indicator header");
return null;
}

// TODO: If in the future we support ARC we might want to check the policy.indicator-hash

// Remove header name and new line at end
bimiIndicator = bimiIndicator.slice("bimi-indicator:".length, -"\r\n".length);
// Remove all whitespace
bimiIndicator = bimiIndicator.replace(new RegExp(`${RfcParser.FWS}`, "g"), "");

return bimiIndicator;
}
5 changes: 5 additions & 0 deletions test/data/bimi/example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 29 additions & 0 deletions test/data/bimi/rfc6376-A.2-with_bimi.eml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Authentication-Results: mx5.messagingengine.com;
bimi=pass header.d=example.com header.selector=default policy.authority=pass
BIMI-Indicator: PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiICBzdGFuZGFsb25l
PSJ5ZXMiPz4KPHN2ZyB2ZXJzaW9uPSIxLjIiIGJhc2VQcm9maWxlPSJ0aW55LXBzIiB2aWV3Qm94
PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHRpdGxl
PkV4YW1wbGU8L3RpdGxlPgo8Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI0MCIgc3Ryb2tlPSJi
bGFjayIgc3Ryb2tlLXdpZHRoPSIzIiBmaWxsPSJyZWQiIC8+Cjwvc3ZnPg==
DKIM-Signature: v=1; a=rsa-sha256; s=brisbane; d=example.com;
c=simple/simple; q=dns/txt; i=[email protected];
h=Received : From : To : Subject : Date : Message-ID;
bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;
b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB
4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHut
KVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV
4bmp/YzhwvcubU4=;
Received: from client1.football.example.com [192.0.2.1]
by submitserver.example.com with SUBMISSION;
Fri, 11 Jul 2003 21:01:54 -0700 (PDT)
From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
Subject: Is dinner ready?
Date: Fri, 11 Jul 2003 21:00:37 -0700 (PDT)
Message-ID: <[email protected]>

Hi.

We lost the game. Are you hungry yet?

Joe.
Loading

0 comments on commit 2381e68

Please sign in to comment.