简单邮件传输协议SMTP

时间:2022-01-02 15:03:00
很多应用都需要发送邮件的功能。在PHP中自带一个mail()函数,但如果想通过mail()函数发送邮件,必须先安装SMTP服务器。如果不想安装SMTP邮件服务器,却想发送邮件,这时,Socket就派上用场了。可以使用Socket连接一个已有的邮件服务器,如163提供的SMTP服务器,然后用它发送邮件。

1. SMTP协议概述
SMTP(Simple Mail Transfer Protocol,简单邮件传输协议)是由源地址到目的地址传送邮件的一组规则,用来控制信件的中转方式。SMTP协议属于TCP/IP协议族,它使每台计算机在发送或中转信件时能找到下一个目的地。通过使用指定的服务器,把Email寄到收信人的服务器上。

SMTP服务器是遵循SMTP协议的邮件服务器,用来发送或中转电子邮件。首先,客户端需要建立一个与SMTP服务器的TCP连接,端口一般为25。在连接建立之后,客户端和服务器先执行一些应用层的握手操作,让SMTP服务器知道客户端的信息,并且对客户端需求做出响应等。

在SMTP握手阶段,客户端向SMTP服务器分别指定发件人和收件人的电子邮件地址。握手阶段完毕,SMTP服务器把客户端发出的邮件消息添加到发信队列中,通过TCP提供的可靠数据传输服务把该消息准确地传送到收件人的服务器。

连接和发送过程如下:
(1)建立TCP连接。
(2)客户端发送HELO命令以标识发件人自己的身份,客户端发送MAIL命令。服务器以OK作为响应,表明准备接收。
(3)使用AUTH命令登录SMTP服务器,输入用户名和密码(注意,用户名和密码都需要base64加密)。
(4)客户端发送RCPT命令,标识该电子邮件的计划接收人,可以有多个RCPT行。服务器以OK作为响应,表示愿意为收件人发送邮件。
(5)协商结束后,使用DATA命令发送。
(6)以 . 号表示结束,输入内容一起发送出去,结束此次发送,用QUIT命令退出。

2. SMTP协议应用:使用Socket发送邮件
SMTP协议建立在TCP协议之上,所以原则上按照SMTP协议的规范,使用Socket跟SMTP服务器进行交互。下面使用PHP的Socket实现发送邮件的功能。本例中使用fsockopen()函数代替socket_*()系列函数。
fsockopen()函数的好处是把Socket连接绑定到一个流上,然后使用各种操作流的函数操作这个Socket连接。fsockopen()函数创建的Socket连接句柄可以提供给诸如fgets()、fputs()、fwrite()、fread()、fclose()等流函数使用。

下面封装一个发送邮件的类Smtp.php文件,按照SMTP协议实现,代码如下:
<?php
class Smtp {
    private $host;    // 要连接的SMTP服务器的地址
    private $port = 25;    // SMTP服务器的端口,默认为25
    private $user;    // 登录SMTP服务器的用户名
    private $pass;    // 登录SMTP服务器的密码或授权码
    private $debug = false;    // 标识是否开启调试模式,默认为否
    private $sock;    // 保存与SMTP服务器连接的句柄
    private $mail_format = 0;    // 标识使用什么格式发送邮件,0为普通文本,1为HTML邮件

    public function __construct($host, $port, $user, $pass, $format=0, $debug=false) {
        $this->host = $host;
        $this->port = $port;
        $this->user = base64_encode($user);    // 此处必须用base_64加密
        $this->pass = base64_encode($pass); // 此处必须用base_64加密
        $this->mail_format = $format;
        $this->debug = $debug;

        // 连接SMTP服务器
        $this->sock = fsockopen($this->host, $this->port, $errno, $errstr, 10);

        if (!$this->sock) {
            exit('Error number: '.$errno.', Error message: '.$errstr. "\n");
        }

        $response = fgets($this->sock); // 获取连接服务器时返回的信息,如果返回的信息中包含字符串220,则说明连接成功
        if (strstr($response, '220')===false) {
            exit("Server error: {$response}\n");
        }
    }

    /**
     * 如果开启了调试模式,则输出调试信息
     * @param unknown $message
     */
    private function show_debug($message) {
        if ($this->debug) {
            echo "

Debug message: $message

"; } } /** * 把命令发送到服务器中执行,并获取服务器返回的消息,判断命令是否执行成功 * @param unknown $cmd 发送到服务器执行的命令 * @param unknown $return_code * @return boolean */ private function do_command($cmd, $return_code) { fwrite($this->sock, $cmd); $response = fgets($this->sock); if (strstr($response, "$return_code")===false) { $this->show_debug("Command: '".$cmd."' execute failed! "."The result is: '".$response."'."); return false; } return true; } /** * 验证邮箱的合法性 * @param unknown $email * @return boolean */ private function is_email($email) { $pattern = "/^[^_][\w\.-]+@[\w]+(\.\w+)+$/i"; if (preg_match($pattern, $email, $matches)) { return true; } else { return false; } } /** * 执行邮件的发送 * @param unknown $from 发件人的邮箱 * @param unknown $to 收件人的邮箱 * @param unknown $subject 邮件的主题 * @param unknown $body 邮件的内容 * @return boolean */ public function send_mail($from, $to, $subject, $body) { if (!$this->is_email($from) || !$this->is_email($to)) { $this->show_debug("Please enter valid from/to email."); return false; } if (empty($subject) || empty($body)) { $this->show_debug("Please enter subject/content."); return false; } // 邮件详情(包含发件人、收件人、邮件主体、邮件正文、内容类型、编码格式等) $detail = "From: ".$from."\r\n"; $detail .= "To: ".$to."\r\n"; $detail .= "Subject: ".$subject."\r\n"; if ($this->mail_format==1) { $detail .= "Content-type: text/html;"."\r\n"; } else { $detail .= "Content-type: text/plain;"."\r\n"; } $detail .= "charset=utf-8"."\r\n"."\r\n"; $detail .= $body; // 根据SMTP协议向服务器发送命令 $res = $this->do_command("HELO ".$this->host."\r\n", 250); if ($res===false) return false; $res = $this->do_command("AUTH LOGIN"."\r\n", 334); if ($res===false) return false; $res = $this->do_command($this->user."\r\n", 334); if ($res===false) return false; $res = $this->do_command($this->pass."\r\n", 235); if ($res===false) return false; $res = $this->do_command("MAIL FROM: <".$from.">\r\n", 250); if ($res===false) return false; $res = $this->do_command("RCPT TO: <".$to.">\r\n", 250); if ($res===false) return false; $res = $this->do_command("DATA\r\n", 354); if ($res===false) return false; $res = $this->do_command($detail."\r\n.\r\n", 250); if ($res===false) return false; $res = $this->do_command("QUIT\r\n", 221); if ($res===false) return false; return true; } }
SMTP发送邮件测试,新建一个test_mail.php,代码如下:
<?php
require ("component/Smtp.php");  // 引入Smtp类文件
$host = "smtp.163.com";  // 要连接的SMTP服务器的地址
$port = 25;    // 服务器的端口
$user = "junjie.3533";    // 注意此处填写登录SMTP服务器的账户名称,不要带@后缀
$pass = "******";    // 登录SMTP服务器的密码或授权码
$from = "junjie.3533@163.com";    // 发件人必须和登录授权账户保持一致
$to = "******@qq.com";    // 收件人
$subject = "Test Mail";    //邮件主题
$body = "This is a example email for you!";    // 邮件正文

$smtp = new Smtp($host, $port, $user, $pass, 0, true);
$res = $smtp->send_mail($from, $to, $subject, $body);
if ($res) {
    echo "success";
} else {
    echo "failed";
}
?>
在浏览器中,访问该php测试文件,就可以发送邮件了。
可以看出,原本单纯依靠PHP无法完成的任务,结合Socket就可以轻而易举地实现。有人会问:PHP与C语言怎么交互?你可能会想到用C语言写PHP扩展,或者使用exec、system等命令,但是,为什么不换一个思路呢?比如,用C语言提供服务,用PHP请求这个服务,PHP就能借助C语言完成许多看似不可能的任务。现在流行的WebGame就是利用C、Java等重量级语言在底层完成复杂的运算,通过Socket把接口提供给PHP使用。