From a93831b9adf8a25ba37161852fd03e64c7d5b33d Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Thu, 16 Apr 2026 17:23:22 +0800 Subject: [PATCH 1/5] chore: send email - fail fast if sender email is not set --- app/src/lib/email/sender.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/lib/email/sender.ts b/app/src/lib/email/sender.ts index 14d6991..6e98b2e 100644 --- a/app/src/lib/email/sender.ts +++ b/app/src/lib/email/sender.ts @@ -40,8 +40,14 @@ class EmailSender extends EmailTransport implements IEmailSender { throw new Error(EmailSchemaMessages.RECIPIENT_EMAIL_MAX) } + const from = transportOptions.auth?.user || process.env.GOOGLE_USER_EMAIL + + if (!from) { + throw new Error(TransportMessages.MISSING_SENDER_EMAIL) + } + return await this.transporter!.sendMail({ - from: transportOptions.auth?.user || process.env.GOOGLE_USER_EMAIL, + from, to: receivers, subject, ...(!isHtml && { text: content }), // Text email @@ -61,4 +67,8 @@ class EmailSender extends EmailTransport implements IEmailSender { } } +const TransportMessages: Record = { + MISSING_SENDER_EMAIL: 'Sender email address is not set. Ensure GOOGLE_USER_EMAIL is defined or pass it via oauth2 options.' +} + export default EmailSender From b709d6d1f232a184589c44b7798f41c885f25f6e Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Thu, 16 Apr 2026 17:30:57 +0800 Subject: [PATCH 2/5] fix: typos and set correct return types --- app/src/lib/email/transport.ts | 4 ++-- app/src/types/email.schema.ts | 4 ++-- app/src/types/schemavalidator.interface.ts | 4 ++-- app/src/types/sender.interface.ts | 5 +++-- app/src/types/transport.schema.ts | 2 +- 5 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/lib/email/transport.ts b/app/src/lib/email/transport.ts index 25b7e46..48479a9 100644 --- a/app/src/lib/email/transport.ts +++ b/app/src/lib/email/transport.ts @@ -39,7 +39,7 @@ class EmailTransport implements IEmailTransport { googleUserEmail: options?.googleUserEmail || process.env.GOOGLE_USER_EMAIL, googleClientId: options?.googleClientId || process.env.GOOGLE_CLIENT_ID, googleClientSecret: options?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET, - googleRereshToken: options?.googleRereshToken || process.env.GOOGLE_REFRESH_TOKEN, + googleRefreshToken: options?.googleRefreshToken || process.env.GOOGLE_REFRESH_TOKEN, } this.#schema?.validate({ data: inputData }) @@ -53,7 +53,7 @@ class EmailTransport implements IEmailTransport { user: inputData.googleUserEmail, clientId: inputData.googleClientId, clientSecret: inputData.googleClientSecret, - refreshToken: inputData.googleRereshToken, + refreshToken: inputData.googleRefreshToken, }, }) } catch (err: unknown) { diff --git a/app/src/types/email.schema.ts b/app/src/types/email.schema.ts index c358028..f7825b2 100644 --- a/app/src/types/email.schema.ts +++ b/app/src/types/email.schema.ts @@ -19,7 +19,7 @@ interface IOptionalParams { * Base schema for email validation within the `EmailTransport.sendEmail()` method * @property {string} [recipient] - (Optional) Email address of a recipient that will receive an email. Required if `recipients[]` is undefined. * @property {string[]} [recipients] - (Optional) Array of one or more email addresses of recipients that will receive an email. Required if `recipient` is undefined. - * @property {string} subject Email message title (max 100 characters) + * @property {string} subject Email message title (max 200 characters) * @property {string} content Email message content can be a simple text or HTML string (max 1500 characters) */ export const BaseEmailSchema = z.object({ @@ -57,7 +57,7 @@ export const EmailSchema = BaseEmailSchema.refine( * @typedef {object} EmailSenderType * @property {string} [recipient] - (Optional) Email address of a recipient that will receive an email. Required if `recipients[]` is undefined. * @property {string[]} [recipients] - (Optional) One (1) or more comma-separated email addresses of recipients that will receive an email. Required if `recipient` is undefined. - * @property {string} subject - Email message title (max 100 characters) + * @property {string} subject - Email message title (max 200 characters) * @property {string} content - Email message content can can be a simple text or HTML string (max 1500 characters) * @property {boolean} isHtml - Flag indicating if the `content` field is in HTML format. Defaults to `false`. */ diff --git a/app/src/types/schemavalidator.interface.ts b/app/src/types/schemavalidator.interface.ts index 6e64128..de8c518 100644 --- a/app/src/types/schemavalidator.interface.ts +++ b/app/src/types/schemavalidator.interface.ts @@ -61,9 +61,9 @@ export interface ISchemaValidator { /** * @description Get the base `ZodObject` schema from a `ZodEffects` schema * @param {ZodSchemaType} schema - The `ZodEffects` schema to get the `ZodObject` base schema from - * @returns {ZodObjectBasicType | null} The base schema or null if it's not a ZodEffects + * @returns {ZodObjectBasicType} The base schema */ - getBaseSchema (schema: ZodSchemaType): ZodObjectBasicType | null; + getBaseSchema (schema: ZodSchemaType): ZodObjectBasicType; /** * Retrieves only the base `ZodObject` subset from `this.schema` using `.pick()`. Finds the base `ZodObject` if `this.schema` is a `ZodEffects` schema. diff --git a/app/src/types/sender.interface.ts b/app/src/types/sender.interface.ts index f1461c7..713acd0 100644 --- a/app/src/types/sender.interface.ts +++ b/app/src/types/sender.interface.ts @@ -1,14 +1,15 @@ import type { EmailType } from '@/types/email.schema.js' +import type { SentMessageInfo } from '@/types/transport.types.js' import SchemaValidator from '@/lib/validator/schemavalidator.js' export interface IEmailSender { /** * Sends an email using Gmail SMTP and Google OAuth2 * @param {EmailType} params Input parameters for sending email - * @returns {Promise} Resolved Promise that sent the email + * @returns {Promise} Resolved Promise with Nodemailer send result * @throws {ZodIssue[]} Input parameter validation error/s */ - sendEmail (params: EmailType): Promise; + sendEmail (params: EmailType): Promise; /** * Retrieves the email sender's local zod schema wrapper diff --git a/app/src/types/transport.schema.ts b/app/src/types/transport.schema.ts index 53e4d52..ce48d62 100644 --- a/app/src/types/transport.schema.ts +++ b/app/src/types/transport.schema.ts @@ -8,7 +8,7 @@ export const TransportOath2Schema = z.object({ googleUserEmail: z.string().email().max(150), googleClientId: z.string().max(200), googleClientSecret: z.string().max(200), - googleRereshToken: z.string().max(500), + googleRefreshToken: z.string().max(500), }) export type TransportOath2SchemaType = z.infer; From de51f45e936673399022a6177b9b95894dc5dea4 Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Thu, 16 Apr 2026 17:34:48 +0800 Subject: [PATCH 3/5] chore: remove try-catch block --- app/src/scripts/cli/lib/handleHtml.ts | 63 ++++++++++++--------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/app/src/scripts/cli/lib/handleHtml.ts b/app/src/scripts/cli/lib/handleHtml.ts index 2f28d51..81720ca 100644 --- a/app/src/scripts/cli/lib/handleHtml.ts +++ b/app/src/scripts/cli/lib/handleHtml.ts @@ -10,42 +10,35 @@ export const handleSendHtmlEmail = async (options: EmailHtmlOptions) => { throw new Error('GOOGLE_USER_EMAIL .env variable is required') } - try { - const { - subject, - content: paragraphs = [], - recipients = [], - wysiwyg = null, - } = options - - if (paragraphs.length === 0 && typeof wysiwyg !== 'string') { - throw new Error('One of content or wysiwyg is required') - } - - // Clean data of whitespace - const emails = recipients.map(email => email.trim()) - - console.log(`Sending email to (${emails.length}) recipients`) - - const emailContent = await buildHtml({ - content: paragraphs, - recipients: emails, - sender: process.env.GOOGLE_USER_EMAIL, - wysiwyg, - }) - - await send({ - subject, - content: emailContent, - recipients: emails, - isHtml: true, - }) - } catch (err: unknown) { - if (err instanceof Error) { - console.log('[ERROR]: handle HTML email') - throw err - } + const { + subject, + content: paragraphs = [], + recipients = [], + wysiwyg = null, + } = options + + if (paragraphs.length === 0 && typeof wysiwyg !== 'string') { + throw new Error('One of content or wysiwyg is required') } + // Clean data of whitespace + const emails = recipients.map(email => email.trim()) + + console.log(`Sending email to (${emails.length}) recipients`) + + const emailContent = await buildHtml({ + content: paragraphs, + recipients: emails, + sender: process.env.GOOGLE_USER_EMAIL, + wysiwyg, + }) + + await send({ + subject, + content: emailContent, + recipients: emails, + isHtml: true, + }) + console.log('Process success') } From 6f3d2d2c9ae9b6c937774be93d230621483b9461 Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Thu, 16 Apr 2026 17:37:19 +0800 Subject: [PATCH 4/5] fix: lint error --- app/src/lib/email/sender.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/lib/email/sender.ts b/app/src/lib/email/sender.ts index 6e98b2e..f25ae56 100644 --- a/app/src/lib/email/sender.ts +++ b/app/src/lib/email/sender.ts @@ -68,7 +68,7 @@ class EmailSender extends EmailTransport implements IEmailSender { } const TransportMessages: Record = { - MISSING_SENDER_EMAIL: 'Sender email address is not set. Ensure GOOGLE_USER_EMAIL is defined or pass it via oauth2 options.' + MISSING_SENDER_EMAIL: 'Sender email address is not set. Ensure GOOGLE_USER_EMAIL is defined or pass it via oauth2 options.', } export default EmailSender From 507c8862289bcce33e44afa32a380ea00cf2871d Mon Sep 17 00:00:00 2001 From: weaponsforge Date: Thu, 16 Apr 2026 17:45:17 +0800 Subject: [PATCH 5/5] fix: typo error --- app/src/__tests__/send.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/__tests__/send.test.ts b/app/src/__tests__/send.test.ts index 780502f..c502922 100644 --- a/app/src/__tests__/send.test.ts +++ b/app/src/__tests__/send.test.ts @@ -10,7 +10,7 @@ describe('Send email test', () => { googleUserEmail: process.env.GOOGLE_USER_EMAIL || '', googleClientId: process.env.GOOGLE_CLIENT_ID || '', googleClientSecret: process.env.GOOGLE_CLIENT_SECRET || '', - googleRereshToken: process.env.GOOGLE_REFRESH_TOKEN || '', + googleRefreshToken: process.env.GOOGLE_REFRESH_TOKEN || '', } await expect(