2017-03-06 22:13:40 +08:00
|
|
|
'use strict';
|
|
|
|
|
2017-07-16 19:37:33 +08:00
|
|
|
const config = require('wild-config');
|
2017-03-06 22:13:40 +08:00
|
|
|
const restify = require('restify');
|
|
|
|
const log = require('npmlog');
|
|
|
|
const Joi = require('joi');
|
|
|
|
const bcrypt = require('bcryptjs');
|
2017-07-18 22:38:05 +08:00
|
|
|
const crypto = require('crypto');
|
2017-03-21 06:07:23 +08:00
|
|
|
const tools = require('./lib/tools');
|
2017-07-18 16:17:36 +08:00
|
|
|
const consts = require('./lib/consts');
|
2017-04-21 04:32:19 +08:00
|
|
|
const UserHandler = require('./lib/user-handler');
|
2017-07-18 22:38:05 +08:00
|
|
|
const ImapNotifier = require('./lib/imap-notifier');
|
2017-03-27 15:36:45 +08:00
|
|
|
const db = require('./lib/db');
|
2017-07-20 18:10:43 +08:00
|
|
|
const MongoPaging = require('mongo-cursor-pagination');
|
2017-07-17 21:32:31 +08:00
|
|
|
const certs = require('./lib/certs').get('api');
|
|
|
|
const ObjectID = require('mongodb').ObjectID;
|
2017-03-06 22:13:40 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const serverOptions = {
|
2017-07-19 16:06:47 +08:00
|
|
|
name: 'Wild Duck API',
|
2017-07-20 18:10:43 +08:00
|
|
|
strictRouting: true,
|
|
|
|
formatters: {
|
|
|
|
'application/json; q=0.4': (req, res, body) => {
|
2017-07-20 21:10:36 +08:00
|
|
|
let data = body ? JSON.stringify(body, false, 2) + '\n' : 'null';
|
2017-07-20 18:10:43 +08:00
|
|
|
res.setHeader('Content-Length', Buffer.byteLength(data));
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
}
|
2017-07-17 21:32:31 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (certs && config.api.secure) {
|
|
|
|
serverOptions.key = certs.key;
|
|
|
|
if (certs.ca) {
|
|
|
|
serverOptions.ca = certs.ca;
|
|
|
|
}
|
|
|
|
serverOptions.certificate = certs.cert;
|
|
|
|
}
|
|
|
|
|
|
|
|
const server = restify.createServer(serverOptions);
|
2017-03-06 22:13:40 +08:00
|
|
|
|
2017-04-21 04:32:19 +08:00
|
|
|
let userHandler;
|
2017-07-18 22:38:05 +08:00
|
|
|
let notifier;
|
2017-03-06 22:13:40 +08:00
|
|
|
|
2017-07-20 18:10:43 +08:00
|
|
|
// disable compression for EventSource response
|
|
|
|
// this needs to be called before gzipResponse
|
|
|
|
server.use((req, res, next) => {
|
|
|
|
if (req.route.path === '/users/:user/updates') {
|
|
|
|
req.headers['accept-encoding'] = '';
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
});
|
|
|
|
server.use(restify.plugins.gzipResponse());
|
|
|
|
|
2017-07-11 03:15:16 +08:00
|
|
|
server.use(restify.plugins.queryParser());
|
2017-06-03 14:51:58 +08:00
|
|
|
server.use(
|
2017-07-11 03:15:16 +08:00
|
|
|
restify.plugins.bodyParser({
|
2017-06-03 14:51:58 +08:00
|
|
|
maxBodySize: 0,
|
|
|
|
mapParams: true,
|
|
|
|
mapFiles: false,
|
|
|
|
overrideParams: false
|
|
|
|
})
|
|
|
|
);
|
2017-07-19 16:06:47 +08:00
|
|
|
server.get(
|
|
|
|
/\/public\/?.*/,
|
|
|
|
restify.plugins.serveStatic({
|
|
|
|
directory: __dirname,
|
|
|
|
default: 'index.html'
|
|
|
|
})
|
|
|
|
);
|
2017-03-06 22:13:40 +08:00
|
|
|
|
2017-07-20 18:10:43 +08:00
|
|
|
server.get({ name: 'users', path: '/users' }, (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
query: Joi.string().alphanum().lowercase().empty('').max(100),
|
|
|
|
limit: Joi.number().default(20).min(1).max(250),
|
|
|
|
next: Joi.string().alphanum().max(100),
|
|
|
|
prev: Joi.string().alphanum().max(100),
|
|
|
|
page: Joi.number().default(1)
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.query, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true,
|
|
|
|
allowUnknown: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let query = result.value.query;
|
|
|
|
let limit = result.value.limit;
|
|
|
|
let page = result.value.page;
|
|
|
|
let pageNext = result.value.next;
|
|
|
|
let pagePrev = result.value.prev;
|
|
|
|
|
|
|
|
let filter = query
|
|
|
|
? {
|
|
|
|
username: {
|
|
|
|
$regex: query,
|
|
|
|
$options: ''
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: {};
|
|
|
|
|
|
|
|
db.users.collection('users').count(filter, (err, total) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let opts = {
|
|
|
|
limit,
|
|
|
|
query: filter,
|
|
|
|
fields: {
|
|
|
|
_id: true,
|
|
|
|
username: true,
|
|
|
|
address: true,
|
|
|
|
storageUsed: true,
|
|
|
|
quota: true,
|
2017-07-20 21:10:36 +08:00
|
|
|
disabled: true
|
2017-07-20 18:10:43 +08:00
|
|
|
},
|
|
|
|
sortAscending: true
|
|
|
|
};
|
|
|
|
|
|
|
|
if (pageNext) {
|
|
|
|
opts.next = pageNext;
|
|
|
|
} else if (pagePrev) {
|
|
|
|
opts.prev = pagePrev;
|
|
|
|
}
|
|
|
|
|
|
|
|
MongoPaging.find(db.users.collection('users'), opts, (err, result) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result.hasPrevious) {
|
|
|
|
page = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let prevUrl = result.hasPrevious
|
|
|
|
? server.router.render('users', {}, { prev: result.previous, limit, query: query || '', page: Math.max(page - 1, 1) })
|
|
|
|
: false;
|
|
|
|
let nextUrl = result.hasNext ? server.router.render('users', {}, { next: result.previous, limit, query: query || '', page: page + 1 }) : false;
|
|
|
|
|
|
|
|
let response = {
|
2017-07-20 21:10:36 +08:00
|
|
|
success: true,
|
2017-07-20 18:10:43 +08:00
|
|
|
query,
|
|
|
|
total,
|
|
|
|
page,
|
|
|
|
prev: prevUrl,
|
|
|
|
next: nextUrl,
|
|
|
|
results: (result.results || []).map(userData => ({
|
|
|
|
id: userData._id.toString(),
|
|
|
|
username: userData.username,
|
|
|
|
address: userData.address,
|
|
|
|
quota: {
|
|
|
|
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
|
|
|
|
used: Math.max(Number(userData.storageUsed) || 0, 0)
|
|
|
|
},
|
2017-07-20 21:10:36 +08:00
|
|
|
disabled: userData.disabled
|
2017-07-20 18:10:43 +08:00
|
|
|
}))
|
|
|
|
};
|
|
|
|
|
|
|
|
res.json(response);
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
server.get({ name: 'addresses', path: '/addresses' }, (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
query: Joi.string().empty('').max(255),
|
|
|
|
limit: Joi.number().default(20).min(1).max(250),
|
|
|
|
next: Joi.string().alphanum().max(100),
|
|
|
|
prev: Joi.string().alphanum().max(100),
|
|
|
|
page: Joi.number().default(1)
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.query, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true,
|
|
|
|
allowUnknown: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let query = result.value.query;
|
|
|
|
let limit = result.value.limit;
|
|
|
|
let page = result.value.page;
|
|
|
|
let pageNext = result.value.next;
|
|
|
|
let pagePrev = result.value.prev;
|
|
|
|
|
|
|
|
let filter = query
|
|
|
|
? {
|
|
|
|
address: {
|
|
|
|
$regex: query.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&'),
|
|
|
|
$options: ''
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: {};
|
|
|
|
|
|
|
|
db.users.collection('addresses').count(filter, (err, total) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let opts = {
|
|
|
|
limit,
|
|
|
|
query: filter,
|
|
|
|
fields: {
|
|
|
|
_id: true,
|
|
|
|
address: true,
|
|
|
|
user: true
|
|
|
|
},
|
|
|
|
sortAscending: true
|
|
|
|
};
|
|
|
|
|
|
|
|
if (pageNext) {
|
|
|
|
opts.next = pageNext;
|
|
|
|
} else if (pagePrev) {
|
|
|
|
opts.prev = pagePrev;
|
|
|
|
}
|
|
|
|
|
|
|
|
MongoPaging.find(db.users.collection('addresses'), opts, (err, result) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result.hasPrevious) {
|
|
|
|
page = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let prevUrl = result.hasPrevious
|
|
|
|
? server.router.render('addresses', {}, { prev: result.previous, limit, query: query || '', page: Math.max(page - 1, 1) })
|
|
|
|
: false;
|
|
|
|
let nextUrl = result.hasNext ? server.router.render('addresses', {}, { next: result.previous, limit, query: query || '', page: page + 1 }) : false;
|
|
|
|
|
|
|
|
let response = {
|
2017-07-20 21:10:36 +08:00
|
|
|
success: true,
|
2017-07-20 18:10:43 +08:00
|
|
|
query,
|
|
|
|
total,
|
|
|
|
page,
|
|
|
|
prev: prevUrl,
|
|
|
|
next: nextUrl,
|
|
|
|
results: (result.results || []).map(addressData => ({
|
|
|
|
id: addressData._id.toString(),
|
|
|
|
address: addressData.address,
|
|
|
|
user: addressData.user.toString()
|
|
|
|
}))
|
|
|
|
};
|
|
|
|
|
|
|
|
res.json(response);
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.post('/users', (req, res, next) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
2017-03-06 22:13:40 +08:00
|
|
|
const schema = Joi.object().keys({
|
2017-03-25 04:26:04 +08:00
|
|
|
username: Joi.string().alphanum().lowercase().min(3).max(30).required(),
|
2017-07-17 21:32:31 +08:00
|
|
|
password: Joi.string().max(256).required(),
|
|
|
|
|
|
|
|
address: Joi.string().email(),
|
|
|
|
|
|
|
|
language: Joi.string().min(2).max(20).lowercase(),
|
|
|
|
retention: Joi.number().min(0).default(0),
|
|
|
|
|
|
|
|
quota: Joi.number().min(0).default(0),
|
|
|
|
recipients: Joi.number().min(0).default(0),
|
|
|
|
forwards: Joi.number().min(0).default(0)
|
2017-03-06 22:13:40 +08:00
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-03-06 22:13:40 +08:00
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
userHandler.create(result.value, (err, id) => {
|
2017-03-06 22:13:40 +08:00
|
|
|
if (err) {
|
2017-03-22 16:30:10 +08:00
|
|
|
res.json({
|
2017-04-21 04:32:19 +08:00
|
|
|
error: err.message,
|
|
|
|
username: result.value.username
|
2017-03-06 22:13:40 +08:00
|
|
|
});
|
2017-03-22 16:30:10 +08:00
|
|
|
return next();
|
2017-03-06 22:13:40 +08:00
|
|
|
}
|
2017-07-12 02:38:23 +08:00
|
|
|
|
2017-04-21 04:32:19 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
success: !!id,
|
|
|
|
id
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
2017-04-21 04:32:19 +08:00
|
|
|
|
|
|
|
return next();
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.put('/users/:user', (req, res, next) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
2017-03-22 16:30:10 +08:00
|
|
|
const schema = Joi.object().keys({
|
2017-07-17 21:32:31 +08:00
|
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
|
|
|
|
password: Joi.string().max(256),
|
|
|
|
|
|
|
|
language: Joi.string().min(2).max(20).lowercase(),
|
|
|
|
|
|
|
|
retention: Joi.number().min(0),
|
|
|
|
quota: Joi.number().min(0),
|
|
|
|
recipients: Joi.number().min(0),
|
|
|
|
forwards: Joi.number().min(0)
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
|
|
|
|
let $set = {};
|
|
|
|
let updates = false;
|
|
|
|
Object.keys(result.value).forEach(key => {
|
|
|
|
if (key === 'user') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (key === 'password') {
|
|
|
|
$set.password = bcrypt.hashSync(result.value[key], 11);
|
|
|
|
return;
|
2017-06-03 14:51:58 +08:00
|
|
|
}
|
2017-07-17 21:32:31 +08:00
|
|
|
$set[key] = result.value[key];
|
|
|
|
updates = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!updates) {
|
|
|
|
res.json({
|
|
|
|
error: 'Nothing was changed'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
db.users.collection('users').findOneAndUpdate({
|
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
$set
|
|
|
|
}, {
|
|
|
|
returnOriginal: false
|
|
|
|
}, (err, result) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result || !result.value) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: true
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
server.post('/users/:user/addresses', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
address: Joi.string().email().required(),
|
|
|
|
main: Joi.boolean().truthy(['Y', 'true', 'yes', 1])
|
|
|
|
});
|
|
|
|
|
|
|
|
let address = tools.normalizeAddress(req.params.address);
|
|
|
|
|
|
|
|
if (/[\u0080-\uFFFF]/.test(req.params.address)) {
|
|
|
|
// replace unicode characters in email addresses before validation
|
|
|
|
req.params.address = req.params.address.replace(/[\u0080-\uFFFF]/g, 'x');
|
|
|
|
}
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-03-22 16:30:10 +08:00
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let main = result.value.main;
|
2017-03-22 16:30:10 +08:00
|
|
|
|
2017-03-25 04:26:04 +08:00
|
|
|
if (address.indexOf('+') >= 0) {
|
2017-03-22 16:30:10 +08:00
|
|
|
res.json({
|
|
|
|
error: 'Address can not contain +'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users.collection('users').findOne({
|
2017-07-17 21:32:31 +08:00
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
2017-03-25 04:26:04 +08:00
|
|
|
}, (err, userData) => {
|
2017-03-22 16:30:10 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-03-25 04:26:04 +08:00
|
|
|
if (!userData) {
|
2017-03-22 16:30:10 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This user does not exist'
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users.collection('addresses').findOne({
|
2017-03-25 04:26:04 +08:00
|
|
|
address
|
|
|
|
}, (err, addressData) => {
|
2017-03-22 16:30:10 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-03-25 04:26:04 +08:00
|
|
|
if (addressData) {
|
2017-03-22 16:30:10 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This email address already exists'
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
// insert alias address to email address registry
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users.collection('addresses').insertOne({
|
2017-07-17 21:32:31 +08:00
|
|
|
user,
|
2017-03-25 04:26:04 +08:00
|
|
|
address,
|
2017-03-22 16:30:10 +08:00
|
|
|
created: new Date()
|
2017-07-17 21:32:31 +08:00
|
|
|
}, (err, r) => {
|
2017-03-22 16:30:10 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-22 16:30:10 +08:00
|
|
|
});
|
|
|
|
return next();
|
2017-03-06 22:13:40 +08:00
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let insertId = r.insertedId;
|
|
|
|
|
2017-03-25 04:26:04 +08:00
|
|
|
let done = () => {
|
2017-07-17 21:32:31 +08:00
|
|
|
// ignore potential user update error
|
2017-03-25 04:26:04 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
success: !!insertId,
|
|
|
|
id: insertId
|
2017-03-25 04:26:04 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
};
|
2017-03-06 22:13:40 +08:00
|
|
|
|
2017-03-25 04:26:04 +08:00
|
|
|
if (!userData.address || main) {
|
|
|
|
// register this address as the default address for that user
|
2017-07-15 03:16:09 +08:00
|
|
|
return db.users.collection('users').findOneAndUpdate(
|
2017-06-03 14:51:58 +08:00
|
|
|
{
|
2017-07-17 21:32:31 +08:00
|
|
|
_id: user
|
2017-06-03 14:51:58 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
$set: {
|
|
|
|
address
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{},
|
|
|
|
done
|
|
|
|
);
|
2017-03-25 04:26:04 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
done();
|
2017-03-06 22:13:40 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.put('/users/:user/addresses/:address', (req, res, next) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-07-17 21:32:31 +08:00
|
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
address: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
main: Joi.boolean().truthy(['Y', 'true', 'yes', 1]).required()
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let address = new ObjectID(result.value.address);
|
|
|
|
let main = result.value.main;
|
2017-04-12 16:32:57 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
if (!main) {
|
2017-04-12 16:32:57 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'Cannot unset main status'
|
2017-04-12 16:32:57 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-03-27 15:36:45 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
db.users.collection('users').findOne({
|
|
|
|
_id: user
|
2017-04-12 16:32:57 +08:00
|
|
|
}, {
|
2017-07-17 21:32:31 +08:00
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-17 21:32:31 +08:00
|
|
|
if (!userData) {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This user does not exist'
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
db.users.collection('addresses').findOne({
|
|
|
|
_id: address
|
|
|
|
}, (err, addressData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!addressData || addressData.user.toString() !== user.toString()) {
|
|
|
|
res.json({
|
|
|
|
error: 'Invalid or unknown email address identifier'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (addressData.address === userData.address) {
|
|
|
|
res.json({
|
|
|
|
error: 'Selected address is already the main email address for the user'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
// insert alias address to email address registry
|
|
|
|
db.users.collection('users').findOneAndUpdate({
|
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
$set: {
|
|
|
|
address: addressData.address
|
|
|
|
}
|
|
|
|
}, {
|
|
|
|
returnOriginal: false
|
|
|
|
}, (err, r) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: !!r.value
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
server.del('/users/:user/addresses/:address', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
address: Joi.string().hex().lowercase().length(24).required()
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: result.error.message
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
2017-07-17 21:32:31 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let address = new ObjectID(result.value.address);
|
|
|
|
|
|
|
|
db.users.collection('users').findOne({
|
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
db.users.collection('addresses').findOne({
|
|
|
|
_id: address
|
|
|
|
}, (err, addressData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!addressData || addressData.user.toString() !== user.toString()) {
|
|
|
|
res.json({
|
|
|
|
error: 'Invalid or unknown email address identifier'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (addressData.address === userData.address) {
|
|
|
|
res.json({
|
|
|
|
error: 'Trying to delete main address. Set a new main address first'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
// insert alias address to email address registry
|
|
|
|
db.users.collection('addresses').deleteOne({
|
|
|
|
_id: address
|
|
|
|
}, (err, r) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: !!r.deletedCount
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.post('/users/:user/quota/reset', (req, res, next) => {
|
2017-03-30 01:06:09 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-07-17 21:32:31 +08:00
|
|
|
user: Joi.string().hex().lowercase().length(24).required()
|
2017-03-30 01:06:09 +08:00
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-03-30 01:06:09 +08:00
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let user = new ObjectID(result.value.iuserd);
|
2017-03-30 01:06:09 +08:00
|
|
|
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users.collection('users').findOne({
|
2017-07-17 21:32:31 +08:00
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
storageUsed: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
2017-03-30 01:06:09 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.messageusername
|
2017-03-30 01:06:09 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
if (!userData) {
|
2017-03-30 01:06:09 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This user does not exist'
|
2017-03-30 01:06:09 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
// calculate mailbox size by aggregating the size's of all messages
|
2017-06-03 14:51:58 +08:00
|
|
|
db.database
|
|
|
|
.collection('messages')
|
|
|
|
.aggregate(
|
2017-07-11 03:15:16 +08:00
|
|
|
[
|
|
|
|
{
|
|
|
|
$match: {
|
2017-07-17 21:32:31 +08:00
|
|
|
user
|
2017-07-11 03:15:16 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$group: {
|
|
|
|
_id: {
|
|
|
|
user: '$user'
|
|
|
|
},
|
|
|
|
storageUsed: {
|
|
|
|
$sum: '$size'
|
|
|
|
}
|
|
|
|
}
|
2017-06-03 14:51:58 +08:00
|
|
|
}
|
2017-07-11 03:15:16 +08:00
|
|
|
],
|
2017-06-03 14:51:58 +08:00
|
|
|
{
|
2017-07-11 03:15:16 +08:00
|
|
|
cursor: {
|
|
|
|
batchSize: 1
|
2017-06-03 14:51:58 +08:00
|
|
|
}
|
2017-03-30 01:06:09 +08:00
|
|
|
}
|
2017-06-03 14:51:58 +08:00
|
|
|
)
|
|
|
|
.toArray((err, result) => {
|
2017-03-30 01:06:09 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-30 01:06:09 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
let storageUsed = (result && result[0] && result[0].storageUsed) || 0;
|
|
|
|
|
|
|
|
// update quota counter
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users.collection('users').findOneAndUpdate({
|
2017-07-17 21:32:31 +08:00
|
|
|
_id: userData._id
|
2017-06-03 14:51:58 +08:00
|
|
|
}, {
|
|
|
|
$set: {
|
|
|
|
storageUsed: Number(storageUsed) || 0
|
|
|
|
}
|
|
|
|
}, {
|
|
|
|
returnOriginal: false
|
|
|
|
}, (err, result) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-06-03 14:51:58 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result || !result.value) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This user does not exist'
|
2017-06-03 14:51:58 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-03-30 01:06:09 +08:00
|
|
|
res.json({
|
2017-06-03 14:51:58 +08:00
|
|
|
success: true,
|
|
|
|
storageUsed: Number(result.value.storageUsed) || 0
|
2017-03-30 01:06:09 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.get('/users/:user', (req, res, next) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-07-17 21:32:31 +08:00
|
|
|
user: Joi.string().hex().lowercase().length(24).required()
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let user = new ObjectID(result.value.user);
|
2017-03-27 15:36:45 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
db.users.collection('users').findOne({
|
|
|
|
_id: user
|
|
|
|
}, (err, userData) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-17 21:32:31 +08:00
|
|
|
if (!userData) {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This user does not exist'
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
db.redis
|
|
|
|
.multi()
|
|
|
|
// sending counters are stored in Redis
|
|
|
|
.get('wdr:' + userData._id.toString())
|
|
|
|
.ttl('wdr:' + userData._id.toString())
|
|
|
|
.get('wdf:' + userData._id.toString())
|
|
|
|
.ttl('wdf:' + userData._id.toString())
|
|
|
|
.exec((err, result) => {
|
|
|
|
if (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
let recipients = Number(userData.recipients) || config.maxRecipients;
|
|
|
|
let forwards = Number(userData.forwards) || config.maxForwards;
|
2017-03-27 15:36:45 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let recipientsSent = Number(result && result[0]) || 0;
|
|
|
|
let recipientsTtl = Number(result && result[1]) || 0;
|
|
|
|
|
|
|
|
let forwardsSent = Number(result && result[2]) || 0;
|
|
|
|
let forwardsTtl = Number(result && result[3]) || 0;
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
id: user,
|
|
|
|
|
|
|
|
username: userData.username,
|
|
|
|
|
2017-07-20 21:10:36 +08:00
|
|
|
address: userData.address,
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
language: userData.language,
|
|
|
|
retention: userData.retention || false,
|
|
|
|
|
|
|
|
limits: {
|
|
|
|
quota: {
|
|
|
|
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
|
|
|
|
used: Math.max(Number(userData.storageUsed) || 0, 0)
|
|
|
|
},
|
|
|
|
recipients: {
|
|
|
|
allowed: recipients,
|
|
|
|
used: recipientsSent,
|
|
|
|
ttl: recipientsTtl >= 0 ? recipientsTtl : false
|
|
|
|
},
|
|
|
|
forwards: {
|
|
|
|
allowed: forwards,
|
|
|
|
used: forwardsSent,
|
|
|
|
ttl: forwardsTtl >= 0 ? forwardsTtl : false
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-07-20 21:10:36 +08:00
|
|
|
activated: userData.activated,
|
|
|
|
disabled: userData.disabled
|
2017-07-17 21:32:31 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.get('/users/:user/addresses', (req, res, next) => {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-07-17 21:32:31 +08:00
|
|
|
user: Joi.string().hex().lowercase().length(24).required()
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let user = new ObjectID(result.value.user);
|
2017-03-27 15:36:45 +08:00
|
|
|
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users.collection('users').findOne({
|
2017-07-17 21:32:31 +08:00
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
2017-03-27 15:36:45 +08:00
|
|
|
}, (err, userData) => {
|
2017-03-06 22:13:40 +08:00
|
|
|
if (err) {
|
2017-03-27 15:36:45 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'This user does not exist'
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
return next();
|
2017-03-06 22:13:40 +08:00
|
|
|
}
|
|
|
|
|
2017-07-15 03:16:09 +08:00
|
|
|
db.users
|
2017-06-03 14:51:58 +08:00
|
|
|
.collection('addresses')
|
|
|
|
.find({
|
2017-07-17 21:32:31 +08:00
|
|
|
user
|
2017-06-03 14:51:58 +08:00
|
|
|
})
|
|
|
|
.sort({
|
|
|
|
address: 1
|
|
|
|
})
|
|
|
|
.toArray((err, addresses) => {
|
2017-04-12 16:32:57 +08:00
|
|
|
if (err) {
|
2017-06-03 14:51:58 +08:00
|
|
|
res.json({
|
2017-07-17 21:32:31 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message
|
2017-06-03 14:51:58 +08:00
|
|
|
});
|
|
|
|
return next();
|
2017-04-12 16:32:57 +08:00
|
|
|
}
|
2017-05-02 21:17:37 +08:00
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
if (!addresses) {
|
|
|
|
addresses = [];
|
|
|
|
}
|
2017-04-12 16:32:57 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
|
|
|
|
addresses: addresses.map(address => ({
|
|
|
|
id: address._id,
|
|
|
|
address: address.address,
|
|
|
|
main: address.address === userData.address,
|
|
|
|
created: address.created
|
|
|
|
}))
|
|
|
|
});
|
2017-05-02 21:17:37 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
2017-06-03 14:51:58 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
server.get('/users/:user/addresses/:address', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
2017-04-12 16:32:57 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
address: Joi.string().hex().lowercase().length(24).required()
|
|
|
|
});
|
2017-04-12 16:32:57 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
2017-04-12 16:32:57 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-05-02 21:17:37 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let address = new ObjectID(result.value.address);
|
2017-04-12 16:32:57 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
db.users.collection('users').findOne({
|
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-05-02 21:17:37 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
db.users.collection('addresses').findOne({
|
|
|
|
_id: address,
|
|
|
|
user
|
|
|
|
}, (err, addressData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!addressData) {
|
|
|
|
res.json({
|
|
|
|
error: 'Invalid or unknown address'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-06-03 14:51:58 +08:00
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
id: addressData._id,
|
|
|
|
address: addressData.address,
|
|
|
|
main: addressData.address === userData.address,
|
|
|
|
created: addressData.created
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
2017-07-17 21:32:31 +08:00
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
});
|
|
|
|
});
|
2017-03-06 22:13:40 +08:00
|
|
|
|
2017-07-18 16:17:36 +08:00
|
|
|
server.get('/users/:user/mailboxes', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string().hex().lowercase().length(24).required()
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
|
|
|
|
db.users.collection('users').findOne({
|
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
db.database
|
|
|
|
.collection('mailboxes')
|
|
|
|
.find({
|
|
|
|
user
|
|
|
|
})
|
|
|
|
.toArray((err, mailboxes) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
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.subscribed !== b.subscribed) {
|
|
|
|
return (a.subscribed ? 0 : 1) - (b.subscribed ? 0 : 1);
|
|
|
|
}
|
|
|
|
return a.path.localeCompare(b.path);
|
|
|
|
});
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
|
|
|
|
mailboxes: mailboxes.map(mailbox => {
|
|
|
|
let path = mailbox.path.split('/');
|
|
|
|
let name = path.pop();
|
|
|
|
|
|
|
|
return {
|
|
|
|
id: mailbox._id,
|
|
|
|
name,
|
|
|
|
path: mailbox.path,
|
|
|
|
specialUse: mailbox.specialUse,
|
|
|
|
modifyIndex: mailbox.modifyIndex
|
|
|
|
};
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
server.get('/users/:user/mailboxes/:mailbox', (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()
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
|
|
|
error: result.error.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
let mailbox = new ObjectID(result.value.mailbox);
|
|
|
|
|
|
|
|
db.users.collection('users').findOne({
|
|
|
|
_id: user
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
db.database.collection('mailboxes').findOne({
|
|
|
|
_id: mailbox,
|
|
|
|
user
|
|
|
|
}, (err, mailboxData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!mailboxData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This mailbox does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let path = mailboxData.path.split('/');
|
|
|
|
let name = path.pop();
|
|
|
|
|
2017-07-20 21:10:36 +08:00
|
|
|
getMailboxCounter(mailbox, false, (err, total) => {
|
2017-07-18 16:17:36 +08:00
|
|
|
if (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
2017-07-20 21:10:36 +08:00
|
|
|
getMailboxCounter(mailbox, 'unseen', (err, unseen) => {
|
|
|
|
if (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
id: mailbox,
|
|
|
|
name,
|
|
|
|
path: mailboxData.path,
|
|
|
|
specialUse: mailboxData.specialUse,
|
|
|
|
modifyIndex: mailboxData.modifyIndex,
|
|
|
|
total,
|
|
|
|
unseen
|
|
|
|
});
|
|
|
|
return next();
|
2017-07-18 16:17:36 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-07-18 22:38:05 +08:00
|
|
|
server.get('/users/:user/updates', (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
|
|
|
|
});
|
|
|
|
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
|
|
|
|
}, {
|
|
|
|
fields: {
|
|
|
|
username: true,
|
|
|
|
address: true
|
|
|
|
}
|
|
|
|
}, (err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let session = { id: crypto.randomBytes(10).toString('base64'), user: { id: userData._id, username: userData.username } };
|
2017-07-19 16:06:47 +08:00
|
|
|
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);
|
|
|
|
};
|
2017-07-18 22:38:05 +08:00
|
|
|
|
|
|
|
let journalReading = false;
|
|
|
|
let journalReader = () => {
|
2017-07-19 16:06:47 +08:00
|
|
|
if (journalReading || closed) {
|
2017-07-18 22:38:05 +08:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
journalReading = true;
|
|
|
|
loadJournalStream(req, res, user, lastEventId, (err, info) => {
|
|
|
|
if (err) {
|
|
|
|
// ignore?
|
|
|
|
}
|
|
|
|
lastEventId = info && info.lastEventId;
|
|
|
|
journalReading = false;
|
2017-07-19 16:06:47 +08:00
|
|
|
if (info && info.processed) {
|
|
|
|
resetIdleComment();
|
|
|
|
}
|
2017-07-18 22:38:05 +08:00
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
let close = () => {
|
2017-07-19 16:06:47 +08:00
|
|
|
closed = true;
|
|
|
|
clearTimeout(idleTimer);
|
2017-07-18 22:38:05 +08:00
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2017-07-19 16:06:47 +08:00
|
|
|
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
2017-07-18 22:38:05 +08:00
|
|
|
|
|
|
|
if (lastEventId) {
|
2017-07-19 16:06:47 +08:00
|
|
|
loadJournalStream(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();
|
|
|
|
}
|
|
|
|
});
|
2017-07-18 22:38:05 +08:00
|
|
|
} else {
|
|
|
|
db.database.collection('journal').findOne({ user }, { sort: { _id: -1 } }, (err, latest) => {
|
|
|
|
if (!err && latest) {
|
|
|
|
lastEventId = latest._id;
|
|
|
|
}
|
|
|
|
setup();
|
2017-07-19 16:06:47 +08:00
|
|
|
sendIdleComment();
|
2017-07-18 22:38:05 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function formatJournalData(e) {
|
|
|
|
let data = {};
|
|
|
|
Object.keys(e).forEach(key => {
|
|
|
|
if (!['_id', 'ignore', 'user'].includes(key)) {
|
|
|
|
data[key] = e[key];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let response = [];
|
2017-07-19 16:06:47 +08:00
|
|
|
response.push('data: ' + JSON.stringify(data, false, 2).split('\n').join('\ndata: '));
|
2017-07-20 21:10:36 +08:00
|
|
|
if (e._id) {
|
|
|
|
response.push('id: ' + e._id.toString());
|
|
|
|
}
|
2017-07-18 22:38:05 +08:00
|
|
|
|
|
|
|
return response.join('\n') + '\n\n';
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadJournalStream(req, res, user, lastEventId, done) {
|
2017-07-20 21:10:36 +08:00
|
|
|
console.log('READ');
|
2017-07-18 22:38:05 +08:00
|
|
|
let query = { user };
|
|
|
|
if (lastEventId) {
|
|
|
|
query._id = { $gt: lastEventId };
|
|
|
|
}
|
2017-07-20 21:10:36 +08:00
|
|
|
|
|
|
|
let mailboxes = new Set();
|
|
|
|
|
2017-07-18 22:38:05 +08:00
|
|
|
let cursor = db.database.collection('journal').find(query).sort({ _id: 1 });
|
|
|
|
let processed = 0;
|
|
|
|
let processNext = () => {
|
|
|
|
cursor.next((err, e) => {
|
|
|
|
if (err) {
|
|
|
|
return done(err);
|
|
|
|
}
|
|
|
|
if (!e) {
|
|
|
|
return cursor.close(() => {
|
2017-07-20 21:10:36 +08:00
|
|
|
if (!mailboxes.size) {
|
|
|
|
return done(null, {
|
|
|
|
lastEventId,
|
|
|
|
processed
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
mailboxes = Array.from(mailboxes);
|
|
|
|
let mailboxPos = 0;
|
|
|
|
let emitCounters = () => {
|
|
|
|
if (mailboxPos >= mailboxes.length) {
|
|
|
|
return done(null, {
|
|
|
|
lastEventId,
|
|
|
|
processed
|
|
|
|
});
|
|
|
|
}
|
|
|
|
let mailbox = mailboxes[mailboxPos++];
|
|
|
|
getMailboxCounter(mailbox, false, (err, total) => {
|
|
|
|
if (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
getMailboxCounter(mailbox, 'unseen', (err, unseen) => {
|
|
|
|
if (err) {
|
|
|
|
// ignore
|
|
|
|
}
|
|
|
|
|
|
|
|
res.write(
|
|
|
|
formatJournalData({
|
|
|
|
command: 'COUNTERS',
|
|
|
|
_id: lastEventId,
|
|
|
|
mailbox,
|
|
|
|
total,
|
|
|
|
unseen
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
setImmediate(emitCounters);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
emitCounters();
|
2017-07-18 22:38:05 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
lastEventId = e._id;
|
|
|
|
|
|
|
|
if (!e || !e.command) {
|
|
|
|
// skip
|
|
|
|
return processNext();
|
|
|
|
}
|
|
|
|
|
2017-07-20 21:10:36 +08:00
|
|
|
switch (e.command) {
|
|
|
|
case 'FETCH':
|
|
|
|
case 'EXISTS':
|
|
|
|
case 'EXPUNGE':
|
|
|
|
if (e.mailbox) {
|
|
|
|
mailboxes.add(e.mailbox.toString());
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2017-07-18 22:38:05 +08:00
|
|
|
res.write(formatJournalData(e));
|
|
|
|
|
|
|
|
processed++;
|
|
|
|
processNext();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
processNext();
|
|
|
|
}
|
|
|
|
|
2017-07-20 21:10:36 +08:00
|
|
|
function getMailboxCounter(mailbox, type, done) {
|
|
|
|
let prefix = type ? type : 'sum';
|
|
|
|
db.redis.get(prefix + ':' + mailbox.toString(), (err, sum) => {
|
|
|
|
if (err) {
|
|
|
|
return done(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sum !== null) {
|
|
|
|
return done(null, Number(sum));
|
|
|
|
}
|
|
|
|
|
|
|
|
// calculate sum
|
|
|
|
let query = { mailbox };
|
|
|
|
if (type) {
|
|
|
|
query[type] = true;
|
|
|
|
}
|
|
|
|
db.database.collection('messages').count(query, (err, sum) => {
|
|
|
|
if (err) {
|
|
|
|
return done(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
// cache calculated sum in redis
|
|
|
|
db.redis.multi().set(prefix + ':' + mailbox.toString(), sum).expire(prefix + ':' + mailbox.toString(), consts.MAILBOX_COUNTER_TTL).exec(() => {
|
|
|
|
done(null, sum);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-03-27 15:36:45 +08:00
|
|
|
module.exports = done => {
|
2017-04-13 16:35:39 +08:00
|
|
|
if (!config.imap.enabled) {
|
|
|
|
return setImmediate(() => done(null, false));
|
|
|
|
}
|
|
|
|
|
2017-03-27 15:36:45 +08:00
|
|
|
let started = false;
|
|
|
|
|
2017-07-15 03:16:09 +08:00
|
|
|
userHandler = new UserHandler({ database: db.database, users: db.users, redis: db.redis });
|
2017-07-18 22:38:05 +08:00
|
|
|
notifier = new ImapNotifier({
|
|
|
|
database: db.database,
|
|
|
|
redis: db.redis
|
|
|
|
});
|
2017-03-27 15:36:45 +08:00
|
|
|
|
|
|
|
server.on('error', err => {
|
|
|
|
if (!started) {
|
|
|
|
started = true;
|
|
|
|
return done(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
log.error('API', err);
|
|
|
|
});
|
|
|
|
|
|
|
|
server.listen(config.api.port, config.api.host, () => {
|
|
|
|
if (started) {
|
|
|
|
return server.close();
|
|
|
|
}
|
|
|
|
started = true;
|
|
|
|
log.info('API', 'Server listening on %s:%s', config.api.host || '0.0.0.0', config.api.port);
|
|
|
|
done(null, server);
|
2017-03-06 22:13:40 +08:00
|
|
|
});
|
|
|
|
};
|