首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP IMAP 擴展實作郵件客戶端核心功能:連線、列表、讀取與 MIME 解析

     編輯:PHP 相關知識

PHP IMAP 擴展提供完整的 IMAP4/POP3/NNTP 協定支援,可構建 Webmail、郵件通知系統、自動回覆機器人等應用。本文涵蓋連線認證、信箱列表、郵件列表分頁、完整郵件讀取、附件處理、MIME 結構解析、編碼轉換等核心功能,並提供生產級封裝類。

環境需求與安裝

# Ubuntu/Debian
apt-get install php-imap
# CentOS/RHEL
yum install php-imap
# Alpine
apk add php8-imap

# 重啟 PHP-FPM/Apache
systemctl restart php8.2-fpm

驗證:php -m | grep imap 或 phpinfo() 中出現 imap 區塊。

核心連線與認證

<?php
class ImapClient
{
    private $conn;
    private string $mailbox;
    
    public function __construct(
        string $host,
        int $port = 993,
        string $username = '',
        string $password = '',
        bool $ssl = true,
        bool $validateCert = true
    ) {
        $flags = '/imap' . ($ssl ? '/ssl' : '') . ($validateCert ? '' : '/novalidate-cert');
        $this->mailbox = "{$flags}{$host}:{$port}";
        
        $this->conn = @imap_open($this->mailbox, $username, $password);
        if (!$this->conn) {
            throw new RuntimeException("IMAP 連線失敗: " . imap_last_error());
        }
    }
    
    public function __destruct()
    {
        if ($this->conn) {
            @imap_close($this->conn, CL_EXPUNGE);
        }
    }
    
    public function getConnection(): resource
    {
        return $this->conn;
    }
}

// 使用範例
// $client = new ImapClient('mail.example.com', 993, 'user@example.com', 'password');
// $conn = $client->getConnection();
?>

信箱(資料夾)管理

<?php
// 列出所有信箱
$mailboxes = imap_list($conn, $mailbox, '*');
foreach ($mailboxes as $mb) {
    // $mb 格式:{imap.example.com:993/imap/ssl}INBOX
    echo imap_utf7_decode($mb) . "\n";
}

// 建立信箱
imap_createmailbox($conn, "{imap.example.com:993/imap/ssl}NewFolder");

// 刪除信箱
imap_deletemailbox($conn, "{imap.example.com:993/imap/ssl}OldFolder");

// 重命名
imap_renamemailbox($conn, "{...}OldName", "{...}NewName");

// 訂閱/取消訂閱
imap_subscribe($conn, "{...}Folder");
imap_unsubscribe($conn, "{...}Folder");
?>

郵件列表與分頁

<?php
function getMessageList(resource $conn, int $page = 1, int $perPage = 20, array $criteria = []): array
{
    // 搜尋條件:ALL, UNSEEN, SEEN, FLAGGED, DELETED, FROM "xxx", SUBJECT "xxx", SINCE "date", BEFORE "date"
    $search = $criteria ? implode(' ', $criteria) : 'ALL';
    $messageNumbers = imap_search($conn, $search, SE_UID);
    
    if (!$messageNumbers) {
        return ['messages' => [], 'total' => 0];
    }
    
    // 最新優先(UID 遞減)
    rsort($messageNumbers);
    $total = count($messageNumbers);
    
    // 分頁
    $offset = ($page - 1) * $perPage;
    $pageUids = array_slice($messageNumbers, $offset, $perPage);
    
    $messages = [];
    foreach ($pageUids as $uid) {
        // 只抓標頭,效能佳
        $header = imap_headerinfo($conn, $uid, 0, 0, FT_UID);
        if (!$header) continue;
        
        $messages[] = [
            'uid' => $uid,
            'subject' => imap_utf8($header->subject ?? ''),
            'from' => parseAddress($header->from ?? []),
            'to' => parseAddress($header->to ?? []),
            'date' => $header->udate ? date('Y-m-d H:i:s', $header->udate) : '',
            'size' => $header->Size ?? 0,
            'flags' => getFlags($header),
            'unseen' => !isset($header->Unseen),
        ];
    }
    
    return ['messages' => $messages, 'total' => $total];
}

function parseAddress(array $addresses): array
{
    $result = [];
    foreach ($addresses as $addr) {
        $result[] = [
            'name' => imap_utf8($addr->personal ?? ''),
            'email' => strtolower("{$addr->mailbox}@{$addr->host}"),
        ];
    }
    return $result;
}

function getFlags(stdClass $header): array
{
    $flags = [];
    if (isset($header->Flagged)) $flags[] = 'flagged';
    if (isset($header->Answered)) $flags[] = 'answered';
    if (isset($header->Deleted)) $flags[] = 'deleted';
    if (isset($header->Draft)) $flags[] = 'draft';
    return $flags;
}
?>

完整郵件讀取與 MIME 解析

<?php
function getFullMessage(resource $conn, int $uid): array
{
    // 取得完整 RFC822 訊息
    $raw = imap_fetchbody($conn, $uid, '', FT_UID | FT_PEEK);
    if (!$raw) return [];
    
    // 解析 MIME 結構
    $structure = imap_fetchstructure($conn, $uid, FT_UID);
    $parsed = parseMimeStructure($structure, $conn, $uid);
    
    return [
        'uid' => $uid,
        'headers' => getFullHeaders($conn, $uid),
        'text' => $parsed['text'] ?? '',
        'html' => $parsed['html'] ?? '',
        'attachments' => $parsed['attachments'] ?? [],
        'inline_images' => $parsed['inline'] ?? [],
    ];
}

function parseMimeStructure(stdClass $structure, resource $conn, int $uid, string $partNum = ''): array
{
    $result = ['text' => '', 'html' => '', 'attachments' => [], 'inline' => []];
    
    if (isset($structure->parts)) {
        // Multipart 郵件:遞迴處理各部分
        foreach ($structure->parts as $i => $part) {
            $subPartNum = $partNum ? "{$partNum}.{$i+1}" : (string)($i+1);
            $sub = parseMimeStructure($part, $conn, $uid, $subPartNum);
            $result['text'] .= $sub['text'];
            $result['html'] .= $sub['html'];
            $result['attachments'] = array_merge($result['attachments'], $sub['attachments']);
            $result['inline'] = array_merge($result['inline'], $sub['inline']);
        }
    } else {
        // 單一部分
        $encoding = $structure->encoding ?? 0;
        $subtype = strtolower($structure->subtype ?? '');
        $type = $structure->type ?? 0;
        $params = $structure->parameters ?? [];
        $dparams = $structure->dparameters ?? [];
        
        $filename = '';
        foreach (array_merge($params, $dparams) as $param) {
            if (($param->attribute ?? '') === 'filename') {
                $filename = imap_utf8($param->value);
                break;
            }
        }
        
        $disposition = strtolower($structure->disposition ?? '');
        $isAttachment = $disposition === 'attachment' || ($filename && $type !== TYPETEXT);
        $isInline = $disposition === 'inline' && $filename;
        
        // 取得內容
        $body = imap_fetchbody($conn, $uid, $partNum, FT_UID | FT_PEEK);
        $body = decodeMimeBody($body, $encoding);
        
        // 字元集轉換
        $charset = 'UTF-8';
        foreach ($params as $param) {
            if (($param->attribute ?? '') === 'charset') {
                $charset = strtoupper($param->value);
                break;
            }
        }
        if ($charset !== 'UTF-8') {
            $body = @iconv($charset, 'UTF-8//IGNORE', $body) ?: $body;
        }
        
        if ($type === TYPETEXT) {
            if ($subtype === 'plain') {
                $result['text'] = $body;
            } elseif ($subtype === 'html') {
                $result['html'] = $body;
            }
        } elseif ($isAttachment) {
            $result['attachments'][] = [
                'part' => $partNum,
                'filename' => $filename,
                'type' => mimeType($type, $subtype),
                'size' => $structure->bytes ?? strlen($body),
                'content' => $body, // 大附件建議改用 imap_fetchbody 串流下載
            ];
        } elseif ($isInline) {
            $result['inline'][] = [
                'part' => $partNum,
                'filename' => $filename,
                'cid' => $structure->id ?? '',
                'content' => $body,
            ];
        }
    }
    return $result;
}

function decodeMimeBody(string $body, int $encoding): string
{
    switch ($encoding) {
        case 1: return quoted_printable_decode($body); // 7bit
        case 2: return $body; // 8bit
        case 3: return base64_decode($body); // base64
        case 4: return quoted_printable_decode($body); // quoted-printable
        case 5: return $body; // binary
        default: return $body;
    }
}

function mimeType(int $type, string $subtype): string
{
    $types = [
        TYPETEXT => 'text',
        TYPEMULTIPART => 'multipart',
        TYPEMESSAGE => 'message',
        TYPEAPPLICATION => 'application',
        TYPEAUDIO => 'audio',
        TYPEIMAGE => 'image',
        TYPEVIDEO => 'video',
        TYPEMODEL => 'model',
        TYPEOTHER => 'application',
    ];
    return ($types[$type] ?? 'application') . '/' . $subtype;
}

function getFullHeaders(resource $conn, int $uid): array
{
    $raw = imap_fetchheader($conn, $uid, FT_UID | FT_PEEK);
    $headers = [];
    foreach (explode("\n", $raw) as $line) {
        if (preg_match('/^([^:]+):\s*(.*)$/', $line, $m)) {
            $key = strtolower($m[1]);
            $headers[$key][] = $m[2];
        }
    }
    return $headers;
}
?>

郵件操作:標記、移動、刪除

<?php
// 標記已讀/未讀
imap_setflag_full($conn, $uid, '\\Seen', ST_UID);      // 標記已讀
imap_clearflag_full($conn, $uid, '\\Seen', ST_UID);    // 標記未讀

// 標記星號/旗標
imap_setflag_full($conn, $uid, '\\Flagged', ST_UID);
imap_clearflag_full($conn, $uid, '\\Flagged', ST_UID);

// 移動到其他信箱(複製 + 標記刪除 + 清除)
function moveMessage(resource $conn, int $uid, string $destMailbox): bool
{
    if (!imap_mail_move($conn, $uid, $destMailbox, ST_UID)) {
        return false;
    }
    // 標記原郵件為刪除
    imap_delete($conn, $uid, FT_UID);
    // 永久清除(可選,視 IMAP 伺服器行為)
    // imap_expunge($conn);
    return true;
}

// 刪除(標記 + 清除)
function deleteMessage(resource $conn, int $uid): bool
{
    imap_delete($conn, $uid, FT_UID);
    return imap_expunge($conn); // 真正刪除
}
?>

發送郵件(SMTP,非 IMAP)

IMAP 僅用於接收/管理,發送需用 SMTP。推薦使用 PHPMailer 或 Symfony Mailer:

<?php
// composer require phpmailer/phpmailer
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;

$mail = new PHPMailer(true);
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = 'user@example.com';
$mail->Password = 'password';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;

$mail->setFrom('user@example.com', 'User Name');
$mail->addAddress('recipient@example.com');
$mail->Subject = 'Test';
$mail->Body = 'Plain text body';
$mail->AltBody = 'HTML body';

$mail->send();
?>

常見問題與除錯

問題原因解決
imap_open(): Couldn't open stream主機/埠/SSL 模式錯誤、憑證驗證失敗、防火牆檢查 imap_last_error(),嘗試 /novalidate-cert,telnet 測試埠
中文亂碼字元集未正確轉換使用 imap_utf8()、iconv(),檢查 MIME charset 參數
大附件記憶體耗盡imap_fetchbody() 一次載入全部分塊讀取:imap_fetchbody($conn, $uid, $part, FT_UID | FT_PEEK, 0, 8192)
搜尋中文關鍵字失效IMAP SEARCH 不支援 UTF-8 關鍵字使用 imap_search($conn, 'CHARSET UTF-8 SUBJECT "關鍵字"') (需伺服器支援)
連線逾時長連線被伺服器斷開定期 imap_ping() 保持連線,或重連機制

效能優化建議

  1. 僅抓標頭列表:imap_headerinfo() 或 imap_fetchheader(),避免 imap_body()。
  2. 使用 UID 而非訊息序號:FT_UID 標誌,避免序號在會話中變動。
  3. 分頁載入:imap_search() 取得 UID 陣列後自行分頁。
  4. 連線複用:長執行腳本保持連線,避免頻繁 imap_open/close。
  5. 大附件串流下載:提供下載連結由前端直接請求 PHP 代理串流,而非載入記憶體。

總結

PHP IMAP 擴展功能強大但 API 較底層,建構 Webmail 需妥善處理:MIME 多層結構遞迴解析、字元集轉換、附件串流、UID 分頁、連線保持、錯誤重試。生產環境建議封裝為服務類,並考慮引入 webklex/php-imap 或 barbushin/php-imap 等現代封裝庫簡化開發。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved