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