From 85cfd0a6e4efb5b46faad72605b156248b9d7f5f Mon Sep 17 00:00:00 2001 From: Alexander Moisseev Date: Wed, 16 Oct 2024 20:14:31 +0300 Subject: [PATCH] Implement MIME decoding for XPC wrapped header --- experiments/scoreColumn.js | 4 +-- manifest.json | 4 +-- scripts/libCommon.js | 69 ++++++++++++++++++++++++++++++++++++-- scripts/libHeader.js | 2 +- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/experiments/scoreColumn.js b/experiments/scoreColumn.js index 2f83f62..f993c9b 100644 --- a/experiments/scoreColumn.js +++ b/experiments/scoreColumn.js @@ -102,7 +102,7 @@ var scoreColumn = class extends ExtensionCommon.ExtensionAPI { // eslint-disable-next-line no-useless-assignment let score = null; if (column === "spamness") { - score = libCommon.getScoreByHdr(hdr, localStorage.header, true); + score = libCommon.getScoreByHdr(hdr, localStorage.header, true, window); } else { const symbols = libHeader.getSymbols(hdr, localStorage.header, true, window); if (!symbols) return ""; @@ -115,7 +115,7 @@ var scoreColumn = class extends ExtensionCommon.ExtensionAPI { // eslint-disable-next-line no-useless-assignment let score = null; if (column === "spamness") { - score = libCommon.getScoreByHdr(hdr, localStorage.header, true); + score = libCommon.getScoreByHdr(hdr, localStorage.header, true, window); if (localStorage["display-columnImageOnlyForPositive"] && score <= 0) return ""; } else { diff --git a/manifest.json b/manifest.json index d6e7a9a..c057a0b 100644 --- a/manifest.json +++ b/manifest.json @@ -4,13 +4,13 @@ "gecko": { "id": "rspamd-spamness@alexander.moisseev", "strict_min_version": "78.0", - "strict_max_version": "130.*" + "strict_max_version": "132.*" } }, "name": "__MSG_extensionName__", "description": "__MSG_extensionDescription__", "default_locale": "en", - "version": "2.4.1", + "version": "2.5.0", "author": "Alexander Moisseev (moiseev@mezonplus.ru)", "homepage_url": "https://github.com/moisseev/rspamd-spamness", "icons": { diff --git a/scripts/libCommon.js b/scripts/libCommon.js index 11b7752..ac71bbf 100644 --- a/scripts/libCommon.js +++ b/scripts/libCommon.js @@ -12,6 +12,11 @@ var libCommon = { ] }; +libCommon.warn = function (msg) { + // eslint-disable-next-line no-console + console.warn(msg); +}; + libCommon.getImageSrc = function (normalized, id) { // eslint-disable-next-line no-useless-assignment let img = null; @@ -38,6 +43,64 @@ libCommon.getUserHeaders = function (header) { : chdrs.split(", "); }; +/** + * Decode a MIME-encoded header. + * Supports 'Q' (quoted-printable) and 'B' (base64) encodings with UTF-8 charset. + * If an unsupported charset or encoding is encountered, the original value is returned. + * + * @param {string} headerValue - The MIME-encoded header value to decode. + * @param {object} [window] - Global window object, used for base64 decoding in Experiments environment. + * @returns {string} - The decoded header value. + */ +libCommon.decodeMimeHeader = function (headerValue, window) { + function decodeQuotedPrintable(str) { + return str + .replace(/_/g, " ") + .replace(/[=]([0-9A-F]{2})/g, (match, hex) => String.fromCharCode(parseInt(hex, 16))); + } + + function decodeMimeWord(mimeWord) { + const regex = /^=\?([^?]+)\?([^?]+)\?(.+?)\?=$/i; + const match = mimeWord.match(regex); + + if (!match) return mimeWord; + + const [, charset, encoding, encodedText] = match; + + // atob is not defined in Experiments environment + const b64Decode = typeof atob === "undefined" ? window.atob : atob; + + let decodedText = null; + + switch (encoding.toUpperCase()) { + case "Q": + decodedText = decodeQuotedPrintable(encodedText); + break; + case "B": + decodedText = b64Decode(encodedText); + break; + default: + libCommon.warn("Invalid MIME encoding (RFC 2047): " + encoding); + } + + if (!decodedText) return mimeWord; + + if (charset.toUpperCase() !== "UTF-8") { + libCommon.warn("Unsupported charset: " + charset); + return decodedText; + } + + // Convert to UTF-8 string + return decodeURIComponent(escape(decodedText)); + } + + return headerValue + // Unfolding + .replace(/[\r\n]+[\s]+/g, "") + // Find and decode encoded words, removing any spaces between them + .replace(/(=\?[^?]+\?[^?]+\?[^?]+\?=)(\s+)?/gi, (_, encodedWord) => decodeMimeWord(encodedWord)); +}; + /** * Get message score from the message headers object. * Returns NaN if a header or message score not found. @@ -45,9 +108,10 @@ libCommon.getUserHeaders = function (header) { * @param {object} hdr - Message headers object. * @param {string} header * @param {boolean} [XPC] - hdr is an XPConnect wrapped object + * @param {object} [window] - Global window object, used for base64 decoding in Experiments environment * @returns {number} - Message score */ -libCommon.getScoreByHdr = function (hdr, header, XPC) { +libCommon.getScoreByHdr = function (hdr, header, XPC, window) { const re = [ // X-Spamd-Result: Rspamd (milter) /: \S+ \[([-\d.]+?) \//, @@ -62,8 +126,9 @@ libCommon.getScoreByHdr = function (hdr, header, XPC) { [...userHeaders, ...libCommon.scoreHeaders].some(function (headerName) { const headerStr = XPC ? hdr.getStringProperty(headerName) : hdr[headerName]; if (!headerStr) return false; + const decodedHeader = XPC ? libCommon.decodeMimeHeader(headerStr, window) : headerStr; re.some(function (regexp) { - const parsed = regexp.exec(headerStr); + const parsed = regexp.exec(decodedHeader); if (parsed) { score = parseFloat(parsed[1]); } diff --git a/scripts/libHeader.js b/scripts/libHeader.js index 1b1cb44..ab908e9 100644 --- a/scripts/libHeader.js +++ b/scripts/libHeader.js @@ -18,7 +18,7 @@ libHeader.getSymbols = function (headers, header, XPC, window) { if (!XPC) return headers[headerName] || null; const headerStr = headers.getStringProperty(headerName); - return headerStr ? [headerStr] : null; + return headerStr ? [libCommon.decodeMimeHeader(headerStr, window)] : null; } function getUserHeaderStr() {