SMTP簡介與PHP簡單實現

來源:互聯網
上載者:User
0.SMTP工作過程簡述

SMTP是客戶和服務模型,之間用簡單的命令,通過NVT ASCII通訊。

以下 用 [S] 代表格服務器,[C] 代表用戶端。

先來看看我用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開頭。只有檔案結束時以--boundary--結尾。

3.郵件DATA結尾要用到 CRLF.CRLF 結尾,可以看到QQ的伺服器也提示了這點。

最後有興趣的可以去看下這些書,有命令的詳解,我就是參考了這些:

1.《深入理解電腦網路》第11章 11.5節 電子郵件服務

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 related 郵件是否引用外部連結        * @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感興趣的朋友有協助。

  • 聯繫我們

    該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

    如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

    A Free Trial That Lets You Build Big!

    Start building with 50+ products and up to 12 months usage for Elastic Compute Service

    • Sales Support

      1 on 1 presale consultation

    • After-Sales Support

      24/7 Technical Support 6 Free Tickets per Quarter Faster Response

    • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.