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 = '&nbsp;'; // 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            '/<\?/' => '&lt;?',
1202            '/\?>/' => '?&gt;',
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 = '&lt;' . rcube::Q($mailto) . '&gt;';
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