mirror of
https://github.com/nodemailer/wildduck.git
synced 2024-12-27 02:10:52 +08:00
477 lines
17 KiB
JavaScript
477 lines
17 KiB
JavaScript
'use strict';
|
|
|
|
const config = require('wild-config');
|
|
const Joi = require('joi');
|
|
const ObjectId = require('mongodb').ObjectId;
|
|
const mobileconfig = require('mobileconfig');
|
|
const uuid = require('uuid');
|
|
const consts = require('../consts');
|
|
const certs = require('../certs').get('api.mobileconfig');
|
|
const tools = require('../tools');
|
|
const roles = require('../roles');
|
|
const util = require('util');
|
|
const { sessSchema, sessIPSchema, booleanSchema } = require('../schemas');
|
|
|
|
module.exports = (db, server, userHandler) => {
|
|
const mobileconfigGetSignedConfig = util.promisify(mobileconfig.getSignedConfig.bind(mobileconfig));
|
|
|
|
server.get(
|
|
'/users/:user/asps',
|
|
tools.responseWrapper(async (req, res) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
showAll: booleanSchema.default(false),
|
|
sess: sessSchema,
|
|
ip: sessIPSchema
|
|
});
|
|
|
|
const result = schema.validate(req.params, {
|
|
abortEarly: false,
|
|
convert: true
|
|
});
|
|
|
|
if (result.error) {
|
|
res.status(400);
|
|
return res.json({
|
|
error: result.error.message,
|
|
code: 'InputValidationError',
|
|
details: tools.validationErrors(result)
|
|
});
|
|
}
|
|
|
|
// permissions check
|
|
if (req.user && req.user === result.value.user) {
|
|
req.validate(roles.can(req.role).readOwn('asps'));
|
|
} else {
|
|
req.validate(roles.can(req.role).readAny('asps'));
|
|
}
|
|
|
|
let user = new ObjectId(result.value.user);
|
|
let showAll = result.value.showAll;
|
|
|
|
let userData;
|
|
|
|
try {
|
|
userData = await db.users.collection('users').findOne(
|
|
{
|
|
_id: user
|
|
},
|
|
{
|
|
projection: {
|
|
address: true
|
|
}
|
|
}
|
|
);
|
|
} catch (err) {
|
|
res.status(500);
|
|
return res.json({
|
|
error: 'MongoDB Error: ' + err.message,
|
|
code: 'InternalDatabaseError'
|
|
});
|
|
}
|
|
|
|
if (!userData) {
|
|
res.status(404);
|
|
return res.json({
|
|
error: 'This user does not exist',
|
|
code: 'UserNotFound'
|
|
});
|
|
}
|
|
|
|
let asps;
|
|
try {
|
|
asps = await db.users
|
|
.collection('asps')
|
|
.find({
|
|
user
|
|
})
|
|
.sort({ _id: 1 })
|
|
.toArray();
|
|
} catch (err) {
|
|
res.status(500);
|
|
return res.json({
|
|
error: 'MongoDB Error: ' + err.message,
|
|
code: 'InternalDatabaseError'
|
|
});
|
|
}
|
|
|
|
if (!asps) {
|
|
asps = [];
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
|
|
results: asps
|
|
.filter(asp => {
|
|
if (showAll) {
|
|
return true;
|
|
}
|
|
if (asp.ttl) {
|
|
return false;
|
|
}
|
|
return true;
|
|
})
|
|
.map(asp => ({
|
|
id: asp._id.toString(),
|
|
description: asp.description,
|
|
scopes: asp.scopes.includes('*') ? [...consts.SCOPES] : asp.scopes,
|
|
lastUse: {
|
|
time: asp.used || false,
|
|
event: asp.authEvent || false
|
|
},
|
|
expires: asp.expires,
|
|
created: asp.created
|
|
}))
|
|
});
|
|
})
|
|
);
|
|
|
|
server.get(
|
|
'/users/:user/asps/:asp',
|
|
tools.responseWrapper(async (req, res) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
asp: Joi.string().hex().lowercase().length(24).required(),
|
|
sess: sessSchema,
|
|
ip: sessIPSchema
|
|
});
|
|
|
|
const result = schema.validate(req.params, {
|
|
abortEarly: false,
|
|
convert: true
|
|
});
|
|
|
|
if (result.error) {
|
|
res.status(400);
|
|
return res.json({
|
|
error: result.error.message,
|
|
code: 'InputValidationError',
|
|
details: tools.validationErrors(result)
|
|
});
|
|
}
|
|
|
|
// permissions check
|
|
if (req.user && req.user === result.value.user) {
|
|
req.validate(roles.can(req.role).readOwn('asps'));
|
|
} else {
|
|
req.validate(roles.can(req.role).readAny('asps'));
|
|
}
|
|
|
|
let user = new ObjectId(result.value.user);
|
|
let asp = new ObjectId(result.value.asp);
|
|
|
|
let aspData;
|
|
|
|
try {
|
|
aspData = await db.users.collection('asps').findOne({
|
|
_id: asp,
|
|
user
|
|
});
|
|
} catch (err) {
|
|
res.status(500);
|
|
return res.json({
|
|
error: 'MongoDB Error: ' + err.message,
|
|
code: 'InternalDatabaseError'
|
|
});
|
|
}
|
|
|
|
if (!aspData) {
|
|
res.status(404);
|
|
return res.json({
|
|
error: 'Invalid or unknown ASP key',
|
|
code: 'AspNotFound'
|
|
});
|
|
}
|
|
|
|
return res.json({
|
|
success: true,
|
|
id: aspData._id.toString(),
|
|
description: aspData.description,
|
|
scopes: aspData.scopes.includes('*') ? [...consts.SCOPES] : aspData.scopes,
|
|
lastUse: {
|
|
time: aspData.used || false,
|
|
event: aspData.authEvent || false
|
|
},
|
|
expires: asp.expires,
|
|
created: aspData.created
|
|
});
|
|
})
|
|
);
|
|
|
|
server.post(
|
|
'/users/:user/asps',
|
|
tools.responseWrapper(async (req, res) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
description: Joi.string().trim().max(255).required(),
|
|
scopes: Joi.array()
|
|
.items(
|
|
Joi.string()
|
|
.valid(...consts.SCOPES, '*')
|
|
.required()
|
|
)
|
|
.unique(),
|
|
address: Joi.string().empty('').email({ tlds: false }),
|
|
password: Joi.string()
|
|
.empty('')
|
|
.pattern(/^[a-z]{16}$/, { name: 'password' }),
|
|
generateMobileconfig: booleanSchema.default(false),
|
|
ttl: Joi.number().empty([0, '']),
|
|
sess: sessSchema,
|
|
ip: sessIPSchema
|
|
});
|
|
|
|
if (typeof req.params.scopes === 'string') {
|
|
req.params.scopes = req.params.scopes
|
|
.split(',')
|
|
.map(scope => scope.trim())
|
|
.filter(scope => scope);
|
|
}
|
|
|
|
const result = schema.validate(req.params, {
|
|
abortEarly: false,
|
|
convert: true
|
|
});
|
|
|
|
if (result.error) {
|
|
res.status(400);
|
|
return res.json({
|
|
error: result.error.message,
|
|
code: 'InputValidationError',
|
|
details: tools.validationErrors(result)
|
|
});
|
|
}
|
|
|
|
// permissions check
|
|
if (req.user && req.user === result.value.user) {
|
|
req.validate(roles.can(req.role).createOwn('asps'));
|
|
} else {
|
|
req.validate(roles.can(req.role).createAny('asps'));
|
|
}
|
|
|
|
let user = new ObjectId(result.value.user);
|
|
let generateMobileconfig = result.value.generateMobileconfig;
|
|
let scopes = result.value.scopes || ['*'];
|
|
let description = result.value.description;
|
|
|
|
if (scopes.includes('*')) {
|
|
scopes = ['*'];
|
|
}
|
|
|
|
if (generateMobileconfig && !scopes.includes('*') && ((!scopes.includes('imap') && !scopes.includes('pop3')) || !scopes.includes('smtp'))) {
|
|
res.status(400);
|
|
return res.json({
|
|
error: 'Profile file requires either imap or pop3 and smtp scopes',
|
|
code: 'InvalidAuthScope'
|
|
});
|
|
}
|
|
|
|
let userData;
|
|
try {
|
|
userData = await db.users.collection('users').findOne(
|
|
{
|
|
_id: user
|
|
},
|
|
{
|
|
projection: {
|
|
username: true,
|
|
name: true,
|
|
address: true
|
|
}
|
|
}
|
|
);
|
|
} catch (err) {
|
|
res.status(500);
|
|
return res.json({
|
|
error: 'MongoDB Error: ' + err.message,
|
|
code: 'InternalDatabaseError'
|
|
});
|
|
}
|
|
|
|
if (!userData) {
|
|
res.status(404);
|
|
return res.json({
|
|
error: 'This user does not exist',
|
|
code: 'UserNotFound'
|
|
});
|
|
}
|
|
|
|
let accountType;
|
|
let accountHost;
|
|
let accountPort;
|
|
let accountSecure;
|
|
let accountAddress;
|
|
let accountName;
|
|
|
|
if (result.value.address) {
|
|
let addressData;
|
|
try {
|
|
addressData = await db.users.collection('addresses').findOne({
|
|
addrview: tools.normalizeAddress(result.value.address, false, {
|
|
removeLabel: true,
|
|
removeDots: true
|
|
})
|
|
});
|
|
} catch (err) {
|
|
res.status(500);
|
|
return res.json({
|
|
error: 'MongoDB Error: ' + err.message,
|
|
code: 'InternalDatabaseError'
|
|
});
|
|
}
|
|
|
|
if (!addressData || !addressData.user.equals(userData._id)) {
|
|
res.status(404);
|
|
return res.json({
|
|
error: 'Invalid or unknown address',
|
|
code: 'AddressNotFound'
|
|
});
|
|
}
|
|
|
|
accountName = addressData.name || userData.name || '';
|
|
accountAddress = addressData.address;
|
|
} else {
|
|
accountName = userData.name || '';
|
|
accountAddress = userData.address;
|
|
}
|
|
|
|
let asp = await userHandler.generateASP(user, result.value);
|
|
|
|
if (!generateMobileconfig) {
|
|
return res.json({
|
|
success: true,
|
|
id: asp.id,
|
|
password: asp.password
|
|
});
|
|
}
|
|
|
|
let profileOpts = {};
|
|
Object.keys(config.api.mobileconfig || {}).forEach(key => {
|
|
profileOpts[key] = (config.api.mobileconfig[key] || '')
|
|
.toString()
|
|
.replace(/\{email\}/g, accountAddress)
|
|
.replace(/\{name\}/g, accountName)
|
|
.trim();
|
|
});
|
|
|
|
if (scopes.includes('*') || scopes.includes('imap')) {
|
|
// prefer IMAP
|
|
accountType = 'EmailTypeIMAP';
|
|
accountHost = config.imap.setup.hostname;
|
|
accountPort = config.imap.setup.port || config.imap.port;
|
|
accountSecure = !!config.imap.setup.secure;
|
|
} else {
|
|
accountType = 'EmailTypePOP';
|
|
accountHost = config.pop3.setup.hostname;
|
|
accountPort = config.pop3.setup.port || config.pop3.port;
|
|
accountSecure = !!config.pop3.setup.secure;
|
|
}
|
|
|
|
let profile = await mobileconfigGetSignedConfig(
|
|
{
|
|
PayloadType: 'Configuration',
|
|
PayloadVersion: 1,
|
|
PayloadIdentifier: profileOpts.identifier + '.' + userData._id,
|
|
PayloadUUID: uuid.v4(),
|
|
PayloadDisplayName: description || profileOpts.displayName,
|
|
PayloadDescription: profileOpts.displayDescription,
|
|
PayloadOrganization: profileOpts.organization || 'WildDuck Mail Server',
|
|
|
|
PayloadContent: [
|
|
{
|
|
PayloadType: 'com.apple.mail.managed',
|
|
PayloadVersion: 1,
|
|
PayloadIdentifier: profileOpts.identifier + '.' + userData._id,
|
|
PayloadUUID: uuid.v4(),
|
|
PayloadDisplayName: 'Email Account',
|
|
PayloadDescription: 'Configures email account',
|
|
PayloadOrganization: profileOpts.organization || 'WildDuck Mail Server',
|
|
|
|
EmailAccountDescription: profileOpts.accountDescription,
|
|
EmailAccountName: accountName,
|
|
EmailAccountType: accountType,
|
|
EmailAddress: accountAddress,
|
|
IncomingMailServerAuthentication: 'EmailAuthPassword',
|
|
IncomingMailServerHostName: accountHost,
|
|
IncomingMailServerPortNumber: accountPort,
|
|
IncomingMailServerUseSSL: accountSecure,
|
|
IncomingMailServerUsername: accountAddress,
|
|
IncomingPassword: asp.password,
|
|
OutgoingPasswordSameAsIncomingPassword: true,
|
|
OutgoingMailServerAuthentication: 'EmailAuthPassword',
|
|
OutgoingMailServerHostName: config.smtp.setup.hostname,
|
|
OutgoingMailServerPortNumber: config.smtp.setup.port || config.smtp.port,
|
|
OutgoingMailServerUseSSL: 'secure' in config.smtp.setup ? !!config.smtp.setup.secure : config.smtp.secure,
|
|
OutgoingMailServerUsername: accountAddress,
|
|
PreventMove: false,
|
|
PreventAppSheet: false,
|
|
SMIMEEnabled: false,
|
|
allowMailDrop: true
|
|
}
|
|
]
|
|
},
|
|
certs
|
|
);
|
|
|
|
return res.json({
|
|
success: true,
|
|
id: asp.id,
|
|
name: accountName,
|
|
address: accountAddress,
|
|
password: asp.password,
|
|
mobileconfig: profile.toString('base64')
|
|
});
|
|
})
|
|
);
|
|
|
|
server.del(
|
|
'/users/:user/asps/:asp',
|
|
tools.responseWrapper(async (req, res) => {
|
|
res.charSet('utf-8');
|
|
|
|
const schema = Joi.object().keys({
|
|
user: Joi.string().hex().lowercase().length(24).required(),
|
|
asp: Joi.string().hex().lowercase().length(24).required(),
|
|
sess: sessSchema,
|
|
ip: sessIPSchema
|
|
});
|
|
|
|
const result = schema.validate(req.params, {
|
|
abortEarly: false,
|
|
convert: true
|
|
});
|
|
|
|
if (result.error) {
|
|
res.status(400);
|
|
return res.json({
|
|
error: result.error.message,
|
|
code: 'InputValidationError',
|
|
details: tools.validationErrors(result)
|
|
});
|
|
}
|
|
|
|
// permissions check
|
|
if (req.user && req.user === result.value.user) {
|
|
req.validate(roles.can(req.role).deleteOwn('asps'));
|
|
} else {
|
|
req.validate(roles.can(req.role).deleteAny('asps'));
|
|
}
|
|
|
|
let user = new ObjectId(result.value.user);
|
|
let asp = new ObjectId(result.value.asp);
|
|
|
|
await userHandler.deleteASP(user, asp, result.value);
|
|
|
|
return res.json({
|
|
success: true
|
|
});
|
|
})
|
|
);
|
|
};
|