Skip to content

Commit 1c1ed97

Browse files
authored
feat(email-nodemailer): add email recipient override config (#16311)
Adds the ability to override recipients for ALL emails being sent via sendEmail with the overrideRecipientAddress config in email adapters. This can be used in deployed testing environments to ensure emails all go to one address.
1 parent aa01a45 commit 1c1ed97

4 files changed

Lines changed: 142 additions & 13 deletions

File tree

packages/email-nodemailer/src/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ import { InvalidConfiguration } from 'payload'
99
export type NodemailerAdapterArgs = {
1010
defaultFromAddress: string
1111
defaultFromName: string
12+
/**
13+
* Override all emails to be sent to this address.
14+
* Useful for testing.
15+
*/
16+
overrideRecipientAddress?: string
17+
/**
18+
* Skip verifying the email SMTP transport.
19+
*/
1220
skipVerify?: boolean
1321
transport?: Transporter
1422
transportOptions?: SMTPConnection.Options
@@ -25,6 +33,7 @@ export const nodemailerAdapter = async (
2533
args?: NodemailerAdapterArgs,
2634
): Promise<NodemailerAdapter> => {
2735
const { defaultFromAddress, defaultFromName, transport } = await buildEmail(args)
36+
const overrideRecipientAddress = args?.overrideRecipientAddress
2837

2938
const adapter: NodemailerAdapter = () => ({
3039
name: 'nodemailer',
@@ -34,6 +43,7 @@ export const nodemailerAdapter = async (
3443
return await transport.sendMail({
3544
from: `${defaultFromName} <${defaultFromAddress}>`,
3645
...message,
46+
...(overrideRecipientAddress ? { to: overrideRecipientAddress } : {}),
3747
})
3848
},
3949
})

packages/email-resend/src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ export type ResendAdapterArgs = {
66
apiKey: string
77
defaultFromAddress: string
88
defaultFromName: string
9+
/**
10+
* Override all emails to be sent to this address.
11+
* Useful for testing.
12+
*/
13+
overrideRecipientAddress?: string
914
}
1015

1116
type ResendAdapter = EmailAdapter<ResendResponse>
@@ -29,9 +34,14 @@ export const resendAdapter = (args: ResendAdapterArgs): ResendAdapter => {
2934
defaultFromAddress,
3035
defaultFromName,
3136
sendEmail: async (message) => {
37+
const modifiedMessage = {
38+
...message,
39+
...(args.overrideRecipientAddress ? { to: args.overrideRecipientAddress } : {}),
40+
}
41+
3242
// Map the Payload email options to Resend email options
3343
const sendEmailOptions = mapPayloadEmailToResendEmail(
34-
message,
44+
modifiedMessage,
3545
defaultFromAddress,
3646
defaultFromName,
3747
)

test/email-nodemailer/config.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,22 @@ export default buildConfigWithDefaults({
1616
collections: [],
1717
email: nodemailerAdapter(),
1818
onInit: async (payload) => {
19-
await payload.create({
20-
collection: 'users',
21-
data: {
22-
email: devUser.email,
23-
password: devUser.password,
24-
},
25-
})
19+
if (process.env.SKIP_ON_INIT !== 'true') {
20+
await payload.create({
21+
collection: 'users',
22+
data: {
23+
email: devUser.email,
24+
password: devUser.password,
25+
},
26+
})
2627

27-
const email = await payload.sendEmail({
28-
subject: 'This was sent on init',
29-
to: 'test@example.com',
30-
})
28+
const email = await payload.sendEmail({
29+
subject: 'This was sent on init',
30+
to: 'test@example.com',
31+
})
3132

32-
payload.logger.info({ email, msg: 'Email sent' })
33+
payload.logger.info({ email, msg: 'Email sent' })
34+
}
3335
},
3436
typescript: {
3537
outputFile: path.resolve(dirname, 'payload-types.ts'),

test/email-nodemailer/int.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import type { NodemailerAdapterArgs } from '@payloadcms/email-nodemailer'
2+
import type { Payload } from 'payload'
3+
4+
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
5+
import path from 'path'
6+
import { fileURLToPath } from 'url'
7+
import { afterAll, beforeAll, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
8+
9+
import { initPayloadInt } from '../__helpers/shared/initPayloadInt.js'
10+
11+
let payload: Payload
12+
let mockedSendEmail: Mock
13+
14+
const overrideRecipientAddress = 'overriden@example.com'
15+
16+
const filename = fileURLToPath(import.meta.url)
17+
const dirname = path.dirname(filename)
18+
19+
type EmailReturnType = {
20+
subject: string
21+
text: string
22+
to: string
23+
}
24+
25+
describe('@payloadcms/email-nodemailer', () => {
26+
beforeAll(async () => {
27+
process.env.SKIP_ON_INIT = 'true'
28+
;({ payload } = await initPayloadInt(dirname))
29+
30+
mockedSendEmail = vi.fn()
31+
})
32+
33+
afterAll(async () => {
34+
if (typeof payload?.db?.destroy === 'function') {
35+
await payload.db.destroy()
36+
}
37+
})
38+
39+
describe('without basic config', () => {
40+
beforeEach(async () => {
41+
// Partially mocked transport
42+
const mockedTransport = {
43+
// eslint-disable-next-line @typescript-eslint/require-await
44+
sendMail: async (message) => {
45+
mockedSendEmail()
46+
return message
47+
},
48+
} as NodemailerAdapterArgs['transport']
49+
50+
const adapter = await nodemailerAdapter({
51+
defaultFromAddress: 'test@example.com',
52+
defaultFromName: 'Test',
53+
skipVerify: true,
54+
transport: mockedTransport,
55+
})
56+
57+
const mockedAdapter = adapter({ payload })
58+
59+
payload.email = mockedAdapter
60+
})
61+
62+
it('sends email with overrideRecipientAddress', async () => {
63+
const email = (await payload.email.sendEmail({
64+
to: 'dev@example.com',
65+
text: 'Hello, world!',
66+
subject: 'Test email',
67+
})) as EmailReturnType
68+
69+
expect(email.to).toEqual('dev@example.com')
70+
})
71+
})
72+
73+
describe('with overrideRecipientAddress', () => {
74+
beforeEach(async () => {
75+
// Partially mocked transport
76+
const mockedTransport = {
77+
// eslint-disable-next-line @typescript-eslint/require-await
78+
sendMail: async (message) => {
79+
mockedSendEmail()
80+
return message
81+
},
82+
} as NodemailerAdapterArgs['transport']
83+
84+
const adapter = await nodemailerAdapter({
85+
overrideRecipientAddress,
86+
defaultFromAddress: 'test@example.com',
87+
defaultFromName: 'Test',
88+
skipVerify: true,
89+
transport: mockedTransport,
90+
})
91+
92+
const mockedAdapter = adapter({ payload })
93+
94+
payload.email = mockedAdapter
95+
})
96+
97+
it('sends email with overrideRecipientAddress', async () => {
98+
const email = (await payload.email.sendEmail({
99+
to: 'dev@example.com',
100+
text: 'Hello, world!',
101+
subject: 'Test email',
102+
})) as EmailReturnType
103+
104+
expect(email.to).toEqual(overrideRecipientAddress)
105+
})
106+
})
107+
})

0 commit comments

Comments
 (0)