网站开发里发邮件这个功能躲不开。用户注册要发验证邮件、下单要发订单通知、找回密码要发重置链接。PHP内置了一个mail()函数能干这事,不用装第三方库,开箱即用。
但有个事必须说在前头: mail()函数依赖服务器的邮件传输代理(MTA,Mail Transfer Agent)。你在自己电脑上装个XAMPP或者WAMP,默认没配邮件服务,直接跑mail()大概率发不出去。生产环境一般运维会配好postfix或者sendmail。
我以前踩过这个坑,本地测试折腾半天没反应,后来用SMTP配合PHPMailer才搞定。所以这节课讲mail()的用法,但实际项目中如果发邮件量大或者需要记录发送日志,可以考虑用第三方组件。
mail()函数语法
bool mail(string $to, string $subject, string $message, string $additional_headers, string $additional_parameters)
参数说明:
| 参数 | 作用 | 备注 |
|---|---|---|
| $to | 收件人 | 支持单人或多人,多个用逗号隔开 |
| $subject | 邮件主题 | 纯文本,不要换行 |
| $message | 邮件正文 | 每行不超70字符,用\r\n换行 |
| $additional_headers | 额外头信息 | From、CC、BCC、MIME等 |
| $additional_parameters | 额外参数 | 传递给sendmail程序,一般不用 |
收件人写法:
user@example.com
user1@example.com, user2@example.com
张三 <zhangsan@example.com>
张三 <zhangsan@example.com>, 李四 <lisi@example.com>
最简单的文本邮件
<?php
// Windows服务器可能需要设置sendmail_from
ini_set("sendmail_from", "noreply@ebingou.cn");
$to = "alan@ebingou.cn";
$subject = "这是一封测试邮件";
$message = "你好,这是一条来自PHP mail()函数的纯文本消息。";
$headers = "From: system@ebingou.cn \r\n";
$result = mail($to, $subject, $message, $headers);
if ($result == true) {
echo "邮件已发送";
} else {
echo "邮件发送失败,请检查服务器邮件配置";
}
?>
收到的邮件内容:
From: system@ebingou.cn
To: alan@ebingou.cn
Subject: 这是一封测试邮件
你好,这是一条来自PHP mail()函数的纯文本消息。
个人经验: ini_set("sendmail_from", ...) 这行只在Windows服务器上有用。Linux服务器是在php.ini里配置sendmail_path。如果你不确定服务器环境,发不出去先问问运维。
用wordwrap()处理长文本
邮件协议要求每行不超过70字符。如果用mail()发几百字不带换行的内容,部分邮件服务器会出问题。
<?php
$long_message = "这是一段很长的文本内容。在PHP中使用mail()发送邮件时,如果一行文字超过了70个字符,可能会被某些邮件服务器截断或导致格式错乱。因此建议使用wordwrap()函数来统一处理换行。";
$wrapped_message = wordwrap($long_message, 70, "\r\n");
if (mail("alan@ebingou.cn", "长文本测试", $wrapped_message)) {
echo "邮件发送成功";
} else {
echo "邮件发送失败";
}
?>
wordwrap()的三个参数: 要处理的字符串、每行较大宽度、换行符用什么(\r\n是邮件标准)。
发送HTML格式邮件
纯文本太单调了,想加个样式、链接、图片什么的,得告诉邮件客户端按HTML解析。
<?php
$to = "alan@ebingou.cn";
$subject = "欢迎注册成功";
$message = "
<div style='font-family:微软雅黑; padding:20px;'>
<h2 style='color:#333;'>欢迎加入!</h2>
<p>您的账号已注册成功,请点击下方链接激活:</p>
<a href='https://www.ebingou.cn/activate?code=abc123'>激活账号</a>
<hr>
<small style='color:#999;'>如果按钮无法点击,请复制以下链接到浏览器:<br>https://www.ebingou.cn/activate?code=abc123</small>
</div>
";
$headers = "From: service@ebingou.cn \r\n";
$headers .= "MIME-Version: 1.0 \r\n";
$headers .= "Content-type: text/html; charset=UTF-8 \r\n";
$result = mail($to, $subject, $message, $headers);
if ($result) {
echo "HTML邮件已发送";
} else {
echo "发送失败";
}
?>
关键点:
-
MIME-Version头告诉邮件客户端版本
-
Content-type: text/html 告诉客户端按HTML渲染
-
charset=UTF-8 保证中文不乱码
个人见解: 有些老旧的邮件客户端(比如Outlook 2007)对CSS支持有限,内联样式比外部样式更稳。写邮件HTML很好用table布局,别指望flex和grid。
发送带附件的邮件
这地方比较复杂。附件需要做base64编码,还要用multipart/mixed边界分隔正文和附件。
代码号学习编程示例:
<?php
$to = "alan@ebingou.cn";
$subject = "报表附件 - 月度销售数据";
$message = "您好,本月销售数据报表请见附件。";
// 要发送的文件路径(换成你自己的)
$file_path = "/tmp/monthly_report.pdf";
$file_name = "月度销售报表.pdf";
// 读取文件内容
$file_handle = fopen($file_path, "r");
if (!$file_handle) {
die("无法打开文件");
}
$file_size = filesize($file_path);
$file_content = fread($file_handle, $file_size);
fclose($file_handle);
// Base64编码,每76字符加换行
$encoded_content = chunk_split(base64_encode($file_content));
// 生成唯一边界标识
$boundary = md5(time());
// 组装邮件头
$headers = "From: report@ebingou.cn\r\n";
$headers .= "MIME-Version: 1.0\r\n";
$headers .= "Content-Type: multipart/mixed; boundary=\"$boundary\"\r\n";
// 正文部分
$body = "--$boundary\r\n";
$body .= "Content-Type: text/plain; charset=UTF-8\r\n";
$body .= "Content-Transfer-Encoding: 8bit\r\n\r\n";
$body .= "$message\r\n";
// 附件部分
$body .= "--$boundary\r\n";
$body .= "Content-Type: application/octet-stream; name=\"$file_name\"\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n";
$body .= "Content-Disposition: attachment; filename=\"$file_name\"\r\n\r\n";
$body .= "$encoded_content\r\n";
$body .= "--$boundary--";
// 发送
$result = mail($to, $subject, $body, $headers);
if ($result) {
echo "带附件的邮件已发送";
} else {
echo "发送失败";
}
?>
说一下这里的设计思路: multipart/mixed表示邮件由多个部分组成,每个部分用boundary隔开。boundary这个值要足够随机,不然可能和邮件正文内容撞车。用md5(time())产生的32位字符串基本够用了。
CC(抄送)用法
<?php
$to = "zhangsan@ebingou.cn";
$subject = "项目进度周报";
$message = "本周已完成数据库优化和缓存层搭建。";
$headers = "From: pm@ebingou.cn\r\n";
$headers .= "CC: lisi@ebingou.cn, wangwu@ebingou.cn";
if (mail($to, $subject, $message, $headers)) {
echo "邮件已发送,已抄送李四和王五";
} else {
echo "发送失败";
}
?>
CC和To的区别: CC的人能看到To是谁,大家都能看到完整的收件人列表。不像BCC。
BCC(密送)用法
<?php
$to = "manager@ebingou.cn";
$subject = "月报 - 请勿外传";
$message = "本月部门数据统计已完成,见附件。";
$headers = "From: hr@ebingou.cn\r\n";
$headers .= "BCC: boss@ebingou.cn"; // 老板秘密收到一份
if (mail($to, $subject, $message, $headers)) {
echo "邮件已发送,密送副本已发出";
} else {
echo "发送失败";
}
?>
实际场景: 给客户发邮件想抄送给内部同事,但又不想让客户看到内部邮箱,就用BCC。BCC接收者出现在邮件里,但To和CC的人都看不到。
多收件人发送
<?php
// 多个收件人用逗号分隔
$to = "user1@ebingou.cn, user2@ebingou.cn, user3@ebingou.cn";
$subject = "系统公告 - 临时维护通知";
$message = "服务器将于今晚22:00进行维护,预计2小时。";
$headers = "From: admin@ebingou.cn\r\n";
mail($to, $subject, $message, $headers);
?>
注意: 收件人太多或者频繁调用mail(),要考虑邮件服务器的发送频率限制。有的主机商一小时只允许发几十封。
邮件头格式规范
邮件头每行末尾必须是\r\n(回车+换行)。多个头信息拼在一起时,下一行直接拼接就行。
$headers = "From: sender@ebingou.cn\r\n";
$headers .= "Reply-To: support@ebingou.cn\r\n";
$headers .= "CC: cc@ebingou.cn\r\n";
$headers .= "BCC: bcc@ebingou.cn\r\n";
$headers .= "X-Priority: 3\r\n"; // 自定义头,非标准但很多客户端认
常用邮件头:
| 头名称 | 作用 |
|---|---|
| From | 发件人地址 |
| Reply-To | 回复地址(可不同于From) |
| CC | 抄送 |
| BCC | 密送 |
| Return-Path | 退信接收地址 |
| X-Mailer | 声明用哪个程序发的(比如PHP) |
常见问题和排查
邮件发不出去,先检查这几项:
-
mail()返回false但不给原因 —— 看PHP错误日志,一般是sendmail路径不对
-
邮件进了垃圾箱 —— 缺少SPF记录,或者发件人域名没做邮件认证
-
中文乱码 —— 检查Content-Type有没有charset=UTF-8
-
附件收不到或打不开 —— 检查boundary边界字符串是否唯一,base64编码是否正确
检查邮件服务器状态的命令(Linux):
# 查看sendmail或postfix是否在运行
systemctl status sendmail
systemctl status postfix
# 看邮件队列
mailq
本节课程知识要点
-
mail()依赖服务器MTA —— 本地测试发不出去是正常的,需要配置或用SMTP代替
-
每行不超过70字符 —— 长文本用wordwrap()处理,避免被截断
-
HTML邮件必须加Content-Type头 —— text/html而不是text/plain
-
附件用base64编码 —— 配合multipart/mixed边界分隔
-
邮件头每行末尾是\r\n —— 不是\n也不是\r,标准的换行序列
-
From头建议实域名 —— 很多邮件服务商会校验,写成@localhost容易被拒收
-
生产环境谨慎用mail() —— 大批量发邮件建议用SMTP + 第三方库(如PHPMailer或Symfony Mailer),可以记录发送日志、处理队列、避免被当成垃圾邮件源
实际项目中的取舍
mail()函数简单直接,但缺点也明显:
-
没有发送失败后的重试机制
-
不方便记录日志
-
附件处理代码写起来麻烦
-
依赖服务器配置,换个环境可能就不工作了
如果你的项目只是偶尔发几封通知邮件,比如用户注册验证,用mail()没问题。如果是电商订单提醒、每日报表这类需要保证送达率的场景,花点时间集成PHPMailer或SwiftMailer,支持SMTP、支持队列、支持日志,后期运维会省心很多。