Initial commit on Forgejo
Some checks are pending
CI and deploy / Test (push) Waiting to run
CI and deploy / Build, push, and deploy (push) Blocked by required conditions

Fresh repository history for elektrine/elektrine-haraka hosted at
https://git.elektrine.com/elektrine/elektrine-haraka.
This commit is contained in:
Maxfield Luke 2026-07-29 04:43:42 -04:00
commit 67c1d41a0c
82 changed files with 8713 additions and 0 deletions

170
lib/spam-extractor.js Normal file
View file

@ -0,0 +1,170 @@
/**
* 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
};