mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-06 21:24:37 +08:00
acl for mailbox listing
This commit is contained in:
parent
3b645d39f3
commit
fabbc597b9
4 changed files with 560 additions and 465 deletions
|
@ -17,6 +17,52 @@
|
|||
"read:any": ["*"],
|
||||
"update:any": ["*"],
|
||||
"delete:any": ["*"]
|
||||
},
|
||||
|
||||
"messages": {
|
||||
"create:any": ["*"],
|
||||
"read:any": ["*"],
|
||||
"update:any": ["*"],
|
||||
"delete:any": ["*"]
|
||||
},
|
||||
|
||||
"mailboxes": {
|
||||
"create:any": ["*"],
|
||||
"read:any": ["*"],
|
||||
"update:any": ["*"],
|
||||
"delete:any": ["*"]
|
||||
}
|
||||
},
|
||||
|
||||
"user": {
|
||||
"addresses": {
|
||||
"create:own": ["*"],
|
||||
"read:own": ["*"],
|
||||
"update:own": ["*"],
|
||||
"delete:own": ["*"]
|
||||
},
|
||||
|
||||
"authentication": {
|
||||
"read:own": ["*"]
|
||||
},
|
||||
|
||||
"users": {
|
||||
"read:own": ["*"],
|
||||
"update:own": ["*"]
|
||||
},
|
||||
|
||||
"messages": {
|
||||
"create:own": ["*"],
|
||||
"read:own": ["*"],
|
||||
"update:own": ["*"],
|
||||
"delete:own": ["*"]
|
||||
},
|
||||
|
||||
"mailboxes": {
|
||||
"create:own": ["*"],
|
||||
"read:own": ["*"],
|
||||
"update:own": ["*"],
|
||||
"delete:own": ["*"]
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -4,8 +4,12 @@ const Joi = require('joi');
|
|||
const ObjectID = require('mongodb').ObjectID;
|
||||
const imapTools = require('../../imap-core/lib/imap-tools');
|
||||
const tools = require('../tools');
|
||||
const roles = require('../roles');
|
||||
const util = require('util');
|
||||
|
||||
module.exports = (db, server, mailboxHandler) => {
|
||||
const getMailboxCounter = util.promisify(tools.getMailboxCounter);
|
||||
|
||||
/**
|
||||
* @api {get} /users/:user/mailboxes List Mailboxes for an User
|
||||
* @apiName GetMailboxes
|
||||
|
@ -69,160 +73,169 @@ module.exports = (db, server, mailboxHandler) => {
|
|||
* "error": "This mailbox does not exist"
|
||||
* }
|
||||
*/
|
||||
server.get('/users/:user/mailboxes', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
server.get(
|
||||
'/users/:user/mailboxes',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
counters: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false)
|
||||
});
|
||||
|
||||
if (req.query.counters) {
|
||||
req.params.counters = req.query.counters;
|
||||
}
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError'
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
counters: Joi.boolean()
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let user = new ObjectID(result.value.user);
|
||||
let counters = result.value.counters;
|
||||
if (req.query.counters) {
|
||||
req.params.counters = req.query.counters;
|
||||
}
|
||||
|
||||
db.users.collection('users').findOne(
|
||||
{
|
||||
_id: user
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
address: true
|
||||
}
|
||||
},
|
||||
(err, userData) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
if (!userData) {
|
||||
res.json({
|
||||
error: 'This user does not exist',
|
||||
code: 'UserNotFound'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
db.database
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).readOwn('mailboxes'));
|
||||
} else {
|
||||
req.validate(roles.can(req.role).readAny('mailboxes'));
|
||||
}
|
||||
|
||||
let user = new ObjectID(result.value.user);
|
||||
let counters = result.value.counters;
|
||||
|
||||
let userData;
|
||||
try {
|
||||
userData = await db.users.collection('users').findOne(
|
||||
{
|
||||
_id: user
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
address: true
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
if (!userData) {
|
||||
res.json({
|
||||
error: 'This user does not exist',
|
||||
code: 'UserNotFound'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let mailboxes;
|
||||
try {
|
||||
mailboxes = await db.database
|
||||
.collection('mailboxes')
|
||||
.find({
|
||||
user
|
||||
})
|
||||
.toArray((err, mailboxes) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!mailboxes) {
|
||||
mailboxes = [];
|
||||
}
|
||||
|
||||
let list = new Map();
|
||||
|
||||
mailboxes = mailboxes
|
||||
.map(mailbox => {
|
||||
list.set(mailbox.path, mailbox);
|
||||
return mailbox;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.path === 'INBOX') {
|
||||
return -1;
|
||||
}
|
||||
if (b.path === 'INBOX') {
|
||||
return 1;
|
||||
}
|
||||
if (a.path.indexOf('INBOX/') === 0 && b.path.indexOf('INBOX/') !== 0) {
|
||||
return -1;
|
||||
}
|
||||
if (a.path.indexOf('INBOX/') !== 0 && b.path.indexOf('INBOX/') === 0) {
|
||||
return 1;
|
||||
}
|
||||
if (a.subscribed !== b.subscribed) {
|
||||
return (a.subscribed ? 0 : 1) - (b.subscribed ? 0 : 1);
|
||||
}
|
||||
return a.path.localeCompare(b.path);
|
||||
});
|
||||
|
||||
let responses = [];
|
||||
let position = 0;
|
||||
let checkMailboxes = () => {
|
||||
if (position >= mailboxes.length) {
|
||||
res.json({
|
||||
success: true,
|
||||
results: responses
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
let mailbox = mailboxes[position++];
|
||||
let path = mailbox.path.split('/');
|
||||
let name = path.pop();
|
||||
|
||||
let response = {
|
||||
id: mailbox._id,
|
||||
name,
|
||||
path: mailbox.path,
|
||||
specialUse: mailbox.specialUse,
|
||||
modifyIndex: mailbox.modifyIndex,
|
||||
subscribed: mailbox.subscribed
|
||||
};
|
||||
|
||||
if (!counters) {
|
||||
responses.push(response);
|
||||
return setImmediate(checkMailboxes);
|
||||
}
|
||||
|
||||
tools.getMailboxCounter(db, mailbox._id, false, (err, total) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
tools.getMailboxCounter(db, mailbox._id, 'unseen', (err, unseen) => {
|
||||
if (err) {
|
||||
// ignore
|
||||
}
|
||||
response.total = total;
|
||||
response.unseen = unseen;
|
||||
responses.push(response);
|
||||
return setImmediate(checkMailboxes);
|
||||
});
|
||||
});
|
||||
};
|
||||
checkMailboxes();
|
||||
});
|
||||
.toArray();
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
if (!mailboxes) {
|
||||
mailboxes = [];
|
||||
}
|
||||
|
||||
let list = new Map();
|
||||
|
||||
mailboxes = mailboxes
|
||||
.map(mailbox => {
|
||||
list.set(mailbox.path, mailbox);
|
||||
return mailbox;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (a.path === 'INBOX') {
|
||||
return -1;
|
||||
}
|
||||
if (b.path === 'INBOX') {
|
||||
return 1;
|
||||
}
|
||||
if (a.path.indexOf('INBOX/') === 0 && b.path.indexOf('INBOX/') !== 0) {
|
||||
return -1;
|
||||
}
|
||||
if (a.path.indexOf('INBOX/') !== 0 && b.path.indexOf('INBOX/') === 0) {
|
||||
return 1;
|
||||
}
|
||||
if (a.subscribed !== b.subscribed) {
|
||||
return (a.subscribed ? 0 : 1) - (b.subscribed ? 0 : 1);
|
||||
}
|
||||
return a.path.localeCompare(b.path);
|
||||
});
|
||||
|
||||
let responses = [];
|
||||
|
||||
for (let mailboxData of mailboxes) {
|
||||
let path = mailboxData.path.split('/');
|
||||
let name = path.pop();
|
||||
|
||||
let response = {
|
||||
id: mailboxData._id,
|
||||
name,
|
||||
path: mailboxData.path,
|
||||
specialUse: mailboxData.specialUse,
|
||||
modifyIndex: mailboxData.modifyIndex,
|
||||
subscribed: mailboxData.subscribed
|
||||
};
|
||||
|
||||
if (!counters) {
|
||||
responses.push(response);
|
||||
continue;
|
||||
}
|
||||
|
||||
let total, unseen;
|
||||
|
||||
try {
|
||||
total = await getMailboxCounter(db, mailboxData._id, false);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
unseen = await tools.getMailboxCounter(db, mailboxData._id, 'unseen');
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
response.total = total;
|
||||
response.unseen = unseen;
|
||||
|
||||
responses.push(response);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
results: responses
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* @api {post} /users/:user/mailboxes Create new Mailbox
|
||||
|
|
|
@ -3,12 +3,14 @@
|
|||
const config = require('wild-config');
|
||||
const log = require('npmlog');
|
||||
const libmime = require('libmime');
|
||||
const util = require('util');
|
||||
const MailComposer = require('nodemailer/lib/mail-composer');
|
||||
const htmlToText = require('html-to-text');
|
||||
const Joi = require('../joi');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const tools = require('../tools');
|
||||
const maildrop = require('../maildrop');
|
||||
const roles = require('../roles');
|
||||
const Transform = require('stream').Transform;
|
||||
|
||||
class StreamCollect extends Transform {
|
||||
|
@ -539,6 +541,8 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
);
|
||||
}
|
||||
|
||||
const submitMessageWrapper = util.promisify(submitMessage);
|
||||
|
||||
/**
|
||||
* @api {post} /users/:user/submit Submit a Message for Delivery
|
||||
* @apiName PostSubmit
|
||||
|
@ -658,59 +662,81 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
* "code": "ERRDISABLEDUSER"
|
||||
* }
|
||||
*/
|
||||
server.post({ name: 'send', path: '/users/:user/submit' }, (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
server.post(
|
||||
{ name: 'send', path: '/users/:user/submit' },
|
||||
tools.asyncifyJson(async (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),
|
||||
|
||||
reference: Joi.object().keys({
|
||||
mailbox: Joi.string()
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
id: Joi.number().required(),
|
||||
action: Joi.string()
|
||||
.valid('reply', 'replyAll', 'forward')
|
||||
.required()
|
||||
}),
|
||||
|
||||
// if true then treat this message as a draft
|
||||
isDraft: Joi.boolean()
|
||||
.empty('')
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false),
|
||||
|
||||
// if set then this message is based on a draft that should be deleted after processing
|
||||
draft: Joi.object().keys({
|
||||
mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
id: Joi.number().required()
|
||||
}),
|
||||
.length(24),
|
||||
|
||||
uploadOnly: Joi.boolean()
|
||||
.empty('')
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false),
|
||||
reference: Joi.object().keys({
|
||||
mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
id: Joi.number().required(),
|
||||
action: Joi.string()
|
||||
.valid('reply', 'replyAll', 'forward')
|
||||
.required()
|
||||
}),
|
||||
|
||||
sendTime: Joi.date(),
|
||||
// if true then treat this message as a draft
|
||||
isDraft: Joi.boolean()
|
||||
.empty('')
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false),
|
||||
|
||||
// if set then this message is based on a draft that should be deleted after processing
|
||||
draft: Joi.object().keys({
|
||||
mailbox: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
id: Joi.number().required()
|
||||
}),
|
||||
|
||||
uploadOnly: Joi.boolean()
|
||||
.empty('')
|
||||
.truthy(['Y', 'true', 'yes', 'on', 1])
|
||||
.falsy(['N', 'false', 'no', 'off', 0, ''])
|
||||
.default(false),
|
||||
|
||||
sendTime: Joi.date(),
|
||||
|
||||
envelope: Joi.object().keys({
|
||||
from: Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
}),
|
||||
to: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
})
|
||||
)
|
||||
}),
|
||||
|
||||
envelope: Joi.object().keys({
|
||||
from: Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
|
@ -719,6 +745,16 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
.email()
|
||||
.required()
|
||||
}),
|
||||
|
||||
replyTo: Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
}),
|
||||
|
||||
to: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
|
@ -728,139 +764,120 @@ module.exports = (db, server, messageHandler, userHandler) => {
|
|||
.email()
|
||||
.required()
|
||||
})
|
||||
)
|
||||
}),
|
||||
),
|
||||
|
||||
from: Joi.object().keys({
|
||||
name: Joi.string()
|
||||
cc: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
})
|
||||
),
|
||||
|
||||
bcc: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
})
|
||||
),
|
||||
|
||||
headers: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
key: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
value: Joi.string()
|
||||
.empty('')
|
||||
.max(100 * 1024)
|
||||
})
|
||||
),
|
||||
|
||||
subject: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
}),
|
||||
|
||||
replyTo: Joi.object().keys({
|
||||
name: Joi.string()
|
||||
text: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
}),
|
||||
.max(1024 * 1024),
|
||||
html: Joi.string()
|
||||
.empty('')
|
||||
.max(1024 * 1024),
|
||||
|
||||
to: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
attachments: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
filename: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
contentType: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
encoding: Joi.string()
|
||||
.empty('')
|
||||
.default('base64'),
|
||||
content: Joi.string().required(),
|
||||
cid: Joi.string()
|
||||
.empty('')
|
||||
.max(255)
|
||||
})
|
||||
),
|
||||
meta: Joi.object().unknown(true),
|
||||
sess: Joi.string().max(255),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
})
|
||||
),
|
||||
|
||||
cc: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
})
|
||||
),
|
||||
|
||||
bcc: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
name: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
address: Joi.string()
|
||||
.email()
|
||||
.required()
|
||||
})
|
||||
),
|
||||
|
||||
headers: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
key: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
value: Joi.string()
|
||||
.empty('')
|
||||
.max(100 * 1024)
|
||||
})
|
||||
),
|
||||
|
||||
subject: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
text: Joi.string()
|
||||
.empty('')
|
||||
.max(1024 * 1024),
|
||||
html: Joi.string()
|
||||
.empty('')
|
||||
.max(1024 * 1024),
|
||||
|
||||
attachments: Joi.array().items(
|
||||
Joi.object().keys({
|
||||
filename: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
contentType: Joi.string()
|
||||
.empty('')
|
||||
.max(255),
|
||||
encoding: Joi.string()
|
||||
.empty('')
|
||||
.default('base64'),
|
||||
content: Joi.string().required(),
|
||||
cid: Joi.string()
|
||||
.empty('')
|
||||
.max(255)
|
||||
})
|
||||
),
|
||||
meta: Joi.object().unknown(true),
|
||||
sess: Joi.string().max(255),
|
||||
ip: Joi.string().ip({
|
||||
version: ['ipv4', 'ipv6'],
|
||||
cidr: 'forbidden'
|
||||
})
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true,
|
||||
allowUnknown: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
result.value.user = new ObjectID(result.value.user);
|
||||
if (result.value.reference && result.value.reference.mailbox) {
|
||||
result.value.reference.mailbox = new ObjectID(result.value.reference.mailbox);
|
||||
}
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true,
|
||||
allowUnknown: true
|
||||
});
|
||||
|
||||
submitMessage(result.value, (err, info) => {
|
||||
if (err) {
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).createOwn('messages'));
|
||||
} else {
|
||||
req.validate(roles.can(req.role).createAny('messages'));
|
||||
}
|
||||
|
||||
result.value.user = new ObjectID(result.value.user);
|
||||
if (result.value.reference && result.value.reference.mailbox) {
|
||||
result.value.reference.mailbox = new ObjectID(result.value.reference.mailbox);
|
||||
}
|
||||
|
||||
let info;
|
||||
try {
|
||||
info = await submitMessageWrapper(result.value);
|
||||
} catch (err) {
|
||||
log.error('API', 'SUBMIT error=%s', err.message);
|
||||
res.json({
|
||||
error: err.message,
|
||||
code: err.code
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
message: info
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: info
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ const crypto = require('crypto');
|
|||
const Joi = require('joi');
|
||||
const ObjectID = require('mongodb').ObjectID;
|
||||
const tools = require('../tools');
|
||||
const roles = require('../roles');
|
||||
const base32 = require('base32.js');
|
||||
|
||||
module.exports = (db, server, notifier) => {
|
||||
|
@ -48,171 +49,189 @@ module.exports = (db, server, notifier) => {
|
|||
* "error": "This user does not exist"
|
||||
* }
|
||||
*/
|
||||
server.get('/users/:user/updates', (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
server.get(
|
||||
'/users/:user/updates',
|
||||
tools.asyncifyJson(async (req, res, next) => {
|
||||
res.charSet('utf-8');
|
||||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
'Last-Event-ID': Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
});
|
||||
|
||||
if (req.header('Last-Event-ID')) {
|
||||
req.params['Last-Event-ID'] = req.header('Last-Event-ID');
|
||||
}
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError'
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
.required(),
|
||||
'Last-Event-ID': Joi.string()
|
||||
.hex()
|
||||
.lowercase()
|
||||
.length(24)
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let user = new ObjectID(result.value.user);
|
||||
let lastEventId = result.value['Last-Event-ID'] ? new ObjectID(result.value['Last-Event-ID']) : false;
|
||||
|
||||
db.users.collection('users').findOne(
|
||||
{
|
||||
_id: user
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
username: true,
|
||||
address: true
|
||||
}
|
||||
},
|
||||
(err, userData) => {
|
||||
if (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
if (!userData) {
|
||||
res.json({
|
||||
error: 'This user does not exist',
|
||||
code: 'UserNotFound'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let session = {
|
||||
id: 'api.' + base32.encode(crypto.randomBytes(10)).toLowerCase(),
|
||||
user: {
|
||||
id: userData._id,
|
||||
username: userData.username
|
||||
}
|
||||
};
|
||||
|
||||
let closed = false;
|
||||
let idleTimer = false;
|
||||
let idleCounter = 0;
|
||||
|
||||
let sendIdleComment = () => {
|
||||
clearTimeout(idleTimer);
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
res.write(': idling ' + ++idleCounter + '\n\n');
|
||||
idleTimer = setTimeout(sendIdleComment, 15 * 1000);
|
||||
};
|
||||
|
||||
let resetIdleComment = () => {
|
||||
clearTimeout(idleTimer);
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
idleTimer = setTimeout(sendIdleComment, 15 * 1000);
|
||||
};
|
||||
|
||||
let journalReading = false;
|
||||
let journalReader = message => {
|
||||
if (journalReading || closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
return res.write(formatJournalData(message));
|
||||
}
|
||||
|
||||
journalReading = true;
|
||||
loadJournalStream(db, req, res, user, lastEventId, (err, info) => {
|
||||
if (err) {
|
||||
// ignore?
|
||||
}
|
||||
lastEventId = info && info.lastEventId;
|
||||
journalReading = false;
|
||||
if (info && info.processed) {
|
||||
resetIdleComment();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let close = () => {
|
||||
closed = true;
|
||||
clearTimeout(idleTimer);
|
||||
notifier.removeListener(session, journalReader);
|
||||
};
|
||||
|
||||
let setup = () => {
|
||||
notifier.addListener(session, journalReader);
|
||||
|
||||
let finished = false;
|
||||
let done = () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
close();
|
||||
return next();
|
||||
};
|
||||
|
||||
req.connection.setTimeout(30 * 60 * 1000, done);
|
||||
req.connection.on('end', done);
|
||||
req.connection.on('close', done);
|
||||
req.connection.on('error', done);
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
||||
|
||||
if (lastEventId) {
|
||||
loadJournalStream(db, req, res, user, lastEventId, (err, info) => {
|
||||
if (err) {
|
||||
res.write('event: error\ndata: ' + err.message.split('\n').join('\ndata: ') + '\n\n');
|
||||
// ignore
|
||||
}
|
||||
setup();
|
||||
if (info && info.processed) {
|
||||
resetIdleComment();
|
||||
} else {
|
||||
sendIdleComment();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
db.database.collection('journal').findOne({ user }, { sort: { _id: -1 } }, (err, latest) => {
|
||||
if (!err && latest) {
|
||||
lastEventId = latest._id;
|
||||
}
|
||||
setup();
|
||||
sendIdleComment();
|
||||
});
|
||||
}
|
||||
if (req.header('Last-Event-ID')) {
|
||||
req.params['Last-Event-ID'] = req.header('Last-Event-ID');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const result = Joi.validate(req.params, schema, {
|
||||
abortEarly: false,
|
||||
convert: true
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
res.json({
|
||||
error: result.error.message,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
// permissions check
|
||||
// should the resource be something else than 'users'?
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).readOwn('users'));
|
||||
} else {
|
||||
req.validate(roles.can(req.role).readAny('users'));
|
||||
}
|
||||
|
||||
let user = new ObjectID(result.value.user);
|
||||
let lastEventId = result.value['Last-Event-ID'] ? new ObjectID(result.value['Last-Event-ID']) : false;
|
||||
|
||||
let userData;
|
||||
|
||||
try {
|
||||
userData = await db.users.collection('users').findOne(
|
||||
{
|
||||
_id: user
|
||||
},
|
||||
{
|
||||
projection: {
|
||||
username: true,
|
||||
address: true
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: 'MongoDB Error: ' + err.message,
|
||||
code: 'InternalDatabaseError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
if (!userData) {
|
||||
res.json({
|
||||
error: 'This user does not exist',
|
||||
code: 'UserNotFound'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
|
||||
let session = {
|
||||
id: 'api.' + base32.encode(crypto.randomBytes(10)).toLowerCase(),
|
||||
user: {
|
||||
id: userData._id,
|
||||
username: userData.username
|
||||
}
|
||||
};
|
||||
|
||||
let closed = false;
|
||||
let idleTimer = false;
|
||||
let idleCounter = 0;
|
||||
|
||||
let sendIdleComment = () => {
|
||||
clearTimeout(idleTimer);
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
res.write(': idling ' + ++idleCounter + '\n\n');
|
||||
idleTimer = setTimeout(sendIdleComment, 15 * 1000);
|
||||
};
|
||||
|
||||
let resetIdleComment = () => {
|
||||
clearTimeout(idleTimer);
|
||||
if (closed) {
|
||||
return;
|
||||
}
|
||||
idleTimer = setTimeout(sendIdleComment, 15 * 1000);
|
||||
};
|
||||
|
||||
let journalReading = false;
|
||||
let journalReader = message => {
|
||||
if (journalReading || closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
return res.write(formatJournalData(message));
|
||||
}
|
||||
|
||||
journalReading = true;
|
||||
loadJournalStream(db, req, res, user, lastEventId, (err, info) => {
|
||||
if (err) {
|
||||
// ignore?
|
||||
}
|
||||
lastEventId = info && info.lastEventId;
|
||||
journalReading = false;
|
||||
if (info && info.processed) {
|
||||
resetIdleComment();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let close = () => {
|
||||
closed = true;
|
||||
clearTimeout(idleTimer);
|
||||
notifier.removeListener(session, journalReader);
|
||||
};
|
||||
|
||||
let setup = () => {
|
||||
notifier.addListener(session, journalReader);
|
||||
|
||||
let finished = false;
|
||||
let done = () => {
|
||||
if (finished) {
|
||||
return;
|
||||
}
|
||||
finished = true;
|
||||
close();
|
||||
return next();
|
||||
};
|
||||
|
||||
// force close after 30 min, otherwise we might end with connections that never close
|
||||
req.connection.setTimeout(30 * 60 * 1000, done);
|
||||
req.connection.on('end', done);
|
||||
req.connection.on('close', done);
|
||||
req.connection.on('error', done);
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
||||
|
||||
if (lastEventId) {
|
||||
loadJournalStream(db, req, res, user, lastEventId, (err, info) => {
|
||||
if (err) {
|
||||
res.write('event: error\ndata: ' + err.message.split('\n').join('\ndata: ') + '\n\n');
|
||||
// ignore
|
||||
}
|
||||
setup();
|
||||
if (info && info.processed) {
|
||||
resetIdleComment();
|
||||
} else {
|
||||
sendIdleComment();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
let latest;
|
||||
try {
|
||||
latest = await db.database.collection('journal').findOne({ user }, { sort: { _id: -1 } });
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
if (latest) {
|
||||
lastEventId = latest._id;
|
||||
}
|
||||
|
||||
setup();
|
||||
sendIdleComment();
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
function formatJournalData(e) {
|
||||
|
|
Loading…
Add table
Reference in a new issue