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