php发邮件类mysendmail探究

  这两天看了一下之前的老项目,里面有个注册集成了邮件功能,再次测试一下,发现邮件已经无法正常发送了,为了找到原因,特地分析了一下代码。

  1 <?php
  2 class MySendMail {
  3     /**
  4      * @var string 邮件传输代理用户名
  5      * @access private
  6      */
  7     private $_userName;
  8 
  9     /**
 10      * @var string 邮件传输代理密码
 11      * @access private
 12      */
 13     private $_password;
 14 
 15     /**
 16      * @var string 邮件传输代理服务器地址
 17      * @access protected
 18      */
 19     protected $_sendServer;
 20 
 21     /**
 22      * @var int 邮件传输代理服务器端口
 23      * @access protected
 24      */
 25     protected $_port=25;
 26 
 27     /**
 28      * @var string 发件人
 29      * @access protected
 30      */
 31     protected $_from;
 32 
 33     /**
 34      * @var string 收件人
 35      * @access protected
 36      */
 37     protected $_to;
 38 
 39     /**
 40      * @var string 抄送
 41      * @access protected
 42      */
 43     protected $_cc;
 44 
 45     /**
 46      * @var string 秘密抄送
 47      * @access protected
 48      */
 49     protected $_bcc;
 50 
 51     /**
 52      * @var string 主题
 53      * @access protected
 54      */
 55     protected $_subject;
 56 
 57     /**
 58      * @var string 邮件正文
 59      * @access protected
 60      */
 61     protected $_body;
 62 
 63     /**
 64      * @var string 附件
 65      * @access protected
 66      */
 67     protected $_attachment;
 68 
 69     /**
 70      * @var reource socket资源
 71      * @access protected
 72      */
 73     protected $_socket;
 74 
 75     /**
 76      * @var string 错误信息
 77      * @access protected
 78      */
 79     protected $_errorMessage;
 80 
 81     /**
 82      * 设置邮件传输代理,如果是可以匿名发送有邮件的服务器,只需传递代理服务器地址就行
 83      * @access public
 84      * @param string $server 代理服务器的ip或者域名
 85      * @param string $username 认证账号
 86      * @param string $password 认证密码
 87      * @param int $port 代理服务器的端口,smtp默认25号端口
 88      * @return boolean
 89      */
 90     public function setServer($server, $username="", $password="", $port=25) {
 91         $this->_sendServer = $server;
 92         $this->_port = $port;
 93         if(!empty($username)) {
 94             $this->_userName = base64_encode($username);
 95         }
 96         if(!empty($password)) {
 97             $this->_password = base64_encode($password);
 98         }
 99         return true;
100     }
101 
102     /**
103      * 设置发件人
104      * @access public
105      * @param string $from 发件人地址
106      * @return boolean
107      */
108     public function setFrom($from) {
109         $this->_from = $from;
110         return true;
111     }
112 
113     /**
114      * 设置收件人,多个收件人,连续调用多次.
115      * @access public
116      * @param string $to 收件人地址
117      * @return boolean
118      */
119     public function setReceiver($to) {
120         if(isset($this->_to)) {
121             if(is_string($this->_to)) {
122                 $this->_to = array($this->_to);
123                 $this->_to[] = $to;
124                 return true;
125             }
126             elseif(is_array($this->_to)) {
127                 $this->_to[] = $to;
128                 return true;
129             }
130             else {
131                 return false;
132             }
133         }
134         else {
135             $this->_to = $to;
136             return true;
137         }
138     }
139 
140     /**
141      * 设置抄送,多个抄送,连续调用多次.
142      * @access public
143      * @param string $cc 抄送地址
144      * @return boolean
145      */
146     public function setCc($cc) {
147         if(isset($this->_cc)) {
148             if(is_string($this->_cc)) {
149                 $this->_cc = array($this->_cc);
150                 $this->_cc[] = $cc;
151                 return true;
152             }
153             elseif(is_array($this->_cc)) {
154                 $this->_cc[] = $cc;
155                 return true;
156             }
157             else {
158                 return false;
159             }
160         }
161         else {
162             $this->_cc = $cc;
163             return true;
164         }
165     }
166 
167     /**
168      * 设置秘密抄送,多个秘密抄送,连续调用多次
169      * @access public
170      * @param string $bcc 秘密抄送地址
171      * @return boolean
172      */
173     public function setBcc($bcc) {
174         if(isset($this->_bcc)) {
175             if(is_string($this->_bcc)) {
176                 $this->_bcc = array($this->_bcc);
177                 $this->_bcc[] = $bcc;
178                 return true;
179             }
180             elseif(is_array($this->_bcc)) {
181                 $this->_bcc[] = $bcc;
182                 return true;
183             }
184             else {
185                 return false;
186             }
187         }
188         else {
189             $this->_bcc = $bcc;
190             return true;
191         }
192     }
193 
194     /**
195      * 设置邮件信息
196      * @access public
197      * @param string $body 邮件主题
198      * @param string $subject 邮件主体内容,可以是纯文本,也可是是HTML文本
199      * @param string $attachment 附件,文件地址
200      * @return boolean
201      */
202     public function setMailInfo($subject, $body, $attachment="") {
203         $this->_subject = $subject;
204         $this->_body = base64_encode($body);
205         if(!empty($attachment)) {
206             $this->_attachment = $attachment;
207         }
208         return true;
209     }
210 
211     /**
212      * 发送邮件
213      * @access public
214      * @return boolean
215      */
216     public function sendMail() {
217         $command = $this->getCommand();
218         $result = $this->socket();
219         foreach ($command as $value) {
220             if($this->sendCommand($value[0], $value[1])) {
221                 continue;
222             }
223             else{
224                 return false;
225             }
226         }
227 
228         //其实这里也没必要关闭,smtp命令:QUIT发出之后,服务器就关闭了连接,本地的socket资源会自动释放
229         $this->close();
230         //echo 'Mail OK!';
231         return true;
232     }
233 
234     /**
235      * 返回错误信息
236      * @return string
237      */
238     public function error(){
239         if(!isset($this->_errorMessage)) {
240             $this->_errorMessage = "";
241         }
242         return $this->_errorMessage;
243     }
244 
245     /**
246      * 返回mail命令
247      * @access protected
248      * @return array
249      */
250     public function getCommand() {
251         $command = array(
252             array("HELO sendmail\r\n", 250),
253             array("EHLO sendmail\r\n", 250)
254         );
255         if(!empty($this->_userName)){
256             $command[] = array("AUTH LOGIN\r\n", 334);
257             $command[] = array($this->_userName . "\r\n", 334);
258             $command[] = array($this->_password . "\r\n", 235);
259         }
260         $command[] = array("MAIL FROM:<" . $this->_from . ">\r\n", 250);
261 
262         $separator = "----=_Part_" . md5($this->_from . time()) . uniqid(); //分隔符
263         //设置发件人
264         $header = "FROM: dolphinphp.cn<" . $this->_from . ">\r\n";
265 
266         //设置收件人
267         if(is_array($this->_to)) {
268             $count = count($this->_to);
269             for($i=0; $i<$count; $i++){
270                 $command[] = array("RCPT TO: <" . $this->_to[$i] . ">\r\n", 250);
271                 if($i == 0){
272                     $header .= "TO: <" . $this->_to[$i] .">";
273                 }
274                 elseif($i + 1 == $count){
275                     $header .= ",<" . $this->_to[$i] .">\r\n";
276                 }
277                 else{
278                     $header .= ",<" . $this->_to[$i] .">";
279                 }
280             }
281         }
282         else{
283             $command[] = array("RCPT TO: <" . $this->_to . ">\r\n", 250);
284             $header .= "TO: <" . $this->_to . ">\r\n";
285         }
286 
287         //设置抄送
288         if(isset($this->_cc)) {
289             if(is_array($this->_cc)) {
290                 $count = count($this->_cc);
291                 for($i=0; $i<$count; $i++){
292                     $command[] = array("RCPT TO: <" . $this->_cc[$i] . ">\r\n", 250);
293                     if($i == 0){
294                         $header .= "CC: <" . $this->_cc[$i] .">";
295                     }
296                     elseif($i + 1 == $count){
297                         $header .= ",<" . $this->_cc[$i] .">\r\n";
298                     }
299                     else{
300                         $header .= ",<" . $this->_cc[$i] .">";
301                     }
302                 }
303             }
304             else{
305                 $command[] = array("RCPT TO: <" . $this->_cc . ">\r\n", 250);
306                 $header .= "CC: <" . $this->_cc . ">\r\n";
307             }
308         }
309 
310         //设置秘密抄送
311         if(isset($this->_bcc)) {
312             if(is_array($this->_bcc)) {
313                 $count = count($this->_bcc);
314                 for($i=0; $i<$count; $i++){
315                     $command[] = array("RCPT TO: <" . $this->_bcc[$i] . ">\r\n", 250);
316                     if($i == 0){
317                         $header .= "BCC: <" . $this->_bcc[$i] .">";
318                     }
319                     elseif($i + 1 == $count){
320                         $header .= ",<" . $this->_bcc[$i] .">\r\n";
321                     }
322                     else{
323                         $header .= ",<" . $this->_bcc[$i] .">";
324                     }
325                 }
326             }
327             else{
328                 $command[] = array("RCPT TO: <" . $this->_bcc . ">\r\n", 250);
329                 $header .= "BCC: <" . $this->_bcc . ">\r\n";
330             }
331         }
332 
333         $header .= "Subject: " . $this->_subject ."\r\n";
334         if(isset($this->_attachment)) {
335             //含有附件的邮件头需要声明成这个
336             $header .= "Content-Type: multipart/mixed;\r\n";
337         }
338         elseif(false){
339             //邮件体含有图片资源的需要声明成这个
340             $header .= "Content-Type: multipart/related;\r\n";
341         }
342         else{
343             //html或者纯文本的邮件声明成这个
344             $header .= "Content-Type: multipart/alternative;\r\n";
345         }
346 
347         //邮件头分隔符
348         $header .= "\t" . 'boundary="' . $separator . '"';
349         $header .= "\r\nMIME-Version: 1.0\r\n";
350         $header .= "\r\n--" . $separator . "\r\n";
351         $header .= "Content-Type:text/html; charset=utf-8\r\n";
352         $header .= "Content-Transfer-Encoding: base64\r\n\r\n";
353         $header .= $this->_body . "\r\n";
354         $header .= "--" . $separator . "\r\n";
355 
356         //加入附件
357         if(isset($this->_attachment)){
358             $header .= "\r\n--" . $separator . "\r\n";
359             $header .= "Content-Type: " . $this->getMIMEType() . '; name="' . basename($this->_attachment) . '"' . "\r\n";
360             $header .= "Content-Transfer-Encoding: base64\r\n";
361             $header .= 'Content-Disposition: attachment; filename="' . basename($this->_attachment) . '"' . "\r\n";
362             $header .= "\r\n";
363             $header .= $this->readFile();
364             $header .= "\r\n--" . $separator . "\r\n";
365         }
366 
367         $header .= "\r\n.\r\n";
368 
369         $command[] = array("DATA\r\n", 354);
370         $command[] = array($header, 250);
371         $command[] = array("QUIT\r\n", 221);
372 
373         return $command;
374     }
375 
376     /**
377      * 发送命令
378      * @access protected
379      * @param string $command 发送到服务器的smtp命令
380      * @param int $code 期望服务器返回的响应吗
381      * @return boolean
382      */
383     protected function sendCommand($command, $code) {
384         //echo 'Send command:' . $command . ',expected code:' . $code . '<br />';
385         //发送命令给服务器
386         try{
387             if(socket_write($this->_socket, $command, strlen($command))){
388 
389                 //当邮件内容分多次发送时,没有$code,服务器没有返回
390                 if(empty($code))  {
391                     return true;
392                 }
393 
394                 //读取服务器返回
395                 $data = trim(socket_read($this->_socket, 1024));
396 //                echo 'response:' . $data . '<br /><br />';die;
397 
398                 if($data) {
399                     $pattern = "/^".$code."/";
400                     if(preg_match($pattern, $data)) {
401                         return true;
402                     }
403                     else{
404                         $this->_errorMessage = "Error:" . $data . "|**| command:";
405                         return false;
406                     }
407                 }
408                 else{
409                     $this->_errorMessage = "Error:" . socket_strerror(socket_last_error());
410                     return false;
411                 }
412             }
413             else{
414                 $this->_errorMessage = "Error:" . socket_strerror(socket_last_error());
415                 return false;
416             }
417 
418         }catch(Exception $e) {
419             $this->_errorMessage = "Error:" . $e->getMessage();
420         }
421     }
422 
423     /**
424      * 读取附件文件内容,返回base64编码后的文件内容
425      * @access protected
426      * @return mixed
427      */
428     protected function readFile() {
429         if(isset($this->_attachment) && file_exists($this->_attachment)) {
430             $file = file_get_contents($this->_attachment);
431             return base64_encode($file);
432         }
433         else {
434             return false;
435         }
436     }
437 
438     /**
439      * 获取附件MIME类型
440      * @access protected
441      * @return mixed
442      */
443     protected function getMIMEType() {
444         if(isset($this->_attachment) && file_exists($this->_attachment)) {
445             $mime = mime_content_type($this->_attachment);
446             if(! preg_match("/gif|jpg|png|jpeg/", $mime)){
447                 $mime = "application/octet-stream";
448             }
449             return $mime;
450         }
451         else {
452             return false;
453         }
454     }
455 
456     /**
457      * 建立到服务器的网络连接
458      * @access private
459      * @return boolean
460      */
461     private function socket() {
462         if(!function_exists("socket_create")) {
463             $this->_errorMessage = "Extension sockets must be enabled";
464             return false;
465         }
466         //创建socket资源
467         $this->_socket = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
468 
469         if(!$this->_socket) {
470             $this->_errorMessage = socket_strerror(socket_last_error());
471             return false;
472         }
473 
474         socket_set_block($this->_socket);//设置阻塞模式
475 
476         //连接服务器
477         if(!socket_connect($this->_socket, $this->_sendServer, $this->_port)) {
478             $this->_errorMessage = socket_strerror(socket_last_error());
479             return false;
480         }
481 
482         return true;
483     }
484 
485     /**
486      * 关闭socket
487      * @access private
488      * @return boolean
489      */
490     private function close() {
491         if(isset($this->_socket) && is_object($this->_socket)) {
492             $this->_socket->close();
493             return true;
494         }
495         $this->_errorMessage = "No resource can to be close";
496         return false;
497     }
498 
499     public static function GetInstance()
500     {
501         return new MySendMail();
502     }
503 
504     public function __construct()
505     {
506 
507     }
508 }
509 
510 /**************************** Test ***********************************/
511 /**
512  * Created by PhpStorm.
513  * User: administrator
514  * Date: 2019-01-11
515  * Time: 14:00
516  */
517 namespace util;
518 /**
519  * 邮件发送类
520  * 支持发送纯文本邮件和HTML格式的邮件,可以多收件人,多抄送,多秘密抄送,带附件的邮件
521  * 需要的php扩展,sockets和Fileinfo。
522  * @example
523  * $mail = new MySendMail();
524  * $mail->setServer("XXXXX", "XXXXX@XXXXX", "XXXXX"); 设置smtp服务器
525  * $mail->setFrom("XXXXX"); 设置发件人
526  * $mail->setReceiver("XXXXX"); 设置收件人,多个收件人,调用多次
527  * $mail->setCc("XXXX"); 设置抄送,多个抄送,调用多次
528  * $mail->setBcc("XXXXX"); 设置秘密抄送,多个秘密抄送,调用多次
529  * $mail->setMailInfo("test", "<b>test</b>"); 设置邮件主题、内容
530  * $mail->sendMail(); 发送
531  * //具体例子
532  * $mail = new MySendMail();
533  * $mail->setServer("smtp.qq.com", "[email protected]", "nrmcxmxepepcffah");
534  * $mail->setFrom("[email protected]");
535  * $mail->setReceiver("[email protected]");
536  * $mail->setReceiver("XXXXX@XXXXX");
537  * $mail->setCc("XXXXX@XXXXX");
538  * $mail->setCc("XXXXX@XXXXX");
539  * $mail->setBcc("XXXXX@XXXXX");
540  * $mail->setBcc("XXXXX@XXXXX");
541  * $mail->setBcc("XXXXX@XXXXX");
542  * $mail->setMailInfo("test", "<b>test</b>");
543  * $mail->sendMail();
544  */
View Code

  在sendmail方法中,它调用了 $command = $this->getCommand(); , 要说一句,在调用sendmail之前,许哟啊设置授权登录用户,smtp服务器,邮件内容等等,而 getCommand() 方法则将邮件发送类的属性格式化成一个二维数组,类似下面这样

 1 array (size=10)
 2   0 => 
 3     array (size=2)
 4       0 => string 'HELO sendmail
 5 ' (length=15)
 6       1 => int 250
 7   1 => 
 8     array (size=2)
 9       0 => string 'EHLO sendmail
10 ' (length=15)
11       1 => int 250
12   2 => 
13     array (size=2)
14       0 => string 'AUTH LOGIN
15 ' (length=12)
16       1 => int 334
17   3 => 
18     array (size=2)
19       0 => string 'xxxxxxxxxxxxxxxxxxxxxxxxxx=
20 ' (length=30)
21       1 => int 334
22   4 => 
23     array (size=2)
24       0 => string 'xxxxxxxxxxxxxxxxxxxxxxxxx==
25 ' (length=26)
26       1 => int 235
27   5 => 
28     array (size=2)
29       0 => string 'MAIL FROM:<[email protected]>
30 ' (length=34)
31       1 => int 250
32   6 => 
33     array (size=2)
34       0 => string 'RCPT TO: <[email protected]>
35 ' (length=30)
36       1 => int 250
37   7 => 
38     array (size=2)
39       0 => string 'DATA
40 ' (length=6)
41       1 => int 354
42   8 => 
43     array (size=2)
44       0 => string 'FROM: dolphinphp.cn<[email protected]>
45 TO: <[email protected]>
46 Subject: dolphin.cn:鍗氬鐢ㄦ埛娉ㄥ唽
47 Content-Type: multipart/alternative;
48     boundary="----=_Part_089f52854a5752061e9a410874b0d6f95e8ca0ddf03d4"
49 MIME-Version: 1.0
50 
51 ------=_Part_089f52854a5752061e9a410874b0d6f95e8ca0ddf03d4
52 Content-Type:text/html; charset=utf-8
53 Content-Transfer-Encoding: base64
54 
55 PHRhYmxlPjx0cj48dGQ+aGFoYTwvdGQ+PC90cj48dGFibGU+
56 ------=_Part_089f52854a5752061e9a410874b0d6f95e8ca0ddf03d4
57 
58 .
59 ' (length=488)
60       1 => int 250
61   9 => 
62     array (size=2)
63       0 => string 'QUIT
64 ' (length=6)
65       1 => int 221
View Code

  数组中的每个数组第一个代表给服务器发送的内容,第二个表示期待服务器返回的状态码。这里还没又发送邮件,在调用了 getCommond()   方法以后,之后又调用了 $this->socket() ,这个方法创建了一个socket资源,这个socket连接了smtp服务器,看了一下 socket() 方法的内容,它创建了一个持久的TCP连接,回到 sendmail() 这个方法,接下来就是foreach发邮件了。它发邮件的过程将在下面讨论,先看看结果,等待了许久,代码执行完了,但是目标账户并没有收到邮件,分析原因,一步一步var_dump,配置没有问题,socket扩展也开了,fileinfo也开了,但还是发送不成功,于是看了一下类内部的errorMessage,发现message解码之后是“操作成功完成”???既然成功完成,为什么还会报错呢,回到代码,在foreach中它又调用了 sendCommond() 这个方法,$data为空的时候它也会报错,在这里调试了一下,发现确实是为空才报错的,由于对soclet也不熟,去官方文档查了一下函数的返回值 。

   图没截完,下拉之后是返回的错误码,但是我打印了一下我的错误码,是0,最后发现返回0就是没有发生错误啊,那就说明这段代码里有bug,既然没有错误,那为啥还发送不成功呢,于是百度了一下有关smtp(简单邮件传输协议)的文章php利用socket发邮件,里面大致讲解了发邮件的一个过程,要遵守smtp协议。mysendmail类发邮件的过程与文章发邮件的过程对比起来就很形象了,在telnet连接成功了以后遍开始了逐行发送的过程。

  于是我本地测试了一下。

   我连接的是465端口,但是无论我在键盘上按什么都没有反应,这也印证了为什么报错,即使在cli模式下发邮件,无论你发什么,服务器都不会返回信息,也就是说,在 sendCommond() 内,你发出去的每一条命令都不会有返回信息,更不会有状态码了,也就是说$data为空,也就是后面报错的原因。至于为什么没反应,这应该是端口的问题,465是基于ssl协议传输的可能会有更多一步的操作,这个没有深入研究打算看看github大佬写的phpmailer再来分析465端口是如何传输的吧。

  至于为什么文章中 telnet 成功发送邮件成功了,是因为它使用的是25端口,但是好像在去年,网易和QQ似乎不再支持25端口了,虽然telnet可以成功脸上服务器的25端口,但是把类的默认端口改成25后发送邮件,会直接被服务器当成垃圾 邮件屏蔽掉。而且咋爱邮箱中配置第三方登录smtp,imtp中已经没有25端口了,如果大家想使用php的邮件发送类,还是建议 composer require phpmailer/phpmailer 

猜你喜欢

转载自www.cnblogs.com/pbvip/p/12657381.html