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:from.>\r\n,'250'] ]; $addrcptto = function($i)use(&$command){ array_push($command,['rcpt to: \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: from.>\r\n;//发件人 $merge = function($m){ return implode('>,\r\n;//收件人组 if($mail->count('cc') != 0)//抄送人组 $data .= 'cc: affect('cc',$merge).>\r\n; if($mail->count('bcc') != 0)//秘密抄送人组 $data .= 'bcc: 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感兴趣的朋友有帮助。