diff --git a/lib/attachments/gridstore-storage.js b/lib/attachments/gridstore-storage.js index 867742d4..4e4702ea 100644 --- a/lib/attachments/gridstore-storage.js +++ b/lib/attachments/gridstore-storage.js @@ -19,25 +19,28 @@ class GridstoreStorage { } get(attachmentId, callback) { - this.gridfs.collection(this.bucketName + '.files').findOne({ - _id: attachmentId - }, (err, attachmentData) => { - if (err) { - return callback(err); - } - if (!attachmentData) { - return callback(new Error('This attachment does not exist')); - } + this.gridfs.collection(this.bucketName + '.files').findOne( + { + _id: attachmentId + }, + (err, attachmentData) => { + if (err) { + return callback(err); + } + if (!attachmentData) { + return callback(new Error('This attachment does not exist')); + } - return callback(null, { - contentType: attachmentData.contentType, - transferEncoding: attachmentData.metadata.transferEncoding, - length: attachmentData.length, - count: attachmentData.metadata.c, - hash: attachmentData._id, - metadata: attachmentData.metadata - }); - }); + return callback(null, { + contentType: attachmentData.contentType, + transferEncoding: attachmentData.metadata.transferEncoding, + length: attachmentData.length, + count: attachmentData.metadata.c, + hash: attachmentData._id, + metadata: attachmentData.metadata + }); + } + ); } create(attachment, hash, callback) { @@ -111,69 +114,79 @@ class GridstoreStorage { let tryCount = 0; let tryStore = () => { - this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate({ - _id: hash - }, { - $inc: { - 'metadata.c': 1, - 'metadata.m': attachment.magic - } - }, { - returnOriginal: false - }, (err, result) => { - if (err) { - return callback(err); - } + if (returned) { + // might be already finished if retrying after delay + return; + } - if (result && result.value) { - // already exists - return callback(null, result.value._id); - } - - // try to insert it - let store = this.gridstore.openUploadStreamWithId(id, null, { - contentType: attachment.contentType, - metadata - }); - - store.once('error', err => { - if (returned) { - return; + this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate( + { + _id: hash + }, + { + $inc: { + 'metadata.c': 1, + 'metadata.m': attachment.magic } - if (err.code === 11000) { - // most probably a race condition, try again - if (tryCount++ < 5) { - if (/attachments\.chunks /.test(err.message)) { - // partial chunks for a probably deleted message detected, try to clean up - return setTimeout(() => this.cleanupGarbage(id, tryStore), 100 + 200 * Math.random()); - } - return setTimeout(tryStore, 10); - } + }, + { + returnOriginal: false + }, + (err, result) => { + if (err) { + return callback(err); } - returned = true; - callback(err); - }); - store.once('finish', () => { - if (returned) { - return; + if (result && result.value) { + // already exists + return callback(null, result.value._id); } - returned = true; - return callback(null, id); - }); - if (!metadata.decoded) { - store.end(attachment.body); - } else { - let decoder = new libbase64.Decoder(); - decoder.pipe(store); - decoder.once('error', err => { - // pass error forward - store.emit('error', err); + // try to insert it + let store = this.gridstore.openUploadStreamWithId(id, null, { + contentType: attachment.contentType, + metadata }); - decoder.end(attachment.body); + + store.once('error', err => { + if (returned) { + return; + } + if (err.code === 11000) { + // most probably a race condition, try again + if (tryCount++ < 5) { + if (/attachments\.chunks /.test(err.message)) { + // partial chunks for a probably deleted message detected, try to clean up + return setTimeout(() => this.cleanupGarbage(id, tryStore), 100 + 200 * Math.random()); + } + return setTimeout(tryStore, 10); + } + } + returned = true; + callback(err); + }); + + store.once('finish', () => { + if (returned) { + return; + } + returned = true; + return callback(null, id); + }); + + if (!metadata.decoded) { + store.end(attachment.body); + } else { + let decoder = new libbase64.Decoder(); + decoder.pipe(store); + decoder.once('error', err => { + // pass error forward + store.emit('error', err); + }); + decoder.end(attachment.body); + } } - }); + ); }; tryStore(); @@ -202,25 +215,29 @@ class GridstoreStorage { if (isNaN(magic) || typeof magic !== 'number') { errors.notify(new Error('Invalid magic "' + magic + '" for ' + id)); } - this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate({ - _id: id - }, { - $inc: { - 'metadata.c': -1, - 'metadata.m': -magic - } - }, { - returnOriginal: false - }, (err, result) => { - if (err) { - return callback(err); - } + this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate( + { + _id: id + }, + { + $inc: { + 'metadata.c': -1, + 'metadata.m': -magic + } + }, + { + returnOriginal: false + }, + (err, result) => { + if (err) { + return callback(err); + } - if (!result || !result.value) { - return callback(null, false); - } + if (!result || !result.value) { + return callback(null, false); + } - /* + /* // disabled as it is preferred that attachments are not deleted immediately but // after a while by a cleanup process. This gives the opportunity to reuse the // attachment @@ -235,8 +252,9 @@ class GridstoreStorage { } */ - return callback(null, true); - }); + return callback(null, true); + } + ); } update(ids, count, magic, callback) { @@ -248,8 +266,8 @@ class GridstoreStorage { { _id: Array.isArray(ids) ? { - $in: ids - } + $in: ids + } : ids }, { @@ -292,28 +310,34 @@ class GridstoreStorage { } // delete file entry first - this.gridfs.collection(this.bucketName + '.files').deleteOne({ - _id: attachment._id, - // make sure that we do not delete a message that is already re-used - 'metadata.c': 0, - 'metadata.m': 0 - }, err => { - if (err) { - return processNext(); - } - - // delete data chunks - this.gridfs.collection(this.bucketName + '.chunks').deleteMany({ - files_id: attachment._id - }, err => { + this.gridfs.collection(this.bucketName + '.files').deleteOne( + { + _id: attachment._id, + // make sure that we do not delete a message that is already re-used + 'metadata.c': 0, + 'metadata.m': 0 + }, + err => { if (err) { - // ignore as we don't really care if we have orphans or not + return processNext(); } - deleted++; - processNext(); - }); - }); + // delete data chunks + this.gridfs.collection(this.bucketName + '.chunks').deleteMany( + { + files_id: attachment._id + }, + err => { + if (err) { + // ignore as we don't really care if we have orphans or not + } + + deleted++; + processNext(); + } + ); + } + ); }); }; @@ -321,27 +345,33 @@ class GridstoreStorage { } cleanupGarbage(id, next) { - this.gridfs.collection(this.bucketName + '.files').findOne({ - _id: id - }, (err, file) => { - if (err) { - return next(err); - } - if (file) { - // attachment entry exists, do nothing - return next(null, false); - } - - // orphaned attachment, delete data chunks - this.gridfs.collection(this.bucketName + '.chunks').deleteMany({ - files_id: id - }, (err, info) => { + this.gridfs.collection(this.bucketName + '.files').findOne( + { + _id: id + }, + (err, file) => { if (err) { return next(err); } - next(null, info.deletedCount); - }); - }); + if (file) { + // attachment entry exists, do nothing + return next(null, false); + } + + // orphaned attachment, delete data chunks + this.gridfs.collection(this.bucketName + '.chunks').deleteMany( + { + files_id: id + }, + (err, info) => { + if (err) { + return next(err); + } + next(null, info.deletedCount); + } + ); + } + ); } } diff --git a/lib/filter-handler.js b/lib/filter-handler.js index b96483ef..89c93018 100644 --- a/lib/filter-handler.js +++ b/lib/filter-handler.js @@ -35,13 +35,15 @@ const defaultSpamHeaderKeys = [ ]; const spamScoreHeader = 'X-Rspamd-Score'; -const spamScoreValue = 15; // everything over this value is spam, under ham +const spamScoreValue = 5.1; // everything over this value is spam, under ham class FilterHandler { constructor(options) { this.db = options.db; this.messageHandler = options.messageHandler; + this.spamScoreValue = options.spamScoreValue || spamScoreValue; + this.spamChecks = options.spamChecks || tools.prepareSpamChecks(defaultSpamHeaderKeys); this.spamHeaderKeys = options.spamHeaderKeys || this.spamChecks.map(check => check.key); @@ -289,7 +291,7 @@ class FilterHandler { } else if (userData.spamLevel === 100) { isSpam = false; } else { - isSpam = (userData.spamLevel / 100) * spamScoreValue * 2 <= spamScore; + isSpam = (userData.spamLevel / 100) * this.spamScoreValue * 2 <= spamScore; } if (isSpam) { filterActions.set('spam', true); diff --git a/lmtp.js b/lmtp.js index e2e6c620..d783f1c5 100644 --- a/lmtp.js +++ b/lmtp.js @@ -25,6 +25,7 @@ config.on('reload', () => { if (filterHandler) { filterHandler.spamChecks = spamChecks; filterHandler.spamHeaderKeys = spamHeaderKeys; + filterHandler.spamScoreValue = config.lmtp.spamScore; } log.info('LMTP', 'Configuration reloaded'); @@ -226,7 +227,8 @@ module.exports = done => { sender: config.sender, messageHandler, spamHeaderKeys, - spamChecks + spamChecks, + spamScoreValue: config.lmtp.spamScore }); let started = false; diff --git a/package.json b/package.json index 83896cc6..c4ad6fb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wildduck", - "version": "1.4.10", + "version": "1.4.11", "description": "IMAP/POP3 server built with Node.js and MongoDB", "main": "server.js", "scripts": {