feat: create email-nodemailer package
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
"build:create-payload-app": "turbo build --filter create-payload-app",
|
||||
"build:db-mongodb": "turbo build --filter db-mongodb",
|
||||
"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:graphql": "turbo build --filter graphql",
|
||||
"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
|
||||
58
packages/email-nodemailer/package.json
Normal file
58
packages/email-nodemailer/package.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"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": {
|
||||
"@types/nodemailer": "6.4.14"
|
||||
}
|
||||
}
|
||||
126
packages/email-nodemailer/src/index.ts
Normal file
126
packages/email-nodemailer/src/index.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { SendMailOptions, Transporter } from 'nodemailer'
|
||||
import type SMTPConnection from 'nodemailer/lib/smtp-connection'
|
||||
import type { EmailAdapter } from 'payload/types'
|
||||
|
||||
import nodemailer from 'nodemailer'
|
||||
import { InvalidConfiguration } from 'payload/errors'
|
||||
|
||||
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: 'info@payloadcms.com',
|
||||
defaultFromName: 'Payload',
|
||||
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' })
|
||||
}
|
||||
}
|
||||
18
packages/email-nodemailer/tsconfig.json
Normal file
18
packages/email-nodemailer/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"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. */,
|
||||
},
|
||||
"exclude": [
|
||||
"dist",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.tsx"
|
||||
],
|
||||
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"],
|
||||
}
|
||||
@@ -531,7 +531,7 @@ export type Config = {
|
||||
*
|
||||
* @see https://payloadcms.com/docs/email/overview
|
||||
*/
|
||||
email?: EmailAdapter<any, unknown>
|
||||
email?: EmailAdapter<any, unknown> | Promise<EmailAdapter<any, unknown>>
|
||||
/** Custom REST endpoints */
|
||||
endpoints?: Endpoint[]
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,9 @@ export type * from '../admin/types.js'
|
||||
export type * from '../uploads/types.js'
|
||||
|
||||
export type { DocumentPermissions, FieldPermissions } from '../auth/index.js'
|
||||
|
||||
export type { MeOperationResult } from '../auth/operations/me.js'
|
||||
export type { EmailAdapter } from '../email/types.js'
|
||||
|
||||
export type {
|
||||
CollapsedPreferences,
|
||||
|
||||
@@ -49,9 +49,6 @@ import { APIKeyAuthentication } from './auth/strategies/apiKey.js'
|
||||
import { JWTAuthentication } from './auth/strategies/jwt.js'
|
||||
import localOperations from './collections/operations/local/index.js'
|
||||
import { validateSchema } from './config/validate.js'
|
||||
import { createNodemailerAdapter } from './email/adapters/nodemailer/index.js'
|
||||
import { emailDefaults } from './email/defaults.js'
|
||||
import { sendEmail } from './email/sendEmail.js'
|
||||
import { fieldAffectsData } from './exports/types.js'
|
||||
import localGlobalOperations from './globals/operations/local/index.js'
|
||||
import flattenFields from './utilities/flattenTopLevelFields.js'
|
||||
@@ -366,8 +363,12 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
|
||||
await this.db.connect()
|
||||
}
|
||||
|
||||
// TODO: Move nodemailer adapter into separate package after verifying all existing functionality
|
||||
this.email = await createNodemailerAdapter(emailDefaults)
|
||||
// Load email adapter
|
||||
if (this.config.email instanceof Promise) {
|
||||
this.email = await this.config.email
|
||||
} else {
|
||||
this.email = this.config.email
|
||||
}
|
||||
|
||||
this.sendEmail = this.email.sendEmail
|
||||
|
||||
|
||||
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@@ -440,6 +440,19 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
|
||||
packages/email-nodemailer:
|
||||
dependencies:
|
||||
nodemailer:
|
||||
specifier: 6.9.10
|
||||
version: 6.9.10
|
||||
payload:
|
||||
specifier: workspace:*
|
||||
version: link:../payload
|
||||
devDependencies:
|
||||
'@types/nodemailer':
|
||||
specifier: 6.4.14
|
||||
version: 6.4.14
|
||||
|
||||
packages/eslint-config-payload:
|
||||
dependencies:
|
||||
'@types/eslint':
|
||||
@@ -1502,6 +1515,9 @@ importers:
|
||||
'@payloadcms/db-postgres':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/db-postgres
|
||||
'@payloadcms/email-nodemailer':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/email-nodemailer
|
||||
'@payloadcms/eslint-config':
|
||||
specifier: workspace:*
|
||||
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
|
||||
40
test/email-nodemailer/config.ts
Normal file
40
test/email-nodemailer/config.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { createNodemailerAdapter } from '@payloadcms/email-nodemailer'
|
||||
import path from 'path'
|
||||
import { getFileByPath } from 'payload/uploads'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
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',
|
||||
})
|
||||
|
||||
// Create image
|
||||
const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
||||
const imageFile = await getFileByPath(imageFilePath)
|
||||
|
||||
await payload.create({
|
||||
collection: 'media',
|
||||
data: {},
|
||||
file: imageFile,
|
||||
})
|
||||
},
|
||||
})
|
||||
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"
|
||||
]
|
||||
}
|
||||
@@ -31,6 +31,11 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
})
|
||||
|
||||
const email = await payload.sendEmail({
|
||||
to: 'test@example.com',
|
||||
subject: 'This was sent on init',
|
||||
})
|
||||
|
||||
// Create image
|
||||
const imageFilePath = path.resolve(dirname, '../uploads/image.png')
|
||||
const imageFile = await getFileByPath(imageFilePath)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"@payloadcms/db-mongodb": "workspace:*",
|
||||
"@payloadcms/db-postgres": "workspace:*",
|
||||
"@payloadcms/eslint-config": "workspace:*",
|
||||
"@payloadcms/email-nodemailer": "workspace:*",
|
||||
"@payloadcms/graphql": "workspace:*",
|
||||
"@payloadcms/live-preview": "workspace:*",
|
||||
"@payloadcms/live-preview-react": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user