Swift.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. <?php
  2. /**
  3. * Swift Mailer Core Component.
  4. * Please read the LICENSE file
  5. * @copyright Chris Corbyn <chris@w3style.co.uk>
  6. * @author Chris Corbyn <chris@w3style.co.uk>
  7. * @package Swift
  8. * @version 3.3.2
  9. * @license GNU Lesser General Public License
  10. */
  11. require_once dirname(__FILE__) . "/Swift/ClassLoader.php";
  12. Swift_ClassLoader::load("Swift_LogContainer");
  13. Swift_ClassLoader::load("Swift_ConnectionBase");
  14. Swift_ClassLoader::load("Swift_BadResponseException");
  15. Swift_ClassLoader::load("Swift_Cache");
  16. Swift_ClassLoader::load("Swift_CacheFactory");
  17. Swift_ClassLoader::load("Swift_Message");
  18. Swift_ClassLoader::load("Swift_RecipientList");
  19. Swift_ClassLoader::load("Swift_BatchMailer");
  20. Swift_ClassLoader::load("Swift_Events");
  21. Swift_ClassLoader::load("Swift_Events_Listener");
  22. /**
  23. * Swift is the central component in the Swift library.
  24. * @package Swift
  25. * @author Chris Corbyn <chris@w3style.co.uk>
  26. * @version 3.3.2
  27. */
  28. class Swift
  29. {
  30. /**
  31. * The version number.
  32. */
  33. const VERSION = "3.3.2";
  34. /**
  35. * Constant to flag Swift not to try and connect upon instantiation
  36. */
  37. const NO_START = 2;
  38. /**
  39. * Constant to tell Swift not to perform the standard SMTP handshake upon connect
  40. */
  41. const NO_HANDSHAKE = 4;
  42. /**
  43. * Constant to ask Swift to start logging
  44. */
  45. const ENABLE_LOGGING = 8;
  46. /**
  47. * Constant to prevent postConnect() being run in the connection
  48. */
  49. const NO_POST_CONNECT = 16;
  50. /**
  51. * The connection object currently active
  52. * @var Swift_Connection
  53. */
  54. public $connection = null;
  55. /**
  56. * The domain name of this server (should technically be a FQDN)
  57. * @var string
  58. */
  59. protected $domain = null;
  60. /**
  61. * Flags to change the behaviour of Swift
  62. * @var int
  63. */
  64. protected $options;
  65. /**
  66. * Loaded plugins, separated into containers according to roles
  67. * @var array
  68. */
  69. protected $listeners = array();
  70. /**
  71. * Constructor
  72. * @param Swift_Connection The connection object to deal with I/O
  73. * @param string The domain name of this server (the client) as a FQDN
  74. * @param int Optional flags
  75. * @throws Swift_ConnectionException If a connection cannot be established or the connection is behaving incorrectly
  76. */
  77. public function __construct(Swift_Connection $conn, $domain=false, $options=null)
  78. {
  79. $this->initializeEventListenerContainer();
  80. $this->setOptions($options);
  81. $log = Swift_LogContainer::getLog();
  82. if ($this->hasOption(self::ENABLE_LOGGING) && !$log->isEnabled())
  83. {
  84. $log->setLogLevel(Swift_Log::LOG_NETWORK);
  85. }
  86. if (!$domain) $domain = !empty($_SERVER["SERVER_ADDR"]) ? "[" . $_SERVER["SERVER_ADDR"] . "]" : "localhost.localdomain";
  87. $this->setDomain($domain);
  88. $this->connection = $conn;
  89. if ($conn && !$this->hasOption(self::NO_START))
  90. {
  91. if ($log->hasLevel(Swift_Log::LOG_EVERYTHING)) $log->add("Trying to connect...", Swift_Log::NORMAL);
  92. $this->connect();
  93. }
  94. }
  95. /**
  96. * Populate the listeners array with the defined listeners ready for plugins
  97. */
  98. protected function initializeEventListenerContainer()
  99. {
  100. Swift_ClassLoader::load("Swift_Events_ListenerMapper");
  101. foreach (Swift_Events_ListenerMapper::getMap() as $interface => $method)
  102. {
  103. if (!isset($this->listeners[$interface]))
  104. $this->listeners[$interface] = array();
  105. }
  106. }
  107. /**
  108. * Add a new plugin to Swift
  109. * Plugins must implement one or more event listeners
  110. * @param Swift_Events_Listener The plugin to load
  111. */
  112. public function attachPlugin(Swift_Events_Listener $plugin, $id)
  113. {
  114. foreach (array_keys($this->listeners) as $key)
  115. {
  116. $listener = "Swift_Events_" . $key;
  117. Swift_ClassLoader::load($listener);
  118. if ($plugin instanceof $listener) $this->listeners[$key][$id] = $plugin;
  119. }
  120. }
  121. /**
  122. * Get an attached plugin if it exists
  123. * @param string The id of the plugin
  124. * @return Swift_Event_Listener
  125. */
  126. public function getPlugin($id)
  127. {
  128. foreach ($this->listeners as $type => $arr)
  129. {
  130. if (isset($arr[$id])) return $this->listeners[$type][$id];
  131. }
  132. return null; //If none found
  133. }
  134. /**
  135. * Remove a plugin attached under the ID of $id
  136. * @param string The ID of the plugin
  137. */
  138. public function removePlugin($id)
  139. {
  140. foreach ($this->listeners as $type => $arr)
  141. {
  142. if (isset($arr[$id]))
  143. {
  144. $this->listeners[$type][$id] = null;
  145. unset($this->listeners[$type][$id]);
  146. }
  147. }
  148. }
  149. /**
  150. * Send a new type of event to all objects which are listening for it
  151. * @param Swift_Events The event to send
  152. * @param string The type of event
  153. */
  154. public function notifyListeners($e, $type)
  155. {
  156. Swift_ClassLoader::load("Swift_Events_ListenerMapper");
  157. if (!empty($this->listeners[$type]) && $notifyMethod = Swift_Events_ListenerMapper::getNotifyMethod($type))
  158. {
  159. $e->setSwift($this);
  160. foreach ($this->listeners[$type] as $k => $listener)
  161. {
  162. $listener->$notifyMethod($e);
  163. }
  164. }
  165. else $e = null;
  166. }
  167. /**
  168. * Check if an option flag has been set
  169. * @param string Option name
  170. * @return boolean
  171. */
  172. public function hasOption($option)
  173. {
  174. return ($this->options & $option);
  175. }
  176. /**
  177. * Adjust the options flags
  178. * E.g. $obj->setOptions(Swift::NO_START | Swift::NO_HANDSHAKE)
  179. * @param int The bits to set
  180. */
  181. public function setOptions($options)
  182. {
  183. $this->options = (int) $options;
  184. }
  185. /**
  186. * Get the current options set (as bits)
  187. * @return int
  188. */
  189. public function getOptions()
  190. {
  191. return (int) $this->options;
  192. }
  193. /**
  194. * Set the FQDN of this server as it will identify itself
  195. * @param string The FQDN of the server
  196. */
  197. public function setDomain($name)
  198. {
  199. $this->domain = (string) $name;
  200. }
  201. /**
  202. * Attempt to establish a connection with the service
  203. * @throws Swift_ConnectionException If the connection cannot be established or behaves oddly
  204. */
  205. public function connect()
  206. {
  207. $this->connection->start();
  208. $greeting = $this->command("", 220);
  209. if (!$this->hasOption(self::NO_HANDSHAKE))
  210. {
  211. $this->handshake($greeting);
  212. }
  213. Swift_ClassLoader::load("Swift_Events_ConnectEvent");
  214. $this->notifyListeners(new Swift_Events_ConnectEvent($this->connection), "ConnectListener");
  215. }
  216. /**
  217. * Disconnect from the MTA
  218. * @throws Swift_ConnectionException If the connection will not stop
  219. */
  220. public function disconnect()
  221. {
  222. $this->command("QUIT");
  223. $this->connection->stop();
  224. Swift_ClassLoader::load("Swift_Events_DisconnectEvent");
  225. $this->notifyListeners(new Swift_Events_DisconnectEvent($this->connection), "DisconnectListener");
  226. }
  227. /**
  228. * Throws an exception if the response code wanted does not match the one returned
  229. * @param Swift_Event_ResponseEvent The full response from the service
  230. * @param int The 3 digit response code wanted
  231. * @throws Swift_BadResponseException If the code does not match
  232. */
  233. protected function assertCorrectResponse(Swift_Events_ResponseEvent $response, $codes)
  234. {
  235. $codes = (array)$codes;
  236. if (!in_array($response->getCode(), $codes))
  237. {
  238. $log = Swift_LogContainer::getLog();
  239. $error = "Expected response code(s) [" . implode(", ", $codes) . "] but got response [" . $response->getString() . "]";
  240. if ($log->hasLevel(Swift_Log::LOG_ERRORS)) $log->add($error, Swift_Log::ERROR);
  241. throw new Swift_BadResponseException($error);
  242. }
  243. }
  244. /**
  245. * Have a polite greeting with the server and work out what it's capable of
  246. * @param Swift_Events_ResponseEvent The initial service line respoonse
  247. * @throws Swift_ConnectionException If conversation is not going very well
  248. */
  249. protected function handshake(Swift_Events_ResponseEvent $greeting)
  250. {
  251. if ($this->connection->getRequiresEHLO() || strpos($greeting->getString(), "ESMTP"))
  252. $this->setConnectionExtensions($this->command("EHLO " . $this->domain, 250));
  253. else $this->command("HELO " . $this->domain, 250);
  254. //Connection might want to do something like authenticate now
  255. if (!$this->hasOption(self::NO_POST_CONNECT)) $this->connection->postConnect($this);
  256. }
  257. /**
  258. * Set the extensions which the service reports in the connection object
  259. * @param Swift_Events_ResponseEvent The list of extensions as reported by the service
  260. */
  261. protected function setConnectionExtensions(Swift_Events_ResponseEvent $list)
  262. {
  263. $le = (strpos($list->getString(), "\r\n") !== false) ? "\r\n" : "\n";
  264. $list = explode($le, $list->getString());
  265. for ($i = 1, $len = count($list); $i < $len; $i++)
  266. {
  267. $extension = substr($list[$i], 4);
  268. $attributes = preg_split("[ =]", $extension);
  269. $this->connection->setExtension($attributes[0], (isset($attributes[1]) ? array_slice($attributes, 1) : array()));
  270. }
  271. }
  272. /**
  273. * Execute a command against the service and get the response
  274. * @param string The command to execute (leave off any CRLF!!!)
  275. * @param int The code to check for in the response, if any. -1 indicates that no response is wanted.
  276. * @return Swift_Events_ResponseEvent The server's response (could be multiple lines)
  277. * @throws Swift_ConnectionException If a code was expected but does not match the one returned
  278. */
  279. public function command($command, $code=null)
  280. {
  281. $log = Swift_LogContainer::getLog();
  282. Swift_ClassLoader::load("Swift_Events_CommandEvent");
  283. if ($command !== "")
  284. {
  285. $command_event = new Swift_Events_CommandEvent($command, $code);
  286. $command = null; //For memory reasons
  287. $this->notifyListeners($command_event, "BeforeCommandListener");
  288. if ($log->hasLevel(Swift_Log::LOG_NETWORK) && $code != -1) $log->add($command_event->getString(), Swift_Log::COMMAND);
  289. $end = ($code != -1) ? "\r\n" : null;
  290. $this->connection->write($command_event->getString(), $end);
  291. $this->notifyListeners($command_event, "CommandListener");
  292. }
  293. if ($code == -1) return null;
  294. Swift_ClassLoader::load("Swift_Events_ResponseEvent");
  295. $response_event = new Swift_Events_ResponseEvent($this->connection->read());
  296. $this->notifyListeners($response_event, "ResponseListener");
  297. if ($log->hasLevel(Swift_Log::LOG_NETWORK)) $log->add($response_event->getString(), Swift_Log::RESPONSE);
  298. if ($command !== "" && $command_event->getCode() !== null)
  299. $this->assertCorrectResponse($response_event, $command_event->getCode());
  300. return $response_event;
  301. }
  302. /**
  303. * Reset a conversation which has gone badly
  304. * @throws Swift_ConnectionException If the service refuses to reset
  305. */
  306. public function reset()
  307. {
  308. $this->command("RSET", 250);
  309. }
  310. /**
  311. * Send a message to any number of recipients
  312. * @param Swift_Message The message to send. This does not need to (and shouldn't really) have any of the recipient headers set.
  313. * @param mixed The recipients to send to. Can be a string, Swift_Address or Swift_RecipientList. Note that all addresses apart from Bcc recipients will appear in the message headers
  314. * @param mixed The address to send the message from. Can either be a string or an instance of Swift_Address.
  315. * @return int The number of successful recipients
  316. * @throws Swift_ConnectionException If sending fails for any reason.
  317. */
  318. public function send(Swift_Message $message, $recipients, $from)
  319. {
  320. Swift_ClassLoader::load("Swift_Message_Encoder");
  321. if (is_string($recipients) && preg_match("/^" . Swift_Message_Encoder::CHEAP_ADDRESS_RE . "\$/", $recipients))
  322. {
  323. $recipients = new Swift_Address($recipients);
  324. }
  325. elseif (!($recipients instanceof Swift_AddressContainer))
  326. throw new Exception("The recipients parameter must either be a valid string email address, ".
  327. "an instance of Swift_RecipientList or an instance of Swift_Address.");
  328. if (is_string($from) && preg_match("/^" . Swift_Message_Encoder::CHEAP_ADDRESS_RE . "\$/", $from))
  329. {
  330. $from = new Swift_Address($from);
  331. }
  332. elseif (!($from instanceof Swift_Address))
  333. throw new Exception("The sender parameter must either be a valid string email address or ".
  334. "an instance of Swift_Address.");
  335. $log = Swift_LogContainer::getLog();
  336. if (!$message->getEncoding() && !$this->connection->hasExtension("8BITMIME"))
  337. {
  338. $message->setEncoding("QP", true, true);
  339. }
  340. $list = $recipients;
  341. if ($recipients instanceof Swift_Address)
  342. {
  343. $list = new Swift_RecipientList();
  344. $list->addTo($recipients);
  345. }
  346. Swift_ClassLoader::load("Swift_Events_SendEvent");
  347. $send_event = new Swift_Events_SendEvent($message, $list, $from, 0);
  348. $this->notifyListeners($send_event, "BeforeSendListener");
  349. $to = $cc = array();
  350. if (!($has_from = $message->getFrom())) $message->setFrom($from);
  351. if (!($has_return_path = $message->getReturnPath())) $message->setReturnPath($from->build(true));
  352. if (!($has_reply_to = $message->getReplyTo())) $message->setReplyTo($from);
  353. if (!($has_message_id = $message->getId())) $message->generateId();
  354. $this->command("MAIL FROM: " . $message->getReturnPath(true), 250);
  355. $failed = 0;
  356. $sent = 0;
  357. $tmp_sent = 0;
  358. $it = $list->getIterator("to");
  359. while ($it->hasNext())
  360. {
  361. $it->next();
  362. $address = $it->getValue();
  363. $to[] = $address->build();
  364. try {
  365. $this->command("RCPT TO: " . $address->build(true), 250);
  366. $tmp_sent++;
  367. } catch (Swift_BadResponseException $e) {
  368. $failed++;
  369. $send_event->addFailedRecipient($address->getAddress());
  370. if ($log->hasLevel(Swift_Log::LOG_FAILURES)) $log->addfailedRecipient($address->getAddress());
  371. }
  372. }
  373. $it = $list->getIterator("cc");
  374. while ($it->hasNext())
  375. {
  376. $it->next();
  377. $address = $it->getValue();
  378. $cc[] = $address->build();
  379. try {
  380. $this->command("RCPT TO: " . $address->build(true), 250);
  381. $tmp_sent++;
  382. } catch (Swift_BadResponseException $e) {
  383. $failed++;
  384. $send_event->addFailedRecipient($address->getAddress());
  385. if ($log->hasLevel(Swift_Log::LOG_FAILURES)) $log->addfailedRecipient($address->getAddress());
  386. }
  387. }
  388. if ($failed == (count($to) + count($cc)))
  389. {
  390. $this->reset();
  391. $this->notifyListeners($send_event, "SendListener");
  392. return 0;
  393. }
  394. if (!($has_to = $message->getTo()) && !empty($to)) $message->setTo($to);
  395. if (!($has_cc = $message->getCc()) && !empty($cc)) $message->setCc($cc);
  396. $this->command("DATA", 354);
  397. $data = $message->build();
  398. while (false !== $bytes = $data->read())
  399. $this->command($bytes, -1);
  400. if ($log->hasLevel(Swift_Log::LOG_NETWORK)) $log->add("<MESSAGE DATA>", Swift_Log::COMMAND);
  401. try {
  402. $this->command("\r\n.", 250);
  403. $sent += $tmp_sent;
  404. } catch (Swift_BadResponseException $e) {
  405. $failed += $tmp_sent;
  406. }
  407. $tmp_sent = 0;
  408. $has_bcc = $message->getBcc();
  409. $it = $list->getIterator("bcc");
  410. while ($it->hasNext())
  411. {
  412. $it->next();
  413. $address = $it->getValue();
  414. if (!$has_bcc) $message->setBcc($address->build());
  415. try {
  416. $this->command("MAIL FROM: " . $message->getReturnPath(true), 250);
  417. $this->command("RCPT TO: " . $address->build(true), 250);
  418. $this->command("DATA", 354);
  419. $data = $message->build();
  420. while (false !== $bytes = $data->read())
  421. $this->command($bytes, -1);
  422. if ($log->hasLevel(Swift_Log::LOG_NETWORK)) $log->add("<MESSAGE DATA>", Swift_Log::COMMAND);
  423. $this->command("\r\n.", 250);
  424. $sent++;
  425. } catch (Swift_BadResponseException $e) {
  426. $failed++;
  427. $send_event->addFailedRecipient($address->getAddress());
  428. if ($log->hasLevel(Swift_Log::LOG_FAILURES)) $log->addfailedRecipient($address->getAddress());
  429. $this->reset();
  430. }
  431. }
  432. $total = count($to) + count($cc) + count($list->getBcc());
  433. $send_event->setNumSent($sent);
  434. $this->notifyListeners($send_event, "SendListener");
  435. if (!$has_return_path) $message->setReturnPath("");
  436. if (!$has_from) $message->setFrom("");
  437. if (!$has_to) $message->setTo("");
  438. if (!$has_reply_to) $message->setReplyTo(null);
  439. if (!$has_cc) $message->setCc(null);
  440. if (!$has_bcc) $message->setBcc(null);
  441. if (!$has_message_id) $message->setId(null);
  442. if ($log->hasLevel(Swift_Log::LOG_NETWORK)) $log->add("Message sent to " . $sent . "/" . $total . " recipients", Swift_Log::NORMAL);
  443. return $sent;
  444. }
  445. /**
  446. * Send a message to a batch of recipients.
  447. * Unlike send() this method ignores Cc and Bcc recipients and does not reveal every recipients' address in the headers
  448. * @param Swift_Message The message to send (leave out the recipient headers unless you are deliberately overriding them)
  449. * @param Swift_RecipientList The addresses to send to
  450. * @param Swift_Address The address the mail is from (sender)
  451. * @return int The number of successful recipients
  452. */
  453. public function batchSend(Swift_Message $message, Swift_RecipientList $to, $from)
  454. {
  455. $batch = new Swift_BatchMailer($this);
  456. return $batch->send($message, $to, $from);
  457. }
  458. }