feat: abstract nodemailer into email adapter interface
This commit is contained in:
@@ -57,7 +57,7 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
|
|||||||
disableEmail,
|
disableEmail,
|
||||||
expiration,
|
expiration,
|
||||||
req: {
|
req: {
|
||||||
payload: { config, emailOptions, sendEmail: email },
|
payload: { config, email },
|
||||||
payload,
|
payload,
|
||||||
},
|
},
|
||||||
req,
|
req,
|
||||||
@@ -132,8 +132,8 @@ export const forgotPasswordOperation = async (incomingArgs: Arguments): Promise<
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
email({
|
email.sendEmail({
|
||||||
from: `"${emailOptions.fromName}" <${emailOptions.fromAddress}>`,
|
from: `"${email.defaultFromName}" <${email.defaultFromAddress}>`,
|
||||||
html,
|
html,
|
||||||
subject,
|
subject,
|
||||||
to: data.email,
|
to: data.email,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { URL } from 'url'
|
import { URL } from 'url'
|
||||||
|
|
||||||
import type { Collection } from '../collections/config/types.js'
|
import type { Collection } from '../collections/config/types.js'
|
||||||
import type { EmailOptions, SanitizedConfig } from '../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 { Payload } from '../index.js'
|
||||||
import type { PayloadRequest } from '../types/index.js'
|
import type { PayloadRequest } from '../types/index.js'
|
||||||
import type { User, VerifyConfig } from './types.js'
|
import type { User, VerifyConfig } from './types.js'
|
||||||
@@ -10,9 +11,8 @@ type Args = {
|
|||||||
collection: Collection
|
collection: Collection
|
||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
disableEmail: boolean
|
disableEmail: boolean
|
||||||
emailOptions: EmailOptions
|
email: EmailAdapter<any, unknown>
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
sendEmail: Payload['sendEmail']
|
|
||||||
token: string
|
token: string
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
@@ -23,9 +23,8 @@ async function sendVerificationEmail(args: Args): Promise<void> {
|
|||||||
collection: { config: collectionConfig },
|
collection: { config: collectionConfig },
|
||||||
config,
|
config,
|
||||||
disableEmail,
|
disableEmail,
|
||||||
emailOptions,
|
email,
|
||||||
req,
|
req,
|
||||||
sendEmail,
|
|
||||||
token,
|
token,
|
||||||
user,
|
user,
|
||||||
} = args
|
} = args
|
||||||
@@ -67,8 +66,8 @@ async function sendVerificationEmail(args: Args): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
sendEmail({
|
email.sendEmail({
|
||||||
from: `"${emailOptions.fromName}" <${emailOptions.fromAddress}>`,
|
from: `"${email.defaultFromName}" <${email.defaultFromName}>`,
|
||||||
html,
|
html,
|
||||||
subject,
|
subject,
|
||||||
to: user.email,
|
to: user.email,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
|
|||||||
fallbackLocale,
|
fallbackLocale,
|
||||||
locale,
|
locale,
|
||||||
payload,
|
payload,
|
||||||
payload: { config, emailOptions },
|
payload: { config, email },
|
||||||
},
|
},
|
||||||
req,
|
req,
|
||||||
showHiddenFields,
|
showHiddenFields,
|
||||||
@@ -272,9 +272,8 @@ export const createOperation = async <TSlug extends keyof GeneratedTypes['collec
|
|||||||
collection: { config: collectionConfig },
|
collection: { config: collectionConfig },
|
||||||
config: payload.config,
|
config: payload.config,
|
||||||
disableEmail: disableVerificationEmail,
|
disableEmail: disableVerificationEmail,
|
||||||
emailOptions,
|
email: payload.email,
|
||||||
req,
|
req,
|
||||||
sendEmail: payload.sendEmail,
|
|
||||||
token: verificationToken,
|
token: verificationToken,
|
||||||
user: result,
|
user: result,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
import type { I18nOptions, TFunction } from '@payloadcms/translations'
|
||||||
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
import type { Options as ExpressFileUploadOptions } from 'express-fileupload'
|
||||||
import type GraphQL from 'graphql'
|
import type GraphQL from 'graphql'
|
||||||
import type { Transporter } from 'nodemailer'
|
|
||||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
|
||||||
import type { DestinationStream, LoggerOptions } from 'pino'
|
import type { DestinationStream, LoggerOptions } from 'pino'
|
||||||
import type React from 'react'
|
import type React from 'react'
|
||||||
import type { default as sharp } from 'sharp'
|
import type { default as sharp } from 'sharp'
|
||||||
@@ -18,6 +16,7 @@ import type {
|
|||||||
SanitizedCollectionConfig,
|
SanitizedCollectionConfig,
|
||||||
} from '../collections/config/types.js'
|
} from '../collections/config/types.js'
|
||||||
import type { DatabaseAdapterResult } from '../database/types.js'
|
import type { DatabaseAdapterResult } from '../database/types.js'
|
||||||
|
import type { EmailAdapter } from '../email/types.js'
|
||||||
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
|
import type { GlobalConfig, Globals, SanitizedGlobalConfig } from '../globals/config/types.js'
|
||||||
import type { Payload } from '../index.js'
|
import type { Payload } from '../index.js'
|
||||||
import type { PayloadRequest, Where } from '../types/index.js'
|
import type { PayloadRequest, Where } from '../types/index.js'
|
||||||
@@ -34,12 +33,6 @@ type Prettify<T> = {
|
|||||||
[K in keyof T]: T[K]
|
[K in keyof T]: T[K]
|
||||||
} & NonNullable<unknown>
|
} & NonNullable<unknown>
|
||||||
|
|
||||||
type Email = {
|
|
||||||
fromAddress: string
|
|
||||||
fromName: string
|
|
||||||
logMockCredentials?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-use-before-define
|
// eslint-disable-next-line no-use-before-define
|
||||||
export type Plugin = (config: Config) => Config | Promise<Config>
|
export type Plugin = (config: Config) => Config | Promise<Config>
|
||||||
|
|
||||||
@@ -85,36 +78,6 @@ export type GeneratePreviewURL = (
|
|||||||
options: GeneratePreviewURLOptions,
|
options: GeneratePreviewURLOptions,
|
||||||
) => Promise<null | string> | null | string
|
) => Promise<null | string> | null | string
|
||||||
|
|
||||||
export type EmailTransport = Email & {
|
|
||||||
transport: Transporter
|
|
||||||
transportOptions?: SMTPConnection.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EmailTransportOptions = Email & {
|
|
||||||
transport?: Transporter
|
|
||||||
transportOptions: SMTPConnection.Options
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EmailOptions = Email | EmailTransport | EmailTransportOptions
|
|
||||||
|
|
||||||
/**
|
|
||||||
* type guard for EmailOptions
|
|
||||||
* @param emailConfig
|
|
||||||
*/
|
|
||||||
export function hasTransport(emailConfig: EmailOptions): emailConfig is EmailTransport {
|
|
||||||
return (emailConfig as EmailTransport).transport !== undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* type guard for EmailOptions
|
|
||||||
* @param emailConfig
|
|
||||||
*/
|
|
||||||
export function hasTransportOptions(
|
|
||||||
emailConfig: EmailOptions,
|
|
||||||
): emailConfig is EmailTransportOptions {
|
|
||||||
return (emailConfig as EmailTransportOptions).transportOptions !== undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GraphQLInfo = {
|
export type GraphQLInfo = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
fields: Record<string, any>
|
fields: Record<string, any>
|
||||||
@@ -162,13 +125,6 @@ export type InitOptions = {
|
|||||||
*/
|
*/
|
||||||
disableOnInit?: boolean
|
disableOnInit?: boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* Configuration for Payload's email functionality
|
|
||||||
*
|
|
||||||
* @see https://payloadcms.com/docs/email/overview
|
|
||||||
*/
|
|
||||||
email?: EmailOptions
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
* A previously instantiated logger instance. Must conform to the PayloadLogger interface which uses Pino
|
||||||
* This allows you to bring your own logger instance and let payload use it
|
* This allows you to bring your own logger instance and let payload use it
|
||||||
@@ -571,11 +527,11 @@ export type Config = {
|
|||||||
/** Default richtext editor to use for richText fields */
|
/** Default richtext editor to use for richText fields */
|
||||||
editor: RichTextAdapter<any, any, any>
|
editor: RichTextAdapter<any, any, any>
|
||||||
/**
|
/**
|
||||||
* Email configuration options. This value is overridden by `email` in Payload.init if passed.
|
* Email Adapter
|
||||||
*
|
*
|
||||||
* @see https://payloadcms.com/docs/email/overview
|
* @see https://payloadcms.com/docs/email/overview
|
||||||
*/
|
*/
|
||||||
email?: EmailOptions
|
email?: EmailAdapter<any, unknown>
|
||||||
/** Custom REST endpoints */
|
/** Custom REST endpoints */
|
||||||
endpoints?: Endpoint[]
|
endpoints?: Endpoint[]
|
||||||
/**
|
/**
|
||||||
|
|||||||
129
packages/payload/src/email/adapters/nodemailer/index.ts
Normal file
129
packages/payload/src/email/adapters/nodemailer/index.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import type { SendMailOptions, Transporter } from 'nodemailer'
|
||||||
|
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
||||||
|
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
|
import type { EmailAdapter } from '../../types.js'
|
||||||
|
|
||||||
|
import InvalidConfiguration from '../../../errors/InvalidConfiguration.js'
|
||||||
|
import { emailDefaults } from '../../defaults.js'
|
||||||
|
|
||||||
|
type Email = {
|
||||||
|
defaultFromAddress: string
|
||||||
|
defaultFromName: string
|
||||||
|
logMockCredentials?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmailTransportOptions = Email & {
|
||||||
|
transport?: Transporter
|
||||||
|
transportOptions: SMTPConnection.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NodemailerAdapterArgs = Email | EmailTransportOptions
|
||||||
|
export type NodemailerAdapter = EmailAdapter<SendMailOptions, unknown>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an email adapter using nodemailer
|
||||||
|
*
|
||||||
|
* If no email configuration is provided, an ethereal email test account is returned
|
||||||
|
*/
|
||||||
|
export const createNodemailerAdapter = async (
|
||||||
|
args: NodemailerAdapterArgs,
|
||||||
|
): Promise<NodemailerAdapter> => {
|
||||||
|
const { defaultFromAddress, defaultFromName, transport } = await buildEmail(args)
|
||||||
|
|
||||||
|
const adapter: NodemailerAdapter = {
|
||||||
|
defaultFromAddress,
|
||||||
|
defaultFromName,
|
||||||
|
sendEmail: async (message) => {
|
||||||
|
return await transport.sendMail({
|
||||||
|
from: `${defaultFromName} <${defaultFromAddress}>`,
|
||||||
|
...message,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return adapter
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildEmail(
|
||||||
|
emailConfig?: NodemailerAdapterArgs,
|
||||||
|
): Promise<{ defaultFromAddress: string; defaultFromName: string; transport: Transporter }> {
|
||||||
|
if (!emailConfig) {
|
||||||
|
return {
|
||||||
|
defaultFromAddress: emailDefaults.defaultFromAddress,
|
||||||
|
defaultFromName: emailDefaults.defaultFromName,
|
||||||
|
transport: await createMockAccount(emailConfig),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureConfigHasFrom(emailConfig)
|
||||||
|
|
||||||
|
// Create or extract transport
|
||||||
|
let transport: Transporter
|
||||||
|
if ('transport' in emailConfig && emailConfig.transport) {
|
||||||
|
;({ transport } = emailConfig)
|
||||||
|
} else if ('transportOptions' in emailConfig && emailConfig.transportOptions) {
|
||||||
|
transport = nodemailer.createTransport(emailConfig.transportOptions)
|
||||||
|
} else {
|
||||||
|
transport = await createMockAccount(emailConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
await verifyTransport(transport)
|
||||||
|
return {
|
||||||
|
defaultFromAddress: emailConfig.defaultFromAddress,
|
||||||
|
defaultFromName: emailConfig.defaultFromName,
|
||||||
|
transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyTransport(transport: Transporter) {
|
||||||
|
try {
|
||||||
|
await transport.verify()
|
||||||
|
} catch (err: unknown) {
|
||||||
|
console.error({ err, msg: 'Error verifying Nodemailer transport.' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureConfigHasFrom = (emailConfig: NodemailerAdapterArgs) => {
|
||||||
|
if (!emailConfig?.defaultFromName || !emailConfig?.defaultFromAddress) {
|
||||||
|
throw new InvalidConfiguration(
|
||||||
|
'Email fromName and fromAddress must be configured when transport is configured',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use ethereal.email to create a mock email account
|
||||||
|
*/
|
||||||
|
async function createMockAccount(emailConfig: NodemailerAdapterArgs) {
|
||||||
|
try {
|
||||||
|
const etherealAccount = await nodemailer.createTestAccount()
|
||||||
|
|
||||||
|
const smtpOptions = {
|
||||||
|
...emailConfig,
|
||||||
|
auth: {
|
||||||
|
pass: etherealAccount.pass,
|
||||||
|
user: etherealAccount.user,
|
||||||
|
},
|
||||||
|
fromAddress: emailConfig?.defaultFromAddress,
|
||||||
|
fromName: emailConfig?.defaultFromName,
|
||||||
|
host: 'smtp.ethereal.email',
|
||||||
|
port: 587,
|
||||||
|
secure: false,
|
||||||
|
}
|
||||||
|
const transport = nodemailer.createTransport(smtpOptions)
|
||||||
|
const { pass, user, web } = etherealAccount
|
||||||
|
|
||||||
|
if (emailConfig?.logMockCredentials) {
|
||||||
|
console.info('E-mail configured with mock configuration')
|
||||||
|
console.info(`Log into mock email provider at ${web}`)
|
||||||
|
console.info(`Mock email account username: ${user}`)
|
||||||
|
console.info(`Mock email account password: ${pass}`)
|
||||||
|
}
|
||||||
|
return transport
|
||||||
|
} catch (err) {
|
||||||
|
console.error({ err, msg: 'There was a problem setting up the mock email handler' })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import type { Transporter } from 'nodemailer'
|
|
||||||
import type { Logger } from 'pino'
|
|
||||||
|
|
||||||
import nodemailer from 'nodemailer'
|
|
||||||
|
|
||||||
import type { EmailOptions, EmailTransport } from '../config/types.js'
|
|
||||||
import type { BuildEmailResult, MockEmailHandler } from './types.js'
|
|
||||||
|
|
||||||
import { hasTransport, hasTransportOptions } from '../config/types.js'
|
|
||||||
import { InvalidConfiguration } from '../errors/index.js'
|
|
||||||
import mockHandler from './mockHandler.js'
|
|
||||||
|
|
||||||
async function handleTransport(
|
|
||||||
transport: Transporter,
|
|
||||||
email: EmailTransport,
|
|
||||||
logger: Logger,
|
|
||||||
): BuildEmailResult {
|
|
||||||
try {
|
|
||||||
await transport.verify()
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(`There is an error with the email configuration you have provided. ${err}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ...email, transport }
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureConfigHasFrom = (emailConfig: EmailOptions) => {
|
|
||||||
if (!emailConfig?.fromName || !emailConfig?.fromAddress) {
|
|
||||||
throw new InvalidConfiguration(
|
|
||||||
'Email fromName and fromAddress must be configured when transport is configured',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleMockAccount = async (emailConfig: EmailOptions, logger: Logger) => {
|
|
||||||
let mockAccount: MockEmailHandler
|
|
||||||
try {
|
|
||||||
mockAccount = await mockHandler(emailConfig)
|
|
||||||
const {
|
|
||||||
account: { pass, user, web },
|
|
||||||
} = mockAccount
|
|
||||||
if (emailConfig?.logMockCredentials) {
|
|
||||||
logger.info('E-mail configured with mock configuration')
|
|
||||||
logger.info(`Log into mock email provider at ${web}`)
|
|
||||||
logger.info(`Mock email account username: ${user}`)
|
|
||||||
logger.info(`Mock email account password: ${pass}`)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
logger.error({ err, msg: 'There was a problem setting up the mock email handler' })
|
|
||||||
}
|
|
||||||
return mockAccount
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function buildEmail(
|
|
||||||
emailConfig: EmailOptions | undefined,
|
|
||||||
logger: Logger,
|
|
||||||
): BuildEmailResult {
|
|
||||||
if (!emailConfig) {
|
|
||||||
return handleMockAccount(emailConfig, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTransport(emailConfig) && emailConfig.transport) {
|
|
||||||
ensureConfigHasFrom(emailConfig)
|
|
||||||
const email = { ...emailConfig }
|
|
||||||
const { transport }: { transport: Transporter } = emailConfig
|
|
||||||
return handleTransport(transport, email, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasTransportOptions(emailConfig) && emailConfig.transportOptions) {
|
|
||||||
ensureConfigHasFrom(emailConfig)
|
|
||||||
const email = { ...emailConfig } as EmailTransport
|
|
||||||
const transport = nodemailer.createTransport(emailConfig.transportOptions)
|
|
||||||
return handleTransport(transport, email, logger)
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleMockAccount(emailConfig, logger)
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { EmailOptions } from '../config/types.js'
|
import type { EmailAdapter } from './types.js'
|
||||||
|
|
||||||
export const defaults: EmailOptions = {
|
export const emailDefaults: Pick<
|
||||||
fromAddress: 'info@payloadcms.com',
|
EmailAdapter<any, unknown>,
|
||||||
fromName: 'Payload',
|
'defaultFromAddress' | 'defaultFromName'
|
||||||
|
> = {
|
||||||
|
defaultFromAddress: 'info@payloadcms.com',
|
||||||
|
defaultFromName: 'Payload',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import nodemailer from 'nodemailer'
|
|
||||||
|
|
||||||
import type { EmailOptions } from '../config/types.js'
|
|
||||||
import type { MockEmailHandler } from './types.js'
|
|
||||||
|
|
||||||
import { defaults as emailDefaults } from './defaults.js'
|
|
||||||
|
|
||||||
const mockEmailHandler = async (emailConfig: EmailOptions): Promise<MockEmailHandler> => {
|
|
||||||
const testAccount = await nodemailer.createTestAccount()
|
|
||||||
|
|
||||||
const smtpOptions = {
|
|
||||||
...emailConfig,
|
|
||||||
auth: {
|
|
||||||
pass: testAccount.pass,
|
|
||||||
user: testAccount.user,
|
|
||||||
},
|
|
||||||
fromAddress: emailConfig?.fromAddress || emailDefaults.fromAddress,
|
|
||||||
fromName: emailConfig?.fromName || emailDefaults.fromName,
|
|
||||||
host: 'smtp.ethereal.email',
|
|
||||||
port: 587,
|
|
||||||
secure: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
account: testAccount,
|
|
||||||
transport: nodemailer.createTransport(smtpOptions),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default mockEmailHandler
|
|
||||||
@@ -1,13 +1,36 @@
|
|||||||
import type { SendMailOptions } from 'nodemailer'
|
import type { SendMailOptions } from 'nodemailer'
|
||||||
|
|
||||||
export default async function sendEmail(message: SendMailOptions): Promise<unknown> {
|
import type { Payload } from '../types/index.js'
|
||||||
|
|
||||||
|
export async function sendEmail(this: Payload, message: SendMailOptions): Promise<unknown> {
|
||||||
let result
|
let result
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const email = await this.email
|
result = await this.email.sendEmail(message)
|
||||||
result = await email.transport.sendMail(message)
|
} catch (err: unknown) {
|
||||||
} catch (err) {
|
let stringifiedTo: string | undefined
|
||||||
this.logger.error(err, `Failed to send mail to ${message.to}, subject: ${message.subject}`)
|
|
||||||
|
if (typeof message.to === 'string') {
|
||||||
|
stringifiedTo = message.to
|
||||||
|
} else if (Array.isArray(message.to)) {
|
||||||
|
stringifiedTo = message.to
|
||||||
|
.map((to) => {
|
||||||
|
if (typeof to === 'string') {
|
||||||
|
return to
|
||||||
|
} else if (to.address) {
|
||||||
|
return to.address
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
.join(', ')
|
||||||
|
} else if (message.to.address) {
|
||||||
|
stringifiedTo = message.to.address
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error({
|
||||||
|
err,
|
||||||
|
msg: `Failed to send mail to ${stringifiedTo}, subject: ${message.subject ?? 'No Subject'}`,
|
||||||
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,10 @@
|
|||||||
import type { TestAccount, Transporter } from 'nodemailer'
|
import type { SendMailOptions } from 'nodemailer'
|
||||||
import type Mail from 'nodemailer/lib/mailer'
|
|
||||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
|
||||||
|
|
||||||
export type Message = {
|
export type EmailAdapter<
|
||||||
from: string
|
TSendEmailOptions extends SendMailOptions,
|
||||||
html: string
|
KSendEmailResponse = unknown,
|
||||||
subject: string
|
> = {
|
||||||
to: string
|
defaultFromAddress: string
|
||||||
|
defaultFromName: string
|
||||||
|
sendEmail: (message: TSendEmailOptions) => Promise<KSendEmailResponse>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MockEmailHandler = { account: TestAccount; transport: Transporter }
|
|
||||||
export type BuildEmailResult = Promise<
|
|
||||||
| {
|
|
||||||
fromAddress: string
|
|
||||||
fromName: string
|
|
||||||
transport: Mail
|
|
||||||
transportOptions?: SMTPConnection.Options
|
|
||||||
}
|
|
||||||
| MockEmailHandler
|
|
||||||
>
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ import type {
|
|||||||
ManyOptions as UpdateManyOptions,
|
ManyOptions as UpdateManyOptions,
|
||||||
Options as UpdateOptions,
|
Options as UpdateOptions,
|
||||||
} from './collections/operations/local/update.js'
|
} from './collections/operations/local/update.js'
|
||||||
import type { EmailOptions, InitOptions, SanitizedConfig } from './config/types.js'
|
import type { InitOptions, SanitizedConfig } from './config/types.js'
|
||||||
import type { BaseDatabaseAdapter, PaginatedDocs } from './database/types.js'
|
import type { BaseDatabaseAdapter, PaginatedDocs } from './database/types.js'
|
||||||
import type { BuildEmailResult } from './email/types.js'
|
import type { EmailAdapter } from './email/types.js'
|
||||||
import type { TypeWithID as GlobalTypeWithID, Globals } from './globals/config/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 FindGlobalOptions } from './globals/operations/local/findOne.js'
|
||||||
import type { Options as FindGlobalVersionByIDOptions } from './globals/operations/local/findVersionByID.js'
|
import type { Options as FindGlobalVersionByIDOptions } from './globals/operations/local/findVersionByID.js'
|
||||||
@@ -49,9 +49,9 @@ import { APIKeyAuthentication } from './auth/strategies/apiKey.js'
|
|||||||
import { JWTAuthentication } from './auth/strategies/jwt.js'
|
import { JWTAuthentication } from './auth/strategies/jwt.js'
|
||||||
import localOperations from './collections/operations/local/index.js'
|
import localOperations from './collections/operations/local/index.js'
|
||||||
import { validateSchema } from './config/validate.js'
|
import { validateSchema } from './config/validate.js'
|
||||||
import buildEmail from './email/build.js'
|
import { createNodemailerAdapter } from './email/adapters/nodemailer/index.js'
|
||||||
import { defaults as emailDefaults } from './email/defaults.js'
|
import { emailDefaults } from './email/defaults.js'
|
||||||
import sendEmail from './email/sendEmail.js'
|
import { sendEmail } from './email/sendEmail.js'
|
||||||
import { fieldAffectsData } from './exports/types.js'
|
import { fieldAffectsData } from './exports/types.js'
|
||||||
import localGlobalOperations from './globals/operations/local/index.js'
|
import localGlobalOperations from './globals/operations/local/index.js'
|
||||||
import flattenFields from './utilities/flattenTopLevelFields.js'
|
import flattenFields from './utilities/flattenTopLevelFields.js'
|
||||||
@@ -104,9 +104,8 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
|||||||
return duplicate<T>(this, options)
|
return duplicate<T>(this, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
email: BuildEmailResult
|
// Do these types need to be injected via GeneratedTypes?
|
||||||
|
email: EmailAdapter<any, unknown>
|
||||||
emailOptions: EmailOptions
|
|
||||||
|
|
||||||
// TODO: re-implement or remove?
|
// TODO: re-implement or remove?
|
||||||
// errorHandler: ErrorHandler
|
// errorHandler: ErrorHandler
|
||||||
@@ -367,17 +366,10 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
|||||||
await this.db.connect()
|
await this.db.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure email service
|
// TODO: Move nodemailer adapter into separate package after verifying all existing functionality
|
||||||
const emailOptions = options.email ? { ...options.email } : this.config.email
|
this.email = await createNodemailerAdapter(emailDefaults)
|
||||||
if (options.email && this.config.email) {
|
|
||||||
this.logger.warn(
|
|
||||||
'Email options provided in both init options and config. Using init options.',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emailOptions = emailOptions ?? emailDefaults
|
this.sendEmail = this.email.sendEmail
|
||||||
this.email = buildEmail(this.emailOptions, this.logger)
|
|
||||||
this.sendEmail = sendEmail.bind(this)
|
|
||||||
|
|
||||||
serverInitTelemetry(this)
|
serverInitTelemetry(this)
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ export default buildConfigWithDefaults({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const email = await payload.sendEmail({
|
|
||||||
to: 'test@example.com',
|
|
||||||
subject: 'This was sent on init',
|
|
||||||
})
|
|
||||||
|
|
||||||
// Create image
|
// Create image
|
||||||
const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
||||||
const imageFile = await getFileByPath(imageFilePath)
|
const imageFile = await getFileByPath(imageFilePath)
|
||||||
|
|||||||
Reference in New Issue
Block a user