Fresh repository history for elektrine/elektrine-haraka hosted at https://git.elektrine.com/elektrine/elektrine-haraka.
184 lines
6.3 KiB
JavaScript
184 lines
6.3 KiB
JavaScript
/**
|
|
* 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');
|
|
});
|
|
};
|