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