This commit is contained in:
Andris Reinman 2017-07-21 15:36:09 +03:00
parent bb105ef3d3
commit 13469f5b74
3 changed files with 293 additions and 71 deletions

232
api.js
View file

@ -1547,7 +1547,9 @@ server.get({ name: 'messages', path: '/users/:user/mailboxes/:mailbox/messages'
ha: true, ha: true,
intro: true, intro: true,
unseen: true, unseen: true,
undeleted: true,
flagged: true, flagged: true,
draft: true,
thread: true thread: true
}, },
paginatedField: 'uid', paginatedField: 'uid',
@ -1616,8 +1618,10 @@ server.get({ name: 'messages', path: '/users/:user/mailboxes/:mailbox/messages'
date: messageData.hdate.toISOString(), date: messageData.hdate.toISOString(),
intro: messageData.intro, intro: messageData.intro,
attachments: !!messageData.ha, attachments: !!messageData.ha,
unseen: messageData.unseen, seen: !messageData.unseen,
flagged: messageData.flagged deleted: !messageData.undeleted,
flagged: messageData.flagged,
draft: messageData.draft
}; };
return response; return response;
}) })
@ -1715,7 +1719,9 @@ server.get({ name: 'search', path: '/users/:user/search' }, (req, res, next) =>
ha: true, ha: true,
intro: true, intro: true,
unseen: true, unseen: true,
undeleted: true,
flagged: true, flagged: true,
draft: true,
thread: true thread: true
}, },
paginatedField: '_id', paginatedField: '_id',
@ -1775,8 +1781,10 @@ server.get({ name: 'search', path: '/users/:user/search' }, (req, res, next) =>
date: messageData.hdate.toISOString(), date: messageData.hdate.toISOString(),
intro: messageData.intro, intro: messageData.intro,
attachments: !!messageData.ha, attachments: !!messageData.ha,
unseen: messageData.unseen, seen: !messageData.unseen,
flagged: messageData.flagged deleted: !messageData.undeleted,
flagged: messageData.flagged,
draft: messageData.draft
}; };
return response; return response;
}) })
@ -1831,9 +1839,13 @@ server.get('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next)
'mimeTree.parsedHeader': true, 'mimeTree.parsedHeader': true,
subject: true, subject: true,
msgid: true, msgid: true,
exp: true,
rdate: true,
ha: true, ha: true,
unseen: true, unseen: true,
undeleted: true,
flagged: true, flagged: true,
draft: true,
attachments: true, attachments: true,
map: true, map: true,
html: 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({ res.json({
success: true, success: true,
id: message.toString() + ':' + uid, id: message.toString() + ':' + uid,
@ -1910,6 +1927,11 @@ server.get('/users/:user/mailboxes/:mailbox/messages/:message', (req, res, next)
messageId: messageData.msgid, messageId: messageData.msgid,
date: messageData.hdate.toISOString(), date: messageData.hdate.toISOString(),
list, list,
expires,
seen: !messageData.unseen,
deleted: !messageData.undeleted,
flagged: messageData.flagged,
draft: messageData.draft,
html: messageData.html, html: messageData.html,
attachments: (messageData.attachments || []) attachments: (messageData.attachments || [])
.map(attachment => { .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) => { server.get('/users/:user/updates', (req, res, next) => {
res.charSet('utf-8'); res.charSet('utf-8');
@ -2158,13 +2376,17 @@ function loadJournalStream(req, res, user, lastEventId, done) {
} }
switch (e.command) { switch (e.command) {
case 'FETCH':
case 'EXISTS': case 'EXISTS':
case 'EXPUNGE': case 'EXPUNGE':
if (e.mailbox) { if (e.mailbox) {
mailboxes.add(e.mailbox.toString()); mailboxes.add(e.mailbox.toString());
} }
break; break;
case 'FETCH':
if (e.mailbox && (e.unseen || e.unseenChange)) {
mailboxes.add(e.mailbox.toString());
}
break;
} }
res.write(formatJournalData(e)); res.write(formatJournalData(e));

View file

@ -369,14 +369,14 @@ class MessageHandler {
return callback(err); return callback(err);
} }
let mailbox = item.value; let mailboxData = item.value;
let uid = mailbox.uidNext; let uid = mailboxData.uidNext;
let modseq = mailbox.modifyIndex + 1; let modseq = mailboxData.modifyIndex + 1;
this.database.collection('messages').findOneAndUpdate({ this.database.collection('messages').findOneAndUpdate({
_id: existing._id, _id: existing._id,
// hash key // hash key
mailbox: mailbox._id, mailbox: mailboxData._id,
uid: existing.uid uid: existing.uid
}, { }, {
$set: { $set: {
@ -398,15 +398,15 @@ class MessageHandler {
let updated = item.value; 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)); 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)); options.session.writeStream.write(options.session.formatResponse('EXISTS', updated.uid));
} }
this.notifier.addEntries( this.notifier.addEntries(
mailbox, mailboxData,
false, false,
{ {
command: 'EXPUNGE', command: 'EXPUNGE',
@ -417,7 +417,7 @@ class MessageHandler {
}, },
() => { () => {
this.notifier.addEntries( this.notifier.addEntries(
mailbox, mailboxData,
false, false,
{ {
command: 'EXISTS', command: 'EXISTS',
@ -428,9 +428,9 @@ class MessageHandler {
unseen: updated.unseen unseen: updated.unseen
}, },
() => { () => {
this.notifier.fire(mailbox.user, mailbox.path); this.notifier.fire(mailboxData.user, mailboxData.path);
return callback(null, true, { return callback(null, true, {
uidValidity: mailbox.uidValidity, uidValidity: mailboxData.uidValidity,
uid, uid,
id: existing._id, id: existing._id,
status: 'update' status: 'update'

View file

@ -1,58 +1,58 @@
{ {
"name": "wildduck", "name": "wildduck",
"version": "1.0.54", "version": "1.0.55",
"description": "IMAP server built with Node.js and MongoDB", "description": "IMAP server built with Node.js and MongoDB",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"test": "grunt" "test": "grunt"
}, },
"keywords": [], "keywords": [],
"author": "Andris Reinman", "author": "Andris Reinman",
"license": "EUPL-1.1", "license": "EUPL-1.1",
"devDependencies": { "devDependencies": {
"browserbox": "^0.9.1", "browserbox": "^0.9.1",
"chai": "^4.1.0", "chai": "^4.1.0",
"eslint-config-nodemailer": "^1.2.0", "eslint-config-nodemailer": "^1.2.0",
"grunt": "^1.0.1", "grunt": "^1.0.1",
"grunt-cli": "^1.2.0", "grunt-cli": "^1.2.0",
"grunt-eslint": "^20.0.0", "grunt-eslint": "^20.0.0",
"grunt-mocha-test": "^0.13.2", "grunt-mocha-test": "^0.13.2",
"mocha": "^3.4.2" "mocha": "^3.4.2"
}, },
"dependencies": { "dependencies": {
"addressparser": "^1.0.1", "addressparser": "^1.0.1",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"generate-password": "^1.3.0", "generate-password": "^1.3.0",
"html-to-text": "^3.3.0", "html-to-text": "^3.3.0",
"iconv-lite": "^0.4.18", "iconv-lite": "^0.4.18",
"joi": "^10.6.0", "joi": "^10.6.0",
"js-yaml": "^3.9.0", "js-yaml": "^3.9.0",
"libbase64": "^0.2.0", "libbase64": "^0.2.0",
"libmime": "^3.1.0", "libmime": "^3.1.0",
"libqp": "^1.1.0", "libqp": "^1.1.0",
"mailsplit": "^4.0.2", "mailsplit": "^4.0.2",
"mongo-cursor-pagination": "^5.0.0", "mongo-cursor-pagination": "^5.0.0",
"mongodb": "^2.2.30", "mongodb": "^2.2.30",
"node-redis-scripty": "0.0.5", "node-redis-scripty": "0.0.5",
"nodemailer": "^4.0.1", "nodemailer": "^4.0.1",
"npmlog": "^4.1.2", "npmlog": "^4.1.2",
"qrcode": "^0.8.2", "qrcode": "^0.8.2",
"redfour": "^1.0.2", "redfour": "^1.0.2",
"redis": "^2.7.1", "redis": "^2.7.1",
"restify": "^5.0.1", "restify": "^5.0.1",
"seq-index": "^1.1.0", "seq-index": "^1.1.0",
"smtp-server": "^3.0.1", "smtp-server": "^3.0.1",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"utf7": "^1.0.2", "utf7": "^1.0.2",
"uuid": "^3.1.0", "uuid": "^3.1.0",
"wild-config": "^1.0.0" "wild-config": "^1.0.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/wildduck-email/wildduck.git" "url": "git://github.com/wildduck-email/wildduck.git"
}, },
"optionalDependencies": { "optionalDependencies": {
"@ronomon/crypto-async": "^2.0.1", "@ronomon/crypto-async": "^2.0.1",
"modern-syslog": "^1.1.4" "modern-syslog": "^1.1.4"
} }
} }