This commit is contained in:
Andris Reinman 2018-09-13 16:00:40 +03:00
parent b30f894099
commit 1dc693d0fe
4 changed files with 169 additions and 135 deletions

View file

@ -19,25 +19,28 @@ class GridstoreStorage {
} }
get(attachmentId, callback) { get(attachmentId, callback) {
this.gridfs.collection(this.bucketName + '.files').findOne({ this.gridfs.collection(this.bucketName + '.files').findOne(
_id: attachmentId {
}, (err, attachmentData) => { _id: attachmentId
if (err) { },
return callback(err); (err, attachmentData) => {
} if (err) {
if (!attachmentData) { return callback(err);
return callback(new Error('This attachment does not exist')); }
} if (!attachmentData) {
return callback(new Error('This attachment does not exist'));
}
return callback(null, { return callback(null, {
contentType: attachmentData.contentType, contentType: attachmentData.contentType,
transferEncoding: attachmentData.metadata.transferEncoding, transferEncoding: attachmentData.metadata.transferEncoding,
length: attachmentData.length, length: attachmentData.length,
count: attachmentData.metadata.c, count: attachmentData.metadata.c,
hash: attachmentData._id, hash: attachmentData._id,
metadata: attachmentData.metadata metadata: attachmentData.metadata
}); });
}); }
);
} }
create(attachment, hash, callback) { create(attachment, hash, callback) {
@ -111,69 +114,79 @@ class GridstoreStorage {
let tryCount = 0; let tryCount = 0;
let tryStore = () => { let tryStore = () => {
this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate({ if (returned) {
_id: hash // might be already finished if retrying after delay
}, { return;
$inc: { }
'metadata.c': 1,
'metadata.m': attachment.magic
}
}, {
returnOriginal: false
}, (err, result) => {
if (err) {
return callback(err);
}
if (result && result.value) { this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate(
// already exists {
return callback(null, result.value._id); _id: hash
} },
{
// try to insert it $inc: {
let store = this.gridstore.openUploadStreamWithId(id, null, { 'metadata.c': 1,
contentType: attachment.contentType, 'metadata.m': attachment.magic
metadata
});
store.once('error', err => {
if (returned) {
return;
} }
if (err.code === 11000) { },
// most probably a race condition, try again {
if (tryCount++ < 5) { returnOriginal: false
if (/attachments\.chunks /.test(err.message)) { },
// partial chunks for a probably deleted message detected, try to clean up (err, result) => {
return setTimeout(() => this.cleanupGarbage(id, tryStore), 100 + 200 * Math.random()); if (err) {
} return callback(err);
return setTimeout(tryStore, 10);
}
} }
returned = true;
callback(err);
});
store.once('finish', () => { if (result && result.value) {
if (returned) { // already exists
return; return callback(null, result.value._id);
} }
returned = true;
return callback(null, id);
});
if (!metadata.decoded) { // try to insert it
store.end(attachment.body); let store = this.gridstore.openUploadStreamWithId(id, null, {
} else { contentType: attachment.contentType,
let decoder = new libbase64.Decoder(); metadata
decoder.pipe(store);
decoder.once('error', err => {
// pass error forward
store.emit('error', err);
}); });
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(); tryStore();
@ -202,25 +215,29 @@ class GridstoreStorage {
if (isNaN(magic) || typeof magic !== 'number') { if (isNaN(magic) || typeof magic !== 'number') {
errors.notify(new Error('Invalid magic "' + magic + '" for ' + id)); errors.notify(new Error('Invalid magic "' + magic + '" for ' + id));
} }
this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate({ this.gridfs.collection(this.bucketName + '.files').findOneAndUpdate(
_id: id {
}, { _id: id
$inc: { },
'metadata.c': -1, {
'metadata.m': -magic $inc: {
} 'metadata.c': -1,
}, { 'metadata.m': -magic
returnOriginal: false }
}, (err, result) => { },
if (err) { {
return callback(err); returnOriginal: false
} },
(err, result) => {
if (err) {
return callback(err);
}
if (!result || !result.value) { if (!result || !result.value) {
return callback(null, false); return callback(null, false);
} }
/* /*
// disabled as it is preferred that attachments are not deleted immediately but // 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 // after a while by a cleanup process. This gives the opportunity to reuse the
// attachment // attachment
@ -235,8 +252,9 @@ class GridstoreStorage {
} }
*/ */
return callback(null, true); return callback(null, true);
}); }
);
} }
update(ids, count, magic, callback) { update(ids, count, magic, callback) {
@ -248,8 +266,8 @@ class GridstoreStorage {
{ {
_id: Array.isArray(ids) _id: Array.isArray(ids)
? { ? {
$in: ids $in: ids
} }
: ids : ids
}, },
{ {
@ -292,28 +310,34 @@ class GridstoreStorage {
} }
// delete file entry first // delete file entry first
this.gridfs.collection(this.bucketName + '.files').deleteOne({ this.gridfs.collection(this.bucketName + '.files').deleteOne(
_id: attachment._id, {
// make sure that we do not delete a message that is already re-used _id: attachment._id,
'metadata.c': 0, // make sure that we do not delete a message that is already re-used
'metadata.m': 0 'metadata.c': 0,
}, err => { 'metadata.m': 0
if (err) { },
return processNext(); err => {
}
// delete data chunks
this.gridfs.collection(this.bucketName + '.chunks').deleteMany({
files_id: attachment._id
}, err => {
if (err) { if (err) {
// ignore as we don't really care if we have orphans or not return processNext();
} }
deleted++; // delete data chunks
processNext(); 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) { cleanupGarbage(id, next) {
this.gridfs.collection(this.bucketName + '.files').findOne({ this.gridfs.collection(this.bucketName + '.files').findOne(
_id: id {
}, (err, file) => { _id: id
if (err) { },
return next(err); (err, file) => {
}
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) { if (err) {
return next(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);
}
);
}
);
} }
} }

View file

@ -35,13 +35,15 @@ const defaultSpamHeaderKeys = [
]; ];
const spamScoreHeader = 'X-Rspamd-Score'; 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 { class FilterHandler {
constructor(options) { constructor(options) {
this.db = options.db; this.db = options.db;
this.messageHandler = options.messageHandler; this.messageHandler = options.messageHandler;
this.spamScoreValue = options.spamScoreValue || spamScoreValue;
this.spamChecks = options.spamChecks || tools.prepareSpamChecks(defaultSpamHeaderKeys); this.spamChecks = options.spamChecks || tools.prepareSpamChecks(defaultSpamHeaderKeys);
this.spamHeaderKeys = options.spamHeaderKeys || this.spamChecks.map(check => check.key); this.spamHeaderKeys = options.spamHeaderKeys || this.spamChecks.map(check => check.key);
@ -289,7 +291,7 @@ class FilterHandler {
} else if (userData.spamLevel === 100) { } else if (userData.spamLevel === 100) {
isSpam = false; isSpam = false;
} else { } else {
isSpam = (userData.spamLevel / 100) * spamScoreValue * 2 <= spamScore; isSpam = (userData.spamLevel / 100) * this.spamScoreValue * 2 <= spamScore;
} }
if (isSpam) { if (isSpam) {
filterActions.set('spam', true); filterActions.set('spam', true);

View file

@ -25,6 +25,7 @@ config.on('reload', () => {
if (filterHandler) { if (filterHandler) {
filterHandler.spamChecks = spamChecks; filterHandler.spamChecks = spamChecks;
filterHandler.spamHeaderKeys = spamHeaderKeys; filterHandler.spamHeaderKeys = spamHeaderKeys;
filterHandler.spamScoreValue = config.lmtp.spamScore;
} }
log.info('LMTP', 'Configuration reloaded'); log.info('LMTP', 'Configuration reloaded');
@ -226,7 +227,8 @@ module.exports = done => {
sender: config.sender, sender: config.sender,
messageHandler, messageHandler,
spamHeaderKeys, spamHeaderKeys,
spamChecks spamChecks,
spamScoreValue: config.lmtp.spamScore
}); });
let started = false; let started = false;

View file

@ -1,6 +1,6 @@
{ {
"name": "wildduck", "name": "wildduck",
"version": "1.4.10", "version": "1.4.11",
"description": "IMAP/POP3 server built with Node.js and MongoDB", "description": "IMAP/POP3 server built with Node.js and MongoDB",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {