replaced browserbox with imapflow, utf7 with iconv-lite

This commit is contained in:
Andris Reinman 2020-05-15 19:02:24 +03:00
parent a3777c43fe
commit 00da4bc4fa
11 changed files with 125 additions and 142 deletions

View file

@ -7,6 +7,6 @@
}, },
"extends": ["nodemailer", "prettier"], "extends": ["nodemailer", "prettier"],
"parserOptions": { "parserOptions": {
"ecmaVersion": 2017 "ecmaVersion": 2018
} }
} }

View file

@ -13,7 +13,7 @@ WildDuck tries to follow Gmail in product design. If there's a decision to be ma
- _MongoDB_ to store all data - _MongoDB_ to store all data
- _Redis_ for pubsub and counters - _Redis_ for pubsub and counters
- _Node.js_ at least version 8.0.0 - _Node.js_ at least version 10.0.0
**Optional requirements** **Optional requirements**
@ -34,7 +34,6 @@ Attachment de-duplication and compression gives up to 56% of storage size reduct
![](https://raw.githubusercontent.com/nodemailer/wildduck/master/assets/storage.png) ![](https://raw.githubusercontent.com/nodemailer/wildduck/master/assets/storage.png)
## Goals of the Project ## Goals of the Project
1. Build a scalable and distributed IMAP/POP3 server that uses clustered database instead of single machine file system as mail store 1. Build a scalable and distributed IMAP/POP3 server that uses clustered database instead of single machine file system as mail store
@ -60,4 +59,3 @@ Attachment de-duplication and compression gives up to 56% of storage size reduct
## License ## License
WildDuck Mail Agent is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html) or later. WildDuck Mail Agent is licensed under the [European Union Public License 1.1](http://ec.europa.eu/idabc/eupl.html) or later.

View file

@ -1,59 +1,47 @@
/* eslint no-console:0 */ /* eslint no-console:0 */
'use strict'; 'use strict';
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const rawpath = process.argv[2];
const rawpath = process.argv[2];
const config = require('wild-config'); const config = require('wild-config');
const BrowserBox = require('browserbox'); const { ImapFlow } = require('imapflow');
const raw = require('fs').readFileSync(rawpath); const raw = require('fs').readFileSync(rawpath);
console.log('Processing %s of %s bytes', rawpath, raw.length); console.log('Processing %s of %s bytes', rawpath, raw.length);
const client = new BrowserBox('localhost', config.imap.port, { const client = new ImapFlow({
useSecureTransport: config.imap.secure, host: '127.0.0.1',
port: config.imap.port,
secure: config.imap.secure,
auth: { auth: {
user: 'myuser', user: 'myuser',
pass: 'verysecret' pass: 'verysecret'
}, },
id: {
name: 'My Client',
version: '0.1'
},
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized: false
},
clientInfo: {
name: 'My Client',
version: '0.1'
} }
}); });
client.onerror = function(err) { client.on('error', err => {
console.log(err); console.log(err);
process.exit(1); process.exit(1);
}; });
client.onauth = function() { client
client.upload('INBOX', raw, false, err => { .connect()
if (err) { .then(() => client.append('INBOX', raw))
console.log(err); .then(() => client.mailboxOpen('INBOX'))
return process.exit(1); .then(mailbox => client.fetchOne(mailbox.exists, { bodyStructure: true, source: true }))
} .then(data => {
console.log(data);
client.selectMailbox('INBOX', (err, mailbox) => { console.log('<<<%s>>>', data.source.toString());
if (err) { return process.exit(0);
console.log(err); })
return process.exit(1); .catch(err => {
} console.log(err);
console.log(mailbox); process.exit(1);
client.listMessages(mailbox.exists, ['BODY.PEEK[]', 'BODYSTRUCTURE'], (err, data) => {
if (err) {
console.log(err);
return process.exit(1);
}
console.log('<<<%s>>>', data[0]['body[]']);
return process.exit(0);
});
});
}); });
};
client.connect();

View file

@ -1,7 +1,6 @@
'use strict'; 'use strict';
const imapTools = require('../imap-tools'); const { normalizeMailbox, utf7decode } = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag CREATE "mailbox" // tag CREATE "mailbox"
@ -20,7 +19,7 @@ module.exports = {
if (!this.acceptUTF8Enabled) { if (!this.acceptUTF8Enabled) {
// decode before normalizing to uncover stuff like ending / etc. // decode before normalizing to uncover stuff like ending / etc.
path = utf7.decode(path); path = utf7decode(path);
} }
// Check if CREATE method is set // Check if CREATE method is set
@ -58,7 +57,7 @@ module.exports = {
}); });
} }
path = imapTools.normalizeMailbox(path); path = normalizeMailbox(path);
let logdata = { let logdata = {
short_message: '[CREATE]', short_message: '[CREATE]',

View file

@ -1,8 +1,7 @@
'use strict'; 'use strict';
const imapHandler = require('../handler/imap-handler'); const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools'); const { normalizeMailbox, utf7encode } = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag GETQUOTAROOT "mailbox" // tag GETQUOTAROOT "mailbox"
@ -18,7 +17,7 @@ module.exports = {
handler(command, callback) { handler(command, callback) {
let path = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString(); let path = Buffer.from((command.attributes[0] && command.attributes[0].value) || '', 'binary').toString();
path = imapTools.normalizeMailbox(path, !this.acceptUTF8Enabled); path = normalizeMailbox(path, !this.acceptUTF8Enabled);
if (typeof this._server.onGetQuota !== 'function') { if (typeof this._server.onGetQuota !== 'function') {
return callback(null, { return callback(null, {
@ -64,7 +63,7 @@ module.exports = {
} }
if (!this.acceptUTF8Enabled) { if (!this.acceptUTF8Enabled) {
path = utf7.encode(path); path = utf7encode(path);
} else { } else {
path = Buffer.from(path); path = Buffer.from(path);
} }

View file

@ -1,8 +1,7 @@
'use strict'; 'use strict';
const imapHandler = require('../handler/imap-handler'); const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools'); const { normalizeMailbox, utf7encode, filterFolders, generateFolderListing } = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag LIST (SPECIAL-USE) "" "%" RETURN (SPECIAL-USE) // tag LIST (SPECIAL-USE) "" "%" RETURN (SPECIAL-USE)
@ -107,7 +106,7 @@ module.exports = {
}); });
} }
let query = imapTools.normalizeMailbox(reference + path, !this.acceptUTF8Enabled); let query = normalizeMailbox(reference + path, !this.acceptUTF8Enabled);
let logdata = { let logdata = {
short_message: '[LIST]', short_message: '[LIST]',
@ -130,7 +129,7 @@ module.exports = {
}); });
} }
imapTools.filterFolders(imapTools.generateFolderListing(list), query).forEach(folder => { filterFolders(generateFolderListing(list), query).forEach(folder => {
if (!folder) { if (!folder) {
return; return;
} }
@ -162,7 +161,7 @@ module.exports = {
let path = folder.path; let path = folder.path;
if (!this.acceptUTF8Enabled) { if (!this.acceptUTF8Enabled) {
path = utf7.encode(path); path = utf7encode(path);
} else { } else {
path = Buffer.from(path); path = Buffer.from(path);
} }

View file

@ -1,8 +1,7 @@
'use strict'; 'use strict';
const imapHandler = require('../handler/imap-handler'); const imapHandler = require('../handler/imap-handler');
const imapTools = require('../imap-tools'); const { normalizeMailbox, utf7encode, filterFolders, generateFolderListing } = require('../imap-tools');
const utf7 = require('utf7').imap;
// tag LSUB "" "%" // tag LSUB "" "%"
@ -32,7 +31,7 @@ module.exports = {
}); });
} }
let query = imapTools.normalizeMailbox(reference + path, !this.acceptUTF8Enabled); let query = normalizeMailbox(reference + path, !this.acceptUTF8Enabled);
let logdata = { let logdata = {
short_message: '[LSUB]', short_message: '[LSUB]',
@ -55,14 +54,14 @@ module.exports = {
}); });
} }
imapTools.filterFolders(imapTools.generateFolderListing(list, true), query).forEach(folder => { filterFolders(generateFolderListing(list, true), query).forEach(folder => {
if (!folder) { if (!folder) {
return; return;
} }
let path = folder.path; let path = folder.path;
if (!this.acceptUTF8Enabled) { if (!this.acceptUTF8Enabled) {
path = utf7.encode(path); path = utf7encode(path);
} else { } else {
path = Buffer.from(path); path = Buffer.from(path);
} }
@ -98,6 +97,6 @@ module.exports = {
// Do folder listing // Do folder listing
// Concat reference and mailbox. No special reference handling whatsoever // Concat reference and mailbox. No special reference handling whatsoever
this._server.onLsub(imapTools.normalizeMailbox(reference + path), this.session, lsubResponse); this._server.onLsub(normalizeMailbox(reference + path), this.session, lsubResponse);
} }
}; };

View file

@ -1,13 +1,19 @@
'use strict'; 'use strict';
const Indexer = require('./indexer/indexer'); const Indexer = require('./indexer/indexer');
const utf7 = require('utf7').imap;
const libmime = require('libmime'); const libmime = require('libmime');
const punycode = require('punycode'); const punycode = require('punycode');
const iconv = require('iconv-lite');
module.exports.systemFlagsFormatted = ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen']; module.exports.systemFlagsFormatted = ['\\Answered', '\\Flagged', '\\Draft', '\\Deleted', '\\Seen'];
module.exports.systemFlags = ['\\answered', '\\flagged', '\\draft', '\\deleted', '\\seen']; module.exports.systemFlags = ['\\answered', '\\flagged', '\\draft', '\\deleted', '\\seen'];
const utf7encode = str => iconv.encode(str, 'utf-7-imap').toString();
const utf7decode = str => iconv.decode(Buffer.from(str), 'utf-7-imap').toString();
module.exports.utf7encode = utf7encode;
module.exports.utf7decode = utf7decode;
module.exports.fetchSchema = { module.exports.fetchSchema = {
body: [ body: [
true, true,
@ -195,11 +201,11 @@ module.exports.searchMapping = {
* @param {range} range Sequence range, eg "1,2,3:7" * @param {range} range Sequence range, eg "1,2,3:7"
* @returns {Boolean} True if the string looks like a sequence range * @returns {Boolean} True if the string looks like a sequence range
*/ */
module.exports.validateSequnce = function(range) { module.exports.validateSequnce = function (range) {
return !!(range.length && /^(\d+|\*)(:\d+|:\*)?(,(\d+|\*)(:\d+|:\*)?)*$/.test(range)); return !!(range.length && /^(\d+|\*)(:\d+|:\*)?(,(\d+|\*)(:\d+|:\*)?)*$/.test(range));
}; };
module.exports.normalizeMailbox = function(mailbox, utf7Encoded) { module.exports.normalizeMailbox = function (mailbox, utf7Encoded) {
if (!mailbox) { if (!mailbox) {
return ''; return '';
} }
@ -214,7 +220,7 @@ module.exports.normalizeMailbox = function(mailbox, utf7Encoded) {
} }
if (utf7Encoded) { if (utf7Encoded) {
parts = parts.map(value => utf7.decode(value)); parts = parts.map(value => utf7decode(value));
} }
mailbox = parts.join('/'); mailbox = parts.join('/');
@ -222,7 +228,7 @@ module.exports.normalizeMailbox = function(mailbox, utf7Encoded) {
return mailbox; return mailbox;
}; };
module.exports.generateFolderListing = function(folders, skipHierarchy) { module.exports.generateFolderListing = function (folders, skipHierarchy) {
let items = new Map(); let items = new Map();
let parents = []; let parents = [];
@ -328,7 +334,7 @@ module.exports.generateFolderListing = function(folders, skipHierarchy) {
return result; return result;
}; };
module.exports.filterFolders = function(folders, query) { module.exports.filterFolders = function (folders, query) {
query = query query = query
// remove excess * and % // remove excess * and %
.replace(/\*\*+/g, '*') .replace(/\*\*+/g, '*')
@ -345,7 +351,7 @@ module.exports.filterFolders = function(folders, query) {
return folders.filter(folder => !!regex.test(folder.path)); return folders.filter(folder => !!regex.test(folder.path));
}; };
module.exports.getMessageRange = function(uidList, range, isUid) { module.exports.getMessageRange = function (uidList, range, isUid) {
range = (range || '').toString(); range = (range || '').toString();
let result = []; let result = [];
@ -390,7 +396,7 @@ module.exports.getMessageRange = function(uidList, range, isUid) {
return result; return result;
}; };
module.exports.packMessageRange = function(uidList) { module.exports.packMessageRange = function (uidList) {
if (!Array.isArray(uidList)) { if (!Array.isArray(uidList)) {
uidList = [].concat(uidList || []); uidList = [].concat(uidList || []);
} }
@ -427,7 +433,7 @@ module.exports.packMessageRange = function(uidList) {
* @param {Date} date Date object to parse * @param {Date} date Date object to parse
* @returns {String} Internaldate formatted date * @returns {String} Internaldate formatted date
*/ */
module.exports.formatInternalDate = function(date) { module.exports.formatInternalDate = function (date) {
let day = date.getUTCDate(), let day = date.getUTCDate(),
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][date.getUTCMonth()], month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][date.getUTCMonth()],
year = date.getUTCFullYear(), year = date.getUTCFullYear(),
@ -485,7 +491,7 @@ module.exports.formatInternalDate = function(date) {
* @param {Object} options Options for the indexer * @param {Object} options Options for the indexer
* @returns {Array} Resolved responses * @returns {Array} Resolved responses
*/ */
module.exports.getQueryResponse = function(query, message, options) { module.exports.getQueryResponse = function (query, message, options) {
options = options || {}; options = options || {};
// for optimization purposes try to use cached mimeTree etc. if available // for optimization purposes try to use cached mimeTree etc. if available

View file

@ -5,52 +5,42 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
const config = require('wild-config'); const config = require('wild-config');
const BrowserBox = require('browserbox'); const { ImapFlow } = require('imapflow');
const client = new BrowserBox('localhost', config.imap.port, { const client = new ImapFlow({
useSecureTransport: config.imap.secure, host: '127.0.0.1',
port: config.imap.port,
secure: config.imap.secure,
auth: { auth: {
user: 'testuser', user: 'testuser',
pass: 'secretpass' pass: 'secretpass'
}, },
id: {
name: 'My Client',
version: '0.1'
},
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized: false
},
clientInfo: {
name: 'My Client',
version: '0.1'
} }
}); });
client.onerror = function(err) { client.on('error', err => {
console.log(err); console.log(err);
process.exit(1); process.exit(1);
}; });
client.onauth = function() { const raw = Buffer.from('from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz\r\n');
client.upload('INBOX', 'from: sender@example.com\r\nto: to@example.com\r\ncc: cc@example.com\r\nsubject: test\r\n\r\nzzzz\r\n', false, err => {
if (err) {
console.log(err);
return process.exit(1);
}
client.selectMailbox('INBOX', (err, mailbox) => { client
if (err) { .connect()
console.log(err); .then(() => client.append('INBOX', raw))
return process.exit(1); .then(() => client.mailboxOpen('INBOX'))
} .then(mailbox => client.fetchOne(mailbox.exists, { bodyStructure: true, source: true }))
console.log(mailbox); .then(data => {
console.log('<<<%s>>>', data.source.toString());
client.listMessages(mailbox.exists, ['BODY.PEEK[]', 'BODYSTRUCTURE'], (err, data) => { return process.exit(0);
if (err) { })
console.log(err); .catch(err => {
return process.exit(1); console.log(err);
} process.exit(1);
console.log('<<<%s>>>', data[0]['body[]']);
return process.exit(0);
});
});
}); });
};
client.connect();

View file

@ -17,10 +17,9 @@
"devDependencies": { "devDependencies": {
"ajv": "6.12.2", "ajv": "6.12.2",
"apidoc": "0.22.1", "apidoc": "0.22.1",
"browserbox": "0.9.1",
"chai": "4.2.0", "chai": "4.2.0",
"docsify-cli": "4.4.0", "docsify-cli": "4.4.0",
"eslint": "6.8.0", "eslint": "7.0.0",
"eslint-config-nodemailer": "1.2.0", "eslint-config-nodemailer": "1.2.0",
"eslint-config-prettier": "6.11.0", "eslint-config-prettier": "6.11.0",
"grunt": "1.1.0", "grunt": "1.1.0",
@ -29,6 +28,7 @@
"grunt-mocha-test": "0.13.3", "grunt-mocha-test": "0.13.3",
"grunt-shell-spawn": "0.4.0", "grunt-shell-spawn": "0.4.0",
"grunt-wait": "0.3.0", "grunt-wait": "0.3.0",
"imapflow": "1.0.46",
"mailparser": "2.7.7", "mailparser": "2.7.7",
"mocha": "7.1.2", "mocha": "7.1.2",
"request": "2.88.2", "request": "2.88.2",
@ -54,7 +54,7 @@
"libbase64": "1.2.1", "libbase64": "1.2.1",
"libmime": "4.2.1", "libmime": "4.2.1",
"libqp": "1.1.0", "libqp": "1.1.0",
"mailsplit": "4.6.4", "mailsplit": "5.0.0",
"mobileconfig": "2.3.1", "mobileconfig": "2.3.1",
"mongo-cursor-pagination": "7.3.0", "mongo-cursor-pagination": "7.3.0",
"mongodb": "3.5.7", "mongodb": "3.5.7",
@ -68,11 +68,11 @@
"qrcode": "1.4.4", "qrcode": "1.4.4",
"restify": "8.5.1", "restify": "8.5.1",
"restify-logger": "2.0.1", "restify-logger": "2.0.1",
"saslprep": "1.0.3",
"seq-index": "1.1.0", "seq-index": "1.1.0",
"smtp-server": "3.6.0", "smtp-server": "3.6.0",
"speakeasy": "2.0.0", "speakeasy": "2.0.0",
"u2f": "0.1.3", "u2f": "0.1.3",
"utf7": "1.0.2",
"uuid": "8.0.0", "uuid": "8.0.0",
"wild-config": "1.5.1", "wild-config": "1.5.1",
"yargs": "15.3.1" "yargs": "15.3.1"
@ -80,5 +80,8 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/wildduck-email/wildduck.git" "url": "git://github.com/wildduck-email/wildduck.git"
},
"engines": {
"node": ">=10.0.0"
} }
} }

View file

@ -10,9 +10,9 @@ const crypto = require('crypto');
const chai = require('chai'); const chai = require('chai');
const request = require('request'); const request = require('request');
const fs = require('fs'); const fs = require('fs');
const BrowserBox = require('browserbox');
const simpleParser = require('mailparser').simpleParser; const simpleParser = require('mailparser').simpleParser;
const nodemailer = require('nodemailer'); const nodemailer = require('nodemailer');
const { ImapFlow } = require('imapflow');
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
lmtp: true, lmtp: true,
@ -340,59 +340,61 @@ describe('Send multiple messages', function () {
crypto.createHash('md5').update(swanJpg).digest('hex') crypto.createHash('md5').update(swanJpg).digest('hex')
]; ];
const client = new BrowserBox('localhost', 9993, { const client = new ImapFlow({
useSecureTransport: true, host: '127.0.0.1',
port: 9993,
secure: true,
auth: { auth: {
user: 'user4', user: 'user4',
pass: 'secretpass' pass: 'secretpass'
}, },
id: {
name: 'My Client',
version: '0.1'
},
tls: { tls: {
rejectUnauthorized: false rejectUnauthorized: false
},
clientInfo: {
name: 'My Client',
version: '0.1'
} }
}); });
client.onerror = err => { client.on('error', err => {
expect(err).to.not.exist; expect(err).to.not.exist;
}; done();
});
client.on('close', () => done());
client.onclose = done; client
.connect()
client.onauth = () => { .then(async () => {
client.listMailboxes((err, result) => { const result = await client.list();
expect(err).to.not.exist; const folders = result.map(mbox => ({ name: mbox.name, specialUse: mbox.specialUse || false })).sort((a, b) => a.name.localeCompare(b.name));
let folders = result.children.map(mbox => ({ name: mbox.name, specialUse: mbox.specialUse || false }));
expect(folders).to.deep.equal([ expect(folders).to.deep.equal([
{ name: 'INBOX', specialUse: false },
{ name: 'Drafts', specialUse: '\\Drafts' }, { name: 'Drafts', specialUse: '\\Drafts' },
{ name: 'INBOX', specialUse: '\\Inbox' },
{ name: 'Junk', specialUse: '\\Junk' }, { name: 'Junk', specialUse: '\\Junk' },
{ name: 'Sent Mail', specialUse: '\\Sent' }, { name: 'Sent Mail', specialUse: '\\Sent' },
{ name: 'Trash', specialUse: '\\Trash' } { name: 'Trash', specialUse: '\\Trash' }
]); ]);
client.selectMailbox('INBOX', { condstore: true }, (err, result) => {
expect(err).to.not.exist;
expect(result.exists).gte(1);
client.listMessages(result.exists, ['uid', 'flags', 'body.peek[]'], (err, messages) => { const mailbox = await client.mailboxOpen('INBOX');
expect(err).to.not.exist; expect(mailbox.exists).gte(1);
expect(messages.length).equal(1);
let messageInfo = messages[0]; let messages = [];
simpleParser(messageInfo['body[]'], (err, parsed) => { for await (let msg of client.fetch(mailbox.exists, { uid: true, source: true })) {
expect(err).to.not.exist; messages.push(msg);
checksums.forEach((checksum, i) => { }
expect(checksum).to.equal(parsed.attachments[i].checksum); expect(messages.length).equal(1);
});
client.close(); let messageInfo = messages[0];
}); let parsed = await simpleParser(messageInfo.source);
}); checksums.forEach((checksum, i) => {
expect(checksum).to.equal(parsed.attachments[i].checksum);
}); });
client.close();
})
.catch(err => {
expect(err).to.not.exist;
client.close();
}); });
};
client.connect();
}); });
}); });