diff --git a/config/default.js b/config/default.js index 8a00e5d..2178cbe 100644 --- a/config/default.js +++ b/config/default.js @@ -132,6 +132,13 @@ module.exports = { }, disableInterfaces: ['forwarder'], // do not bounce messages from this interface sendingZone: 'bounces', + + // send a warning email about delayed delivery + delayEmail: { + enabled: true, + after: 3 * 3600 * 1000 + }, + zoneConfig: { // specify zone specific bounce options myzonename: { diff --git a/lib/mail-queue.js b/lib/mail-queue.js index 5f6866b..f6d304e 100644 --- a/lib/mail-queue.js +++ b/lib/mail-queue.js @@ -745,31 +745,38 @@ class MailQueue { * * @param {Object} delivery Message object * @param {Number} ttl TTL in ms. Once this time is over the message is reinserted to queue - * @param {Number} responseData SMTP response or description + * @param {Object} responseData SMTP response or description * @param {Function} callback Run once the message is removed from active queue */ deferDelivery(delivery, ttl, responseData, callback) { // add metainfo about postponing the delivery - delivery._deferred = delivery._deferred || { - first: Date.now(), - count: 0 - }; - delivery._deferred.count++; - delivery._deferred.last = Date.now(); - delivery._deferred.next = Date.now() + ttl; - delivery._deferred.response = responseData.response; - delivery._deferred.log = responseData.log || delivery._deferred.log; - delivery.queued = new Date(Math.max(delivery._deferred.next, Date.now())); - delivery.locked = false; + + const now = Date.now(); let updates = { $set: { - _deferred: delivery._deferred, - queued: delivery.queued, + '_deferred.last': now, + '_deferred.next': now + ttl, + queued: new Date(now + ttl), locked: false + }, + $inc: { + '_deferred.count': 1 } }; + if (!delivery._deferred) { + updates.$set['_deferred.first'] = now; + } + + if (responseData.response) { + updates.$set['_deferred.response'] = responseData.response; + } + + if (responseData.log) { + updates.$set['_deferred.log'] = responseData.log; + } + if (responseData.updates && typeof responseData.updates === 'object') { Object.keys(responseData.updates).forEach(key => { if (key.charAt(0) === '$') { @@ -777,7 +784,7 @@ class MailQueue { return; // not allowed } // $inc etc. - updates[key] = responseData.updates[key]; + updates[key] = Object.assign(updates[key] || {}, responseData.updates[key]); return; } if (!updates.$set.hasOwnProperty(key)) { @@ -787,13 +794,16 @@ class MailQueue { } let collection = this.mongodb.collection(this.options.collection); - collection.updateOne( + collection.findOneAndUpdate( { id: delivery.id, seq: delivery.seq }, updates, - err => { + { + returnOriginal: true + }, + (err, item) => { if (err) { return callback(err); } @@ -802,6 +812,34 @@ class MailQueue { log.verbose('Queue', '%s.%s UNLOCK (key="%s")', delivery.id, delivery.seq, delivery._lock); this.locks.release(delivery._lock); + if (item && item.value) { + let firstCheck = item.value._deferred && item.value._deferred.first; + let prevLastCheck = item.value._deferred && item.value._deferred.last; + let lastCheck = now; + + if (firstCheck && prevLastCheck) { + return plugins.handler.runHooks( + 'queue:delayed', + [ + delivery, + responseData, + { + first: firstCheck, + prev: prevLastCheck, + last: lastCheck + } + ], + err => { + if (err) { + log.error('Queue', '%s.%s queue:delayed %s', delivery.id, delivery.seq, err.message); + } + + return callback(null, true); + } + ); + } + } + return callback(null, true); } ); diff --git a/lib/queue-server.js b/lib/queue-server.js index 1624f36..c4ca6fb 100644 --- a/lib/queue-server.js +++ b/lib/queue-server.js @@ -180,7 +180,7 @@ class QueueServer { ); }); } - case 'DEFER': + case 'DEFER': { if (!client.zone) { return client.send({ req: data.req, @@ -190,6 +190,7 @@ class QueueServer { deliveryStatusCounter.inc({ status: 'deferred' }); + return this.deferDelivery(client.zone, client.id, data, (err, response) => { if (!client) { // client already errored or closed @@ -206,11 +207,12 @@ class QueueServer { response }); }); + } case 'BOUNCE': { bounceCounter.inc(); - let bounce = data; + const bounce = data; bounce.headers = new Headers(bounce.headers || []); plugins.handler.runHooks( 'queue:bounce', diff --git a/plugins/core/email-bounce.js b/plugins/core/email-bounce.js index 0559884..3fc82aa 100644 --- a/plugins/core/email-bounce.js +++ b/plugins/core/email-bounce.js @@ -4,7 +4,7 @@ const os = require('os'); const MimeNode = require('nodemailer/lib/mime-node'); module.exports.title = 'Email Bounce Notification'; -module.exports.init = function(app, done) { +module.exports.init = function (app, done) { // generate a multipart/report DSN failure response function generateBounceMessage(bounce) { let headers = bounce.headers; @@ -72,10 +72,7 @@ Status: 5.0.0 ` ); - rootNode - .createChild('text/rfc822-headers') - .setHeader('Content-Description', 'Undelivered Message Headers') - .setContent(headers.build()); + rootNode.createChild('text/rfc822-headers').setHeader('Content-Description', 'Undelivered Message Headers').setContent(headers.build()); return rootNode; } @@ -132,5 +129,29 @@ Status: 5.0.0 }); }); + app.addHook('queue:delayed', async (delivery, bounce, options) => { + if (!app.config.delayEmail || !app.config.delayEmail.enabled) { + return; + } + + // check if past required time + let prevDiff = options.prev - options.first; + let curDiff = options.last - options.first; + if (prevDiff > app.config.delayEmail.after || curDiff < app.config.delayEmail.after) { + return; + } + + app.logger.info( + 'Bounce', + 'Should send a delay email %s', + JSON.stringify({ + id: delivery.id, + seq: delivery.seq, + recipient: delivery.recipient, + response: bounce.response + }) + ); + }); + done(); };