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] 応答は 3 桁のコードで始まります。次に、[C] はコマンドを渡し、[S] が応答するのを待ちます。
ここで注意すべき点は、
1 です。改行には CRLF (rn) が使用されます。
2.MIME は、ユーザー定義の境界区切り文字を挿入することで本文と複数の添付ファイルを分離するために使用されます。各セクションは --boundary で始まります。 --boundary-- で終わるファイルのみです。
3. 電子メールの DATA の末尾は CRLF.CRLF でなければなりません。QQ サーバーもこれを要求することがわかります。
最後に、興味のある方は、先ほど参照したコマンドについて詳しく説明しています。
1. 「コンピュータ ネットワークの詳細」セクション 11.5 電子メール サービス
2. 『TCP/IP 詳細解説 第 1 巻: プロトコル』の第 28 章 SMTP: Simple Mail Transfer Protocol
およびインターネット上の一部のネットユーザーのコードを参照しました。
ここでまだ少し疑問があります。つまり、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 の送信メンバー関数のみがパブリックです。
以下に、パラメーターが $_POST から渡されると仮定して、これら 2 つのクラスの使用例を示します。
$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 に興味のある友人の役に立つことを願っています。