1<?php
2
3/**
4 * Swift Mailer SMTP Connection component.
5 * Please read the LICENSE file
6 * @author Chris Corbyn <chris@w3style.co.uk>
7 * @package Swift_Connection
8 * @license GNU Lesser General Public License
9 */
10
11require_once dirname(__FILE__) . "/../ClassLoader.php";
12Swift_ClassLoader::load("Swift_ConnectionBase");
13Swift_ClassLoader::load("Swift_Authenticator");
14
15/**
16 * Swift SMTP Connection
17 * @package Swift_Connection
18 * @author Chris Corbyn <chris@w3style.co.uk>
19 */
20class Swift_Connection_SMTP extends Swift_ConnectionBase
21{
22  /**
23   * Constant for TLS connections
24   */
25  const ENC_TLS = 2;
26  /**
27   * Constant for SSL connections
28   */
29  const ENC_SSL = 4;
30  /**
31   * Constant for unencrypted connections
32   */
33  const ENC_OFF = 8;
34  /**
35   * Constant for the default SMTP port
36   */
37  const PORT_DEFAULT = 25;
38  /**
39   * Constant for the default secure SMTP port
40   */
41  const PORT_SECURE = 465;
42  /**
43   * Constant for auto-detection of paramters
44   */
45  const AUTO_DETECT = -2;
46  /**
47   * A connection handle
48   * @var resource
49   */
50  protected $handle = null;
51  /**
52   * The remote port number
53   * @var int
54   */
55  protected $port = null;
56  /**
57   * Encryption type to use
58   * @var int
59   */
60  protected $encryption = null;
61  /**
62   * A connection timeout
63   * @var int
64   */
65  protected $timeout = 15;
66  /**
67   * A username to authenticate with
68   * @var string
69   */
70  protected $username = false;
71  /**
72   * A password to authenticate with
73   * @var string
74   */
75  protected $password = false;
76  /**
77   * Loaded authentication mechanisms
78   * @var array
79   */
80  protected $authenticators = array();
81  /**
82   * Fsockopen() error codes.
83   * @var int
84   */
85  protected $errno;
86  /**
87   * Fsockopen() error codes.
88   * @var string
89   */
90  protected $errstr;
91
92  /**
93   * Constructor
94   * @param string The remote server to connect to
95   * @param int The remote port to connect to
96   * @param int The encryption level to use
97   */
98  public function __construct($server="localhost", $port=null, $encryption=null)
99  {
100    $this->setServer($server);
101    $this->setEncryption($encryption);
102    $this->setPort($port);
103  }
104  /**
105   * Set the timeout to connect in seconds
106   * @param int Timeout to use
107   */
108  public function setTimeout($time)
109  {
110    $this->timeout = (int) $time;
111  }
112  /**
113   * Get the timeout currently set for connecting
114   * @return int
115   */
116  public function getTimeout()
117  {
118    return $this->timeout;
119  }
120  /**
121   * Set the remote server to connect to as a FQDN
122   * @param string Server name
123   */
124  public function setServer($server)
125  {
126    if ($server == self::AUTO_DETECT)
127    {
128      $server = @ini_get("SMTP");
129      if (!$server) $server = "localhost";
130    }
131    $this->server = (string) $server;
132  }
133  /**
134   * Get the remote server name
135   * @return string
136   */
137  public function getServer()
138  {
139    return $this->server;
140  }
141  /**
142   * Set the remote port number to connect to
143   * @param int Port number
144   */
145  public function setPort($port)
146  {
147    if ($port == self::AUTO_DETECT)
148    {
149      $port = @ini_get("SMTP_PORT");
150    }
151    if (!$port) $port = ($this->getEncryption() == self::ENC_OFF) ? self::PORT_DEFAULT : self::PORT_SECURE;
152    $this->port = (int) $port;
153  }
154  /**
155   * Get the remote port number currently used to connect
156   * @return int
157   */
158  public function getPort()
159  {
160    return $this->port;
161  }
162  /**
163   * Provide a username for authentication
164   * @param string The username
165   */
166  public function setUsername($user)
167  {
168    $this->setRequiresEHLO(true);
169    $this->username = $user;
170  }
171  /**
172   * Get the username for authentication
173   * @return string
174   */
175  public function getUsername()
176  {
177    return $this->username;
178  }
179  /**
180   * Set the password for SMTP authentication
181   * @param string Password to use
182   */
183  public function setPassword($pass)
184  {
185    $this->setRequiresEHLO(true);
186    $this->password = $pass;
187  }
188  /**
189   * Get the password for authentication
190   * @return string
191   */
192  public function getPassword()
193  {
194    return $this->password;
195  }
196  /**
197   * Add an authentication mechanism to authenticate with
198   * @param Swift_Authenticator
199   */
200  public function attachAuthenticator(Swift_Authenticator $auth)
201  {
202    $this->authenticators[$auth->getAuthExtensionName()] = $auth;
203    $log = Swift_LogContainer::getLog();
204    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
205    {
206      $log->add("Authentication mechanism '" . $auth->getAuthExtensionName() . "' attached.");
207    }
208  }
209  /**
210   * Set the encryption level to use on the connection
211   * See the constants ENC_TLS, ENC_SSL and ENC_OFF
212   * NOTE: PHP needs to have been compiled with OpenSSL for SSL and TLS to work
213   * NOTE: Some PHP installations will not have the TLS stream wrapper
214   * @param int Level of encryption
215   */
216  public function setEncryption($enc)
217  {
218    if (!$enc) $enc = self::ENC_OFF;
219    $this->encryption = (int) $enc;
220  }
221  /**
222   * Get the current encryption level used
223   * This method returns an integer corresponding to one of the constants ENC_TLS, ENC_SSL or ENC_OFF
224   * @return int
225   */
226  public function getEncryption()
227  {
228    return $this->encryption;
229  }
230  /**
231   * Read a full response from the buffer
232   * inner !feof() patch provided by Christian Rodriguez:
233   * <a href="http://www.flyspray.org/">www.flyspray.org</a>
234   * @return string
235   * @throws Swift_ConnectionException Upon failure to read
236   */
237  public function read()
238  {
239    if (!$this->handle) throw new Swift_ConnectionException(
240      "The SMTP connection is not alive and cannot be read from."  . $this->smtpErrors());
241    $ret = "";
242    $line = 0;
243    while (!feof($this->handle))
244    {
245      $line++;
246      stream_set_timeout($this->handle, $this->timeout);
247      $tmp = @fgets($this->handle);
248      if ($tmp === false && !feof($this->handle))
249      {
250        throw new Swift_ConnectionException(
251        "There was a problem reading line " . $line . " of an SMTP response. The response so far was:<br />[" . $ret .
252        "].  It appears the connection has died without saying goodbye to us! Too many emails in one go perhaps?"  .
253        $this->smtpErrors());
254      }
255      $ret .= trim($tmp) . "\r\n";
256      if ($tmp{3} == " ") break;
257    }
258    return $ret = substr($ret, 0, -2);
259  }
260  /**
261   * Write a command to the server (leave off trailing CRLF)
262   * @param string The command to send
263   * @throws Swift_ConnectionException Upon failure to write
264   */
265  public function write($command, $end="\r\n")
266  {
267    if (!$this->handle) throw new Swift_ConnectionException(
268      "The SMTP connection is not alive and cannot be written to."  .
269      $this->smtpErrors());
270    if (!@fwrite($this->handle, $command . $end) && !empty($command)) throw new Swift_ConnectionException("The SMTP connection did not allow the command '" . $command . "' to be sent." . $this->smtpErrors());
271  }
272  /**
273   * Try to start the connection
274   * @throws Swift_ConnectionException Upon failure to start
275   */
276  public function start()
277  {
278    if ($this->port === null)
279    {
280      switch ($this->encryption)
281      {
282        case self::ENC_TLS: case self::ENC_SSL:
283          $this->port = 465;
284        break;
285        case null: default:
286          $this->port = 25;
287        break;
288      }
289    }
290
291    $server = $this->server;
292    if ($this->encryption == self::ENC_TLS) $server = "tls://" . $server;
293    elseif ($this->encryption == self::ENC_SSL) $server = "ssl://" . $server;
294
295    $log = Swift_LogContainer::getLog();
296    if ($log->hasLevel(Swift_log::LOG_EVERYTHING))
297    {
298      $log->add("Trying to connect to SMTP server at '" . $server . ":" . $this->port);
299    }
300
301    if (!$this->handle = @fsockopen($server, $this->port, $errno, $errstr, $this->timeout))
302    {
303      $error_msg = "The SMTP connection failed to start [" . $server . ":" . $this->port . "]: fsockopen returned Error Number " . $errno . " and Error String '" . $errstr . "'";
304      if ($log->isEnabled())
305      {
306        $log->add($error_msg, Swift_Log::ERROR);
307      }
308      $this->handle = null;
309      throw new Swift_ConnectionException($error_msg);
310    }
311    $this->errno =& $errno;
312    $this->errstr =& $errstr;
313  }
314  /**
315   * Get the smtp error string as recorded by fsockopen()
316   * @return string
317   */
318  public function smtpErrors()
319  {
320    return " (fsockopen: " . $this->errstr . "#" . $this->errno . ") ";
321  }
322  /**
323   * Authenticate if required to do so
324   * @param Swift An instance of Swift
325   * @throws Swift_ConnectionException If authentication fails
326   */
327  public function postConnect(Swift $instance)
328  {
329    if ($this->getUsername() && $this->getPassword())
330    {
331      $this->runAuthenticators($this->getUsername(), $this->getPassword(), $instance);
332    }
333  }
334  /**
335   * Run each authenticator in turn an try for a successful login
336   * If none works, throw an exception
337   * @param string Username
338   * @param string Password
339   * @param Swift An instance of swift
340   * @throws Swift_ConnectionException Upon failure to authenticate
341   */
342  public function runAuthenticators($user, $pass, Swift $swift)
343  {
344    $log = Swift_LogContainer::getLog();
345    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
346    {
347      $log->add("Trying to authenticate with username '" . $user . "'.");
348    }
349    //Load in defaults
350    if (empty($this->authenticators))
351    {
352      if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
353      {
354        $log->add("No authenticators loaded; looking for defaults.");
355      }
356      $dir = dirname(__FILE__) . "/../Authenticator";
357      $handle = opendir($dir);
358      while (false !== $file = readdir($handle))
359      {
360        if (preg_match("/^[A-Za-z0-9-]+\\.php\$/", $file))
361        {
362          $name = preg_replace('/[^a-zA-Z0-9]+/', '', substr($file, 0, -4));
363          require_once $dir . "/" . $file;
364          $class = "Swift_Authenticator_" . $name;
365          $this->attachAuthenticator(new $class());
366        }
367      }
368      closedir($handle);
369    }
370
371    $tried = 0;
372    $looks_supported = true;
373
374    //Allow everything we have if the server has the audacity not to help us out.
375    if (!$this->hasExtension("AUTH"))
376    {
377      if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
378      {
379        $log->add("Server (perhaps wrongly) is not advertising AUTH... manually overriding.");
380      }
381      $looks_supported = false;
382      $this->setExtension("AUTH", array_keys($this->authenticators));
383    }
384
385    foreach ($this->authenticators as $name => $obj)
386    {
387      //Server supports this authentication mechanism
388      if (in_array($name, $this->getAttributes("AUTH")) || $name{0} == "*")
389      {
390        $tried++;
391        if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
392        {
393          $log->add("Trying '" . $name . "' authentication...");
394        }
395        if ($this->authenticators[$name]->isAuthenticated($user, $pass, $swift))
396        {
397          if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
398          {
399            $log->add("Success! Authentication accepted.");
400          }
401          return true;
402        }
403      }
404    }
405
406    //Server doesn't support authentication
407    if (!$looks_supported && $tried == 0)
408      throw new Swift_ConnectionException("Authentication is not supported by the server but a username and password was given.");
409
410    if ($tried == 0)
411      throw new Swift_ConnectionException("No authentication mechanisms were tried since the server did not support any of the ones loaded. " .
412      "Loaded authenticators: [" . implode(", ", array_keys($this->authenticators)) . "]");
413    else
414      throw new Swift_ConnectionException("Authentication failed using username '" . $user . "' and password '". str_repeat("*", strlen($pass)) . "'");
415  }
416  /**
417   * Try to close the connection
418   * @throws Swift_ConnectionException Upon failure to close
419   */
420  public function stop()
421  {
422    $log = Swift_LogContainer::getLog();
423    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
424    {
425      $log->add("Closing down SMTP connection.");
426    }
427    if ($this->handle)
428    {
429      if (!fclose($this->handle))
430      {
431        throw new Swift_ConnectionException("The SMTP connection could not be closed for an unknown reason." . $this->smtpErrors());
432      }
433      $this->handle = null;
434    }
435  }
436  /**
437   * Check if the SMTP connection is alive
438   * @return boolean
439   */
440  public function isAlive()
441  {
442    return ($this->handle !== null);
443  }
444  /**
445   * Destructor.
446   * Cleans up any open connections.
447   */
448  public function __destruct()
449  {
450    $this->stop();
451  }
452}
453