Skip to content

IMAP / POP3 / SMTP

PHP 提供了 IMAP 扩展用于处理电子邮件的收发。IMAP 扩展不仅支持 IMAP 协议,还同时支持 POP3 和 NNTP 协议,配合 SMTP 可以构建完整的邮件处理系统。本节将讲解邮件收发、邮箱管理和附件处理。

前置知识

阅读本节前,建议先了解:cURL 详解文件系统操作

基础概念

邮件协议

协议端口用途说明
IMAP143/993读取邮件服务器端管理,支持文件夹
POP3110/995读取邮件下载到本地,简单模式
SMTP25/465/587发送邮件发送协议

安装

bash
# 编译安装
./configure --with-imap --with-imap-ssl

# Ubuntu/Debian
sudo apt-get install php-imap

# 需要 libc-client-dev
sudo apt-get install libc-client-dev

IMAP 连接

基本连接

php
<?php
declare(strict_types=1);

/**
 * IMAP 邮箱连接类
 */
class MailboxConnection
{
    private $imapStream;

    /**
     * 连接 IMAP 服务器
     *
     * @param string $mailbox 邮箱地址,格式: {host:port/flags}folder
     */
    public function connect(
        string $host,
        string $username,
        string $password,
        int $port = 993,
        bool $ssl = true,
        string $folder = 'INBOX'
    ): void {
        $flags = $ssl ? '/ssl/novalidate-cert' : '/notls';

        $mailbox = "{{$host}:{$port}{$flags}}{$folder}";

        // 连接并打开邮箱
        $this->imapStream = imap_open($mailbox, $username, $password);

        if ($this->imapStream === false) {
            throw new RuntimeException(
                "IMAP 连接失败: " . imap_last_error()
            );
        }
    }

    public function getStream()
    {
        return $this->imapStream;
    }

    public function close(): void
    {
        if ($this->imapStream) {
            imap_close($this->imapStream, CL_EXPUNGE);
        }
    }

    public function __destruct()
    {
        $this->close();
    }
}

// 使用示例
$mailbox = new MailboxConnection();
$mailbox->connect('imap.example.com', 'user@example.com', 'password');

连接参数详解

php
<?php
declare(strict_types=1);

// 邮箱地址格式: {host:port/flags}folder

// IMAP over SSL
$mailbox = '{imap.example.com:993/ssl/novalidate-cert}INBOX';

// IMAP without SSL
$mailbox = '{imap.example.com:143/notls}INBOX';

// POP3 over SSL
$mailbox = '{pop.example.com:995/pop3/ssl}INBOX';

// POP3 without SSL
$mailbox = '{pop.example.com:110/pop3/notls}INBOX';

// 带详细 flag
$mailbox = '{imap.example.com:993/imap/ssl/novalidate-cert}INBOX';

// Flag 说明:
// /ssl      - 使用 SSL/TLS
// /tls      - 使用 STARTTLS(先明文后升级为 TLS)
// /notls    - 不使用 TLS
// /novalidate-cert - 不验证证书
// /validate-cert   - 验证证书

读取邮件

列出邮件

php
<?php
declare(strict_types=1);

/**
 * 邮件读取操作
 */
class MailReader
{
    public function __construct(
        private readonly $imapStream
    ) {}

    /**
     * 获取邮箱信息
     */
    public function getMailboxInfo(): array
    {
        $check = imap_check($this->imapStream);

        return [
            'date'      => $check->Date,
            'driver'    => $check->Driver,
            'mailbox'   => $check->Mailbox,
            'nmsgs'     => $check->Nmsgs,
            'recent'    => $check->Recent,
        ];
    }

    /**
     * 获取邮件概览列表
     */
    public function getOverview(int $page = 1, int $perPage = 20): array
    {
        $check = imap_check($this->imapStream);
        $totalMessages = $check->Nmsgs;

        $startMsg = $totalMessages - (($page - 1) * $perPage);
        $endMsg = $startMsg - $perPage + 1;

        if ($startMsg < 1) {
            $startMsg = 1;
        }

        $overview = imap_fetch_overview(
            $this->imapStream,
            "{$startMsg}:{$endMsg}"
        );

        $emails = [];
        foreach ($overview as $item) {
            $emails[] = [
                'uid'     => (int) $item->uid,
                'msgno'   => (int) $item->msgno,
                'subject' => $this->decodeMimeString($item->subject ?? ''),
                'from'    => $item->from ?? '',
                'to'      => $item->to ?? '',
                'date'    => $item->date ?? '',
                'size'    => (int) ($item->size ?? 0),
                'seen'    => (bool) ($item->seen ?? false),
                'recent'  => (bool) ($item->recent ?? false),
                'flagged' => (bool) ($item->flagged ?? false),
                'answered'=> (bool) ($item->answered ?? false),
            ];
        }

        return [
            'emails' => $emails,
            'total'  => $totalMessages,
            'page'   => $page,
            'per_page' => $perPage,
        ];
    }

    /**
     * 解码 MIME 编码的字符串
     */
    private function decodeMimeString(string $str): string
    {
        $decoded = imap_mime_header_decode($str);
        $result = '';
        foreach ($decoded as $part) {
            $result .= $part->text;
        }
        return $result;
    }
}

// 使用示例
// $reader = new MailReader($mailbox->getStream());
// $info = $reader->getMailboxInfo();
// echo "邮件总数: {$info['nmsgs']}" . PHP_EOL;
//
// $emails = $reader->getOverview(1, 10);
// foreach ($emails['emails'] as $email) {
//     echo "{$email['subject']} - {$email['from']}" . PHP_EOL;
// }

读取邮件详情

php
<?php
declare(strict_types=1);

class MailDetailReader
{
    public function __construct(
        private readonly $imapStream
    ) {}

    /**
     * 获取邮件完整信息
     */
    public function getFullEmail(int $msgNo): array
    {
        // 获取邮件头
        $header = imap_headerinfo($this->imapStream, $msgNo);

        // 获取邮件结构
        $structure = imap_fetchstructure($this->imapStream, $msgNo);

        // 获取纯文本正文
        $plainText = $this->getBodyPart($msgNo, $structure, 'plain');

        // 获取 HTML 正文
        $htmlBody = $this->getBodyPart($msgNo, $structure, 'html');

        // 获取附件列表
        $attachments = $this->getAttachments($msgNo, $structure);

        return [
            'subject'    => $this->decodeHeader($header->subject ?? ''),
            'from'       => $this->parseAddress($header->from),
            'to'         => $this->parseAddress($header->to),
            'cc'         => $this->parseAddress($header->cc),
            'date'       => $header->date ?? '',
            'date_unix'  => $header->udate ?? 0,
            'plain_text' => $plainText,
            'html_body'  => $htmlBody,
            'attachments' => $attachments,
            'reply_to'   => $this->parseAddress($header->reply_to),
            'message_id' => $header->message_id ?? '',
            'size'       => $header->Size ?? 0,
        ];
    }

    private function getBodyPart(
        int $msgNo,
        object $structure,
        string $mimeType
    ): string {
        if (!isset($structure->parts)) {
            $encoding = $structure->encoding ?? 0;
            $body = imap_fetchbody($this->imapStream, $msgNo, '1');

            if ($encoding === 3) { // BASE64
                $body = base64_decode($body);
            } elseif ($encoding === 4) { // QP
                $body = quoted_printable_decode($body);
            }

            return $body;
        }

        foreach ($structure->parts as $partNo => $part) {
            $subType = strtolower($part->subtype ?? '');
            if ($subType === $mimeType) {
                if ($part->ifdisposition && strtolower($part->disposition ?? '') === 'attachment') {
                    continue; // 跳过附件
                }
                $body = imap_fetchbody(
                    $this->imapStream,
                    $msgNo,
                    (string) ($partNo + 1)
                );
                $body = $this->decodeBody($body, $part->encoding ?? 0);
                return $body;
            }

            if (isset($part->parts)) {
                foreach ($part->parts as $subPartNo => $subPart) {
                    if (strtolower($subPart->subtype ?? '') === $mimeType) {
                        $body = imap_fetchbody(
                            $this->imapStream,
                            $msgNo,
                            (string) ($partNo + 1) . '.' . ($subPartNo + 1)
                        );
                        $body = $this->decodeBody($body, $subPart->encoding ?? 0);
                        return $body;
                    }
                }
            }
        }

        return '';
    }

    private function decodeBody(string $body, int $encoding): string
    {
        return match ($encoding) {
            3 => base64_decode($body),
            4 => quoted_printable_decode($body),
            default => $body,
        };
    }

    private function getAttachments(int $msgNo, object $structure): array
    {
        $attachments = [];
        $this->parseAttachments($msgNo, $structure, '', $attachments);
        return $attachments;
    }

    private function parseAttachments(
        int $msgNo,
        object $structure,
        string $partId,
        array &$attachments
    ): void {
        if (isset($structure->parts)) {
            foreach ($structure->parts as $index => $part) {
                $newPartId = $partId === '' ? (string) ($index + 1) : $partId . '.' . ($index + 1);
                $this->parseAttachments($msgNo, $part, $newPartId, $attachments);
            }
            return;
        }

        if (
            isset($structure->ifdisposition) &&
            strtolower($structure->disposition ?? '') === 'attachment'
        ) {
            $filename = $this->getAttachmentName($structure);

            $attachments[] = [
                'filename' => $filename,
                'size'      => $structure->bytes ?? 0,
                'type'      => $structure->subtype ?? '',
                'part_id'   => $partId,
                'encoding'  => $structure->encoding ?? 0,
            ];
        }
    }

    private function getAttachmentName(object $part): string
    {
        if (!empty($part->filename)) {
            return $this->decodeHeader($part->filename);
        }

        if (!empty($part->dparameters)) {
            foreach ($part->dparameters as $param) {
                if (strtolower($param->attribute) === 'filename') {
                    return $this->decodeHeader($param->value);
                }
            }
        }

        return 'unnamed_attachment';
    }

    private function decodeHeader(string $str): string
    {
        $decoded = imap_mime_header_decode($str);
        $result = '';
        foreach ($decoded as $part) {
            $result .= $part->text;
        }
        return $result;
    }

    private function parseAddress(array|string|null $addresses): array
    {
        if (empty($addresses)) {
            return [];
        }

        $parsed = [];
        foreach ((array) $addresses as $addr) {
            $parsed[] = $addr->mailbox . '@' . $addr->host;
        }
        return $parsed;
    }
}

下载附件

php
<?php
declare(strict_types=1);

/**
 * 下载邮件附件
 */
function downloadAttachment(
    $imapStream,
    int $msgNo,
    string $partId,
    string $filename,
    int $encoding,
    string $saveDir = '/tmp/attachments'
): string {
    $saveDir = rtrim($saveDir, '/');
    if (!is_dir($saveDir)) {
        mkdir($saveDir, 0755, true);
    }

    $data = imap_fetchbody($imapStream, $msgNo, $partId);

    // 解码
    if ($encoding === 3) {
        $data = base64_decode($data);
    } elseif ($encoding === 4) {
        $data = quoted_printable_decode($data);
    }

    $savePath = $saveDir . '/' . $filename;
    file_put_contents($savePath, $data);

    return $savePath;
}

SMTP 发送邮件

使用 imap_mail 发送

php
<?php
declare(strict_types=1);

/**
 * 使用 imap_mail 发送邮件
 */
function sendEmail(
    string $to,
    string $subject,
    string $body,
    string $headers = '',
    string $cc = '',
    string $bcc = '',
    string $rpath = ''
): bool {
    $envelope = [
        'from' => 'noreply@example.com',
    ];

    // 简单发送
    $result = imap_mail(
        $to,           // 收件人
        $subject,      // 主题
        $body,         // 正文
        $headers,      // 额外头部
        $cc,           // 抄送
        $bcc,          // 密送
        $rpath         // 返回路径
    );

    return $result;
}

// HTML 邮件
$headers = "From: sender@example.com\r\n"
    . "MIME-Version: 1.0\r\n"
    . "Content-Type: text/html; charset=UTF-8\r\n";

sendEmail(
    'recipient@example.com',
    '测试邮件',
    '<h1>你好</h1><p>这是一封测试邮件</p>',
    $headers
);

邮箱操作

文件夹管理

php
<?php
declare(strict_types=1);

class FolderManager
{
    public function __construct(
        private readonly $imapStream
    ) {}

    /**
     * 列出所有文件夹
     */
    public function listFolders(): array
    {
        $folders = imap_listmailbox(
            $this->imapStream,
            '{imap.example.com:993/ssl}'
        );

        return $folders !== false ? $folders : [];
    }

    /**
     * 创建文件夹
     */
    public function createFolder(string $name): bool
    {
        $result = imap_createmailbox(
            $this->imapStream,
            imap_utf7_encode("{imap.example.com:993/ssl}" . $name)
        );

        if (!$result) {
            throw new RuntimeException("创建文件夹失败: " . imap_last_error());
        }

        return true;
    }

    /**
     * 删除文件夹
     */
    public function deleteFolder(string $name): bool
    {
        return imap_deletemailbox(
            $this->imapStream,
            "{imap.example.com:993/ssl}{$name}"
        );
    }

    /**
     * 重命名文件夹
     */
    public function renameFolder(string $oldName, string $newName): bool
    {
        return imap_renamemailbox(
            $this->imapStream,
            "{imap.example.com:993/ssl}{$oldName}",
            "{imap.example.com:993/ssl}{$newName}"
        );
    }
}

邮件标记操作

php
<?php
declare(strict_types=1);

class MailFlagsManager
{
    public function __construct(
        private readonly $imapStream
    ) {}

    /**
     * 标记为已读
     */
    public function markAsRead(int $msgNo): void
    {
        imap_setflag_full(
            $this->imapStream,
            (string) $msgNo,
            '\\Seen'
        );
    }

    /**
     * 标记为未读
     */
    public function markAsUnread(int $msgNo): void
    {
        imap_clearflag_full(
            $this->imapStream,
            (string) $msgNo,
            '\\Seen'
        );
    }

    /**
     * 标记为已删除
     */
    public function markAsDeleted(int $msgNo): void
    {
        imap_delete($this->imapStream, $msgNo);
    }

    /**
     * 移动邮件到另一个文件夹
     */
    public function moveToFolder(int $msgNo, string $folder): bool
    {
        return imap_mail_move(
            $this->imapStream,
            (string) $msgNo,
            $folder
        );
    }

    /**
     * 复制邮件
     */
    public function copyToFolder(int $msgNo, string $folder): bool
    {
        return imap_mail_copy(
            $this->imapStream,
            (string) $msgNo,
            $folder
        );
    }

    /**
     * 搜索邮件
     */
    public function search(string $criteria): array
    {
        $results = imap_search($this->imapStream, $criteria);
        return $results !== false ? $results : [];
    }

    /**
     * 搜索未读邮件
     */
    public function searchUnread(): array
    {
        return $this->search('UNSEEN');
    }

    /**
     * 搜索最近 N 天的邮件
     */
    public function searchRecent(int $days = 7): array
    {
        $date = date('d-M-Y', strtotime("-{$days} days"));
        return $this->search("SINCE {$date}");
    }
}

实战示例

完整邮件客户端

php
<?php
declare(strict_types=1);

/**
 * 邮件客户端封装
 */
class EmailClient
{
    private $imap;

    public function __construct(
        private readonly string $host,
        private readonly string $username,
        private readonly string $password,
        private readonly int $port = 993,
        private readonly bool $ssl = true
    ) {}

    public function connect(): void
    {
        $flags = $this->ssl ? '/ssl/novalidate-cert' : '/notls';
        $mailbox = "{{$this->host}:{$this->port}{$flags}}INBOX";

        $this->imap = imap_open($mailbox, $this->username, $this->password);
        if ($this->imap === false) {
            throw new RuntimeException("连接失败: " . imap_last_error());
        }
    }

    public function getUnreadCount(): int
    {
        $result = imap_search($this->imap, 'UNSEEN');
        return $result !== false ? count($result) : 0;
    }

    public function getRecentEmails(int $limit = 10): array
    {
        $emails = [];
        $check = imap_check($this->imap);
        $total = $check->Nmsgs;
        $start = max(1, $total - $limit + 1);

        $overview = imap_fetch_overview($this->imap, "{$start}:{$total}");
        if ($overview === false) {
            return $emails;
        }

        foreach (array_reverse($overview) as $msg) {
            $emails[] = [
                'uid'     => (int) $msg->uid,
                'subject' => $msg->subject,
                'from'    => $msg->from,
                'date'    => $msg->date,
                'seen'    => (bool) $msg->seen,
            ];
        }

        return $emails;
    }

    public function close(): void
    {
        if ($this->imap) {
            imap_close($this->imap, CL_EXPUNGE);
        }
    }

    public function __destruct()
    {
        $this->close();
    }
}

注意事项

安全提示

安全警告

  • 在开发阶段可以使用 /novalidate-cert,生产环境应使用合法证书
  • 不要在代码中硬编码密码,使用环境变量
  • 处理用户上传的邮件时注意 XSS 防护
  • 使用 SSL/TLS 加密连接(端口 993/995)

性能优化

php
<?php
declare(strict_types=1);

// 1. 使用 IMAP UID 代替消息序号(UID 在会话间不变)
$uids = imap_search($this->imap, 'ALL', SE_UID);

// 2. 限制获取范围,避免拉取全部邮件
$overview = imap_fetch_overview($this->imap, '1:100');

// 3. 使用 IMAP SORT 扩展(如安装)
$sorted = imap_sort($this->imap, SORTDATE, 1);

// 4. 及时关闭连接
imap_close($this->imap, CL_EXPUNGE);

最佳实践

  1. 使用 UID:UID 比 msgno 更可靠,不会因删除操作而改变
  2. 设置超时:使用 imap_timeout 设置合理的超时时间
  3. 异常处理:检查所有 IMAP 函数的返回值
  4. 编码处理:使用 imap_mime_header_decode 解码邮件头
  5. 连接池:频繁操作时复用连接

下一节

继续学习:Mail 扩展

参考链接