1<?php
2
3/**
4 * Swift Mailer Sendmail 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");
13
14/**
15 * Swift Sendmail Connection
16 * @package Swift_Connection
17 * @author Chris Corbyn <chris@w3style.co.uk>
18 */
19class Swift_Connection_Sendmail extends Swift_ConnectionBase
20{
21  /**
22   * Constant for auto-detection of paths
23   */
24  const AUTO_DETECT = -2;
25  /**
26   * Flags for the MTA (options such as bs or t)
27   * @var string
28   */
29  protected $flags = null;
30  /**
31   * The full path to the MTA
32   * @var string
33   */
34  protected $path = null;
35  /**
36   * The type of last request sent
37   * For example MAIL, RCPT, DATA
38   * @var string
39   */
40  protected $request = null;
41  /**
42   * The process handle
43   * @var resource
44   */
45  protected $proc;
46  /**
47   * I/O pipes for the process
48   * @var array
49   */
50  protected $pipes;
51  /**
52   * Switches to true for just one command when DATA has been issued
53   * @var boolean
54   */
55  protected $send = false;
56  /**
57   * The timeout in seconds before giving up
58   * @var int Seconds
59   */
60  protected $timeout = 10;
61
62  /**
63   * Constructor
64   * @param string The command to execute
65   * @param int The timeout in seconds before giving up
66   */
67  public function __construct($command="/usr/sbin/sendmail -bs", $timeout=10)
68  {
69    $this->setCommand($command);
70    $this->setTimeout($timeout);
71  }
72  /**
73   * Set the timeout on the process
74   * @param int The number of seconds
75   */
76  public function setTimeout($secs)
77  {
78    $this->timeout = (int)$secs;
79  }
80  /**
81   * Get the timeout on the process
82   * @return int
83   */
84  public function getTimeout()
85  {
86    return $this->timeout;
87  }
88  /**
89   * Set the operating flags for the MTA
90   * @param string
91   */
92  public function setFlags($flags)
93  {
94    $this->flags = $flags;
95  }
96  /**
97   * Get the operating flags for the MTA
98   * @return string
99   */
100  public function getFlags()
101  {
102    return $this->flags;
103  }
104  /**
105   * Set the path to the binary
106   * @param string The path (must be absolute!)
107   */
108  public function setPath($path)
109  {
110    if ($path == self::AUTO_DETECT) $path = $this->findSendmail();
111    $this->path = $path;
112  }
113  /**
114   * Get the path to the binary
115   * @return string
116   */
117  public function getPath()
118  {
119    return $this->path;
120  }
121  /**
122   * For auto-detection of sendmail path
123   * Thanks to "Joe Cotroneo" for providing the enhancement
124   * @return string
125   */
126  public function findSendmail()
127  {
128    $log = Swift_LogContainer::getLog();
129    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
130    {
131      $log->add("Sendmail path auto-detection in progress.  Trying `which sendmail`");
132    }
133    $path = @trim(shell_exec('which sendmail'));
134    if (!is_executable($path))
135    {
136      if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
137      {
138        $log->add("No luck so far, trying some common paths...");
139      }
140      $common_locations = array(
141        '/usr/bin/sendmail',
142        '/usr/lib/sendmail',
143        '/var/qmail/bin/sendmail',
144        '/bin/sendmail',
145        '/usr/sbin/sendmail',
146        '/sbin/sendmail'
147      );
148      foreach ($common_locations as $path)
149      {
150        if (is_executable($path)) return $path;
151      }
152      if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
153      {
154        $log->add("Falling back to /usr/sbin/sendmail (but it doesn't look good)!");
155      }
156      //Fallback (swift will still throw an error)
157      return "/usr/sbin/sendmail";
158    }
159    else return $path;
160  }
161  /**
162   * Set the sendmail command (path + flags)
163   * @param string Command
164   * @throws Swift_ConnectionException If the command is not correctly structured
165   */
166  public function setCommand($command)
167  {
168    if ($command == self::AUTO_DETECT) $command = $this->findSendmail() . " -bs";
169
170    if (!strrpos($command, " -"))
171    {
172      throw new Swift_ConnectionException("Cannot set sendmail command with no command line flags. e.g. /usr/sbin/sendmail -t");
173    }
174    $path = substr($command, 0, strrpos($command, " -"));
175    $flags = substr($command, strrpos($command, " -")+2);
176    $this->setPath($path);
177    $this->setFlags($flags);
178  }
179  /**
180   * Get the sendmail command (path + flags)
181   * @return string
182   */
183  public function getCommand()
184  {
185    return $this->getPath() . " -" . $this->getFlags();
186  }
187  /**
188   * Write a command to the open pipe
189   * @param string The command to write
190   * @throws Swift_ConnectionException If the pipe cannot be written to
191   */
192  protected function pipeIn($command, $end="\r\n")
193  {
194    if (!$this->isAlive()) throw new Swift_ConnectionException("The sendmail process is not alive and cannot be written to.");
195    if (!@fwrite($this->pipes[0], $command . $end)  && !empty($command)) throw new Swift_ConnectionException("The sendmail process did not allow the command '" . $command . "' to be sent.");
196    fflush($this->pipes[0]);
197  }
198  /**
199   * Read data from the open pipe
200   * @return string
201   * @throws Swift_ConnectionException If the pipe is not operating as expected
202   */
203  protected function pipeOut()
204  {
205    if (strpos($this->getFlags(), "t") !== false) return;
206    if (!$this->isAlive()) throw new Swift_ConnectionException("The sendmail process is not alive and cannot be read from.");
207    $ret = "";
208    $line = 0;
209    while (true)
210    {
211      $line++;
212      stream_set_timeout($this->pipes[1], $this->timeout);
213      $tmp = @fgets($this->pipes[1]);
214      if ($tmp === false)
215      {
216        throw new Swift_ConnectionException("There was a problem reading line " . $line . " of a sendmail SMTP response. The response so far was:<br />[" . $ret . "].  It appears the process has died.");
217      }
218      $ret .= trim($tmp) . "\r\n";
219      if ($tmp{3} == " ") break;
220    }
221    fflush($this->pipes[1]);
222    return $ret = substr($ret, 0, -2);
223  }
224  /**
225   * Read a full response from the buffer (this is spoofed if running in -t mode)
226   * @return string
227   * @throws Swift_ConnectionException Upon failure to read
228   */
229  public function read()
230  {
231    if (strpos($this->getFlags(), "t") !== false)
232    {
233      switch (strtolower($this->request))
234      {
235        case null:
236          return "220 Greetings";
237        case "helo": case "ehlo":
238          return "250 hello";
239        case "mail": case "rcpt": case "rset":
240          return "250 ok";
241        case "quit":
242          return "221 bye";
243        case "data":
244          $this->send = true;
245          return "354 go ahead";
246        default:
247          return "250 ok";
248      }
249    }
250    else return $this->pipeOut();
251  }
252  /**
253   * Write a command to the process (leave off trailing CRLF)
254   * @param string The command to send
255   * @throws Swift_ConnectionException Upon failure to write
256   */
257  public function write($command, $end="\r\n")
258  {
259    if (strpos($this->getFlags(), "t") !== false)
260    {
261      if (!$this->send && strpos($command, " ")) $command = substr($command, strpos($command, " ")+1);
262      elseif ($this->send)
263      {
264        $this->pipeIn($command);
265      }
266      $this->request = $command;
267      $this->send = (strtolower($command) == "data");
268    }
269    else $this->pipeIn($command, $end);
270  }
271  /**
272   * Try to start the connection
273   * @throws Swift_ConnectionException Upon failure to start
274   */
275  public function start()
276  {
277    $log = Swift_LogContainer::getLog();
278    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
279    {
280      $log->add("Trying to start a sendmail process.");
281    }
282    if (!$this->getPath() || !$this->getFlags())
283    {
284      throw new Swift_ConnectionException("Sendmail cannot be started without a path to the binary including flags.");
285    }
286    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
287    {
288      $log->add("Trying to stat the executable '" . $this->getPath() . "'.");
289    }
290    if (!@lstat($this->getPath()))
291    {
292      throw new Swift_ConnectionException(
293        "Sendmail cannot be seen with lstat().  The command given [" . $this->getCommand() . "] does not appear to be valid.");
294    }
295
296    $pipes_spec = array(
297      array("pipe", "r"),
298      array("pipe", "w"),
299      array("pipe", "w")
300    );
301
302    $this->proc = proc_open($this->getCommand(), $pipes_spec, $this->pipes);
303
304    if (!$this->isAlive())
305    {
306      throw new Swift_ConnectionException(
307      "The sendmail process failed to start.  Please verify that the path exists and PHP has permission to execute it.");
308    }
309  }
310  /**
311   * Try to close the connection
312   */
313  public function stop()
314  {
315    $log = Swift_LogContainer::getLog();
316    if ($log->hasLevel(Swift_Log::LOG_EVERYTHING))
317    {
318      $log->add("Terminating sendmail process.");
319    }
320    foreach ((array)$this->pipes as $pipe)
321    {
322      @fclose($pipe);
323    }
324
325    if ($this->proc)
326    {
327      proc_close($this->proc);
328      $this->pipes = null;
329      $this->proc = null;
330    }
331  }
332  /**
333   * Check if the process is still alive
334   * @return boolean
335   */
336  public function isAlive()
337  {
338    return ($this->proc !== false
339      && is_resource($this->proc)
340      && is_resource($this->pipes[0])
341      && is_resource($this->pipes[1])
342      && $this->proc !== null);
343  }
344  /**
345   * Destructor.
346   * Cleans up by stopping any running processes.
347   */
348  public function __destruct()
349  {
350    $this->stop();
351  }
352}
353