发送邮件是网站的常用功能,用户激活、找回密码等场景常需要发送邮件到用户邮箱。本文先回顾发送邮件的相关概念,再给出使用 PHP 发送邮件的示例代码。
发送短信
从功能上看,短信和邮件类似,用途常是通知和安全校验。发送短信(基本上)需要向供应商付费,所以短信供应商有动力提供清晰的文档,易用的接口方便用户接入。一般而言,发送短信的是:
寻找供应商,例如阿里大鱼、聚合数据等;
注册账户,获取 appid 和 appkey;
申请模板;
查看接口文档,集成到应用中;
调用 API 发送短信。
流程简单易懂,接入和使用也十分便捷,基本上一两小时内就能对接和测试好。用户无需考虑讯息在通讯过程中的编码、寻址下发等细节,缺点是要付费。
邮件一般是免费服务,相关支持没那么到位,这也要理解。各种编程语言发送邮件的类库不少,从信源角度看基本可以分成两类:从本机发送和从第三方邮件服务商发送。为了理解邮件发送的流程,先介绍一些相关概念。
相关概念
大部分接触到互联网的人都有使用邮件的经验,但基本上限于邮件客户端、网页端和提供商这几个概念。作为一个开发,理解本节中的以下概念能更好的帮你掌握邮件通讯中的细节。
MUA:Mail User Agent,邮件用户代理。用户代理是开发中经常接触到的词,主要指 理解人的意图并代表用户向资源方请求的工具。例如浏览器是最常用的用户代理,以 HTTP/HTTPS 协议格式向 web 服务器发送请求,并解析响应,渲染后呈现给用户。邮件用户代理,常见的是 Foxmail、Outlook 这类工具,人们写好邮件后,按格式封装邮件内容与邮件服务器通讯。
MTA:Mail Transfer Agent,邮件传输代理,帮用户收发邮件的程序。常说的邮件服务器指的就是 MTA,开源的程序有 sendmail,postfix,QMail 等。
MRA:Mail Retrieval Agent,邮件收取代理,将用户的邮件从邮件服务器取回本地。邮件客户端是常见的 MRA。
SMTP:Simple Mail Transfer Protocol,简单邮件传输协议。用户与邮件服务器、邮件服务器互相传递邮件均使用该协议(默认明文,可使用 SSLTLS 加密)。
POP3/IMAP:Post Office Protocol version 3/Internet Message Access Protocol,邮局协议版本 3 或网络信息获取协议,客户端从服务端获取邮件时使用的协议。
用户 A(163 邮箱)向用户 B(Gmail 邮箱)发信,用户 B 获取信件的过程涉及到上述的概念。流程和概念关系可用如下简图表示:
注:上图给出的是邮件发送的大体流程,其他 MSA、MDA、ESMTP、SMTPS 等可能会出现在整个流程中,但不影响邮件收发的理解。下文中会提到的缩写和概念会注明,其他请自行查询。
postfix
Linux 下发送邮件的软件主要是 sendmail 和 postfix,它们在系统中充当上文概念中的 MTA/MDA(Mail Delivery Agent,邮件投递代理)角色。它帮助用户向外发送邮件,接收邮件投递到用户信箱(默认位置 /var/spool/mail/ 用户名)。
sendmail 是老牌的邮件软件,知名度非常高。但是 Wietse(Wietse Zweitze Venema)用的不爽,于是有了 postfix。postfix 命令(几乎)兼容于 sendmail,但更高效和安全(后缀 fix 的由来),是目前大部分 Linux 发行版的默认邮件收发软件,推荐使用 postfix 而非 sendmail。
postfix 的主要配置文件是 /etc/postfix/main.cf,配置文件的注释非常全,选项基本是自解释的。最重要的几个配置是:myhostname、myorigin、inet_interfaces、inet_protocols 以及 mydestination(如果你打算收外网来信的话)。
需要注意 inet_interfaces 配置为 localhost 时,inet_protocols 的值应为 ipv4,否则可能会出现类似 postfix: fatal: parameter inet_interfaces: no local interface found for ::1 的错误提示。
与邮件相关的几个常用 postfix 命令是:
postquque,查看邮件发送队列。postqueue - p 可取代 sendmail 中的 mailq 命令,postqueue - f 刷新队列(强制尝试发送队列中的邮件)。
postcat,查看未发送邮件的信息。例如 postcat -q xxxx(xxxx 是 postqueue 或者 mailq 显示的未发送队列 ID)可查看邮件的详细信息,postcat -b -q xxxxx 只查看邮件正文。
postsuper,超级用户才可使用的邮件管理程序。postsuper -d xxxx,删除队列 ID 为 xxxxx 的邮件;postsuper -h xxxxx,暂停队列 ID 为 xxxx 的邮件发送;postsuper -d ALL,删除队列中的所有邮件。
以上介绍对于发送邮件基本已足够。注意,mail 命令发送的邮件能投递的前提是 postfix 正在运行(ps aux | grep postfix | grep -v grep 输出不为空)。
有了 postfix,配置好后可以对外发送邮件,也能收取外网发送过来的邮件,但限于命令行操作。想用 foxmail 等客户端收发邮件,需要让服务器支持 POP3/IMAP 协议。开源的 dovecot 可以实现这个功能。dovecot 服务于收邮件而非发送,了解其对开发中的帮助不大。
如果想搭建一套完整的邮件系统(包括网页端支持、垃圾邮件过滤、病毒查杀、传输加密等),建议参考或使用国产开源的 EwoMail。
了解 postfix 对开发中发送邮件帮助有多大?说实话,几乎没有帮助。原因是为了防止垃圾邮件泛滥,各大云服务器厂商屏蔽了 25 端口(Google Cloud 连 465 都干掉了)。亚马逊云通过申请还有放行的可能(但有速率和每日额度限制),其他厂商几乎不会让你使用自己的域名从本机直接发送邮件。封禁 25 端口,必须使用第三方的邮件服务,几乎是业界的标准做法。
聪明的人可能想到,使用 465 加密端口(基于 SMTPS,SMTP over SSL 协议)或 587 端口(SMTP over STARTTLS 协议)发送邮件,是不是就能绕开限制了?阿里云 / 腾讯云等厂商并不封禁 465 端口,发送邮件可以使用该端口而无需申请。但注意 465 和 587 端口是客户端和邮件服务器通讯使用的端口,邮件服务器之间通讯使用 25 端口。你可以通过 465 端口连接到 Gmail 邮箱对外发送邮件,但无法让 postfix 使用 465 端口投递邮件到 hotmail 邮件服务器。
总结来说,sendmail/postfix 作为垃圾和欺诈邮件泛滥前的邮件服务器软件,对业界贡献很大。随着云服务器的盛行,几乎无法以指向本机的域名向外发送邮件,sendmail/postfix 除了在本机内发送提醒邮件,用处已然不大。要对外发送邮件,要么自建机房,要么使用第三方邮件系统。
PHP 的 mail 函数
作为 PHP 开发中,了解 sendmail/postfix 还是有点用处。mail 函数默认使用 sendmail/postfix 发送邮件,了解相关配置,就能知道为啥能工作 / 为啥不能工作。
简单来说,要让 PHP 自带的 mail 函数正常工作,需要做以下事情:
申请域名,在 DNS 解析中设置 MX 记录,指向本机(非合法主机(FQDN, Fully Qualified Domain Name)发送的邮件都会被当做垃圾邮件直接丢弃);
安装 sendmail/postfix,配置软件并运行;
配置防火墙、安全组,放行端口。
发送效率低、非面向对象的调用方式,配置麻烦以及云服务器厂商的封锁,是使用 mail 函数的最大阻碍。所以做 PHP 以来,本人并未直接用过 mail 函数。
PHP 发送邮件
发个邮件要了解这么多,会让人觉得很心累。说好的 PHP 是最好的语言呢?
PHP 发送邮件也可以很简单,推荐方式就是使用 Swift Mailer 或 PHPMailer 等类库。引入这些类库后,注册第三方邮箱(比如 Gmail、QQ 等),填好用户名密码,配置好 STMP 地址和端口,就能像发送短信一样轻松发送邮件。当然这些类库也支持使用 sendmail/postfix 发送邮件,但我想你不会再这样做了。
以 Swift Mailer 为例,直接上代码说明使用 PHP 发送邮件也是一个非常简单的事情!
首先,在项目中引入 Swift Mailer:
composer require “swiftmailer/swiftmailer:^6.0”
然后准备好邮件内容(以文本文件为例,不带附件):
$message = (new Swift_Message(‘Test Message’))
->setFrom(['tlanyan@tlanyan.me' => 'tlanyan'])
->setTo(['tlanyan1@tlanyan.me'])
->setBody('Hello, this is a test mail from Swift Mailer!');
接着,设置好邮件传输方式(使用 Gmail 邮箱):
$transport = (new Swift_SmtpTransport(‘smtp.gmail.com’, 465, ‘ssl’))
->setUsername('username')
->setPassword('password');
或者使用 sendmail/postfix 的方式(不推荐):
$transport = (new Swift_SendmailTransport());
最后,使用 transport 构造 mailer 实例,发送邮件:
$mailer = new Swift_Mailer($transport);
$result = $mailer->send($message);
老板再也不用担心发送邮件收不到了,So easy!
总结
本文先回顾了发送邮件的相关概念,说明不推荐使用内置的 mail 函数原因,最后给出了使用第三方类库发送邮件的代码示例。
phper 在进阶的时候总会遇到一些问题和瓶颈,业务代码写多了没有方向感,不知道该从那里入手去提升,对此我整理了一些资料,包括但不限于:分布式架构、高可扩展、高性能、高并发、服务器性能调优、TP6,laravel,YII2,Redis,Swoole、Kafka、Mysql 优化、shell 脚本、Docker、微服务、Nginx 等多个知识点高级进阶干货需要的可以免费分享给大家,需要请戳这里