mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-02-28 18:04:41 +08:00
initial plugin system
This commit is contained in:
parent
2c6a87eb7c
commit
60daa09f67
7 changed files with 519 additions and 343 deletions
|
@ -1,10 +1,10 @@
|
|||
[example]
|
||||
|
||||
name="Example Plugin"
|
||||
enabled = true
|
||||
|
||||
# $WD: path of wildduck module root
|
||||
# $CONFIG: path of config root
|
||||
path="$WD/plugins/example.js"
|
||||
path = "$WD/plugins/example.js"
|
||||
|
||||
# Additional config options
|
||||
value1 = "Example config option"
|
||||
|
|
|
@ -5,6 +5,7 @@ const ObjectID = require('mongodb').ObjectID;
|
|||
const db = require('./db');
|
||||
const forward = require('./forward');
|
||||
const autoreply = require('./autoreply');
|
||||
const plugins = require('./plugins');
|
||||
|
||||
const defaultSpamHeaderKeys = [
|
||||
{
|
||||
|
@ -206,8 +207,9 @@ class FilterHandler {
|
|||
}))
|
||||
);
|
||||
|
||||
let forwardTargets = new Set();
|
||||
let forwardTargetUrls = new Set();
|
||||
let forwardTargets = new Map();
|
||||
|
||||
plugins.runHooks('filter', filters, forwardTargets, () => {
|
||||
let matchingFilters = [];
|
||||
let filterActions = new Map();
|
||||
|
||||
|
@ -226,12 +228,16 @@ class FilterHandler {
|
|||
} else {
|
||||
Object.keys(filter.action).forEach(key => {
|
||||
if (key === 'forward') {
|
||||
forwardTargets.add(filter.action[key]);
|
||||
[].concat(filter.action[key] || []).forEach(address => {
|
||||
forwardTargets.set(address, { type: 'mail', value: address });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (key === 'targetUrl') {
|
||||
forwardTargetUrls.add(filter.action[key]);
|
||||
[].concat(filter.action[key] || []).forEach(address => {
|
||||
forwardTargets.set(address, { type: 'http', value: address });
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -246,23 +252,23 @@ class FilterHandler {
|
|||
let forwardMessage = done => {
|
||||
if (userData.forward && !filterActions.get('delete')) {
|
||||
// forward to default recipient only if the message is not deleted
|
||||
forwardTargets.add(userData.forward);
|
||||
forwardTargets.set(userData.forward, { type: 'mail', value: userData.forward });
|
||||
}
|
||||
|
||||
if (userData.targetUrl && !filterActions.get('delete')) {
|
||||
// forward to default URL only if the message is not deleted
|
||||
forwardTargetUrls.add(userData.targetUrl);
|
||||
forwardTargets.set(userData.targetUrl, { type: 'http', value: userData.targetUrl });
|
||||
}
|
||||
|
||||
// never forward messages marked as spam
|
||||
if ((!forwardTargets.size && !forwardTargetUrls.size) || filterActions.get('spam')) {
|
||||
if (!forwardTargets.size || filterActions.get('spam')) {
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
// check limiting counters
|
||||
this.messageHandler.counters.ttlcounter(
|
||||
'wdf:' + userData._id.toString(),
|
||||
forwardTargets.size + forwardTargetUrls.size,
|
||||
forwardTargets.size,
|
||||
userData.forwards,
|
||||
false,
|
||||
(err, result) => {
|
||||
|
@ -281,8 +287,9 @@ class FilterHandler {
|
|||
sender,
|
||||
recipient,
|
||||
|
||||
forward: forwardTargets.size ? Array.from(forwardTargets) : false,
|
||||
targetUrl: forwardTargetUrls.size ? Array.from(forwardTargetUrls) : false,
|
||||
targets: forwardTargets.size
|
||||
? Array.from(forwardTargets).map(row => ({ type: row[1].type, value: row[1].value }))
|
||||
: false,
|
||||
|
||||
chunks,
|
||||
chunklen
|
||||
|
@ -324,7 +331,7 @@ class FilterHandler {
|
|||
sender,
|
||||
recipient,
|
||||
Array.from(forwardTargets)
|
||||
.concat(forwardTargetUrls)
|
||||
.map(row => row[0])
|
||||
.join(','),
|
||||
err.message
|
||||
);
|
||||
|
@ -338,7 +345,7 @@ class FilterHandler {
|
|||
sender,
|
||||
recipient,
|
||||
Array.from(forwardTargets)
|
||||
.concat(Array.from(forwardTargetUrls))
|
||||
.map(row => row[0])
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
@ -468,6 +475,7 @@ class FilterHandler {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,21 +9,19 @@ module.exports = (options, callback) => {
|
|||
return callback(null, false);
|
||||
}
|
||||
|
||||
let message = maildrop(
|
||||
{
|
||||
let mail = {
|
||||
parentId: options.parentId,
|
||||
reason: 'forward',
|
||||
|
||||
from: options.sender,
|
||||
to: options.recipient,
|
||||
|
||||
forward: options.forward,
|
||||
http: !!options.targetUrl,
|
||||
targeUrl: options.targetUrl,
|
||||
targets: options.targets,
|
||||
|
||||
interface: 'forwarder'
|
||||
},
|
||||
(err, ...args) => {
|
||||
};
|
||||
|
||||
let message = maildrop(mail, (err, ...args) => {
|
||||
if (err || !args[0]) {
|
||||
return callback(err, ...args);
|
||||
}
|
||||
|
@ -34,13 +32,10 @@ module.exports = (options, callback) => {
|
|||
parentId: options.parentId,
|
||||
from: options.sender,
|
||||
to: options.recipient,
|
||||
forward: options.forward,
|
||||
http: !!options.targetUrl,
|
||||
targeUrl: options.targetUrl,
|
||||
targets: options.targets,
|
||||
created: new Date()
|
||||
}, () => callback(err, args && args[0] && args[0].id));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
setImmediate(() => {
|
||||
let pos = 0;
|
||||
|
|
|
@ -12,6 +12,7 @@ const os = require('os');
|
|||
const hostname = os.hostname().toLowerCase();
|
||||
const addressparser = require('addressparser');
|
||||
const punycode = require('punycode');
|
||||
const plugins = require('./plugins');
|
||||
|
||||
let gridstore;
|
||||
|
||||
|
@ -157,20 +158,32 @@ module.exports = (options, callback) => {
|
|||
|
||||
let deliveries = [];
|
||||
|
||||
if (options.targeUrl) {
|
||||
let targetUrls = [].concat(options.targeUrl || []).map(targetUrl => ({
|
||||
if (options.targets) {
|
||||
options.targets.forEach(target => {
|
||||
switch (target.type) {
|
||||
case 'mail':
|
||||
deliveries.push({
|
||||
to: target.value
|
||||
});
|
||||
break;
|
||||
case 'relay':
|
||||
deliveries.push({
|
||||
to: options.to,
|
||||
mx: target.value.mx,
|
||||
mxPort: target.value.mxPort,
|
||||
mxAuth: target.value.mxAuth,
|
||||
mxSecure: target.value.mxSecure
|
||||
});
|
||||
break;
|
||||
case 'http':
|
||||
deliveries.push({
|
||||
to: options.to,
|
||||
http: true,
|
||||
targetUrl
|
||||
}));
|
||||
deliveries = deliveries.concat(targetUrls);
|
||||
targetUrl: target.value
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (options.forward) {
|
||||
let forwards = [].concat(options.forward || []).map(forward => ({
|
||||
to: forward
|
||||
}));
|
||||
deliveries = deliveries.concat(forwards);
|
||||
});
|
||||
}
|
||||
|
||||
if (!deliveries.length) {
|
||||
|
@ -204,6 +217,7 @@ module.exports = (options, callback) => {
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.runHooks('maildrop', options, envelope, deliveries, () => {
|
||||
envelope.headers = envelope.headers.getList();
|
||||
setMeta(id, envelope, err => {
|
||||
if (err) {
|
||||
|
@ -231,8 +245,6 @@ module.exports = (options, callback) => {
|
|||
|
||||
// actual recipient address
|
||||
recipient: recipient.to,
|
||||
http: recipient.http,
|
||||
targetUrl: recipient.targetUrl,
|
||||
|
||||
locked: false,
|
||||
lockTime: 0,
|
||||
|
@ -244,6 +256,17 @@ module.exports = (options, callback) => {
|
|||
created: date
|
||||
};
|
||||
|
||||
if (recipient.http) {
|
||||
delivery.http = recipient.http;
|
||||
delivery.targetUrl = recipient.targetUrl;
|
||||
}
|
||||
|
||||
['mx', 'mxPort', 'mxAuth', 'mxSecure'].forEach(key => {
|
||||
if (recipient[key]) {
|
||||
delivery[key] = recipient[key];
|
||||
}
|
||||
});
|
||||
|
||||
documents.push(delivery);
|
||||
}
|
||||
|
||||
|
@ -259,7 +282,7 @@ module.exports = (options, callback) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
messageSplitter.pipe(dkimStream);
|
||||
return messageSplitter;
|
||||
};
|
||||
|
|
140
lib/plugins.js
140
lib/plugins.js
|
@ -2,11 +2,143 @@
|
|||
|
||||
const config = require('wild-config');
|
||||
const pathlib = require('path');
|
||||
const log = require('npmlog');
|
||||
const db = require('./db');
|
||||
|
||||
const WD_PATH = pathlib.join(__dirname, '..');
|
||||
const CONFIG_PATH = pathlib.join(__dirname, '..');
|
||||
const CONFIG_PATH = config.configDirectory || WD_PATH;
|
||||
|
||||
module.exports = next => {
|
||||
console.log(config);
|
||||
setImmediate(next);
|
||||
const hooks = new Map();
|
||||
|
||||
class PluginInstance {
|
||||
constructor(key, config) {
|
||||
this.db = db;
|
||||
|
||||
this.key = key;
|
||||
this.config = config || {};
|
||||
|
||||
this.logger = {};
|
||||
['silly', 'verbose', 'info', 'http', 'warn', 'error', 'debug', 'err'].forEach(level => {
|
||||
this.logger[level] = (...args) => {
|
||||
switch (level) {
|
||||
case 'debug':
|
||||
level = 'verbose';
|
||||
break;
|
||||
case 'err':
|
||||
level = 'error';
|
||||
break;
|
||||
}
|
||||
log[level]('[' + key + ']', ...args);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
addHook(hook, handler) {
|
||||
hook = (hook || '')
|
||||
.toString()
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase();
|
||||
if (!hook) {
|
||||
return;
|
||||
}
|
||||
if (!hooks.has(hook)) {
|
||||
hooks.set(hook, []);
|
||||
}
|
||||
hooks.get(hook).push({ plugin: this, handler });
|
||||
}
|
||||
|
||||
init(done) {
|
||||
if (!this.config.path) {
|
||||
this.logger.debug('Plugin path not provided, skipping');
|
||||
return setImmediate(done);
|
||||
}
|
||||
try {
|
||||
let pluginPath = this.config.path.replace(/\$WD/g, WD_PATH).replace(/\$CONFIG/g, CONFIG_PATH);
|
||||
this.module = require(pluginPath); //eslint-disable-line global-require
|
||||
} catch (E) {
|
||||
this.logger.error('Failed to load plugin. %s', E.message);
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
if (typeof this.module.init !== 'function') {
|
||||
this.logger.debug('Init method not found');
|
||||
return setImmediate(done);
|
||||
}
|
||||
|
||||
try {
|
||||
return this.module.init(this, err => {
|
||||
if (err) {
|
||||
this.logger.error('Initialization resulted with an error. %s', err.message);
|
||||
} else {
|
||||
this.logger.debug('Plugin "%s" initialized', this.module.title || this.key);
|
||||
}
|
||||
return setImmediate(done);
|
||||
});
|
||||
} catch (E) {
|
||||
this.logger.error('Failed executing init method. %s', E.message);
|
||||
return setImmediate(done);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.init = next => {
|
||||
let keys = Object.keys(config.plugins || {});
|
||||
|
||||
let pos = 0;
|
||||
let loadNextPlugin = () => {
|
||||
if (pos >= keys.length) {
|
||||
return setImmediate(next);
|
||||
}
|
||||
let key = keys[pos++];
|
||||
if (!config.plugins[key].enabled) {
|
||||
return setImmediate(loadNextPlugin);
|
||||
}
|
||||
let plugin = new PluginInstance(key, config.plugins[key]);
|
||||
plugin.init(loadNextPlugin);
|
||||
};
|
||||
setImmediate(loadNextPlugin);
|
||||
};
|
||||
|
||||
module.exports.runHooks = (hook, ...args) => {
|
||||
let next = args.pop();
|
||||
|
||||
hook = (hook || '')
|
||||
.toString()
|
||||
.replace(/\s+/g, '')
|
||||
.toLowerCase();
|
||||
|
||||
if (!hook || !hooks.has(hook)) {
|
||||
return setImmediate(next);
|
||||
}
|
||||
|
||||
let handlers = hooks.get(hook);
|
||||
let pos = 0;
|
||||
let processHandler = () => {
|
||||
if (pos >= handlers.length) {
|
||||
return setImmediate(next);
|
||||
}
|
||||
let entry = handlers[pos++];
|
||||
let returned = false;
|
||||
try {
|
||||
entry.handler(...args, err => {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
|
||||
if (err) {
|
||||
entry.plugin.logger.error('Failed processing hook %s. %s', hook, err.message);
|
||||
}
|
||||
setImmediate(processHandler);
|
||||
});
|
||||
} catch (E) {
|
||||
if (returned) {
|
||||
return;
|
||||
}
|
||||
returned = true;
|
||||
entry.plugin.logger.error('Failed processing hook %s. %s', hook, E.message);
|
||||
setImmediate(processHandler);
|
||||
}
|
||||
};
|
||||
setImmediate(processHandler);
|
||||
};
|
||||
|
|
15
plugins/example.js
Normal file
15
plugins/example.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
module.exports.title = 'Example Plugin';
|
||||
|
||||
module.exports.init = (app, done) => {
|
||||
// do your initialization stuff here
|
||||
|
||||
// init hook is called immediatelly after server is started
|
||||
app.addHook('init', next => {
|
||||
app.logger.info('Example plugin initialized. Value1=%s', app.config.value1);
|
||||
next();
|
||||
});
|
||||
|
||||
setImmediate(done);
|
||||
};
|
|
@ -81,12 +81,14 @@ db.connect(err => {
|
|||
}
|
||||
}
|
||||
|
||||
plugins(err => {
|
||||
plugins.init(err => {
|
||||
if (err) {
|
||||
log.error('App', 'Failed to start plugins');
|
||||
errors.notify(err);
|
||||
return setTimeout(() => process.exit(1), 3000);
|
||||
}
|
||||
|
||||
plugins.runHooks('init', () => {
|
||||
log.info('App', 'All servers started, ready to process some mail');
|
||||
});
|
||||
});
|
||||
|
@ -94,4 +96,5 @@ db.connect(err => {
|
|||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue