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 5943 2012-03-02 11:56:25Z 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 if ($rcmail->config->get('messages_cache') || $rcmail->config->get('enable_caching')) {
173 $db->query("DELETE FROM ".get_table_name('cache_messages')
174 ." WHERE changed < " . $db->fromunixtime($ts));
176 $db->query("DELETE FROM ".get_table_name('cache_index')
177 ." WHERE changed < " . $db->fromunixtime($ts));
179 $db->query("DELETE FROM ".get_table_name('cache_thread')
180 ." WHERE changed < " . $db->fromunixtime($ts));
183 $db->query("DELETE FROM ".get_table_name('cache')
184 ." WHERE created < " . $db->fromunixtime($ts));
189 * Catch an error and throw an exception.
191 * @param int Level of the error
192 * @param string Error message
194 function rcube_error_handler($errno, $errstr)
196 throw new ErrorException($errstr, 0, $errno);
201 * Convert a string from one charset to another.
202 * Uses mbstring and iconv functions if possible
204 * @param string Input string
205 * @param string Suspected charset of the input string
206 * @param string Target charset to convert to; defaults to RCMAIL_CHARSET
207 * @return string Converted string
209 function rcube_charset_convert($str, $from, $to=NULL)
211 static $iconv_options = null;
212 static $mbstring_loaded = null;
213 static $mbstring_list = null;
218 $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : rcube_parse_charset($to);
219 $from = rcube_parse_charset($from);
221 if ($from == $to || empty($str) || empty($from))
224 // convert charset using iconv module
225 if (function_exists('iconv') && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') {
226 if ($iconv_options === null) {
227 // ignore characters not available in output charset
228 $iconv_options = '//IGNORE';
229 if (iconv('', $iconv_options, '') === false) {
230 // iconv implementation does not support options
235 // throw an exception if iconv reports an illegal character in input
236 // it means that input string has been truncated
237 set_error_handler('rcube_error_handler', E_NOTICE);
239 $_iconv = iconv($from, $to . $iconv_options, $str);
240 } catch (ErrorException $e) {
243 restore_error_handler();
244 if ($_iconv !== false) {
249 if ($mbstring_loaded === null)
250 $mbstring_loaded = extension_loaded('mbstring');
252 // convert charset using mbstring module
253 if ($mbstring_loaded) {
254 $aliases['WINDOWS-1257'] = 'ISO-8859-13';
256 if ($mbstring_list === null) {
257 $mbstring_list = mb_list_encodings();
258 $mbstring_list = array_map('strtoupper', $mbstring_list);
261 $mb_from = $aliases[$from] ? $aliases[$from] : $from;
262 $mb_to = $aliases[$to] ? $aliases[$to] : $to;
264 // return if encoding found, string matches encoding and convert succeeded
265 if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) {
266 if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from)))
271 // convert charset using bundled classes/functions
272 if ($to == 'UTF-8') {
273 if ($from == 'UTF7-IMAP') {
274 if ($_str = utf7_to_utf8($str))
277 else if ($from == 'UTF-7') {
278 if ($_str = rcube_utf7_to_utf8($str))
281 else if (($from == 'ISO-8859-1') && function_exists('utf8_encode')) {
282 return utf8_encode($str);
284 else if (class_exists('utf8')) {
286 $conv = new utf8($from);
288 $conv->loadCharset($from);
290 if($_str = $conv->strToUtf8($str))
296 // encode string for output
297 if ($from == 'UTF-8') {
298 // @TODO: we need a function for UTF-7 (RFC2152) conversion
299 if ($to == 'UTF7-IMAP' || $to == 'UTF-7') {
300 if ($_str = utf8_to_utf7($str))
303 else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
304 return utf8_decode($str);
306 else if (class_exists('utf8')) {
308 $conv = new utf8($to);
310 $conv->loadCharset($from);
312 if ($_str = $conv->strToUtf8($str))
318 // return UTF-8 or original string
324 * Parse and validate charset name string (see #1485758).
325 * Sometimes charset string is malformed, there are also charset aliases
326 * but we need strict names for charset conversion (specially utf8 class)
328 * @param string Input charset name
329 * @return string The validated charset name
331 function rcube_parse_charset($input)
333 static $charsets = array();
334 $charset = strtoupper($input);
336 if (isset($charsets[$input]))
337 return $charsets[$input];
339 $charset = preg_replace(array(
340 '/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO
341 '/\$.*$/', // e.g. _ISO-8859-JP$SIO
342 '/UNICODE-1-1-*/', // RFC1641/1642
343 '/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8)
346 if ($charset == 'BINARY')
347 return $charsets[$input] = null;
349 # Aliases: some of them from HTML5 spec.
351 'USASCII' => 'WINDOWS-1252',
352 'ANSIX31101983' => 'WINDOWS-1252',
353 'ANSIX341968' => 'WINDOWS-1252',
354 'UNKNOWN8BIT' => 'ISO-8859-15',
355 'UNKNOWN' => 'ISO-8859-15',
356 'USERDEFINED' => 'ISO-8859-15',
357 'KSC56011987' => 'EUC-KR',
360 'UNICODE' => 'UTF-8',
361 'UTF7IMAP' => 'UTF7-IMAP',
362 'TIS620' => 'WINDOWS-874',
363 'ISO88599' => 'WINDOWS-1254',
364 'ISO885911' => 'WINDOWS-874',
365 'MACROMAN' => 'MACINTOSH',
367 '128' => 'SHIFT-JIS',
372 '161' => 'WINDOWS-1253',
373 '162' => 'WINDOWS-1254',
374 '163' => 'WINDOWS-1258',
375 '177' => 'WINDOWS-1255',
376 '178' => 'WINDOWS-1256',
377 '186' => 'WINDOWS-1257',
378 '204' => 'WINDOWS-1251',
379 '222' => 'WINDOWS-874',
380 '238' => 'WINDOWS-1250',
382 'WINDOWS949' => 'UHC',
385 // allow A-Z and 0-9 only
386 $str = preg_replace('/[^A-Z0-9]/', '', $charset);
388 if (isset($aliases[$str]))
389 $result = $aliases[$str];
391 else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m))
392 $result = 'UTF-' . $m[1] . $m[2];
394 else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
395 $iso = 'ISO-8859-' . ($m[1] ? $m[1] : 1);
396 // some clients sends windows-1252 text as latin1,
397 // it is safe to use windows-1252 for all latin1
398 $result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
400 // handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
401 else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
402 $result = 'WINDOWS-' . $m[2];
405 else if (preg_match('/LATIN(.*)/', $str, $m)) {
406 $aliases = array('2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
407 '7' => 13, '8' => 14, '9' => 15, '10' => 16,
408 'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8);
410 // some clients sends windows-1252 text as latin1,
411 // it is safe to use windows-1252 for all latin1
413 $result = 'WINDOWS-1252';
415 // if iconv is not supported we need ISO labels, it's also safe for iconv
416 else if (!empty($aliases[$m[1]])) {
417 $result = 'ISO-8859-'.$aliases[$m[1]];
419 // iconv requires convertion of e.g. LATIN-1 to LATIN1
428 $charsets[$input] = $result;
435 * Converts string from standard UTF-7 (RFC 2152) to UTF-8.
437 * @param string Input string
438 * @return string The converted string
440 function rcube_utf7_to_utf8($str)
443 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
444 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
445 0,0,0,0, 0,0,0,0, 0,0,0,1, 0,0,0,0,
446 1,1,1,1, 1,1,1,1, 1,1,0,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,
449 0,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1,
450 1,1,1,1, 1,1,1,1, 1,1,1,0, 0,0,0,0,
453 $u7len = strlen($str);
457 for ($i=0; $u7len > 0; $i++, $u7len--)
466 for (; $u7len > 0; $i++, $u7len--)
470 if (!$Index_64[ord($u7)])
482 $res .= rcube_utf16_to_utf8(base64_decode($ch));
494 * Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
496 * @param string Input string
497 * @return string The converted string
499 function rcube_utf16_to_utf8($str)
504 for ($i = 0; $i < $len; $i += 2) {
505 $c = ord($str[$i]) << 8 | ord($str[$i + 1]);
506 if ($c >= 0x0001 && $c <= 0x007F) {
508 } else if ($c > 0x07FF) {
509 $dec .= chr(0xE0 | (($c >> 12) & 0x0F));
510 $dec .= chr(0x80 | (($c >> 6) & 0x3F));
511 $dec .= chr(0x80 | (($c >> 0) & 0x3F));
513 $dec .= chr(0xC0 | (($c >> 6) & 0x1F));
514 $dec .= chr(0x80 | (($c >> 0) & 0x3F));
522 * Replacing specials characters to a specific encoding type
524 * @param string Input string
525 * @param string Encoding type: text|html|xml|js|url
526 * @param string Replace mode for tags: show|replace|remove
527 * @param boolean Convert newlines
528 * @return string The quoted string
530 function rep_specialchars_output($str, $enctype='', $mode='', $newlines=TRUE)
532 static $html_encode_arr = false;
533 static $js_rep_table = false;
534 static $xml_rep_table = false;
537 $enctype = $OUTPUT->type;
539 // encode for HTML output
540 if ($enctype=='html')
542 if (!$html_encode_arr)
544 $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
545 unset($html_encode_arr['?']);
548 $ltpos = strpos($str, '<');
549 $encode_arr = $html_encode_arr;
551 // don't replace quotes and html tags
552 if (($mode=='show' || $mode=='') && $ltpos!==false && strpos($str, '>', $ltpos)!==false)
554 unset($encode_arr['"']);
555 unset($encode_arr['<']);
556 unset($encode_arr['>']);
557 unset($encode_arr['&']);
559 else if ($mode=='remove')
560 $str = strip_tags($str);
562 $out = strtr($str, $encode_arr);
564 // avoid douple quotation of &
565 $out = preg_replace('/&([A-Za-z]{2,6}|#[0-9]{2,4});/', '&\\1;', $out);
567 return $newlines ? nl2br($out) : $out;
570 // if the replace tables for XML and JS are not yet defined
571 if ($js_rep_table===false)
573 $js_rep_table = $xml_rep_table = array();
574 $xml_rep_table['&'] = '&';
576 for ($c=160; $c<256; $c++) // can be increased to support more charsets
577 $xml_rep_table[chr($c)] = "&#$c;";
579 $xml_rep_table['"'] = '"';
580 $js_rep_table['"'] = '\\"';
581 $js_rep_table["'"] = "\\'";
582 $js_rep_table["\\"] = "\\\\";
583 // Unicode line and paragraph separators (#1486310)
584 $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A8))] = '
';
585 $js_rep_table[chr(hexdec(E2)).chr(hexdec(80)).chr(hexdec(A9))] = '
';
588 // encode for javascript use
590 return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
592 // encode for plaintext
593 if ($enctype=='text')
594 return str_replace("\r\n", "\n", $mode=='remove' ? strip_tags($str) : $str);
597 return rawurlencode($str);
601 return strtr($str, $xml_rep_table);
603 // no encoding given -> return original string
608 * Quote a given string.
609 * Shortcut function for rep_specialchars_output
611 * @return string HTML-quoted string
612 * @see rep_specialchars_output()
614 function Q($str, $mode='strict', $newlines=TRUE)
616 return rep_specialchars_output($str, 'html', $mode, $newlines);
620 * Quote a given string for javascript output.
621 * Shortcut function for rep_specialchars_output
623 * @return string JS-quoted string
624 * @see rep_specialchars_output()
628 return rep_specialchars_output($str, 'js');
633 * Read input value and convert it for internal use
634 * Performs stripslashes() and charset conversion if necessary
636 * @param string Field name to read
637 * @param int Source to get value from (GPC)
638 * @param boolean Allow HTML tags in field value
639 * @param string Charset to convert into
640 * @return string Field value or NULL if not available
642 function get_input_value($fname, $source, $allow_html=FALSE, $charset=NULL)
646 if ($source == RCUBE_INPUT_GET) {
647 if (isset($_GET[$fname]))
648 $value = $_GET[$fname];
650 else if ($source == RCUBE_INPUT_POST) {
651 if (isset($_POST[$fname]))
652 $value = $_POST[$fname];
654 else if ($source == RCUBE_INPUT_GPC) {
655 if (isset($_POST[$fname]))
656 $value = $_POST[$fname];
657 else if (isset($_GET[$fname]))
658 $value = $_GET[$fname];
659 else if (isset($_COOKIE[$fname]))
660 $value = $_COOKIE[$fname];
663 return parse_input_value($value, $allow_html, $charset);
667 * Parse/validate input value. See get_input_value()
668 * Performs stripslashes() and charset conversion if necessary
670 * @param string Input value
671 * @param boolean Allow HTML tags in field value
672 * @param string Charset to convert into
673 * @return string Parsed value
675 function parse_input_value($value, $allow_html=FALSE, $charset=NULL)
682 if (is_array($value)) {
683 foreach ($value as $idx => $val)
684 $value[$idx] = parse_input_value($val, $allow_html, $charset);
688 // strip single quotes if magic_quotes_sybase is enabled
689 if (ini_get('magic_quotes_sybase'))
690 $value = str_replace("''", "'", $value);
691 // strip slashes if magic_quotes enabled
692 else if (get_magic_quotes_gpc() || get_magic_quotes_runtime())
693 $value = stripslashes($value);
695 // remove HTML tags if not allowed
697 $value = strip_tags($value);
699 $output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null;
701 // remove invalid characters (#1488124)
702 if ($output_charset == 'UTF-8')
703 $value = rc_utf8_clean($value);
705 // convert to internal charset
706 if ($charset && $output_charset)
707 $value = rcube_charset_convert($value, $output_charset, $charset);
713 * Convert array of request parameters (prefixed with _)
714 * to a regular array with non-prefixed keys.
716 * @param int Source to get value from (GPC)
717 * @return array Hash array with all request parameters
719 function request2param($mode = RCUBE_INPUT_GPC, $ignore = 'task|action')
722 $src = $mode == RCUBE_INPUT_GET ? $_GET : ($mode == RCUBE_INPUT_POST ? $_POST : $_REQUEST);
723 foreach ($src as $key => $value) {
724 $fname = $key[0] == '_' ? substr($key, 1) : $key;
725 if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname))
726 $out[$fname] = get_input_value($key, $mode);
733 * Remove all non-ascii and non-word chars
736 function asciiwords($str, $css_id = false, $replace_with = '')
738 $allowed = 'a-z0-9\_\-' . (!$css_id ? '\.' : '');
739 return preg_replace("/[^$allowed]/i", $replace_with, $str);
743 * Convert the given string into a valid HTML identifier
744 * Same functionality as done in app.js with rcube_webmail.html_identifier()
746 function html_identifier($str, $encode=false)
749 return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
751 return asciiwords($str, true, '_');
755 * Remove single and double quotes from given string
757 * @param string Input value
758 * @return string Dequoted string
760 function strip_quotes($str)
762 return str_replace(array("'", '"'), '', $str);
767 * Remove new lines characters from given string
769 * @param string Input value
770 * @return string Stripped string
772 function strip_newlines($str)
774 return preg_replace('/[\r\n]/', '', $str);
779 * Create a HTML table based on the given data
781 * @param array Named table attributes
782 * @param mixed Table row data. Either a two-dimensional array or a valid SQL result set
783 * @param array List of cols to show
784 * @param string Name of the identifier col
785 * @return string HTML table code
787 function rcube_table_output($attrib, $table_data, $a_show_cols, $id_col)
791 $table = new html_table(/*array('cols' => count($a_show_cols))*/);
794 if (!$attrib['noheader'])
795 foreach ($a_show_cols as $col)
796 $table->add_header($col, Q(rcube_label($col)));
799 if (!is_array($table_data))
801 $db = $RCMAIL->get_dbh();
802 while ($table_data && ($sql_arr = $db->fetch_assoc($table_data)))
804 $table->add_row(array('id' => 'rcmrow' . html_identifier($sql_arr[$id_col])));
807 foreach ($a_show_cols as $col)
808 $table->add($col, Q($sql_arr[$col]));
814 foreach ($table_data as $row_data)
816 $class = !empty($row_data['class']) ? $row_data['class'] : '';
818 $table->add_row(array('id' => 'rcmrow' . html_identifier($row_data[$id_col]), 'class' => $class));
821 foreach ($a_show_cols as $col)
822 $table->add($col, Q(is_array($row_data[$col]) ? $row_data[$col][0] : $row_data[$col]));
828 return $table->show($attrib);
833 * Create an edit field for inclusion on a form
835 * @param string col field name
836 * @param string value field value
837 * @param array attrib HTML element attributes for field
838 * @param string type HTML element type (default 'text')
839 * @return string HTML field definition
841 function rcmail_get_edit_field($col, $value, $attrib, $type='text')
843 static $colcounts = array();
846 $attrib['name'] = $fname . ($attrib['array'] ? '[]' : '');
847 $attrib['class'] = trim($attrib['class'] . ' ff_' . $col);
849 if ($type == 'checkbox') {
850 $attrib['value'] = '1';
851 $input = new html_checkbox($attrib);
853 else if ($type == 'textarea') {
854 $attrib['cols'] = $attrib['size'];
855 $input = new html_textarea($attrib);
857 else if ($type == 'select') {
858 $input = new html_select($attrib);
859 $input->add('---', '');
860 $input->add(array_values($attrib['options']), array_keys($attrib['options']));
863 if ($attrib['type'] != 'text' && $attrib['type'] != 'hidden')
864 $attrib['type'] = 'text';
865 $input = new html_inputfield($attrib);
868 // use value from post
869 if (isset($_POST[$fname])) {
870 $postvalue = get_input_value($fname, RCUBE_INPUT_POST, true);
871 $value = $attrib['array'] ? $postvalue[intval($colcounts[$col]++)] : $postvalue;
874 $out = $input->show($value);
881 * Replace all css definitions with #container [def]
882 * and remove css-inlined scripting
884 * @param string CSS source code
885 * @param string Container ID to use as prefix
886 * @return string Modified CSS source
888 function rcmail_mod_css_styles($source, $container_id, $allow_remote=false)
891 $replacements = new rcube_string_replacer;
893 // ignore the whole block if evil styles are detected
894 $source = rcmail_xss_entity_decode($source);
895 $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
896 $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');
897 if (preg_match("/$evilexpr/i", $stripped))
898 return '/* evil! */';
900 // cut out all contents between { and }
901 while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
902 $styles = substr($source, $pos+1, $pos2-($pos+1));
904 // check every line of a style block...
906 $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
907 foreach ($a_styles as $line) {
908 $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
909 // ... and only allow strict url() values
910 if (stripos($stripped, 'url(') && !preg_match('!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims', $line)) {
911 $a_styles = array('/* evil! */');
915 $styles = join(";\n", $a_styles);
918 $key = $replacements->add($styles);
919 $source = substr($source, 0, $pos+1) . $replacements->get_replacement($key) . substr($source, $pos2, strlen($source)-$pos2);
923 // remove html comments and add #container to each tag selector.
924 // also replace body definition because we also stripped off the <body> tag
925 $styles = preg_replace(
927 '/(^\s*<!--)|(-->\s*$)/',
928 '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
929 '/'.preg_quote($container_id, '/').'\s+body/i',
933 "\\1#$container_id \\2",
938 // put block contents back in
939 $styles = $replacements->resolve($styles);
946 * Decode escaped entities used by known XSS exploits.
947 * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
949 * @param string CSS content to decode
950 * @return string Decoded string
952 function rcmail_xss_entity_decode($content)
954 $out = html_entity_decode(html_entity_decode($content));
955 $out = preg_replace_callback('/\\\([0-9a-f]{4})/i', 'rcmail_xss_entity_decode_callback', $out);
956 $out = preg_replace('#/\*.*\*/#Ums', '', $out);
962 * preg_replace_callback callback for rcmail_xss_entity_decode_callback
964 * @param array matches result from preg_replace_callback
965 * @return string decoded entity
967 function rcmail_xss_entity_decode_callback($matches)
969 return chr(hexdec($matches[1]));
973 * Compose a valid attribute string for HTML tags
975 * @param array Named tag attributes
976 * @param array List of allowed attributes
977 * @return string HTML formatted attribute string
979 function create_attrib_string($attrib, $allowed_attribs=array('id', 'class', 'style'))
981 // allow the following attributes to be added to the <iframe> tag
983 foreach ($allowed_attribs as $a)
984 if (isset($attrib[$a]))
985 $attrib_str .= sprintf(' %s="%s"', $a, str_replace('"', '"', $attrib[$a]));
992 * Convert a HTML attribute string attributes to an associative array (name => value)
994 * @param string Input string
995 * @return array Key-value pairs of parsed attributes
997 function parse_attrib_string($str)
1000 preg_match_all('/\s*([-_a-z]+)=(["\'])??(?(2)([^\2]*)\2|(\S+?))/Ui', stripslashes($str), $regs, PREG_SET_ORDER);
1002 // convert attributes to an associative array (name => value)
1004 foreach ($regs as $attr) {
1005 $attrib[strtolower($attr[1])] = html_entity_decode($attr[3] . $attr[4]);
1014 * Improved equivalent to strtotime()
1016 * @param string Date string
1019 function rcube_strtotime($date)
1021 // check for MS Outlook vCard date format YYYYMMDD
1022 if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) {
1023 return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
1025 else if (is_numeric($date))
1028 // support non-standard "GMTXXXX" literal
1029 $date = preg_replace('/GMT\s*([+-][0-9]+)/', '\\1', $date);
1031 // if date parsing fails, we have a date in non-rfc format.
1032 // remove token from the end and try again
1033 while ((($ts = @strtotime($date)) === false) || ($ts < 0)) {
1034 $d = explode(' ', $date);
1037 $date = implode(' ', $d);
1045 * Convert the given date to a human readable form
1046 * This uses the date formatting properties from config
1048 * @param mixed Date representation (string or timestamp)
1049 * @param string Date format to use
1050 * @param bool Enables date convertion according to user timezone
1052 * @return string Formatted date string
1054 function format_date($date, $format=NULL, $convert=true)
1056 global $RCMAIL, $CONFIG;
1059 $ts = rcube_strtotime($date);
1065 // get user's timezone offset
1066 $tz = $RCMAIL->config->get_timezone();
1068 // convert time to user's timezone
1069 $timestamp = $ts - date('Z', $ts) + ($tz * 3600);
1071 // get current timestamp in user's timezone
1072 $now = time(); // local time
1073 $now -= (int)date('Z'); // make GMT time
1074 $now += ($tz * 3600); // user's time
1081 // define date format depending on current time
1083 $now_date = getdate($now);
1084 $today_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday'], $now_date['year']);
1085 $week_limit = mktime(0, 0, 0, $now_date['mon'], $now_date['mday']-6, $now_date['year']);
1087 if ($CONFIG['prettydate'] && $timestamp > $today_limit && $timestamp < $now) {
1088 $format = $RCMAIL->config->get('date_today', $RCMAIL->config->get('time_format', 'H:i'));
1091 else if ($CONFIG['prettydate'] && $timestamp > $week_limit && $timestamp < $now)
1092 $format = $RCMAIL->config->get('date_short', 'D H:i');
1094 $format = $RCMAIL->config->get('date_long', 'Y-m-d H:i');
1097 // strftime() format
1098 if (preg_match('/%[a-z]+/i', $format)) {
1099 $format = strftime($format, $timestamp);
1100 return $today ? (rcube_label('today') . ' ' . $format) : $format;
1103 // parse format string manually in order to provide localized weekday and month names
1104 // an alternative would be to convert the date() format string to fit with strftime()
1106 for($i=0; $i<strlen($format); $i++) {
1107 if ($format[$i]=='\\') // skip escape chars
1110 // write char "as-is"
1111 if ($format[$i]==' ' || $format{$i-1}=='\\')
1112 $out .= $format[$i];
1114 else if ($format[$i]=='D')
1115 $out .= rcube_label(strtolower(date('D', $timestamp)));
1117 else if ($format[$i]=='l')
1118 $out .= rcube_label(strtolower(date('l', $timestamp)));
1119 // month name (short)
1120 else if ($format[$i]=='M')
1121 $out .= rcube_label(strtolower(date('M', $timestamp)));
1122 // month name (long)
1123 else if ($format[$i]=='F')
1124 $out .= rcube_label('long'.strtolower(date('M', $timestamp)));
1125 else if ($format[$i]=='x')
1126 $out .= strftime('%x %X', $timestamp);
1128 $out .= date($format[$i], $timestamp);
1132 $label = rcube_label('today');
1133 // replcae $ character with "Today" label (#1486120)
1134 if (strpos($out, '$') !== false) {
1135 $out = preg_replace('/\$/', $label, $out, 1);
1138 $out = $label . ' ' . $out;
1147 * Compose a valid representation of name and e-mail address
1149 * @param string E-mail address
1150 * @param string Person name
1151 * @return string Formatted string
1153 function format_email_recipient($email, $name='')
1155 if ($name && $name != $email) {
1156 // Special chars as defined by RFC 822 need to in quoted string (or escaped).
1157 return sprintf('%s <%s>', preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $name) ? '"'.addcslashes($name, '"').'"' : $name, trim($email));
1160 return trim($email);
1165 * Return the mailboxlist in HTML
1167 * @param array Named parameters
1168 * @return string HTML code for the gui object
1170 function rcmail_mailbox_list($attrib)
1173 static $a_mailboxes;
1175 $attrib += array('maxlength' => 100, 'realnames' => false);
1177 // add some labels to client
1178 $RCMAIL->output->add_label('purgefolderconfirm', 'deletemessagesconfirm');
1180 $type = $attrib['type'] ? $attrib['type'] : 'ul';
1181 unset($attrib['type']);
1183 if ($type=='ul' && !$attrib['id'])
1184 $attrib['id'] = 'rcmboxlist';
1186 if (empty($attrib['folder_name']))
1187 $attrib['folder_name'] = '*';
1190 $mbox_name = $RCMAIL->imap->get_mailbox_name();
1192 // build the folders tree
1193 if (empty($a_mailboxes)) {
1195 $a_folders = $RCMAIL->imap->list_mailboxes('', $attrib['folder_name'], $attrib['folder_filter']);
1196 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1197 $a_mailboxes = array();
1199 foreach ($a_folders as $folder)
1200 rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1203 // allow plugins to alter the folder tree or to localize folder names
1204 $hook = $RCMAIL->plugins->exec_hook('render_mailboxlist', array('list' => $a_mailboxes, 'delimiter' => $delimiter));
1206 if ($type == 'select') {
1207 $select = new html_select($attrib);
1209 // add no-selection option
1210 if ($attrib['noselection'])
1211 $select->add(rcube_label($attrib['noselection']), '');
1213 rcmail_render_folder_tree_select($hook['list'], $mbox_name, $attrib['maxlength'], $select, $attrib['realnames']);
1214 $out = $select->show();
1217 $js_mailboxlist = array();
1218 $out = html::tag('ul', $attrib, rcmail_render_folder_tree_html($hook['list'], $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib);
1220 $RCMAIL->output->add_gui_object('mailboxlist', $attrib['id']);
1221 $RCMAIL->output->set_env('mailboxes', $js_mailboxlist);
1222 $RCMAIL->output->set_env('collapsed_folders', (string)$RCMAIL->config->get('collapsed_folders'));
1230 * Return the mailboxlist as html_select object
1232 * @param array Named parameters
1233 * @return html_select HTML drop-down object
1235 function rcmail_mailbox_select($p = array())
1239 $p += array('maxlength' => 100, 'realnames' => false);
1240 $a_mailboxes = array();
1242 if (empty($p['folder_name']))
1243 $p['folder_name'] = '*';
1245 if ($p['unsubscribed'])
1246 $list = $RCMAIL->imap->list_unsubscribed('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1248 $list = $RCMAIL->imap->list_mailboxes('', $p['folder_name'], $p['folder_filter'], $p['folder_rights']);
1250 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1252 foreach ($list as $folder) {
1253 if (empty($p['exceptions']) || !in_array($folder, $p['exceptions']))
1254 rcmail_build_folder_tree($a_mailboxes, $folder, $delimiter);
1257 $select = new html_select($p);
1259 if ($p['noselection'])
1260 $select->add($p['noselection'], '');
1262 rcmail_render_folder_tree_select($a_mailboxes, $mbox, $p['maxlength'], $select, $p['realnames'], 0, $p);
1269 * Create a hierarchical array of the mailbox list
1273 function rcmail_build_folder_tree(&$arrFolders, $folder, $delm='/', $path='')
1277 // Handle namespace prefix
1280 $n_folder = $folder;
1281 $folder = $RCMAIL->imap->mod_mailbox($folder);
1283 if ($n_folder != $folder) {
1284 $prefix = substr($n_folder, 0, -strlen($folder));
1288 $pos = strpos($folder, $delm);
1290 if ($pos !== false) {
1291 $subFolders = substr($folder, $pos+1);
1292 $currentFolder = substr($folder, 0, $pos);
1294 // sometimes folder has a delimiter as the last character
1295 if (!strlen($subFolders))
1297 else if (!isset($arrFolders[$currentFolder]))
1300 $virtual = $arrFolders[$currentFolder]['virtual'];
1303 $subFolders = false;
1304 $currentFolder = $folder;
1308 $path .= $prefix.$currentFolder;
1310 if (!isset($arrFolders[$currentFolder])) {
1311 $arrFolders[$currentFolder] = array(
1313 'name' => rcube_charset_convert($currentFolder, 'UTF7-IMAP'),
1314 'virtual' => $virtual,
1315 'folders' => array());
1318 $arrFolders[$currentFolder]['virtual'] = $virtual;
1320 if (strlen($subFolders))
1321 rcmail_build_folder_tree($arrFolders[$currentFolder]['folders'], $subFolders, $delm, $path.$delm);
1326 * Return html for a structured list <ul> for the mailbox tree
1330 function rcmail_render_folder_tree_html(&$arrFolders, &$mbox_name, &$jslist, $attrib, $nestLevel=0)
1332 global $RCMAIL, $CONFIG;
1334 $maxlength = intval($attrib['maxlength']);
1335 $realnames = (bool)$attrib['realnames'];
1336 $msgcounts = $RCMAIL->imap->get_cache('messagecount');
1339 foreach ($arrFolders as $key => $folder) {
1341 $folder_class = rcmail_folder_classname($folder['id']);
1342 $collapsed = strpos($CONFIG['collapsed_folders'], '&'.rawurlencode($folder['id']).'&') !== false;
1343 $unread = $msgcounts ? intval($msgcounts[$folder['id']]['UNSEEN']) : 0;
1345 if ($folder_class && !$realnames) {
1346 $foldername = rcube_label($folder_class);
1349 $foldername = $folder['name'];
1351 // shorten the folder name to a given length
1352 if ($maxlength && $maxlength > 1) {
1353 $fname = abbreviate_string($foldername, $maxlength);
1354 if ($fname != $foldername)
1355 $title = $foldername;
1356 $foldername = $fname;
1360 // make folder name safe for ids and class names
1361 $folder_id = html_identifier($folder['id'], true);
1362 $classes = array('mailbox');
1364 // set special class for Sent, Drafts, Trash and Junk
1366 $classes[] = $folder_class;
1368 if ($folder['id'] == $mbox_name)
1369 $classes[] = 'selected';
1371 if ($folder['virtual'])
1372 $classes[] = 'virtual';
1374 $classes[] = 'unread';
1376 $js_name = JQ($folder['id']);
1377 $html_name = Q($foldername) . ($unread ? html::span('unreadcount', " ($unread)") : '');
1378 $link_attrib = $folder['virtual'] ? array() : array(
1379 'href' => rcmail_url('', array('_mbox' => $folder['id'])),
1380 'onclick' => sprintf("return %s.command('list','%s',this)", JS_OBJECT_NAME, $js_name),
1381 'rel' => $folder['id'],
1385 $out .= html::tag('li', array(
1386 'id' => "rcmli".$folder_id,
1387 'class' => join(' ', $classes),
1389 html::a($link_attrib, $html_name) .
1390 (!empty($folder['folders']) ? html::div(array(
1391 'class' => ($collapsed ? 'collapsed' : 'expanded'),
1392 'style' => "position:absolute",
1393 'onclick' => sprintf("%s.command('collapse-folder', '%s')", JS_OBJECT_NAME, $js_name)
1394 ), ' ') : ''));
1396 $jslist[$folder_id] = array('id' => $folder['id'], 'name' => $foldername, 'virtual' => $folder['virtual']);
1398 if (!empty($folder['folders'])) {
1399 $out .= html::tag('ul', array('style' => ($collapsed ? "display:none;" : null)),
1400 rcmail_render_folder_tree_html($folder['folders'], $mbox_name, $jslist, $attrib, $nestLevel+1));
1411 * Return html for a flat list <select> for the mailbox tree
1415 function rcmail_render_folder_tree_select(&$arrFolders, &$mbox_name, $maxlength, &$select, $realnames=false, $nestLevel=0, $opts=array())
1421 foreach ($arrFolders as $key => $folder) {
1422 // skip exceptions (and its subfolders)
1423 if (!empty($opts['exceptions']) && in_array($folder['id'], $opts['exceptions'])) {
1427 // skip folders in which it isn't possible to create subfolders
1428 if (!empty($opts['skip_noinferiors']) && ($attrs = $RCMAIL->imap->mailbox_attributes($folder['id']))
1429 && in_array('\\Noinferiors', $attrs)
1434 if (!$realnames && ($folder_class = rcmail_folder_classname($folder['id'])))
1435 $foldername = rcube_label($folder_class);
1437 $foldername = $folder['name'];
1439 // shorten the folder name to a given length
1440 if ($maxlength && $maxlength>1)
1441 $foldername = abbreviate_string($foldername, $maxlength);
1444 $select->add(str_repeat(' ', $nestLevel*4) . $foldername, $folder['id']);
1446 if (!empty($folder['folders']))
1447 $out .= rcmail_render_folder_tree_select($folder['folders'], $mbox_name, $maxlength,
1448 $select, $realnames, $nestLevel+1, $opts);
1456 * Return internal name for the given folder if it matches the configured special folders
1460 function rcmail_folder_classname($folder_id)
1464 if ($folder_id == 'INBOX')
1467 // for these mailboxes we have localized labels and css classes
1468 foreach (array('sent', 'drafts', 'trash', 'junk') as $smbx)
1470 if ($folder_id == $CONFIG[$smbx.'_mbox'])
1477 * Try to localize the given IMAP folder name.
1478 * UTF-7 decode it in case no localized text was found
1480 * @param string Folder name
1481 * @return string Localized folder name in UTF-8 encoding
1483 function rcmail_localize_foldername($name)
1485 if ($folder_class = rcmail_folder_classname($name))
1486 return rcube_label($folder_class);
1488 return rcube_charset_convert($name, 'UTF7-IMAP');
1492 function rcmail_localize_folderpath($path)
1496 $protect_folders = $RCMAIL->config->get('protect_default_folders');
1497 $default_folders = (array) $RCMAIL->config->get('default_imap_folders');
1498 $delimiter = $RCMAIL->imap->get_hierarchy_delimiter();
1499 $path = explode($delimiter, $path);
1502 foreach ($path as $idx => $dir) {
1503 $directory = implode($delimiter, array_slice($path, 0, $idx+1));
1504 if ($protect_folders && in_array($directory, $default_folders)) {
1506 $result[] = rcmail_localize_foldername($directory);
1509 $result[] = rcube_charset_convert($dir, 'UTF7-IMAP');
1513 return implode($delimiter, $result);
1517 function rcmail_quota_display($attrib)
1522 $attrib['id'] = 'rcmquotadisplay';
1524 if(isset($attrib['display']))
1525 $_SESSION['quota_display'] = $attrib['display'];
1527 $OUTPUT->add_gui_object('quotadisplay', $attrib['id']);
1529 $quota = rcmail_quota_content($attrib);
1531 $OUTPUT->add_script('rcmail.set_quota('.json_serialize($quota).');', 'docready');
1533 return html::span($attrib, '');
1537 function rcmail_quota_content($attrib=NULL)
1541 $quota = $RCMAIL->imap->get_quota();
1542 $quota = $RCMAIL->plugins->exec_hook('quota', $quota);
1544 $quota_result = (array) $quota;
1545 $quota_result['type'] = isset($_SESSION['quota_display']) ? $_SESSION['quota_display'] : '';
1547 if (!$quota['total'] && $RCMAIL->config->get('quota_zero_as_unlimited')) {
1548 $quota_result['title'] = rcube_label('unlimited');
1549 $quota_result['percent'] = 0;
1551 else if ($quota['total']) {
1552 if (!isset($quota['percent']))
1553 $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
1555 $title = sprintf('%s / %s (%.0f%%)',
1556 show_bytes($quota['used'] * 1024), show_bytes($quota['total'] * 1024),
1557 $quota_result['percent']);
1559 $quota_result['title'] = $title;
1561 if ($attrib['width'])
1562 $quota_result['width'] = $attrib['width'];
1563 if ($attrib['height'])
1564 $quota_result['height'] = $attrib['height'];
1567 $quota_result['title'] = rcube_label('unknown');
1568 $quota_result['percent'] = 0;
1571 return $quota_result;
1576 * Outputs error message according to server error/response codes
1578 * @param string Fallback message label
1579 * @param string Fallback message label arguments
1583 function rcmail_display_server_error($fallback=null, $fallback_args=null)
1587 $err_code = $RCMAIL->imap->get_error_code();
1588 $res_code = $RCMAIL->imap->get_response_code();
1590 if ($res_code == rcube_imap::NOPERM) {
1591 $RCMAIL->output->show_message('errornoperm', 'error');
1593 else if ($res_code == rcube_imap::READONLY) {
1594 $RCMAIL->output->show_message('errorreadonly', 'error');
1596 else if ($err_code && ($err_str = $RCMAIL->imap->get_error_str())) {
1597 // try to detect access rights problem and display appropriate message
1598 if (stripos($err_str, 'Permission denied') !== false)
1599 $RCMAIL->output->show_message('errornoperm', 'error');
1601 $RCMAIL->output->show_message('servererrormsg', 'error', array('msg' => $err_str));
1603 else if ($fallback) {
1604 $RCMAIL->output->show_message($fallback, 'error', $fallback_args);
1612 * Output HTML editor scripts
1614 * @param string Editor mode
1617 function rcube_html_editor($mode='')
1619 global $RCMAIL, $CONFIG;
1621 $hook = $RCMAIL->plugins->exec_hook('html_editor', array('mode' => $mode));
1626 $lang = strtolower($_SESSION['language']);
1628 // TinyMCE uses 'tw' for zh_TW (which is wrong, because tw is a code of Twi language)
1629 $lang = ($lang == 'zh_tw') ? 'tw' : substr($lang, 0, 2);
1631 if (!file_exists(INSTALL_PATH . 'program/js/tiny_mce/langs/'.$lang.'.js'))
1634 $RCMAIL->output->include_script('tiny_mce/tiny_mce.js');
1635 $RCMAIL->output->include_script('editor.js');
1636 $RCMAIL->output->add_script(sprintf("rcmail_editor_init(%s)",
1639 'skin_path' => '$__skin_path',
1641 'spellcheck' => intval($CONFIG['enable_spellcheck']),
1642 'spelldict' => intval($CONFIG['spellcheck_dictionary']),
1648 * Replaces TinyMCE's emoticon images with plain-text representation
1650 * @param string HTML content
1651 * @return string HTML content
1653 function rcmail_replace_emoticons($html)
1656 '8-)' => 'smiley-cool',
1657 ':-#' => 'smiley-foot-in-mouth',
1658 ':-*' => 'smiley-kiss',
1659 ':-X' => 'smiley-sealed',
1660 ':-P' => 'smiley-tongue-out',
1661 ':-@' => 'smiley-yell',
1662 ":'(" => 'smiley-cry',
1663 ':-(' => 'smiley-frown',
1664 ':-D' => 'smiley-laughing',
1665 ':-)' => 'smiley-smile',
1666 ':-S' => 'smiley-undecided',
1667 ':-$' => 'smiley-embarassed',
1668 'O:-)' => 'smiley-innocent',
1669 ':-|' => 'smiley-money-mouth',
1670 ':-O' => 'smiley-surprised',
1671 ';-)' => 'smiley-wink',
1674 foreach ($emoticons as $idx => $file) {
1675 // <img title="Cry" src="http://.../program/js/tiny_mce/plugins/emotions/img/smiley-cry.gif" border="0" alt="Cry" />
1676 $search[] = '/<img title="[a-z ]+" src="https?:\/\/[a-z0-9_.\/-]+\/tiny_mce\/plugins\/emotions\/img\/'.$file.'.gif"[^>]+\/>/i';
1680 return preg_replace($search, $replace, $html);
1685 * Send the given message using the configured method
1687 * @param object $message Reference to Mail_MIME object
1688 * @param string $from Sender address string
1689 * @param array $mailto Array of recipient address strings
1690 * @param array $smtp_error SMTP error array (reference)
1691 * @param string $body_file Location of file with saved message body (reference),
1692 * used when delay_file_io is enabled
1693 * @param array $smtp_opts SMTP options (e.g. DSN request)
1695 * @return boolean Send status.
1697 function rcmail_deliver_message(&$message, $from, $mailto, &$smtp_error, &$body_file=null, $smtp_opts=null)
1699 global $CONFIG, $RCMAIL;
1701 $headers = $message->headers();
1703 // send thru SMTP server using custom SMTP library
1704 if ($CONFIG['smtp_server']) {
1705 // generate list of recipients
1706 $a_recipients = array($mailto);
1708 if (strlen($headers['Cc']))
1709 $a_recipients[] = $headers['Cc'];
1710 if (strlen($headers['Bcc']))
1711 $a_recipients[] = $headers['Bcc'];
1713 // clean Bcc from header for recipients
1714 $send_headers = $headers;
1715 unset($send_headers['Bcc']);
1716 // here too, it because txtHeaders() below use $message->_headers not only $send_headers
1717 unset($message->_headers['Bcc']);
1719 $smtp_headers = $message->txtHeaders($send_headers, true);
1721 if ($message->getParam('delay_file_io')) {
1722 // use common temp dir
1723 $temp_dir = $RCMAIL->config->get('temp_dir');
1724 $body_file = tempnam($temp_dir, 'rcmMsg');
1725 if (PEAR::isError($mime_result = $message->saveMessageBody($body_file))) {
1726 raise_error(array('code' => 650, 'type' => 'php',
1727 'file' => __FILE__, 'line' => __LINE__,
1728 'message' => "Could not create message: ".$mime_result->getMessage()),
1732 $msg_body = fopen($body_file, 'r');
1734 $msg_body = $message->get();
1738 if (!is_object($RCMAIL->smtp))
1739 $RCMAIL->smtp_init(true);
1741 $sent = $RCMAIL->smtp->send_mail($from, $a_recipients, $smtp_headers, $msg_body, $smtp_opts);
1742 $smtp_response = $RCMAIL->smtp->get_response();
1743 $smtp_error = $RCMAIL->smtp->get_error();
1747 raise_error(array('code' => 800, 'type' => 'smtp', 'line' => __LINE__, 'file' => __FILE__,
1748 'message' => "SMTP error: ".join("\n", $smtp_response)), TRUE, FALSE);
1750 // send mail using PHP's mail() function
1752 // unset some headers because they will be added by the mail() function
1753 $headers_enc = $message->headers($headers);
1754 $headers_php = $message->_headers;
1755 unset($headers_php['To'], $headers_php['Subject']);
1757 // reset stored headers and overwrite
1758 $message->_headers = array();
1759 $header_str = $message->txtHeaders($headers_php);
1762 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1763 if (preg_match_all('/<([^@]+@[^>]+)>/', $headers_enc['To'], $m)) {
1764 $headers_enc['To'] = implode(', ', $m[1]);
1768 $msg_body = $message->get();
1770 if (PEAR::isError($msg_body))
1771 raise_error(array('code' => 650, 'type' => 'php',
1772 'file' => __FILE__, 'line' => __LINE__,
1773 'message' => "Could not create message: ".$msg_body->getMessage()),
1776 $delim = $RCMAIL->config->header_delimiter();
1777 $to = $headers_enc['To'];
1778 $subject = $headers_enc['Subject'];
1779 $header_str = rtrim($header_str);
1781 if ($delim != "\r\n") {
1782 $header_str = str_replace("\r\n", $delim, $header_str);
1783 $msg_body = str_replace("\r\n", $delim, $msg_body);
1784 $to = str_replace("\r\n", $delim, $to);
1785 $subject = str_replace("\r\n", $delim, $subject);
1788 if (ini_get('safe_mode'))
1789 $sent = mail($to, $subject, $msg_body, $header_str);
1791 $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
1796 $RCMAIL->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $msg_body));
1798 // remove MDN headers after sending
1799 unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
1801 // get all recipients
1803 $mailto .= $headers['Cc'];
1804 if ($headers['Bcc'])
1805 $mailto .= $headers['Bcc'];
1806 if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
1807 $mailto = implode(', ', array_unique($m[1]));
1809 if ($CONFIG['smtp_log']) {
1810 write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
1811 $RCMAIL->user->get_username(),
1812 $_SERVER['REMOTE_ADDR'],
1814 !empty($smtp_response) ? join('; ', $smtp_response) : ''));
1818 if (is_resource($msg_body)) {
1822 $message->_headers = array();
1823 $message->headers($headers);
1829 // Returns unique Message-ID
1830 function rcmail_gen_message_id()
1834 $local_part = md5(uniqid('rcmail'.mt_rand(),true));
1835 $domain_part = $RCMAIL->user->get_username('domain');
1837 // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
1838 if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
1839 if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['HTTP_HOST']))
1840 && preg_match('/\.[a-z]+$/i', $host)) {
1841 $domain_part = $host;
1843 else if (($host = preg_replace('/:[0-9]+$/', '', $_SERVER['SERVER_NAME']))
1844 && preg_match('/\.[a-z]+$/i', $host)) {
1845 $domain_part = $host;
1849 return sprintf('<%s@%s>', $local_part, $domain_part);
1853 // Returns RFC2822 formatted current date in user's timezone
1854 function rcmail_user_date()
1856 global $RCMAIL, $CONFIG;
1858 // get user's timezone
1859 $tz = $RCMAIL->config->get_timezone();
1861 $date = time() + $tz * 60 * 60;
1862 $date = gmdate('r', $date);
1863 $tz = sprintf('%+05d', intval($tz) * 100 + ($tz - intval($tz)) * 60);
1864 $date = preg_replace('/[+-][0-9]{4}$/', $tz, $date);
1871 * Check if working in SSL mode
1873 * @param integer HTTPS port number
1874 * @param boolean Enables 'use_https' option checking
1877 function rcube_https_check($port=null, $use_https=true)
1881 if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
1883 if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https')
1885 if ($port && $_SERVER['SERVER_PORT'] == $port)
1887 if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https'))
1895 * For backward compatibility.
1897 * @global rcmail $RCMAIL
1898 * @param string $var_name Variable name.
1901 function rcube_sess_unset($var_name=null)
1905 $RCMAIL->session->remove($var_name);
1910 * Replaces hostname variables
1912 * @param string $name Hostname
1913 * @param string $host Optional IMAP hostname
1916 function rcube_parse_host($name, $host='')
1919 $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
1920 // %d - domain name without first part, e.g. %n=mail.domain.tld, %d=domain.tld
1921 $d = preg_replace('/^[^\.]+\./', '', $n);
1923 $h = $_SESSION['imap_host'] ? $_SESSION['imap_host'] : $host;
1924 // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
1925 $z = preg_replace('/^[^\.]+\./', '', $h);
1926 // %s - domain name after the '@' from e-mail address provided at login screen. Returns FALSE if an invalid email is provided
1927 if ( strpos($name, '%s') !== false ){
1928 $user_email = rcube_idn_convert(get_input_value('_user', RCUBE_INPUT_POST), true);
1929 if ( preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s) < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false )
1933 $name = str_replace(array('%n', '%d', '%h', '%z', '%s'), array($n, $d, $h, $z, $s[2]), $name);
1939 * E-mail address validation
1941 * @param string $email Email address
1942 * @param boolean $dns_check True to check dns
1945 function check_email($email, $dns_check=true)
1947 // Check for invalid characters
1948 if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email))
1951 // Check for length limit specified by RFC 5321 (#1486453)
1952 if (strlen($email) > 254)
1955 $email_array = explode('@', $email);
1957 // Check that there's one @ symbol
1958 if (count($email_array) < 2)
1961 $domain_part = array_pop($email_array);
1962 $local_part = implode('@', $email_array);
1964 // from PEAR::Validate
1966 ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
1967 ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) #2 OR dot-atom (RFC5322)
1970 if (!preg_match($regexp, $local_part))
1973 // Check domain part
1974 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))
1975 return true; // IP address
1977 // If not an IP address
1978 $domain_array = explode('.', $domain_part);
1979 if (sizeof($domain_array) < 2)
1980 return false; // Not enough parts to be a valid domain
1982 foreach ($domain_array as $part)
1983 if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part))
1986 if (!$dns_check || !rcmail::get_instance()->config->get('email_dns_check'))
1989 if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
1991 @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
1992 foreach ($lookup as $line) {
1993 if (strpos($line, 'MX preference'))
1999 // find MX record(s)
2000 if (getmxrr($domain_part, $mx_records))
2003 // find any DNS record
2004 if (checkdnsrr($domain_part, 'ANY'))
2012 * Idn_to_ascii wrapper.
2013 * Intl/Idn modules version of this function doesn't work with e-mail address
2015 function rcube_idn_to_ascii($str)
2017 return rcube_idn_convert($str, true);
2021 * Idn_to_ascii wrapper.
2022 * Intl/Idn modules version of this function doesn't work with e-mail address
2024 function rcube_idn_to_utf8($str)
2026 return rcube_idn_convert($str, false);
2029 function rcube_idn_convert($input, $is_utf=false)
2031 if ($at = strpos($input, '@')) {
2032 $user = substr($input, 0, $at);
2033 $domain = substr($input, $at+1);
2039 $domain = $is_utf ? idn_to_ascii($domain) : idn_to_utf8($domain);
2041 if ($domain === false) {
2045 return $at ? $user . '@' . $domain : $domain;
2050 * Helper class to turn relative urls into absolute ones
2051 * using a predefined base
2053 class rcube_base_replacer
2057 public function __construct($base)
2059 $this->base_url = $base;
2062 public function callback($matches)
2064 return $matches[1] . '="' . self::absolute_url($matches[3], $this->base_url) . '"';
2067 public function replace($body)
2069 return preg_replace_callback(array(
2070 '/(src|background|href)=(["\']?)([^"\'\s]+)(\2|\s|>)/Ui',
2071 '/(url\s*\()(["\']?)([^"\'\)\s]+)(\2)\)/Ui',
2073 array($this, 'callback'), $body);
2077 * Convert paths like ../xxx to an absolute path using a base url
2079 * @param string $path Relative path
2080 * @param string $base_url Base URL
2082 * @return string Absolute URL
2084 public static function absolute_url($path, $base_url)
2086 $host_url = $base_url;
2089 // check if path is an absolute URL
2090 if (preg_match('/^[fhtps]+:\/\//', $path)) {
2094 // check if path is a content-id scheme
2095 if (strpos($path, 'cid:') === 0) {
2099 // cut base_url to the last directory
2100 if (strrpos($base_url, '/') > 7) {
2101 $host_url = substr($base_url, 0, strpos($base_url, '/', 7));
2102 $base_url = substr($base_url, 0, strrpos($base_url, '/'));
2105 // $path is absolute
2106 if ($path[0] == '/') {
2107 $abs_path = $host_url.$path;
2110 // strip './' because its the same as ''
2111 $path = preg_replace('/^\.\//', '', $path);
2113 if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
2114 foreach ($matches as $a_match) {
2115 if (strrpos($base_url, '/')) {
2116 $base_url = substr($base_url, 0, strrpos($base_url, '/'));
2118 $path = substr($path, 3);
2122 $abs_path = $base_url.'/'.$path;
2130 /****** debugging and logging functions ********/
2133 * Print or write debug messages
2135 * @param mixed Debug message or data
2140 $args = func_get_args();
2142 if (class_exists('rcmail', false)) {
2143 $rcmail = rcmail::get_instance();
2144 if (is_object($rcmail->plugins)) {
2145 $plugin = $rcmail->plugins->exec_hook('console', array('args' => $args));
2146 if ($plugin['abort'])
2148 $args = $plugin['args'];
2153 foreach ($args as $arg)
2154 $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
2156 write_log('console', join(";\n", $msg));
2161 * Append a line to a logfile in the logs directory.
2162 * Date will be added automatically to the line.
2164 * @param $name name of log file
2165 * @param line Line to append
2168 function write_log($name, $line)
2170 global $CONFIG, $RCMAIL;
2172 if (!is_string($line))
2173 $line = var_export($line, true);
2175 if (empty($CONFIG['log_date_format']))
2176 $CONFIG['log_date_format'] = 'd-M-Y H:i:s O';
2178 $date = date($CONFIG['log_date_format']);
2180 // trigger logging hook
2181 if (is_object($RCMAIL) && is_object($RCMAIL->plugins)) {
2182 $log = $RCMAIL->plugins->exec_hook('write_log', array('name' => $name, 'date' => $date, 'line' => $line));
2183 $name = $log['name'];
2184 $line = $log['line'];
2185 $date = $log['date'];
2190 if ($CONFIG['log_driver'] == 'syslog') {
2191 $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
2192 syslog($prio, $line);
2196 $line = sprintf("[%s]: %s\n", $date, $line);
2198 // log_driver == 'file' is assumed here
2199 if (empty($CONFIG['log_dir']))
2200 $CONFIG['log_dir'] = INSTALL_PATH.'logs';
2202 // try to open specific log file for writing
2203 $logfile = $CONFIG['log_dir'].'/'.$name;
2204 if ($fp = @fopen($logfile, 'a')) {
2211 trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
2219 * Write login data (name, ID, IP address) to the 'userlogins' log file.
2223 function rcmail_log_login()
2227 if (!$RCMAIL->config->get('log_logins') || !$RCMAIL->user)
2230 write_log('userlogins', sprintf('Successful login for %s (ID: %d) from %s in session %s',
2231 $RCMAIL->user->get_username(), $RCMAIL->user->ID, rcmail_remote_ip(), session_id()));
2236 * Returns remote IP address and forwarded addresses if found
2238 * @return string Remote IP address(es)
2240 function rcmail_remote_ip()
2242 $address = $_SERVER['REMOTE_ADDR'];
2244 // append the NGINX X-Real-IP header, if set
2245 if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
2246 $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
2248 // append the X-Forwarded-For header, if set
2249 if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
2250 $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
2253 if (!empty($remote_ip))
2254 $address .= '(' . implode(',', $remote_ip) . ')';
2261 * Check whether the HTTP referer matches the current request
2263 * @return boolean True if referer is the same host+path, false if not
2265 function rcube_check_referer()
2267 $uri = parse_url($_SERVER['REQUEST_URI']);
2268 $referer = parse_url(rc_request_header('Referer'));
2269 return $referer['host'] == rc_request_header('Host') && $referer['path'] == $uri['path'];
2277 function rcube_timer()
2279 return microtime(true);
2287 function rcube_print_time($timer, $label='Timer', $dest='console')
2289 static $print_count = 0;
2292 $now = rcube_timer();
2293 $diff = $now-$timer;
2296 $label = 'Timer '.$print_count;
2298 write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
2303 * Throw system error and show error page
2305 * @param array Named parameters
2306 * - code: Error code (required)
2307 * - type: Error type [php|db|imap|javascript] (required)
2308 * - message: Error message
2309 * - file: File where error occured
2310 * - line: Line where error occured
2311 * @param boolean True to log the error
2312 * @param boolean Terminate script execution
2314 // may be defined in Installer
2315 if (!function_exists('raise_error')) {
2316 function raise_error($arg=array(), $log=false, $terminate=false)
2318 global $__page_content, $CONFIG, $OUTPUT, $ERROR_CODE, $ERROR_MESSAGE;
2320 // report bug (if not incompatible browser)
2321 if ($log && $arg['type'] && $arg['message'])
2322 rcube_log_bug($arg);
2324 // display error page and terminate script
2326 $ERROR_CODE = $arg['code'];
2327 $ERROR_MESSAGE = $arg['message'];
2328 include INSTALL_PATH . 'program/steps/utils/error.inc';
2336 * Report error according to configured debug_level
2338 * @param array Named parameters
2340 * @see raise_error()
2342 function rcube_log_bug($arg_arr)
2346 $program = strtoupper($arg_arr['type']);
2347 $level = $CONFIG['debug_level'];
2349 // disable errors for ajax requests, write to log instead (#1487831)
2350 if (($level & 4) && !empty($_REQUEST['_remote'])) {
2351 $level = ($level ^ 4) | 1;
2354 // write error to local log file
2356 $post_query = ($_SERVER['REQUEST_METHOD'] == 'POST' ? '?_task='.urlencode($_POST['_task']).'&_action='.urlencode($_POST['_action']) : '');
2357 $log_entry = sprintf("%s Error: %s%s (%s %s)",
2359 $arg_arr['message'],
2360 $arg_arr['file'] ? sprintf(' in %s on line %d', $arg_arr['file'], $arg_arr['line']) : '',
2361 $_SERVER['REQUEST_METHOD'],
2362 $_SERVER['REQUEST_URI'] . $post_query);
2364 if (!write_log('errors', $log_entry)) {
2365 // send error to PHPs error handler if write_log didn't succeed
2366 trigger_error($arg_arr['message']);
2370 // report the bug to the global bug reporting system
2372 // TODO: Send error via HTTP
2375 // show error if debug_mode is on
2377 print "<b>$program Error";
2379 if (!empty($arg_arr['file']) && !empty($arg_arr['line']))
2380 print " in $arg_arr[file] ($arg_arr[line])";
2382 print ':</b> ';
2383 print nl2br($arg_arr['message']);
2389 function rcube_upload_progress()
2393 $prefix = ini_get('apc.rfc1867_prefix');
2395 'action' => $RCMAIL->action,
2396 'name' => get_input_value('_progress', RCUBE_INPUT_GET),
2399 if (function_exists('apc_fetch')) {
2400 $status = apc_fetch($prefix . $params['name']);
2402 if (!empty($status)) {
2403 $status['percent'] = round($status['current']/$status['total']*100);
2404 $params = array_merge($status, $params);
2408 if (isset($params['percent']))
2409 $params['text'] = rcube_label(array('name' => 'uploadprogress', 'vars' => array(
2410 'percent' => $params['percent'] . '%',
2411 'current' => show_bytes($params['current']),
2412 'total' => show_bytes($params['total'])
2415 $RCMAIL->output->command('upload_progress_update', $params);
2416 $RCMAIL->output->send();
2419 function rcube_upload_init()
2423 // Enable upload progress bar
2424 if (($seconds = $RCMAIL->config->get('upload_progress')) && ini_get('apc.rfc1867')) {
2425 if ($field_name = ini_get('apc.rfc1867_name')) {
2426 $RCMAIL->output->set_env('upload_progress_name', $field_name);
2427 $RCMAIL->output->set_env('upload_progress_time', (int) $seconds);
2431 // find max filesize value
2432 $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
2433 $max_postsize = parse_bytes(ini_get('post_max_size'));
2434 if ($max_postsize && $max_postsize < $max_filesize)
2435 $max_filesize = $max_postsize;
2437 $RCMAIL->output->set_env('max_filesize', $max_filesize);
2438 $max_filesize = show_bytes($max_filesize);
2439 $RCMAIL->output->set_env('filesizeerror', rcube_label(array(
2440 'name' => 'filesizeerror', 'vars' => array('size' => $max_filesize))));
2442 return $max_filesize;
2446 * Initializes client-side autocompletion
2448 function rcube_autocomplete_init()
2458 if (($threads = (int)$RCMAIL->config->get('autocomplete_threads')) > 0) {
2459 $book_types = (array) $RCMAIL->config->get('autocomplete_addressbooks', 'sql');
2460 if (count($book_types) > 1) {
2461 $RCMAIL->output->set_env('autocomplete_threads', $threads);
2462 $RCMAIL->output->set_env('autocomplete_sources', $book_types);
2466 $RCMAIL->output->set_env('autocomplete_max', (int)$RCMAIL->config->get('autocomplete_max', 15));
2467 $RCMAIL->output->set_env('autocomplete_min_length', $RCMAIL->config->get('autocomplete_min_length'));
2468 $RCMAIL->output->add_label('autocompletechars', 'autocompletemore');