Allow setting tags for addresses

This commit is contained in:
Andris Reinman 2018-01-16 12:37:18 +02:00
parent 879e25e649
commit 8896e03541
7 changed files with 347 additions and 108 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. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-15T11:52:17.862Z", "url": "http://apidocjs.com", "version": "0.17.6" } });
define({ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-16T10:36:55.798Z", "url": "http://apidocjs.com", "version": "0.17.6" } });

View file

@ -1 +1 @@
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-15T11:52:17.862Z", "url": "http://apidocjs.com", "version": "0.17.6" } }
{ "name": "wildduck", "version": "1.0.0", "description": "WildDuck API docs. Under construction, see old docs here: https://github.com/nodemailer/wildduck/blob/master/docs/api.md", "title": "WildDuck API", "url": "http://localhost:8080", "sampleUrl": false, "defaultVersion": "0.0.0", "apidoc": "0.3.0", "generator": { "name": "apidoc", "time": "2018-01-16T10:36:55.798Z", "url": "http://apidocjs.com", "version": "0.17.6" } }

View file

@ -65,6 +65,14 @@ indexes:
key:
addrview: 1
- collection: addresses
type: users # index applies to users database
index:
name: address_tags
key:
tagsview: 1
sparse: true
- collection: addresses
type: users # index applies to users database
index:
@ -254,6 +262,13 @@ indexes:
mailbox: 1
idate: 1
- collection: messages
index:
name: by_idate_reverse
key:
mailbox: 1
idate: -1
- collection: messages
index:
name: by_hdate

View file

@ -19,6 +19,8 @@ module.exports = (db, server) => {
* }
*
* @apiParam {String} [query] Partial match of an address
* @apiParam {String} [tags] Comma separated list of tags. The Address must have at least one to be set
* @apiParam {String} [requiredTags] Comma separated list of tags. The Address 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
@ -76,6 +78,14 @@ module.exports = (db, server) => {
.trim()
.empty('')
.max(255),
tags: Joi.string()
.trim()
.empty('')
.max(1024),
requiredTags: Joi.string()
.trim()
.empty('')
.max(1024),
limit: Joi.number()
.default(20)
.min(1)
@ -121,6 +131,42 @@ module.exports = (db, server) => {
}) ||
{};
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;
}
db.users.collection('addresses').count(filter, (err, total) => {
if (err) {
res.json({
@ -136,6 +182,7 @@ module.exports = (db, server) => {
_id: true,
address: true,
user: true,
tags: true,
targets: true
},
paginatedField: 'addrview',
@ -171,7 +218,8 @@ module.exports = (db, server) => {
id: addressData._id.toString(),
address: addressData.address,
user: addressData.user,
forwarded: addressData.targets && true
forwarded: addressData.targets && true,
tags: addressData.tags || []
}))
};
@ -198,6 +246,7 @@ module.exports = (db, server) => {
*
* @apiParam {String} user ID of the User
* @apiParam {String} address E-mail Address
* @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
*
@ -246,7 +295,12 @@ module.exports = (db, server) => {
.falsy(['N', 'false', 'no', 'off', 0, '']),
allowWildcard: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', 1])
.falsy(['N', 'false', 'no', 'off', 0, ''])
.falsy(['N', 'false', 'no', 'off', 0, '']),
tags: Joi.array().items(
Joi.string()
.trim()
.max(128)
)
});
const result = Joi.validate(req.params, schema, {
@ -298,6 +352,23 @@ module.exports = (db, server) => {
}
}
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());
}
db.users.collection('users').findOne(
{
_id: user
@ -344,53 +415,57 @@ module.exports = (db, server) => {
return next();
}
addressData = {
user,
address,
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
created: new Date()
};
if (result.value.tags) {
addressData.tags = result.value.tags;
addressData.tagsview = result.value.tags;
}
// insert alias address to email address registry
db.users.collection('addresses').insertOne(
{
user,
address,
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
created: new Date()
},
(err, r) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
let insertId = r.insertedId;
let done = () => {
// ignore potential user update error
res.json({
success: !!insertId,
id: insertId
});
return next();
};
if (!userData.address || main) {
// register this address as the default address for that user
return db.users.collection('users').findOneAndUpdate(
{
_id: user
},
{
$set: {
address
}
},
{},
done
);
}
done();
db.users.collection('addresses').insertOne(addressData, (err, r) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
);
let insertId = r.insertedId;
let done = () => {
// ignore potential user update error
res.json({
success: !!insertId,
id: insertId
});
return next();
};
if (!userData.address || main) {
// register this address as the default address for that user
return db.users.collection('users').findOneAndUpdate(
{
_id: user
},
{
$set: {
address
}
},
{},
done
);
}
done();
});
}
);
}
@ -415,6 +490,7 @@ module.exports = (db, server) => {
* @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
* @apiSuccess {String[]} results.tags List of tags associated with the Address
*
* @apiError error Description of the error
*
@ -524,6 +600,7 @@ module.exports = (db, server) => {
id: address._id,
address: address.address,
main: address.address === userData.address,
tags: address.tags || [],
created: address.created
}))
});
@ -681,6 +758,7 @@ module.exports = (db, server) => {
* @apiParam {String} user ID of the User
* @apiParam {String} address ID of the Address
* @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
*
@ -705,7 +783,7 @@ module.exports = (db, server) => {
* "error": "This user does not exist"
* }
*/
server.put('/users/:user/addresses/:address', (req, res, next) => {
server.put('/users/:user/addresses/:id', (req, res, next) => {
res.charSet('utf-8');
const schema = Joi.object().keys({
@ -714,7 +792,7 @@ module.exports = (db, server) => {
.lowercase()
.length(24)
.required(),
address: Joi.string()
id: Joi.string()
.hex()
.lowercase()
.length(24)
@ -722,7 +800,12 @@ module.exports = (db, server) => {
main: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', 1])
.falsy(['N', 'false', 'no', 'off', 0, ''])
.required()
.required(),
tags: Joi.array().items(
Joi.string()
.trim()
.max(128)
)
});
const result = Joi.validate(req.params, schema, {
@ -739,16 +822,43 @@ module.exports = (db, server) => {
}
let user = new ObjectID(result.value.user);
let address = new ObjectID(result.value.address);
let id = new ObjectID(result.value.id);
let main = result.value.main;
if (!main) {
if (main === false) {
res.json({
error: 'Cannot unset main status'
});
return next();
}
let updates = {};
if (result.value.address) {
let address = tools.normalizeAddress(result.value.address);
let addrview = address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@'));
updates.address = address;
updates.addrview = addrview;
}
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()));
updates.tags = tags;
updates.tagsview = tags.map(tag => tag.toLowerCase());
}
db.users.collection('users').findOne(
{
_id: user
@ -776,7 +886,7 @@ module.exports = (db, server) => {
db.users.collection('addresses').findOne(
{
_id: address
_id: id
},
(err, addressData) => {
if (err) {
@ -796,48 +906,106 @@ module.exports = (db, server) => {
return next();
}
if (addressData.address === userData.address) {
if (addressData.address.indexOf('*') >= 0 && result.value.address && result.value.address !== addressData.address) {
res.json({
error: 'Selected address is already the main email address for the user'
error: 'Can not change special address',
code: 'ChangeNotAllowed'
});
return next();
}
if (addressData.address.indexOf('*') >= 0 && main) {
if (result.value.address && result.value.address.indexOf('*') >= 0 && result.value.address !== addressData.address) {
res.json({
error: 'Can not change special address',
code: 'ChangeNotAllowed'
});
return next();
}
if ((result.value.address || addressData.address).indexOf('*') >= 0 && main) {
res.json({
error: 'Can not set wildcard address as default'
});
return next();
}
// insert alias address to email address registry
db.users.collection('users').findOneAndUpdate(
{
_id: user
},
{
$set: {
address: addressData.address
}
},
{
returnOriginal: false
},
(err, r) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
if (result.value.address && addressData.address === userData.address && result.value.address !== addressData.address) {
// main address was changed, update user data as well
main = true;
addressData.address = result.value.address;
}
let updateAddressData = done => {
if (!Object.keys(updates).length) {
return done();
}
db.users.collection('addresses').findOneAndUpdate(
{
_id: addressData._id
},
{
$set: updates
},
{
returnOriginal: false
},
err => {
if (err) {
if (err.code === 11000) {
res.json({
error: 'Address already exists',
code: 'AddressExistsError'
});
} else {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
}
return next();
}
return done();
}
);
};
updateAddressData(() => {
if (!main) {
// nothing to do anymore
res.json({
success: !!r.value
success: true
});
return next();
}
);
db.users.collection('users').findOneAndUpdate(
{
_id: user
},
{
$set: {
address: addressData.address
}
},
{
returnOriginal: false
},
(err, r) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
res.json({
success: !!r.value
});
return next();
}
);
});
}
);
}
@ -1006,6 +1174,7 @@ module.exports = (db, server) => {
* @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")
* @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
* @apiParam {String[]} [tags] A list of tags associated with this address
* @apiParam {Object} [autoreply] Autoreply information
* @apiParam {Boolean} [autoreply.status] If true, then autoreply is enabled for this address
* @apiParam {String} [autoreply.start] Either a date string or boolean false to disable start time checks
@ -1089,7 +1258,12 @@ module.exports = (db, server) => {
.empty('')
.trim()
.max(128 * 1024)
})
}),
tags: Joi.array().items(
Joi.string()
.trim()
.max(128)
)
});
const result = Joi.validate(req.params, schema, {
@ -1137,6 +1311,23 @@ module.exports = (db, server) => {
};
}
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());
}
// needed to resolve users for addresses
let addrlist = [];
let cachedAddrviews = new WeakMap();
@ -1258,33 +1449,38 @@ module.exports = (db, server) => {
resolveUsers(() => {
// insert alias address to email address registry
db.users.collection('addresses').insertOne(
{
address,
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
targets,
forwards,
autoreply: result.value.autoreply,
created: new Date()
},
(err, r) => {
if (err) {
res.json({
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
let insertId = r.insertedId;
let addressData = {
address,
addrview: address.substr(0, address.indexOf('@')).replace(/\./g, '') + address.substr(address.indexOf('@')),
targets,
forwards,
autoreply: result.value.autoreply,
created: new Date()
};
if (result.value.tags) {
addressData.tags = result.value.tags;
addressData.tagsview = result.value.tags;
}
db.users.collection('addresses').insertOne(addressData, (err, r) => {
if (err) {
res.json({
success: !!insertId,
id: insertId
error: 'MongoDB Error: ' + err.message,
code: 'InternalDatabaseError'
});
return next();
}
);
let insertId = r.insertedId;
res.json({
success: !!insertId,
id: insertId
});
return next();
});
});
}
);
@ -1304,6 +1500,7 @@ module.exports = (db, server) => {
* @apiParam {String} [address] New address. Only affects normal addresses, special addresses that include \* can not be changed
* @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"). 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
* @apiParam {Object} [autoreply] Autoreply information
* @apiParam {Boolean} [autoreply.status] If true, then autoreply is enabled for this address
* @apiParam {String} [autoreply.start] Either a date string or boolean false to disable start time checks
@ -1376,7 +1573,12 @@ module.exports = (db, server) => {
.empty('')
.trim()
.max(128 * 1024)
})
}),
tags: Joi.array().items(
Joi.string()
.trim()
.max(128)
)
});
const result = Joi.validate(req.params, schema, {
@ -1432,6 +1634,23 @@ module.exports = (db, server) => {
});
}
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()));
updates.tags = tags;
updates.tagsview = tags.map(tag => tag.toLowerCase());
}
db.users.collection('addresses').findOne(
{
_id: id
@ -1718,6 +1937,7 @@ module.exports = (db, server) => {
* @apiSuccess {String} autoreply.text Autoreply plaintext content
* @apiSuccess {String} autoreply.html Autoreply HTML content
* @apiSuccess {String} created Datestring of the time the address was created
* @apiSuccess {String[]} results.tags List of tags associated with the Address
*
* @apiError error Description of the error
*
@ -1824,6 +2044,7 @@ module.exports = (db, server) => {
}
},
autoreply: addressData.autoreply || { status: false },
tags: addressData.tags || [],
created: addressData.created
});
@ -1860,6 +2081,7 @@ module.exports = (db, server) => {
* @apiSuccess {String} autoreply.subject Autoreply subject line
* @apiSuccess {String} autoreply.text Autoreply plaintext content
* @apiSuccess {String} autoreply.html Autoreply HTML content
* @apiSuccess {String[]} tags List of tags associated with the Address
* @apiSuccess {String} created Datestring of the time the address was created
*
* @apiError error Description of the error
@ -1968,6 +2190,7 @@ module.exports = (db, server) => {
id: addressData._id,
address: addressData.address,
user: addressData.user,
tags: addressData.tags || [],
created: addressData.created
});
return next();
@ -2001,6 +2224,7 @@ module.exports = (db, server) => {
}
},
autoreply: addressData.autoreply || { status: false },
tags: addressData.tags || [],
created: addressData.created
});

View file

@ -235,7 +235,7 @@ module.exports = (db, server, messageHandler) => {
draft: true,
thread: true
},
paginatedField: 'uid',
paginatedField: 'idate',
sortAscending
};
@ -1568,7 +1568,7 @@ module.exports = (db, server, messageHandler) => {
.lowercase()
.length(24),
message: Joi.string()
.regex(/^\d+(,\d+)*$|^\d+:\d+$|/i)
.regex(/^\d+(,\d+)*$|^\d+:\d+$/i)
.required(),
seen: Joi.boolean()
.truthy(['Y', 'true', 'yes', 'on', 1])