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