4 +-----------------------------------------------------------------------+
5 | program/include/main.inc |
7 | This file is part of the Roundcube Webmail client |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team |
9 | Licensed under the GNU GPL |
12 | Provide basic functions for the webmail package |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com> |
16 +-----------------------------------------------------------------------+
18 $Id: main.inc 5715 2012-01-05 10:28:24Z alec $
23 * Roundcube Webmail common functions
26 * @author Thomas Bruederli <roundcube@gmail.com>
29 require_once 'utf7.inc';
30 require_once INSTALL_PATH . 'program/include/rcube_shared.inc';
32 // define constannts for input reading
33 define('RCUBE_INPUT_GET', 0x0101);
34 define('RCUBE_INPUT_POST', 0x0102);
35 define('RCUBE_INPUT_GPC', 0x0103);
40 * Return correct name for a specific database table
42 * @param string Table name
43 * @return string Translated table name
45 function get_table_name($table)
49 // return table name if configured
50 $config_key = 'db_table_'.$table;
52 if (strlen($CONFIG[$config_key]))
53 return $CONFIG[$config_key];
60 * Return correct name for a specific database sequence
61 * (used for Postgres only)
63 * @param string Secuence name
64 * @return string Translated sequence name
66 function get_sequence_name($sequence)
68 // return sequence name if configured
69 $config_key = 'db_sequence_'.$sequence;
70 $opt = rcmail::get_instance()->config->get($config_key);
80 * Get localized text in the desired language
81 * It's a global wrapper for rcmail::gettext()
83 * @param mixed Named parameters array or label name
84 * @param string Domain to search in (e.g. plugin name)
85 * @return string Localized text
86 * @see rcmail::gettext()
88 function rcube_label($p, $domain=null)
90 return rcmail::get_instance()->gettext($p, $domain);
95 * Global wrapper of rcmail::text_exists()
96 * to check whether a text label is defined
98 * @see rcmail::text_exists()
100 function rcube_label_exists($name, $domain=null, &$ref_domain = null)
102 return rcmail::get_instance()->text_exists($name, $domain, $ref_domain);
107 * Overwrite action variable
109 * @param string New action value
111 function rcmail_overwrite_action($action)
113 $app = rcmail::get_instance();
114 $app->action = $action;
115 $app->output->set_env('action', $action);
120 * Compose an URL for a specific action
122 * @param string Request action
123 * @param array More URL parameters
124 * @param string Request task (omit if the same)
125 * @return The application URL
127 function rcmail_url($action, $p=array(), $task=null)
129 $app = rcmail::get_instance();
130 return $app->url((array)$p + array('_action' => $action, 'task' => $task));
135 * Garbage collector function for temp files.
136 * Remove temp files older than two days
138 function rcmail_temp_gc()
140 $rcmail = rcmail::get_instance();
142 $tmp = unslashify($rcmail->config->get('temp_dir'));
143 $expire = mktime() - 172800; // expire in 48 hours
145 if ($dir = opendir($tmp)) {
146 while (($fname = readdir($dir)) !== false) {
147 if ($fname{0} == '.')
150 if (filemtime($tmp.'/'.$fname) < $expire)
151 @unlink($tmp.'/'.$fname);
160 * Garbage collector for cache entries.
161 * Remove all expired message cache records
164 function rcmail_cache_gc()
166 $rcmail = rcmail::get_instance();
167 $db = $rcmail->get_dbh();
169 // get target timestamp
170 $ts = get_offset_time($rcmail->config->get('message_cache_lifetime', '30d'), -1);
172 $db->query("DELETE FROM ".get_table_name('cache_messages')
173 ." WHERE changed < " . $db->fromunixtime($ts));
175 $db->query("DELETE FROM ".get_table_name('cache_index')
176 ." WHERE changed < " . $db->fromunixtime($ts));
178 $db->query("DELETE FROM ".get_table_name('cache_thread')
179 ." WHERE changed < " . $db->fromunixtime($ts));
181 $db->query("DELETE FROM ".get_table_name('cache')
182 ." WHERE created < " . $db->fromunixtime($ts));
187 * Catch an error and throw an exception.
189 * @param int Level of the error
190 * @param string Error message
192 function rcube_error_handler($errno, $errstr)
194 throw new ErrorException($errstr, 0, $errno);
199 * Convert a string from one charset to another.
200 * Uses mbstring and iconv functions if possible
202 * @param string Input string
203 * @param string Suspected charset of the input string
204 * @param string Target charset to convert to; defaults to RCMAIL_CHARSET
205 * @return string Converted string
207 function rcube_charset_convert($str, $from, $to=NULL)
209 static $iconv_options = null;
210 static $mbstring_loaded = null;
211 static $mbstring_list = null;
216 $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : rcube_parse_charset($to);
217 $from = rcube_parse_charset($from);
219 if ($from == $to || empty($str) || empty($from))
222 // convert charset using iconv module
223 if (function_exists('iconv') && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
224 if ($iconv_options === null) {
225 // ignore characters not available in output charset
226 $iconv_options = '//IGNORE';
227 if (iconv('', $iconv_options, '') === false) {
228 // iconv implementation does not support options
233 // throw an exception if iconv reports an illegal character in input
234 // it means that input string has been truncated
235 set_error_handler('rcube_error_handler', E_NOTICE);
237 $_iconv = iconv($from, $to . $iconv_options, $str);
238 } catch (ErrorException $e) {
241 restore_error_handler();
242 if ($_iconv !== false) {
247 if ($mbstring_loaded === null)
248 $mbstring_loaded = extension_loaded('mbstring');
250 // convert charset using mbstring module
251 if ($mbstring_loaded) {
252 $aliases['WINDOWS-1257'] = 'ISO-8859-13';
254 if ($mbstring_list === null) {
255 $mbstring_list = mb_list_encodings();
256 $mbstring_list = array_map('strtoupper', $mbstring_list);
259 $mb_from = $aliases[$from] ? $aliases[$from] : $from;
260 $mb_to = $aliases[$to] ? $aliases[$to] : $to;
262 // return if encoding found, string matches encoding and convert succeeded
263 if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
264 if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from)))
269 // convert charset using bundled classes/functions
270 if ($to == 'UTF-8') {
271 if ($from == 'UTF7-IMAP') {
272 if ($_str = utf7_to_utf8($str))
275 else if ($from == 'UTF-7') {
276 if ($_str = rcube_utf7_to_utf8($str))
279 else if (($from == 'ISO-8859-1') && function_exists('utf8_encode')) {
280 return utf8_encode($str);
282 else if (class_exists('utf8')) {
284 $conv = new utf8($from);
286 $conv->loadCharset($from);
288 if($_str = $conv->strToUtf8($str))
294 // encode string for output
295 if ($from == 'UTF-8') {
296 // @TODO: we need a function for UTF-7 (RFC2152) conversion
297 if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
298 if ($_str = utf8_to_utf7($str))
301 else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
302 return utf8_decode($str);
304 else if (class_exists('utf8')) {
306 $conv = new utf8($to);
308 $conv->loadCharset($from);
310 if ($_str = $conv->strToUtf8($str))
316 // return UTF-8 or original string
322 * Parse and validate charset name string (see #1485758).
323 * Sometimes charset string is malformed, there are also charset aliases
324 * but we need strict names for charset conversion (specially utf8 class)
326 * @param string Input charset name
327 * @return string The validated charset name
329 function rcube_parse_charset($input)
331 static $charsets = array();
332 $charset = strtoupper($input);
334 if (isset($charsets[$input]))
335 return $charsets[$input];
337 $charset = preg_replace(array(
338 '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO
339 '/\$.*$/', // e.g. _ISO-8859-JP$SIO
340 '/UNICODE-1-1-*/', // RFC1641/1642
341 '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8)
344 if ($charset == 'BINARY')
345 return $charsets[$input] = null;
347 # Aliases: some of them from HTML5 spec.
349 'USASCII' => 'WINDOWS-1252',
350 'ANSIX31101983' => 'WINDOWS-1252',
351 'ANSIX341968' => 'WINDOWS-1252',
352 'UNKNOWN8BIT' => 'ISO-8859-15',
353 'UNKNOWN' => 'ISO-8859-15',
354 'USERDEFINED' => 'ISO-8859-15',
355 'KSC56011987' => 'EUC-KR',
358 'UNICODE' => 'UTF-8',
359 'UTF7IMAP' => 'UTF7-IMAP',
360 'TIS620' => 'WINDOWS-874',
361 'ISO88599' => 'WINDOWS-1254',
362 'ISO885911' => 'WINDOWS-874',
363 'MACROMAN' => 'MACINTOSH',
365 '128' => 'SHIFT-JIS',
370 '161' => 'WINDOWS-1253',
371 '162' => 'WINDOWS-1254',
372 '163' => 'WINDOWS-1258',
373 '177' => 'WINDOWS-1255',
374 '178' => 'WINDOWS-1256',
375 '186' => 'WINDOWS-1257',
376 '204' => 'WINDOWS-1251',
377 '222' => 'WINDOWS-874',
378 '238' => 'WINDOWS-1250',
380 'WINDOWS949' => 'UHC',
383 // allow A-Z and 0-9 only
384 $str = preg_replace('/[^A-Z0-9]/', '', $charset);
386 if (isset($aliases[$str]))
387 $result = $aliases[$str];
389 else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m))
390 $result = 'UTF-' . $m[1] . $m[2];
392 else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
393 $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
394 // some clients sends windows-1252 text as latin1,
395 // it is safe to use windows-1252 for all latin1
396 $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
398 // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
399 else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
400 $result = 'WINDOWS-' . $m[2];
403 else if (preg_match('/LATIN(.*)/', $str, $m)) {
404 $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
405 '7' => 13, '8' => 14, '9' => 15, '10' => 16,
406 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8);
408 // some clients sends windows-1252 text as latin1,
409 // it is safe to use windows-1252 for all latin1
411 $result = 'WINDOWS-1252';
413 // if iconv is not supported we need ISO labels, it's also safe for iconv
414 else if (!empty($aliases[$m[1]])) {
415 $result = 'ISO-8859-'.$aliases[$m[1]];
417 // iconv requires convertion of e.g. LATIN-1 to LATIN1
426 $charsets[$input] = $result;
433 * Converts string from standard UTF-7 (RFC 2152) to UTF-8.
435 * @param string Input string
436 * @return string The converted string
438 function rcube_utf7_to_utf8($str)
441 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
442 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
443 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
444 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
445 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
446 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
447 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
448 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
451 $u7len = strlen($str);
455 for ($i=0; $u7len > 0; $i++, $u7len--)
464 for (; $u7len > 0; $i++, $u7len--)
468 if (!$Index_64[ord($u7)])
480 $res .= rcube_utf16_to_utf8(base64_decode($ch));
492 * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
494 * @param string Input string
495 * @return string The converted string
497 function rcube_utf16_to_utf8($str)
502 for ($i = 0; $i < $len; $i += 2) {
503 $c = ord($str[$i]) << 8 | ord($str[$i + 1]);
504 if ($c >= 0x0001 && $c <= 0x007F) {
506 } else if ($c > 0x07FF) {
507 $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
508 $dec .= chr(0x80 | (($c >> 6) & 0x3F));
509 $dec .= chr(0x80 | (($c >> 0) & 0x3F));
511 $dec .= chr(0xC0 | (($c >> 6) & 0x1F));
512 $dec .= chr(0x80 | (($c >> 0) & 0x3F));
520 * Replacing specials characters to a specific encoding type
522 * @param string Input string
523 * @param string Encoding type: text|html|xml|js|url
524 * @param string Replace mode for tags: show|replace|remove
525 * @param boolean Convert newlines
526 * @return string The quoted string
528 function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE)
530 static $html_encode_arr = false;
531 static $js_rep_table = false;
532 static $xml_rep_table = false;
535 $enctype = $OUTPUT->type;
537 // encode for HTML output
538 if ($enctype=='html')
540 if (!$html_encode_arr)
542 $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
543 unset($html_encode_arr['?']);
546 $ltpos = strpos($str, '<');
547 $encode_arr = $html_encode_arr;
549 // don't replace quotes and html tags
550 if (($mode=='show' || $mode=='') && $ltpos!==false && strpos($str, '>', $ltpos)!==false)
552 unset($encode_arr['"']);
553 unset($encode_arr['<']);
554 unset($encode_arr['>']);
555 unset($encode_arr['&']);
557 else if ($mode=='remove')
558 $str = strip_tags($str);
560 $out = strtr($str, $encode_arr);
562 // avoid douple quotation of &
563 $out = preg_replace('/&([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
565 return $newlines ? nl2br($out) : $out;
568 // if the replace tables for XML and JS are not yet defined
569 if ($js_rep_table===false)
571 $js_rep_table = $xml_rep_table = array();
572 $xml_rep_table['&'] = '&';
574 for ($c=160; $c<256; $c++) // can be increased to support more charsets
575 $xml_rep_table[chr($c)] = "&#$c;";
577 $xml_rep_table['"'] = '"';
578 $js_rep_table['"'] = '\\"';
579 $js_rep_table["'"] = "\\'";
580 $js_rep_table["\\"] = "\\\\";
581 // Unicode line and paragraph separators (#1486310)
582 $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '
';
583 $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '
';
586 // encode for javascript use
588 return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
590 // encode for plaintext
591 if ($enctype=='text')
592 return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
595 return rawurlencode($str);
599 return strtr($str, $xml_rep_table);
601 // no encoding given -> return original string
606 * Quote a given string.
607 * Shortcut function for rep_specialchars_output
609 * @return string HTML-quoted string
610 * @see rep_specialchars_output()
612 function Q($str, $mode='strict', $newlines=TRUE)
614 return rep_specialchars_output($str, 'html', $mode, $newlines);
618 * Quote a given string for javascript output.
619 * Shortcut function for rep_specialchars_output
621 * @return string JS-quoted string
622 * @see rep_specialchars_output()
626 return rep_specialchars_output($str, 'js');
631 * Read input value and convert it for internal use
632 * Performs stripslashes() and charset conversion if necessary
634 * @param string Field name to read
635 * @param int Source to get value from (GPC)
636 * @param boolean Allow HTML tags in field value
637 * @param string Charset to convert into
638 * @return string Field value or NULL if not available
640 function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
644 if ($source == RCUBE_INPUT_GET) {
645 if (isset($_GET[$fname]))
646 $value = $_GET[$fname];
648 else if ($source == RCUBE_INPUT_POST) {
649 if (isset($_POST[$fname]))
650 $value = $_POST[$fname];
652 else if ($source == RCUBE_INPUT_GPC) {
653 if (isset($_POST[$fname]))
654 $value = $_POST[$fname];
655 else if (isset($_GET[$fname]))
656 $value = $_GET[$fname];
657 else if (isset($_COOKIE[$fname]))
658 $value = $_COOKIE[$fname];
661 return parse_input_value($value, $allow_html, $charset);
665 * Parse/validate input value. See get_input_value()
666 * Performs stripslashes() and charset conversion if necessary
668 * @param string Input value
669 * @param boolean Allow HTML tags in field value
670 * @param string Charset to convert into
671 * @return string Parsed value
673 function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
680 if (is_array($value)) {
681 foreach ($value as $idx => $val)
682 $value[$idx] = parse_input_value($val, $allow_html, $charset);
686 // strip single quotes if magic_quotes_sybase is enabled
687 if (ini_get('magic_quotes_sybase'))
688 $value = str_replace("''", "'", $value);
689 // strip slashes if magic_quotes enabled
690 else if (get_magic_quotes_gpc() || get_magic_quotes_runtime())
691 $value = stripslashes($value);
693 // remove HTML tags if not allowed
695 $value = strip_tags($value);
697 $output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null;
699 // remove invalid characters (#1488124)
700 if ($output_charset == 'UTF-8')
701 $value = rc_utf8_clean($value);
703 // convert to internal charset
704 if ($charset && $output_charset)
705 $value = rcube_charset_convert($value, $output_charset, $charset);
711 * Convert array of request parameters (prefixed with _)
712 * to a regular array with non-prefixed keys.
714 * @param int Source to get value from (GPC)
715 * @return array Hash array with all request parameters
717 function request2param($mode = RCUBE_INPUT_GPC, $ignore = 'task|action')
720 $src = $mode == RCUBE_INPUT_GET ? $_GET : ($mode == RCUBE_INPUT_POST ? $_POST : $_REQUEST);
721 foreach ($src as $key => $value) {
722 $fname = $key[0] == '_' ? substr($key, 1) : $key;
723 if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname))
724 $out[$fname] = get_input_value($key, $mode);
731 * Remove all non-ascii and non-word chars
734 function asciiwords($str, $css_id = false, $replace_with = '')
736 $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
737 return preg_replace("/[^$allowed]/i", $replace_with, $str);
741 * Convert the given string into a valid HTML identifier
742 * Same functionality as done in app.js with rcube_webmail.html_identifier()
744 function html_identifier($str, $encode=false)
747 return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
749 return asciiwords($str, true, '_');
753 * Remove single and double quotes from given string
755 * @param string Input value
756 * @return string Dequoted string
758 function strip_quotes($str)
760 return str_replace(array("'", '"'), '', $str);
765 * Remove new lines characters from given string
767 * @param string Input value
768 * @return string Stripped string
770 function strip_newlines($str)
772 return preg_replace('/[\r\n]/', '', $str);
777 * Create a HTML table based on the given data
779 * @param array Named table attributes
780 * @param mixed Table row data. Either a two-dimensional array or a valid SQL result set
781 * @param array List of cols to show
782 * @param string Name of the identifier col
783 * @return string HTML table code
785 function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
789 $table = new html_table(/*array('cols' => count($a_show_cols))*/);
792 if (!$attrib['noheader'])
793 foreach ($a_show_cols as $col)
794 $table->add_header($col, Q(rcube_label($col)));
797 if (!is_array($table_data))
799 $db = $RCMAIL->get_dbh();
800 while ($table_data && ($sql_arr = $db->fetch_assoc($table_data)))
802 $table->add_row(array('id' => 'rcmrow' . html_identifier($sql_arr[$id_col])));
805 foreach ($a_show_cols as $col)
806 $table->add($col, Q($sql_arr[$col]));
812 foreach ($table_data as $row_data)
814 $class = !empty($row_data['class']) ? $row_data['class'] : '';
816 $table->add_row(array('id' => 'rcmrow' . html_identifier($row_data[$id_col]), 'class' => $class));
819 foreach ($a_show_cols as $col)
820 $table->add($col, Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
826 return $table->show($attrib);
831 * Create an edit field for inclusion on a form
833 * @param string col field name
834 * @param string value field value
835 * @param array attrib HTML element attributes for field
836 * @param string type HTML element type (default 'text')
837 * @return string HTML field definition
839 function rcmail_get_edit_field($col, $value, $attrib, $type='text')
841 static $colcounts = array();
844 $attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
845 $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
847 if ($type == 'checkbox') {
848 $attrib['value'] = '1';
849 $input = new html_checkbox($attrib);
851 else if ($type == 'textarea') {
852 $attrib['cols'] = $attrib['size'];
853 $input = new html_textarea($attrib);
855 else if ($type == 'select') {
856 $input = new html_select($attrib);
857 $input->add('---', '');
858 $input->add(array_values($attrib['options']), array_keys($attrib['options']));
861 if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden')
862 $attrib['type'] = 'text';
863 $input = new html_inputfield($attrib);
866 // use value from post
867 if (isset($_POST[$fname])) {
868 $postvalue = get_input_value($fname, RCUBE_INPUT_POST, true);
869 $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
872 $out = $input->show($value);
879 * Replace all css definitions with #container [def]
880 * and remove css-inlined scripting
882 * @param string CSS source code
883 * @param string Container ID to use as prefix
884 * @return string Modified CSS source
886 function rcmail_mod_css_styles($source, $container_id, $allow_remote=false)
889 $replacements = new rcube_string_replacer;
891 // ignore the whole block if evil styles are detected
892 $source = rcmail_xss_entity_decode($source);
893 $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
894 $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');
895 if (preg_match("/$evilexpr/i", $stripped))
896 return '/* evil! */';
898 // cut out all contents between { and }
899 while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
900 $styles = substr($source, $pos+1, $pos2-($pos+1));
902 // check every line of a style block...
904 $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
905 foreach ($a_styles as $line) {
906 $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
907 // ... and only allow strict url() values
908 if (stripos($stripped, 'url(') && !preg_match('!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims', $line)) {
909 $a_styles = array('/* evil! */');
913 $styles = join(";\n", $a_styles);
916 $key = $replacements->add($styles);
917 $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
921 // remove html comments and add #container to each tag selector.
922 // also replace body definition because we also stripped off the <body> tag
923 $styles = preg_replace(
925 '/(^\s*<!--)|(-->\s*$)/',
926 '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
927 '/'.preg_quote($container_id, '/').'\s+body/i',
931 "\\1#$container_id \\2",
936 // put block contents back in
937 $styles = $replacements->resolve($styles);
944 * Decode escaped entities used by known XSS exploits.
945 * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
947 * @param string CSS content to decode
948 * @return string Decoded string
950 function rcmail_xss_entity_decode($content)
952 $out = html_entity_decode(html_entity_decode($content));
953 $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
954 $out = preg_replace('#/\*.*\*/#Ums', '', $out);
960 * preg_replace_callback callback for rcmail_xss_entity_decode_callback
962 * @param array matches result from preg_replace_callback
963 * @return string decoded entity
965 function rcmail_xss_entity_decode_callback($matches)
967 return chr(hexdec($matches[1]));
971 * Compose a valid attribute string for HTML tags
973 * @param array Named tag attributes
974 * @param array List of allowed attributes
975 * @return string HTML formatted attribute string
977 function create_attrib_string($attrib, $allowed_attribs=array('id', 'class', 'style'))
979 // allow the following attributes to be added to the <iframe> tag
981 foreach ($allowed_attribs as $a)
982 if (isset($attrib[$a]))
983 $attrib_str .= sprintf(' %s="%s"', $a, str_replace('"', '"', $attrib[$a]));
990 * Convert a HTML attribute string attributes to an associative array (name => value)
992 * @param string Input string
993 * @return array Key-value pairs of parsed attributes
995 function parse_attrib_string($str)
998 preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', stripslashes($str), $regs, PREG_SET_ORDER);
1000 // convert attributes to an associative array (name => value)
1002 foreach ($regs as $attr) {
1003 $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
1012 * Improved equivalent to strtotime()
1014 * @param string Date string
1017 function rcube_strtotime($date)
1019 // check for MS Outlook vCard date format YYYYMMDD
1020 if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) {
1021 return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
1023 else if (is_numeric($date))
1026 // support non-standard "GMTXXXX" literal
1027 $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
1029 // if date parsing fails, we have a date in non-rfc format.
1030 // remove token from the end and try again
1031 while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
1032 $d = explode(' ', $date);
1035 $date = implode(' ', $d);
1043 * Convert the given date to a human readable form
1044 * This uses the date formatting properties from config
1046 * @param mixed Date representation (string or timestamp)
1047 * @param string Date format to use
1048 * @param bool Enables date convertion according to user timezone
1050 * @return string Formatted date string
1052 function format_date($date, $format=NULL, $convert=true)
1054 global $RCMAIL, $CONFIG;
1057 $ts = rcube_strtotime($date);
1063 // get user's timezone offset
1064 $tz = $RCMAIL->config->get_timezone();
1066 // convert time to user's timezone
1067 $timestamp = $ts - date('Z', $ts) + ($tz * 3600);
1069 // get current timestamp in user's timezone
1070 $now = time(); // local time
1071 $now -= (int)date('Z'); // make GMT time
1072 $now += ($tz * 3600); // user's time
1079 // define date format depending on current time
1081 $now_date = getdate($now);
1082 $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1083 $week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1085 if ($CONFIG['prettydate'] && $timestamp > $today_limit && $timestamp < $now) {
1086 $format = $RCMAIL->config->get('date_today', $RCMAIL->config->get('time_format', 'H:i'));
1089 else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now)
1090 $format = $RCMAIL->config->get('date_short', 'D H:i');
1092 $format = $RCMAIL->config->get('date_long', 'Y-m-d H:i');
1095 // strftime() format
1096 if (preg_match('/%[a-z]+/i', $format)) {
1097 $format = strftime($format, $timestamp);
1098 return $today ? (rcube_label('today') . ' ' . $format) : $format;
1101 // parse format string manually in order to provide localized weekday and month names
1102 // an alternative would be to convert the date() format string to fit with strftime()
1104 for($i=0; $i<strlen($format); $i++) {
1105 if ($format[$i]=='\\') // skip escape chars
1108 // write char "as-is"
1109 if ($format[$i]==' ' || $format{$i-1}=='\\')
1110 $out .= $format[$i];
1112 else if ($format[$i]=='D')
1113 $out .= rcube_label(strtolower(date('D', $timestamp)));
1115 else if ($format[$i]=='l')
1116 $out .= rcube_label(strtolower(date('l', $timestamp)));
1117 // month name (short)
1118 else if ($format[$i]=='M')
1119 $out .= rcube_label(strtolower(date('M', $timestamp)));
1120 // month name (long)
1121 else if ($format[$i]=='F')
1122 $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
1123 else if ($format[$i]=='x')
1124 $out .= strftime('%x %X', $timestamp);
1126 $out .= date($format[$i], $timestamp);
1130 $label = rcube_label('today');
1131 // replcae $ character with "Today" label (#1486120)
1132 if (strpos($out, '$') !== false) {
1133 $out = preg_replace('/\$/', $label, $out, 1);
1136 $out = $label . ' ' . $out;
1145 * Compose a valid representation of name and e-mail address
1147 * @param string E-mail address
1148 * @param string Person name
1149 * @return string Formatted string
1151 function format_email_recipient($email, $name='')
1153 if ($name && $name != $email) {
1154 // Special chars as defined by RFC 822 need to in quoted string (or escaped).
1155 return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, trim($email));
1158 return trim($email);
1163 * Return the mailboxlist in HTML
1165 * @param array Named parameters
1166 * @return string HTML code for the gui object
1168 function rcmail_mailbox_list($attrib)
1171 static $a_mailboxes;
1173 $attrib += array('maxlength' => 100, 'realnames' => false);
1175 // add some labels to client
1176 $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1178 $type = $attrib['type'] ? $attrib['type'] : 'ul';
1179 unset($attrib['type']);
1181 if ($type=='ul' && !$attrib['id'])
1182 $attrib['id'] = 'rcmboxlist';
1184 if (empty($attrib['folder_name']))
1185 $attrib['folder_name'] = '*';
1188 $mbox_name = $RCMAIL->imap->get_mailbox_name();
1190 // build the folders tree
1191 if (empty($a_mailboxes)) {
1193 $a_folders = $RCMAIL->imap->list_mailboxes('', $attrib['folder_name'], $attrib['folder_filter']);
1194 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1195 $a_mailboxes = array();
1197 foreach ($a_folders as $folder)
1198 rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1201 // allow plugins to alter the folder tree or to localize folder names
1202 $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
1204 if ($type == 'select') {
1205 $select = new html_select($attrib);
1207 // add no-selection option
1208 if ($attrib['noselection'])
1209 $select->add(rcube_label($attrib['noselection']), '');
1211 rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1212 $out = $select->show();
1215 $js_mailboxlist = array();
1216 $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1218 $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
1219 $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
1220 $RCMAIL->output->set_env('collapsed_folders', (string)$RCMAIL->config->get('collapsed_folders'));
1228 * Return the mailboxlist as html_select object
1230 * @param array Named parameters
1231 * @return html_select HTML drop-down object
1233 function rcmail_mailbox_select($p = array())
1237 $p += array('maxlength' => 100, 'realnames' => false);
1238 $a_mailboxes = array();
1240 if (empty($p['folder_name']))
1241 $p['folder_name'] = '*';
1243 if ($p['unsubscribed'])
1244 $list = $RCMAIL->imap->list_unsubscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1246 $list = $RCMAIL->imap->list_mailboxes('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1248 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1250 foreach ($list as $folder) {
1251 if (empty($p['exceptions']) || !in_array($folder, $p['exceptions']))
1252 rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1255 $select = new html_select($p);
1257 if ($p['noselection'])
1258 $select->add($p['noselection'], '');
1260 rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
1267 * Create a hierarchical array of the mailbox list
1271 function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
1275 // Handle namespace prefix
1278 $n_folder = $folder;
1279 $folder = $RCMAIL->imap->mod_mailbox($folder);
1281 if ($n_folder != $folder) {
1282 $prefix = substr($n_folder, 0, -strlen($folder));
1286 $pos = strpos($folder, $delm);
1288 if ($pos !== false) {
1289 $subFolders = substr($folder, $pos+1);
1290 $currentFolder = substr($folder, 0, $pos);
1292 // sometimes folder has a delimiter as the last character
1293 if (!strlen($subFolders))
1295 else if (!isset($arrFolders[$currentFolder]))
1298 $virtual = $arrFolders[$currentFolder]['virtual'];
1301 $subFolders = false;
1302 $currentFolder = $folder;
1306 $path .= $prefix.$currentFolder;
1308 if (!isset($arrFolders[$currentFolder])) {
1309 $arrFolders[$currentFolder] = array(
1311 'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
1312 'virtual' => $virtual,
1313 'folders' => array());
1316 $arrFolders[$currentFolder]['virtual'] = $virtual;
1318 if (strlen($subFolders))
1319 rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1324 * Return html for a structured list <ul> for the mailbox tree
1328 function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
1330 global $RCMAIL, $CONFIG;
1332 $maxlength = intval($attrib['maxlength']);
1333 $realnames = (bool)$attrib['realnames'];
1334 $msgcounts = $RCMAIL->imap->get_cache('messagecount');
1337 foreach ($arrFolders as $key => $folder) {
1339 $folder_class = rcmail_folder_classname($folder['id']);
1340 $collapsed = strpos($CONFIG['collapsed_folders'], '&'.rawurlencode($folder['id']).'&') !== false;
1341 $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1343 if ($folder_class && !$realnames) {
1344 $foldername = rcube_label($folder_class);
1347 $foldername = $folder['name'];
1349 // shorten the folder name to a given length
1350 if ($maxlength && $maxlength > 1) {
1351 $fname = abbreviate_string($foldername, $maxlength);
1352 if ($fname != $foldername)
1353 $title = $foldername;
1354 $foldername = $fname;
1358 // make folder name safe for ids and class names
1359 $folder_id = html_identifier($folder['id'], true);
1360 $classes = array('mailbox');
1362 // set special class for Sent, Drafts, Trash and Junk
1364 $classes[] = $folder_class;
1366 if ($folder['id'] == $mbox_name)
1367 $classes[] = 'selected';
1369 if ($folder['virtual'])
1370 $classes[] = 'virtual';
1372 $classes[] = 'unread';
1374 $js_name = JQ($folder['id']);
1375 $html_name = Q($foldername) . ($unread ? html::span('unreadcount', " ($unread)") : '');
1376 $link_attrib = $folder['virtual'] ? array() : array(
1377 'href' => rcmail_url('', array('_mbox' => $folder['id'])),
1378 'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
1379 'rel' => $folder['id'],
1383 $out .= html::tag('li', array(
1384 'id' => "rcmli".$folder_id,
1385 'class' => join(' ', $classes),
1387 html::a($link_attrib, $html_name) .
1388 (!empty($folder['folders']) ? html::div(array(
1389 'class' => ($collapsed ? 'collapsed' : 'expanded'),
1390 'style' => "position:absolute",
1391 'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
1392 ), ' ') : ''));
1394 $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
1396 if (!empty($folder['folders'])) {
1397 $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
1398 rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1409 * Return html for a flat list <select> for the mailbox tree
1413 function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0, $opts=array())
1419 foreach ($arrFolders as $key => $folder) {
1420 // skip exceptions (and its subfolders)
1421 if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
1425 // skip folders in which it isn't possible to create subfolders
1426 if (!empty($opts['skip_noinferiors']) && ($attrs = $RCMAIL->imap->mailbox_attributes($folder['id']))
1427 && in_array('\\Noinferiors', $attrs)
1432 if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1433 $foldername = rcube_label($folder_class);
1435 $foldername = $folder['name'];
1437 // shorten the folder name to a given length
1438 if ($maxlength && $maxlength>1)
1439 $foldername = abbreviate_string($foldername, $maxlength);
1442 $select->add(str_repeat(' ', $nestLevel*4) . $foldername, $folder['id']);
1444 if (!empty($folder['folders']))
1445 $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
1446 $select, $realnames, $nestLevel+1, $opts);
1454 * Return internal name for the given folder if it matches the configured special folders
1458 function rcmail_folder_classname($folder_id)
1462 if ($folder_id == 'INBOX')
1465 // for these mailboxes we have localized labels and css classes
1466 foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1468 if ($folder_id == $CONFIG[$smbx.'_mbox'])
1475 * Try to localize the given IMAP folder name.
1476 * UTF-7 decode it in case no localized text was found
1478 * @param string Folder name
1479 * @return string Localized folder name in UTF-8 encoding
1481 function rcmail_localize_foldername($name)
1483 if ($folder_class = rcmail_folder_classname($name))
1484 return rcube_label($folder_class);
1486 return rcube_charset_convert($name, 'UTF7-IMAP');
1490 function rcmail_localize_folderpath($path)
1494 $protect_folders = $RCMAIL->config->get('protect_default_folders');
1495 $default_folders = (array) $RCMAIL->config->get('default_imap_folders');
1496 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1497 $path = explode($delimiter, $path);
1500 foreach ($path as $idx => $dir) {
1501 $directory = implode($delimiter, array_slice($path, 0, $idx+1));
1502 if ($protect_folders && in_array($directory, $default_folders)) {
1504 $result[] = rcmail_localize_foldername($directory);
1507 $result[] = rcube_charset_convert($dir, 'UTF7-IMAP');
1511 return implode($delimiter, $result);
1515 function rcmail_quota_display($attrib)
1520 $attrib['id'] = 'rcmquotadisplay';
1522 if(isset($attrib['display']))
1523 $_SESSION['quota_display'] = $attrib['display'];
1525 $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1527 $quota = rcmail_quota_content($attrib);
1529 $OUTPUT->add_script('rcmail.set_quota('.json_serialize($quota).');', 'docready');
1531 return html::span($attrib, '');
1535 function rcmail_quota_content($attrib=NULL)
1539 $quota = $RCMAIL->imap->get_quota();
1540 $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1542 $quota_result = (array) $quota;
1543 $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1545 if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1546 $quota_result['title'] = rcube_label('unlimited');
1547 $quota_result['percent'] = 0;
1549 else if ($quota['total']) {
1550 if (!isset($quota['percent']))
1551 $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1553 $title = sprintf('%s / %s (%.0f%%)',
1554 show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
1555 $quota_result['percent']);
1557 $quota_result['title'] = $title;
1559 if ($attrib['width'])
1560 $quota_result['width'] = $attrib['width'];
1561 if ($attrib['height'])
1562 $quota_result['height'] = $attrib['height'];
1565 $quota_result['title'] = rcube_label('unknown');
1566 $quota_result['percent'] = 0;
1569 return $quota_result;
1574 * Outputs error message according to server error/response codes
1576 * @param string Fallback message label
1577 * @param string Fallback message label arguments
1581 function rcmail_display_server_error($fallback=null, $fallback_args=null)
1585 $err_code = $RCMAIL->imap->get_error_code();
1586 $res_code = $RCMAIL->imap->get_response_code();
1588 if ($res_code == rcube_imap::NOPERM) {
1589 $RCMAIL->output->show_message('errornoperm', 'error');
1591 else if ($res_code == rcube_imap::READONLY) {
1592 $RCMAIL->output->show_message('errorreadonly', 'error');
1594 else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
1595 // try to detect access rights problem and display appropriate message
1596 if (stripos($err_str, 'Permission denied') !== false)
1597 $RCMAIL->output->show_message('errornoperm', 'error');
1599 $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1601 else if ($fallback) {
1602 $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1610 * Output HTML editor scripts
1612 * @param string Editor mode
1615 function rcube_html_editor($mode='')
1617 global $RCMAIL, $CONFIG;
1619 $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1624 $lang = strtolower($_SESSION['language']);
1626 // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
1627 $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
1629 if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
1632 $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1633 $RCMAIL->output->include_script('editor.js');
1634 $RCMAIL->output->add_script(sprintf("rcmail_editor_init(%s)",
1637 'skin_path' => '$__skin_path',
1639 'spellcheck' => intval($CONFIG['enable_spellcheck']),
1640 'spelldict' => intval($CONFIG['spellcheck_dictionary']),
1646 * Replaces TinyMCE's emoticon images with plain-text representation
1648 * @param string HTML content
1649 * @return string HTML content
1651 function rcmail_replace_emoticons($html)
1654 '8-)' => 'smiley-cool',
1655 ':-#' => 'smiley-foot-in-mouth',
1656 ':-*' => 'smiley-kiss',
1657 ':-X' => 'smiley-sealed',
1658 ':-P' => 'smiley-tongue-out',
1659 ':-@' => 'smiley-yell',
1660 ":'(" => 'smiley-cry',
1661 ':-(' => 'smiley-frown',
1662 ':-D' => 'smiley-laughing',
1663 ':-)' => 'smiley-smile',
1664 ':-S' => 'smiley-undecided',
1665 ':-$' => 'smiley-embarassed',
1666 'O:-)' => 'smiley-innocent',
1667 ':-|' => 'smiley-money-mouth',
1668 ':-O' => 'smiley-surprised',
1669 ';-)' => 'smiley-wink',
1672 foreach ($emoticons as $idx => $file) {
1673 // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1674 $search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1678 return preg_replace($search, $replace, $html);
1683 * Send the given message using the configured method
1685 * @param object $message Reference to Mail_MIME object
1686 * @param string $from Sender address string
1687 * @param array $mailto Array of recipient address strings
1688 * @param array $smtp_error SMTP error array (reference)
1689 * @param string $body_file Location of file with saved message body (reference),
1690 * used when delay_file_io is enabled
1691 * @param array $smtp_opts SMTP options (e.g. DSN request)
1693 * @return boolean Send status.
1695 function rcmail_deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file=null, $smtp_opts=null)
1697 global $CONFIG, $RCMAIL;
1699 $headers = $message->headers();
1701 // send thru SMTP server using custom SMTP library
1702 if ($CONFIG['smtp_server']) {
1703 // generate list of recipients
1704 $a_recipients = array($mailto);
1706 if (strlen($headers['Cc']))
1707 $a_recipients[] = $headers['Cc'];
1708 if (strlen($headers['Bcc']))
1709 $a_recipients[] = $headers['Bcc'];
1711 // clean Bcc from header for recipients
1712 $send_headers = $headers;
1713 unset($send_headers['Bcc']);
1714 // here too, it because txtHeaders() below use $message->_headers not only $send_headers
1715 unset($message->_headers['Bcc']);
1717 $smtp_headers = $message->txtHeaders($send_headers, true);
1719 if ($message->getParam('delay_file_io')) {
1720 // use common temp dir
1721 $temp_dir = $RCMAIL->config->get('temp_dir');
1722 $body_file = tempnam($temp_dir, 'rcmMsg');
1723 if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
1724 raise_error(array('code' => 650, 'type' => 'php',
1725 'file' => __FILE__, 'line' => __LINE__,
1726 'message' => "Could not create message: ".$mime_result->getMessage()),
1730 $msg_body = fopen($body_file, 'r');
1732 $msg_body = $message->get();
1736 if (!is_object($RCMAIL->smtp))
1737 $RCMAIL->smtp_init(true);
1739 $sent = $RCMAIL->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
1740 $smtp_response = $RCMAIL->smtp->get_response();
1741 $smtp_error = $RCMAIL->smtp->get_error();
1745 raise_error(array('code' => 800, 'type' => 'smtp', 'line' => __LINE__, 'file' => __FILE__,
1746 'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
1748 // send mail using PHP's mail() function
1750 // unset some headers because they will be added by the mail() function
1751 $headers_enc = $message->headers($headers);
1752 $headers_php = $message->_headers;
1753 unset($headers_php['To'], $headers_php['Subject']);
1755 // reset stored headers and overwrite
1756 $message->_headers = array();
1757 $header_str = $message->txtHeaders($headers_php);
1760 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1761 if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
1762 $headers_enc['To'] = implode(', ', $m[1]);
1766 $msg_body = $message->get();
1768 if (PEAR::isError($msg_body))
1769 raise_error(array('code' => 650, 'type' => 'php',
1770 'file' => __FILE__, 'line' => __LINE__,
1771 'message' => "Could not create message: ".$msg_body->getMessage()),
1774 $delim = $RCMAIL->config->header_delimiter();
1775 $to = $headers_enc['To'];
1776 $subject = $headers_enc['Subject'];
1777 $header_str = rtrim($header_str);
1779 if ($delim != "\r\n") {
1780 $header_str = str_replace("\r\n", $delim, $header_str);
1781 $msg_body = str_replace("\r\n", $delim, $msg_body);
1782 $to = str_replace("\r\n", $delim, $to);
1783 $subject = str_replace("\r\n", $delim, $subject);
1786 if (ini_get('safe_mode'))
1787 $sent = mail($to, $subject, $msg_body, $header_str);
1789 $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1794 $RCMAIL->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1796 // remove MDN headers after sending
1797 unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1799 // get all recipients
1801 $mailto .= $headers['Cc'];
1802 if ($headers['Bcc'])
1803 $mailto .= $headers['Bcc'];
1804 if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1805 $mailto = implode(', ', array_unique($m[1]));
1807 if ($CONFIG['smtp_log']) {
1808 write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1809 $RCMAIL->user->get_username(),
1810 $_SERVER['REMOTE_ADDR'],
1812 !empty($smtp_response) ? join('; ', $smtp_response) : ''));
1816 if (is_resource($msg_body)) {
1820 $message->_headers = array();
1821 $message->headers($headers);
1827 // Returns unique Message-ID
1828 function rcmail_gen_message_id()
1832 $local_part = md5(uniqid('rcmail'.mt_rand(),true));
1833 $domain_part = $RCMAIL->user->get_username('domain');
1835 // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1836 if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1837 if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']))
1838 && preg_match('/\.[a-z]+$/i', $host)) {
1839 $domain_part = $host;
1841 else if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['SERVER_NAME']))
1842 && preg_match('/\.[a-z]+$/i', $host)) {
1843 $domain_part = $host;
1847 return sprintf('<%s@%s>', $local_part, $domain_part);
1851 // Returns RFC2822 formatted current date in user's timezone
1852 function rcmail_user_date()
1854 global $RCMAIL, $CONFIG;
1856 // get user's timezone
1857 $tz = $RCMAIL->config->get_timezone();
1859 $date = time() + $tz * 60 * 60;
1860 $date = gmdate('r', $date);
1861 $tz = sprintf('%+05d', intval($tz) * 100 + ($tz - intval($tz)) * 60);
1862 $date = preg_replace('/[+-][0-9]{4}$/', $tz, $date);
1869 * Check if working in SSL mode
1871 * @param integer HTTPS port number
1872 * @param boolean Enables 'use_https' option checking
1875 function rcube_https_check($port=null, $use_https=true)
1879 if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
1881 if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
1883 if ($port && $_SERVER['SERVER_PORT'] == $port)
1885 if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
1893 * For backward compatibility.
1895 * @global rcmail $RCMAIL
1896 * @param string $var_name Variable name.
1899 function rcube_sess_unset($var_name=null)
1903 $RCMAIL->session->remove($var_name);
1908 * Replaces hostname variables
1910 * @param string $name Hostname
1911 * @param string $host Optional IMAP hostname
1914 function rcube_parse_host($name, $host='')
1917 $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1918 // %d - domain name without first part, e.g. %n=mail.domain.tld, %d=domain.tld
1919 $d = preg_replace('/^[^\.]+\./', '', $n);
1921 $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
1922 // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1923 $z = preg_replace('/^[^\.]+\./', '', $h);
1924 // %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided
1925 if ( strpos($name, '%s') !== false ){
1926 $user_email = rcube_idn_convert(get_input_value('_user', RCUBE_INPUT_POST), true);
1927 if ( preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s) < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false )
1931 $name = str_replace(array('%n', '%d', '%h', '%z', '%s'), array($n, $d, $h, $z, $s[2]), $name);
1937 * E-mail address validation
1939 * @param string $email Email address
1940 * @param boolean $dns_check True to check dns
1943 function check_email($email, $dns_check=true)
1945 // Check for invalid characters
1946 if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
1949 // Check for length limit specified by RFC 5321 (#1486453)
1950 if (strlen($email) > 254)
1953 $email_array = explode('@', $email);
1955 // Check that there's one @ symbol
1956 if (count($email_array) < 2)
1959 $domain_part = array_pop($email_array);
1960 $local_part = implode('@', $email_array);
1962 // from PEAR::Validate
1964 ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
1965 ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) #2 OR dot-atom (RFC5322)
1968 if (!preg_match($regexp, $local_part))
1971 // Check domain part
1972 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))
1973 return true; // IP address
1975 // If not an IP address
1976 $domain_array = explode('.', $domain_part);
1977 if (sizeof($domain_array) < 2)
1978 return false; // Not enough parts to be a valid domain
1980 foreach ($domain_array as $part)
1981 if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
1984 if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
1987 if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1989 @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1990 foreach ($lookup as $line) {
1991 if (strpos($line, 'MX preference'))
1997 // find MX record(s)
1998 if (getmxrr($domain_part, $mx_records))
2001 // find any DNS record
2002 if (checkdnsrr($domain_part, 'ANY'))
2010 * Idn_to_ascii wrapper.
2011 * Intl/Idn modules version of this function doesn't work with e-mail address
2013 function rcube_idn_to_ascii($str)
2015 return rcube_idn_convert($str, true);
2019 * Idn_to_ascii wrapper.
2020 * Intl/Idn modules version of this function doesn't work with e-mail address
2022 function rcube_idn_to_utf8($str)
2024 return rcube_idn_convert($str, false);
2027 function rcube_idn_convert($input, $is_utf=false)
2029 if ($at = strpos($input, '@')) {
2030 $user = substr($input, 0, $at);
2031 $domain = substr($input, $at+1);
2037 $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
2039 if ($domain === false) {
2043 return $at ? $user . '@' . $domain : $domain;
2048 * Helper class to turn relative urls into absolute ones
2049 * using a predefined base
2051 class rcube_base_replacer
2055 public function __construct($base)
2057 $this->base_url = $base;
2060 public function callback($matches)
2062 return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"';
2065 public function replace($body)
2067 return preg_replace_callback(array(
2068 '/(src|background|href)=(["\']?)([^"\'\s]+)(\2|\s|>)/Ui',
2069 '/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/Ui',
2071 array($this, 'callback'), $body);
2075 * Convert paths like ../xxx to an absolute path using a base url
2077 * @param string $path Relative path
2078 * @param string $base_url Base URL
2080 * @return string Absolute URL
2082 public static function absolute_url($path, $base_url)
2084 $host_url = $base_url;
2087 // check if path is an absolute URL
2088 if (preg_match('/^[fhtps]+:\/\//', $path)) {
2092 // check if path is a content-id scheme
2093 if (strpos($path, 'cid:') === 0) {
2097 // cut base_url to the last directory
2098 if (strrpos($base_url, '/') > 7) {
2099 $host_url = substr($base_url, 0, strpos($base_url, '/', 7));
2100 $base_url = substr($base_url, 0, strrpos($base_url, '/'));
2103 // $path is absolute
2104 if ($path[0] == '/') {
2105 $abs_path = $host_url.$path;
2108 // strip './' because its the same as ''
2109 $path = preg_replace('/^\.\//', '', $path);
2111 if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
2112 foreach ($matches as $a_match) {
2113 if (strrpos($base_url, '/')) {
2114 $base_url = substr($base_url, 0, strrpos($base_url, '/'));
2116 $path = substr($path, 3);
2120 $abs_path = $base_url.'/'.$path;
2128 /****** debugging and logging functions ********/
2131 * Print or write debug messages
2133 * @param mixed Debug message or data
2138 $args = func_get_args();
2140 if (class_exists('rcmail', false)) {
2141 $rcmail = rcmail::get_instance();
2142 if (is_object($rcmail->plugins)) {
2143 $plugin = $rcmail->plugins->exec_hook('console', array('args' => $args));
2144 if ($plugin['abort'])
2146 $args = $plugin['args'];
2151 foreach ($args as $arg)
2152 $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
2154 write_log('console', join(";\n", $msg));
2159 * Append a line to a logfile in the logs directory.
2160 * Date will be added automatically to the line.
2162 * @param $name name of log file
2163 * @param line Line to append
2166 function write_log($name, $line)
2168 global $CONFIG, $RCMAIL;
2170 if (!is_string($line))
2171 $line = var_export($line, true);
2173 if (empty($CONFIG['log_date_format']))
2174 $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
2176 $date = date($CONFIG['log_date_format']);
2178 // trigger logging hook
2179 if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
2180 $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
2181 $name = $log['name'];
2182 $line = $log['line'];
2183 $date = $log['date'];
2188 if ($CONFIG['log_driver'] == 'syslog') {
2189 $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
2190 syslog($prio, $line);
2194 $line = sprintf("[%s]: %s\n", $date, $line);
2196 // log_driver == 'file' is assumed here
2197 if (empty($CONFIG['log_dir']))
2198 $CONFIG['log_dir'] = INSTALL_PATH.'logs';
2200 // try to open specific log file for writing
2201 $logfile = $CONFIG['log_dir'].'/'.$name;
2202 if ($fp = @fopen($logfile, 'a')) {
2209 trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
2217 * Write login data (name, ID, IP address) to the 'userlogins' log file.
2221 function rcmail_log_login()
2225 if (!$RCMAIL->config->get('log_logins') || !$RCMAIL->user)
2228 write_log('userlogins', sprintf('Successful login for %s (ID: %d) from %s in session %s',
2229 $RCMAIL->user->get_username(), $RCMAIL->user->ID, rcmail_remote_ip(), session_id()));
2234 * Returns remote IP address and forwarded addresses if found
2236 * @return string Remote IP address(es)
2238 function rcmail_remote_ip()
2240 $address = $_SERVER['REMOTE_ADDR'];
2242 // append the NGINX X-Real-IP header, if set
2243 if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
2244 $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
2246 // append the X-Forwarded-For header, if set
2247 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
2248 $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
2251 if (!empty($remote_ip))
2252 $address .= '(' . implode(',', $remote_ip) . ')';
2259 * Check whether the HTTP referer matches the current request
2261 * @return boolean True if referer is the same host+path, false if not
2263 function rcube_check_referer()
2265 $uri = parse_url($_SERVER['REQUEST_URI']);
2266 $referer = parse_url(rc_request_header('Referer'));
2267 return $referer['host'] == rc_request_header('Host') && $referer['path'] == $uri['path'];
2275 function rcube_timer()
2277 return microtime(true);
2285 function rcube_print_time($timer, $label='Timer', $dest='console')
2287 static $print_count = 0;
2290 $now = rcube_timer();
2291 $diff = $now-$timer;
2294 $label = 'Timer '.$print_count;
2296 write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
2301 * Throw system error and show error page
2303 * @param array Named parameters
2304 * - code: Error code (required)
2305 * - type: Error type [php|db|imap|javascript] (required)
2306 * - message: Error message
2307 * - file: File where error occured
2308 * - line: Line where error occured
2309 * @param boolean True to log the error
2310 * @param boolean Terminate script execution
2312 // may be defined in Installer
2313 if (!function_exists('raise_error')) {
2314 function raise_error($arg=array(), $log=false, $terminate=false)
2316 global $__page_content, $CONFIG, $OUTPUT, $ERROR_CODE, $ERROR_MESSAGE;
2318 // report bug (if not incompatible browser)
2319 if ($log && $arg['type'] && $arg['message'])
2320 rcube_log_bug($arg);
2322 // display error page and terminate script
2324 $ERROR_CODE = $arg['code'];
2325 $ERROR_MESSAGE = $arg['message'];
2326 include INSTALL_PATH . 'program/steps/utils/error.inc';
2334 * Report error according to configured debug_level
2336 * @param array Named parameters
2338 * @see raise_error()
2340 function rcube_log_bug($arg_arr)
2344 $program = strtoupper($arg_arr['type']);
2345 $level = $CONFIG['debug_level'];
2347 // disable errors for ajax requests, write to log instead (#1487831)
2348 if (($level & 4) && !empty($_REQUEST['_remote'])) {
2349 $level = ($level ^ 4) | 1;
2352 // write error to local log file
2354 $post_query = ($_SERVER['REQUEST_METHOD'] == 'POST' ? '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']) : '');
2355 $log_entry = sprintf("%s Error: %s%s (%s %s)",
2357 $arg_arr['message'],
2358 $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
2359 $_SERVER['REQUEST_METHOD'],
2360 $_SERVER['REQUEST_URI'] . $post_query);
2362 if (!write_log('errors', $log_entry)) {
2363 // send error to PHPs error handler if write_log didn't succeed
2364 trigger_error($arg_arr['message']);
2368 // report the bug to the global bug reporting system
2370 // TODO: Send error via HTTP
2373 // show error if debug_mode is on
2375 print "<b>$program Error";
2377 if (!empty($arg_arr['file']) && !empty($arg_arr['line']))
2378 print " in $arg_arr[file] ($arg_arr[line])";
2380 print ':</b> ';
2381 print nl2br($arg_arr['message']);
2387 function rcube_upload_progress()
2391 $prefix = ini_get('apc.rfc1867_prefix');
2393 'action' => $RCMAIL->action,
2394 'name' => get_input_value('_progress', RCUBE_INPUT_GET),
2397 if (function_exists('apc_fetch')) {
2398 $status = apc_fetch($prefix . $params['name']);
2400 if (!empty($status)) {
2401 $status['percent'] = round($status['current']/$status['total']*100);
2402 $params = array_merge($status, $params);
2406 if (isset($params['percent']))
2407 $params['text'] = rcube_label(array('name' => 'uploadprogress', 'vars' => array(
2408 'percent' => $params['percent'] . '%',
2409 'current' => show_bytes($params['current']),
2410 'total' => show_bytes($params['total'])
2413 $RCMAIL->output->command('upload_progress_update', $params);
2414 $RCMAIL->output->send();
2417 function rcube_upload_init()
2421 // Enable upload progress bar
2422 if (($seconds = $RCMAIL->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
2423 if ($field_name = ini_get('apc.rfc1867_name')) {
2424 $RCMAIL->output->set_env('upload_progress_name', $field_name);
2425 $RCMAIL->output->set_env('upload_progress_time', (int) $seconds);
2429 // find max filesize value
2430 $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
2431 $max_postsize = parse_bytes(ini_get('post_max_size'));
2432 if ($max_postsize && $max_postsize < $max_filesize)
2433 $max_filesize = $max_postsize;
2435 $RCMAIL->output->set_env('max_filesize', $max_filesize);
2436 $max_filesize = show_bytes($max_filesize);
2437 $RCMAIL->output->set_env('filesizeerror', rcube_label(array(
2438 'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
2440 return $max_filesize;
2444 * Initializes client-side autocompletion
2446 function rcube_autocomplete_init()
2456 if (($threads = (int)$RCMAIL->config->get('autocomplete_threads')) > 0) {
2457 $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
2458 if (count($book_types) > 1) {
2459 $RCMAIL->output->set_env('autocomplete_threads', $threads);
2460 $RCMAIL->output->set_env('autocomplete_sources', $book_types);
2464 $RCMAIL->output->set_env('autocomplete_max', (int)$RCMAIL->config->get('autocomplete_max', 15));
2465 $RCMAIL->output->set_env('autocomplete_min_length', $RCMAIL->config->get('autocomplete_min_length'));
2466 $RCMAIL->output->add_label('autocompletechars', 'autocompletemore');