From 13469f5b74f8585e85aa3ad814179db7a5ef43a9 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Fri, 21 Jul 2017 15:36:09 +0300 Subject: [PATCH] v1.0.55 --- api.js | 232 ++++++++++++++++++++++++++++++++++++++++- lib/message-handler.js | 20 ++-- package.json | 112 ++++++++++---------- 3 files changed, 293 insertions(+), 71 deletions(-) diff --git a/api.js b/api.js index 54a7c8fc..19438705 100644 --- a/api.js +++ b/api.js @@ -1547,7 +1547,9 @@ server.get({ name: 'messages', path: '/users/:user/mailboxes/:mailbox/messages' ha: true, intro: true, unseen: true, + undeleted: true, flagged: true, + draft: true, thread: true }, paginatedField: 'uid', @@ -1616,8 +1618,10 @@ server.get({ name: 'messages', path: '/users/:user/mailboxes/:mailbox/messages' date: messageData.hdate.toISOString(), intro: messageData.intro, attachments: !!messageData.ha, - unseen: messageData.unseen, - flagged: messageData.flagged + seen: !messageData.unseen, + deleted: !messageData.undeleted, + flagged: messageData.flagged, + draft: messageData.draft }; return response; }) @@ -1715,7 +1719,9 @@ server.get({ name: 'search', path: '/users/:user/search' }, (req, res, next) => ha: true, intro: true, unseen: true, + undeleted: true, flagged: true, + draft: true, thread: true }, paginatedField: '_id', @@ -1775,8 +1781,10 @@ server.get({ name: 'search', path: '/users/:user/search' }, (req, res, next) => date: messageData.hdate.toISOString(), intro: messageData.intro, attachments: !!messageData.ha, - unseen: messageData.unseen, - flagged: messageData.flagged + seen: !messageData.unseen, + deleted: !messageData.undeleted, + flagged: messageData.flagged, + draft: messageData.draft }; return response; }) @@ -1831,9 +1839,13 @@ server.get('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next) 'mimeTree.parsedHeader': true, subject: true, msgid: true, + exp: true, + rdate: true, ha: true, unseen: true, + undeleted: true, flagged: true, + draft: true, attachments: true, map: true, html: true @@ -1899,6 +1911,11 @@ server.get('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next) }; } + let expires; + if (messageData.exp) { + expires = new Date(messageData.rdate).toISOString(); + } + res.json({ success: true, id: message.toString() + ':' + uid, @@ -1910,6 +1927,11 @@ server.get('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next) messageId: messageData.msgid, date: messageData.hdate.toISOString(), list, + expires, + seen: !messageData.unseen, + deleted: !messageData.undeleted, + flagged: messageData.flagged, + draft: messageData.draft, html: messageData.html, attachments: (messageData.attachments || []) .map(attachment => { @@ -1931,6 +1953,202 @@ server.get('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next) }); }); +server.put('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next) => { + res.charSet('utf-8'); + + const schema = Joi.object().keys({ + user: Joi.string().hex().lowercase().length(24).required(), + mailbox: Joi.string().hex().lowercase().length(24).required(), + message: Joi.string().regex(/^[0-9a-f]{24}:\d{1,10}/).lowercase().required(), + seen: Joi.boolean().truthy(['Y', 'true', 'yes', 1]), + deleted: Joi.boolean().truthy(['Y', 'true', 'yes', 1]), + flagged: Joi.boolean().truthy(['Y', 'true', 'yes', 1]), + draft: Joi.boolean().truthy(['Y', 'true', 'yes', 1]), + expires: Joi.alternatives().try(Joi.date(), Joi.boolean().truthy(['Y', 'true', 'yes', 1]).allow(false)) + }); + + const result = Joi.validate(req.params, schema, { + abortEarly: false, + convert: true + }); + + if (result.error) { + res.json({ + error: result.error.message + }); + return next(); + } + + let messageparts = result.value.message.split(':'); + let user = new ObjectID(result.value.user); + let mailbox = new ObjectID(result.value.mailbox); + let message = new ObjectID(messageparts[0]); + let uid = Number(messageparts[1]); + + let updates = { $set: {} }; + let update = false; + let addFlags = []; + let removeFlags = []; + + Object.keys(result.value || {}).forEach(key => { + switch (key) { + case 'seen': + updates.$set.unseen = !result.value.seen; + if (result.value.seen) { + addFlags.push('\\Seen'); + } else { + removeFlags.push('\\Seen'); + } + update = true; + break; + + case 'deleted': + updates.$set.undeleted = !result.value.deleted; + if (result.value.deleted) { + addFlags.push('\\Deleted'); + } else { + removeFlags.push('\\Deleted'); + } + update = true; + break; + + case 'flagged': + updates.$set.flagged = result.value.flagged; + if (result.value.flagged) { + addFlags.push('\\Flagged'); + } else { + removeFlags.push('\\Flagged'); + } + update = true; + break; + + case 'draft': + updates.$set.flagged = result.value.draft; + if (result.value.draft) { + addFlags.push('\\Draft'); + } else { + removeFlags.push('\\Draft'); + } + update = true; + break; + + case 'expires': + if (result.value.expires) { + updates.$set.exp = true; + updates.$set.rdate = result.value.expires.getTime(); + } else { + updates.$set.exp = false; + } + update = true; + break; + } + }); + + if (!update) { + res.json({ + error: 'Nothing was changed' + }); + return next(); + } + + if (addFlags.length) { + if (!updates.$addToSet) { + updates.$addToSet = {}; + } + updates.$addToSet.flags = { $each: addFlags }; + } + + if (removeFlags.length) { + if (!updates.$pull) { + updates.$pull = {}; + } + updates.$pull.flags = { $in: removeFlags }; + } + + // acquire new MODSEQ + db.database.collection('mailboxes').findOneAndUpdate({ + _id: mailbox, + user + }, { + $inc: { + // allocate new MODSEQ value + modifyIndex: 1 + } + }, { + returnOriginal: false + }, (err, item) => { + if (err) { + res.json({ + error: err.message + }); + return next(); + } + + if (!item || !item.value) { + // was not able to acquire a lock + res.json({ + error: 'Mailbox is missing' + }); + return next(); + } + + let mailboxData = item.value; + + updates.$set.modseq = mailboxData.modifyIndex; + + db.database.collection('messages').findOneAndUpdate({ + _id: message, + // hash key + mailbox, + uid + }, updates, { + projection: { + flags: true, + exp: true, + rdate: true + }, + returnOriginal: false + }, (err, item) => { + if (err) { + res.json({ + error: err.message + }); + return next(); + } + + if (!item || !item.value) { + // message was not found for whatever reason + res.json({ + error: 'Message was not found' + }); + return next(); + } + + let messageData = item.value; + + notifier.addEntries( + mailboxData, + false, + { + command: 'FETCH', + uid, + flags: messageData.flags, + message: message._id, + unseenChange: !!result.value.unseen + }, + () => { + notifier.fire(mailboxData.user, mailboxData.path); + + res.json({ + success: true + }); + return next(); + } + ); + }); + }); +}); + server.get('/users/:user/updates', (req, res, next) => { res.charSet('utf-8'); @@ -2158,13 +2376,17 @@ function loadJournalStream(req, res, user, lastEventId, done) { } switch (e.command) { - case 'FETCH': case 'EXISTS': case 'EXPUNGE': if (e.mailbox) { mailboxes.add(e.mailbox.toString()); } break; + case 'FETCH': + if (e.mailbox && (e.unseen || e.unseenChange)) { + mailboxes.add(e.mailbox.toString()); + } + break; } res.write(formatJournalData(e)); diff --git a/lib/message-handler.js b/lib/message-handler.js index 6a2f7139..c98f0a30 100644 --- a/lib/message-handler.js +++ b/lib/message-handler.js @@ -369,14 +369,14 @@ class MessageHandler { return callback(err); } - let mailbox = item.value; - let uid = mailbox.uidNext; - let modseq = mailbox.modifyIndex + 1; + let mailboxData = item.value; + let uid = mailboxData.uidNext; + let modseq = mailboxData.modifyIndex + 1; this.database.collection('messages').findOneAndUpdate({ _id: existing._id, // hash key - mailbox: mailbox._id, + mailbox: mailboxData._id, uid: existing.uid }, { $set: { @@ -398,15 +398,15 @@ class MessageHandler { let updated = item.value; - if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { + if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) { options.session.writeStream.write(options.session.formatResponse('EXPUNGE', existing.uid)); } - if (options.session && options.session.selected && options.session.selected.mailbox === mailbox.path) { + if (options.session && options.session.selected && options.session.selected.mailbox === mailboxData.path) { options.session.writeStream.write(options.session.formatResponse('EXISTS', updated.uid)); } this.notifier.addEntries( - mailbox, + mailboxData, false, { command: 'EXPUNGE', @@ -417,7 +417,7 @@ class MessageHandler { }, () => { this.notifier.addEntries( - mailbox, + mailboxData, false, { command: 'EXISTS', @@ -428,9 +428,9 @@ class MessageHandler { unseen: updated.unseen }, () => { - this.notifier.fire(mailbox.user, mailbox.path); + this.notifier.fire(mailboxData.user, mailboxData.path); return callback(null, true, { - uidValidity: mailbox.uidValidity, + uidValidity: mailboxData.uidValidity, uid, id: existing._id, status: 'update' diff --git a/package.json b/package.json index 6a9c5ceb..2473c08d 100644 --- a/package.json +++ b/package.json @@ -1,58 +1,58 @@ { - "name": "wildduck", - "version": "1.0.54", - "description": "IMAP server built with Node.js and MongoDB", - "main": "server.js", - "scripts": { - "test": "grunt" - }, - "keywords": [], - "author": "Andris Reinman", - "license": "EUPL-1.1", - "devDependencies": { - "browserbox": "^0.9.1", - "chai": "^4.1.0", - "eslint-config-nodemailer": "^1.2.0", - "grunt": "^1.0.1", - "grunt-cli": "^1.2.0", - "grunt-eslint": "^20.0.0", - "grunt-mocha-test": "^0.13.2", - "mocha": "^3.4.2" - }, - "dependencies": { - "addressparser": "^1.0.1", - "bcryptjs": "^2.4.3", - "generate-password": "^1.3.0", - "html-to-text": "^3.3.0", - "iconv-lite": "^0.4.18", - "joi": "^10.6.0", - "js-yaml": "^3.9.0", - "libbase64": "^0.2.0", - "libmime": "^3.1.0", - "libqp": "^1.1.0", - "mailsplit": "^4.0.2", - "mongo-cursor-pagination": "^5.0.0", - "mongodb": "^2.2.30", - "node-redis-scripty": "0.0.5", - "nodemailer": "^4.0.1", - "npmlog": "^4.1.2", - "qrcode": "^0.8.2", - "redfour": "^1.0.2", - "redis": "^2.7.1", - "restify": "^5.0.1", - "seq-index": "^1.1.0", - "smtp-server": "^3.0.1", - "speakeasy": "^2.0.0", - "utf7": "^1.0.2", - "uuid": "^3.1.0", - "wild-config": "^1.0.0" - }, - "repository": { - "type": "git", - "url": "git://github.com/wildduck-email/wildduck.git" - }, - "optionalDependencies": { - "@ronomon/crypto-async": "^2.0.1", - "modern-syslog": "^1.1.4" - } + "name": "wildduck", + "version": "1.0.55", + "description": "IMAP server built with Node.js and MongoDB", + "main": "server.js", + "scripts": { + "test": "grunt" + }, + "keywords": [], + "author": "Andris Reinman", + "license": "EUPL-1.1", + "devDependencies": { + "browserbox": "^0.9.1", + "chai": "^4.1.0", + "eslint-config-nodemailer": "^1.2.0", + "grunt": "^1.0.1", + "grunt-cli": "^1.2.0", + "grunt-eslint": "^20.0.0", + "grunt-mocha-test": "^0.13.2", + "mocha": "^3.4.2" + }, + "dependencies": { + "addressparser": "^1.0.1", + "bcryptjs": "^2.4.3", + "generate-password": "^1.3.0", + "html-to-text": "^3.3.0", + "iconv-lite": "^0.4.18", + "joi": "^10.6.0", + "js-yaml": "^3.9.0", + "libbase64": "^0.2.0", + "libmime": "^3.1.0", + "libqp": "^1.1.0", + "mailsplit": "^4.0.2", + "mongo-cursor-pagination": "^5.0.0", + "mongodb": "^2.2.30", + "node-redis-scripty": "0.0.5", + "nodemailer": "^4.0.1", + "npmlog": "^4.1.2", + "qrcode": "^0.8.2", + "redfour": "^1.0.2", + "redis": "^2.7.1", + "restify": "^5.0.1", + "seq-index": "^1.1.0", + "smtp-server": "^3.0.1", + "speakeasy": "^2.0.0", + "utf7": "^1.0.2", + "uuid": "^3.1.0", + "wild-config": "^1.0.0" + }, + "repository": { + "type": "git", + "url": "git://github.com/wildduck-email/wildduck.git" + }, + "optionalDependencies": { + "@ronomon/crypto-async": "^2.0.1", + "modern-syslog": "^1.1.4" + } }