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 /** Source Ip */ 31 protected $_sourceIp; 32 33 /** Return an array of params for the Buffer */ 34 abstract protected function _getBufferParams(); 35 36 /** 37 * Creates a new EsmtpTransport using the given I/O buffer. 38 * 39 * @param Swift_Transport_IoBuffer $buf 40 * @param Swift_Events_EventDispatcher $dispatcher 41 */ 42 public function __construct(Swift_Transport_IoBuffer $buf, Swift_Events_EventDispatcher $dispatcher) 43 { 44 $this->_eventDispatcher = $dispatcher; 45 $this->_buffer = $buf; 46 $this->_lookupHostname(); 47 } 48 49 /** 50 * Set the name of the local domain which Swift will identify itself as. 51 * 52 * This should be a fully-qualified domain name and should be truly the domain 53 * you're using. 54 * 55 * If your server doesn't have a domain name, use the IP in square 56 * brackets (i.e. [127.0.0.1]). 57 * 58 * @param string $domain 59 * 60 * @return $this 61 */ 62 public function setLocalDomain($domain) 63 { 64 $this->_domain = $domain; 65 66 return $this; 67 } 68 69 /** 70 * Get the name of the domain Swift will identify as. 71 * 72 * @return string 73 */ 74 public function getLocalDomain() 75 { 76 return $this->_domain; 77 } 78 79 /** 80 * Sets the source IP. 81 * 82 * @param string $source 83 */ 84 public function setSourceIp($source) 85 { 86 $this->_sourceIp = $source; 87 } 88 89 /** 90 * Returns the IP used to connect to the destination. 91 * 92 * @return string 93 */ 94 public function getSourceIp() 95 { 96 return $this->_sourceIp; 97 } 98 99 /** 100 * Start the SMTP connection. 101 */ 102 public function start() 103 { 104 if (!$this->_started) { 105 if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) { 106 $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStarted'); 107 if ($evt->bubbleCancelled()) { 108 return; 109 } 110 } 111 112 try { 113 $this->_buffer->initialize($this->_getBufferParams()); 114 } catch (Swift_TransportException $e) { 115 $this->_throwException($e); 116 } 117 $this->_readGreeting(); 118 $this->_doHeloCommand(); 119 120 if ($evt) { 121 $this->_eventDispatcher->dispatchEvent($evt, 'transportStarted'); 122 } 123 124 $this->_started = true; 125 } 126 } 127 128 /** 129 * Test if an SMTP connection has been established. 130 * 131 * @return bool 132 */ 133 public function isStarted() 134 { 135 return $this->_started; 136 } 137 138 /** 139 * Send the given Message. 140 * 141 * Recipient/sender data will be retrieved from the Message API. 142 * The return value is the number of recipients who were accepted for delivery. 143 * 144 * @param Swift_Mime_Message $message 145 * @param string[] $failedRecipients An array of failures by-reference 146 * 147 * @return int 148 */ 149 public function send(Swift_Mime_Message $message, &$failedRecipients = null) 150 { 151 $sent = 0; 152 $failedRecipients = (array) $failedRecipients; 153 154 if ($evt = $this->_eventDispatcher->createSendEvent($this, $message)) { 155 $this->_eventDispatcher->dispatchEvent($evt, 'beforeSendPerformed'); 156 if ($evt->bubbleCancelled()) { 157 return 0; 158 } 159 } 160 161 if (!$reversePath = $this->_getReversePath($message)) { 162 $this->_throwException(new Swift_TransportException( 163 'Cannot send message without a sender address' 164 ) 165 ); 166 } 167 168 $to = (array) $message->getTo(); 169 $cc = (array) $message->getCc(); 170 $tos = array_merge($to, $cc); 171 $bcc = (array) $message->getBcc(); 172 173 $message->setBcc(array()); 174 175 try { 176 $sent += $this->_sendTo($message, $reversePath, $tos, $failedRecipients); 177 $sent += $this->_sendBcc($message, $reversePath, $bcc, $failedRecipients); 178 } catch (Exception $e) { 179 $message->setBcc($bcc); 180 throw $e; 181 } 182 183 $message->setBcc($bcc); 184 185 if ($evt) { 186 if ($sent == count($to) + count($cc) + count($bcc)) { 187 $evt->setResult(Swift_Events_SendEvent::RESULT_SUCCESS); 188 } elseif ($sent > 0) { 189 $evt->setResult(Swift_Events_SendEvent::RESULT_TENTATIVE); 190 } else { 191 $evt->setResult(Swift_Events_SendEvent::RESULT_FAILED); 192 } 193 $evt->setFailedRecipients($failedRecipients); 194 $this->_eventDispatcher->dispatchEvent($evt, 'sendPerformed'); 195 } 196 197 $message->generateId(); //Make sure a new Message ID is used 198 199 return $sent; 200 } 201 202 /** 203 * Stop the SMTP connection. 204 */ 205 public function stop() 206 { 207 if ($this->_started) { 208 if ($evt = $this->_eventDispatcher->createTransportChangeEvent($this)) { 209 $this->_eventDispatcher->dispatchEvent($evt, 'beforeTransportStopped'); 210 if ($evt->bubbleCancelled()) { 211 return; 212 } 213 } 214 215 try { 216 $this->executeCommand("QUIT\r\n", array(221)); 217 } catch (Swift_TransportException $e) { 218 } 219 220 try { 221 $this->_buffer->terminate(); 222 223 if ($evt) { 224 $this->_eventDispatcher->dispatchEvent($evt, 'transportStopped'); 225 } 226 } catch (Swift_TransportException $e) { 227 $this->_throwException($e); 228 } 229 } 230 $this->_started = false; 231 } 232 233 /** 234 * Register a plugin. 235 * 236 * @param Swift_Events_EventListener $plugin 237 */ 238 public function registerPlugin(Swift_Events_EventListener $plugin) 239 { 240 $this->_eventDispatcher->bindEventListener($plugin); 241 } 242 243 /** 244 * Reset the current mail transaction. 245 */ 246 public function reset() 247 { 248 $this->executeCommand("RSET\r\n", array(250)); 249 } 250 251 /** 252 * Get the IoBuffer where read/writes are occurring. 253 * 254 * @return Swift_Transport_IoBuffer 255 */ 256 public function getBuffer() 257 { 258 return $this->_buffer; 259 } 260 261 /** 262 * Run a command against the buffer, expecting the given response codes. 263 * 264 * If no response codes are given, the response will not be validated. 265 * If codes are given, an exception will be thrown on an invalid response. 266 * 267 * @param string $command 268 * @param int[] $codes 269 * @param string[] $failures An array of failures by-reference 270 * 271 * @return string 272 */ 273 public function executeCommand($command, $codes = array(), &$failures = null) 274 { 275 $failures = (array) $failures; 276 $seq = $this->_buffer->write($command); 277 $response = $this->_getFullResponse($seq); 278 if ($evt = $this->_eventDispatcher->createCommandEvent($this, $command, $codes)) { 279 $this->_eventDispatcher->dispatchEvent($evt, 'commandSent'); 280 } 281 $this->_assertResponseCode($response, $codes); 282 283 return $response; 284 } 285 286 /** Read the opening SMTP greeting */ 287 protected function _readGreeting() 288 { 289 $this->_assertResponseCode($this->_getFullResponse(0), array(220)); 290 } 291 292 /** Send the HELO welcome */ 293 protected function _doHeloCommand() 294 { 295 $this->executeCommand( 296 sprintf("HELO %s\r\n", $this->_domain), array(250) 297 ); 298 } 299 300 /** Send the MAIL FROM command */ 301 protected function _doMailFromCommand($address) 302 { 303 $this->executeCommand( 304 sprintf("MAIL FROM:<%s>\r\n", $address), array(250) 305 ); 306 } 307 308 /** Send the RCPT TO command */ 309 protected function _doRcptToCommand($address) 310 { 311 $this->executeCommand( 312 sprintf("RCPT TO:<%s>\r\n", $address), array(250, 251, 252) 313 ); 314 } 315 316 /** Send the DATA command */ 317 protected function _doDataCommand() 318 { 319 $this->executeCommand("DATA\r\n", array(354)); 320 } 321 322 /** Stream the contents of the message over the buffer */ 323 protected function _streamMessage(Swift_Mime_Message $message) 324 { 325 $this->_buffer->setWriteTranslations(array("\r\n." => "\r\n..")); 326 try { 327 $message->toByteStream($this->_buffer); 328 $this->_buffer->flushBuffers(); 329 } catch (Swift_TransportException $e) { 330 $this->_throwException($e); 331 } 332 $this->_buffer->setWriteTranslations(array()); 333 $this->executeCommand("\r\n.\r\n", array(250)); 334 } 335 336 /** Determine the best-use reverse path for this message */ 337 protected function _getReversePath(Swift_Mime_Message $message) 338 { 339 $return = $message->getReturnPath(); 340 $sender = $message->getSender(); 341 $from = $message->getFrom(); 342 $path = null; 343 if (!empty($return)) { 344 $path = $return; 345 } elseif (!empty($sender)) { 346 // Don't use array_keys 347 reset($sender); // Reset Pointer to first pos 348 $path = key($sender); // Get key 349 } elseif (!empty($from)) { 350 reset($from); // Reset Pointer to first pos 351 $path = key($from); // Get key 352 } 353 354 return $path; 355 } 356 357 /** Throw a TransportException, first sending it to any listeners */ 358 protected function _throwException(Swift_TransportException $e) 359 { 360 if ($evt = $this->_eventDispatcher->createTransportExceptionEvent($this, $e)) { 361 $this->_eventDispatcher->dispatchEvent($evt, 'exceptionThrown'); 362 if (!$evt->bubbleCancelled()) { 363 throw $e; 364 } 365 } else { 366 throw $e; 367 } 368 } 369 370 /** Throws an Exception if a response code is incorrect */ 371 protected function _assertResponseCode($response, $wanted) 372 { 373 list($code) = sscanf($response, '%3d'); 374 $valid = (empty($wanted) || in_array($code, $wanted)); 375 376 if ($evt = $this->_eventDispatcher->createResponseEvent($this, $response, 377 $valid)) { 378 $this->_eventDispatcher->dispatchEvent($evt, 'responseReceived'); 379 } 380 381 if (!$valid) { 382 $this->_throwException( 383 new Swift_TransportException( 384 'Expected response code '.implode('/', $wanted).' but got code '. 385 '"'.$code.'", with message "'.$response.'"', 386 $code) 387 ); 388 } 389 } 390 391 /** Get an entire multi-line response using its sequence number */ 392 protected function _getFullResponse($seq) 393 { 394 $response = ''; 395 try { 396 do { 397 $line = $this->_buffer->readLine($seq); 398 $response .= $line; 399 } while (null !== $line && false !== $line && ' ' != $line[3]); 400 } catch (Swift_TransportException $e) { 401 $this->_throwException($e); 402 } catch (Swift_IoException $e) { 403 $this->_throwException( 404 new Swift_TransportException( 405 $e->getMessage()) 406 ); 407 } 408 409 return $response; 410 } 411 412 /** Send an email to the given recipients from the given reverse path */ 413 private function _doMailTransaction($message, $reversePath, array $recipients, array &$failedRecipients) 414 { 415 $sent = 0; 416 $this->_doMailFromCommand($reversePath); 417 foreach ($recipients as $forwardPath) { 418 try { 419 $this->_doRcptToCommand($forwardPath); 420 ++$sent; 421 } catch (Swift_TransportException $e) { 422 $failedRecipients[] = $forwardPath; 423 } 424 } 425 426 if ($sent != 0) { 427 $this->_doDataCommand(); 428 $this->_streamMessage($message); 429 } else { 430 $this->reset(); 431 } 432 433 return $sent; 434 } 435 436 /** Send a message to the given To: recipients */ 437 private function _sendTo(Swift_Mime_Message $message, $reversePath, array $to, array &$failedRecipients) 438 { 439 if (empty($to)) { 440 return 0; 441 } 442 443 return $this->_doMailTransaction($message, $reversePath, array_keys($to), 444 $failedRecipients); 445 } 446 447 /** Send a message to all Bcc: recipients */ 448 private function _sendBcc(Swift_Mime_Message $message, $reversePath, array $bcc, array &$failedRecipients) 449 { 450 $sent = 0; 451 foreach ($bcc as $forwardPath => $name) { 452 $message->setBcc(array($forwardPath => $name)); 453 $sent += $this->_doMailTransaction( 454 $message, $reversePath, array($forwardPath), $failedRecipients 455 ); 456 } 457 458 return $sent; 459 } 460 461 /** Try to determine the hostname of the server this is run on */ 462 private function _lookupHostname() 463 { 464 if (!empty($_SERVER['SERVER_NAME']) && $this->_isFqdn($_SERVER['SERVER_NAME'])) { 465 $this->_domain = $_SERVER['SERVER_NAME']; 466 } elseif (!empty($_SERVER['SERVER_ADDR'])) { 467 // Set the address literal tag (See RFC 5321, section: 4.1.3) 468 if (false === strpos($_SERVER['SERVER_ADDR'], ':')) { 469 $prefix = ''; // IPv4 addresses are not tagged. 470 } else { 471 $prefix = 'IPv6:'; // Adding prefix in case of IPv6. 472 } 473 474 $this->_domain = sprintf('[%s%s]', $prefix, $_SERVER['SERVER_ADDR']); 475 } 476 } 477 478 /** Determine is the $hostname is a fully-qualified name */ 479 private function _isFqdn($hostname) 480 { 481 // We could do a really thorough check, but there's really no point 482 if (false !== $dotPos = strpos($hostname, '.')) { 483 return ($dotPos > 0) && ($dotPos != strlen($hostname) - 1); 484 } 485 486 return false; 487 } 488 489 /** 490 * Destructor. 491 */ 492 public function __destruct() 493 { 494 try { 495 $this->stop(); 496 } catch (Exception $e) { 497 } 498 } 499} 500