Comparing version 1.4.0 to 1.5.0
@@ -113,3 +113,19 @@ 'use strict'; | ||
}, | ||
sendingZone: 'bounces' | ||
sendingZone: 'bounces', | ||
zoneConfig: { | ||
// specify zone specific bounce options | ||
myzonename: { | ||
// if true then ignore this block, revert to default | ||
disabled: true, | ||
// if not set then default mailerDaemon config is used | ||
mailerDaemon: { | ||
name: 'Mail Delivery Subsystem', | ||
// [HOSTNAME] will be replaced with the hostname that was used to send this message | ||
address: 'mailer-daemon@[HOSTNAME]' | ||
}, | ||
// use same queue for handling bounces as for the original message | ||
// if not set then default queue is used | ||
sendingZone: 'myzonename' | ||
} | ||
} | ||
}, | ||
@@ -213,3 +229,3 @@ | ||
api: { | ||
port: 8080, | ||
port: 12080, | ||
// bind to localhost only | ||
@@ -216,0 +232,0 @@ host: '127.0.0.1', |
@@ -8,2 +8,4 @@ 'use strict'; | ||
module.exports.mongoclient = false; | ||
module.exports.database = false; | ||
@@ -19,3 +21,3 @@ module.exports.senderDb = false; | ||
if (!config) { | ||
return callback(null, main); | ||
return callback(null, false); | ||
} | ||
@@ -30,2 +32,5 @@ if (config && !/[:/]/.test(config)) { | ||
} | ||
if (main && db.s && db.s.options && db.s.options.dbName) { | ||
db = db.db(db.s.options.dbName); | ||
} | ||
return callback(null, db); | ||
@@ -40,18 +45,27 @@ }); | ||
} | ||
module.exports.database = db; | ||
getDBConnection(db, config.dbs.gridfs, (err, db) => { | ||
module.exports.mongoclient = db; | ||
if (db.s && db.s.options && db.s.options.dbName) { | ||
module.exports.database = db.db(db.s.options.dbName); | ||
} else { | ||
module.exports.database = db; | ||
} | ||
getDBConnection(db, config.dbs.gridfs, (err, gdb) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
module.exports.gridfs = db; | ||
getDBConnection(db, config.dbs.users, (err, db) => { | ||
module.exports.gridfs = gdb || module.exports.database; | ||
getDBConnection(db, config.dbs.users, (err, udb) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
module.exports.users = db; | ||
getDBConnection(db, config.dbs.sender, (err, db) => { | ||
module.exports.users = udb || module.exports.database; | ||
getDBConnection(db, config.dbs.sender, (err, sdb) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
module.exports.senderDb = db; | ||
module.exports.senderDb = sdb || module.exports.database; | ||
@@ -58,0 +72,0 @@ module.exports.redisConfig = redisConfig(config.dbs.redis); |
@@ -113,14 +113,20 @@ 'use strict'; | ||
setMeta(id, data, callback) { | ||
this.mongodb.collection(this.options.gfs + '.files').findAndModify({ | ||
filename: 'message ' + id | ||
}, false, { | ||
$set: { | ||
'metadata.data': data | ||
this.mongodb.collection(this.options.gfs + '.files').findAndModify( | ||
{ | ||
filename: 'message ' + id | ||
}, | ||
false, | ||
{ | ||
$set: { | ||
'metadata.data': data | ||
} | ||
}, | ||
{}, | ||
err => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
return callback(); | ||
} | ||
}, {}, err => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
return callback(); | ||
}); | ||
); | ||
} | ||
@@ -135,15 +141,18 @@ | ||
getMeta(id, callback) { | ||
this.mongodb.collection(this.options.gfs + '.files').findOne({ | ||
filename: 'message ' + id | ||
}, (err, item) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
this.mongodb.collection(this.options.gfs + '.files').findOne( | ||
{ | ||
filename: 'message ' + id | ||
}, | ||
(err, item) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (!item) { | ||
return callback(null, false); | ||
if (!item) { | ||
return callback(null, false); | ||
} | ||
return callback(null, (item && item.metadata && item.metadata.data) || {}); | ||
} | ||
return callback(null, (item && item.metadata && item.metadata.data) || {}); | ||
}); | ||
); | ||
} | ||
@@ -483,53 +492,56 @@ | ||
this.mongodb.collection('suppressionlist').findOne({ | ||
$or: [ | ||
{ | ||
address: (delivery.recipient || '').toLowerCase().trim() | ||
}, | ||
{ | ||
domain: delivery.domain | ||
this.mongodb.collection('suppressionlist').findOne( | ||
{ | ||
$or: [ | ||
{ | ||
address: (delivery.recipient || '').toLowerCase().trim() | ||
}, | ||
{ | ||
domain: delivery.domain | ||
} | ||
] | ||
}, | ||
(err, suppressed) => { | ||
if (err) { | ||
// just ignore, not important, even though should not happen | ||
} | ||
] | ||
}, (err, suppressed) => { | ||
if (err) { | ||
// just ignore, not important, even though should not happen | ||
} | ||
if (suppressed) { | ||
return this.releaseDelivery(delivery, err => { | ||
if (err) { | ||
this.locks.release(delivery._lock); | ||
return callback(err); | ||
} | ||
log.info( | ||
'Queue', | ||
'%s.%s DROP[suppressed] Recipient %s was found from suppression list', | ||
delivery.id, | ||
delivery.seq, | ||
delivery.recipient | ||
); | ||
if (suppressed) { | ||
return this.releaseDelivery(delivery, err => { | ||
if (err) { | ||
this.locks.release(delivery._lock); | ||
return callback(err); | ||
} | ||
log.info( | ||
'Queue', | ||
'%s.%s DROP[suppressed] Recipient %s was found from suppression list', | ||
delivery.id, | ||
delivery.seq, | ||
delivery.recipient | ||
); | ||
let suppresskey = ''; | ||
let suppressvalue = ''; | ||
let suppresskey = ''; | ||
let suppressvalue = ''; | ||
if (suppressed.address && (delivery.recipient || '').toLowerCase().trim() === suppressed.address) { | ||
suppresskey = 'suppressed address'; | ||
suppressvalue = suppressed.address; | ||
} else if (suppressed.domain && delivery.domain === suppressed.domain) { | ||
suppresskey = 'suppressed domain'; | ||
suppressvalue = suppressed.domain; | ||
} | ||
if (suppressed.address && (delivery.recipient || '').toLowerCase().trim() === suppressed.address) { | ||
suppresskey = 'suppressed address'; | ||
suppressvalue = suppressed.address; | ||
} else if (suppressed.domain && delivery.domain === suppressed.domain) { | ||
suppresskey = 'suppressed domain'; | ||
suppressvalue = suppressed.domain; | ||
} | ||
remotelog(delivery.id, delivery.seq, 'DROP', { | ||
reason: 'Recipient was found from suppression list', | ||
recipient: delivery.recipient, | ||
[suppresskey]: suppressvalue | ||
remotelog(delivery.id, delivery.seq, 'DROP', { | ||
reason: 'Recipient was found from suppression list', | ||
recipient: delivery.recipient, | ||
[suppresskey]: suppressvalue | ||
}); | ||
// try to find another delivery | ||
return setImmediate(tryNext); | ||
}); | ||
} | ||
// try to find another delivery | ||
return setImmediate(tryNext); | ||
}); | ||
return callback(null, delivery); | ||
} | ||
return callback(null, delivery); | ||
}); | ||
); | ||
}); | ||
@@ -827,13 +839,16 @@ } | ||
log.verbose('Queue', '%s REMOVE', id); | ||
this.mongodb.collection(this.options.gfs + '.files').findOne({ | ||
filename: 'message ' + id | ||
}, (err, entry) => { | ||
if (err) { | ||
return callback(err); | ||
this.mongodb.collection(this.options.gfs + '.files').findOne( | ||
{ | ||
filename: 'message ' + id | ||
}, | ||
(err, entry) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (!entry) { | ||
return callback(null, false); | ||
} | ||
this.gridstore.delete(entry._id, callback); | ||
} | ||
if (!entry) { | ||
return callback(null, false); | ||
} | ||
this.gridstore.delete(entry._id, callback); | ||
}); | ||
); | ||
} | ||
@@ -1140,12 +1155,15 @@ | ||
this.mongodb.collection(this.options.collection).find(query).count((err, count) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
this.mongodb | ||
.collection(this.options.collection) | ||
.find(query) | ||
.count((err, count) => { | ||
if (err) { | ||
return reject(err); | ||
} | ||
resolve({ | ||
key: zone, | ||
value: count | ||
resolve({ | ||
key: zone, | ||
value: count | ||
}); | ||
}); | ||
}); | ||
}); | ||
@@ -1170,4 +1188,4 @@ | ||
this.closing = true; | ||
if (this.mongodb) { | ||
this.mongodb.close(() => false); | ||
if (db.mongoclient) { | ||
db.mongoclient.close(() => false); | ||
} | ||
@@ -1200,121 +1218,140 @@ this.stopPeriodicCheck(); | ||
// ensure indexes for the gridstore queries | ||
this.mongodb.collection(this.options.gfs + '.files').createIndexes([ | ||
{ | ||
key: { | ||
uploadDate: 1 | ||
}, | ||
name: 'mailupload' | ||
}, | ||
{ | ||
key: { | ||
filename: 1 | ||
}, | ||
name: 'mailfile' | ||
} | ||
], () => { | ||
// ensure indexes for the suppression list table | ||
this.mongodb.collection('suppressionlist').createIndexes([ | ||
this.mongodb.collection(this.options.gfs + '.files').createIndexes( | ||
[ | ||
{ | ||
key: { | ||
address: 1 | ||
uploadDate: 1 | ||
}, | ||
name: 'suppressed_address' | ||
name: 'mailupload' | ||
}, | ||
{ | ||
key: { | ||
domain: 1 | ||
filename: 1 | ||
}, | ||
name: 'suppressed_domain' | ||
}, | ||
{ | ||
key: { | ||
created: -1 | ||
}, | ||
name: 'list_by_newer' | ||
name: 'mailfile' | ||
} | ||
], () => { | ||
// ensure indexes for the queue queries | ||
this.mongodb.collection(this.options.collection).createIndexes([ | ||
{ | ||
key: { | ||
created: 1 | ||
], | ||
() => { | ||
// ensure indexes for the suppression list table | ||
this.mongodb.collection('suppressionlist').createIndexes( | ||
[ | ||
{ | ||
key: { | ||
address: 1 | ||
}, | ||
name: 'suppressed_address' | ||
}, | ||
name: 'findall' | ||
}, | ||
{ | ||
key: { | ||
sendingZone: 1, | ||
queued: 1, | ||
locked: 1, | ||
assigned: 1, | ||
domain: 1 | ||
{ | ||
key: { | ||
domain: 1 | ||
}, | ||
name: 'suppressed_domain' | ||
}, | ||
name: 'search_next' | ||
}, | ||
{ | ||
key: { | ||
id: 1, | ||
seq: 1 | ||
}, | ||
name: 'delivery' | ||
}, | ||
{ | ||
key: { | ||
locked: 1, | ||
assigned: 1, | ||
lockTime: 1 | ||
}, | ||
name: 'message_locking' | ||
} | ||
], () => { | ||
// release old locks | ||
this.mongodb.collection(this.options.collection).updateMany({ | ||
locked: true, | ||
// only touch messages assigned to this instance | ||
assigned: this.instanceId | ||
}, { | ||
$set: { | ||
locked: false, | ||
lockTime: 0 | ||
{ | ||
key: { | ||
created: -1 | ||
}, | ||
name: 'list_by_newer' | ||
} | ||
}, { | ||
w: 1, | ||
multi: true | ||
}, (err, r) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
], | ||
() => { | ||
// ensure indexes for the queue queries | ||
this.mongodb.collection(this.options.collection).createIndexes( | ||
[ | ||
{ | ||
key: { | ||
created: 1 | ||
}, | ||
name: 'findall' | ||
}, | ||
{ | ||
key: { | ||
sendingZone: 1, | ||
queued: 1, | ||
locked: 1, | ||
assigned: 1, | ||
domain: 1 | ||
}, | ||
name: 'search_next' | ||
}, | ||
{ | ||
key: { | ||
id: 1, | ||
seq: 1 | ||
}, | ||
name: 'delivery' | ||
}, | ||
{ | ||
key: { | ||
locked: 1, | ||
assigned: 1, | ||
lockTime: 1 | ||
}, | ||
name: 'message_locking' | ||
} | ||
], | ||
() => { | ||
// release old locks | ||
this.mongodb.collection(this.options.collection).updateMany( | ||
{ | ||
locked: true, | ||
// only touch messages assigned to this instance | ||
assigned: this.instanceId | ||
}, | ||
{ | ||
$set: { | ||
locked: false, | ||
lockTime: 0 | ||
} | ||
}, | ||
{ | ||
w: 1, | ||
multi: true | ||
}, | ||
(err, r) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (r.result.n) { | ||
log.verbose('GC', 'Released %s expired locks for queued messages', r.result.n); | ||
} | ||
if (r.result.n) { | ||
log.verbose('GC', 'Released %s expired locks for queued messages', r.result.n); | ||
} | ||
// assign unassigned messages | ||
this.mongodb.collection(this.options.collection).updateMany({ | ||
assigned: { | ||
$exists: false | ||
} | ||
}, { | ||
$set: { | ||
assigned: 'no' | ||
} | ||
}, { | ||
w: 1, | ||
multi: true | ||
}, (err, r) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
// assign unassigned messages | ||
this.mongodb.collection(this.options.collection).updateMany( | ||
{ | ||
assigned: { | ||
$exists: false | ||
} | ||
}, | ||
{ | ||
$set: { | ||
assigned: 'no' | ||
} | ||
}, | ||
{ | ||
w: 1, | ||
multi: true | ||
}, | ||
(err, r) => { | ||
if (err) { | ||
return callback(err); | ||
} | ||
if (r.result.n) { | ||
log.verbose('GC', 'Updated %s unassigned messages', r.result.n); | ||
if (r.result.n) { | ||
log.verbose('GC', 'Updated %s unassigned messages', r.result.n); | ||
} | ||
this.startPeriodicCheck(); | ||
return setImmediate(() => callback(null, true)); | ||
} | ||
); | ||
} | ||
); | ||
} | ||
this.startPeriodicCheck(); | ||
return setImmediate(() => callback(null, true)); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); | ||
); | ||
} | ||
); | ||
} | ||
); | ||
}); | ||
@@ -1321,0 +1358,0 @@ } |
@@ -1115,2 +1115,3 @@ 'use strict'; | ||
zone: this.zone.name, | ||
from: delivery.from, | ||
@@ -1117,0 +1118,0 @@ to: delivery.recipient, |
{ | ||
"name": "zone-mta", | ||
"private": false, | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"description": "Tiny outbound MTA", | ||
@@ -45,3 +45,3 @@ "main": "app.js", | ||
"grunt-eslint": "^20.1.0", | ||
"moment": "^2.20.0", | ||
"moment": "^2.20.1", | ||
"random-message": "^1.1.0", | ||
@@ -48,0 +48,0 @@ "zip-stream": "^1.2.0" |
@@ -9,6 +9,15 @@ 'use strict'; | ||
// generate a multipart/report DSN failure response | ||
function generateBounceMessage(from, to, bounce) { | ||
function generateBounceMessage(bounce) { | ||
let headers = bounce.headers; | ||
let messageId = headers.getFirst('Message-ID'); | ||
let cfg = app.config.zoneConfig[bounce.zone]; | ||
if (!cfg || cfg.disabled) { | ||
cfg = {}; | ||
} | ||
let from = cfg.mailerDaemon || app.config.mailerDaemon; | ||
let to = bounce.from; | ||
let sendingZone = cfg.sendingZone || app.config.sendingZone; | ||
let rootNode = new MimeNode('multipart/report; report-type=delivery-status'); | ||
@@ -21,3 +30,3 @@ | ||
rootNode.setHeader('To', to); | ||
rootNode.setHeader('X-Sending-Zone', app.config.sendingZone); | ||
rootNode.setHeader('X-Sending-Zone', sendingZone); | ||
rootNode.setHeader('X-Failed-Recipients', bounce.to); | ||
@@ -32,4 +41,7 @@ rootNode.setHeader('Auto-Submitted', 'auto-replied'); | ||
rootNode.createChild('text/plain').setHeader('Content-Description', 'Notification').setContent( | ||
`Delivery to the following recipient failed permanently: | ||
rootNode | ||
.createChild('text/plain') | ||
.setHeader('Content-Description', 'Notification') | ||
.setContent( | ||
`Delivery to the following recipient failed permanently: | ||
${bounce.to} | ||
@@ -42,6 +54,9 @@ | ||
` | ||
); | ||
); | ||
rootNode.createChild('message/delivery-status').setHeader('Content-Description', 'Delivery report').setContent( | ||
`Reporting-MTA: dns; ${bounce.name || os.hostname()} | ||
rootNode | ||
.createChild('message/delivery-status') | ||
.setHeader('Content-Description', 'Delivery report') | ||
.setContent( | ||
`Reporting-MTA: dns; ${bounce.name || os.hostname()} | ||
X-ZoneMTA-Queue-ID: ${bounce.id} | ||
@@ -55,12 +70,15 @@ X-ZoneMTA-Sender: rfc822; ${bounce.from} | ||
` + | ||
(bounce.mxHostname | ||
? `Remote-MTA: dns; ${bounce.mxHostname} | ||
(bounce.mxHostname | ||
? `Remote-MTA: dns; ${bounce.mxHostname} | ||
` | ||
: '') + | ||
`Diagnostic-Code: smtp; ${bounce.response} | ||
: '') + | ||
`Diagnostic-Code: smtp; ${bounce.response} | ||
` | ||
); | ||
); | ||
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()); | ||
@@ -99,3 +117,3 @@ return rootNode; | ||
let mail = generateBounceMessage(app.config.mailerDaemon, bounce.from, bounce); | ||
let mail = generateBounceMessage(bounce); | ||
@@ -102,0 +120,0 @@ app.getQueue().generateId((err, id) => { |
461208
9315