1<?php 2 3/** 4 +-----------------------------------------------------------------------+ 5 | This file is part of the Roundcube Webmail client | 6 | | 7 | Copyright (C) The Roundcube Dev Team | 8 | | 9 | Licensed under the GNU General Public License version 3 or | 10 | any later version with exceptions for skins & plugins. | 11 | See the README file for a full license statement. | 12 | | 13 | PURPOSE: | 14 | Provide webmail functionality and GUI objects | 15 +-----------------------------------------------------------------------+ 16 | Author: Thomas Bruederli <roundcube@gmail.com> | 17 | Author: Aleksander Machniak <alec@alec.pl> | 18 +-----------------------------------------------------------------------+ 19*/ 20 21class rcmail_action_mail_index extends rcmail_action 22{ 23 public static $aliases = [ 24 'refresh' => 'check-recent', 25 'preview' => 'show', 26 'print' => 'show', 27 'expunge' => 'folder-expunge', 28 'purge' => 'folder-purge', 29 'remove-attachment' => 'attachment-delete', 30 'rename-attachment' => 'attachment-rename', 31 'display-attachment' => 'attachment-display', 32 'upload' => 'attachment-upload', 33 ]; 34 35 protected static $PRINT_MODE = false; 36 protected static $REMOTE_OBJECTS; 37 protected static $SUSPICIOUS_EMAIL = false; 38 39 /** 40 * Request handler. 41 * 42 * @param array $args Arguments from the previous step(s) 43 */ 44 public function run($args = []) 45 { 46 $rcmail = rcmail::get_instance(); 47 48 // always instantiate storage object (but not connect to server yet) 49 $rcmail->storage_init(); 50 51 // init environment - set current folder, page, list mode 52 self::init_env(); 53 54 // set message set for search result 55 if ( 56 !empty($_REQUEST['_search']) 57 && isset($_SESSION['search']) 58 && isset($_SESSION['search_request']) 59 && $_SESSION['search_request'] == $_REQUEST['_search'] 60 ) { 61 $rcmail->storage->set_search_set($_SESSION['search']); 62 63 $rcmail->output->set_env('search_request', $_REQUEST['_search']); 64 $rcmail->output->set_env('search_text', $_SESSION['last_text_search']); 65 } 66 67 // remove mbox part from _uid 68 $uid = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_GPC); 69 if ($uid && !is_array($uid) && preg_match('/^\d+-.+/', $uid)) { 70 list($uid, $mbox) = explode('-', $uid, 2); 71 if (isset($_GET['_uid'])) { 72 $_GET['_uid'] = $uid; 73 } 74 if (isset($_POST['_uid'])) { 75 $_POST['_uid'] = $uid; 76 } 77 $_REQUEST['_uid'] = $uid; 78 79 // override mbox 80 if (!empty($mbox)) { 81 $_GET['_mbox'] = $mbox; 82 $_POST['_mbox'] = $mbox; 83 $rcmail->storage->set_folder($_SESSION['mbox'] = $mbox); 84 } 85 } 86 87 if (!empty($_SESSION['browser_caps']) && !$rcmail->output->ajax_call) { 88 $rcmail->output->set_env('browser_capabilities', $_SESSION['browser_caps']); 89 } 90 91 // set main env variables, labels and page title 92 if (empty($rcmail->action) || $rcmail->action == 'list') { 93 // connect to storage server and trigger error on failure 94 $rcmail->storage_connect(); 95 96 $mbox_name = $rcmail->storage->get_folder(); 97 98 if (empty($rcmail->action)) { 99 $rcmail->output->set_env('search_mods', self::search_mods()); 100 101 $scope = rcube_utils::get_input_string('_scope', rcube_utils::INPUT_GET); 102 if (!$scope && isset($_SESSION['search_scope']) && $rcmail->output->get_env('search_request')) { 103 $scope = $_SESSION['search_scope']; 104 } 105 106 if ($scope && preg_match('/^(all|sub)$/i', $scope)) { 107 $rcmail->output->set_env('search_scope', strtolower($scope)); 108 } 109 110 self::list_pagetitle(); 111 } 112 113 $threading = (bool) $rcmail->storage->get_threading(); 114 $delimiter = $rcmail->storage->get_hierarchy_delimiter(); 115 116 // set current mailbox and some other vars in client environment 117 $rcmail->output->set_env('mailbox', $mbox_name); 118 $rcmail->output->set_env('pagesize', $rcmail->storage->get_pagesize()); 119 $rcmail->output->set_env('current_page', isset($_SESSION['page']) ? max(1, (int) $_SESSION['page']) : 1); 120 $rcmail->output->set_env('delimiter', $delimiter); 121 $rcmail->output->set_env('threading', $threading); 122 $rcmail->output->set_env('threads', $threading || $rcmail->storage->get_capability('THREAD')); 123 $rcmail->output->set_env('reply_all_mode', (int) $rcmail->config->get('reply_all_mode')); 124 $rcmail->output->set_env('layout', $rcmail->config->get('layout') ?: 'widescreen'); 125 $rcmail->output->set_env('quota', $rcmail->storage->get_capability('QUOTA')); 126 127 // set special folders 128 foreach (['drafts', 'trash', 'junk'] as $mbox) { 129 if ($folder = $rcmail->config->get($mbox . '_mbox')) { 130 $rcmail->output->set_env($mbox . '_mailbox', $folder); 131 } 132 } 133 134 if (!empty($_GET['_uid'])) { 135 $rcmail->output->set_env('list_uid', $_GET['_uid']); 136 } 137 138 // set configuration 139 self::set_env_config(['delete_junk', 'flag_for_deletion', 'read_when_deleted', 140 'skip_deleted', 'display_next', 'message_extwin', 'forward_attachment']); 141 142 if (!$rcmail->output->ajax_call) { 143 $rcmail->output->add_label('checkingmail', 'deletemessage', 'movemessagetotrash', 144 'movingmessage', 'copyingmessage', 'deletingmessage', 'markingmessage', 145 'copy', 'move', 'quota', 'replyall', 'replylist', 'stillsearching', 146 'flagged', 'unflagged', 'unread', 'deleted', 'replied', 'forwarded', 147 'priority', 'withattachment', 'fileuploaderror', 'mark', 'markallread', 148 'folders-cur', 'folders-sub', 'folders-all', 'cancel', 'bounce', 'bouncemsg', 149 'sendingmessage'); 150 } 151 } 152 153 // register UI objects 154 $rcmail->output->add_handlers([ 155 'mailboxlist' => [$rcmail, 'folder_list'], 156 'quotadisplay' => [$this, 'quota_display'], 157 'messages' => [$this, 'message_list'], 158 'messagecountdisplay' => [$this, 'messagecount_display'], 159 'listmenulink' => [$this, 'options_menu_link'], 160 'mailboxname' => [$this, 'mailbox_name_display'], 161 'messageimportform' => [$this, 'message_import_form'], 162 'searchfilter' => [$this, 'search_filter'], 163 'searchinterval' => [$this, 'search_interval'], 164 'searchform' => [$rcmail->output, 'search_form'], 165 ]); 166 } 167 168 /** 169 * Sets storage properties and session 170 */ 171 public static function init_env() 172 { 173 $rcmail = rcmail::get_instance(); 174 175 $default_threading = $rcmail->config->get('default_list_mode', 'list') == 'threads'; 176 $a_threading = $rcmail->config->get('message_threading', []); 177 $message_sort_col = $rcmail->config->get('message_sort_col'); 178 $message_sort_order = $rcmail->config->get('message_sort_order'); 179 180 // set imap properties and session vars 181 if (!strlen($mbox = rcube_utils::get_input_value('_mbox', rcube_utils::INPUT_GPC, true))) { 182 $mbox = isset($_SESSION['mbox']) && strlen($_SESSION['mbox']) ? $_SESSION['mbox'] : 'INBOX'; 183 } 184 185 // We handle 'page' argument on 'list' and 'getunread' to prevent from 186 // race condition and unintentional page overwrite in session. 187 // Also, when entering the Mail UI (#7932) 188 if (empty($rcmail->action) || $rcmail->action == 'list' || $rcmail->action == 'getunread') { 189 $page = isset($_GET['_page']) ? intval($_GET['_page']) : 0; 190 if (!$page) { 191 $page = !empty($_SESSION['page']) ? $_SESSION['page'] : 1; 192 } 193 194 $_SESSION['page'] = $page; 195 } 196 197 $rcmail->storage->set_folder($_SESSION['mbox'] = $mbox); 198 $rcmail->storage->set_page(isset($_SESSION['page']) ? $_SESSION['page'] : 1); 199 200 // set default sort col/order to session 201 if (!isset($_SESSION['sort_col'])) { 202 $_SESSION['sort_col'] = $message_sort_col ?: ''; 203 } 204 if (!isset($_SESSION['sort_order'])) { 205 $_SESSION['sort_order'] = strtoupper($message_sort_order) == 'ASC' ? 'ASC' : 'DESC'; 206 } 207 208 // set threads mode 209 if (isset($_GET['_threads'])) { 210 if ($_GET['_threads']) { 211 // re-set current page number when listing mode changes 212 if (!$a_threading[$_SESSION['mbox']]) { 213 $rcmail->storage->set_page($_SESSION['page'] = 1); 214 } 215 216 $a_threading[$_SESSION['mbox']] = true; 217 } 218 else { 219 // re-set current page number when listing mode changes 220 if ($a_threading[$_SESSION['mbox']]) { 221 $rcmail->storage->set_page($_SESSION['page'] = 1); 222 } 223 224 $a_threading[$_SESSION['mbox']] = false; 225 } 226 227 $rcmail->user->save_prefs(['message_threading' => $a_threading]); 228 } 229 230 $threading = isset($a_threading[$_SESSION['mbox']]) ? $a_threading[$_SESSION['mbox']] : $default_threading; 231 232 $rcmail->storage->set_threading($threading); 233 } 234 235 /** 236 * Sets page title 237 */ 238 public static function list_pagetitle() 239 { 240 $rcmail = rcmail::get_instance(); 241 242 if ($rcmail->output->get_env('search_request')) { 243 $pagetitle = $rcmail->gettext('searchresult'); 244 } 245 else { 246 $mbox_name = $rcmail->output->get_env('mailbox') ?: $rcmail->storage->get_folder(); 247 $delimiter = $rcmail->storage->get_hierarchy_delimiter(); 248 $pagetitle = self::localize_foldername($mbox_name, true); 249 $pagetitle = str_replace($delimiter, " \xC2\xBB ", $pagetitle); 250 } 251 252 $rcmail->output->set_pagetitle($pagetitle); 253 } 254 255 /** 256 * Returns default search mods 257 */ 258 public static function search_mods() 259 { 260 $rcmail = rcmail::get_instance(); 261 $mods = $rcmail->config->get('search_mods'); 262 263 if (empty($mods)) { 264 $mods = ['*' => ['subject' => 1, 'from' => 1]]; 265 266 foreach (['sent', 'drafts'] as $mbox) { 267 if ($mbox = $rcmail->config->get($mbox . '_mbox')) { 268 $mods[$mbox] = ['subject' => 1, 'to' => 1]; 269 } 270 } 271 } 272 273 return $mods; 274 } 275 276 /** 277 * Returns 'to' if current folder is configured Sent or Drafts 278 * or their subfolders, otherwise returns 'from'. 279 * 280 * @return string Column name 281 */ 282 public static function message_list_smart_column_name() 283 { 284 $rcmail = rcmail::get_instance(); 285 $delim = $rcmail->storage->get_hierarchy_delimiter(); 286 $sent_mbox = $rcmail->config->get('sent_mbox'); 287 $drafts_mbox = $rcmail->config->get('drafts_mbox'); 288 $mbox = $rcmail->output->get_env('mailbox'); 289 290 if (!is_string($mbox) || !strlen($mbox)) { 291 $mbox = $rcmail->storage->get_folder(); 292 } 293 294 if ((strpos($mbox.$delim, $sent_mbox.$delim) === 0 || strpos($mbox.$delim, $drafts_mbox.$delim) === 0) 295 && strtoupper($mbox) != 'INBOX' 296 ) { 297 return 'to'; 298 } 299 300 return 'from'; 301 } 302 303 /** 304 * Returns configured messages list sorting column name 305 * The name is context-sensitive, which means if sorting is set to 'fromto' 306 * it will return 'from' or 'to' according to current folder type. 307 * 308 * @return string Column name 309 */ 310 public static function sort_column() 311 { 312 $rcmail = rcmail::get_instance(); 313 314 if (isset($_SESSION['sort_col'])) { 315 $column = $_SESSION['sort_col']; 316 } 317 else { 318 $column = $rcmail->config->get('message_sort_col'); 319 } 320 321 // get name of smart From/To column in folder context 322 if ($column == 'fromto') { 323 $column = self::message_list_smart_column_name(); 324 } 325 326 return $column; 327 } 328 329 /** 330 * Returns configured message list sorting order 331 * 332 * @return string Sorting order (ASC|DESC) 333 */ 334 public static function sort_order() 335 { 336 if (isset($_SESSION['sort_order'])) { 337 return $_SESSION['sort_order']; 338 } 339 340 return rcmail::get_instance()->config->get('message_sort_order'); 341 } 342 343 /** 344 * return the message list as HTML table 345 */ 346 function message_list($attrib) 347 { 348 $rcmail = rcmail::get_instance(); 349 350 // add some labels to client 351 $rcmail->output->add_label('from', 'to'); 352 353 // add id to message list table if not specified 354 if (empty($attrib['id'])) { 355 $attrib['id'] = 'rcubemessagelist'; 356 } 357 358 // define list of cols to be displayed based on parameter or config 359 if (empty($attrib['columns'])) { 360 $list_cols = $rcmail->config->get('list_cols'); 361 $a_show_cols = !empty($list_cols) && is_array($list_cols) ? $list_cols : ['subject']; 362 363 $rcmail->output->set_env('col_movable', !in_array('list_cols', (array) $rcmail->config->get('dont_override'))); 364 } 365 else { 366 $a_show_cols = preg_split('/[\s,;]+/', str_replace(["'", '"'], '', $attrib['columns'])); 367 $attrib['columns'] = $a_show_cols; 368 } 369 370 // save some variables for use in ajax list 371 $_SESSION['list_attrib'] = $attrib; 372 373 // make sure 'threads' and 'subject' columns are present 374 if (!in_array('subject', $a_show_cols)) { 375 array_unshift($a_show_cols, 'subject'); 376 } 377 if (!in_array('threads', $a_show_cols)) { 378 array_unshift($a_show_cols, 'threads'); 379 } 380 381 $listcols = $a_show_cols; 382 383 // set client env 384 $rcmail->output->add_gui_object('messagelist', $attrib['id']); 385 $rcmail->output->set_env('autoexpand_threads', intval($rcmail->config->get('autoexpand_threads'))); 386 $rcmail->output->set_env('sort_col', $_SESSION['sort_col']); 387 $rcmail->output->set_env('sort_order', $_SESSION['sort_order']); 388 $rcmail->output->set_env('messages', []); 389 $rcmail->output->set_env('listcols', $listcols); 390 $rcmail->output->set_env('listcols_widescreen', ['threads', 'subject', 'fromto', 'date', 'size', 'flag', 'attachment']); 391 392 $rcmail->output->include_script('list.js'); 393 394 $table = new html_table($attrib); 395 396 if (empty($attrib['noheader'])) { 397 $allcols = array_merge($listcols, ['threads', 'subject', 'fromto', 'date', 'size', 'flag', 'attachment']); 398 $allcols = array_unique($allcols); 399 400 foreach (self::message_list_head($attrib, $allcols) as $col => $cell) { 401 if (in_array($col, $listcols)) { 402 $table->add_header(['class' => $cell['className'], 'id' => $cell['id']], $cell['html']); 403 } 404 } 405 } 406 407 return $table->show(); 408 } 409 410 /** 411 * return javascript commands to add rows to the message list 412 */ 413 public static function js_message_list($a_headers, $insert_top = false, $a_show_cols = null) 414 { 415 $rcmail = rcmail::get_instance(); 416 417 if (empty($a_show_cols)) { 418 if (!empty($_SESSION['list_attrib']['columns'])) { 419 $a_show_cols = $_SESSION['list_attrib']['columns']; 420 } 421 else { 422 $list_cols = $rcmail->config->get('list_cols'); 423 $a_show_cols = !empty($list_cols) && is_array($list_cols) ? $list_cols : ['subject']; 424 } 425 } 426 else { 427 if (!is_array($a_show_cols)) { 428 $a_show_cols = preg_split('/[\s,;]+/', str_replace(["'", '"'], '', $a_show_cols)); 429 } 430 $head_replace = true; 431 } 432 433 $delimiter = $rcmail->storage->get_hierarchy_delimiter(); 434 $search_set = $rcmail->storage->get_search_set(); 435 $multifolder = $search_set && !empty($search_set[1]->multi); 436 437 // add/remove 'folder' column to the list on multi-folder searches 438 if ($multifolder && !in_array('folder', $a_show_cols)) { 439 $a_show_cols[] = 'folder'; 440 $head_replace = true; 441 } 442 else if (!$multifolder && ($found = array_search('folder', $a_show_cols)) !== false) { 443 unset($a_show_cols[$found]); 444 $head_replace = true; 445 } 446 447 $mbox = $rcmail->output->get_env('mailbox'); 448 if (!is_string($mbox) || !strlen($mbox)) { 449 $mbox = $rcmail->storage->get_folder(); 450 } 451 452 // make sure 'threads' and 'subject' columns are present 453 if (!in_array('subject', $a_show_cols)) { 454 array_unshift($a_show_cols, 'subject'); 455 } 456 if (!in_array('threads', $a_show_cols)) { 457 array_unshift($a_show_cols, 'threads'); 458 } 459 460 // Make sure there are no duplicated columns (#1486999) 461 $a_show_cols = array_unique($a_show_cols); 462 $_SESSION['list_attrib']['columns'] = $a_show_cols; 463 464 // Plugins may set header's list_cols/list_flags and other rcube_message_header variables 465 // and list columns 466 $plugin = $rcmail->plugins->exec_hook('messages_list', ['messages' => $a_headers, 'cols' => $a_show_cols]); 467 468 $a_show_cols = $plugin['cols']; 469 $a_headers = $plugin['messages']; 470 471 // make sure minimum required columns are present (needed for widescreen layout) 472 $allcols = array_merge($a_show_cols, ['threads', 'subject', 'fromto', 'date', 'size', 'flag', 'attachment']); 473 $allcols = array_unique($allcols); 474 475 $thead = !empty($head_replace) ? self::message_list_head($_SESSION['list_attrib'], $allcols) : null; 476 477 // get name of smart From/To column in folder context 478 $smart_col = self::message_list_smart_column_name(); 479 $rcmail->output->command('set_message_coltypes', array_values($a_show_cols), $thead, $smart_col); 480 481 if ($multifolder && $_SESSION['search_scope'] == 'all') { 482 $rcmail->output->command('select_folder', ''); 483 } 484 485 $rcmail->output->set_env('multifolder_listing', $multifolder); 486 487 if (empty($a_headers)) { 488 return; 489 } 490 491 // remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here 492 foreach (['threads', 'attachment', 'flag', 'status', 'priority'] as $col) { 493 if (($key = array_search($col, $allcols)) !== false) { 494 unset($allcols[$key]); 495 } 496 } 497 498 $sort_col = $_SESSION['sort_col']; 499 500 // loop through message headers 501 foreach ($a_headers as $header) { 502 if (empty($header) || empty($header->size)) { 503 continue; 504 } 505 506 // make message UIDs unique by appending the folder name 507 if ($multifolder) { 508 $header->uid .= '-' . $header->folder; 509 $header->flags['skip_mbox_check'] = true; 510 if (!empty($header->parent_uid)) { 511 $header->parent_uid .= '-' . $header->folder; 512 } 513 } 514 515 $a_msg_cols = []; 516 $a_msg_flags = []; 517 518 // format each col; similar as in self::message_list() 519 foreach ($allcols as $col) { 520 $col_name = $col == 'fromto' ? $smart_col : $col; 521 522 if (in_array($col_name, ['from', 'to', 'cc', 'replyto'])) { 523 $cont = self::address_string($header->$col_name, 3, false, null, $header->charset); 524 if (empty($cont)) { 525 $cont = ' '; // for widescreen mode 526 } 527 } 528 else if ($col == 'subject') { 529 $cont = trim(rcube_mime::decode_header($header->$col, $header->charset)); 530 if (!$cont) { 531 $cont = $rcmail->gettext('nosubject'); 532 } 533 $cont = rcube::SQ($cont); 534 } 535 else if ($col == 'size') { 536 $cont = self::show_bytes($header->$col); 537 } 538 else if ($col == 'date') { 539 $cont = $rcmail->format_date($sort_col == 'arrival' ? $header->internaldate : $header->date); 540 } 541 else if ($col == 'folder') { 542 if (!isset($last_folder) || !isset($last_folder_name) || $last_folder !== $header->folder) { 543 $last_folder = $header->folder; 544 $last_folder_name = self::localize_foldername($last_folder, true); 545 $last_folder_name = str_replace($delimiter, " \xC2\xBB ", $last_folder_name); 546 } 547 548 $cont = rcube::SQ($last_folder_name); 549 } 550 else { 551 $cont = rcube::SQ($header->$col); 552 } 553 554 $a_msg_cols[$col] = $cont; 555 } 556 557 $a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags)); 558 559 if (!empty($header->depth)) { 560 $a_msg_flags['depth'] = $header->depth; 561 } 562 else if (!empty($header->has_children)) { 563 $roots[] = $header->uid; 564 } 565 if (!empty($header->parent_uid)) { 566 $a_msg_flags['parent_uid'] = $header->parent_uid; 567 } 568 if (!empty($header->has_children)) { 569 $a_msg_flags['has_children'] = $header->has_children; 570 } 571 if (!empty($header->unread_children)) { 572 $a_msg_flags['unread_children'] = $header->unread_children; 573 } 574 if (!empty($header->flagged_children)) { 575 $a_msg_flags['flagged_children'] = $header->flagged_children; 576 } 577 if (!empty($header->others['list-post'])) { 578 $a_msg_flags['ml'] = 1; 579 } 580 if (!empty($header->priority)) { 581 $a_msg_flags['prio'] = (int) $header->priority; 582 } 583 584 $a_msg_flags['ctype'] = rcube::Q($header->ctype); 585 $a_msg_flags['mbox'] = $header->folder; 586 587 // merge with plugin result (Deprecated, use $header->flags) 588 if (!empty($header->list_flags) && is_array($header->list_flags)) { 589 $a_msg_flags = array_merge($a_msg_flags, $header->list_flags); 590 } 591 if (!empty($header->list_cols) && is_array($header->list_cols)) { 592 $a_msg_cols = array_merge($a_msg_cols, $header->list_cols); 593 } 594 595 $rcmail->output->command('add_message_row', $header->uid, $a_msg_cols, $a_msg_flags, $insert_top); 596 } 597 598 if ($rcmail->storage->get_threading()) { 599 $roots = isset($roots) ? (array) $roots : []; 600 $rcmail->output->command('init_threads', $roots, $mbox); 601 } 602 } 603 604 /* 605 * Creates <THEAD> for message list table 606 */ 607 public static function message_list_head($attrib, $a_show_cols) 608 { 609 $rcmail = rcmail::get_instance(); 610 611 // check to see if we have some settings for sorting 612 $sort_col = $_SESSION['sort_col']; 613 $sort_order = $_SESSION['sort_order']; 614 615 $dont_override = (array) $rcmail->config->get('dont_override'); 616 $disabled_sort = in_array('message_sort_col', $dont_override); 617 $disabled_order = in_array('message_sort_order', $dont_override); 618 619 $rcmail->output->set_env('disabled_sort_col', $disabled_sort); 620 $rcmail->output->set_env('disabled_sort_order', $disabled_order); 621 622 // define sortable columns 623 if ($disabled_sort) { 624 $a_sort_cols = $sort_col && !$disabled_order ? [$sort_col] : []; 625 } 626 else { 627 $a_sort_cols = ['subject', 'date', 'from', 'to', 'fromto', 'size', 'cc']; 628 } 629 630 if (!empty($attrib['optionsmenuicon'])) { 631 $params = []; 632 foreach ($attrib as $key => $val) { 633 if (preg_match('/^optionsmenu(.+)$/', $key, $matches)) { 634 $params[$matches[1]] = $val; 635 } 636 } 637 638 $list_menu = self::options_menu_link($params); 639 } 640 641 $cells = $coltypes = []; 642 643 // get name of smart From/To column in folder context 644 $smart_col = null; 645 if (array_search('fromto', $a_show_cols) !== false) { 646 $smart_col = self::message_list_smart_column_name(); 647 } 648 649 foreach ($a_show_cols as $col) { 650 $label = ''; 651 $sortable = false; 652 $rel_col = $col == 'date' && $sort_col == 'arrival' ? 'arrival' : $col; 653 654 // get column name 655 switch ($col) { 656 case 'flag': 657 $col_name = html::span('flagged', $rcmail->gettext('flagged')); 658 break; 659 case 'attachment': 660 case 'priority': 661 $col_name = html::span($col, $rcmail->gettext($col)); 662 break; 663 case 'status': 664 $col_name = html::span($col, $rcmail->gettext('readstatus')); 665 break; 666 case 'threads': 667 $col_name = !empty($list_menu) ? $list_menu : ''; 668 break; 669 case 'fromto': 670 $label = $rcmail->gettext($smart_col); 671 $col_name = rcube::Q($label); 672 break; 673 default: 674 $label = $rcmail->gettext($col); 675 $col_name = rcube::Q($label); 676 } 677 678 // make sort links 679 if (in_array($col, $a_sort_cols)) { 680 $sortable = true; 681 $col_name = html::a([ 682 'href' => "./#sort", 683 'class' => 'sortcol', 684 'rel' => $rel_col, 685 'title' => $rcmail->gettext('sortby') 686 ], $col_name); 687 } 688 else if (empty($col_name) || $col_name[0] != '<') { 689 $col_name = '<span class="' . $col .'">' . $col_name . '</span>'; 690 } 691 692 $sort_class = $rel_col == $sort_col && !$disabled_order ? " sorted$sort_order" : ''; 693 $class_name = $col.$sort_class; 694 695 // put it all together 696 $cells[$col] = ['className' => $class_name, 'id' => "rcm$col", 'html' => $col_name]; 697 $coltypes[$col] = ['className' => $class_name, 'id' => "rcm$col", 'label' => $label, 'sortable' => $sortable]; 698 } 699 700 $rcmail->output->set_env('coltypes', $coltypes); 701 702 return $cells; 703 } 704 705 public static function options_menu_link($attrib = []) 706 { 707 $rcmail = rcmail::get_instance(); 708 $title = $rcmail->gettext(!empty($attrib['label']) ? $attrib['label'] : 'listoptions'); 709 $inner = $title; 710 $onclick = sprintf( 711 "return %s.command('menu-open', '%s', this, event)", 712 rcmail_output::JS_OBJECT_NAME, 713 !empty($attrib['ref']) ? $attrib['ref'] : 'messagelistmenu' 714 ); 715 716 // Backwards compatibility, attribute renamed in v1.5 717 if (isset($attrib['optionsmenuicon'])) { 718 $attrib['icon'] = $attrib['optionsmenuicon']; 719 } 720 721 if (!empty($attrib['icon']) && $attrib['icon'] != 'true') { 722 $inner = html::img(['src' => $rcmail->output->asset_url($attrib['icon'], true), 'alt' => $title]); 723 } 724 else if (!empty($attrib['innerclass'])) { 725 $inner = html::span($attrib['innerclass'], $inner); 726 } 727 728 return html::a([ 729 'href' => '#list-options', 730 'onclick' => $onclick, 731 'class' => isset($attrib['class']) ? $attrib['class'] : 'listmenu', 732 'id' => isset($attrib['id']) ? $attrib['id'] : 'listmenulink', 733 'title' => $title, 734 'tabindex' => '0', 735 ], $inner 736 ); 737 } 738 739 public static function messagecount_display($attrib) 740 { 741 $rcmail = rcmail::get_instance(); 742 743 if (empty($attrib['id'])) { 744 $attrib['id'] = 'rcmcountdisplay'; 745 } 746 747 $rcmail->output->add_gui_object('countdisplay', $attrib['id']); 748 749 $content = $rcmail->action != 'show' ? self::get_messagecount_text() : $rcmail->gettext('loading'); 750 751 return html::span($attrib, $content); 752 } 753 754 public static function get_messagecount_text($count = null, $page = null) 755 { 756 $rcmail = rcmail::get_instance(); 757 758 if ($page === null) { 759 $page = $rcmail->storage->get_page(); 760 } 761 762 $page_size = $rcmail->storage->get_pagesize(); 763 $start_msg = ($page-1) * $page_size + 1; 764 $max = $count; 765 766 if ($max === null && $rcmail->action) { 767 $max = $rcmail->storage->count(null, $rcmail->storage->get_threading() ? 'THREADS' : 'ALL'); 768 } 769 770 if (!$max) { 771 $out = $rcmail->storage->get_search_set() ? $rcmail->gettext('nomessages') : $rcmail->gettext('mailboxempty'); 772 } 773 else { 774 $out = $rcmail->gettext([ 775 'name' => $rcmail->storage->get_threading() ? 'threadsfromto' : 'messagesfromto', 776 'vars' => [ 777 'from' => $start_msg, 778 'to' => min($max, $start_msg + $page_size - 1), 779 'count' => $max 780 ] 781 ]); 782 } 783 784 return rcube::Q($out); 785 } 786 787 public static function mailbox_name_display($attrib) 788 { 789 $rcmail = rcmail::get_instance(); 790 791 if (empty($attrib['id'])) { 792 $attrib['id'] = 'rcmmailboxname'; 793 } 794 795 $rcmail->output->add_gui_object('mailboxname', $attrib['id']); 796 797 return html::span($attrib, self::get_mailbox_name_text()); 798 } 799 800 public static function get_mailbox_name_text() 801 { 802 $rcmail = rcmail::get_instance(); 803 $mbox = $rcmail->output->get_env('mailbox'); 804 805 if (!is_string($mbox) || !strlen($mbox)) { 806 $mbox = $rcmail->storage->get_folder(); 807 } 808 809 return self::localize_foldername($mbox); 810 } 811 812 public static function send_unread_count($mbox_name, $force = false, $count = null, $mark = '') 813 { 814 $rcmail = rcmail::get_instance(); 815 $old_unseen = self::get_unseen_count($mbox_name); 816 $unseen = $count; 817 818 if ($unseen === null) { 819 $unseen = $rcmail->storage->count($mbox_name, 'UNSEEN', $force); 820 } 821 822 if ($unseen !== $old_unseen || ($mbox_name == 'INBOX')) { 823 $rcmail->output->command('set_unread_count', $mbox_name, $unseen, 824 ($mbox_name == 'INBOX'), $unseen && $mark ? $mark : ''); 825 } 826 827 self::set_unseen_count($mbox_name, $unseen); 828 829 return $unseen; 830 } 831 832 public static function set_unseen_count($mbox_name, $count) 833 { 834 // @TODO: this data is doubled (session and cache tables) if caching is enabled 835 836 // Make sure we have an array here (#1487066) 837 if (!isset($_SESSION['unseen_count']) || !is_array($_SESSION['unseen_count'])) { 838 $_SESSION['unseen_count'] = []; 839 } 840 841 $_SESSION['unseen_count'][$mbox_name] = $count; 842 } 843 844 public static function get_unseen_count($mbox_name) 845 { 846 if (!empty($_SESSION['unseen_count']) && array_key_exists($mbox_name, $_SESSION['unseen_count'])) { 847 return $_SESSION['unseen_count'][$mbox_name]; 848 } 849 } 850 851 /** 852 * Sets message is_safe flag according to 'show_images' option value 853 * 854 * @param rcube_message $message Mail message object 855 */ 856 public static function check_safe($message) 857 { 858 $rcmail = rcmail::get_instance(); 859 860 if (empty($message->is_safe) 861 && ($show_images = $rcmail->config->get('show_images')) 862 && $message->has_html_part() 863 ) { 864 switch ($show_images) { 865 case 3: // trusted senders only 866 case 1: // all my contacts 867 if (!empty($message->sender['mailto'])) { 868 $type = rcube_addressbook::TYPE_TRUSTED_SENDER; 869 870 if ($show_images == 1) { 871 $type |= rcube_addressbook::TYPE_RECIPIENT | rcube_addressbook::TYPE_WRITEABLE; 872 } 873 874 if ($rcmail->contact_exists($message->sender['mailto'], $type)) { 875 $message->set_safe(true); 876 } 877 } 878 879 $rcmail->plugins->exec_hook('message_check_safe', ['message' => $message]); 880 break; 881 882 case 2: // always 883 $message->set_safe(true); 884 break; 885 } 886 } 887 888 return !empty($message->is_safe); 889 } 890 891 /** 892 * Cleans up the given message HTML Body (for displaying) 893 * 894 * @param string $html HTML 895 * @param array $p Display parameters 896 * @param array $cid_replaces CID map replaces (inline images) 897 * 898 * @return string Clean HTML 899 */ 900 public static function wash_html($html, $p, $cid_replaces = []) 901 { 902 $rcmail = rcmail::get_instance(); 903 904 $p += ['safe' => false, 'inline_html' => true, 'css_prefix' => null, 'container_id' => null]; 905 906 // charset was converted to UTF-8 in rcube_storage::get_message_part(), 907 // change/add charset specification in HTML accordingly, 908 // washtml's DOMDocument methods cannot work without that 909 $meta = '<meta charset="' . RCUBE_CHARSET . '" />'; 910 911 // remove old meta tag and add the new one, making sure that it is placed in the head (#3510, #7116) 912 $html = preg_replace('/<meta[^>]+charset=[a-z0-9_"-]+[^>]*>/Ui', '', $html); 913 $html = preg_replace('/(<head[^>]*>)/Ui', '\\1'.$meta, $html, -1, $rcount); 914 915 if (!$rcount) { 916 // Note: HTML without <html> tag may still be a valid input (#6713) 917 if (($pos = stripos($html, '<html')) === false) { 918 $html = '<html><head>' . $meta . '</head>' . $html; 919 } 920 else { 921 $pos = strpos($html, '>', $pos); 922 $html = substr_replace($html, '<head>' . $meta . '</head>', $pos + 1, 0); 923 } 924 } 925 926 // clean HTML with washtml by Frederic Motte 927 $wash_opts = [ 928 'show_washed' => false, 929 'allow_remote' => $p['safe'], 930 'blocked_src' => $rcmail->output->asset_url('program/resources/blocked.gif'), 931 'charset' => RCUBE_CHARSET, 932 'cid_map' => $cid_replaces, 933 'html_elements' => ['body'], 934 'css_prefix' => $p['css_prefix'], 935 'container_id' => $p['container_id'], 936 ]; 937 938 if (empty($p['inline_html'])) { 939 $wash_opts['html_elements'] = ['html','head','title','body','link']; 940 } 941 if (!empty($p['safe'])) { 942 $wash_opts['html_attribs'] = ['rel','type']; 943 } 944 945 // overwrite washer options with options from plugins 946 if (isset($p['html_elements'])) { 947 $wash_opts['html_elements'] = $p['html_elements']; 948 } 949 if (isset($p['html_attribs'])) { 950 $wash_opts['html_attribs'] = $p['html_attribs']; 951 } 952 953 // initialize HTML washer 954 $washer = new rcube_washtml($wash_opts); 955 956 if (empty($p['skip_washer_form_callback'])) { 957 $washer->add_callback('form', 'rcmail_action_mail_index::washtml_callback'); 958 } 959 960 // allow CSS styles, will be sanitized by rcmail_washtml_callback() 961 if (empty($p['skip_washer_style_callback'])) { 962 $washer->add_callback('style', 'rcmail_action_mail_index::washtml_callback'); 963 } 964 965 // modify HTML links to open a new window if clicked 966 if (empty($p['skip_washer_link_callback'])) { 967 $washer->add_callback('a', 'rcmail_action_mail_index::washtml_link_callback'); 968 $washer->add_callback('area', 'rcmail_action_mail_index::washtml_link_callback'); 969 $washer->add_callback('link', 'rcmail_action_mail_index::washtml_link_callback'); 970 } 971 972 // Remove non-UTF8 characters (#1487813) 973 $html = rcube_charset::clean($html); 974 975 $html = $washer->wash($html); 976 self::$REMOTE_OBJECTS = $washer->extlinks; 977 978 return $html; 979 } 980 981 /** 982 * Convert the given message part to proper HTML 983 * which can be displayed the message view 984 * 985 * @param string $body Message part body 986 * @param rcube_message_part $part Message part 987 * @param array $p Display parameters array 988 * 989 * @return string Formatted HTML string 990 */ 991 public static function print_body($body, $part, $p = []) 992 { 993 $rcmail = rcmail::get_instance(); 994 995 // trigger plugin hook 996 $data = $rcmail->plugins->exec_hook('message_part_before', 997 [ 998 'type' => $part->ctype_secondary, 999 'body' => $body, 1000 'id' => $part->mime_id 1001 ] + $p + [ 1002 'safe' => false, 1003 'plain' => false, 1004 'inline_html' => true 1005 ] 1006 ); 1007 1008 // convert html to text/plain 1009 if ($data['plain'] && ($data['type'] == 'html' || $data['type'] == 'enriched')) { 1010 if ($data['type'] == 'enriched') { 1011 $data['body'] = rcube_enriched::to_html($data['body']); 1012 } 1013 1014 $body = $rcmail->html2text($data['body']); 1015 $part->ctype_secondary = 'plain'; 1016 } 1017 // text/html 1018 else if ($data['type'] == 'html') { 1019 $body = self::wash_html($data['body'], $data, $part->replaces); 1020 $part->ctype_secondary = $data['type']; 1021 } 1022 // text/enriched 1023 else if ($data['type'] == 'enriched') { 1024 $body = rcube_enriched::to_html($data['body']); 1025 $body = self::wash_html($body, $data, $part->replaces); 1026 $part->ctype_secondary = 'html'; 1027 } 1028 else { 1029 // assert plaintext 1030 $body = $data['body']; 1031 $part->ctype_secondary = $data['type'] = 'plain'; 1032 } 1033 1034 // free some memory (hopefully) 1035 unset($data['body']); 1036 1037 // plaintext postprocessing 1038 if ($part->ctype_secondary == 'plain') { 1039 $flowed = isset($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed'; 1040 $delsp = isset($part->ctype_parameters['delsp']) && $part->ctype_parameters['delsp'] == 'yes'; 1041 $body = self::plain_body($body, $flowed, $delsp); 1042 } 1043 1044 // allow post-processing of the message body 1045 $data = $rcmail->plugins->exec_hook('message_part_after', [ 1046 'type' => $part->ctype_secondary, 1047 'body' => $body, 1048 'id' => $part->mime_id 1049 ] + $data); 1050 1051 return $data['body']; 1052 } 1053 1054 /** 1055 * Handle links and citation marks in plain text message 1056 * 1057 * @param string $body Plain text string 1058 * @param bool $flowed Set to True if the source text is in format=flowed 1059 * @param bool $delsp Enable 'delsp' option of format=flowed text 1060 * 1061 * @return string Formatted HTML string 1062 */ 1063 public static function plain_body($body, $flowed = false, $delsp = false) 1064 { 1065 $options = [ 1066 'flowed' => $flowed, 1067 'wrap' => !$flowed, 1068 'replacer' => 'rcmail_string_replacer', 1069 'delsp' => $delsp 1070 ]; 1071 1072 $text2html = new rcube_text2html($body, false, $options); 1073 $body = $text2html->get_html(); 1074 1075 return $body; 1076 } 1077 1078 /** 1079 * Callback function for washtml cleaning class 1080 */ 1081 public static function washtml_callback($tagname, $attrib, $content, $washtml) 1082 { 1083 $out = ''; 1084 1085 switch ($tagname) { 1086 case 'form': 1087 $out = html::div('form', $content); 1088 break; 1089 1090 case 'style': 1091 // Crazy big styles may freeze the browser (#1490539) 1092 // remove content with more than 5k lines 1093 if (substr_count($content, "\n") > 5000) { 1094 break; 1095 } 1096 1097 // decode all escaped entities and reduce to ascii strings 1098 $decoded = rcube_utils::xss_entity_decode($content); 1099 $stripped = preg_replace('/[^a-zA-Z\(:;]/', '', $decoded); 1100 1101 // now check for evil strings like expression, behavior or url() 1102 if (!preg_match('/expression|behavior|javascript:|import[^a]/i', $stripped)) { 1103 if (!$washtml->get_config('allow_remote') && preg_match('/url\((?!data:image)/', $stripped)) { 1104 $washtml->extlinks = true; 1105 } 1106 else { 1107 $out = html::tag('style', ['type' => 'text/css'], $decoded); 1108 } 1109 } 1110 } 1111 1112 return $out; 1113 } 1114 1115 public static function part_image_type($part) 1116 { 1117 $mimetype = strtolower($part->mimetype); 1118 1119 // Skip TIFF/WEBP images if browser doesn't support this format 1120 // ...until we can convert them to JPEG 1121 $tiff_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['tiff']); 1122 $tiff_support = $tiff_support || rcube_image::is_convertable('image/tiff'); 1123 $webp_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['webp']); 1124 $webp_support = $webp_support || rcube_image::is_convertable('image/webp'); 1125 1126 if ((!$tiff_support && $mimetype == 'image/tiff') || (!$webp_support && $mimetype == 'image/webp')) { 1127 return; 1128 } 1129 1130 // Content-Type: image/*... 1131 if (strpos($mimetype, 'image/') === 0) { 1132 return $mimetype; 1133 } 1134 1135 // Many clients use application/octet-stream, we'll detect mimetype 1136 // by checking filename extension 1137 1138 // Supported image filename extensions to image type map 1139 $types = [ 1140 'jpg' => 'image/jpeg', 1141 'jpeg' => 'image/jpeg', 1142 'png' => 'image/png', 1143 'gif' => 'image/gif', 1144 'bmp' => 'image/bmp', 1145 ]; 1146 1147 if ($tiff_support) { 1148 $types['tif'] = 'image/tiff'; 1149 $types['tiff'] = 'image/tiff'; 1150 } 1151 1152 if ($webp_support) { 1153 $types['webp'] = 'image/webp'; 1154 } 1155 1156 if ($part->filename 1157 && $mimetype == 'application/octet-stream' 1158 && preg_match('/\.([^.]+)$/i', $part->filename, $m) 1159 && ($extension = strtolower($m[1])) 1160 && isset($types[$extension]) 1161 ) { 1162 return $types[$extension]; 1163 } 1164 } 1165 1166 /** 1167 * Modify a HTML message that it can be displayed inside a HTML page 1168 */ 1169 public static function html4inline($body, &$args) 1170 { 1171 $last_pos = 0; 1172 $is_safe = !empty($args['safe']); 1173 $prefix = isset($args['css_prefix']) ? $args['css_prefix'] : null; 1174 $cont_id = trim( 1175 (!empty($args['container_id']) ? $args['container_id'] : '') 1176 . (!empty($args['body_class']) ? ' div.' . $args['body_class'] : '') 1177 ); 1178 1179 // find STYLE tags 1180 while (($pos = stripos($body, '<style', $last_pos)) !== false && ($pos2 = stripos($body, '</style>', $pos+1))) { 1181 $pos = strpos($body, '>', $pos) + 1; 1182 $len = $pos2 - $pos; 1183 1184 // replace all css definitions with #container [def] 1185 $styles = substr($body, $pos, $len); 1186 $styles = rcube_utils::mod_css_styles($styles, $cont_id, $is_safe, $prefix); 1187 1188 $body = substr_replace($body, $styles, $pos, $len); 1189 $last_pos = $pos2 + strlen($styles) - $len; 1190 } 1191 1192 $replace = [ 1193 // add comments around html and other tags 1194 '/(<!DOCTYPE[^>]*>)/i' => '<!--\\1-->', 1195 '/(<\?xml[^>]*>)/i' => '<!--\\1-->', 1196 '/(<\/?html[^>]*>)/i' => '<!--\\1-->', 1197 '/(<\/?head[^>]*>)/i' => '<!--\\1-->', 1198 '/(<title[^>]*>.*<\/title>)/Ui' => '<!--\\1-->', 1199 '/(<\/?meta[^>]*>)/i' => '<!--\\1-->', 1200 // quote <? of php and xml files that are specified as text/html 1201 '/<\?/' => '<?', 1202 '/\?>/' => '?>', 1203 ]; 1204 1205 $regexp = '/<body([^>]*)/'; 1206 1207 // Handle body attributes that doesn't play nicely with div elements 1208 if (preg_match($regexp, $body, $m)) { 1209 $style = []; 1210 $attrs = $m[0]; 1211 1212 // Get bgcolor, we'll set it as background-color of the message container 1213 if (!empty($m[1]) && preg_match('/bgcolor=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) { 1214 $style['background-color'] = $mb[1]; 1215 $attrs = preg_replace('/\s?bgcolor=["\']*[a-z0-9#]+["\']*/i', '', $attrs); 1216 } 1217 1218 // Get text color, we'll set it as font color of the message container 1219 if (!empty($m[1]) && preg_match('/text=["\']*([a-z0-9#]+)["\']*/i', $attrs, $mb)) { 1220 $style['color'] = $mb[1]; 1221 $attrs = preg_replace('/\s?text=["\']*[a-z0-9#]+["\']*/i', '', $attrs); 1222 } 1223 1224 // Get background, we'll set it as background-image of the message container 1225 if (!empty($m[1]) && preg_match('/background=["\']*([^"\'>\s]+)["\']*/', $attrs, $mb)) { 1226 $style['background-image'] = 'url('.$mb[1].')'; 1227 $attrs = preg_replace('/\s?background=["\']*([^"\'>\s]+)["\']*/', '', $attrs); 1228 } 1229 1230 if (!empty($style)) { 1231 $body = preg_replace($regexp, rtrim($attrs), $body, 1); 1232 } 1233 1234 // handle body styles related to background image 1235 if (!empty($style['background-image'])) { 1236 // get body style 1237 if (preg_match('/#'.preg_quote($cont_id, '/').'\s+\{([^}]+)}/i', $body, $m)) { 1238 // get background related style 1239 $regexp = '/(background-position|background-repeat)\s*:\s*([^;]+);/i'; 1240 if (preg_match_all($regexp, $m[1], $matches, PREG_SET_ORDER)) { 1241 foreach ($matches as $m) { 1242 $style[$m[1]] = $m[2]; 1243 } 1244 } 1245 } 1246 } 1247 1248 if (!empty($style)) { 1249 foreach ($style as $idx => $val) { 1250 $style[$idx] = $idx . ': ' . $val; 1251 } 1252 1253 $args['container_attrib']['style'] = implode('; ', $style); 1254 } 1255 1256 // replace <body> with <div> 1257 if (!empty($args['body_class'])) { 1258 $replace['/<body([^>]*)>/i'] = '<div class="' . $args['body_class'] . '"\\1>'; 1259 } 1260 else { 1261 $replace['/<body/i'] = '<div'; 1262 } 1263 1264 $replace['/<\/body>/i'] = '</div>'; 1265 } 1266 // make sure there's 'rcmBody' div, we need it for proper css modification 1267 // its name is hardcoded in self::message_body() also 1268 else if (!empty($args['body_class'])) { 1269 $body = '<div class="' . $args['body_class'] . '">' . $body . '</div>'; 1270 } 1271 1272 // Clean up, and replace <body> with <div> 1273 $body = preg_replace(array_keys($replace), array_values($replace), $body); 1274 1275 return $body; 1276 } 1277 1278 /** 1279 * Parse link (a, link, area) attributes and set correct target 1280 */ 1281 public static function washtml_link_callback($tag, $attribs, $content, $washtml) 1282 { 1283 $rcmail = rcmail::get_instance(); 1284 $attrib = html::parse_attrib_string($attribs); 1285 1286 // Remove non-printable characters in URL (#1487805) 1287 if (isset($attrib['href'])) { 1288 $attrib['href'] = preg_replace('/[\x00-\x1F]/', '', $attrib['href']); 1289 1290 if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) { 1291 $tempurl = 'tmp-' . md5($attrib['href']) . '.css'; 1292 $_SESSION['modcssurls'][$tempurl] = $attrib['href']; 1293 $attrib['href'] = $rcmail->url([ 1294 'task' => 'utils', 1295 'action' => 'modcss', 1296 'u' => $tempurl, 1297 'c' => $washtml->get_config('container_id'), 1298 'p' => $washtml->get_config('css_prefix'), 1299 ]); 1300 $content = null; 1301 } 1302 else if (preg_match('/^mailto:(.+)/i', $attrib['href'], $mailto)) { 1303 $url_parts = explode('?', html_entity_decode($mailto[1], ENT_QUOTES, 'UTF-8'), 2); 1304 $mailto = $url_parts[0]; 1305 $url = isset($url_parts[1]) ? $url_parts[1] : ''; 1306 1307 // #6020: use raw encoding for correct "+" character handling as specified in RFC6068 1308 $url = rawurldecode($url); 1309 $mailto = rawurldecode($mailto); 1310 $addresses = rcube_mime::decode_address_list($mailto, null, true); 1311 $mailto = []; 1312 1313 // do sanity checks on recipients 1314 foreach ($addresses as $idx => $addr) { 1315 if (rcube_utils::check_email($addr['mailto'], false)) { 1316 $addresses[$idx] = $addr['mailto']; 1317 $mailto[] = $addr['string']; 1318 } 1319 else { 1320 unset($addresses[$idx]); 1321 } 1322 } 1323 1324 if (!empty($addresses)) { 1325 $attrib['href'] = 'mailto:' . implode(',', $addresses); 1326 $attrib['onclick'] = sprintf( 1327 "return %s.command('compose','%s',this)", 1328 rcmail_output::JS_OBJECT_NAME, 1329 rcube::JQ(implode(',', $mailto) . ($url ? "?$url" : ''))); 1330 } 1331 else { 1332 $attrib['href'] = '#NOP'; 1333 $attrib['onclick'] = ''; 1334 } 1335 } 1336 else if (!empty($attrib['href']) && $attrib['href'][0] != '#') { 1337 $attrib['target'] = '_blank'; 1338 } 1339 1340 // Better security by adding rel="noreferrer" (#1484686) 1341 if (($tag == 'a' || $tag == 'area') && $attrib['href'] && $attrib['href'][0] != '#') { 1342 $attrib['rel'] = 'noreferrer'; 1343 } 1344 } 1345 1346 // allowed attributes for a|link|area tags 1347 $allow = ['href','name','target','onclick','id','class','style','title', 1348 'rel','type','media','alt','coords','nohref','hreflang','shape']; 1349 1350 return html::tag($tag, $attrib, $content, $allow); 1351 } 1352 1353 /** 1354 * Decode address string and re-format it as HTML links 1355 */ 1356 public static function address_string($input, $max = null, $linked = false, $addicon = null, $default_charset = null, $title = null) 1357 { 1358 $a_parts = rcube_mime::decode_address_list($input, null, true, $default_charset); 1359 1360 if (!count($a_parts)) { 1361 return null; 1362 } 1363 1364 $rcmail = rcmail::get_instance(); 1365 $c = count($a_parts); 1366 $j = 0; 1367 $out = ''; 1368 $allvalues = []; 1369 $shown_addresses = []; 1370 $show_email = $rcmail->config->get('message_show_email'); 1371 1372 if ($addicon && !isset($_SESSION['writeable_abook'])) { 1373 $_SESSION['writeable_abook'] = $rcmail->get_address_sources(true) ? true : false; 1374 } 1375 1376 foreach ($a_parts as $part) { 1377 $j++; 1378 1379 $name = $part['name']; 1380 $mailto = $part['mailto']; 1381 $string = $part['string']; 1382 $valid = rcube_utils::check_email($mailto, false); 1383 1384 // phishing email prevention (#1488981), e.g. "valid@email.addr <phishing@email.addr>" 1385 if (!$show_email && $valid && $name && $name != $mailto && preg_match('/@|@|﹫/', $name)) { 1386 $name = ''; 1387 } 1388 1389 // IDNA ASCII to Unicode 1390 if ($name == $mailto) { 1391 $name = rcube_utils::idn_to_utf8($name); 1392 } 1393 if ($string == $mailto) { 1394 $string = rcube_utils::idn_to_utf8($string); 1395 } 1396 $mailto = rcube_utils::idn_to_utf8($mailto); 1397 1398 // Homograph attack detection (#6891) 1399 if (!self::$SUSPICIOUS_EMAIL) { 1400 self::$SUSPICIOUS_EMAIL = rcube_spoofchecker::check($mailto); 1401 } 1402 1403 if (self::$PRINT_MODE) { 1404 $address = '<' . rcube::Q($mailto) . '>'; 1405 if ($name) { 1406 $address = rcube::SQ($name) . ' ' . $address; 1407 } 1408 } 1409 else if ($valid) { 1410 if ($linked) { 1411 $attrs = [ 1412 'href' => 'mailto:' . $mailto, 1413 'class' => 'rcmContactAddress', 1414 'onclick' => sprintf("return %s.command('compose','%s',this)", 1415 rcmail_output::JS_OBJECT_NAME, rcube::JQ(format_email_recipient($mailto, $name))), 1416 ]; 1417 1418 if ($show_email && $name && $mailto) { 1419 $content = rcube::SQ($name ? sprintf('%s <%s>', $name, $mailto) : $mailto); 1420 } 1421 else { 1422 $content = rcube::SQ($name ?: $mailto); 1423 $attrs['title'] = $mailto; 1424 } 1425 1426 $address = html::a($attrs, $content); 1427 } 1428 else { 1429 $address = html::span(['title' => $mailto, 'class' => "rcmContactAddress"], 1430 rcube::SQ($name ?: $mailto)); 1431 } 1432 1433 if ($addicon && $_SESSION['writeable_abook']) { 1434 $label = $rcmail->gettext('addtoaddressbook'); 1435 $icon = html::img([ 1436 'src' => $rcmail->output->asset_url($addicon, true), 1437 'alt' => $label, 1438 'class' => 'noselect', 1439 ]); 1440 $address .= html::a([ 1441 'href' => "#add", 1442 'title' => $label, 1443 'class' => 'rcmaddcontact', 1444 'onclick' => sprintf("return %s.command('add-contact','%s',this)", 1445 rcmail_output::JS_OBJECT_NAME, rcube::JQ($string)), 1446 ], 1447 $addicon == 'virtual' ? '' : $icon 1448 ); 1449 } 1450 } 1451 else { 1452 $address = $name ? rcube::Q($name) : ''; 1453 if ($mailto) { 1454 $address = trim($address . ' ' . rcube::Q($name ? sprintf('<%s>', $mailto) : $mailto)); 1455 } 1456 } 1457 1458 $address = html::span('adr', $address); 1459 $allvalues[] = $address; 1460 1461 if (empty($moreadrs)) { 1462 $out .= ($out ? ', ' : '') . $address; 1463 $shown_addresses[] = $address; 1464 } 1465 1466 if ($max && $j == $max && $c > $j) { 1467 if ($linked) { 1468 $moreadrs = $c - $j; 1469 } 1470 else { 1471 $out .= '...'; 1472 break; 1473 } 1474 } 1475 } 1476 1477 if (!empty($moreadrs)) { 1478 $label = rcube::Q($rcmail->gettext(['name' => 'andnmore', 'vars' => ['nr' => $moreadrs]])); 1479 1480 if (self::$PRINT_MODE) { 1481 $out .= ', ' . html::a([ 1482 'href' => '#more', 1483 'class' => 'morelink', 1484 'onclick' => '$(this).hide().next().show()', 1485 ], $label) 1486 . html::span(['style' => 'display:none'], join(', ', array_diff($allvalues, $shown_addresses))); 1487 } 1488 else { 1489 $out .= ', ' . html::a([ 1490 'href' => '#more', 1491 'class' => 'morelink', 1492 'onclick' => sprintf("return %s.simple_dialog('%s','%s',null,{cancel_button:'close'})", 1493 rcmail_output::JS_OBJECT_NAME, 1494 rcube::JQ(join(', ', $allvalues)), 1495 rcube::JQ($title)) 1496 ], $label); 1497 } 1498 } 1499 1500 return $out; 1501 } 1502 1503 /** 1504 * Wrap text to a given number of characters per line 1505 * but respect the mail quotation of replies messages (>). 1506 * Finally add another quotation level by prepending the lines 1507 * with > 1508 * 1509 * @param string $text Text to wrap 1510 * @param int $length The line width 1511 * @param bool $quote Enable quote indentation 1512 * 1513 * @return string The wrapped text 1514 */ 1515 public static function wrap_and_quote($text, $length = 72, $quote = true) 1516 { 1517 // Rebuild the message body with a maximum of $max chars, while keeping quoted message. 1518 $max = max(75, $length + 8); 1519 $lines = preg_split('/\r?\n/', trim($text)); 1520 $out = ''; 1521 1522 foreach ($lines as $line) { 1523 // don't wrap already quoted lines 1524 if (isset($line[0]) && $line[0] == '>') { 1525 $line = rtrim($line); 1526 if ($quote) { 1527 $line = '>' . $line; 1528 } 1529 } 1530 // wrap lines above the length limit, but skip these 1531 // special lines with links list created by rcube_html2text 1532 else if (mb_strlen($line) > $max && !preg_match('|^\[[0-9]+\] https?://\S+$|', $line)) { 1533 $newline = ''; 1534 1535 foreach (explode("\n", rcube_mime::wordwrap($line, $length - 2)) as $l) { 1536 if ($quote) { 1537 $newline .= strlen($l) ? "> $l\n" : ">\n"; 1538 } 1539 else { 1540 $newline .= "$l\n"; 1541 } 1542 } 1543 1544 $line = rtrim($newline); 1545 } 1546 else if ($quote) { 1547 $line = '> ' . $line; 1548 } 1549 1550 // Append the line 1551 $out .= $line . "\n"; 1552 } 1553 1554 return rtrim($out, "\n"); 1555 } 1556 1557 /** 1558 * Return attachment filename, handle empty filename case 1559 * 1560 * @param rcube_message_part $attachment Message part 1561 * @param bool $display Convert to a description text for "special" types 1562 * 1563 * @return string Filename 1564 */ 1565 public static function attachment_name($attachment, $display = false) 1566 { 1567 $rcmail = rcmail::get_instance(); 1568 1569 $filename = (string) $attachment->filename; 1570 $filename = str_replace(["\r", "\n"], '', $filename); 1571 1572 if ($filename === '') { 1573 if ($attachment->mimetype == 'text/html') { 1574 $filename = $rcmail->gettext('htmlmessage'); 1575 } 1576 else { 1577 $ext = array_first((array) rcube_mime::get_mime_extensions($attachment->mimetype)); 1578 $filename = $rcmail->gettext('messagepart') . ' ' . $attachment->mime_id; 1579 if ($ext) { 1580 $filename .= '.' . $ext; 1581 } 1582 } 1583 } 1584 1585 // Display smart names for some known mimetypes 1586 if ($display) { 1587 if (preg_match('/application\/(pgp|pkcs7)-signature/i', $attachment->mimetype)) { 1588 $filename = $rcmail->gettext('digitalsig'); 1589 } 1590 } 1591 1592 return $filename; 1593 } 1594 1595 public static function search_filter($attrib) 1596 { 1597 $rcmail = rcmail::get_instance(); 1598 1599 if (empty($attrib['id'])) { 1600 $attrib['id'] = 'rcmlistfilter'; 1601 } 1602 1603 if (!self::get_bool_attr($attrib, 'noevent')) { 1604 $attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . '.filter_mailbox(this.value)'; 1605 } 1606 1607 // Content-Type values of messages with attachments 1608 // the same as in app.js:add_message_row() 1609 $ctypes = ['application/', 'multipart/m', 'multipart/signed', 'multipart/report']; 1610 1611 // Build search string of "with attachment" filter 1612 $attachment = trim(str_repeat(' OR', count($ctypes)-1)); 1613 foreach ($ctypes as $type) { 1614 $attachment .= ' HEADER Content-Type ' . rcube_imap_generic::escape($type); 1615 } 1616 1617 $select = new html_select($attrib); 1618 $select->add($rcmail->gettext('all'), 'ALL'); 1619 $select->add($rcmail->gettext('unread'), 'UNSEEN'); 1620 $select->add($rcmail->gettext('flagged'), 'FLAGGED'); 1621 $select->add($rcmail->gettext('unanswered'), 'UNANSWERED'); 1622 if (!$rcmail->config->get('skip_deleted')) { 1623 $select->add($rcmail->gettext('deleted'), 'DELETED'); 1624 $select->add($rcmail->gettext('undeleted'), 'UNDELETED'); 1625 } 1626 $select->add($rcmail->gettext('withattachment'), $attachment); 1627 $select->add($rcmail->gettext('priority').': '.$rcmail->gettext('highest'), 'HEADER X-PRIORITY 1'); 1628 $select->add($rcmail->gettext('priority').': '.$rcmail->gettext('high'), 'HEADER X-PRIORITY 2'); 1629 $select->add($rcmail->gettext('priority').': '.$rcmail->gettext('normal'), 'NOT HEADER X-PRIORITY 1 NOT HEADER X-PRIORITY 2 NOT HEADER X-PRIORITY 4 NOT HEADER X-PRIORITY 5'); 1630 $select->add($rcmail->gettext('priority').': '.$rcmail->gettext('low'), 'HEADER X-PRIORITY 4'); 1631 $select->add($rcmail->gettext('priority').': '.$rcmail->gettext('lowest'), 'HEADER X-PRIORITY 5'); 1632 1633 $rcmail->output->add_gui_object('search_filter', $attrib['id']); 1634 1635 $selected = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GET); 1636 1637 if (!$selected && !empty($_REQUEST['_search'])) { 1638 $selected = $_SESSION['search_filter']; 1639 } 1640 1641 return $select->show($selected ?: 'ALL'); 1642 } 1643 1644 public static function search_interval($attrib) 1645 { 1646 $rcmail = rcmail::get_instance(); 1647 1648 if (empty($attrib['id'])) { 1649 $attrib['id'] = 'rcmsearchinterval'; 1650 } 1651 1652 $select = new html_select($attrib); 1653 $select->add('', ''); 1654 1655 foreach (['1W', '1M', '1Y', '-1W', '-1M', '-1Y'] as $value) { 1656 $select->add($rcmail->gettext('searchinterval' . $value), $value); 1657 } 1658 1659 $rcmail->output->add_gui_object('search_interval', $attrib['id']); 1660 1661 return $select->show(!empty($_REQUEST['_search']) ? $_SESSION['search_interval'] : ''); 1662 } 1663 1664 public static function message_error() 1665 { 1666 $rcmail = rcmail::get_instance(); 1667 1668 // ... display message error page 1669 if ($rcmail->output->template_exists('messageerror')) { 1670 // Set env variables for messageerror.html template 1671 if ($rcmail->action == 'show') { 1672 $mbox_name = $rcmail->storage->get_folder(); 1673 1674 $rcmail->output->set_env('mailbox', $mbox_name); 1675 $rcmail->output->set_env('uid', null); 1676 } 1677 1678 $rcmail->output->show_message('messageopenerror', 'error'); 1679 $rcmail->output->send('messageerror'); 1680 } 1681 else { 1682 $rcmail->raise_error(['code' => 410], false, true); 1683 } 1684 } 1685 1686 public static function message_import_form($attrib = []) 1687 { 1688 $rcmail = rcmail::get_instance(); 1689 1690 $rcmail->output->add_label('selectimportfile', 'importwait', 'importmessages', 'import'); 1691 1692 $description = $rcmail->gettext('mailimportdesc'); 1693 $input_attr = [ 1694 'multiple' => true, 1695 'name' => '_file[]', 1696 'accept' => '.eml,.mbox,.msg,message/rfc822,text/*', 1697 ]; 1698 1699 if (class_exists('ZipArchive', false)) { 1700 $input_attr['accept'] .= '.zip,application/zip,application/x-zip'; 1701 $description .= ' ' . $rcmail->gettext('mailimportzip'); 1702 } 1703 1704 $attrib['prefix'] = html::tag('input', ['type' => 'hidden', 'name' => '_unlock', 'value' => '']) 1705 . html::tag('input', ['type' => 'hidden', 'name' => '_framed', 'value' => '1']) 1706 . html::p(null, $description); 1707 1708 return self::upload_form($attrib, 'importform', 'import-messages', $input_attr); 1709 } 1710 1711 // Return mimetypes supported by the browser 1712 public static function supported_mimetypes() 1713 { 1714 $rcmail = rcube::get_instance(); 1715 1716 // mimetypes supported by the browser (default settings) 1717 $mimetypes = (array) $rcmail->config->get('client_mimetypes'); 1718 1719 // Remove unsupported types, which makes that attachment which cannot be 1720 // displayed in a browser will be downloaded directly without displaying an overlay page 1721 if (empty($_SESSION['browser_caps']['pdf']) && ($key = array_search('application/pdf', $mimetypes)) !== false) { 1722 unset($mimetypes[$key]); 1723 } 1724 1725 if (empty($_SESSION['browser_caps']['flash']) && ($key = array_search('application/x-shockwave-flash', $mimetypes)) !== false) { 1726 unset($mimetypes[$key]); 1727 } 1728 1729 // We cannot securely preview XML files as we do not have a proper parser 1730 if (($key = array_search('text/xml', $mimetypes)) !== false) { 1731 unset($mimetypes[$key]); 1732 } 1733 1734 foreach (['tiff', 'webp'] as $type) { 1735 if (empty($_SESSION['browser_caps'][$type]) && ($key = array_search('image/' . $type, $mimetypes)) !== false) { 1736 // can we convert it to jpeg? 1737 if (!rcube_image::is_convertable('image/' . $type)) { 1738 unset($mimetypes[$key]); 1739 } 1740 } 1741 } 1742 1743 // @TODO: support mail preview for compose attachments 1744 if ($rcmail->action != 'compose' && !in_array('message/rfc822', $mimetypes)) { 1745 $mimetypes[] = 'message/rfc822'; 1746 } 1747 1748 return array_values($mimetypes); 1749 } 1750} 1751