diff --git a/config/roles.json b/config/roles.json
index 9b1b269d..42d224a0 100644
--- a/config/roles.json
+++ b/config/roles.json
@@ -237,12 +237,12 @@
},
"userlisting": {
- "read:own": ["*", "!tags", "!metaData", "!disabledScopes"]
+ "read:own": ["*", "!tags", "!disabledScopes", "!internalData"]
},
"users": {
- "read:own": ["*", "!tags", "!metaData", "!disabledScopes"],
- "update:own": ["*", "!tags", "!metaData", "!disabledScopes"]
+ "read:own": ["*", "!tags", "!disabledScopes", "!internalData"],
+ "update:own": ["*", "!tags", "!disabledScopes", "!internalData"]
},
"asps": {
diff --git a/lib/api/users.js b/lib/api/users.js
index 001ca841..2a6a90e4 100644
--- a/lib/api/users.js
+++ b/lib/api/users.js
@@ -31,6 +31,7 @@ module.exports = (db, server, userHandler) => {
* @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 {Boolean} [metaData] If true, then includes metaData
in the response
+ * @apiParam {Boolean} [internalData] If true, then includes internalData
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 nextCursor
response value
@@ -54,6 +55,7 @@ module.exports = (db, server, userHandler) => {
* @apiSuccess {Boolean} results.encryptForwarded If true
then forwarded messages are encrypted
* @apiSuccess {Object} results.quota Quota usage limits
* @apiSuccess {Object} [results.metaData] Custom metadata value. Included if metaData
query argument was true
+ * @apiSuccess {Object} [results.internalData] Custom metadata value for internal use. Included if internalData
query argument was true and request was not made using user-role token
* @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 true
then the User has a password set and can authenticate
@@ -114,6 +116,7 @@ module.exports = (db, server, userHandler) => {
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,
@@ -253,6 +256,10 @@ module.exports = (db, server, userHandler) => {
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) {
@@ -307,6 +314,10 @@ module.exports = (db, server, userHandler) => {
values.metaData = formatMetaData(userData.metaData);
}
+ if (userData.internalData) {
+ values.internalData = formatMetaData(userData.internalData);
+ }
+
return permission.filter(values);
})
};
@@ -342,6 +353,7 @@ module.exports = (db, server, userHandler) => {
* @apiParam {Boolean} [encryptForwarded] If true
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 {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} [language] Language code for the User
* @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} [spamLevel=50] Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam
@@ -493,6 +505,14 @@ module.exports = (db, server, userHandler) => {
Joi.object()
),
+ internalData: Joi.alternatives().try(
+ Joi.string()
+ .empty('')
+ .trim()
+ .max(1024 * 1024),
+ Joi.object()
+ ),
+
pubKey: Joi.string()
.empty('')
.trim()
@@ -659,29 +679,31 @@ module.exports = (db, server, userHandler) => {
return next();
}
- if (result.value.metaData) {
- if (typeof result.value.metaData === 'object') {
- try {
- result.value.metaData = JSON.stringify(result.value.metaData);
- } catch (err) {
- res.json({
- error: 'metaData value must be serializable to JSON',
- code: 'InputValidationError'
- });
- return next();
- }
- } else {
- try {
- let value = JSON.parse(result.value.metaData);
- if (!value || typeof value !== 'object') {
- throw new Error('Not an object');
+ 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();
}
- } catch (err) {
- res.json({
- error: 'metaData value must be valid JSON object string',
- code: 'InputValidationError'
- });
- return next();
}
}
}
@@ -842,6 +864,7 @@ module.exports = (db, server, userHandler) => {
* @apiSuccess {String} keyInfo.address E-mail address listed in public key
* @apiSuccess {String} keyInfo.fingerprint Fingerprint of the public key
* @apiSuccess {Object} metaData Custom metadata object set for this user
+ * @apiSuccess {Object} internalData Custom interna metadata object set for this user. Not available for user-role tokens
* @apiSuccess {String[]} targets List of forwarding targets
* @apiSuccess {Number} spamLevel Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam
* @apiSuccess {Object} limits Account limits and usage
@@ -1088,6 +1111,7 @@ module.exports = (db, server, userHandler) => {
keyInfo,
metaData: formatMetaData(userData.metaData),
+ internalData: formatMetaData(userData.internalData),
targets: [].concat(userData.targets || []).map(targetData => targetData.value),
@@ -1183,6 +1207,7 @@ module.exports = (db, server, userHandler) => {
* @apiParam {Boolean} [encryptForwarded] If true
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 {Object|String} [metaData] Optional metadata, must be an object or JSON formatted string of an object
+ * @apiParam {Object|String} [internalData] Optional internal metadata, must be an object or JSON formatted string of an object. Not available for user-role tokens
* @apiParam {String} [language] Language code for the User
* @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} [spamLevel] Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam
@@ -1266,6 +1291,14 @@ module.exports = (db, server, userHandler) => {
Joi.object()
),
+ internalData: Joi.alternatives().try(
+ Joi.string()
+ .empty('')
+ .trim()
+ .max(1024 * 1024),
+ Joi.object()
+ ),
+
pubKey: Joi.string()
.empty('')
.trim()
@@ -1417,29 +1450,31 @@ module.exports = (db, server, userHandler) => {
return next();
}
- if (result.value.metaData) {
- if (typeof result.value.metaData === 'object') {
- try {
- result.value.metaData = JSON.stringify(result.value.metaData);
- } catch (err) {
- res.json({
- error: 'metaData value must be serializable to JSON',
- code: 'InputValidationError'
- });
- return next();
- }
- } else {
- try {
- let value = JSON.parse(result.value.metaData);
- if (!value || typeof value !== 'object') {
- throw new Error('Not an object');
+ 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();
}
- } catch (err) {
- res.json({
- error: 'metaData value must be valid JSON object string',
- code: 'InputValidationError'
- });
- return next();
}
}
}
diff --git a/package.json b/package.json
index 9bf87bdd..87e5a004 100644
--- a/package.json
+++ b/package.json
@@ -15,11 +15,11 @@
"author": "Andris Reinman",
"license": "EUPL-1.1+",
"devDependencies": {
- "ajv": "6.12.4",
+ "ajv": "6.12.5",
"apidoc": "0.25.0",
"chai": "4.2.0",
"docsify-cli": "4.4.1",
- "eslint": "7.8.1",
+ "eslint": "7.9.0",
"eslint-config-nodemailer": "1.2.0",
"eslint-config-prettier": "6.11.0",
"grunt": "1.3.0",
@@ -57,7 +57,7 @@
"mailsplit": "5.0.0",
"mobileconfig": "2.3.1",
"mongo-cursor-pagination": "7.3.1",
- "mongodb": "3.6.1",
+ "mongodb": "3.6.2",
"mongodb-extended-json": "1.11.0",
"node-forge": "0.10.0",
"nodemailer": "6.4.11",
@@ -76,7 +76,7 @@
"unixcrypt": "1.0.11",
"uuid": "8.3.0",
"wild-config": "1.5.1",
- "yargs": "16.0.0"
+ "yargs": "16.0.3"
},
"repository": {
"type": "git",