mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-11-09 16:01:06 +08:00
Decode base64 encoded attachments before storing to DB
This commit is contained in:
parent
9b54586e2a
commit
ee4a76e30e
6 changed files with 85 additions and 26 deletions
9
config/attachments.toml
Normal file
9
config/attachments.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
# Attachment storage options
|
||||||
|
|
||||||
|
# For now there's only a single option for attachment storage
|
||||||
|
type="gridstore"
|
||||||
|
bucket="attachments"
|
||||||
|
|
||||||
|
# If true then decodes base64 encoded attachments to binary before storing to DB.
|
||||||
|
# Decoding base64 attachments expects consistent line length and default base64 alphabet
|
||||||
|
decodeBase64=true
|
||||||
|
|
@ -37,9 +37,7 @@ bugsnagCode=""
|
||||||
#secret="a secret cat"
|
#secret="a secret cat"
|
||||||
|
|
||||||
[attachments]
|
[attachments]
|
||||||
# For now there's only a single option for attachment storage
|
# @include "attachments.toml"
|
||||||
type="gridstore"
|
|
||||||
bucket="attachments"
|
|
||||||
|
|
||||||
[log]
|
[log]
|
||||||
level="silly"
|
level="silly"
|
||||||
|
|
|
||||||
|
|
@ -193,18 +193,24 @@ class Indexer {
|
||||||
attachmentId = mimeTree.attachmentMap[node.attachmentId];
|
attachmentId = mimeTree.attachmentMap[node.attachmentId];
|
||||||
}
|
}
|
||||||
|
|
||||||
let attachmentStream = this.attachmentStorage.createReadStream(attachmentId);
|
return this.attachmentStorage.get(attachmentId, (err, attachmentData) => {
|
||||||
|
if (err) {
|
||||||
|
return res.emit('error', err);
|
||||||
|
}
|
||||||
|
|
||||||
attachmentStream.once('error', err => {
|
let attachmentStream = this.attachmentStorage.createReadStream(attachmentId, attachmentData);
|
||||||
res.errored = err;
|
|
||||||
|
attachmentStream.once('error', err => {
|
||||||
|
// res.errored = err;
|
||||||
|
res.emit('error', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
attachmentStream.once('end', () => finalize());
|
||||||
|
|
||||||
|
attachmentStream.pipe(res, {
|
||||||
|
end: false
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
attachmentStream.once('end', () => finalize());
|
|
||||||
|
|
||||||
attachmentStream.pipe(res, {
|
|
||||||
end: false
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
|
|
@ -399,6 +405,7 @@ class Indexer {
|
||||||
magic: maildata.magic,
|
magic: maildata.magic,
|
||||||
contentType,
|
contentType,
|
||||||
transferEncoding,
|
transferEncoding,
|
||||||
|
lineCount: node.lineCount,
|
||||||
body: node.body
|
body: node.body
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -885,10 +885,21 @@ module.exports = (db, server, messageHandler) => {
|
||||||
'Content-Type': attachmentData.contentType || 'application/octet-stream'
|
'Content-Type': attachmentData.contentType || 'application/octet-stream'
|
||||||
});
|
});
|
||||||
|
|
||||||
let attachmentStream = messageHandler.attachmentStorage.createReadStream(attachmentId);
|
let decode = true;
|
||||||
|
|
||||||
|
if (attachmentData.metadata.decoded) {
|
||||||
|
attachmentData.metadata.decoded = false;
|
||||||
|
decode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attachmentStream = messageHandler.attachmentStorage.createReadStream(attachmentId, attachmentData);
|
||||||
|
|
||||||
attachmentStream.once('error', err => res.emit('error', err));
|
attachmentStream.once('error', err => res.emit('error', err));
|
||||||
|
|
||||||
|
if (!decode) {
|
||||||
|
return attachmentStream.pipe(res);
|
||||||
|
}
|
||||||
|
|
||||||
if (attachmentData.transferEncoding === 'base64') {
|
if (attachmentData.transferEncoding === 'base64') {
|
||||||
attachmentStream.pipe(new libbase64.Decoder()).pipe(res);
|
attachmentStream.pipe(new libbase64.Decoder()).pipe(res);
|
||||||
} else if (attachmentData.transferEncoding === 'quoted-printable') {
|
} else if (attachmentData.transferEncoding === 'quoted-printable') {
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,8 @@ class AttachmentStorage {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
createReadStream(id) {
|
createReadStream(id, attachmentData) {
|
||||||
return this.storage.createReadStream(id);
|
return this.storage.createReadStream(id, attachmentData);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteMany(ids, magic, callback) {
|
deleteMany(ids, magic, callback) {
|
||||||
|
|
@ -68,7 +68,15 @@ class AttachmentStorage {
|
||||||
let algo = 'sha256';
|
let algo = 'sha256';
|
||||||
|
|
||||||
if (!cryptoAsync) {
|
if (!cryptoAsync) {
|
||||||
setImmediate(() => callback(null, crypto.createHash(algo).update(input).digest('hex')));
|
setImmediate(() =>
|
||||||
|
callback(
|
||||||
|
null,
|
||||||
|
crypto
|
||||||
|
.createHash(algo)
|
||||||
|
.update(input)
|
||||||
|
.digest('hex')
|
||||||
|
)
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
const GridFSBucket = require('mongodb').GridFSBucket;
|
const GridFSBucket = require('mongodb').GridFSBucket;
|
||||||
const libbase64 = require('libbase64');
|
const libbase64 = require('libbase64');
|
||||||
|
|
||||||
const FEATURE_DECODE_ATTACHMENTS = false;
|
// Set to false to disable base64 decoding feature
|
||||||
|
const FEATURE_DECODE_ATTACHMENTS = true;
|
||||||
|
|
||||||
class GridstoreStorage {
|
class GridstoreStorage {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
|
|
@ -87,8 +88,18 @@ class GridstoreStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lineLen && lineLen <= 998) {
|
if (lineLen && lineLen <= 998) {
|
||||||
metadata.decoded = true;
|
if (attachment.body.length === lineLen && lineLen < 76) {
|
||||||
metadata.lineLen = lineLen;
|
lineLen = 76;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if expected line count matches with attachment line count
|
||||||
|
let expectedLineCount = Math.ceil(attachment.body.length / (lineLen + 2));
|
||||||
|
|
||||||
|
// allow 1 line shift
|
||||||
|
if (attachment.lineCount >= expectedLineCount - 1 && attachment.lineCount <= expectedLineCount + 1) {
|
||||||
|
metadata.decoded = true;
|
||||||
|
metadata.lineLen = lineLen;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,8 +173,23 @@ class GridstoreStorage {
|
||||||
tryStore();
|
tryStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
createReadStream(id) {
|
createReadStream(id, attachmentData) {
|
||||||
return this.gridstore.openDownloadStream(id);
|
let stream = this.gridstore.openDownloadStream(id);
|
||||||
|
if (attachmentData.metadata.decoded) {
|
||||||
|
let encoder = new libbase64.Encoder({
|
||||||
|
lineLength: attachmentData.metadata.lineLen
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.once('error', err => {
|
||||||
|
// pass error forward
|
||||||
|
encoder.emit('error', err);
|
||||||
|
});
|
||||||
|
stream.pipe(encoder);
|
||||||
|
|
||||||
|
return encoder;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(id, magic, callback) {
|
delete(id, magic, callback) {
|
||||||
|
|
@ -254,7 +280,7 @@ class GridstoreStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete file entry first
|
// delete file entry first
|
||||||
this.gridfs.collection('attachments.files').deleteOne({
|
this.gridfs.collection(this.bucketName + '.files').deleteOne({
|
||||||
_id: attachment._id,
|
_id: attachment._id,
|
||||||
// make sure that we do not delete a message that is already re-used
|
// make sure that we do not delete a message that is already re-used
|
||||||
'metadata.c': 0,
|
'metadata.c': 0,
|
||||||
|
|
@ -265,7 +291,7 @@ class GridstoreStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete data chunks
|
// delete data chunks
|
||||||
this.gridfs.collection('attachments.chunks').deleteMany({
|
this.gridfs.collection(this.bucketName + '.chunks').deleteMany({
|
||||||
files_id: attachment._id
|
files_id: attachment._id
|
||||||
}, err => {
|
}, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -283,7 +309,7 @@ class GridstoreStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupGarbage(id, next) {
|
cleanupGarbage(id, next) {
|
||||||
this.gridfs.collection('attachments.files').findOne({
|
this.gridfs.collection(this.bucketName + '.files').findOne({
|
||||||
_id: id
|
_id: id
|
||||||
}, (err, file) => {
|
}, (err, file) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
@ -295,7 +321,7 @@ class GridstoreStorage {
|
||||||
}
|
}
|
||||||
|
|
||||||
// orphaned attachment, delete data chunks
|
// orphaned attachment, delete data chunks
|
||||||
this.gridfs.collection('attachments.chunks').deleteMany({
|
this.gridfs.collection(this.bucketName + '.chunks').deleteMany({
|
||||||
files_id: id
|
files_id: id
|
||||||
}, (err, info) => {
|
}, (err, info) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue