IMAP / POP3 / SMTP
PHP 提供了 IMAP 扩展用于处理电子邮件的收发。IMAP 扩展不仅支持 IMAP 协议,还同时支持 POP3 和 NNTP 协议,配合 SMTP 可以构建完整的邮件处理系统。本节将讲解邮件收发、邮箱管理和附件处理。
基础概念
邮件协议
| 协议 | 端口 | 用途 | 说明 |
|---|---|---|---|
| IMAP | 143/993 | 读取邮件 | 服务器端管理,支持文件夹 |
| POP3 | 110/995 | 读取邮件 | 下载到本地,简单模式 |
| SMTP | 25/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-devIMAP 连接
基本连接
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);最佳实践
- 使用 UID:UID 比 msgno 更可靠,不会因删除操作而改变
- 设置超时:使用
imap_timeout设置合理的超时时间 - 异常处理:检查所有 IMAP 函数的返回值
- 编码处理:使用
imap_mime_header_decode解码邮件头 - 连接池:频繁操作时复用连接
下一节
继续学习:Mail 扩展