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