Skip to content

邮件

您可以使用 @adonisjs/mail 包从 AdonisJS 应用程序发送电子邮件。邮件包基于 Nodemailer 构建,带来了以下优于 Nodemailer 的生活质量改进。

  • 用于配置邮件消息的流畅 API。
  • 能够将电子邮件定义为类以获得更好的组织和更轻松的测试。
  • 一套广泛的官方维护的传输。包括 smtpsesmailgunsparkpostresendbrevo
  • 使用 Fakes API 改进的测试体验。
  • 用于队列邮件的邮件信使。
  • 用于生成日历事件的功能 API。

安装

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

sh
node ace add @adonisjs/mail

# 通过 CLI 标志预定义要使用的传输
node ace add @adonisjs/mail --transports=resend --transports=smtp

:::disclosure

  1. 使用检测到的包管理器安装 @adonisjs/mail 包。

  2. adonisrc.ts 文件中注册以下服务提供者和命令。

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

  4. 为所选邮件服务定义环境变量及其验证

:::

配置

邮件包的配置存储在 config/mail.ts 文件中。在此文件中,您可以配置多个电子邮件服务作为 mailers 在应用程序中使用。

另请参阅:配置模板

ts
import env from '#start/env'
import { defineConfig, transports } from '@adonisjs/mail'

const mailConfig = defineConfig({
  default: 'smtp',

  /**
   * "from" 属性的静态地址。除非在 Email 上
   * 设置了显式的 from 地址,否则将使用它
   */
  from: {
    address: '',
    name: '',
  },

  /**
   * "reply-to" 属性的静态地址。除非在 Email 上
   * 设置了显式的 replyTo 地址,否则将使用它
   */
  replyTo: {
    address: '',
    name: '',
  },

  /**
   * mailers 对象可用于配置多个 mailer,
   * 每个使用不同的传输或具有不同选项的相同传输。
   */
  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 之间切换。

基本示例

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

mail.send 方法将 Message 类的实例传递给回调,并使用配置文件中配置的 default mailer 传递电子邮件。

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

ts
import User from '#models/user'
import { HttpContext } from '@adonisjs/core/http'
// highlight-start
import mail from '@adonisjs/mail/services/main'
// highlight-end

export default class UsersController {
  async store({ request }: HttpContext) {
    /**
     * 仅用于演示。您应该在将数据存储到
     * 数据库之前验证数据。
     */
    const user = await User.create(request.all())

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

队列邮件

由于发送电子邮件可能很耗时,您可能希望将它们推送到队列并在后台发送电子邮件。您可以使用 mail.sendLater 方法执行相同的操作。

sendLater 方法接受与 send 方法相同的参数。但是,它不是立即发送电子邮件,而是使用邮件信使将其排队。

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

默认情况下,邮件信使使用内存队列,这意味着如果您的进程在有待处理作业时死亡,队列将丢弃作业。如果您的应用程序 UI 允许通过手动操作重新发送电子邮件,这可能不是什么大问题。但是,您始终可以配置自定义信使并使用数据库支持的队列。

使用 bullmq 队列邮件

sh
npm i bullmq

在以下示例中,我们使用 mail.setMessenger 方法配置一个自定义队列,该队列在底层使用 bullmq 存储作业。

我们将编译的电子邮件、运行时配置和 mailer 名称存储在作业中。稍后,我们将在 worker 进程中使用这些数据发送电子邮件。

ts
import { Queue } from 'bullmq'
import mail from '@adonisjs/mail/services/main'

// highlight-start
const emailsQueue = new Queue('emails')
// highlight-end

// highlight-start
mail.setMessenger((mailer) => {
  return {
    async queue(mailMessage, config) {
      await emailsQueue.add('send_email', {
        mailMessage,
        config,
        mailerName: mailer.name,
      })
    }
  }
})
// highlight-end

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

在以下示例中:

  • 我们处理 emails 队列中名为 send_email 的作业。
  • 从作业数据访问编译的邮件消息、运行时配置和 mailer 名称。
  • 并使用 mailer.sendCompiled 方法发送电子邮件。
ts
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 类的实例。

ts
import mail from '@adonisjs/mail/services/main'

mail.use() // 默认 mailer 的实例
mail.use('mailgun') // Mailgun mailer 实例

您可以调用 mailer.sendmailer.sendLater 方法使用 mailer 实例发送电子邮件。例如:

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

mailer 实例在进程的生命周期内缓存。您可以使用 mail.close 方法销毁现有实例并从头开始重新创建新实例。

ts
import mail from '@adonisjs/mail/services/main'

/**
 * 关闭传输并从缓存中删除实例
 */
await mail.close('mailgun')

/**
 * 创建新实例
 */
mail.use('mailgun')

基于类的邮件

您可以将电子邮件移动到专用邮件类中,而不是在 mail.send 方法闭包中编写电子邮件,以获得更好的组织和更轻松的测试

邮件类存储在 ./app/mails 目录中,每个文件代表一封电子邮件。您可以通过运行 make:mail ace 命令创建邮件类。

另请参阅:Make mail 命令

sh
node ace make:mail verify_email

邮件类扩展 BaseMail 类,并使用以下属性和方法进行脚手架。您可以使用 this.message 属性在 prepare 方法中配置邮件消息。

ts
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')
  }
}

使用邮件类发送电子邮件

您可以调用 mail.send 方法并传递邮件类的实例来发送电子邮件。例如:

ts
// title: 发送邮件
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'

await mail.send(new VerifyEmailNotification())
ts
// title: 队列邮件
import mail from '@adonisjs/mail/services/main'
import VerifyEmailNotification from '#mails/verify_email'

await mail.sendLater(new VerifyEmailNotification())

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

ts
/**
 * 创建用户
 */
const user = await User.create(payload)

await mail.send(
  /**
   * 将用户传递给邮件类
   */
  new VerifyEmailNotification(user)
)

伪造 mailer

在测试期间,您可能希望使用伪造 mailer 来防止应用程序发送电子邮件。伪造 mailer 将所有传出电子邮件收集在内存中,并提供易于使用的 API 来针对它们编写断言。

在以下示例中:

  • 我们首先使用 mail.fake 方法创建 FakeMailer 的实例。
  • 接下来,我们调用 /register 端点 API。
  • 最后,我们使用伪造 mailer 的 mails 属性断言 VerifyEmailNotification 已发送。
ts
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 }) => {
    // highlight-start
    /**
     * 打开伪造模式
     */
    const { mails } = mail.fake()
    // highlight-end

    /**
     * 发出 API 调用
     */
    await client
      .post(route('users.store'))
      .send(userData)

    // highlight-start
    /**
     * 断言控制器确实发送了 VerifyEmailNotification 邮件
     */
    mails.assertSent(VerifyEmailNotification, ({ message }) => {
      return message
        .hasTo(userData.email)
        .hasSubject('Verify email address')
    })
    // highlight-end
  })
})

完成测试编写后,您必须使用 mail.restore 方法恢复伪造。

ts
test('create a new user account', async ({ client, route, cleanup }) => {
  const { mails } = mail.fake()

  /**
   * 清理钩子在测试成功完成或出现错误后执行。
   */
  cleanup(() => {
    mail.restore()
  })
})

编写断言

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

ts
const { mails } = mail.fake()

/**
 * 断言电子邮件已发送
 */
mails.assertSent(VerifyEmailNotification)

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

回调函数接收邮件类的实例,您可以使用 .message 属性访问消息对象。

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

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

ts
mails.assertSent(VerifyEmailNotification, (email) => {
  email.message.assertTo(userData.email)
  email.message.assertFrom('info@example.org')
  email.message.assertSubject('Verify your email address')

  /**
   * 所有断言都通过,所以返回 true 将电子邮件视为已发送。
   */
  return true
})

断言电子邮件未发送

您可以使用 mails.assertNotSent 方法断言电子邮件未发送。此方法与 assertSent 方法相反,接受相同的参数。

ts
const { mails } = mail.fake()

mails.assertNotSent(PasswordResetNotification)

断言电子邮件计数

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

ts
const { mails } = mail.fake()

// 断言总共发送了 2 封电子邮件
mails.assertSentCount(2)

// 断言只发送了一封 VerifyEmailNotification
mails.assertSentCount(VerifyEmailNotification, 1)
ts
const { mails } = mail.fake()

// 断言没有发送任何电子邮件
mails.assertNoneSent()

为队列邮件编写断言

如果您使用 mail.sendLater 方法将电子邮件排队,可以使用以下方法为它们编写断言。

ts
const { mails } = mail.fake()

/**
 * 断言 "VerifyEmailNotification" 邮件已排队
 * 可选地,您可以传递查找函数以缩小电子邮件范围
 */
mails.assertQueued(VerifyEmailNotification)

/**
 * 断言 "VerifyEmailNotification" 邮件未排队
 * 可选地,您可以传递查找函数以缩小电子邮件范围
 */
mails.assertNotQueued(PasswordResetNotification)

/**
 * 断言总共排队了两封电子邮件。
 */
mails.assertQueuedCount(2)

/**
 * 断言 "VerifyEmailNotification" 邮件只排队了一次
 */
mails.assertQueuedCount(VerifyEmailNotification , 1)

/**
 * 断言没有任何邮件排队
 */
mails.assertNoneQueued()