邮件

邮件(Mail)

你可以使用 @adonisjs/mail 包从 AdonisJS 应用发送电子邮件。该邮件包构建于 Nodemailer 之上,相比 Nodemailer 带来了以下使用体验上的改进:

  • 用于配置邮件内容的流式(Fluent)API。
  • 可将邮件定义为类,从而获得更好的组织方式与更易测试的代码。
  • 一套由官方维护的丰富传输器(transports)。包括 smtpsesmailgunsparkpostresendbrevo
  • 借助 Fakes API 获得更好的测试体验。
  • 用于排队发送邮件的邮件信使(Mail messenger)。
  • 用于生成日历事件的函数式 API。

安装

使用以下命令安装并配置该包:

node ace add @adonisjs/mail
# Pre-define transports to use via CLI flag
node ace add @adonisjs/mail --transports=resend --transports=smtp
  1. 使用检测到的包管理器安装 @adonisjs/mail 包。

  2. adonisrc.ts 文件中注册以下服务提供者(service provider)与命令(command)。

    {
    commands: [
    // ...other commands
    () => import('@adonisjs/mail/commands')
    ],
    providers: [
    // ...other providers
    () => import('@adonisjs/mail/mail_provider')
    ]
    }
  3. 创建 config/mail.ts 文件。

  4. 为所选邮件服务定义环境变量及其校验规则。

配置

邮件包的配置保存在 config/mail.ts 文件中。在该文件内,你可以将多个邮件服务配置为 mailers 以便在应用中使用。

参见:配置桩(Config stub)

import env from '#start/env'
import { defineConfig, transports } from '@adonisjs/mail'
const mailConfig = defineConfig({
default: 'smtp',
/**
* A static address for the "from" property. It will be
* used unless an explicit from address is set on the
* Email
*/
from: {
address: '',
name: '',
},
/**
* A static address for the "reply-to" property. It will
* be used unless an explicit replyTo address is set on the
* Email
*/
replyTo: {
address: '',
name: '',
},
/**
* The mailers object can be used to configure multiple mailers
* each using a different transport or the same transport with different
* options.
*/
mailers: {
smtp: transports.smtp({
host: env.get('SMTP_HOST'),
port: env.get('SMTP_PORT'),
}),
resend: transports.resend({
key: env.get('RESEND_API_KEY'),
baseUrl: 'https://api.resend.com',
}),
},
})

default

默认用于发送邮件的 mailer 名称。

from

用于 from 属性的全局静态地址。除非在邮件上显式定义了 from 地址,否则将使用此全局地址。

replyTo

用于 reply-to 属性的全局静态地址。除非在邮件上显式定义了 replyTo 地址,否则将使用此全局地址。

mailers

mailers 对象用于配置一个或多个用于发送邮件的 mailer。你可以在运行时使用 mail.use 方法在多个 mailer 之间切换。

传输器配置

以下是官方支持的传输器所接受的配置选项的完整参考。

参见:配置对象的 TypeScript 类型


以下配置选项会被发送到 Mailgun 的 /messages.mime API 端点。

{
mailers: {
mailgun: transports.mailgun({
baseUrl: 'https://api.mailgun.net/v3',
key: env.get('MAILGUN_API_KEY'),
domain: env.get('MAILGUN_DOMAIN'),
/**
* The following options can be overridden at
* runtime when calling the `mail.send` method.
*/
oDkim: true,
oTags: ['transactional', 'adonisjs_app'],
oDeliverytime: new Date(2024, 8, 18),
oTestMode: false,
oTracking: false,
oTrackingClick: false,
oTrackingOpens: false,
headers: {
// h:prefixed headers
},
variables: {
appId: '',
userId: '',
// v:prefixed variables
}
})
}
}

以下配置选项会原样转发给 Nodemailer。因此也请查阅 Nodemailer 文档

{
mailers: {
smtp: transports.smtp({
host: env.get('SMTP_HOST'),
port: env.get('SMTP_PORT'),
secure: false,
auth: {
type: 'login',
user: env.get('SMTP_USERNAME'),
pass: env.get('SMTP_PASSWORD')
},
tls: {},
ignoreTLS: false,
requireTLS: false,
pool: false,
maxConnections: 5,
maxMessages: 100,
})
}
}

以下配置选项会原样转发给 Nodemailer。因此也请查阅 Nodemailer SES 传输器文档

请务必安装 @aws-sdk/client-ses 包以使用 SES 传输器。

{
mailers: {
ses: transports.ses({
/**
* Forwarded to aws sdk
*/
apiVersion: '2010-12-01',
region: 'us-east-1',
credentials: {
accessKeyId: env.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: env.get('AWS_SECRET_ACCESS_KEY'),
},
/**
* Nodemailer specific
*/
sendingRate: 10,
maxConnections: 5,
})
}
}

以下配置选项会被发送到 SparkPost 的 /transmissions API 端点。

{
mailers: {
sparkpost: transports.sparkpost({
baseUrl: 'https://api.sparkpost.com/api/v1',
key: env.get('SPARKPOST_API_KEY'),
/**
* The following options can be overridden at
* runtime when calling the `mail.send` method.
*/
startTime: new Date(),
openTracking: false,
clickTracking: false,
initialOpen: false,
transactional: true,
sandbox: false,
skipSuppression: false,
ipPool: '',
})
}
}

以下配置选项会被发送到 Resend 的 /emails API 端点。

{
mailers: {
resend: transports.resend({
baseUrl: 'https://api.resend.com',
key: env.get('RESEND_API_KEY'),
/**
* The following options can be overridden at
* runtime when calling the `mail.send` method.
*/
tags: [
{
name: 'category',
value: 'confirm_email'
}
]
})
}
}

基础示例

初始配置完成后,你可以使用 mail.send 方法发送邮件。邮件服务是使用配置文件创建的 MailManager 类的单例(singleton)实例。

mail.send 方法会向回调传入 Message 类的实例,并使用配置文件中配置的 default mailer 投递邮件。

在以下示例中,我们在创建新用户账户后从控制器触发一封邮件。

import User from '#models/user'
import { HttpContext } from '@adonisjs/core/http'
import mail from '@adonisjs/mail/services/main'
export default class UsersController {
async store({ request }: HttpContext) {
/**
* For demonstration only. You should validate the data
* before storing it inside the database.
*/
const user = await User.create(request.all())
await mail.send((message) => {
message
.to(user.email)
.from('info@example.org')
.subject('Verify your email address')
.htmlView('emails/verify_email', { user })
})
}
}

排队发送邮件

由于发送邮件可能比较耗时,你可能希望将它们推入队列,在后台发送。你可以使用 mail.sendLater 方法实现这一点。

sendLater 方法接受的参数与 send 方法相同。不过,它不会立即发送邮件,而是使用**邮件信使(Mail messenger)**将其加入队列。

await mail.send((message) => {
await mail.sendLater((message) => {
message
.to(user.email)
.from('info@example.org')
.subject('Verify your email address')
.htmlView('emails/verify_email', { user })
})

默认情况下,邮件信使使用内存队列,这意味着如果你的进程在还有待处理任务时崩溃,队列会丢弃这些任务。如果你的应用界面允许用户通过手动操作重新发送邮件,这可能不是大问题。不过,你始终可以配置自定义的邮件信使,使用基于数据库的队列。

使用 bullmq 排队发送邮件

npm i bullmq

在以下示例中,我们使用 mail.setMessenger 方法配置一个自定义队列,它在底层使用 bullmq 存储任务(job)。

我们将编译后的邮件、运行时配置以及 mailer 名称存储在任务中。稍后,我们将在工作进程(worker)中使用这些数据发送邮件。

import { Queue } from 'bullmq'
import mail from '@adonisjs/mail/services/main'
const emailsQueue = new Queue('emails')
mail.setMessenger((mailer) => {
return {
async queue(mailMessage, config) {
await emailsQueue.add('send_email', {
mailMessage,
config,
mailerName: mailer.name,
})
}
}
})

最后,我们来编写队列 Worker 的代码。根据你的应用工作流程,你可能需要启动另一个进程来让 worker 处理任务。

在以下示例中:

  • 我们从 emails 队列中处理名为 send_email 的任务。
  • 从任务数据中获取编译后的邮件消息、运行时配置以及 mailer 名称。
  • 使用 mailer.sendCompiled 方法发送邮件。
import { Worker } from 'bullmq'
import mail from '@adonisjs/mail/services/main'
new Worker('emails', async (job) => {
if (job.name === 'send_email') {
const {
mailMessage,
config,
mailerName
} = job.data
await mail
.use(mailerName)
.sendCompiled(mailMessage, config)
}
})

就这样!你可以继续使用 mail.sendLater 方法。不过这一次,邮件将被排入一个 Redis 数据库中的队列。

在多个 mailer 之间切换

你可以使用 mail.use 方法在已配置的 mailer 之间切换。mail.use 方法接受 mailer 的名称(即在配置文件中定义的名称),并返回 Mailer 类的实例。

import mail from '@adonisjs/mail/services/main'
mail.use() // Instance of default mailer
mail.use('mailgun') // Mailgun mailer instance

你可以调用 mailer.sendmailer.sendLater 方法来使用某个 mailer 实例发送邮件。例如:

await mail
.use('mailgun')
.send((message) => {
})
await mail
.use('mailgun')
.sendLater((message) => {
})

mailer 实例会在进程的整个生命周期内被缓存。你可以使用 mail.close 方法销毁现有实例,并从头创建一个新的实例。

import mail from '@adonisjs/mail/services/main'
/**
* Close transport and remove instance from
* cache
*/
await mail.close('mailgun')
/**
* Create a fresh instance
*/
mail.use('mailgun')

配置模板引擎

默认情况下,邮件包被配置为使用 Edge 模板引擎 来定义邮件的 HTML纯文本(Plain text) 内容。

不过,如下例所示,你也可以通过覆盖 Message.templateEngine 属性来注册自定义的模板引擎。

参见:定义邮件内容

import { Message } from '@adonisjs/mail'
Message.templateEngine = {
async render(templatePath, data) {
return someTemplateEngine.render(templatePath, data)
}
}

事件

请查阅 事件参考指南 以查看 @adonisjs/mail 包派发的事件列表。

配置邮件消息

邮件的属性使用 Message 类定义。使用 mail.sendmail.sendLater 方法创建的回调函数会收到该类的一个实例。

import { Message } from '@adonisjs/mail'
import mail from '@adonisjs/mail/services/main'
await mail.send((message) => {
console.log(message instanceof Message) // true
})
await mail.sendLater((message) => {
console.log(message instanceof Message) // true
})

定义主题与发件人

你可以使用 message.subject 方法定义邮件主题,使用 message.from 方法定义发件人。

await mail.send((message) => {
message
.subject('Verify your email address')
.from('info@example.org')
})

from 方法接受字符串形式的电子邮件地址,或一个包含发件人名称与电子邮件地址的对象。

message
.from({
address: 'info@example.com',
name: 'AdonisJS'
})

发件人也可以在配置文件中全局定义。如果没有为单封邮件显式定义发件人,则会使用全局发件人。

const mailConfig = defineConfig({
from: {
address: 'info@example.com',
name: 'AdonisJS'
}
})

定义收件人

你可以使用 message.tomessage.ccmessage.bcc 方法定义邮件收件人。这些方法接受字符串形式的电子邮件地址,或一个包含收件人名称与电子邮件地址的对象。

await mail.send((message) => {
message
.to(user.email)
.cc(user.team.email)
.bcc(user.team.admin.email)
})
await mail.send((message) => {
message
.to({
address: user.email,
name: user.fullName,
})
.cc({
address: user.team.email,
name: user.team.name,
})
.bcc({
address: user.team.admin.email,
name: user.team.admin.fullName,
})
})

你可以将多个 ccbcc 收件人定义为电子邮件地址数组,或包含收件人名称与电子邮件地址的对象数组。

await mail.send((message) => {
message
.cc(['first@example.com', 'second@example.com'])
.bcc([
{
name: 'First recipient',
address: 'first@example.com'
},
{
name: 'Second recipient',
address: 'second@example.com'
}
])
})

你也可以使用 message.replyTo 方法定义 replyTo 电子邮件地址。

await mail.send((message) => {
message
.from('info@example.org')
.replyTo('noreply@example.org')
})

定义邮件内容

你可以使用 message.htmlmessage.text 方法定义邮件的 HTML纯文本 内容。

await mail.send((message) => {
/**
* HTML contents
*/
message.html(`
<h1> Verify email address </h1>
<p> <a href="https://myapp.com">Click here</a> to verify your email address </a>
`)
/**
* Plain text contents
*/
message.text(`
Verify email address
Please visit https://myapp.com to verify your email address
`)
})

使用 Edge 模板

由于在行内编写内容可能比较繁琐,你可以改用 Edge 模板。如果你已经配置好 Edge,可以使用 message.htmlViewmessage.textView 方法渲染模板。

Create templates
node ace make:view emails/verify_email_html
node ace make:view emails/verify_email_text
Use them for defining contents
await mail.send((message) => {
message.htmlView('emails/verify_email_html', stateToShare)
message.textView('emails/verify_email_text', stateToShare)
})

使用 MJML 编写邮件标记

MJML 是一种用于创建电子邮件的标记语言,无需编写所有复杂的 HTML 即可让你的邮件在每个邮件客户端中都呈现良好。

第一步是从 npm 安装 mjml 包。

npm i mjml

完成后,你可以通过将其包裹在 @mjml 标签中,在 Edge 模板内编写 MJML 标记。

由于 MJML 的输出包含 htmlheadbody 标签,因此无需在你的 Edge 模板中定义它们。

@mjml()
<mjml>
<mj-body>
<mj-section>
<mj-column>
<mj-text>
Hello World!
</mj-text>
</mj-column>
</mj-section>
</mj-body>
</mjml>
@end

你可以将 MJML 配置选项 作为属性传递给 @mjml 标签。

@mjml({
keepComments: false,
fonts: {
Lato: 'https://fonts.googleapis.com/css?family=Lato:400,500,700'
}
})

添加附件

你可以使用 message.attach 方法在邮件中发送附件。attach 方法接受你想要作为附件发送的文件的绝对路径或文件系统 URL。

import app from '@adonisjs/core/services/app'
await mail.send((message) => {
message.attach(app.makePath('uploads/invoice.pdf'))
})

你可以使用 options.filename 属性为附件定义文件名。

message.attach(app.makePath('uploads/invoice.pdf'), {
filename: 'invoice_october_2023.pdf'
})

message.attach 方法接受的完整选项列表如下。

选项 描述
filename 附件的显示名称。默认值为附件路径的基本名称(basename)。
contentType 附件的内容类型。若未设置,将根据文件扩展名推断 contentType
contentDisposition 附件的内容处置(Content-Disposition)类型。默认值为 attachment
headers

附件节点的自定义请求头。headers 属性是一个键值对。

从流和缓冲区添加附件

你可以使用 message.attachData 方法从流和缓冲区创建邮件附件。该方法接受可读流或缓冲区作为第一个参数,选项对象作为第二个参数。

在使用 mail.sendLater 方法排队发送邮件时,不应使用 message.attachData 方法。由于排队任务会被序列化并持久化到数据库中,附加原始数据会增大存储体积。

此外,如果使用 message.attachData 方法附加流,排队发送邮件将会失败。

message.attachData(fs.createReadStream('./invoice.pdf'), {
filename: 'invoice_october_2023.pdf'
})
message.attachData(Buffer.from('aGVsbG8gd29ybGQh'), {
encoding: 'base64',
filename: 'greeting.txt',
})

嵌入图片

你可以使用 embedImage 视图(view)助手在邮件内容中嵌入图片。embedImage 方法在底层使用 CID 将图片标记为附件,并使用其内容 ID 作为图片的来源(source)。

<img src="{{
embedImage(app.makePath('assets/hero.jpg'))
}}" />

对应的输出 HTML 如下:

<img src="cid:a-random-content-id" />

以下附件会被自动定义在邮件负载(payload)上。

{
attachments: [{
path: '/root/app/assets/hero.jpg',
filename: 'hero.jpg',
cid: 'a-random-content-id'
}]
}

从缓冲区嵌入图片

embedImage 方法类似,你可以使用 embedImageData 方法从原始数据嵌入图片。

<img src="{{
embedImageData(rawBuffer, { filename: 'hero.jpg' })
}}" />

添加日历事件

你可以使用 message.icalEvent 方法将日历事件附加到邮件。icalEvent 方法接受事件内容作为第一个参数,选项对象作为第二个参数。

const contents = 'BEGIN:VCALENDAR\r\nPRODID:-//ACME/DesktopCalendar//EN\r\nMETHOD:REQUEST\r\n...'
await mail.send((message) => {
message.icalEvent(contents, {
method: 'PUBLISH',
filename: 'invite.ics',
})
})

由于手动定义事件文件内容可能比较繁琐,你可以向 icalEvent 方法传入回调函数来使用 JavaScript API 生成邀请内容。

传给回调函数的 calendar 对象是 ical-generator npm 包的引用,因此也请务必阅读该包的 README 文件。

message.icalEvent((calendar) => {
calendar
.createEvent({
summary: 'Adding support for ALS',
start: DateTime.local().plus({ minutes: 30 }),
end: DateTime.local().plus({ minutes: 60 }),
})
}, {
method: 'PUBLISH',
filename: 'invite.ics',
})

从文件或 URL 读取邀请内容

你可以使用 icalEventFromFileicalEventFromUrl 方法从文件或 HTTP URL 定义邀请内容。

message.icalEventFromFile(
app.resourcesPath('calendar-invites/invite.ics'),
{
filename: 'invite.ics',
method: 'PUBLISH'
}
)
message.icalEventFromUrl(
'https://myapp.com/users/1/invite.ics',
{
filename: 'invite.ics',
method: 'PUBLISH'
}
)

定义邮件请求头

你可以使用 message.header 方法定义额外的邮件请求头。该方法接受请求头键名作为第一个参数,值作为第二个参数。

message.header('x-my-key', 'header value')
/**
* Define an array of values
*/
message.header('x-my-key', ['header value', 'another value'])

默认情况下,邮件请求头会被编码并折叠,以满足"纯 ASCII 消息、每行不超过 78 字节"的要求。不过,如果你想绕过编码规则,可以使用 message.preparedHeader 方法设置请求头。

message.preparedHeader(
'x-unprocessed',
'a really long header or value with non-ascii characters 👮',
)

定义 List 请求头

Message 类包含一些辅助方法,可轻松定义复杂的请求头,如 List-UnsubscribeList-Help。你可以访问 nodemailer 网站 了解 List 请求头的编码规则。

message.listHelp('admin@example.com?subject=help')
// List-Help: <mailto:admin@example.com?subject=help>
message.listUnsubscribe({
url: 'http://example.com',
comment: 'Comment'
})
// List-Unsubscribe: <http://example.com> (Comment)
/**
* Repeating header multiple times
*/
message.listSubscribe('admin@example.com?subject=subscribe')
message.listSubscribe({
url: 'http://example.com',
comment: 'Subscribe'
})
// List-Subscribe: <mailto:admin@example.com?subject=subscribe>
// List-Subscribe: <http://example.com> (Subscribe)

对于所有其他任意的 List 请求头,你可以使用 addListHeader 方法。

message.addListHeader('post', 'http://example.com/post')
// List-Post: <http://example.com/post>

基于类的邮件

除了在 mail.send 方法的闭包中编写邮件外,你还可以将它们移到专用的邮件类中,以获得更好的组织方式和更易测试的代码

邮件类存放在 ./app/mails 目录下,每个文件代表一封邮件。你可以通过运行 make:mail ace 命令来创建邮件类。

参见:创建邮件命令

node ace make:mail verify_email

邮件类继承自 BaseMail 类,并通过以下属性和方法进行脚手架生成。你可以在 prepare 方法内使用 this.message 属性配置邮件消息。

import User from '#models/user'
import { BaseMail } from '@adonisjs/mail'
export default class VerifyEmailNotification extends BaseMail {
from = 'sender_email@example.org'
subject = 'Verify email'
prepare() {
this.message.to('user_email@example.org')
}
}

from

配置发件人的电子邮件地址。如果省略该属性,则必须调用 message.from 方法来定义发件人。

subject

配置邮件主题。如果省略该属性,则必须使用 message.subject 方法来定义邮件主题。

replyTo

配置 replyTo 电子邮件地址。

prepare

prepare 方法由 build 方法自动调用,用于准备待发送的邮件消息。你必须在该方法内定义邮件内容、附件、收件人等。

build Inherited

build 方法继承自 BaseMail 类。该方法在发送邮件时自动调用。如果你决定覆盖该方法,请务必参考原始实现

使用邮件类发送邮件

你可以调用 mail.send 方法并传入邮件类的实例来发送邮件。例如:

Send mail
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'
await mail.send(new VerifyEmailNotification())
Queue mail
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'
await mail.sendLater(new VerifyEmailNotification())

你可以使用构造函数参数与邮件类共享数据。例如:

/**
* Creating a user
*/
const user = await User.create(payload)
await mail.send(
/**
* Passing user to the mail class
*/
new VerifyEmailNotification(user)
)

测试邮件类

使用邮件类的主要好处之一是更好的测试体验。你可以在不发送邮件的情况下构建邮件类,并针对消息属性编写断言(assertion)。

import { test } from '@japa/runner'
import VerifyEmailNotification from '#mails/verify_email'
test.group('Verify email notification', () => {
test('prepare email for sending', async () => {
const email = new VerifyEmailNotification()
/**
* Build email message and render templates to
* compute the email HTML and plain text
* contents
*/
await email.buildWithContents()
/**
* Write assertions to ensure the message is built
* as expected
*/
email.message.assertTo('user@example.org')
email.message.assertFrom('info@example.org')
email.message.assertSubject('Verify email address')
email.message.assertReplyTo('no-reply@example.org')
})
})

你也可以针对消息内容编写断言,如下所示。

const email = new VerifyEmailNotification()
await email.buildWithContents()
email.message.assertHtmlIncludes(
`<a href="/emails/1/verify"> Verify email address </a>`
)
email.message.assertTextIncludes('Verify email address')

此外,你还可以针对附件编写断言。这些断言仅适用于基于文件的附件,不适用于流或原始内容。

const email = new VerifyEmailNotification()
await email.buildWithContents()
email.message.assertAttachment(
app.makePath('uploads/invoice.pdf')
)

欢迎查看 Message 类的源代码,了解所有可用的断言方法。

伪造邮件器(Fake mailer)

你可能希望在测试期间使用伪造(Fake)邮件器,以防止应用发送邮件。伪造邮件器会在内存中收集所有外发邮件,并提供易于使用的 API 来对其编写断言。

在以下示例中:

  • 我们首先使用 mail.fake 方法创建 FakeMailer 的实例。
  • 接着,我们调用 /register 端点 API。
  • 最后,我们使用伪造邮件器的 mails 属性断言已发送 VerifyEmailNotification
import { test } from '@japa/runner'
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'
test.group('Users | register', () => {
test('create a new user account', async ({ client, route }) => {
/**
* Turn on the fake mode
*/
const { mails } = mail.fake()
/**
* Make an API call
*/
await client
.post(route('users.store'))
.send(userData)
/**
* Assert the controller indeed sent the
* VerifyEmailNotification mail
*/
mails.assertSent(VerifyEmailNotification, ({ message }) => {
return message
.hasTo(userData.email)
.hasSubject('Verify email address')
})
})
})

编写完测试后,你必须使用 mail.restore 方法恢复(restore)伪造状态。

test('create a new user account', async ({ client, route, cleanup }) => {
const { mails } = mail.fake()
/**
* The cleanup hooks are executed after the test
* finishes successfully or with an error.
*/
cleanup(() => {
mail.restore()
})
})

编写断言

mails.assertSent 方法接受邮件类构造函数作为第一个参数,当找不到任何预期类的邮件时会抛出异常。

const { mails } = mail.fake()
/**
* Assert the email was sent
*/
mails.assertSent(VerifyEmailNotification)

你可以向 assertSent 方法传入回调函数来进一步检查邮件是否发送给了预期的收件人,或是否具有正确的主题。

回调函数会收到邮件类的一个实例,你可以使用 .message 属性来访问 消息 对象。

mails.assertSent(VerifyEmailNotification, (email) => {
return email.message.hasTo(userData.email)
})

你可以在回调中对 message 对象运行断言。例如:

mails.assertSent(VerifyEmailNotification, (email) => {
email.message.assertTo(userData.email)
email.message.assertFrom('info@example.org')
email.message.assertSubject('Verify your email address')
/**
* All assertions passed, so return true to consider the
* email as sent.
*/
return true
})

断言邮件未被发送

你可以使用 mails.assertNotSent 方法断言测试期间邮件未被发送。该方法与 assertSent 方法相反,接受相同的参数。

const { mails } = mail.fake()
mails.assertNotSent(PasswordResetNotification)

断言邮件数量

最后,你可以使用 assertSentCountassertNoneSent 方法断言已发送邮件的数量。

const { mails } = mail.fake()
// Assert 2 emails were sent in total
mails.assertSentCount(2)
// Assert only one VerifyEmailNotification was sent
mails.assertSentCount(VerifyEmailNotification, 1)
const { mails } = mail.fake()
// Assert zero emails were sent
mails.assertNoneSent()

为排队的邮件编写断言

如果你使用 mail.sendLater 方法对邮件进行了排队,可以使用以下方法为其编写断言。

const { mails } = mail.fake()
/**
* Assert "VerifyEmailNotification" email was queued
* Optionally, you may pass the finder function to
* narrow down the email
*/
mails.assertQueued(VerifyEmailNotification)
/**
* Assert "VerifyEmailNotification" email was not queued
* Optionally, you may pass the finder function to
* narrow down the email
*/
mails.assertNotQueued(PasswordResetNotification)
/**
* Assert two emails were queued in total.
*/
mails.assertQueuedCount(2)
/**
* Assert "VerifyEmailNotification" email was queued
* only once
*/
mails.assertQueuedCount(VerifyEmailNotification , 1)
/**
* Assert nothing was queued
*/
mails.assertNoneQueued()

获取已发送或已排队邮件的列表

你可以使用 mails.sentmails.queued 方法获取测试期间发送/排队的邮件数组。

const { mails } = mail.fake()
const sentEmails = mails.sent()
const queuedEmails = mails.queued()
const email = sentEmails.find((email) => {
return email instanceof VerifyEmailNotification
})
if (email) {
email.message.assertTo(userData.email)
email.message.assertFrom(userData.email)
email.message.assertHtmlIncludes('<a href="/verify/email"> Verify your email address</a>')
}

创建自定义传输器(transports)

AdonisJS 邮件传输器构建于 Nodemailer 传输器 之上;因此,在将其注册到邮件包之前,你必须先创建/使用一个 nodemailer 传输器。

在本指南中,我们将把 nodemailer-postmark-transport 封装为 AdonisJS 邮件传输器。

npm i nodemailer nodemailer-postmark-transport

正如下例所示,发送邮件的重活由 Nodemailer 完成。AdonisJS 传输器充当适配器,将消息转发给 nodemailer,并将其响应规范化(normalize)为 MailResponse 类的实例。

import nodemailer from 'nodemailer'
import nodemailerTransport from 'nodemailer-postmark-transport'
import { MailResponse } from '@adonisjs/mail'
import type {
NodeMailerMessage,
MailTransportContract
} from '@adonisjs/mail/types'
/**
* Configuration accepted by the transport
*/
export type PostmarkConfig = {
auth: {
apiKey: string
}
}
/**
* Transport implementation
*/
export class PostmarkTransport implements MailTransportContract {
#config: PostmarkConfig
constructor(config: PostmarkConfig) {
this.#config = config
}
#createNodemailerTransport(config: PostmarkConfig) {
return nodemailer.createTransport(nodemailerTransport(config))
}
async send(
message: NodeMailerMessage,
config?: PostmarkConfig
): Promise<MailResponse> {
/**
* Create nodemailer transport
*/
const transporter = this.#createNodemailerTransport({
...this.#config,
...config,
})
/**
* Send email
*/
const response = await transporter.sendMail(message)
/**
* Normalize response to an instance of the "MailResponse" class
*/
return new MailResponse(response.messageId, response.envelope, response)
}
}

创建配置工厂函数

要在 config/mail.ts 文件中引用上述传输器,你必须创建一个返回该传输器实例的工厂函数。

你可以将以下代码写在传输器实现的同一文件中。

import type {
NodeMailerMessage,
MailTransportContract,
MailManagerTransportFactory
} from '@adonisjs/mail/types'
export function PostmarkTransport(
config: PostmarkConfig
): MailManagerTransportFactory {
return () => {
return new PostmarkTransport(config)
}
}

使用传输器

最后,你可以使用 postmarkTransport 辅助函数在配置文件中引用该传输器。

import env from '#start/env'
import { defineConfig } from '@adonisjs/mail'
import { postmarkTransport } from 'my-custom-package'
const mailConfig = defineConfig({
mailers: {
postmark: postmarkTransport({
auth: {
apiKey: env.get('POSTMARK_API_KEY'),
},
}),
},
})