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 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 * $this->CRLF 66 * @var string 67 */ 68 protected $CRLF = "\r\n"; 69 70 /** 71 * @var Message 72 */ 73 protected $message; 74 75 /** 76 * @var LoggerInterface - Used to make things prettier than self::$logger 77 */ 78 protected $logger; 79 80 /** 81 * Stack of all commands issued to SMTP 82 * @var array 83 */ 84 protected $commandStack = array(); 85 86 /** 87 * Stack of all results issued to SMTP 88 * @var array 89 */ 90 protected $resultStack = array(); 91 92 public function __construct(LoggerInterface $logger=null) 93 { 94 $this->logger = $logger; 95 } 96 97 /** 98 * set server and port 99 * @param string $host server 100 * @param int $port port 101 * @param string $secure ssl tls 102 * @return $this 103 */ 104 public function setServer($host, $port, $secure=null) 105 { 106 $this->host = $host; 107 $this->port = $port; 108 $this->secure = $secure; 109 if(!$this->ehlo) $this->ehlo = $host; 110 $this->logger && $this->logger->debug("Set: the server"); 111 return $this; 112 } 113 114 /** 115 * auth with server 116 * @param string $username 117 * @param string $password 118 * @return $this 119 */ 120 public function setAuth($username, $password){ 121 $this->username = $username; 122 $this->password = $password; 123 $this->logger && $this->logger->debug("Set: the auth"); 124 return $this; 125 } 126 127 /** 128 * set the EHLO message 129 * @param $ehlo 130 * @return $this 131 */ 132 public function setEhlo($ehlo){ 133 $this->ehlo = $ehlo; 134 return $this; 135 } 136 137 /** 138 * Send the message 139 * 140 * @param Message $message 141 * @return $this 142 * @throws CodeException 143 * @throws CryptoException 144 * @throws SMTPException 145 */ 146 public function send(Message $message){ 147 $this->logger && $this->logger->debug('Set: a message will be sent'); 148 $this->message = $message; 149 $this->connect() 150 ->ehlo(); 151 152 if ($this->secure === 'tls'){ 153 $this->starttls() 154 ->ehlo(); 155 } 156 $this->authLogin() 157 ->mailFrom() 158 ->rcptTo() 159 ->data() 160 ->quit(); 161 return fclose($this->smtp); 162 } 163 164 /** 165 * connect the server 166 * SUCCESS 220 167 * @return $this 168 * @throws CodeException 169 * @throws SMTPException 170 */ 171 protected function connect(){ 172 $this->logger && $this->logger->debug("Connecting to {$this->host} at {$this->port}"); 173 $host = ($this->secure == 'ssl') ? 'ssl://' . $this->host : $this->host; 174 $this->smtp = @fsockopen($host, $this->port); 175 //set block mode 176 // stream_set_blocking($this->smtp, 1); 177 if (!$this->smtp){ 178 throw new SMTPException("Could not open SMTP Port."); 179 } 180 $code = $this->getCode(); 181 if ($code !== '220'){ 182 throw new CodeException('220', $code, array_pop($this->resultStack)); 183 } 184 return $this; 185 } 186 187 /** 188 * SMTP STARTTLS 189 * SUCCESS 220 190 * @return $this 191 * @throws CodeException 192 * @throws CryptoException 193 * @throws SMTPException 194 */ 195 protected function starttls(){ 196 $in = "STARTTLS" . $this->CRLF; 197 $code = $this->pushStack($in); 198 if ($code !== '220'){ 199 throw new CodeException('220', $code, array_pop($this->resultStack)); 200 } 201 if(!stream_socket_enable_crypto($this->smtp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { 202 throw new CryptoException("Start TLS failed to enable crypto"); 203 } 204 return $this; 205 } 206 207 /** 208 * SMTP EHLO 209 * SUCCESS 250 210 * @return $this 211 * @throws CodeException 212 * @throws SMTPException 213 */ 214 protected function ehlo(){ 215 $in = "EHLO " . $this->ehlo . $this->CRLF; 216 $code = $this->pushStack($in); 217 if ($code !== '250'){ 218 throw new CodeException('250', $code, array_pop($this->resultStack)); 219 } 220 return $this; 221 } 222 223 /** 224 * SMTP AUTH LOGIN 225 * SUCCESS 334 226 * SUCCESS 334 227 * SUCCESS 235 228 * @return $this 229 * @throws CodeException 230 * @throws SMTPException 231 */ 232 protected function authLogin() 233 { 234 if ($this->username === null && $this->password === null) { 235 // Unless the user has specifically set a username/password 236 // Do not try to authorize. 237 return $this; 238 } 239 240 $in = "AUTH LOGIN" . $this->CRLF; 241 $code = $this->pushStack($in); 242 if ($code !== '334'){ 243 throw new CodeException('334', $code, array_pop($this->resultStack)); 244 } 245 $in = base64_encode($this->username) . $this->CRLF; 246 $code = $this->pushStack($in); 247 if ($code !== '334'){ 248 throw new CodeException('334', $code, array_pop($this->resultStack)); 249 } 250 $in = base64_encode($this->password) . $this->CRLF; 251 $code = $this->pushStack($in); 252 if ($code !== '235'){ 253 throw new CodeException('235', $code, array_pop($this->resultStack)); 254 } 255 return $this; 256 } 257 258 /** 259 * SMTP MAIL FROM 260 * SUCCESS 250 261 * @return $this 262 * @throws CodeException 263 * @throws SMTPException 264 */ 265 protected function mailFrom(){ 266 $in = "MAIL FROM:<{$this->message->getFromEmail()}>" . $this->CRLF; 267 $code = $this->pushStack($in); 268 if ($code !== '250') { 269 throw new CodeException('250', $code, array_pop($this->resultStack)); 270 } 271 return $this; 272 } 273 274 /** 275 * SMTP RCPT TO 276 * SUCCESS 250 277 * @return $this 278 * @throws CodeException 279 * @throws SMTPException 280 */ 281 protected function rcptTo(){ 282 foreach ($this->message->getTo() as $toEmail) { 283 $in = "RCPT TO:<" . $toEmail . ">" . $this->CRLF; 284 $code = $this->pushStack($in); 285 if ($code !== '250') { 286 throw new CodeException('250', $code, array_pop($this->resultStack)); 287 } 288 } 289 return $this; 290 } 291 292 /** 293 * SMTP DATA 294 * SUCCESS 354 295 * SUCCESS 250 296 * @return $this 297 * @throws CodeException 298 * @throws SMTPException 299 */ 300 protected function data(){ 301 $in = "DATA" . $this->CRLF; 302 $code = $this->pushStack($in); 303 if ($code !== '354') { 304 throw new CodeException('354', $code, array_pop($this->resultStack)); 305 } 306 $in = $this->message->toString(); 307 $code = $this->pushStack($in); 308 if ($code !== '250'){ 309 throw new CodeException('250', $code, array_pop($this->resultStack)); 310 } 311 return $this; 312 } 313 314 /** 315 * SMTP QUIT 316 * SUCCESS 221 317 * @return $this 318 * @throws CodeException 319 * @throws SMTPException 320 */ 321 protected function quit(){ 322 $in = "QUIT" . $this->CRLF; 323 $code = $this->pushStack($in); 324 if ($code !== '221'){ 325 throw new CodeException('221', $code, array_pop($this->resultStack)); 326 } 327 return $this; 328 } 329 330 protected function pushStack($string) 331 { 332 $this->commandStack[] = $string; 333 fputs($this->smtp, $string, strlen($string)); 334 $this->logger && $this->logger->debug('Sent: '. $string); 335 return $this->getCode(); 336 } 337 338 /** 339 * get smtp response code 340 * once time has three digital and a space 341 * @return string 342 * @throws SMTPException 343 */ 344 protected function getCode() { 345 while ($str = fgets($this->smtp, 515)) { 346 $this->logger && $this->logger->debug("Got: ". $str); 347 $this->resultStack[] = $str; 348 if(substr($str,3,1) == " ") { 349 $code = substr($str,0,3); 350 return $code; 351 } 352 } 353 throw new SMTPException("SMTP Server did not respond with anything I recognized"); 354 } 355 356} 357 358