PHP Mail 函数详解

61次阅读
没有评论

本文汇总 PHP 内置 mail() 函数的用法、常见示例(纯文本、HTML、附件)、常见问题排查与安全建议,并给出使用更可靠库(如 PHPMailer)的示例。


1. 基本概念

  • 函数签名bool mail(string $to, string $subject, string $message, array|string $additional_headers = [], string $additional_parameters = "")
  • 返回值:成功调用邮件传输代理(MTA)则返回 true,否则 false。注意:true 并不保证邮件被送达,只表示 PHP 成功将邮件交给本地 MTA(如 sendmail、exim、postfix)或通过配置的 SMTP 发送。
  • 行结束符:HTTP/SMTP 协议要求行结束使用 \r\n。在构造头部时请使用 \r\n;Windows 与 Linux 上均建议使用 \r\n

2. 简单示例(纯文本)

$to = 'recipient@example.com';
$subject = '测试邮件';
$message = "这是一封测试邮件。\n多行内容示例。";
$headers = "From: sender@example.com\r\n" .
           "Reply-To: sender@example.com\r\n" .
           "X-Mailer: PHP/" . phpversion();

if (mail($to, $subject, $message, $headers)) {
    echo "邮件发送请求已提交。";
} else {
    echo "邮件发送请求失败。";
}

注意:若在 additional_parameters 中使用 -f 指定发信人(envelope sender),例如 mail(..., $headers, '-fwebmaster@example.com'),可让 MTA 设置 Return-Path(某些主机需要此项以避免被拒)。

3. 发送 HTML 邮件

$to = 'recipient@example.com';
$subject = 'HTML 邮件示例';
$html = '
<html><body><h1>标题</h1>
<p>这是 <b>HTML</b> 格式的邮件。</p></body></html>';
$headers = "MIME-Version: 1.0\r\n" .
           "Content-type: text/html; charset=UTF-8\r\n" .
           "From: Sender <sender@example.com>\r\n";

mail($to, $subject, $html, $headers);

4. 附件(通过 MIME multipart

使用 mail() 发送附件需要自己构建 MIME 边界并进行 base64 编码。示例:

$to = 'recipient@example.com';
$subject = '含附件的邮件';
$message = "这封邮件包含一个附件,请查看。";

$filePath = '/path/to/file.pdf';
$fileName = basename($filePath);
$fileData = chunk_split(base64_encode(file_get_contents($filePath)));

$boundary = md5(time());
$headers = "From: sender@example.com\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: 7bit\r\n\r\n";
$body .= $message . "\r\n\r\n";

$body .= "--{$boundary}\r\n";
$body .= "Content-Type: application/octet-stream; name=\"{$fileName}\"\r\n";
$body .= "Content-Transfer-Encoding: base64\r\n";
$body .= "Content-Disposition: attachment; filename=\"{$fileName}\"\r\n\r\n";
$body .= $fileData . "\r\n";
$body .= "--{$boundary}--";

mail($to, $subject, $body, $headers);

提醒:构建 multipart 内容时细节较多,编码、边界、头部必须严格正确,否则邮件客户端无法识别附件或显示错误。

5. 常见问题与排查

  • mail() 返回 true 但邮件未送达:检查本机 MTA 日志(例如 /var/log/mail.log/var/log/maillog)。很多共享主机限制外发或要求验证发信地址。
  • 邮件被当作垃圾邮件:配置 SPF、DKIM、DMARC,确保发信域名与邮件服务器匹配;邮件正文和头部不要包含垃圾邮件特征。
  • Header 注入攻击:不要把未验证的用户输入直接放入 FromSubjectheaders。应剥离换行符(\r\n)并严格验证邮箱格式。
  • 换行问题:务必使用 \r\n,并避免头部末尾多余空行。
  • Windows 与 sendmail 不同:Windows 下 PHP 使用 SMTP 配置(php.iniSMTP/smtp_port/sendmail_from),Linux 下通常通过 sendmail 程序交由本地 MTA 处理。

6. 安全建议

  • 验证并清理所有来自用户的邮件相关输入。
  • 使用 filter_var($email, FILTER_VALIDATE_EMAIL) 检查邮箱格式。
  • 尽可能使用经过验证的 SMTP(带 TLS)和身份验证的方式发送邮件,避免依赖系统 MTA。

7. 更稳健的替代方案:PHPMailer(推荐)

PHPMailer、SwiftMailer(现已由 Symfony Mailer 替代)能更方便地发送 HTML、附件与 SMTP 验证,且处理了许多边界细节。

安装(Composer):

composer require phpmailer/phpmailer

示例(使用 SMTP):

use PHPMailer\\PHPMailer\\PHPMailer;
use PHPMailer\\PHPMailer\\Exception;

require 'vendor/autoload.php';

$mail = new PHPMailer(true);
try {
    $mail->isSMTP();
    $mail->Host = 'smtp.example.com';
    $mail->SMTPAuth = true;
    $mail->Username = 'smtp_user';
    $mail->Password = 'smtp_pass';
    $mail->SMTPSecure = 'tls';
    $mail->Port = 587;

    $mail->setFrom('sender@example.com', 'Sender Name');
    $mail->addAddress('recipient@example.com', 'Recipient');
    $mail->Subject = 'PHPMailer 测试';
    $mail->isHTML(true);
    $mail->Body = '
<h1>测试邮件</h1>
<p>通过 SMTP 发送</p>';
    $mail->addAttachment('/path/to/file.pdf');

    $mail->send();
    echo '邮件已发送';
} catch (Exception $e) {
    echo "邮件发送失败: {$mail->ErrorInfo}";
}

PHPMailer 更适合生产环境,支持 SMTP 验证、TLS、OAuth 等高级功能,并且更容易调试。

7.1 TLS 与证书校验(强制验证服务器证书)

在生产环境中应始终使用加密的 SMTP 连接(STARTTLS 或 SSL/TLS),并尽可能验证服务器证书以防止中间人攻击。PHPMailer 提供 SMTPOptions 来控制底层流的验证行为。示例如下:

use PHPMailer\PHPMailer\PHPMailer;
require 'vendor/autoload.php';

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = 'smtp_user';
$mail->Password = 'smtp_pass';
// 使用 STARTTLS
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // 或 'tls'
$mail->Port = 587;

// 强制证书校验
$mail->SMTPOptions = [
    'ssl' => [
        'verify_peer' => true,
        'verify_peer_name' => true,
        'allow_self_signed' => false,
        // 指定 CA 文件(可选),例如在某些系统上需要指定完整路径
        'cafile' => '/etc/ssl/certs/ca-certificates.crt',
    ],
];

$mail->setFrom('sender@example.com', 'Sender');
$mail->addAddress('recipient@example.com');
$mail->Subject = '带 TLS 与证书校验的邮件';
$mail->Body = '测试 TLS 与证书校验';

$mail->send();

要点说明:

  • verify_peerverify_peer_name 都应设为 true 以确保服务器证书由受信任的 CA 签发且主机名匹配。
  • allow_self_signed 设为 false 可避免接受自签名证书(除非你明确控制并信任该证书)。
  • 在某些托管或开发环境中,系统默认的 CA 列表不全,可以通过 cafile 指定 CA 证书文件(PEM 格式)。

如果需要允许“机会性 TLS”(由服务器协商 STARTTLS),可保持 SMTPAutoTLStrue(PHPMailer 默认),但仍建议在生产环境明确要求 SMTPSecure 并开启证书检验。

7.2 使用 OAuth2(以 Gmail 为例)

许多邮件服务(如 Gmail)推荐或要求使用 OAuth2 而非传统用户名/密码登录。PHPMailer 支持 XOAUTH2:你需要安装 league/oauth2-client 及对应提供者(例如 league/oauth2-google),并在 Google Cloud Console 中为你的应用创建 OAuth 凭据与刷新令牌(refresh token)。

示例(使用刷新令牌):

use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\OAuth;
use League\OAuth2\Client\Provider\Google;

require 'vendor/autoload.php';

$provider = new Google([
    'clientId' => 'GOOGLE_CLIENT_ID',
    'clientSecret' => 'GOOGLE_CLIENT_SECRET',
]);

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.gmail.com';
$mail->Port = 587;
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->SMTPAuth = true;

// 使用 XOAUTH2
$mail->AuthType = 'XOAUTH2';
$mail->setOAuth(new OAuth([
    'provider' => $provider,
    'clientId' => 'GOOGLE_CLIENT_ID',
    'clientSecret' => 'GOOGLE_CLIENT_SECRET',
    'refreshToken' => 'REFRESH_TOKEN_FROM_OAUTH_FLOW',
    'userName' => 'your-email@gmail.com',
]));

$mail->setFrom('your-email@gmail.com', 'Your Name');
$mail->addAddress('recipient@example.com');
$mail->Subject = '通过 OAuth2 发送的邮件';
$mail->Body = '这是通过 OAuth2 授权发送的邮件示例。';

$mail->send();

注意事项与准备工作:

  • 需要在 Google API 控制台启用 Gmail API 并创建 OAuth 客户端,获取 clientIdclientSecret,并用授权流程拿到 refreshToken
  • 安装依赖:
composer require phpmailer/phpmailer league/oauth2-client league/oauth2-google
  • OAuth2 的 token 刷新与管理可交由 league/oauth2-client 处理,PHPMailer 的 OAuth 封装会在发送时使用 refreshToken 自动获取或刷新访问令牌。
  • 对于其他邮件服务(非 Gmail),请参考对应服务的 OAuth2 提供者或实现自定义 Provider。

7.3 证书指纹(可选的“固定”验证)

在对安全要求极高的场景,可以在建立连接后对服务器证书进行指纹校验(certificate pinning)。PHPMailer 本身不直接提供指纹校验接口,但可以通过 SMTP 子类化或在应用层用 stream_socket_enable_crypto() 之类的底层函数检索对端证书并验证指纹。这类方法更复杂且易出错,通常只在你能控制服务器证书且需要更强保障时使用。

示例思路(伪代码):

// 建立 socket/SMTP 连接后,通过 stream_get_meta_data / stream_context 获取证书信息
$cert = /* 获取对端证书信息 */;
$fingerprint = hash('sha256', $cert);
if ($fingerprint !== '期望的指纹值') {
    // 中止连接
}

如非必要,优先使用 CA 验证 + cafile/capath 的方式来保证连接安全。

8. 调试小技巧

  • 使用本地 SMTP 捕获工具(MailHog、Mailtrap、MailCatcher)进行开发测试,避免真实外发。
  • 在 Linux 上通过 telnet smtp.example.com 25 手工测试 SMTP 交互。
  • 查看 PHP 错误日志与 MTA 日志获取更多信息。

9. 总结

  • mail() 适合发送简单通知或测试,但在生产环境建议使用 PHPMailer 或其他库通过认证 SMTP 发送,以提高可交付性和安全性。
  • 发送附件与 HTML 时要特别小心 MIME 构造与编码,避免直接拼接用户输入到头部以防注入攻击。

正文完
 1
评论(没有评论)

YanQS's Blog