SMTP简介与PHP简单实现

0.SMTP工做过程简述

SMTP是客户和服务模型,之间用简单的命令,经过NVT ASCII通讯。php

如下 用 [S] 表明服务器,[C] 表明客户端。html

先来看看我用QQ邮箱发送邮件后的一些信息(密码之类的被我修改了):数组

[S]220 smtp.qq.com Esmtp QQ Mail Server
[C]EHLO localhost 
[S]250-smtp.qq.com 250-PIPELINING 250-SIZE 73400320 250-AUTH LOGIN PLAIN 250-AUTH=LOGIN 250-MAILCOMPRESS 250 8BITMIME
[C]AUTH LOGIN 
[S]334 ABCDEFGHI
[C]username 
[S]334 ABCDEFGHI
[C]password 
[S]235 Authentication successful
[C]MAIL FROM: 
[S]250 Ok
[C]RCPT TO:  
[S]250 Ok
[C]RCPT TO:  
[S]250 Ok
[C]RCPT TO:  
[S]250 Ok
[C]DATA 
[S]354 End data with .
[C]FROM:  TO:  CC:  BCC   Subject: Test mail Subject MIME-Version: 1.0 Content-Type: multipart/mixed; boundary="[BOUNDARY:4f78098b1b3fb4f42ac473f8c86cbebe]>>>" --[BOUNDARY:4f78098b1b3fb4f42ac473f8c86cbebe]>>> Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: base64 BASE64编码的正文 --[BOUNDARY:4f78098b1b3fb4f42ac473f8c86cbebe]>>> Content-Type: image/x-icon Content-Transfer-Encoding: base64 Content-Disposition: attachment; filename="favicon.ico" BASE64编码的附件 --[BOUNDARY:4f78098b1b3fb4f42ac473f8c86cbebe]>>>-- . 
[S]250 Ok: queued as
[C]QUIT 
[S]221 Bye

基本上就是由[S]先响应链接发出220开头的ASCII信息。对,每次[S]的回复都以一个三位码开头。而后[C]传递命令过去,等待[S]回复。安全

这里须要注意的几点是服务器

1.换行是用 CRLF也就是\r\n。网络

2.MIME用到来隔开正文和多个附件之间会插入一个用户定义的boundary分隔符。每部分以--boundary开头。只有MIME结束时以--boundary--结尾。socket

3.邮件DATA结尾要用到 CRLF.CRLF 结尾,能够看到QQ的服务器也提示了这点。tcp

最后有兴趣的能够去看下这些书,有命令的详解,我就是参考了这些:函数

1.《深刻理解计算机网络》第11章 11.5节 电子邮件服务this

2.《TCP/IP详解 卷1:协议》第28章 SMTP:简单邮件传送协议

以及在网上参考了一些网友的代码。

这里我还有一点疑惑,就是 EHLO或HELO后面跟的到底是什么?书上说“必须是彻底合格的客户主机名”。但是我看有的网友传的是sendmail,而localhost感受对于服务器也意义不大。不过我试后都经过了。

1. PHP简单地实现SMTP

首先定义一个Mail类,来处理邮件的一些信息。

class Mail {
        private $from;
        private $to;
        private $cc;
        private $bcc;
        private $type;
        private $subject;
        private $content;
        private $related;
        private $attachment;

        /**
        * @param from 发件人
        * @param to 收件人 或 收件人数组
        * @param subject 主题
        * @param content 内容
        * @param type 内容类型 html 或 plain,默认plain
        * @param related 内容是否引用外部连接 默认FALSE
        */
        function __construct($from,$to,$subject,
                            $content,$type='plain',$related=FALSE){
            $this->from = $from;
            $this->to = is_array($to) ? $to : [$to];
            $this->cc = [];
            $this->bcc = [];
            $this->type = $type;
            $this->subject = $subject;
            $this->content = $content;
            $this->related = $related;
            $this->attachment = [];
        }

        /**
        * @param to 收件人 或 收件人数组
        */
        function addTO($to){
            if(is_array($to))
                $this->to = array_merge($this->to,$to);
            else array_push($this->to,$to);
        }

        /**
        * @param cc 抄送人 或 抄送人数组
        */
        function addCC($cc){
            if(is_array($cc))
                $this->cc = array_merge($this->cc,$cc);
            else array_push($this->cc,$cc);
        }

        /**
        * @param bcc 秘密抄送人 或 秘密抄送人数组
        */
        function addBCC($bcc){
            if(is_array($bcc))
                $this->bcc = array_merge($this->bcc,$bcc);
            else array_push($this->bcc,$bcc);
        }

        /**
        * @param path 附件路径 或 附件路径数组
        */
        function addAttachment($path){
            if(is_array($path))
                $this->attachment = array_merge($this->attachment,$path);
            else array_push($this->attachment,$path);
        }

        /**
        * @param name 成员变量名
        * @return 非数组成员变量值
        */
        function __get($name){
            if(isset($this->$name) && !is_array($this->$name))
                return $this->$name;
            else user_error('Invalid Property: '.__CLASS__.'::'.$name);
        }

        /**
        * @param name 数组型成员变量名
        * @param visitor 遍历整个数组并调用之
        */
        function expose($name, $visitor){
            if(isset($this->$name) && is_array($this->$name))
                foreach($this->$name as $i)$visitor($i);
            else user_error('Invalid Property: '.__CLASS__.'::'.$name);
        }

        /**
        * @param name 数组型成员变量名
        * @param caller 做用于数组的调用
        * @return 返回调用后的返回值
        */
        function affect($name, $caller){
            if(isset($this->$name) && is_array($this->$name))
                return $caller($this->$name);
            else user_error('Invalid Property: '.__CLASS__.'::'.$name);
        }

        /**
        * @param name 数组型成员名
        * @return 数组成员长度
        */
        function count($name){
            if(isset($this->$name) && is_array($this->$name))
                return count($this->$name);
            else user_error('Invalid Property: '.__CLASS__.'::'.$name);
        }
    }

接着就是SMTPSender这个用于发送邮件的类:

class SMTPSender {
        private $host;
        private $port;
        private $username;
        private $password;
        private $security;

        /**
        * @param host 服务器地址
        * @param port 服务器端口
        * @param username 邮箱帐户
        * @param password 邮箱密码
        * @param security 安全层 SSL SSL2 SSL3 TLS
        */
        function __construct($host,$port,
                            $username,$password,
                            $security=NULL){
            $this->host = $host;
            $this->port = $port;
            $this->username = $username;
            $this->password = $password;
            $this->security = $security;
        }

        /**
        * @param mail Mail对象
        * @param timeout 链接超时,单位秒,默认10秒
        * @return 错误信息,无错误返回NULL
        */
        function send($mail,$timeout=10){
            $address = 'tcp://'.$this->host.':'.$this->port;
            $socket = stream_socket_client($address,$errno,$errstr,$timeout);
            if(!$socket)return $errno.' error:'.$errstr;
            try {
                //设置安全套接字
                if(isset($this->security))
                    if(!self::setSecurity($socket, $this->security))
                        return 'set security failed';
                //阻塞模式
                if(!stream_set_blocking($socket,TRUE))
                    return 'set stream blocking failed';
                //获取服务器响应
                $message = trim(fread($socket,1024));
                if(substr($message,0,3) != '220')
                    return 'Invalid Server: '.$message;
                //发送命令给服务器
                $command = self::makeCommand($this,$mail);
                foreach($command as $i){
                    $error = self::command($socket,$i[0],$i[1]);
                    if($error != NULL)return $error;
                }
                return NULL;//成功
            }catch(Exception $e){
                return '[SMTP]Exception:'.$e->getMessage();
            }finally{
                stream_socket_shutdown($socket,STREAM_SHUT_WR);
            }
        }

        /**
        * @param socket 套接字
        * @param command SMTP命令
        * @param code 期待的SMTP返回码
        * @return 错误信息,无错误返回NULL
        */
        private static function command($socket,$command,$code){
            if(fwrite($socket,$command)){
                $data = trim(fread($socket,1024));
                if(!$data)return '[SMTP Server not tip]';
                if(substr($data,0,3) == $code)return NULL;//成功
                else return '[SMTP]Error: '.$data;
            }else return '[SMTP] send command failed';
        }

        /**
        * @param server SMTP服务器信息
        * @param related 邮件是否引用外部连接
        * @return 错误信息,无错误返回NULL
        */
        private static function makeCommand($info,$mail){
            $command = [
                ["EHLO localhost\r\n",'250'],
                ["AUTH LOGIN\r\n",'334'],
                [base64_encode($info->username)."\r\n",'334'],
                [base64_encode($info->password)."\r\n",'235'],
                ['MAIL FROM:<'.$mail->from.">\r\n",'250']
            ];
            $addRCPTTO = function($i)use(&$command){
                array_push($command,['RCPT TO: <'.$i.">\r\n",'250']);
            };
            $mail->expose('to',$addRCPTTO);//收件人
            $mail->expose('cc',$addRCPTTO);//抄送人
            $mail->expose('bcc',$addRCPTTO);//秘密抄送人
            array_push($command,["DATA\r\n",'354']);
            array_push($command,[self::makeData($mail),'250']);
            array_push($command,["QUIT\r\n",'221']);
            return $command;
        }

        /**
        * @param mail 邮件
        * @return 返回生成的DATA报文
        */
        private static function makeData($mail){
            //邮件基本信息
            $data = 'FROM: <'.$mail->from.">\r\n";//发件人
            $merge = function($m){ return implode('>,<',$m); };
            $data .= 'TO: <'.$mail->affect('to',$merge).">\r\n";//收件人组
            if($mail->count('cc') != 0)//抄送人组
                $data .= 'CC: <'.$mail->affect('cc',$merge).">\r\n";
            if($mail->count('bcc') != 0)//秘密抄送人组
                $data .= 'BCC: <'.$mail->affect('bcc',$merge).">\r\n";
            $data .= "Subject: ".$mail->subject."\r\n";//主题
            //设置MIME 块
            $data .= "MIME-Version: 1.0\r\n";
            $data .= 'Content-Type: multipart/';
            $hasAttachment = $mail->count('attachment') != 0;
            if($hasAttachment)$data .= "mixed;\r\n";
            else if($mail->related)$data .= "related;\r\n";
            else $data .= "alternative;\r\n";
            $boundary = '[BOUNDARY:'.md5(uniqid()).']>>>';
            $data .= "\tboundary=\"".$boundary."\"\r\n\r\n";
            //正文内容
            $data .= '--'.$boundary."\r\n";
            $data .= 'Content-Type: text/'.$mail->type."; charset=utf-8\r\n";
            $data .= "Content-Transfer-Encoding: base64\r\n\r\n";
            $data .= base64_encode($mail->content)."\r\n\r\n";
            //附件
            if($hasAttachment)$mail->expose('attachment',function($i)use(&$data,$boundary){
                if(!is_file($i))return;
                $type = mime_content_type($i);
                $name = basename($i);
                $file = base64_encode(file_get_contents($i));
                $data .= '--'.$boundary."\r\n";
                $data .= 'Content-Type: '.$type."\r\n";
                $data .= "Content-Transfer-Encoding: base64\r\n";
                $data .= 'Content-Disposition: attachment; filename="'.$name."\"\r\n\r\n";
                $data .= $file."\r\n\r\n";
            });
            //结束块 和 结束邮件
            $data .= "--".$boundary."--\r\n\r\n.\r\n";
            return $data;
        }

        /**
        * @param socket 套接字
        * @param type   安全层类型 SSL SSL2 SSL3 TLS
        * @return 设置是否成功的BOOL值
        */
        private static function setSecurity($socket, $type){
            $method = NULL;
            if($type == 'SSL')$method = STREAM_CRYPTO_METHOD_SSLv23_CLIENT;
            else if($type == 'SSL2')$method = STREAM_CRYPTO_METHOD_SSLv2_CLIENT;
            else if($type == 'SSL3')$method = STREAM_CRYPTO_METHOD_SSLv3_CLIENT;
            else if($type == 'TLS')$method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
            if($method == NULL) return FALSE;
            stream_socket_enable_crypto($socket,TRUE,$method);
            return TRUE;
        }
    }

SMTPSender只有send这个成员函数是公开的。

下面我给出一个使用这两个类的例子,假设参数从$_POST传入:

$mail = new Mail(
    $_POST['from'],
    explode(';',$_POST['to']),
    $_POST['subject'],
    'adfdsgsgsdfsdfdsafsd!!!!!@@@@文本内容123456789'
);
if(isset($_POST['cc']))$mail->addCC(explode(';',$_POST['cc']));
if(isset($_POST['bcc']))$mail->addBCC(explode(';',$_POST['bcc']));
$mail->addAttachment('./demo/favicon.ico');
$sender = new SMTPSender(
    $_POST['host'],$_POST['port'],
    $_POST['username'],
    $_POST['password'],
    $_POST['security']
);
$error = $sender->send($mail);

但愿这些对SMTP感兴趣的朋友有帮助。

相关文章
相关标签/搜索