1<?php 2/***************************************************\ 3 * 4 * Mailer (https://github.com/txthinking/Mailer) 5 * 6 * A lightweight PHP SMTP mail sender. 7 * Implement RFC0821, RFC0822, RFC1869, RFC2045, RFC2821 8 * 9 * Support html body, don't worry that the receiver's 10 * mail client can't support html, because Mailer will 11 * send both text/plain and text/html body, so if the 12 * mail client can't support html, it will display the 13 * text/plain body. 14 * 15 * Create Date 2012-07-25. 16 * Under the MIT license. 17 * 18 \***************************************************/ 19 20namespace Tx\Mailer; 21 22use Psr\Log\LoggerInterface; 23use Tx\Mailer\Exceptions\CodeException; 24use Tx\Mailer\Exceptions\CryptoException; 25use Tx\Mailer\Exceptions\SMTPException; 26 27class SMTP 28{ 29 /** 30 * smtp socket 31 */ 32 protected $smtp; 33 34 /** 35 * smtp server 36 */ 37 protected $host; 38 39 /** 40 * smtp server port 41 */ 42 protected $port; 43 44 /** 45 * smtp secure ssl tls tlsv1.0 tlsv1.1 tlsv1.2 46 */ 47 protected $secure; 48 49 /** 50 * smtp allow insecure ssl 51 */ 52 protected $allowInsecure; 53 54 /** 55 * EHLO message 56 */ 57 protected $ehlo; 58 59 /** 60 * smtp username 61 */ 62 protected $username; 63 64 /** 65 * smtp password 66 */ 67 protected $password; 68 69 /** 70 * oauth access token 71 */ 72 protected $oauthToken; 73 74 /** 75 * $this->CRLF 76 * @var string 77 */ 78 protected $CRLF = "\r\n"; 79 80 /** 81 * @var Message 82 */ 83 protected $message; 84 85 /** 86 * @var LoggerInterface - Used to make things prettier than self::$logger 87 */ 88 protected $logger; 89 90 /** 91 * Stack of all commands issued to SMTP 92 * @var array 93 */ 94 protected $commandStack = array(); 95 96 /** 97 * Stack of all results issued to SMTP 98 * @var array 99 */ 100 protected $resultStack = array(); 101 102 public function __construct(LoggerInterface $logger=null) 103 { 104 $this->logger = $logger; 105 } 106 107 /** 108 * set server and port 109 * @param string $host server 110 * @param int $port port 111 * @param string $secure ssl tls 112 * @param bool $allowInsecure skip certificate verification? 113 * @return $this 114 */ 115 public function setServer($host, $port, $secure=null, $allowInsecure=null) 116 { 117 $this->host = $host; 118 $this->port = $port; 119 $this->secure = $secure; 120 $this->allowInsecure = (bool) $allowInsecure; 121 if(!$this->ehlo) $this->ehlo = $host; 122 $this->logger && $this->logger->debug("Set: the server"); 123 return $this; 124 } 125 126 /** 127 * auth login with server 128 * @param string $username 129 * @param string $password 130 * @return $this 131 */ 132 public function setAuth($username, $password) 133 { 134 $this->username = $username; 135 $this->password = $password; 136 $this->logger && $this->logger->debug("Set: the auth login"); 137 return $this; 138 } 139 140 /** 141 * auth oauthbearer with server 142 * @param string $accessToken 143 * @return $this 144 */ 145 public function setOAuth($accessToken) 146 { 147 $this->oauthToken = $accessToken; 148 $this->logger && $this->logger->debug("Set: the auth oauthbearer"); 149 return $this; 150 } 151 152 /** 153 * set the EHLO message 154 * @param $ehlo 155 * @return $this 156 */ 157 public function setEhlo($ehlo) 158 { 159 $this->ehlo = $ehlo; 160 return $this; 161 } 162 163 /** 164 * Send the message 165 * 166 * @param Message $message 167 * @return bool 168 * @throws CodeException 169 * @throws CryptoException 170 * @throws SMTPException 171 */ 172 public function send(Message $message) 173 { 174 $this->logger && $this->logger->debug('Set: a message will be sent'); 175 $this->message = $message; 176 $this->connect() 177 ->ehlo(); 178 179 if ($this->secure === 'tls' || $this->secure === 'tlsv1.0' || $this->secure === 'tlsv1.1' | $this->secure === 'tlsv1.2') { 180 $this->starttls() 181 ->ehlo(); 182 } 183 184 if ($this->username !== null || $this->password !== null) { 185 $this->authLogin(); 186 } elseif ($this->oauthToken !== null) { 187 $this->authOAuthBearer(); 188 } 189 $this->mailFrom() 190 ->rcptTo() 191 ->data() 192 ->quit(); 193 return fclose($this->smtp); 194 } 195 196 /** 197 * connect the server 198 * SUCCESS 220 199 * @return $this 200 * @throws CodeException 201 * @throws SMTPException 202 */ 203 protected function connect() 204 { 205 $this->logger && $this->logger->debug("Connecting to {$this->host} at {$this->port}"); 206 $host = ($this->secure == 'ssl') ? 'ssl://' . $this->host : $this->host; 207 // Create connection 208 $context = stream_context_create([]); 209 if ($this->allowInsecure) { 210 $context = stream_context_create([ 211 'ssl' => [ 212 'security_level' => 0, 213 'verify_peer' => false, 214 'verify_peer_name' => false 215 ] 216 ]); 217 } 218 $this->smtp = @stream_socket_client( 219 $host.':'.$this->port, 220 $error_code, 221 $error_message, 222 ini_get('default_socket_timeout'), 223 STREAM_CLIENT_CONNECT, 224 $context 225 ); 226 if (!$this->smtp){ 227 throw new SMTPException("Could not open SMTP Port to $host:{$this->port}"); 228 } 229 $code = $this->getCode(); 230 if ($code !== '220'){ 231 throw new CodeException('220', $code, array_pop($this->resultStack)); 232 } 233 return $this; 234 } 235 236 /** 237 * SMTP STARTTLS 238 * SUCCESS 220 239 * @return $this 240 * @throws CodeException 241 * @throws CryptoException 242 * @throws SMTPException 243 */ 244 protected function starttls() 245 { 246 $in = "STARTTLS" . $this->CRLF; 247 $code = $this->pushStack($in); 248 if ($code !== '220'){ 249 throw new CodeException('220', $code, array_pop($this->resultStack)); 250 } 251 252 if ($this->secure !== 'tls' && version_compare(phpversion(), '5.6.0', '<')) { 253 throw new CryptoException('Crypto type expected PHP 5.6 or greater'); 254 } 255 256 if ($this->allowInsecure) { 257 stream_context_set_option($this->smtp, 'ssl', 'verify_peer', false); 258 stream_context_set_option($this->smtp, 'ssl', 'verify_peer_name', false); 259 stream_context_set_option($this->smtp, 'ssl', 'allow_self_signed', true); 260 } 261 262 if(!\stream_socket_enable_crypto($this->smtp, true, STREAM_CRYPTO_METHOD_ANY_CLIENT)) { 263 throw new CryptoException("Start TLS failed to enable crypto"); 264 } 265 return $this; 266 } 267 268 /** 269 * SMTP EHLO 270 * SUCCESS 250 271 * @return $this 272 * @throws CodeException 273 * @throws SMTPException 274 */ 275 protected function ehlo() 276 { 277 $in = "EHLO " . $this->ehlo . $this->CRLF; 278 $code = $this->pushStack($in); 279 if ($code !== '250'){ 280 throw new CodeException('250', $code, array_pop($this->resultStack)); 281 } 282 return $this; 283 } 284 285 /** 286 * SMTP AUTH LOGIN 287 * SUCCESS 334 288 * SUCCESS 334 289 * SUCCESS 235 290 * @return $this 291 * @throws CodeException 292 * @throws SMTPException 293 */ 294 protected function authLogin() 295 { 296 $in = "AUTH LOGIN" . $this->CRLF; 297 $code = $this->pushStack($in); 298 if ($code !== '334'){ 299 throw new CodeException('334', $code, array_pop($this->resultStack)); 300 } 301 $in = base64_encode($this->username) . $this->CRLF; 302 $code = $this->pushStack($in); 303 if ($code !== '334'){ 304 throw new CodeException('334', $code, array_pop($this->resultStack)); 305 } 306 $in = base64_encode($this->password) . $this->CRLF; 307 $code = $this->pushStack($in); 308 if ($code !== '235'){ 309 throw new CodeException('235', $code, array_pop($this->resultStack)); 310 } 311 return $this; 312 } 313 314 /** 315 * SMTP AUTH OAUTHBEARER 316 * SUCCESS 235 317 * @return $this 318 * @throws CodeException 319 * @throws SMTPException 320 */ 321 protected function authOAuthBearer() 322 { 323 $authStr = sprintf("n,a=%s,%shost=%s%sport=%s%sauth=Bearer %s%s%s", 324 $this->message->getFromEmail(), 325 chr(1), 326 $this->host, 327 chr(1), 328 $this->port, 329 chr(1), 330 $this->oauthToken, 331 chr(1), 332 chr(1) 333 ); 334 $authStr = base64_encode($authStr); 335 $in = "AUTH OAUTHBEARER $authStr" . $this->CRLF; 336 $code = $this->pushStack($in); 337 if ($code !== '235'){ 338 throw new CodeException('235', $code, array_pop($this->resultStack)); 339 } 340 return $this; 341 } 342 343 /** 344 * SMTP AUTH XOAUTH2 345 * SUCCESS 235 346 * @return $this 347 * @throws CodeException 348 * @throws SMTPException 349 */ 350 protected function authXOAuth2() 351 { 352 $authStr = sprintf("user=%s%sauth=Bearer %s%s%s", 353 $this->message->getFromEmail(), 354 chr(1), 355 $this->oauthToken, 356 chr(1), 357 chr(1) 358 ); 359 $authStr = base64_encode($authStr); 360 $in = "AUTH XOAUTH2 $authStr" . $this->CRLF; 361 $code = $this->pushStack($in); 362 if ($code !== '235'){ 363 throw new CodeException('235', $code, array_pop($this->resultStack)); 364 } 365 return $this; 366 } 367 368 /** 369 * SMTP MAIL FROM 370 * SUCCESS 250 371 * @return $this 372 * @throws CodeException 373 * @throws SMTPException 374 */ 375 protected function mailFrom() 376 { 377 $in = "MAIL FROM:<{$this->message->getFromEmail()}>" . $this->CRLF; 378 $code = $this->pushStack($in); 379 if ($code !== '250') { 380 throw new CodeException('250', $code, array_pop($this->resultStack)); 381 } 382 return $this; 383 } 384 385 /** 386 * SMTP RCPT TO 387 * SUCCESS 250 388 * @return $this 389 * @throws CodeException 390 * @throws SMTPException 391 */ 392 protected function rcptTo() 393 { 394 $to = array_merge( 395 $this->message->getTo(), 396 $this->message->getCc(), 397 $this->message->getBcc() 398 ); 399 foreach ($to as $toEmail=>$_) { 400 $in = "RCPT TO:<" . $toEmail . ">" . $this->CRLF; 401 $code = $this->pushStack($in); 402 if ($code !== '250') { 403 throw new CodeException('250', $code, array_pop($this->resultStack)); 404 } 405 } 406 return $this; 407 } 408 409 /** 410 * SMTP DATA 411 * SUCCESS 354 412 * SUCCESS 250 413 * @return $this 414 * @throws CodeException 415 * @throws SMTPException 416 */ 417 protected function data() 418 { 419 $in = "DATA" . $this->CRLF; 420 $code = $this->pushStack($in); 421 if ($code !== '354') { 422 throw new CodeException('354', $code, array_pop($this->resultStack)); 423 } 424 $in = $this->message->toString(); 425 $code = $this->pushStack($in); 426 if ($code !== '250'){ 427 throw new CodeException('250', $code, array_pop($this->resultStack)); 428 } 429 return $this; 430 } 431 432 /** 433 * SMTP QUIT 434 * SUCCESS 221 435 * @return $this 436 * @throws CodeException 437 * @throws SMTPException 438 */ 439 protected function quit() 440 { 441 $in = "QUIT" . $this->CRLF; 442 $code = $this->pushStack($in); 443 if ($code !== '221'){ 444 throw new CodeException('221', $code, array_pop($this->resultStack)); 445 } 446 return $this; 447 } 448 449 protected function pushStack($string) 450 { 451 $this->commandStack[] = $string; 452 fputs($this->smtp, $string, strlen($string)); 453 $this->logger && $this->logger->debug('Sent: '. $string); 454 return $this->getCode(); 455 } 456 457 /** 458 * get smtp response code 459 * once time has three digital and a space 460 * @return string 461 * @throws SMTPException 462 */ 463 protected function getCode() 464 { 465 while ($str = fgets($this->smtp, 515)) { 466 $this->logger && $this->logger->debug("Got: ". $str); 467 $this->resultStack[] = $str; 468 if(substr($str,3,1) == " ") { 469 $code = substr($str,0,3); 470 return $code; 471 } 472 } 473 throw new SMTPException("SMTP Server did not respond with anything I recognized"); 474 } 475 476} 477 478