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感興趣的朋友有幫助。

相關文章
相關標籤/搜索