This commit is contained in:
Andris Reinman 2017-05-07 15:09:14 +03:00
parent b0a538c31e
commit ceb0934549
5 changed files with 175 additions and 13 deletions

View file

@ -10,7 +10,12 @@ module.exports = (options, callback) => {
let message = maildrop({
from: options.sender,
to: options.forward,
to: options.recipient,
forward: options.forward,
http: !!options.targetUrl,
targeUrl: options.targetUrl,
interface: 'forwarder'
}, callback);

View file

@ -7,9 +7,113 @@ const DkimStream = require('./dkim-stream');
const MessageSplitter = require('./message-splitter');
const seqIndex = new SeqIndex();
const GridFs = require('grid-fs');
const uuid = require('uuid');
const os = require('os');
const hostname = os.hostname();
const addressparser = require('addressparser');
const punycode = require('punycode');
let gridstore;
function convertAddresses(addresses, withNames, addressList) {
addressList = addressList || new Map();
flatten(addresses || []).forEach(address => {
if (address.address) {
let normalized = normalizeAddress(address, withNames);
let key = typeof normalized === 'string' ? normalized : normalized.address;
addressList.set(key, normalized);
} else if (address.group) {
convertAddresses(address.group, withNames, addressList);
}
});
return addressList;
}
function parseAddressList(headers, key, withNames) {
return parseAddressses(headers.getDecoded(key).map(header => header.value), withNames);
}
function parseAddressses(headerList, withNames) {
let map = convertAddresses(headerList.map(address => {
if (typeof address === 'string') {
address = addressparser(address);
}
return address;
}), withNames);
return Array.from(map).map(entry => entry[1]);
}
function normalizeDomain(domain) {
return punycode.toASCII(domain.toLowerCase().trim());
}
// helper function to flatten arrays
function flatten(arr) {
let flat = [].concat(...arr);
return flat.some(Array.isArray) ? flatten(flat) : flat;
}
function normalizeAddress(address, withNames) {
if (typeof address === 'string') {
address = {
address
};
}
if (!address || !address.address) {
return '';
}
let user = address.address.substr(0, address.address.lastIndexOf('@'));
let domain = address.address.substr(address.address.lastIndexOf('@') + 1);
let addr = user.trim() + '@' + normalizeDomain(domain);
if (withNames) {
return {
name: address.name || '',
address: addr
};
}
return addr;
}
function updateHeaders(envelope) {
// Fetch sender and receiver addresses
envelope.parsedEnvelope = {
from: parseAddressList(envelope.headers, 'from').shift() || false,
to: parseAddressList(envelope.headers, 'to'),
cc: parseAddressList(envelope.headers, 'cc'),
bcc: parseAddressList(envelope.headers, 'bcc'),
replyTo: parseAddressList(envelope.headers, 'reply-to').shift() || false,
sender: parseAddressList(envelope.headers, 'sender').shift() || false
};
// Check Message-ID: value. Add if missing
let mId = envelope.headers.getFirst('message-id');
if (!mId) {
mId = '<' + uuid.v4() + '@' + (envelope.from.substr(envelope.from.lastIndexOf('@') + 1) || hostname) + '>';
envelope.headers.remove('message-id'); // in case there's an empty value
envelope.headers.add('Message-ID', mId);
}
envelope.messageId = mId;
// Check Date: value. Add if missing or invalid or future date
let date = envelope.headers.getFirst('date');
let dateVal = new Date(date);
if (!date || dateVal.toString() === 'Invalid Date' || dateVal < new Date(1000)) {
date = new Date().toUTCString().replace(/GMT/, '+0000');
envelope.headers.remove('date'); // remove old empty or invalid values
envelope.headers.add('Date', date);
}
envelope.date = date;
// Remove BCC if present
envelope.headers.remove('bcc');
}
module.exports = (options, callback) => {
if (!config.sender.enabled) {
return callback(null, false);
@ -22,7 +126,7 @@ module.exports = (options, callback) => {
let envelope = {
id,
from: options.from,
from: options.from || '',
to: Array.isArray(options.to) ? options.to : [].concat(options.to || []),
interface: options.interface || 'maildrop',
@ -34,7 +138,31 @@ module.exports = (options, callback) => {
}
};
if (!envelope.to.length) {
let deliveries = [];
if (options.targeUrl) {
let targetUrls = [].concat(options.targeUrl || []).map(targetUrl => ({
to: options.to,
http: true,
targetUrl
}));
deliveries = deliveries.concat(targetUrls);
}
if (options.forward) {
let forwards = [].concat(options.forward || []).map(forward => ({
to: forward
}));
deliveries = deliveries.concat(forwards);
}
if (!deliveries.length) {
deliveries = envelope.to.map(to => ({
to
}));
}
if (!deliveries.length) {
return callback(null, false);
}
@ -42,7 +170,8 @@ module.exports = (options, callback) => {
let dkimStream = new DkimStream();
messageSplitter.once('headers', headers => {
envelope.headers = headers.getList();
envelope.headers = headers;
updateHeaders(envelope);
});
dkimStream.on('hash', bodyHash => {
@ -59,6 +188,7 @@ module.exports = (options, callback) => {
return callback(err);
}
envelope.headers = envelope.headers.getList();
setMeta(id, envelope, err => {
if (err) {
return removeMessage(id, () => callback(err));
@ -66,11 +196,11 @@ module.exports = (options, callback) => {
let date = new Date();
for (let i = 0, len = envelope.to.length; i < len; i++) {
for (let i = 0, len = deliveries.length; i < len; i++) {
let recipient = envelope.to[i];
let recipient = deliveries[i];
let deliveryZone = options.zone || config.sender.zone || 'default';
let recipientDomain = recipient.substr(recipient.lastIndexOf('@') + 1).replace(/[\[\]]/g, '');
let recipientDomain = recipient.to.substr(recipient.to.lastIndexOf('@') + 1).replace(/[\[\]]/g, '');
seq++;
let deliverySeq = (seq < 0x100 ? '0' : '') + (seq < 0x10 ? '0' : '') + seq.toString(16);
@ -83,7 +213,9 @@ module.exports = (options, callback) => {
sendingZone: deliveryZone,
// actual recipient address
recipient,
recipient: recipient.to,
http: recipient.http,
targetUrl: recipient.targetUrl,
locked: false,
lockTime: 0,

View file

@ -485,6 +485,14 @@ class UserHandler {
name: data.name
};
if (data.forward) {
update.forward = data.forward;
}
if (data.targetUrl) {
update.targetUrl = data.targetUrl;
}
if (data.password) {
update.password = bcrypt.hashSync(data.password, 11);
}

24
lmtp.js
View file

@ -72,7 +72,8 @@ const serverOptions = {
fields: {
filters: true,
forwards: true,
forward: true
forward: true,
targetUrl: true
}
}, (err, user) => {
if (err) {
@ -174,6 +175,7 @@ const serverOptions = {
} : []);
let forwardTargets = new Set();
let forwardTargetUrls = new Set();
let matchingFilters = [];
let filterActions = new Map();
@ -195,6 +197,12 @@ const serverOptions = {
forwardTargets.add(filter.action[key]);
return;
}
if (key === 'targetUrl') {
forwardTargetUrls.add(filter.action[key]);
return;
}
// if a previous filter already has set a value then do not touch it
if (!filterActions.has(key)) {
filterActions.set(key, filter.action[key]);
@ -209,13 +217,18 @@ const serverOptions = {
forwardTargets.add(user.forward);
}
if (user.targetUrl && !filterActions.get('delete')) {
// forward to default URL only if the message is not deleted
forwardTargetUrls.add(user.targetUrl);
}
// never forward messages marked as spam
if (!forwardTargets.size || filterActions.get('spam')) {
if ((!forwardTargets.size && !forwardTargetUrls.size) || filterActions.get('spam')) {
return setImmediate(done);
}
// check limiting counters
messageHandler.counters.ttlcounter('wdf:' + user._id.toString(), forwardTargets.size, user.forwards, (err, result) => {
messageHandler.counters.ttlcounter('wdf:' + user._id.toString(), forwardTargets.size + forwardTargetUrls.size, user.forwards, (err, result) => {
if (err) {
// failed checks
log.error('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + user._id.toString(), err.message);
@ -228,7 +241,10 @@ const serverOptions = {
user,
sender,
recipient,
forward: Array.from(forwardTargets),
forward: forwardTargets.size ? Array.from(forwardTargets): false,
targetUrl: forwardTargetUrls.size ? Array.from(forwardTargetUrls) : false,
chunks
}, done);
});

View file

@ -1,6 +1,6 @@
{
"name": "wildduck",
"version": "1.0.26",
"version": "1.0.27",
"description": "IMAP server built with Node.js and MongoDB",
"main": "server.js",
"scripts": {
@ -19,6 +19,7 @@
"mocha": "^3.3.0"
},
"dependencies": {
"addressparser": "^1.0.1",
"bcryptjs": "^2.4.3",
"config": "^1.25.1",
"generate-password": "^1.3.0",