1<?php 2 3/* 4 * This file is part of SwiftMailer. 5 * (c) 2004-2009 Chris Corbyn 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11/** 12 * Sends Messages over SMTP. 13 * 14 * @author Chris Corbyn 15 */ 16abstract class Swift_Transport_AbstractSmtpTransport implements Swift_Transport 17{ 18 /** Input-Output buffer for sending/receiving SMTP commands and responses */ 19 protected $buffer; 20 21 /** Connection status */ 22 protected $started = false; 23 24 /** The domain name to use in HELO command */ 25 protected $domain = '[127.0.0.1]'; 26 27 /** The event dispatching layer */ 28 protected $eventDispatcher; 29 30 protected $addressEncoder; 31 32 /** Whether the PIPELINING SMTP extension is enabled (RFC 2920) */ 33 protected $pipelining = null; 34 35 /** The pipelined commands waiting for response */ 36 protected $pipeline = []; 37 38 /** Source Ip */ 39 protected $sourceIp; 40 41 /** Return an array of params for the Buffer */ 42 abstract protected function getBufferParams(); 43 44 /** 45 * Creates a new EsmtpTransport using the given I/O buffer. 46 * 47 * @param string $localDomain 48 */ 49 public function __construct(Swift_Transport_IoBuffer $buf, Swift_Events_EventDispatcher $dispatcher, $localDomain = '127.0.0.1', Swift_AddressEncoder $addressEncoder = null) 50 { 51 $this->buffer = $buf; 52 $this->eventDispatcher = $dispatcher; 53 $this->addressEncoder = $addressEncoder ?? new Swift_AddressEncoder_IdnAddressEncoder(); 54 $this->setLocalDomain($localDomain); 55 } 56 57 /** 58 * Set the name of the local domain which Swift will identify itself as. 59 * 60 * This should be a fully-qualified domain name and should be truly the domain 61 * you're using. 62 * 63 * If your server does not have a domain name, use the IP address. This will 64 * automatically be wrapped in square brackets as described in RFC 5321, 65 * section 4.1.3. 66 * 67 * @param string $domain 68 * 69 * @return $this 70 */ 71 public function setLocalDomain($domain) 72 { 73 if ('[' !== substr($domain, 0, 1)) { 74 if (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { 75 $domain = '['.$domain.']'; 76 } elseif (filter_var($domain, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { 77 $domain = '[IPv6:'.$domain.']'; 78 } 79 } 80 81 $this->domain = $domain; 82 83 return $this; 84 } 85 86 /** 87 * Get the name of the domain Swift will identify as. 88 * 89 * If an IP address was specified, this will be returned wrapped in square 90 * brackets as described in RFC 5321, section 4.1.3. 91 * 92 * @return string 93 */ 94 public function getLocalDomain() 95 { 96 return $this->domain; 97 } 98 99 /** 100 * Sets the source IP. 101 * 102 * @param string $source 103 */ 104 public function setSourceIp($source) 105 { 106 $this->sourceIp = $source; 107 } 108 109 /** 110 * Returns the IP used to connect to the destination. 111 * 112 * @return string 113 */ 114 public function getSourceIp() 115 { 116 return $this->sourceIp; 117 } 118 119 public function setAddressEncoder(Swift_AddressEncoder $addressEncoder) 120 { 121 $this->addressEncoder = $addressEncoder; 122 } 123 124 public function getAddressEncoder() 125 { 126 return $this->addressEncoder; 127 } 128 129 /** 130 * Start the SMTP connection. 131 */ 132 public function start() 133 { 134 if (!$this->started) { 135 if ($evt = $this->eventDispatcher->createTransportChangeEvent($this)) { 136 $this->eventDispatcher->dispatchEvent($evt, 'beforeTransportStarted'); 137 if ($evt->bubbleCancelled()) { 138 return; 139 } 140 } 141 142 try { 143 $this->buffer->initialize($this->getBufferParams()); 144 } catch (Swift_TransportException $e) { 145 $this->throwException($e); 146 } 147 $this->readGreeting(); 148 $this->doHeloCommand(); 149 150 if ($evt) { 151 $this->eventDispatcher->dispatchEvent($evt, 'transportStarted'); 152 } 153 154 $this->started = true; 155 } 156 } 157 158 /** 159 * Test if an SMTP connection has been established. 160 * 161 * @return bool 162 */ 163 public function isStarted() 164 { 165 return $this->started; 166 } 167 168 /** 169 * Send the given Message. 170 * 171 * Recipient/sender data will be retrieved from the Message API. 172 * The return value is the number of recipients who were accepted for delivery. 173 * 174 * @param string[] $failedRecipients An array of failures by-reference 175 * 176 * @return int 177 */ 178 public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null) 179 { 180 if (!$this->isStarted()) { 181 $this->start(); 182 } 183 184 $sent = 0; 185 $failedRecipients = (array) $failedRecipients; 186 187 if ($evt = $this->eventDispatcher->createSendEvent($this, $message)) { 188 $this->eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); 189 if ($evt->bubbleCancelled()) { 190 return 0; 191 } 192 } 193 194 if (!$reversePath = $this->getReversePath($message)) { 195 $this->throwException(new Swift_TransportException('Cannot send message without a sender address')); 196 } 197 198 $to = (array) $message->getTo(); 199 $cc = (array) $message->getCc(); 200 $bcc = (array) $message->getBcc(); 201 $tos = array_merge($to, $cc, $bcc); 202 203 $message->setBcc([]); 204 205 try { 206 $sent += $this->sendTo($message, $reversePath, $tos, $failedRecipients); 207 } finally { 208 $message->setBcc($bcc); 209 } 210 211 if ($evt) { 212 if ($sent == \count($to) + \count($cc) + \count($bcc)) { 213 $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); 214 } elseif ($sent > 0) { 215 $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE); 216 } else { 217 $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED); 218 } 219 $evt->setFailedRecipients($failedRecipients); 220 $this->eventDispatcher->dispatchEvent($evt, 'sendPerformed'); 221 } 222 223 $message->generateId(); //Make sure a new Message ID is used 224 225 return $sent; 226 } 227 228 /** 229 * Stop the SMTP connection. 230 */ 231 public function stop() 232 { 233 if ($this->started) { 234 if ($evt = $this->eventDispatcher->createTransportChangeEvent($this)) { 235 $this->eventDispatcher->dispatchEvent($evt, 'beforeTransportStopped'); 236 if ($evt->bubbleCancelled()) { 237 return; 238 } 239 } 240 241 try { 242 $this->executeCommand("QUIT\r\n", [221]); 243 } catch (Swift_TransportException $e) { 244 } 245 246 try { 247 $this->buffer->terminate(); 248 249 if ($evt) { 250 $this->eventDispatcher->dispatchEvent($evt, 'transportStopped'); 251 } 252 } catch (Swift_TransportException $e) { 253 $this->throwException($e); 254 } 255 } 256 $this->started = false; 257 } 258 259 /** 260 * {@inheritdoc} 261 */ 262 public function ping() 263 { 264 try { 265 if (!$this->isStarted()) { 266 $this->start(); 267 } 268 269 $this->executeCommand("NOOP\r\n", [250]); 270 } catch (Swift_TransportException $e) { 271 try { 272 $this->stop(); 273 } catch (Swift_TransportException $e) { 274 } 275 276 return false; 277 } 278 279 return true; 280 } 281 282 /** 283 * Register a plugin. 284 */ 285 public function registerPlugin(Swift_Events_EventListener $plugin) 286 { 287 $this->eventDispatcher->bindEventListener($plugin); 288 } 289 290 /** 291 * Reset the current mail transaction. 292 */ 293 public function reset() 294 { 295 $this->executeCommand("RSET\r\n", [250], $failures, true); 296 } 297 298 /** 299 * Get the IoBuffer where read/writes are occurring. 300 * 301 * @return Swift_Transport_IoBuffer 302 */ 303 public function getBuffer() 304 { 305 return $this->buffer; 306 } 307 308 /** 309 * Run a command against the buffer, expecting the given response codes. 310 * 311 * If no response codes are given, the response will not be validated. 312 * If codes are given, an exception will be thrown on an invalid response. 313 * If the command is RCPT TO, and the pipeline is non-empty, no exception 314 * will be thrown; instead the failing address is added to $failures. 315 * 316 * @param string $command 317 * @param int[] $codes 318 * @param string[] $failures An array of failures by-reference 319 * @param bool $pipeline Do not wait for response 320 * @param string $address the address, if command is RCPT TO 321 * 322 * @return string|null The server response, or null if pipelining is enabled 323 */ 324 public function executeCommand($command, $codes = [], &$failures = null, $pipeline = false, $address = null) 325 { 326 $failures = (array) $failures; 327 $seq = $this->buffer->write($command); 328 if ($evt = $this->eventDispatcher->createCommandEvent($this, $command, $codes)) { 329 $this->eventDispatcher->dispatchEvent($evt, 'commandSent'); 330 } 331 332 $this->pipeline[] = [$command, $seq, $codes, $address]; 333 334 if ($pipeline && $this->pipelining) { 335 return null; 336 } 337 338 $response = null; 339 340 while ($this->pipeline) { 341 list($command, $seq, $codes, $address) = array_shift($this->pipeline); 342 $response = $this->getFullResponse($seq); 343 try { 344 $this->assertResponseCode($response, $codes); 345 } catch (Swift_TransportException $e) { 346 if ($this->pipeline && $address) { 347 $failures[] = $address; 348 } else { 349 $this->throwException($e); 350 } 351 } 352 } 353 354 return $response; 355 } 356 357 /** Read the opening SMTP greeting */ 358 protected function readGreeting() 359 { 360 $this->assertResponseCode($this->getFullResponse(0), [220]); 361 } 362 363 /** Send the HELO welcome */ 364 protected function doHeloCommand() 365 { 366 $this->executeCommand( 367 sprintf("HELO %s\r\n", $this->domain), [250] 368 ); 369 } 370 371 /** Send the MAIL FROM command */ 372 protected function doMailFromCommand($address) 373 { 374 $address = $this->addressEncoder->encodeString($address); 375 $this->executeCommand( 376 sprintf("MAIL FROM:<%s>\r\n", $address), [250], $failures, true 377 ); 378 } 379 380 /** Send the RCPT TO command */ 381 protected function doRcptToCommand($address) 382 { 383 $address = $this->addressEncoder->encodeString($address); 384 $this->executeCommand( 385 sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252], $failures, true, $address 386 ); 387 } 388 389 /** Send the DATA command */ 390 protected function doDataCommand(&$failedRecipients) 391 { 392 $this->executeCommand("DATA\r\n", [354], $failedRecipients); 393 } 394 395 /** Stream the contents of the message over the buffer */ 396 protected function streamMessage(Swift_Mime_SimpleMessage $message) 397 { 398 $this->buffer->setWriteTranslations(["\r\n." => "\r\n.."]); 399 try { 400 $message->toByteStream($this->buffer); 401 $this->buffer->flushBuffers(); 402 } catch (Swift_TransportException $e) { 403 $this->throwException($e); 404 } 405 $this->buffer->setWriteTranslations([]); 406 $this->executeCommand("\r\n.\r\n", [250]); 407 } 408 409 /** Determine the best-use reverse path for this message */ 410 protected function getReversePath(Swift_Mime_SimpleMessage $message) 411 { 412 $return = $message->getReturnPath(); 413 $sender = $message->getSender(); 414 $from = $message->getFrom(); 415 $path = null; 416 if (!empty($return)) { 417 $path = $return; 418 } elseif (!empty($sender)) { 419 // Don't use array_keys 420 reset($sender); // Reset Pointer to first pos 421 $path = key($sender); // Get key 422 } elseif (!empty($from)) { 423 reset($from); // Reset Pointer to first pos 424 $path = key($from); // Get key 425 } 426 427 return $path; 428 } 429 430 /** Throw a TransportException, first sending it to any listeners */ 431 protected function throwException(Swift_TransportException $e) 432 { 433 if ($evt = $this->eventDispatcher->createTransportExceptionEvent($this, $e)) { 434 $this->eventDispatcher->dispatchEvent($evt, 'exceptionThrown'); 435 if (!$evt->bubbleCancelled()) { 436 throw $e; 437 } 438 } else { 439 throw $e; 440 } 441 } 442 443 /** Throws an Exception if a response code is incorrect */ 444 protected function assertResponseCode($response, $wanted) 445 { 446 if (!$response) { 447 $this->throwException(new Swift_TransportException('Expected response code '.implode('/', $wanted).' but got an empty response')); 448 } 449 450 list($code) = sscanf($response, '%3d'); 451 $valid = (empty($wanted) || \in_array($code, $wanted)); 452 453 if ($evt = $this->eventDispatcher->createResponseEvent($this, $response, 454 $valid)) { 455 $this->eventDispatcher->dispatchEvent($evt, 'responseReceived'); 456 } 457 458 if (!$valid) { 459 $this->throwException(new Swift_TransportException('Expected response code '.implode('/', $wanted).' but got code "'.$code.'", with message "'.$response.'"', $code)); 460 } 461 } 462 463 /** Get an entire multi-line response using its sequence number */ 464 protected function getFullResponse($seq) 465 { 466 $response = ''; 467 try { 468 do { 469 $line = $this->buffer->readLine($seq); 470 $response .= $line; 471 } while (null !== $line && false !== $line && ' ' != $line[3]); 472 } catch (Swift_TransportException $e) { 473 $this->throwException($e); 474 } catch (Swift_IoException $e) { 475 $this->throwException(new Swift_TransportException($e->getMessage(), 0, $e)); 476 } 477 478 return $response; 479 } 480 481 /** Send an email to the given recipients from the given reverse path */ 482 private function doMailTransaction($message, $reversePath, array $recipients, array &$failedRecipients) 483 { 484 $sent = 0; 485 $this->doMailFromCommand($reversePath); 486 foreach ($recipients as $forwardPath) { 487 try { 488 $this->doRcptToCommand($forwardPath); 489 ++$sent; 490 } catch (Swift_TransportException $e) { 491 $failedRecipients[] = $forwardPath; 492 } catch (Swift_AddressEncoderException $e) { 493 $failedRecipients[] = $forwardPath; 494 } 495 } 496 497 if (0 != $sent) { 498 $sent += \count($failedRecipients); 499 $this->doDataCommand($failedRecipients); 500 $sent -= \count($failedRecipients); 501 502 $this->streamMessage($message); 503 } else { 504 $this->reset(); 505 } 506 507 return $sent; 508 } 509 510 /** Send a message to the given To: recipients */ 511 private function sendTo(Swift_Mime_SimpleMessage $message, $reversePath, array $to, array &$failedRecipients) 512 { 513 if (empty($to)) { 514 return 0; 515 } 516 517 return $this->doMailTransaction($message, $reversePath, array_keys($to), 518 $failedRecipients); 519 } 520 521 /** 522 * Destructor. 523 */ 524 public function __destruct() 525 { 526 try { 527 $this->stop(); 528 } catch (Exception $e) { 529 } 530 } 531 532 public function __sleep() 533 { 534 throw new \BadMethodCallException('Cannot serialize '.__CLASS__); 535 } 536 537 public function __wakeup() 538 { 539 throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); 540 } 541} 542