mirror of
https://github.com/roundcube/roundcubemail.git
synced 2026-03-03 23:04:01 +01:00
This moves the warning icon that is triggered by the homograph check from the generic "notification area" (between headers and body) to the header area, before the address that the warning is referring to. The previous warning left it unclear which address was found to be problematic, which now is obvious. Additionally there's now a test to check for these warnings to show up in the DOM.
1688 lines
64 KiB
PHP
1688 lines
64 KiB
PHP
<?php
|
||
|
||
/*
|
||
+-----------------------------------------------------------------------+
|
||
| This file is part of the Roundcube Webmail client |
|
||
| |
|
||
| Copyright (C) The Roundcube Dev Team |
|
||
| |
|
||
| Licensed under the GNU General Public License version 3 or |
|
||
| any later version with exceptions for skins & plugins. |
|
||
| See the README file for a full license statement. |
|
||
| |
|
||
| PURPOSE: |
|
||
| Provide webmail functionality and GUI objects |
|
||
+-----------------------------------------------------------------------+
|
||
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
||
| Author: Aleksander Machniak <alec@alec.pl> |
|
||
+-----------------------------------------------------------------------+
|
||
*/
|
||
|
||
class rcmail_action_mail_index extends rcmail_action
|
||
{
|
||
public static $aliases = [
|
||
'refresh' => 'check-recent',
|
||
'preview' => 'show',
|
||
'print' => 'show',
|
||
'expunge' => 'folder-expunge',
|
||
'purge' => 'folder-purge',
|
||
'remove-attachment' => 'attachment-delete',
|
||
'rename-attachment' => 'attachment-rename',
|
||
'display-attachment' => 'attachment-display',
|
||
'upload' => 'attachment-upload',
|
||
];
|
||
|
||
protected static $PRINT_MODE = false;
|
||
protected static $REMOTE_OBJECTS;
|
||
protected static $SUSPICIOUS_EMAIL = false;
|
||
protected static $wash_html_body_attrs = [];
|
||
|
||
/**
|
||
* Request handler.
|
||
*
|
||
* @param array $args Arguments from the previous step(s)
|
||
*/
|
||
#[\Override]
|
||
public function run($args = [])
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
// always instantiate storage object (but not connect to server yet)
|
||
$rcmail->storage_init();
|
||
|
||
// init environment - set current folder, page, list mode
|
||
self::init_env();
|
||
|
||
// set message set for search result
|
||
if (
|
||
!empty($_REQUEST['_search'])
|
||
&& isset($_SESSION['search'])
|
||
&& isset($_SESSION['search_request'])
|
||
&& $_SESSION['search_request'] == $_REQUEST['_search']
|
||
) {
|
||
$rcmail->storage->set_search_set($_SESSION['search']);
|
||
|
||
$rcmail->output->set_env('search_request', $_REQUEST['_search']);
|
||
$rcmail->output->set_env('search_text', $_SESSION['last_text_search']);
|
||
}
|
||
|
||
// remove mbox part from _uid
|
||
$uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GPC);
|
||
if ($uid && preg_match('/^\d+-.+/', $uid)) {
|
||
[$uid, $mbox] = explode('-', $uid, 2);
|
||
if (isset($_GET['_uid'])) {
|
||
$_GET['_uid'] = $uid;
|
||
}
|
||
if (isset($_POST['_uid'])) {
|
||
$_POST['_uid'] = $uid;
|
||
}
|
||
$_REQUEST['_uid'] = $uid;
|
||
|
||
// override mbox
|
||
if (!empty($mbox)) {
|
||
$_GET['_mbox'] = $mbox;
|
||
$_POST['_mbox'] = $mbox;
|
||
$rcmail->storage->set_folder($_SESSION['mbox'] = $mbox);
|
||
}
|
||
}
|
||
|
||
if (!empty($_SESSION['browser_caps']) && !$rcmail->output->ajax_call) {
|
||
$rcmail->output->set_env('browser_capabilities', $_SESSION['browser_caps']);
|
||
}
|
||
|
||
// set main env variables, labels and page title
|
||
if (empty($rcmail->action) || $rcmail->action == 'list') {
|
||
// connect to storage server and trigger error on failure
|
||
$rcmail->storage_connect();
|
||
|
||
$mbox_name = $rcmail->storage->get_folder();
|
||
|
||
if (empty($rcmail->action)) {
|
||
$rcmail->output->set_env('search_mods', self::search_mods());
|
||
$rcmail->output->set_env('search_scope', self::search_scope());
|
||
|
||
self::list_pagetitle();
|
||
}
|
||
|
||
$threading = (bool) $rcmail->storage->get_threading();
|
||
$delimiter = $rcmail->storage->get_hierarchy_delimiter();
|
||
|
||
// set current mailbox and some other vars in client environment
|
||
$rcmail->output->set_env('mailbox', $mbox_name);
|
||
$rcmail->output->set_env('pagesize', $rcmail->storage->get_pagesize());
|
||
$rcmail->output->set_env('current_page', max(1, $_SESSION['page'] ?? 1));
|
||
$rcmail->output->set_env('delimiter', $delimiter);
|
||
$rcmail->output->set_env('threading', $threading);
|
||
$rcmail->output->set_env('threads', $threading || $rcmail->storage->get_capability('THREAD'));
|
||
$rcmail->output->set_env('reply_all_mode', (int) $rcmail->config->get('reply_all_mode'));
|
||
$rcmail->output->set_env('layout', $rcmail->config->get('layout') ?: 'widescreen');
|
||
$rcmail->output->set_env('quota', $rcmail->storage->get_capability('QUOTA'));
|
||
|
||
// set special folders
|
||
foreach (['drafts', 'trash', 'junk'] as $mbox) {
|
||
if ($folder = $rcmail->config->get($mbox . '_mbox')) {
|
||
$rcmail->output->set_env($mbox . '_mailbox', $folder);
|
||
}
|
||
}
|
||
|
||
if (!empty($_GET['_uid'])) {
|
||
$rcmail->output->set_env('list_uid', $_GET['_uid']);
|
||
}
|
||
|
||
// set configuration
|
||
self::set_env_config(['delete_junk', 'flag_for_deletion', 'read_when_deleted',
|
||
'skip_deleted', 'display_next', 'message_extwin', 'forward_attachment']);
|
||
|
||
if (!$rcmail->output->ajax_call) {
|
||
$rcmail->output->add_label('checkingmail', 'deletemessage', 'movemessagetotrash',
|
||
'movingmessage', 'copyingmessage', 'deletingmessage', 'markingmessage',
|
||
'copy', 'move', 'quota', 'replyall', 'replylist', 'stillsearching',
|
||
'flagged', 'unflagged', 'unread', 'deleted', 'replied', 'forwarded',
|
||
'priority', 'withattachment', 'fileuploaderror', 'mark', 'markallread',
|
||
'markasread', 'markasunread', 'markasflagged', 'markasunflagged', 'undeletemessage',
|
||
'folders-cur', 'folders-sub', 'folders-all', 'cancel', 'bounce', 'bouncemsg',
|
||
'sendingmessage');
|
||
}
|
||
}
|
||
|
||
// register UI objects
|
||
$rcmail->output->add_handlers([
|
||
'mailboxlist' => [$rcmail, 'folder_list'],
|
||
'quotadisplay' => [$this, 'quota_display'],
|
||
'messages' => [$this, 'message_list'],
|
||
'messagecountdisplay' => [$this, 'messagecount_display'],
|
||
'listmenulink' => [$this, 'options_menu_link'],
|
||
'mailboxname' => [$this, 'mailbox_name_display'],
|
||
'messageimportform' => [$this, 'message_import_form'],
|
||
'searchfilter' => [$this, 'search_filter'],
|
||
'searchinterval' => [$this, 'search_interval'],
|
||
'searchform' => [$rcmail->output, 'search_form'],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Sets storage properties and session
|
||
*/
|
||
public static function init_env()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
$default_threading = $rcmail->config->get('default_list_mode', 'list') == 'threads';
|
||
$a_threading = $rcmail->config->get('message_threading', []);
|
||
$message_sort_col = $rcmail->config->get('message_sort_col');
|
||
$message_sort_order = $rcmail->config->get('message_sort_order');
|
||
|
||
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_GPC, true);
|
||
|
||
// set imap properties and session vars
|
||
if (!strlen($mbox)) {
|
||
$mbox = isset($_SESSION['mbox']) && strlen($_SESSION['mbox']) ? $_SESSION['mbox'] : 'INBOX';
|
||
}
|
||
|
||
// We handle 'page' argument on 'list' and 'getunread' to prevent from
|
||
// race condition and unintentional page overwrite in session.
|
||
// Also, when entering the Mail UI (#7932)
|
||
if (empty($rcmail->action) || $rcmail->action == 'list' || $rcmail->action == 'getunread') {
|
||
$page = isset($_GET['_page']) ? intval($_GET['_page']) : 0;
|
||
if (!$page) {
|
||
$page = !empty($_SESSION['page']) ? $_SESSION['page'] : 1;
|
||
}
|
||
|
||
$_SESSION['page'] = $page;
|
||
}
|
||
|
||
$rcmail->storage->set_folder($_SESSION['mbox'] = $mbox);
|
||
$rcmail->storage->set_page($_SESSION['page'] ?? 1);
|
||
|
||
// set default sort col/order to session
|
||
if (!isset($_SESSION['sort_col'])) {
|
||
$_SESSION['sort_col'] = $message_sort_col ?: '';
|
||
}
|
||
if (!isset($_SESSION['sort_order'])) {
|
||
$_SESSION['sort_order'] = strtoupper($message_sort_order) == 'ASC' ? 'ASC' : 'DESC';
|
||
}
|
||
|
||
// set threads mode
|
||
if (isset($_GET['_threads'])) {
|
||
if ($_GET['_threads']) {
|
||
// re-set current page number when listing mode changes
|
||
if (empty($a_threading[$_SESSION['mbox']])) {
|
||
$rcmail->storage->set_page($_SESSION['page'] = 1);
|
||
}
|
||
|
||
$a_threading[$_SESSION['mbox']] = true;
|
||
} else {
|
||
// re-set current page number when listing mode changes
|
||
if (!empty($a_threading[$_SESSION['mbox']])) {
|
||
$rcmail->storage->set_page($_SESSION['page'] = 1);
|
||
}
|
||
|
||
$a_threading[$_SESSION['mbox']] = false;
|
||
}
|
||
|
||
$rcmail->user->save_prefs(['message_threading' => $a_threading]);
|
||
}
|
||
|
||
$threading = $a_threading[$_SESSION['mbox']] ?? $default_threading;
|
||
|
||
$rcmail->storage->set_threading($threading);
|
||
}
|
||
|
||
/**
|
||
* Sets page title
|
||
*/
|
||
public static function list_pagetitle()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if ($rcmail->output->get_env('search_request')) {
|
||
$pagetitle = $rcmail->gettext('searchresult');
|
||
} else {
|
||
$mbox_name = $rcmail->output->get_env('mailbox') ?: $rcmail->storage->get_folder();
|
||
$delimiter = $rcmail->storage->get_hierarchy_delimiter();
|
||
$pagetitle = self::localize_foldername($mbox_name, true);
|
||
$pagetitle = str_replace($delimiter, " \xC2\xBB ", $pagetitle);
|
||
}
|
||
|
||
$rcmail->output->set_pagetitle($pagetitle);
|
||
}
|
||
|
||
/**
|
||
* Returns default search mods
|
||
*/
|
||
public static function search_mods()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$mods = $rcmail->config->get('search_mods');
|
||
|
||
if (empty($mods)) {
|
||
$mods = ['*' => ['subject' => 1, 'from' => 1]];
|
||
|
||
foreach (['sent', 'drafts'] as $mbox) {
|
||
if ($mbox = $rcmail->config->get($mbox . '_mbox')) {
|
||
$mods[$mbox] = ['subject' => 1, 'to' => 1];
|
||
}
|
||
}
|
||
}
|
||
|
||
return $mods;
|
||
}
|
||
|
||
/**
|
||
* Returns a requested or default search scope
|
||
*/
|
||
public static function search_scope()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$scope = rcube_utils::get_input_string('_scope', rcube_utils::INPUT_GET);
|
||
|
||
if (!$scope && isset($_SESSION['search_scope']) && $rcmail->output->get_env('search_request')) {
|
||
$scope = $_SESSION['search_scope'];
|
||
}
|
||
|
||
if (!$scope) {
|
||
$scope = $rcmail->config->get('search_scope');
|
||
}
|
||
|
||
if (!$scope || !preg_match('/^(all|sub|base)$/i', $scope)) {
|
||
$scope = 'base';
|
||
}
|
||
|
||
return strtolower($scope);
|
||
}
|
||
|
||
/**
|
||
* Returns 'to' if current folder is configured Sent or Drafts
|
||
* or their subfolders, otherwise returns 'from'.
|
||
*
|
||
* @return string Column name
|
||
*/
|
||
public static function message_list_smart_column_name()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$delim = $rcmail->storage->get_hierarchy_delimiter();
|
||
$sent_mbox = $rcmail->config->get('sent_mbox');
|
||
$drafts_mbox = $rcmail->config->get('drafts_mbox');
|
||
$mbox = $rcmail->output->get_env('mailbox');
|
||
|
||
if (!is_string($mbox) || !strlen($mbox)) {
|
||
$mbox = $rcmail->storage->get_folder();
|
||
}
|
||
|
||
if ((str_starts_with($mbox . $delim, $sent_mbox . $delim)
|
||
|| str_starts_with($mbox . $delim, $drafts_mbox . $delim))
|
||
&& strtoupper($mbox) != 'INBOX'
|
||
) {
|
||
return 'to';
|
||
}
|
||
|
||
return 'from';
|
||
}
|
||
|
||
/**
|
||
* Returns configured messages list sorting column name
|
||
* The name is context-sensitive, which means if sorting is set to 'fromto'
|
||
* it will return 'from' or 'to' according to current folder type.
|
||
*
|
||
* @return string Column name
|
||
*/
|
||
public static function sort_column()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (isset($_SESSION['sort_col'])) {
|
||
$column = $_SESSION['sort_col'];
|
||
} else {
|
||
$column = $rcmail->config->get('message_sort_col');
|
||
}
|
||
|
||
// get name of smart From/To column in folder context
|
||
if ($column == 'fromto') {
|
||
$column = self::message_list_smart_column_name();
|
||
}
|
||
|
||
return $column;
|
||
}
|
||
|
||
/**
|
||
* Returns configured message list sorting order
|
||
*
|
||
* @return string Sorting order (ASC|DESC)
|
||
*/
|
||
public static function sort_order()
|
||
{
|
||
if (isset($_SESSION['sort_order'])) {
|
||
return $_SESSION['sort_order'];
|
||
}
|
||
|
||
return rcmail::get_instance()->config->get('message_sort_order');
|
||
}
|
||
|
||
/**
|
||
* return the message list as HTML table
|
||
*/
|
||
public function message_list($attrib)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
// add some labels to client
|
||
$rcmail->output->add_label('from', 'to');
|
||
|
||
// add id to message list table if not specified
|
||
if (empty($attrib['id'])) {
|
||
$attrib['id'] = 'rcubemessagelist';
|
||
}
|
||
|
||
// define list of cols to be displayed based on parameter or config
|
||
if (empty($attrib['columns'])) {
|
||
$list_cols = $rcmail->config->get('list_cols');
|
||
$a_show_cols = !empty($list_cols) && is_array($list_cols) ? $list_cols : ['subject'];
|
||
|
||
$rcmail->output->set_env('col_movable', !in_array('list_cols', (array) $rcmail->config->get('dont_override')));
|
||
} else {
|
||
$a_show_cols = preg_split('/[\s,;]+/', str_replace(["'", '"'], '', $attrib['columns']));
|
||
$attrib['columns'] = $a_show_cols;
|
||
}
|
||
|
||
// save some variables for use in ajax list
|
||
$_SESSION['list_attrib'] = $attrib;
|
||
|
||
// make sure 'threads' and 'subject' columns are present
|
||
if (!in_array('subject', $a_show_cols)) {
|
||
array_unshift($a_show_cols, 'subject');
|
||
}
|
||
if (!in_array('threads', $a_show_cols)) {
|
||
array_unshift($a_show_cols, 'threads');
|
||
}
|
||
|
||
$listcols = $a_show_cols;
|
||
|
||
// set client env
|
||
$rcmail->output->add_gui_object('messagelist', $attrib['id']);
|
||
$rcmail->output->set_env('autoexpand_threads', intval($rcmail->config->get('autoexpand_threads')));
|
||
$rcmail->output->set_env('sort_col', $_SESSION['sort_col']);
|
||
$rcmail->output->set_env('sort_order', $_SESSION['sort_order']);
|
||
$rcmail->output->set_env('messages', []);
|
||
$rcmail->output->set_env('listcols', $listcols);
|
||
$rcmail->output->set_env('listcols_widescreen', ['threads', 'subject', 'fromto', 'date', 'size', 'flag', 'attachment']);
|
||
|
||
$rcmail->output->include_script('list.js');
|
||
|
||
$table = new html_table($attrib);
|
||
|
||
if (empty($attrib['noheader'])) {
|
||
$allcols = array_merge($listcols, ['threads', 'subject', 'fromto', 'date', 'size', 'flag', 'attachment']);
|
||
$allcols = array_unique($allcols);
|
||
|
||
foreach (self::message_list_head($attrib, $allcols) as $col => $cell) {
|
||
if (in_array($col, $listcols)) {
|
||
$table->add_header(['class' => $cell['className'], 'id' => $cell['id']], $cell['html']);
|
||
}
|
||
}
|
||
}
|
||
|
||
return $table->show();
|
||
}
|
||
|
||
/**
|
||
* Returns javascript commands to add rows to the message list
|
||
*
|
||
* @param array<rcube_message_header> $a_headers List of messages
|
||
*/
|
||
public static function js_message_list($a_headers, $insert_top = false, $a_show_cols = null)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (empty($a_show_cols)) {
|
||
if (!empty($_SESSION['list_attrib']['columns'])) {
|
||
$a_show_cols = $_SESSION['list_attrib']['columns'];
|
||
} else {
|
||
$list_cols = $rcmail->config->get('list_cols');
|
||
$a_show_cols = !empty($list_cols) && is_array($list_cols) ? $list_cols : ['subject'];
|
||
}
|
||
} else {
|
||
if (!is_array($a_show_cols)) {
|
||
$a_show_cols = preg_split('/[\s,;]+/', str_replace(["'", '"'], '', $a_show_cols));
|
||
}
|
||
$head_replace = true;
|
||
}
|
||
|
||
$delimiter = $rcmail->storage->get_hierarchy_delimiter();
|
||
$search_set = $rcmail->storage->get_search_set();
|
||
$multifolder = $search_set && !empty($search_set[1]->multi);
|
||
|
||
// add/remove 'folder' column to the list on multi-folder searches
|
||
if ($multifolder && !in_array('folder', $a_show_cols)) {
|
||
$a_show_cols[] = 'folder';
|
||
$head_replace = true;
|
||
} elseif (!$multifolder && ($found = array_search('folder', $a_show_cols)) !== false) {
|
||
unset($a_show_cols[$found]);
|
||
$head_replace = true;
|
||
}
|
||
|
||
$mbox = $rcmail->output->get_env('mailbox');
|
||
if (!is_string($mbox) || !strlen($mbox)) {
|
||
$mbox = $rcmail->storage->get_folder();
|
||
}
|
||
|
||
// make sure 'threads' and 'subject' columns are present
|
||
if (!in_array('subject', $a_show_cols)) {
|
||
array_unshift($a_show_cols, 'subject');
|
||
}
|
||
if (!in_array('threads', $a_show_cols)) {
|
||
array_unshift($a_show_cols, 'threads');
|
||
}
|
||
|
||
// Make sure there are no duplicated columns (#1486999)
|
||
$a_show_cols = array_unique($a_show_cols);
|
||
$_SESSION['list_attrib']['columns'] = $a_show_cols;
|
||
|
||
// Plugins may set header's list_cols/list_flags and other rcube_message_header variables
|
||
// and list columns
|
||
$plugin = $rcmail->plugins->exec_hook('messages_list', ['messages' => $a_headers, 'cols' => $a_show_cols]);
|
||
|
||
$a_show_cols = $plugin['cols'];
|
||
$a_headers = $plugin['messages'];
|
||
|
||
// make sure minimum required columns are present (needed for widescreen layout)
|
||
$allcols = array_merge($a_show_cols, ['threads', 'subject', 'fromto', 'date', 'size', 'flag', 'attachment']);
|
||
$allcols = array_unique($allcols);
|
||
|
||
$thead = !empty($head_replace) ? self::message_list_head($_SESSION['list_attrib'], $allcols) : null;
|
||
|
||
// get name of smart From/To column in folder context
|
||
$smart_col = self::message_list_smart_column_name();
|
||
$rcmail->output->command('set_message_coltypes', array_values($a_show_cols), $thead, $smart_col);
|
||
|
||
if ($multifolder && $_SESSION['search_scope'] == 'all') {
|
||
$rcmail->output->command('select_folder', '');
|
||
}
|
||
|
||
$rcmail->output->set_env('multifolder_listing', $multifolder);
|
||
|
||
if (empty($a_headers)) {
|
||
return;
|
||
}
|
||
|
||
// remove 'threads', 'attachment', 'flag', 'status' columns, we don't need them here
|
||
foreach (['threads', 'attachment', 'flag', 'status', 'priority'] as $col) {
|
||
if (($key = array_search($col, $allcols)) !== false) {
|
||
unset($allcols[$key]);
|
||
}
|
||
}
|
||
|
||
$sort_col = $_SESSION['sort_col'];
|
||
$roots = [];
|
||
|
||
// loop through message headers
|
||
foreach ($a_headers as $header) {
|
||
/** @var ?rcube_message_header $header */
|
||
if (empty($header) || empty($header->size)) {
|
||
continue;
|
||
}
|
||
|
||
// make message UIDs unique by appending the folder name
|
||
if ($multifolder) {
|
||
$header->uid .= '-' . $header->folder;
|
||
$header->flags['skip_mbox_check'] = true;
|
||
if (!empty($header->parent_uid)) {
|
||
$header->parent_uid .= '-' . $header->folder;
|
||
}
|
||
}
|
||
|
||
$a_msg_cols = [];
|
||
$a_msg_flags = [];
|
||
|
||
// format each col; similar as in self::message_list()
|
||
foreach ($allcols as $col) {
|
||
$col_name = $col == 'fromto' ? $smart_col : $col;
|
||
|
||
if (in_array($col_name, ['from', 'to', 'cc', 'replyto'])) {
|
||
$cont = self::address_string($header->{$col_name}, 3, false, null, $header->charset, null, false);
|
||
if (empty($cont)) {
|
||
$cont = ' '; // for widescreen mode
|
||
}
|
||
} elseif ($col == 'subject') {
|
||
$cont = trim(rcube_mime::decode_header($header->subject, $header->charset));
|
||
if (!$cont) {
|
||
$cont = $rcmail->gettext('nosubject');
|
||
}
|
||
$cont = rcube::SQ($cont);
|
||
} elseif ($col == 'size') {
|
||
$cont = self::show_bytes($header->size);
|
||
} elseif ($col == 'date') {
|
||
$cont = $rcmail->format_date($sort_col == 'arrival' ? $header->internaldate : $header->date);
|
||
} elseif ($col == 'folder') {
|
||
if (!isset($last_folder) || !isset($last_folder_name) || $last_folder !== $header->folder) {
|
||
$last_folder = $header->folder;
|
||
$last_folder_name = self::localize_foldername($last_folder, true);
|
||
$last_folder_name = str_replace($delimiter, " \xC2\xBB ", $last_folder_name);
|
||
}
|
||
|
||
$cont = rcube::SQ($last_folder_name);
|
||
} elseif (isset($header->{$col})) {
|
||
$cont = rcube::SQ($header->{$col});
|
||
} else {
|
||
$cont = '';
|
||
}
|
||
|
||
$a_msg_cols[$col] = $cont;
|
||
}
|
||
|
||
$a_msg_flags = array_change_key_case(array_map('intval', (array) $header->flags));
|
||
|
||
if (!empty($header->depth)) {
|
||
$a_msg_flags['depth'] = $header->depth;
|
||
} elseif (!empty($header->has_children)) {
|
||
$roots[] = $header->uid;
|
||
}
|
||
if (!empty($header->parent_uid)) {
|
||
$a_msg_flags['parent_uid'] = $header->parent_uid;
|
||
}
|
||
if (!empty($header->has_children)) {
|
||
$a_msg_flags['has_children'] = $header->has_children;
|
||
}
|
||
if (!empty($header->unread_children)) {
|
||
$a_msg_flags['unread_children'] = $header->unread_children;
|
||
}
|
||
if (!empty($header->flagged_children)) {
|
||
$a_msg_flags['flagged_children'] = $header->flagged_children;
|
||
}
|
||
if (!empty($header->others['list-post'])) {
|
||
$a_msg_flags['ml'] = 1;
|
||
}
|
||
if (!empty($header->priority)) {
|
||
$a_msg_flags['prio'] = (int) $header->priority;
|
||
}
|
||
|
||
$a_msg_flags['ctype'] = rcube::Q($header->ctype);
|
||
$a_msg_flags['mbox'] = $header->folder;
|
||
|
||
// Merge with plugin result
|
||
// @phpstan-ignore-next-line Deprecated, use $header->flags
|
||
if (!empty($header->list_flags) && is_array($header->list_flags)) {
|
||
// @phpstan-ignore-next-line
|
||
$a_msg_flags = array_merge($a_msg_flags, $header->list_flags);
|
||
}
|
||
if (!empty($header->list_cols)) {
|
||
$a_msg_cols = array_merge($a_msg_cols, $header->list_cols);
|
||
}
|
||
|
||
$rcmail->output->command('add_message_row', $header->uid, $a_msg_cols, $a_msg_flags, $insert_top);
|
||
}
|
||
|
||
if ($rcmail->storage->get_threading()) {
|
||
$rcmail->output->command('init_threads', $roots, $mbox);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Creates <THEAD> for message list table
|
||
*/
|
||
public static function message_list_head($attrib, $a_show_cols)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
// check to see if we have some settings for sorting
|
||
$sort_col = $_SESSION['sort_col'];
|
||
$sort_order = $_SESSION['sort_order'];
|
||
|
||
$dont_override = (array) $rcmail->config->get('dont_override');
|
||
$disabled_sort = in_array('message_sort_col', $dont_override);
|
||
$disabled_order = in_array('message_sort_order', $dont_override);
|
||
|
||
$rcmail->output->set_env('disabled_sort_col', $disabled_sort);
|
||
$rcmail->output->set_env('disabled_sort_order', $disabled_order);
|
||
|
||
// define sortable columns
|
||
if ($disabled_sort) {
|
||
$a_sort_cols = $sort_col && !$disabled_order ? [$sort_col] : [];
|
||
} else {
|
||
$a_sort_cols = ['subject', 'date', 'from', 'to', 'fromto', 'size', 'cc'];
|
||
}
|
||
|
||
if (!empty($attrib['optionsmenuicon'])) {
|
||
$params = [];
|
||
foreach ($attrib as $key => $val) {
|
||
if (preg_match('/^optionsmenu(.+)$/', $key, $matches)) {
|
||
$params[$matches[1]] = $val;
|
||
}
|
||
}
|
||
|
||
$list_menu = self::options_menu_link($params);
|
||
}
|
||
|
||
$cells = $coltypes = [];
|
||
|
||
// get name of smart From/To column in folder context
|
||
$smart_col = null;
|
||
if (array_search('fromto', $a_show_cols) !== false) {
|
||
$smart_col = self::message_list_smart_column_name();
|
||
}
|
||
|
||
foreach ($a_show_cols as $col) {
|
||
// sanity check
|
||
if (!preg_match('/^[a-zA-Z_-]+$/', $col)) {
|
||
continue;
|
||
}
|
||
|
||
$label = '';
|
||
$sortable = false;
|
||
$rel_col = $col == 'date' && $sort_col == 'arrival' ? 'arrival' : $col;
|
||
|
||
// get column name
|
||
switch ($col) {
|
||
case 'flag':
|
||
$col_name = html::span('flagged', $rcmail->gettext('flagged'));
|
||
break;
|
||
case 'attachment':
|
||
case 'priority':
|
||
$col_name = html::span($col, $rcmail->gettext($col));
|
||
break;
|
||
case 'status':
|
||
$col_name = html::span($col, $rcmail->gettext('readstatus'));
|
||
break;
|
||
case 'threads':
|
||
$col_name = !empty($list_menu) ? $list_menu : '';
|
||
break;
|
||
case 'fromto':
|
||
$label = $rcmail->gettext($smart_col);
|
||
$col_name = rcube::Q($label);
|
||
break;
|
||
default:
|
||
$label = $rcmail->gettext($col);
|
||
$col_name = rcube::Q($label);
|
||
}
|
||
|
||
// make sort links
|
||
if (in_array($col, $a_sort_cols)) {
|
||
$sortable = true;
|
||
$col_name = html::a([
|
||
'href' => './#sort',
|
||
'class' => 'sortcol',
|
||
'rel' => $rel_col,
|
||
'title' => $rcmail->gettext('sortby'),
|
||
], $col_name);
|
||
} elseif (empty($col_name) || $col_name[0] != '<') {
|
||
$col_name = '<span class="' . $col . '">' . $col_name . '</span>';
|
||
}
|
||
|
||
$sort_class = $rel_col == $sort_col && !$disabled_order ? " sorted{$sort_order}" : '';
|
||
$class_name = $col . $sort_class;
|
||
|
||
// put it all together
|
||
$cells[$col] = ['className' => $class_name, 'id' => "rcm{$col}", 'html' => $col_name];
|
||
$coltypes[$col] = ['className' => $class_name, 'id' => "rcm{$col}", 'label' => $label, 'sortable' => $sortable];
|
||
}
|
||
|
||
$rcmail->output->set_env('coltypes', $coltypes);
|
||
|
||
return $cells;
|
||
}
|
||
|
||
public static function options_menu_link($attrib = [])
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$title = $rcmail->gettext(!empty($attrib['label']) ? $attrib['label'] : 'listoptions');
|
||
$inner = $title;
|
||
$onclick = sprintf(
|
||
"return %s.command('menu-open', '%s', this, event)",
|
||
rcmail_output::JS_OBJECT_NAME,
|
||
!empty($attrib['ref']) ? $attrib['ref'] : 'messagelistmenu'
|
||
);
|
||
|
||
// Backwards compatibility, attribute renamed in v1.5
|
||
if (isset($attrib['optionsmenuicon'])) {
|
||
$attrib['icon'] = $attrib['optionsmenuicon'];
|
||
}
|
||
|
||
if (!empty($attrib['icon']) && $attrib['icon'] != 'true') {
|
||
$inner = html::img(['src' => $rcmail->output->asset_url($attrib['icon'], true), 'alt' => $title]);
|
||
} elseif (!empty($attrib['innerclass'])) {
|
||
$inner = html::span($attrib['innerclass'], $inner);
|
||
}
|
||
|
||
return html::a([
|
||
'href' => '#list-options',
|
||
'onclick' => $onclick,
|
||
'class' => $attrib['class'] ?? 'listmenu',
|
||
'id' => $attrib['id'] ?? 'listmenulink',
|
||
'title' => $title,
|
||
'tabindex' => '0',
|
||
], $inner
|
||
);
|
||
}
|
||
|
||
public static function messagecount_display($attrib)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (empty($attrib['id'])) {
|
||
$attrib['id'] = 'rcmcountdisplay';
|
||
}
|
||
|
||
$rcmail->output->add_gui_object('countdisplay', $attrib['id']);
|
||
|
||
$content = $rcmail->action != 'show' ? self::get_messagecount_text() : $rcmail->gettext('loading');
|
||
|
||
return html::span($attrib, $content);
|
||
}
|
||
|
||
public static function get_messagecount_text($count = null, $page = null)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if ($page === null) {
|
||
$page = $rcmail->storage->get_page();
|
||
}
|
||
|
||
$page_size = $rcmail->storage->get_pagesize();
|
||
$start_msg = ($page - 1) * $page_size + 1;
|
||
$max = $count;
|
||
|
||
if ($max === null && $rcmail->action) {
|
||
$max = $rcmail->storage->count(null, $rcmail->storage->get_threading() ? 'THREADS' : 'ALL');
|
||
}
|
||
|
||
if (!$max) {
|
||
$out = $rcmail->storage->get_search_set() ? $rcmail->gettext('nomessages') : $rcmail->gettext('mailboxempty');
|
||
} else {
|
||
$out = $rcmail->gettext([
|
||
'name' => $rcmail->storage->get_threading() ? 'threadsfromto' : 'messagesfromto',
|
||
'vars' => [
|
||
'from' => $start_msg,
|
||
'to' => min($max, $start_msg + $page_size - 1),
|
||
'count' => $max,
|
||
],
|
||
]);
|
||
}
|
||
|
||
return rcube::Q($out);
|
||
}
|
||
|
||
public static function mailbox_name_display($attrib)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (empty($attrib['id'])) {
|
||
$attrib['id'] = 'rcmmailboxname';
|
||
}
|
||
|
||
$rcmail->output->add_gui_object('mailboxname', $attrib['id']);
|
||
|
||
return html::span($attrib, self::get_mailbox_name_text());
|
||
}
|
||
|
||
public static function get_mailbox_name_text()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$mbox = $rcmail->output->get_env('mailbox');
|
||
|
||
if (!is_string($mbox) || !strlen($mbox)) {
|
||
$mbox = $rcmail->storage->get_folder();
|
||
}
|
||
|
||
return self::localize_foldername($mbox);
|
||
}
|
||
|
||
public static function send_unread_count($mbox_name, $force = false, $count = null, $mark = '')
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$old_unseen = self::get_unseen_count($mbox_name);
|
||
$unseen = $count;
|
||
|
||
if ($unseen === null) {
|
||
$unseen = $rcmail->storage->count($mbox_name, 'UNSEEN', $force);
|
||
}
|
||
|
||
if ($unseen !== $old_unseen || ($mbox_name == 'INBOX')) {
|
||
$rcmail->output->command('set_unread_count', $mbox_name, $unseen,
|
||
$mbox_name == 'INBOX', $unseen && $mark ? $mark : '');
|
||
}
|
||
|
||
self::set_unseen_count($mbox_name, $unseen);
|
||
|
||
return $unseen;
|
||
}
|
||
|
||
public static function set_unseen_count($mbox_name, $count)
|
||
{
|
||
// @TODO: this data is doubled (session and cache tables) if caching is enabled
|
||
|
||
// Make sure we have an array here (#1487066)
|
||
if (!isset($_SESSION['unseen_count']) || !is_array($_SESSION['unseen_count'])) {
|
||
$_SESSION['unseen_count'] = [];
|
||
}
|
||
|
||
$_SESSION['unseen_count'][$mbox_name] = $count;
|
||
}
|
||
|
||
public static function get_unseen_count($mbox_name)
|
||
{
|
||
if (!empty($_SESSION['unseen_count']) && array_key_exists($mbox_name, $_SESSION['unseen_count'])) {
|
||
return $_SESSION['unseen_count'][$mbox_name];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Sets message is_safe flag according to 'show_images' option value
|
||
*
|
||
* @param rcube_message $message Mail message object
|
||
*/
|
||
public static function check_safe($message)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (empty($message->is_safe)
|
||
&& ($show_images = $rcmail->config->get('show_images'))
|
||
&& $message->has_html_part()
|
||
) {
|
||
switch ($show_images) {
|
||
case 3: // trusted senders only
|
||
case 1: // all my contacts
|
||
if (!empty($message->sender['mailto'])) {
|
||
$type = rcube_addressbook::TYPE_TRUSTED_SENDER;
|
||
|
||
if ($show_images == 1) {
|
||
$type |= rcube_addressbook::TYPE_RECIPIENT | rcube_addressbook::TYPE_WRITEABLE;
|
||
}
|
||
|
||
if ($rcmail->contact_exists($message->sender['mailto'], $type)) {
|
||
$message->set_safe(true);
|
||
}
|
||
}
|
||
|
||
$rcmail->plugins->exec_hook('message_check_safe', ['message' => $message]);
|
||
break;
|
||
case 2: // always
|
||
$message->set_safe(true);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return !empty($message->is_safe);
|
||
}
|
||
|
||
/**
|
||
* Cleans up the given message HTML Body (for displaying)
|
||
*
|
||
* @param string $html HTML
|
||
* @param array $p Display parameters
|
||
* @param array $cid_replaces CID map replaces (inline images)
|
||
*
|
||
* @return string Clean HTML
|
||
*/
|
||
public static function wash_html($html, $p, $cid_replaces = [])
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
$p += ['safe' => false, 'inline_html' => true, 'css_prefix' => null, 'container_id' => null];
|
||
|
||
// charset was converted to UTF-8 in rcube_storage::get_message_part(),
|
||
// change/add charset specification in HTML accordingly,
|
||
// washtml's DOMDocument methods cannot work without that
|
||
$meta = '<meta charset="' . RCUBE_CHARSET . '" />';
|
||
|
||
// remove old meta tag and add the new one, making sure that it is placed in the head (#3510, #7116)
|
||
$html = preg_replace('/<meta[^>]+charset=[a-z0-9_"-]+[^>]*>/Ui', '', $html);
|
||
$html = preg_replace('/(<head[^>]*>)/Ui', '\1' . $meta, $html, -1, $rcount);
|
||
|
||
if (!$rcount) {
|
||
// Note: HTML without <html> tag may still be a valid input (#6713)
|
||
if (($pos = stripos($html, '<html')) === false) {
|
||
$html = '<html><head>' . $meta . '</head>' . $html;
|
||
} else {
|
||
$pos = strpos($html, '>', $pos);
|
||
$html = substr_replace($html, '<head>' . $meta . '</head>', $pos + 1, 0);
|
||
}
|
||
}
|
||
|
||
// clean HTML with washtml by Frederic Motte
|
||
$wash_opts = [
|
||
'show_washed' => false,
|
||
'add_comments' => $p['add_comments'] ?? true,
|
||
'allow_remote' => $p['safe'],
|
||
'blocked_src' => $rcmail->output->asset_url('program/resources/blocked.gif'),
|
||
'cid_map' => $cid_replaces,
|
||
'html_elements' => ['body'],
|
||
'ignore_elements' => $p['ignore_elements'] ?? [],
|
||
'css_prefix' => $p['css_prefix'],
|
||
// internal configuration
|
||
'container_id' => $p['container_id'],
|
||
'body_class' => $p['body_class'] ?? '',
|
||
];
|
||
|
||
if (empty($p['inline_html'])) {
|
||
$wash_opts['html_elements'] = ['html', 'head', 'title', 'body', 'link'];
|
||
}
|
||
if (!empty($p['safe'])) {
|
||
$wash_opts['html_attribs'] = ['rel', 'type'];
|
||
}
|
||
|
||
// overwrite washer options with options from plugins
|
||
if (isset($p['html_elements'])) {
|
||
$wash_opts['html_elements'] = $p['html_elements'];
|
||
}
|
||
if (isset($p['html_attribs'])) {
|
||
$wash_opts['html_attribs'] = $p['html_attribs'];
|
||
}
|
||
|
||
// initialize HTML washer
|
||
$washer = new rcube_washtml($wash_opts);
|
||
|
||
self::$wash_html_body_attrs = [];
|
||
|
||
if (!empty($p['inline_html'])) {
|
||
$washer->add_callback('body', 'rcmail_action_mail_index::washtml_callback');
|
||
|
||
if ($wash_opts['body_class']) {
|
||
self::$wash_html_body_attrs['class'] = $wash_opts['body_class'];
|
||
}
|
||
|
||
if ($wash_opts['container_id']) {
|
||
self::$wash_html_body_attrs['id'] = $wash_opts['container_id'];
|
||
}
|
||
}
|
||
|
||
if (empty($p['skip_washer_form_callback'])) {
|
||
$washer->add_callback('form', 'rcmail_action_mail_index::washtml_callback');
|
||
}
|
||
|
||
// allow CSS styles, will be sanitized by self::washtml_callback()
|
||
if (empty($p['skip_washer_style_callback'])) {
|
||
$washer->add_callback('style', 'rcmail_action_mail_index::washtml_callback');
|
||
}
|
||
|
||
// modify HTML links to open a new window if clicked
|
||
if (empty($p['skip_washer_link_callback'])) {
|
||
$washer->add_callback('a', 'rcmail_action_mail_index::washtml_link_callback');
|
||
$washer->add_callback('area', 'rcmail_action_mail_index::washtml_link_callback');
|
||
$washer->add_callback('link', 'rcmail_action_mail_index::washtml_link_callback');
|
||
}
|
||
|
||
// Remove non-UTF8 characters (#1487813)
|
||
$html = rcube_charset::clean($html);
|
||
|
||
$html = $washer->wash($html);
|
||
self::$REMOTE_OBJECTS = $washer->extlinks;
|
||
|
||
// There was no <body>, but a wrapper element is required
|
||
if (!empty($p['inline_html']) && !empty(self::$wash_html_body_attrs)) {
|
||
$html = html::tag('div', self::$wash_html_body_attrs, $html);
|
||
}
|
||
|
||
return $html;
|
||
}
|
||
|
||
/**
|
||
* Convert the given message part to proper HTML
|
||
* which can be displayed the message view
|
||
*
|
||
* @param string $body Message part body
|
||
* @param rcube_message_part $part Message part
|
||
* @param array $p Display parameters array
|
||
*
|
||
* @return string Formatted HTML string
|
||
*/
|
||
public static function print_body($body, $part, $p = [])
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
// trigger plugin hook
|
||
$data = $rcmail->plugins->exec_hook('message_part_before',
|
||
[
|
||
'type' => $part->ctype_secondary,
|
||
'body' => $body,
|
||
'id' => $part->mime_id,
|
||
] + $p + [
|
||
'safe' => false,
|
||
'plain' => false,
|
||
'inline_html' => true,
|
||
]
|
||
);
|
||
|
||
// convert html to text/plain
|
||
if ($data['plain'] && ($data['type'] == 'html' || $data['type'] == 'enriched')) {
|
||
if ($data['type'] == 'enriched') {
|
||
$data['body'] = rcube_enriched::to_html($data['body']);
|
||
}
|
||
|
||
$body = $rcmail->html2text($data['body']);
|
||
$part->ctype_secondary = 'plain';
|
||
}
|
||
// text/html
|
||
elseif ($data['type'] == 'html') {
|
||
$body = self::wash_html($data['body'], $data, $part->replaces);
|
||
$part->ctype_secondary = $data['type'];
|
||
}
|
||
// text/enriched
|
||
elseif ($data['type'] == 'enriched') {
|
||
$body = rcube_enriched::to_html($data['body']);
|
||
$body = self::wash_html($body, $data, $part->replaces);
|
||
$part->ctype_secondary = 'html';
|
||
}
|
||
// markdown
|
||
elseif ($data['type'] === 'markdown' || $data['type'] === 'x-markdown') {
|
||
$body = rcube_markdown::to_html($data['body']);
|
||
$body = self::wash_html($body, $data, $part->replaces);
|
||
$part->ctype_secondary = 'html';
|
||
} else {
|
||
// assert plaintext
|
||
$body = $data['body'];
|
||
$part->ctype_secondary = $data['type'] = 'plain';
|
||
}
|
||
|
||
// free some memory (hopefully)
|
||
unset($data['body']);
|
||
|
||
// plaintext postprocessing
|
||
if ($part->ctype_secondary == 'plain') {
|
||
$flowed = isset($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed';
|
||
$delsp = isset($part->ctype_parameters['delsp']) && $part->ctype_parameters['delsp'] == 'yes';
|
||
$body = self::plain_body($body, $flowed, $delsp);
|
||
}
|
||
|
||
// allow post-processing of the message body
|
||
$data = $rcmail->plugins->exec_hook('message_part_after', [
|
||
'type' => $part->ctype_secondary,
|
||
'body' => $body,
|
||
'id' => $part->mime_id,
|
||
] + $data);
|
||
|
||
return $data['body'];
|
||
}
|
||
|
||
/**
|
||
* Handle links and citation marks in plain text message
|
||
*
|
||
* @param string $body Plain text string
|
||
* @param bool $flowed Set to True if the source text is in format=flowed
|
||
* @param bool $delsp Enable 'delsp' option of format=flowed text
|
||
*
|
||
* @return string Formatted HTML string
|
||
*/
|
||
public static function plain_body($body, $flowed = false, $delsp = false)
|
||
{
|
||
$options = [
|
||
'flowed' => $flowed,
|
||
'replacer' => 'rcmail_string_replacer',
|
||
'delsp' => $delsp,
|
||
];
|
||
|
||
$text2html = new rcube_text2html($body, false, $options);
|
||
$body = $text2html->get_html();
|
||
|
||
return $body;
|
||
}
|
||
|
||
/**
|
||
* Callback function for washtml cleaning class
|
||
*/
|
||
public static function washtml_callback($tagname, $attrib, $content, $washtml)
|
||
{
|
||
$out = '';
|
||
|
||
switch ($tagname) {
|
||
case 'form':
|
||
$out = html::div('form', $content);
|
||
break;
|
||
case 'style':
|
||
// Crazy big styles may freeze the browser (#1490539)
|
||
// remove content with more than 5k lines
|
||
if (substr_count($content, "\n") > 5000) {
|
||
break;
|
||
}
|
||
|
||
// decode all escaped entities and reduce to ascii strings
|
||
$decoded = rcube_utils::xss_entity_decode($content);
|
||
$stripped = preg_replace('/[^a-zA-Z\(:;]/', '', $decoded);
|
||
|
||
// now check for evil strings like expression, behavior or url()
|
||
if (!preg_match('/expression|behavior|javascript:|import[^a]/i', $stripped)) {
|
||
if (!$washtml->get_config('allow_remote') && preg_match('/url\((?!data:image)/', $stripped)) {
|
||
$washtml->extlinks = true;
|
||
} else {
|
||
$out = $decoded;
|
||
}
|
||
}
|
||
|
||
if (strlen($out)) {
|
||
$css_prefix = $washtml->get_config('css_prefix');
|
||
$is_safe = $washtml->get_config('allow_remote');
|
||
$body_class = $washtml->get_config('body_class') ?: '';
|
||
$cont_id = $washtml->get_config('container_id') ?: '';
|
||
$cont_id = trim($cont_id . ($body_class ? " div.{$body_class}" : ''));
|
||
|
||
$out = rcube_utils::mod_css_styles($out, $cont_id, $is_safe, $css_prefix);
|
||
|
||
$out = html::tag('style', ['type' => 'text/css'], $out);
|
||
}
|
||
|
||
break;
|
||
case 'body':
|
||
$style = [];
|
||
$attrs = self::$wash_html_body_attrs;
|
||
|
||
foreach (html::parse_attrib_string($attrib) as $attr_name => $value) {
|
||
switch (strtolower($attr_name)) {
|
||
case 'bgcolor':
|
||
// Get bgcolor, we'll set it as background-color of the message container
|
||
if (preg_match('/^([a-z0-9#]+)$/i', $value, $m)) {
|
||
$style['background-color'] = $value;
|
||
}
|
||
break;
|
||
case 'text':
|
||
// Get text color, we'll set it as font color of the message container
|
||
if (preg_match('/^([a-z0-9#]+)$/i', $value, $m)) {
|
||
$style['color'] = $value;
|
||
}
|
||
break;
|
||
case 'background':
|
||
// Get background, we'll set it as background-image of the message container
|
||
if (preg_match('/^([^\s]+)$/', $value, $m)) {
|
||
$style['background-image'] = "url({$value})";
|
||
}
|
||
break;
|
||
default:
|
||
$attrs[$attr_name] = $value;
|
||
}
|
||
}
|
||
|
||
if (!empty($style)) {
|
||
foreach ($style as $idx => $val) {
|
||
$style[$idx] = $idx . ': ' . $val;
|
||
}
|
||
|
||
if (isset($attrs['style'])) {
|
||
$attrs['style'] = trim($attrs['style'], '; ') . '; ' . implode('; ', $style);
|
||
} else {
|
||
$attrs['style'] = implode('; ', $style);
|
||
}
|
||
}
|
||
|
||
$out = html::tag('div', $attrs, $content);
|
||
self::$wash_html_body_attrs = [];
|
||
break;
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* Detect if a message attachment is an image (that can be displayed in the browser).
|
||
*
|
||
* @param rcube_message_part $part Message part - attachment
|
||
*
|
||
* @return string|null Image MIME type
|
||
*/
|
||
public static function part_image_type($part)
|
||
{
|
||
$mimetype = strtolower($part->mimetype);
|
||
|
||
// Skip TIFF/WEBP images if browser doesn't support this format
|
||
// ...until we can convert them to JPEG
|
||
$tiff_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['tiff']);
|
||
$tiff_support = $tiff_support || rcube_image::is_convertable('image/tiff');
|
||
$webp_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps']['webp']);
|
||
$webp_support = $webp_support || rcube_image::is_convertable('image/webp');
|
||
|
||
if ((!$tiff_support && $mimetype == 'image/tiff') || (!$webp_support && $mimetype == 'image/webp')) {
|
||
return null;
|
||
}
|
||
|
||
// Content-Type: image/*...
|
||
if (str_starts_with($mimetype, 'image/')) {
|
||
return $mimetype;
|
||
}
|
||
|
||
// Many clients use application/octet-stream, we'll detect mimetype
|
||
// by checking filename extension
|
||
|
||
// Supported image filename extensions to image type map
|
||
$types = [
|
||
'jpg' => 'image/jpeg',
|
||
'jpeg' => 'image/jpeg',
|
||
'png' => 'image/png',
|
||
'gif' => 'image/gif',
|
||
'bmp' => 'image/bmp',
|
||
];
|
||
|
||
if ($tiff_support) {
|
||
$types['tif'] = 'image/tiff';
|
||
$types['tiff'] = 'image/tiff';
|
||
}
|
||
|
||
if ($webp_support) {
|
||
$types['webp'] = 'image/webp';
|
||
}
|
||
|
||
if ($part->filename
|
||
&& $mimetype == 'application/octet-stream'
|
||
&& preg_match('/\.([^.]+)$/i', $part->filename, $m)
|
||
&& ($extension = strtolower($m[1]))
|
||
&& isset($types[$extension])
|
||
) {
|
||
return $types[$extension];
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* Parse link (a, link, area) attributes and set correct target
|
||
*/
|
||
public static function washtml_link_callback($tag, $attribs, $content, $washtml)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
$attrib = html::parse_attrib_string($attribs);
|
||
|
||
// Remove non-printable characters in URL (#1487805)
|
||
if (isset($attrib['href'])) {
|
||
$attrib['href'] = preg_replace('/[\x00-\x1F]/', '', $attrib['href']);
|
||
|
||
if ($tag == 'link' && preg_match('/^https?:\/\//i', $attrib['href'])) {
|
||
$tempurl = 'tmp-' . md5($attrib['href']) . '.css';
|
||
$_SESSION['modcssurls'][$tempurl] = $attrib['href'];
|
||
$attrib['href'] = $rcmail->url([
|
||
'task' => 'utils',
|
||
'action' => 'modcss',
|
||
'u' => $tempurl,
|
||
'c' => $washtml->get_config('container_id'),
|
||
'p' => $washtml->get_config('css_prefix'),
|
||
]);
|
||
$content = null;
|
||
} elseif (preg_match('/^mailto:(.+)/i', $attrib['href'], $mailto)) {
|
||
$url_parts = explode('?', html_entity_decode($mailto[1], \ENT_QUOTES, 'UTF-8'), 2);
|
||
$mailto = $url_parts[0];
|
||
$url = $url_parts[1] ?? '';
|
||
|
||
// #6020: use raw encoding for correct "+" character handling as specified in RFC6068
|
||
$url = rawurldecode($url);
|
||
$mailto = rawurldecode($mailto);
|
||
$addresses = rcube_mime::decode_address_list($mailto, null, true);
|
||
$mailto = [];
|
||
|
||
// do sanity checks on recipients
|
||
foreach ($addresses as $idx => $addr) {
|
||
if (rcube_utils::check_email($addr['mailto'], false)) {
|
||
$addresses[$idx] = $addr['mailto'];
|
||
$mailto[] = $addr['string'];
|
||
} else {
|
||
unset($addresses[$idx]);
|
||
}
|
||
}
|
||
|
||
if (!empty($addresses)) {
|
||
$attrib['href'] = 'mailto:' . implode(',', $addresses);
|
||
$attrib['onclick'] = sprintf(
|
||
"return %s.command('compose','%s',this)",
|
||
rcmail_output::JS_OBJECT_NAME,
|
||
rcube::JQ(implode(',', $mailto) . ($url ? "?{$url}" : '')));
|
||
} else {
|
||
$attrib['href'] = '#NOP';
|
||
$attrib['onclick'] = '';
|
||
}
|
||
} elseif (!empty($attrib['href']) && $attrib['href'][0] != '#') {
|
||
$attrib['target'] = '_blank';
|
||
}
|
||
|
||
// Better security by adding rel="noreferrer" (#1484686)
|
||
if (($tag == 'a' || $tag == 'area') && $attrib['href'] && $attrib['href'][0] != '#') {
|
||
$attrib['rel'] = 'noreferrer';
|
||
}
|
||
}
|
||
|
||
// allowed attributes for a|link|area tags
|
||
$allow = ['href', 'name', 'target', 'onclick', 'id', 'class', 'style', 'title',
|
||
'rel', 'type', 'media', 'alt', 'coords', 'nohref', 'hreflang', 'shape'];
|
||
|
||
return html::tag($tag, $attrib, $content, $allow);
|
||
}
|
||
|
||
/**
|
||
* Decode address string and re-format it as HTML links
|
||
*/
|
||
public static function address_string($input, $max = null, $linked = false, $addicon = null,
|
||
$default_charset = null, $title = null, $spoofcheck = true)
|
||
{
|
||
$a_parts = rcube_mime::decode_address_list($input, null, true, $default_charset);
|
||
|
||
if (!count($a_parts)) {
|
||
return null;
|
||
}
|
||
|
||
$rcmail = rcmail::get_instance();
|
||
$c = count($a_parts);
|
||
$j = 0;
|
||
$out = '';
|
||
$allvalues = [];
|
||
$shown_addresses = [];
|
||
$show_email = $rcmail->config->get('message_show_email');
|
||
|
||
if ($addicon && !isset($_SESSION['writeable_abook'])) {
|
||
$_SESSION['writeable_abook'] = $rcmail->get_address_sources(true) ? true : false;
|
||
}
|
||
|
||
foreach ($a_parts as $part) {
|
||
$j++;
|
||
|
||
$name = $part['name'];
|
||
$mailto = $part['mailto'];
|
||
$string = $part['string'];
|
||
$valid = rcube_utils::check_email($mailto, false);
|
||
|
||
// IDNA ASCII to Unicode
|
||
if ($name == $mailto) {
|
||
$name = rcube_utils::idn_to_utf8($name);
|
||
}
|
||
if ($string == $mailto) {
|
||
$string = rcube_utils::idn_to_utf8($string);
|
||
}
|
||
$mailto = rcube_utils::idn_to_utf8($mailto);
|
||
|
||
// Homograph attack detection (#6891),
|
||
// and phishing email prevention (#1488981) (e.g. "valid@email.addr <phishing@email.addr>")
|
||
$show_fraud_warning = $spoofcheck
|
||
&& !self::$PRINT_MODE // Don't show the warning in print mode, because there's no danger of interaction.
|
||
&& (rcube_spoofchecker::check($mailto) || ($name && $name != $mailto && preg_match('/@|@|﹫/', $name)));
|
||
|
||
if (self::$PRINT_MODE) {
|
||
$address = '<' . rcube::Q($mailto) . '>';
|
||
if ($name) {
|
||
$address = rcube::SQ($name) . ' ' . $address;
|
||
}
|
||
} elseif ($valid) {
|
||
if ($linked) {
|
||
$attrs = [
|
||
'href' => 'mailto:' . $mailto,
|
||
'class' => 'rcmContactAddress',
|
||
'onclick' => sprintf("return %s.command('compose','%s',this)",
|
||
rcmail_output::JS_OBJECT_NAME, rcube::JQ(format_email_recipient($mailto, $name))),
|
||
];
|
||
|
||
// In case of a fraud warning always show all details, regardless of the config.
|
||
if ($show_fraud_warning || ($show_email && $name && $mailto)) {
|
||
$content = rcube::SQ(sprintf('%s <%s>', $name, $mailto));
|
||
} else {
|
||
$content = rcube::SQ($name ?: $mailto);
|
||
$attrs['title'] = $mailto;
|
||
}
|
||
|
||
$address = html::a($attrs, $content);
|
||
} else {
|
||
$address = html::span(['title' => $mailto, 'class' => 'rcmContactAddress'],
|
||
rcube::SQ($name ?: $mailto));
|
||
}
|
||
|
||
if ($addicon && $_SESSION['writeable_abook']) {
|
||
$label = $rcmail->gettext('addtoaddressbook');
|
||
$icon = html::img([
|
||
'src' => $rcmail->output->asset_url($addicon, true),
|
||
'alt' => $label,
|
||
'class' => 'noselect',
|
||
]);
|
||
$address .= html::a([
|
||
'href' => '#add',
|
||
'title' => $label,
|
||
'class' => 'rcmaddcontact',
|
||
'onclick' => sprintf("return %s.command('add-contact','%s',this)",
|
||
rcmail_output::JS_OBJECT_NAME, rcube::JQ($string)),
|
||
],
|
||
$addicon == 'virtual' ? '' : $icon
|
||
);
|
||
}
|
||
} else {
|
||
$address = $name ? rcube::Q($name) : '';
|
||
if ($mailto) {
|
||
$address = trim($address . ' ' . rcube::Q($name ? sprintf('<%s>', $mailto) : $mailto));
|
||
}
|
||
}
|
||
|
||
if ($show_fraud_warning) {
|
||
// $content = rcube::SQ(sprintf('%s <%s>', $name, $mailto));
|
||
$msg = $rcmail->gettext('suspiciousemail');
|
||
$prefix = html::span([
|
||
'class' => 'suspicious-address-warning',
|
||
'title' => $msg,
|
||
'role' => 'img',
|
||
'aria-label' => $msg,
|
||
], '');
|
||
$address = $prefix . $address;
|
||
}
|
||
|
||
$address = html::span('adr', $address);
|
||
$allvalues[] = $address;
|
||
|
||
if (empty($moreadrs)) {
|
||
$out .= ($out ? ', ' : '') . $address;
|
||
$shown_addresses[] = $address;
|
||
}
|
||
|
||
if ($max && $j == $max && $c > $j) {
|
||
if ($linked) {
|
||
$moreadrs = $c - $j;
|
||
} else {
|
||
$out .= '...';
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($moreadrs)) {
|
||
$label = rcube::Q($rcmail->gettext(['name' => 'andnmore', 'vars' => ['nr' => $moreadrs]]));
|
||
|
||
if (self::$PRINT_MODE) {
|
||
$out .= ', ' . html::a([
|
||
'href' => '#more',
|
||
'class' => 'morelink',
|
||
'onclick' => '$(this).hide().next().show()',
|
||
], $label)
|
||
. html::span(['style' => 'display:none'], implode(', ', array_diff($allvalues, $shown_addresses)));
|
||
} else {
|
||
$out .= ', ' . html::a([
|
||
'href' => '#more',
|
||
'class' => 'morelink',
|
||
'onclick' => sprintf("return %s.simple_dialog('%s','%s',null,{cancel_button:'close'})",
|
||
rcmail_output::JS_OBJECT_NAME,
|
||
rcube::JQ(implode(', ', $allvalues)),
|
||
rcube::JQ($title)),
|
||
], $label);
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* Return attachment filename, handle empty filename case
|
||
*
|
||
* @param rcube_message_part $attachment Message part
|
||
* @param bool $display Convert to a description text for "special" types
|
||
*
|
||
* @return string Filename
|
||
*/
|
||
public static function attachment_name($attachment, $display = false)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
$filename = (string) $attachment->filename;
|
||
$filename = str_replace(["\r", "\n"], '', $filename);
|
||
|
||
if ($filename === '') {
|
||
if ($attachment->mimetype == 'text/html') {
|
||
$filename = $rcmail->gettext('htmlmessage');
|
||
} else {
|
||
$ext = array_first((array) rcube_mime::get_mime_extensions($attachment->mimetype));
|
||
$filename = $rcmail->gettext('messagepart') . ' ' . $attachment->mime_id;
|
||
if ($ext) {
|
||
$filename .= '.' . $ext;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Display smart names for some known mimetypes
|
||
if ($display) {
|
||
if (preg_match('/application\/(pgp|pkcs7)-signature/i', $attachment->mimetype)) {
|
||
$filename = $rcmail->gettext('digitalsig');
|
||
}
|
||
}
|
||
|
||
return $filename;
|
||
}
|
||
|
||
public static function search_filter($attrib)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (empty($attrib['id'])) {
|
||
$attrib['id'] = 'rcmlistfilter';
|
||
}
|
||
|
||
if (!self::get_bool_attr($attrib, 'noevent')) {
|
||
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . '.filter_mailbox(this.value)';
|
||
}
|
||
|
||
// Content-Type values of messages with attachments
|
||
// the same as in app.js:add_message_row()
|
||
$ctypes = ['application/', 'multipart/mixed', 'multipart/signed', 'multipart/report'];
|
||
|
||
// Build search string of "with attachment" filter
|
||
$attachment = trim(str_repeat(' OR', count($ctypes) - 1));
|
||
foreach ($ctypes as $type) {
|
||
$attachment .= ' HEADER Content-Type ' . rcube_imap_generic::escape($type);
|
||
}
|
||
|
||
$select = new html_select($attrib);
|
||
$select->add($rcmail->gettext('all'), 'ALL');
|
||
$select->add($rcmail->gettext('unread'), 'UNSEEN');
|
||
$select->add($rcmail->gettext('flagged'), 'FLAGGED');
|
||
$select->add($rcmail->gettext('unanswered'), 'UNANSWERED');
|
||
if (!$rcmail->config->get('skip_deleted')) {
|
||
$select->add($rcmail->gettext('deleted'), 'DELETED');
|
||
$select->add($rcmail->gettext('undeleted'), 'UNDELETED');
|
||
}
|
||
$select->add($rcmail->gettext('withattachment'), $attachment);
|
||
$select->add($rcmail->gettext('priority') . ': ' . $rcmail->gettext('highest'), 'HEADER X-PRIORITY 1');
|
||
$select->add($rcmail->gettext('priority') . ': ' . $rcmail->gettext('high'), 'HEADER X-PRIORITY 2');
|
||
$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');
|
||
$select->add($rcmail->gettext('priority') . ': ' . $rcmail->gettext('low'), 'HEADER X-PRIORITY 4');
|
||
$select->add($rcmail->gettext('priority') . ': ' . $rcmail->gettext('lowest'), 'HEADER X-PRIORITY 5');
|
||
|
||
$rcmail->output->add_gui_object('search_filter', $attrib['id']);
|
||
|
||
$selected = rcube_utils::get_input_string('_filter', rcube_utils::INPUT_GET);
|
||
|
||
if (!$selected && !empty($_REQUEST['_search'])) {
|
||
$selected = $_SESSION['search_filter'];
|
||
}
|
||
|
||
return $select->show($selected ?: 'ALL');
|
||
}
|
||
|
||
public static function search_interval($attrib)
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
if (empty($attrib['id'])) {
|
||
$attrib['id'] = 'rcmsearchinterval';
|
||
}
|
||
|
||
$select = new html_select($attrib);
|
||
$select->add('', '');
|
||
|
||
foreach (['1W', '1M', '1Y', '-1W', '-1M', '-1Y'] as $value) {
|
||
$select->add($rcmail->gettext('searchinterval' . $value), $value);
|
||
}
|
||
|
||
$rcmail->output->add_gui_object('search_interval', $attrib['id']);
|
||
|
||
return $select->show(!empty($_REQUEST['_search']) ? $_SESSION['search_interval'] : '');
|
||
}
|
||
|
||
public static function message_error()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
// ... display message error page
|
||
if ($rcmail->output->template_exists('messageerror')) {
|
||
// Set env variables for messageerror.html template
|
||
if ($rcmail->action == 'show') {
|
||
$mbox_name = $rcmail->storage->get_folder();
|
||
|
||
$rcmail->output->set_env('mailbox', $mbox_name);
|
||
$rcmail->output->set_env('uid', null);
|
||
}
|
||
|
||
$rcmail->output->show_message('messageopenerror', 'error');
|
||
$rcmail->output->send('messageerror');
|
||
} else {
|
||
$rcmail->raise_error(410, false, true);
|
||
}
|
||
}
|
||
|
||
public static function message_import_form($attrib = [])
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
$rcmail->output->add_label('selectimportfile', 'importwait', 'importmessages', 'import');
|
||
|
||
$description = $rcmail->gettext('mailimportdesc');
|
||
$input_attr = [
|
||
'multiple' => true,
|
||
'name' => '_file[]',
|
||
'accept' => '.eml,.mbox,.msg,message/rfc822,text/*',
|
||
];
|
||
|
||
if (class_exists('ZipArchive', false)) {
|
||
$input_attr['accept'] .= '.zip,application/zip,application/x-zip';
|
||
$description .= ' ' . $rcmail->gettext('mailimportzip');
|
||
}
|
||
|
||
$attrib['prefix'] = html::tag('input', ['type' => 'hidden', 'name' => '_unlock', 'value' => ''])
|
||
. html::tag('input', ['type' => 'hidden', 'name' => '_framed', 'value' => '1'])
|
||
. html::p(null, $description);
|
||
|
||
return self::upload_form($attrib, 'importform', 'import-messages', $input_attr);
|
||
}
|
||
|
||
// Return mimetypes supported by the browser
|
||
public static function supported_mimetypes()
|
||
{
|
||
$rcmail = rcmail::get_instance();
|
||
|
||
// mimetypes supported by the browser (default settings)
|
||
$mimetypes = (array) $rcmail->config->get('client_mimetypes');
|
||
|
||
// Remove unsupported types, which makes that attachment which cannot be
|
||
// displayed in a browser will be downloaded directly without displaying an overlay page
|
||
if (empty($_SESSION['browser_caps']['pdf']) && ($key = array_search('application/pdf', $mimetypes)) !== false) {
|
||
unset($mimetypes[$key]);
|
||
}
|
||
|
||
if (empty($_SESSION['browser_caps']['flash']) && ($key = array_search('application/x-shockwave-flash', $mimetypes)) !== false) {
|
||
unset($mimetypes[$key]);
|
||
}
|
||
|
||
// We cannot securely preview XML files as we do not have a proper parser
|
||
if (($key = array_search('text/xml', $mimetypes)) !== false) {
|
||
unset($mimetypes[$key]);
|
||
}
|
||
|
||
foreach (['tiff', 'webp'] as $type) {
|
||
if (empty($_SESSION['browser_caps'][$type]) && ($key = array_search('image/' . $type, $mimetypes)) !== false) {
|
||
// can we convert it to jpeg?
|
||
if (!rcube_image::is_convertable('image/' . $type)) {
|
||
unset($mimetypes[$key]);
|
||
}
|
||
}
|
||
}
|
||
|
||
// @TODO: support mail preview for compose attachments
|
||
if ($rcmail->action != 'compose' && !in_array('message/rfc822', $mimetypes)) {
|
||
$mimetypes[] = 'message/rfc822';
|
||
}
|
||
|
||
return array_values($mimetypes);
|
||
}
|
||
}
|