mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-09-20 15:26:03 +08:00
2618 lines
74 KiB
JavaScript
2618 lines
74 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('wild-config');
|
|
const restify = require('restify');
|
|
const log = require('npmlog');
|
|
const Joi = require('joi');
|
|
const bcrypt = require('bcryptjs');
|
|
const crypto = require('crypto');
|
|
const tools = require('./lib/tools');
|
|
const consts = require('./lib/consts');
|
|
const UserHandler = require('./lib/user-handler');
|
|
const MailboxHandler = require('./lib/mailbox-handler');
|
|
const MessageHandler = require('./lib/message-handler');
|
|
const ImapNotifier = require('./lib/imap-notifier');
|
|
const db = require('./lib/db');
|
|
const MongoPaging = require('mongo-cursor-pagination');
|
|
const certs = require('./lib/certs').get('api');
|
|
const ObjectID = require('mongodb').ObjectID;
|
|
const imapTools = require('./imap-core/lib/imap-tools');
|
|
const libmime = require('libmime');
|
|
const addressparser = require('addressparser');
|
|
const punycode = require('punycode');
|
|
|
|
const serverOptions = {
|
|
name: 'Wild Duck API',
|
|
strictRouting: true,
|
|
formatters: {
|
|
'application/json; q=0.4': (req, res, body) => {
|
|
let data = body ? JSON.stringify(body, false, 2) + '\n' : 'null';
|
|
res.setHeader('Content-Length', Buffer.byteLength(data));
|
|
return data;
|
|
}
|
|
}
|
|
};
|
|
|
|
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);
|
|
|
|
let userHandler;
|
|
let mailboxHandler;
|
|
let messageHandler;
|
|
let notifier;
|
|
|
|
// 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());
|
|
|
|
server.use(restify.plugins.queryParser());
|
|
server.use(
|
|
restify.plugins.bodyParser({
|
|
maxBodySize: 0,
|
|
mapParams: true,
|
|
mapFiles: false,
|
|
overrideParams: false
|
|
})
|
|
);
|
|
server.get(
|
|
/\/public\/?.*/,
|
|
restify.plugins.serveStatic({
|
|
directory: __dirname,
|
|
default: 'index.html'
|
|
})
|
|
);
|
|
|
|
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: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let opts = {
|
|
limit,
|
|
query: filter,
|
|
fields: {
|
|
_id: true,
|
|
username: true,
|
|
address: true,
|
|
storageUsed: true,
|
|
quota: true,
|
|
disabled: true
|
|
},
|
|
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: err.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.next, limit, query: query || '', page: page + 1 }) : false;
|
|
|
|
let response = {
|
|
success: true,
|
|
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)
|
|
},
|
|
disabled: userData.disabled
|
|
}))
|
|
};
|
|
|
|
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: err.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: err.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.next, limit, query: query || '', page: page + 1 }) : false;
|
|
|
|
let response = {
|
|
success: true,
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
|
|
server.post('/users', (req, res, next) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
username: Joi.string().alphanum().lowercase().min(3).max(30).required(),
|
|
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)
|
|
});
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
abortEarly: false,
|
|
convert: true
|
|
});
|
|
|
|
if (result.error) {
|
|
res.json({
|
|
error: result.error.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
userHandler.create(result.value, (err, id) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message,
|
|
username: result.value.username
|
|
});
|
|
return next();
|
|
}
|
|
|
|
res.json({
|
|
success: !!id,
|
|
id
|
|
});
|
|
|
|
return next();
|
|
});
|
|
});
|
|
|
|
server.put('/users/:user', (req, res, next) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
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
|
|
});
|
|
|
|
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;
|
|
}
|
|
$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
|
|
});
|
|
|
|
if (result.error) {
|
|
res.json({
|
|
error: result.error.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
let main = result.value.main;
|
|
|
|
if (address.indexOf('+') >= 0) {
|
|
res.json({
|
|
error: 'Address can not contain +'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
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({
|
|
address
|
|
}, (err, addressData) => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'MongoDB Error: ' + err.message
|
|
});
|
|
return next();
|
|
}
|
|
if (addressData) {
|
|
res.json({
|
|
error: 'This email address already exists'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
// insert alias address to email address registry
|
|
db.users.collection('addresses').insertOne({
|
|
user,
|
|
address,
|
|
created: new Date()
|
|
}, (err, r) => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'MongoDB Error: ' + err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let insertId = r.insertedId;
|
|
|
|
let done = () => {
|
|
// ignore potential user update error
|
|
res.json({
|
|
success: !!insertId,
|
|
id: insertId
|
|
});
|
|
return next();
|
|
};
|
|
|
|
if (!userData.address || main) {
|
|
// register this address as the default address for that user
|
|
return db.users.collection('users').findOneAndUpdate(
|
|
{
|
|
_id: user
|
|
},
|
|
{
|
|
$set: {
|
|
address
|
|
}
|
|
},
|
|
{},
|
|
done
|
|
);
|
|
}
|
|
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
server.put('/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(),
|
|
main: Joi.boolean().truthy(['Y', 'true', 'yes', 1]).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 address = new ObjectID(result.value.address);
|
|
let main = result.value.main;
|
|
|
|
if (!main) {
|
|
res.json({
|
|
error: 'Cannot unset main status'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
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: '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) {
|
|
res.json({
|
|
error: result.error.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
server.post('/users/:user/quota/reset', (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.iuserd);
|
|
|
|
db.users.collection('users').findOne({
|
|
_id: user
|
|
}, {
|
|
fields: {
|
|
storageUsed: true
|
|
}
|
|
}, (err, userData) => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'MongoDB Error: ' + err.messageusername
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (!userData) {
|
|
res.json({
|
|
error: 'This user does not exist'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
// calculate mailbox size by aggregating the size's of all messages
|
|
db.database
|
|
.collection('messages')
|
|
.aggregate(
|
|
[
|
|
{
|
|
$match: {
|
|
user
|
|
}
|
|
},
|
|
{
|
|
$group: {
|
|
_id: {
|
|
user: '$user'
|
|
},
|
|
storageUsed: {
|
|
$sum: '$size'
|
|
}
|
|
}
|
|
}
|
|
],
|
|
{
|
|
cursor: {
|
|
batchSize: 1
|
|
}
|
|
}
|
|
)
|
|
.toArray((err, result) => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'MongoDB Error: ' + err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let storageUsed = (result && result[0] && result[0].storageUsed) || 0;
|
|
|
|
// update quota counter
|
|
db.users.collection('users').findOneAndUpdate({
|
|
_id: userData._id
|
|
}, {
|
|
$set: {
|
|
storageUsed: Number(storageUsed) || 0
|
|
}
|
|
}, {
|
|
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,
|
|
storageUsed: Number(result.value.storageUsed) || 0
|
|
});
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
server.get('/users/:user', (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
|
|
}, (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.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;
|
|
|
|
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,
|
|
|
|
address: userData.address,
|
|
|
|
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
|
|
}
|
|
},
|
|
|
|
activated: userData.activated,
|
|
disabled: userData.disabled
|
|
});
|
|
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
|
|
server.get('/users/:user/addresses', (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.users
|
|
.collection('addresses')
|
|
.find({
|
|
user
|
|
})
|
|
.sort({
|
|
address: 1
|
|
})
|
|
.toArray((err, addresses) => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'MongoDB Error: ' + err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (!addresses) {
|
|
addresses = [];
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
|
|
addresses: addresses.map(address => ({
|
|
id: address._id,
|
|
address: address.address,
|
|
main: address.address === userData.address,
|
|
created: address.created
|
|
}))
|
|
});
|
|
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
|
|
server.get('/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) {
|
|
res.json({
|
|
error: result.error.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
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,
|
|
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();
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
id: addressData._id,
|
|
address: addressData.address,
|
|
main: addressData.address === userData.address,
|
|
created: addressData.created
|
|
});
|
|
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
|
|
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(),
|
|
counters: Joi.boolean().truthy(['Y', 'true', 'yes', 1]).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
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
let counters = result.value.counters;
|
|
|
|
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);
|
|
});
|
|
|
|
let responses = [];
|
|
let position = 0;
|
|
let checkMailboxes = () => {
|
|
if (position >= mailboxes.length) {
|
|
res.json({
|
|
success: true,
|
|
mailboxes: 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);
|
|
}
|
|
|
|
getMailboxCounter(mailbox._id, false, (err, total) => {
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
getMailboxCounter(mailbox._id, 'unseen', (err, unseen) => {
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
response.total = total;
|
|
response.unseen = unseen;
|
|
responses.push(response);
|
|
return setImmediate(checkMailboxes);
|
|
});
|
|
});
|
|
};
|
|
checkMailboxes();
|
|
});
|
|
});
|
|
});
|
|
|
|
server.post('/users/:user/mailboxes', (req, res, next) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
path: Joi.string().regex(/\/{2,}|\/$/g, { invert: true }).required(),
|
|
retention: Joi.number().min(0)
|
|
});
|
|
|
|
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 path = imapTools.normalizeMailbox(result.value.path);
|
|
let retention = result.value.retention;
|
|
|
|
let opts = {
|
|
subscribed: true
|
|
};
|
|
if (retention) {
|
|
opts.retention = retention;
|
|
}
|
|
|
|
mailboxHandler.create(user, path, opts, (err, status, id) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (typeof status === 'string') {
|
|
res.json({
|
|
error: 'Mailbox creation failed with code ' + status
|
|
});
|
|
return next();
|
|
}
|
|
|
|
res.json({
|
|
success: !!status,
|
|
id
|
|
});
|
|
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();
|
|
|
|
getMailboxCounter(mailbox, false, (err, total) => {
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
getMailboxCounter(mailbox, 'unseen', (err, unseen) => {
|
|
if (err) {
|
|
// ignore
|
|
}
|
|
res.json({
|
|
success: true,
|
|
id: mailbox,
|
|
name,
|
|
path: mailboxData.path,
|
|
specialUse: mailboxData.specialUse,
|
|
modifyIndex: mailboxData.modifyIndex,
|
|
subscribed: mailboxData.subscribed,
|
|
total,
|
|
unseen
|
|
});
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
server.put('/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(),
|
|
path: Joi.string().regex(/\/{2,}|\/$/g, { invert: true }),
|
|
retention: Joi.number().min(0),
|
|
subscribed: Joi.boolean().truthy(['Y', 'true', 'yes', 1])
|
|
});
|
|
|
|
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);
|
|
|
|
let updates = {};
|
|
let update = false;
|
|
Object.keys(result.value || {}).forEach(key => {
|
|
if (!['user', 'mailbox'].includes(key)) {
|
|
updates[key] = result.value[key];
|
|
update = true;
|
|
}
|
|
});
|
|
|
|
if (!update) {
|
|
res.json({
|
|
error: 'Nothing was changed'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
mailboxHandler.update(user, mailbox, updates, (err, status) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (typeof status === 'string') {
|
|
res.json({
|
|
error: 'Mailbox update failed with code ' + status
|
|
});
|
|
return next();
|
|
}
|
|
|
|
res.json({
|
|
success: true
|
|
});
|
|
return next();
|
|
});
|
|
});
|
|
|
|
server.del('/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);
|
|
|
|
mailboxHandler.del(user, mailbox, (err, status) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (typeof status === 'string') {
|
|
res.json({
|
|
error: 'Mailbox deletion failed with code ' + status
|
|
});
|
|
return next();
|
|
}
|
|
|
|
res.json({
|
|
success: true
|
|
});
|
|
return next();
|
|
});
|
|
});
|
|
|
|
server.get({ name: 'messages', path: '/users/:user/mailboxes/:mailbox/messages' }, (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(),
|
|
limit: Joi.number().default(20).min(1).max(250),
|
|
order: Joi.any().allow(['asc', 'desc']).default('desc'),
|
|
next: Joi.string().alphanum().max(100),
|
|
prev: Joi.string().alphanum().max(100),
|
|
page: Joi.number().default(1)
|
|
});
|
|
|
|
req.query.user = req.params.user;
|
|
req.query.mailbox = req.params.mailbox;
|
|
|
|
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 user = new ObjectID(result.value.user);
|
|
let mailbox = new ObjectID(result.value.mailbox);
|
|
let limit = result.value.limit;
|
|
let page = result.value.page;
|
|
let pageNext = result.value.next;
|
|
let pagePrev = result.value.prev;
|
|
let sortAscending = result.value.order === 'asc';
|
|
|
|
db.database.collection('mailboxes').findOne({
|
|
_id: mailbox,
|
|
user
|
|
}, {
|
|
fields: {
|
|
path: true,
|
|
specialUse: true
|
|
}
|
|
}, (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 filter = {
|
|
mailbox
|
|
};
|
|
|
|
getFilteredMessageCount(filter, (err, total) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let opts = {
|
|
limit,
|
|
query: filter,
|
|
fields: {
|
|
_id: true,
|
|
uid: true,
|
|
'meta.from': true,
|
|
hdate: true,
|
|
subject: true,
|
|
'mimeTree.parsedHeader.from': true,
|
|
'mimeTree.parsedHeader.sender': true,
|
|
ha: true,
|
|
intro: true,
|
|
unseen: true,
|
|
undeleted: true,
|
|
flagged: true,
|
|
draft: true,
|
|
thread: true
|
|
},
|
|
paginatedField: 'uid',
|
|
sortAscending
|
|
};
|
|
|
|
if (pageNext) {
|
|
opts.next = pageNext;
|
|
} else if (pagePrev) {
|
|
opts.prev = pagePrev;
|
|
}
|
|
|
|
MongoPaging.find(db.users.collection('messages'), 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(
|
|
'messages',
|
|
{ user: user.toString(), mailbox: mailbox.toString() },
|
|
{ prev: result.previous, limit, order: sortAscending ? 'asc' : 'desc', page: Math.max(page - 1, 1) }
|
|
)
|
|
: false;
|
|
let nextUrl = result.hasNext
|
|
? server.router.render(
|
|
'messages',
|
|
{ user: user.toString(), mailbox: mailbox.toString() },
|
|
{ next: result.next, limit, order: sortAscending ? 'asc' : 'desc', page: page + 1 }
|
|
)
|
|
: false;
|
|
|
|
let response = {
|
|
success: true,
|
|
total,
|
|
page,
|
|
prev: prevUrl,
|
|
next: nextUrl,
|
|
specialUse: mailboxData.specialUse,
|
|
results: (result.results || []).map(messageData => {
|
|
let parsedHeader = (messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
|
let from = parsedHeader.from ||
|
|
parsedHeader.sender || [
|
|
{
|
|
name: '',
|
|
address: (messageData.meta && messageData.meta.from) || ''
|
|
}
|
|
];
|
|
decodeAddresses(from);
|
|
|
|
let response = {
|
|
// we need that uid value for sharding
|
|
// uid in a mailbox is immutable
|
|
id: messageData._id.toString() + ':' + messageData.uid,
|
|
mailbox,
|
|
thread: messageData.thread,
|
|
from: from && from[0],
|
|
subject: messageData.subject,
|
|
date: messageData.hdate.toISOString(),
|
|
intro: messageData.intro,
|
|
attachments: !!messageData.ha,
|
|
seen: !messageData.unseen,
|
|
deleted: !messageData.undeleted,
|
|
flagged: messageData.flagged,
|
|
draft: messageData.draft
|
|
};
|
|
return response;
|
|
})
|
|
};
|
|
|
|
res.json(response);
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
server.get({ name: 'search', path: '/users/:user/search' }, (req, res, next) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
query: Joi.string().max(255).required(),
|
|
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)
|
|
});
|
|
|
|
req.query.user = req.params.user;
|
|
|
|
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 user = new ObjectID(result.value.user);
|
|
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;
|
|
|
|
db.database.collection('users').findOne({
|
|
_id: user
|
|
}, {
|
|
fields: {
|
|
username: true,
|
|
address: true,
|
|
specialUse: 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 filter = {
|
|
user,
|
|
searchable: true,
|
|
$text: { $search: query, $language: 'none' }
|
|
};
|
|
|
|
getFilteredMessageCount(filter, (err, total) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let opts = {
|
|
limit,
|
|
query: filter,
|
|
fields: {
|
|
_id: true,
|
|
uid: true,
|
|
mailbox: true,
|
|
'meta.from': true,
|
|
hdate: true,
|
|
subject: true,
|
|
'mimeTree.parsedHeader.from': true,
|
|
'mimeTree.parsedHeader.sender': true,
|
|
ha: true,
|
|
intro: true,
|
|
unseen: true,
|
|
undeleted: true,
|
|
flagged: true,
|
|
draft: true,
|
|
thread: true
|
|
},
|
|
paginatedField: '_id',
|
|
sortAscending: false
|
|
};
|
|
|
|
if (pageNext) {
|
|
opts.next = pageNext;
|
|
} else if (pagePrev) {
|
|
opts.prev = pagePrev;
|
|
}
|
|
|
|
MongoPaging.find(db.users.collection('messages'), 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('search', { user: user.toString() }, { prev: result.previous, limit, query, page: Math.max(page - 1, 1) })
|
|
: false;
|
|
let nextUrl = result.hasNext
|
|
? server.router.render('search', { user: user.toString() }, { next: result.next, limit, query, page: page + 1 })
|
|
: false;
|
|
|
|
let response = {
|
|
success: true,
|
|
total,
|
|
page,
|
|
prev: prevUrl,
|
|
next: nextUrl,
|
|
results: (result.results || []).map(messageData => {
|
|
let parsedHeader = (messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
|
let from = parsedHeader.from ||
|
|
parsedHeader.sender || [
|
|
{
|
|
name: '',
|
|
address: (messageData.meta && messageData.meta.from) || ''
|
|
}
|
|
];
|
|
decodeAddresses(from);
|
|
|
|
let response = {
|
|
// we need that uid value for sharding
|
|
// uid in a mailbox is immutable
|
|
id: messageData._id.toString() + ':' + messageData.uid,
|
|
mailbox: messageData.mailbox,
|
|
thread: messageData.thread,
|
|
from: from && from[0],
|
|
subject: messageData.subject,
|
|
date: messageData.hdate.toISOString(),
|
|
intro: messageData.intro,
|
|
attachments: !!messageData.ha,
|
|
seen: !messageData.unseen,
|
|
deleted: !messageData.undeleted,
|
|
flagged: messageData.flagged,
|
|
draft: messageData.draft
|
|
};
|
|
return response;
|
|
})
|
|
};
|
|
|
|
res.json(response);
|
|
return next();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
server.get('/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()
|
|
});
|
|
|
|
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]);
|
|
|
|
db.users.collection('messages').findOne({
|
|
_id: message,
|
|
mailbox,
|
|
uid,
|
|
user
|
|
}, {
|
|
fields: {
|
|
_id: true,
|
|
thread: true,
|
|
'meta.from': true,
|
|
'meta.to': true,
|
|
hdate: true,
|
|
'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
|
|
}
|
|
}, (err, messageData) => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'MongoDB Error: ' + err.message
|
|
});
|
|
return next();
|
|
}
|
|
if (!messageData) {
|
|
res.json({
|
|
error: 'This message does not exist'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
let parsedHeader = (messageData.mimeTree && messageData.mimeTree.parsedHeader) || {};
|
|
|
|
let from = parsedHeader.from ||
|
|
parsedHeader.sender || [
|
|
{
|
|
name: '',
|
|
address: (messageData.meta && messageData.meta.from) || ''
|
|
}
|
|
];
|
|
decodeAddresses(from);
|
|
|
|
let replyTo = parsedHeader['reply-to'];
|
|
if (replyTo) {
|
|
decodeAddresses(replyTo);
|
|
}
|
|
|
|
let to = parsedHeader.to;
|
|
if (to) {
|
|
decodeAddresses(to);
|
|
}
|
|
|
|
let cc = parsedHeader.cc;
|
|
if (cc) {
|
|
decodeAddresses(cc);
|
|
}
|
|
|
|
let list;
|
|
if (parsedHeader['list-id'] || parsedHeader['list-unsubscribe']) {
|
|
let listId = parsedHeader['list-id'];
|
|
if (listId) {
|
|
listId = addressparser(listId.toString());
|
|
decodeAddresses(listId);
|
|
listId = listId.shift();
|
|
}
|
|
|
|
let listUnsubscribe = parsedHeader['list-unsubscribe'];
|
|
if (listUnsubscribe) {
|
|
listUnsubscribe = addressparser(listUnsubscribe.toString());
|
|
decodeAddresses(listUnsubscribe);
|
|
}
|
|
|
|
list = {
|
|
id: listId,
|
|
unsubscribe: listUnsubscribe
|
|
};
|
|
}
|
|
|
|
let expires;
|
|
if (messageData.exp) {
|
|
expires = new Date(messageData.rdate).toISOString();
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
id: message.toString() + ':' + uid,
|
|
from: from[0],
|
|
replyTo,
|
|
to,
|
|
cc,
|
|
subject: messageData.subject,
|
|
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 => {
|
|
let id = messageData.map[attachment.id];
|
|
if (!id) {
|
|
return false;
|
|
}
|
|
return {
|
|
id,
|
|
fileName: attachment.fileName,
|
|
contentType: attachment.contentType,
|
|
related: attachment.related,
|
|
sizeKb: attachment.sizeKb
|
|
};
|
|
})
|
|
.filter(attachment => attachment)
|
|
});
|
|
return 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(),
|
|
newMailbox: Joi.string().hex().lowercase().length(24),
|
|
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 newMailbox = result.value.newMailbox ? new ObjectID(result.value.newMailbox) : false;
|
|
let message = new ObjectID(messageparts[0]);
|
|
let uid = Number(messageparts[1]);
|
|
|
|
if (newMailbox) {
|
|
return messageHandler.move(
|
|
{
|
|
user,
|
|
source: { user, mailbox },
|
|
destination: { user, mailbox: newMailbox },
|
|
updates: result.value,
|
|
returnIds: true,
|
|
messages: [uid]
|
|
},
|
|
(err, result, info) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (!info || !info.destinationUid || !info.destinationUid.length) {
|
|
res.json({
|
|
error: 'Could not move message, check if message exists'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
mailbox: newMailbox,
|
|
id: info && info.destinationUid && info.destinationUid[0]
|
|
});
|
|
return next();
|
|
}
|
|
);
|
|
}
|
|
|
|
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.del('/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()
|
|
});
|
|
|
|
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]);
|
|
|
|
db.database.collection('messages').findOne({
|
|
_id: message,
|
|
mailbox,
|
|
uid
|
|
}, {
|
|
fields: {
|
|
_id: true,
|
|
mailbox: true,
|
|
uid: true,
|
|
size: true,
|
|
map: true,
|
|
magic: true,
|
|
unseen: true
|
|
}
|
|
}, (err, messageData) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
if (!messageData) {
|
|
res.json({
|
|
error: 'Message was not found'
|
|
});
|
|
return next();
|
|
}
|
|
|
|
return messageHandler.del(
|
|
{
|
|
user,
|
|
mailbox: { user, mailbox },
|
|
message: messageData
|
|
},
|
|
err => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
res.json({
|
|
success: true
|
|
});
|
|
return next();
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
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 } };
|
|
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 = () => {
|
|
if (journalReading || closed) {
|
|
return;
|
|
}
|
|
journalReading = true;
|
|
loadJournalStream(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);
|
|
};
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/event-stream' });
|
|
|
|
if (lastEventId) {
|
|
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();
|
|
}
|
|
});
|
|
} else {
|
|
db.database.collection('journal').findOne({ user }, { sort: { _id: -1 } }, (err, latest) => {
|
|
if (!err && latest) {
|
|
lastEventId = latest._id;
|
|
}
|
|
setup();
|
|
sendIdleComment();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
function formatJournalData(e) {
|
|
let data = {};
|
|
Object.keys(e).forEach(key => {
|
|
if (!['_id', 'ignore', 'user', 'modseq', 'unseenChange', 'created'].includes(key)) {
|
|
if (e.command !== 'COUNTERS' && key === 'unseen') {
|
|
return;
|
|
}
|
|
data[key] = e[key];
|
|
}
|
|
});
|
|
|
|
let response = [];
|
|
response.push('data: ' + JSON.stringify(data, false, 2).split('\n').join('\ndata: '));
|
|
if (e._id) {
|
|
response.push('id: ' + e._id.toString());
|
|
}
|
|
|
|
return response.join('\n') + '\n\n';
|
|
}
|
|
|
|
function loadJournalStream(req, res, user, lastEventId, done) {
|
|
let query = { user };
|
|
if (lastEventId) {
|
|
query._id = { $gt: lastEventId };
|
|
}
|
|
|
|
let mailboxes = new Set();
|
|
|
|
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(() => {
|
|
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 = new ObjectID(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();
|
|
});
|
|
}
|
|
|
|
lastEventId = e._id;
|
|
|
|
if (!e || !e.command) {
|
|
// skip
|
|
return processNext();
|
|
}
|
|
|
|
switch (e.command) {
|
|
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));
|
|
|
|
processed++;
|
|
processNext();
|
|
});
|
|
};
|
|
|
|
processNext();
|
|
}
|
|
|
|
function getMailboxCounter(mailbox, type, done) {
|
|
let prefix = type ? type : 'total';
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function getFilteredMessageCount(filter, done) {
|
|
if (Object.keys(filter).length === 1 && filter.mailbox) {
|
|
// try to use cached value to get the count
|
|
return getMailboxCounter(filter.mailbox, false, done);
|
|
}
|
|
|
|
db.database.collection('messages').count(filter, (err, total) => {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
done(null, total);
|
|
});
|
|
}
|
|
|
|
function decodeAddresses(addresses) {
|
|
addresses.forEach(address => {
|
|
address.name = (address.name || '').toString();
|
|
if (address.name) {
|
|
try {
|
|
address.name = libmime.decodeWords(address.name);
|
|
} catch (E) {
|
|
//ignore, keep as is
|
|
}
|
|
}
|
|
if (/@xn--/.test(address.address)) {
|
|
address.address =
|
|
address.address.substr(0, address.address.lastIndexOf('@') + 1) +
|
|
punycode.toUnicode(address.address.substr(address.address.lastIndexOf('@') + 1));
|
|
}
|
|
if (address.group) {
|
|
decodeAddresses(address.group);
|
|
}
|
|
});
|
|
}
|
|
|
|
module.exports = done => {
|
|
if (!config.imap.enabled) {
|
|
return setImmediate(() => done(null, false));
|
|
}
|
|
|
|
let started = false;
|
|
|
|
notifier = new ImapNotifier({
|
|
database: db.database,
|
|
redis: db.redis
|
|
});
|
|
userHandler = new UserHandler({ database: db.database, users: db.users, redis: db.redis });
|
|
mailboxHandler = new MailboxHandler({ database: db.database, users: db.users, redis: db.redis, notifier });
|
|
messageHandler = new MessageHandler({ database: db.database, gridfs: db.gridfs, redis: db.redis });
|
|
|
|
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);
|
|
});
|
|
};
|