From 61d2813be9ce479711eb4ab48ae4c637ba920f36 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Mon, 22 Oct 2018 12:00:24 +0300 Subject: [PATCH] v1.9.0 --- README.md | 83 ++++++++--------- config/default.toml | 4 - config/spamheaders.toml | 31 ------- examples/push-message.js | 9 -- lib/filter-handler.js | 96 +++++++------------ lib/message-handler.js | 7 +- lib/tools.js | 44 --------- lmtp.js | 16 ---- package.json | 2 +- test/filtering-test.js | 196 --------------------------------------- 10 files changed, 77 insertions(+), 411 deletions(-) delete mode 100644 config/spamheaders.toml diff --git a/README.md b/README.md index f2799520..a21f2f9e 100644 --- a/README.md +++ b/README.md @@ -11,14 +11,14 @@ WildDuck tries to follow Gmail in product design. If there's a decision to be ma ## Requirements -* _MongoDB_ to store all data -* _Redis_ for pubsub and counters -* _Node.js_ at least version 8.0.0 +- _MongoDB_ to store all data +- _Redis_ for pubsub and counters +- _Node.js_ at least version 8.0.0 **Optional requirements** -* Redis Sentinel for automatic Redis failover -* Build tools to install optional dependencies that need compiling +- Redis Sentinel for automatic Redis failover +- Build tools to install optional dependencies that need compiling WildDuck can be installed on any Node.js compatible platform. @@ -48,7 +48,7 @@ Tested on a 10$ DigitalOcean Ubuntu 16.04 instance. ![](https://cldup.com/TZoTfxPugm.png) -* Web interface at https://wildduck.email that uses WildDuck API +- Web interface at https://wildduck.email that uses WildDuck API ### Manual install @@ -157,9 +157,9 @@ specific, so (at least in theory) it could be replaced with any object store. Here's a list of alternative email servers that also use a database for storing email messages: -* [DBMail](http://www.dbmail.org/) (MySQL, IMAP) -* [Archiveopteryx](http://archiveopteryx.org/) (PostgreSQL, IMAP) -* [ElasticInbox](http://www.elasticinbox.com/) (Cassandra, POP3) +- [DBMail](http://www.dbmail.org/) (MySQL, IMAP) +- [Archiveopteryx](http://archiveopteryx.org/) (PostgreSQL, IMAP) +- [ElasticInbox](http://www.elasticinbox.com/) (Cassandra, POP3) ### How does it work? @@ -182,25 +182,25 @@ and the user continues to see the old state. WildDuck IMAP server supports the following IMAP standards: -* The entire **IMAP4rev1** suite with some minor differences from the spec. See below for [IMAP Protocol Differences](#imap-protocol-differences) for a complete +- The entire **IMAP4rev1** suite with some minor differences from the spec. See below for [IMAP Protocol Differences](#imap-protocol-differences) for a complete list -* **IDLE** ([RFC2177](https://tools.ietf.org/html/rfc2177)) – notfies about new and deleted messages and also about flag updates -* **CONDSTORE** ([RFC4551](https://tools.ietf.org/html/rfc4551)) and **ENABLE** ([RFC5161](https://tools.ietf.org/html/rfc5161)) – supports most of the spec, +- **IDLE** ([RFC2177](https://tools.ietf.org/html/rfc2177)) – notfies about new and deleted messages and also about flag updates +- **CONDSTORE** ([RFC4551](https://tools.ietf.org/html/rfc4551)) and **ENABLE** ([RFC5161](https://tools.ietf.org/html/rfc5161)) – supports most of the spec, except metadata stuff which is ignored -* **STARTTLS** ([RFC2595](https://tools.ietf.org/html/rfc2595)) -* **NAMESPACE** ([RFC2342](https://tools.ietf.org/html/rfc2342)) – minimal support, just lists the single user namespace with hierarchy separator -* **UNSELECT** ([RFC3691](https://tools.ietf.org/html/rfc3691)) -* **UIDPLUS** ([RFC4315](https://tools.ietf.org/html/rfc4315)) -* **SPECIAL-USE** ([RFC6154](https://tools.ietf.org/html/rfc6154)) -* **ID** ([RFC2971](https://tools.ietf.org/html/rfc2971)) -* **MOVE** ([RFC6851](https://tools.ietf.org/html/rfc6851)) -* **AUTHENTICATE PLAIN** ([RFC4959](https://tools.ietf.org/html/rfc4959)) and **SASL-IR** -* **APPENDLIMIT** ([RFC7889](https://tools.ietf.org/html/rfc7889)) – maximum global allowed message size is advertised in CAPABILITY listing -* **UTF8=ACCEPT** ([RFC6855](https://tools.ietf.org/html/rfc6855)) – this also means that WildDuck natively supports unicode email usernames. For example +- **STARTTLS** ([RFC2595](https://tools.ietf.org/html/rfc2595)) +- **NAMESPACE** ([RFC2342](https://tools.ietf.org/html/rfc2342)) – minimal support, just lists the single user namespace with hierarchy separator +- **UNSELECT** ([RFC3691](https://tools.ietf.org/html/rfc3691)) +- **UIDPLUS** ([RFC4315](https://tools.ietf.org/html/rfc4315)) +- **SPECIAL-USE** ([RFC6154](https://tools.ietf.org/html/rfc6154)) +- **ID** ([RFC2971](https://tools.ietf.org/html/rfc2971)) +- **MOVE** ([RFC6851](https://tools.ietf.org/html/rfc6851)) +- **AUTHENTICATE PLAIN** ([RFC4959](https://tools.ietf.org/html/rfc4959)) and **SASL-IR** +- **APPENDLIMIT** ([RFC7889](https://tools.ietf.org/html/rfc7889)) – maximum global allowed message size is advertised in CAPABILITY listing +- **UTF8=ACCEPT** ([RFC6855](https://tools.ietf.org/html/rfc6855)) – this also means that WildDuck natively supports unicode email usernames. For example [андрис@уайлддак.орг](mailto:андрис@уайлддак.орг) is a valid email address that is hosted by a test instance of WildDuck -* **QUOTA** ([RFC2087](https://tools.ietf.org/html/rfc2087)) – Quota size is global for an account, using a single quota root. Be aware that quota size does not +- **QUOTA** ([RFC2087](https://tools.ietf.org/html/rfc2087)) – Quota size is global for an account, using a single quota root. Be aware that quota size does not mean actual byte storage in disk, it is calculated as the sum of the [RFC822](https://tools.ietf.org/html/rfc822) sources of stored messages. -* **COMPRESS=DEFLATE** ([RFC4978](https://tools.ietf.org/html/rfc4978)) – Compress traffic between the client and the server +- **COMPRESS=DEFLATE** ([RFC4978](https://tools.ietf.org/html/rfc4978)) – Compress traffic between the client and the server WildDuck more or less passes the [ImapTest](https://www.imapwiki.org/ImapTest/TestFeatures) Stress Testing run. Common errors that arise in the test are unknown labels (WildDuck doesn't send unsolicited `FLAGS` updates even though it does send unsolicited `FETCH FLAGS` updates) and sometimes NO for `STORE` @@ -218,12 +218,12 @@ clients. In addition to the required POP3 commands ([RFC1939](https://tools.ietf.org/html/rfc1939)) WildDuck supports the following extensions: -* **UIDL** -* **USER** -* **PASS** -* **SASL PLAIN** -* **PIPELINING** -* **TOP** +- **UIDL** +- **USER** +- **PASS** +- **SASL PLAIN** +- **PIPELINING** +- **TOP** #### POP3 command behaviors @@ -248,7 +248,7 @@ If a messages is deleted by a client this message gets marked as Seen and moved ## Message filtering -WildDuck has built-in message filtering in LMTP server. This is somewhat similar to Sieve even though the filters are not scripts. +WildDuck has built-in message filtering. This is somewhat similar to Sieve even though the filters are not scripts. Filters can be managed via the [WildDuck API](https://api.wildduck.email/#api-Filters). @@ -289,27 +289,24 @@ Use [WildDuck MTA](https://github.com/nodemailer/wildduck-mta) (which under the [ZoneMTA-WildDuck](https://github.com/nodemailer/zonemta-wildduck) plugin). This gives you an outbound SMTP server that uses WildDuck accounts for authentication. The plugin authenticates user credentials and also rewrites headers if -needed (if the header From: address does not match user address or aliases then it is rewritten). Additionally a copy of the sent message is uploaded to the -Sent Mail folder. Local delivery is done directly to WildDuck LMTP. +needed (if the header From: address does not match user address or aliases then it is rewritten). ## Inbound SMTP -Use [Haraka](http://haraka.github.io/) with [queue/lmtp](http://haraka.github.io/manual/plugins/queue/lmtp.html) plugin to deliver messages to WildDuck and -[haraka-plugins-wildduck](https://github.com/nodemailer/haraka-plugin-wildduck) to validate recipient addresses and quota usage against the WildDuck users -database. +Use [Haraka](http://haraka.github.io/) with [haraka-plugins-wildduck](https://github.com/nodemailer/haraka-plugin-wildduck) to validate recipient addresses and quota usage against the WildDuck users database and to store/filter messages. ## Future considerations -* Optimize FETCH queries to load only partial data for BODY subparts -* Parse incoming message into the mime tree as a stream. Currently the entire message is buffered in memory before being parsed. -* Maybe allow some kind of message manipulation through plugins -* WildDuck does not plan to be the most feature-rich IMAP client in the world. Most IMAP extensions are useless because there aren't too many clients that are +- Optimize FETCH queries to load only partial data for BODY subparts +- Parse incoming message into the mime tree as a stream. Currently the entire message is buffered in memory before being parsed. +- Maybe allow some kind of message manipulation through plugins +- WildDuck does not plan to be the most feature-rich IMAP client in the world. Most IMAP extensions are useless because there aren't too many clients that are able to benefit from these extensions. There are a few extensions though that would make sense to be added to WildDuck: - * IMAP4 non-synchronizing literals, LITERAL- ([RFC7888](https://tools.ietf.org/html/rfc7888)). Synchronized literals are needed for APPEND to check mailbox + - IMAP4 non-synchronizing literals, LITERAL- ([RFC7888](https://tools.ietf.org/html/rfc7888)). Synchronized literals are needed for APPEND to check mailbox quota, small values could go with the non-synchronizing version. - * LIST-STATUS ([RFC5819](https://tools.ietf.org/html/rfc5819)) - * _What else?_ (definitely not NOTIFY nor QRESYNC) + - LIST-STATUS ([RFC5819](https://tools.ietf.org/html/rfc5819)) + - _What else?_ (definitely not NOTIFY nor QRESYNC) ## Operating WildDuck diff --git a/config/default.toml b/config/default.toml index 1792b412..b8581521 100644 --- a/config/default.toml +++ b/config/default.toml @@ -26,10 +26,6 @@ maxForwards=2000 # If set then reports errors to Bugsnag bugsnagCode="" -# Header rules for routing spam -# This does not affect WildDuck plugin for Haraka -# @include "spamheaders.toml" - [dbs] # @include "dbs.toml" diff --git a/config/spamheaders.toml b/config/spamheaders.toml deleted file mode 100644 index bca2b709..00000000 --- a/config/spamheaders.toml +++ /dev/null @@ -1,31 +0,0 @@ -# key is a case insensitive header key -# value is a trimmed case-insensitive regex -# target is either special folder or exact name (normalized unicode string, not UTF7) -# special folders: ""INBOX", "\\Sent", "\\Trash", "\\Junk", "\\Drafts", "\\Archive" - -[[spamHeader]] -# If this header exists and starts with "yes" then the message is treated as spam -# This is SpamAssassin header. -key="X-Spam-Status" -value="^yes" -target="\\Junk" - -[[spamHeader]] -# If this header exists and starts with "yes" then the message is treated as spam -# This is Rspamd header. For the same with SpamAssassin use "X-Spam-Status" -key="X-Rspamd-Spam" -value="^yes" -target="\\Junk" - -# Treat as spam if message has header with 6 or more plus signs: -# X-Rspamd-Bar: ++++++ -[[spamHeader]] -key="X-Rspamd-Bar" -value="^\\+{6}" -target="\\Junk" - -[[spamHeader]] -# If this header has a value, then it contains a virus, treat as spam -key="X-Haraka-Virus" -value="." -target="\\Junk" diff --git a/examples/push-message.js b/examples/push-message.js index 1aaadfbc..916212c2 100644 --- a/examples/push-message.js +++ b/examples/push-message.js @@ -35,15 +35,6 @@ function send() { to: recipients }, - headers: { - /* - // uncomment to send the messge to Junk - 'X-Rspamd-Bar': '/', - 'X-Rspamd-Report': 'R_PARTS_DIFFER(0.5) MIME_GOOD(-0.1) R_DKIM_ALLOW(-0.2) R_SPF_ALLOW(-0.2)', - 'X-Rspamd-Score': '22.6' - */ - }, - from: 'Kärbes 🐧 ', to: recipients .map((rcpt, i) => ({ name: 'Recipient #' + (i + 1), address: rcpt })) diff --git a/lib/filter-handler.js b/lib/filter-handler.js index 3db84ff4..9eccf9d2 100644 --- a/lib/filter-handler.js +++ b/lib/filter-handler.js @@ -8,45 +8,11 @@ const Maildropper = require('./maildropper'); const tools = require('./tools'); const consts = require('./consts'); -const defaultSpamHeaderKeys = [ - { - key: 'X-Spam-Status', - value: '^yes', - target: '\\Junk' - }, - - { - key: 'X-Rspamd-Spam', - value: '^yes', - target: '\\Junk' - }, - /* - { - key: 'X-Rspamd-Bar', - value: '^\\+{6}', - target: '\\Junk' - }, - */ - { - key: 'X-Haraka-Virus', - value: '.', - target: '\\Junk' - } -]; - -const spamScoreHeader = 'X-Rspamd-Score'; -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); - this.maildrop = new Maildropper({ db: this.db, zone: options.sender.zone, @@ -164,8 +130,7 @@ class FilterHandler { return this.messageHandler.prepareMessage( { - mimeTree: options.mimeTree, - indexedHeaders: this.spamHeaderKeys + mimeTree: options.mimeTree }, next ); @@ -173,8 +138,7 @@ class FilterHandler { let raw = Buffer.concat(chunks, chunklen); return this.messageHandler.prepareMessage( { - raw, - indexedHeaders: this.spamHeaderKeys + raw }, next ); @@ -239,20 +203,8 @@ class FilterHandler { // ignore, as filtering is not so important } - filters = (filters || []).filter(filter => !filter.disabled).concat( - this.spamChecks.map((check, i) => ({ - id: 'SPAM#' + (i + 1), - query: { - headers: { - [check.key]: check.value - } - }, - action: { - // only applies if any other filter does not already mark message as spam or ham - spam: true - } - })) - ); + // remove disabled filters + filters = (filters || []).filter(filter => !filter.disabled); let isEncrypted = false; let forwardTargets = new Map(); @@ -260,8 +212,6 @@ class FilterHandler { let matchingFilters = []; let filterActions = new Map(); - let spamScore = parseFloat([].concat(prepared.mimeTree.parsedHeader[spamScoreHeader.toLowerCase()] || []).shift(), 10) || 0; - filters // apply all filters to the message .map(filter => checkFilter(filter, prepared, maildata)) @@ -298,7 +248,28 @@ class FilterHandler { isSpam = false; filterActions.set('spam', false); } else if (!filterActions.has('spam')) { - isSpam = (userData.spamLevel / 100) * this.spamScoreValue * 2 <= spamScore; + let spamLevel; + switch (meta.spamAction) { + case 'reject': + spamLevel = 25; + break; + + case 'rewrite subject': + case 'soft reject': + spamLevel = 50; + break; + + case 'greylist': + case 'add header': + spamLevel = 75; + break; + + case 'no action': + default: + spamLevel = 100; + break; + } + isSpam = userData.spamLevel >= spamLevel; } if (isSpam && !filterActions.has('spam')) { @@ -328,8 +299,7 @@ class FilterHandler { return this.messageHandler.prepareMessage( { - raw: Buffer.concat([extraHeader, encrypted]), - indexedHeaders: this.spamHeaderKeys + raw: Buffer.concat([extraHeader, encrypted]) }, (err, preparedEncrypted) => { if (err) { @@ -376,9 +346,9 @@ class FilterHandler { (err, result) => { if (err) { // failed checks - log.error('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + userData._id.toString(), err.message); + log.error('Filter', 'FRWRDFAIL key=%s error=%s', 'wdf:' + userData._id.toString(), err.message); } else if (!result.success) { - log.silly('LMTP', 'FRWRDFAIL key=%s error=%s', 'wdf:' + userData._id.toString(), 'Precondition failed'); + log.silly('Filter', 'FRWRDFAIL key=%s error=%s', 'wdf:' + userData._id.toString(), 'Precondition failed'); return done(); } @@ -464,7 +434,7 @@ class FilterHandler { forwardMessage((err, id) => { if (err) { log.error( - 'LMTP', + 'Filter', '%s FRWRDFAIL from=%s to=%s target=%s error=%s', prepared.id.toString(), sender, @@ -483,7 +453,7 @@ class FilterHandler { }); outbound.push(id); log.silly( - 'LMTP', + 'Filter', '%s FRWRDOK id=%s from=%s to=%s target=%s', prepared.id.toString(), id, @@ -497,11 +467,11 @@ class FilterHandler { sendAutoreply((err, id) => { if (err) { - log.error('LMTP', '%s AUTOREPLYFAIL from=%s to=%s error=%s', prepared.id.toString(), '<>', sender, err.message); + log.error('Filter', '%s AUTOREPLYFAIL from=%s to=%s error=%s', prepared.id.toString(), '<>', sender, err.message); } else if (id) { filterResults.push({ autoreply: sender, 'autoreply-queue-id': id }); outbound.push(id); - log.silly('LMTP', '%s AUTOREPLYOK id=%s from=%s to=%s', prepared.id.toString(), id, '<>', sender); + log.silly('Filter', '%s AUTOREPLYOK id=%s from=%s to=%s', prepared.id.toString(), id, '<>', sender); } if (filterActions.get('delete')) { diff --git a/lib/message-handler.js b/lib/message-handler.js index da8e0cf7..77e1d6af 100644 --- a/lib/message-handler.js +++ b/lib/message-handler.js @@ -1160,9 +1160,8 @@ class MessageHandler { }); } - generateIndexedHeaders(headersArray, options) { + generateIndexedHeaders(headersArray) { // allow configuring extra header keys that are indexed - let indexedHeaders = options && options.indexedHeaders; return (headersArray || []) .map(line => { line = Buffer.from(line, 'binary').toString(); @@ -1172,7 +1171,7 @@ class MessageHandler { .trim() .toLowerCase(); - if (!INDEXED_HEADERS.includes(key) && (!indexedHeaders || !indexedHeaders.includes(key))) { + if (!INDEXED_HEADERS.includes(key)) { // do not index this header return false; } @@ -1260,7 +1259,7 @@ class MessageHandler { let msgid = envelope[9] || '<' + uuidV1() + '@wildduck.email>'; - let headers = this.generateIndexedHeaders(mimeTree.header, options); + let headers = this.generateIndexedHeaders(mimeTree.header); let prepared = { id, diff --git a/lib/tools.js b/lib/tools.js index 1be098d4..aca38934 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -356,49 +356,6 @@ function escapeRegexStr(string) { return string.replace(RegExp('[' + specials.join('\\') + ']', 'g'), '\\$&'); } -function prepareSpamChecks(spamHeader) { - return (Array.isArray(spamHeader) ? spamHeader : [].concat(spamHeader || [])) - .map(header => { - if (!header) { - return false; - } - - // If only a single header key is specified, check if it matches Yes - if (typeof header === 'string') { - header = { - key: header, - value: '^yes', - target: '\\Junk' - }; - } - - let key = (header.key || '') - .toString() - .trim() - .toLowerCase(); - let value = (header.value || '').toString().trim(); - try { - if (value) { - value = new RegExp(value, 'i'); - value.isRegex = true; - } - } catch (E) { - value = false; - //log.error('LMTP', 'Failed loading spam header rule %s. %s', JSON.stringify(header.value), E.message); - } - if (!key || !value) { - return false; - } - let target = (header.target || '').toString().trim() || 'INBOX'; - return { - key, - value, - target - }; - }) - .filter(check => check); -} - function getRelayData(url) { let urlparts = urllib.parse(url); let targetMx = { @@ -478,7 +435,6 @@ module.exports = { decodeAddresses, getMailboxCounter, getEmailTemplates, - prepareSpamChecks, getRelayData, isId, uview, diff --git a/lmtp.js b/lmtp.js index f6b508a7..163f404d 100644 --- a/lmtp.js +++ b/lmtp.js @@ -19,18 +19,8 @@ let messageHandler; let userHandler; let filterHandler; let loggelf; -let spamChecks, spamHeaderKeys; config.on('reload', () => { - spamChecks = tools.prepareSpamChecks(config.spamHeader); - spamHeaderKeys = spamChecks.map(check => check.key); - - if (filterHandler) { - filterHandler.spamChecks = spamChecks; - filterHandler.spamHeaderKeys = spamHeaderKeys; - filterHandler.spamScoreValue = config.lmtp.spamScore; - } - log.info('LMTP', 'Configuration reloaded'); }); @@ -241,9 +231,6 @@ module.exports = done => { gelf.emit('gelf.log', message); }; - spamChecks = tools.prepareSpamChecks(config.spamHeader); - spamHeaderKeys = spamChecks.map(check => check.key); - messageHandler = new MessageHandler({ database: db.database, users: db.users, @@ -265,9 +252,6 @@ module.exports = done => { db, sender: config.sender, messageHandler, - spamHeaderKeys, - spamChecks, - spamScoreValue: config.lmtp.spamScore, loggelf: message => loggelf(message) }); diff --git a/package.json b/package.json index 5ea10394..084527d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wildduck", - "version": "1.8.1", + "version": "1.9.0", "description": "IMAP/POP3 server built with Node.js and MongoDB", "main": "server.js", "scripts": { diff --git a/test/filtering-test.js b/test/filtering-test.js index a243641a..4bf7b9b4 100644 --- a/test/filtering-test.js +++ b/test/filtering-test.js @@ -330,202 +330,6 @@ describe('Send multiple messages', function() { ); }); - it('Send should send mail to spam', done => { - let recipients = ['user1@example.com', 'user2@example.com', 'user3@example.com', 'user4@example.com', 'user5@example.com']; - let subject = 'Test ööö message [' + Date.now() + ']'; - transporter.sendMail( - { - envelope: { - from: 'andris@kreata.ee', - to: recipients - }, - - headers: { - // set to Yes to send this message to Junk folder - 'x-rspamd-spam': 'Yes' - }, - - from: 'Kärbes 🐧 ', - to: recipients.map((rcpt, i) => ({ name: 'User #' + (i + 1), address: rcpt })), - subject, - text: 'Hello world! Current time is ' + new Date().toString(), - html: - '

Hello world! Current time is ' + - new Date().toString() + - '

', - attachments: [ - // attachment as plaintext - { - filename: 'notes.txt', - content: 'Some notes about this e-mail', - contentType: 'text/plain' // optional, would be detected from the filename - }, - - // Small Binary Buffer attachment, should be kept with message - { - filename: 'image.png', - content: Buffer.from( - 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD/' + - '//+l2Z/dAAAAM0lEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4U' + - 'g9C9zwz3gVLMDA/A6P9/AFGGFyjOXZtQAAAAAElFTkSuQmCC', - 'base64' - ), - - cid: 'note@example.com' // should be as unique as possible - }, - - // Large Binary Buffer attachment, should be kept separately - { - path: __dirname + '/../examples/swan.jpg', - filename: 'swän.jpg' - } - ] - }, - (err, info) => { - expect(err).to.not.exist; - expect(info.accepted).to.deep.equal(['user1@example.com', 'user2@example.com', 'user3@example.com', 'user4@example.com', 'user5@example.com']); - - let getFirstMessage = (userId, callback) => { - request(URL + '/users/' + userId + '/mailboxes', { json: true }, (err, meta, response) => { - expect(err).to.not.exist; - expect(response.success).to.be.true; - - let inbox = response.results.find(mbox => mbox.specialUse === '\\Junk'); - request(URL + '/users/' + userId + '/mailboxes/' + inbox.id + '/messages', { json: true }, (err, meta, response) => { - expect(err).to.not.exist; - expect(response.success).to.be.true; - - let message = response.results[0]; - expect(message).to.exist; - - request(URL + '/users/' + userId + '/mailboxes/' + inbox.id + '/messages/' + message.id, { json: true }, (err, meta, message) => { - expect(err).to.not.exist; - - let processAttachments = next => { - let pos = 0; - let getAttachments = () => { - if (pos >= message.attachments.length) { - return next(); - } - let attachment = message.attachments[pos++]; - request( - URL + - '/users/' + - message.user + - '/mailboxes/' + - message.mailbox + - '/messages/' + - message.id + - '/attachments/' + - attachment.id, - { encoding: null }, - (err, meta, raw) => { - expect(err).to.not.exist; - attachment.raw = raw; - setImmediate(getAttachments); - } - ); - }; - setImmediate(getAttachments); - }; - - processAttachments(() => { - request( - URL + '/users/' + userId + '/mailboxes/' + inbox.id + '/messages/' + message.id + '/message.eml', - (err, meta, raw) => { - expect(err).to.not.exist; - - message.raw = raw; - - simpleParser(raw, (err, parsed) => { - expect(err).to.not.exist; - message.parsed = parsed; - callback(null, message); - }); - } - ); - }); - }); - }); - }); - }; - - let checkNormalUsers = next => { - let npos = 0; - let nusers = [1, 4, 5]; - let checkUser = () => { - if (npos >= nusers.length) { - return next(); - } - let user = nusers[npos++]; - getFirstMessage(userIds[user - 1], (err, message) => { - expect(err).to.not.exist; - expect(message.subject).to.equal(subject); - expect(message.attachments.length).to.equal(3); - expect(message.parsed.attachments.length).to.equal(3); - for (let i = 0; i < message.attachments.length; i++) { - let hashA = crypto - .createHash('md5') - .update(message.attachments[i].raw) - .digest('hex'); - let hashB = crypto - .createHash('md5') - .update(message.parsed.attachments[i].content) - .digest('hex'); - expect(hashA).equal(hashB); - } - expect(message.parsed.to.value).deep.equal([ - { address: 'user1@example.com', name: 'User #1' }, - { address: 'user2@example.com', name: 'User #2' }, - { address: 'user3@example.com', name: 'User #3' }, - { address: 'user4@example.com', name: 'User #4' }, - { address: 'user5@example.com', name: 'User #5' } - ]); - expect(message.parsed.headers.get('delivered-to').value[0].address).equal('user' + user + '@example.com'); - - setImmediate(checkUser); - }); - }; - setImmediate(checkUser); - }; - - let checkEncryptedUsers = next => { - let npos = 0; - let nusers = [2, 3]; - let checkUser = () => { - if (npos >= nusers.length) { - return next(); - } - let user = nusers[npos++]; - getFirstMessage(userIds[user - 1], (err, message) => { - expect(err).to.not.exist; - - expect(message.subject).to.equal(subject); - expect(message.parsed.to.value).deep.equal([ - { address: 'user1@example.com', name: 'User #1' }, - { address: 'user2@example.com', name: 'User #2' }, - { address: 'user3@example.com', name: 'User #3' }, - { address: 'user4@example.com', name: 'User #4' }, - { address: 'user5@example.com', name: 'User #5' } - ]); - expect(message.parsed.headers.get('delivered-to').value[0].address).equal('user' + user + '@example.com'); - expect(message.parsed.attachments.length).equal(2); - expect(message.parsed.attachments[0].contentType).equal('application/pgp-encrypted'); - expect(message.parsed.attachments[0].content.toString()).equal('Version: 1\r\n'); - expect(message.parsed.attachments[1].contentType).equal('application/octet-stream'); - expect(message.parsed.attachments[1].filename).equal('encrypted.asc'); - expect(message.parsed.attachments[1].size).gte(1000000); - setImmediate(checkUser); - }); - }; - setImmediate(checkUser); - }; - - checkNormalUsers(() => checkEncryptedUsers(() => done())); - } - ); - }); - it('should fetch messages from IMAP', done => { let imagePng = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD/' +