source: github/program/include/rcube_smtp.php @ 63d4d611

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since 63d4d611 was 63d4d611, checked in by alecpl <alec@…>, 3 years ago
  • Re-implemented SMTP proxy authorization support
  • Property mode set to 100644
File size: 13.9 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_smtp.php                                        |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2010, Roundcube Dev. - Switzerland                 |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   Provide SMTP functionality using socket connections                 |
13 |                                                                       |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16 +-----------------------------------------------------------------------+
17
18 $Id$
19
20*/
21
22// define headers delimiter
23define('SMTP_MIME_CRLF', "\r\n");
24
25/**
26 * Class to provide SMTP functionality using PEAR Net_SMTP
27 *
28 * @package    Mail
29 * @author     Thomas Bruederli <roundcube@gmail.com>
30 * @author     Aleksander Machniak <alec@alec.pl>
31 */
32class rcube_smtp
33{
34
35  private $conn = null;
36  private $response;
37  private $error;
38
39
40  /**
41   * SMTP Connection and authentication
42   *
43   * @param string Server host
44   * @param string Server port
45   * @param string User name
46   * @param string Password
47   *
48   * @return bool  Returns true on success, or false on error
49   */
50  public function connect($host=null, $port=null, $user=null, $pass=null)
51  {
52    $RCMAIL = rcmail::get_instance();
53 
54    // disconnect/destroy $this->conn
55    $this->disconnect();
56   
57    // reset error/response var
58    $this->error = $this->response = null;
59 
60    // let plugins alter smtp connection config
61    $CONFIG = $RCMAIL->plugins->exec_hook('smtp_connect', array(
62      'smtp_server'    => $host ? $host : $RCMAIL->config->get('smtp_server'),
63      'smtp_port'      => $port ? $port : $RCMAIL->config->get('smtp_port', 25),
64      'smtp_user'      => $user ? $user : $RCMAIL->config->get('smtp_user'),
65      'smtp_pass'      => $pass ? $pass : $RCMAIL->config->get('smtp_pass'),
66      'smtp_auth_cid'  => $RCMAIL->config->get('smtp_auth_cid'),
67      'smtp_auth_pw'   => $RCMAIL->config->get('smtp_auth_pw'),
68      'smtp_auth_type' => $RCMAIL->config->get('smtp_auth_type'),
69      'smtp_helo_host' => $RCMAIL->config->get('smtp_helo_host'),
70      'smtp_timeout'   => $RCMAIL->config->get('smtp_timeout'),
71    ));
72
73    $smtp_host = rcube_parse_host($CONFIG['smtp_server']);
74    // when called from Installer it's possible to have empty $smtp_host here
75    if (!$smtp_host) $smtp_host = 'localhost';
76    $smtp_port = is_numeric($CONFIG['smtp_port']) ? $CONFIG['smtp_port'] : 25;
77    $smtp_host_url = parse_url($smtp_host);
78
79    // overwrite port
80    if (isset($smtp_host_url['host']) && isset($smtp_host_url['port']))
81    {
82      $smtp_host = $smtp_host_url['host'];
83      $smtp_port = $smtp_host_url['port'];
84    }
85
86    // re-write smtp host
87    if (isset($smtp_host_url['host']) && isset($smtp_host_url['scheme']))
88      $smtp_host = sprintf('%s://%s', $smtp_host_url['scheme'], $smtp_host_url['host']);
89
90    // remove TLS prefix and set flag for use in Net_SMTP::auth()
91    if (preg_match('#^tls://#i', $smtp_host)) {
92      $smtp_host = preg_replace('#^tls://#i', '', $smtp_host);
93      $use_tls = true;
94    }
95
96    if (!empty($CONFIG['smtp_helo_host']))
97      $helo_host = $CONFIG['smtp_helo_host'];
98    else if (!empty($_SERVER['SERVER_NAME']))
99      $helo_host = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
100    else
101      $helo_host = 'localhost';
102
103    // IDNA Support
104    $smtp_host = idn_to_ascii($smtp_host);
105
106    $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host);
107
108    if($RCMAIL->config->get('smtp_debug'))
109      $this->conn->setDebug(true, array($this, 'debug_handler'));
110
111    // try to connect to server and exit on failure
112    $result = $this->conn->connect($smtp_timeout);
113
114    if (PEAR::isError($result)) {
115      $this->response[] = "Connection failed: ".$result->getMessage();
116      $this->error = array('label' => 'smtpconnerror', 'vars' => array('code' => $this->conn->_code));
117      $this->conn = null;
118      return false;
119    }
120
121    $smtp_user = str_replace('%u', $_SESSION['username'], $CONFIG['smtp_user']);
122    $smtp_pass = str_replace('%p', $RCMAIL->decrypt($_SESSION['password']), $CONFIG['smtp_pass']);
123    $smtp_auth_type = empty($CONFIG['smtp_auth_type']) ? NULL : $CONFIG['smtp_auth_type'];
124
125    if (!empty($CONFIG['smtp_auth_cid'])) {
126      $smtp_authz = $smtp_user;
127      $smtp_user  = $CONFIG['smtp_auth_cid'];
128      $smtp_pass  = $CONFIG['smtp_auth_pw'];
129    }
130
131    // attempt to authenticate to the SMTP server
132    if ($smtp_user && $smtp_pass)
133    {
134      // IDNA Support
135      if (strpos($smtp_user, '@'))
136        $smtp_user = idn_to_ascii($smtp_user);
137
138      $result = $this->conn->auth($smtp_user, $smtp_pass, $smtp_auth_type, $use_tls, $smtp_authz);
139
140      if (PEAR::isError($result))
141      {
142        $this->error = array('label' => 'smtpautherror', 'vars' => array('code' => $this->conn->_code));
143        $this->response[] .= 'Authentication failure: ' . $result->getMessage() . ' (Code: ' . $result->getCode() . ')';
144        $this->reset();
145        $this->disconnect();
146        return false;
147      }
148    }
149
150    return true;
151  }
152
153
154  /**
155   * Function for sending mail
156   *
157   * @param string Sender e-Mail address
158   *
159   * @param mixed  Either a comma-seperated list of recipients
160   *               (RFC822 compliant), or an array of recipients,
161   *               each RFC822 valid. This may contain recipients not
162   *               specified in the headers, for Bcc:, resending
163   *               messages, etc.
164   * @param mixed  The message headers to send with the mail
165   *               Either as an associative array or a finally
166   *               formatted string
167   * @param mixed  The full text of the message body, including any Mime parts
168   *               or file handle
169   * @param array  Delivery options (e.g. DSN request)
170   *
171   * @return bool  Returns true on success, or false on error
172   */
173  public function send_mail($from, $recipients, &$headers, &$body, $opts=null)
174  {
175    if (!is_object($this->conn))
176      return false;
177
178    // prepare message headers as string
179    if (is_array($headers))
180    {
181      if (!($headerElements = $this->_prepare_headers($headers))) {
182        $this->reset();
183        return false;
184      }
185
186      list($from, $text_headers) = $headerElements;
187    }
188    else if (is_string($headers))
189      $text_headers = $headers;
190    else
191    {
192      $this->reset();
193      $this->response[] = "Invalid message headers";
194      return false;
195    }
196
197    // exit if no from address is given
198    if (!isset($from))
199    {
200      $this->reset();
201      $this->response[] = "No From address has been provided";
202      return false;
203    }
204
205    // RFC3461: Delivery Status Notification
206    if ($opts['dsn']) {
207      $exts = $this->conn->getServiceExtensions();
208
209      if (!isset($exts['DSN'])) {
210        $this->error = array('label' => 'smtpdsnerror');
211        $this->response[] = "DSN not supported";
212        return false;
213      }
214
215      $from_params      = 'RET=HDRS';
216      $recipient_params = 'NOTIFY=SUCCESS,FAILURE';
217    }
218
219    // RFC2298.3: remove envelope sender address
220    if (preg_match('/Content-Type: multipart\/report/', $text_headers)
221      && preg_match('/report-type=disposition-notification/', $text_headers)
222    ) {
223      $from = '';
224    }
225
226    // set From: address
227    if (PEAR::isError($this->conn->mailFrom($from, $from_params)))
228    {
229      $err = $this->conn->getResponse();
230      $this->error = array('label' => 'smtpfromerror', 'vars' => array(
231        'from' => $from, 'code' => $this->conn->_code, 'msg' => $err[1]));
232      $this->response[] = "Failed to set sender '$from'";
233      $this->reset();
234      return false;
235    }
236
237    // prepare list of recipients
238    $recipients = $this->_parse_rfc822($recipients);
239    if (PEAR::isError($recipients))
240    {
241      $this->error = array('label' => 'smtprecipientserror');
242      $this->reset();
243      return false;
244    }
245
246    // set mail recipients
247    foreach ($recipients as $recipient)
248    {
249      if (PEAR::isError($this->conn->rcptTo($recipient, $recipient_params))) {
250        $err = $this->conn->getResponse();
251        $this->error = array('label' => 'smtptoerror', 'vars' => array(
252          'to' => $recipient, 'code' => $this->conn->_code, 'msg' => $err[1]));
253        $this->response[] = "Failed to add recipient '$recipient'";
254        $this->reset();
255        return false;
256      }
257    }
258
259    if (is_resource($body))
260    {
261      // file handle
262      $data = $body;
263      $text_headers = preg_replace('/[\r\n]+$/', '', $text_headers);
264    } else {
265      // Concatenate headers and body so it can be passed by reference to SMTP_CONN->data
266      // so preg_replace in SMTP_CONN->quotedata will store a reference instead of a copy.
267      // We are still forced to make another copy here for a couple ticks so we don't really
268      // get to save a copy in the method call.
269      $data = $text_headers . "\r\n" . $body;
270
271      // unset old vars to save data and so we can pass into SMTP_CONN->data by reference.
272      unset($text_headers, $body);
273    }
274
275    // Send the message's headers and the body as SMTP data.
276    if (PEAR::isError($result = $this->conn->data($data, $text_headers)))
277    {
278      $err = $this->conn->getResponse();
279      if (!in_array($err[0], array(354, 250, 221)))
280        $msg = sprintf('[%d] %s', $err[0], $err[1]);
281      else
282        $msg = $result->getMessage();
283
284      $this->error = array('label' => 'smtperror', 'vars' => array('msg' => $msg));
285      $this->response[] = "Failed to send data";
286      $this->reset();
287      return false;
288    }
289
290    $this->response[] = join(': ', $this->conn->getResponse());
291    return true;
292  }
293
294
295  /**
296   * Reset the global SMTP connection
297   * @access public
298   */
299  public function reset()
300  {
301    if (is_object($this->conn))
302      $this->conn->rset();
303  }
304
305
306  /**
307   * Disconnect the global SMTP connection
308   * @access public
309   */
310  public function disconnect()
311  {
312    if (is_object($this->conn)) {
313      $this->conn->disconnect();
314      $this->conn = null;
315    }
316  }
317
318
319  /**
320   * This is our own debug handler for the SMTP connection
321   * @access public
322   */
323  public function debug_handler(&$smtp, $message)
324  {
325    write_log('smtp', preg_replace('/\r\n$/', '', $message));
326  }
327
328
329  /**
330   * Get error message
331   * @access public
332   */
333  public function get_error()
334  {
335    return $this->error;
336  }
337
338
339  /**
340   * Get server response messages array
341   * @access public
342   */
343  public function get_response()
344  {
345    return $this->response;
346  }
347
348
349  /**
350   * Take an array of mail headers and return a string containing
351   * text usable in sending a message.
352   *
353   * @param array $headers The array of headers to prepare, in an associative
354   *              array, where the array key is the header name (ie,
355   *              'Subject'), and the array value is the header
356   *              value (ie, 'test'). The header produced from those
357   *              values would be 'Subject: test'.
358   *
359   * @return mixed Returns false if it encounters a bad address,
360   *               otherwise returns an array containing two
361   *               elements: Any From: address found in the headers,
362   *               and the plain text version of the headers.
363   * @access private
364   */
365  private function _prepare_headers($headers)
366  {
367    $lines = array();
368    $from = null;
369
370    foreach ($headers as $key => $value)
371    {
372      if (strcasecmp($key, 'From') === 0)
373      {
374        $addresses = $this->_parse_rfc822($value);
375
376        if (is_array($addresses))
377          $from = $addresses[0];
378
379        // Reject envelope From: addresses with spaces.
380        if (strstr($from, ' '))
381          return false;
382
383        $lines[] = $key . ': ' . $value;
384      }
385      else if (strcasecmp($key, 'Received') === 0)
386      {
387        $received = array();
388        if (is_array($value))
389        {
390          foreach ($value as $line)
391            $received[] = $key . ': ' . $line;
392        }
393        else
394        {
395          $received[] = $key . ': ' . $value;
396        }
397
398        // Put Received: headers at the top.  Spam detectors often
399        // flag messages with Received: headers after the Subject:
400        // as spam.
401        $lines = array_merge($received, $lines);
402      }
403      else
404      {
405        // If $value is an array (i.e., a list of addresses), convert
406        // it to a comma-delimited string of its elements (addresses).
407        if (is_array($value))
408          $value = implode(', ', $value);
409
410        $lines[] = $key . ': ' . $value;
411      }
412    }
413   
414    return array($from, join(SMTP_MIME_CRLF, $lines) . SMTP_MIME_CRLF);
415  }
416
417  /**
418   * Take a set of recipients and parse them, returning an array of
419   * bare addresses (forward paths) that can be passed to sendmail
420   * or an smtp server with the rcpt to: command.
421   *
422   * @param mixed Either a comma-seperated list of recipients
423   *              (RFC822 compliant), or an array of recipients,
424   *              each RFC822 valid.
425   *
426   * @return array An array of forward paths (bare addresses).
427   * @access private
428   */
429  private function _parse_rfc822($recipients)
430  {
431    // if we're passed an array, assume addresses are valid and implode them before parsing.
432    if (is_array($recipients))
433      $recipients = implode(', ', $recipients);
434   
435    $addresses = array();
436    $recipients = rcube_explode_quoted_string(',', $recipients);
437
438    reset($recipients);
439    while (list($k, $recipient) = each($recipients))
440    {
441      $a = explode(" ", $recipient);
442      while (list($k2, $word) = each($a))
443      {
444        if (strpos($word, "@") > 0 && $word[strlen($word)-1] != '"')
445        {
446          $word = preg_replace('/^<|>$/', '', trim($word));
447          if (in_array($word, $addresses)===false)
448            array_push($addresses, $word);
449        }
450      }
451    }
452    return $addresses;
453  }
454
455}
Note: See TracBrowser for help on using the repository browser.