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

View file

@ -0,0 +1,184 @@
/**
* Elektrine Async Queue Plugin
*
* Keeps SMTP transactions fast by enqueueing inbound messages to Redis,
* then acknowledging the SMTP queue hook immediately.
*/
'use strict';
const crypto = require('crypto');
const constants = require('haraka-constants');
const {
config,
domains
} = require('../lib');
const queue_lib = require('../lib/queue-client');
const telemetry = require('../lib/telemetry');
function parse_stream_data(arg1, arg2) {
if (arg2 !== undefined) {
if (arg1 instanceof Error) throw arg1;
return arg2;
}
return arg1;
}
function read_transaction_raw(transaction) {
return new Promise((resolve, reject) => {
const message_stream = transaction && transaction.message_stream;
if (!message_stream) {
return resolve(Buffer.from(''));
}
const on_data = (arg1, arg2) => {
try {
const raw = parse_stream_data(arg1, arg2);
if (Buffer.isBuffer(raw)) return resolve(raw);
if (typeof raw === 'string') return resolve(Buffer.from(raw, 'binary'));
return resolve(Buffer.from(String(raw || ''), 'utf8'));
} catch (err) {
return reject(err);
}
};
try {
if (typeof message_stream.get_data === 'function') {
message_stream.get_data(on_data);
return;
}
if (typeof message_stream.get_data_string === 'function') {
message_stream.get_data_string((raw) => {
if (typeof raw === 'string') {
resolve(Buffer.from(raw, 'binary'));
} else {
resolve(Buffer.from(''));
}
});
return;
}
reject(new Error('transaction.message_stream does not support get_data')); // eslint-disable-line max-len
} catch (err) {
reject(err);
}
});
}
function transaction_spam_notes(transaction) {
const notes = transaction && transaction.notes ? transaction.notes : {};
const spamassassin = notes.spamassassin || null;
if (!spamassassin || typeof spamassassin !== 'object') return null;
return {
score: spamassassin.score !== undefined ? spamassassin.score : null,
required: spamassassin.reqd !== undefined ? spamassassin.reqd : null,
flag: spamassassin.flag !== undefined ? spamassassin.flag : null,
tests: spamassassin.tests || null
};
}
exports.register = function () {
this.load_config();
this.logger = telemetry.create_plugin_logger(this, 'async_queue');
this.queue_client = new queue_lib.QueueClient(this.cfg, this.logger);
domains.init((msg) => this.logger.info('domains_init', { message: msg }))
.then(() => {
this.logger.info('domains_cache_ready');
})
.catch((err) => {
this.logger.warn('domains_cache_init_failed', { message: err.message });
});
this.register_hook('queue', 'enqueue_inbound_message');
};
exports.load_config = function () {
const haraka_cfg = this.config.get('elektrine_queue.ini', {
booleans: ['+main.enabled', '+main.include_headers', '+main.include_body', '+main.include_attachments']
}, () => {
this.load_config();
});
this.cfg = config.load(haraka_cfg);
};
exports.enqueue_inbound_message = function (next, connection) {
const plugin = this;
const transaction = connection.transaction;
if (!plugin.cfg.webhook_enabled) {
plugin.logger.warn('queue_disabled');
return next(constants.DENYSOFT, 'Inbound processing is temporarily disabled');
}
if (!transaction) {
return next(constants.OK, 'No transaction to enqueue');
}
const rcpt_to = transaction.rcpt_to.map((rcpt) => rcpt.address());
const has_local_recipient = transaction.rcpt_to.some((rcpt) => domains.is_local_domain(rcpt.host));
if (!has_local_recipient) {
plugin.logger.warn('non_local_recipient_in_inbound_profile', {
transaction_id: transaction.uuid,
recipients: rcpt_to
});
return next(constants.DENY, 'Inbound role only accepts local recipients');
}
read_transaction_raw(transaction)
.then((raw_buffer) => {
if (raw_buffer.length > plugin.cfg.queue_max_raw_bytes) {
throw new Error(`Message exceeds queue_max_raw_bytes (${raw_buffer.length} > ${plugin.cfg.queue_max_raw_bytes})`); // eslint-disable-line max-len
}
const message_id = transaction.uuid || crypto.randomUUID();
const payload = {
schema_version: 1,
message_id,
enqueued_at: new Date().toISOString(),
mail_from: transaction.mail_from ? transaction.mail_from.address() : '',
rcpt_to,
data_bytes: transaction.data_bytes || raw_buffer.length,
remote: {
ip: connection.remote && connection.remote.ip,
host: connection.remote && connection.remote.host,
info: connection.remote && connection.remote.info
},
hello: connection.hello ? {
host: connection.hello.host,
verb: connection.hello.verb
} : null,
tls: Boolean(connection.tls),
spamassassin: transaction_spam_notes(transaction),
raw_rfc822_base64: raw_buffer.toString('base64')
};
return plugin.queue_client.enqueue(plugin.cfg.queue_name, payload)
.then(() => {
plugin.logger.info('message_enqueued', {
transaction_id: transaction.uuid,
message_id,
rcpt_count: rcpt_to.length,
bytes: payload.data_bytes,
queue: plugin.cfg.queue_name
});
return next(constants.OK, 'Queued for async processing');
});
})
.catch((err) => {
plugin.logger.error('enqueue_failed', {
transaction_id: transaction.uuid,
message: err.message
});
return next(constants.DENYSOFT, 'Temporary queueing failure, please retry');
});
};