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