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 |   Common code for generating and saving/sending mail message          |
15 |   with support for common user interface elements                     |
16 +-----------------------------------------------------------------------+
17 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18 | Author: Aleksander Machniak <alec@alec.pl>                            |
19 +-----------------------------------------------------------------------+
20*/
21
22/**
23 * Common code for generating and saving/sending mail message
24 * with support for common user interface elements.
25 *
26 * @package Webmail
27 */
28class rcmail_sendmail
29{
30    public $data    = [];
31    public $options = [];
32
33    protected $parse_data = [];
34    protected $message_form;
35    protected $rcmail;
36
37    // define constants for message compose mode
38    const MODE_NONE    = 'none';
39    const MODE_REPLY   = 'reply';
40    const MODE_FORWARD = 'forward';
41    const MODE_DRAFT   = 'draft';
42    const MODE_EDIT    = 'edit';
43
44
45    /**
46     * Object constructor
47     *
48     * @param array $data    Compose data
49     * @param array $options Operation options:
50     *    savedraft (bool) - Enable save-draft mode
51     *    sendmail (bool) - Enable send-mail mode
52     *    saveonly (bool) - Enable save-only mode
53     *    message (object) - Message object to get some data from
54     *    error_handler (callback) - Error handler
55     */
56    public function __construct($data = [], $options = [])
57    {
58        $this->rcmail  = rcube::get_instance();
59        $this->data    = (array) $data;
60        $this->options = (array) $options;
61
62        $this->options['sendmail_delay'] = (int) $this->rcmail->config->get('sendmail_delay');
63
64        if (empty($options['error_handler'])) {
65            $this->options['error_handler'] = function() { return false; };
66        }
67
68        if (empty($this->data['mode'])) {
69            $this->data['mode'] = self::MODE_NONE;
70        }
71
72        if (!empty($this->options['message'])) {
73            $this->compose_init($this->options['message']);
74        }
75    }
76
77    /**
78     * Collect input data for message headers
79     *
80     * @return array Message headers
81     */
82    public function headers_input()
83    {
84        if (!empty($this->options['sendmail']) && $this->options['sendmail_delay']) {
85            $last_time = $this->rcmail->config->get('last_message_time');
86            $wait_sec  = time() - $this->options['sendmail_delay'] - intval($last_time);
87
88            if ($wait_sec < 0) {
89                return $this->options['error_handler']('senttooquickly', 'error', ['sec' => $wait_sec * -1]);
90            }
91        }
92
93        // set default charset
94        if (empty($this->options['charset'])) {
95            $charset = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST) ?: $this->rcmail->output->get_charset();
96            $this->options['charset'] = $charset;
97        }
98
99        $charset = $this->options['charset'];
100
101        $this->parse_data = [];
102
103        $mailto  = $this->email_input_format(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true, $charset), true);
104        $mailcc  = $this->email_input_format(rcube_utils::get_input_value('_cc', rcube_utils::INPUT_POST, true, $charset), true);
105        $mailbcc = $this->email_input_format(rcube_utils::get_input_value('_bcc', rcube_utils::INPUT_POST, true, $charset), true);
106
107        if (!empty($this->parse_data['INVALID_EMAIL']) && empty($this->options['savedraft'])) {
108            return $this->options['error_handler']('emailformaterror', 'error', ['email' => $this->parse_data['INVALID_EMAIL']]);
109        }
110
111        if (($max_recipients = (int) $this->rcmail->config->get('max_recipients')) > 0) {
112            if ($this->parse_data['RECIPIENT_COUNT'] > $max_recipients) {
113                return $this->options['error_handler']('toomanyrecipients', 'error', ['max' => $max_recipients]);
114            }
115        }
116
117        if (empty($mailto) && !empty($mailcc)) {
118            $mailto = $mailcc;
119            $mailcc = null;
120        }
121        else if (empty($mailto)) {
122            $mailto = 'undisclosed-recipients:;';
123        }
124
125        $dont_override = (array) $this->rcmail->config->get('dont_override');
126        $mdn_enabled   = in_array('mdn_default', $dont_override) ? $this->rcmail->config->get('mdn_default') : !empty($_POST['_mdn']);
127        $dsn_enabled   = in_array('dsn_default', $dont_override) ? $this->rcmail->config->get('dsn_default') : !empty($_POST['_dsn']);
128        $subject       = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, true, $charset);
129        $from          = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST, true, $charset);
130        $replyto       = rcube_utils::get_input_value('_replyto', rcube_utils::INPUT_POST, true, $charset);
131        $followupto    = rcube_utils::get_input_value('_followupto', rcube_utils::INPUT_POST, true, $charset);
132        $from_string   = '';
133
134        // Get sender name and address from identity...
135        if (is_numeric($from)) {
136            if (is_array($identity_arr = $this->get_identity($from))) {
137                if ($identity_arr['mailto']) {
138                    $from = $identity_arr['mailto'];
139                }
140                if ($identity_arr['string']) {
141                    $from_string = $identity_arr['string'];
142                }
143            }
144            else {
145                $from = null;
146            }
147        }
148        else {
149            // ... if there is no identity record, this might be a custom from
150            $from_addresses = rcube_mime::decode_address_list($from);
151
152            if (count($from_addresses) == 1) {
153                $from        = $from_addresses[1]['mailto'];
154                $from_string = $from_addresses[1]['string'];
155            }
156            // ... otherwise it's empty or invalid
157            else {
158                $from = null;
159            }
160        }
161
162        // check 'From' address (identity may be incomplete)
163        if (empty($this->options['savedraft']) && empty($this->options['saveonly']) && empty($from)) {
164            return $this->options['error_handler']('nofromaddress', 'error');
165        }
166
167        if (!$from_string && $from) {
168            $from_string = $from;
169        }
170
171        $from_string = rcube_charset::convert($from_string, RCUBE_CHARSET, $charset);
172
173        if (!empty($this->data['param']['message-id'])) {
174            $message_id  = $this->data['param']['message-id'];
175        }
176        else {
177            $message_id = $this->rcmail->gen_message_id($from);
178        }
179
180        $this->options['dsn_enabled'] = $dsn_enabled;
181        $this->options['from']        = $from;
182        $this->options['mailto']      = $mailto;
183
184        // compose headers array
185        $headers = [
186            'Received'         => $this->header_received(),
187            'Date'             => $this->rcmail->user_date(),
188            'From'             => $from_string,
189            'To'               => $mailto,
190            'Cc'               => $mailcc,
191            'Bcc'              => $mailbcc,
192            'Subject'          => trim($subject),
193            'Reply-To'         => $this->email_input_format($replyto),
194            'Mail-Reply-To'    => $this->email_input_format($replyto),
195            'Mail-Followup-To' => $this->email_input_format($followupto),
196            'In-Reply-To'      => isset($this->data['reply_msgid']) ? $this->data['reply_msgid'] : null,
197            'References'       => isset($this->data['references']) ? $this->data['references'] : null,
198            'User-Agent'       => $this->rcmail->config->get('useragent'),
199            'Message-ID'       => $message_id,
200            'X-Sender'         => $from,
201        ];
202
203        if (!empty($identity_arr['organization'])) {
204            $headers['Organization'] = $identity_arr['organization'];
205        }
206
207        if ($mdn_enabled) {
208            $headers['Disposition-Notification-To'] = $from_string;
209        }
210
211        if (!empty($_POST['_priority'])) {
212            $priority     = intval($_POST['_priority']);
213            $a_priorities = [1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest'];
214
215            if (!empty($a_priorities[$priority])) {
216                $headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($a_priorities[$priority]));
217            }
218        }
219
220        // remember reply/forward UIDs in special headers
221        if (!empty($this->options['savedraft'])) {
222            // Note: We ignore <UID>.<PART> forwards/replies here
223            if (
224                !empty($this->data['reply_uid'])
225                && ($uid = $this->data['reply_uid'])
226                && !preg_match('/^\d+\.[0-9.]+$/', $uid)
227            ) {
228                $headers['X-Draft-Info'] = $this->draftinfo_encode([
229                        'type'   => 'reply',
230                        'uid'    => $uid,
231                        'folder' => $this->data['mailbox']
232                ]);
233            }
234            else if (
235                !empty($this->data['forward_uid'])
236                && ($uid = rcube_imap_generic::compressMessageSet($this->data['forward_uid']))
237                && !preg_match('/^\d+[0-9.]+$/', $uid)
238            ) {
239                $headers['X-Draft-Info'] = $this->draftinfo_encode([
240                        'type'   => 'forward',
241                        'uid'    => $uid,
242                        'folder' => $this->data['mailbox']
243                ]);
244            }
245        }
246
247        return array_filter($headers);
248    }
249
250    /**
251     * Set charset and transfer encoding on the message
252     *
253     * @param Mail_mime $message Message object
254     * @param bool      $flowed  Enable format=flowed
255     */
256    public function set_message_encoding($message, $flowed = false)
257    {
258        $text_charset      = $this->options['charset'];
259        $transfer_encoding = '7bit';
260        $head_encoding     = 'quoted-printable';
261
262        // choose encodings for plain/text body and message headers
263        if (preg_match('/ISO-2022/i', $text_charset)) {
264            $head_encoding = 'base64'; // RFC1468
265        }
266        else if (preg_match('/[^\x00-\x7F]/', $message->getTXTBody())) {
267            $transfer_encoding = $this->rcmail->config->get('force_7bit') ? 'quoted-printable' : '8bit';
268        }
269        else if ($this->options['charset'] == 'UTF-8') {
270            $text_charset = 'US-ASCII';
271        }
272
273        if ($flowed) {
274            $text_charset .= ";\r\n format=flowed";
275        }
276
277        // encoding settings for mail composing
278        $message->setParam('text_encoding', $transfer_encoding);
279        $message->setParam('html_encoding', 'quoted-printable');
280        $message->setParam('head_encoding', $head_encoding);
281        $message->setParam('head_charset', $this->options['charset']);
282        $message->setParam('html_charset', $this->options['charset']);
283        $message->setParam('text_charset', $text_charset);
284    }
285
286    /**
287     * Create a message to be saved/sent
288     *
289     * @param array  $headers     Message headers
290     * @param string $body        Message body
291     * @param bool   $isHtml      The body is HTML or not
292     * @param array  $attachments Optional message attachments array
293     *
294     * @return Mail_mime Message object
295     */
296    public function create_message($headers, $body, $isHtml = false, $attachments = [])
297    {
298        $charset = $this->options['charset'];
299        $flowed  = !empty($this->options['savedraft']) || $this->rcmail->config->get('send_format_flowed', true);
300
301        // create PEAR::Mail_mime instance
302        $MAIL_MIME = new Mail_mime("\r\n");
303
304        // Check if we have enough memory to handle the message in it
305        // It's faster than using files, so we'll do this if we only can
306        if (is_array($attachments)) {
307            $memory = 0;
308            foreach ($attachments as $attachment) {
309                $memory += $attachment['size'];
310            }
311
312            // Yeah, Net_SMTP needs up to 12x more memory, 1.33 is for base64
313            if (!rcube_utils::mem_check($memory * 1.33 * 12)) {
314                $MAIL_MIME->setParam('delay_file_io', true);
315            }
316        }
317
318        $plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', [
319                'body'    => $body,
320                'type'    => $isHtml ? 'html' : 'plain',
321                'message' => $MAIL_MIME
322        ]);
323
324        // For HTML-formatted messages, construct the MIME message with both
325        // the HTML part and the plain-text part
326        if ($isHtml) {
327            $MAIL_MIME->setHTMLBody($plugin['body']);
328
329            $plain_body = $this->rcmail->html2text($plugin['body'], ['width' => 0, 'charset' => $charset]);
330            $plain_body = $this->format_plain_body($plain_body, $flowed);
331
332            // There's no sense to use multipart/alternative if the text/plain
333            // part would be blank. Completely blank text/plain part may confuse
334            // some mail clients (#5283)
335            if (strlen(trim($plain_body)) > 0) {
336                $plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', [
337                        'body'    => $plain_body,
338                        'type'    => 'alternative',
339                        'message' => $MAIL_MIME
340                ]);
341
342                // add a plain text version of the e-mail as an alternative part.
343                $MAIL_MIME->setTXTBody($plugin['body']);
344            }
345
346            // Extract image Data URIs into message attachments (#1488502)
347            $this->extract_inline_images($MAIL_MIME, $this->options['from']);
348        }
349        else {
350            $body = $this->format_plain_body($plugin['body'], $flowed);
351
352            $MAIL_MIME->setTXTBody($body, false, true);
353        }
354
355        // encoding settings for mail composing
356        $this->set_message_encoding($MAIL_MIME, $flowed);
357
358        // pass headers to message object
359        $MAIL_MIME->headers($headers);
360
361        return $MAIL_MIME;
362    }
363
364    /**
365     * Prepare plain text content for the message (format=flowed and wrapping)
366     *
367     * @param string $body   Plain text message body
368     * @param bool   $flowed Enable format=flowed formatting
369     *
370     * @return string Formatted content
371     */
372    protected function format_plain_body($body, $flowed = false)
373    {
374        // set line length for body wrapping
375        $line_length = $this->rcmail->config->get('line_length', 72);
376        $charset     = $this->options['charset'];
377
378        if ($flowed) {
379            $body = rcube_mime::format_flowed($body, min($line_length + 2, 79), $charset);
380        }
381        else {
382            $body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset);
383        }
384
385        $body = wordwrap($body, 998, "\r\n", true);
386
387        // make sure all line endings are CRLF (#1486712)
388        $body = preg_replace('/\r?\n/', "\r\n", $body);
389
390        return $body;
391    }
392
393    /**
394     * Message delivery, and setting Replied/Forwarded flag on success
395     *
396     * @param Mail_mime $message    Message object
397     * @param bool      $disconnect Close SMTP connection after delivery
398     *
399     * @return bool True on success, False on failure
400     */
401    public function deliver_message($message, $disconnect = true)
402    {
403        // Handle Delivery Status Notification request
404        $smtp_opts     = ['dsn' => $this->options['dsn_enabled']];
405        $smtp_error    = null;
406        $mailbody_file = null;
407
408        $sent = $this->rcmail->deliver_message($message,
409            $this->options['from'],
410            $this->options['mailto'],
411            $smtp_error, $mailbody_file, $smtp_opts, $disconnect
412        );
413
414        // return to compose page if sending failed
415        if (!$sent) {
416            // remove temp file
417            if ($mailbody_file) {
418                unlink($mailbody_file);
419            }
420
421            if ($smtp_error && is_string($smtp_error)) {
422                $this->options['error_handler']($smtp_error, 'error');
423            }
424            else if ($smtp_error && !empty($smtp_error['label'])) {
425                $this->options['error_handler']($smtp_error['label'], 'error', $smtp_error['vars']);
426            }
427            else {
428                $this->options['error_handler']('sendingfailed', 'error');
429            }
430
431            return false;
432        }
433
434        $message->mailbody_file = $mailbody_file;
435
436        // save message sent time
437        if ($this->options['sendmail_delay']) {
438            $this->rcmail->user->save_prefs(['last_message_time' => time()]);
439        }
440
441        // Collect recipients' addresses
442        $this->collect_recipients($message);
443
444        // set replied/forwarded flag
445        if (!empty($this->data['reply_uid'])) {
446            foreach (rcmail::get_uids($this->data['reply_uid'], $this->data['mailbox']) as $mbox => $uids) {
447                // skip <UID>.<PART> replies
448                if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) {
449                    $this->rcmail->storage->set_flag($uids, 'ANSWERED', $mbox);
450                }
451            }
452        }
453        else if (!empty($this->data['forward_uid'])) {
454            foreach (rcmail::get_uids($this->data['forward_uid'], $this->data['mailbox']) as $mbox => $uids) {
455                // skip <UID>.<PART> forwards
456                if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) {
457                    $this->rcmail->storage->set_flag($uids, 'FORWARDED', $mbox);
458                }
459            }
460        }
461
462        return true;
463    }
464
465    /**
466     * Save the message into Drafts folder (in savedraft mode)
467     * or in Sent mailbox if specified/configured
468     *
469     * @param Mail_mime $message Message object
470     *
471     * @return mixed Operation status
472     */
473    public function save_message($message)
474    {
475        $store_folder = false;
476        $store_target = null;
477        $saved        = false;
478
479        // Determine which folder to save message
480        if (!empty($this->options['savedraft'])) {
481            $store_target = $this->rcmail->config->get('drafts_mbox');
482        }
483        else if (!$this->rcmail->config->get('no_save_sent_messages')) {
484            if (isset($_POST['_store_target'])) {
485                $store_target = rcube_utils::get_input_value('_store_target', rcube_utils::INPUT_POST, true);
486            }
487            else {
488                $store_target = $this->rcmail->config->get('sent_mbox');
489            }
490        }
491
492        if ($store_target) {
493            $storage = $this->rcmail->get_storage();
494
495            // check if folder is subscribed
496            if ($storage->folder_exists($store_target, true)) {
497                $store_folder = true;
498            }
499            // folder may be existing but not subscribed (#1485241)
500            else if (!$storage->folder_exists($store_target)) {
501                $store_folder = $storage->create_folder($store_target, true);
502            }
503            else if ($storage->subscribe($store_target)) {
504                $store_folder = true;
505            }
506
507            // append message to sent box
508            if ($store_folder) {
509                // message body in file
510                if (!empty($message->mailbody_file) || $message->getParam('delay_file_io')) {
511                    $headers = $message->txtHeaders();
512
513                    // file already created
514                    if (!empty($message->mailbody_file)) {
515                        $msg = $message->mailbody_file;
516                    }
517                    else {
518                        $message->mailbody_file = rcube_utils::temp_filename('msg');
519                        $msg = $message->saveMessageBody($message->mailbody_file);
520
521                        if (!is_a($msg, 'PEAR_Error')) {
522                            $msg = $message->mailbody_file;
523                        }
524                    }
525                }
526                else {
527                    $msg     = $message->getMessage();
528                    $headers = '';
529                }
530
531                if (is_a($msg, 'PEAR_Error')) {
532                    rcube::raise_error([
533                        'code' => 650, 'file' => __FILE__, 'line' => __LINE__,
534                        'message' => "Could not create message: ".$msg->getMessage()],
535                        true, false);
536                }
537                else {
538                    $is_file = !empty($message->mailbody_file);
539                    $saved   = $storage->save_message($store_target, $msg, $headers, $is_file, ['SEEN']);
540                }
541            }
542
543            // raise error if saving failed
544            if (!$saved) {
545                rcube::raise_error(['code' => 800, 'type' => 'imap',
546                    'file' => __FILE__, 'line' => __LINE__,
547                    'message' => "Could not save message in $store_target"], true, false);
548            }
549        }
550
551        if (!empty($message->mailbody_file)) {
552            unlink($message->mailbody_file);
553            unset($message->mailbody_file);
554        }
555
556        $this->options['store_target'] = $store_target;
557        $this->options['store_folder'] = $store_folder;
558
559        return $saved;
560    }
561
562    /**
563     * If enabled, returns Received header content to be prepended
564     * to message headers
565     *
566     * @return string|null Received header content
567     */
568    public function header_received()
569    {
570        if ($this->rcmail->config->get('http_received_header')) {
571            $nldlm       = "\r\n\t";
572            $http_header = 'from ';
573
574            // FROM/VIA
575            if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
576                $hosts        = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
577                $http_header .= $this->received_host($hosts[0]) . $nldlm . ' via ';
578            }
579
580            $http_header .= $this->received_host($_SERVER['REMOTE_ADDR']);
581
582            // BY
583            $http_header .= $nldlm . 'by ' . rcube_utils::server_name('HTTP_HOST');
584
585            // WITH
586            $http_header .= $nldlm . 'with HTTP (' . $_SERVER['SERVER_PROTOCOL']
587                . ' ' . $_SERVER['REQUEST_METHOD'] . '); ' . date('r');
588
589            return wordwrap($http_header, 69, $nldlm);
590        }
591    }
592
593    /**
594     * Converts host address into host spec. for Received header
595     */
596    protected function received_host($host)
597    {
598        $hostname = gethostbyaddr($host);
599        $result   = $this->encrypt_host($hostname);
600
601        if ($host != $hostname) {
602            $result .= ' (' . $this->encrypt_host($host) . ')';
603        }
604
605        return $result;
606    }
607
608    /**
609     * Encrypt host IP or hostname for Received header
610     */
611    protected function encrypt_host($host)
612    {
613        if ($this->rcmail->config->get('http_received_header_encrypt')) {
614            return $this->rcmail->encrypt($host);
615        }
616
617        if (!preg_match('/[^0-9:.]/', $host)) {
618            return "[$host]";
619        }
620
621        return $host;
622    }
623
624    /**
625     * Returns user identity record
626     *
627     * @param int $id Identity ID
628     *
629     * @return array|false User identity data, False if there's no such identity
630     */
631    public function get_identity($id)
632    {
633        if ($sql_arr = $this->rcmail->user->get_identity($id)) {
634            $out = $sql_arr;
635
636            if (!empty($this->options['charset']) && $this->options['charset'] != RCUBE_CHARSET) {
637                foreach ($out as $k => $v) {
638                    $out[$k] = rcube_charset::convert($v, RCUBE_CHARSET, $this->options['charset']);
639                }
640            }
641
642            $out['mailto'] = $sql_arr['email'];
643            $out['string'] = format_email_recipient($sql_arr['email'], $sql_arr['name']);
644
645            return $out;
646        }
647
648        return false;
649    }
650
651    /**
652     * Extract image attachments from HTML message (data URIs)
653     *
654     * @param Mail_mime $message Message object
655     * @param string    $from    Sender email address
656     */
657    public static function extract_inline_images($message, $from)
658    {
659        $body   = $message->getHTMLBody();
660        $offset = 0;
661        $list   = [];
662        $domain = 'localhost';
663        $regexp = '#img[^>]+src=[\'"](data:([^;]*);base64,([a-z0-9+/=\r\n]+))([\'"])#i';
664
665        if (preg_match_all($regexp, $body, $matches, PREG_OFFSET_CAPTURE)) {
666            // get domain for the Content-ID, must be the same as in Mail_Mime::get()
667            if (preg_match('#@([0-9a-zA-Z\-\.]+)#', $from, $m)) {
668                $domain = $m[1];
669            }
670
671            foreach ($matches[1] as $idx => $m) {
672                $data = preg_replace('/\r\n/', '', $matches[3][$idx][0]);
673                $data = base64_decode($data);
674
675                if (empty($data)) {
676                    continue;
677                }
678
679                $hash      = md5($data) . '@' . $domain;
680                $mime_type = $matches[2][$idx][0];
681
682                if (empty($mime_type)) {
683                    $mime_type = rcube_mime::image_content_type($data);
684                }
685
686                // add the image to the MIME message
687                if (empty($list[$hash])) {
688                    $ext         = preg_replace('#^[^/]+/#', '', $mime_type);
689                    $name        = substr($hash, 0, 8) . '.' . $ext;
690                    $list[$hash] = $name;
691
692                    $message->addHTMLImage($data, $mime_type, $name, false, $hash);
693                }
694
695                $name = $list[$hash];
696                $body = substr_replace($body, $name, $m[1] + $offset, strlen($m[0]));
697                $offset += strlen($name) - strlen($m[0]);
698            }
699        }
700
701        $message->setHTMLBody($body);
702    }
703
704    /**
705     * Parse and cleanup email address input (and count addresses)
706     *
707     * @param string $mailto Address input
708     * @param bool   $count  Do count recipients (count saved in $this->parse_data['RECIPIENT_COUNT'])
709     * @param bool   $check  Validate addresses (errors saved in $this->parse_data['INVALID_EMAIL'])
710     *
711     * @return string Canonical recipients string (comma separated)
712     */
713    public function email_input_format($mailto, $count = false, $check = true)
714    {
715        if (!isset($this->parse_data['RECIPIENT_COUNT'])) {
716            $this->parse_data['RECIPIENT_COUNT'] = 0;
717        }
718
719        if (empty($mailto)) {
720            return '';
721        }
722
723        // convert to UTF-8 to preserve \x2c(,) and \x3b(;) used in ISO-2022-JP;
724        $charset = $this->options['charset'];
725        if ($charset != RCUBE_CHARSET) {
726            $mailto = rcube_charset::convert($mailto, $charset, RCUBE_CHARSET);
727        }
728        if (preg_match('/ISO-2022/i', $charset)) {
729            $use_base64 = true;
730        }
731
732        // simplified email regexp, supporting quoted local part
733        $email_regexp = '(\S+|("[^"]+"))@\S+';
734
735        $delim   = ',;';
736        $regexp  = ["/[$delim]\s*[\r\n]+/", '/[\r\n]+/', "/[$delim]\s*\$/m", '/;/', '/(\S{1})(<'.$email_regexp.'>)/U'];
737        $replace = [', ', ', ', '', ',', '\\1 \\2'];
738
739        // replace new lines and strip ending ', ', make address input more valid
740        $mailto = trim(preg_replace($regexp, $replace, $mailto));
741        $items  = rcube_utils::explode_quoted_string("[$delim]", $mailto);
742        $result = [];
743
744        foreach ($items as $item) {
745            $item = trim($item);
746            // address in brackets without name (do nothing)
747            if (preg_match('/^<'.$email_regexp.'>$/', $item)) {
748                $item     = rcube_utils::idn_to_ascii(trim($item, '<>'));
749                $result[] = $item;
750            }
751            // address without brackets and without name (add brackets)
752            else if (preg_match('/^'.$email_regexp.'$/', $item)) {
753                // Remove trailing non-letter characters (#7899)
754                $item     = preg_replace('/[^a-zA-Z]$/', '', $item);
755                $item     = rcube_utils::idn_to_ascii($item);
756                $result[] = $item;
757            }
758            // address with name (handle name)
759            else if (preg_match('/<*'.$email_regexp.'>*$/', $item, $matches)) {
760                $address = $matches[0];
761                $name    = trim(str_replace($address, '', $item));
762                if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
763                    $name = substr($name, 1, -1);
764                }
765
766                // encode "name" field
767                if (!empty($use_base64)) {
768                    $name = rcube_charset::convert($name, RCUBE_CHARSET, $charset);
769                    $name = Mail_mimePart::encodeMB($name, $charset, 'base64');
770                }
771                else {
772                    $name = stripcslashes($name);
773                }
774
775                $address  = rcube_utils::idn_to_ascii(trim($address, '<>'));
776                $result[] = format_email_recipient($address, $name);
777                $item     = $address;
778            }
779
780            // check address format
781            $item = trim($item, '<>');
782            if ($item && $check && !rcube_utils::check_email($item)) {
783                $this->parse_data['INVALID_EMAIL'] = $item;
784                return;
785            }
786        }
787
788        if ($count) {
789            $this->parse_data['RECIPIENT_COUNT'] += count($result);
790        }
791
792        return implode(', ', $result);
793    }
794
795    /**
796     * Returns configured generic message footer
797     *
798     * @param bool $isHtml Return HTML or Plain text version of the footer?
799     *
800     * @return string|null Footer content
801     */
802    public function generic_message_footer($isHtml)
803    {
804        if ($isHtml && ($file = $this->rcmail->config->get('generic_message_footer_html'))) {
805            $html_footer = true;
806        }
807        else {
808            $file = $this->rcmail->config->get('generic_message_footer');
809            $html_footer = false;
810        }
811
812        if ($file && realpath($file)) {
813            // sanity check
814            if (!preg_match('/\.(php|ini|conf)$/', $file) && strpos($file, '/etc/') === false) {
815                $footer = file_get_contents($file);
816                if ($isHtml && !$html_footer) {
817                    $t2h    = new rcube_text2html($footer, false);
818                    $footer = $t2h->get_html();
819                }
820
821                if (!empty($this->options['charset']) && $this->options['charset'] != RCUBE_CHARSET) {
822                    $footer = rcube_charset::convert($footer, RCUBE_CHARSET, $this->options['charset']);
823                }
824
825                return $footer;
826            }
827        }
828    }
829
830    /**
831     * Encode data array into a string for use in X-Draft-Info header
832     *
833     * @param array $data Data array
834     *
835     * @return string Decoded data as a string
836     */
837    public static function draftinfo_encode($data)
838    {
839        $parts = [];
840
841        foreach ($data as $key => $val) {
842            $encode  = $key == 'folder' || strpos($val, ';') !== false;
843            $parts[] = $key . '=' . ($encode ? 'B::' . base64_encode($val) : $val);
844        }
845
846        return implode('; ', $parts);
847    }
848
849    /**
850     * Decode X-Draft-Info header value into an array
851     *
852     * @param string $str Encoded data string (see self::draftinfo_encode())
853     *
854     * @return array Decoded data
855     */
856    public static function draftinfo_decode($str)
857    {
858        $info = [];
859
860        foreach (preg_split('/;\s+/', $str) as $part) {
861            list($key, $val) = explode('=', $part, 2);
862            if (strpos($val, 'B::') === 0) {
863                $val = base64_decode(substr($val, 3));
864            }
865            else if ($key == 'folder') {
866                $val = base64_decode($val);
867            }
868
869            $info[$key] = $val;
870        }
871
872        return $info;
873    }
874
875    /**
876     * Header (From, To, Cc, etc.) input object for templates
877     */
878    public function headers_output($attrib)
879    {
880        list($form_start,) = $this->form_tags($attrib);
881
882        $out          = '';
883        $part         = strtolower($attrib['part']);
884        $fname        = null;
885        $field_type   = null;
886        $allow_attrib = [];
887        $param        = $part;
888
889        switch ($part) {
890        case 'from':
891            return $form_start . $this->compose_header_from($attrib);
892
893        case 'to':
894        case 'cc':
895        case 'bcc':
896            $fname  = '_' . $part;
897
898            $allow_attrib = ['id', 'class', 'style', 'cols', 'rows', 'tabindex'];
899            $field_type   = 'html_textarea';
900            break;
901
902        case 'replyto':
903        case 'reply-to':
904            $fname  = '_replyto';
905            $param  = 'replyto';
906
907        case 'followupto':
908        case 'followup-to':
909            if (!$fname) {
910                $fname  = '_followupto';
911                $param  = 'followupto';
912            }
913
914            $allow_attrib = ['id', 'class', 'style', 'size', 'tabindex'];
915            $field_type   = 'html_inputfield';
916            break;
917        }
918
919        if ($fname && $field_type) {
920            // pass the following attributes to the form class
921            $field_attrib = ['name' => $fname, 'spellcheck' => 'false'];
922            foreach ($attrib as $attr => $value) {
923                if (stripos($attr, 'data-') === 0 || in_array($attr, $allow_attrib)) {
924                    $field_attrib[$attr] = $value;
925                }
926            }
927
928            $mode = isset($this->data['mode']) ? $this->data['mode'] : null;
929
930            // create textarea object
931            $input = new $field_type($field_attrib);
932            $out   = $input->show($this->compose_header_value($param, $mode));
933        }
934
935        if ($form_start) {
936            $out = $form_start . $out;
937        }
938
939        // configure autocompletion
940        rcmail_action::autocomplete_init();
941
942        return $out;
943    }
944
945    /**
946     * Returns From header input element
947     */
948    protected function compose_header_from($attrib)
949    {
950        // pass the following attributes to the form class
951        $field_attrib = ['name' => '_from'];
952        foreach ($attrib as $attr => $value) {
953            if (in_array($attr, ['id', 'class', 'style', 'size', 'tabindex'])) {
954                $field_attrib[$attr] = $value;
955            }
956        }
957
958        if (!empty($this->options['message']->identities)) {
959            $a_signatures = [];
960            $identities   = [];
961            $top_posting  = intval($this->rcmail->config->get('reply_mode')) > 0
962                && !$this->rcmail->config->get('sig_below')
963                && ($this->data['mode'] == self::MODE_REPLY || $this->data['mode'] == self::MODE_FORWARD);
964
965            $separator     = $top_posting ? '---' : '-- ';
966            $add_separator = (bool) $this->rcmail->config->get('sig_separator');
967
968            $field_attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".change_identity(this)";
969            $select_from = new html_select($field_attrib);
970
971            // create SELECT element
972            foreach ($this->options['message']->identities as $sql_arr) {
973                $identity_id = $sql_arr['identity_id'];
974                $select_from->add(format_email_recipient($sql_arr['email'], $sql_arr['name']), $identity_id);
975
976                // add signature to array
977                if (!empty($sql_arr['signature']) && empty($this->data['param']['nosig'])) {
978                    $text = $html = $sql_arr['signature'];
979
980                    if ($sql_arr['html_signature']) {
981                        $text = $this->rcmail->html2text($html, ['links' => false]);
982                        $text = trim($text, "\r\n");
983                    }
984                    else {
985                        $t2h  = new rcube_text2html($text, false);
986                        $html = $t2h->get_html();
987                    }
988
989                    if ($add_separator && !preg_match('/^--[ -]\r?\n/m', $text)) {
990                        $text = $separator . "\n" . ltrim($text, "\r\n");
991                        $html = $separator . "<br>" . $html;
992                    }
993
994                    $a_signatures[$identity_id]['text'] = $text;
995                    $a_signatures[$identity_id]['html'] = $html;
996                }
997
998                // add bcc and reply-to
999                if (!empty($sql_arr['reply-to'])) {
1000                    $identities[$identity_id]['replyto'] = $sql_arr['reply-to'];
1001                }
1002                if (!empty($sql_arr['bcc'])) {
1003                    $identities[$identity_id]['bcc'] = $sql_arr['bcc'];
1004                }
1005
1006                $identities[$identity_id]['email'] = $sql_arr['email'];
1007            }
1008
1009            $out = $select_from->show($this->options['message']->compose['from']);
1010
1011            // add signatures to client
1012            $this->rcmail->output->set_env('signatures', $a_signatures);
1013            $this->rcmail->output->set_env('identities', $identities);
1014        }
1015        // no identities, display text input field
1016        else {
1017            $from = isset($this->options['message']->compose['from']) ? $this->options['message']->compose['from'] : null;
1018            $field_attrib['class'] = 'from_address';
1019            $input_from = new html_inputfield($field_attrib);
1020            $out = $input_from->show($from);
1021        }
1022
1023        return $out;
1024    }
1025
1026    /**
1027     * Set the value of specified header depending on compose mode
1028     */
1029    protected function compose_header_value($header, $mode)
1030    {
1031        $fvalue        = '';
1032        $decode_header = true;
1033        $message       = $this->options['message'];
1034        $charset       = !empty($message->headers) ? $message->headers->charset : RCUBE_CHARSET;
1035        $separator     = ', ';
1036
1037        // we have a set of recipients stored is session
1038        if (
1039            $header == 'to'
1040            && !empty($this->data['param']['mailto'])
1041            && ($mailto_id = $this->data['param']['mailto'])
1042            && !empty($_SESSION['mailto'][$mailto_id])
1043        ) {
1044            $fvalue        = urldecode($_SESSION['mailto'][$mailto_id]);
1045            $decode_header = false;
1046            $charset       = $this->rcmail->output->charset;
1047
1048            // make session to not grow up too much
1049            $this->rcmail->session->remove("mailto.$mailto_id");
1050        }
1051        else if (!empty($_POST['_' . $header])) {
1052            $fvalue  = rcube_utils::get_input_value('_' . $header, rcube_utils::INPUT_POST, true);
1053            $charset = $this->rcmail->output->charset;
1054        }
1055        else if (!empty($this->data['param'][$header])) {
1056            $fvalue  = $this->data['param'][$header];
1057            $charset = $this->rcmail->output->charset;
1058        }
1059        else if ($mode == self::MODE_REPLY) {
1060            // get recipient address(es) out of the message headers
1061            if ($header == 'to') {
1062                $mailfollowup = isset($message->headers->others['mail-followup-to']) ? $message->headers->others['mail-followup-to'] : [];
1063                $mailreplyto  = isset($message->headers->others['mail-reply-to']) ? $message->headers->others['mail-reply-to'] : [];
1064                $reply_all    = isset($message->reply_all) ? $message->reply_all : null;
1065
1066                // Reply to mailing list...
1067                if ($reply_all == 'list' && $mailfollowup) {
1068                    $fvalue = $mailfollowup;
1069                }
1070                else if ($reply_all == 'list'
1071                    && preg_match('/<mailto:([^>]+)>/i', $message->headers->others['list-post'], $m)
1072                ) {
1073                    $fvalue = $m[1];
1074                }
1075                // Reply to...
1076                else if ($reply_all && $mailfollowup) {
1077                    $fvalue = $mailfollowup;
1078                }
1079                else if ($mailreplyto) {
1080                    $fvalue = $mailreplyto;
1081                }
1082                else if (!empty($message->headers->replyto)) {
1083                    $fvalue  = $message->headers->replyto;
1084                    $replyto = true;
1085                }
1086                else if (!empty($message->headers->from)) {
1087                    $fvalue = $message->headers->from;
1088                }
1089
1090                // Reply to message sent by yourself (#1487074, #1489230, #1490439)
1091                // Reply-To address need to be unset (#1490233)
1092                if (!empty($message->compose['ident']) && empty($replyto)) {
1093                    foreach ([$fvalue, $message->get_header('from')] as $sender) {
1094                        $senders = rcube_mime::decode_address_list($sender, null, false, $charset, true);
1095
1096                        if (in_array($message->compose['ident']['email_ascii'], $senders)) {
1097                            $fvalue = $message->headers->to;
1098                            break;
1099                        }
1100                    }
1101                }
1102            }
1103            // add recipient of original message if reply to all
1104            else if ($header == 'cc' && !empty($message->reply_all) && $message->reply_all != 'list') {
1105                if ($v = $message->headers->to) {
1106                    $fvalue .= $v;
1107                }
1108                if ($v = $message->headers->cc) {
1109                    $fvalue .= (!empty($fvalue) ? $separator : '') . $v;
1110                }
1111
1112                // Deliberately ignore 'Sender' header (#6506)
1113
1114                // When To: and Reply-To: are the same we add From: address to the list (#1489037)
1115                if ($v = $message->headers->from) {
1116                    $to      = $message->headers->to;
1117                    $replyto = $message->headers->replyto;
1118                    $from    = rcube_mime::decode_address_list($v, null, false, $charset, true);
1119                    $to      = rcube_mime::decode_address_list($to, null, false, $charset, true);
1120                    $replyto = rcube_mime::decode_address_list($replyto, null, false, $charset, true);
1121
1122                    if (!empty($replyto) && !count(array_diff($to, $replyto)) && count(array_diff($from, $to))) {
1123                        $fvalue .= (!empty($fvalue) ? $separator : '') . $v;
1124                    }
1125                }
1126            }
1127        }
1128        else if (in_array($mode, [self::MODE_DRAFT, self::MODE_EDIT])) {
1129            // get drafted headers
1130            if ($header == 'to' && !empty($message->headers->to)) {
1131                $fvalue = $message->get_header('to', true);
1132            }
1133            else if ($header == 'cc' && !empty($message->headers->cc)) {
1134                $fvalue = $message->get_header('cc', true);
1135            }
1136            else if ($header == 'bcc' && !empty($message->headers->bcc)) {
1137                $fvalue = $message->get_header('bcc', true);
1138            }
1139            else if ($header == 'replyto' && !empty($message->headers->others['mail-reply-to'])) {
1140                $fvalue = $message->get_header('mail-reply-to');
1141            }
1142            else if ($header == 'replyto' && !empty($message->headers->replyto)) {
1143                $fvalue = $message->get_header('reply-to');
1144            }
1145            else if ($header == 'followupto' && !empty($message->headers->others['mail-followup-to'])) {
1146                $fvalue = $message->get_header('mail-followup-to');
1147            }
1148        }
1149
1150        // split recipients and put them back together in a unique way
1151        if (!empty($fvalue) && in_array($header, ['to', 'cc', 'bcc'])) {
1152            $from_email   = @mb_strtolower($message->compose['ident']['email']);
1153            $to_addresses = rcube_mime::decode_address_list($fvalue, null, $decode_header, $charset);
1154            $fvalue       = [];
1155
1156            foreach ($to_addresses as $addr_part) {
1157                if (empty($addr_part['mailto'])) {
1158                    continue;
1159                }
1160
1161                // According to RFC5321 local part of email address is case-sensitive
1162                // however, here it is better to compare addresses in case-insensitive manner
1163                $mailto    = format_email(rcube_utils::idn_to_utf8($addr_part['mailto']));
1164                $mailto_lc = mb_strtolower($addr_part['mailto']);
1165
1166                if (
1167                    ($header == 'to' || $mode != self::MODE_REPLY || $mailto_lc != $from_email)
1168                    && (empty($message->recipients) || !in_array($mailto_lc, (array) $message->recipients))
1169                ) {
1170                    if ($addr_part['name'] && $mailto != $addr_part['name']) {
1171                        $mailto = format_email_recipient($mailto, $addr_part['name']);
1172                    }
1173
1174                    $fvalue[]              = $mailto;
1175                    $message->recipients[] = $mailto_lc;
1176                }
1177            }
1178
1179            $fvalue = implode($separator, $fvalue);
1180        }
1181
1182        return $fvalue;
1183    }
1184
1185    /**
1186     * Creates reply subject by removing common subject
1187     * prefixes/suffixes from the original message subject
1188     *
1189     * @param string $subject Subject string
1190     *
1191     * @return string Modified subject string
1192     */
1193    public static function reply_subject($subject)
1194    {
1195        $subject = trim($subject);
1196
1197        //  Add config options for subject prefixes (#7929)
1198        $subject = rcube_utils::remove_subject_prefix($subject, 'reply');
1199        $subject = rcmail::get_instance()->config->get('response_prefix', 'Re:') . ' ' . $subject;
1200
1201        return trim($subject);
1202    }
1203
1204    /**
1205     * Subject input object for templates
1206     *
1207     * @param array $attrib Object attributes
1208     *
1209     * @return string HTML content
1210     */
1211    public function compose_subject($attrib)
1212    {
1213        list($form_start, $form_end) = $this->form_tags($attrib);
1214        unset($attrib['form']);
1215
1216        $attrib['name']       = '_subject';
1217        $attrib['spellcheck'] = 'true';
1218
1219        $textfield = new html_inputfield($attrib);
1220        $subject   = '';
1221
1222        // use subject from post
1223        if (isset($_POST['_subject'])) {
1224            $subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE);
1225        }
1226        else if (!empty($this->data['param']['subject'])) {
1227            $subject = $this->data['param']['subject'];
1228        }
1229        // create a reply-subject
1230        else if ($this->data['mode'] == self::MODE_REPLY) {
1231            $subject = self::reply_subject($this->options['message']->subject);
1232        }
1233        // create a forward-subject
1234        else if ($this->data['mode'] == self::MODE_FORWARD) {
1235            //  Add config options for subject prefixes (#7929)
1236            $subject = rcube_utils::remove_subject_prefix($this->options['message']->subject, 'forward');
1237            $subject = trim($this->rcmail->config->get('forward_prefix', 'Fwd:') . ' ' . $subject);
1238        }
1239        // create a draft-subject
1240        else if ($this->data['mode'] == self::MODE_DRAFT || $this->data['mode'] == self::MODE_EDIT) {
1241            $subject = $this->options['message']->subject;
1242        }
1243
1244        $out = $form_start ? "$form_start\n" : '';
1245        $out .= $textfield->show($subject);
1246        $out .= $form_end ? "\n$form_end" : '';
1247
1248        return $out;
1249    }
1250
1251    /**
1252     * Returns compose form tag (if not used already)
1253     *
1254     * @param array $attrib Form attributes
1255     */
1256    public function form_tags($attrib)
1257    {
1258        if (isset($attrib['noform']) && rcube_utils::get_boolean((string) $attrib['noform'])) {
1259            return ['', ''];
1260        }
1261
1262        $form_start = '';
1263        if (!$this->message_form) {
1264            $hiddenfields = new html_hiddenfield(['name' => '_task', 'value' => $this->rcmail->task]);
1265            $hiddenfields->add(['name' => '_action', 'value' => 'send']);
1266            $hiddenfields->add(['name' => '_id', 'value' => isset($this->data['id']) ? $this->data['id'] : '']);
1267            $hiddenfields->add(['name' => '_attachments']);
1268
1269            if (empty($attrib['form'])) {
1270                $form_attr  = [
1271                    'name'   => 'form',
1272                    'method' => 'post',
1273                    'class'  => !empty($attrib['class']) ? $attrib['class'] : '',
1274                ];
1275                $form_start = $this->rcmail->output->form_tag($form_attr);
1276            }
1277
1278            $form_start .= $hiddenfields->show();
1279        }
1280
1281        $form_end  = ($this->message_form && empty($attrib['form'])) ? '</form>' : '';
1282        $form_name = !empty($attrib['form']) ? $attrib['form'] : 'form';
1283
1284        if (!$this->message_form) {
1285            $this->rcmail->output->add_gui_object('messageform', $form_name);
1286        }
1287
1288        $this->message_form = $form_name;
1289
1290        return [$form_start, $form_end];
1291    }
1292
1293    /**
1294     * Returns compose form "head"
1295     */
1296    public function form_head($attrib)
1297    {
1298        list($form_start,) = $this->form_tags($attrib);
1299
1300        return $form_start;
1301    }
1302
1303    /**
1304     * Folder selector object for templates
1305     *
1306     * @param array $attrib Object attributes
1307     *
1308     * @return string HTML content
1309     */
1310    public function folder_selector($attrib)
1311    {
1312        if (isset($_POST['_store_target'])) {
1313            $mbox = $_POST['_store_target'];
1314        }
1315        else {
1316            $mbox = isset($this->data['param']['sent_mbox']) ? $this->data['param']['sent_mbox'] : null;
1317        }
1318
1319        $params = [
1320            'noselection'   => '- ' . $this->rcmail->gettext('dontsave') . ' -',
1321            'folder_filter' => 'mail',
1322            'folder_rights' => 'w',
1323        ];
1324
1325        $attrib['name'] = '_store_target';
1326        $select = rcmail_action::folder_selector(array_merge($attrib, $params));
1327
1328        return $select->show($mbox, $attrib);
1329    }
1330
1331    /**
1332     * Mail Disposition Notification checkbox object for templates
1333     *
1334     * @param array $attrib Object attributes
1335     *
1336     * @return string HTML content
1337     */
1338    public function mdn_checkbox($attrib)
1339    {
1340        list($form_start, $form_end) = $this->form_tags($attrib);
1341        unset($attrib['form']);
1342
1343        if (empty($attrib['id'])) {
1344            $attrib['id'] = 'receipt';
1345        }
1346
1347        $attrib['name']  = '_mdn';
1348        $attrib['value'] = '1';
1349
1350        $checkbox = new html_checkbox($attrib);
1351
1352        if (isset($_POST['_mdn'])) {
1353            $mdn_default = $_POST['_mdn'];
1354        }
1355        else if (in_array($this->data['mode'], [self::MODE_DRAFT, self::MODE_EDIT])) {
1356            $mdn_default = (bool) $this->options['message']->headers->mdn_to;
1357        }
1358        else {
1359            $mdn_default = $this->rcmail->config->get('mdn_default');
1360        }
1361
1362        $out = $form_start ? "$form_start\n" : '';
1363        $out .= $checkbox->show($mdn_default);
1364        $out .= $form_end ? "\n$form_end" : '';
1365
1366        return $out;
1367    }
1368
1369    /**
1370     * Delivery Status Notification checkbox object for templates
1371     *
1372     * @param array $attrib Object attributes
1373     *
1374     * @return string HTML content
1375     */
1376    public function dsn_checkbox($attrib)
1377    {
1378        list($form_start, $form_end) = $this->form_tags($attrib);
1379        unset($attrib['form']);
1380
1381        if (empty($attrib['id'])) {
1382            $attrib['id'] = 'dsn';
1383        }
1384
1385        $attrib['name']  = '_dsn';
1386        $attrib['value'] = '1';
1387
1388        $checkbox = new html_checkbox($attrib);
1389
1390        if (isset($_POST['_dsn'])) {
1391            $dsn_value = (int) $_POST['_dsn'];
1392        }
1393        else {
1394            $dsn_value = $this->rcmail->config->get('dsn_default');
1395        }
1396
1397        $out = $form_start ? "$form_start\n" : '';
1398        $out .= $checkbox->show($dsn_value);
1399        $out .= $form_end ? "\n$form_end" : '';
1400
1401        return $out;
1402    }
1403
1404    /**
1405     * Priority selector object for templates
1406     *
1407     * @param array $attrib Object attributes
1408     *
1409     * @return string HTML content
1410     */
1411    public function priority_selector($attrib)
1412    {
1413        list($form_start, $form_end) = $this->form_tags($attrib);
1414        unset($attrib['form']);
1415
1416        $attrib['name'] = '_priority';
1417        $prio_list = [
1418            $this->rcmail->gettext('lowest')  => 5,
1419            $this->rcmail->gettext('low')     => 4,
1420            $this->rcmail->gettext('normal')  => 0,
1421            $this->rcmail->gettext('high')    => 2,
1422            $this->rcmail->gettext('highest') => 1,
1423        ];
1424
1425        $selector = new html_select($attrib);
1426        $selector->add(array_keys($prio_list), array_values($prio_list));
1427
1428        if (isset($_POST['_priority'])) {
1429            $sel = (int) $_POST['_priority'];
1430        }
1431        else if (isset($this->options['message']->headers->priority)
1432            && intval($this->options['message']->headers->priority) != 3
1433        ) {
1434            $sel = (int) $this->options['message']->headers->priority;
1435        }
1436        else {
1437            $sel = 0;
1438        }
1439
1440        $out = $form_start ? "$form_start\n" : '';
1441        $out .= $selector->show((int) $sel);
1442        $out .= $form_end ? "\n$form_end" : '';
1443
1444        return $out;
1445    }
1446
1447    /**
1448     * Helper to create Sent folder if it does not exists
1449     *
1450     * @param string $folder Folder name to check
1451     * @param bool   $create Create if does not exist
1452     *
1453     * @return bool True if the folder exists, False otherwise
1454     */
1455    public static function check_sent_folder($folder, $create = false)
1456    {
1457        $rcmail = rcmail::get_instance();
1458
1459        // we'll not save the message, so it doesn't matter
1460        if ($rcmail->config->get('no_save_sent_messages')) {
1461            return true;
1462        }
1463
1464        if ($rcmail->storage->folder_exists($folder, true)) {
1465            return true;
1466        }
1467
1468        // folder may exist but isn't subscribed (#1485241)
1469        if ($create) {
1470            if (!$rcmail->storage->folder_exists($folder)) {
1471                return $rcmail->storage->create_folder($folder, true);
1472            }
1473            else {
1474                return $rcmail->storage->subscribe($folder);
1475            }
1476        }
1477
1478        return false;
1479    }
1480
1481    /**
1482     * Initialize mail compose UI elements
1483     */
1484    protected function compose_init($message)
1485    {
1486        $message->compose = [];
1487
1488        // get user's identities
1489        $message->identities = $this->rcmail->user->list_identities(null, true);
1490
1491        // Set From field value
1492        if (!empty($_POST['_from'])) {
1493            $message->compose['from'] = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
1494        }
1495        else if (!empty($this->data['param']['from'])) {
1496            $message->compose['from'] = $this->data['param']['from'];
1497        }
1498        else if (!empty($message->identities)) {
1499            $ident = self::identity_select($message, $message->identities, $this->data['mode']);
1500
1501            $message->compose['from']  = $ident['identity_id'];
1502            $message->compose['ident'] = $ident;
1503        }
1504
1505        $this->rcmail->output->add_handlers([
1506                'storetarget'      => [$this, 'folder_selector'],
1507                'composeheaders'   => [$this, 'headers_output'],
1508                'composesubject'   => [$this, 'compose_subject'],
1509                'priorityselector' => [$this, 'priority_selector'],
1510                'mdncheckbox'      => [$this, 'mdn_checkbox'],
1511                'dsncheckbox'      => [$this, 'dsn_checkbox'],
1512                'composeformhead'  => [$this, 'form_head'],
1513        ]);
1514
1515        // add some labels to client
1516        $this->rcmail->output->add_label('nosubject', 'nosenderwarning', 'norecipientwarning',
1517            'nosubjectwarning', 'cancel', 'nobodywarning', 'notsentwarning', 'savingmessage',
1518            'sendingmessage', 'searching', 'disclosedrecipwarning', 'disclosedreciptitle',
1519            'bccinstead', 'nosubjecttitle', 'sendmessage');
1520
1521        $this->rcmail->output->set_env('max_disclosed_recipients', (int) $this->rcmail->config->get('max_disclosed_recipients', 5));
1522    }
1523
1524    /**
1525     * Detect recipient identity from specified message
1526     *
1527     * @param rcube_message $message    Message object
1528     * @param array         $identities User identities (if NULL all user identities will be used)
1529     * @param string        $mode       Composing mode (see self::MODE_*)
1530     *
1531     * @return array Selected user identity (or the default identity) data
1532     */
1533    public static function identity_select($message, $identities = null, $mode = null)
1534    {
1535        $a_recipients = [];
1536        $a_names      = [];
1537
1538        if ($identities === null) {
1539            $identities = rcmail::get_instance()->user->list_identities(null, true);
1540        }
1541
1542        if (!$mode) {
1543            $mode = self::MODE_REPLY;
1544        }
1545
1546        // extract all recipients of the reply-message
1547        if (!empty($message->headers)) {
1548            $charset = $message->headers->charset;
1549
1550            if (in_array($mode, [self::MODE_REPLY, self::MODE_FORWARD])) {
1551                $a_to = rcube_mime::decode_address_list($message->headers->to, null, true, $charset);
1552                foreach ($a_to as $addr) {
1553                    if (!empty($addr['mailto'])) {
1554                        $a_recipients[] = strtolower($addr['mailto']);
1555                        $a_names[]      = $addr['name'];
1556                    }
1557                }
1558
1559                if (!empty($message->headers->cc)) {
1560                    $a_cc = rcube_mime::decode_address_list($message->headers->cc, null, true, $charset);
1561                    foreach ($a_cc as $addr) {
1562                        if (!empty($addr['mailto'])) {
1563                            $a_recipients[] = strtolower($addr['mailto']);
1564                            $a_names[]      = $addr['name'];
1565                        }
1566                    }
1567                }
1568            }
1569
1570            // decode From: address
1571            if (!empty($message->headers)) {
1572                $from = array_first(rcube_mime::decode_address_list($message->headers->from, null, true, $charset));
1573                $from['mailto'] = isset($from['mailto']) ? strtolower($from['mailto']) : '';
1574            }
1575        }
1576
1577        if (empty($from)) {
1578            $from = ['mailto' => ''];
1579        }
1580
1581        $from_idx   = null;
1582        $found_idx  = ['to' => null, 'from' => null];
1583        $check_from = in_array($mode, [self::MODE_DRAFT, self::MODE_EDIT, self::MODE_REPLY]);
1584
1585        // Select identity
1586        foreach ($identities as $idx => $ident) {
1587            // use From: header when in edit/draft or reply-to-self
1588            if ($check_from && $from['mailto'] == strtolower($ident['email_ascii'])) {
1589                // remember first matching identity address
1590                if ($found_idx['from'] === null) {
1591                    $found_idx['from'] = $idx;
1592                }
1593                // match identity name
1594                if ($from['name'] && $ident['name'] && $from['name'] == $ident['name']) {
1595                    $from_idx = $idx;
1596                    break;
1597                }
1598            }
1599
1600            // use replied/forwarded message recipients
1601            if (($found = array_search(strtolower($ident['email_ascii']), $a_recipients)) !== false) {
1602                // remember first matching identity address
1603                if ($found_idx['to'] === null) {
1604                    $found_idx['to'] = $idx;
1605                }
1606                // match identity name
1607                if ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name']) {
1608                    $from_idx = $idx;
1609                    break;
1610                }
1611            }
1612        }
1613
1614        // If matching by name+address didn't find any matches,
1615        // get first found identity (address) if any
1616        if ($from_idx === null) {
1617            $from_idx = $found_idx['to'] !== null ? $found_idx['to'] : $found_idx['from'];
1618        }
1619
1620        // Try Return-Path
1621        if ($from_idx === null && !empty($message->headers->others['return-path'])) {
1622            $return_path = $message->headers->others['return-path'];
1623            $return_path = array_map('strtolower', (array) $return_path);
1624
1625            foreach ($identities as $idx => $ident) {
1626                // Return-Path header contains an email address, but on some mailing list
1627                // it can be e.g. <pear-dev-return-55250-local=domain.tld@lists.php.net>
1628                // where local@domain.tld is the address we're looking for (#1489241)
1629                $ident1 = strtolower($ident['email_ascii']);
1630                $ident2 = str_replace('@', '=', $ident1);
1631                $ident1 = '<' . $ident1 . '>';
1632                $ident2 = '-' . $ident2 . '@';
1633
1634                foreach ($return_path as $path) {
1635                    if ($path == $ident1 || stripos($path, $ident2)) {
1636                        $from_idx = $idx;
1637                        break 2;
1638                    }
1639                }
1640            }
1641        }
1642
1643        // See identity_select plugin for example usage of this hook
1644        $plugin = rcmail::get_instance()->plugins->exec_hook('identity_select', [
1645                'message'    => $message,
1646                'identities' => $identities,
1647                'selected'   => $from_idx
1648        ]);
1649
1650        $selected = $plugin['selected'];
1651
1652        // default identity is always first on the list
1653        if ($selected === null) {
1654            $selected = 0;
1655        }
1656
1657        return isset($identities[$selected]) ? $identities[$selected] : null;
1658    }
1659
1660    /**
1661     * Collect message recipients' addresses
1662     *
1663     * @param Mail_Mime $message The email message
1664     */
1665    public static function collect_recipients($message)
1666    {
1667        $rcmail = rcube::get_instance();
1668
1669        // Find the addressbook source
1670        $collected_recipients = $rcmail->config->get('collected_recipients');
1671
1672        if (!strlen($collected_recipients)) {
1673            return;
1674        }
1675
1676        $source = $rcmail->get_address_book($collected_recipients);
1677
1678        if (!$source) {
1679            return;
1680        }
1681
1682        $headers = $message->headers();
1683
1684        // extract recipients
1685        $recipients = (array) $headers['To'];
1686
1687        if (!empty($headers['Cc'])) {
1688            $recipients[] = $headers['Cc'];
1689        }
1690
1691        if (!empty($headers['Bcc'])) {
1692            $recipients[] = $headers['Bcc'];
1693        }
1694
1695        $addresses = rcube_mime::decode_address_list($recipients);
1696        $type      = rcube_addressbook::TYPE_DEFAULT | rcube_addressbook::TYPE_RECIPIENT;
1697
1698        foreach ($addresses as $address) {
1699            $contact = [
1700                'name'  => $address['name'],
1701                'email' => $address['mailto'],
1702            ];
1703
1704            if (!$rcmail->contact_exists($contact['email'], $type)) {
1705                $rcmail->contact_create($contact, $source);
1706            }
1707        }
1708    }
1709}
1710