From 85e09ecc772618ea2bccc7912181c6217a6e7b9c Mon Sep 17 00:00:00 2001 From: NickOvt Date: Wed, 24 Jan 2024 10:07:30 +0200 Subject: [PATCH 1/2] feat(api-addresses): ZMS-115 (#608) * List registered Addresses API endpoint added to API docs generation * Create new Address API endpoint to API generation * Create new Address API endpoint to API generation. List registered Addresses for a User api endpoint added to api generation * Request Addresses information api endpoint added to api generation * Update Address information api endpoint added to api endpoint generation * Delete an Address endpoint added to api generation * List addresses from communication register endpoint added to api docs generation. Add example to userId * Create new forwarded Address api endpoint added to api docs generation * Update forwarded Address information API endpoint added to API docs generation * Delete a forwarded Address endpoint added to API docs generation * Request forwarded Addresses information endpoint added to API docs generation * Get Address info endpoint added to API docs generation * Rename domain in addresses endpoint added to API docs generation --- lib/api/addresses.js | 692 ++++++++++++++++------ lib/schemas/request/addresses-schemas.js | 30 + lib/schemas/request/general-schemas.js | 8 +- lib/schemas/response/addresses-schemas.js | 59 ++ 4 files changed, 622 insertions(+), 167 deletions(-) create mode 100644 lib/schemas/request/addresses-schemas.js create mode 100644 lib/schemas/response/addresses-schemas.js diff --git a/lib/api/addresses.js b/lib/api/addresses.js index 80496ffa..63a0a0b3 100644 --- a/lib/api/addresses.js +++ b/lib/api/addresses.js @@ -19,26 +19,76 @@ const { ADDRESS_FORWARDED_DELETED, ADDRESS_DOMAIN_RENAMED } = require('../events'); +const { successRes } = require('../schemas/response/general-schemas'); +const { + GetAddressesResult, + GetUserAddressesResult, + GetUserAddressesregisterResult, + AddressLimits, + AutoreplyInfo +} = require('../schemas/response/addresses-schemas'); +const { userId, addressEmail, addressId } = require('../schemas/request/general-schemas'); +const { Autoreply } = require('../schemas/request/addresses-schemas'); module.exports = (db, server, userHandler, settingsHandler) => { server.get( - { name: 'addresses', path: '/addresses' }, + { + name: 'addresses', + path: '/addresses', + summary: 'List registered Addresses', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + queryParams: { + query: Joi.string().trim().empty('').max(255).description('Partial match of an address'), + forward: Joi.string().trim().empty('').max(255).description('Partial match of a forward email address or URL'), + tags: Joi.string().trim().empty('').max(1024).description('Comma separated list of tags. The Address must have at least one to be set'), + requiredTags: Joi.string() + .trim() + .empty('') + .max(1024) + .description('Comma separated list of tags. The Address must have all listed tags to be set'), + metaData: booleanSchema.description('If true, then includes metaData in the response'), + internalData: booleanSchema.description('If true, then includes internalData in the response. Not shown for user-role tokens.'), + limit: Joi.number().default(20).min(1).max(250).description('How many records to return'), + next: nextPageCursorSchema, + previous: previousPageCursorSchema, + page: pageNrSchema, + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: {}, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + query: Joi.string().required().description('Partial match of an address'), + total: Joi.number().required().description('How many results were found'), + page: Joi.number().required().description('Current page number. Derived from page query argument'), + previousCursor: Joi.alternatives() + .try(Joi.string(), booleanSchema) + .required() + .description('Either a cursor string or false if there are not any previous results'), + nextCursor: Joi.alternatives() + .try(Joi.string(), booleanSchema) + .required() + .description('Either a cursor string or false if there are not any next results'), + results: Joi.array().items(GetAddressesResult).required().description('Address listing') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - query: Joi.string().trim().empty('').max(255), - forward: 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, - page: pageNrSchema, - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -221,23 +271,58 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.post( - '/users/:user/addresses', + { + path: '/users/:user/addresses', + summary: 'Create new Address', + description: + 'Add a new email address for a User. Addresses can contain unicode characters. Dots in usernames are normalized so no need to create both "firstlast@example.com" and "first.last@example.com" Special addresses `*@example.com`, `*suffix@example.com` and `username@*` catches all emails to these domains or users without a registered destination (requires allowWildcard argument)', + tags: ['Addresses'], + validationObjs: { + requestBody: { + address: Joi.alternatives() + .try(addressEmail, Joi.string().regex(/^\w+@\*$/, 'special address')) + .description('E-mail Address'), + name: Joi.string().empty('').trim().max(128).description('Identity name'), + main: booleanSchema.description('Indicates if this is the default address for the User'), + allowWildcard: booleanSchema.description( + 'If true then address value can be in the form of `*@example.com`, `*suffix@example.com` and `username@*`, otherwise using * is not allowed. Static suffix can be up to 32 characters long.' + ), + tags: Joi.array().items(Joi.string().trim().max(128)).description('A list of tags associated with this address'), + + metaData: metaDataSchema.label('metaData').description('Optional metadata, must be an object or JSON formatted string'), + internalData: metaDataSchema + .label('internalData') + .description( + 'Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens' + ), + + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: { + user: userId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: Joi.string().required().description('ID of the address') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - address: [Joi.string().email({ tlds: false }).required(), Joi.string().regex(/^\w+@\*$/, 'special address')], - name: Joi.string().empty('').trim().max(128), - main: booleanSchema, - allowWildcard: booleanSchema, - tags: Joi.array().items(Joi.string().trim().max(128)), - - metaData: metaDataSchema.label('metaData'), - internalData: metaDataSchema.label('internalData'), + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; - sess: sessSchema, - ip: sessIPSchema + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -494,16 +579,41 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/users/:user/addresses', + { + path: '/users/:user/addresses', + summary: 'List registered Addresses for a User', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + queryParams: { + metaData: booleanSchema.description('If true, then includes metaData in the response'), + internalData: booleanSchema.description('If true, then includes internalData in the response. Not shown for user-role tokens.'), + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + results: Joi.array().items(GetUserAddressesResult).required().description('Address listing') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - metaData: booleanSchema, - internalData: booleanSchema, - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -614,15 +724,46 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/users/:user/addresses/:address', + { + path: '/users/:user/addresses/:address', + summary: 'Request Addresses information', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + queryParams: { + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId, + address: addressId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + id: addressId, + name: Joi.string().required().description('Identity name'), + address: addressEmail, + main: booleanSchema.required().description('Indicates if this is the default address for the User'), + created: Joi.date().required().description('Datestring of the time the address was created'), + tags: Joi.array().items(Joi.string()).required().description('List of tags associated with the Address'), + metaData: Joi.object({}).description('Metadata object (if available)'), + internalData: Joi.object({}).description('Internal metadata object (if available), not included for user-role requests') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - address: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -725,23 +866,55 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.put( - '/users/:user/addresses/:id', + { + path: '/users/:user/addresses/:id', + summary: 'Update Address information', + tags: ['Addresses'], + validationObjs: { + requestBody: { + name: Joi.string().empty('').trim().max(128).description('Identity name'), + address: Joi.string() + .email({ tlds: false }) + .description( + 'New address if you want to rename existing address. Only affects normal addresses, special addresses that include * can not be changed' + ), + main: booleanSchema.required().description('Indicates if this is the default address for the User'), + tags: Joi.array().items(Joi.string().trim().max(128)).description('A list of tags associated with this address'), + + metaData: metaDataSchema.label('metaData').description('Optional metadata, must be an object or JSON formatted string'), + internalData: metaDataSchema + .label('internalData') + .description( + 'Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens' + ), + + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: { + user: userId, + id: addressId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - id: Joi.string().hex().lowercase().length(24).required(), - name: Joi.string().empty('').trim().max(128), - address: Joi.string().email({ tlds: false }), - main: booleanSchema, - tags: Joi.array().items(Joi.string().trim().max(128)), - - metaData: metaDataSchema.label('metaData'), - internalData: metaDataSchema.label('internalData'), + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; - sess: sessSchema, - ip: sessIPSchema + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -954,15 +1127,39 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.del( - '/users/:user/addresses/:address', + { + path: '/users/:user/addresses/:address', + summary: 'Delete an Address', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + pathParams: { + user: userId, + address: addressId + }, + queryParams: { + sess: sessSchema, + ip: sessIPSchema + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - address: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -1077,16 +1274,41 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/users/:user/addressregister', + { + path: '/users/:user/addressregister', + summary: 'List addresses from communication register', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + queryParams: { + query: Joi.string().trim().empty('').max(255).required().description('Prefix of an address or a name').example('`query=john`'), + limit: Joi.number().default(20).min(1).max(250).description('How many records to return').example('`limit=25`'), + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + user: userId + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + results: Joi.array().items(GetUserAddressesregisterResult).required().description('Address listing') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - user: Joi.string().hex().lowercase().length(24).required(), - query: Joi.string().trim().empty('').max(255).required(), - limit: Joi.number().default(20).min(1).max(250), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -1219,48 +1441,65 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.post( - '/addresses/forwarded', + { + path: '/addresses/forwarded', + summary: 'Create new forwarded Address', + description: + 'Add a new forwarded email address. Addresses can contain unicode characters. Dots in usernames are normalized so no need to create both "firstlast@example.com" and "first.last@example.com" Special addresses `*@example.com` and `username@*` catches all emails to these domains or users without a registered destination (requires allowWildcard argument)', + tags: ['Addresses'], + validationObjs: { + requestBody: { + address: Joi.alternatives() + .try(addressEmail, Joi.string().regex(/^\w+@\*$/, 'special address')) + .required() + .description('E-mail Address'), + name: Joi.string().empty('').trim().max(128).description('Identity name'), + targets: Joi.array() + .items( + Joi.string().email({ tlds: false }), + Joi.string().uri({ + scheme: [/smtps?/, /https?/], + allowRelative: false, + relativeOnly: false + }) + ) + .description( + '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' + ), + forwards: Joi.number().min(0).default(0).description('Daily allowed forwarding count for this address'), + allowWildcard: booleanSchema.description( + 'If true then address value can be in the form of `*@example.com`, otherwise using * is not allowed' + ), + autoreply: Autoreply, + tags: Joi.array().items(Joi.string().trim().max(128)).description('A list of tags associated with this address'), + metaData: metaDataSchema.label('metaData').description('Optional metadata, must be an object or JSON formatted string'), + internalData: metaDataSchema + .label('internalData') + .description( + 'Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens' + ), + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: {}, + response: { + 200: { + description: 'Success', + model: Joi.object({ success: successRes, id: addressId }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - address: Joi.alternatives() - .try(Joi.string().email({ tlds: false }).required(), Joi.string().regex(/^\w+@\*$/, 'special address')) - .required(), - name: Joi.string().empty('').trim().max(128), - targets: Joi.array().items( - Joi.string().email({ tlds: false }), - Joi.string().uri({ - scheme: [/smtps?/, /https?/], - allowRelative: false, - relativeOnly: false - }) - ), - forwards: Joi.number().min(0).default(0), - allowWildcard: booleanSchema, - autoreply: Joi.object().keys({ - status: booleanSchema.default(true), - start: Joi.date().empty('').allow(false), - end: Joi.date().empty('').allow(false), - name: Joi.string().empty('').trim().max(128), - subject: Joi.string() - .empty('') - .trim() - .max(2 * 1024), - text: Joi.string() - .empty('') - .trim() - .max(128 * 1024), - html: Joi.string() - .empty('') - .trim() - .max(128 * 1024) - }), - tags: Joi.array().items(Joi.string().trim().max(128)), - metaData: metaDataSchema.label('metaData'), - internalData: metaDataSchema.label('internalData'), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -1505,47 +1744,60 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.put( - '/addresses/forwarded/:id', + { + path: '/addresses/forwarded/:id', + summary: 'Update forwarded Address information', + tags: ['Addresses'], + validationObjs: { + requestBody: { + address: Joi.string() + .email({ tlds: false }) + .description('New address. Only affects normal addresses, special addresses that include * can not be changed'), + name: Joi.string().empty('').trim().max(128).description('Identity name'), + targets: Joi.array() + .items( + Joi.string().email({ tlds: false }), + Joi.string().uri({ + scheme: [/smtps?/, /https?/], + allowRelative: false, + relativeOnly: false + }) + ) + .description( + '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' + ), + forwards: Joi.number().min(0).description('Daily allowed forwarding count for this address'), + autoreply: Autoreply, + tags: Joi.array().items(Joi.string().trim().max(128)).description('A list of tags associated with this address'), + metaData: metaDataSchema.label('metaData').description('Optional metadata, must be an object or JSON formatted string'), + internalData: metaDataSchema + .label('internalData') + .description( + 'Optional metadata for internal use, must be an object or JSON formatted string of an object. Not available for user-role tokens' + ), + forwardedDisabled: booleanSchema.description('If true then disables forwarded address (stops forwarding messages)'), + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: { id: addressId }, + response: { + 200: { + description: 'Success', + model: Joi.object({ success: successRes }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - id: Joi.string().hex().lowercase().length(24).required(), - address: Joi.string().email({ tlds: false }), - name: Joi.string().empty('').trim().max(128), - targets: Joi.array().items( - Joi.string().email({ tlds: false }), - Joi.string().uri({ - scheme: [/smtps?/, /https?/], - allowRelative: false, - relativeOnly: false - }) - ), - forwards: Joi.number().min(0), - autoreply: Joi.object().keys({ - status: booleanSchema, - start: Joi.date().empty('').allow(false), - end: Joi.date().empty('').allow(false), - name: Joi.string().empty('').trim().max(128), - subject: Joi.string() - .empty('') - .trim() - .max(2 * 1024), - text: Joi.string() - .empty('') - .trim() - .max(128 * 1024), - html: Joi.string() - .empty('') - .trim() - .max(128 * 1024) - }), - tags: Joi.array().items(Joi.string().trim().max(128)), - metaData: metaDataSchema.label('metaData'), - internalData: metaDataSchema.label('internalData'), - forwardedDisabled: booleanSchema, - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -1790,14 +2042,34 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.del( - '/addresses/forwarded/:address', + { + path: '/addresses/forwarded/:address', + summary: 'Delete a forwarded Address', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + queryParams: { + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { address: addressId }, + response: { + 200: { + description: 'Success', + model: Joi.object({ success: successRes }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - address: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -1869,14 +2141,43 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/addresses/forwarded/:address', + { + path: '/addresses/forwarded/:address', + summary: 'Request forwarded Addresses information', + tags: ['Addresses'], + validationObjs: { + requestBody: {}, + queryParams: { sess: sessSchema, ip: sessIPSchema }, + pathParams: { address: addressId }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: addressId, + address: addressEmail, + name: Joi.string().required().description('Identity name'), + targets: Joi.array().items(Joi.string()).description('List of forwarding targets'), + limits: AddressLimits, + autoreply: AutoreplyInfo, + created: Joi.date().required().description('Datestring of the time the address was created'), + tags: Joi.array().items(Joi.string()).required().description('List of tags associated with the Address'), + metaData: Joi.object({}).description('Metadata object (if available)'), + internalData: Joi.object({}).description('Internal metadata object (if available), not included for user-role requests') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - address: Joi.string().hex().lowercase().length(24).required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -1970,15 +2271,48 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.get( - '/addresses/resolve/:address', + { + path: '/addresses/resolve/:address', + summary: 'Get Address info', + tags: ['Addrresses'], + validationObjs: { + requestBody: {}, + queryParams: { + allowWildcard: booleanSchema.description('If true then resolves also wildcard addresses'), + sess: sessSchema, + ip: sessIPSchema + }, + pathParams: { + address: Joi.alternatives().try(addressId, addressEmail).required().description('ID of the Address or e-mail address string') + }, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + id: addressId, + address: addressEmail, + name: Joi.string().required().description('Identity name'), + user: userId.description('ID of the user if the address belongs to a User'), + targets: Joi.array().items(Joi.string()).description('List of forwarding targets if this is a Forwarded address'), + limits: AddressLimits, + autoreply: AutoreplyInfo, + tags: Joi.array().items(Joi.string()).required().description('List of tags associated with the Address'), + created: Joi.date().required().description('Datestring of the time the address was created') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - address: [Joi.string().hex().lowercase().length(24).required(), Joi.string().email({ tlds: false })], - allowWildcard: booleanSchema, - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { @@ -2098,15 +2432,43 @@ module.exports = (db, server, userHandler, settingsHandler) => { ); server.put( - '/addresses/renameDomain', + { + path: '/addresses/renameDomain', + summary: 'Rename domain in addresses', + description: 'Renames domain names for addresses, DKIM keys and Domain Aliases', + tags: ['Addresses'], + validationObjs: { + requestBody: { + oldDomain: Joi.string().required().description('Old Domain Name'), + newDomain: Joi.string().required().description('New Domain Name'), + sess: sessSchema, + ip: sessIPSchema + }, + queryParams: {}, + pathParams: {}, + response: { + 200: { + description: 'Success', + model: Joi.object({ + success: successRes, + modifiedAddresses: Joi.number().required().description('Number of modified addresses'), + modifiedUsers: Joi.number().required().description('Number of modified users'), + modifiedDkim: Joi.number().required().description('Number of modified DKIM keys'), + modifiedAliases: Joi.number().required().description('Number of modified Domain Aliases') + }) + } + } + } + }, tools.responseWrapper(async (req, res) => { res.charSet('utf-8'); - const schema = Joi.object().keys({ - oldDomain: Joi.string().required(), - newDomain: Joi.string().required(), - sess: sessSchema, - ip: sessIPSchema + const { pathParams, requestBody, queryParams } = req.route.spec.validationObjs; + + const schema = Joi.object({ + ...pathParams, + ...queryParams, + ...requestBody }); const result = schema.validate(req.params, { diff --git a/lib/schemas/request/addresses-schemas.js b/lib/schemas/request/addresses-schemas.js new file mode 100644 index 00000000..77b9c889 --- /dev/null +++ b/lib/schemas/request/addresses-schemas.js @@ -0,0 +1,30 @@ +'use strict'; + +const Joi = require('joi'); +const { booleanSchema } = require('../../schemas'); + +const Autoreply = Joi.object({ + status: booleanSchema.default(true).description('If true, then autoreply is enabled for this address'), + start: Joi.date().empty('').allow(false).description('Either a date string or boolean false to disable start time checks'), + end: Joi.date().empty('').allow(false).description('Either a date string or boolean false to disable end time checks'), + name: Joi.string().empty('').trim().max(128).description('Name that is used for the From: header in autoreply message'), + subject: Joi.string() + .empty('') + .trim() + .max(2 * 1024) + .description('Autoreply subject line'), + text: Joi.string() + .empty('') + .trim() + .max(128 * 1024) + .description('Autoreply plaintext content'), + html: Joi.string() + .empty('') + .trim() + .max(128 * 1024) + .description('Autoreply HTML content') +}) + .description('Autoreply information') + .$_setFlag('objectName', 'Autoreply'); + +module.exports = { Autoreply }; diff --git a/lib/schemas/request/general-schemas.js b/lib/schemas/request/general-schemas.js index 2eef368e..398b9c72 100644 --- a/lib/schemas/request/general-schemas.js +++ b/lib/schemas/request/general-schemas.js @@ -2,12 +2,16 @@ const Joi = require('joi'); -const userId = Joi.string().hex().lowercase().length(24).required().description('ID of the User'); +const userId = Joi.string().hex().lowercase().length(24).required().description('Example: `507f1f77bcf86cd799439011`\nID of the User'); const mailboxId = Joi.string().hex().lowercase().length(24).required().description('ID of the Mailbox'); const messageId = Joi.number().min(1).required().description('Message ID'); +const addressEmail = Joi.string().email({ tlds: false }).required().description('E-mail Address'); +const addressId = Joi.string().hex().lowercase().length(24).required().description('ID of the Address'); module.exports = { userId, mailboxId, - messageId + messageId, + addressEmail, + addressId }; diff --git a/lib/schemas/response/addresses-schemas.js b/lib/schemas/response/addresses-schemas.js new file mode 100644 index 00000000..52e7ed11 --- /dev/null +++ b/lib/schemas/response/addresses-schemas.js @@ -0,0 +1,59 @@ +'use strict'; + +const Joi = require('joi'); +const { booleanSchema } = require('../../schemas'); +const { addressId, addressEmail } = require('../request/general-schemas'); + +const GetAddressesResult = Joi.object({ + id: Joi.string().required().description('ID of the Address'), + name: Joi.string().required().description('Identity name'), + address: Joi.string().required().description('E-mail address string'), + user: Joi.string().required().description('User ID this address belongs to if this is a User address'), + forwarded: booleanSchema.required().description('If true then it is a forwarded address'), + forwardedDisabled: booleanSchema.required().description('If true then the forwarded address is disabled'), + target: Joi.array().items(Joi.string()).description('List of forwarding targets') +}).$_setFlag('objectName', 'GetAddressesResult'); + +const GetUserAddressesResult = Joi.object({ + id: addressId, + name: Joi.string().required().description('Identity name'), + address: addressEmail, + main: booleanSchema.required().description('Indicates if this is the default address for the User'), + created: Joi.date().required().description('Datestring of the time the address was created'), + tags: Joi.array().items(Joi.string()).required().description('List of tags associated with the Address'), + metaData: Joi.object({}).description('Metadata object (if available)'), + internalData: Joi.object({}).description('Internal metadata object (if available), not included for user-role requests') +}).$_setFlag('objectName', 'GetUserAddressesResult'); + +const GetUserAddressesregisterResult = Joi.object({ + id: addressId, + name: Joi.string().description('Name from address header'), + address: addressEmail +}).$_setFlag('objectName', 'GetUserAddressesregisterResult'); + +const AddressLimits = Joi.object({ + forwards: Joi.object({ + allowed: Joi.number().required().description('How many messages per 24 hours can be forwarded'), + used: Joi.number().required().description('How many messages are forwarded during current 24 hour period'), + ttl: Joi.number().required().description('Time until the end of current 24 hour period') + }) + .required() + .description('Forwarding quota') + .$_setFlag('objectName', 'Forwards') +}) + .required() + .description('Account limits and usage') + .$_setFlag('objectName', 'AddressLimits'); + +const AutoreplyInfo = Joi.object({ + status: booleanSchema.required().description('If true, then autoreply is enabled for this address'), + name: Joi.string().required().description('Name that is used for the From: header in autoreply message'), + subject: Joi.string().required().description('Autoreply subject line'), + text: Joi.string().required().description('Autoreply plaintext content'), + html: Joi.string().required().description('Autoreply HTML content') +}) + .required() + .description('Autoreply information') + .$_setFlag('objectName', 'AutoreplyInfo'); + +module.exports = { GetAddressesResult, GetUserAddressesResult, GetUserAddressesregisterResult, AddressLimits, AutoreplyInfo }; From 816114f655e34adc15dc27ee13530fdc094b01e0 Mon Sep 17 00:00:00 2001 From: Louis Laureys Date: Thu, 25 Jan 2024 08:02:55 +0100 Subject: [PATCH 2/2] fix(message-threading): Take non-standard but conventional subject prefixes into account (#605) --- lib/message-handler.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/message-handler.js b/lib/message-handler.js index 088d52c9..08c3ca91 100644 --- a/lib/message-handler.js +++ b/lib/message-handler.js @@ -1456,12 +1456,13 @@ class MessageHandler { options = options || {}; subject = subject.replace(/\s+/g, ' ').trim(); + // `Re: [EXTERNAL] Re: Fwd: Example subject (fwd)` becomes `Example subject` if (options.removePrefix) { let match = true; while (match) { match = false; subject = subject - .replace(/^(re|fwd?)\s*:|\s*\(fwd\)\s*$/gi, () => { + .replace(/^(re|fwd?)\s*:|^\[.+?\](?=\s.+)|\s*\(fwd\)\s*$/gi, () => { match = true; return ''; })