2017-07-26 16:52:55 +08:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
const config = require('wild-config');
|
2017-11-27 20:20:57 +08:00
|
|
|
const Joi = require('../joi');
|
2017-11-27 05:54:53 +08:00
|
|
|
const MongoPaging = require('mongo-cursor-pagination-node6');
|
2017-07-26 16:52:55 +08:00
|
|
|
const ObjectID = require('mongodb').ObjectID;
|
|
|
|
const tools = require('../tools');
|
2017-10-03 18:09:16 +08:00
|
|
|
const errors = require('../errors');
|
2017-08-03 20:02:02 +08:00
|
|
|
const openpgp = require('openpgp');
|
2017-08-09 21:45:52 +08:00
|
|
|
const addressparser = require('addressparser');
|
|
|
|
const libmime = require('libmime');
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
module.exports = (db, server, userHandler) => {
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {get} /users List registered Users
|
|
|
|
* @apiName GetUsers
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} [query] Partial match of username or default email address
|
|
|
|
* @apiParam {String} [tags] Comma separated list of tags. The User must have at least one to be set
|
|
|
|
* @apiParam {String} [requiredTags] Comma separated list of tags. The User must have all listed tags to be set
|
|
|
|
* @apiParam {Number} [limit=20] How many records to return
|
|
|
|
* @apiParam {Number} [page=1] Current page number. Informational only, page numbers start from 1
|
|
|
|
* @apiParam {Number} [next] Cursor value for next page, retrieved from <code>nextCursor</code> response value
|
|
|
|
* @apiParam {Number} [previous] Cursor value for previous page, retrieved from <code>previousCursor</code> response value
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {Number} total How many results were found
|
|
|
|
* @apiSuccess {Number} page Current page number. Derived from <code>page</code> query argument
|
|
|
|
* @apiSuccess {String} previousCursor Either a cursor string or false if there are not any previous results
|
|
|
|
* @apiSuccess {String} nextCursor Either a cursor string or false if there are not any next results
|
|
|
|
* @apiSuccess {Object[]} results User listing
|
|
|
|
* @apiSuccess {String} results.id Users unique ID (24 byte hex)
|
|
|
|
* @apiSuccess {String} results.username Username of the User
|
|
|
|
* @apiSuccess {String} results.name Name of the User
|
|
|
|
* @apiSuccess {String} results.address Main email address of the User
|
|
|
|
* @apiSuccess {String[]} results.tags List of tags associated with the User'
|
|
|
|
* @apiSuccess {String[]} results.forward A list of email addresses to forward all incoming emails
|
|
|
|
* @apiSuccess {Boolean} results.encryptMessages If <code>true</code> then received messages are encrypted
|
|
|
|
* @apiSuccess {Boolean} results.encryptForwarded If <code>true</code> then forwarded messages are encrypted
|
|
|
|
* @apiSuccess {Object} results.quota Quota usage limits
|
|
|
|
* @apiSuccess {Number} results.quota.allowed Allowed quota of the user in bytes
|
|
|
|
* @apiSuccess {Number} results.quota.used Space used in bytes
|
|
|
|
* @apiSuccess {Boolean} results.hasPasswordSet If <code>true</code> then the User has a password set and can authenticate
|
|
|
|
* @apiSuccess {Boolean} results.activated Is the account activated
|
|
|
|
* @apiSuccess {Boolean} results.disabled If <code>true</code> then the user can not authenticate or receive any new mail
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i http://localhost:8080/users
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "total": 1,
|
|
|
|
* "page": 1,
|
|
|
|
* "previousCursor": false,
|
|
|
|
* "nextCursor": false,
|
|
|
|
* "results": [
|
|
|
|
* {
|
|
|
|
* "id": "59cb948ad80a820b68f05230",
|
|
|
|
* "username": "myuser",
|
|
|
|
* "name": "John Doe",
|
|
|
|
* "address": "john@example.com",
|
|
|
|
* "tags": [],
|
|
|
|
* "forward": [],
|
|
|
|
* "encryptMessages": false,
|
|
|
|
* "encryptForwarded": false,
|
|
|
|
* "quota": {
|
|
|
|
* "allowed": 1073741824,
|
|
|
|
* "used": 17799833
|
|
|
|
* },
|
|
|
|
* "hasPasswordSet": true,
|
|
|
|
* "activated": true,
|
|
|
|
* "disabled": false
|
|
|
|
* }
|
|
|
|
* ]
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "Database error"
|
|
|
|
* }
|
|
|
|
*/
|
2017-07-26 16:52:55 +08:00
|
|
|
server.get({ name: 'users', path: '/users' }, (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
query: Joi.string()
|
2017-09-04 19:49:04 +08:00
|
|
|
.empty('')
|
2017-09-01 19:50:53 +08:00
|
|
|
.lowercase()
|
2017-09-04 19:49:04 +08:00
|
|
|
.max(128),
|
2017-11-03 20:11:59 +08:00
|
|
|
tags: Joi.string()
|
|
|
|
.trim()
|
|
|
|
.empty('')
|
|
|
|
.max(1024),
|
|
|
|
requiredTags: Joi.string()
|
|
|
|
.trim()
|
|
|
|
.empty('')
|
|
|
|
.max(1024),
|
2017-09-01 19:50:53 +08:00
|
|
|
limit: Joi.number()
|
|
|
|
.default(20)
|
|
|
|
.min(1)
|
|
|
|
.max(250),
|
|
|
|
next: Joi.string()
|
2017-09-04 19:49:04 +08:00
|
|
|
.empty('')
|
2017-11-27 20:20:57 +08:00
|
|
|
.mongoCursor()
|
2017-09-04 19:49:04 +08:00
|
|
|
.max(1024),
|
2017-09-01 19:50:53 +08:00
|
|
|
previous: Joi.string()
|
2017-09-04 19:49:04 +08:00
|
|
|
.empty('')
|
2017-11-27 20:20:57 +08:00
|
|
|
.mongoCursor()
|
2017-09-04 19:49:04 +08:00
|
|
|
.max(1024),
|
2017-07-26 16:52:55 +08:00
|
|
|
page: Joi.number().default(1)
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.query, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true,
|
|
|
|
allowUnknown: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let query = result.value.query;
|
|
|
|
let limit = result.value.limit;
|
|
|
|
let page = result.value.page;
|
|
|
|
let pageNext = result.value.next;
|
2017-09-04 20:04:43 +08:00
|
|
|
let pagePrevious = result.value.previous;
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
let filter = query
|
|
|
|
? {
|
2017-11-03 20:11:59 +08:00
|
|
|
$or: [
|
|
|
|
{
|
|
|
|
address: {
|
|
|
|
$regex: query.replace(/\./g, ''),
|
|
|
|
$options: ''
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
unameview: {
|
|
|
|
$regex: query.replace(/\./g, ''),
|
|
|
|
$options: ''
|
|
|
|
}
|
|
|
|
}
|
|
|
|
]
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
|
|
|
: {};
|
|
|
|
|
2017-11-03 20:11:59 +08:00
|
|
|
let tagSeen = new Set();
|
|
|
|
|
|
|
|
let requiredTags = (result.value.requiredTags || '')
|
|
|
|
.split(',')
|
|
|
|
.map(tag => tag.toLowerCase().trim())
|
|
|
|
.filter(tag => {
|
|
|
|
if (tag && !tagSeen.has(tag)) {
|
|
|
|
tagSeen.add(tag);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
let tags = (result.value.tags || '')
|
|
|
|
.split(',')
|
|
|
|
.map(tag => tag.toLowerCase().trim())
|
|
|
|
.filter(tag => {
|
|
|
|
if (tag && !tagSeen.has(tag)) {
|
|
|
|
tagSeen.add(tag);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
|
|
|
|
|
|
|
let tagsview = {};
|
|
|
|
if (requiredTags.length) {
|
|
|
|
tagsview.$all = requiredTags;
|
|
|
|
}
|
|
|
|
if (tags.length) {
|
|
|
|
tagsview.$in = tags;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (requiredTags.length || tags.length) {
|
|
|
|
filter.tagsview = tagsview;
|
2017-10-24 20:25:26 +08:00
|
|
|
}
|
|
|
|
|
2017-07-26 16:52:55 +08:00
|
|
|
db.users.collection('users').count(filter, (err, total) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let opts = {
|
|
|
|
limit,
|
|
|
|
query: filter,
|
|
|
|
fields: {
|
|
|
|
_id: true,
|
|
|
|
username: true,
|
|
|
|
name: true,
|
|
|
|
address: true,
|
2017-11-03 20:11:59 +08:00
|
|
|
tags: true,
|
2017-07-26 16:52:55 +08:00
|
|
|
storageUsed: true,
|
2017-10-24 19:30:33 +08:00
|
|
|
forward: true,
|
|
|
|
targetUrl: true,
|
2017-07-26 16:52:55 +08:00
|
|
|
quota: true,
|
2017-11-03 20:11:59 +08:00
|
|
|
activated: true,
|
2017-10-27 20:45:51 +08:00
|
|
|
disabled: true,
|
2017-11-03 20:11:59 +08:00
|
|
|
password: true,
|
2017-10-30 19:41:53 +08:00
|
|
|
encryptMessages: true,
|
|
|
|
encryptForwarded: true
|
2017-07-26 16:52:55 +08:00
|
|
|
},
|
|
|
|
sortAscending: true
|
|
|
|
};
|
|
|
|
|
|
|
|
if (pageNext) {
|
|
|
|
opts.next = pageNext;
|
2017-09-04 20:04:43 +08:00
|
|
|
} else if (pagePrevious) {
|
|
|
|
opts.previous = pagePrevious;
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
MongoPaging.find(db.users.collection('users'), opts, (err, result) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result.hasPrevious) {
|
|
|
|
page = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let response = {
|
|
|
|
success: true,
|
|
|
|
query,
|
|
|
|
total,
|
|
|
|
page,
|
2017-09-01 19:50:53 +08:00
|
|
|
previousCursor: result.hasPrevious ? result.previous : false,
|
|
|
|
nextCursor: result.hasNext ? result.next : false,
|
2017-07-26 16:52:55 +08:00
|
|
|
results: (result.results || []).map(userData => ({
|
|
|
|
id: userData._id.toString(),
|
|
|
|
username: userData.username,
|
|
|
|
name: userData.name,
|
|
|
|
address: userData.address,
|
2017-11-03 20:11:59 +08:00
|
|
|
tags: userData.tags || [],
|
2017-11-13 21:27:37 +08:00
|
|
|
forward: [].concat(userData.forward || []),
|
2017-10-24 19:30:33 +08:00
|
|
|
targetUrl: userData.targetUrl,
|
2017-10-27 20:45:51 +08:00
|
|
|
encryptMessages: !!userData.encryptMessages,
|
2017-10-30 19:41:53 +08:00
|
|
|
encryptForwarded: !!userData.encryptForwarded,
|
2017-07-26 16:52:55 +08:00
|
|
|
quota: {
|
|
|
|
allowed: Number(userData.quota) || config.maxStorage * 1024 * 1024,
|
|
|
|
used: Math.max(Number(userData.storageUsed) || 0, 0)
|
|
|
|
},
|
2017-11-03 20:11:59 +08:00
|
|
|
hasPasswordSet: !!userData.password,
|
|
|
|
activated: userData.activated,
|
2017-07-26 16:52:55 +08:00
|
|
|
disabled: userData.disabled
|
|
|
|
}))
|
|
|
|
};
|
|
|
|
|
|
|
|
res.json(response);
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {post} /users Create new user
|
|
|
|
* @apiName PostUser
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
2017-12-21 15:59:56 +08:00
|
|
|
* @apiParam {String} name Username of the User. Alphanumeric value. Must start with a letter, dots are allowed but informational only (<em>"user.name"</em> is the same as <em>"username"</em>)
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiParam {String} [name] Name of the User
|
2017-12-21 15:59:56 +08:00
|
|
|
* @apiParam {String} password Password for the account. Set to boolean <code>false</code> to disable password usage
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiParam {String} [address] Default email address for the User (autogenerated if not set)
|
|
|
|
* @apiParam {Boolean} [emptyAddress] If true then do not autogenerate missing email address for the User. Only needed if you want to create an user account that does not have any email address associated
|
|
|
|
* @apiParam {Number} [retention] Default retention time in ms. Set to <code>0</code> to disable
|
|
|
|
* @apiParam {Boolean} [encryptMessages] If <code>true</code> then received messages are encrypted
|
|
|
|
* @apiParam {Boolean} [encryptForwarded] If <code>true</code> then forwarded messages are encrypted
|
|
|
|
* @apiParam {String} [pubKey] Public PGP key for the User that is used for encryption. Use empty string to remove the key
|
|
|
|
* @apiParam {String} [language] Language code for the User
|
|
|
|
* @apiParam {String[]} [forward] A list of email addresses to forward all incoming emails
|
|
|
|
* @apiParam {String} [targetUrl] An URL to post all incoming emails
|
|
|
|
* @apiParam {Number} [quota] Allowed quota of the user in bytes
|
|
|
|
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
|
|
|
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
2017-12-08 20:29:00 +08:00
|
|
|
* @apiParam {Number} [imapMaxUpload] How many bytes can be uploaded via IMAP during 24 hour
|
|
|
|
* @apiParam {Number} [imapMaxDownload] How many bytes can be downloaded via IMAP during 24 hour
|
|
|
|
* @apiParam {Number} [pop3MaxDownload] How many bytes can be downloaded via POP3 during 24 hour
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiParam {String} [sess] Session identifier for the logs
|
|
|
|
* @apiParam {String} [ip] IP address for the logs
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {String} id ID for the created User
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XPOST http://localhost:8080/users \
|
|
|
|
* -H 'Content-type: application/json' \
|
|
|
|
* -d '{
|
|
|
|
* "username": "myuser",
|
|
|
|
* "password": "verysecret",
|
|
|
|
* "name": "John Doe"
|
|
|
|
* }'
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "id": "5a1bda70bfbd1442cd96c6f0"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This username already exists"
|
|
|
|
* }
|
|
|
|
*/
|
2017-07-26 16:52:55 +08:00
|
|
|
server.post('/users', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
username: Joi.string()
|
|
|
|
.lowercase()
|
|
|
|
.regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username')
|
2017-10-24 19:30:33 +08:00
|
|
|
.min(1)
|
|
|
|
.max(32)
|
2017-09-01 19:50:53 +08:00
|
|
|
.required(),
|
|
|
|
password: Joi.string()
|
2017-10-24 19:30:33 +08:00
|
|
|
.allow(false)
|
2017-09-01 19:50:53 +08:00
|
|
|
.max(256)
|
|
|
|
.required(),
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
address: Joi.string().email(),
|
2017-09-06 19:53:09 +08:00
|
|
|
emptyAddress: Joi.boolean()
|
|
|
|
.truthy(['Y', 'true', 'yes', 1])
|
|
|
|
.default(false),
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-09-01 19:50:53 +08:00
|
|
|
language: Joi.string()
|
|
|
|
.min(2)
|
|
|
|
.max(20)
|
|
|
|
.lowercase(),
|
|
|
|
retention: Joi.number()
|
|
|
|
.min(0)
|
|
|
|
.default(0),
|
2017-07-26 16:52:55 +08:00
|
|
|
|
|
|
|
name: Joi.string().max(256),
|
2017-11-13 21:27:37 +08:00
|
|
|
forward: Joi.array().items(Joi.string().email()),
|
2017-07-26 16:52:55 +08:00
|
|
|
targetUrl: Joi.string().max(256),
|
|
|
|
|
2017-09-01 19:50:53 +08:00
|
|
|
quota: Joi.number()
|
|
|
|
.min(0)
|
|
|
|
.default(0),
|
|
|
|
recipients: Joi.number()
|
|
|
|
.min(0)
|
|
|
|
.default(0),
|
|
|
|
forwards: Joi.number()
|
|
|
|
.min(0)
|
|
|
|
.default(0),
|
|
|
|
|
2017-12-08 20:29:00 +08:00
|
|
|
imapMaxUpload: Joi.number().min(0),
|
|
|
|
imapMaxDownload: Joi.number().min(0),
|
|
|
|
pop3MaxDownload: Joi.number().min(0),
|
|
|
|
|
2017-11-03 20:11:59 +08:00
|
|
|
tags: Joi.array().items(
|
|
|
|
Joi.string()
|
|
|
|
.trim()
|
|
|
|
.max(128)
|
|
|
|
),
|
|
|
|
|
2017-09-01 19:50:53 +08:00
|
|
|
pubKey: Joi.string()
|
|
|
|
.empty('')
|
|
|
|
.trim()
|
|
|
|
.regex(/^-----BEGIN PGP PUBLIC KEY BLOCK-----/, 'PGP key format'),
|
|
|
|
encryptMessages: Joi.boolean()
|
|
|
|
.truthy(['Y', 'true', 'yes', 1])
|
|
|
|
.default(false),
|
2017-10-30 19:41:53 +08:00
|
|
|
encryptForwarded: Joi.boolean()
|
|
|
|
.truthy(['Y', 'true', 'yes', 1])
|
|
|
|
.default(false),
|
|
|
|
sess: Joi.string().max(255),
|
2017-07-26 16:52:55 +08:00
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-11-13 21:27:37 +08:00
|
|
|
if (result.value.forward) {
|
|
|
|
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
if ('pubKey' in req.params && !result.value.pubKey) {
|
|
|
|
result.value.pubKey = '';
|
|
|
|
}
|
|
|
|
|
2017-11-03 20:11:59 +08:00
|
|
|
if (result.value.tags) {
|
|
|
|
let tagSeen = new Set();
|
|
|
|
let tags = result.value.tags
|
|
|
|
.map(tag => tag.trim())
|
|
|
|
.filter(tag => {
|
|
|
|
if (tag && !tagSeen.has(tag.toLowerCase())) {
|
|
|
|
tagSeen.add(tag.toLowerCase());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
})
|
|
|
|
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
|
|
|
|
|
|
result.value.tags = tags;
|
|
|
|
result.value.tagsview = tags.map(tag => tag.toLowerCase());
|
|
|
|
}
|
|
|
|
|
2017-11-28 19:57:38 +08:00
|
|
|
if (result.value.address && result.value.address.indexOf('*') >= 0) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: 'Invalid character in email address: *',
|
|
|
|
code: 'InputValidationError'
|
2017-11-28 19:57:38 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
checkPubKey(result.value.pubKey, err => {
|
2017-07-26 16:52:55 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: 'PGP key validation failed. ' + err.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-08-09 21:45:52 +08:00
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
userHandler.create(result.value, (err, id) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: err.message,
|
2017-12-21 16:31:34 +08:00
|
|
|
code: err.code,
|
2017-08-03 20:02:02 +08:00
|
|
|
username: result.value.username
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
res.json({
|
|
|
|
success: !!id,
|
|
|
|
id
|
|
|
|
});
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
return next();
|
|
|
|
});
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-12-21 15:59:56 +08:00
|
|
|
/**
|
|
|
|
* @api {get} /users/resolve/:username Resolve ID for an username
|
|
|
|
* @apiName GetUsername
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} username Username of the User. Alphanumeric value. Must start with a letter, dots are allowed but informational only (<em>"user.name"</em> is the same as <em>"username"</em>)
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {String} id Users unique ID (24 byte hex)
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i http://localhost:8080/users/resolve/testuser
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "id": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
|
|
|
server.get('/users/resolve/:username', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
username: Joi.string()
|
|
|
|
.lowercase()
|
|
|
|
.regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username')
|
|
|
|
.min(1)
|
|
|
|
.max(32)
|
|
|
|
.required()
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-12-21 15:59:56 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let username = result.value.username;
|
|
|
|
|
|
|
|
db.users.collection('users').findOne(
|
|
|
|
{
|
|
|
|
unameview: username.replace(/\./g, '')
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: {
|
|
|
|
_id: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
(err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
id: userData._id
|
|
|
|
});
|
|
|
|
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
2017-11-24 01:27:06 +08:00
|
|
|
/**
|
|
|
|
* @api {get} /users/:id Request User information
|
|
|
|
* @apiName GetUser
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiParam {String} id Users unique ID.
|
2017-11-24 01:27:06 +08:00
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {String} id Users unique ID (24 byte hex)
|
|
|
|
* @apiSuccess {String} username Username of the User
|
|
|
|
* @apiSuccess {String} name Name of the User
|
|
|
|
* @apiSuccess {String} address Main email address of the User
|
|
|
|
* @apiSuccess {Number} retention Default retention time in ms. <code>false</code> if not enabled
|
|
|
|
* @apiSuccess {String[]} enabled2fa List of enabled 2FA methods
|
|
|
|
* @apiSuccess {Boolean} encryptMessages If <code>true</code> then received messages are encrypted
|
|
|
|
* @apiSuccess {Boolean} encryptForwarded If <code>true</code> then forwarded messages are encrypted
|
|
|
|
* @apiSuccess {String} pubKey Public PGP key for the User that is used for encryption
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiSuccess {Object} keyInfo Information about public key or <code>false</code> if key is not available
|
2017-11-24 01:27:06 +08:00
|
|
|
* @apiSuccess {String} keyInfo.name Name listed in public key
|
|
|
|
* @apiSuccess {String} keyInfo.address E-mail address listed in public key
|
|
|
|
* @apiSuccess {String} keyInfo.fingerprint Fingerprint of the public key
|
|
|
|
* @apiSuccess {String[]} forward A list of email addresses to forward all incoming emails
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiSuccess {String} targetUrl An URL to post all incoming emails
|
|
|
|
* @apiSuccess {Object} limits Account limits and usage
|
|
|
|
* @apiSuccess {Object} limits.quota Quota usage limits
|
2017-11-24 01:27:06 +08:00
|
|
|
* @apiSuccess {Number} limits.quota.allowed Allowed quota of the user in bytes
|
|
|
|
* @apiSuccess {Number} limits.quota.used Space used in bytes
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiSuccess {Object} limits.recipients Sending quota
|
2017-11-24 01:27:06 +08:00
|
|
|
* @apiSuccess {Number} limits.recipients.allowed How many messages per 24 hour can be sent
|
|
|
|
* @apiSuccess {Number} limits.recipients.used How many messages are sent during current 24 hour period
|
|
|
|
* @apiSuccess {Number} limits.recipients.ttl Time until the end of current 24 hour period
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiSuccess {Object} limits.forwards Forwarding quota
|
2017-11-24 01:27:06 +08:00
|
|
|
* @apiSuccess {Number} limits.forwards.allowed How many messages per 24 hour can be forwarded
|
|
|
|
* @apiSuccess {Number} limits.forwards.used How many messages are forwarded during current 24 hour period
|
|
|
|
* @apiSuccess {Number} limits.forwards.ttl Time until the end of current 24 hour period
|
|
|
|
* @apiSuccess {String[]} tags List of tags associated with the User
|
|
|
|
* @apiSuccess {Boolean} hasPasswordSet If <code>true</code> then the User has a password set and can authenticate
|
|
|
|
* @apiSuccess {Boolean} activated Is the account activated
|
|
|
|
* @apiSuccess {Boolean} disabled If <code>true</code> then the user can not authenticate or receive any new mail
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i http://localhost:8080/users/59fc66a03e54454869460e45
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "id": "59fc66a03e54454869460e45",
|
|
|
|
* "username": "testuser01",
|
|
|
|
* "name": null,
|
|
|
|
* "address": "testuser01@example.com",
|
|
|
|
* "retention": false,
|
|
|
|
* "enabled2fa": [],
|
|
|
|
* "encryptMessages": false,
|
|
|
|
* "encryptForwarded": false,
|
|
|
|
* "pubKey": "",
|
|
|
|
* "keyInfo": false,
|
|
|
|
* "forward": [],
|
|
|
|
* "targetUrl": "",
|
|
|
|
* "limits": {
|
|
|
|
* "quota": {
|
|
|
|
* "allowed": 107374182400,
|
|
|
|
* "used": 289838
|
|
|
|
* },
|
|
|
|
* "recipients": {
|
|
|
|
* "allowed": 2000,
|
|
|
|
* "used": 0,
|
|
|
|
* "ttl": false
|
|
|
|
* },
|
|
|
|
* "forwards": {
|
|
|
|
* "allowed": 2000,
|
|
|
|
* "used": 0,
|
|
|
|
* "ttl": false
|
|
|
|
* }
|
|
|
|
* },
|
|
|
|
* "tags": ["green", "blue"],
|
|
|
|
* "hasPasswordSet": true,
|
|
|
|
* "activated": true,
|
|
|
|
* "disabled": false
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
2017-07-26 16:52:55 +08:00
|
|
|
server.get('/users/:user', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
|
|
|
.required()
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
db.users.collection('users').findOne(
|
|
|
|
{
|
|
|
|
_id: user
|
|
|
|
},
|
|
|
|
(err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-11-23 17:51:37 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
2017-11-23 17:51:37 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +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
|
|
|
|
errors.notify(err, { userId: user });
|
|
|
|
}
|
2017-10-03 18:09:16 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
let recipients = Number(userData.recipients) || config.maxRecipients;
|
|
|
|
let forwards = Number(userData.forwards) || config.maxForwards;
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
let recipientsSent = Number(result && result[0] && result[0][1]) || 0;
|
|
|
|
let recipientsTtl = Number(result && result[1] && result[1][1]) || 0;
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
let forwardsSent = Number(result && result[2] && result[2][1]) || 0;
|
|
|
|
let forwardsTtl = Number(result && result[3] && result[3][1]) || 0;
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
id: user,
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
username: userData.username,
|
|
|
|
name: userData.name,
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
address: userData.address,
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
language: userData.language,
|
|
|
|
retention: userData.retention || false,
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
enabled2fa: Array.isArray(userData.enabled2fa) ? userData.enabled2fa : [].concat(userData.enabled2fa ? 'totp' : []),
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
encryptMessages: userData.encryptMessages,
|
|
|
|
encryptForwarded: userData.encryptForwarded,
|
|
|
|
pubKey: userData.pubKey,
|
|
|
|
keyInfo: getKeyInfo(userData.pubKey),
|
2017-08-03 20:02:02 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
forward: [].concat(userData.forward || []),
|
|
|
|
targetUrl: userData.targetUrl,
|
2017-07-30 03:08:43 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
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-26 16:52:55 +08:00
|
|
|
},
|
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
tags: userData.tags || [],
|
|
|
|
hasPasswordSet: !!userData.password,
|
|
|
|
activated: userData.activated,
|
|
|
|
disabled: userData.disabled
|
|
|
|
});
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
return next();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
);
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {put} /users/:id Update User information
|
|
|
|
* @apiName PutUser
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} id Users unique ID.
|
|
|
|
* @apiParam {String} [name] Name of the User
|
|
|
|
* @apiParam {String} [existingPassword] If provided then validates against account password before applying any changes
|
2017-12-21 15:59:56 +08:00
|
|
|
* @apiParam {String} [password] New password for the account. Set to boolean <code>false</code> to disable password usage
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiParam {Number} [retention] Default retention time in ms. Set to <code>0</code> to disable
|
|
|
|
* @apiParam {Boolean} [encryptMessages] If <code>true</code> then received messages are encrypted
|
|
|
|
* @apiParam {Boolean} [encryptForwarded] If <code>true</code> then forwarded messages are encrypted
|
|
|
|
* @apiParam {String} [pubKey] Public PGP key for the User that is used for encryption. Use empty string to remove the key
|
|
|
|
* @apiParam {String} [language] Language code for the User
|
|
|
|
* @apiParam {String[]} [forward] A list of email addresses to forward all incoming emails
|
|
|
|
* @apiParam {String} [targetUrl] An URL to post all incoming emails
|
|
|
|
* @apiParam {Number} [quota] Allowed quota of the user in bytes
|
|
|
|
* @apiParam {Number} [recipients] How many messages per 24 hour can be sent
|
|
|
|
* @apiParam {Number} [forwards] How many messages per 24 hour can be forwarded
|
2017-12-08 20:29:00 +08:00
|
|
|
* @apiParam {Number} [imapMaxUpload] How many bytes can be uploaded via IMAP during 24 hour
|
|
|
|
* @apiParam {Number} [imapMaxDownload] How many bytes can be downloaded via IMAP during 24 hour
|
|
|
|
* @apiParam {Number} [pop3MaxDownload] How many bytes can be downloaded via POP3 during 24 hour
|
2017-11-27 20:20:57 +08:00
|
|
|
* @apiParam {Boolean} [disabled] If true then disables user account (can not login, can not receive messages)
|
|
|
|
* @apiParam {String} [sess] Session identifier for the logs
|
|
|
|
* @apiParam {String} [ip] IP address for the logs
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XPUT http://localhost:8080/users/59fc66a03e54454869460e45 \
|
|
|
|
* -H 'Content-type: application/json' \
|
|
|
|
* -d '{
|
|
|
|
* "name": "Updated user name"
|
|
|
|
* }'
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
2017-07-26 16:52:55 +08:00
|
|
|
server.put('/users/:user', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
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),
|
2017-11-13 21:27:37 +08:00
|
|
|
forward: Joi.array().items(Joi.string().email()),
|
2017-09-01 19:50:53 +08:00
|
|
|
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]),
|
2017-10-30 19:41:53 +08:00
|
|
|
encryptForwarded: Joi.boolean()
|
|
|
|
.empty('')
|
|
|
|
.truthy(['Y', 'true', 'yes', 1]),
|
2017-07-26 16:52:55 +08:00
|
|
|
retention: Joi.number().min(0),
|
|
|
|
quota: Joi.number().min(0),
|
|
|
|
recipients: Joi.number().min(0),
|
|
|
|
forwards: Joi.number().min(0),
|
|
|
|
|
2017-12-08 20:29:00 +08:00
|
|
|
imapMaxUpload: Joi.number().min(0),
|
|
|
|
imapMaxDownload: Joi.number().min(0),
|
|
|
|
pop3MaxDownload: Joi.number().min(0),
|
|
|
|
|
2017-11-03 20:11:59 +08:00
|
|
|
tags: Joi.array().items(
|
|
|
|
Joi.string()
|
|
|
|
.trim()
|
|
|
|
.max(128)
|
|
|
|
),
|
|
|
|
|
2017-10-24 20:17:36 +08:00
|
|
|
disabled: Joi.boolean()
|
|
|
|
.empty('')
|
|
|
|
.truthy(['Y', 'true', 'yes', 1]),
|
2017-10-30 19:41:53 +08:00
|
|
|
sess: Joi.string().max(255),
|
2017-07-26 16:52:55 +08:00
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
2017-11-13 21:27:37 +08:00
|
|
|
|
|
|
|
if (result.value.forward) {
|
|
|
|
result.value.forward = [].concat(result.value.forward || []).map(fwd => tools.normalizeAddress(fwd));
|
2017-07-30 03:08:43 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!result.value.targetUrl && 'targetUrl' in req.params) {
|
|
|
|
result.value.targetUrl = '';
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result.value.name && 'name' in req.params) {
|
|
|
|
result.value.name = '';
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
if (!result.value.pubKey && 'pubKey' in req.params) {
|
|
|
|
result.value.pubKey = '';
|
|
|
|
}
|
|
|
|
|
2017-11-03 20:11:59 +08:00
|
|
|
if (result.value.tags) {
|
|
|
|
let tagSeen = new Set();
|
|
|
|
let tags = result.value.tags
|
|
|
|
.map(tag => tag.trim())
|
|
|
|
.filter(tag => {
|
|
|
|
if (tag && !tagSeen.has(tag.toLowerCase())) {
|
|
|
|
tagSeen.add(tag.toLowerCase());
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
})
|
|
|
|
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
|
|
result.value.tags = tags;
|
|
|
|
result.value.tagsview = tags.map(tag => tag.toLowerCase());
|
|
|
|
}
|
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
checkPubKey(result.value.pubKey, err => {
|
2017-07-26 16:52:55 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: 'PGP key validation failed. ' + err.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-08-03 20:02:02 +08:00
|
|
|
|
|
|
|
userHandler.update(user, result.value, (err, success) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: err.message,
|
|
|
|
code: err.code
|
2017-08-03 20:02:02 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
res.json({
|
|
|
|
success
|
|
|
|
});
|
|
|
|
return next();
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {put} /users/:id/logout Log out User
|
|
|
|
* @apiName PutUserLogout
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiDescription This method logs out all user sessions in IMAP
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} id Users unique ID.
|
|
|
|
* @apiParam {String} [reason] Message to be shown to connected IMAP client
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XPUT http://localhost:8080/users/59fc66a03e54454869460e45/logout \
|
|
|
|
* -H 'Content-type: application/json' \
|
|
|
|
* -d '{
|
|
|
|
* "reason": "Logout requested from API"
|
|
|
|
* }'
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
2017-10-02 21:30:27 +08:00
|
|
|
server.put('/users/:user/logout', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
|
|
|
.required(),
|
|
|
|
reason: Joi.string()
|
|
|
|
.empty('')
|
|
|
|
.max(128),
|
2017-10-30 19:41:53 +08:00
|
|
|
sess: Joi.string().max(255),
|
2017-10-02 21:30:27 +08:00
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-10-02 21:30:27 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
userHandler.logout(result.value.user, result.value.reason || 'Logout requested from API', (err, success) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: err.message,
|
|
|
|
code: err.code
|
2017-10-02 21:30:27 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
res.json({
|
|
|
|
success
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {post} /users/:id/quota/reset Recalculate User quota
|
|
|
|
* @apiName PostUserQuota
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiDescription This method recalculates quota usage for an User. Normally not needed, only use it if quota numbers are way off.
|
|
|
|
* This method is not transactional, so if the user is currently receiving new messages then the resulting value is not exact.
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} id Users unique ID.
|
|
|
|
* @apiParam {String} [reason] Message to be shown to connected IMAP client
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {Number} storageUsed Calculated quota usage for the user
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XPOST http://localhost:8080/users/59fc66a03e54454869460e45/quota/reset \
|
|
|
|
* -H 'Content-type: application/json' \
|
|
|
|
* -d '{}'
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "storageUsed": 1234567
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
2017-07-26 16:52:55 +08:00
|
|
|
server.post('/users/:user/quota/reset', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
2017-11-03 20:11:59 +08:00
|
|
|
.required(),
|
|
|
|
sess: Joi.string().max(255),
|
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-08-05 20:55:29 +08:00
|
|
|
let user = new ObjectID(result.value.user);
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
db.users.collection('users').findOne(
|
|
|
|
{
|
|
|
|
_id: user
|
|
|
|
},
|
|
|
|
{
|
|
|
|
fields: {
|
|
|
|
storageUsed: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
(err, userData) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-11-23 17:51:37 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
if (!userData) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
2017-11-23 17:51:37 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
// calculate mailbox size by aggregating the size's of all messages
|
|
|
|
// NB! Scattered query
|
|
|
|
db.database
|
|
|
|
.collection('messages')
|
|
|
|
.aggregate(
|
|
|
|
[
|
|
|
|
{
|
|
|
|
$match: {
|
|
|
|
user
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$group: {
|
|
|
|
_id: {
|
|
|
|
user: '$user'
|
|
|
|
},
|
|
|
|
storageUsed: {
|
|
|
|
$sum: '$size'
|
|
|
|
}
|
|
|
|
}
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
2017-11-23 17:51:37 +08:00
|
|
|
],
|
2017-07-26 16:52:55 +08:00
|
|
|
{
|
2017-11-23 17:51:37 +08:00
|
|
|
cursor: {
|
|
|
|
batchSize: 1
|
2017-07-26 16:52:55 +08:00
|
|
|
}
|
|
|
|
}
|
2017-11-23 17:51:37 +08:00
|
|
|
)
|
|
|
|
.toArray((err, result) => {
|
2017-07-26 16:52:55 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
let storageUsed = (result && result[0] && result[0].storageUsed) || 0;
|
2017-07-26 16:52:55 +08:00
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
// update quota counter
|
|
|
|
db.users.collection('users').findOneAndUpdate(
|
|
|
|
{
|
|
|
|
_id: userData._id
|
|
|
|
},
|
|
|
|
{
|
|
|
|
$set: {
|
|
|
|
storageUsed: Number(storageUsed) || 0
|
|
|
|
}
|
|
|
|
},
|
|
|
|
{
|
|
|
|
returnOriginal: false
|
|
|
|
},
|
|
|
|
(err, result) => {
|
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'MongoDB Error: ' + err.message,
|
|
|
|
code: 'InternalDatabaseError'
|
2017-11-23 17:51:37 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!result || !result.value) {
|
|
|
|
res.json({
|
2017-12-21 15:59:56 +08:00
|
|
|
error: 'This user does not exist',
|
|
|
|
code: 'UserNotFound'
|
2017-11-23 17:51:37 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
storageUsed: Number(result.value.storageUsed) || 0
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
);
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
2017-11-23 17:51:37 +08:00
|
|
|
}
|
|
|
|
);
|
2017-07-26 16:52:55 +08:00
|
|
|
});
|
2017-08-05 20:55:29 +08:00
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {post} /users/:id/password/reset Reset password for an User
|
|
|
|
* @apiName ResetUserPassword
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiDescription This method generates a new temporary password for an User.
|
|
|
|
* Additionally it removes all two-factor authentication settings
|
|
|
|
*
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} id Users unique ID.
|
|
|
|
* @apiParam {String} [sess] Session identifier for the logs
|
|
|
|
* @apiParam {String} [ip] IP address for the logs
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
* @apiSuccess {String} password Temporary password
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XPOST http://localhost:8080/users/5a1bda70bfbd1442cd96/password/reset \
|
|
|
|
* -H 'Content-type: application/json' \
|
|
|
|
* -d '{
|
|
|
|
* "ip": "127.0.0.1"
|
|
|
|
* }'
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true,
|
|
|
|
* "password": "temporarypass"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
2017-08-05 20:55:29 +08:00
|
|
|
server.post('/users/:user/password/reset', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
2017-09-01 19:50:53 +08:00
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
2017-11-03 20:11:59 +08:00
|
|
|
.required(),
|
|
|
|
sess: Joi.string().max(255),
|
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
2017-08-05 20:55:29 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-08-05 20:55:29 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
|
|
|
|
2017-11-03 20:11:59 +08:00
|
|
|
userHandler.reset(user, result.value, (err, password) => {
|
2017-08-05 20:55:29 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: err.message,
|
|
|
|
code: err.code
|
2017-08-05 20:55:29 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
res.json({
|
|
|
|
success: true,
|
|
|
|
password
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
2017-11-17 19:37:53 +08:00
|
|
|
|
2017-11-27 20:20:57 +08:00
|
|
|
/**
|
|
|
|
* @api {delete} /users/:id Delete an User
|
|
|
|
* @apiName DeleteUser
|
|
|
|
* @apiGroup Users
|
|
|
|
* @apiHeader {String} X-Access-Token Optional access token if authentication is enabled
|
|
|
|
* @apiHeaderExample {json} Header-Example:
|
|
|
|
* {
|
|
|
|
* "X-Access-Token": "59fc66a03e54454869460e45"
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiParam {String} id Users unique ID.
|
|
|
|
* @apiParam {String} [sess] Session identifier for the logs
|
|
|
|
* @apiParam {String} [ip] IP address for the logs
|
|
|
|
*
|
|
|
|
* @apiSuccess {Boolean} success Indicates successful response
|
|
|
|
*
|
|
|
|
* @apiError error Description of the error
|
|
|
|
*
|
|
|
|
* @apiExample {curl} Example usage:
|
|
|
|
* curl -i -XDELETE http://localhost:8080/users/5a1bda70bfbd1442cd96c6f0?ip=127.0.0.1
|
|
|
|
*
|
|
|
|
* @apiSuccessExample {json} Success-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "success": true
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @apiErrorExample {json} Error-Response:
|
|
|
|
* HTTP/1.1 200 OK
|
|
|
|
* {
|
|
|
|
* "error": "This user does not exist"
|
|
|
|
* }
|
|
|
|
*/
|
2017-11-17 19:37:53 +08:00
|
|
|
server.del('/users/:user', (req, res, next) => {
|
|
|
|
res.charSet('utf-8');
|
|
|
|
|
|
|
|
const schema = Joi.object().keys({
|
|
|
|
user: Joi.string()
|
|
|
|
.hex()
|
|
|
|
.lowercase()
|
|
|
|
.length(24)
|
2017-11-23 17:51:37 +08:00
|
|
|
.required(),
|
|
|
|
sess: Joi.string().max(255),
|
|
|
|
ip: Joi.string().ip({
|
|
|
|
version: ['ipv4', 'ipv6'],
|
|
|
|
cidr: 'forbidden'
|
|
|
|
})
|
2017-11-17 19:37:53 +08:00
|
|
|
});
|
|
|
|
|
2017-11-23 17:51:37 +08:00
|
|
|
if (req.query.sess) {
|
|
|
|
req.params.sess = req.query.sess;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (req.query.ip) {
|
|
|
|
req.params.ip = req.query.ip;
|
|
|
|
}
|
|
|
|
|
2017-11-17 19:37:53 +08:00
|
|
|
const result = Joi.validate(req.params, schema, {
|
|
|
|
abortEarly: false,
|
|
|
|
convert: true
|
|
|
|
});
|
|
|
|
|
|
|
|
if (result.error) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: result.error.message,
|
|
|
|
code: 'InputValidationError'
|
2017-11-17 19:37:53 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
|
|
|
|
let user = new ObjectID(result.value.user);
|
2017-11-23 17:51:37 +08:00
|
|
|
userHandler.delete(user, {}, (err, status) => {
|
2017-11-17 19:37:53 +08:00
|
|
|
if (err) {
|
|
|
|
res.json({
|
2017-12-21 16:31:34 +08:00
|
|
|
error: err.message,
|
|
|
|
code: err.code
|
2017-11-17 19:37:53 +08:00
|
|
|
});
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
res.json({
|
|
|
|
success: status
|
|
|
|
});
|
|
|
|
return next();
|
|
|
|
});
|
|
|
|
});
|
2017-07-26 16:52:55 +08:00
|
|
|
};
|
2017-08-03 20:02:02 +08:00
|
|
|
|
2017-08-09 21:45:52 +08:00
|
|
|
function getKeyInfo(pubKey) {
|
|
|
|
if (!pubKey) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// try to encrypt something with that key
|
|
|
|
let armored;
|
|
|
|
try {
|
|
|
|
armored = openpgp.key.readArmored(pubKey).keys;
|
|
|
|
} catch (E) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!armored || !armored[0]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
let fingerprint = armored[0].primaryKey.fingerprint;
|
|
|
|
let name, address;
|
|
|
|
if (armored && armored[0] && armored[0].users && armored[0].users[0] && armored[0].users[0].userId) {
|
|
|
|
let user = addressparser(armored[0].users[0].userId.userid);
|
|
|
|
if (user && user[0] && user[0].address) {
|
|
|
|
address = tools.normalizeAddress(user[0].address);
|
|
|
|
try {
|
|
|
|
name = libmime.decodeWords(user[0].name || '').trim();
|
|
|
|
} catch (E) {
|
|
|
|
// failed to parse value
|
|
|
|
name = user[0].name || '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
address,
|
|
|
|
fingerprint
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-08-09 21:45:52 +08:00
|
|
|
if (!armored || !armored[0]) {
|
|
|
|
return done(new Error('Did not find key information'));
|
|
|
|
}
|
|
|
|
|
|
|
|
let fingerprint = armored[0].primaryKey.fingerprint;
|
|
|
|
let name, address;
|
|
|
|
if (armored && armored[0] && armored[0].users && armored[0].users[0] && armored[0].users[0].userId) {
|
|
|
|
let user = addressparser(armored[0].users[0].userId.userid);
|
|
|
|
if (user && user[0] && user[0].address) {
|
|
|
|
address = tools.normalizeAddress(user[0].address);
|
|
|
|
try {
|
|
|
|
name = libmime.decodeWords(user[0].name || '').trim();
|
|
|
|
} catch (E) {
|
|
|
|
// failed to parse value
|
|
|
|
name = user[0].name || '';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-03 20:02:02 +08:00
|
|
|
openpgp
|
|
|
|
.encrypt({
|
|
|
|
data: 'Hello, World!',
|
|
|
|
publicKeys: armored
|
|
|
|
})
|
|
|
|
.then(ciphertext => {
|
|
|
|
if (/^-----BEGIN PGP MESSAGE/.test(ciphertext.data)) {
|
|
|
|
// everything checks out
|
2017-08-09 21:45:52 +08:00
|
|
|
return done(null, {
|
|
|
|
address,
|
|
|
|
name,
|
|
|
|
fingerprint
|
|
|
|
});
|
2017-08-03 20:02:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return done(new Error('Unexpected message'));
|
|
|
|
})
|
|
|
|
.catch(err => done(err));
|
|
|
|
}
|