2017-03-07 00:27:04 +08:00
|
|
|
'use strict';
|
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
// Simple LMTP server that accepts all messages for valid recipients
|
|
|
|
|
2017-07-16 19:37:33 +08:00
|
|
|
const config = require('wild-config');
|
2017-03-07 00:27:04 +08:00
|
|
|
const log = require('npmlog');
|
2021-08-30 16:18:42 +08:00
|
|
|
const ObjectId = require('mongodb').ObjectId;
|
2017-03-07 00:27:04 +08:00
|
|
|
const SMTPServer = require('smtp-server').SMTPServer;
|
2017-03-21 06:07:23 +08:00
|
|
|
const tools = require('./lib/tools');
|
|
|
|
const MessageHandler = require('./lib/message-handler');
|
2017-12-01 21:04:32 +08:00
|
|
|
const UserHandler = require('./lib/user-handler');
|
2017-10-18 17:42:51 +08:00
|
|
|
const FilterHandler = require('./lib/filter-handler');
|
2017-03-27 15:36:45 +08:00
|
|
|
const db = require('./lib/db');
|
2017-10-05 20:14:43 +08:00
|
|
|
const certs = require('./lib/certs');
|
2018-10-18 15:37:32 +08:00
|
|
|
const Gelf = require('gelf');
|
|
|
|
const os = require('os');
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-03-21 06:07:23 +08:00
|
|
|
let messageHandler;
|
2017-12-01 21:04:32 +08:00
|
|
|
let userHandler;
|
2017-10-18 17:42:51 +08:00
|
|
|
let filterHandler;
|
2018-10-18 15:37:32 +08:00
|
|
|
let loggelf;
|
2017-09-04 18:19:52 +08:00
|
|
|
|
|
|
|
config.on('reload', () => {
|
|
|
|
log.info('LMTP', 'Configuration reloaded');
|
|
|
|
});
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-04-12 19:52:29 +08:00
|
|
|
const serverOptions = {
|
|
|
|
lmtp: true,
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-12-01 16:02:40 +08:00
|
|
|
secure: config.lmtp.secure,
|
|
|
|
secured: config.lmtp.secured,
|
|
|
|
|
2017-03-07 00:27:04 +08:00
|
|
|
// log to console
|
|
|
|
logger: {
|
|
|
|
info(...args) {
|
|
|
|
args.shift();
|
2017-04-12 19:52:29 +08:00
|
|
|
log.info('LMTP', ...args);
|
2017-03-07 00:27:04 +08:00
|
|
|
},
|
|
|
|
debug(...args) {
|
|
|
|
args.shift();
|
2017-04-12 19:52:29 +08:00
|
|
|
log.silly('LMTP', ...args);
|
2017-03-07 00:27:04 +08:00
|
|
|
},
|
|
|
|
error(...args) {
|
|
|
|
args.shift();
|
2017-04-12 19:52:29 +08:00
|
|
|
log.error('LMTP', ...args);
|
2017-03-07 00:27:04 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-09-11 03:53:12 +08:00
|
|
|
name: config.lmtp.name || false,
|
2017-03-07 00:27:04 +08:00
|
|
|
|
|
|
|
// not required but nice-to-have
|
2018-01-02 21:04:01 +08:00
|
|
|
banner: config.lmtp.banner || 'Welcome to WildDuck Mail Server',
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-09-11 03:53:12 +08:00
|
|
|
disabledCommands: ['AUTH'].concat(config.lmtp.disableSTARTTLS ? 'STARTTLS' : []),
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-03-30 01:06:09 +08:00
|
|
|
onMailFrom(address, session, callback) {
|
|
|
|
// reset session entries
|
2017-04-12 19:52:29 +08:00
|
|
|
session.users = [];
|
2017-03-30 01:06:09 +08:00
|
|
|
|
2017-04-12 19:52:29 +08:00
|
|
|
// accept alls sender addresses
|
2017-03-30 01:06:09 +08:00
|
|
|
return callback();
|
|
|
|
},
|
|
|
|
|
2017-03-07 00:27:04 +08:00
|
|
|
// Validate RCPT TO envelope address. Example allows all addresses that do not start with 'deny'
|
|
|
|
// If this method is not set, all addresses are allowed
|
2017-03-22 16:30:10 +08:00
|
|
|
onRcptTo(rcpt, session, callback) {
|
|
|
|
let originalRecipient = tools.normalizeAddress(rcpt.address);
|
2017-12-01 21:04:32 +08:00
|
|
|
userHandler.get(
|
|
|
|
originalRecipient,
|
|
|
|
{
|
|
|
|
name: true,
|
|
|
|
forwards: true,
|
2018-01-21 03:38:56 +08:00
|
|
|
targets: true,
|
2017-12-01 21:04:32 +08:00
|
|
|
autoreply: true,
|
|
|
|
encryptMessages: true,
|
|
|
|
encryptForwarded: true,
|
2018-01-30 22:14:15 +08:00
|
|
|
pubKey: true,
|
|
|
|
spamLevel: true
|
2017-12-01 21:04:32 +08:00
|
|
|
},
|
|
|
|
(err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
log.error('LMTP', err);
|
|
|
|
return callback(new Error('Database error'));
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
return callback(new Error('Unknown recipient'));
|
2017-11-28 19:57:38 +08:00
|
|
|
}
|
|
|
|
|
2017-12-01 21:04:32 +08:00
|
|
|
if (!session.users) {
|
|
|
|
session.users = [];
|
|
|
|
}
|
2017-11-28 19:57:38 +08:00
|
|
|
|
2017-12-01 21:04:32 +08:00
|
|
|
session.users.push({
|
|
|
|
recipient: originalRecipient,
|
|
|
|
user: userData
|
|
|
|
});
|
2017-11-28 19:57:38 +08:00
|
|
|
|
2017-12-01 21:04:32 +08:00
|
|
|
callback();
|
|
|
|
}
|
|
|
|
);
|
2017-03-07 00:27:04 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
// Handle message stream
|
|
|
|
onData(stream, session, callback) {
|
|
|
|
let chunks = [];
|
|
|
|
let chunklen = 0;
|
2017-03-27 15:36:45 +08:00
|
|
|
|
2017-04-13 16:35:39 +08:00
|
|
|
stream.on('readable', () => {
|
2017-03-07 00:27:04 +08:00
|
|
|
let chunk;
|
2017-04-13 16:35:39 +08:00
|
|
|
while ((chunk = stream.read()) !== null) {
|
2017-04-12 19:52:29 +08:00
|
|
|
chunks.push(chunk);
|
|
|
|
chunklen += chunk.length;
|
2017-03-07 00:27:04 +08:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
stream.once('error', err => {
|
2017-04-12 19:52:29 +08:00
|
|
|
log.error('LMTP', err);
|
2017-03-07 00:27:04 +08:00
|
|
|
callback(new Error('Error reading from stream'));
|
|
|
|
});
|
|
|
|
|
2017-04-13 16:35:39 +08:00
|
|
|
stream.once('end', () => {
|
2017-06-03 14:51:58 +08:00
|
|
|
let sender = tools.normalizeAddress((session.envelope.mailFrom && session.envelope.mailFrom.address) || '');
|
2017-04-12 19:52:29 +08:00
|
|
|
let responses = [];
|
|
|
|
let users = session.users;
|
2017-03-07 00:27:04 +08:00
|
|
|
let stored = 0;
|
2017-04-13 16:35:39 +08:00
|
|
|
|
2021-08-30 16:18:42 +08:00
|
|
|
let transactionId = new ObjectId();
|
2017-10-18 21:32:01 +08:00
|
|
|
let prepared = false;
|
|
|
|
|
2017-03-07 00:27:04 +08:00
|
|
|
let storeNext = () => {
|
|
|
|
if (stored >= users.length) {
|
2020-03-15 19:25:17 +08:00
|
|
|
return callback(
|
|
|
|
null,
|
|
|
|
responses.map(r => r.response)
|
|
|
|
);
|
2017-03-07 00:27:04 +08:00
|
|
|
}
|
|
|
|
|
2017-04-12 19:52:29 +08:00
|
|
|
let rcptData = users[stored++];
|
|
|
|
let recipient = rcptData.recipient;
|
2017-09-16 00:11:42 +08:00
|
|
|
let userData = rcptData.user;
|
2017-04-12 19:52:29 +08:00
|
|
|
|
2017-09-16 00:11:42 +08:00
|
|
|
let response = responses.filter(r => r.userData === userData);
|
2017-04-12 19:52:29 +08:00
|
|
|
if (response.length) {
|
|
|
|
responses.push(response[0]);
|
|
|
|
return storeNext();
|
|
|
|
}
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-10-18 17:42:51 +08:00
|
|
|
filterHandler.process(
|
|
|
|
{
|
2017-10-18 21:32:01 +08:00
|
|
|
mimeTree: prepared && prepared.mimeTree,
|
|
|
|
maildata: prepared && prepared.maildata,
|
2017-10-18 17:42:51 +08:00
|
|
|
user: userData,
|
|
|
|
sender,
|
|
|
|
recipient,
|
|
|
|
chunks,
|
|
|
|
chunklen,
|
|
|
|
meta: {
|
|
|
|
transactionId,
|
2017-10-20 19:39:20 +08:00
|
|
|
source: 'MX',
|
2017-10-18 17:42:51 +08:00
|
|
|
from: sender,
|
2017-10-20 18:43:44 +08:00
|
|
|
to: [recipient],
|
2017-10-18 17:42:51 +08:00
|
|
|
origin: session.remoteAddress,
|
|
|
|
originhost: session.clientHostname,
|
|
|
|
transhost: session.hostNameAppearsAs,
|
|
|
|
transtype: session.transmissionType,
|
2017-10-20 18:43:44 +08:00
|
|
|
time: new Date()
|
2017-10-18 17:42:51 +08:00
|
|
|
}
|
|
|
|
},
|
2017-10-18 21:32:01 +08:00
|
|
|
(err, response, preparedResponse) => {
|
2017-09-04 18:19:52 +08:00
|
|
|
if (err) {
|
2017-10-18 17:42:51 +08:00
|
|
|
// ???
|
2017-09-04 18:19:52 +08:00
|
|
|
}
|
2017-10-18 21:32:01 +08:00
|
|
|
|
2017-10-18 17:42:51 +08:00
|
|
|
if (response) {
|
|
|
|
responses.push(response);
|
|
|
|
}
|
2017-10-18 21:32:01 +08:00
|
|
|
|
|
|
|
if (!prepared && preparedResponse) {
|
|
|
|
prepared = preparedResponse;
|
|
|
|
}
|
|
|
|
|
2017-10-18 17:42:51 +08:00
|
|
|
setImmediate(storeNext);
|
|
|
|
}
|
|
|
|
);
|
2017-03-07 00:27:04 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
storeNext();
|
|
|
|
});
|
|
|
|
}
|
2017-04-12 19:52:29 +08:00
|
|
|
};
|
|
|
|
|
2017-10-05 20:14:43 +08:00
|
|
|
certs.loadTLSOptions(serverOptions, 'lmtp');
|
2017-04-12 19:52:29 +08:00
|
|
|
|
|
|
|
const server = new SMTPServer(serverOptions);
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-10-05 20:14:43 +08:00
|
|
|
certs.registerReload(server, 'lmtp');
|
|
|
|
|
2017-03-21 06:07:23 +08:00
|
|
|
module.exports = done => {
|
2017-04-12 19:52:29 +08:00
|
|
|
if (!config.lmtp.enabled) {
|
2017-03-07 00:27:04 +08:00
|
|
|
return setImmediate(() => done(null, false));
|
|
|
|
}
|
|
|
|
|
2018-10-18 15:37:32 +08:00
|
|
|
const component = config.log.gelf.component || 'wildduck';
|
|
|
|
const hostname = config.log.gelf.hostname || os.hostname();
|
|
|
|
const gelf =
|
|
|
|
config.log.gelf && config.log.gelf.enabled
|
|
|
|
? new Gelf(config.log.gelf.options)
|
|
|
|
: {
|
|
|
|
// placeholder
|
2020-03-15 19:25:17 +08:00
|
|
|
emit: (key, message) => log.info('Gelf', JSON.stringify(message))
|
2018-10-18 15:37:32 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
loggelf = message => {
|
|
|
|
if (typeof message === 'string') {
|
|
|
|
message = {
|
|
|
|
short_message: message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
message = message || {};
|
2018-10-18 16:53:14 +08:00
|
|
|
|
|
|
|
if (!message.short_message || message.short_message.indexOf(component.toUpperCase()) !== 0) {
|
|
|
|
message.short_message = component.toUpperCase() + ' ' + (message.short_message || '');
|
|
|
|
}
|
|
|
|
|
2018-10-18 15:37:32 +08:00
|
|
|
message.facility = component; // facility is deprecated but set by the driver if not provided
|
|
|
|
message.host = hostname;
|
|
|
|
message.timestamp = Date.now() / 1000;
|
|
|
|
message._component = component;
|
|
|
|
Object.keys(message).forEach(key => {
|
|
|
|
if (!message[key]) {
|
|
|
|
delete message[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
gelf.emit('gelf.log', message);
|
|
|
|
};
|
|
|
|
|
2017-08-07 02:25:10 +08:00
|
|
|
messageHandler = new MessageHandler({
|
|
|
|
database: db.database,
|
|
|
|
users: db.users,
|
2017-08-07 16:29:29 +08:00
|
|
|
redis: db.redis,
|
|
|
|
gridfs: db.gridfs,
|
2018-10-18 15:37:32 +08:00
|
|
|
attachments: config.attachments,
|
|
|
|
loggelf: message => loggelf(message)
|
2017-08-07 02:25:10 +08:00
|
|
|
});
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-12-01 21:04:32 +08:00
|
|
|
userHandler = new UserHandler({
|
|
|
|
database: db.database,
|
|
|
|
users: db.users,
|
|
|
|
redis: db.redis,
|
2018-10-18 15:37:32 +08:00
|
|
|
loggelf: message => loggelf(message)
|
2017-12-01 21:04:32 +08:00
|
|
|
});
|
|
|
|
|
2017-10-18 17:42:51 +08:00
|
|
|
filterHandler = new FilterHandler({
|
2017-12-21 20:24:28 +08:00
|
|
|
db,
|
|
|
|
sender: config.sender,
|
2017-10-18 17:42:51 +08:00
|
|
|
messageHandler,
|
2018-10-18 15:37:32 +08:00
|
|
|
loggelf: message => loggelf(message)
|
2017-10-18 17:42:51 +08:00
|
|
|
});
|
|
|
|
|
2017-03-27 15:36:45 +08:00
|
|
|
let started = false;
|
2017-03-07 00:27:04 +08:00
|
|
|
|
2017-03-27 15:36:45 +08:00
|
|
|
server.on('error', err => {
|
|
|
|
if (!started) {
|
2017-03-07 00:27:04 +08:00
|
|
|
started = true;
|
2017-03-27 15:36:45 +08:00
|
|
|
return done(err);
|
|
|
|
}
|
2017-04-12 19:52:29 +08:00
|
|
|
log.error('LMTP', err);
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
|
2017-04-12 19:52:29 +08:00
|
|
|
server.listen(config.lmtp.port, config.lmtp.host, () => {
|
2017-03-27 15:36:45 +08:00
|
|
|
if (started) {
|
|
|
|
return server.close();
|
|
|
|
}
|
|
|
|
started = true;
|
|
|
|
done(null, server);
|
2017-03-07 00:27:04 +08:00
|
|
|
});
|
|
|
|
};
|