Initial commit on Forgejo
Fresh repository history for elektrine/elektrine-haraka hosted at https://git.elektrine.com/elektrine/elektrine-haraka.
This commit is contained in:
commit
67c1d41a0c
82 changed files with 8713 additions and 0 deletions
184
plugins/elektrine_async_queue.js
Normal file
184
plugins/elektrine_async_queue.js
Normal 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');
|
||||
});
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue