Skip to content

Commit

Permalink
Implement MIME decoding for XPC wrapped header
Browse files Browse the repository at this point in the history
  • Loading branch information
moisseev committed Oct 16, 2024
1 parent c4021ec commit 85cfd0a
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 7 deletions.
4 changes: 2 additions & 2 deletions experiments/scoreColumn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
Expand All @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
69 changes: 67 additions & 2 deletions scripts/libCommon.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,16 +43,75 @@ 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.
*
* @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.]+?) \//,
Expand All @@ -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]);
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/libHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 85cfd0a

Please sign in to comment.