mime decoder

This commit is contained in:
Ryahn 2025-02-23 10:49:08 -06:00
parent 80d8c8d28a
commit 59c7245010
2 changed files with 134 additions and 33 deletions

View File

@ -3,43 +3,57 @@ exports.register = function () {
const MessageService = require("../../services/MessageService"); const MessageService = require("../../services/MessageService");
const path = require('path'); const path = require('path');
const DailyStats = require(path.join(__dirname, '../../../db/models/DailyStats')); const DailyStats = require(path.join(__dirname, '../../../db/models/DailyStats'));
const MimeDecoder = require(path.join(
__dirname,
"../../../utils/mimeDecoder"
));
plugin.store_message = async function (next, connection) { plugin.store_message = async function (next, connection) {
const transaction = connection.transaction; const transaction = connection.transaction;
if (!transaction) return next(); if (!transaction) return next();
try { try {
// Get the body content by processing the message stream buffers // Get the body content by processing the message stream buffers
let body = ''; let body = "";
if (transaction.message_stream && transaction.message_stream._queue) { if (transaction.message_stream && transaction.message_stream._queue) {
// Convert the buffer to string // Convert the buffer to string
const fullMessage = Buffer.from(transaction.message_stream._queue[0]).toString('utf8'); const fullMessage = Buffer.from(
transaction.message_stream._queue[0]
).toString("utf8");
// Split on double newline to separate headers and body // Split on double newline to separate headers and body
const parts = fullMessage.split('\r\n\r\n'); const parts = fullMessage.split("\r\n\r\n");
if (parts.length > 1) { if (parts.length > 1) {
// Get everything after the headers // Get everything after the headers
body = parts.slice(1).join('\r\n\r\n').trim(); body = parts.slice(1).join("\r\n\r\n").trim();
}
} }
const rawSubject = transaction.header
? transaction.header.get("subject")
: "";
const decodedSubject = MimeDecoder.decode(rawSubject);
const messageData = {
from: transaction.mail_from.address(),
to: transaction.rcpt_to.map((addr) => addr.address()).join(", "),
subject: decodedSubject,
body: body,
headers: transaction.header ? transaction.header.headers_decoded : {},
};
await MessageService.store(messageData);
await DailyStats.incrementMessageCount();
next();
} catch (error) {
connection.logerror(
plugin,
`Failed to store message: ${error.message}`
);
next();
} }
};
const messageData = {
from: transaction.mail_from.address(),
to: transaction.rcpt_to.map((addr) => addr.address()).join(", "),
subject: transaction.header ? transaction.header.get('subject') : '',
body: body,
headers: transaction.header ? transaction.header.headers_decoded : {},
};
await MessageService.store(messageData);
await DailyStats.incrementMessageCount();
next();
} catch (error) {
connection.logerror(plugin, `Failed to store message: ${error.message}`);
next();
}
};
plugin.register_hook('data_post', 'store_message'); plugin.register_hook('data_post', 'store_message');
}; };

87
src/utils/mimeDecoder.js Normal file
View File

@ -0,0 +1,87 @@
// mimeDecoder.js
/**
* Decodes MIME encoded-word format strings commonly used in email headers
* Handles both Base64 and Quoted-Printable encoding methods
* Format: =?charset?encoding?encoded-text?=
*/
class MimeDecoder {
/**
* Decodes a complete MIME encoded-word string
* @param {string} str - The MIME encoded string
* @returns {string} - The decoded string
*/
static decode(str) {
if (!str) return "";
// Remove any newlines that might be present
str = str.replace(/\n/g, "");
// Regular expression to match MIME encoded-word format
const mimeRegex = /=\?([^?]+)\?([BQbq])\?([^?]*)\?=/g;
return str.replace(mimeRegex, (match, charset, encoding, text) => {
try {
if (encoding.toUpperCase() === "B") {
// Handle Base64 encoding
const buffer = Buffer.from(text, "base64");
return buffer.toString(this.normalizeCharset(charset));
} else if (encoding.toUpperCase() === "Q") {
// Handle Quoted-Printable encoding
return this.decodeQuotedPrintable(text, charset);
}
return text;
} catch (error) {
console.error("Error decoding MIME string:", error);
return match; // Return original string if decoding fails
}
});
}
/**
* Normalizes charset names to Node.js compatible charset encodings
* @param {string} charset - The charset name from the MIME string
* @returns {string} - Normalized charset name
*/
static normalizeCharset(charset) {
const charsetMap = {
utf8: "utf8",
"utf-8": "utf8",
"iso-8859-1": "latin1",
"iso8859-1": "latin1",
"windows-1252": "latin1",
};
return charsetMap[charset.toLowerCase()] || charset;
}
/**
* Decodes Quoted-Printable encoded text
* @param {string} text - The Quoted-Printable encoded text
* @param {string} charset - The character set of the encoded text
* @returns {string} - Decoded text
*/
static decodeQuotedPrintable(text, charset) {
// Replace underscore with space (per QP spec)
text = text.replace(/_/g, " ");
// Replace hex encoded characters
text = text.replace(/=([0-9A-F]{2})/gi, (match, hex) => {
try {
return Buffer.from(hex, "hex").toString(this.normalizeCharset(charset));
} catch (error) {
return match;
}
});
return text;
}
}
// Example usage:
/*
const decodedSubject = MimeDecoder.decode('=?utf-8?B?TUVHQS1FbWFpbGJlc3TDpHRpZ3VuZyBlcmZvcmRlcmxpY2g=?=');
console.log(decodedSubject); // Outputs: MEGA-Emailbestätigung erforderlich
*/
module.exports = MimeDecoder;