Skip to content

Commit

Permalink
Detect if should send out a delayed delivery notification email (no s…
Browse files Browse the repository at this point in the history
…ending yet)
  • Loading branch information
andris9 committed Jan 18, 2024
1 parent de238ed commit 3769fc3
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 24 deletions.
7 changes: 7 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
72 changes: 55 additions & 17 deletions lib/mail-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -745,39 +745,46 @@ 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) === '$') {
if (!['$inc', '$mul'].includes(key)) {
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)) {
Expand All @@ -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);
}
Expand All @@ -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);
}
);
Expand Down
6 changes: 4 additions & 2 deletions lib/queue-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ class QueueServer {
);
});
}
case 'DEFER':
case 'DEFER': {
if (!client.zone) {
return client.send({
req: data.req,
Expand All @@ -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
Expand All @@ -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',
Expand Down
31 changes: 26 additions & 5 deletions plugins/core/email-bounce.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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();
};

0 comments on commit 3769fc3

Please sign in to comment.