feat!: email adapter (#5901)
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"build:create-payload-app": "turbo build --filter create-payload-app",
|
"build:create-payload-app": "turbo build --filter create-payload-app",
|
||||||
"build:db-mongodb": "turbo build --filter db-mongodb",
|
"build:db-mongodb": "turbo build --filter db-mongodb",
|
||||||
"build:db-postgres": "turbo build --filter db-postgres",
|
"build:db-postgres": "turbo build --filter db-postgres",
|
||||||
|
"build:email-nodemailer": "turbo build --filter email-nodemailer",
|
||||||
"build:eslint-config-payload": "turbo build --filter eslint-config-payload",
|
"build:eslint-config-payload": "turbo build --filter eslint-config-payload",
|
||||||
"build:graphql": "turbo build --filter graphql",
|
"build:graphql": "turbo build --filter graphql",
|
||||||
"build:live-preview": "turbo build --filter live-preview",
|
"build:live-preview": "turbo build --filter live-preview",
|
||||||
|
|||||||
10
packages/email-nodemailer/.eslintignore
Normal file
10
packages/email-nodemailer/.eslintignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
7
packages/email-nodemailer/.eslintrc.cjs
Normal file
7
packages/email-nodemailer/.eslintrc.cjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
packages/email-nodemailer/.prettierignore
Normal file
10
packages/email-nodemailer/.prettierignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.tmp
|
||||||
|
**/.git
|
||||||
|
**/.hg
|
||||||
|
**/.pnp.*
|
||||||
|
**/.svn
|
||||||
|
**/.yarn/**
|
||||||
|
**/build
|
||||||
|
**/dist/**
|
||||||
|
**/node_modules
|
||||||
|
**/temp
|
||||||
15
packages/email-nodemailer/.swcrc
Normal file
15
packages/email-nodemailer/.swcrc
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/swcrc",
|
||||||
|
"sourceMaps": true,
|
||||||
|
"jsc": {
|
||||||
|
"target": "esnext",
|
||||||
|
"parser": {
|
||||||
|
"syntax": "typescript",
|
||||||
|
"tsx": true,
|
||||||
|
"dts": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"module": {
|
||||||
|
"type": "es6"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
packages/email-nodemailer/LICENSE.md
Normal file
22
packages/email-nodemailer/LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018-2022 Payload CMS, LLC <info@payloadcms.com>
|
||||||
|
Portions Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
1
packages/email-nodemailer/README.md
Normal file
1
packages/email-nodemailer/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Nodemailer Email Adapter
|
||||||
59
packages/email-nodemailer/package.json
Normal file
59
packages/email-nodemailer/package.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "@payloadcms/email-nodemailer",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"description": "Payload Nodemailer Email Adapter",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/payloadcms/payload.git",
|
||||||
|
"directory": "packages/email-nodemailer"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"homepage": "https://payloadcms.com",
|
||||||
|
"author": "Payload CMS, Inc.",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm build:swc && pnpm build:types",
|
||||||
|
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly --outDir dist",
|
||||||
|
"build:clean": "find . \\( -type d \\( -name build -o -name dist -o -name .cache \\) -o -type f -name tsconfig.tsbuildinfo \\) -exec rm -rf {} + && pnpm build",
|
||||||
|
"clean": "rimraf {dist,*.tsbuildinfo}",
|
||||||
|
"prepublishOnly": "pnpm clean && pnpm turbo build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"nodemailer": "6.9.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"payload": "workspace:*"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts",
|
||||||
|
"require": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"registry": "https://registry.npmjs.org/",
|
||||||
|
"types": "./dist/index.d.ts"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.20.2"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"devDependencies": {
|
||||||
|
"payload": "workspace:*",
|
||||||
|
"@types/nodemailer": "6.4.14"
|
||||||
|
}
|
||||||
|
}
|
||||||
123
packages/email-nodemailer/src/index.ts
Normal file
123
packages/email-nodemailer/src/index.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
import type { Transporter } from 'nodemailer'
|
||||||
|
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
||||||
|
import type { EmailAdapter } from 'payload/config'
|
||||||
|
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
import { InvalidConfiguration } from 'payload/errors'
|
||||||
|
|
||||||
|
export type NodemailerAdapterArgs = {
|
||||||
|
defaultFromAddress: string
|
||||||
|
defaultFromName: string
|
||||||
|
skipVerify?: boolean
|
||||||
|
transport?: Transporter
|
||||||
|
transportOptions?: SMTPConnection.Options
|
||||||
|
}
|
||||||
|
|
||||||
|
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 nodemailerAdapter = 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) {
|
||||||
|
const transport = await createMockAccount(emailConfig)
|
||||||
|
if (!transport) throw new InvalidConfiguration('Unable to create Nodemailer test account.')
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultFromAddress: 'info@payloadcms.com',
|
||||||
|
defaultFromName: 'Payload',
|
||||||
|
transport,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailConfig.skipVerify !== false) {
|
||||||
|
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.' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
|
||||||
|
console.info('E-mail configured with ethereal.email test account. ')
|
||||||
|
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: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
console.error({ err, msg: 'There was a problem setting up the mock email handler' })
|
||||||
|
throw new InvalidConfiguration(
|
||||||
|
`Unable to create Nodemailer test account. Error: ${err.message}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
throw new InvalidConfiguration('Unable to create Nodemailer test account.')
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/email-nodemailer/tsconfig.json
Normal file
19
packages/email-nodemailer/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true, // Make sure typescript knows that this module depends on their references
|
||||||
|
"noEmit": false /* Do not emit outputs. */,
|
||||||
|
"emitDeclarationOnly": true,
|
||||||
|
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
|
||||||
|
"rootDir": "./src" /* Specify the root folder within your source files. */,
|
||||||
|
"strict": true,
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../payload" },
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -60,7 +60,6 @@
|
|||||||
"minimist": "1.2.8",
|
"minimist": "1.2.8",
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"monaco-editor": "0.38.0",
|
"monaco-editor": "0.38.0",
|
||||||
"nodemailer": "6.9.10",
|
|
||||||
"pino": "8.15.0",
|
"pino": "8.15.0",
|
||||||
"pino-pretty": "10.2.0",
|
"pino-pretty": "10.2.0",
|
||||||
"pluralize": "8.0.0",
|
"pluralize": "8.0.0",
|
||||||
|
|||||||
@@ -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,8 +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 { Payload } from '../index.js'
|
import type { InitializedEmailAdapter } from '../email/types.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,22 +10,20 @@ type Args = {
|
|||||||
collection: Collection
|
collection: Collection
|
||||||
config: SanitizedConfig
|
config: SanitizedConfig
|
||||||
disableEmail: boolean
|
disableEmail: boolean
|
||||||
emailOptions: EmailOptions
|
email: InitializedEmailAdapter
|
||||||
req: PayloadRequest
|
req: PayloadRequest
|
||||||
sendEmail: Payload['sendEmail']
|
|
||||||
token: string
|
token: string
|
||||||
user: User
|
user: User
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendVerificationEmail(args: Args): Promise<void> {
|
export async function sendVerificationEmail(args: Args): Promise<void> {
|
||||||
// Verify token from e-mail
|
// Verify token from e-mail
|
||||||
const {
|
const {
|
||||||
collection: { config: collectionConfig },
|
collection: { config: collectionConfig },
|
||||||
config,
|
config,
|
||||||
disableEmail,
|
disableEmail,
|
||||||
emailOptions,
|
email,
|
||||||
req,
|
req,
|
||||||
sendEmail,
|
|
||||||
token,
|
token,
|
||||||
user,
|
user,
|
||||||
} = args
|
} = args
|
||||||
@@ -67,13 +65,11 @@ 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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default sendVerificationEmail
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type {
|
|||||||
} from '../config/types.js'
|
} from '../config/types.js'
|
||||||
|
|
||||||
import executeAccess from '../../auth/executeAccess.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 { registerLocalStrategy } from '../../auth/strategies/local/register.js'
|
||||||
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
||||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||||
@@ -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,9 +1,7 @@
|
|||||||
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 { DestinationStream, LoggerOptions, P } from 'pino'
|
||||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
|
||||||
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'
|
||||||
import type { DeepRequired } from 'ts-essentials'
|
import type { DeepRequired } from 'ts-essentials'
|
||||||
@@ -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, SendEmailOptions } 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
|
||||||
@@ -573,11 +529,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 | Promise<EmailAdapter>
|
||||||
/** Custom REST endpoints */
|
/** Custom REST endpoints */
|
||||||
endpoints?: Endpoint[]
|
endpoints?: Endpoint[]
|
||||||
/**
|
/**
|
||||||
@@ -736,3 +692,5 @@ export type EntityDescription =
|
|||||||
| EntityDescriptionFunction
|
| EntityDescriptionFunction
|
||||||
| Record<string, string>
|
| Record<string, string>
|
||||||
| string
|
| string
|
||||||
|
|
||||||
|
export type { EmailAdapter, SendEmailOptions }
|
||||||
|
|||||||
@@ -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 { InitializedEmailAdapter } from './types.js'
|
||||||
|
|
||||||
export const defaults: EmailOptions = {
|
export const emailDefaults: Pick<
|
||||||
fromAddress: 'info@payloadcms.com',
|
InitializedEmailAdapter,
|
||||||
fromName: 'Payload',
|
'defaultFromAddress' | 'defaultFromName'
|
||||||
|
> = {
|
||||||
|
defaultFromAddress: 'info@payloadcms.com',
|
||||||
|
defaultFromName: 'Payload',
|
||||||
}
|
}
|
||||||
|
|||||||
23
packages/payload/src/email/getStringifiedToAddress.ts
Normal file
23
packages/payload/src/email/getStringifiedToAddress.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { SendEmailOptions } from './types.js'
|
||||||
|
|
||||||
|
export const getStringifiedToAddress = (message: SendEmailOptions): string | undefined => {
|
||||||
|
let stringifiedTo: string | undefined
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return stringifiedTo
|
||||||
|
}
|
||||||
@@ -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,20 @@
|
|||||||
import type { SendMailOptions } from 'nodemailer'
|
import type { Payload } from '../types/index.js'
|
||||||
|
import type { SendEmailOptions } from './types.js'
|
||||||
|
|
||||||
export default async function sendEmail(message: SendMailOptions): Promise<unknown> {
|
import { getStringifiedToAddress } from './getStringifiedToAddress.js'
|
||||||
|
|
||||||
|
export async function sendEmail(this: Payload, message: SendEmailOptions): 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) {
|
const stringifiedTo = getStringifiedToAddress(message)
|
||||||
this.logger.error(err, `Failed to send mail to ${message.to}, subject: ${message.subject}`)
|
|
||||||
|
this.logger.error({
|
||||||
|
err,
|
||||||
|
msg: `Failed to send mail to ${stringifiedTo}, subject: ${message.subject ?? 'No Subject'}`,
|
||||||
|
})
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
packages/payload/src/email/stdoutAdapter.ts
Normal file
15
packages/payload/src/email/stdoutAdapter.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { EmailAdapter } from './types.js'
|
||||||
|
|
||||||
|
import { emailDefaults } from './defaults.js'
|
||||||
|
import { getStringifiedToAddress } from './getStringifiedToAddress.js'
|
||||||
|
|
||||||
|
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,21 +1,31 @@
|
|||||||
import type { TestAccount, Transporter } from 'nodemailer'
|
import type { SendMailOptions as NodemailerSendMailOptions } from 'nodemailer'
|
||||||
import type Mail from 'nodemailer/lib/mailer'
|
|
||||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
|
||||||
|
|
||||||
export type Message = {
|
import type { Payload } from '../types/index.js'
|
||||||
from: string
|
|
||||||
html: string
|
|
||||||
subject: string
|
|
||||||
to: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MockEmailHandler = { account: TestAccount; transport: Transporter }
|
type Prettify<T> = {
|
||||||
export type BuildEmailResult = Promise<
|
[K in keyof T]: T[K]
|
||||||
| {
|
} & NonNullable<unknown>
|
||||||
fromAddress: string
|
|
||||||
fromName: string
|
/**
|
||||||
transport: Mail
|
* Options for sending an email. Allows access to the PayloadRequest object
|
||||||
transportOptions?: SMTPConnection.Options
|
*/
|
||||||
}
|
export type SendEmailOptions = Prettify<NodemailerSendMailOptions>
|
||||||
| MockEmailHandler
|
|
||||||
|
/**
|
||||||
|
* 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: SendEmailOptions) => Promise<TSendEmailResponse>
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ export type * from '../admin/types.js'
|
|||||||
export type * from '../uploads/types.js'
|
export type * from '../uploads/types.js'
|
||||||
|
|
||||||
export type { DocumentPermissions, FieldPermissions } from '../auth/index.js'
|
export type { DocumentPermissions, FieldPermissions } from '../auth/index.js'
|
||||||
|
|
||||||
export type { MeOperationResult } from '../auth/operations/me.js'
|
export type { MeOperationResult } from '../auth/operations/me.js'
|
||||||
|
export type { EmailAdapter as PayloadEmailAdapter, SendEmailOptions } from '../email/types.js'
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
CollapsedPreferences,
|
CollapsedPreferences,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql'
|
import type { ExecutionResult, GraphQLSchema, ValidationRule } from 'graphql'
|
||||||
import type { OperationArgs, Request as graphQLRequest } from 'graphql-http'
|
import type { OperationArgs, Request as graphQLRequest } from 'graphql-http'
|
||||||
import type { SendMailOptions } from 'nodemailer'
|
|
||||||
import type pino from 'pino'
|
import type pino from 'pino'
|
||||||
|
|
||||||
import crypto from 'crypto'
|
import crypto from 'crypto'
|
||||||
@@ -34,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 { InitializedEmailAdapter } 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'
|
||||||
@@ -50,9 +49,7 @@ 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 { stdoutAdapter } from './email/stdoutAdapter.js'
|
||||||
import { defaults as emailDefaults } from './email/defaults.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'
|
||||||
@@ -116,9 +113,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
|||||||
return duplicate<T>(this, options)
|
return duplicate<T>(this, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
email: BuildEmailResult
|
email: InitializedEmailAdapter
|
||||||
|
|
||||||
emailOptions: EmailOptions
|
|
||||||
|
|
||||||
// TODO: re-implement or remove?
|
// TODO: re-implement or remove?
|
||||||
// errorHandler: ErrorHandler
|
// errorHandler: ErrorHandler
|
||||||
@@ -267,7 +262,7 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
|||||||
|
|
||||||
secret: string
|
secret: string
|
||||||
|
|
||||||
sendEmail: (message: SendMailOptions) => Promise<unknown>
|
sendEmail: InitializedEmailAdapter['sendEmail']
|
||||||
|
|
||||||
types: {
|
types: {
|
||||||
arrayTypes: any
|
arrayTypes: any
|
||||||
@@ -379,17 +374,21 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
|||||||
await this.db.connect()
|
await this.db.connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure email service
|
// Load email adapter
|
||||||
const emailOptions = options.email ? { ...options.email } : this.config.email
|
if (this.config.email instanceof Promise) {
|
||||||
if (options.email && this.config.email) {
|
const awaitedAdapter = await this.config.email
|
||||||
|
this.email = awaitedAdapter({ payload: this })
|
||||||
|
} else if (this.config.email) {
|
||||||
|
this.email = this.config.email({ payload: this })
|
||||||
|
} else {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
'Email options provided in both init options and config. Using init options.',
|
`No email adapter provided. Email will be written to stdout. More info at https://payloadcms.com/docs/email/overview.`,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.email = stdoutAdapter({ payload: this })
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.525.0",
|
"@aws-sdk/client-s3": "^3.525.0",
|
||||||
"@aws-sdk/credential-providers": "^3.525.0",
|
"@aws-sdk/credential-providers": "^3.525.0",
|
||||||
"@aws-sdk/lib-storage": "^3.525.0",
|
"@aws-sdk/lib-storage": "^3.525.0",
|
||||||
|
"@payloadcms/email-nodemailer": "workspace:^",
|
||||||
"amazon-cognito-identity-js": "^6.1.2",
|
"amazon-cognito-identity-js": "^6.1.2",
|
||||||
"nodemailer": "6.9.10",
|
"nodemailer": "6.9.10",
|
||||||
"resend": "^0.17.2"
|
"resend": "^0.17.2"
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { Config } from 'payload/config'
|
import type { Config } from 'payload/config'
|
||||||
|
import type { Payload } from 'payload'
|
||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
import { defaults } from 'payload/config'
|
import { defaults } from 'payload/config'
|
||||||
|
|
||||||
@@ -6,18 +8,36 @@ import { payloadCloudEmail } from './email.js'
|
|||||||
|
|
||||||
describe('email', () => {
|
describe('email', () => {
|
||||||
let defaultConfig: Config
|
let defaultConfig: Config
|
||||||
|
const skipVerify = true
|
||||||
|
const defaultDomain = 'test.com'
|
||||||
|
const apiKey = 'test'
|
||||||
|
let createTransportSpy: jest.SpyInstance
|
||||||
|
|
||||||
|
const mockedPayload: Payload = jest.fn() as unknown as Payload
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// @ts-expect-error No need for db or editor
|
defaultConfig = defaults as Config
|
||||||
defaultConfig = { ...defaults }
|
|
||||||
|
createTransportSpy = jest.spyOn(nodemailer, 'createTransport').mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
verify: jest.fn(),
|
||||||
|
} as unknown as ReturnType<typeof nodemailer.createTransport>
|
||||||
|
})
|
||||||
|
|
||||||
|
const createTestAccountSpy = jest.spyOn(nodemailer, 'createTestAccount').mockResolvedValue({
|
||||||
|
pass: 'password',
|
||||||
|
user: 'user',
|
||||||
|
web: 'ethereal.email',
|
||||||
|
} as unknown as nodemailer.TestAccount)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('not in Payload Cloud', () => {
|
describe('not in Payload Cloud', () => {
|
||||||
it('should return undefined', () => {
|
it('should return undefined', async () => {
|
||||||
const email = payloadCloudEmail({
|
const email = await payloadCloudEmail({
|
||||||
apiKey: 'test',
|
apiKey,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
defaultDomain: 'test',
|
defaultDomain,
|
||||||
|
skipVerify,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(email).toBeUndefined()
|
expect(email).toBeUndefined()
|
||||||
@@ -29,35 +49,35 @@ describe('email', () => {
|
|||||||
process.env.PAYLOAD_CLOUD = 'true'
|
process.env.PAYLOAD_CLOUD = 'true'
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should respect PAYLOAD_CLOUD env var', () => {
|
it('should respect PAYLOAD_CLOUD env var', async () => {
|
||||||
const email = payloadCloudEmail({
|
const email = await payloadCloudEmail({
|
||||||
apiKey: 'test',
|
apiKey,
|
||||||
config: defaultConfig,
|
config: defaultConfig,
|
||||||
defaultDomain: 'test',
|
defaultDomain,
|
||||||
|
skipVerify,
|
||||||
})
|
})
|
||||||
expect(email?.fromName).toBeDefined()
|
expect(email).toBeDefined()
|
||||||
expect(email?.fromAddress).toBeDefined()
|
|
||||||
expect(email?.transport?.transporter.name).toEqual('SMTP')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow setting fromName and fromAddress', () => {
|
it('should allow setting fromName and fromAddress', async () => {
|
||||||
const fromName = 'custom from name'
|
const defaultFromName = 'custom from name'
|
||||||
const fromAddress = 'custom@fromaddress.com'
|
const defaultFromAddress = 'custom@fromaddress.com'
|
||||||
const configWithFrom: Config = {
|
const configWithFrom: Config = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
email: {
|
|
||||||
fromAddress,
|
|
||||||
fromName,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
const email = payloadCloudEmail({
|
const email = await payloadCloudEmail({
|
||||||
apiKey: 'test',
|
apiKey,
|
||||||
config: configWithFrom,
|
config: configWithFrom,
|
||||||
defaultDomain: 'test',
|
defaultDomain,
|
||||||
|
skipVerify,
|
||||||
|
defaultFromName,
|
||||||
|
defaultFromAddress,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(email?.fromName).toEqual(fromName)
|
const initializedEmail = email({ payload: mockedPayload })
|
||||||
expect(email?.fromAddress).toEqual(fromAddress)
|
|
||||||
|
expect(initializedEmail.defaultFromName).toEqual(defaultFromName)
|
||||||
|
expect(initializedEmail.defaultFromAddress).toEqual(defaultFromAddress)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import type { EmailTransport } from 'payload/config'
|
import type { NodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
|
|
||||||
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
import type { PayloadCloudEmailOptions } from './types.js'
|
import type { PayloadCloudEmailOptions } from './types.js'
|
||||||
|
|
||||||
export const payloadCloudEmail = (args: PayloadCloudEmailOptions): EmailTransport | undefined => {
|
export const payloadCloudEmail = async (
|
||||||
|
args: PayloadCloudEmailOptions,
|
||||||
|
): Promise<NodemailerAdapter> | undefined => {
|
||||||
if (process.env.PAYLOAD_CLOUD !== 'true' || !args) {
|
if (process.env.PAYLOAD_CLOUD !== 'true' || !args) {
|
||||||
return undefined
|
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)
|
if (!args.defaultDomain)
|
||||||
throw new Error('defaultDomain must be provided to use Payload Cloud Email')
|
throw new Error('defaultDomain must be provided to use Payload Cloud Email')
|
||||||
|
|
||||||
const { apiKey, config, defaultDomain } = args
|
// Check if already has email configuration
|
||||||
|
|
||||||
|
if (args.config.email) {
|
||||||
|
console.log(
|
||||||
|
'Payload Cloud Email is enabled but email configuration is already provided in Payload config. If this is intentional, set `email: false` in the Payload Cloud plugin options.',
|
||||||
|
)
|
||||||
|
return args.config.email
|
||||||
|
}
|
||||||
|
|
||||||
|
const { apiKey, defaultDomain, skipVerify } = args
|
||||||
|
|
||||||
const customDomainEnvs = Object.keys(process.env).filter(
|
const customDomainEnvs = Object.keys(process.env).filter(
|
||||||
(e) => e.startsWith('PAYLOAD_CLOUD_EMAIL_DOMAIN_') && !e.endsWith('API_KEY'),
|
(e) => e.startsWith('PAYLOAD_CLOUD_EMAIL_DOMAIN_') && !e.endsWith('API_KEY'),
|
||||||
@@ -27,23 +39,14 @@ export const payloadCloudEmail = (args: PayloadCloudEmailOptions): EmailTranspor
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fromName = config.email?.fromName || 'Payload CMS'
|
const defaultFromName = args.defaultFromName || 'Payload CMS'
|
||||||
const fromAddress =
|
const defaultFromAddress =
|
||||||
config.email?.fromAddress || `cms@${customDomains.length ? customDomains[0] : defaultDomain}`
|
args.defaultFromAddress || `cms@${customDomains.length ? customDomains[0] : defaultDomain}`
|
||||||
|
|
||||||
const existingTransport = config.email && 'transport' in config.email && config.email?.transport
|
const emailAdapter = await nodemailerAdapter({
|
||||||
|
defaultFromAddress,
|
||||||
if (existingTransport) {
|
defaultFromName,
|
||||||
return {
|
skipVerify,
|
||||||
fromAddress,
|
|
||||||
fromName,
|
|
||||||
transport: existingTransport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
fromAddress,
|
|
||||||
fromName,
|
|
||||||
transport: nodemailer.createTransport({
|
transport: nodemailer.createTransport({
|
||||||
auth: {
|
auth: {
|
||||||
pass: apiKey,
|
pass: apiKey,
|
||||||
@@ -53,5 +56,7 @@ export const payloadCloudEmail = (args: PayloadCloudEmailOptions): EmailTranspor
|
|||||||
port: 465,
|
port: 465,
|
||||||
secure: true,
|
secure: true,
|
||||||
}),
|
}),
|
||||||
}
|
})
|
||||||
|
|
||||||
|
return emailAdapter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,44 @@
|
|||||||
import type { Config } from 'payload/config'
|
import type { Config } from 'payload/config'
|
||||||
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
import nodemailer from 'nodemailer'
|
import nodemailer from 'nodemailer'
|
||||||
import { defaults } from 'payload/config'
|
import { defaults } from 'payload/config'
|
||||||
|
|
||||||
import { payloadCloud } from './plugin.js'
|
import { payloadCloud } from './plugin.js'
|
||||||
|
import { NodemailerAdapter, nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
|
|
||||||
|
const mockedPayload: Payload = jest.fn() as unknown as Payload
|
||||||
|
|
||||||
describe('plugin', () => {
|
describe('plugin', () => {
|
||||||
|
let createTransportSpy: jest.SpyInstance
|
||||||
|
|
||||||
|
const skipVerify = true
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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').mockResolvedValueOnce({
|
||||||
|
pass: 'password',
|
||||||
|
user: 'user',
|
||||||
|
web: 'ethereal.email',
|
||||||
|
} as unknown as nodemailer.TestAccount)
|
||||||
|
})
|
||||||
|
|
||||||
describe('not in Payload Cloud', () => {
|
describe('not in Payload Cloud', () => {
|
||||||
// eslint-disable-next-line jest/expect-expect
|
// eslint-disable-next-line jest/expect-expect
|
||||||
it('should return unmodified config', () => {
|
it('should return unmodified config', async () => {
|
||||||
const plugin = payloadCloud()
|
const plugin = payloadCloud()
|
||||||
const config = plugin(createConfig())
|
const config = await plugin(createConfig())
|
||||||
|
|
||||||
assertNoCloudStorage(config)
|
assertNoCloudStorage(config)
|
||||||
assertNoCloudEmail(config)
|
expect(config.email).toBeUndefined()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -26,17 +51,17 @@ describe('plugin', () => {
|
|||||||
|
|
||||||
describe('storage', () => {
|
describe('storage', () => {
|
||||||
// eslint-disable-next-line jest/expect-expect
|
// eslint-disable-next-line jest/expect-expect
|
||||||
it('should default to using payload cloud storage', () => {
|
it('should default to using payload cloud storage', async () => {
|
||||||
const plugin = payloadCloud()
|
const plugin = payloadCloud()
|
||||||
const config = plugin(createConfig())
|
const config = await plugin(createConfig())
|
||||||
|
|
||||||
assertCloudStorage(config)
|
assertCloudStorage(config)
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line jest/expect-expect
|
// eslint-disable-next-line jest/expect-expect
|
||||||
it('should allow opt-out', () => {
|
it('should allow opt-out', async () => {
|
||||||
const plugin = payloadCloud({ storage: false })
|
const plugin = payloadCloud({ storage: false })
|
||||||
const config = plugin(createConfig())
|
const config = await plugin(createConfig())
|
||||||
|
|
||||||
assertNoCloudStorage(config)
|
assertNoCloudStorage(config)
|
||||||
})
|
})
|
||||||
@@ -44,33 +69,39 @@ describe('plugin', () => {
|
|||||||
|
|
||||||
describe('email', () => {
|
describe('email', () => {
|
||||||
// eslint-disable-next-line jest/expect-expect
|
// eslint-disable-next-line jest/expect-expect
|
||||||
it('should default to using payload cloud email', () => {
|
it('should default to using payload cloud email', async () => {
|
||||||
const plugin = payloadCloud()
|
const plugin = payloadCloud()
|
||||||
const config = plugin(createConfig())
|
const config = await plugin(createConfig())
|
||||||
|
|
||||||
assertCloudEmail(config)
|
expect(createTransportSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
host: 'smtp.resend.com',
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line jest/expect-expect
|
// eslint-disable-next-line jest/expect-expect
|
||||||
it('should allow opt-out', () => {
|
it('should allow opt-out', async () => {
|
||||||
const plugin = payloadCloud({ email: false })
|
const plugin = payloadCloud({ email: false })
|
||||||
const config = plugin(createConfig())
|
const config = await plugin(createConfig())
|
||||||
|
|
||||||
assertNoCloudEmail(config)
|
expect(config.email).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
// eslint-disable-next-line jest/expect-expect
|
// eslint-disable-next-line jest/expect-expect
|
||||||
it('should allow PAYLOAD_CLOUD_EMAIL_* env vars to be unset', () => {
|
it('should allow PAYLOAD_CLOUD_EMAIL_* env vars to be unset', async () => {
|
||||||
delete process.env.PAYLOAD_CLOUD_EMAIL_API_KEY
|
delete process.env.PAYLOAD_CLOUD_EMAIL_API_KEY
|
||||||
delete process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN
|
delete process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN
|
||||||
|
|
||||||
const plugin = payloadCloud()
|
const plugin = payloadCloud()
|
||||||
const config = plugin(createConfig())
|
const config = await plugin(createConfig())
|
||||||
|
|
||||||
assertNoCloudEmail(config)
|
expect(config.email).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not modify existing email transport', () => {
|
it('should not modify existing email transport', async () => {
|
||||||
|
const logSpy = jest.spyOn(console, 'log')
|
||||||
|
|
||||||
const existingTransport = nodemailer.createTransport({
|
const existingTransport = nodemailer.createTransport({
|
||||||
name: 'existing-transport',
|
name: 'existing-transport',
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
@@ -81,38 +112,49 @@ describe('plugin', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const configWithTransport = createConfig({
|
const configWithTransport = createConfig({
|
||||||
email: {
|
email: await nodemailerAdapter({
|
||||||
fromAddress: 'test@test.com',
|
defaultFromAddress: 'test@test.com',
|
||||||
fromName: 'Test',
|
defaultFromName: 'Test',
|
||||||
transport: existingTransport,
|
transport: existingTransport,
|
||||||
},
|
skipVerify,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const plugin = payloadCloud()
|
const plugin = payloadCloud()
|
||||||
const config = plugin(configWithTransport)
|
const config = await plugin(configWithTransport)
|
||||||
|
|
||||||
expect(
|
expect(logSpy).toHaveBeenCalledWith(
|
||||||
config.email && 'transport' in config.email && config.email.transport?.transporter.name,
|
expect.stringContaining('Payload Cloud Email is enabled but'),
|
||||||
).toEqual('existing-transport')
|
)
|
||||||
|
|
||||||
assertNoCloudEmail(config)
|
// expect(config.email).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should allow setting fromName and fromAddress', () => {
|
it('should allow setting fromName and fromAddress', async () => {
|
||||||
|
const defaultFromName = 'Test'
|
||||||
|
const defaultFromAddress = 'test@test.com'
|
||||||
const configWithPartialEmail = createConfig({
|
const configWithPartialEmail = createConfig({
|
||||||
email: {
|
email: await nodemailerAdapter({
|
||||||
fromAddress: 'test@test.com',
|
defaultFromAddress,
|
||||||
fromName: 'Test',
|
defaultFromName,
|
||||||
},
|
skipVerify,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const plugin = payloadCloud()
|
const plugin = payloadCloud()
|
||||||
const config = plugin(configWithPartialEmail)
|
const config = await plugin(configWithPartialEmail)
|
||||||
|
const emailConfig = config.email as Awaited<ReturnType<typeof nodemailerAdapter>>
|
||||||
|
|
||||||
expect(config.email?.fromName).toEqual(configWithPartialEmail.email?.fromName)
|
const initializedEmail = emailConfig({ payload: mockedPayload })
|
||||||
expect(config.email?.fromAddress).toEqual(configWithPartialEmail.email?.fromAddress)
|
|
||||||
|
|
||||||
assertCloudEmail(config)
|
expect(initializedEmail.defaultFromName).toEqual(defaultFromName)
|
||||||
|
expect(initializedEmail.defaultFromAddress).toEqual(defaultFromAddress)
|
||||||
|
|
||||||
|
expect(createTransportSpy).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
host: 'smtp.resend.com',
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -126,20 +168,8 @@ function assertNoCloudStorage(config: Config) {
|
|||||||
expect(config.upload?.useTempFiles).toBeFalsy()
|
expect(config.upload?.useTempFiles).toBeFalsy()
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertCloudEmail(config: Config) {
|
async function assertCloudEmail(config: Config) {
|
||||||
if (config.email && 'transport' in config.email) {
|
expect(config.email && 'name' in config.email).toStrictEqual('Nodemailer - SMTP')
|
||||||
expect(config.email?.transport?.transporter.name).toEqual('SMTP')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Asserts that plugin did not run */
|
|
||||||
function assertNoCloudEmail(config: Config) {
|
|
||||||
// No transport set
|
|
||||||
if (!config.email) return
|
|
||||||
|
|
||||||
if ('transport' in config.email) {
|
|
||||||
expect(config.email?.transport?.transporter.name).not.toEqual('SMTP')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function createConfig(overrides?: Partial<Config>): Config {
|
function createConfig(overrides?: Partial<Config>): Config {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { getStaticHandler } from './staticHandler.js'
|
|||||||
|
|
||||||
export const payloadCloud =
|
export const payloadCloud =
|
||||||
(pluginOptions?: PluginOptions) =>
|
(pluginOptions?: PluginOptions) =>
|
||||||
(incomingConfig: Config): Config => {
|
async (incomingConfig: Config): Promise<Config> => {
|
||||||
let config = { ...incomingConfig }
|
let config = { ...incomingConfig }
|
||||||
|
|
||||||
if (process.env.PAYLOAD_CLOUD !== 'true') {
|
if (process.env.PAYLOAD_CLOUD !== 'true') {
|
||||||
@@ -83,10 +83,13 @@ export const payloadCloud =
|
|||||||
const apiKey = process.env.PAYLOAD_CLOUD_EMAIL_API_KEY
|
const apiKey = process.env.PAYLOAD_CLOUD_EMAIL_API_KEY
|
||||||
const defaultDomain = process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN
|
const defaultDomain = process.env.PAYLOAD_CLOUD_DEFAULT_DOMAIN
|
||||||
if (pluginOptions?.email !== false && apiKey && defaultDomain) {
|
if (pluginOptions?.email !== false && apiKey && defaultDomain) {
|
||||||
config.email = payloadCloudEmail({
|
config.email = await payloadCloudEmail({
|
||||||
apiKey,
|
apiKey,
|
||||||
config,
|
config,
|
||||||
defaultDomain,
|
defaultDomain,
|
||||||
|
defaultFromAddress: pluginOptions?.email?.defaultFromAddress,
|
||||||
|
defaultFromName: pluginOptions?.email?.defaultFromName,
|
||||||
|
skipVerify: pluginOptions?.email?.skipVerify,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,13 +42,22 @@ export interface PayloadCloudEmailOptions {
|
|||||||
apiKey: string
|
apiKey: string
|
||||||
config: Config
|
config: Config
|
||||||
defaultDomain: string
|
defaultDomain: string
|
||||||
|
defaultFromAddress?: string
|
||||||
|
defaultFromName?: string
|
||||||
|
skipVerify?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginOptions {
|
export interface PluginOptions {
|
||||||
/** Payload Cloud Email
|
/** Payload Cloud Email
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
email?: false
|
email?:
|
||||||
|
| {
|
||||||
|
defaultFromAddress: string
|
||||||
|
defaultFromName: string
|
||||||
|
skipVerify?: boolean
|
||||||
|
}
|
||||||
|
| false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payload Cloud API endpoint
|
* Payload Cloud API endpoint
|
||||||
|
|||||||
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@@ -443,6 +443,19 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../payload
|
version: link:../payload
|
||||||
|
|
||||||
|
packages/email-nodemailer:
|
||||||
|
dependencies:
|
||||||
|
nodemailer:
|
||||||
|
specifier: 6.9.10
|
||||||
|
version: 6.9.10
|
||||||
|
devDependencies:
|
||||||
|
'@types/nodemailer':
|
||||||
|
specifier: 6.4.14
|
||||||
|
version: 6.4.14
|
||||||
|
payload:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../payload
|
||||||
|
|
||||||
packages/eslint-config-payload:
|
packages/eslint-config-payload:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/eslint':
|
'@types/eslint':
|
||||||
@@ -794,9 +807,6 @@ importers:
|
|||||||
monaco-editor:
|
monaco-editor:
|
||||||
specifier: 0.38.0
|
specifier: 0.38.0
|
||||||
version: 0.38.0
|
version: 0.38.0
|
||||||
nodemailer:
|
|
||||||
specifier: 6.9.10
|
|
||||||
version: 6.9.10
|
|
||||||
pino:
|
pino:
|
||||||
specifier: 8.15.0
|
specifier: 8.15.0
|
||||||
version: 8.15.0
|
version: 8.15.0
|
||||||
@@ -963,6 +973,9 @@ importers:
|
|||||||
'@aws-sdk/lib-storage':
|
'@aws-sdk/lib-storage':
|
||||||
specifier: ^3.525.0
|
specifier: ^3.525.0
|
||||||
version: 3.550.0(@aws-sdk/client-s3@3.550.0)
|
version: 3.550.0(@aws-sdk/client-s3@3.550.0)
|
||||||
|
'@payloadcms/email-nodemailer':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../email-nodemailer
|
||||||
amazon-cognito-identity-js:
|
amazon-cognito-identity-js:
|
||||||
specifier: ^6.1.2
|
specifier: ^6.1.2
|
||||||
version: 6.3.12
|
version: 6.3.12
|
||||||
@@ -1527,6 +1540,9 @@ importers:
|
|||||||
'@payloadcms/db-postgres':
|
'@payloadcms/db-postgres':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/db-postgres
|
version: link:../packages/db-postgres
|
||||||
|
'@payloadcms/email-nodemailer':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../packages/email-nodemailer
|
||||||
'@payloadcms/eslint-config':
|
'@payloadcms/eslint-config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/eslint-config-payload
|
version: link:../packages/eslint-config-payload
|
||||||
|
|||||||
8
test/email-nodemailer/.eslintrc.cjs
Normal file
8
test/email-nodemailer/.eslintrc.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['payload-types.ts'],
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.eslint.json'],
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
},
|
||||||
|
}
|
||||||
2
test/email-nodemailer/.gitignore
vendored
Normal file
2
test/email-nodemailer/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/media
|
||||||
|
/media-gif
|
||||||
26
test/email-nodemailer/config.ts
Normal file
26
test/email-nodemailer/config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createNodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
|
|
||||||
|
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||||
|
import { devUser } from '../credentials.js'
|
||||||
|
|
||||||
|
export default buildConfigWithDefaults({
|
||||||
|
// ...extend config here
|
||||||
|
collections: [],
|
||||||
|
email: createNodemailerAdapter(),
|
||||||
|
onInit: async (payload) => {
|
||||||
|
await payload.create({
|
||||||
|
collection: 'users',
|
||||||
|
data: {
|
||||||
|
email: devUser.email,
|
||||||
|
password: devUser.password,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const email = await payload.sendEmail({
|
||||||
|
to: 'test@example.com',
|
||||||
|
subject: 'This was sent on init',
|
||||||
|
})
|
||||||
|
|
||||||
|
payload.logger.info({ msg: 'Email sent', email })
|
||||||
|
},
|
||||||
|
})
|
||||||
50
test/email-nodemailer/payload-types.ts
Normal file
50
test/email-nodemailer/payload-types.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/**
|
||||||
|
* This file was automatically generated by Payload.
|
||||||
|
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||||
|
* and re-run `payload generate:types` to regenerate this file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface Config {
|
||||||
|
collections: {
|
||||||
|
posts: Post
|
||||||
|
media: Media
|
||||||
|
users: User
|
||||||
|
}
|
||||||
|
globals: {
|
||||||
|
menu: Menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface Post {
|
||||||
|
id: string
|
||||||
|
text?: string
|
||||||
|
associatedMedia?: string | Media
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
export interface Media {
|
||||||
|
id: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
url?: string
|
||||||
|
filename?: string
|
||||||
|
mimeType?: string
|
||||||
|
filesize?: number
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
export interface User {
|
||||||
|
id: string
|
||||||
|
updatedAt: string
|
||||||
|
createdAt: string
|
||||||
|
email?: string
|
||||||
|
resetPasswordToken?: string
|
||||||
|
resetPasswordExpiration?: string
|
||||||
|
loginAttempts?: number
|
||||||
|
lockUntil?: string
|
||||||
|
password?: string
|
||||||
|
}
|
||||||
|
export interface Menu {
|
||||||
|
id: string
|
||||||
|
globalText?: string
|
||||||
|
}
|
||||||
13
test/email-nodemailer/tsconfig.eslint.json
Normal file
13
test/email-nodemailer/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
// extend your base config to share compilerOptions, etc
|
||||||
|
//"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
// ensure that nobody can accidentally use this config for a build
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// whatever paths you intend to lint
|
||||||
|
"./**/*.ts",
|
||||||
|
"./**/*.tsx"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { getFileByPath } from 'payload/uploads'
|
import { getFileByPath } from 'payload/uploads'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
@@ -15,6 +16,7 @@ export default buildConfigWithDefaults({
|
|||||||
// ...extend config here
|
// ...extend config here
|
||||||
collections: [PostsCollection, MediaCollection],
|
collections: [PostsCollection, MediaCollection],
|
||||||
globals: [MenuGlobal],
|
globals: [MenuGlobal],
|
||||||
|
email: nodemailerAdapter(),
|
||||||
onInit: async (payload) => {
|
onInit: async (payload) => {
|
||||||
await payload.create({
|
await payload.create({
|
||||||
collection: 'users',
|
collection: 'users',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"@payloadcms/db-mongodb": "workspace:*",
|
"@payloadcms/db-mongodb": "workspace:*",
|
||||||
"@payloadcms/db-postgres": "workspace:*",
|
"@payloadcms/db-postgres": "workspace:*",
|
||||||
"@payloadcms/eslint-config": "workspace:*",
|
"@payloadcms/eslint-config": "workspace:*",
|
||||||
|
"@payloadcms/email-nodemailer": "workspace:*",
|
||||||
"@payloadcms/graphql": "workspace:*",
|
"@payloadcms/graphql": "workspace:*",
|
||||||
"@payloadcms/live-preview": "workspace:*",
|
"@payloadcms/live-preview": "workspace:*",
|
||||||
"@payloadcms/live-preview-react": "workspace:*",
|
"@payloadcms/live-preview-react": "workspace:*",
|
||||||
|
|||||||
Reference in New Issue
Block a user