source: subversion/branches/devel-framework/roundcubemail/program/include/rcmail.php @ 5881

Last change on this file since 5881 was 5881, checked in by thomasb, 15 months ago

Split rcmail functionality into rcube base class (framework) and keep application specific stuff in rcmail

  • Property svn:keywords set to Id
File size: 38.0 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcmail.php                                            |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
9 | Copyright (C) 2011, Kolab Systems AG                                  |
10 | Licensed under the GNU GPL                                            |
11 |                                                                       |
12 | PURPOSE:                                                              |
13 |   Application class providing core functions and holding              |
14 |   instances of all 'global' objects like db- and imap-connections     |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Application class of Roundcube Webmail
26 * implemented as singleton
27 *
28 * @package Core
29 */
30class rcmail extends rcube
31{
32  /**
33   * Main tasks.
34   *
35   * @var array
36   */
37  static public $main_tasks = array('mail','settings','addressbook','login','logout','utils','dummy');
38
39  /**
40   * Current task.
41   *
42   * @var string
43   */
44  public $task;
45
46  /**
47   * Current action.
48   *
49   * @var string
50   */
51  public $action = '';
52  public $comm_path = './';
53
54  private $address_books = array();
55  private $action_map = array();
56
57
58  /**
59   * This implements the 'singleton' design pattern
60   *
61   * @return rcmail The one and only instance
62   */
63  static function get_instance()
64  {
65    if (!self::$instance || !is_a(self::$instance, 'rcmail')) {
66      self::$instance = new rcmail();
67      self::$instance->startup();  // init AFTER object was linked with self::$instance
68    }
69
70    return self::$instance;
71  }
72
73
74  /**
75   * Initial startup function
76   * to register session, create database and imap connections
77   */
78  protected function startup()
79  {
80    $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS);
81
82    // start session
83    $this->session_init();
84
85    // create user object
86    $this->set_user(new rcube_user($_SESSION['user_id']));
87
88    // configure session (after user config merge!)
89    $this->session_configure();
90
91    // set task and action properties
92    $this->set_task(rcube_ui::get_input_value('_task', rcube_ui::INPUT_GPC));
93    $this->action = asciiwords(rcube_ui::get_input_value('_action', rcube_ui::INPUT_GPC));
94
95    // reset some session parameters when changing task
96    if ($this->task != 'utils') {
97      if ($this->session && $_SESSION['task'] != $this->task)
98        $this->session->remove('page');
99      // set current task to session
100      $_SESSION['task'] = $this->task;
101    }
102
103    // init output class
104    if (!empty($_REQUEST['_remote']))
105      $GLOBALS['OUTPUT'] = $this->json_init();
106    else
107      $GLOBALS['OUTPUT'] = $this->load_gui(!empty($_REQUEST['_framed']));
108
109    // load plugins
110    $this->plugins->init($this, $this->task);
111    $this->plugins->load_plugins((array)$this->config->get('plugins', array()), array('filesystem_attachments', 'jqueryui'));
112  }
113
114
115  /**
116   * Setter for application task
117   *
118   * @param string Task to set
119   */
120  public function set_task($task)
121  {
122    $task = asciiwords($task);
123
124    if ($this->user && $this->user->ID)
125      $task = !$task ? 'mail' : $task;
126    else
127      $task = 'login';
128
129    $this->task = $task;
130    $this->comm_path = $this->url(array('task' => $this->task));
131
132    if ($this->output)
133      $this->output->set_env('task', $this->task);
134  }
135
136
137  /**
138   * Setter for system user object
139   *
140   * @param rcube_user Current user instance
141   */
142  public function set_user($user)
143  {
144    if (is_object($user)) {
145      $this->user = $user;
146
147      // overwrite config with user preferences
148      $this->config->set_user_prefs((array)$this->user->get_prefs());
149    }
150
151    $_SESSION['language'] = $this->user->language = $this->language_prop($this->config->get('language', $_SESSION['language']));
152
153    // set localization
154    setlocale(LC_ALL, $_SESSION['language'] . '.utf8', 'en_US.utf8');
155
156    // workaround for http://bugs.php.net/bug.php?id=18556
157    if (in_array($_SESSION['language'], array('tr_TR', 'ku', 'az_AZ')))
158      setlocale(LC_CTYPE, 'en_US' . '.utf8');
159  }
160
161
162  /**
163   * Return instance of the internal address book class
164   *
165   * @param string  Address book identifier
166   * @param boolean True if the address book needs to be writeable
167   *
168   * @return rcube_contacts Address book object
169   */
170  public function get_address_book($id, $writeable = false)
171  {
172    $contacts    = null;
173    $ldap_config = (array)$this->config->get('ldap_public');
174    $abook_type  = strtolower($this->config->get('address_book_type'));
175
176    // 'sql' is the alias for '0' used by autocomplete
177    if ($id == 'sql')
178        $id = '0';
179
180    // use existing instance
181    if (isset($this->address_books[$id]) && is_object($this->address_books[$id])
182      && is_a($this->address_books[$id], 'rcube_addressbook')
183      && (!$writeable || !$this->address_books[$id]->readonly)
184    ) {
185      $contacts = $this->address_books[$id];
186    }
187    else if ($id && $ldap_config[$id]) {
188      $contacts = new rcube_ldap($ldap_config[$id], $this->config->get('ldap_debug'), $this->config->mail_domain($_SESSION['storage_host']));
189    }
190    else if ($id === '0') {
191      $contacts = new rcube_contacts($this->db, $this->get_user_id());
192    }
193    else {
194      $plugin = $this->plugins->exec_hook('addressbook_get', array('id' => $id, 'writeable' => $writeable));
195
196      // plugin returned instance of a rcube_addressbook
197      if ($plugin['instance'] instanceof rcube_addressbook) {
198        $contacts = $plugin['instance'];
199      }
200      // get first source from the list
201      else if (!$id) {
202        $source = reset($this->get_address_sources($writeable));
203        if (!empty($source)) {
204          $contacts = $this->get_address_book($source['id']);
205          if ($contacts)
206            $id = $source['id'];
207        }
208      }
209    }
210
211    if (!$contacts) {
212      self::raise_error(array(
213        'code' => 700, 'type' => 'php',
214        'file' => __FILE__, 'line' => __LINE__,
215        'message' => "Addressbook source ($id) not found!"),
216        true, true);
217    }
218
219    // add to the 'books' array for shutdown function
220    $this->address_books[$id] = $contacts;
221
222    return $contacts;
223  }
224
225
226  /**
227   * Return address books list
228   *
229   * @param boolean True if the address book needs to be writeable
230   *
231   * @return array  Address books array
232   */
233  public function get_address_sources($writeable = false)
234  {
235    $abook_type = strtolower($this->config->get('address_book_type'));
236    $ldap_config = $this->config->get('ldap_public');
237    $autocomplete = (array) $this->config->get('autocomplete_addressbooks');
238    $list = array();
239
240    // We are using the DB address book
241    if ($abook_type != 'ldap') {
242      if (!isset($this->address_books['0']))
243        $this->address_books['0'] = new rcube_contacts($this->db, $this->get_user_id());
244      $list['0'] = array(
245        'id'       => '0',
246        'name'     => $this->gettext('personaladrbook'),
247        'groups'   => $this->address_books['0']->groups,
248        'readonly' => $this->address_books['0']->readonly,
249        'autocomplete' => in_array('sql', $autocomplete),
250        'undelete' => $this->address_books['0']->undelete && $this->config->get('undo_timeout'),
251      );
252    }
253
254    if ($ldap_config) {
255      $ldap_config = (array) $ldap_config;
256      foreach ($ldap_config as $id => $prop)
257        $list[$id] = array(
258          'id'       => $id,
259          'name'     => $prop['name'],
260          'groups'   => is_array($prop['groups']),
261          'readonly' => !$prop['writable'],
262          'hidden'   => $prop['hidden'],
263          'autocomplete' => in_array($id, $autocomplete)
264        );
265    }
266
267    $plugin = $this->plugins->exec_hook('addressbooks_list', array('sources' => $list));
268    $list = $plugin['sources'];
269
270    foreach ($list as $idx => $item) {
271      // register source for shutdown function
272      if (!is_object($this->address_books[$item['id']]))
273        $this->address_books[$item['id']] = $item;
274      // remove from list if not writeable as requested
275      if ($writeable && $item['readonly'])
276          unset($list[$idx]);
277    }
278
279    return $list;
280  }
281
282
283  /**
284   * Init output object for GUI and add common scripts.
285   * This will instantiate a rcmail_template object and set
286   * environment vars according to the current session and configuration
287   *
288   * @param boolean True if this request is loaded in a (i)frame
289   * @return rcube_output_html Reference to HTML output object
290   */
291  public function load_gui($framed = false)
292  {
293    // init output page
294    if (!($this->output instanceof rcube_output_html))
295      $this->output = new rcube_output_html($this->task, $framed);
296
297    // set keep-alive/check-recent interval
298    if ($this->session && ($keep_alive = $this->session->get_keep_alive())) {
299      $this->output->set_env('keep_alive', $keep_alive);
300    }
301
302    if ($framed) {
303      $this->comm_path .= '&_framed=1';
304      $this->output->set_env('framed', true);
305    }
306
307    $this->output->set_env('task', $this->task);
308    $this->output->set_env('action', $this->action);
309    $this->output->set_env('comm_path', $this->comm_path);
310    $this->output->set_charset(RCMAIL_CHARSET);
311
312    // add some basic labels to client
313    $this->output->add_label('loading', 'servererror');
314
315    return $this->output;
316  }
317
318
319  /**
320   * Create an output object for JSON responses
321   *
322   * @return rcube_output_json Reference to JSON output object
323   */
324  public function json_init()
325  {
326    if (!($this->output instanceof rcube_output_json))
327      $this->output = new rcube_output_json($this->task);
328
329    return $this->output;
330  }
331
332
333  /**
334   * Create session object and start the session.
335   */
336  public function session_init()
337  {
338    // session started (Installer?)
339    if (session_id())
340      return;
341
342    $sess_name   = $this->config->get('session_name');
343    $sess_domain = $this->config->get('session_domain');
344    $lifetime    = $this->config->get('session_lifetime', 0) * 60;
345
346    // set session domain
347    if ($sess_domain) {
348      ini_set('session.cookie_domain', $sess_domain);
349    }
350    // set session garbage collecting time according to session_lifetime
351    if ($lifetime) {
352      ini_set('session.gc_maxlifetime', $lifetime * 2);
353    }
354
355    ini_set('session.cookie_secure', rcube_ui::https_check());
356    ini_set('session.name', $sess_name ? $sess_name : 'roundcube_sessid');
357    ini_set('session.use_cookies', 1);
358    ini_set('session.use_only_cookies', 1);
359    ini_set('session.serialize_handler', 'php');
360
361    // use database for storing session data
362    $this->session = new rcube_session($this->get_dbh(), $this->config);
363
364    $this->session->register_gc_handler(array($this, 'temp_gc'));
365    if ($this->config->get('enable_caching'))
366      $this->session->register_gc_handler(array($this, 'cache_gc'));
367
368    // start PHP session (if not in CLI mode)
369    if ($_SERVER['REMOTE_ADDR'])
370      session_start();
371
372    // set initial session vars
373    if (!$_SESSION['user_id'])
374      $_SESSION['temp'] = true;
375  }
376
377
378  /**
379   * Configure session object internals
380   */
381  public function session_configure()
382  {
383    if (!$this->session)
384      return;
385
386    $lifetime = $this->config->get('session_lifetime', 0) * 60;
387
388    // set keep-alive/check-recent interval
389    if ($keep_alive = $this->config->get('keep_alive')) {
390      // be sure that it's less than session lifetime
391      if ($lifetime)
392        $keep_alive = min($keep_alive, $lifetime - 30);
393      $keep_alive = max(60, $keep_alive);
394      $this->session->set_keep_alive($keep_alive);
395    }
396
397    $this->session->set_secret($this->config->get('des_key') . $_SERVER['HTTP_USER_AGENT']);
398    $this->session->set_ip_check($this->config->get('ip_check'));
399  }
400
401
402  /**
403   * Perfom login to the mail server and to the webmail service.
404   * This will also create a new user entry if auto_create_user is configured.
405   *
406   * @param string Mail storage (IMAP) user name
407   * @param string Mail storage (IMAP) password
408   * @param string Mail storage (IMAP) host
409   *
410   * @return boolean True on success, False on failure
411   */
412  function login($username, $pass, $host=NULL)
413  {
414    if (empty($username)) {
415      return false;
416    }
417
418    $config = $this->config->all();
419
420    if (!$host)
421      $host = $config['default_host'];
422
423    // Validate that selected host is in the list of configured hosts
424    if (is_array($config['default_host'])) {
425      $allowed = false;
426      foreach ($config['default_host'] as $key => $host_allowed) {
427        if (!is_numeric($key))
428          $host_allowed = $key;
429        if ($host == $host_allowed) {
430          $allowed = true;
431          break;
432        }
433      }
434      if (!$allowed)
435        return false;
436      }
437    else if (!empty($config['default_host']) && $host != self::parse_host($config['default_host']))
438      return false;
439
440    // parse $host URL
441    $a_host = parse_url($host);
442    if ($a_host['host']) {
443      $host = $a_host['host'];
444      $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
445      if (!empty($a_host['port']))
446        $port = $a_host['port'];
447      else if ($ssl && $ssl != 'tls' && (!$config['default_port'] || $config['default_port'] == 143))
448        $port = 993;
449    }
450
451    if (!$port) {
452        $port = $config['default_port'];
453    }
454
455    /* Modify username with domain if required
456       Inspired by Marco <P0L0_notspam_binware.org>
457    */
458    // Check if we need to add domain
459    if (!empty($config['username_domain']) && strpos($username, '@') === false) {
460      if (is_array($config['username_domain']) && isset($config['username_domain'][$host]))
461        $username .= '@'.self::parse_host($config['username_domain'][$host], $host);
462      else if (is_string($config['username_domain']))
463        $username .= '@'.self::parse_host($config['username_domain'], $host);
464    }
465
466    // Convert username to lowercase. If storage backend
467    // is case-insensitive we need to store always the same username (#1487113)
468    if ($config['login_lc']) {
469      $username = mb_strtolower($username);
470    }
471
472    // try to resolve email address from virtuser table
473    if (strpos($username, '@') && ($virtuser = rcube_user::email2user($username))) {
474      $username = $virtuser;
475    }
476
477    // Here we need IDNA ASCII
478    // Only rcube_contacts class is using domain names in Unicode
479    $host = rcube_idn_to_ascii($host);
480    if (strpos($username, '@')) {
481      // lowercase domain name
482      list($local, $domain) = explode('@', $username);
483      $username = $local . '@' . mb_strtolower($domain);
484      $username = rcube_idn_to_ascii($username);
485    }
486
487    // user already registered -> overwrite username
488    if ($user = rcube_user::query($username, $host))
489      $username = $user->data['username'];
490
491    if (!$this->storage)
492      $this->storage_init();
493
494    // try to log in
495    if (!($login = $this->storage->connect($host, $username, $pass, $port, $ssl))) {
496      // try with lowercase
497      $username_lc = mb_strtolower($username);
498      if ($username_lc != $username) {
499        // try to find user record again -> overwrite username
500        if (!$user && ($user = rcube_user::query($username_lc, $host)))
501          $username_lc = $user->data['username'];
502
503        if ($login = $this->storage->connect($host, $username_lc, $pass, $port, $ssl))
504          $username = $username_lc;
505      }
506    }
507
508    // exit if login failed
509    if (!$login) {
510      return false;
511    }
512
513    // user already registered -> update user's record
514    if (is_object($user)) {
515      // update last login timestamp
516      $user->touch();
517    }
518    // create new system user
519    else if ($config['auto_create_user']) {
520      if ($created = rcube_user::create($username, $host)) {
521        $user = $created;
522      }
523      else {
524        self::raise_error(array(
525          'code' => 620, 'type' => 'php',
526          'file' => __FILE__, 'line' => __LINE__,
527          'message' => "Failed to create a user record. Maybe aborted by a plugin?"
528          ), true, false);
529      }
530    }
531    else {
532      self::raise_error(array(
533        'code' => 621, 'type' => 'php',
534        'file' => __FILE__, 'line' => __LINE__,
535        'message' => "Access denied for new user $username. 'auto_create_user' is disabled"
536        ), true, false);
537    }
538
539    // login succeeded
540    if (is_object($user) && $user->ID) {
541      // Configure environment
542      $this->set_user($user);
543      $this->set_storage_prop();
544      $this->session_configure();
545
546      // fix some old settings according to namespace prefix
547      $this->fix_namespace_settings($user);
548
549      // create default folders on first login
550      if ($config['create_default_folders'] && (!empty($created) || empty($user->data['last_login']))) {
551        $this->storage->create_default_folders();
552      }
553
554      // set session vars
555      $_SESSION['user_id']      = $user->ID;
556      $_SESSION['username']     = $user->data['username'];
557      $_SESSION['storage_host'] = $host;
558      $_SESSION['storage_port'] = $port;
559      $_SESSION['storage_ssl']  = $ssl;
560      $_SESSION['password']     = $this->encrypt($pass);
561      $_SESSION['login_time']   = mktime();
562
563      if (isset($_REQUEST['_timezone']) && $_REQUEST['_timezone'] != '_default_')
564        $_SESSION['timezone'] = floatval($_REQUEST['_timezone']);
565      if (isset($_REQUEST['_dstactive']) && $_REQUEST['_dstactive'] != '_default_')
566        $_SESSION['dst_active'] = intval($_REQUEST['_dstactive']);
567
568      // force reloading complete list of subscribed mailboxes
569      $this->storage->clear_cache('mailboxes', true);
570
571      return true;
572    }
573
574    return false;
575  }
576
577
578  /**
579   * Auto-select IMAP host based on the posted login information
580   *
581   * @return string Selected IMAP host
582   */
583  public function autoselect_host()
584  {
585    $default_host = $this->config->get('default_host');
586    $host = null;
587
588    if (is_array($default_host)) {
589      $post_host = rcube_ui::get_input_value('_host', rcube_ui::INPUT_POST);
590
591      // direct match in default_host array
592      if ($default_host[$post_host] || in_array($post_host, array_values($default_host))) {
593        $host = $post_host;
594      }
595
596      // try to select host by mail domain
597      list($user, $domain) = explode('@', rcube_ui::get_input_value('_user', rcube_ui::INPUT_POST));
598      if (!empty($domain)) {
599        foreach ($default_host as $storage_host => $mail_domains) {
600          if (is_array($mail_domains) && in_array($domain, $mail_domains)) {
601            $host = $storage_host;
602            break;
603          }
604        }
605      }
606
607      // take the first entry if $host is still an array
608      if (empty($host)) {
609        $host = array_shift($default_host);
610      }
611    }
612    else if (empty($default_host)) {
613      $host = rcube_ui::get_input_value('_host', rcube_ui::INPUT_POST);
614    }
615    else
616      $host = self::parse_host($default_host);
617
618    return $host;
619  }
620
621
622  /**
623   * Destroy session data and remove cookie
624   */
625  public function kill_session()
626  {
627    $this->plugins->exec_hook('session_destroy');
628
629    $this->session->kill();
630    $_SESSION = array('language' => $this->user->language, 'temp' => true);
631    $this->user->reset();
632  }
633
634
635  /**
636   * Do server side actions on logout
637   */
638  public function logout_actions()
639  {
640    $config = $this->config->all();
641
642    // on logout action we're not connected to imap server
643    if (($config['logout_purge'] && !empty($config['trash_mbox'])) || $config['logout_expunge']) {
644      if (!$this->session->check_auth())
645        return;
646
647      $this->storage_connect();
648    }
649
650    if ($config['logout_purge'] && !empty($config['trash_mbox'])) {
651      $this->storage->clear_folder($config['trash_mbox']);
652    }
653
654    if ($config['logout_expunge']) {
655      $this->storage->expunge_folder('INBOX');
656    }
657
658    // Try to save unsaved user preferences
659    if (!empty($_SESSION['preferences'])) {
660      $this->user->save_prefs(unserialize($_SESSION['preferences']));
661    }
662  }
663
664
665  /**
666   * Generate a unique token to be used in a form request
667   *
668   * @return string The request token
669   */
670  public function get_request_token()
671  {
672    $sess_id = $_COOKIE[ini_get('session.name')];
673    if (!$sess_id) $sess_id = session_id();
674    $plugin = $this->plugins->exec_hook('request_token', array('value' => md5('RT' . $this->get_user_id() . $this->config->get('des_key') . $sess_id)));
675    return $plugin['value'];
676  }
677
678
679  /**
680   * Check if the current request contains a valid token
681   *
682   * @param int Request method
683   * @return boolean True if request token is valid false if not
684   */
685  public function check_request($mode = rcube_ui::INPUT_POST)
686  {
687    $token = rcube_ui::get_input_value('_token', $mode);
688    $sess_id = $_COOKIE[ini_get('session.name')];
689    return !empty($sess_id) && $token == $this->get_request_token();
690  }
691
692
693  /**
694   * Create unique authorization hash
695   *
696   * @param string Session ID
697   * @param int Timestamp
698   * @return string The generated auth hash
699   */
700  private function get_auth_hash($sess_id, $ts)
701  {
702    $auth_string = sprintf('rcmail*sess%sR%s*Chk:%s;%s',
703      $sess_id,
704      $ts,
705      $this->config->get('ip_check') ? $_SERVER['REMOTE_ADDR'] : '***.***.***.***',
706      $_SERVER['HTTP_USER_AGENT']);
707
708    if (function_exists('sha1'))
709      return sha1($auth_string);
710    else
711      return md5($auth_string);
712  }
713
714
715  /**
716   * Build a valid URL to this instance of Roundcube
717   *
718   * @param mixed Either a string with the action or url parameters as key-value pairs
719   * @return string Valid application URL
720   */
721  public function url($p)
722  {
723    if (!is_array($p))
724      $p = array('_action' => @func_get_arg(0));
725
726    $task = $p['_task'] ? $p['_task'] : ($p['task'] ? $p['task'] : $this->task);
727    $p['_task'] = $task;
728    unset($p['task']);
729
730    $url = './';
731    $delm = '?';
732    foreach (array_reverse($p) as $key => $val) {
733      if ($val !== '') {
734        $par = $key[0] == '_' ? $key : '_'.$key;
735        $url .= $delm.urlencode($par).'='.urlencode($val);
736        $delm = '&';
737      }
738    }
739    return $url;
740  }
741
742
743  /**
744   * Helper method to set a cookie with the current path and host settings
745   *
746   * @param string Cookie name
747   * @param string Cookie value
748   * @param string Expiration time
749   */
750  public static function setcookie($name, $value, $exp = 0)
751  {
752    if (headers_sent())
753      return;
754
755    $cookie = session_get_cookie_params();
756
757    setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'],
758      rcube_ui::https_check(), true);
759  }
760
761  /**
762   * Registers action aliases for current task
763   *
764   * @param array $map Alias-to-filename hash array
765   */
766  public function register_action_map($map)
767  {
768    if (is_array($map)) {
769      foreach ($map as $idx => $val) {
770        $this->action_map[$idx] = $val;
771      }
772    }
773  }
774
775  /**
776   * Returns current action filename
777   *
778   * @param array $map Alias-to-filename hash array
779   */
780  public function get_action_file()
781  {
782    if (!empty($this->action_map[$this->action])) {
783      return $this->action_map[$this->action];
784    }
785
786    return strtr($this->action, '-', '_') . '.inc';
787  }
788
789  /**
790   * Fixes some user preferences according to namespace handling change.
791   * Old Roundcube versions were using folder names with removed namespace prefix.
792   * Now we need to add the prefix on servers where personal namespace has prefix.
793   *
794   * @param rcube_user $user User object
795   */
796  private function fix_namespace_settings($user)
797  {
798    $prefix     = $this->storage->get_namespace('prefix');
799    $prefix_len = strlen($prefix);
800
801    if (!$prefix_len)
802      return;
803
804    $prefs = $this->config->all();
805    if (!empty($prefs['namespace_fixed']))
806      return;
807
808    // Build namespace prefix regexp
809    $ns     = $this->storage->get_namespace();
810    $regexp = array();
811
812    foreach ($ns as $entry) {
813      if (!empty($entry)) {
814        foreach ($entry as $item) {
815          if (strlen($item[0])) {
816            $regexp[] = preg_quote($item[0], '/');
817          }
818        }
819      }
820    }
821    $regexp = '/^('. implode('|', $regexp).')/';
822
823    // Fix preferences
824    $opts = array('drafts_mbox', 'junk_mbox', 'sent_mbox', 'trash_mbox', 'archive_mbox');
825    foreach ($opts as $opt) {
826      if ($value = $prefs[$opt]) {
827        if ($value != 'INBOX' && !preg_match($regexp, $value)) {
828          $prefs[$opt] = $prefix.$value;
829        }
830      }
831    }
832
833    if (!empty($prefs['default_folders'])) {
834      foreach ($prefs['default_folders'] as $idx => $name) {
835        if ($name != 'INBOX' && !preg_match($regexp, $name)) {
836          $prefs['default_folders'][$idx] = $prefix.$name;
837        }
838      }
839    }
840
841    if (!empty($prefs['search_mods'])) {
842      $folders = array();
843      foreach ($prefs['search_mods'] as $idx => $value) {
844        if ($idx != 'INBOX' && $idx != '*' && !preg_match($regexp, $idx)) {
845          $idx = $prefix.$idx;
846        }
847        $folders[$idx] = $value;
848      }
849      $prefs['search_mods'] = $folders;
850    }
851
852    if (!empty($prefs['message_threading'])) {
853      $folders = array();
854      foreach ($prefs['message_threading'] as $idx => $value) {
855        if ($idx != 'INBOX' && !preg_match($regexp, $idx)) {
856          $idx = $prefix.$idx;
857        }
858        $folders[$prefix.$idx] = $value;
859      }
860      $prefs['message_threading'] = $folders;
861    }
862
863    if (!empty($prefs['collapsed_folders'])) {
864      $folders     = explode('&&', $prefs['collapsed_folders']);
865      $count       = count($folders);
866      $folders_str = '';
867
868      if ($count) {
869          $folders[0]        = substr($folders[0], 1);
870          $folders[$count-1] = substr($folders[$count-1], 0, -1);
871      }
872
873      foreach ($folders as $value) {
874        if ($value != 'INBOX' && !preg_match($regexp, $value)) {
875          $value = $prefix.$value;
876        }
877        $folders_str .= '&'.$value.'&';
878      }
879      $prefs['collapsed_folders'] = $folders_str;
880    }
881
882    $prefs['namespace_fixed'] = true;
883
884    // save updated preferences and reset imap settings (default folders)
885    $user->save_prefs($prefs);
886    $this->set_storage_prop();
887  }
888
889
890    /**
891     * Overwrite action variable
892     *
893     * @param string New action value
894     */
895    public function overwrite_action($action)
896    {
897        $this->action = $action;
898        $this->output->set_env('action', $action);
899    }
900
901
902    /**
903     * Send the given message using the configured method.
904     *
905     * @param object $message    Reference to Mail_MIME object
906     * @param string $from       Sender address string
907     * @param array  $mailto     Array of recipient address strings
908     * @param array  $smtp_error SMTP error array (reference)
909     * @param string $body_file  Location of file with saved message body (reference),
910     *                           used when delay_file_io is enabled
911     * @param array  $smtp_opts  SMTP options (e.g. DSN request)
912     *
913     * @return boolean Send status.
914     */
915    public function deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file = null, $smtp_opts = null)
916    {
917        $headers = $message->headers();
918
919        // send thru SMTP server using custom SMTP library
920        if ($this->config->get('smtp_server')) {
921            // generate list of recipients
922            $a_recipients = array($mailto);
923
924            if (strlen($headers['Cc']))
925                $a_recipients[] = $headers['Cc'];
926            if (strlen($headers['Bcc']))
927                $a_recipients[] = $headers['Bcc'];
928
929            // clean Bcc from header for recipients
930            $send_headers = $headers;
931            unset($send_headers['Bcc']);
932            // here too, it because txtHeaders() below use $message->_headers not only $send_headers
933            unset($message->_headers['Bcc']);
934
935            $smtp_headers = $message->txtHeaders($send_headers, true);
936
937            if ($message->getParam('delay_file_io')) {
938                // use common temp dir
939                $temp_dir = $this->config->get('temp_dir');
940                $body_file = tempnam($temp_dir, 'rcmMsg');
941                if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
942                    self::raise_error(array('code' => 650, 'type' => 'php',
943                        'file' => __FILE__, 'line' => __LINE__,
944                        'message' => "Could not create message: ".$mime_result->getMessage()),
945                        TRUE, FALSE);
946                    return false;
947                }
948                $msg_body = fopen($body_file, 'r');
949            }
950            else {
951                $msg_body = $message->get();
952            }
953
954            // send message
955            if (!is_object($this->smtp)) {
956                $this->smtp_init(true);
957            }
958
959            $sent = $this->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
960            $smtp_response = $this->smtp->get_response();
961            $smtp_error = $this->smtp->get_error();
962
963            // log error
964            if (!$sent) {
965                self::raise_error(array('code' => 800, 'type' => 'smtp',
966                    'line' => __LINE__, 'file' => __FILE__,
967                    'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
968            }
969        }
970        // send mail using PHP's mail() function
971        else {
972            // unset some headers because they will be added by the mail() function
973            $headers_enc = $message->headers($headers);
974            $headers_php = $message->_headers;
975            unset($headers_php['To'], $headers_php['Subject']);
976
977            // reset stored headers and overwrite
978            $message->_headers = array();
979            $header_str = $message->txtHeaders($headers_php);
980
981            // #1485779
982            if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
983                if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
984                    $headers_enc['To'] = implode(', ', $m[1]);
985                }
986            }
987
988            $msg_body = $message->get();
989
990            if (PEAR::isError($msg_body)) {
991                self::raise_error(array('code' => 650, 'type' => 'php',
992                    'file' => __FILE__, 'line' => __LINE__,
993                    'message' => "Could not create message: ".$msg_body->getMessage()),
994                    TRUE, FALSE);
995            }
996            else {
997                $delim   = $this->config->header_delimiter();
998                $to      = $headers_enc['To'];
999                $subject = $headers_enc['Subject'];
1000                $header_str = rtrim($header_str);
1001
1002                if ($delim != "\r\n") {
1003                    $header_str = str_replace("\r\n", $delim, $header_str);
1004                    $msg_body   = str_replace("\r\n", $delim, $msg_body);
1005                    $to         = str_replace("\r\n", $delim, $to);
1006                    $subject    = str_replace("\r\n", $delim, $subject);
1007                }
1008
1009                if (ini_get('safe_mode'))
1010                    $sent = mail($to, $subject, $msg_body, $header_str);
1011                else
1012                    $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1013            }
1014        }
1015
1016        if ($sent) {
1017            $this->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1018
1019            // remove MDN headers after sending
1020            unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1021
1022            // get all recipients
1023            if ($headers['Cc'])
1024                $mailto .= $headers['Cc'];
1025            if ($headers['Bcc'])
1026                $mailto .= $headers['Bcc'];
1027            if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1028                $mailto = implode(', ', array_unique($m[1]));
1029
1030            if ($this->config->get('smtp_log')) {
1031                self::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1032                    $this->user->get_username(),
1033                    $_SERVER['REMOTE_ADDR'],
1034                    $mailto,
1035                    !empty($smtp_response) ? join('; ', $smtp_response) : ''));
1036            }
1037        }
1038
1039        if (is_resource($msg_body)) {
1040            fclose($msg_body);
1041        }
1042
1043        $message->_headers = array();
1044        $message->headers($headers);
1045
1046        return $sent;
1047    }
1048
1049
1050    /**
1051     * Unique Message-ID generator.
1052     *
1053     * @return string Message-ID
1054     */
1055    public function gen_message_id()
1056    {
1057        $local_part  = md5(uniqid('rcmail'.mt_rand(),true));
1058        $domain_part = $this->user->get_username('domain');
1059
1060        // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1061        if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1062            foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
1063                $host = preg_replace('/:[0-9]+$/', '', $host);
1064                if ($host && preg_match('/\.[a-z]+$/i', $host)) {
1065                    $domain_part = $host;
1066                }
1067            }
1068        }
1069
1070        return sprintf('<%s@%s>', $local_part, $domain_part);
1071    }
1072
1073
1074    /**
1075     * Returns RFC2822 formatted current date in user's timezone
1076     *
1077     * @return string Date
1078     */
1079    public function user_date()
1080    {
1081        // get user's timezone
1082        try {
1083            $tz   = new DateTimeZone($this->config->get('timezone'));
1084            $date = new DateTime('now', $tz);
1085        }
1086        catch (Exception $e) {
1087            $date = new DateTime();
1088        }
1089
1090        return $date->format('r');
1091    }
1092
1093
1094    /**
1095     * E-mail address validation.
1096     *
1097     * @param string $email Email address
1098     * @param boolean $dns_check True to check dns
1099     *
1100     * @return boolean True on success, False if address is invalid
1101     */
1102    public function check_email($email, $dns_check=true)
1103    {
1104        // Check for invalid characters
1105        if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email)) {
1106            return false;
1107        }
1108
1109        // Check for length limit specified by RFC 5321 (#1486453)
1110        if (strlen($email) > 254) {
1111            return false;
1112        }
1113
1114        $email_array = explode('@', $email);
1115
1116        // Check that there's one @ symbol
1117        if (count($email_array) < 2) {
1118            return false;
1119        }
1120
1121        $domain_part = array_pop($email_array);
1122        $local_part  = implode('@', $email_array);
1123
1124        // from PEAR::Validate
1125        $regexp = '&^(?:
1126                ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")|                                         #1 quoted name
1127                ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*))  #2 OR dot-atom (RFC5322)
1128                $&xi';
1129
1130        if (!preg_match($regexp, $local_part)) {
1131            return false;
1132        }
1133
1134        // Check domain part
1135        if (preg_match('/^\[*(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}\]*$/', $domain_part)) {
1136            return true; // IP address
1137        }
1138        else {
1139            // If not an IP address
1140            $domain_array = explode('.', $domain_part);
1141            // Not enough parts to be a valid domain
1142            if (sizeof($domain_array) < 2) {
1143                return false;
1144            }
1145
1146            foreach ($domain_array as $part) {
1147                if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
1148                    return false;
1149                }
1150            }
1151
1152            if (!$dns_check || !$this->config->get('email_dns_check')) {
1153                return true;
1154            }
1155
1156            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1157                $lookup = array();
1158                @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1159                foreach ($lookup as $line) {
1160                    if (strpos($line, 'MX preference')) {
1161                        return true;
1162                    }
1163                }
1164                return false;
1165            }
1166
1167            // find MX record(s)
1168            if (getmxrr($domain_part, $mx_records)) {
1169                return true;
1170            }
1171
1172            // find any DNS record
1173            if (checkdnsrr($domain_part, 'ANY')) {
1174                return true;
1175            }
1176        }
1177
1178        return false;
1179    }
1180
1181
1182    /**
1183     * Write login data (name, ID, IP address) to the 'userlogins' log file.
1184     */
1185    public function log_login()
1186    {
1187        if (!$this->config->get('log_logins')) {
1188            return;
1189        }
1190
1191        $user_name = $this->get_user_name();
1192        $user_id   = $this->get_user_id();
1193
1194        if (!$user_id) {
1195            return;
1196        }
1197
1198        self::write_log('userlogins',
1199            sprintf('Successful login for %s (ID: %d) from %s in session %s',
1200                $user_name, $user_id, self::remote_ip(), session_id()));
1201    }
1202
1203
1204    /**
1205     * Check whether the HTTP referer matches the current request
1206     *
1207     * @return boolean True if referer is the same host+path, false if not
1208     */
1209    public static function check_referer()
1210    {
1211        $uri = parse_url($_SERVER['REQUEST_URI']);
1212        $referer = parse_url(rcube_request_header('Referer'));
1213        return $referer['host'] == rcube_request_header('Host') && $referer['path'] == $uri['path'];
1214    }
1215
1216
1217    /**
1218     * Garbage collector function for temp files.
1219     * Remove temp files older than two days
1220     */
1221    public function temp_gc()
1222    {
1223        $tmp = unslashify($this->config->get('temp_dir'));
1224        $expire = mktime() - 172800;  // expire in 48 hours
1225
1226        if ($dir = opendir($tmp)) {
1227            while (($fname = readdir($dir)) !== false) {
1228                if ($fname{0} == '.') {
1229                    continue;
1230                }
1231
1232                if (filemtime($tmp.'/'.$fname) < $expire) {
1233                    @unlink($tmp.'/'.$fname);
1234                }
1235            }
1236
1237            closedir($dir);
1238        }
1239    }
1240
1241
1242    /**
1243     * Garbage collector for cache entries.
1244     * Remove all expired message cache records
1245     */
1246    public function cache_gc()
1247    {
1248        $db = $this->get_dbh();
1249
1250        // get target timestamp
1251        $ts = get_offset_time($this->config->get('message_cache_lifetime', '30d'), -1);
1252
1253        $db->query("DELETE FROM " . $db->table_name('cache_messages')
1254            ." WHERE changed < " . $db->fromunixtime($ts));
1255
1256        $db->query("DELETE FROM " . $db->table_name('cache_index')
1257            ." WHERE changed < " . $db->fromunixtime($ts));
1258
1259        $db->query("DELETE FROM " . $db->table_name('cache_thread')
1260            ." WHERE changed < " . $db->fromunixtime($ts));
1261
1262        $db->query("DELETE FROM " . $db->table_name('cache')
1263            ." WHERE created < " . $db->fromunixtime($ts));
1264    }
1265
1266}
Note: See TracBrowser for help on using the repository browser.