diff --git a/lib/forward.js b/lib/forward.js index 23c6238e..9527b6bb 100644 --- a/lib/forward.js +++ b/lib/forward.js @@ -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); diff --git a/lib/maildrop.js b/lib/maildrop.js index 2e107720..132fbfc1 100644 --- a/lib/maildrop.js +++ b/lib/maildrop.js @@ -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, diff --git a/lib/user-handler.js b/lib/user-handler.js index a80897ef..e1255a99 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -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); } diff --git a/lmtp.js b/lmtp.js index 0eaae91d..ec5c7548 100644 --- a/lmtp.js +++ b/lmtp.js @@ -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); }); diff --git a/package.json b/package.json index da86ef38..58d4a8cc 100644 --- a/package.json +++ b/package.json @@ -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",