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;
}
?>
<?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); // 真正刪除
}
?>
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() 保持連線,或重連機制 |
imap_headerinfo() 或 imap_fetchheader(),避免 imap_body()。FT_UID 標誌,避免序號在會話中變動。imap_search() 取得 UID 陣列後自行分頁。imap_open/close。PHP IMAP 擴展功能強大但 API 較底層,建構 Webmail 需妥善處理:MIME 多層結構遞迴解析、字元集轉換、附件串流、UID 分頁、連線保持、錯誤重試。生產環境建議封裝為服務類,並考慮引入 webklex/php-imap 或 barbushin/php-imap 等現代封裝庫簡化開發。