Skip to content

Mail 扩展

PHP 的 mail() 函数是最基本的邮件发送方式,适合从 PHP 脚本发送简单邮件。虽然功能有限,但在特定场景下(如系统通知、表单邮件)仍然常用。本节将讲解 mail() 函数的用法以及更推荐使用的 PHPMailer 替代方案。

前置知识

阅读本节前,建议先了解:IMAP / POP3 / SMTP字符串处理

基础概念

mail() 函数

PHP 的 mail() 函数使用系统默认的邮件发送程序(sendmail、postfix 等)来发送邮件。

安装

bash
# mail() 函数通常是 PHP 默认启用的
# 需要系统安装 sendmail 或 postfix

# Ubuntu/Debian
sudo apt-get install sendmail
# 或
sudo apt-get install postfix

# php.ini 配置
; sendmail_path = /usr/sbin/sendmail -t -i

mail() 基本用法

发送纯文本邮件

php
<?php
declare(strict_types=1);

/**
 * 使用 mail() 发送纯文本邮件
 */
function sendPlainTextEmail(
    string $to,
    string $subject,
    string $message,
    string $from = 'noreply@example.com'
): bool {
    $headers = "From: {$from}\r\n";
    $headers .= "Reply-To: {$from}\r\n";
    $headers .= "X-Mailer: PHP/" . PHP_VERSION . "\r\n";
    $headers .= "MIME-Version: 1.0\r\n";

    $result = mail($to, $subject, $message, $headers);

    if (!$result) {
        throw new RuntimeException("邮件发送失败");
    }

    return true;
}

// 使用示例
sendPlainTextEmail(
    'recipient@example.com',
    '测试邮件',
    "你好,\n\n这是一封测试邮件。\n\n谢谢。"
);

发送 HTML 邮件

php
<?php
declare(strict_types=1);

/**
 * 发送 HTML 邮件
 */
function sendHtmlEmail(
    string $to,
    string $subject,
    string $htmlContent,
    string $textContent = '',
    string $from = 'noreply@example.com'
): bool {
    $boundary = '----=_Part_' . md5(uniqid((string) time()));

    $headers = "From: {$from}\r\n";
    $headers .= "MIME-Version: 1.0\r\n";
    $headers .= "Content-Type: multipart/alternative; boundary=\"{$boundary}\"\r\n";

    $body = "--{$boundary}\r\n";
    $body .= "Content-Type: text/plain; charset=UTF-8\r\n";
    $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
    $body .= chunk_split(base64_encode($textContent ?: strip_tags($textContent))) . "\r\n";

    $body .= "--{$boundary}\r\n";
    $body .= "Content-Type: text/html; charset=UTF-8\r\n";
    $body .= "Content-Transfer-Encoding: base64\r\n\r\n";
    $body .= chunk_split(base64_encode($htmlContent)) . "\r\n";

    $body .= "--{$boundary}--\r\n";

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

// 使用示例
$html = <<<HTML
<html>
<body>
    <h1>你好,用户</h1>
    <p>这是一封 <strong>HTML</strong> 邮件。</p>
    <table style="border: 1px solid #ccc; width: 100%;">
        <tr><td>产品</td><td>价格</td></tr>
        <tr><td>Widget</td><td>99.00</td></tr>
    </table>
</body>
</html>
HTML;

$text = "你好,用户\n这是一封 HTML 邮件的纯文本版本。";

sendHtmlEmail(
    'recipient@example.com',
    'HTML 测试邮件',
    $html,
    $text
);

发送带附件的邮件

php
<?php
declare(strict_types=1);

/**
 * 发送带附件的邮件
 */
function sendEmailWithAttachment(
    string $to,
    string $subject,
    string $body,
    array $attachments = [],
    string $from = 'noreply@example.com'
): bool {
    $boundary = '----=_Mixed_' . md5(uniqid((string) time()));

    $headers = "From: {$from}\r\n";
    $headers .= "MIME-Version: 1.0\r\n";
    $headers .= "Content-Type: multipart/mixed; boundary=\"{$boundary}\"\r\n";

    $message = "--{$boundary}\r\n";
    $message .= "Content-Type: text/plain; charset=UTF-8\r\n";
    $message .= "Content-Transfer-Encoding: base64\r\n\r\n";
    $message .= chunk_split(base64_encode($body)) . "\r\n";

    foreach ($attachments as $attachment) {
        $filePath = $attachment['path'];
        $fileName = $attachment['name'] ?? basename($filePath);
        $fileType = mime_content_type($filePath);

        if (!file_exists($filePath)) {
            continue;
        }

        $fileContent = file_get_contents($filePath);

        $message .= "--{$boundary}\r\n";
        $message .= "Content-Type: {$fileType}; name=\"{$fileName}\"\r\n";
        $message .= "Content-Transfer-Encoding: base64\r\n";
        $message .= "Content-Disposition: attachment; filename=\"{$fileName}\"\r\n\r\n";
        $message .= chunk_split(base64_encode($fileContent)) . "\r\n";
    }

    $message .= "--{$boundary}--\r\n";

    return mail($to, $subject, $message, $headers);
}

// 使用示例
sendEmailWithAttachment(
    'recipient@example.com',
    '带附件的邮件',
    '请查收附件中的文件。',
    [
        ['path' => '/path/to/report.pdf'],
        ['path' => '/path/to/data.csv', 'name' => 'report-data.csv'],
    ]
);

附加参数

额外命令行参数

php
<?php
declare(strict_types=1);

// mail() 的第五个参数:额外的 sendmail 命令行参数
// -f sender@example.com  指定发件人地址(envelope sender)

$result = mail(
    $to,
    $subject,
    $message,
    $headers,
    '-f sender@example.com'
);

// 设置 Return-Path 头
$headers .= "Return-Path: bounces@example.com\r\n";

发送给多个收件人

php
<?php
declare(strict_types=1);

// 多个收件人(逗号分隔)
$to = 'user1@example.com, user2@example.com';

// 使用 Cc 和 Bcc
$headers = "From: sender@example.com\r\n";
$headers .= "Cc: cc1@example.com, cc2@example.com\r\n";
$headers .= "Bcc: bcc@example.com\r\n";

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

使用 PHPMailer(推荐)

为什么推荐 PHPMailer

mail() 函数的局限性:

  • 依赖系统 sendmail 程序
  • SMTP 认证支持有限
  • HTML 邮件构建复杂
  • 附件处理繁琐
  • 错误处理不够详细
  • 缺少邮件模板支持

安装 PHPMailer

bash
composer require phpmailer/phpmailer

PHPMailer 示例

php
<?php
declare(strict_types=1);

use PHPMailer\PHPMailer\PHPMailer;

/**
 * 基于 PHPMailer 的邮件发送器
 */
class MailSender
{
    private PHPMailer $mailer;

    public function __construct(
        string $host,
        int $port = 587,
        string $username = '',
        string $password = '',
        string $fromName = '',
        bool $smtpAuth = true
    ) {
        $this->mailer = new PHPMailer(true);

        // SMTP 配置
        $this->mailer->isSMTP();
        $this->mailer->Host = $host;
        $this->mailer->Port = $port;
        $this->mailer->SMTPAuth = $smtpAuth;
        $this->mailer->Username = $username;
        $this->mailer->Password = $password;

        // 加密方式
        $this->mailer->SMTPSecure = match ($port) {
            465 => PHPMailer::ENCRYPTION_SMTPS,
            587 => PHPMailer::ENCRYPTION_STARTTLS,
            default => '',
        };

        // 字符编码
        $this->mailer->CharSet = 'UTF-8';

        // 发件人
        $this->mailer->setFrom($username, $fromName);
    }

    /**
     * 发送 HTML 邮件
     */
    public function sendHtml(
        string $to,
        string $subject,
        string $html,
        string $text = ''
    ): bool {
        $this->mailer->clearAddresses();
        $this->mailer->addAddress($to);
        $this->mailer->Subject = $subject;
        $this->mailer->Body = $html;
        $this->mailer->AltBody = $text;
        $this->mailer->isHTML(true);

        try {
            return $this->mailer->send();
        } catch (\Exception $e) {
            throw new RuntimeException("邮件发送失败: " . $e->getMessage());
        }
    }

    /**
     * 发送带附件的邮件
     */
    public function sendWithAttachments(
        string $to,
        string $subject,
        string $body,
        array $files = []
    ): bool {
        $this->mailer->clearAddresses();
        $this->mailer->clearAttachments();
        $this->mailer->addAddress($to);
        $this->mailer->Subject = $subject;
        $this->mailer->Body = $body;

        foreach ($files as $file) {
            if ($file instanceof \CURLFile) {
                $this->mailer->addAttachment($file->getFilename());
            } elseif (is_string($file)) {
                $this->mailer->addAttachment($file);
            } elseif (is_array($file)) {
                $this->mailer->addAttachment(
                    $file['path'],
                    $file['name'] ?? '',
                    $file['encoding'] ?? PHPMailer::ENCODING_BASE64
                );
            }
        }

        try {
            return $this->mailer->send();
        } catch (\Exception $e) {
            throw new RuntimeException("邮件发送失败: " . $e->getMessage());
        }
    }

    /**
     * 发送模板邮件
     */
    public function sendTemplate(
        string $to,
        string $templatePath,
        array $variables = []
    ): bool {
        $html = file_get_contents($templatePath);
        foreach ($variables as $key => $value) {
            $html = str_replace('{{' . $key . '}}', (string) $value, $html);
        }

        $subject = $variables['subject'] ?? '通知';
        return $this->sendHtml($to, $subject, $html);
    }

    /**
     * 获取错误信息
     */
    public function getError(): string
    {
        return $this->mailer->ErrorInfo;
    }
}

// 使用示例
$mailer = new MailSender(
    'smtp.example.com',
    587,
    'user@example.com',
    'password',
    '应用通知'
);

$mailer->sendHtml(
    'recipient@example.com',
    '欢迎注册',
    '<h1>欢迎</h1><p>感谢你的注册!</p>',
    '欢迎感谢你的注册!'
);

注意事项

mail() 常见问题

php
<?php
declare(strict_types=1);

// 1. 邮件可能进入垃圾箱
//    - 正确配置 SPF、DKIM、rDNS
//    - 使用 PHPMailer 代替 mail()

// 2. 换行符必须是 \r\n
$headers = "From: sender@example.com\r\n"; // 正确
// $headers = "From: sender@example.com\n"; // 可能导致问题

// 3. 主题行编码
$subject = '=?UTF-8?B?' . base64_encode('中文主题') . '?=';

// 4. 收件人格式
$to = 'User Name <user@example.com>'; // 正确
$to = '"User Name" <user@example.com>'; // 含特殊字符时

调试邮件发送

php
<?php
declare(strict_types=1);

// 启用 sendmail 详细日志(php.ini)
// sendmail_path = /usr/sbin/sendmail -t -i -v

// 使用 PHPMailer 调试
$mail = new PHPMailer(true);
$mail->SMTPDebug = PHPMailer::DEBUG_SERVER;
$mail->Debugoutput = function ($str, $level) {
    echo "DEBUG [{$level}]: {$str}" . PHP_EOL;
};

最佳实践

  1. 使用 PHPMailer:生产环境推荐使用 PHPMailer 代替 mail()
  2. 正确设置换行符:邮件头使用 \r\n
  3. HTML + 纯文本双版本:确保纯文本客户端也能阅读
  4. SPF/DKIM 配置:正确配置域名的邮件认证记录
  5. 错误处理:检查发送结果,记录失败日志
  6. 异步发送:大量邮件使用队列系统异步发送

下一节

继续学习:Zip 扩展

参考链接