chore: adjust email pattern
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { Transporter } from 'nodemailer'
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
||||
import type { EmailAdapter, SendMailOptions } from 'payload/types'
|
||||
import type { EmailAdapter } from 'payload/config'
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
import { InvalidConfiguration } from 'payload/errors'
|
||||
@@ -14,19 +14,19 @@ export type NodemailerAdapterArgs = {
|
||||
transportOptions?: SMTPConnection.Options
|
||||
}
|
||||
|
||||
export type NodemailerAdapter = EmailAdapter<SendMailOptions, unknown>
|
||||
export type NodemailerAdapter = EmailAdapter<unknown>
|
||||
|
||||
/**
|
||||
* Creates an email adapter using nodemailer
|
||||
*
|
||||
* If no email configuration is provided, an ethereal email test account is returned
|
||||
*/
|
||||
export const createNodemailerAdapter = async (
|
||||
export const nodemailerAdapter = async (
|
||||
args?: NodemailerAdapterArgs,
|
||||
): Promise<NodemailerAdapter> => {
|
||||
const { defaultFromAddress, defaultFromName, transport } = await buildEmail(args)
|
||||
|
||||
const adapter: NodemailerAdapter = {
|
||||
const adapter: NodemailerAdapter = () => ({
|
||||
defaultFromAddress,
|
||||
defaultFromName,
|
||||
sendEmail: async (message) => {
|
||||
@@ -35,14 +35,15 @@ export const createNodemailerAdapter = async (
|
||||
...message,
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
})
|
||||
return adapter
|
||||
}
|
||||
|
||||
async function buildEmail(
|
||||
emailConfig?: NodemailerAdapterArgs,
|
||||
): Promise<{ defaultFromAddress: string; defaultFromName: string; transport: Transporter }> {
|
||||
async function buildEmail(emailConfig?: NodemailerAdapterArgs): Promise<{
|
||||
defaultFromAddress: string
|
||||
defaultFromName: string
|
||||
transport: Transporter
|
||||
}> {
|
||||
if (!emailConfig) {
|
||||
const transport = await createMockAccount(emailConfig)
|
||||
if (!transport) throw new InvalidConfiguration('Unable to create Nodemailer test account.')
|
||||
|
||||
@@ -2,8 +2,7 @@ import { URL } from 'url'
|
||||
|
||||
import type { Collection } from '../collections/config/types.js'
|
||||
import type { SanitizedConfig } from '../config/types.js'
|
||||
import type { EmailAdapter } from '../email/types.js'
|
||||
import type { Payload } from '../index.js'
|
||||
import type { InitializedEmailAdapter } from '../email/types.js'
|
||||
import type { PayloadRequest } from '../types/index.js'
|
||||
import type { User, VerifyConfig } from './types.js'
|
||||
|
||||
@@ -11,13 +10,13 @@ type Args = {
|
||||
collection: Collection
|
||||
config: SanitizedConfig
|
||||
disableEmail: boolean
|
||||
email: EmailAdapter<any, unknown>
|
||||
email: InitializedEmailAdapter
|
||||
req: PayloadRequest
|
||||
token: string
|
||||
user: User
|
||||
}
|
||||
|
||||
async function sendVerificationEmail(args: Args): Promise<void> {
|
||||
export async function sendVerificationEmail(args: Args): Promise<void> {
|
||||
// Verify token from e-mail
|
||||
const {
|
||||
collection: { config: collectionConfig },
|
||||
@@ -74,5 +73,3 @@ async function sendVerificationEmail(args: Args): Promise<void> {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default sendVerificationEmail
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
} from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import sendVerificationEmail from '../../auth/sendVerificationEmail.js'
|
||||
import { sendVerificationEmail } from '../../auth/sendVerificationEmail.js'
|
||||
import { registerLocalStrategy } from '../../auth/strategies/local/register.js'
|
||||
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
||||
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
||||
import type GraphQL from 'graphql'
|
||||
import type { DestinationStream, LoggerOptions } from 'pino'
|
||||
import type { DestinationStream, LoggerOptions, P } from 'pino'
|
||||
import type React from 'react'
|
||||
import type { default as sharp } from 'sharp'
|
||||
import type { DeepRequired } from 'ts-essentials'
|
||||
@@ -16,7 +16,7 @@ import type {
|
||||
SanitizedCollectionConfig,
|
||||
} from '../collections/config/types.js'
|
||||
import type { DatabaseAdapterResult } from '../database/types.js'
|
||||
import type { EmailAdapter, SendMailOptions } from '../email/types.js'
|
||||
import type { EmailAdapter, SendEmailOptions } from '../email/types.js'
|
||||
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||
import type { Payload } from '../index.js'
|
||||
import type { PayloadRequest, Where } from '../types/index.js'
|
||||
@@ -531,7 +531,7 @@ export type Config = {
|
||||
*
|
||||
* @see https://payloadcms.com/docs/email/overview
|
||||
*/
|
||||
email?: EmailAdapter<any, unknown> | Promise<EmailAdapter<any, unknown>>
|
||||
email?: EmailAdapter | Promise<EmailAdapter>
|
||||
/** Custom REST endpoints */
|
||||
endpoints?: Endpoint[]
|
||||
/**
|
||||
@@ -690,3 +690,5 @@ export type EntityDescription =
|
||||
| EntityDescriptionFunction
|
||||
| Record<string, string>
|
||||
| string
|
||||
|
||||
export type { EmailAdapter, SendEmailOptions }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { EmailAdapter } from './types.js'
|
||||
import type { InitializedEmailAdapter } from './types.js'
|
||||
|
||||
export const emailDefaults: Pick<
|
||||
EmailAdapter<any, unknown>,
|
||||
InitializedEmailAdapter,
|
||||
'defaultFromAddress' | 'defaultFromName'
|
||||
> = {
|
||||
defaultFromAddress: 'info@payloadcms.com',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SendMailOptions } from 'nodemailer'
|
||||
import type { SendEmailOptions } from './types.js'
|
||||
|
||||
export const getStringifiedToAddress = (message: SendMailOptions): string | undefined => {
|
||||
export const getStringifiedToAddress = (message: SendEmailOptions): string | undefined => {
|
||||
let stringifiedTo: string | undefined
|
||||
|
||||
if (typeof message.to === 'string') {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { SendMailOptions } from 'nodemailer'
|
||||
|
||||
import type { Payload } from '../types/index.js'
|
||||
import type { SendEmailOptions } from './types.js'
|
||||
|
||||
import { getStringifiedToAddress } from './getStringifiedToAddress.js'
|
||||
|
||||
export async function sendEmail(this: Payload, message: SendMailOptions): Promise<unknown> {
|
||||
export async function sendEmail(this: Payload, message: SendEmailOptions): Promise<unknown> {
|
||||
let result
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,23 +1,15 @@
|
||||
import type { SendMailOptions } from 'nodemailer'
|
||||
|
||||
import type { Payload } from '../index.js'
|
||||
import type { EmailAdapter } from './types.js'
|
||||
|
||||
import { emailDefaults } from './defaults.js'
|
||||
import { getStringifiedToAddress } from './getStringifiedToAddress.js'
|
||||
|
||||
export type StdoutAdapter = EmailAdapter<SendMailOptions, void>
|
||||
|
||||
export const createStdoutAdapter = (payload: Payload) => {
|
||||
const stdoutAdapter: StdoutAdapter = {
|
||||
defaultFromAddress: emailDefaults.defaultFromAddress,
|
||||
defaultFromName: emailDefaults.defaultFromName,
|
||||
sendEmail: async (message) => {
|
||||
const stringifiedTo = getStringifiedToAddress(message)
|
||||
const res = `EMAIL NON-DELIVERY. To: '${stringifiedTo}', Subject: '${message.subject}'`
|
||||
payload.logger.info({ msg: res })
|
||||
return Promise.resolve()
|
||||
},
|
||||
}
|
||||
return stdoutAdapter
|
||||
}
|
||||
export const stdoutAdapter: EmailAdapter<void> = ({ payload }) => ({
|
||||
defaultFromAddress: emailDefaults.defaultFromAddress,
|
||||
defaultFromName: emailDefaults.defaultFromName,
|
||||
sendEmail: async (message) => {
|
||||
const stringifiedTo = getStringifiedToAddress(message)
|
||||
const res = `EMAIL NON-DELIVERY. To: '${stringifiedTo}', Subject: '${message.subject}'`
|
||||
payload.logger.info({ msg: res })
|
||||
return Promise.resolve()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
import type { SendMailOptions } from 'nodemailer'
|
||||
import type { SendMailOptions as NodemailerSendMailOptions } from 'nodemailer'
|
||||
|
||||
export type { SendMailOptions }
|
||||
import type { Payload } from '../types/index.js'
|
||||
|
||||
export type EmailAdapter<
|
||||
TSendEmailOptions extends SendMailOptions,
|
||||
KSendEmailResponse = unknown,
|
||||
> = {
|
||||
type Prettify<T> = {
|
||||
[K in keyof T]: T[K]
|
||||
} & NonNullable<unknown>
|
||||
|
||||
/**
|
||||
* Options for sending an email. Allows access to the PayloadRequest object
|
||||
*/
|
||||
export type SendEmailOptions = Prettify<NodemailerSendMailOptions>
|
||||
|
||||
/**
|
||||
* Email adapter after it has been initialized. This is used internally by Payload.
|
||||
*/
|
||||
export type InitializedEmailAdapter<TSendEmailResponse = unknown> = ReturnType<
|
||||
EmailAdapter<TSendEmailResponse>
|
||||
>
|
||||
|
||||
/**
|
||||
* Email adapter interface. Allows a generic type for the response of the sendEmail method.
|
||||
*
|
||||
* This is the interface to use if you are creating a new email adapter.
|
||||
*/
|
||||
|
||||
export type EmailAdapter<TSendEmailResponse = unknown> = ({ payload }: { payload: Payload }) => {
|
||||
defaultFromAddress: string
|
||||
defaultFromName: string
|
||||
sendEmail: (message: TSendEmailOptions) => Promise<KSendEmailResponse>
|
||||
sendEmail: (message: SendEmailOptions) => Promise<TSendEmailResponse>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export type * from '../uploads/types.js'
|
||||
export type { DocumentPermissions, FieldPermissions } from '../auth/index.js'
|
||||
|
||||
export type { MeOperationResult } from '../auth/operations/me.js'
|
||||
export type { EmailAdapter, SendMailOptions } from '../email/types.js'
|
||||
export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from '../email/types.js'
|
||||
|
||||
export type {
|
||||
CollapsedPreferences,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql'
|
||||
import type { OperationArgs, Request as graphQLRequest } from 'graphql-http'
|
||||
import type { SendMailOptions } from 'nodemailer'
|
||||
import type pino from 'pino'
|
||||
|
||||
import crypto from 'crypto'
|
||||
@@ -35,7 +34,7 @@ import type {
|
||||
} from './collections/operations/local/update.js'
|
||||
import type { InitOptions, SanitizedConfig } from './config/types.js'
|
||||
import type { BaseDatabaseAdapter, PaginatedDocs } from './database/types.js'
|
||||
import type { EmailAdapter } from './email/types.js'
|
||||
import type { InitializedEmailAdapter } from './email/types.js'
|
||||
import type { TypeWithID as GlobalTypeWithID, Globals } from './globals/config/types.js'
|
||||
import type { Options as FindGlobalOptions } from './globals/operations/local/findOne.js'
|
||||
import type { Options as FindGlobalVersionByIDOptions } from './globals/operations/local/findVersionByID.js'
|
||||
@@ -49,7 +48,7 @@ import { APIKeyAuthentication } from './auth/strategies/apiKey.js'
|
||||
import { JWTAuthentication } from './auth/strategies/jwt.js'
|
||||
import localOperations from './collections/operations/local/index.js'
|
||||
import { validateSchema } from './config/validate.js'
|
||||
import { createStdoutAdapter } from './email/stdoutAdapter.js'
|
||||
import { stdoutAdapter } from './email/stdoutAdapter.js'
|
||||
import { fieldAffectsData } from './exports/types.js'
|
||||
import localGlobalOperations from './globals/operations/local/index.js'
|
||||
import flattenFields from './utilities/flattenTopLevelFields.js'
|
||||
@@ -102,8 +101,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
return duplicate<T>(this, options)
|
||||
}
|
||||
|
||||
// Do these types need to be injected via GeneratedTypes?
|
||||
email: EmailAdapter<any, unknown>
|
||||
email: InitializedEmailAdapter
|
||||
|
||||
// TODO: re-implement or remove?
|
||||
// errorHandler: ErrorHandler
|
||||
@@ -252,7 +250,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
|
||||
secret: string
|
||||
|
||||
sendEmail: (message: SendMailOptions) => Promise<unknown>
|
||||
sendEmail: InitializedEmailAdapter['sendEmail']
|
||||
|
||||
types: {
|
||||
arrayTypes: any
|
||||
@@ -366,18 +364,19 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
|
||||
// Load email adapter
|
||||
if (this.config.email instanceof Promise) {
|
||||
this.email = await this.config.email
|
||||
const awaitedAdapter = await this.config.email
|
||||
this.email = awaitedAdapter({ payload: this })
|
||||
} else if (this.config.email) {
|
||||
this.email = this.config.email
|
||||
this.email = this.config.email({ payload: this })
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`No email adapter provided. Email will be written to stdout. More info at https://payloadcms.com/docs/email/overview.`,
|
||||
)
|
||||
|
||||
this.email = createStdoutAdapter(this)
|
||||
this.email = stdoutAdapter({ payload: this })
|
||||
}
|
||||
|
||||
this.sendEmail = this.email.sendEmail
|
||||
this.sendEmail = this.email['sendEmail']
|
||||
|
||||
serverInitTelemetry(this)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Payload } from 'payload'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
import { defaults } from 'payload/config'
|
||||
@@ -12,6 +13,8 @@ describe('email', () => {
|
||||
const apiKey = 'test'
|
||||
let createTransportSpy: jest.SpyInstance
|
||||
|
||||
const mockedPayload: Payload = jest.fn() as unknown as Payload
|
||||
|
||||
beforeEach(() => {
|
||||
defaultConfig = defaults as Config
|
||||
|
||||
@@ -71,8 +74,10 @@ describe('email', () => {
|
||||
defaultFromAddress,
|
||||
})
|
||||
|
||||
expect(email.defaultFromName).toEqual(defaultFromName)
|
||||
expect(email.defaultFromAddress).toEqual(defaultFromAddress)
|
||||
const initializedEmail = email({ payload: mockedPayload })
|
||||
|
||||
expect(initializedEmail.defaultFromName).toEqual(defaultFromName)
|
||||
expect(initializedEmail.defaultFromAddress).toEqual(defaultFromAddress)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { NodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
|
||||
import { createNodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import nodemailer from 'nodemailer'
|
||||
|
||||
import type { PayloadCloudEmailOptions } from './types.js'
|
||||
@@ -12,7 +12,7 @@ export const payloadCloudEmail = async (
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (!args.apiKey) throw new Error('apiKey must be provided to use Payload Cloud Email ')
|
||||
if (!args.apiKey) throw new Error('apiKey must be provided to use Payload Cloud Email')
|
||||
if (!args.defaultDomain)
|
||||
throw new Error('defaultDomain must be provided to use Payload Cloud Email')
|
||||
|
||||
@@ -43,7 +43,7 @@ export const payloadCloudEmail = async (
|
||||
const defaultFromAddress =
|
||||
args.defaultFromAddress || `cms@${customDomains.length ? customDomains[0] : defaultDomain}`
|
||||
|
||||
const emailAdapter = await createNodemailerAdapter({
|
||||
const emailAdapter = await nodemailerAdapter({
|
||||
defaultFromAddress,
|
||||
defaultFromName,
|
||||
skipVerify,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { Config } from 'payload/config'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
import { defaults } from 'payload/config'
|
||||
|
||||
import { payloadCloud } from './plugin.js'
|
||||
import { createNodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import { NodemailerAdapter, nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
|
||||
const mockedPayload: Payload = jest.fn() as unknown as Payload
|
||||
|
||||
describe('plugin', () => {
|
||||
let createTransportSpy: jest.SpyInstance
|
||||
@@ -12,13 +15,16 @@ describe('plugin', () => {
|
||||
const skipVerify = true
|
||||
|
||||
beforeEach(() => {
|
||||
createTransportSpy = jest.spyOn(nodemailer, 'createTransport').mockImplementation(() => {
|
||||
createTransportSpy = jest.spyOn(nodemailer, 'createTransport').mockImplementationOnce(() => {
|
||||
return {
|
||||
verify: jest.fn(),
|
||||
transporter: {
|
||||
name: 'Nodemailer - SMTP',
|
||||
},
|
||||
} as unknown as ReturnType<typeof nodemailer.createTransport>
|
||||
})
|
||||
|
||||
const createTestAccountSpy = jest.spyOn(nodemailer, 'createTestAccount').mockResolvedValue({
|
||||
const createTestAccountSpy = jest.spyOn(nodemailer, 'createTestAccount').mockResolvedValueOnce({
|
||||
pass: 'password',
|
||||
user: 'user',
|
||||
web: 'ethereal.email',
|
||||
@@ -67,7 +73,11 @@ describe('plugin', () => {
|
||||
const plugin = payloadCloud()
|
||||
const config = await plugin(createConfig())
|
||||
|
||||
assertCloudEmail(config)
|
||||
expect(createTransportSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
host: 'smtp.resend.com',
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// eslint-disable-next-line jest/expect-expect
|
||||
@@ -102,7 +112,7 @@ describe('plugin', () => {
|
||||
})
|
||||
|
||||
const configWithTransport = createConfig({
|
||||
email: await createNodemailerAdapter({
|
||||
email: await nodemailerAdapter({
|
||||
defaultFromAddress: 'test@test.com',
|
||||
defaultFromName: 'Test',
|
||||
transport: existingTransport,
|
||||
@@ -124,7 +134,7 @@ describe('plugin', () => {
|
||||
const defaultFromName = 'Test'
|
||||
const defaultFromAddress = 'test@test.com'
|
||||
const configWithPartialEmail = createConfig({
|
||||
email: await createNodemailerAdapter({
|
||||
email: await nodemailerAdapter({
|
||||
defaultFromAddress,
|
||||
defaultFromName,
|
||||
skipVerify,
|
||||
@@ -133,12 +143,18 @@ describe('plugin', () => {
|
||||
|
||||
const plugin = payloadCloud()
|
||||
const config = await plugin(configWithPartialEmail)
|
||||
const emailConfig = config.email as Awaited<ReturnType<typeof createNodemailerAdapter>>
|
||||
const emailConfig = config.email as Awaited<ReturnType<typeof nodemailerAdapter>>
|
||||
|
||||
expect(emailConfig.defaultFromName).toEqual(defaultFromName)
|
||||
expect(emailConfig.defaultFromAddress).toEqual(defaultFromAddress)
|
||||
const initializedEmail = emailConfig({ payload: mockedPayload })
|
||||
|
||||
assertCloudEmail(config)
|
||||
expect(initializedEmail.defaultFromName).toEqual(defaultFromName)
|
||||
expect(initializedEmail.defaultFromAddress).toEqual(defaultFromAddress)
|
||||
|
||||
expect(createTransportSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
host: 'smtp.resend.com',
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -152,10 +168,8 @@ function assertNoCloudStorage(config: Config) {
|
||||
expect(config.upload?.useTempFiles).toBeFalsy()
|
||||
}
|
||||
|
||||
function assertCloudEmail(config: Config) {
|
||||
expect(
|
||||
config.email && 'sendEmail' in config.email && typeof config.email.sendEmail === 'function',
|
||||
).toBe(true)
|
||||
async function assertCloudEmail(config: Config) {
|
||||
expect(config.email && 'name' in config.email).toStrictEqual('Nodemailer - SMTP')
|
||||
}
|
||||
|
||||
function createConfig(overrides?: Partial<Config>): Config {
|
||||
|
||||
Reference in New Issue
Block a user