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 5151 2011-08-31 12:49:44Z 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)
102 return rcmail::get_instance()->text_exists($name, $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('messages')."
173 WHERE created < " . $db->fromunixtime($ts));
175 $db->query("DELETE FROM ".get_table_name('cache')."
176 WHERE created < " . $db->fromunixtime($ts));
181 * Catch an error and throw an exception.
183 * @param int Level of the error
184 * @param string Error message
186 function rcube_error_handler($errno, $errstr)
188 throw new ErrorException($errstr, 0, $errno);
193 * Convert a string from one charset to another.
194 * Uses mbstring and iconv functions if possible
196 * @param string Input string
197 * @param string Suspected charset of the input string
198 * @param string Target charset to convert to; defaults to RCMAIL_CHARSET
199 * @return string Converted string
201 function rcube_charset_convert($str, $from, $to=NULL)
203 static $iconv_options = null;
204 static $mbstring_loaded = null;
205 static $mbstring_list = null;
210 $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : rcube_parse_charset($to);
211 $from = rcube_parse_charset($from);
213 if ($from == $to || empty($str) || empty($from))
216 // convert charset using iconv module
217 if (function_exists('iconv') && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
218 if ($iconv_options === null) {
219 // ignore characters not available in output charset
220 $iconv_options = '//IGNORE';
221 if (iconv('', $iconv_options, '') === false) {
222 // iconv implementation does not support options
227 // throw an exception if iconv reports an illegal character in input
228 // it means that input string has been truncated
229 set_error_handler('rcube_error_handler', E_NOTICE);
231 $_iconv = iconv($from, $to . $iconv_options, $str);
232 } catch (ErrorException $e) {
235 restore_error_handler();
236 if ($_iconv !== false) {
241 if ($mbstring_loaded === null)
242 $mbstring_loaded = extension_loaded('mbstring');
244 // convert charset using mbstring module
245 if ($mbstring_loaded) {
246 $aliases['WINDOWS-1257'] = 'ISO-8859-13';
248 if ($mbstring_list === null) {
249 $mbstring_list = mb_list_encodings();
250 $mbstring_list = array_map('strtoupper', $mbstring_list);
253 $mb_from = $aliases[$from] ? $aliases[$from] : $from;
254 $mb_to = $aliases[$to] ? $aliases[$to] : $to;
256 // return if encoding found, string matches encoding and convert succeeded
257 if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
258 if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from)))
263 // convert charset using bundled classes/functions
264 if ($to == 'UTF-8') {
265 if ($from == 'UTF7-IMAP') {
266 if ($_str = utf7_to_utf8($str))
269 else if ($from == 'UTF-7') {
270 if ($_str = rcube_utf7_to_utf8($str))
273 else if (($from == 'ISO-8859-1') && function_exists('utf8_encode')) {
274 return utf8_encode($str);
276 else if (class_exists('utf8')) {
278 $conv = new utf8($from);
280 $conv->loadCharset($from);
282 if($_str = $conv->strToUtf8($str))
288 // encode string for output
289 if ($from == 'UTF-8') {
290 // @TODO: we need a function for UTF-7 (RFC2152) conversion
291 if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
292 if ($_str = utf8_to_utf7($str))
295 else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
296 return utf8_decode($str);
298 else if (class_exists('utf8')) {
300 $conv = new utf8($to);
302 $conv->loadCharset($from);
304 if ($_str = $conv->strToUtf8($str))
310 // return UTF-8 or original string
316 * Parse and validate charset name string (see #1485758).
317 * Sometimes charset string is malformed, there are also charset aliases
318 * but we need strict names for charset conversion (specially utf8 class)
320 * @param string Input charset name
321 * @return string The validated charset name
323 function rcube_parse_charset($input)
325 static $charsets = array();
326 $charset = strtoupper($input);
328 if (isset($charsets[$input]))
329 return $charsets[$input];
331 $charset = preg_replace(array(
332 '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO
333 '/\$.*$/', // e.g. _ISO-8859-JP$SIO
334 '/UNICODE-1-1-*/', // RFC1641/1642
335 '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8)
338 if ($charset == 'BINARY')
339 return $charsets[$input] = null;
341 # Aliases: some of them from HTML5 spec.
343 'USASCII' => 'WINDOWS-1252',
344 'ANSIX31101983' => 'WINDOWS-1252',
345 'ANSIX341968' => 'WINDOWS-1252',
346 'UNKNOWN8BIT' => 'ISO-8859-15',
347 'UNKNOWN' => 'ISO-8859-15',
348 'USERDEFINED' => 'ISO-8859-15',
349 'KSC56011987' => 'EUC-KR',
352 'UNICODE' => 'UTF-8',
353 'UTF7IMAP' => 'UTF7-IMAP',
354 'TIS620' => 'WINDOWS-874',
355 'ISO88599' => 'WINDOWS-1254',
356 'ISO885911' => 'WINDOWS-874',
357 'MACROMAN' => 'MACINTOSH',
359 '128' => 'SHIFT-JIS',
364 '161' => 'WINDOWS-1253',
365 '162' => 'WINDOWS-1254',
366 '163' => 'WINDOWS-1258',
367 '177' => 'WINDOWS-1255',
368 '178' => 'WINDOWS-1256',
369 '186' => 'WINDOWS-1257',
370 '204' => 'WINDOWS-1251',
371 '222' => 'WINDOWS-874',
372 '238' => 'WINDOWS-1250',
374 'WINDOWS949' => 'UHC',
377 // allow A-Z and 0-9 only
378 $str = preg_replace('/[^A-Z0-9]/', '', $charset);
380 if (isset($aliases[$str]))
381 $result = $aliases[$str];
383 else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m))
384 $result = 'UTF-' . $m[1] . $m[2];
386 else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
387 $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
388 // some clients sends windows-1252 text as latin1,
389 // it is safe to use windows-1252 for all latin1
390 $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
392 // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
393 else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
394 $result = 'WINDOWS-' . $m[2];
397 else if (preg_match('/LATIN(.*)/', $str, $m)) {
398 $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
399 '7' => 13, '8' => 14, '9' => 15, '10' => 16,
400 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8);
402 // some clients sends windows-1252 text as latin1,
403 // it is safe to use windows-1252 for all latin1
405 $result = 'WINDOWS-1252';
407 // if iconv is not supported we need ISO labels, it's also safe for iconv
408 else if (!empty($aliases[$m[1]])) {
409 $result = 'ISO-8859-'.$aliases[$m[1]];
411 // iconv requires convertion of e.g. LATIN-1 to LATIN1
420 $charsets[$input] = $result;
427 * Converts string from standard UTF-7 (RFC 2152) to UTF-8.
429 * @param string Input string
430 * @return string The converted string
432 function rcube_utf7_to_utf8($str)
435 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
436 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
437 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
438 1,1,1,1, 1,1,1,1, 1,1,0,0, 0,0,0,0,
439 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
440 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
441 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
442 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
445 $u7len = strlen($str);
449 for ($i=0; $u7len > 0; $i++, $u7len--)
458 for (; $u7len > 0; $i++, $u7len--)
462 if (!$Index_64[ord($u7)])
474 $res .= rcube_utf16_to_utf8(base64_decode($ch));
486 * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
488 * @param string Input string
489 * @return string The converted string
491 function rcube_utf16_to_utf8($str)
496 for ($i = 0; $i < $len; $i += 2) {
497 $c = ord($str[$i]) << 8 | ord($str[$i + 1]);
498 if ($c >= 0x0001 && $c <= 0x007F) {
500 } else if ($c > 0x07FF) {
501 $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
502 $dec .= chr(0x80 | (($c >> 6) & 0x3F));
503 $dec .= chr(0x80 | (($c >> 0) & 0x3F));
505 $dec .= chr(0xC0 | (($c >> 6) & 0x1F));
506 $dec .= chr(0x80 | (($c >> 0) & 0x3F));
514 * Replacing specials characters to a specific encoding type
516 * @param string Input string
517 * @param string Encoding type: text|html|xml|js|url
518 * @param string Replace mode for tags: show|replace|remove
519 * @param boolean Convert newlines
520 * @return string The quoted string
522 function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE)
524 static $html_encode_arr = false;
525 static $js_rep_table = false;
526 static $xml_rep_table = false;
529 $enctype = $OUTPUT->type;
531 // encode for HTML output
532 if ($enctype=='html')
534 if (!$html_encode_arr)
536 $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
537 unset($html_encode_arr['?']);
540 $ltpos = strpos($str, '<');
541 $encode_arr = $html_encode_arr;
543 // don't replace quotes and html tags
544 if (($mode=='show' || $mode=='') && $ltpos!==false && strpos($str, '>', $ltpos)!==false)
546 unset($encode_arr['"']);
547 unset($encode_arr['<']);
548 unset($encode_arr['>']);
549 unset($encode_arr['&']);
551 else if ($mode=='remove')
552 $str = strip_tags($str);
554 $out = strtr($str, $encode_arr);
556 // avoid douple quotation of &
557 $out = preg_replace('/&([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
559 return $newlines ? nl2br($out) : $out;
562 // if the replace tables for XML and JS are not yet defined
563 if ($js_rep_table===false)
565 $js_rep_table = $xml_rep_table = array();
566 $xml_rep_table['&'] = '&';
568 for ($c=160; $c<256; $c++) // can be increased to support more charsets
569 $xml_rep_table[chr($c)] = "&#$c;";
571 $xml_rep_table['"'] = '"';
572 $js_rep_table['"'] = '\\"';
573 $js_rep_table["'"] = "\\'";
574 $js_rep_table["\\"] = "\\\\";
575 // Unicode line and paragraph separators (#1486310)
576 $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '
';
577 $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '
';
580 // encode for javascript use
582 return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
584 // encode for plaintext
585 if ($enctype=='text')
586 return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
589 return rawurlencode($str);
593 return strtr($str, $xml_rep_table);
595 // no encoding given -> return original string
600 * Quote a given string.
601 * Shortcut function for rep_specialchars_output
603 * @return string HTML-quoted string
604 * @see rep_specialchars_output()
606 function Q($str, $mode='strict', $newlines=TRUE)
608 return rep_specialchars_output($str, 'html', $mode, $newlines);
612 * Quote a given string for javascript output.
613 * Shortcut function for rep_specialchars_output
615 * @return string JS-quoted string
616 * @see rep_specialchars_output()
620 return rep_specialchars_output($str, 'js');
625 * Read input value and convert it for internal use
626 * Performs stripslashes() and charset conversion if necessary
628 * @param string Field name to read
629 * @param int Source to get value from (GPC)
630 * @param boolean Allow HTML tags in field value
631 * @param string Charset to convert into
632 * @return string Field value or NULL if not available
634 function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
638 if ($source==RCUBE_INPUT_GET && isset($_GET[$fname]))
639 $value = $_GET[$fname];
640 else if ($source==RCUBE_INPUT_POST && isset($_POST[$fname]))
641 $value = $_POST[$fname];
642 else if ($source==RCUBE_INPUT_GPC)
644 if (isset($_POST[$fname]))
645 $value = $_POST[$fname];
646 else if (isset($_GET[$fname]))
647 $value = $_GET[$fname];
648 else if (isset($_COOKIE[$fname]))
649 $value = $_COOKIE[$fname];
652 return parse_input_value($value, $allow_html, $charset);
656 * Parse/validate input value. See get_input_value()
657 * Performs stripslashes() and charset conversion if necessary
659 * @param string Input value
660 * @param boolean Allow HTML tags in field value
661 * @param string Charset to convert into
662 * @return string Parsed value
664 function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
671 if (is_array($value)) {
672 foreach ($value as $idx => $val)
673 $value[$idx] = parse_input_value($val, $allow_html, $charset);
677 // strip single quotes if magic_quotes_sybase is enabled
678 if (ini_get('magic_quotes_sybase'))
679 $value = str_replace("''", "'", $value);
680 // strip slashes if magic_quotes enabled
681 else if (get_magic_quotes_gpc() || get_magic_quotes_runtime())
682 $value = stripslashes($value);
684 // remove HTML tags if not allowed
686 $value = strip_tags($value);
688 // convert to internal charset
689 if (is_object($OUTPUT) && $charset)
690 return rcube_charset_convert($value, $OUTPUT->get_charset(), $charset);
696 * Convert array of request parameters (prefixed with _)
697 * to a regular array with non-prefixed keys.
699 * @param int Source to get value from (GPC)
700 * @return array Hash array with all request parameters
702 function request2param($mode = RCUBE_INPUT_GPC)
705 $src = $mode == RCUBE_INPUT_GET ? $_GET : ($mode == RCUBE_INPUT_POST ? $_POST : $_REQUEST);
706 foreach ($src as $key => $value) {
707 $fname = $key[0] == '_' ? substr($key, 1) : $key;
708 $out[$fname] = get_input_value($key, $mode);
715 * Remove all non-ascii and non-word chars
718 function asciiwords($str, $css_id = false, $replace_with = '')
720 $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
721 return preg_replace("/[^$allowed]/i", $replace_with, $str);
725 * Convert the given string into a valid HTML identifier
726 * Same functionality as done in app.js with this.identifier_expr
729 function html_identifier($str)
731 return asciiwords($str, true, '_');
735 * Remove single and double quotes from given string
737 * @param string Input value
738 * @return string Dequoted string
740 function strip_quotes($str)
742 return str_replace(array("'", '"'), '', $str);
747 * Remove new lines characters from given string
749 * @param string Input value
750 * @return string Stripped string
752 function strip_newlines($str)
754 return preg_replace('/[\r\n]/', '', $str);
759 * Create a HTML table based on the given data
761 * @param array Named table attributes
762 * @param mixed Table row data. Either a two-dimensional array or a valid SQL result set
763 * @param array List of cols to show
764 * @param string Name of the identifier col
765 * @return string HTML table code
767 function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
771 $table = new html_table(/*array('cols' => count($a_show_cols))*/);
774 if (!$attrib['noheader'])
775 foreach ($a_show_cols as $col)
776 $table->add_header($col, Q(rcube_label($col)));
779 if (!is_array($table_data))
781 $db = $RCMAIL->get_dbh();
782 while ($table_data && ($sql_arr = $db->fetch_assoc($table_data)))
784 $zebra_class = $c % 2 ? 'even' : 'odd';
785 $table->add_row(array('id' => 'rcmrow' . html_identifier($sql_arr[$id_col]), 'class' => $zebra_class));
788 foreach ($a_show_cols as $col)
789 $table->add($col, Q($sql_arr[$col]));
796 foreach ($table_data as $row_data)
798 $zebra_class = $c % 2 ? 'even' : 'odd';
799 if (!empty($row_data['class']))
800 $zebra_class .= ' '.$row_data['class'];
802 $table->add_row(array('id' => 'rcmrow' . html_identifier($row_data[$id_col]), 'class' => $zebra_class));
805 foreach ($a_show_cols as $col)
806 $table->add($col, Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
812 return $table->show($attrib);
817 * Create an edit field for inclusion on a form
819 * @param string col field name
820 * @param string value field value
821 * @param array attrib HTML element attributes for field
822 * @param string type HTML element type (default 'text')
823 * @return string HTML field definition
825 function rcmail_get_edit_field($col, $value, $attrib, $type='text')
827 static $colcounts = array();
830 $attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
831 $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
833 if ($type == 'checkbox') {
834 $attrib['value'] = '1';
835 $input = new html_checkbox($attrib);
837 else if ($type == 'textarea') {
838 $attrib['cols'] = $attrib['size'];
839 $input = new html_textarea($attrib);
841 else if ($type == 'select') {
842 $input = new html_select($attrib);
843 $input->add('---', '');
844 $input->add(array_values($attrib['options']), array_keys($attrib['options']));
847 if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden')
848 $attrib['type'] = 'text';
849 $input = new html_inputfield($attrib);
852 // use value from post
853 if (isset($_POST[$fname])) {
854 $postvalue = get_input_value($fname, RCUBE_INPUT_POST, true);
855 $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
858 $out = $input->show($value);
865 * Replace all css definitions with #container [def]
866 * and remove css-inlined scripting
868 * @param string CSS source code
869 * @param string Container ID to use as prefix
870 * @return string Modified CSS source
872 function rcmail_mod_css_styles($source, $container_id)
875 $replacements = new rcube_string_replacer;
877 // ignore the whole block if evil styles are detected
878 $stripped = preg_replace('/[^a-z\(:;]/', '', rcmail_xss_entity_decode($source));
879 if (preg_match('/expression|behavior|url\(|import[^a]/', $stripped))
880 return '/* evil! */';
882 // remove css comments (sometimes used for some ugly hacks)
883 $source = preg_replace('!/\*(.+)\*/!Ums', '', $source);
885 // cut out all contents between { and }
886 while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos)))
888 $key = $replacements->add(substr($source, $pos+1, $pos2-($pos+1)));
889 $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
893 // remove html comments and add #container to each tag selector.
894 // also replace body definition because we also stripped off the <body> tag
895 $styles = preg_replace(
897 '/(^\s*<!--)|(-->\s*$)/',
898 '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
899 '/'.preg_quote($container_id, '/').'\s+body/i',
903 "\\1#$container_id \\2",
908 // put block contents back in
909 $styles = $replacements->resolve($styles);
916 * Decode escaped entities used by known XSS exploits.
917 * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
919 * @param string CSS content to decode
920 * @return string Decoded string
922 function rcmail_xss_entity_decode($content)
924 $out = html_entity_decode(html_entity_decode($content));
925 $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
926 $out = preg_replace('#/\*.*\*/#Um', '', $out);
932 * preg_replace_callback callback for rcmail_xss_entity_decode_callback
934 * @param array matches result from preg_replace_callback
935 * @return string decoded entity
937 function rcmail_xss_entity_decode_callback($matches)
939 return chr(hexdec($matches[1]));
943 * Compose a valid attribute string for HTML tags
945 * @param array Named tag attributes
946 * @param array List of allowed attributes
947 * @return string HTML formatted attribute string
949 function create_attrib_string($attrib, $allowed_attribs=array('id', 'class', 'style'))
951 // allow the following attributes to be added to the <iframe> tag
953 foreach ($allowed_attribs as $a)
954 if (isset($attrib[$a]))
955 $attrib_str .= sprintf(' %s="%s"', $a, str_replace('"', '"', $attrib[$a]));
962 * Convert a HTML attribute string attributes to an associative array (name => value)
964 * @param string Input string
965 * @return array Key-value pairs of parsed attributes
967 function parse_attrib_string($str)
970 preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', stripslashes($str), $regs, PREG_SET_ORDER);
972 // convert attributes to an associative array (name => value)
974 foreach ($regs as $attr) {
975 $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
984 * Improved equivalent to strtotime()
986 * @param string Date string
989 function rcube_strtotime($date)
991 // check for MS Outlook vCard date format YYYYMMDD
992 if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) {
993 return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
995 else if (is_numeric($date))
998 // support non-standard "GMTXXXX" literal
999 $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
1001 // if date parsing fails, we have a date in non-rfc format.
1002 // remove token from the end and try again
1003 while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
1004 $d = explode(' ', $date);
1007 $date = implode(' ', $d);
1015 * Convert the given date to a human readable form
1016 * This uses the date formatting properties from config
1018 * @param mixed Date representation (string or timestamp)
1019 * @param string Date format to use
1020 * @return string Formatted date string
1022 function format_date($date, $format=NULL)
1024 global $RCMAIL, $CONFIG;
1029 $ts = rcube_strtotime($date);
1034 // get user's timezone
1035 $tz = $RCMAIL->config->get_timezone();
1037 // convert time to user's timezone
1038 $timestamp = $ts - date('Z', $ts) + ($tz * 3600);
1040 // get current timestamp in user's timezone
1041 $now = time(); // local time
1042 $now -= (int)date('Z'); // make GMT time
1043 $now += ($tz * 3600); // user's time
1044 $now_date = getdate($now);
1046 $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1047 $week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1049 // define date format depending on current time
1051 if ($CONFIG['prettydate'] && $timestamp > $today_limit && $timestamp < $now) {
1052 $format = $CONFIG['date_today'] ? $CONFIG['date_today'] : 'H:i';
1055 else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now)
1056 $format = $CONFIG['date_short'] ? $CONFIG['date_short'] : 'D H:i';
1058 $format = $CONFIG['date_long'] ? $CONFIG['date_long'] : 'd.m.Y H:i';
1061 // strftime() format
1062 if (preg_match('/%[a-z]+/i', $format)) {
1063 $format = strftime($format, $timestamp);
1064 return $today ? (rcube_label('today') . ' ' . $format) : $format;
1067 // parse format string manually in order to provide localized weekday and month names
1068 // an alternative would be to convert the date() format string to fit with strftime()
1070 for($i=0; $i<strlen($format); $i++) {
1071 if ($format[$i]=='\\') // skip escape chars
1074 // write char "as-is"
1075 if ($format[$i]==' ' || $format{$i-1}=='\\')
1076 $out .= $format[$i];
1078 else if ($format[$i]=='D')
1079 $out .= rcube_label(strtolower(date('D', $timestamp)));
1081 else if ($format[$i]=='l')
1082 $out .= rcube_label(strtolower(date('l', $timestamp)));
1083 // month name (short)
1084 else if ($format[$i]=='M')
1085 $out .= rcube_label(strtolower(date('M', $timestamp)));
1086 // month name (long)
1087 else if ($format[$i]=='F')
1088 $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
1089 else if ($format[$i]=='x')
1090 $out .= strftime('%x %X', $timestamp);
1092 $out .= date($format[$i], $timestamp);
1096 $label = rcube_label('today');
1097 // replcae $ character with "Today" label (#1486120)
1098 if (strpos($out, '$') !== false) {
1099 $out = preg_replace('/\$/', $label, $out, 1);
1102 $out = $label . ' ' . $out;
1111 * Compose a valid representation of name and e-mail address
1113 * @param string E-mail address
1114 * @param string Person name
1115 * @return string Formatted string
1117 function format_email_recipient($email, $name='')
1119 if ($name && $name != $email) {
1120 // Special chars as defined by RFC 822 need to in quoted string (or escaped).
1121 return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, trim($email));
1124 return trim($email);
1129 * Return the mailboxlist in HTML
1131 * @param array Named parameters
1132 * @return string HTML code for the gui object
1134 function rcmail_mailbox_list($attrib)
1137 static $a_mailboxes;
1139 $attrib += array('maxlength' => 100, 'realnames' => false);
1141 // add some labels to client
1142 $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1144 $type = $attrib['type'] ? $attrib['type'] : 'ul';
1145 unset($attrib['type']);
1147 if ($type=='ul' && !$attrib['id'])
1148 $attrib['id'] = 'rcmboxlist';
1150 if (empty($attrib['folder_name']))
1151 $attrib['folder_name'] = '*';
1154 $mbox_name = $RCMAIL->imap->get_mailbox_name();
1156 // build the folders tree
1157 if (empty($a_mailboxes)) {
1159 $a_folders = $RCMAIL->imap->list_mailboxes('', $attrib['folder_name'], $attrib['folder_filter']);
1160 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1161 $a_mailboxes = array();
1163 foreach ($a_folders as $folder)
1164 rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1167 // allow plugins to alter the folder tree or to localize folder names
1168 $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
1170 if ($type == 'select') {
1171 $select = new html_select($attrib);
1173 // add no-selection option
1174 if ($attrib['noselection'])
1175 $select->add(rcube_label($attrib['noselection']), '');
1177 rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1178 $out = $select->show();
1181 $js_mailboxlist = array();
1182 $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1184 $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
1185 $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
1186 $RCMAIL->output->set_env('collapsed_folders', (string)$RCMAIL->config->get('collapsed_folders'));
1194 * Return the mailboxlist as html_select object
1196 * @param array Named parameters
1197 * @return html_select HTML drop-down object
1199 function rcmail_mailbox_select($p = array())
1203 $p += array('maxlength' => 100, 'realnames' => false);
1204 $a_mailboxes = array();
1206 if (empty($p['folder_name']))
1207 $p['folder_name'] = '*';
1209 if ($p['unsubscribed'])
1210 $list = $RCMAIL->imap->list_unsubscribed('', $p['folder_name'], $p['folder_filter']);
1212 $list = $RCMAIL->imap->list_mailboxes('', $p['folder_name'], $p['folder_filter']);
1214 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1216 foreach ($list as $folder) {
1217 if (empty($p['exceptions']) || !in_array($folder, $p['exceptions']))
1218 rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1221 $select = new html_select($p);
1223 if ($p['noselection'])
1224 $select->add($p['noselection'], '');
1226 rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p['exceptions']);
1233 * Create a hierarchical array of the mailbox list
1237 function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
1241 // Handle namespace prefix
1244 $n_folder = $folder;
1245 $folder = $RCMAIL->imap->mod_mailbox($folder);
1247 if ($n_folder != $folder) {
1248 $prefix = substr($n_folder, 0, -strlen($folder));
1252 $pos = strpos($folder, $delm);
1254 if ($pos !== false) {
1255 $subFolders = substr($folder, $pos+1);
1256 $currentFolder = substr($folder, 0, $pos);
1258 // sometimes folder has a delimiter as the last character
1259 if (!strlen($subFolders))
1261 else if (!isset($arrFolders[$currentFolder]))
1264 $virtual = $arrFolders[$currentFolder]['virtual'];
1267 $subFolders = false;
1268 $currentFolder = $folder;
1272 $path .= $prefix.$currentFolder;
1274 if (!isset($arrFolders[$currentFolder])) {
1275 // Check \Noselect option (if options are in cache)
1276 if (!$virtual && ($opts = $RCMAIL->imap->mailbox_options($path))) {
1277 $virtual = in_array('\\Noselect', $opts);
1280 $arrFolders[$currentFolder] = array(
1282 'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
1283 'virtual' => $virtual,
1284 'folders' => array());
1287 $arrFolders[$currentFolder]['virtual'] = $virtual;
1289 if (strlen($subFolders))
1290 rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1295 * Return html for a structured list <ul> for the mailbox tree
1299 function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
1301 global $RCMAIL, $CONFIG;
1303 $maxlength = intval($attrib['maxlength']);
1304 $realnames = (bool)$attrib['realnames'];
1305 $msgcounts = $RCMAIL->imap->get_cache('messagecount');
1309 foreach ($arrFolders as $key => $folder) {
1310 $zebra_class = (($nestLevel+1)*$idx) % 2 == 0 ? 'even' : 'odd';
1313 if (($folder_class = rcmail_folder_classname($folder['id'])) && !$realnames) {
1314 $foldername = rcube_label($folder_class);
1317 $foldername = $folder['name'];
1319 // shorten the folder name to a given length
1320 if ($maxlength && $maxlength > 1) {
1321 $fname = abbreviate_string($foldername, $maxlength);
1322 if ($fname != $foldername)
1323 $title = $foldername;
1324 $foldername = $fname;
1328 // make folder name safe for ids and class names
1329 $folder_id = html_identifier($folder['id']);
1330 $classes = array('mailbox');
1332 // set special class for Sent, Drafts, Trash and Junk
1333 if ($folder['id'] == $CONFIG['sent_mbox'])
1334 $classes[] = 'sent';
1335 else if ($folder['id'] == $CONFIG['drafts_mbox'])
1336 $classes[] = 'drafts';
1337 else if ($folder['id'] == $CONFIG['trash_mbox'])
1338 $classes[] = 'trash';
1339 else if ($folder['id'] == $CONFIG['junk_mbox'])
1340 $classes[] = 'junk';
1341 else if ($folder['id'] == 'INBOX')
1342 $classes[] = 'inbox';
1344 $classes[] = '_'.asciiwords($folder_class ? $folder_class : strtolower($folder['id']), true);
1346 $classes[] = $zebra_class;
1348 if ($folder['id'] == $mbox_name)
1349 $classes[] = 'selected';
1351 $collapsed = strpos($CONFIG['collapsed_folders'], '&'.rawurlencode($folder['id']).'&') !== false;
1352 $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1354 if ($folder['virtual'])
1355 $classes[] = 'virtual';
1357 $classes[] = 'unread';
1359 $js_name = JQ($folder['id']);
1360 $html_name = Q($foldername) . ($unread ? html::span('unreadcount', " ($unread)") : '');
1361 $link_attrib = $folder['virtual'] ? array() : array(
1362 'href' => rcmail_url('', array('_mbox' => $folder['id'])),
1363 'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
1364 'rel' => $folder['id'],
1368 $out .= html::tag('li', array(
1369 'id' => "rcmli".$folder_id,
1370 'class' => join(' ', $classes),
1372 html::a($link_attrib, $html_name) .
1373 (!empty($folder['folders']) ? html::div(array(
1374 'class' => ($collapsed ? 'collapsed' : 'expanded'),
1375 'style' => "position:absolute",
1376 'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
1377 ), ' ') : ''));
1379 $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
1381 if (!empty($folder['folders'])) {
1382 $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
1383 rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1395 * Return html for a flat list <select> for the mailbox tree
1399 function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0, $exceptions=array())
1403 foreach ($arrFolders as $key => $folder) {
1404 if (empty($exceptions) || !in_array($folder['id'], $exceptions)) {
1405 if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1406 $foldername = rcube_label($folder_class);
1408 $foldername = $folder['name'];
1410 // shorten the folder name to a given length
1411 if ($maxlength && $maxlength>1)
1412 $foldername = abbreviate_string($foldername, $maxlength);
1415 $select->add(str_repeat(' ', $nestLevel*4) . $foldername, $folder['id']);
1417 else if ($nestLevel)
1420 if (!empty($folder['folders']))
1421 $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
1422 $select, $realnames, $nestLevel+1, $exceptions);
1430 * Return internal name for the given folder if it matches the configured special folders
1434 function rcmail_folder_classname($folder_id)
1438 if ($folder_id == 'INBOX')
1441 // for these mailboxes we have localized labels and css classes
1442 foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1444 if ($folder_id == $CONFIG[$smbx.'_mbox'])
1451 * Try to localize the given IMAP folder name.
1452 * UTF-7 decode it in case no localized text was found
1454 * @param string Folder name
1455 * @return string Localized folder name in UTF-8 encoding
1457 function rcmail_localize_foldername($name)
1459 if ($folder_class = rcmail_folder_classname($name))
1460 return rcube_label($folder_class);
1462 return rcube_charset_convert($name, 'UTF7-IMAP');
1466 function rcmail_localize_folderpath($path)
1470 $protect_folders = $RCMAIL->config->get('protect_default_folders');
1471 $default_folders = (array) $RCMAIL->config->get('default_imap_folders');
1472 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1473 $path = explode($delimiter, $path);
1476 foreach ($path as $idx => $dir) {
1477 $directory = implode($delimiter, array_slice($path, 0, $idx+1));
1478 if ($protect_folders && in_array($directory, $default_folders)) {
1480 $result[] = rcmail_localize_foldername($directory);
1483 $result[] = rcube_charset_convert($dir, 'UTF7-IMAP');
1487 return implode($delimiter, $result);
1491 function rcmail_quota_display($attrib)
1496 $attrib['id'] = 'rcmquotadisplay';
1498 if(isset($attrib['display']))
1499 $_SESSION['quota_display'] = $attrib['display'];
1501 $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1503 $quota = rcmail_quota_content($attrib);
1505 $OUTPUT->add_script('rcmail.set_quota('.json_serialize($quota).');', 'docready');
1507 return html::span($attrib, '');
1511 function rcmail_quota_content($attrib=NULL)
1515 $quota = $RCMAIL->imap->get_quota();
1516 $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1518 $quota_result = (array) $quota;
1519 $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1521 if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1522 $quota_result['title'] = rcube_label('unlimited');
1523 $quota_result['percent'] = 0;
1525 else if ($quota['total']) {
1526 if (!isset($quota['percent']))
1527 $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1529 $title = sprintf('%s / %s (%.0f%%)',
1530 show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
1531 $quota_result['percent']);
1533 $quota_result['title'] = $title;
1535 if ($attrib['width'])
1536 $quota_result['width'] = $attrib['width'];
1537 if ($attrib['height'])
1538 $quota_result['height'] = $attrib['height'];
1541 $quota_result['title'] = rcube_label('unknown');
1542 $quota_result['percent'] = 0;
1545 return $quota_result;
1550 * Outputs error message according to server error/response codes
1552 * @param string Fallback message label
1553 * @param string Fallback message label arguments
1557 function rcmail_display_server_error($fallback=null, $fallback_args=null)
1561 $err_code = $RCMAIL->imap->get_error_code();
1562 $res_code = $RCMAIL->imap->get_response_code();
1564 if ($res_code == rcube_imap::NOPERM) {
1565 $RCMAIL->output->show_message('errornoperm', 'error');
1567 else if ($res_code == rcube_imap::READONLY) {
1568 $RCMAIL->output->show_message('errorreadonly', 'error');
1570 else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
1571 $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1573 else if ($fallback) {
1574 $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1582 * Output HTML editor scripts
1584 * @param string Editor mode
1587 function rcube_html_editor($mode='')
1589 global $RCMAIL, $CONFIG;
1591 $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1596 $lang = strtolower($_SESSION['language']);
1598 // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
1599 $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
1601 if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
1604 $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1605 $RCMAIL->output->include_script('editor.js');
1606 $RCMAIL->output->add_script(sprintf("rcmail_editor_init('\$__skin_path', '%s', %d, '%s');",
1607 JQ($lang), intval($CONFIG['enable_spellcheck']), $mode),
1613 * Replaces TinyMCE's emoticon images with plain-text representation
1615 * @param string HTML content
1616 * @return string HTML content
1618 function rcmail_replace_emoticons($html)
1621 '8-)' => 'smiley-cool',
1622 ':-#' => 'smiley-foot-in-mouth',
1623 ':-*' => 'smiley-kiss',
1624 ':-X' => 'smiley-sealed',
1625 ':-P' => 'smiley-tongue-out',
1626 ':-@' => 'smiley-yell',
1627 ":'(" => 'smiley-cry',
1628 ':-(' => 'smiley-frown',
1629 ':-D' => 'smiley-laughing',
1630 ':-)' => 'smiley-smile',
1631 ':-S' => 'smiley-undecided',
1632 ':-$' => 'smiley-embarassed',
1633 'O:-)' => 'smiley-innocent',
1634 ':-|' => 'smiley-money-mouth',
1635 ':-O' => 'smiley-surprised',
1636 ';-)' => 'smiley-wink',
1639 foreach ($emoticons as $idx => $file) {
1640 // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1641 $search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1645 return preg_replace($search, $replace, $html);
1650 * Send the given message using the configured method
1652 * @param object $message Reference to Mail_MIME object
1653 * @param string $from Sender address string
1654 * @param array $mailto Array of recipient address strings
1655 * @param array $smtp_error SMTP error array (reference)
1656 * @param string $body_file Location of file with saved message body (reference),
1657 * used when delay_file_io is enabled
1658 * @param array $smtp_opts SMTP options (e.g. DSN request)
1660 * @return boolean Send status.
1662 function rcmail_deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file=null, $smtp_opts=null)
1664 global $CONFIG, $RCMAIL;
1666 $headers = $message->headers();
1668 // send thru SMTP server using custom SMTP library
1669 if ($CONFIG['smtp_server']) {
1670 // generate list of recipients
1671 $a_recipients = array($mailto);
1673 if (strlen($headers['Cc']))
1674 $a_recipients[] = $headers['Cc'];
1675 if (strlen($headers['Bcc']))
1676 $a_recipients[] = $headers['Bcc'];
1678 // clean Bcc from header for recipients
1679 $send_headers = $headers;
1680 unset($send_headers['Bcc']);
1681 // here too, it because txtHeaders() below use $message->_headers not only $send_headers
1682 unset($message->_headers['Bcc']);
1684 $smtp_headers = $message->txtHeaders($send_headers, true);
1686 if ($message->getParam('delay_file_io')) {
1687 // use common temp dir
1688 $temp_dir = $RCMAIL->config->get('temp_dir');
1689 $body_file = tempnam($temp_dir, 'rcmMsg');
1690 if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
1691 raise_error(array('code' => 650, 'type' => 'php',
1692 'file' => __FILE__, 'line' => __LINE__,
1693 'message' => "Could not create message: ".$mime_result->getMessage()),
1697 $msg_body = fopen($body_file, 'r');
1699 $msg_body = $message->get();
1703 if (!is_object($RCMAIL->smtp))
1704 $RCMAIL->smtp_init(true);
1706 $sent = $RCMAIL->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
1707 $smtp_response = $RCMAIL->smtp->get_response();
1708 $smtp_error = $RCMAIL->smtp->get_error();
1712 raise_error(array('code' => 800, 'type' => 'smtp', 'line' => __LINE__, 'file' => __FILE__,
1713 'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
1715 // send mail using PHP's mail() function
1717 // unset some headers because they will be added by the mail() function
1718 $headers_enc = $message->headers($headers);
1719 $headers_php = $message->_headers;
1720 unset($headers_php['To'], $headers_php['Subject']);
1722 // reset stored headers and overwrite
1723 $message->_headers = array();
1724 $header_str = $message->txtHeaders($headers_php);
1727 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1728 if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
1729 $headers_enc['To'] = implode(', ', $m[1]);
1733 $msg_body = $message->get();
1735 if (PEAR::isError($msg_body))
1736 raise_error(array('code' => 650, 'type' => 'php',
1737 'file' => __FILE__, 'line' => __LINE__,
1738 'message' => "Could not create message: ".$msg_body->getMessage()),
1741 $delim = $RCMAIL->config->header_delimiter();
1742 $to = $headers_enc['To'];
1743 $subject = $headers_enc['Subject'];
1744 $header_str = rtrim($header_str);
1746 if ($delim != "\r\n") {
1747 $header_str = str_replace("\r\n", $delim, $header_str);
1748 $msg_body = str_replace("\r\n", $delim, $msg_body);
1749 $to = str_replace("\r\n", $delim, $to);
1750 $subject = str_replace("\r\n", $delim, $subject);
1753 if (ini_get('safe_mode'))
1754 $sent = mail($to, $subject, $msg_body, $header_str);
1756 $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1761 $RCMAIL->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1763 // remove MDN headers after sending
1764 unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1766 // get all recipients
1768 $mailto .= $headers['Cc'];
1769 if ($headers['Bcc'])
1770 $mailto .= $headers['Bcc'];
1771 if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1772 $mailto = implode(', ', array_unique($m[1]));
1774 if ($CONFIG['smtp_log']) {
1775 write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1776 $RCMAIL->user->get_username(),
1777 $_SERVER['REMOTE_ADDR'],
1779 !empty($smtp_response) ? join('; ', $smtp_response) : ''));
1783 if (is_resource($msg_body)) {
1787 $message->_headers = array();
1788 $message->headers($headers);
1794 // Returns unique Message-ID
1795 function rcmail_gen_message_id()
1799 $local_part = md5(uniqid('rcmail'.mt_rand(),true));
1800 $domain_part = $RCMAIL->user->get_username('domain');
1802 // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1803 if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1804 if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']))
1805 && preg_match('/\.[a-z]+$/i', $host)) {
1806 $domain_part = $host;
1808 else if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['SERVER_NAME']))
1809 && preg_match('/\.[a-z]+$/i', $host)) {
1810 $domain_part = $host;
1814 return sprintf('<%s@%s>', $local_part, $domain_part);
1818 // Returns RFC2822 formatted current date in user's timezone
1819 function rcmail_user_date()
1821 global $RCMAIL, $CONFIG;
1823 // get user's timezone
1824 $tz = $RCMAIL->config->get_timezone();
1826 $date = time() + $tz * 60 * 60;
1827 $date = gmdate('r', $date);
1828 $tz = sprintf('%+05d', intval($tz) * 100 + ($tz - intval($tz)) * 60);
1829 $date = preg_replace('/[+-][0-9]{4}$/', $tz, $date);
1836 * Check if working in SSL mode
1838 * @param integer HTTPS port number
1839 * @param boolean Enables 'use_https' option checking
1842 function rcube_https_check($port=null, $use_https=true)
1846 if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
1848 if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
1850 if ($port && $_SERVER['SERVER_PORT'] == $port)
1852 if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
1860 * For backward compatibility.
1862 * @global rcmail $RCMAIL
1863 * @param string $var_name Variable name.
1866 function rcube_sess_unset($var_name=null)
1870 $RCMAIL->session->remove($var_name);
1875 * Replaces hostname variables
1877 * @param string $name Hostname
1878 * @param string $host Optional IMAP hostname
1881 function rcube_parse_host($name, $host='')
1884 $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1885 // %d - domain name without first part, e.g. %n=mail.domain.tld, %d=domain.tld
1886 $d = preg_replace('/^[^\.]+\./', '', $n);
1888 $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
1889 // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1890 $z = preg_replace('/^[^\.]+\./', '', $h);
1891 // %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided
1892 if ( strpos($name, '%s') !== false ){
1893 $user_email = rcube_idn_convert(get_input_value('_user', RCUBE_INPUT_POST), true);
1894 if ( preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s) < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false )
1898 $name = str_replace(array('%n', '%d', '%h', '%z', '%s'), array($n, $d, $h, $z, $s[2]), $name);
1904 * E-mail address validation
1906 * @param string $email Email address
1907 * @param boolean $dns_check True to check dns
1910 function check_email($email, $dns_check=true)
1912 // Check for invalid characters
1913 if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
1916 // Check for length limit specified by RFC 5321 (#1486453)
1917 if (strlen($email) > 254)
1920 $email_array = explode('@', $email);
1922 // Check that there's one @ symbol
1923 if (count($email_array) < 2)
1926 $domain_part = array_pop($email_array);
1927 $local_part = implode('@', $email_array);
1929 // from PEAR::Validate
1931 ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
1932 ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) #2 OR dot-atom (RFC5322)
1935 if (!preg_match($regexp, $local_part))
1938 // Check domain part
1939 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))
1940 return true; // IP address
1942 // If not an IP address
1943 $domain_array = explode('.', $domain_part);
1944 if (sizeof($domain_array) < 2)
1945 return false; // Not enough parts to be a valid domain
1947 foreach ($domain_array as $part)
1948 if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
1951 if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
1954 if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1956 @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1957 foreach ($lookup as $line) {
1958 if (strpos($line, 'MX preference'))
1964 // find MX record(s)
1965 if (getmxrr($domain_part, $mx_records))
1968 // find any DNS record
1969 if (checkdnsrr($domain_part, 'ANY'))
1977 * Idn_to_ascii wrapper.
1978 * Intl/Idn modules version of this function doesn't work with e-mail address
1980 function rcube_idn_to_ascii($str)
1982 return rcube_idn_convert($str, true);
1986 * Idn_to_ascii wrapper.
1987 * Intl/Idn modules version of this function doesn't work with e-mail address
1989 function rcube_idn_to_utf8($str)
1991 return rcube_idn_convert($str, false);
1994 function rcube_idn_convert($input, $is_utf=false)
1996 if ($at = strpos($input, '@')) {
1997 $user = substr($input, 0, $at);
1998 $domain = substr($input, $at+1);
2004 $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
2006 if ($domain === false) {
2010 return $at ? $user . '@' . $domain : $domain;
2015 * Helper class to turn relative urls into absolute ones
2016 * using a predefined base
2018 class rcube_base_replacer
2022 public function __construct($base)
2024 $this->base_url = $base;
2027 public function callback($matches)
2029 return $matches[1] . '="' . make_absolute_url($matches[3], $this->base_url) . '"';
2034 /****** debugging and logging functions ********/
2037 * Print or write debug messages
2039 * @param mixed Debug message or data
2044 $args = func_get_args();
2046 if (class_exists('rcmail', false)) {
2047 $rcmail = rcmail::get_instance();
2048 if (is_object($rcmail->plugins)) {
2049 $plugin = $rcmail->plugins->exec_hook('console', array('args' => $args));
2050 if ($plugin['abort'])
2052 $args = $plugin['args'];
2057 foreach ($args as $arg)
2058 $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
2060 write_log('console', join(";\n", $msg));
2065 * Append a line to a logfile in the logs directory.
2066 * Date will be added automatically to the line.
2068 * @param $name name of log file
2069 * @param line Line to append
2072 function write_log($name, $line)
2074 global $CONFIG, $RCMAIL;
2076 if (!is_string($line))
2077 $line = var_export($line, true);
2079 if (empty($CONFIG['log_date_format']))
2080 $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
2082 $date = date($CONFIG['log_date_format']);
2084 // trigger logging hook
2085 if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
2086 $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
2087 $name = $log['name'];
2088 $line = $log['line'];
2089 $date = $log['date'];
2094 if ($CONFIG['log_driver'] == 'syslog') {
2095 $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
2096 syslog($prio, $line);
2100 $line = sprintf("[%s]: %s\n", $date, $line);
2102 // log_driver == 'file' is assumed here
2103 if (empty($CONFIG['log_dir']))
2104 $CONFIG['log_dir'] = INSTALL_PATH.'logs';
2106 // try to open specific log file for writing
2107 $logfile = $CONFIG['log_dir'].'/'.$name;
2108 if ($fp = @fopen($logfile, 'a')) {
2115 trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
2123 * Write login data (name, ID, IP address) to the 'userlogins' log file.
2127 function rcmail_log_login()
2131 if (!$RCMAIL->config->get('log_logins') || !$RCMAIL->user)
2134 write_log('userlogins', sprintf('Successful login for %s (ID: %d) from %s in session %s',
2135 $RCMAIL->user->get_username(), $RCMAIL->user->ID, rcmail_remote_ip(), session_id()));
2140 * Returns remote IP address and forwarded addresses if found
2142 * @return string Remote IP address(es)
2144 function rcmail_remote_ip()
2146 $address = $_SERVER['REMOTE_ADDR'];
2148 // append the NGINX X-Real-IP header, if set
2149 if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
2150 $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
2152 // append the X-Forwarded-For header, if set
2153 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
2154 $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
2157 if (!empty($remote_ip))
2158 $address .= '(' . implode(',', $remote_ip) . ')';
2165 * Check whether the HTTP referer matches the current request
2167 * @return boolean True if referer is the same host+path, false if not
2169 function rcube_check_referer()
2171 $uri = parse_url($_SERVER['REQUEST_URI']);
2172 $referer = parse_url(rc_request_header('Referer'));
2173 return $referer['host'] == rc_request_header('Host') && $referer['path'] == $uri['path'];
2181 function rcube_timer()
2183 return microtime(true);
2191 function rcube_print_time($timer, $label='Timer', $dest='console')
2193 static $print_count = 0;
2196 $now = rcube_timer();
2197 $diff = $now-$timer;
2200 $label = 'Timer '.$print_count;
2202 write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
2207 * Throw system error and show error page
2209 * @param array Named parameters
2210 * - code: Error code (required)
2211 * - type: Error type [php|db|imap|javascript] (required)
2212 * - message: Error message
2213 * - file: File where error occured
2214 * - line: Line where error occured
2215 * @param boolean True to log the error
2216 * @param boolean Terminate script execution
2218 // may be defined in Installer
2219 if (!function_exists('raise_error')) {
2220 function raise_error($arg=array(), $log=false, $terminate=false)
2222 global $__page_content, $CONFIG, $OUTPUT, $ERROR_CODE, $ERROR_MESSAGE;
2224 // report bug (if not incompatible browser)
2225 if ($log && $arg['type'] && $arg['message'])
2226 rcube_log_bug($arg);
2228 // display error page and terminate script
2230 $ERROR_CODE = $arg['code'];
2231 $ERROR_MESSAGE = $arg['message'];
2232 include INSTALL_PATH . 'program/steps/utils/error.inc';
2240 * Report error according to configured debug_level
2242 * @param array Named parameters
2244 * @see raise_error()
2246 function rcube_log_bug($arg_arr)
2250 $program = strtoupper($arg_arr['type']);
2251 $level = $CONFIG['debug_level'];
2253 // disable errors for ajax requests, write to log instead (#1487831)
2254 if (($level & 4) && !empty($_REQUEST['_remote'])) {
2255 $level = ($level ^ 4) | 1;
2258 // write error to local log file
2260 $post_query = ($_SERVER['REQUEST_METHOD'] == 'POST' ? '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']) : '');
2261 $log_entry = sprintf("%s Error: %s%s (%s %s)",
2263 $arg_arr['message'],
2264 $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
2265 $_SERVER['REQUEST_METHOD'],
2266 $_SERVER['REQUEST_URI'] . $post_query);
2268 if (!write_log('errors', $log_entry)) {
2269 // send error to PHPs error handler if write_log didn't succeed
2270 trigger_error($arg_arr['message']);
2274 // report the bug to the global bug reporting system
2276 // TODO: Send error via HTTP
2279 // show error if debug_mode is on
2281 print "<b>$program Error";
2283 if (!empty($arg_arr['file']) && !empty($arg_arr['line']))
2284 print " in $arg_arr[file] ($arg_arr[line])";
2286 print ':</b> ';
2287 print nl2br($arg_arr['message']);
2293 function rcube_upload_progress()
2297 $prefix = ini_get('apc.rfc1867_prefix');
2299 'action' => $RCMAIL->action,
2300 'name' => get_input_value('_progress', RCUBE_INPUT_GET),
2303 if (function_exists('apc_fetch')) {
2304 $status = apc_fetch($prefix . $params['name']);
2306 if (!empty($status)) {
2307 $status['percent'] = round($status['current']/$status['total']*100);
2308 $params = array_merge($status, $params);
2312 if (isset($params['percent']))
2313 $params['text'] = rcube_label(array('name' => 'uploadprogress', 'vars' => array(
2314 'percent' => $params['percent'] . '%',
2315 'current' => show_bytes($params['current']),
2316 'total' => show_bytes($params['total'])
2319 $RCMAIL->output->command('upload_progress_update', $params);
2320 $RCMAIL->output->send();
2323 function rcube_upload_init()
2327 // Enable upload progress bar
2328 if (($seconds = $RCMAIL->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
2329 if ($field_name = ini_get('apc.rfc1867_name')) {
2330 $RCMAIL->output->set_env('upload_progress_name', $field_name);
2331 $RCMAIL->output->set_env('upload_progress_time', (int) $seconds);
2335 // find max filesize value
2336 $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
2337 $max_postsize = parse_bytes(ini_get('post_max_size'));
2338 if ($max_postsize && $max_postsize < $max_filesize)
2339 $max_filesize = $max_postsize;
2341 $RCMAIL->output->set_env('max_filesize', $max_filesize);
2342 $max_filesize = show_bytes($max_filesize);
2343 $RCMAIL->output->set_env('filesizeerror', rcube_label(array(
2344 'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
2346 return $max_filesize;
2350 * Initializes client-side autocompletion
2352 function rcube_autocomplete_init()
2362 if (($threads = (int)$RCMAIL->config->get('autocomplete_threads')) > 0) {
2363 $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
2364 if (count($book_types) > 1) {
2365 $RCMAIL->output->set_env('autocomplete_threads', $threads);
2366 $RCMAIL->output->set_env('autocomplete_sources', $book_types);
2370 $RCMAIL->output->set_env('autocomplete_max', (int)$RCMAIL->config->get('autocomplete_max', 15));
2371 $RCMAIL->output->set_env('autocomplete_min_length', $RCMAIL->config->get('autocomplete_min_length'));
2372 $RCMAIL->output->add_label('autocompletechars');