发送邮件使用的是 SMTP 协议 (简单邮件传输协议), 用于邮件服务器和邮件发送方之间。
邮件的发送过程大致如下:
- 在邮件发送方和邮件服务器间建立 TCP 连接, 服务器响应 220 表示连接成功;
- 发送方通过HELO命令标识自己的身份. 服务器响应 250 表示准备接收邮件;
- 发送方通过AUTH LOGIN命令进行登录, 以 163 邮件服务器为例, 登录账号分别是 base64 编码过的邮箱账号和 163 的客户端授权码. 服务器响应 334 表示账号验证通过, 响应 235 表示授权码验证通过;
- 发送方通过MAIL FROM命令指定邮件的发送者. 服务器响应 250 表示成功;
- 发送方通过RCPT TO命令指定邮件接收地址, 服务器响应 250 表示成功;
- 发送方通过DATA命令发送邮件, 邮件内容包括邮件头和邮件正文部分. 服务器响应 250 表示成功;
- 发送方通过QUIT命令断开连接.
Windows 下可以通过 telnet 发送邮件。
邮件头的基本格式为:
Date: Feb 7 20:30:39 2007 // 发送日期 From: "发送者" <发送者邮箱> To: "接受者" <接收者邮箱> Subject: 邮件标题 Content-Type: text/plain; // 邮件正文类型
邮件头主要配置项:
邮件内容的具体格式和结构, 可以参考: https://help.aliyun.com/knowled。
示例一: 发送简单邮件
sendEmail.php
header('Content-type: text/html; charset=utf-8'); $server = 'smtp.163.com'; $errno = null; $error = null; // 设置邮件头 $email = 'Date: ' . date('j F G:i:s Y', time()) . "\r\n"; // 发送日期 $email .= 'From: "哪里多" <******@163.com>' . "\r\n"; // 发送者 $email .= "To: \"二柱子\" <******@qq.com>\r\n"; // 接收者 $email .= "Subject: 测试邮件\r\n"; // 邮件标题 $email .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; // 邮件内容类型,邮件头和正文间空一行分割 $email .= "邮件正文\r\n\r\n\r\n"; // 正文内容结束后空两行 $email .= ".\r\n"; // 邮件内容结束后以 . 命令标识结束 try { // fsockopen()函数: 建立一个网络连接或Unix套接字, 返回一个文件句柄, 可以使用文件操作函数操作返回的资源 $sockHandle = fsockopen($server, 25, $errno, $error, 60); if($sockHandle === false) { exit('无法建立连接'); } $response = fgets($sockHandle); if(strpos($response, '220') !== 0) { exit('连接邮件服务器失败'); } fwrite($sockHandle, "HELO test-user\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('helo命令执行失败'); } fwrite($sockHandle, "AUTH LOGIN\r\n"); $response = fgets($sockHandle); if(strpos($response, '334') !== 0) { exit('AUTH LOGIN命令执行失败'); } fwrite($sockHandle, base64_encode("******@163.com") . "\r\n"); $response = fgets($sockHandle); if(strpos($response, '334') !== 0) { exit('账号验证失败'); } // 163邮箱的客户端授权码 fwrite($sockHandle, base64_encode("******") . "\r\n"); $response = fgets($sockHandle); if(strpos($response, '235') !== 0) { exit('密码验证失败'); } fwrite($sockHandle, "MAIL FROM: <******@163.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('mail from命令执行失败'); } fwrite($sockHandle, "RCPT TO: <******@qq.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('rcpt to命令执行失败'); } fwrite($sockHandle, "DATA\r\n"); $response = fgets($sockHandle); if(strpos($response, '354') !== 0) { exit('data命令执行失败'); } fwrite($sockHandle, $email); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('发送邮件失败'); } fwrite($sockHandle, "QUIT\r\n"); echo '发送邮件成功' . PHP_EOL; fclose($sockHandle); }catch(Exception $e) { var_dump($e->getMessage()); var_dump($e->getTrace()); var_dump($e->getLine()); fclose($sockHandle); }
通过命令行执行脚本文件:
示例二: 发送携带单个附件的邮件
邮件携带附件时, 邮件头的格式类似 HTTP 请求中的上传文件时请求头的格式, 都需要在头部附加说明附件的内容和其他信息.
sendEmailWithAttachment.php
header('Content-type: text/html; charset=utf-8'); $server = 'smtp.163.com'; $errno = null; $error = null; // 设置邮件头 $email = 'Date: ' . date('j F G:i:s Y', time()) . "\r\n"; $email .= 'From: "哪里多" <***@163.com>' . "\r\n"; $email .= "To: \"二柱子\" <***@qq.com>\r\n"; $email .= "Subject: 测试邮件\r\n"; $email .= "Content-Type: multipart/mixed; boundary=----delimiter1----\r\n\r\n"; // 与post方式上传附件类似,需设定段体边界。邮件头和正文间空一行分割 $email .= "------delimiter1----\r\n"; // 段体开始边界格式:--{$boundary} $email .= "Content-Type: multipart/alternative; boundary=----delimiter2----\r\n\r\n"; // 邮件正文段体边界 $email .= "------delimiter2----\r\n"; // 邮件正文部分段体 $email .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; $email .= "邮件正文\r\n"; // 正文内容结束后空两行 $email .= "------delimiter2------\r\n\r\n"; // 正文部分结束边界,段体结束边界格式:--{$boundary}-- $email .= "------delimiter1----\r\n"; // 邮件附件段体设置,发送多个附件时,重复设置该部分段体 $email .= "Content-Type: application/octet-stream; name=\"1542533630(1).jpg\"\r\n"; $email .= "Content-Transfer-Encoding: base64\r\n"; $email .= "Content-Disposition: attachment; filename=\"test.jpg\"\r\n\r\n"; // 附件下载名称 $file = base64_encode(file_get_contents('./1542533630(1).jpg')); $email .= $file . "\r\n\r\n"; $email .= "------delimiter1------\r\n"; // 邮件实体设置结束边界 $email .= ".\r\n"; // 邮件内容结束后以 . 命令表示邮件内容设置完成 try { // fsockopen()函数: 建立一个网络连接或Unix套接字, 返回一个文件句柄, 可以使用文件操作函数操作返回的资源 $sockHandle = fsockopen($server, 25, $errno, $error, 60); if($sockHandle === false) { exit('无法建立连接'); } $response = fgets($sockHandle); if(strpos($response, '220') !== 0) { exit('连接邮件服务器失败'); } fwrite($sockHandle, "HELO test-user\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('helo命令执行失败'); } fwrite($sockHandle, "AUTH LOGIN\r\n"); $response = fgets($sockHandle); if(strpos($response, '334') !== 0) { exit('AUTH LOGIN命令执行失败'); } fwrite($sockHandle, base64_encode("***@163.com") . "\r\n"); $response = fgets($sockHandle); if(strpos($response, '334') !== 0) { exit('账号验证失败'); } // 163邮箱的客户端授权码 fwrite($sockHandle, base64_encode("***") . "\r\n"); $response = fgets($sockHandle); if(strpos($response, '235') !== 0) { exit('密码验证失败'); } fwrite($sockHandle, "MAIL FROM: <***@163.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('mail from命令执行失败'); } fwrite($sockHandle, "RCPT TO: <***@qq.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('rcpt to命令执行失败'); } fwrite($sockHandle, "DATA\r\n"); $response = fgets($sockHandle); if(strpos($response, '354') !== 0) { exit('data命令执行失败'); } fwrite($sockHandle, $email); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('发送邮件失败'); } fwrite($sockHandle, "QUIT\r\n"); echo '发送邮件成功' . PHP_EOL; fclose($sockHandle); }catch(Exception $e) { var_dump($e->getMessage()); var_dump($e->getTrace()); var_dump($e->getLine()); fclose($sockHandle); }
通过命令行运行脚本:
邮箱成功接收到邮件:
示例三: 发送携带多个附件的邮件
sendEmailWithMultiAttachment.php
header('Content-type: text/html; charset=utf-8'); $server = 'smtp.163.com'; $errno = null; $error = null; // 设置邮件头 $email = 'Date: ' . date('j F G:i:s Y', time()) . "\r\n"; $email .= 'From: "哪里多" <***@163.com>' . "\r\n"; $email .= "To: \"二柱子\" <***@qq.com>\r\n"; $email .= "Subject: 急报\r\n"; $email .= "Content-Type: multipart/mixed; boundary=----delimiter1----\r\n\r\n"; // 邮件头和正文间空一行分割 $email .= "------delimiter1----\r\n"; $email .= "Content-Type: multipart/alternative; boundary=----delimiter2----\r\n\r\n"; $email .= "------delimiter2----\r\n"; $email .= "Content-Type: text/plain; charset=UTF-8\r\n\r\n"; $email .= "见信速回\r\n"; // 正文内容结束后空两行 $email .= "------delimiter2------\r\n\r\n"; $email .= "------delimiter1----\r\n"; // 附件1段体 $email .= "Content-Type: application/octet-stream; name=\"baobiao1.jpg\"\r\n"; $email .= "Content-Transfer-Encoding: base64\r\n"; $email .= "Content-Disposition: attachment; filename=\"baobiao1.jpg\"\r\n\r\n"; // 附件下载名称 $file = base64_encode(file_get_contents('./1542533630(1).jpg')); $email .= $file . "\r\n\r\n"; $email .= "------delimiter1----\r\n"; // 附件2段体 $email .= "Content-Type: application/octet-stream; name=\"baobiao2.jpg\"\r\n"; $email .= "Content-Transfer-Encoding: base64\r\n"; $email .= "Content-Disposition: attachment; filename=\"baobiao2.jpg\"\r\n\r\n"; // 附件下载名称 //$file = base64_encode(file_get_contents('./1542533630(1).jpg')); $email .= $file . "\r\n\r\n"; $email .= "------delimiter1------\r\n"; $email .= ".\r\n"; // 邮件内容结束后以 . 命令标识结束 try { // fsockopen()函数: 建立一个网络连接或Unix套接字, 返回一个文件句柄, 可以使用文件操作函数操作返回的资源 $sockHandle = fsockopen($server, 25, $errno, $error, 60); if($sockHandle === false) { exit('无法建立连接'); } $response = fgets($sockHandle); if(strpos($response, '220') !== 0) { exit('连接邮件服务器失败'); } fwrite($sockHandle, "HELO test-user\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('helo命令执行失败'); } fwrite($sockHandle, "AUTH LOGIN\r\n"); $response = fgets($sockHandle); if(strpos($response, '334') !== 0) { exit('AUTH LOGIN命令执行失败'); } fwrite($sockHandle, base64_encode("***@163.com") . "\r\n"); $response = fgets($sockHandle); if(strpos($response, '334') !== 0) { exit('账号验证失败'); } // 163邮箱的客户端授权码 fwrite($sockHandle, base64_encode("***") . "\r\n"); $response = fgets($sockHandle); if(strpos($response, '235') !== 0) { exit('密码验证失败'); } fwrite($sockHandle, "MAIL FROM: <***@163.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('mail from命令执行失败'); } fwrite($sockHandle, "RCPT TO: <***@qq.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('rcpt to命令执行失败'); } fwrite($sockHandle, "DATA\r\n"); $response = fgets($sockHandle); if(strpos($response, '354') !== 0) { exit('data命令执行失败'); } fwrite($sockHandle, $email); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { echo $response; exit('发送邮件失败'); } fwrite($sockHandle, "QUIT\r\n"); echo '发送邮件成功' . PHP_EOL; fclose($sockHandle); }catch(Exception $e) { var_dump($e->getMessage()); var_dump($e->getTrace()); var_dump($e->getLine()); fclose($sockHandle); }
通过命令行运行脚本:
邮箱接收到邮件:
如果需要设置抄送项, 在邮件头中配置抄送项Cc即可, 如:
Cc: <抄送人1@qq.com>, <抄送人2@163.com>
然后通过执行命令RCPT TO设置抄送人:
fwrite($sockHandle, "RCPT TO: <抄送人2@163.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { echo $response; exit('抄送命令执行失败'); }
退信的处理
邮件内容不规范, 或相同内容重复发送时, 可能导致退信, 发送失败.
- 如果是重复内容反复发送导致的退信, 更换发送人账号即可.
- 也可以通过将发件人添加到收件人解决退信问题. 此时邮件头中To的配置项为:
To: 收件人1 <***@qq.com>, 发件人 <***@163.com>\r\n
在通过命令设置发件人时, 通过反复执行RCPT TO命令, 设置多个收件人.
fwrite($sockHandle, "RCPT TO:<收件人1@qq.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('收件人1命令执行失败'); } fwrite($sockHandle, "RCPT TO: <发件人@163.com>\r\n"); $response = fgets($sockHandle); if(strpos($response, '250') !== 0) { exit('收件人2命令执行失败'); }
来源:oschina
链接:https://my.oschina.net/u/4479011/blog/4255202