跳到内容

使用 Mailer 发送电子邮件

编辑此页

安装

Symfony 的 Mailer 和 Mime 组件构成了一个强大的系统,用于创建和发送电子邮件 - 完整支持多部分消息、Twig 集成、CSS 内联、文件附件以及更多功能。通过以下方式安装它们

1
$ composer require symfony/mailer

传输设置

电子邮件通过“传输”方式传递。开箱即用,你可以通过在你的 .env 文件中配置 DSN 来通过 SMTP 传递电子邮件(userpassport 参数是可选的)

1
2
# .env
MAILER_DSN=smtp://user:[email protected]:port
1
2
3
4
# config/packages/mailer.yaml
framework:
    mailer:
        dsn: '%env(MAILER_DSN)%'

警告

如果用户名、密码或主机包含 URI 中被视为特殊的字符(例如 : / ? # [ ] @ ! $ & ' ( ) * + , ; =),你必须对它们进行编码。请参阅 RFC 3986 以获取保留字符的完整列表,或使用 urlencode 函数对它们进行编码。

使用内置传输

DSN 协议 示例 描述
smtp smtp://user:[email protected]:25 Mailer 使用 SMTP 服务器发送电子邮件
sendmail sendmail://default Mailer 使用本地 sendmail 二进制文件发送电子邮件
native native://default Mailer 使用在 php.inisendmail_path 设置中配置的 sendmail 二进制文件和选项。在 Windows 主机上,当 sendmail_path 未配置时,Mailer 回退到 smtpsmtp_port php.ini 设置。

警告

当使用 native://default 时,如果 php.ini 使用 sendmail -t 命令,你将没有错误报告,并且 Bcc 标头将不会被删除。强烈建议不要使用 native://default,因为你无法控制 sendmail 的配置方式(如果可能,首选使用 sendmail://default)。

使用第三方传输

除了使用你自己的 SMTP 服务器或 sendmail 二进制文件,你还可以通过第三方提供商发送电子邮件

服务 安装方式 Webhook 支持
Amazon SES composer require symfony/amazon-mailer  
Azure composer require symfony/azure-mailer  
Brevo composer require symfony/brevo-mailer
Infobip composer require symfony/infobip-mailer  
Mailgun composer require symfony/mailgun-mailer
Mailjet composer require symfony/mailjet-mailer
Mailomat composer require symfony/mailomat-mailer
MailPace composer require symfony/mail-pace-mailer  
MailerSend composer require symfony/mailer-send-mailer
Mailtrap composer require symfony/mailtrap-mailer
Mandrill composer require symfony/mailchimp-mailer
Postal composer require symfony/postal-mailer  
Postmark composer require symfony/postmark-mailer
Resend composer require symfony/resend-mailer
Scaleway composer require symfony/scaleway-mailer  
SendGrid composer require symfony/sendgrid-mailer
Sweego composer require symfony/sweego-mailer

7.1

Azure 和 Resend 集成在 Symfony 7.1 中引入。

7.2

Mailomat、Mailtrap、Postal 和 Sweego 集成在 Symfony 7.2 中引入。

注意

为了方便起见,Symfony 还提供了对 Gmail 的支持 (composer require symfony/google-mailer),但这不应在生产环境中使用。在开发中,你可能应该使用 电子邮件捕获器 来代替。请注意,大多数受支持的提供商也提供免费层级。

每个库都包含一个 Symfony Flex recipe,它将向你的 .env 文件添加一个配置示例。例如,假设你想使用 SendGrid。首先,安装它

1
$ composer require symfony/sendgrid-mailer

你现在将在你的 .env 文件中新添一行,你可以取消注释它

1
2
# .env
MAILER_DSN=sendgrid://KEY@default

MAILER_DSN 不是一个真实的地址:它是一个方便的格式,可以将大部分配置工作卸载到 mailer。 sendgrid 方案激活了你刚刚安装的 SendGrid 提供商,它了解所有关于如何通过 SendGrid 传递消息的信息。你唯一需要更改的部分是 KEY 占位符。

每个提供商都有不同的环境变量,Mailer 使用这些变量来配置用于传递的实际协议、地址和身份验证。有些还具有可以使用查询参数在 MAILER_DSN 末尾配置的选项 - 例如 Amazon SES、Mailgun 或 Scaleway 的 ?region=。有些提供商支持通过 httpapismtp 发送。Symfony 选择最佳可用传输,但你可以强制使用其中一种

1
2
3
# .env
# force to use SMTP instead of HTTP (which is the default)
MAILER_DSN=sendgrid+smtp://$SENDGRID_KEY@default

下表显示了每个第三方提供商可用的完整 DSN 格式列表

提供商 格式
Amazon SES
  • SMTP ses+smtp://USERNAME:PASSWORD@default
  • HTTP ses+https://ACCESS_KEY:SECRET_KEY@default
  • API ses+api://ACCESS_KEY:SECRET_KEY@default
Azure
  • API azure+api://ACS_RESOURCE_NAME:KEY@default
Brevo
  • SMTP brevo+smtp://USERNAME:PASSWORD@default
  • HTTP n/a
  • API brevo+api://KEY@default
Google Gmail
  • SMTP gmail+smtp://USERNAME:APP-PASSWORD@default
  • HTTP n/a
  • API n/a
Infobip
  • SMTP infobip+smtp://KEY@default
  • HTTP n/a
  • API infobip+api://KEY@BASE_URL
Mandrill
  • SMTP mandrill+smtp://USERNAME:PASSWORD@default
  • HTTP mandrill+https://KEY@default
  • API mandrill+api://KEY@default
MailerSend
  • SMTP mailersend+smtp://KEY@default
  • HTTP n/a
  • API mailersend+api://KEY@BASE_URL
Mailgun
  • SMTP mailgun+smtp://USERNAME:PASSWORD@default
  • HTTP mailgun+https://KEY:DOMAIN@default
  • API mailgun+api://KEY:DOMAIN@default
Mailjet
  • SMTP mailjet+smtp://ACCESS_KEY:SECRET_KEY@default
  • HTTP n/a
  • API mailjet+api://ACCESS_KEY:SECRET_KEY@default
Mailomat
  • SMTP mailomat+smtp://USERNAME:PASSWORD@default
  • HTTP n/a
  • API mailomat+api://KEY@default
MailPace
  • SMTP mailpace+api://API_TOKEN@default
  • HTTP n/a
  • API mailpace+api://API_TOKEN@default
Mailtrap
  • SMTP mailtrap+smtp://PASSWORD@default
  • HTTP n/a
  • API mailtrap+api://API_TOKEN@default
Postal
  • SMTP n/a
  • HTTP n/a
  • API postal+api://API_KEY@BASE_URL
Postmark
  • SMTP postmark+smtp://ID@default
  • HTTP n/a
  • API postmark+api://KEY@default
Resend
  • SMTP resend+smtp://resend:API_KEY@default
  • HTTP n/a
  • API resend+api://API_KEY@default
Scaleway
  • SMTP scaleway+smtp://PROJECT_ID:API_KEY@default
  • HTTP n/a
  • API scaleway+api://PROJECT_ID:API_KEY@default
Sendgrid
  • SMTP sendgrid+smtp://KEY@default
  • HTTP n/a
  • API sendgrid+api://KEY@default
Sweego
  • SMTP sweego+smtp://LOGIN:PASSWORD@HOST:PORT
  • HTTP n/a
  • API sweego+api://API_KEY@default

警告

如果你的凭据包含特殊字符,你必须对其进行 URL 编码。例如,DSN ses+smtp://ABC1234:abc+12/345@default 应配置为 ses+smtp://ABC1234:abc%2B12%2F345@default

警告

如果你想将 ses+smtp 传输与 Messenger 一起使用以 在后台发送消息,你需要将 ping_threshold 参数添加到你的 MAILER_DSN,其值低于 10ses+smtp://USERNAME:PASSWORD@default?ping_threshold=9

警告

如果你在使用 Amazon SES 传输时发送自定义标头(以便稍后通过 webhook 接收它们),请确保使用 ses+https 提供商,因为它是唯一支持它们的提供商。

注意

当使用 SMTP 时,在抛出异常之前发送消息的默认超时是在 default_socket_timeout PHP.ini 选项中定义的值。

注意

除了 SMTP 之外,许多第三方传输还提供 Web API 来发送电子邮件。为此,你必须(除了 bridge 之外)通过 composer require symfony/http-client 安装 HttpClient 组件。

注意

要使用 Google Gmail,你必须拥有启用两步验证 (2FA) 的 Google 帐户,并且你必须使用 应用密码 进行身份验证。另请注意,当你更改 Google 帐户密码时,Google 会撤销你的应用密码,然后你需要生成新的应用密码。目前不支持使用其他方法(如 XOAUTH2Gmail API)。你应仅将 Gmail 用于测试目的,并在生产环境中使用真正的提供商。

提示

如果你想覆盖提供商的默认主机(以使用类似 requestbin.com 的服务调试问题),请将 default 更改为你的主机

1
2
# .env
MAILER_DSN=mailgun+https://KEY:[email protected]

请注意,协议始终为 HTTPs,并且无法更改。

注意

特定的传输,例如 mailgun+smtp 旨在无需任何手动配置即可工作。对于任何这些 <provider>+smtp 传输,不支持通过将端口附加到你的 DSN 来更改端口。如果你需要更改端口,请使用 smtp 传输,如下所示

1
2
# .env
MAILER_DSN=smtp://KEY:[email protected]:25

提示

一些第三方邮件发送商在使用 API 时,支持通过 webhook 进行状态回调。有关更多详细信息,请参阅 Webhook 文档

高可用性

Symfony 的 mailer 通过一种称为“故障转移”的技术支持 高可用性,以确保即使一个邮件服务器发生故障,电子邮件也能发送。

故障转移传输配置有两个或多个传输以及 failover 关键字

1
MAILER_DSN="failover(postmark+api://ID@default sendgrid+smtp://KEY@default)"

故障转移传输开始使用第一个传输,如果它失败,它将使用下一个传输重试相同的传递,直到其中一个传输成功(或直到所有传输都失败)。

负载均衡

Symfony 的 mailer 通过一种称为“轮询调度”的技术支持 负载均衡,以在多个传输之间分配邮件发送工作负载。

轮询调度传输配置有两个或多个传输以及 roundrobin 关键字

1
MAILER_DSN="roundrobin(postmark+api://ID@default sendgrid+smtp://KEY@default)"

轮询调度传输从随机选择的传输开始,然后为每个后续电子邮件切换到下一个可用传输。

与故障转移传输一样,轮询调度重试传递,直到传输成功(或所有传输都失败)。与故障转移传输相比,它将负载分散到其所有传输上。

TLS 对等验证

默认情况下,SMTP 传输执行 TLS 对等验证。此行为可通过 verify_peer 选项配置。虽然出于安全原因不建议禁用此验证,但在开发应用程序或使用自签名证书时,它可能很有用

1
$dsn = 'smtp://user:[email protected]?verify_peer=0';

TLS 对等指纹验证

可以使用 peer_fingerprint 选项强制执行额外的指纹验证。当使用自签名证书并且需要禁用 verify_peer 但仍然需要安全性时,这尤其有用。指纹可以指定为 SHA1 或 MD5 哈希值

1
$dsn = 'smtp://user:[email protected]?peer_fingerprint=6A1CF3B08D175A284C30BC10DE19162307C7286E';

禁用自动 TLS

7.1

禁用自动 TLS 的选项在 Symfony 7.1 中引入。

默认情况下,当 OpenSSL 扩展启用且 SMTP 服务器支持 STARTTLS 时,Mailer 组件将使用加密。可以通过在 EsmtpTransport 实例上调用 setAutoTls(false),或在 DSN 中将 auto_tls 选项设置为 false 来关闭此行为

1
$dsn = 'smtp://user:[email protected]?auto_tls=false';

警告

不建议在通过 Internet 连接到 SMTP 服务器时禁用 TLS,但当应用程序和 SMTP 服务器都在安全网络中时,这可能很有用,在这种情况下,不需要额外的加密。

注意

此设置仅在使用 smtp:// 协议时有效。

覆盖默认 SMTP 验证器

默认情况下,SMTP 传输将尝试使用 SMTP 服务器上可用的所有身份验证方法依次登录。在某些情况下,重新定义支持的身份验证方法以确保首先使用首选方法可能很有用。

这可以从 EsmtpTransport 构造函数或使用 setAuthenticators() 方法完成

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator;
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;

// Choose one of these two options:

// Option 1: pass the authenticators to the constructor
$transport = new EsmtpTransport(
    host: 'oauth-smtp.domain.tld',
    authenticators: [new XOAuth2Authenticator()]
);

// Option 2: call a method to redefine the authenticators
$transport->setAuthenticators([new XOAuth2Authenticator()]);

其他选项

command

sendmail 传输要执行的命令

1
$dsn = 'sendmail://default?command=/usr/sbin/sendmail%20-oi%20-t'
local_domain

要在 HELO 命令中使用的域名

1
$dsn = 'smtps://smtp.example.com?local_domain=example.org'
restart_threshold

在重新启动传输之前要发送的最大消息数。它可以与 restart_threshold_sleep 一起使用

1
$dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1'
restart_threshold_sleep

停止和重新启动传输之间休眠的秒数。通常将其与 restart_threshold 结合使用

1
$dsn = 'smtps://smtp.example.com?restart_threshold=10&restart_threshold_sleep=1'
ping_threshold

在 ping 服务器所需的两条消息之间的最小秒数

1
$dsn = 'smtps://smtp.example.com?ping_threshold=200'
max_per_second

每秒要发送的消息数(0 表示禁用此限制)

1
$dsn = 'smtps://smtp.example.com?max_per_second=2'

自定义传输工厂

如果你想支持你自己的自定义 DSN (acme://...),你可以创建一个自定义传输工厂。为此,创建一个实现 TransportFactoryInterface 的类,或者,如果你愿意,扩展 AbstractTransportFactory 类以节省一些样板代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/Mailer/AcmeTransportFactory.php
final class AcmeTransportFactory extends AbstractTransportFactory
{
    public function create(Dsn $dsn): TransportInterface
    {
        // parse the given DSN, extract data/credentials from it
        // and then, create and return the transport
    }

    protected function getSupportedSchemes(): array
    {
        // this supports DSN starting with `acme://`
        return ['acme'];
    }
}

在创建自定义传输类后,将其注册为应用程序中的服务,并使用 mailer.transport_factory 标签 标记它

创建和发送消息

要发送电子邮件,通过类型提示 Mailer 实例,并创建一个 Email 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/Controller/MailerController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;
use Symfony\Component\Routing\Attribute\Route;

class MailerController extends AbstractController
{
    #[Route('/email')]
    public function sendEmail(MailerInterface $mailer): Response
    {
        $email = (new Email())
            ->from('[email protected]')
            ->to('[email protected]')
            //->cc('[email protected]')
            //->bcc('[email protected]')
            //->replyTo('[email protected]')
            //->priority(Email::PRIORITY_HIGH)
            ->subject('Time for Symfony Mailer!')
            ->text('Sending emails is fun again!')
            ->html('<p>See Twig integration for better HTML integration!</p>');

        $mailer->send($email);

        // ...
    }
}

就是这样!消息将通过您配置的传输方式立即发送。如果您希望异步发送电子邮件以提高性能,请阅读“异步发送消息”章节。此外,如果您的应用程序安装了 Messenger 组件,则默认情况下所有电子邮件都将异步发送(但您可以更改它)。

电子邮件地址

所有需要电子邮件地址的方法(from()to() 等)都接受字符串或地址对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
use Symfony\Component\Mime\Address;

$email = (new Email())
    // email address as a simple string
    ->from('[email protected]')

    // non-ASCII characters are supported both in the local part and the domain;
    // if the SMTP server doesn't support this feature, you'll see an exception
    ->from('jânë.dœ@ëxãmplę.com')

    // email address as an object
    ->from(new Address('[email protected]'))

    // defining the email address and name as an object
    // (email clients will display the name)
    ->from(new Address('[email protected]', 'Fabien'))

    // defining the email address and name as a string
    // (the format must match: 'Name <[email protected]>')
    ->from(Address::create('Fabien Potencier <[email protected]>'))

    // ...
;

提示

您可以全局配置电子邮件,以便为所有消息设置相同的 From 电子邮件地址,而无需在每次创建新电子邮件时都调用 ->from()

7.2

Symfony 7.2 中引入了对非 ASCII 电子邮件地址(例如 jânë.dœ@ëxãmplę.com)的支持。

注意

地址的本地部分(@ 之前的部分)可以包含 UTF-8 字符,但发件人地址除外(以避免退回邮件问题)。例如:föóbà[email protected]用户@example.comθσερ@example.com 等。

使用 addTo()addCc()addBcc() 方法添加更多地址

1
2
3
4
5
6
7
8
$email = (new Email())
    ->to('[email protected]')
    ->addTo('[email protected]')
    ->cc('[email protected]')
    ->addCc('[email protected]')

    // ...
;

或者,您可以将多个地址传递给每个方法

1
2
3
4
5
6
7
8
$toAddresses = ['[email protected]', new Address('[email protected]')];

$email = (new Email())
    ->to(...$toAddresses)
    ->cc('[email protected]', '[email protected]')

    // ...
;

消息头

消息包含许多标头字段来描述其内容。Symfony 会自动设置所有必需的标头,但您也可以设置自己的标头。标头有不同的类型(Id 标头、Mailbox 标头、Date 标头等),但大多数时候您将设置文本标头

1
2
3
4
5
6
7
8
9
10
11
12
$email = (new Email())
    ->getHeaders()
        // this non-standard header tells compliant autoresponders ("email holiday mode")
        // to not reply to this message because it's an automated email
        ->addTextHeader('X-Auto-Response-Suppress', 'OOF, DR, RN, NRN, AutoReply')

        // use an array if you want to add a header with multiple values
        // (for example in the "References" or "In-Reply-To" header)
        ->addIdHeader('References', ['[email protected]', '[email protected]'])

        // ...
;

提示

您可以全局配置电子邮件,以便为所有已发送的电子邮件设置相同的标头,而无需在每次创建新电子邮件时都调用 ->addTextHeader()

消息内容

电子邮件消息的文本和 HTML 内容可以是字符串(通常是渲染某些模板的结果)或 PHP 资源

1
2
3
4
5
6
7
8
9
10
$email = (new Email())
    // ...
    // simple contents defined as a string
    ->text('Lorem ipsum...')
    ->html('<p>Lorem ipsum...</p>')

    // attach a file stream
    ->text(fopen('/path/to/emails/user_signup.txt', 'r'))
    ->html(fopen('/path/to/emails/user_signup.html', 'r'))
;

提示

您还可以使用 Twig 模板来渲染 HTML 和文本内容。请阅读本文后面的“Twig: HTML & CSS”章节以了解更多信息。

文件附件

使用带有 FileaddPart() 方法来添加文件系统中存在的文件

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File;
// ...

$email = (new Email())
    // ...
    ->addPart(new DataPart(new File('/path/to/documents/terms-of-use.pdf')))
    // optionally you can tell email clients to display a custom name for the file
    ->addPart(new DataPart(new File('/path/to/documents/privacy.pdf'), 'Privacy Policy'))
    // optionally you can provide an explicit MIME type (otherwise it's guessed)
    ->addPart(new DataPart(new File('/path/to/documents/contract.doc'), 'Contract', 'application/msword'))
;

或者,您可以通过将其直接传递给 DataPart 来附加来自流的内容

1
2
3
4
$email = (new Email())
    // ...
    ->addPart(new DataPart(fopen('/path/to/documents/contract.doc', 'r')))
;

嵌入图片

如果您想在电子邮件中显示图像,则必须嵌入它们而不是作为附件添加。当使用 Twig 渲染电子邮件内容时,如本文后面所述,图像会自动嵌入。否则,您需要手动嵌入它们。

首先,使用 addPart() 方法从文件或流中添加图像

1
2
3
4
5
6
7
$email = (new Email())
    // ...
    // get the image contents from a PHP resource
    ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline())
    // get the image contents from an existing file
    ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline())
;

使用 asInline() 方法嵌入内容而不是附加它。

这两种方法的第二个可选参数是图像名称(MIME 标准中的“Content-ID”)。它的值是一个任意字符串,在每封电子邮件消息中都必须是唯一的,并且稍后用于引用 HTML 内容中的图像

1
2
3
4
5
6
7
8
9
10
11
$email = (new Email())
    // ...
    ->addPart((new DataPart(fopen('/path/to/images/logo.png', 'r'), 'logo', 'image/png'))->asInline())
    ->addPart((new DataPart(new File('/path/to/images/signature.gif'), 'footer-signature', 'image/gif'))->asInline())

    // reference images using the syntax 'cid:' + "image embed name"
    ->html('<img src="cid:logo"> ... <img src="cid:footer-signature"> ...')

    // use the same syntax for images included as HTML background images
    ->html('... <div background="cid:footer-signature"> ... </div> ...')
;

电子邮件源中存在的实际 Content-ID 值将由 Symfony 随机生成。您还可以使用 DataPart::setContentId() 方法为图像定义自定义 Content-ID,并将其用作其 cid 引用

1
2
3
4
5
6
7
8
9
$part = new DataPart(new File('/path/to/images/signature.gif'));
// according to the spec, the Content-ID value must include at least one '@' character
$part->setContentId('footer-signature@my-app');

$email = (new Email())
    // ...
    ->addPart($part->asInline())
    ->html('... <img src="cid:footer-signature@my-app"> ...')
;

全局配置电子邮件

您可以全局配置此值,以便在所有已发送的电子邮件上设置该值,而无需在您创建的每封 Email 上都调用 ->from()->to() 和标头也是如此。

1
2
3
4
5
6
7
8
9
10
# config/packages/mailer.yaml
framework:
    mailer:
        envelope:
            sender: '[email protected]'
            recipients: ['[email protected]', '[email protected]']
        headers:
            From: 'Fabien <[email protected]>'
            Bcc: '[email protected]'
            X-Custom-Header: 'foobar'

警告

某些第三方提供商不支持在 headers 中使用类似 from 这样的关键字。在设置任何全局标头之前,请查看您的提供商的文档。

处理发送失败

当您的传输方式(SMTP 服务器或第三方提供商)接受邮件以进行进一步传递时,Symfony Mailer 认为发送成功。消息稍后可能会丢失或因您的提供商的某些问题而未送达,但这超出了您的 Symfony 应用程序的范围。

如果在将电子邮件移交给您的传输方式时出现错误,Symfony 将抛出 TransportExceptionInterface。捕获该异常以从错误中恢复或显示一些消息

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;

$email = new Email();
// ...
try {
    $mailer->send($email);
} catch (TransportExceptionInterface $e) {
    // some error prevented the email sending; display an
    // error message or try to resend the message
}

调试电子邮件

TransportInterfacesend() 方法返回的 SentMessage 对象提供了对原始消息 (getOriginalMessage()) 和一些调试信息 (getDebug()) 的访问,例如 HTTP 传输完成的 HTTP 调用,这对于调试错误很有用。

您还可以通过侦听 SentMessageEvent 来访问 SentMessage,并通过侦听 FailedMessageEvent 来检索 getDebug()

注意

如果您的代码使用了 MailerInterface,则需要将其替换为 TransportInterface 才能返回 SentMessage 对象。

注意

某些邮件程序提供商在发送电子邮件时会更改 Message-IdSentMessage 中的 getMessageId() 方法始终返回消息的最终 ID(是 Symfony 生成的原始随机 ID 还是邮件程序提供商生成的新 ID)。

与邮件程序传输相关的异常(那些实现 TransportException 的异常)也通过 getDebug() 方法提供此调试信息。

Twig: HTML & CSS

Mime 组件与 Twig 模板引擎集成,以提供高级功能,例如 CSS 样式内联以及对 HTML/CSS 框架的支持,以创建复杂的 HTML 电子邮件消息。首先,请确保已安装 Twig

1
2
3
4
$ composer require symfony/twig-bundle

# or if you're using the component in a non-Symfony app:
# composer require symfony/twig-bridge

HTML 内容

要使用 Twig 定义电子邮件的内容,请使用 TemplatedEmail 类。此类扩展了普通的 Email 类,但为 Twig 模板添加了一些新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
use Symfony\Bridge\Twig\Mime\TemplatedEmail;

$email = (new TemplatedEmail())
    ->from('[email protected]')
    ->to(new Address('[email protected]'))
    ->subject('Thanks for signing up!')

    // path of the Twig template to render
    ->htmlTemplate('emails/signup.html.twig')

    // change locale used in the template, e.g. to match user's locale
    ->locale('de')

    // pass variables (name => value) to the template
    ->context([
        'expiration_date' => new \DateTime('+7 days'),
        'username' => 'foo',
    ])
;

然后,创建模板

1
2
3
4
5
6
7
8
9
10
11
12
{# templates/emails/signup.html.twig #}
<h1>Welcome {{ email.toName }}!</h1>

<p>
    You signed up as {{ username }} the following email:
</p>
<p><code>{{ email.to[0].address }}</code></p>

<p>
    <a href="#">Activate your account</a>
    (this link is valid until {{ expiration_date|date('F jS') }})
</p>

Twig 模板可以访问 TemplatedEmail 类的 context() 方法中传递的任何参数,以及名为 email 的特殊变量,它是 WrappedTemplatedEmail 的实例。

文本内容

TemplatedEmail 的文本内容未明确定义时,它会自动从 HTML 内容生成。

Symfony 在生成电子邮件的文本版本时使用以下策略

  • 如果已配置显式的 HTML 到文本转换器(请参阅 twig.mailer.html_to_text_converter),则调用它;
  • 如果未配置,并且您的应用程序中安装了 league/html-to-markdown,则它会使用它将 HTML 转换为 Markdown(以便文本电子邮件具有一定的视觉吸引力);
  • 否则,它将 strip_tags PHP 函数应用于原始 HTML 内容。

如果您想自己定义文本内容,请使用前面章节中解释的 text() 方法或 TemplatedEmail 类提供的 textTemplate() 方法

1
2
3
4
5
6
7
8
9
+use Symfony\Bridge\Twig\Mime\TemplatedEmail;

 $email = (new TemplatedEmail())
     // ...

     ->htmlTemplate('emails/signup.html.twig')
+     ->textTemplate('emails/signup.txt.twig')
     // ...
 ;

嵌入图片

当使用 Twig 渲染电子邮件内容时,您可以像往常一样引用图像文件,而无需处理前面章节中解释的 <img src="cid: ..."> 语法。首先,为了简化操作,定义一个名为 images 的 Twig 命名空间,它指向存储图像的任何目录

1
2
3
4
5
6
7
# config/packages/twig.yaml
twig:
    # ...

    paths:
        # point this wherever your images live
        '%kernel.project_dir%/assets/images': images

现在,使用特殊的 email.image() Twig 助手将图像嵌入到电子邮件内容中

1
2
3
4
5
{# '@images/' refers to the Twig namespace defined earlier #}
<img src="{{ email.image('@images/logo.png') }}" alt="Logo">

<h1>Welcome {{ email.toName }}!</h1>
{# ... #}

内联 CSS 样式

设计电子邮件的 HTML 内容与设计普通的 HTML 页面非常不同。首先,大多数电子邮件客户端仅支持所有 CSS 功能的子集。此外,像 Gmail 这样的流行电子邮件客户端不支持在 <style> ... </style> 部分中定义样式,您必须内联所有 CSS 样式

CSS 内联意味着每个 HTML 标记都必须定义一个 style 属性,其中包含其所有 CSS 样式。这可能会使组织 CSS 变得混乱。这就是为什么 Twig 提供了一个 CssInlinerExtension,它可以为您自动完成所有操作。使用以下命令安装它

1
$ composer require twig/extra-bundle twig/cssinliner-extra

扩展程序已自动启用。要使用它,请使用 inline_css 过滤器包装整个模板

1
2
3
4
5
6
7
8
9
10
11
{% apply inline_css %}
    <style>
        {# here, define your CSS styles as usual #}
        h1 {
            color: #333;
        }
    </style>

    <h1>Welcome {{ email.toName }}!</h1>
    {# ... #}
{% endapply %}

使用外部 CSS 文件

您还可以在外部文件中定义 CSS 样式,并将它们作为参数传递给过滤器

1
2
3
4
{% apply inline_css(source('@styles/email.css')) %}
    <h1>Welcome {{ username }}!</h1>
    {# ... #}
{% endapply %}

您可以将无限数量的参数传递给 inline_css() 以加载多个 CSS 文件。为了使此示例有效,您还需要定义一个新的名为 styles 的 Twig 命名空间,它指向 email.css 所在的目录

1
2
3
4
5
6
7
# config/packages/twig.yaml
twig:
    # ...

    paths:
        # point this wherever your css files live
        '%kernel.project_dir%/assets/styles': styles

渲染 Markdown 内容

Twig 提供了另一个名为 MarkdownExtension 的扩展程序,它使您可以使用 Markdown 语法定义电子邮件内容。要使用它,请安装扩展程序和一个 Markdown 转换库(该扩展程序与几个流行的库兼容)

1
2
# instead of league/commonmark, you can also use erusev/parsedown or michelf/php-markdown
$ composer require twig/extra-bundle twig/markdown-extra league/commonmark

该扩展程序添加了一个 markdown_to_html 过滤器,您可以使用它将部分或整个电子邮件内容从 Markdown 转换为 HTML

1
2
3
4
5
6
7
8
9
{% apply markdown_to_html %}
    Welcome {{ email.toName }}!
    ===========================

    You signed up to our site using the following email:
    `{{ email.to[0].address }}`

    [Activate your account]({{ url('...') }})
{% endapply %}

Inky 电子邮件模板语言

创建在每个电子邮件客户端上都能正常工作的精美设计的电子邮件非常复杂,以至于有专门用于此目的的 HTML/CSS 框架。最流行的框架之一称为 Inky。它定义了一种基于某些类似 HTML 的标记的语法,这些标记稍后会转换为发送给用户的真实 HTML 代码

1
2
3
4
5
6
<!-- a simplified example of the Inky syntax -->
<container>
    <row>
        <columns>This is a column.</columns>
    </row>
</container>

Twig 通过 InkyExtension 提供与 Inky 的集成。首先,在您的应用程序中安装扩展程序

1
$ composer require twig/extra-bundle twig/inky-extra

该扩展程序添加了一个 inky_to_html 过滤器,可用于将部分或整个电子邮件内容从 Inky 转换为 HTML

1
2
3
4
5
6
7
8
9
10
11
12
{% apply inky_to_html %}
    <container>
        <row class="header">
            <columns>
                <spacer size="16"></spacer>
                <h1 class="text-center">Welcome {{ email.toName }}!</h1>
            </columns>

            {# ... #}
        </row>
    </container>
{% endapply %}

您可以组合所有过滤器来创建复杂的电子邮件消息

1
2
3
{% apply inky_to_html|inline_css(source('@styles/foundation-emails.css')) %}
    {# ... #}
{% endapply %}

这利用了我们之前创建的styles Twig 命名空间。例如,您可以直接从 GitHub 下载 foundation-emails.css 文件并将其保存在 assets/styles 中。

签名和加密消息

可以对电子邮件消息进行签名和/或加密,以提高其完整性和安全性。这两个选项可以组合起来,以加密已签名的消息和/或签名已加密的消息。

在签名/加密消息之前,请确保您拥有

提示

使用 OpenSSL 生成证书时,请确保添加 -addtrust emailProtection 命令选项。

警告

签名和加密消息要求其内容完全呈现。例如,模板化电子邮件的内容由 MessageListener 呈现。因此,如果您想签名和/或加密此类消息,则需要在 MessageEvent 侦听器中在其运行后执行此操作(您需要为您的侦听器设置负优先级)。

签名消息

签名消息时,将为消息的整个内容(包括附件)生成加密哈希。此哈希作为附件添加,以便收件人可以验证收到的消息的完整性。但是,对于不支持签名消息的邮件代理,原始消息的内容仍然可读,因此如果您想隐藏其内容,则还必须加密消息。

您可以使用 S/MIME 或 DKIM 签名消息。在这两种情况下,证书和私钥都必须是 PEM 编码的,并且可以使用例如 OpenSSL 创建,也可以在官方证书颁发机构 (CA) 处获得。电子邮件收件人必须在受信任颁发者列表中拥有 CA 证书才能验证签名。

警告

如果您使用消息签名,则发送给 Bcc 的内容将从消息中删除。如果您需要向多个收件人发送消息,则需要为每个收件人计算新的签名。

S/MIME 签名者

S/MIME 是公钥加密和 MIME 数据签名的标准。它需要同时使用证书和私钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\Mime\Crypto\SMimeSigner;
use Symfony\Component\Mime\Email;

$email = (new Email())
    ->from('[email protected]')
    // ...
    ->html('...');

$signer = new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key');
// if the private key has a passphrase, pass it as the third argument
// new SMimeSigner('/path/to/certificate.crt', '/path/to/certificate-private-key.key', 'the-passphrase');

$signedEmail = $signer->sign($email);
// now use the Mailer component to send this $signedEmail instead of the original email

提示

SMimeSigner 类定义了其他可选参数,用于传递中间证书,并使用 openssl_pkcs7_sign PHP 函数的按位运算符选项来配置签名过程。

DKIM 签名者

DKIM 是一种电子邮件身份验证方法,它将链接到域名的数字签名附加到每封外发电子邮件消息。它需要私钥,但不需要证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
use Symfony\Component\Mime\Crypto\DkimSigner;
use Symfony\Component\Mime\Email;

$email = (new Email())
    ->from('[email protected]')
    // ...
    ->html('...');

// first argument: same as openssl_pkey_get_private(), either a string with the
// contents of the private key or the absolute path to it (prefixed with 'file://')
// second and third arguments: the domain name and "selector" used to perform a DNS lookup
// (the selector is a string used to point to a specific DKIM public key record in your DNS)
$signer = new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf');
// if the private key has a passphrase, pass it as the fifth argument
// new DkimSigner('file:///path/to/private-key.key', 'example.com', 'sf', [], 'the-passphrase');

$signedEmail = $signer->sign($email);
// now use the Mailer component to send this $signedEmail instead of the original email

// DKIM signer provides many config options and a helper object to configure them
use Symfony\Component\Mime\Crypto\DkimOptions;

$signedEmail = $signer->sign($email, (new DkimOptions())
    ->bodyCanon('relaxed')
    ->headerCanon('relaxed')
    ->headersToIgnore(['Message-ID'])
    ->toArray()
);

加密消息

加密消息时,将使用证书加密整个消息(包括附件)。因此,只有拥有相应私钥的收件人才能读取原始消息内容

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\Mime\Crypto\SMimeEncrypter;
use Symfony\Component\Mime\Email;

$email = (new Email())
    ->from('[email protected]')
    // ...
    ->html('...');

$encrypter = new SMimeEncrypter('/path/to/certificate.crt');
$encryptedEmail = $encrypter->encrypt($email);
// now use the Mailer component to send this $encryptedEmail instead of the original email

您可以将多个证书传递给 SMimeEncrypter 构造函数,它将根据 To 选项选择适当的证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$firstEmail = (new Email())
    // ...
    ->to('[email protected]');

$secondEmail = (new Email())
    // ...
    ->to('[email protected]');

// the second optional argument of SMimeEncrypter defines which encryption algorithm is used
// (it must be one of these constants: https://php.ac.cn/manual/en/openssl.ciphers.php)
$encrypter = new SMimeEncrypter([
    // key = email recipient; value = path to the certificate file
    '[email protected]' => '/path/to/first-certificate.crt',
    '[email protected]' => '/path/to/second-certificate.crt',
]);

$firstEncryptedEmail = $encrypter->encrypt($firstEmail);
$secondEncryptedEmail = $encrypter->encrypt($secondEmail);

多重电子邮件传输

您可能希望使用多个邮件程序传输方式来传递消息。可以通过将 dsn 配置条目替换为 transports 条目来配置它,例如

1
2
3
4
5
6
# config/packages/mailer.yaml
framework:
    mailer:
        transports:
            main: '%env(MAILER_DSN)%'
            alternative: '%env(MAILER_DSN_IMPORTANT)%'

默认情况下,使用第一个传输方式。可以通过添加 X-Transport 标头来选择其他传输方式(Mailer 会自动从最终电子邮件中删除该标头)

1
2
3
4
5
6
// Send using first transport ("main"):
$mailer->send($email);

// ... or use the transport "alternative":
$email->getHeaders()->addTextHeader('X-Transport', 'alternative');
$mailer->send($email);

异步发送消息

当您调用 $mailer->send($email) 时,电子邮件会立即发送到传输方式。为了提高性能,您可以利用 Messenger 通过 Messenger 传输方式稍后发送消息。

首先,请按照 Messenger 文档并配置传输方式。一旦一切设置完毕,当您调用 $mailer->send() 时,SendEmailMessage 消息将通过默认消息总线 (messenger.default_bus) 分派。假设您有一个名为 async 的传输方式,您可以将消息路由到那里

1
2
3
4
5
6
7
8
# config/packages/messenger.yaml
framework:
    messenger:
        transports:
            async: "%env(MESSENGER_TRANSPORT_DSN)%"

        routing:
            'Symfony\Component\Mailer\Messenger\SendEmailMessage': async

因此,消息将发送到传输方式以供稍后处理,而不是立即传递(请参阅 Messenger:同步和排队消息处理)。请注意,电子邮件的“渲染”(计算标头、正文渲染等)也会被延迟,并且仅在 Messenger 处理程序发送电子邮件之前发生。

异步发送电子邮件时,其实例必须是可序列化的。对于 Mailer 实例始终如此,但在发送 TemplatedEmail 时,您必须确保 context 是可序列化的。如果您有不可序列化的变量(例如 Doctrine 实体),请将其替换为更具体的变量,或者在调用 $mailer->send($email) 之前渲染电子邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\BodyRendererInterface;

public function action(MailerInterface $mailer, BodyRendererInterface $bodyRenderer): void
{
    $email = (new TemplatedEmail())
        ->htmlTemplate($template)
        ->context($context)
    ;
    $bodyRenderer->render($email);

    $mailer->send($email);
}

您可以使用 message_bus 选项配置用于分派消息的总线。您还可以将其设置为 false 以直接调用 Mailer 传输方式并禁用异步传递。

1
2
3
4
# config/packages/mailer.yaml
framework:
    mailer:
        message_bus: app.another_bus

注意

在长时间运行的脚本的情况下,并且当 Mailer 使用 SmtpTransport 时,您可以手动断开与 SMTP 服务器的连接,以避免在发送电子邮件之间保持与 SMTP 服务器的打开连接。您可以使用 stop() 方法执行此操作。

您还可以通过添加 X-Bus-Transport 标头来选择传输方式(该标头将自动从最终消息中删除)

1
2
3
// Use the bus transport "app.another_bus":
$email->getHeaders()->addTextHeader('X-Bus-Transport', 'app.another_bus');
$mailer->send($email);

向电子邮件添加标签和元数据

某些第三方传输方式支持电子邮件标签元数据,这些标签和元数据可用于分组、跟踪和工作流。您可以使用 TagHeaderMetadataHeader 类添加这些标签和元数据。如果您的传输方式支持标头,它将它们转换为适当的格式

1
2
3
4
5
6
use Symfony\Component\Mailer\Header\MetadataHeader;
use Symfony\Component\Mailer\Header\TagHeader;

$email->getHeaders()->add(new TagHeader('password-reset'));
$email->getHeaders()->add(new MetadataHeader('Color', 'blue'));
$email->getHeaders()->add(new MetadataHeader('Client-ID', '12345'));

如果您的传输方式不支持标签和元数据,它们将作为自定义标头添加

1
2
3
X-Tag: password-reset
X-Metadata-Color: blue
X-Metadata-Client-ID: 12345

以下传输方式当前支持标签和元数据

  • Brevo
  • Mailgun
  • Mailtrap
  • Mandrill
  • Postmark
  • Sendgrid

以下传输方式仅支持标签

  • MailPace
  • Resend

以下传输方式仅支持元数据

  • Amazon SES(请注意,Amazon 将此功能称为“标签”,但 Symfony 称其为“元数据”,因为它包含键和值)

草稿邮件

DraftEmailEmail 的一个特殊实例。其目的是构建电子邮件(包含正文、附件等),并使其可下载为带有 X-Unsent 标头的 .eml 文件。许多电子邮件客户端可以打开这些文件并将其解释为草稿电子邮件。您可以使用它们来创建高级 mailto: 链接。

这是一个使其可供下载的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/Controller/DownloadEmailController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Mime\DraftEmail;
use Symfony\Component\Routing\Attribute\Route;

class DownloadEmailController extends AbstractController
{
    #[Route('/download-email')]
    public function __invoke(): Response
    {
        $message = (new DraftEmail())
            ->html($this->renderView(/* ... */))
            ->addPart(/* ... */)
        ;

        $response = new Response($message->toString());
        $contentDisposition = $response->headers->makeDisposition(
            ResponseHeaderBag::DISPOSITION_ATTACHMENT,
            'download.eml'
        );
        $response->headers->set('Content-Type', 'message/rfc822');
        $response->headers->set('Content-Disposition', $contentDisposition);

        return $response;
    }
}

注意

由于 DraftEmail 可以创建为没有 To/From,因此它们无法通过邮件程序发送。

Mailer 事件

MessageEvent

事件类MessageEvent

MessageEvent 允许在发送电子邮件之前更改 Mailer 消息和信封

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mime\Email;

public function onMessage(MessageEvent $event): void
{
    $message = $event->getMessage();
    if (!$message instanceof Email) {
        return;
    }
    // do something with the message (logging, ...)

    // and/or add some Messenger stamps
    $event->addStamp(new SomeMessengerStamp());
}

如果您想阻止消息发送,请调用 reject()(它也会停止事件传播)

1
2
3
4
5
6
use Symfony\Component\Mailer\Event\MessageEvent;

public function onMessage(MessageEvent $event): void
{
    $event->reject();
}

执行此命令以找出为此事件注册了哪些侦听器及其优先级

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\MessageEvent"

SentMessageEvent

事件类SentMessageEvent

SentMessageEvent 允许您对 SentMessage 类执行操作,以访问原始消息 (getOriginalMessage()) 和一些调试信息 (getDebug()),例如 HTTP 传输进行的 HTTP 调用,这对于调试错误很有用

1
2
3
4
5
6
7
8
9
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\SentMessageEvent;

public function onMessage(SentMessageEvent $event): void
{
    $message $event->getMessage();

    // do something with the message (e.g. get its id)
}

执行此命令以找出为此事件注册了哪些侦听器及其优先级

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\SentMessageEvent"

FailedMessageEvent

事件类FailedMessageEvent

FailedMessageEvent 允许在发生故障时对初始消息执行操作,并提供一些调试信息 (getDebug()),例如 HTTP 传输进行的 HTTP 调用,这对于调试错误很有用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\FailedMessageEvent;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;

public function onMessage(FailedMessageEvent $event): void
{
    // e.g you can get more information on this error when sending an email
    $error = $event->getError();
    if ($error instanceof TransportExceptionInterface) {
        $error->getDebug();
    }

    // do something with the message
}

执行此命令以找出为此事件注册了哪些侦听器及其优先级

1
$ php bin/console debug:event-dispatcher "Symfony\Component\Mailer\Event\FailedMessageEvent"

开发与调试

启用电子邮件捕获器

在本地开发时,建议使用电子邮件捕获器。如果您已通过 Symfony 配方启用 Docker 支持,则会自动配置电子邮件捕获器。此外,如果您正在使用 Symfony 本地 Web 服务器,则邮件程序 DSN 会通过 symfony 二进制 Docker 集成自动公开。

发送测试邮件

Symfony 提供了一个用于发送电子邮件的命令,这在开发期间用于测试发送电子邮件是否正常工作非常有用

1
2
3
# the only mandatory argument is the recipient address
# (check the command help to learn about its options)
$ php bin/console mailer:test [email protected]

如果配置了 Messenger 总线,此命令将绕过它,以便即使 Messenger 消费者未运行时也能轻松测试电子邮件。

禁用发送

在开发(或测试)时,您可能希望完全禁用消息的传递。您可以通过在您的 .env 配置文件或邮件程序配置文件(例如在 devtest 环境中)中使用 null://null 作为邮件程序 DSN 来实现此目的

1
2
3
4
5
# config/packages/mailer.yaml
when@dev:
    framework:
        mailer:
            dsn: 'null://null'

注意

如果您正在使用 Messenger 并路由到传输方式,则消息仍然会发送到该传输方式。

始终发送到同一地址

您可能希望始终将电子邮件发送到特定地址,而不是真实地址,而不是完全禁用传递

1
2
3
4
5
6
# config/packages/mailer.yaml
when@dev:
    framework:
        mailer:
            envelope:
                recipients: ['[email protected]']

使用 allowed_recipients 选项来指定 recipients 选项中定义的行为的例外情况;允许定向到这些特定收件人的电子邮件保持其原始目的地

1
2
3
4
5
6
7
8
9
10
# config/packages/mailer.yaml
when@dev:
    framework:
        mailer:
            envelope:
                recipients: ['[email protected]']
                allowed_recipients:
                    - '[email protected]'
                    # you can also use regular expression to define allowed recipients
                    - 'internal-.*@example.(com|fr)'

通过此配置,所有电子邮件都将发送到 [email protected],但发送到 [email protected][email protected] 等的电子邮件除外,这些电子邮件将照常接收。

7.1

allowed_recipients 选项在 Symfony 7.1 中引入。

编写功能测试

Symfony 提供了许多内置的邮件程序断言,以功能性地测试是否已发送电子邮件、其内容或标头等。它们在扩展 KernelTestCase 的测试类中或在使用 MailerAssertionsTrait 时可用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// tests/Controller/MailControllerTest.php
namespace App\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class MailControllerTest extends WebTestCase
{
    public function testMailIsSentAndContentIsOk(): void
    {
        $client = static::createClient();
        $client->request('GET', '/mail/send');
        $this->assertResponseIsSuccessful();

        $this->assertEmailCount(1); // use assertQueuedEmailCount() when using Messenger

        $email = $this->getMailerMessage();

        $this->assertEmailHtmlBodyContains($email, 'Welcome');
        $this->assertEmailTextBodyContains($email, 'Welcome');
    }
}

提示

如果您的控制器在发送电子邮件后返回重定向响应,请确保您的客户端不遵循重定向。内核在遵循重定向后会重新启动,并且消息将从邮件程序事件处理程序中丢失。

本作品,包括代码示例,根据 Creative Commons BY-SA 3.0 许可协议获得许可。
目录
    版本