mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-11-11 01:54:04 +08:00
538 lines
17 KiB
JavaScript
538 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('wild-config');
|
|
const Joi = require('joi');
|
|
const MongoPaging = require('mongo-cursor-pagination');
|
|
const ObjectID = require('mongodb').ObjectID;
|
|
const tools = require('../tools');
|
|
const openpgp = require('openpgp');
|
|
|
|
module.exports = (db, server, userHandler) => {
|
|
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
|
|
? {
|
|
unameview: {
|
|
$regex: query.replace(/\./g, ''),
|
|
$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,
|
|
name: 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,
|
|
name: userData.name,
|
|
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.post('/users', (req, res, next) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
username: Joi.string().lowercase().regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username').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),
|
|
|
|
name: Joi.string().max(256),
|
|
forward: Joi.string().email(),
|
|
targetUrl: Joi.string().max(256),
|
|
|
|
quota: Joi.number().min(0).default(0),
|
|
recipients: Joi.number().min(0).default(0),
|
|
forwards: Joi.number().min(0).default(0),
|
|
|
|
pubKey: Joi.string().empty('').trim().regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
|
|
encryptMessages: Joi.boolean().truthy(['Y', 'true', 'yes', 1]).default(false),
|
|
|
|
ip: Joi.string().ip({
|
|
version: ['ipv4', 'ipv6'],
|
|
cidr: 'forbidden'
|
|
})
|
|
});
|
|
|
|
let forward = req.params.forward ? tools.normalizeAddress(req.params.forward) : false;
|
|
|
|
if (forward && /[\u0080-\uFFFF]/.test(forward)) {
|
|
// replace unicode characters in email addresses before validation
|
|
req.params.forward = forward.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();
|
|
}
|
|
|
|
if (forward) {
|
|
result.value.forward = forward;
|
|
}
|
|
|
|
if ('pubKey' in req.params && !result.value.pubKey) {
|
|
result.value.pubKey = '';
|
|
}
|
|
|
|
checkPubKey(result.value.pubKey, err => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'PGP key validation failed. ' + err.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.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,
|
|
name: userData.name,
|
|
|
|
address: userData.address,
|
|
|
|
language: userData.language,
|
|
retention: userData.retention || false,
|
|
|
|
enabled2fa: userData.enabled2fa,
|
|
|
|
encryptMessages: userData.encryptMessages,
|
|
pubKey: userData.pubKey,
|
|
|
|
forward: userData.forward,
|
|
targetUrl: userData.targetUrl,
|
|
|
|
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.put('/users/:user', (req, res, next) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
|
|
existingPassword: Joi.string().empty('').min(1).max(256),
|
|
password: Joi.string().min(8).max(256),
|
|
|
|
language: Joi.string().min(2).max(20).lowercase(),
|
|
|
|
name: Joi.string().empty('').max(256),
|
|
forward: Joi.string().empty('').email(),
|
|
targetUrl: Joi.string().empty('').max(256),
|
|
|
|
pubKey: Joi.string().empty('').trim().regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
|
|
encryptMessages: Joi.boolean().empty('').truthy(['Y', 'true', 'yes', 1]),
|
|
|
|
retention: Joi.number().min(0),
|
|
quota: Joi.number().min(0),
|
|
recipients: Joi.number().min(0),
|
|
forwards: Joi.number().min(0),
|
|
|
|
ip: Joi.string().ip({
|
|
version: ['ipv4', 'ipv6'],
|
|
cidr: 'forbidden'
|
|
})
|
|
});
|
|
|
|
let forward = req.params.forward ? tools.normalizeAddress(req.params.forward) : false;
|
|
|
|
if (forward && /[\u0080-\uFFFF]/.test(forward)) {
|
|
// replace unicode characters in email addresses before validation
|
|
req.params.forward = forward.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);
|
|
if (forward) {
|
|
result.value.forward = forward;
|
|
} else if (!result.value.forward && 'forward' in req.params) {
|
|
result.value.forward = '';
|
|
}
|
|
|
|
if (!result.value.targetUrl && 'targetUrl' in req.params) {
|
|
result.value.targetUrl = '';
|
|
}
|
|
|
|
if (!result.value.name && 'name' in req.params) {
|
|
result.value.name = '';
|
|
}
|
|
|
|
if (!result.value.pubKey && 'pubKey' in req.params) {
|
|
result.value.pubKey = '';
|
|
}
|
|
|
|
checkPubKey(result.value.pubKey, err => {
|
|
if (err) {
|
|
res.json({
|
|
error: 'PGP key validation failed. ' + err.message
|
|
});
|
|
return next();
|
|
}
|
|
|
|
userHandler.update(user, result.value, (err, success) => {
|
|
if (err) {
|
|
res.json({
|
|
error: err.message
|
|
});
|
|
return next();
|
|
}
|
|
res.json({
|
|
success
|
|
});
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
function checkPubKey(pubKey, done) {
|
|
if (!pubKey) {
|
|
return done();
|
|
}
|
|
|
|
// try to encrypt something with that key
|
|
let armored;
|
|
try {
|
|
armored = openpgp.key.readArmored(pubKey).keys;
|
|
} catch (E) {
|
|
return done(E);
|
|
}
|
|
|
|
openpgp
|
|
.encrypt({
|
|
data: 'Hello, World!',
|
|
publicKeys: armored
|
|
})
|
|
.then(ciphertext => {
|
|
if (/^-----BEGIN PGP MESSAGE/.test(ciphertext.data)) {
|
|
// everything checks out
|
|
return done();
|
|
}
|
|
|
|
return done(new Error('Unexpected message'));
|
|
})
|
|
.catch(err => done(err));
|
|
}
|