From 5d6a3bc8335134618722b81f3fa0c90043476185 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sun, 3 Jan 2021 03:13:23 -0500 Subject: [PATCH 1/5] refactor: email build changed to be more readable and work according to docs --- src/config/types.ts | 39 ++++++++++++++++++++-------- src/email/build.ts | 62 ++++++++++++++++++++++----------------------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/src/config/types.ts b/src/config/types.ts index 20f7c6d8b7..0c28447ee0 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -11,20 +11,39 @@ import { PayloadRequest } from '../express/types'; import InitializeGraphQL from '../graphql'; import { Where } from '../types'; -type MockEmailTransport = { - transport?: 'mock'; - fromName?: string; - fromAddress?: string; -}; - -type ValidEmailTransport = { - transport: Transporter; - transportOptions?: SMTPConnection.Options; +type Email = { fromName: string; fromAddress: string; +} + +export type EmailTransport = Email & { + transport: Transporter; + transportOptions?: SMTPConnection.Options; }; -export type EmailOptions = ValidEmailTransport | MockEmailTransport; +export type EmailTransportOptions = Email & { + transport?: Transporter; + transportOptions: SMTPConnection.Options; +}; + +export type EmailOptions = EmailTransport | EmailTransportOptions | Email; + +/** + * 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 InitOptions = { express?: Express; diff --git a/src/email/build.ts b/src/email/build.ts index 4c8f6ec3fa..5aebdf76af 100644 --- a/src/email/build.ts +++ b/src/email/build.ts @@ -1,5 +1,5 @@ import nodemailer, { Transporter } from 'nodemailer'; -import { EmailOptions } from '../config/types'; +import { EmailOptions, EmailTransport, hasTransport, hasTransportOptions } from '../config/types'; import { InvalidConfiguration } from '../errors'; import mockHandler from './mockHandler'; import Logger from '../utilities/logger'; @@ -7,37 +7,9 @@ import { BuildEmailResult } from './types'; const logger = Logger(); -export default async function buildEmail(emailConfig: EmailOptions): BuildEmailResult { - if (!emailConfig.transport || emailConfig.transport === 'mock') { - const mockAccount = await mockHandler(emailConfig); - // Only log mock credentials if was explicitly set in config - if (emailConfig.transport === 'mock') { - const { account: { web, user, pass } } = mockAccount; - 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}`); - } - return mockAccount; - } - - const email = { ...emailConfig }; - - if (!email.fromName || !email.fromAddress) { - throw new InvalidConfiguration('Email fromName and fromAddress must be configured when transport is configured'); - } - - let transport: Transporter; - // TODO: Is this ever populated when not using 'mock'? - if (emailConfig.transport) { - transport = emailConfig.transport; - } else if (emailConfig.transportOptions) { - transport = nodemailer.createTransport(emailConfig.transportOptions); - } - +async function handleTransport(transport: Transporter, email: EmailTransport): BuildEmailResult { try { await transport.verify(); - email.transport = transport; } catch (err) { logger.error( 'There is an error with the email configuration you have provided.', @@ -45,5 +17,33 @@ export default async function buildEmail(emailConfig: EmailOptions): BuildEmailR ); } - return email; + return { ...email, transport }; +} + +export default async function buildEmail(emailConfig: EmailOptions): BuildEmailResult { + + if (!emailConfig.fromName || !emailConfig.fromAddress) { + throw new InvalidConfiguration('Email fromName and fromAddress must be configured when transport is configured'); + } + + if (hasTransport(emailConfig) && emailConfig.transport) { + const email = { ...emailConfig }; + const { transport } : {transport: Transporter} = emailConfig; + return handleTransport(transport, email); + } + + if (hasTransportOptions(emailConfig) && emailConfig.transportOptions) { + const email = { ...emailConfig } as EmailTransport; + const transport = nodemailer.createTransport(emailConfig.transportOptions); + return handleTransport(transport, email); + } + + const mockAccount = await mockHandler(emailConfig); + // Only log mock credentials if was explicitly set in config + const { account: { web, user, pass } } = mockAccount; + 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}`); + return mockAccount; } From 3233e903d97797c0760c48cc40c47c6f9d164664 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sun, 3 Jan 2021 03:15:13 -0500 Subject: [PATCH 2/5] docs: adds email documentation --- docs/Email/overview.mdx | 140 ++++++++++++++++++++++++++++++++++++++-- src/index.ts | 2 +- 2 files changed, 134 insertions(+), 8 deletions(-) diff --git a/docs/Email/overview.mdx b/docs/Email/overview.mdx index bb1f051002..82b1d1ba28 100644 --- a/docs/Email/overview.mdx +++ b/docs/Email/overview.mdx @@ -1,14 +1,140 @@ --- title: Email Functionality -label: Email +label: Overview order: 10 --- -Talk about how the Payload email service works. +### Introduction -- Transport options -- Payload Config (needs #config anchor) -- Nodemailer -- Auth options -- Mock credentials (how to use) +Payload comes ready to send your application's email. Whether you simply need built-in password reset +email to work or you want customers to get an order confirmation email, you're almost there. Payload makes use of +[NodeMailer](https://nodemailer.com) for email and won't get in your way for those already familiar. +For email to send from your Payload server, some configuration is required. The settings you provide will be set +in the `email` property object of your payload init call. Payload will make use of the transport that you have configured for it for things like reset password or verifying new user accounts and email send methods are available to you as well on your payload instance. + +### Configuration + +**Three ways to set it up** + +1. **Default**: When email is not needed, a mock email handler will be created and used when nothing is provided. This is ideal for development environments and can be changed later when ready to [go to production](/docs/production). +1. **Recommended**: Set the `transportOptions` and Payload will do the set up for you. +1. **Advanced**: The `transport` object can be assigned a nodemailer transport object set up in your server scripts and given for Payload to use. + +The following options are configurable in the `email` property object as part of the options object when calling payload.init(). + +| Option | Description | +| ---------------------------- | -------------| +| **`transport`** | The NodeMailer transport object for when you want to do it yourself, not needed when transportOptions is set | +| **`transportOptions`** | An object that configures the transporter that Payload will create. For all the available options see the [NodeMailer documentation](https://nodemailer.com/smtp/) or see the examples below | +| **`fromName`** * | The name part of the From field that will be seen on the delivered email | +| **`fromAddress`** * | The email address part of the From field that will be used when delivering email | + +*\* An asterisk denotes that a property is required.* + +### Use SMTP + +Simple Mail Transfer Protocol, also known as SMTP can be passed in using the `transportOptions` object on the `email` options. + +**Example email part using SMTP:** +```js +payload.init({ + email: { + transportOptions: { + host: process.env.SMTP_HOST, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + }, + port: 587, + secure: true, // use TLS + tls: { + // do not fail on invalid certs + rejectUnauthorized: false + } + }, + fromName: 'hello', + fromAddress: 'hello@example.com' + } + // ... +``` + + + It is best practice to avoid saving credentials or API keys directly in your code, use environment variables. + + +### Use an email service + +Many third party mail providers are available and offer benefits beyond basic SMTP. As an example your payload init could look this if you wanted to use SendGrid.com though the same approach would work for any other [NodeMailer transports](https://nodemailer.com/transports/) shown here or provided by another third party. + +```js +const nodemailerSendgrid = require('nodemailer-sendgrid'); +const payload = require('@payloadcms/payload'); + +const sendGridAPIKey = process.env.SENDGRID_API_KEY; + +payload.init({ + ...sendGridAPIKey ? { + email: { + transportOptions: nodemailerSendgrid({ + apiKey: sendGridAPIKey, + }), + fromName: 'Admin', + fromAddress: 'admin@example.com', + }, + } : {}, +}); +``` + +### Use a custom NodeMailer transport +To take full control of the mail transport you may wish to use `nodemailer.createTransport()` on your server and provide it to Payload init. + +```js +const payload = require('@payloadcms/payload'); +const nodemailer = require('nodemailer'); + +const transport = await nodemailer.createTransport({ + host: process.env.SMTP_HOST, + port: 587, + auth: { + user: process.env.SMTP_USER, + pass: process.env.SMTP_PASS + }, +}); + +payload.init({ + email: { + fromName: 'Admin', + fromAddress: 'admin@example.com', + transport + }, + // ... +``` + +### Sending Mail +With a working transport you can call it anywhere you have access to payload by calling `payload.sendEmail(message)`. The `message` will contain the `to`, `subject` and `email` or `text` for the email being sent. To see all available message configuration options see [NodeMailer](https://nodemailer.com/message). + +### Mock transport +By default, Payload uses a mock implementation that only sends mail to the [ethereal](https://ethereal.email) capture service that will never reach a user's inbox. While in development you may wish to make use of the captured messages which is why the payload output during server output helpfully logs this out on the server console. + +**Console output when starting payload with a mock email instance** +``` +[06:37:21] INFO (payload): Starting Payload... +[06:37:22] INFO (payload): Payload Demo Initialized +[06:37:22] INFO (payload): listening on 3000... +[06:37:22] INFO (payload): Connected to Mongo server successfully! +[06:37:23] INFO (payload): E-mail configured with mock configuration +[06:37:23] INFO (payload): Log into mock email provider at https://ethereal.email +[06:37:23] INFO (payload): Mock email account username: hhav5jw7doo4euev@ethereal.email +[06:37:23] INFO (payload): Mock email account password: VNdGcvDZeyEhtuPBqf +``` + +The mock email handler is used when payload is started with neither `transport` or `transportOptions` to know how to deliver email. + + + The randomly generated email account username and password will be different each time the Payload server starts. + + +### Using multiple mail providers + +Payload supports the use of a single transporter of email, but there is nothing stopping you from having more. Consider a use case where sending bulk email is handled differently than transactional email and could be done using a [hook](/docs/hooks/config). diff --git a/src/index.ts b/src/index.ts index e10d459cd5..1b1b0d4574 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,7 +106,7 @@ export class Payload { } this.license = options.license; - this.emailOptions = { ...(options.email || {}) }; + this.emailOptions = { ...(options.email) }; this.secret = crypto .createHash('sha256') .update(options.secret) From 57d2c8602fb81a5d67d34a38c25a0429c2b9c44b Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sun, 3 Jan 2021 03:15:54 -0500 Subject: [PATCH 3/5] fix: demo email start on payload init --- demo/payload.config.ts | 5 ----- demo/server.ts | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/demo/payload.config.ts b/demo/payload.config.ts index 16432937bb..c62d9522e3 100644 --- a/demo/payload.config.ts +++ b/demo/payload.config.ts @@ -50,11 +50,6 @@ export default buildConfig({ }, webpack: (config) => config, }, - email: { - transport: 'mock', - fromName: 'Payload', - fromAddress: 'hello@payloadcms.com', - }, collections: [ Admin, AllFields, diff --git a/demo/server.ts b/demo/server.ts index 098efa7af5..794e3d7dfc 100644 --- a/demo/server.ts +++ b/demo/server.ts @@ -11,6 +11,10 @@ payload.init({ secret: 'SECRET_KEY', mongoURL: 'mongodb://localhost/payload', express: expressApp, + email: { + fromName: 'Payload', + fromAddress: 'hello@payloadcms.com', + }, onInit: (app) => { app.logger.info('Payload Demo Initialized'); }, From cf89d4cb56add645e68cf0be31d943b734dabe39 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sun, 3 Jan 2021 03:17:18 -0500 Subject: [PATCH 4/5] fix: default config value for email removed as the property was moved out of config --- src/config/defaults.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/config/defaults.ts b/src/config/defaults.ts index 208199e443..e7c96c71f2 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -18,11 +18,6 @@ export const defaults = { scss: path.resolve(__dirname, '../admin/scss/overrides.scss'), }, upload: {}, - email: { - transport: 'mock', - fromName: 'Payload CMS', - fromAddress: 'cms@payloadcms.com', - }, graphQL: { mutations: {}, queries: {}, From faec969752622c70e9175cc226d888bf32ec732c Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Sun, 3 Jan 2021 03:20:29 -0500 Subject: [PATCH 5/5] fix: payload config remove types for email --- src/config/types.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/config/types.ts b/src/config/types.ts index 0c28447ee0..53f78bc338 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -55,19 +55,6 @@ export type InitOptions = { onInit?: (payload: Payload) => void; }; -export type SendEmailOptions = { - from: string; - to: string; - subject: string; - html: string; -}; - -export type MockEmailCredentials = { - user: string; - pass: string; - web: string; -}; - export type AccessResult = boolean | Where; export type Access = (args?: any) => AccessResult; @@ -118,7 +105,6 @@ export type PayloadConfig = { }, middleware?: any[] }, - email?: EmailOptions; defaultDepth?: number; maxDepth?: number; rateLimit?: {