Fresh repository history for elektrine/elektrine-haraka hosted at https://git.elektrine.com/elektrine/elektrine-haraka.
170 lines
5.5 KiB
JavaScript
170 lines
5.5 KiB
JavaScript
/**
|
|
* Spam Extractor Module
|
|
*
|
|
* Extracts spam information from Haraka connection/transaction notes
|
|
* and email headers. Supports SpamAssassin results.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
/**
|
|
* Default spam info structure
|
|
* @returns {Object} Default spam info object
|
|
*/
|
|
function get_default_spam_info() {
|
|
return {
|
|
status: 'unknown',
|
|
score: 0.0,
|
|
threshold: 5.0,
|
|
report: null,
|
|
status_header: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Extract spam information from multiple sources
|
|
* Checks transaction notes, connection notes, and headers in order of priority.
|
|
*
|
|
* @param {Object} connection - Haraka connection object
|
|
* @param {Object} transaction - Haraka transaction object
|
|
* @param {Object} headers - Parsed email headers (from mailparser)
|
|
* @param {Object} [options] - Optional settings
|
|
* @param {boolean} [options.debug=false] - Enable debug logging
|
|
* @param {Function} [options.logger] - Logger function
|
|
* @returns {Object} Spam info object
|
|
*/
|
|
function extract(connection, transaction, headers, options = {}) {
|
|
const { debug = false, logger = console.log } = options;
|
|
|
|
if (debug) logger('Starting spam extraction');
|
|
|
|
const spam_info = get_default_spam_info();
|
|
|
|
// Method 1: Check transaction notes (highest priority)
|
|
if (transaction && transaction.notes && transaction.notes.spamassassin) {
|
|
if (debug) logger('Found spamassassin in transaction.notes');
|
|
return parse_spamassassin_notes(transaction.notes.spamassassin, spam_info, debug, logger);
|
|
}
|
|
|
|
// Method 2: Check connection notes
|
|
if (connection && connection.notes && connection.notes.spamassassin) {
|
|
if (debug) logger('Found spamassassin in connection.notes');
|
|
return parse_spamassassin_notes(connection.notes.spamassassin, spam_info, debug, logger);
|
|
}
|
|
|
|
// Method 3: Check email headers (fallback)
|
|
if (headers) {
|
|
const header_result = parse_spam_headers(headers, spam_info, debug, logger);
|
|
if (header_result.status !== 'unknown') {
|
|
return header_result;
|
|
}
|
|
}
|
|
|
|
if (debug) logger('No spam info found in any source');
|
|
return spam_info;
|
|
}
|
|
|
|
/**
|
|
* Parse SpamAssassin notes from Haraka
|
|
* @param {Object} sa - SpamAssassin notes object
|
|
* @param {Object} spam_info - Base spam info object
|
|
* @param {boolean} debug - Debug mode
|
|
* @param {Function} logger - Logger function
|
|
* @returns {Object} Updated spam info
|
|
*/
|
|
function parse_spamassassin_notes(sa, spam_info, debug, logger) {
|
|
if (debug) logger(`Parsing SpamAssassin notes: ${JSON.stringify(sa)}`);
|
|
|
|
if (sa.score !== undefined) {
|
|
spam_info.score = parseFloat(sa.score) || 0.0;
|
|
}
|
|
|
|
if (sa.reqd !== undefined) {
|
|
spam_info.threshold = parseFloat(sa.reqd) || 5.0;
|
|
}
|
|
|
|
if (sa.flag !== undefined) {
|
|
spam_info.status = sa.flag === 'Yes' ? 'spam' : 'ham';
|
|
}
|
|
|
|
if (sa.tests) {
|
|
spam_info.report = sa.tests;
|
|
}
|
|
|
|
return spam_info;
|
|
}
|
|
|
|
/**
|
|
* Parse spam information from email headers
|
|
* @param {Object} headers - Email headers (Map or object)
|
|
* @param {Object} spam_info - Base spam info object
|
|
* @param {boolean} debug - Debug mode
|
|
* @param {Function} logger - Logger function
|
|
* @returns {Object} Updated spam info
|
|
*/
|
|
function parse_spam_headers(headers, spam_info, debug, logger) {
|
|
if (debug) logger('Checking headers for spam info');
|
|
|
|
// Get header values (handle both Map and object formats)
|
|
const get_header = (name) => {
|
|
if (headers instanceof Map) {
|
|
return headers.get(name) || headers.get(name.toLowerCase());
|
|
}
|
|
return headers[name] || headers[name.toLowerCase()];
|
|
};
|
|
|
|
const spam_status = get_header('X-Spam-Status') || get_header('x-spam-status');
|
|
const spam_score = get_header('X-Spam-Score') || get_header('x-spam-score');
|
|
const spam_report = get_header('X-Spam-Report') || get_header('x-spam-report');
|
|
|
|
if (spam_status || spam_score) {
|
|
if (debug) logger(`Found spam headers - Status: ${spam_status}, Score: ${spam_score}`);
|
|
|
|
if (spam_status && typeof spam_status === 'string') {
|
|
spam_info.status_header = spam_status;
|
|
|
|
// Parse "Yes, score=5.2, required=5.0" or "No, score=-1.6, required=5.0"
|
|
const score_match = spam_status.match(/score=([-\d.]+)/);
|
|
const req_match = spam_status.match(/required=([-\d.]+)/);
|
|
|
|
if (score_match) {
|
|
spam_info.score = parseFloat(score_match[1]) || 0.0;
|
|
}
|
|
if (req_match) {
|
|
spam_info.threshold = parseFloat(req_match[1]) || 5.0;
|
|
}
|
|
|
|
spam_info.status = spam_status.startsWith('Yes') ? 'spam' : 'ham';
|
|
}
|
|
|
|
// Override score if separate header exists
|
|
if (spam_score && typeof spam_score === 'string') {
|
|
spam_info.score = parseFloat(spam_score) || spam_info.score;
|
|
}
|
|
|
|
// Add report if available
|
|
if (spam_report && typeof spam_report === 'string') {
|
|
spam_info.report = spam_report;
|
|
}
|
|
}
|
|
|
|
return spam_info;
|
|
}
|
|
|
|
/**
|
|
* Determine if an email should be marked as spam based on score
|
|
* @param {number} score - Spam score
|
|
* @param {number} [threshold=5.0] - Spam threshold
|
|
* @returns {boolean} True if spam
|
|
*/
|
|
function is_spam(score, threshold = 5.0) {
|
|
return score >= threshold;
|
|
}
|
|
|
|
module.exports = {
|
|
extract,
|
|
get_default_spam_info,
|
|
parse_spamassassin_notes,
|
|
parse_spam_headers,
|
|
is_spam
|
|
};
|