这两天看了一下之前的老项目,里面有个注册集成了邮件功能,再次测试一下,发现邮件已经无法正常发送了,为了找到原因,特地分析了一下代码。
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", "XXXXX@qq.com", "nrmcxmxepepcffah"); 534 * $mail->setFrom("XXXXX@qq.com"); 535 * $mail->setReceiver("XXXXX@qq.com"); 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 */
在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:<xxxxxxxxxx@126.com> 30 ‘ (length=34) 31 1 => int 250 32 6 => 33 array (size=2) 34 0 => string ‘RCPT TO: <xxxxxxxxxxx@qq.com> 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<xxxxxxxx5@126.com> 45 TO: <1169252235@qq.com> 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
数组中的每个数组第一个代表给服务器发送的内容,第二个表示期待服务器返回的状态码。这里还没又发送邮件,在调用了 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
原文:https://www.cnblogs.com/pbvip/p/12657381.html