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感受對於服務器也意義不大。不過我試後都經過了。
首先定義一個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感興趣的朋友有幫助。