allow name properties for addresses

This commit is contained in:
Andris Reinman 2018-01-24 13:37:57 +02:00
parent 3c14de845e
commit 261f28c15c
8 changed files with 298 additions and 204 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-24T09:29:05.158Z", "url": "http://apidocjs.com", "version": "0.17.6" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-24T11:37:44.868Z", "url": "http://apidocjs.com", "version": "0.17.6" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-24T09:29:05.158Z", "url": "http://apidocjs.com", "version": "0.17.6" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs", "title": "WildDuck API", "url": "https://api.wildduck.email", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-24T11:37:44.868Z", "url": "http://apidocjs.com", "version": "0.17.6" } }

View file

@ -33,6 +33,7 @@ module.exports = (db, server) => {
* @apiSuccess {String} nextCursor Either a cursor string or false if there are not any next results
* @apiSuccess {Object[]} results Address listing
* @apiSuccess {String} results.id ID of the Address
* @apiSuccess {String} results.name Identity name
* @apiSuccess {String} results.address E-mail address string
* @apiSuccess {String} results.user User ID this address belongs to if this is an User address
* @apiSuccess {Boolean} results.forwarded If true then it is a forwarded address
@ -181,6 +182,7 @@ module.exports = (db, server) => {
fields: {
_id: true,
address: true,
name: true,
user: true,
tags: true,
targets: true
@ -216,6 +218,7 @@ module.exports = (db, server) => {
nextCursor: result.hasNext ? result.next : false,
results: (result.results || []).map(addressData => ({
id: addressData._id.toString(),
name: addressData.name || false,
address: addressData.address,
user: addressData.user,
forwarded: addressData.targets && true,
@ -246,6 +249,7 @@ module.exports = (db, server) => {
*
* @apiParam {String} user ID of the User
* @apiParam {String} address E-mail Address
* @apiParam {String} [name] Identity name
* @apiParam {String[]} [tags] A list of tags associated with this address
* @apiParam {Boolean} [main=false] Indicates if this is the default address for the User
* @apiParam {Boolean} [allowWildcard=false] If <code>true</code> then address value can be in the form of <code>*@example.com</code>, otherwise using * is not allowed
@ -290,6 +294,11 @@ module.exports = (db, server) => {
.required(),
Joi.string().regex(/^\w+@\*$/, 'special address')
],
name: Joi.string()
.empty('')
.trim()
.max(128)
.required(),
main: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', 1])
.falsy(['N', 'false', 'no', 'off', 0, '']),
@ -318,6 +327,7 @@ module.exports = (db, server) => {
let user = new ObjectID(result.value.user);
let main = result.value.main;
let name = result.value.name;
let address = tools.normalizeAddress(result.value.address);
if (address.indexOf('+') >= 0) {
@ -417,6 +427,7 @@ module.exports = (db, server) => {
addressData = {
user,
name,
address,
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
created: new Date()
@ -487,6 +498,7 @@ module.exports = (db, server) => {
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {Object[]} results Address listing
* @apiSuccess {String} results.id ID of the Address
* @apiSuccess {String} results.name Identity name
* @apiSuccess {String} results.address E-mail address string
* @apiSuccess {Boolean} results.main Indicates if this is the default address for the User
* @apiSuccess {String} results.created Datestring of the time the address was created
@ -553,6 +565,7 @@ module.exports = (db, server) => {
},
{
fields: {
name: true,
address: true
}
},
@ -598,6 +611,7 @@ module.exports = (db, server) => {
results: addresses.map(address => ({
id: address._id,
name: address.name || false,
address: address.address,
main: address.address === userData.address,
tags: address.tags || [],
@ -626,6 +640,7 @@ module.exports = (db, server) => {
*
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Address
* @apiSuccess {String} name Identity name
* @apiSuccess {String} address E-mail address string
* @apiSuccess {Boolean} main Indicates if this is the default address for the User
* @apiSuccess {String} created Datestring of the time the address was created
@ -689,6 +704,7 @@ module.exports = (db, server) => {
},
{
fields: {
name: true,
address: true
}
},
@ -733,6 +749,7 @@ module.exports = (db, server) => {
res.json({
success: true,
id: addressData._id,
name: addressData.name || false,
address: addressData.address,
main: addressData.address === userData.address,
created: addressData.created
@ -756,8 +773,11 @@ module.exports = (db, server) => {
* }
*
* @apiParam {String} user ID of the User
* @apiParam {String} address ID of the Address
* @apiParam {String} id ID of the Address
* @apiParam {String} [name] Identity name
* @apiParam {String} [address] New address if you want to rename existing address. Only affects normal addresses, special addresses that include \* can not be changed
* @apiParam {Boolean} main Indicates if this is the default address for the User
* @apiParam {String[]} [tags] A list of tags associated with this address
*
* @apiSuccess {Boolean} success Indicates successful response
@ -797,6 +817,10 @@ module.exports = (db, server) => {
.lowercase()
.length(24)
.required(),
name: Joi.string()
.empty('')
.trim()
.max(128),
address: Joi.string().email(),
main: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', 1])
@ -842,6 +866,10 @@ module.exports = (db, server) => {
updates.addrview = addrview;
}
if (result.value.name) {
updates.name = result.value.name;
}
if (result.value.tags) {
let tagSeen = new Set();
let tags = result.value.tags
@ -1171,6 +1199,7 @@ module.exports = (db, server) => {
* }
*
* @apiParam {String} address E-mail Address
* @apiParam {String} [name] Identity name
* @apiParam {String[]} [targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to
* @apiParam {Number} [forwards] Daily allowed forwarding count for this address
* @apiParam {Boolean} [allowWildcard=false] If <code>true</code> then address value can be in the form of <code>*@example.com</code>, otherwise using * is not allowed
@ -1224,6 +1253,10 @@ module.exports = (db, server) => {
.required(),
Joi.string().regex(/^\w+@\*$/, 'special address')
],
name: Joi.string()
.empty('')
.trim()
.max(128),
targets: Joi.array().items(
Joi.string().email(),
Joi.string().uri({
@ -1288,6 +1321,7 @@ module.exports = (db, server) => {
let address = tools.normalizeAddress(result.value.address);
let addrview = address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@'));
let name = result.value.name;
let targets = result.value.targets || [];
let forwards = result.value.forwards;
@ -1462,6 +1496,7 @@ module.exports = (db, server) => {
// insert alias address to email address registry
let addressData = {
name,
address,
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
targets,
@ -1509,6 +1544,7 @@ module.exports = (db, server) => {
*
* @apiParam {String} id ID of the Address
* @apiParam {String} [address] New address. Only affects normal addresses, special addresses that include \* can not be changed
* @apiParam {String} [name] Identity name
* @apiParam {String[]} [targets] An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to. If set then overwrites previous targets array
* @apiParam {Number} [forwards] Daily allowed forwarding count for this address
* @apiParam {String[]} [tags] A list of tags associated with this address
@ -1556,6 +1592,10 @@ module.exports = (db, server) => {
.length(24)
.required(),
address: Joi.string().email(),
name: Joi.string()
.empty('')
.trim()
.max(128),
targets: Joi.array().items(
Joi.string().email(),
Joi.string().uri({
@ -1626,6 +1666,10 @@ module.exports = (db, server) => {
updates.forwards = result.value.forwards;
}
if (result.value.name) {
updates.name = result.value.name;
}
if (result.value.autoreply) {
if (!result.value.autoreply.name && 'name' in req.params.autoreply) {
result.value.autoreply.name = '';
@ -1947,6 +1991,7 @@ module.exports = (db, server) => {
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Address
* @apiSuccess {String} address E-mail address string
* @apiSuccess {String} name Identity name
* @apiSuccess {String[]} targets List of forwarding targets
* @apiSuccess {Object} limits Account limits and usage
* @apiSuccess {Object} limits.forwards Forwarding quota
@ -2057,6 +2102,7 @@ module.exports = (db, server) => {
res.json({
success: true,
id: addressData._id,
name: addressData.name || false,
address: addressData.address,
targets: addressData.targets && addressData.targets.map(t => t.value),
limits: {
@ -2092,6 +2138,7 @@ module.exports = (db, server) => {
* @apiSuccess {Boolean} success Indicates successful response
* @apiSuccess {String} id ID of the Address
* @apiSuccess {String} address E-mail address string
* @apiSuccess {String} name Identity name
* @apiSuccess {String} user ID of the user if the address belongs to an User
* @apiSuccess {String[]} targets List of forwarding targets if this is a Forwarded address
* @apiSuccess {Object} limits Account limits and usage for Forwarded address
@ -2238,6 +2285,7 @@ module.exports = (db, server) => {
res.json({
success: true,
id: addressData._id,
name: addressData.name || '',
address: addressData.address,
targets: addressData.targets && addressData.targets.map(t => t.value),
limits: {

View file

@ -67,7 +67,7 @@ module.exports = (db, server, userHandler) => {
.try(
Joi.string()
.lowercase()
.regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username')
.regex(/^[a-z0-9](?:\.?[a-z0-9-]+)*$/, 'username')
.min(3)
.max(30),
Joi.string().email()

View file

@ -348,9 +348,9 @@ module.exports = (db, server, userHandler) => {
const schema = Joi.object().keys({
username: Joi.string()
.lowercase()
.regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username')
.regex(/^[a-z0-9](?:\.?[a-z0-9-]+)*$/, 'username')
.min(1)
.max(32)
.max(128)
.required(),
password: Joi.string()
.allow(false)
@ -570,7 +570,7 @@ module.exports = (db, server, userHandler) => {
const schema = Joi.object().keys({
username: Joi.string()
.lowercase()
.regex(/^[a-z](?:\.?[a-z0-9]+)*$/, 'username')
.regex(/^[a-z0-9](?:\.?[a-z0-9-]+)*$/, 'username')
.min(1)
.max(32)
.required()

View file

@ -193,35 +193,35 @@ class UserHandler {
let checkAddress = next => {
if (ObjectID.isValid(username)) {
return next(null, {
_id: username
query: { _id: username }
});
}
if (username.indexOf('@') < 0) {
// assume regular username
return next(null, {
unameview: username.replace(/\./g, '')
query: { unameview: username.replace(/\./g, '') }
});
}
this.resolveAddress(username, false, (err, addressData) => {
this.resolveAddress(username, { fields: { name: true } }, (err, addressData) => {
if (err) {
return callback(err);
}
if (addressData.user) {
return next(null, { _id: addressData.user });
return next(null, { query: { _id: addressData.user }, addressData });
}
return callback(null, false);
});
};
checkAddress((err, query) => {
checkAddress((err, data) => {
if (err) {
return callback(err);
}
this.users.collection('users').findOne(
query,
data.query,
{
fields
},
@ -231,6 +231,11 @@ class UserHandler {
return callback(err);
}
if (userData && fields.name && data.addressData) {
// override name
userData.name = data.addressData.name || userData.name;
}
return callback(null, userData);
}
);
@ -715,228 +720,269 @@ class UserHandler {
return callback(err);
}
let junkRetention = consts.JUNK_RETENTION;
let address = data.address ? data.address : data.username + '@' + (config.emailDomain || os.hostname()).toLowerCase();
address = tools.normalizeAddress(address).replace(/\+[^@]*@/, '@');
let addrview = address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@'));
// Insert user data
// Users with an empty password can not log in
let hash = data.password ? bcrypt.hashSync(data.password, consts.BCRYPT_ROUNDS) : '';
let id = new ObjectID();
userData = {
_id: id,
username: data.username,
// dotless version
unameview: data.username.replace(/\./g, ''),
name: data.name,
// security
password: '', // set this later. having no password prevents login
enabled2fa: [],
seed: '', // 2fa seed value
// default email address
address: '', // set this later
// quota
storageUsed: 0,
quota: data.quota || 0,
recipients: data.recipients || 0,
forwards: data.forwards || 0,
targets: [].concat(data.targets || []),
// autoreply status
// off by default, can be changed later by user through the API
autoreply: false,
pubKey: data.pubKey || '',
encryptMessages: !!data.encryptMessages,
encryptForwarded: !!data.encryptForwarded,
// default retention for user mailboxes
retention: data.retention || 0,
created: new Date(),
requirePasswordChange: false,
// until setup value is not true, this account is not usable
activated: false,
disabled: true
};
if (data.tags && data.tags.length) {
userData.tags = data.tags;
}
this.users.collection('users').insertOne(userData, err => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
let response;
switch (err.code) {
case 11000:
response = 'Selected user already exists';
err.code = 'UserExistsError';
break;
default:
response = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
}
err.message = response;
return callback(err);
let checkAddress = done => {
if (data.emptyAddress) {
return done();
}
let mailboxes = this.getMailboxes(data.language).map(mailbox => {
mailbox.user = id;
if (['\\Trash', '\\Junk'].includes(mailbox.specialUse)) {
mailbox.retention = data.retention ? Math.min(data.retention, junkRetention) : junkRetention;
} else {
mailbox.retention = data.retention;
}
return mailbox;
});
this.database.collection('mailboxes').insertMany(
mailboxes,
this.users.collection('addresses').findOne(
{
w: 1,
ordered: false
addrview
},
err => {
{
fields: {
_id: true
}
},
(err, addressData) => {
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
log.error('DB', 'CREATEFAIL username=%s address=%s error=%s', data.username, address, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
return callback(err);
}
let ensureAddress = done => {
if (data.emptyAddress) {
return done(null, '');
}
let address = data.address ? data.address : data.username + '@' + (config.emailDomain || os.hostname()).toLowerCase();
if (addressData) {
let err = new Error('This address already exists');
err.code = 'AddressExistsError';
return callback(err);
}
let addressData = {
user: id,
address,
// dotless version
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
created: new Date()
};
done();
}
);
};
if (data.tags && data.tags.length && data.addTagsToAddress) {
addressData.tags = data.tags;
}
checkAddress(() => {
let junkRetention = consts.JUNK_RETENTION;
// insert alias address to email address registry
this.users.collection('addresses').insertOne(addressData, err => {
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
// Insert user data
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
// Users with an empty password can not log in
let hash = data.password ? bcrypt.hashSync(data.password, consts.BCRYPT_ROUNDS) : '';
let id = new ObjectID();
let response;
switch (err.code) {
case 11000:
response = 'Selected email address already exists';
break;
default:
response = 'Database Error, failed to create user';
}
userData = {
_id: id,
err.message = response;
return done(err);
}
username: data.username,
// dotless version
unameview: data.username.replace(/\./g, ''),
done(null, address);
});
};
name: data.name,
ensureAddress((err, address) => {
// security
password: '', // set this later. having no password prevents login
enabled2fa: [],
seed: '', // 2fa seed value
// default email address
address: '', // set this later
// quota
storageUsed: 0,
quota: data.quota || 0,
recipients: data.recipients || 0,
forwards: data.forwards || 0,
targets: [].concat(data.targets || []),
// autoreply status
// off by default, can be changed later by user through the API
autoreply: false,
pubKey: data.pubKey || '',
encryptMessages: !!data.encryptMessages,
encryptForwarded: !!data.encryptForwarded,
// default retention for user mailboxes
retention: data.retention || 0,
created: new Date(),
requirePasswordChange: false,
// until setup value is not true, this account is not usable
activated: false,
disabled: true
};
if (data.tags && data.tags.length) {
userData.tags = data.tags;
}
this.users.collection('users').insertOne(userData, err => {
if (err) {
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
let response;
switch (err.code) {
case 11000:
response = 'Selected user already exists';
err.code = 'UserExistsError';
break;
default:
response = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
}
err.message = response;
return callback(err);
}
let mailboxes = this.getMailboxes(data.language).map(mailbox => {
mailbox.user = id;
if (['\\Trash', '\\Junk'].includes(mailbox.specialUse)) {
mailbox.retention = data.retention ? Math.min(data.retention, junkRetention) : junkRetention;
} else {
mailbox.retention = data.retention;
}
return mailbox;
});
this.database.collection('mailboxes').insertMany(
mailboxes,
{
w: 1,
ordered: false
},
err => {
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
return callback(err);
}
// register this address as the default address for that user
return this.users.collection('users').findOneAndUpdate(
{
_id: id,
activated: false
},
{
$set: {
password: hash,
address,
activated: true,
disabled: false
}
},
{ returnOriginal: false },
(err, result) => {
let ensureAddress = done => {
if (data.emptyAddress) {
return done(null, '');
}
let addressData = {
user: id,
address,
// dotless version
addrview,
created: new Date()
};
if (data.tags && data.tags.length && data.addTagsToAddress) {
addressData.tags = data.tags;
}
// insert alias address to email address registry
this.users.collection('addresses').insertOne(addressData, err => {
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
return callback(err);
let response;
switch (err.code) {
case 11000:
response = 'Selected email address already exists';
err.code = 'AddressExistsError';
break;
default:
response = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
}
err.message = response;
return done(err);
}
let userData = result.value;
done(null, address);
});
};
if (!userData) {
// should never happen
return callback(null, id);
}
let createSuccess = () =>
this.logAuthEvent(
id,
{
action: 'account created',
result: 'success',
sess: data.sess,
ip: data.ip
},
() => callback(null, id)
);
if (!this.messageHandler || data.emptyAddress) {
return createSuccess();
}
let parsedName = humanname.parse(userData.name || '');
this.pushDefaultMessages(
userData,
{
NAME: userData.name || userData.username || address,
FNAME: parsedName.firstName,
LNAME: parsedName.lastName,
DOMAIN: address.substr(address.indexOf('@') + 1),
EMAIL: address
},
() => createSuccess()
);
ensureAddress((err, address) => {
if (err) {
return callback(err);
}
);
});
}
);
// register this address as the default address for that user
return this.users.collection('users').findOneAndUpdate(
{
_id: id,
activated: false
},
{
$set: {
password: hash,
address,
activated: true,
disabled: false
}
},
{ returnOriginal: false },
(err, result) => {
if (err) {
// try to rollback
this.users.collection('users').deleteOne({ _id: id }, () => false);
this.database.collection('mailboxes').deleteMany({ user: id }, () => false);
this.users.collection('addresses').deleteOne({ user: id }, () => false);
log.error('DB', 'CREATEFAIL username=%s error=%s', data.username, err.message);
err.message = 'Database Error, failed to create user';
err.code = 'InternalDatabaseError';
return callback(err);
}
let userData = result.value;
if (!userData) {
// should never happen
return callback(null, id);
}
let createSuccess = () =>
this.logAuthEvent(
id,
{
action: 'account created',
result: 'success',
sess: data.sess,
ip: data.ip
},
() => callback(null, id)
);
if (!this.messageHandler || data.emptyAddress) {
return createSuccess();
}
let parsedName = humanname.parse(userData.name || '');
this.pushDefaultMessages(
userData,
{
NAME: userData.name || userData.username || address,
FNAME: parsedName.firstName,
LNAME: parsedName.lastName,
DOMAIN: address.substr(address.indexOf('@') + 1),
EMAIL: address
},
() => createSuccess()
);
}
);
});
}
);
});
});
}
);