diff --git a/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts b/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts index 54d680f12b8..de9d1eab38b 100644 --- a/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts +++ b/packages/nodes-base/nodes/Aws/SES/AwsSes.node.ts @@ -867,7 +867,7 @@ export class AwsSes implements INodeType { const params = [ 'Action=DeleteCustomVerificationEmailTemplate', - `TemplateName=${templateName}`, + `TemplateName=${encodeURIComponent(templateName)}`, ]; responseData = await awsApiRequestSOAP.call( @@ -884,7 +884,7 @@ export class AwsSes implements INodeType { if (operation === 'get') { const templateName = this.getNodeParameter('templateName', i) as string; - const params = [`TemplateName=${templateName}`]; + const params = [`TemplateName=${encodeURIComponent(templateName)}`]; responseData = await awsApiRequestSOAP.call( this, @@ -925,7 +925,7 @@ export class AwsSes implements INodeType { } if (operation === 'send') { - const email = this.getNodeParameter('email', i) as string[]; + const email = this.getNodeParameter('email', i) as string; const templateName = this.getNodeParameter('templateName', i) as string; @@ -933,12 +933,14 @@ export class AwsSes implements INodeType { const params = [ 'Action=SendCustomVerificationEmail', - `TemplateName=${templateName}`, - `EmailAddress=${email}`, + `TemplateName=${encodeURIComponent(templateName)}`, + `EmailAddress=${encodeURIComponent(email)}`, ]; if (additionalFields.configurationSetName) { - params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`); + params.push( + `ConfigurationSetName=${encodeURIComponent(additionalFields.configurationSetName as string)}`, + ); } responseData = await awsApiRequestSOAP.call( @@ -959,27 +961,35 @@ export class AwsSes implements INodeType { const params = [ 'Action=UpdateCustomVerificationEmailTemplate', - `TemplateName=${templateName}`, + `TemplateName=${encodeURIComponent(templateName)}`, ]; if (updateFields.FailureRedirectionURL) { - params.push(`FailureRedirectionURL=${updateFields.FailureRedirectionURL}`); + params.push( + `FailureRedirectionURL=${encodeURIComponent(updateFields.FailureRedirectionURL as string)}`, + ); } if (updateFields.email) { - params.push(`FromEmailAddress=${updateFields.email}`); + params.push(`FromEmailAddress=${encodeURIComponent(updateFields.email as string)}`); } if (updateFields.successRedirectionURL) { - params.push(`SuccessRedirectionURL=${updateFields.successRedirectionURL}`); + params.push( + `SuccessRedirectionURL=${encodeURIComponent(updateFields.successRedirectionURL as string)}`, + ); } if (updateFields.templateContent) { - params.push(`TemplateContent=${updateFields.templateContent}`); + params.push( + `TemplateContent=${encodeURIComponent(updateFields.templateContent as string)}`, + ); } if (updateFields.templateSubject) { - params.push(`TemplateSubject=${updateFields.templateSubject}`); + params.push( + `TemplateSubject=${encodeURIComponent(updateFields.templateSubject as string)}`, + ); } responseData = await awsApiRequestSOAP.call( @@ -1031,19 +1041,25 @@ export class AwsSes implements INodeType { } if (additionalFields.configurationSetName) { - params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`); + params.push( + `ConfigurationSetName=${encodeURIComponent(additionalFields.configurationSetName as string)}`, + ); } if (additionalFields.returnPath) { - params.push(`ReturnPath=${additionalFields.returnPath}`); + params.push( + `ReturnPath=${encodeURIComponent(additionalFields.returnPath as string)}`, + ); } if (additionalFields.returnPathArn) { - params.push(`ReturnPathArn=${additionalFields.returnPathArn}`); + params.push( + `ReturnPathArn=${encodeURIComponent(additionalFields.returnPathArn as string)}`, + ); } if (additionalFields.sourceArn) { - params.push(`SourceArn=${additionalFields.sourceArn}`); + params.push(`SourceArn=${encodeURIComponent(additionalFields.sourceArn as string)}`); } if (additionalFields.replyToAddresses) { @@ -1083,7 +1099,10 @@ export class AwsSes implements INodeType { const fromEmail = this.getNodeParameter('fromEmail', i) as string; const additionalFields = this.getNodeParameter('additionalFields', i); const templateDataUi = this.getNodeParameter('templateDataUi', i) as IDataObject; - const params = [`Template=${template}`, `Source=${encodeURIComponent(fromEmail)}`]; + const params = [ + `Template=${encodeURIComponent(template)}`, + `Source=${encodeURIComponent(fromEmail)}`, + ]; if (toAddresses.length) { setParameter(params, 'Destination.ToAddresses.member', toAddresses); @@ -1096,19 +1115,25 @@ export class AwsSes implements INodeType { } if (additionalFields.configurationSetName) { - params.push(`ConfigurationSetName=${additionalFields.configurationSetName}`); + params.push( + `ConfigurationSetName=${encodeURIComponent(additionalFields.configurationSetName as string)}`, + ); } if (additionalFields.returnPath) { - params.push(`ReturnPath=${additionalFields.returnPath}`); + params.push( + `ReturnPath=${encodeURIComponent(additionalFields.returnPath as string)}`, + ); } if (additionalFields.returnPathArn) { - params.push(`ReturnPathArn=${additionalFields.returnPathArn}`); + params.push( + `ReturnPathArn=${encodeURIComponent(additionalFields.returnPathArn as string)}`, + ); } if (additionalFields.sourceArn) { - params.push(`SourceArn=${additionalFields.sourceArn}`); + params.push(`SourceArn=${encodeURIComponent(additionalFields.sourceArn as string)}`); } if (additionalFields.replyToAddresses) { @@ -1193,7 +1218,7 @@ export class AwsSes implements INodeType { if (operation === 'delete') { const templateName = this.getNodeParameter('templateName', i) as string; - const params = [`TemplateName=${templateName}`]; + const params = [`TemplateName=${encodeURIComponent(templateName)}`]; responseData = await awsApiRequestSOAP.call( this, @@ -1208,7 +1233,7 @@ export class AwsSes implements INodeType { if (operation === 'get') { const templateName = this.getNodeParameter('templateName', i) as string; - const params = [`TemplateName=${templateName}`]; + const params = [`TemplateName=${encodeURIComponent(templateName)}`]; responseData = await awsApiRequestSOAP.call( this, diff --git a/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts b/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts index 9f4289dca4b..693be44cf7a 100644 --- a/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts +++ b/packages/nodes-base/nodes/Aws/SES/test/AwsSes.node.test.ts @@ -1,5 +1,5 @@ import { NodeTestHarness } from '@nodes-testing/node-test-harness'; -import { NodeConnectionTypes, type WorkflowTestData } from 'n8n-workflow'; +import { NodeConnectionTypes, type INodeParameters, type WorkflowTestData } from 'n8n-workflow'; import assert from 'node:assert'; import qs from 'node:querystring'; @@ -8,9 +8,55 @@ import { credentials } from '../../__tests__/credentials'; describe('AwsSes Node', () => { const testHarness = new NodeTestHarness(); const email = 'test+user@example.com'; + const configurationSetName = 'config&segment=value'; + const returnPath = 'bounce+tag@example.com'; + const returnPathArn = 'arn:aws:ses:eu-central-1:123456789012:identity/bounce+tag@example.com'; + const sourceArn = 'arn:aws:ses:eu-central-1:123456789012:identity/example.com&segment=value'; + const templateName = 'Template&segment=value'; const templateData = { Name: 'Special. Characters @#$%^&*()_-', }; + const createWorkflowData = ( + parameters: INodeParameters, + ): WorkflowTestData['input']['workflowData'] => ({ + nodes: [ + { + parameters: {}, + id: 'b30ae9d4-6a92-4b62-92f4-5810b4718c66', + name: 'When clicking ‘Execute workflow’', + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [720, 380], + }, + { + parameters, + id: '07955ca4-e9c9-415a-8175-dda8ad3204fd', + name: 'AWS SES', + type: 'n8n-nodes-base.awsSes', + typeVersion: 1, + position: [940, 380], + credentials: { + aws: { + id: '1', + name: 'AWS', + }, + }, + }, + ], + connections: { + 'When clicking ‘Execute workflow’': { + main: [ + [ + { + node: 'AWS SES', + type: NodeConnectionTypes.Main, + index: 0, + }, + ], + ], + }, + }, + }); const tests: WorkflowTestData[] = [ { description: 'should create customVerificationEmail', @@ -94,61 +140,28 @@ describe('AwsSes Node', () => { }, }, { - description: 'should URIencode params for sending email with template', + description: 'should preserve reserved characters when sending email with a template', input: { - workflowData: { - nodes: [ - { - parameters: {}, - type: 'n8n-nodes-base.manualTrigger', - typeVersion: 1, - position: [-180, 520], - id: '363e874a-9054-4a64-bc3f-786719dde626', - name: 'When clicking ‘Execute workflow’', - }, - { - parameters: { - operation: 'sendTemplate', - templateName: '=Template11', - fromEmail: 'test+user@example.com', - toAddresses: ['test+user@example.com'], - templateDataUi: { - templateDataValues: [ - { - key: 'Name', - value: '=Special. Characters @#$%^&*()_-', - }, - ], - }, - additionalFields: {}, + workflowData: createWorkflowData({ + operation: 'sendTemplate', + templateName, + fromEmail: email, + toAddresses: [email], + templateDataUi: { + templateDataValues: [ + { + key: 'Name', + value: templateData.Name, }, - type: 'n8n-nodes-base.awsSes', - typeVersion: 1, - position: [60, 520], - id: '13bbf4ef-8320-45d1-9210-61b62794a108', - name: 'AWS SES', - credentials: { - aws: { - id: 'Nz0QZhzu3MvfK4TQ', - name: 'AWS account', - }, - }, - }, - ], - connections: { - 'When clicking ‘Execute workflow’': { - main: [ - [ - { - node: 'AWS SES', - type: NodeConnectionTypes.Main, - index: 0, - }, - ], - ], - }, + ], }, - }, + additionalFields: { + configurationSetName, + returnPath, + returnPathArn, + sourceArn, + }, + }), }, output: { nodeData: { 'AWS SES': [[{ json: { success: 'true' } }]] }, @@ -158,7 +171,7 @@ describe('AwsSes Node', () => { mocks: [ { method: 'post', - path: `/?Action=SendTemplatedEmail&Template=Template11&Source=${encodeURIComponent(email)}&Destination.ToAddresses.member.1=${encodeURIComponent(email)}&TemplateData=${encodeURIComponent(JSON.stringify(templateData))}`, + path: `/?Action=SendTemplatedEmail&Template=${encodeURIComponent(templateName)}&Source=${encodeURIComponent(email)}&Destination.ToAddresses.member.1=${encodeURIComponent(email)}&ConfigurationSetName=${encodeURIComponent(configurationSetName)}&ReturnPath=${encodeURIComponent(returnPath)}&ReturnPathArn=${encodeURIComponent(returnPathArn)}&SourceArn=${encodeURIComponent(sourceArn)}&TemplateData=${encodeURIComponent(JSON.stringify(templateData))}`, statusCode: 200, responseBody: 'true', @@ -166,6 +179,104 @@ describe('AwsSes Node', () => { ], }, }, + { + description: 'should preserve reserved characters when sending an email', + input: { + workflowData: createWorkflowData({ + resource: 'email', + operation: 'send', + fromEmail: email, + toAddresses: [email], + subject: 'Test subject', + body: 'Test body', + isBodyHtml: false, + additionalFields: { + configurationSetName, + returnPath, + returnPathArn, + sourceArn, + }, + }), + }, + output: { + nodeData: { + 'AWS SES': [[{ json: { SendEmailResponse: { success: 'true' } } }]], + }, + }, + nock: { + baseUrl: 'https://email.eu-central-1.amazonaws.com', + mocks: [ + { + method: 'post', + path: `/?Action=SendEmail&Message.Subject.Data=Test%20subject&Source=${encodeURIComponent(email)}&Message.Body.Text.Data=Test%20body&Destination.ToAddresses.member.1=${encodeURIComponent(email)}&ConfigurationSetName=${encodeURIComponent(configurationSetName)}&ReturnPath=${encodeURIComponent(returnPath)}&ReturnPathArn=${encodeURIComponent(returnPathArn)}&SourceArn=${encodeURIComponent(sourceArn)}`, + statusCode: 200, + responseBody: 'true', + }, + ], + }, + }, + { + description: 'should preserve reserved characters when getting a template', + input: { + workflowData: createWorkflowData({ + resource: 'template', + operation: 'get', + templateName, + }), + }, + output: { + nodeData: { 'AWS SES': [[{ json: { success: 'true' } }]] }, + }, + nock: { + baseUrl: 'https://email.eu-central-1.amazonaws.com', + mocks: [ + { + method: 'post', + path: `/?Action=GetTemplate&TemplateName=${encodeURIComponent(templateName)}`, + statusCode: 200, + responseBody: 'true', + }, + ], + }, + }, + { + description: 'should preserve reserved characters when sending a custom verification email', + input: { + workflowData: createWorkflowData({ + resource: 'customVerificationEmail', + operation: 'send', + email, + templateName, + additionalFields: { + configurationSetName, + }, + }), + }, + output: { + nodeData: { 'AWS SES': [[{ json: { success: 'true' } }]] }, + }, + nock: { + baseUrl: 'https://email.eu-central-1.amazonaws.com', + mocks: [ + { + method: 'post', + path: '/', + requestBody: (body: string) => { + assert.deepEqual(qs.parse(body), { + Action: 'SendCustomVerificationEmail', + TemplateName: templateName, + EmailAddress: email, + ConfigurationSetName: configurationSetName, + }); + return true; + }, + statusCode: 200, + responseBody: + 'true', + }, + ], + }, + }, ]; for (const testData of tests) { diff --git a/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts b/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts index de6331c226c..917d27b5a74 100644 --- a/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts +++ b/packages/nodes-base/nodes/Aws/SQS/AwsSqs.node.ts @@ -316,12 +316,12 @@ export class AwsSqs implements INodeType { '', ) as string; if (messageDeduplicationId) { - params.push(`MessageDeduplicationId=${messageDeduplicationId}`); + params.push(`MessageDeduplicationId=${encodeURIComponent(messageDeduplicationId)}`); } const messageGroupId = this.getNodeParameter('messageGroupId', i) as string; if (messageGroupId) { - params.push(`MessageGroupId=${messageGroupId}`); + params.push(`MessageGroupId=${encodeURIComponent(messageGroupId)}`); } } @@ -331,8 +331,12 @@ export class AwsSqs implements INodeType { this.getNodeParameter('options.messageAttributes.string', i, []) as INodeParameters[] ).forEach((attribute) => { attributeCount++; - params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`); - params.push(`MessageAttribute.${attributeCount}.Value.StringValue=${attribute.value}`); + params.push( + `MessageAttribute.${attributeCount}.Name=${encodeURIComponent(attribute.name as string)}`, + ); + params.push( + `MessageAttribute.${attributeCount}.Value.StringValue=${encodeURIComponent(attribute.value as string)}`, + ); params.push(`MessageAttribute.${attributeCount}.Value.DataType=String`); }); @@ -345,8 +349,12 @@ export class AwsSqs implements INodeType { const dataPropertyName = attribute.dataPropertyName as string; const binaryData = this.helpers.assertBinaryData(i, dataPropertyName); - params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`); - params.push(`MessageAttribute.${attributeCount}.Value.BinaryValue=${binaryData.data}`); + params.push( + `MessageAttribute.${attributeCount}.Name=${encodeURIComponent(attribute.name as string)}`, + ); + params.push( + `MessageAttribute.${attributeCount}.Value.BinaryValue=${encodeURIComponent(binaryData.data)}`, + ); params.push(`MessageAttribute.${attributeCount}.Value.DataType=Binary`); }); @@ -355,8 +363,12 @@ export class AwsSqs implements INodeType { this.getNodeParameter('options.messageAttributes.number', i, []) as INodeParameters[] ).forEach((attribute) => { attributeCount++; - params.push(`MessageAttribute.${attributeCount}.Name=${attribute.name}`); - params.push(`MessageAttribute.${attributeCount}.Value.StringValue=${attribute.value}`); + params.push( + `MessageAttribute.${attributeCount}.Name=${encodeURIComponent(attribute.name as string)}`, + ); + params.push( + `MessageAttribute.${attributeCount}.Value.StringValue=${encodeURIComponent(attribute.value as number)}`, + ); params.push(`MessageAttribute.${attributeCount}.Value.DataType=Number`); }); diff --git a/packages/nodes-base/nodes/Aws/SQS/test/AwsSqs.node.test.ts b/packages/nodes-base/nodes/Aws/SQS/test/AwsSqs.node.test.ts new file mode 100644 index 00000000000..ea1314402ea --- /dev/null +++ b/packages/nodes-base/nodes/Aws/SQS/test/AwsSqs.node.test.ts @@ -0,0 +1,93 @@ +import { NodeTestHarness } from '@nodes-testing/node-test-harness'; +import { NodeConnectionTypes, type WorkflowTestData } from 'n8n-workflow'; + +import { credentials } from '../../__tests__/credentials'; + +describe('AwsSqs Node', () => { + const messageAttributeName = 'label&kind=primary'; + const messageAttributeValue = 'left&segment=value'; + const messageDeduplicationId = 'dedup&segment=value'; + const messageGroupId = 'group&segment=value'; + const queuePath = '/123456789012/n8n-node-test-fifo.fifo'; + + const testData: WorkflowTestData = { + description: 'should preserve reserved characters in request parameter values', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '5d4c45ed-1368-4ea3-a97c-2d46664f4656', + name: 'When clicking ‘Execute workflow’', + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [720, 380], + }, + { + parameters: { + queue: `https://sqs.eu-central-1.amazonaws.com${queuePath}`, + queueType: 'fifo', + sendInputData: false, + message: 'test message', + messageGroupId, + options: { + messageDeduplicationId, + messageAttributes: { + string: [ + { + name: messageAttributeName, + value: messageAttributeValue, + }, + ], + }, + }, + }, + id: 'bf664a84-bd26-413b-94d3-7f2935883ce3', + name: 'AWS SQS', + type: 'n8n-nodes-base.awsSqs', + typeVersion: 1, + position: [940, 380], + credentials: { + aws: { + id: '1', + name: 'AWS', + }, + }, + }, + ], + connections: { + 'When clicking ‘Execute workflow’': { + main: [ + [ + { + node: 'AWS SQS', + type: NodeConnectionTypes.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeData: { + 'AWS SQS': [[{ json: { MessageId: 'message-id' } }]], + }, + }, + nock: { + baseUrl: 'https://sqs.eu-central-1.amazonaws.com', + mocks: [ + { + method: 'get', + path: `${queuePath}?Version=2012-11-05&Action=SendMessage&MessageBody=test%20message&MessageDeduplicationId=${encodeURIComponent(messageDeduplicationId)}&MessageGroupId=${encodeURIComponent(messageGroupId)}&MessageAttribute.1.Name=${encodeURIComponent(messageAttributeName)}&MessageAttribute.1.Value.StringValue=${encodeURIComponent(messageAttributeValue)}&MessageAttribute.1.Value.DataType=String`, + statusCode: 200, + responseBody: + 'message-id', + }, + ], + }, + }; + + new NodeTestHarness().setupTest(testData, { credentials }); +});