From 5d5b2b2dccaa0b1e9dcb54299d0c6a0b7486911e Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 18 Dec 2024 16:01:18 +0800 Subject: [PATCH 1/2] chore: limit postman to max 50 recipients --- .../__tests__/common/parameters.test.ts | 22 +++++ .../src/apps/postman/common/parameters.ts | 92 +++++++++++-------- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts b/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts index 0158e214d..ac5bad631 100644 --- a/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts +++ b/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts @@ -2,6 +2,10 @@ import { assert, beforeEach, describe, expect, it } from 'vitest' import { transactionalEmailSchema } from '../../common/parameters' +function generateEmails(length: number) { + return Array.from({ length }, (_, i) => `user${i + 1}@example.com`).join(',') +} + describe('postman transactional email schema zod validation', () => { let validPayload: Record @@ -155,4 +159,22 @@ describe('postman transactional email schema zod validation', () => { expect(result.error?.errors[0].message).toEqual('Invalid CC emails') }, ) + + it.each([ + { recipient: 10, cc: 41 }, + { recipient: 41, cc: 10 }, + { recipient: 50, cc: 1 }, + { recipient: 1, cc: 50 }, + ])( + 'should fail if total number of emails in Recipient email(s) and CC recipient email(s) exceeds 50', + ({ recipient, cc }) => { + validPayload.destinationEmail = generateEmails(recipient) + validPayload.destinationEmailCc = generateEmails(cc) + const result = transactionalEmailSchema.safeParse(validPayload) + assert(result.success === false) + expect(result.error?.errors[0].message).toEqual( + 'The total number of emails in Recipient email(s) and CC recipient email(s) must not exceed 50', + ) + }, + ) }) diff --git a/packages/backend/src/apps/postman/common/parameters.ts b/packages/backend/src/apps/postman/common/parameters.ts index 78b99d8ed..8918690f6 100644 --- a/packages/backend/src/apps/postman/common/parameters.ts +++ b/packages/backend/src/apps/postman/common/parameters.ts @@ -87,45 +87,57 @@ export const transactionalEmailFields: IField[] = [ }, ] -export const transactionalEmailSchema = z.object({ - subject: z.string().min(1, { message: 'Empty subject' }).trim(), - body: z - .string() - .min(1, { message: 'Empty body' }) - // for backward-compatibility with content produced by the old editor - .transform((v) => v.replace(/\n/g, '
')), - destinationEmail: z - .string() - .transform((value, ctx) => - validateEmails(value, ctx, 'Invalid recipient emails'), - ), - destinationEmailCc: z - .string() - .transform((value, ctx) => validateEmails(value, ctx, 'Invalid CC emails')) - .optional(), - replyTo: z.preprocess((value) => { - if (typeof value !== 'string') { - return value - } - return value.trim() === '' ? undefined : value.trim() - }, z.string().email({ message: 'Invalid reply to email' }).optional()), - senderName: z.string().min(1, { message: 'Empty sender name' }).trim(), - attachments: z.array(z.string()).transform((array, context) => { - const result: string[] = [] - for (const value of array) { - // Account for optional attachment fields with no response. - if (!value) { - continue +export const transactionalEmailSchema = z + .object({ + subject: z.string().min(1, { message: 'Empty subject' }).trim(), + body: z + .string() + .min(1, { message: 'Empty body' }) + // for backward-compatibility with content produced by the old editor + .transform((v) => v.replace(/\n/g, '
')), + destinationEmail: z + .string() + .transform((value, ctx) => + validateEmails(value, ctx, 'Invalid recipient emails'), + ), + destinationEmailCc: z + .string() + .transform((value, ctx) => + validateEmails(value, ctx, 'Invalid CC emails'), + ) + .optional(), + replyTo: z.preprocess((value) => { + if (typeof value !== 'string') { + return value } - if (!parseS3Id(value)) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: `${value} is not a S3 ID.`, - }) - return z.NEVER + return value.trim() === '' ? undefined : value.trim() + }, z.string().email({ message: 'Invalid reply to email' }).optional()), + senderName: z.string().min(1, { message: 'Empty sender name' }).trim(), + attachments: z.array(z.string()).transform((array, context) => { + const result: string[] = [] + for (const value of array) { + // Account for optional attachment fields with no response. + if (!value) { + continue + } + if (!parseS3Id(value)) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value} is not a S3 ID.`, + }) + return z.NEVER + } + result.push(value) } - result.push(value) - } - return result - }), -}) + return result + }), + }) + .refine( + (data) => + data.destinationEmail.length + (data.destinationEmailCc?.length ?? 0) <= + 50, // Note: Postman limits a maximum of 50 recipients (including primary recipient and CC recipients) + { + message: + 'The total number of emails in Recipient email(s) and CC recipient email(s) must not exceed 50', + }, + ) From cd62329bf5f663a7b3df97826f3d6d4039a45bed Mon Sep 17 00:00:00 2001 From: Kevin Date: Wed, 18 Dec 2024 17:00:28 +0800 Subject: [PATCH 2/2] fix: limit cc recipients to 49 --- .../__tests__/common/parameters.test.ts | 8 +- .../src/apps/postman/common/parameters.ts | 100 +++++++++--------- 2 files changed, 52 insertions(+), 56 deletions(-) diff --git a/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts b/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts index ac5bad631..cab5374cd 100644 --- a/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts +++ b/packages/backend/src/apps/postman/__tests__/common/parameters.test.ts @@ -161,9 +161,9 @@ describe('postman transactional email schema zod validation', () => { ) it.each([ - { recipient: 10, cc: 41 }, - { recipient: 41, cc: 10 }, - { recipient: 50, cc: 1 }, + { recipient: 10, cc: 50 }, + { recipient: 41, cc: 51 }, + { recipient: 50, cc: 52 }, { recipient: 1, cc: 50 }, ])( 'should fail if total number of emails in Recipient email(s) and CC recipient email(s) exceeds 50', @@ -173,7 +173,7 @@ describe('postman transactional email schema zod validation', () => { const result = transactionalEmailSchema.safeParse(validPayload) assert(result.success === false) expect(result.error?.errors[0].message).toEqual( - 'The total number of emails in Recipient email(s) and CC recipient email(s) must not exceed 50', + 'The total number of CC recipient emails must not exceed 49', ) }, ) diff --git a/packages/backend/src/apps/postman/common/parameters.ts b/packages/backend/src/apps/postman/common/parameters.ts index 8918690f6..d6b6aa19e 100644 --- a/packages/backend/src/apps/postman/common/parameters.ts +++ b/packages/backend/src/apps/postman/common/parameters.ts @@ -87,57 +87,53 @@ export const transactionalEmailFields: IField[] = [ }, ] -export const transactionalEmailSchema = z - .object({ - subject: z.string().min(1, { message: 'Empty subject' }).trim(), - body: z - .string() - .min(1, { message: 'Empty body' }) - // for backward-compatibility with content produced by the old editor - .transform((v) => v.replace(/\n/g, '
')), - destinationEmail: z - .string() - .transform((value, ctx) => - validateEmails(value, ctx, 'Invalid recipient emails'), - ), - destinationEmailCc: z - .string() - .transform((value, ctx) => - validateEmails(value, ctx, 'Invalid CC emails'), - ) - .optional(), - replyTo: z.preprocess((value) => { - if (typeof value !== 'string') { - return value +export const transactionalEmailSchema = z.object({ + subject: z.string().min(1, { message: 'Empty subject' }).trim(), + body: z + .string() + .min(1, { message: 'Empty body' }) + // for backward-compatibility with content produced by the old editor + .transform((v) => v.replace(/\n/g, '
')), + destinationEmail: z + .string() + .transform((value, ctx) => + validateEmails(value, ctx, 'Invalid recipient emails'), + ), + destinationEmailCc: z + .string() + .transform((value, ctx) => validateEmails(value, ctx, 'Invalid CC emails')) + /** + * NOTE: Postman limits a maximum of 50 recipients (including primary recipient and CC recipients) + * Currently, Plumber sends emails to the main recipient individually, + * hence a limit of 49 CC recipients is enforced. + */ + .refine((value) => value.length <= 49, { + message: 'The total number of CC recipient emails must not exceed 49', + }) + .optional(), + replyTo: z.preprocess((value) => { + if (typeof value !== 'string') { + return value + } + return value.trim() === '' ? undefined : value.trim() + }, z.string().email({ message: 'Invalid reply to email' }).optional()), + senderName: z.string().min(1, { message: 'Empty sender name' }).trim(), + attachments: z.array(z.string()).transform((array, context) => { + const result: string[] = [] + for (const value of array) { + // Account for optional attachment fields with no response. + if (!value) { + continue } - return value.trim() === '' ? undefined : value.trim() - }, z.string().email({ message: 'Invalid reply to email' }).optional()), - senderName: z.string().min(1, { message: 'Empty sender name' }).trim(), - attachments: z.array(z.string()).transform((array, context) => { - const result: string[] = [] - for (const value of array) { - // Account for optional attachment fields with no response. - if (!value) { - continue - } - if (!parseS3Id(value)) { - context.addIssue({ - code: z.ZodIssueCode.custom, - message: `${value} is not a S3 ID.`, - }) - return z.NEVER - } - result.push(value) + if (!parseS3Id(value)) { + context.addIssue({ + code: z.ZodIssueCode.custom, + message: `${value} is not a S3 ID.`, + }) + return z.NEVER } - return result - }), - }) - .refine( - (data) => - data.destinationEmail.length + (data.destinationEmailCc?.length ?? 0) <= - 50, // Note: Postman limits a maximum of 50 recipients (including primary recipient and CC recipients) - { - message: - 'The total number of emails in Recipient email(s) and CC recipient email(s) must not exceed 50', - }, - ) + result.push(value) + } + return result + }), +})