From a671df6ed4af62496f59fdd5af2c9c906b2f2102 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Wed, 29 Mar 2017 21:22:26 +0300 Subject: [PATCH] Added support for MOVE --- README.md | 1 + examples/push-message.js | 2 +- imap-core/lib/commands/capability.js | 2 +- imap-core/lib/commands/move.js | 18 +--- imap.js | 135 ++++++++++++++++++++++++++- lib/imap-notifier.js | 4 +- 6 files changed, 139 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 65105119..f3d2d590 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Wild Duck IMAP server supports the following IMAP standards: - **UIDPLUS** - **SPECIAL-USE** - **ID** +- **MOVE** (RFC6851) - **AUTHENTICATE PLAIN** and **SASL-IR** - **APPENDLIMIT** (RFC7889) – maximum global allowed message size is advertised in CAPABILITY listing - **UTF8=ACCEPT** (RFC6855) – this also means that Wild Duck natively supports unicode email usernames. For example <андрис@уайлддак.орг> is a valid email address that is hosted by a test instance of Wild Duck diff --git a/examples/push-message.js b/examples/push-message.js index 8ed5bb15..6387f8dc 100644 --- a/examples/push-message.js +++ b/examples/push-message.js @@ -22,7 +22,7 @@ const transporter = nodemailer.createTransport({ }); let sent = 0; -let total = 10000; +let total = 5; let startTime = Date.now(); function send() { diff --git a/imap-core/lib/commands/capability.js b/imap-core/lib/commands/capability.js index 715186d2..ed37ba53 100644 --- a/imap-core/lib/commands/capability.js +++ b/imap-core/lib/commands/capability.js @@ -30,7 +30,7 @@ module.exports = { capabilities.push('UTF8=ACCEPT'); capabilities.push('QUOTA'); - // capabilities.push('MOVE'); + capabilities.push('MOVE'); if (this._server.options.maxMessage) { capabilities.push('APPENDLIMIT=' + this._server.options.maxMessage); diff --git a/imap-core/lib/commands/move.js b/imap-core/lib/commands/move.js index 53a507ac..d74df35e 100644 --- a/imap-core/lib/commands/move.js +++ b/imap-core/lib/commands/move.js @@ -1,7 +1,6 @@ 'use strict'; let imapTools = require('../imap-tools'); -let imapHandler = require('../handler/imap-handler'); module.exports = { state: 'Selected', @@ -48,27 +47,12 @@ module.exports = { return callback(err); } - let code = typeof success === 'string' ? success.toUpperCase() : false; - - if (success === true) { - this.send(imapHandler.compiler({ - tag: '*', - command: 'OK', - attributes: [{ - type: 'SECTION', - section: [{ - type: 'TEXT', - value: 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid) - }] - }] - })); - } + let code = typeof success === 'string' ? success.toUpperCase() : 'COPYUID ' + info.uidValidity + ' ' + imapTools.packMessageRange(info.sourceUid) + ' ' + imapTools.packMessageRange(info.destinationUid); callback(null, { response: success === true ? 'OK' : 'NO', code }); - }); } }; diff --git a/imap.js b/imap.js index 4d5c6e7c..10e9c22d 100644 --- a/imap.js +++ b/imap.js @@ -548,7 +548,6 @@ server.onStore = function (path, update, session, callback) { }); }; -// EXPUNGE deletes all messages in selected mailbox marked with \Delete // EXPUNGE deletes all messages in selected mailbox marked with \Delete server.onExpunge = function (path, update, session, callback) { this.logger.debug('[%s] Deleting messages from "%s"', session.id, path); @@ -690,7 +689,9 @@ server.onCopy = function (path, update, session, callback) { uid: { $in: update.messages } - }); // no projection as we need to copy the entire message + }).sort([ + ['uid', 1] + ]); // no projection as we need to copy the entire message let copiedMessages = 0; let copiedStorage = 0; @@ -800,6 +801,136 @@ server.onCopy = function (path, update, session, callback) { }); }; +// MOVE / UID MOVE sequence mailbox +server.onMove = function (path, update, session, callback) { + this.logger.debug('[%s] Moving messages from "%s" to "%s"', session.id, path, update.destination); + db.database.collection('mailboxes').findOne({ + user: session.user.id, + path + }, (err, mailbox) => { + if (err) { + return callback(err); + } + if (!mailbox) { + return callback(null, 'NONEXISTENT'); + } + + db.database.collection('mailboxes').findOne({ + user: session.user.id, + path: update.destination + }, (err, target) => { + if (err) { + return callback(err); + } + if (!target) { + return callback(null, 'TRYCREATE'); + } + + let cursor = db.database.collection('messages').find({ + mailbox: mailbox._id, + uid: { + $in: update.messages + } + }).project({ + uid: 1 + }).sort([ + ['uid', 1] + ]); + + let sourceUid = []; + let destinationUid = []; + + let processNext = () => { + cursor.next((err, message) => { + if (err) { + return callback(err); + } + if (!message) { + return cursor.close(() => { + db.database.collection('mailboxes').findOneAndUpdate({ + _id: mailbox._id + }, { + $inc: { + // increase the mailbox modification index + // to indicate that something happened + modifyIndex: 1 + } + }, { + uidNext: true + }, () => { + this.notifier.fire(session.user.id, target.path); + return callback(null, true, { + uidValidity: target.uidValidity, + sourceUid, + destinationUid + }); + }); + }); + } + + sourceUid.unshift(message.uid); + db.database.collection('mailboxes').findOneAndUpdate({ + _id: target._id + }, { + $inc: { + uidNext: 1 + } + }, { + uidNext: true + }, (err, item) => { + if (err) { + return callback(err); + } + + if (!item || !item.value) { + // was not able to acquire a lock + return callback(null, 'TRYCREATE'); + } + + let uidNext = item.value.uidNext; + destinationUid.unshift(uidNext); + + // update message, change mailbox from old to new one + db.database.collection('messages').findOneAndUpdate({ + _id: message._id + }, { + $set: { + mailbox: target._id, + // new mailbox means new UID + uid: uidNext, + // this will be changed later by the notification system + modseq: 0 + } + }, err => { + if (err) { + return callback(err); + } + + session.writeStream.write(session.formatResponse('EXPUNGE', message.uid)); + + // mark messages as deleted from old mailbox + this.notifier.addEntries(session.user.id, path, { + command: 'EXPUNGE', + ignore: session.id, + uid: message.uid + }, () => { + // mark messages as added to old mailbox + this.notifier.addEntries(session.user.id, target.path, { + command: 'EXISTS', + uid: uidNext, + message: message._id + }, processNext); + }); + }); + }); + }); + }; + + processNext(); + }); + }); +}; + // sends results to socket server.onFetch = function (path, options, session, callback) { this.logger.debug('[%s] Requested FETCH for "%s"', session.id, path); diff --git a/lib/imap-notifier.js b/lib/imap-notifier.js index 20cd104e..0a4ab8b8 100644 --- a/lib/imap-notifier.js +++ b/lib/imap-notifier.js @@ -210,10 +210,10 @@ class ImapNotifier extends EventEmitter { } let entry = entries[updated++]; - let setModseq = !!entry.modseq; + let setModseq = !entry.modseq; entry.mailbox = mailbox._id; - if (!setModseq) { + if (setModseq) { entry.modseq = ++startIndex; }