mirror of
https://github.com/nodemailer/wildduck.git
synced 2025-09-10 07:06:42 +08:00
started with metaData for addresses
This commit is contained in:
parent
89dad94ae3
commit
f2f14ca8fc
4 changed files with 231 additions and 33 deletions
|
@ -243,13 +243,13 @@
|
|||
|
||||
"user": {
|
||||
"addresslisting": {
|
||||
"read:own": ["*"]
|
||||
"read:own": ["*", "!internalData"]
|
||||
},
|
||||
|
||||
"addresses": {
|
||||
"create:own": ["*"],
|
||||
"read:own": ["*"],
|
||||
"update:own": ["*"],
|
||||
"create:own": ["*", "!internalData"],
|
||||
"read:own": ["*", "!internalData"],
|
||||
"update:own": ["*", "!internalData"],
|
||||
"delete:own": ["*"]
|
||||
},
|
||||
|
||||
|
|
|
@ -25,6 +25,8 @@ module.exports = (db, server, userHandler) => {
|
|||
* @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 {Boolean} [metaData] If true, then includes <code>metaData</code> in the response
|
||||
* @apiParam {Boolean} [internalData] If true, then includes <code>internalData</code> in the response. Not shown for user-role tokens.
|
||||
* @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
|
||||
|
@ -86,6 +88,8 @@ module.exports = (db, server, userHandler) => {
|
|||
query: Joi.string().trim().empty('').max(255),
|
||||
tags: Joi.string().trim().empty('').max(1024),
|
||||
requiredTags: Joi.string().trim().empty('').max(1024),
|
||||
metaData: booleanSchema,
|
||||
internalData: booleanSchema,
|
||||
limit: Joi.number().default(20).min(1).max(250),
|
||||
next: nextPageCursorSchema,
|
||||
previous: previousPageCursorSchema,
|
||||
|
@ -202,6 +206,14 @@ module.exports = (db, server, userHandler) => {
|
|||
sortAscending: true
|
||||
};
|
||||
|
||||
if (result.value.metaData) {
|
||||
opts.fields.projection.metaData = true;
|
||||
}
|
||||
|
||||
if (result.value.internalData) {
|
||||
opts.fields.projection.internalData = true;
|
||||
}
|
||||
|
||||
if (pageNext) {
|
||||
opts.next = pageNext;
|
||||
} else if ((!page || page > 1) && pagePrevious) {
|
||||
|
@ -230,16 +242,28 @@ module.exports = (db, server, userHandler) => {
|
|||
page,
|
||||
previousCursor: listing.hasPrevious ? listing.previous : false,
|
||||
nextCursor: listing.hasNext ? listing.next : false,
|
||||
results: (listing.results || []).map(addressData => ({
|
||||
id: addressData._id.toString(),
|
||||
name: addressData.name || false,
|
||||
address: addressData.address,
|
||||
user: addressData.user,
|
||||
forwarded: !!addressData.targets,
|
||||
forwardedDisabled: !!(addressData.targets && addressData.forwardedDisabled),
|
||||
targets: addressData.targets && addressData.targets.map(t => t.value),
|
||||
tags: addressData.tags || []
|
||||
}))
|
||||
results: (listing.results || []).map(addressData => {
|
||||
let values = {
|
||||
id: addressData._id.toString(),
|
||||
name: addressData.name || false,
|
||||
address: addressData.address,
|
||||
user: addressData.user,
|
||||
forwarded: !!addressData.targets,
|
||||
forwardedDisabled: !!(addressData.targets && addressData.forwardedDisabled),
|
||||
targets: addressData.targets && addressData.targets.map(t => t.value),
|
||||
tags: addressData.tags || []
|
||||
};
|
||||
|
||||
if (addressData.metaData) {
|
||||
values.metaData = formatMetaData(addressData.metaData);
|
||||
}
|
||||
|
||||
if (addressData.internalData) {
|
||||
values.internalData = formatMetaData(addressData.internalData);
|
||||
}
|
||||
|
||||
return permission.filter(values);
|
||||
})
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
|
@ -268,6 +292,8 @@ module.exports = (db, server, userHandler) => {
|
|||
* @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>, <code>\*suffix@example.com</code> and <code>username@\*</code>, otherwise using \* is not allowed. Static suffix can be up to 32 characters long.
|
||||
* @apiParam {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object
|
||||
* @apiParam {Object|String} [internalData] Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {String} id ID of the Address
|
||||
|
@ -306,6 +332,23 @@ module.exports = (db, server, userHandler) => {
|
|||
main: booleanSchema,
|
||||
allowWildcard: booleanSchema,
|
||||
tags: Joi.array().items(Joi.string().trim().max(128)),
|
||||
|
||||
metaData: Joi.alternatives().try(
|
||||
Joi.string()
|
||||
.empty('')
|
||||
.trim()
|
||||
.max(1024 * 1024),
|
||||
Joi.object()
|
||||
),
|
||||
|
||||
internalData: Joi.alternatives().try(
|
||||
Joi.string()
|
||||
.empty('')
|
||||
.trim()
|
||||
.max(1024 * 1024),
|
||||
Joi.object()
|
||||
),
|
||||
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
@ -442,6 +485,35 @@ module.exports = (db, server, userHandler) => {
|
|||
result.value.tagsview = tags.map(tag => tag.toLowerCase());
|
||||
}
|
||||
|
||||
for (let key of ['metaData', 'internalData']) {
|
||||
if (result.value[key]) {
|
||||
if (typeof result.value[key] === 'object') {
|
||||
try {
|
||||
result.value[key] = JSON.stringify(result.value[key]);
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: `${key} value must be serializable to JSON`,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let value = JSON.parse(result.value[key]);
|
||||
if (!value || typeof value !== 'object') {
|
||||
throw new Error('Not an object');
|
||||
}
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: `${key} value must be valid JSON object string`,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let userData;
|
||||
try {
|
||||
userData = await db.users.collection('users').findOne(
|
||||
|
@ -504,6 +576,14 @@ module.exports = (db, server, userHandler) => {
|
|||
addressData.tagsview = result.value.tags;
|
||||
}
|
||||
|
||||
if (result.value.metaData) {
|
||||
addressData.metaData = result.value.metaData;
|
||||
}
|
||||
|
||||
if (result.value.internalData) {
|
||||
addressData.internalData = result.value.internalData;
|
||||
}
|
||||
|
||||
let r;
|
||||
// insert alias address to email address registry
|
||||
try {
|
||||
|
@ -555,6 +635,8 @@ module.exports = (db, server, userHandler) => {
|
|||
* }
|
||||
*
|
||||
* @apiParam {String} user ID of the User
|
||||
* @apiParam {Boolean} [metaData] If true, then includes <code>metaData</code> in the response
|
||||
* @apiParam {Boolean} [internalData] If true, then includes <code>internalData</code> in the response. Not shown for user-role tokens.
|
||||
*
|
||||
* @apiSuccess {Boolean} success Indicates successful response
|
||||
* @apiSuccess {Object[]} results Address listing
|
||||
|
@ -601,6 +683,8 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
const schema = Joi.object().keys({
|
||||
user: Joi.string().hex().lowercase().length(24).required(),
|
||||
metaData: booleanSchema,
|
||||
internalData: booleanSchema,
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
@ -623,12 +707,16 @@ module.exports = (db, server, userHandler) => {
|
|||
let user = new ObjectID(result.value.user);
|
||||
|
||||
// permissions check
|
||||
let permission;
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).readOwn('addresses'));
|
||||
permission = roles.can(req.role).readOwn('addresses');
|
||||
} else {
|
||||
req.validate(roles.can(req.role).readAny('addresses'));
|
||||
permission = roles.can(req.role).readAny('addresses');
|
||||
}
|
||||
|
||||
// permissions check
|
||||
req.validate(permission);
|
||||
|
||||
let userData;
|
||||
try {
|
||||
userData = await db.users.collection('users').findOne(
|
||||
|
@ -685,14 +773,26 @@ module.exports = (db, server, userHandler) => {
|
|||
res.json({
|
||||
success: true,
|
||||
|
||||
results: addresses.map(address => ({
|
||||
id: address._id,
|
||||
name: address.name || false,
|
||||
address: address.address,
|
||||
main: address.address === userData.address,
|
||||
tags: address.tags || [],
|
||||
created: address.created
|
||||
}))
|
||||
results: addresses.map(addressData => {
|
||||
let values = {
|
||||
id: addressData._id.toString(),
|
||||
name: addressData.name || false,
|
||||
address: addressData.address,
|
||||
main: addressData.address === userData.address,
|
||||
tags: addressData.tags || [],
|
||||
created: addressData.created
|
||||
};
|
||||
|
||||
if (result.value.metaData && addressData.metaData) {
|
||||
values.metaData = formatMetaData(addressData.metaData);
|
||||
}
|
||||
|
||||
if (result.value.internalData && addressData.internalData) {
|
||||
values.internalData = formatMetaData(addressData.internalData);
|
||||
}
|
||||
|
||||
return permission.filter(values);
|
||||
})
|
||||
});
|
||||
|
||||
return next();
|
||||
|
@ -770,11 +870,13 @@ module.exports = (db, server, userHandler) => {
|
|||
let user = new ObjectID(result.value.user);
|
||||
|
||||
// permissions check
|
||||
let permission;
|
||||
if (req.user && req.user === result.value.user) {
|
||||
req.validate(roles.can(req.role).readOwn('addresses'));
|
||||
permission = roles.can(req.role).readOwn('addresses');
|
||||
} else {
|
||||
req.validate(roles.can(req.role).readAny('addresses'));
|
||||
permission = roles.can(req.role).readAny('addresses');
|
||||
}
|
||||
req.validate(permission);
|
||||
|
||||
let address = new ObjectID(result.value.address);
|
||||
|
||||
|
@ -829,14 +931,24 @@ module.exports = (db, server, userHandler) => {
|
|||
return next();
|
||||
}
|
||||
|
||||
res.json({
|
||||
let value = {
|
||||
success: true,
|
||||
id: addressData._id,
|
||||
id: addressData._id.toString(),
|
||||
name: addressData.name || false,
|
||||
address: addressData.address,
|
||||
main: addressData.address === userData.address,
|
||||
created: addressData.created
|
||||
});
|
||||
};
|
||||
|
||||
if (addressData.metaData) {
|
||||
value.metaData = formatMetaData(addressData.metaData);
|
||||
}
|
||||
|
||||
if (addressData.internalData) {
|
||||
value.internalData = formatMetaData(addressData.internalData);
|
||||
}
|
||||
|
||||
res.json(permission.filter(value));
|
||||
|
||||
return next();
|
||||
})
|
||||
|
@ -857,6 +969,8 @@ module.exports = (db, server, userHandler) => {
|
|||
* @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 {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object
|
||||
* @apiParam {Object|String} [internalData] Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens
|
||||
|
||||
* @apiParam {String[]} [tags] A list of tags associated with this address
|
||||
*
|
||||
|
@ -895,6 +1009,23 @@ module.exports = (db, server, userHandler) => {
|
|||
address: Joi.string().email({ tlds: false }),
|
||||
main: booleanSchema,
|
||||
tags: Joi.array().items(Joi.string().trim().max(128)),
|
||||
|
||||
metaData: Joi.alternatives().try(
|
||||
Joi.string()
|
||||
.empty('')
|
||||
.trim()
|
||||
.max(1024 * 1024),
|
||||
Joi.object()
|
||||
),
|
||||
|
||||
internalData: Joi.alternatives().try(
|
||||
Joi.string()
|
||||
.empty('')
|
||||
.trim()
|
||||
.max(1024 * 1024),
|
||||
Joi.object()
|
||||
),
|
||||
|
||||
sess: sessSchema,
|
||||
ip: sessIPSchema
|
||||
});
|
||||
|
@ -1044,6 +1175,36 @@ module.exports = (db, server, userHandler) => {
|
|||
addressData.address = result.value.address;
|
||||
}
|
||||
|
||||
for (let key of ['metaData', 'internalData']) {
|
||||
if (result.value[key]) {
|
||||
if (typeof result.value[key] === 'object') {
|
||||
try {
|
||||
updates[key] = JSON.stringify(result.value[key]);
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: `${key} value must be serializable to JSON`,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
let value = JSON.parse(result.value[key]);
|
||||
if (!value || typeof value !== 'object') {
|
||||
throw new Error('Not an object');
|
||||
}
|
||||
updates[key] = result.value[key];
|
||||
} catch (err) {
|
||||
res.json({
|
||||
error: `${key} value must be valid JSON object string`,
|
||||
code: 'InputValidationError'
|
||||
});
|
||||
return next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length) {
|
||||
try {
|
||||
await db.users.collection('addresses').updateOne(
|
||||
|
@ -2300,7 +2461,7 @@ module.exports = (db, server, userHandler) => {
|
|||
if (addressData.user) {
|
||||
res.json({
|
||||
success: true,
|
||||
id: addressData._id,
|
||||
id: addressData._id.toString(),
|
||||
address: addressData.address,
|
||||
user: addressData.user,
|
||||
tags: addressData.tags || [],
|
||||
|
@ -2328,7 +2489,7 @@ module.exports = (db, server, userHandler) => {
|
|||
|
||||
res.json({
|
||||
success: true,
|
||||
id: addressData._id,
|
||||
id: addressData._id.toString(),
|
||||
name: addressData.name || '',
|
||||
address: addressData.address,
|
||||
targets: addressData.targets && addressData.targets.map(t => t.value),
|
||||
|
@ -2545,3 +2706,14 @@ module.exports = (db, server, userHandler) => {
|
|||
})
|
||||
);
|
||||
};
|
||||
|
||||
function formatMetaData(metaData) {
|
||||
if (typeof metaData === 'string') {
|
||||
try {
|
||||
metaData = JSON.parse(metaData);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return metaData || {};
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
"mailparser": "3.0.0",
|
||||
"mocha": "8.1.3",
|
||||
"request": "2.88.2",
|
||||
"supertest": "4.0.2"
|
||||
"supertest": "5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@phc/pbkdf2": "1.1.14",
|
||||
|
|
|
@ -191,7 +191,10 @@ describe('API tests', function () {
|
|||
.post(`/users/${userId}/addresses`)
|
||||
.send({
|
||||
address: 'alias1@example.com',
|
||||
main: true
|
||||
main: true,
|
||||
metaData: {
|
||||
tere: 123
|
||||
}
|
||||
})
|
||||
.expect(200);
|
||||
expect(response1.body.success).to.be.true;
|
||||
|
@ -214,8 +217,10 @@ describe('API tests', function () {
|
|||
|
||||
it('should GET /users/:user/addresses (updated listing)', async () => {
|
||||
const response = await server.get(`/users/${userId}/addresses`).expect(200);
|
||||
|
||||
expect(response.body.success).to.be.true;
|
||||
expect(response.body.results.length).to.equal(3);
|
||||
|
||||
response.body.results.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
expect(response.body.results[0].address).to.equal('testuser@example.com');
|
||||
|
@ -223,7 +228,9 @@ describe('API tests', function () {
|
|||
|
||||
expect(response.body.results[1].address).to.equal('alias1@example.com');
|
||||
expect(response.body.results[1].main).to.be.true;
|
||||
expect(response.body.results[1].metaData).to.not.exist;
|
||||
|
||||
// no metaData present
|
||||
expect(response.body.results[2].address).to.equal('alias2@example.com');
|
||||
expect(response.body.results[2].main).to.be.false;
|
||||
|
||||
|
@ -235,6 +242,25 @@ describe('API tests', function () {
|
|||
expect(response.body.success).to.be.true;
|
||||
});
|
||||
|
||||
it('should GET /users/:user/addresses (with metaData)', async () => {
|
||||
const response = await server.get(`/users/${userId}/addresses?metaData=true`).expect(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
expect(response.body.results.length).to.equal(2);
|
||||
response.body.results.sort((a, b) => a.id.localeCompare(b.id));
|
||||
|
||||
expect(response.body.results[1].address).to.equal('alias1@example.com');
|
||||
expect(response.body.results[1].main).to.be.true;
|
||||
expect(response.body.results[1].metaData.tere).to.equal(123);
|
||||
|
||||
address = response.body.results[1];
|
||||
});
|
||||
|
||||
it('should GET /users/:user/address/:address', async () => {
|
||||
const response = await server.get(`/users/${userId}/addresses/${address.id}`).expect(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
expect(response.body.metaData.tere).to.equal(123);
|
||||
});
|
||||
|
||||
it('should GET /users/:user/addresses (after DELETE)', async () => {
|
||||
const response = await server.get(`/users/${userId}/addresses`).expect(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
|
|
Loading…
Add table
Reference in a new issue