From abc6f28fea3f57fe4fd5433225c777c8fb05e85e Mon Sep 17 00:00:00 2001 From: Nicolas Marino Date: Thu, 8 Jan 2026 23:07:39 -0500 Subject: [PATCH 1/5] upgrade nodemailer version and added some minor refactor. --- package.json | 6 ++--- src/transports/brevo.ts | 18 +++++++------ src/transports/mailgun.ts | 20 +++++++------- src/transports/resend.ts | 16 ++++++----- src/transports/sparkpost.ts | 22 ++++++++------- src/utils.ts | 22 +++++++++++++++ tests/integration/transports/resend.spec.ts | 30 +++++++++++++++++++++ 7 files changed, 97 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 02baeae..51d28e3 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "edge.js": "^6.3.0", "eslint": "^9.38.0", "luxon": "^3.7.2", - "mjml": "^4.16.1", + "mjml": "^4.18.0", "prettier": "^3.6.2", "release-it": "^19.0.5", "sinon": "^21.0.0", @@ -82,11 +82,11 @@ "dependencies": { "@poppinss/macroable": "^1.1.0", "@poppinss/object-builder": "^1.1.0", - "@types/nodemailer": "^7.0.3", + "@types/nodemailer": "^7.0.4", "fastq": "^1.19.1", "ical-generator": "^9.0.0", "ky": "^1.13.0", - "nodemailer": "^7.0.10" + "nodemailer": "^7.0.11" }, "peerDependencies": { "@adonisjs/assembler": "^8.0.0-next.14", diff --git a/src/transports/brevo.ts b/src/transports/brevo.ts index 67cb8b9..8a3caf1 100644 --- a/src/transports/brevo.ts +++ b/src/transports/brevo.ts @@ -8,13 +8,14 @@ */ import ky from 'ky' -import { type Transport, createTransport } from 'nodemailer' +import { createTransport, type Transport } from 'nodemailer' import type { Address } from 'nodemailer/lib/mailer/index.js' import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' +import { validateConfig, normalizeBaseUrl } from '../utils.js' import type { BrevoConfig, NodeMailerMessage, @@ -116,21 +117,22 @@ class NodeMailerTransport implements Transport { * Returns base url for sending emails */ #getBaseUrl(): string { - return this.#config.baseUrl.replace(/\/$/, '') + return normalizeBaseUrl(this.#config.baseUrl) } /** * Send mail */ async send(mail: MailMessage, callback: (err: Error | null, info: BrevoSentMessageInfo) => void) { - const url = `${this.#getBaseUrl()}/smtp/email` - const envelope = mail.message.getEnvelope() - const payload = this.#preparePayload(mail) + try { + validateConfig('Brevo', this.#config) + const url = `${this.#getBaseUrl()}/smtp/email` + const envelope = mail.message.getEnvelope() + const payload = this.#preparePayload(mail) - debug('brevo email url %s', url) - debug('brevo email payload %O', payload) + debug('brevo email url %s', url) + debug('brevo email payload %O', payload) - try { const response = await ky.post<{ messageId: string }>(url, { headers: { 'Accept': 'application/json', diff --git a/src/transports/mailgun.ts b/src/transports/mailgun.ts index 5c1440b..e09fa23 100644 --- a/src/transports/mailgun.ts +++ b/src/transports/mailgun.ts @@ -17,6 +17,7 @@ import debug from '../debug.js' import { streamToBlob } from '../utils.js' import { MailResponse } from '../mail_response.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' +import { validateConfig, normalizeBaseUrl } from '../utils.js' import type { MailgunConfig, NodeMailerMessage, @@ -69,9 +70,9 @@ class NodeMailerTransport implements Transport { * Returns base url for sending emails */ #getBaseUrl(): string { - return this.#config.domain - ? `${this.#config.baseUrl.replace(/\/$/, '')}/${this.#config.domain}` - : this.#config.baseUrl.replace(/\/$/, '') + const baseUrl = normalizeBaseUrl(this.#config.baseUrl) + + return this.#config.domain ? `${baseUrl}/${this.#config.domain}` : baseUrl } /** @@ -185,14 +186,15 @@ class NodeMailerTransport implements Transport { mail: MailMessage, callback: (err: Error | null, info: MailgunSentMessageInfo) => void ) { - const envelope = mail.message.getEnvelope() - const url = `${this.#getBaseUrl()}/messages.mime` - const form = await this.#createFormData(mail) + try { + validateConfig('Mailgun', this.#config) + const envelope = mail.message.getEnvelope() + const url = `${this.#getBaseUrl()}/messages.mime` + const form = await this.#createFormData(mail) - debug('mailgun mail url %s', url) - debug('mailgun mail envelope %s', envelope) + debug('mailgun mail url %s', url) + debug('mailgun mail envelope %s', envelope) - try { const response = await ky.post<{ id: string }>(url, { headers: { Accept: 'application/json', diff --git a/src/transports/resend.ts b/src/transports/resend.ts index deb1a25..07d6aab 100644 --- a/src/transports/resend.ts +++ b/src/transports/resend.ts @@ -14,6 +14,7 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' +import { validateConfig, normalizeBaseUrl } from '../utils.js' import type { ResendConfig, NodeMailerMessage, @@ -129,7 +130,7 @@ class NodeMailerTransport implements Transport { * Returns the normalized base URL for the API */ #getBaseUrl() { - return this.#config.baseUrl.replace(/\/$/, '') + return normalizeBaseUrl(this.#config.baseUrl) } /** @@ -139,14 +140,15 @@ class NodeMailerTransport implements Transport { mail: MailMessage, callback: (err: Error | null, info: ResendSentMessageInfo) => void ) { - const url = `${this.#getBaseUrl()}/emails` - const envelope = mail.message.getEnvelope() - const payload = this.#preparePayload(mail) + try { + validateConfig('Resend', this.#config) + const url = `${this.#getBaseUrl()}/emails` + const envelope = mail.message.getEnvelope() + const payload = this.#preparePayload(mail) - debug('resend mail url "%s"', url) - debug('resend mail payload %O', payload) + debug('resend mail url "%s"', url) + debug('resend mail payload %O', payload) - try { const response = await ky.post<{ id: string }>(url, { json: payload, headers: { diff --git a/src/transports/sparkpost.ts b/src/transports/sparkpost.ts index 2d0aa29..df1aeb9 100644 --- a/src/transports/sparkpost.ts +++ b/src/transports/sparkpost.ts @@ -16,6 +16,7 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' +import { validateConfig, normalizeBaseUrl } from '../utils.js' import type { SparkPostConfig, NodeMailerMessage, @@ -41,7 +42,7 @@ class NodeMailerTransport implements Transport { * Returns base url for sending emails */ #getBaseUrl(): string { - return this.#config.baseUrl.replace(/\/$/, '') + return normalizeBaseUrl(this.#config.baseUrl) } /** @@ -132,17 +133,18 @@ class NodeMailerTransport implements Transport { mail: MailMessage, callback: (err: Error | null, info: SparkPostSentMessageInfo) => void ) { - const url = `${this.#getBaseUrl()}/transmissions` - const options = this.#getOptions(this.#config) - const envelope = mail.message.getEnvelope() - const recipients = this.#getRecipients(mail) + try { + validateConfig('SparkPost', this.#config) + const url = `${this.#getBaseUrl()}/transmissions` + const options = this.#getOptions(this.#config) + const envelope = mail.message.getEnvelope() + const recipients = this.#getRecipients(mail) - debug('sparkpost mail url "%s"', url) - debug('sparkpost mail options %O', options) - debug('sparkpost mail envelope %O', envelope) - debug('sparkpost mail recipients %O', recipients) + debug('sparkpost mail url "%s"', url) + debug('sparkpost mail options %O', options) + debug('sparkpost mail envelope %O', envelope) + debug('sparkpost mail recipients %O', recipients) - try { /** * The sparkpost API doesn't accept the multipart stream and hence we * need to convert the stream to a string diff --git a/src/utils.ts b/src/utils.ts index 6ca63bb..8158d8c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,6 +7,8 @@ * file that was distributed with this source code. */ +import { RuntimeException } from '@adonisjs/core/exceptions' + /** * Convert a stream to a blob */ @@ -20,3 +22,23 @@ export function streamToBlob(stream: NodeJS.ReadableStream, mimeType: string) { .once('error', reject) }) } + +/** + * Validates the transport configuration + */ +export function validateConfig(transportName: string, config: { key: string; baseUrl: string }) { + if (!config.key) { + throw new RuntimeException(`${transportName} transport: "key" is not defined`) + } + + if (!config.baseUrl) { + throw new RuntimeException(`${transportName} transport: "baseUrl" is not defined`) + } +} + +/** + * Returns the normalized base URL for the API + */ +export function normalizeBaseUrl(baseUrl: string) { + return baseUrl.replace(/\/$/, '') +} diff --git a/tests/integration/transports/resend.spec.ts b/tests/integration/transports/resend.spec.ts index f93be9a..4bf2aaa 100644 --- a/tests/integration/transports/resend.spec.ts +++ b/tests/integration/transports/resend.spec.ts @@ -63,4 +63,34 @@ test.group('Resend Transport', () => { assert.deepEqual(email.body.from, process.env.RESEND_FROM_EMAIL) assert.deepEqual(email.body.subject, 'Adonisv6') }) + + test('throw error when key is missing', async ({ assert }) => { + const resend = new ResendTransport({ + baseUrl: process.env.RESEND_BASE_URL!, + } as any) + + const message = new Message() + message.from(process.env.RESEND_FROM_EMAIL!) + message.to(process.env.RESEND_TO_EMAIL!) + + await assert.rejects( + () => resend.send(message.toJSON().message), + 'Unable to send email using the resend transport' + ) + }) + + test('throw error when baseUrl is missing', async ({ assert }) => { + const resend = new ResendTransport({ + key: '123', + } as any) + + const message = new Message() + message.from(process.env.RESEND_FROM_EMAIL!) + message.to(process.env.RESEND_TO_EMAIL!) + + await assert.rejects( + () => resend.send(message.toJSON().message), + 'Unable to send email using the resend transport' + ) + }) }) From 819ef7317e3247f85d18c3381360e8c02a89e429 Mon Sep 17 00:00:00 2001 From: Nicolas Marino Date: Thu, 8 Jan 2026 23:18:16 -0500 Subject: [PATCH 2/5] fixed test env variable --- tests/integration/transports/resend.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/transports/resend.spec.ts b/tests/integration/transports/resend.spec.ts index 4bf2aaa..6c72f29 100644 --- a/tests/integration/transports/resend.spec.ts +++ b/tests/integration/transports/resend.spec.ts @@ -75,13 +75,13 @@ test.group('Resend Transport', () => { await assert.rejects( () => resend.send(message.toJSON().message), - 'Unable to send email using the resend transport' + 'Resend transport: "key" is not defined' ) }) test('throw error when baseUrl is missing', async ({ assert }) => { const resend = new ResendTransport({ - key: '123', + key: process.env.RESEND_API_KEY!, } as any) const message = new Message() @@ -90,7 +90,7 @@ test.group('Resend Transport', () => { await assert.rejects( () => resend.send(message.toJSON().message), - 'Unable to send email using the resend transport' + 'Resend transport: "baseUrl" is not defined' ) }) }) From 381ba5fcf79e2e54ea9f6587b9c3a21f22fe5a36 Mon Sep 17 00:00:00 2001 From: Nicolas Marino Date: Sat, 10 Jan 2026 10:21:51 -0500 Subject: [PATCH 3/5] feat: validate mail transport config during instantiation --- src/errors.ts | 9 +++++++++ src/transports/brevo.ts | 3 ++- src/transports/mailgun.ts | 11 ++++++----- src/transports/resend.ts | 3 ++- src/transports/sparkpost.ts | 3 ++- src/utils.ts | 32 ++++++++++++++++++++------------ tests/unit/define_config.spec.ts | 28 ++++++++++++++++++++++------ 7 files changed, 63 insertions(+), 26 deletions(-) diff --git a/src/errors.ts b/src/errors.ts index 829b2b1..b248885 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -9,6 +9,15 @@ import { Exception } from '@adonisjs/core/exceptions' +/** + * The error is raised when one or more configuration + * properties are missing or invalid + */ +export const E_INVALID_CONFIG = class InvalidConfigException extends Exception { + static status = 500 + static code = 'E_INVALID_CONFIG' +} + /** * The error is raised when the transport is unable to * send the email diff --git a/src/transports/brevo.ts b/src/transports/brevo.ts index 8a3caf1..2951ede 100644 --- a/src/transports/brevo.ts +++ b/src/transports/brevo.ts @@ -14,8 +14,8 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' -import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import { validateConfig, normalizeBaseUrl } from '../utils.js' +import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { BrevoConfig, NodeMailerMessage, @@ -167,6 +167,7 @@ export class BrevoTransport implements MailTransportContract { #config: BrevoConfig constructor(config: BrevoConfig) { + validateConfig('brevo', config) this.#config = config } diff --git a/src/transports/mailgun.ts b/src/transports/mailgun.ts index e09fa23..fa63ca9 100644 --- a/src/transports/mailgun.ts +++ b/src/transports/mailgun.ts @@ -14,10 +14,10 @@ import { type Transport, createTransport } from 'nodemailer' import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' -import { streamToBlob } from '../utils.js' +import { streamToBlob, validateConfig, normalizeBaseUrl } from '../utils.js' import { MailResponse } from '../mail_response.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' -import { validateConfig, normalizeBaseUrl } from '../utils.js' + import type { MailgunConfig, NodeMailerMessage, @@ -70,9 +70,9 @@ class NodeMailerTransport implements Transport { * Returns base url for sending emails */ #getBaseUrl(): string { - const baseUrl = normalizeBaseUrl(this.#config.baseUrl) - - return this.#config.domain ? `${baseUrl}/${this.#config.domain}` : baseUrl + return this.#config.domain + ? `${normalizeBaseUrl(this.#config.baseUrl)}/${this.#config.domain}` + : normalizeBaseUrl(this.#config.baseUrl) } /** @@ -229,6 +229,7 @@ export class MailgunTransport implements MailTransportContract { #config: MailgunConfig constructor(config: MailgunConfig) { + validateConfig('mailgun', config) this.#config = config } diff --git a/src/transports/resend.ts b/src/transports/resend.ts index 07d6aab..922ff8c 100644 --- a/src/transports/resend.ts +++ b/src/transports/resend.ts @@ -13,8 +13,8 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' -import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import { validateConfig, normalizeBaseUrl } from '../utils.js' +import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { ResendConfig, NodeMailerMessage, @@ -183,6 +183,7 @@ export class ResendTransport implements MailTransportContract { #config: ResendConfig constructor(config: ResendConfig) { + validateConfig('resend', config) this.#config = config } diff --git a/src/transports/sparkpost.ts b/src/transports/sparkpost.ts index df1aeb9..4878149 100644 --- a/src/transports/sparkpost.ts +++ b/src/transports/sparkpost.ts @@ -15,8 +15,8 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' -import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import { validateConfig, normalizeBaseUrl } from '../utils.js' +import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { SparkPostConfig, NodeMailerMessage, @@ -187,6 +187,7 @@ export class SparkPostTransport implements MailTransportContract { #config: SparkPostConfig constructor(config: SparkPostConfig) { + validateConfig('sparkpost', config) this.#config = config } diff --git a/src/utils.ts b/src/utils.ts index 8158d8c..d6ee9a7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { RuntimeException } from '@adonisjs/core/exceptions' +import { E_INVALID_CONFIG } from './errors.js' /** * Convert a stream to a blob @@ -24,21 +24,29 @@ export function streamToBlob(stream: NodeJS.ReadableStream, mimeType: string) { } /** - * Validates the transport configuration + * Normalize the base URL by removing the trailing slash */ -export function validateConfig(transportName: string, config: { key: string; baseUrl: string }) { +export function normalizeBaseUrl(baseUrl: string) { + return baseUrl.replace(/\/$/, '') +} + +/** + * Validate transport config to ensure the "key" and + * "baseUrl" are present. + */ +export function validateConfig( + transportName: string, + config: { key: string; baseUrl: string | undefined } +) { if (!config.key) { - throw new RuntimeException(`${transportName} transport: "key" is not defined`) + throw new E_INVALID_CONFIG( + `Invalid config for "${transportName}" transport. The "key" property is missing` + ) } if (!config.baseUrl) { - throw new RuntimeException(`${transportName} transport: "baseUrl" is not defined`) + throw new E_INVALID_CONFIG( + `Invalid config for "${transportName}" transport. The "baseUrl" property is missing` + ) } } - -/** - * Returns the normalized base URL for the API - */ -export function normalizeBaseUrl(baseUrl: string) { - return baseUrl.replace(/\/$/, '') -} diff --git a/tests/unit/define_config.spec.ts b/tests/unit/define_config.spec.ts index 76ad04a..6d694ea 100644 --- a/tests/unit/define_config.spec.ts +++ b/tests/unit/define_config.spec.ts @@ -40,17 +40,17 @@ test.group('Define config', () => { assert.instanceOf(sesFactory(), SESTransport) expectTypeOf(sesFactory()).toMatchTypeOf() - const mailgunProvider = transports.mailgun({ key: '', baseUrl: '', domain: '' }) + const mailgunProvider = transports.mailgun({ key: 'dummy', baseUrl: 'dummy', domain: 'dummy' }) const mailgunFactory = await mailgunProvider.resolver(app) assert.instanceOf(mailgunFactory(), MailgunTransport) expectTypeOf(mailgunFactory()).toMatchTypeOf() - const sparkpostProvider = transports.sparkpost({ key: '', baseUrl: '' }) + const sparkpostProvider = transports.sparkpost({ key: 'dummy', baseUrl: 'dummy' }) const sparkpostFactory = await sparkpostProvider.resolver(app) assert.instanceOf(sparkpostFactory(), SparkPostTransport) expectTypeOf(sparkpostFactory()).toMatchTypeOf() - const resendProvider = transports.resend({ key: '', baseUrl: '' }) + const resendProvider = transports.resend({ key: 'dummy', baseUrl: 'dummy' }) const resendFactory = await resendProvider.resolver(app) assert.instanceOf(resendFactory(), ResendTransport) expectTypeOf(resendFactory()).toMatchTypeOf() @@ -68,9 +68,9 @@ test.group('Define config', () => { secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, }, }), - mailgun: transports.mailgun({ key: '', baseUrl: '', domain: '' }), - sparkpost: transports.sparkpost({ key: '', baseUrl: '' }), - resend: transports.resend({ key: '', baseUrl: '' }), + mailgun: transports.mailgun({ key: 'dummy', baseUrl: 'dummy', domain: 'dummy' }), + sparkpost: transports.sparkpost({ key: 'dummy', baseUrl: 'dummy' }), + resend: transports.resend({ key: 'dummy', baseUrl: 'dummy' }), }, }) @@ -90,4 +90,20 @@ test.group('Define config', () => { assert.instanceOf(mail.use('sparkpost').transport, SparkPostTransport) assert.instanceOf(mail.use('resend').transport, ResendTransport) }) + + test('throw error when key is missing', async ({ assert }) => { + assert.throws( + // @ts-expect-error + () => new ResendTransport({ baseUrl: 'dummy' }), + 'Invalid config for "resend" transport. The "key" property is missing' + ) + }) + + test('throw error when baseUrl is missing', async ({ assert }) => { + assert.throws( + // @ts-expect-error + () => new ResendTransport({ key: 'dummy' }), + 'Invalid config for "resend" transport. The "baseUrl" property is missing' + ) + }) }) From 07725cc06044884a498a953282852a311e0b6a62 Mon Sep 17 00:00:00 2001 From: Nicolas Marino Date: Sat, 10 Jan 2026 10:33:42 -0500 Subject: [PATCH 4/5] feat: removed validate from send --- src/transports/brevo.ts | 2 +- src/transports/mailgun.ts | 1 - src/transports/resend.ts | 2 +- src/transports/sparkpost.ts | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/transports/brevo.ts b/src/transports/brevo.ts index 2951ede..724c981 100644 --- a/src/transports/brevo.ts +++ b/src/transports/brevo.ts @@ -16,6 +16,7 @@ import debug from '../debug.js' import { MailResponse } from '../mail_response.js' import { validateConfig, normalizeBaseUrl } from '../utils.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' + import type { BrevoConfig, NodeMailerMessage, @@ -125,7 +126,6 @@ class NodeMailerTransport implements Transport { */ async send(mail: MailMessage, callback: (err: Error | null, info: BrevoSentMessageInfo) => void) { try { - validateConfig('Brevo', this.#config) const url = `${this.#getBaseUrl()}/smtp/email` const envelope = mail.message.getEnvelope() const payload = this.#preparePayload(mail) diff --git a/src/transports/mailgun.ts b/src/transports/mailgun.ts index fa63ca9..fce2080 100644 --- a/src/transports/mailgun.ts +++ b/src/transports/mailgun.ts @@ -187,7 +187,6 @@ class NodeMailerTransport implements Transport { callback: (err: Error | null, info: MailgunSentMessageInfo) => void ) { try { - validateConfig('Mailgun', this.#config) const envelope = mail.message.getEnvelope() const url = `${this.#getBaseUrl()}/messages.mime` const form = await this.#createFormData(mail) diff --git a/src/transports/resend.ts b/src/transports/resend.ts index 922ff8c..29f3bbf 100644 --- a/src/transports/resend.ts +++ b/src/transports/resend.ts @@ -15,6 +15,7 @@ import debug from '../debug.js' import { MailResponse } from '../mail_response.js' import { validateConfig, normalizeBaseUrl } from '../utils.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' + import type { ResendConfig, NodeMailerMessage, @@ -141,7 +142,6 @@ class NodeMailerTransport implements Transport { callback: (err: Error | null, info: ResendSentMessageInfo) => void ) { try { - validateConfig('Resend', this.#config) const url = `${this.#getBaseUrl()}/emails` const envelope = mail.message.getEnvelope() const payload = this.#preparePayload(mail) diff --git a/src/transports/sparkpost.ts b/src/transports/sparkpost.ts index 4878149..cbadd9d 100644 --- a/src/transports/sparkpost.ts +++ b/src/transports/sparkpost.ts @@ -17,6 +17,7 @@ import debug from '../debug.js' import { MailResponse } from '../mail_response.js' import { validateConfig, normalizeBaseUrl } from '../utils.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' + import type { SparkPostConfig, NodeMailerMessage, @@ -134,7 +135,6 @@ class NodeMailerTransport implements Transport { callback: (err: Error | null, info: SparkPostSentMessageInfo) => void ) { try { - validateConfig('SparkPost', this.#config) const url = `${this.#getBaseUrl()}/transmissions` const options = this.#getOptions(this.#config) const envelope = mail.message.getEnvelope() From 1a5acb89a98e1f1f9ec056edc19750f4b21e6f23 Mon Sep 17 00:00:00 2001 From: Nicolas Marino Date: Sat, 10 Jan 2026 15:13:03 -0500 Subject: [PATCH 5/5] refactor: introduce BaseApiTransport to centralize config validation --- src/transports/base_api_transport.ts | 29 ++++++++++++++++++++++++++++ src/transports/brevo.ts | 13 +++++-------- src/transports/mailgun.ts | 13 +++++-------- src/transports/resend.ts | 13 +++++-------- src/transports/sparkpost.ts | 13 +++++-------- 5 files changed, 49 insertions(+), 32 deletions(-) create mode 100644 src/transports/base_api_transport.ts diff --git a/src/transports/base_api_transport.ts b/src/transports/base_api_transport.ts new file mode 100644 index 0000000..34c6b2b --- /dev/null +++ b/src/transports/base_api_transport.ts @@ -0,0 +1,29 @@ +/* + * @adonisjs/mail + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { validateConfig } from '../utils.js' +import type { MailResponse } from '../mail_response.js' +import type { NodeMailerMessage, MailTransportContract } from '../types.js' + +/** + * Base class for HTTP based transports to validate the config + * and normalize the base URL + */ +export abstract class BaseApiTransport< + Config extends { key: string; baseUrl: string }, +> implements MailTransportContract { + constructor( + protected name: string, + protected config: Config + ) { + validateConfig(name, config) + } + + abstract send(message: NodeMailerMessage, config?: any): Promise> +} diff --git a/src/transports/brevo.ts b/src/transports/brevo.ts index 724c981..219c4fe 100644 --- a/src/transports/brevo.ts +++ b/src/transports/brevo.ts @@ -14,7 +14,8 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' -import { validateConfig, normalizeBaseUrl } from '../utils.js' +import { BaseApiTransport } from './base_api_transport.js' +import { normalizeBaseUrl } from '../utils.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { @@ -22,7 +23,6 @@ import type { NodeMailerMessage, BrevoRuntimeConfig, BrevoSentMessageInfo, - MailTransportContract, } from '../types.js' /** @@ -163,12 +163,9 @@ class NodeMailerTransport implements Transport { /** * Transport for sending emails using the Brevo `/emails/send` API. */ -export class BrevoTransport implements MailTransportContract { - #config: BrevoConfig - +export class BrevoTransport extends BaseApiTransport { constructor(config: BrevoConfig) { - validateConfig('brevo', config) - this.#config = config + super('brevo', config) } /** @@ -178,7 +175,7 @@ export class BrevoTransport implements MailTransportContract { message: NodeMailerMessage, config?: BrevoRuntimeConfig ): Promise> { - const brevoTransport = new NodeMailerTransport({ ...this.#config, ...config }) + const brevoTransport = new NodeMailerTransport({ ...this.config, ...config }) const transporter = createTransport(brevoTransport) const brevoResponse = await transporter.sendMail(message) diff --git a/src/transports/mailgun.ts b/src/transports/mailgun.ts index fce2080..8768731 100644 --- a/src/transports/mailgun.ts +++ b/src/transports/mailgun.ts @@ -14,15 +14,15 @@ import { type Transport, createTransport } from 'nodemailer' import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' -import { streamToBlob, validateConfig, normalizeBaseUrl } from '../utils.js' +import { streamToBlob, normalizeBaseUrl } from '../utils.js' import { MailResponse } from '../mail_response.js' +import { BaseApiTransport } from './base_api_transport.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { MailgunConfig, NodeMailerMessage, MailgunRuntimeConfig, - MailTransportContract, MailgunSentMessageInfo, } from '../types.js' @@ -224,12 +224,9 @@ class NodeMailerTransport implements Transport { * AdonisJS Mail transport for sending emails using the * Mailgun's `/messages.mime` API endpoint. */ -export class MailgunTransport implements MailTransportContract { - #config: MailgunConfig - +export class MailgunTransport extends BaseApiTransport { constructor(config: MailgunConfig) { - validateConfig('mailgun', config) - this.#config = config + super('mailgun', config) } /** @@ -239,7 +236,7 @@ export class MailgunTransport implements MailTransportContract { message: NodeMailerMessage, config?: MailgunRuntimeConfig ): Promise> { - const mailgunTransport = new NodeMailerTransport({ ...this.#config, ...config }) + const mailgunTransport = new NodeMailerTransport({ ...this.config, ...config }) const transporter = createTransport(mailgunTransport) const mailgunResponse = await transporter.sendMail(message) diff --git a/src/transports/resend.ts b/src/transports/resend.ts index 29f3bbf..59f26ed 100644 --- a/src/transports/resend.ts +++ b/src/transports/resend.ts @@ -13,13 +13,13 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' -import { validateConfig, normalizeBaseUrl } from '../utils.js' +import { BaseApiTransport } from './base_api_transport.js' +import { normalizeBaseUrl } from '../utils.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { ResendConfig, NodeMailerMessage, - MailTransportContract, ResendRuntimeConfig, ResendSentMessageInfo, } from '../types.js' @@ -179,12 +179,9 @@ class NodeMailerTransport implements Transport { /** * Transport for sending using the Resend `/emails` API. */ -export class ResendTransport implements MailTransportContract { - #config: ResendConfig - +export class ResendTransport extends BaseApiTransport { constructor(config: ResendConfig) { - validateConfig('resend', config) - this.#config = config + super('resend', config) } /** @@ -194,7 +191,7 @@ export class ResendTransport implements MailTransportContract { message: NodeMailerMessage, config?: ResendRuntimeConfig ): Promise> { - const resendTransport = new NodeMailerTransport({ ...this.#config, ...config }) + const resendTransport = new NodeMailerTransport({ ...this.config, ...config }) const transporter = createTransport(resendTransport) const resendResponse = await transporter.sendMail(message) diff --git a/src/transports/sparkpost.ts b/src/transports/sparkpost.ts index cbadd9d..17221a1 100644 --- a/src/transports/sparkpost.ts +++ b/src/transports/sparkpost.ts @@ -15,13 +15,13 @@ import type MailMessage from 'nodemailer/lib/mailer/mail-message.js' import debug from '../debug.js' import { MailResponse } from '../mail_response.js' -import { validateConfig, normalizeBaseUrl } from '../utils.js' +import { BaseApiTransport } from './base_api_transport.js' +import { normalizeBaseUrl } from '../utils.js' import { E_MAIL_TRANSPORT_ERROR } from '../errors.js' import type { SparkPostConfig, NodeMailerMessage, - MailTransportContract, SparkPostRuntimeConfig, SparkPostSentMessageInfo, } from '../types.js' @@ -183,12 +183,9 @@ class NodeMailerTransport implements Transport { * AdonisJS mail transport implementation to send emails * using Sparkpost's `/message.mime` API endpoint. */ -export class SparkPostTransport implements MailTransportContract { - #config: SparkPostConfig - +export class SparkPostTransport extends BaseApiTransport { constructor(config: SparkPostConfig) { - validateConfig('sparkpost', config) - this.#config = config + super('sparkpost', config) } /** @@ -198,7 +195,7 @@ export class SparkPostTransport implements MailTransportContract { message: NodeMailerMessage, config?: SparkPostRuntimeConfig ): Promise> { - const nodemailerTransport = new NodeMailerTransport({ ...this.#config, ...config }) + const nodemailerTransport = new NodeMailerTransport({ ...this.config, ...config }) const transporter = createTransport(nodemailerTransport) const sparkPostResponse = await transporter.sendMail(message)