diff --git a/program/include/rcmail_install.php b/program/include/rcmail_install.php index d45be364d..dfd8b28a0 100644 --- a/program/include/rcmail_install.php +++ b/program/include/rcmail_install.php @@ -17,7 +17,7 @@ * Class to control the installation process of the Roundcube Webmail package * * @category Install - * @package Roundcube + * @package Webmail * @author Thomas Bruederli */ class rcmail_install diff --git a/program/include/rcmail_sendmail.php b/program/include/rcmail_sendmail.php new file mode 100644 index 000000000..2c120bab0 --- /dev/null +++ b/program/include/rcmail_sendmail.php @@ -0,0 +1,1529 @@ + | + | Author: Aleksander Machniak | + +-----------------------------------------------------------------------+ +*/ + +/** + * Common code for generating and saving/sending mail message + * with support for common user interface elements. + * + * @package Webmail + */ +class rcmail_sendmail +{ + public $data = array(); + public $options = array(); + + protected $parse_data = array(); + protected $compose_form; + + // define constants for message compose mode + const MODE_REPLY = 'reply'; + const MODE_FORWARD = 'forward'; + const MODE_DRAFT = 'draft'; + const MODE_EDIT = 'edit'; + + + /** + * Object constructor + * + * @param array $data Compose data + * @param array $options Operation options: + * savedraft (bool) - Enable save-draft mode + * sendmail (bool) - Enable send-mail mode + * saveonly (bool) - Enable save-only mode + * message (object) - Message object to get some data from + * error_handler (callback) - Error handler + */ + public function __construct($data = array(), $options = array()) + { + $this->rcmail = rcube::get_instance(); + $this->data = (array) $data; + $this->options = (array) $options; + + $this->options['sendmail_delay'] = (int) $this->rcmail->config->get('sendmail_delay'); + + if (empty($options['error_handler'])) { + $this->options['error_handler'] = function() { return false; }; + } + + if ($this->options['message']) { + $this->compose_init($this->options['message']); + } + } + + /** + * Collect input data for message headers + * + * @return array Message headers + */ + public function headers_input() + { + if ($this->options['sendmail'] && $this->options['sendmail_delay']) { + $last_time = $this->rcmail->config->get('last_message_time'); + $wait_sec = time() - $this->options['sendmail_delay'] - intval($last_time); + + if ($wait_sec < 0) { + return $this->options['error_handler']('senttooquickly', 'error', array('sec' => $wait_sec * -1)); + } + } + + // set default charset + if (!($charset = $this->options['charset'])) { + $charset = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST) ?: $this->rcmail->output->get_charset(); + $this->options['charset'] = $charset; + } + + $this->parse_data = array(); + + $mailto = $this->email_input_format(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, true, $charset), true); + $mailcc = $this->email_input_format(rcube_utils::get_input_value('_cc', rcube_utils::INPUT_POST, true, $charset), true); + $mailbcc = $this->email_input_format(rcube_utils::get_input_value('_bcc', rcube_utils::INPUT_POST, true, $charset), true); + + if ($this->parse_data['INVALID_EMAIL'] && !$this->options['savedraft']) { + return $this->options['error_handler']('emailformaterror', 'error', array('email' => $this->parse_data['INVALID_EMAIL'])); + } + + if (($max_recipients = (int) $this->rcmail->config->get('max_recipients')) > 0) { + if ($this->parse_data['RECIPIENT_COUNT'] > $max_recipients) { + return $this->options['error_handler']('toomanyrecipients', 'error', array('max' => $max_recipients)); + } + } + + if (empty($mailto) && !empty($mailcc)) { + $mailto = $mailcc; + $mailcc = null; + } + else if (empty($mailto)) { + $mailto = 'undisclosed-recipients:;'; + } + + $dont_override = (array) $this->rcmail->config->get('dont_override'); + $mdn_enabled = in_array('mdn_default', $dont_override) ? $this->rcmail->config->get('mdn_default') : !empty($_POST['_mdn']); + $dsn_enabled = in_array('dsn_default', $dont_override) ? $this->rcmail->config->get('dsn_default') : !empty($_POST['_dsn']); + $subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, true, $charset); + $from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST, true, $charset); + $replyto = rcube_utils::get_input_value('_replyto', rcube_utils::INPUT_POST, true, $charset); + $followupto = rcube_utils::get_input_value('_followupto', rcube_utils::INPUT_POST, true, $charset); + + // Get sender name and address from identity... + if (is_numeric($from)) { + if (is_array($identity_arr = $this->get_identity($from))) { + if ($identity_arr['mailto']) { + $from = $identity_arr['mailto']; + } + if ($identity_arr['string']) { + $from_string = $identity_arr['string']; + } + } + else { + $from = null; + } + } + // ... if there is no identity record, this might be a custom from + else if (($from_string = $this->email_input_format($from)) + && preg_match('/(\S+@\S+)/', $from_string, $m) + ) { + $from = trim($m[1], '<>'); + } + // ... otherwise it's empty or invalid + else { + $from = null; + } + + // check 'From' address (identity may be incomplete) + if (!$this->options['savedraft'] && !$this->options['saveonly'] && empty($from)) { + return $this->options['error_handler']('nofromaddress', 'error'); + } + + if (!$from_string && $from) { + $from_string = $from; + } + + $from_string = rcube_charset::convert($from_string, RCUBE_CHARSET, $charset); + $message_id = $this->data['param']['message-id']; + + if (!$message_id) { + $message_id = $this->rcmail->gen_message_id($from); + } + + $this->options['dsn_enabled'] = $dsn_enabled; + $this->options['from'] = $from; + $this->options['mailto'] = $mailto; + + // compose headers array + $headers = array( + 'Received' => $this->header_received(), + 'Date' => $this->rcmail->user_date(), + 'From' => $from_string, + 'To' => $mailto, + 'Cc' => $mailcc, + 'Bcc' => $mailbcc, + 'Subject' => trim($subject), + 'Reply-To' => $this->email_input_format($replyto), + 'Mail-Reply-To' => $headers['Reply-To'], + 'Mail-Followup-To' => $this->email_input_format($followupto), + 'In-Reply-To' => $this->data['reply_msgid'], + 'References' => $this->data['references'], + 'User-Agent' => $this->rcmail->config->get('useragent'), + 'Message-ID' => $message_id, + 'X-Sender' => $from, + ); + + if (!empty($identity_arr['organization'])) { + $headers['Organization'] = $identity_arr['organization']; + } + + if ($mdn_enabled) { + $headers['Return-Receipt-To'] = $from_string; + $headers['Disposition-Notification-To'] = $from_string; + } + + if (!empty($_POST['_priority'])) { + $priority = intval($_POST['_priority']); + $a_priorities = array(1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest'); + + if ($str_priority = $a_priorities[$priority]) { + $headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority)); + } + } + + // remember reply/forward UIDs in special headers + if ($this->options['savedraft']) { + // Note: We ignore . forwards/replies here + if (($uid = $this->data['reply_uid']) && !preg_match('/^\d+\.[0-9.]+$/', $uid)) { + $headers['X-Draft-Info'] = $this->draftinfo_encode(array( + 'type' => 'reply', + 'uid' => $uid, + 'folder' => $this->data['mailbox'] + )); + } + else if (!empty($this->data['forward_uid']) + && ($uid = rcube_imap_generic::compressMessageSet($this->data['forward_uid'])) + && !preg_match('/^\d+[0-9.]+$/', $uid) + ) { + $headers['X-Draft-Info'] = $this->draftinfo_encode(array( + 'type' => 'forward', + 'uid' => $uid, + 'folder' => $this->data['mailbox'] + )); + } + } + + return array_filter($headers); + } + + /** + * Set charset and transfer encoding on the message + * + * @param Mail_mime $message Message object + * @param bool $flowed Enable format=flowed + */ + public function set_message_encoding($message, $flowed = false) + { + $text_charset = $this->options['charset']; + $transfer_encoding = '7bit'; + $head_encoding = 'quoted-printable'; + + // choose encodings for plain/text body and message headers + if (preg_match('/ISO-2022/i', $text_charset)) { + $head_encoding = 'base64'; // RFC1468 + } + else if (preg_match('/[^\x00-\x7F]/', $message->getTXTBody())) { + $transfer_encoding = $this->rcmail->config->get('force_7bit') ? 'quoted-printable' : '8bit'; + } + else if ($message_charset == 'UTF-8') { + $text_charset = 'US-ASCII'; + } + + if ($flowed) { + $text_charset .= ";\r\n format=flowed"; + } + + // encoding settings for mail composing + $message->setParam('text_encoding', $transfer_encoding); + $message->setParam('html_encoding', 'quoted-printable'); + $message->setParam('head_encoding', $head_encoding); + $message->setParam('head_charset', $this->options['charset']); + $message->setParam('html_charset', $this->options['charset']); + $message->setParam('text_charset', $text_charset); + } + + /** + * Create a message to be saved/sent + * + * @param array $headers Message headers + * @param string $body Message body + * @param bool $isHtml The body is HTML or not + * @param array $attachments Optional message attachments array + * + * @return Mail_mime Message object + */ + public function create_message($headers, $body, $isHtml = false, $attachments = array()) + { + // set line length for body wrapping + $line_length = $this->rcmail->config->get('line_length', 72); + $charset = $this->options['charset']; + $flowed = $this->options['savedraft'] || $this->rcmail->config->get('send_format_flowed', true); + + // create PEAR::Mail_mime instance + $MAIL_MIME = new Mail_mime("\r\n"); + + // Check if we have enough memory to handle the message in it + // It's faster than using files, so we'll do this if we only can + if (is_array($attachments) && ($mem_limit = parse_bytes(ini_get('memory_limit')))) { + $memory = 0; + foreach ($attachments as $id => $attachment) { + $memory += $attachment['size']; + } + + // Yeah, Net_SMTP needs up to 12x more memory, 1.33 is for base64 + if (!rcube_utils::mem_check($memory * 1.33 * 12)) { + $MAIL_MIME->setParam('delay_file_io', true); + } + } + + $plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', array( + 'body' => $body, + 'type' => $isHtml ? 'html' : 'plain', + 'message' => $MAIL_MIME + )); + + // For HTML-formatted messages, construct the MIME message with both + // the HTML part and the plain-text part + if ($isHtml) { + $MAIL_MIME->setHTMLBody($plugin['body']); + + $plain_body = $this->rcmail->html2text($plugin['body'], array('width' => 0, 'charset' => $charset)); + $plain_body = rcube_mime::wordwrap($plain_body, $line_length, "\r\n", false, $charset); + $plain_body = wordwrap($plain_body, 998, "\r\n", true); + + // There's no sense to use multipart/alternative if the text/plain + // part would be blank. Completely blank text/plain part may confuse + // some mail clients (#5283) + if (strlen(trim($plain_body)) > 0) { + // make sure all line endings are CRLF (#1486712) + $plain_body = preg_replace('/\r?\n/', "\r\n", $plain_body); + + $plugin = $this->rcmail->plugins->exec_hook('message_outgoing_body', array( + 'body' => $plain_body, + 'type' => 'alternative', + 'message' => $MAIL_MIME + )); + + // add a plain text version of the e-mail as an alternative part. + $MAIL_MIME->setTXTBody($plugin['body']); + } + + // Extract image Data URIs into message attachments (#1488502) + $this->extract_inline_images($MAIL_MIME, $this->options['from']); + } + else { + $body = $plugin['body']; + + // compose format=flowed content if enabled + if ($flowed) { + $body = rcube_mime::format_flowed($body, min($line_length + 2, 79), $charset); + } + else { + $body = rcube_mime::wordwrap($body, $line_length, "\r\n", false, $charset); + } + + $body = wordwrap($body, 998, "\r\n", true); + + $MAIL_MIME->setTXTBody($body, false, true); + } + + // encoding settings for mail composing + $this->set_message_encoding($MAIL_MIME, $flowed); + + // pass headers to message object + $MAIL_MIME->headers($headers); + + return $MAIL_MIME; + } + + /** + * Message delivery, and setting Replied/Forwarded flag on success + * + * @param Mail_mime $message Message object + */ + public function deliver_message($message) + { + // Handle Delivery Status Notification request + $smtp_opts = array('dsn' => $this->options['dsn_enabled']); + + $sent = $this->rcmail->deliver_message($message, + $this->options['from'], + $this->options['mailto'], + $smtp_error, $mailbody_file, $smtp_opts, true + ); + + // return to compose page if sending failed + if (!$sent) { + // remove temp file + if ($mailbody_file) { + unlink($mailbody_file); + } + + if ($smtp_error && is_string($smtp_error)) { + return $this->options['error_handler']($smtp_error, 'error'); + } + else if ($smtp_error && !empty($smtp_error['label'])) { + return $this->options['error_handler']($smtp_error['label'], 'error', $smtp_error['vars']); + } + else { + return $this->options['error_handler']('sendingfailed', 'error'); + } + } + + $message->mailbody_file = $mailbody_file; + + // save message sent time + if ($this->options['sendmail_delay']) { + $this->rcmail->user->save_prefs(array('last_message_time' => time())); + } + + // set replied/forwarded flag + if ($this->data['reply_uid']) { + foreach (rcmail::get_uids($this->data['reply_uid'], $this->data['mailbox']) as $mbox => $uids) { + // skip . replies + if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) { + $this->rcmail->storage->set_flag($uids, 'ANSWERED', $mbox); + } + } + } + else if ($this->data['forward_uid']) { + foreach (rcmail::get_uids($this->data['forward_uid'], $this->data['mailbox']) as $mbox => $uids) { + // skip . forwards + if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) { + $this->rcmail->storage->set_flag($uids, 'FORWARDED', $mbox); + } + } + } + } + + /** + * Save the message into Drafts folder (in savedraft mode) + * or in Sent mailbox if specified/configured + * + * @param Mail_mime $message Message object + * + * @return mixed Operation status + */ + public function save_message($message) + { + // Determine which folder to save message + if ($this->options['savedraft']) { + $store_target = $this->rcmail->config->get('drafts_mbox'); + } + else if (!$this->rcmail->config->get('no_save_sent_messages')) { + if (isset($_POST['_store_target'])) { + $store_target = rcube_utils::get_input_value('_store_target', rcube_utils::INPUT_POST, true); + } + else { + $store_target = $this->rcmail->config->get('sent_mbox'); + } + } + + if ($store_target) { + $storage = $this->rcmail->get_storage(); + + // check if folder is subscribed + if ($storage->folder_exists($store_target, true)) { + $store_folder = true; + } + // folder may be existing but not subscribed (#1485241) + else if (!$storage->folder_exists($store_target)) { + $store_folder = $storage->create_folder($store_target, true); + } + else if ($storage->subscribe($store_target)) { + $store_folder = true; + } + + // append message to sent box + if ($store_folder) { + // message body in file + if ($message->mailbody_file || $message->getParam('delay_file_io')) { + $headers = $message->txtHeaders(); + + // file already created + if ($message->mailbody_file) { + $msg = $message->mailbody_file; + } + else { + $temp_dir = $this->rcmail->config->get('temp_dir'); + $mailbody_file = tempnam($temp_dir, 'rcmMsg'); + $msg = $message->saveMessageBody($mailbody_file); + + if (!is_a($msg, 'PEAR_Error')) { + $msg = $mailbody_file; + } + } + } + else { + $msg = $message->getMessage(); + $headers = ''; + } + + if (is_a($msg, 'PEAR_Error')) { + rcube::raise_error(array( + 'code' => 650, 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not create message: ".$msg->getMessage()), + true, false); + } + else { + $saved = $storage->save_message($store_target, $msg, $headers, + $message->mailbody_file ? true : false, array('SEEN')); + } + } + + // raise error if saving failed + if (!$saved) { + rcube::raise_error(array('code' => 800, 'type' => 'imap', + 'file' => __FILE__, 'line' => __LINE__, + 'message' => "Could not save message in $store_target"), true, false); + } + } + + if ($message->mailbody_file) { + unlink($message->mailbody_file); + unset($message->mailbody_file); + } + + $this->options['store_target'] = $store_target; + $this->options['store_folder'] = $store_folder; + + return $saved; + } + + /** + * If enabled, returns Received header content to be prepended + * to message headers + * + * @return string Received header content + */ + public function header_received() + { + if ($this->rcmail->config->get('http_received_header')) { + $nldlm = "\r\n\t"; + $http_header = 'from '; + + // FROM/VIA + if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { + $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2); + $http_header .= $this->received_host($hosts[0]) . $nldlm . ' via '; + } + + $http_header .= $this->received_host($_SERVER['REMOTE_ADDR']); + + // BY + $http_header .= $nldlm . 'by ' . $_SERVER['HTTP_HOST']; + + // WITH + $http_header .= $nldlm . 'with HTTP (' . $_SERVER['SERVER_PROTOCOL'] + . ' ' . $_SERVER['REQUEST_METHOD'] . '); ' . date('r'); + + return wordwrap($http_header, 69, $nldlm); + } + } + + /** + * Converts host address into host spec. for Received header + */ + protected function received_host($host) + { + $hostname = gethostbyaddr($host); + $result = $this->encrypt_host($hostname); + + if ($host != $hostname) { + $result .= ' (' . $this->encrypt_host($host) . ')'; + } + + return $result; + } + + /** + * Encrypt host IP or hostname for Received header + */ + protected function encrypt_host($host) + { + if ($this->rcmail->config->get('http_received_header_encrypt')) { + return $this->rcmail->encrypt($host); + } + + if (!preg_match('/[^0-9:.]/', $host)) { + return "[$host]"; + } + + return $host; + } + + /** + * Returns user identity record + * + * @param int $id Identity ID + * + * @return array User identity data + */ + public function get_identity($id) + { + if ($sql_arr = $this->rcmail->user->get_identity($id)) { + $out = $sql_arr; + + if ($this->options['charset'] != RCUBE_CHARSET) { + foreach ($out as $k => $v) { + $out[$k] = rcube_charset::convert($v, RCUBE_CHARSET, $this->options['charset']); + } + } + + $out['mailto'] = $sql_arr['email']; + $out['string'] = format_email_recipient($sql_arr['email'], $sql_arr['name']); + + return $out; + } + + return false; + } + + /** + * Extract image attachments from HTML message (data URIs) + * + * @param Mail_mime $message Message object + * @param string $from Sender email address + */ + public static function extract_inline_images($message, $from) + { + $body = $message->getHTMLBody(); + $offset = 0; + $list = array(); + $domain = 'localhost'; + $regexp = '#img[^>]+src=[\'"](data:([^;]*);base64,([a-z0-9+/=\r\n]+))([\'"])#i'; + + if (preg_match_all($regexp, $body, $matches, PREG_OFFSET_CAPTURE)) { + // get domain for the Content-ID, must be the same as in Mail_Mime::get() + if (preg_match('#@([0-9a-zA-Z\-\.]+)#', $from, $m)) { + $domain = $m[1]; + } + + foreach ($matches[1] as $idx => $m) { + $data = preg_replace('/\r\n/', '', $matches[3][$idx][0]); + $data = base64_decode($data); + + if (empty($data)) { + continue; + } + + $hash = md5($data) . '@' . $domain; + $mime_type = $matches[2][$idx][0]; + $name = $list[$hash]; + + if (empty($mime_type)) { + $mime_type = rcube_mime::image_content_type($data); + } + + // add the image to the MIME message + if (!$name) { + $ext = preg_replace('#^[^/]+/#', '', $mime_type); + $name = substr($hash, 0, 8) . '.' . $ext; + $list[$hash] = $name; + + $message->addHTMLImage($data, $mime_type, $name, false, $hash); + } + + $body = substr_replace($body, $name, $m[1] + $offset, strlen($m[0])); + $offset += strlen($name) - strlen($m[0]); + } + } + + $message->setHTMLBody($body); + } + + /** + * Parse and cleanup email address input (and count addresses) + * + * @param string Address input + * @param boolean Do count recipients (saved in global $RECIPIENT_COUNT) + * @param boolean Validate addresses (errors saved in global $EMAIL_FORMAT_ERROR) + * + * @return string Canonical recipients string (comma separated) + */ + public function email_input_format($mailto, $count = false, $check = true) + { + // simplified email regexp, supporting quoted local part + $email_regexp = '(\S+|("[^"]+"))@\S+'; + + $delim = ',;'; + $regexp = array("/[$delim]\s*[\r\n]+/", '/[\r\n]+/', "/[$delim]\s*\$/m", '/;/', '/(\S{1})(<'.$email_regexp.'>)/U'); + $replace = array(', ', ', ', '', ',', '\\1 \\2'); + + // replace new lines and strip ending ', ', make address input more valid + $mailto = trim(preg_replace($regexp, $replace, $mailto)); + $items = rcube_utils::explode_quoted_string("[$delim]", $mailto); + $result = array(); + + foreach ($items as $item) { + $item = trim($item); + // address in brackets without name (do nothing) + if (preg_match('/^<'.$email_regexp.'>$/', $item)) { + $item = rcube_utils::idn_to_ascii(trim($item, '<>')); + $result[] = $item; + } + // address without brackets and without name (add brackets) + else if (preg_match('/^'.$email_regexp.'$/', $item)) { + $item = rcube_utils::idn_to_ascii($item); + $result[] = $item; + } + // address with name (handle name) + else if (preg_match('/<*'.$email_regexp.'>*$/', $item, $matches)) { + $address = $matches[0]; + $name = trim(str_replace($address, '', $item)); + if ($name[0] == '"' && $name[count($name)-1] == '"') { + $name = substr($name, 1, -1); + } + $name = stripcslashes($name); + $address = rcube_utils::idn_to_ascii(trim($address, '<>')); + $result[] = format_email_recipient($address, $name); + $item = $address; + } + + // check address format + $item = trim($item, '<>'); + if ($item && $check && !rcube_utils::check_email($item)) { + $this->parse_data['INVALID_EMAIL'] = $item; + return; + } + } + + if ($count) { + $this->parse_data['RECIPIENT_COUNT'] += count($result); + } + + return implode(', ', $result); + } + + /** + * Returns configured generic message footer + * + * @param bool $isHtml Return HTML or Plain text version of the footer? + * + * @return string Footer content + */ + public function generic_message_footer($isHtml) + { + if ($isHtml && ($file = $this->rcmail->config->get('generic_message_footer_html'))) { + $html_footer = true; + } + else { + $file = $this->rcmail->config->get('generic_message_footer'); + $html_footer = false; + } + + if ($file && realpath($file)) { + // sanity check + if (!preg_match('/\.(php|ini|conf)$/', $file) && strpos($file, '/etc/') === false) { + $footer = file_get_contents($file); + if ($isHtml && !$html_footer) { + $t2h = new rcube_text2html($footer, false); + $footer = $t2h->get_html(); + } + + if ($this->options['charset'] && $this->options['charset'] != RCUBE_CHARSET) { + $footer = rcube_charset::convert($footer, RCUBE_CHARSET, $this->options['charset']); + } + + return $footer; + } + } + + return false; + } + + /** + * Encode data array into a string for use in X-Draft-Info header + * + * @param array $data Data array + * + * @return string Decoded data as a string + */ + public static function draftinfo_encode($data) + { + $parts = array(); + foreach ($data as $key => $val) { + $encode = $key == 'folder' || strpos($val, ';') !== false; + $parts[] = $key . '=' . ($encode ? 'B::' . base64_encode($val) : $val); + } + + return join('; ', $parts); + } + + /** + * Decode X-Draft-Info header value into an array + * + * @param string $str Encoded data string (see self::draftinfo_encode()) + * + * @return array Decoded data + */ + public static function draftinfo_decode($str) + { + $info = array(); + + foreach (preg_split('/;\s+/', $str) as $part) { + list($key, $val) = explode('=', $part, 2); + if (strpos($val, 'B::') === 0) { + $val = base64_decode(substr($val, 3)); + } + else if ($key == 'folder') { + $val = base64_decode($val); + } + + $info[$key] = $val; + } + + return $info; + } + + /** + * Header (From, To, Cc, etc.) input object for templates + */ + public function headers_output($attrib) + { + list($form_start,) = $this->form_tags($attrib); + + $out = ''; + $part = strtolower($attrib['part']); + + switch ($part) { + case 'from': + return $form_start . $this->compose_header_from($attrib); + + case 'to': + case 'cc': + case 'bcc': + $fname = '_' . $part; + $header = $param = $part; + + $allow_attrib = array('id', 'class', 'style', 'cols', 'rows', 'tabindex'); + $field_type = 'html_textarea'; + break; + + case 'replyto': + case 'reply-to': + $fname = '_replyto'; + $param = 'replyto'; + $header = 'reply-to'; + + case 'followupto': + case 'followup-to': + if (!$fname) { + $fname = '_followupto'; + $param = 'followupto'; + $header = 'mail-followup-to'; + } + + $allow_attrib = array('id', 'class', 'style', 'size', 'tabindex'); + $field_type = 'html_inputfield'; + break; + } + + if ($fname && $field_type) { + // pass the following attributes to the form class + $field_attrib = array('name' => $fname, 'spellcheck' => 'false'); + foreach ($attrib as $attr => $value) { + if (stripos($attr, 'data-') === 0 || in_array($attr, $allow_attrib)) { + $field_attrib[$attr] = $value; + } + } + + // create teaxtarea object + $input = new $field_type($field_attrib); + $out = $input->show($this->compose_header_value($param, $this->data['mode'])); + } + + if ($form_start) { + $out = $form_start . $out; + } + + // configure autocompletion + $this->rcmail->autocomplete_init(); + + return $out; + } + + /** + * Returns From header input element + */ + protected function compose_header_from($attrib) + { + global $MESSAGE, $OUTPUT, $RCMAIL, $COMPOSE; + + // pass the following attributes to the form class + $field_attrib = array('name' => '_from'); + foreach ($attrib as $attr => $value) { + if (in_array($attr, array('id', 'class', 'style', 'size', 'tabindex'))) { + $field_attrib[$attr] = $value; + } + } + + if (count($this->options['message']->identities)) { + $a_signatures = array(); + $identities = array(); + $top_posting = intval($this->rcmail->config->get('reply_mode')) > 0 + && !$this->rcmail->config->get('sig_below') + && ($this->data['mode'] == self::MODE_REPLY || $this->data['mode'] == self::MODE_FORWARD); + + $separator = $top_posting ? '---' : '-- '; + $add_separator = (bool) $this->rcmail->config->get('sig_separator'); + + $field_attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . ".change_identity(this)"; + $select_from = new html_select($field_attrib); + + // create SELECT element + foreach ($this->options['message']->identities as $sql_arr) { + $identity_id = $sql_arr['identity_id']; + $select_from->add(format_email_recipient($sql_arr['email'], $sql_arr['name']), $identity_id); + + // add signature to array + if (!empty($sql_arr['signature']) && empty($this->data['param']['nosig'])) { + $text = $html = $sql_arr['signature']; + + if ($sql_arr['html_signature']) { + $text = $this->rcmail->html2text($html, array('links' => false)); + $text = trim($text, "\r\n"); + } + else { + $t2h = new rcube_text2html($text, false); + $html = $t2h->get_html(); + } + + if ($add_separator && !preg_match('/^--[ -]\r?\n/m', $text)) { + $text = $separator . "\n" . ltrim($text, "\r\n"); + $html = $separator . "
" . $html; + } + + $a_signatures[$identity_id]['text'] = $text; + $a_signatures[$identity_id]['html'] = $html; + } + + // add bcc and reply-to + if (!empty($sql_arr['reply-to'])) { + $identities[$identity_id]['replyto'] = $sql_arr['reply-to']; + } + if (!empty($sql_arr['bcc'])) { + $identities[$identity_id]['bcc'] = $sql_arr['bcc']; + } + + $identities[$identity_id]['email'] = $sql_arr['email']; + } + + $out = $select_from->show($this->options['message']->compose['from']); + + // add signatures to client + $this->rcmail->output->set_env('signatures', $a_signatures); + $this->rcmail->output->set_env('identities', $identities); + } + // no identities, display text input field + else { + $field_attrib['class'] = 'from_address'; + $input_from = new html_inputfield($field_attrib); + $out = $input_from->show($this->options['message']->compose['from']); + } + + return $out; + } + + /** + * Set the value of specified header depending on compose mode + */ + protected function compose_header_value($header, $mode) + { + $fvalue = ''; + $decode_header = true; + $message = $this->options['message']; + $charset = $message->headers->charset; + $separator = ', '; + + // we have a set of recipients stored is session + if ($header == 'to' && ($mailto_id = $this->data['param']['mailto']) + && $_SESSION['mailto'][$mailto_id] + ) { + $fvalue = urldecode($_SESSION['mailto'][$mailto_id]); + $decode_header = false; + $charset = $this->rcmail->output->charset; + + // make session to not grow up too much + $this->rcmail->session->remove("mailto.$mailto_id"); + } + else if (!empty($_POST['_' . $header])) { + $fvalue = rcube_utils::get_input_value('_' . $header, rcube_utils::INPUT_POST, true); + $charset = $this->rcmail->output->charset; + } + else if (!empty($COMPOSE['param'][$header])) { + $fvalue = $COMPOSE['param'][$header]; + $charset = $RCMAIL->output->charset; + } + else if ($mode == self::MODE_REPLY) { + // get recipent address(es) out of the message headers + if ($header == 'to') { + $mailfollowup = $message->headers->others['mail-followup-to']; + $mailreplyto = $message->headers->others['mail-reply-to']; + + // Reply to mailing list... + if ($message->reply_all == 'list' && $mailfollowup) { + $fvalue = $mailfollowup; + } + else if ($message->reply_all == 'list' + && preg_match('/]+)>/i', $message->headers->others['list-post'], $m) + ) { + $fvalue = $m[1]; + } + // Reply to... + else if ($message->reply_all && $mailfollowup) { + $fvalue = $mailfollowup; + } + else if ($mailreplyto) { + $fvalue = $mailreplyto; + } + else if (!empty($message->headers->replyto)) { + $fvalue = $message->headers->replyto; + $replyto = true; + } + else if (!empty($message->headers->from)) { + $fvalue = $message->headers->from; + } + + // Reply to message sent by yourself (#1487074, #1489230, #1490439) + // Reply-To address need to be unset (#1490233) + if (!empty($message->compose['ident']) && empty($replyto)) { + foreach (array($fvalue, $message->headers->from) as $sender) { + $senders = rcube_mime::decode_address_list($sender, null, false, $charset, true); + + if (in_array($message->compose['ident']['email_ascii'], $senders)) { + $fvalue = $message->headers->to; + break; + } + } + } + } + // add recipient of original message if reply to all + else if ($header == 'cc' && !empty($message->reply_all) && $message->reply_all != 'list') { + if ($v = $message->headers->to) { + $fvalue .= $v; + } + if ($v = $message->headers->cc) { + $fvalue .= (!empty($fvalue) ? $separator : '') . $v; + } + // Use Sender header (#1489011) + if ($v = $message->headers->get('Sender', false)) { + // Skip common mailing lists addresses: *-bounces@ and *-request@ (#1490452) + if (empty($message->headers->others['list-post']) + || !preg_match('/-(bounces|request)@/', $v) + ) { + $fvalue .= (!empty($fvalue) ? $separator : '') . $v; + } + } + + // When To: and Reply-To: are the same we add From: address to the list (#1489037) + if ($v = $message->headers->from) { + $to = $message->headers->to; + $replyto = $message->headers->replyto; + $from = rcube_mime::decode_address_list($v, null, false, $charset, true); + $to = rcube_mime::decode_address_list($to, null, false, $charset, true); + $replyto = rcube_mime::decode_address_list($replyto, null, false, $charset, true); + + if (count($replyto) && !count(array_diff($to, $replyto)) && count(array_diff($from, $to))) { + $fvalue .= (!empty($fvalue) ? $separator : '') . $v; + } + } + } + } + else if (in_array($mode, array(self::MODE_DRAFT, self::MODE_EDIT))) { + // get drafted headers + if ($header == 'to' && !empty($message->headers->to)) { + $fvalue = $message->get_header('to', true); + } + else if ($header == 'cc' && !empty($message->headers->cc)) { + $fvalue = $message->get_header('cc', true); + } + else if ($header == 'bcc' && !empty($message->headers->bcc)) { + $fvalue = $message->get_header('bcc', true); + } + else if ($header == 'replyto' && !empty($message->headers->others['mail-reply-to'])) { + $fvalue = $message->get_header('mail-reply-to'); + } + else if ($header == 'replyto' && !empty($message->headers->replyto)) { + $fvalue = $message->get_header('reply-to'); + } + else if ($header == 'followupto' && !empty($message->headers->others['mail-followup-to'])) { + $fvalue = $message->get_header('mail-followup-to'); + } + } + + // split recipients and put them back together in a unique way + if (!empty($fvalue) && in_array($header, array('to', 'cc', 'bcc'))) { + $from_email = @mb_strtolower($message->compose['ident']['email']); + $to_addresses = rcube_mime::decode_address_list($fvalue, null, $decode_header, $charset); + $fvalue = array(); + + foreach ($to_addresses as $addr_part) { + if (empty($addr_part['mailto'])) { + continue; + } + + // According to RFC5321 local part of email address is case-sensitive + // however, here it is better to compare addresses in case-insensitive manner + $mailto = format_email(rcube_utils::idn_to_utf8($addr_part['mailto'])); + $mailto_lc = mb_strtolower($addr_part['mailto']); + + if (($header == 'to' || $mode != self::MODE_REPLY || $mailto_lc != $from_email) + && !in_array($mailto_lc, (array) $message->recipients) + ) { + if ($addr_part['name'] && $mailto != $addr_part['name']) { + $mailto = format_email_recipient($mailto, $addr_part['name']); + } + + $fvalue[] = $mailto; + $message->recipients[] = $mailto_lc; + } + } + + $fvalue = implode($separator, $fvalue); + } + + return $fvalue; + } + + /** + * Creates reply subject by removing common subject + * prefixes/suffixes from the original message subject + * + * @param string $subject Subject string + * + * @return string Modified subject string + */ + public static function reply_subject($subject) + { + $subject = trim($subject); + + // replace Re:, Re[x]:, Re-x (#1490497) + $prefix = '/^(re:|re\[\d\]:|re-\d:)\s*/i'; + do { + $subject = preg_replace($prefix, '', $subject, -1, $count); + } + while ($count); + + // replace (was: ...) (#1489375) + $subject = preg_replace('/\s*\([wW]as:[^\)]+\)\s*$/', '', $subject); + + return 'Re: ' . $subject; + } + + /** + * Subject input object for templates + */ + public function compose_subject($attrib) + { + list($form_start, $form_end) = $this->form_tags($attrib); + unset($attrib['form']); + + $attrib['name'] = '_subject'; + $attrib['spellcheck'] = 'true'; + + $textfield = new html_inputfield($attrib); + $subject = ''; + + // use subject from post + if (isset($_POST['_subject'])) { + $subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE); + } + else if (!empty($this->data['param']['subject'])) { + $subject = $this->data['param']['subject']; + } + // create a reply-subject + else if ($this->data['mode'] == self::MODE_REPLY) { + $subject = self::reply_subject($this->options['message']->subject); + } + // create a forward-subject + else if ($this->data['mode'] == self::MODE_FORWARD) { + if (preg_match('/^fwd:/i', $this->options['message']->subject)) { + $subject = $this->options['message']->subject; + } + else { + $subject = 'Fwd: ' . $this->options['message']->subject; + } + } + // creeate a draft-subject + else if ($this->data['mode'] == self::MODE_DRAFT || $this->data['mode'] == self::MODE_EDIT) { + $subject = $this->options['message']->subject; + } + + $out = $form_start ? "$form_start\n" : ''; + $out .= $textfield->show($subject); + $out .= $form_end ? "\n$form_end" : ''; + + return $out; + } + + /** + * Returns compose form tag (if not used already) + */ + public function form_tags($attrib) + { + if (rcube_utils::get_boolean((string) $attrib['noform'])) { + return array('', ''); + } + + $form_start = ''; + if (!$this->message_form) { + $hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $this->rcmail->task)); + $hiddenfields->add(array('name' => '_action', 'value' => 'send')); + $hiddenfields->add(array('name' => '_id', 'value' => $this->data['id'])); + $hiddenfields->add(array('name' => '_attachments')); + + $form_start = empty($attrib['form']) ? $this->rcmail->output->form_tag(array('name' => "form", 'method' => "post")) : ''; + $form_start .= $hiddenfields->show(); + } + + $form_end = ($this->message_form && !strlen($attrib['form'])) ? '' : ''; + $form_name = $attrib['form'] ?: 'form'; + + if (!$this->message_form) { + $this->rcmail->output->add_gui_object('messageform', $form_name); + } + + $this->message_form = $form_name; + + return array($form_start, $form_end); + } + + /** + * Returns compose form "head" + */ + public function form_head($attrib) + { + list($form_start,) = $this->form_tags($attrib); + + return $form_start; + } + + /** + * Folder selector object for templates + */ + public function folder_selector($attrib) + { + $attrib['name'] = '_store_target'; + $select = $this->rcmail->folder_selector(array_merge($attrib, array( + 'noselection' => '- ' . $this->rcmail->gettext('dontsave') . ' -', + 'folder_filter' => 'mail', + 'folder_rights' => 'w', + ))); + + return $select->show(isset($_POST['_store_target']) ? $_POST['_store_target'] : $this->data['param']['sent_mbox'], $attrib); + } + + /** + * Mail Disposition Notification checkbox object for templates + */ + public function mdn_checkbox($attrib) + { + list($form_start, $form_end) = $this->form_tags($attrib); + unset($attrib['form']); + + if (!isset($attrib['id'])) { + $attrib['id'] = 'receipt'; + } + + $attrib['name'] = '_mdn'; + $attrib['value'] = '1'; + + $checkbox = new html_checkbox($attrib); + + if (isset($_POST['_mdn'])) { + $mdn_default = $_POST['_mdn']; + } + else if (in_array($this->data['mode'], array(self::MODE_DRAFT, self::MODE_EDIT))) { + $mdn_default = (bool) $this->options['message']->headers->mdn_to; + } + else { + $mdn_default = $this->rcmail->config->get('mdn_default'); + } + + $out = $form_start ? "$form_start\n" : ''; + $out .= $checkbox->show($mdn_default); + $out .= $form_end ? "\n$form_end" : ''; + + return $out; + } + + /** + * Delivery Status Notification checkbox object for templates + */ + public function dsn_checkbox($attrib) + { + list($form_start, $form_end) = $this->form_tags($attrib); + unset($attrib['form']); + + if (!isset($attrib['id'])) { + $attrib['id'] = 'dsn'; + } + + $attrib['name'] = '_dsn'; + $attrib['value'] = '1'; + + $checkbox = new html_checkbox($attrib); + + if (isset($_POST['_dsn'])) { + $dsn_value = (int) $_POST['_dsn']; + } + else { + $dsn_value = $this->rcmail->config->get('dsn_default'); + } + + $out = $form_start ? "$form_start\n" : ''; + $out .= $checkbox->show($dsn_value); + $out .= $form_end ? "\n$form_end" : ''; + + return $out; + } + + /** + * Priority selector object for templates + */ + public function priority_selector($attrib) + { + list($form_start, $form_end) = $this->form_tags($attrib); + unset($attrib['form']); + + $attrib['name'] = '_priority'; + $prio_list = array( + $this->rcmail->gettext('lowest') => 5, + $this->rcmail->gettext('low') => 4, + $this->rcmail->gettext('normal') => 0, + $this->rcmail->gettext('high') => 2, + $this->rcmail->gettext('highest') => 1, + ); + + $selector = new html_select($attrib); + $selector->add(array_keys($prio_list), array_values($prio_list)); + + if (isset($_POST['_priority'])) { + $sel = (int) $_POST['_priority']; + } + else if (isset($this->options['message']->headers->priority) + && intval($this->options['message']->headers->priority) != 3 + ) { + $sel = (int) $this->options['message']->headers->priority; + } + else { + $sel = 0; + } + + $out = $form_start ? "$form_start\n" : ''; + $out .= $selector->show((int) $sel); + $out .= $form_end ? "\n$form_end" : ''; + + return $out; + } + + /** + * Helper to create Sent folder if not exists + */ + public static function check_sent_folder($folder, $create = false) + { + $rcmail = rcmail::get_instance(); + + // we'll not save the message, so it doesn't matter + if ($rcmail->config->get('no_save_sent_messages')) { + return true; + } + + if ($rcmail->storage->folder_exists($folder, true)) { + return true; + } + + // folder may exist but isn't subscribed (#1485241) + if ($create) { + if (!$rcmail->storage->folder_exists($folder)) + return $rcmail->storage->create_folder($folder, true); + else + return $rcmail->storage->subscribe($folder); + } + + return false; + } + + /** + * Initialize mail compose UI elements + */ + protected function compose_init($message) + { + $message->compose = array(); + + // get user's identities + $message->identities = $this->rcmail->user->list_identities(null, true); + + // Set From field value + if (!empty($_POST['_from'])) { + $message->compose['from'] = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST); + } + else if (!empty($this->data['param']['from'])) { + $message->compose['from'] = $this->data['param']['from']; + } + else if (count($message->identities)) { + $ident = self::identity_select($message, $message->identities, $this->data['mode']); + + $message->compose['from'] = $ident['identity_id']; + $message->compose['ident'] = $ident; + } + + $this->rcmail->output->add_handlers(array( + 'storetarget' => array($this, 'folder_selector'), + 'composeheaders' => array($this, 'headers_output'), + 'composesubject' => array($this, 'compose_subject'), + 'priorityselector' => array($this, 'priority_selector'), + 'mdncheckbox' => array($this, 'mdn_checkbox'), + 'dsncheckbox' => array($this, 'dsn_checkbox'), + 'composeformhead' => array($this, 'form_head'), + )); + + $this->rcmail->output->set_env('max_disclosed_recipients', (int) $this->rcmail->config->get('max_disclosed_recipients', 5)); + } + + /** + * Detect recipient identity from specified message + * + * @param rcube_message $message Message object + * @param array $identities User identities (if NULL all user identities will be used) + * @param string $mode Composing mode (see self::MODE_*) + * + * @return array Selected user identity (or the default identity) data + */ + public static function identity_select($message, $identities = null, $mode = null) + { + $a_recipients = array(); + $a_names = array(); + + if ($identities === null) { + $identities = rcmail::get_instance()->user->list_identities(null, true); + } + + if (!$mode) { + $mode = self::MODE_REPLY; + } + + // extract all recipients of the reply-message + if (is_object($message->headers) && in_array($mode, array(self::MODE_REPLY, self::MODE_FORWARD))) { + $a_to = rcube_mime::decode_address_list($message->headers->to, null, true, $message->headers->charset); + foreach ($a_to as $addr) { + if (!empty($addr['mailto'])) { + $a_recipients[] = strtolower($addr['mailto']); + $a_names[] = $addr['name']; + } + } + + if (!empty($message->headers->cc)) { + $a_cc = rcube_mime::decode_address_list($message->headers->cc, null, true, $message->headers->charset); + foreach ($a_cc as $addr) { + if (!empty($addr['mailto'])) { + $a_recipients[] = strtolower($addr['mailto']); + $a_names[] = $addr['name']; + } + } + } + } + + // decode From: address + $from = rcube_mime::decode_address_list($message->headers->from, null, true, $message->headers->charset); + $from = array_shift($from); + $from['mailto'] = strtolower($from['mailto']); + + $from_idx = null; + $found_idx = array('to' => null, 'from' => null); + $check_from = in_array($mode, array(self::MODE_DRAFT, self::MODE_EDIT, self::MODE_REPLY)); + + // Select identity + foreach ($identities as $idx => $ident) { + // use From: header when in edit/draft or reply-to-self + if ($check_from && $from['mailto'] == strtolower($ident['email_ascii'])) { + // remember first matching identity address + if ($found_idx['from'] === null) { + $found_idx['from'] = $idx; + } + // match identity name + if ($from['name'] && $ident['name'] && $from['name'] == $ident['name']) { + $from_idx = $idx; + break; + } + } + // use replied/forwarded message recipients + else if (($found = array_search(strtolower($ident['email_ascii']), $a_recipients)) !== false) { + // remember first matching identity address + if ($found_idx['to'] === null) { + $found_idx['to'] = $idx; + } + // match identity name + if ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name']) { + $from_idx = $idx; + break; + } + } + } + + // If matching by name+address didn't find any matches, + // get first found identity (address) if any + if ($from_idx === null) { + $from_idx = $found_idx['from'] !== null ? $found_idx['from'] : $found_idx['to']; + } + + // Try Return-Path + if ($from_idx === null && ($return_path = $message->headers->others['return-path'])) { + $return_path = array_map('strtolower', (array) $return_path); + + foreach ($identities as $idx => $ident) { + // Return-Path header contains an email address, but on some mailing list + // it can be e.g. + // where local@domain.tld is the address we're looking for (#1489241) + $ident1 = strtolower($ident['email_ascii']); + $ident2 = str_replace('@', '=', $ident1); + $ident1 = '<' . $ident1 . '>'; + $ident2 = '-' . $ident2 . '@'; + + foreach ($return_path as $path) { + if ($path == $ident1 || stripos($path, $ident2)) { + $from_idx = $idx; + break 2; + } + } + } + } + + // See identity_select plugin for example usage of this hook + $plugin = rcmail::get_instance()->plugins->exec_hook('identity_select', array( + 'message' => $message, + 'identities' => $identities, + 'selected' => $from_idx + )); + + $selected = $plugin['selected']; + + // default identity is always first on the list + return $identities[$selected !== null ? $selected : 0]; + } +} diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc index 0a8486a77..07656bf23 100644 --- a/program/steps/mail/compose.inc +++ b/program/steps/mail/compose.inc @@ -5,7 +5,7 @@ | program/steps/mail/compose.inc | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2016, The Roundcube Dev Team | + | Copyright (C) 2005-2017, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -19,18 +19,12 @@ +-----------------------------------------------------------------------+ */ -// define constants for message compose mode -define('RCUBE_COMPOSE_REPLY', 'reply'); -define('RCUBE_COMPOSE_FORWARD', 'forward'); -define('RCUBE_COMPOSE_DRAFT', 'draft'); -define('RCUBE_COMPOSE_EDIT', 'edit'); +$COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GET); +$COMPOSE = null; -$MESSAGE_FORM = null; -$COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GET); -$COMPOSE = null; - -if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) - $COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID]; +if ($COMPOSE_ID && $_SESSION['compose_data_'.$COMPOSE_ID]) { + $COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID]; +} // give replicated session storage some time to synchronize $retries = 0; @@ -67,7 +61,7 @@ if (!is_array($COMPOSE)) { // check if folder for saving sent messages exists and is subscribed (#1486802) if ($sent_folder = $COMPOSE['param']['sent_mbox']) { - rcmail_check_sent_folder($sent_folder, true); + rcmail_sendmail::check_sent_folder($sent_folder, true); } // redirect to a unique URL with all parameters stored in session @@ -99,7 +93,6 @@ $OUTPUT->set_env('mailbox', $RCMAIL->storage->get_folder()); $OUTPUT->set_env('top_posting', intval($RCMAIL->config->get('reply_mode')) > 0); $OUTPUT->set_env('sig_below', $RCMAIL->config->get('sig_below')); $OUTPUT->set_env('save_localstorage', (bool)$RCMAIL->config->get('compose_save_localstorage')); -$OUTPUT->set_env('max_disclosed_recipients', (int) $RCMAIL->config->get('max_disclosed_recipients', 5)); $OUTPUT->set_env('is_sent', false); $OUTPUT->set_env('mimetypes', rcmail_supported_mimetypes()); @@ -125,20 +118,20 @@ if ($font_size = $RCMAIL->config->get('default_font_size')) { // get reference message and set compose mode if ($msg_uid = $COMPOSE['param']['draft_uid']) { - $compose_mode = RCUBE_COMPOSE_DRAFT; + $compose_mode = rcmail_sendmail::MODE_DRAFT; $OUTPUT->set_env('draft_id', $msg_uid); $RCMAIL->storage->set_folder($drafts_mbox); } else if ($msg_uid = $COMPOSE['param']['reply_uid']) { - $compose_mode = RCUBE_COMPOSE_REPLY; + $compose_mode = rcmail_sendmail::MODE_REPLY; } else if ($msg_uid = $COMPOSE['param']['forward_uid']) { - $compose_mode = RCUBE_COMPOSE_FORWARD; + $compose_mode = rcmail_sendmail::MODE_FORWARD; $COMPOSE['forward_uid'] = $msg_uid; $COMPOSE['as_attachment'] = !empty($COMPOSE['param']['attachment']); } else if ($msg_uid = $COMPOSE['param']['uid']) { - $compose_mode = RCUBE_COMPOSE_EDIT; + $compose_mode = rcmail_sendmail::MODE_EDIT; } if ($compose_mode) { @@ -146,7 +139,7 @@ if ($compose_mode) { $OUTPUT->set_env('compose_mode', $compose_mode); } -if ($compose_mode == RCUBE_COMPOSE_EDIT || $compose_mode == RCUBE_COMPOSE_DRAFT) { +if ($compose_mode == rcmail_sendmail::MODE_EDIT || $compose_mode == rcmail_sendmail::MODE_DRAFT) { // don't add signature in draft/edit mode, we'll also not remove the old-one // but only on page display, later we should be able to change identity/sig (#1489229) if ($config_show_sig == 1 || $config_show_sig == 2) { @@ -157,7 +150,7 @@ else if ($config_show_sig == 1) $OUTPUT->set_env('show_sig', true); else if ($config_show_sig == 2 && empty($compose_mode)) $OUTPUT->set_env('show_sig', true); -else if ($config_show_sig == 3 && ($compose_mode == RCUBE_COMPOSE_REPLY || $compose_mode == RCUBE_COMPOSE_FORWARD)) +else if ($config_show_sig == 3 && ($compose_mode == rcmail_sendmail::MODE_REPLY || $compose_mode == rcmail_sendmail::MODE_FORWARD)) $OUTPUT->set_env('show_sig', true); // set line length for body wrapping @@ -173,7 +166,7 @@ if (!empty($msg_uid) && empty($COMPOSE['as_attachment'])) { } else { $prefer_html = $RCMAIL->config->get('prefer_html') || $RCMAIL->config->get('htmleditor') - || $compose_mode == RCUBE_COMPOSE_DRAFT || $compose_mode == RCUBE_COMPOSE_EDIT; + || $compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT; $RCMAIL->config->set('prefer_html', $prefer_html); } @@ -192,8 +185,8 @@ if (!empty($msg_uid) && empty($COMPOSE['as_attachment'])) { if (!$MESSAGE->headers) { // error } - else if ($compose_mode == RCUBE_COMPOSE_FORWARD || $compose_mode == RCUBE_COMPOSE_REPLY) { - if ($compose_mode == RCUBE_COMPOSE_REPLY) { + else if ($compose_mode == rcmail_sendmail::MODE_FORWARD || $compose_mode == rcmail_sendmail::MODE_REPLY) { + if ($compose_mode == rcmail_sendmail::MODE_REPLY) { $COMPOSE['reply_uid'] = $MESSAGE->context === null ? $msg_uid : null; if (!empty($COMPOSE['param']['all'])) { @@ -209,16 +202,16 @@ if (!empty($msg_uid) && empty($COMPOSE['as_attachment'])) { // Save the sent message in the same folder of the message being replied to if ($RCMAIL->config->get('reply_same_folder') && ($sent_folder = $COMPOSE['mailbox']) - && rcmail_check_sent_folder($sent_folder, false) + && rcmail_sendmail::check_sent_folder($sent_folder, false) ) { $COMPOSE['param']['sent_mbox'] = $sent_folder; } } - else if ($compose_mode == RCUBE_COMPOSE_DRAFT || $compose_mode == RCUBE_COMPOSE_EDIT) { - if ($compose_mode == RCUBE_COMPOSE_DRAFT) { + else if ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) { + if ($compose_mode == rcmail_sendmail::MODE_DRAFT) { if ($draft_info = $MESSAGE->headers->get('x-draft-info')) { // get reply_uid/forward_uid to flag the original message when sending - $info = rcmail_draftinfo_decode($draft_info); + $info = rcmail_sendmail::draftinfo_decode($draft_info); if ($info['type'] == 'reply') $COMPOSE['reply_uid'] = $info['uid']; @@ -229,7 +222,7 @@ if (!empty($msg_uid) && empty($COMPOSE['as_attachment'])) { // Save the sent message in the same folder of the message being replied to if ($RCMAIL->config->get('reply_same_folder') && ($sent_folder = $info['folder']) - && rcmail_check_sent_folder($sent_folder, false) + && rcmail_sendmail::check_sent_folder($sent_folder, false) ) { $COMPOSE['param']['sent_mbox'] = $sent_folder; } @@ -265,51 +258,27 @@ if (!empty($COMPOSE['reply_msgid'])) { $OUTPUT->set_env('reply_msgid', $COMPOSE['reply_msgid']); } -$MESSAGE->compose = array(); - -// get user's identities -$MESSAGE->identities = $RCMAIL->user->list_identities(null, true); - -// Set From field value -if (!empty($_POST['_from'])) { - $MESSAGE->compose['from'] = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST); -} -else if (!empty($COMPOSE['param']['from'])) { - $MESSAGE->compose['from'] = $COMPOSE['param']['from']; -} -else if (count($MESSAGE->identities)) { - $ident = rcmail_identity_select($MESSAGE, $MESSAGE->identities, $compose_mode); - - $MESSAGE->compose['from'] = $ident['identity_id']; - $MESSAGE->compose['ident'] = $ident; -} +// Initialize helper class to build the UI +$SENDMAIL = new rcmail_sendmail($COMPOSE, array('message' => $MESSAGE)); // process $MESSAGE body/attachments, set $MESSAGE_BODY/$HTML_MODE vars and some session data $MESSAGE_BODY = rcmail_prepare_message_body(); -$OUTPUT->include_script('publickey.js'); - -// register UI objects +// register UI objects (Note: some objects are registered by rcmail_sendmail above) $OUTPUT->add_handlers(array( - 'composeformhead' => 'rcmail_compose_form_head', - 'composeheaders' => 'rcmail_compose_headers', - 'composesubject' => 'rcmail_compose_subject', 'composebody' => 'rcmail_compose_body', 'composeattachmentlist' => 'rcmail_compose_attachment_list', 'composeattachmentform' => 'rcmail_compose_attachment_form', 'composeattachment' => 'rcmail_compose_attachment_field', - 'filedroparea' => 'compose_file_drop_area', - 'priorityselector' => 'rcmail_priority_selector', + 'filedroparea' => 'rcmail_compose_file_drop_area', 'editorselector' => 'rcmail_editor_selector', - 'receiptcheckbox' => 'rcmail_mdn_checkbox', // deprecated - 'mdncheckbox' => 'rcmail_mdn_checkbox', - 'dsncheckbox' => 'rcmail_dsn_checkbox', - 'storetarget' => 'rcmail_store_target_selection', 'addressbooks' => 'rcmail_addressbook_list', 'addresslist' => 'rcmail_contacts_list', 'responseslist' => 'rcmail_compose_responses_list', )); +$OUTPUT->include_script('publickey.js'); + $OUTPUT->send('compose'); @@ -412,316 +381,6 @@ function rcmail_process_compose_params(&$COMPOSE) } } -function rcmail_compose_form_head($attrib) -{ - list($form_start,) = get_form_tags($attrib); - - return $form_start; -} - -function rcmail_compose_headers($attrib) -{ - global $RCMAIL, $MESSAGE; - - list($form_start,) = get_form_tags($attrib); - - $out = ''; - $part = strtolower($attrib['part']); - - switch ($part) { - case 'from': - return $form_start . rcmail_compose_header_from($attrib); - - case 'to': - case 'cc': - case 'bcc': - $fname = '_' . $part; - $header = $param = $part; - - $allow_attrib = array('id', 'class', 'style', 'cols', 'rows', 'tabindex'); - $field_type = 'html_textarea'; - break; - - case 'replyto': - case 'reply-to': - $fname = '_replyto'; - $param = 'replyto'; - $header = 'reply-to'; - - case 'followupto': - case 'followup-to': - if (!$fname) { - $fname = '_followupto'; - $param = 'followupto'; - $header = 'mail-followup-to'; - } - - $allow_attrib = array('id', 'class', 'style', 'size', 'tabindex'); - $field_type = 'html_inputfield'; - break; - } - - if ($fname && $field_type) { - // pass the following attributes to the form class - $field_attrib = array('name' => $fname, 'spellcheck' => 'false'); - foreach ($attrib as $attr => $value) { - if (stripos($attr, 'data-') === 0 || in_array($attr, $allow_attrib)) { - $field_attrib[$attr] = $value; - } - } - - // create teaxtarea object - $input = new $field_type($field_attrib); - $out = $input->show(rcmail_compose_header_value($param)); - } - - if ($form_start) { - $out = $form_start . $out; - } - - // configure autocompletion - $RCMAIL->autocomplete_init(); - - return $out; -} - - -function rcmail_compose_header_from($attrib) -{ - global $MESSAGE, $OUTPUT, $RCMAIL, $COMPOSE; - - // pass the following attributes to the form class - $field_attrib = array('name' => '_from'); - foreach ($attrib as $attr => $value) { - if (in_array($attr, array('id', 'class', 'style', 'size', 'tabindex'))) { - $field_attrib[$attr] = $value; - } - } - - if (count($MESSAGE->identities)) { - $a_signatures = array(); - $identities = array(); - $top_posting = intval($RCMAIL->config->get('reply_mode')) > 0 - && !$RCMAIL->config->get('sig_below') - && ($COMPOSE['mode'] == RCUBE_COMPOSE_REPLY || $COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD); - - $separator = $top_posting ? '---' : '-- '; - $add_separator = (bool) $RCMAIL->config->get('sig_separator'); - - $field_attrib['onchange'] = rcmail_output::JS_OBJECT_NAME.".change_identity(this)"; - $select_from = new html_select($field_attrib); - - // create SELECT element - foreach ($MESSAGE->identities as $sql_arr) { - $identity_id = $sql_arr['identity_id']; - $select_from->add(format_email_recipient($sql_arr['email'], $sql_arr['name']), $identity_id); - - // add signature to array - if (!empty($sql_arr['signature']) && empty($COMPOSE['param']['nosig'])) { - $text = $html = $sql_arr['signature']; - - if ($sql_arr['html_signature']) { - $text = $RCMAIL->html2text($html, array('links' => false)); - $text = trim($text, "\r\n"); - } - else { - $t2h = new rcube_text2html($text, false); - $html = $t2h->get_html(); - } - - if ($add_separator && !preg_match('/^--[ -]\r?\n/m', $text)) { - $text = $separator . "\n" . ltrim($text, "\r\n"); - $html = $separator . "
" . $html; - } - - $a_signatures[$identity_id]['text'] = $text; - $a_signatures[$identity_id]['html'] = $html; - } - - // add bcc and reply-to - if (!empty($sql_arr['reply-to'])) { - $identities[$identity_id]['replyto'] = $sql_arr['reply-to']; - } - if (!empty($sql_arr['bcc'])) { - $identities[$identity_id]['bcc'] = $sql_arr['bcc']; - } - - $identities[$identity_id]['email'] = $sql_arr['email']; - } - - $out = $select_from->show($MESSAGE->compose['from']); - - // add signatures to client - $OUTPUT->set_env('signatures', $a_signatures); - $OUTPUT->set_env('identities', $identities); - } - // no identities, display text input field - else { - $field_attrib['class'] = 'from_address'; - $input_from = new html_inputfield($field_attrib); - $out = $input_from->show($MESSAGE->compose['from']); - } - - return $out; -} - -function rcmail_compose_header_value($header) -{ - global $COMPOSE, $MESSAGE; - - $RCMAIL = rcube::get_instance(); - $fvalue = ''; - $decode_header = true; - $charset = $MESSAGE->headers->charset; - $separator = ', '; - - // we have a set of recipients stored is session - if ($header == 'to' && ($mailto_id = $COMPOSE['param']['mailto']) - && $_SESSION['mailto'][$mailto_id] - ) { - $fvalue = urldecode($_SESSION['mailto'][$mailto_id]); - $decode_header = false; - $charset = $RCMAIL->output->charset; - - // make session to not grow up too much - unset($_SESSION['mailto'][$mailto_id]); - $COMPOSE['param']['to'] = $fvalue; - } - else if (!empty($_POST['_' . $header])) { - $fvalue = rcube_utils::get_input_value('_' . $header, rcube_utils::INPUT_POST, true); - $charset = $RCMAIL->output->charset; - } - else if (!empty($COMPOSE['param'][$header])) { - $fvalue = $COMPOSE['param'][$header]; - $charset = $RCMAIL->output->charset; - } - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_REPLY) { - // get recipent address(es) out of the message headers - if ($header == 'to') { - $mailfollowup = $MESSAGE->headers->others['mail-followup-to']; - $mailreplyto = $MESSAGE->headers->others['mail-reply-to']; - - // Reply to mailing list... - if ($MESSAGE->reply_all == 'list' && $mailfollowup) { - $fvalue = $mailfollowup; - } - else if ($MESSAGE->reply_all == 'list' - && preg_match('/]+)>/i', $MESSAGE->headers->others['list-post'], $m) - ) { - $fvalue = $m[1]; - } - // Reply to... - else if ($MESSAGE->reply_all && $mailfollowup) { - $fvalue = $mailfollowup; - } - else if ($mailreplyto) { - $fvalue = $mailreplyto; - } - else if (!empty($MESSAGE->headers->replyto)) { - $fvalue = $MESSAGE->headers->replyto; - $replyto = true; - } - else if (!empty($MESSAGE->headers->from)) { - $fvalue = $MESSAGE->headers->from; - } - - // Reply to message sent by yourself (#1487074, #1489230, #1490439) - // Reply-To address need to be unset (#1490233) - if (!empty($MESSAGE->compose['ident']) && empty($replyto)) { - foreach (array($fvalue, $MESSAGE->headers->from) as $sender) { - $senders = rcube_mime::decode_address_list($sender, null, false, $charset, true); - - if (in_array($MESSAGE->compose['ident']['email_ascii'], $senders)) { - $fvalue = $MESSAGE->headers->to; - break; - } - } - } - } - // add recipient of original message if reply to all - else if ($header == 'cc' && !empty($MESSAGE->reply_all) && $MESSAGE->reply_all != 'list') { - if ($v = $MESSAGE->headers->to) { - $fvalue .= $v; - } - if ($v = $MESSAGE->headers->cc) { - $fvalue .= (!empty($fvalue) ? $separator : '') . $v; - } - // Use Sender header (#1489011) - if ($v = $MESSAGE->headers->get('Sender', false)) { - // Skip common mailing lists addresses: *-bounces@ and *-request@ (#1490452) - if (empty($MESSAGE->headers->others['list-post']) || !preg_match('/-(bounces|request)@/', $v)) { - $fvalue .= (!empty($fvalue) ? $separator : '') . $v; - } - } - - // When To: and Reply-To: are the same we add From: address to the list (#1489037) - if ($v = $MESSAGE->headers->from) { - $from = rcube_mime::decode_address_list($v, null, false, $charset, true); - $to = rcube_mime::decode_address_list($MESSAGE->headers->to, null, false, $charset, true); - $replyto = rcube_mime::decode_address_list($MESSAGE->headers->replyto, null, false, $charset, true); - - if (count($replyto) && !count(array_diff($to, $replyto)) && count(array_diff($from, $to))) { - $fvalue .= (!empty($fvalue) ? $separator : '') . $v; - } - } - } - } - else if (in_array($COMPOSE['mode'], array(RCUBE_COMPOSE_DRAFT, RCUBE_COMPOSE_EDIT))) { - // get drafted headers - if ($header == 'to' && !empty($MESSAGE->headers->to)) { - $fvalue = $MESSAGE->get_header('to', true); - } - else if ($header == 'cc' && !empty($MESSAGE->headers->cc)) { - $fvalue = $MESSAGE->get_header('cc', true); - } - else if ($header == 'bcc' && !empty($MESSAGE->headers->bcc)) { - $fvalue = $MESSAGE->get_header('bcc', true); - } - else if ($header == 'replyto' && !empty($MESSAGE->headers->others['mail-reply-to'])) { - $fvalue = $MESSAGE->get_header('mail-reply-to'); - } - else if ($header == 'replyto' && !empty($MESSAGE->headers->replyto)) { - $fvalue = $MESSAGE->get_header('reply-to'); - } - else if ($header == 'followupto' && !empty($MESSAGE->headers->others['mail-followup-to'])) { - $fvalue = $MESSAGE->get_header('mail-followup-to'); - } - } - - // split recipients and put them back together in a unique way - if (!empty($fvalue) && in_array($header, array('to', 'cc', 'bcc'))) { - $from_email = @mb_strtolower($MESSAGE->compose['ident']['email']); - $to_addresses = rcube_mime::decode_address_list($fvalue, null, $decode_header, $charset); - $fvalue = array(); - - foreach ($to_addresses as $addr_part) { - if (empty($addr_part['mailto'])) { - continue; - } - - // According to RFC5321 local part of email address is case-sensitive - // however, here it is better to compare addresses in case-insensitive manner - $mailto = format_email(rcube_utils::idn_to_utf8($addr_part['mailto'])); - $mailto_lc = mb_strtolower($addr_part['mailto']); - - if (($header == 'to' || $COMPOSE['mode'] != RCUBE_COMPOSE_REPLY || $mailto_lc != $from_email) - && !in_array($mailto_lc, (array) $MESSAGE->recipients) - ) { - if ($addr_part['name'] && $mailto != $addr_part['name']) { - $mailto = format_email_recipient($mailto, $addr_part['name']); - } - - $fvalue[] = $mailto; - $MESSAGE->recipients[] = $mailto_lc; - } - } - - $fvalue = implode($separator, $fvalue); - } - - return $fvalue; -} - function rcmail_compose_editor_mode() { global $RCMAIL, $COMPOSE; @@ -740,13 +399,13 @@ function rcmail_compose_editor_mode() else if (isset($_POST['_is_html'])) { $useHtml = !empty($_POST['_is_html']); } - else if ($compose_mode == RCUBE_COMPOSE_DRAFT || $compose_mode == RCUBE_COMPOSE_EDIT) { + else if ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) { $useHtml = rcmail_message_is_html(); } - else if ($compose_mode == RCUBE_COMPOSE_REPLY) { + else if ($compose_mode == rcmail_sendmail::MODE_REPLY) { $useHtml = $html_editor == 1 || ($html_editor >= 2 && rcmail_message_is_html()); } - else if ($compose_mode == RCUBE_COMPOSE_FORWARD) { + else if ($compose_mode == rcmail_sendmail::MODE_FORWARD) { $useHtml = $html_editor == 1 || $html_editor == 4 || ($html_editor == 3 && rcmail_message_is_html()); } @@ -778,14 +437,14 @@ function rcmail_prepare_message_body() $isHtml = (bool) $COMPOSE['param']['html']; } // forward as attachment - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD && $COMPOSE['as_attachment']) { + else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD && $COMPOSE['as_attachment']) { $isHtml = rcmail_compose_editor_mode(); $body = ''; rcmail_write_forward_attachments(); } // reply/edit/draft/forward - else if ($COMPOSE['mode'] && ($COMPOSE['mode'] != RCUBE_COMPOSE_REPLY || intval($RCMAIL->config->get('reply_mode')) != -1)) { + else if ($COMPOSE['mode'] && ($COMPOSE['mode'] != rcmail_sendmail::MODE_REPLY || intval($RCMAIL->config->get('reply_mode')) != -1)) { $isHtml = rcmail_compose_editor_mode(); $messages = array(); @@ -832,7 +491,7 @@ function rcmail_prepare_message_body() } // compose reply-body - if ($COMPOSE['mode'] == RCUBE_COMPOSE_REPLY) { + if ($COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) { $body = rcmail_create_reply_body($body, $isHtml); if ($MESSAGE->pgp_mime) { @@ -840,11 +499,11 @@ function rcmail_prepare_message_body() } } // forward message body inline - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD) { + else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) { $body = rcmail_create_forward_body($body, $isHtml); } // load draft message body - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_DRAFT || $COMPOSE['mode'] == RCUBE_COMPOSE_EDIT) { + else if ($COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT || $COMPOSE['mode'] == rcmail_sendmail::MODE_EDIT) { $body = rcmail_create_draft_body($body, $isHtml); } } @@ -910,7 +569,7 @@ function rcmail_compose_part_body($part, $isHtml = false) } else { // try to remove the signature - if ($COMPOSE['mode'] != RCUBE_COMPOSE_DRAFT && $COMPOSE['mode'] != RCUBE_COMPOSE_EDIT) { + if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT && $COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT) { if ($RCMAIL->config->get('strip_existing_sig', true)) { $body = rcmail_remove_signature($body); } @@ -929,7 +588,7 @@ function rcmail_compose_part_body($part, $isHtml = false) if ($part->ctype_secondary == 'html') { // use html part if it has been used for message (pre)viewing // decrease line length for quoting - $len = $COMPOSE['mode'] == RCUBE_COMPOSE_REPLY ? $LINE_LENGTH-2 : $LINE_LENGTH; + $len = $COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY ? $LINE_LENGTH-2 : $LINE_LENGTH; $body = $RCMAIL->html2text($body, array('width' => $len)); } else { @@ -938,7 +597,7 @@ function rcmail_compose_part_body($part, $isHtml = false) } // try to remove the signature - if ($COMPOSE['mode'] != RCUBE_COMPOSE_DRAFT && $COMPOSE['mode'] != RCUBE_COMPOSE_EDIT) { + if ($COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT && $COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT) { if ($RCMAIL->config->get('strip_existing_sig', true)) { $body = rcmail_remove_signature($body); } @@ -951,9 +610,9 @@ function rcmail_compose_part_body($part, $isHtml = false) function rcmail_compose_body($attrib) { - global $RCMAIL, $OUTPUT, $HTML_MODE, $MESSAGE_BODY; + global $RCMAIL, $OUTPUT, $HTML_MODE, $MESSAGE_BODY, $SENDMAIL; - list($form_start, $form_end) = get_form_tags($attrib); + list($form_start, $form_end) = $SENDMAIL->form_tags($attrib); unset($attrib['form']); if (empty($attrib['id'])) { @@ -1041,7 +700,6 @@ function rcmail_compose_body($attrib) . ' style="width:0;height:0;border:none;visibility:hidden;" aria-hidden="true">'; } - function rcmail_create_reply_body($body, $bodyIsHtml) { global $RCMAIL, $MESSAGE, $LINE_LENGTH; @@ -1239,7 +897,7 @@ function rcmail_create_draft_body($body, $bodyIsHtml) return $body; } - +// Removes signature from the message body function rcmail_remove_signature($body) { global $RCMAIL; @@ -1261,7 +919,6 @@ function rcmail_remove_signature($body) return $body; } - function rcmail_write_compose_attachments(&$message, $bodyIsHtml) { global $RCMAIL, $COMPOSE; @@ -1290,12 +947,12 @@ function rcmail_write_compose_attachments(&$message, $bodyIsHtml) } // skip message attachments in reply mode - if ($part->ctype_primary == 'message' && $COMPOSE['mode'] == RCUBE_COMPOSE_REPLY) { + if ($part->ctype_primary == 'message' && $COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) { continue; } // skip inline images when forwarding in text mode - if ($part->content_id && $part->disposition == 'inline' && !$bodyIsHtml && $COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD) { + if ($part->content_id && $part->disposition == 'inline' && !$bodyIsHtml && $COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) { continue; } @@ -1332,7 +989,6 @@ function rcmail_write_compose_attachments(&$message, $bodyIsHtml) return $cid_map; } - function rcmail_write_inline_attachments(&$message) { global $RCMAIL, $COMPOSE; @@ -1445,6 +1101,7 @@ function rcmail_write_forward_attachments() } } +// Saves an image as attachment function rcmail_save_image($path, $mimetype = '', $data = null) { global $COMPOSE; @@ -1484,6 +1141,7 @@ function rcmail_save_image($path, $mimetype = '', $data = null) return false; } +// Unicode-safe basename() function rcmail_basename($filename) { // basename() is not unicode safe and locale dependent @@ -1496,69 +1154,8 @@ function rcmail_basename($filename) } /** - * Creates reply subject by removing common subject - * prefixes/suffixes from the original message subject + * Attachments list object for templates */ -function rcmail_reply_subject($subject) -{ - $subject = trim($subject); - - // replace Re:, Re[x]:, Re-x (#1490497) - $prefix = '/^(re:|re\[\d\]:|re-\d:)\s*/i'; - do { - $subject = preg_replace($prefix, '', $subject, -1, $count); - } - while ($count); - - // replace (was: ...) (#1489375) - $subject = preg_replace('/\s*\([wW]as:[^\)]+\)\s*$/', '', $subject); - - return 'Re: ' . $subject; -} - -function rcmail_compose_subject($attrib) -{ - global $MESSAGE, $COMPOSE; - - list($form_start, $form_end) = get_form_tags($attrib); - unset($attrib['form']); - - $attrib['name'] = '_subject'; - $attrib['spellcheck'] = 'true'; - - $textfield = new html_inputfield($attrib); - $subject = ''; - - // use subject from post - if (isset($_POST['_subject'])) { - $subject = rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE); - } - else if (!empty($COMPOSE['param']['subject'])) { - $subject = $COMPOSE['param']['subject']; - } - // create a reply-subject - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_REPLY) { - $subject = rcmail_reply_subject($MESSAGE->subject); - } - // create a forward-subject - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_FORWARD) { - if (preg_match('/^fwd:/i', $MESSAGE->subject)) - $subject = $MESSAGE->subject; - else - $subject = 'Fwd: '.$MESSAGE->subject; - } - // creeate a draft-subject - else if ($COMPOSE['mode'] == RCUBE_COMPOSE_DRAFT || $COMPOSE['mode'] == RCUBE_COMPOSE_EDIT) { - $subject = $MESSAGE->subject; - } - - $out = $form_start ? "$form_start\n" : ''; - $out .= $textfield->show($subject); - $out .= $form_end ? "\n$form_end" : ''; - - return $out; -} - function rcmail_compose_attachment_list($attrib) { global $RCMAIL, $OUTPUT, $COMPOSE; @@ -1645,7 +1242,9 @@ function rcmail_compose_attachment_list($attrib) return html::tag('ul', $attrib, $out, html::$common_attrib); } - +/** + * Attachment upload form object for templates + */ function rcmail_compose_attachment_form($attrib) { global $RCMAIL; @@ -1653,98 +1252,22 @@ function rcmail_compose_attachment_form($attrib) return $RCMAIL->upload_form($attrib, 'uploadform', 'send-attachment', array('multiple' => true)); } -function rcmail_priority_selector($attrib) -{ - global $RCMAIL, $MESSAGE; - - list($form_start, $form_end) = get_form_tags($attrib); - unset($attrib['form']); - - $attrib['name'] = '_priority'; - $prio_list = array( - $RCMAIL->gettext('lowest') => 5, - $RCMAIL->gettext('low') => 4, - $RCMAIL->gettext('normal') => 0, - $RCMAIL->gettext('high') => 2, - $RCMAIL->gettext('highest') => 1, - ); - - $selector = new html_select($attrib); - $selector->add(array_keys($prio_list), array_values($prio_list)); - - if (isset($_POST['_priority'])) - $sel = $_POST['_priority']; - else if (isset($MESSAGE->headers->priority) && intval($MESSAGE->headers->priority) != 3) - $sel = $MESSAGE->headers->priority; - else - $sel = 0; - - $out = $form_start ? "$form_start\n" : ''; - $out .= $selector->show((int) $sel); - $out .= $form_end ? "\n$form_end" : ''; - - return $out; -} - - -function rcmail_mdn_checkbox($attrib) -{ - global $RCMAIL, $MESSAGE; - - list($form_start, $form_end) = get_form_tags($attrib); - unset($attrib['form']); - - if (!isset($attrib['id'])) - $attrib['id'] = 'receipt'; - - $attrib['name'] = '_mdn'; - $attrib['value'] = '1'; - - $checkbox = new html_checkbox($attrib); - - if (isset($_POST['_mdn'])) - $mdn_default = $_POST['_mdn']; - else if (in_array($COMPOSE['mode'], array(RCUBE_COMPOSE_DRAFT, RCUBE_COMPOSE_EDIT))) - $mdn_default = (bool) $MESSAGE->headers->mdn_to; - else - $mdn_default = $RCMAIL->config->get('mdn_default'); - - $out = $form_start ? "$form_start\n" : ''; - $out .= $checkbox->show($mdn_default); - $out .= $form_end ? "\n$form_end" : ''; - - return $out; -} - - -function rcmail_dsn_checkbox($attrib) +/** + * Register a certain container as active area to drop files onto + */ +function rcmail_compose_file_drop_area($attrib) { - global $RCMAIL; - - list($form_start, $form_end) = get_form_tags($attrib); - unset($attrib['form']); - - if (!isset($attrib['id'])) - $attrib['id'] = 'dsn'; - - $attrib['name'] = '_dsn'; - $attrib['value'] = '1'; - - $checkbox = new html_checkbox($attrib); - - if (isset($_POST['_dsn'])) - $dsn_value = (int) $_POST['_dsn']; - else - $dsn_value = $RCMAIL->config->get('dsn_default'); - - $out = $form_start ? "$form_start\n" : ''; - $out .= $checkbox->show($dsn_value); - $out .= $form_end ? "\n$form_end" : ''; + global $OUTPUT; - return $out; + if ($attrib['id']) { + $OUTPUT->add_gui_object('filedrop', $attrib['id']); + $OUTPUT->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments')); + } } - +/** + * Editor mode selector object for templates + */ function rcmail_editor_selector($attrib) { global $RCMAIL; @@ -1768,78 +1291,9 @@ function rcmail_editor_selector($attrib) return $select->show($useHtml ? 'html' : 'plain'); } - -function rcmail_store_target_selection($attrib) -{ - global $COMPOSE, $RCMAIL; - - $attrib['name'] = '_store_target'; - $select = $RCMAIL->folder_selector(array_merge($attrib, array( - 'noselection' => '- ' . $RCMAIL->gettext('dontsave') . ' -', - 'folder_filter' => 'mail', - 'folder_rights' => 'w', - ))); - - return $select->show(isset($_POST['_store_target']) ? $_POST['_store_target'] : $COMPOSE['param']['sent_mbox'], $attrib); -} - - -function rcmail_check_sent_folder($folder, $create=false) -{ - global $RCMAIL; - - // we'll not save the message, so it doesn't matter - if ($RCMAIL->config->get('no_save_sent_messages')) { - return true; - } - - if ($RCMAIL->storage->folder_exists($folder, true)) { - return true; - } - - // folder may exist but isn't subscribed (#1485241) - if ($create) { - if (!$RCMAIL->storage->folder_exists($folder)) - return $RCMAIL->storage->create_folder($folder, true); - else - return $RCMAIL->storage->subscribe($folder); - } - - return false; -} - - -function get_form_tags($attrib) -{ - global $RCMAIL, $MESSAGE_FORM, $COMPOSE; - - if (rcube_utils::get_boolean((string) $attrib['noform'])) { - return array('', ''); - } - - $form_start = ''; - if (!$MESSAGE_FORM) { - $hiddenfields = new html_hiddenfield(array('name' => '_task', 'value' => $RCMAIL->task)); - $hiddenfields->add(array('name' => '_action', 'value' => 'send')); - $hiddenfields->add(array('name' => '_id', 'value' => $COMPOSE['id'])); - $hiddenfields->add(array('name' => '_attachments')); - - $form_start = empty($attrib['form']) ? $RCMAIL->output->form_tag(array('name' => "form", 'method' => "post")) : ''; - $form_start .= $hiddenfields->show(); - } - - $form_end = ($MESSAGE_FORM && !strlen($attrib['form'])) ? '' : ''; - $form_name = $attrib['form'] ?: 'form'; - - if (!$MESSAGE_FORM) - $RCMAIL->output->add_gui_object('messageform', $form_name); - - $MESSAGE_FORM = $form_name; - - return array($form_start, $form_end); -} - - +/** + * Addressbooks list object for templates + */ function rcmail_addressbook_list($attrib = array()) { global $RCMAIL, $OUTPUT; @@ -1874,7 +1328,9 @@ function rcmail_addressbook_list($attrib = array()) return html::tag('ul', $attrib, $out, html::$common_attrib); } -// return the contacts list as HTML table +/** + * Contacts list object for templates + */ function rcmail_contacts_list($attrib = array()) { global $RCMAIL, $OUTPUT; @@ -1890,23 +1346,8 @@ function rcmail_contacts_list($attrib = array()) return $RCMAIL->table_output($attrib, array(), array('name'), 'ID'); } - -/** - * Register a certain container as active area to drop files onto - */ -function compose_file_drop_area($attrib) -{ - global $OUTPUT; - - if ($attrib['id']) { - $OUTPUT->add_gui_object('filedrop', $attrib['id']); - $OUTPUT->set_env('filedrop', array('action' => 'upload', 'fieldname' => '_attachments')); - } -} - - /** - * + * Responses list object for templates */ function rcmail_compose_responses_list($attrib) { @@ -1919,11 +1360,11 @@ function rcmail_compose_responses_list($attrib) foreach ($RCMAIL->get_compose_responses(true) as $response) { $key = $response['key']; $item = html::a(array( - 'href' => '#'.urlencode($response['name']), - 'class' => rtrim('insertresponse ' . $attrib['itemclass']), - 'unselectable' => 'on', - 'tabindex' => '0', - 'rel' => $key, + 'href' => '#' . urlencode($response['name']), + 'class' => rtrim('insertresponse ' . $attrib['itemclass']), + 'unselectable' => 'on', + 'tabindex' => '0', + 'rel' => $key, ), rcube::Q($response['name'])); $jsenv[$key] = $response; diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index b087f736c..886448977 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -1825,36 +1825,6 @@ function rcmail_wrap_and_quote($text, $length = 72, $quote = true) return rtrim($out, "\n"); } -function rcmail_draftinfo_encode($p) -{ - $parts = array(); - foreach ($p as $key => $val) { - $encode = $key == 'folder' || strpos($val, ';') !== false; - $parts[] = $key . '=' . ($encode ? 'B::' . base64_encode($val) : $val); - } - - return join('; ', $parts); -} - -function rcmail_draftinfo_decode($str) -{ - $info = array(); - - foreach (preg_split('/;\s+/', $str) as $part) { - list($key, $val) = explode('=', $part, 2); - if (strpos($val, 'B::') === 0) { - $val = base64_decode(substr($val, 3)); - } - else if ($key == 'folder') { - $val = base64_decode($val); - } - - $info[$key] = $val; - } - - return $info; -} - /** * Send the MDN response * @@ -1874,7 +1844,7 @@ function rcmail_send_mdn($message, &$smtp_error) if ($message->headers->mdn_to && empty($message->headers->flags['MDNSENT']) && ($RCMAIL->storage->check_permflag('MDNSENT') || $RCMAIL->storage->check_permflag('*')) ) { - $identity = rcmail_identity_select($message); + $identity = rcmail_sendmail::identity_select($message); $sender = format_email_recipient($identity['email'], $identity['name']); $recipient = array_shift(rcube_mime::decode_address_list( $message->headers->mdn_to, 1, true, $message->headers->charset)); @@ -1943,110 +1913,11 @@ function rcmail_send_mdn($message, &$smtp_error) /** * Detect recipient identity from specified message + * @deprecated Use rcmail_sendmail::identity_select() */ function rcmail_identity_select($MESSAGE, $identities = null, $compose_mode = 'reply') { - $a_recipients = array(); - $a_names = array(); - - if ($identities === null) { - $identities = rcmail::get_instance()->user->list_identities(null, true); - } - - // extract all recipients of the reply-message - if (is_object($MESSAGE->headers) && in_array($compose_mode, array('reply', 'forward'))) { - $a_to = rcube_mime::decode_address_list($MESSAGE->headers->to, null, true, $MESSAGE->headers->charset); - foreach ($a_to as $addr) { - if (!empty($addr['mailto'])) { - $a_recipients[] = strtolower($addr['mailto']); - $a_names[] = $addr['name']; - } - } - - if (!empty($MESSAGE->headers->cc)) { - $a_cc = rcube_mime::decode_address_list($MESSAGE->headers->cc, null, true, $MESSAGE->headers->charset); - foreach ($a_cc as $addr) { - if (!empty($addr['mailto'])) { - $a_recipients[] = strtolower($addr['mailto']); - $a_names[] = $addr['name']; - } - } - } - } - - // decode From: address - $from = rcube_mime::decode_address_list($MESSAGE->headers->from, null, true, $MESSAGE->headers->charset); - $from = array_shift($from); - $from['mailto'] = strtolower($from['mailto']); - - $from_idx = null; - $found_idx = array('to' => null, 'from' => null); - $check_from = in_array($compose_mode, array('draft', 'edit', 'reply')); - - // Select identity - foreach ($identities as $idx => $ident) { - // use From: header when in edit/draft or reply-to-self - if ($check_from && $from['mailto'] == strtolower($ident['email_ascii'])) { - // remember first matching identity address - if ($found_idx['from'] === null) { - $found_idx['from'] = $idx; - } - // match identity name - if ($from['name'] && $ident['name'] && $from['name'] == $ident['name']) { - $from_idx = $idx; - break; - } - } - // use replied/forwarded message recipients - else if (($found = array_search(strtolower($ident['email_ascii']), $a_recipients)) !== false) { - // remember first matching identity address - if ($found_idx['to'] === null) { - $found_idx['to'] = $idx; - } - // match identity name - if ($a_names[$found] && $ident['name'] && $a_names[$found] == $ident['name']) { - $from_idx = $idx; - break; - } - } - } - - // If matching by name+address didn't find any matches, - // get first found identity (address) if any - if ($from_idx === null) { - $from_idx = $found_idx['from'] !== null ? $found_idx['from'] : $found_idx['to']; - } - - // Try Return-Path - if ($from_idx === null && ($return_path = $MESSAGE->headers->others['return-path'])) { - $return_path = array_map('strtolower', (array) $return_path); - - foreach ($identities as $idx => $ident) { - // Return-Path header contains an email address, but on some mailing list - // it can be e.g. - // where local@domain.tld is the address we're looking for (#1489241) - $ident1 = strtolower($ident['email_ascii']); - $ident2 = str_replace('@', '=', $ident1); - $ident1 = '<' . $ident1 . '>'; - $ident2 = '-' . $ident2 . '@'; - - foreach ($return_path as $path) { - if ($path == $ident1 || stripos($path, $ident2)) { - $from_idx = $idx; - break 2; - } - } - } - } - - // See identity_select plugin for example usage of this hook - $plugin = rcmail::get_instance()->plugins->exec_hook('identity_select', - array('message' => $MESSAGE, 'identities' => $identities, 'selected' => $from_idx)); - - $selected = $plugin['selected']; - - // default identity is always first on the list - return $identities[$selected !== null ? $selected : 0]; + return rcmail_sendmail::identity_select($MESSAGE, $identities, $compose_mode); } // Fixes some content-type names diff --git a/program/steps/mail/sendmail.inc b/program/steps/mail/sendmail.inc index 8c1e1d04c..438796bfb 100644 --- a/program/steps/mail/sendmail.inc +++ b/program/steps/mail/sendmail.inc @@ -5,7 +5,7 @@ | program/steps/mail/sendmail.inc | | | | This file is part of the Roundcube Webmail client | - | Copyright (C) 2005-2013, The Roundcube Dev Team | + | Copyright (C) 2005-2017, The Roundcube Dev Team | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -15,23 +15,18 @@ | Compose a new mail message and send it or store as draft | +-----------------------------------------------------------------------+ | Author: Thomas Bruederli | + | Author: Aleksander Machniak | +-----------------------------------------------------------------------+ */ // remove all scripts and act as called in frame $OUTPUT->reset(); -$OUTPUT->framed = TRUE; - -$saveonly = !empty($_GET['_saveonly']); -$savedraft = !empty($_POST['_draft']) && !$saveonly; -$sendmail_delay = (int) $RCMAIL->config->get('sendmail_delay'); -$drafts_mbox = $RCMAIL->config->get('drafts_mbox'); +$OUTPUT->framed = true; $COMPOSE_ID = rcube_utils::get_input_value('_id', rcube_utils::INPUT_GPC); $COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID]; -/****** checks ********/ - +// Sanity checks if (!isset($COMPOSE['id'])) { rcube::raise_error(array('code' => 500, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, @@ -41,217 +36,29 @@ if (!isset($COMPOSE['id'])) { $OUTPUT->send('iframe'); } -if (!$savedraft) { - if (empty($_POST['_to']) && empty($_POST['_cc']) && empty($_POST['_bcc']) && $_POST['_message']) { - $OUTPUT->show_message('sendingfailed', 'error'); - $OUTPUT->send('iframe'); - } - - if ($sendmail_delay) { - $wait_sec = time() - $sendmail_delay - intval($RCMAIL->config->get('last_message_time')); - if ($wait_sec < 0) { - $OUTPUT->show_message('senttooquickly', 'error', array('sec' => $wait_sec * -1)); +$saveonly = !empty($_GET['_saveonly']); +$savedraft = !empty($_POST['_draft']) && !$saveonly; +$SENDMAIL = new rcmail_sendmail($COMPOSE, array( + 'sendmail' => true, + 'saveonly' => $saveonly, + 'savedraft' => $savedraft, + 'error_handler' => function() use ($OUTPUT) { + call_user_func_array(array($OUTPUT, 'show_message'), func_get_args()); $OUTPUT->send('iframe'); } - } -} - - -/****** compose message ********/ - -// set default charset -$message_charset = rcube_utils::get_input_value('_charset', rcube_utils::INPUT_POST) ?: $OUTPUT->get_charset(); - -$EMAIL_FORMAT_ERROR = NULL; -$RECIPIENT_COUNT = 0; - -$mailto = rcmail_email_input_format(rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST, TRUE, $message_charset), true); -$mailcc = rcmail_email_input_format(rcube_utils::get_input_value('_cc', rcube_utils::INPUT_POST, TRUE, $message_charset), true); -$mailbcc = rcmail_email_input_format(rcube_utils::get_input_value('_bcc', rcube_utils::INPUT_POST, TRUE, $message_charset), true); - -if ($EMAIL_FORMAT_ERROR && !$savedraft) { - $OUTPUT->show_message('emailformaterror', 'error', array('email' => $EMAIL_FORMAT_ERROR)); - $OUTPUT->send('iframe'); -} - -if (empty($mailto) && !empty($mailcc)) { - $mailto = $mailcc; - $mailcc = null; -} -else if (empty($mailto)) { - $mailto = 'undisclosed-recipients:;'; -} - -// Get sender name and address... -$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST, true, $message_charset); -// ... from identity... -if (is_numeric($from)) { - if (is_array($identity_arr = rcmail_get_identity($from))) { - if ($identity_arr['mailto']) - $from = $identity_arr['mailto']; - if ($identity_arr['string']) - $from_string = $identity_arr['string']; - } - else { - $from = null; - } -} -// ... if there is no identity record, this might be a custom from -else if (($from_string = rcmail_email_input_format($from)) - && preg_match('/(\S+@\S+)/', $from_string, $m) -) { - $from = trim($m[1], '<>'); -} -// ... otherwise it's empty or invalid -else { - $from = null; -} - -// check 'From' address (identity may be incomplete) -if (!$savedraft && !$saveonly && empty($from)) { - $OUTPUT->show_message('nofromaddress', 'error'); - $OUTPUT->send('iframe'); -} - -if (!$from_string && $from) { - $from_string = $from; -} +)); -if (empty($COMPOSE['param']['message-id'])) { - $COMPOSE['param']['message-id'] = $RCMAIL->gen_message_id($from); -} -$message_id = $COMPOSE['param']['message-id']; - -// compose headers array -$headers = array(); - -// if configured, the Received headers goes to top, for good measure -if ($RCMAIL->config->get('http_received_header')) { - $nldlm = "\r\n\t"; - $http_header = 'from '; - - // FROM/VIA - if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2); - $http_header .= rcmail_received_host($hosts[0]) . $nldlm . ' via '; - } - - $http_header .= rcmail_received_host($_SERVER['REMOTE_ADDR']); - - // BY - $http_header .= $nldlm . 'by ' . $_SERVER['HTTP_HOST']; - - // WITH - $http_header .= $nldlm . 'with HTTP (' . $_SERVER['SERVER_PROTOCOL'] - . ' ' . $_SERVER['REQUEST_METHOD'] . '); ' . date('r'); +// Collect input for message headers +$headers = $SENDMAIL->headers_input(); - $headers['Received'] = wordwrap($http_header, 69, $nldlm); -} - -$headers['Date'] = $RCMAIL->user_date(); -$headers['From'] = rcube_charset::convert($from_string, RCUBE_CHARSET, $message_charset); -$headers['To'] = $mailto; - -// additional recipients -if (!empty($mailcc)) { - $headers['Cc'] = $mailcc; -} -if (!empty($mailbcc)) { - $headers['Bcc'] = $mailbcc; -} - -if (($max_recipients = (int) $RCMAIL->config->get('max_recipients')) > 0) { - if ($RECIPIENT_COUNT > $max_recipients) { - $OUTPUT->show_message('toomanyrecipients', 'error', array('max' => $max_recipients)); - $OUTPUT->send('iframe'); - } -} +$COMPOSE['param']['message-id'] = $headers['Message-ID']; -$dont_override = (array) $RCMAIL->config->get('dont_override'); -$mdn_enabled = in_array('mdn_default', $dont_override) ? $RCMAIL->config->get('mdn_default') : !empty($_POST['_mdn']); -$dsn_enabled = in_array('dsn_default', $dont_override) ? $RCMAIL->config->get('dsn_default') : !empty($_POST['_dsn']); -$subject = trim(rcube_utils::get_input_value('_subject', rcube_utils::INPUT_POST, TRUE, $message_charset)); - -if (strlen($subject)) { - $headers['Subject'] = $subject; -} -if (!empty($identity_arr['organization'])) { - $headers['Organization'] = $identity_arr['organization']; -} -if ($hdr = rcube_utils::get_input_value('_replyto', rcube_utils::INPUT_POST, TRUE, $message_charset)) { - $headers['Reply-To'] = rcmail_email_input_format($hdr); -} -if (!empty($headers['Reply-To'])) { - $headers['Mail-Reply-To'] = $headers['Reply-To']; -} -if ($hdr = rcube_utils::get_input_value('_followupto', rcube_utils::INPUT_POST, TRUE, $message_charset)) { - $headers['Mail-Followup-To'] = rcmail_email_input_format($hdr); -} - -// remember reply/forward UIDs in special headers -if ($savedraft) { - // Note: We ignore . forwards/replies here - if (($uid = $COMPOSE['reply_uid']) && !preg_match('/^\d+\.[0-9.]+$/', $uid)) { - $headers['X-Draft-Info'] = array('type' => 'reply', 'uid' => $uid); - } - else if (!empty($COMPOSE['forward_uid']) - && ($uid = rcube_imap_generic::compressMessageSet($COMPOSE['forward_uid'])) - && !preg_match('/^\d+[0-9.]+$/', $uid) - ) { - $headers['X-Draft-Info'] = array('type' => 'forward', 'uid' => $uid); - } -} - -if (!empty($COMPOSE['reply_msgid'])) { - $headers['In-Reply-To'] = $COMPOSE['reply_msgid']; -} -if (!empty($COMPOSE['references'])) { - $headers['References'] = $COMPOSE['references']; -} - -if (!empty($_POST['_priority'])) { - $priority = intval($_POST['_priority']); - $a_priorities = array(1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest'); - - if ($str_priority = $a_priorities[$priority]) { - $headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority)); - } -} - -if ($mdn_enabled) { - $headers['Return-Receipt-To'] = $from_string; - $headers['Disposition-Notification-To'] = $from_string; -} - -// additional headers -$headers['Message-ID'] = $message_id; -$headers['X-Sender'] = $from; - -if (is_array($headers['X-Draft-Info'])) { - $headers['X-Draft-Info'] = rcmail_draftinfo_encode($headers['X-Draft-Info'] + array('folder' => $COMPOSE['mailbox'])); -} -if ($hdr = $RCMAIL->config->get('useragent')) { - $headers['User-Agent'] = $hdr; -} - -// exec hook for header checking and manipulation -// Depracated: use message_before_send hook instead -$data = $RCMAIL->plugins->exec_hook('message_outgoing_headers', array('headers' => $headers)); - -// sending aborted by plugin -if ($data['abort'] && !$savedraft) { - $OUTPUT->show_message($data['message'] ?: 'sendingfailed'); - $OUTPUT->send('iframe'); -} -else { - $headers = $data['headers']; -} - -$isHtml = (bool) rcube_utils::get_input_value('_is_html', rcube_utils::INPUT_POST); - -// fetch message body -$message_body = rcube_utils::get_input_value('_message', rcube_utils::INPUT_POST, TRUE, $message_charset); +$message_id = $headers['Message-ID']; +$message_charset = $SENDMAIL->options['charset']; +$message_body = rcube_utils::get_input_value('_message', rcube_utils::INPUT_POST, true, $message_charset); +$isHtml = (bool) rcube_utils::get_input_value('_is_html', rcube_utils::INPUT_POST); +// Reset message body and attachments in Mailvelope mode if (isset($_POST['_pgpmime'])) { $pgp_mime = rcube_utils::get_input_value('_pgpmime', rcube_utils::INPUT_POST); $isHtml = false; @@ -278,7 +85,8 @@ if ($isHtml) { // append doctype and html/body wrappers $bstyle = !empty($bstyle) ? (" style='" . implode($bstyle, '; ') . "'") : ''; $message_body = '' - . '' + . '' . "\r\n" . $message_body; } @@ -310,8 +118,10 @@ if (!$savedraft) { } // Check spelling before send - if ($RCMAIL->config->get('spellcheck_before_send') && $RCMAIL->config->get('enable_spellcheck') - && empty($COMPOSE['spell_checked']) && !empty($message_body) + if ($RCMAIL->config->get('spellcheck_before_send') + && $RCMAIL->config->get('enable_spellcheck') + && empty($COMPOSE['spell_checked']) + && !empty($message_body) ) { $message_body = str_replace("\r\n", "\n", $message_body); $spellchecker = new rcube_spellchecker(rcube_utils::get_input_value('_lang', rcube_utils::INPUT_GPC)); @@ -335,8 +145,7 @@ if (!$savedraft) { } // generic footer for all messages - if ($footer = rcmail_generic_message_footer($isHtml)) { - $footer = rcube_charset::convert($footer, RCUBE_CHARSET, $message_charset); + if ($footer = $SENDMAIL->generic_message_footer($isHtml)) { $message_body .= "\r\n" . $footer; } } @@ -357,96 +166,31 @@ if ($files = rcube_utils::get_input_value('_attachments', rcube_utils::INPUT_POS $COMPOSE['attachments'] = array_merge(array_filter($files), $COMPOSE['attachments']); } -// set line length for body wrapping -$LINE_LENGTH = $RCMAIL->config->get('line_length', 72); - // Since we can handle big messages with disk usage, we need more time to work -@set_time_limit(0); - -// create PEAR::Mail_mime instance -$MAIL_MIME = new Mail_mime("\r\n"); - -// Check if we have enough memory to handle the message in it -// It's faster than using files, so we'll do this if we only can -if (is_array($COMPOSE['attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit')))) { - $memory = 0; - foreach ($COMPOSE['attachments'] as $id => $attachment) { - $memory += $attachment['size']; - } - - // Yeah, Net_SMTP needs up to 12x more memory, 1.33 is for base64 - if (!rcube_utils::mem_check($memory * 1.33 * 12)) { - $MAIL_MIME->setParam('delay_file_io', true); - } -} - -// For HTML-formatted messages, construct the MIME message with both -// the HTML part and the plain-text part -if ($isHtml) { - $plugin = $RCMAIL->plugins->exec_hook('message_outgoing_body', - array('body' => $message_body, 'type' => 'html', 'message' => $MAIL_MIME)); - - $MAIL_MIME->setHTMLBody($plugin['body']); - - $plainTextPart = $RCMAIL->html2text($plugin['body'], array('width' => 0, 'charset' => $message_charset)); - $plainTextPart = rcube_mime::wordwrap($plainTextPart, $LINE_LENGTH, "\r\n", false, $message_charset); - $plainTextPart = wordwrap($plainTextPart, 998, "\r\n", true); - - // There's no sense to use multipart/alternative if the text/plain - // part would be blank. Completely blank text/plain part may confuse - // some mail clients (#5283) - if (strlen(trim($plainTextPart)) > 0) { - // make sure all line endings are CRLF (#1486712) - $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart); +@set_time_limit(360); - $plugin = $RCMAIL->plugins->exec_hook('message_outgoing_body', - array('body' => $plainTextPart, 'type' => 'alternative', 'message' => $MAIL_MIME)); - - // add a plain text version of the e-mail as an alternative part. - $MAIL_MIME->setTXTBody($plugin['body']); - } - - // Extract image Data URIs into message attachments (#1488502) - rcmail_extract_inline_images($MAIL_MIME, $from); -} -else { - $plugin = $RCMAIL->plugins->exec_hook('message_outgoing_body', - array('body' => $message_body, 'type' => 'plain', 'message' => $MAIL_MIME)); - - $message_body = $plugin['body']; - - // compose format=flowed content if enabled - if ($flowed = ($savedraft || $RCMAIL->config->get('send_format_flowed', true))) - $message_body = rcube_mime::format_flowed($message_body, min($LINE_LENGTH+2, 79), $message_charset); - else - $message_body = rcube_mime::wordwrap($message_body, $LINE_LENGTH, "\r\n", false, $message_charset); - - $message_body = wordwrap($message_body, 998, "\r\n", true); - - $MAIL_MIME->setTXTBody($message_body, false, true); -} +// create PEAR::Mail_mime instance, set headers, body and params +$MAIL_MIME = $SENDMAIL->create_message($headers, $message_body, $isHtml, $COMPOSE['attachments']); // add stored attachments, if any if (is_array($COMPOSE['attachments'])) { foreach ($COMPOSE['attachments'] as $id => $attachment) { // This hook retrieves the attachment contents from the file storage backend $attachment = $RCMAIL->plugins->exec_hook('attachment_get', $attachment); + $is_inline = false; if ($isHtml) { $dispurl = '/[\'"]\S+display-attachment\S+file=rcmfile' . preg_quote($attachment['id']) . '[\'"]/'; $message_body = $MAIL_MIME->getHTMLBody(); $is_inline = preg_match($dispurl, $message_body); } - else { - $is_inline = false; - } // inline image if ($is_inline) { // Mail_Mime does not support many inline attachments with the same name (#1489406) // we'll generate cid: urls here to workaround this $cid = preg_replace('/[^0-9a-zA-Z]/', '', uniqid(time(), true)); - if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $from, $matches)) { + if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $SENDMAIL->options['from'], $matches)) { $cid .= $matches[1]; } else { @@ -483,25 +227,6 @@ if (is_array($COMPOSE['attachments'])) { } } -$text_charset = $message_charset; -$transfer_encoding = '7bit'; -$head_encoding = 'quoted-printable'; - -// choose encodings for plain/text body and message headers -if (preg_match('/ISO-2022/i', $message_charset)) { - $head_encoding = 'base64'; // RFC1468 -} -else if (preg_match('/[^\x00-\x7F]/', $MAIL_MIME->getTXTBody())) { - $transfer_encoding = $RCMAIL->config->get('force_7bit') ? 'quoted-printable' : '8bit'; -} -else if ($message_charset == 'UTF-8') { - $text_charset = 'US-ASCII'; -} - -if ($flowed) { - $text_charset .= ";\r\n format=flowed"; -} - // compose PGP/Mime message if ($pgp_mime) { $MAIL_MIME->addAttachment(new Mail_mimePart('Version: 1', array( @@ -519,163 +244,33 @@ if ($pgp_mime) { $MAIL_MIME->setParam('preamble', 'This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)'); } -// encoding settings for mail composing -$MAIL_MIME->setParam('text_encoding', $transfer_encoding); -$MAIL_MIME->setParam('html_encoding', 'quoted-printable'); -$MAIL_MIME->setParam('head_encoding', $head_encoding); -$MAIL_MIME->setParam('head_charset', $message_charset); -$MAIL_MIME->setParam('html_charset', $message_charset); -$MAIL_MIME->setParam('text_charset', $text_charset); - -// pass headers to message object -$MAIL_MIME->headers($headers); - // This hook allows to modify the message before send or save action $plugin = $RCMAIL->plugins->exec_hook('message_ready', array('message' => $MAIL_MIME)); $MAIL_MIME = $plugin['message']; -// Begin SMTP Delivery Block +// Deliver the message over SMTP if (!$savedraft && !$saveonly) { - // Handle Delivery Status Notification request - $smtp_opts['dsn'] = $dsn_enabled; - - $sent = $RCMAIL->deliver_message($MAIL_MIME, $from, $mailto, - $smtp_error, $mailbody_file, $smtp_opts, true); - - // return to compose page if sending failed - if (!$sent) { - // remove temp file - if ($mailbody_file) { - unlink($mailbody_file); - } - - if ($smtp_error && is_string($smtp_error)) { - $OUTPUT->show_message($smtp_error, 'error'); - } - else if ($smtp_error && !empty($smtp_error['label'])) { - $OUTPUT->show_message($smtp_error['label'], 'error', $smtp_error['vars']); - } - else { - $OUTPUT->show_message('sendingfailed', 'error'); - } - - $OUTPUT->send('iframe'); - } - - // save message sent time - if ($sendmail_delay) { - $RCMAIL->user->save_prefs(array('last_message_time' => time())); - } - - // set replied/forwarded flag - if ($COMPOSE['reply_uid']) { - foreach (rcmail::get_uids($COMPOSE['reply_uid'], $COMPOSE['mailbox']) as $mbox => $uids) { - // skip . replies - if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) { - $RCMAIL->storage->set_flag($uids, 'ANSWERED', $mbox); - } - } - } - else if ($COMPOSE['forward_uid']) { - foreach (rcmail::get_uids($COMPOSE['forward_uid'], $COMPOSE['mailbox']) as $mbox => $uids) { - // skip . forwards - if (!preg_match('/^\d+\.[0-9.]+$/', implode(',', (array) $uids))) { - $RCMAIL->storage->set_flag($uids, 'FORWARDED', $mbox); - } - } - } -} - -// Determine which folder to save message -if ($savedraft) { - $store_target = $drafts_mbox; + $sent = $SENDMAIL->deliver_message($MAIL_MIME); } -else if (!$RCMAIL->config->get('no_save_sent_messages')) { - if (isset($_POST['_store_target'])) { - $store_target = rcube_utils::get_input_value('_store_target', rcube_utils::INPUT_POST); - } - else { - $store_target = $RCMAIL->config->get('sent_mbox'); - } -} - -if ($store_target) { - // check if folder is subscribed - if ($RCMAIL->storage->folder_exists($store_target, true)) { - $store_folder = true; - } - // folder may be existing but not subscribed (#1485241) - else if (!$RCMAIL->storage->folder_exists($store_target)) { - $store_folder = $RCMAIL->storage->create_folder($store_target, true); - } - else if ($RCMAIL->storage->subscribe($store_target)) { - $store_folder = true; - } - - // append message to sent box - if ($store_folder) { - // message body in file - if ($mailbody_file || $MAIL_MIME->getParam('delay_file_io')) { - $headers = $MAIL_MIME->txtHeaders(); - - // file already created - if ($mailbody_file) { - $msg = $mailbody_file; - } - else { - $temp_dir = $RCMAIL->config->get('temp_dir'); - $mailbody_file = tempnam($temp_dir, 'rcmMsg'); - $msg = $MAIL_MIME->saveMessageBody($mailbody_file); - - if (!is_a($msg, 'PEAR_Error')) { - $msg = $mailbody_file; - } - } - } - else { - $msg = $MAIL_MIME->getMessage(); - $headers = ''; - } - - if (is_a($msg, 'PEAR_Error')) { - rcube::raise_error(array('code' => 650, 'type' => 'php', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not create message: ".$msg->getMessage()), - true, false); - } - else { - $saved = $RCMAIL->storage->save_message($store_target, $msg, $headers, - $mailbody_file ? true : false, array('SEEN')); - } - - if ($mailbody_file) { - unlink($mailbody_file); - $mailbody_file = null; - } - } - - // raise error if saving failed - if (!$saved) { - rcube::raise_error(array('code' => 800, 'type' => 'imap', - 'file' => __FILE__, 'line' => __LINE__, - 'message' => "Could not save message in $store_target"), true, false); - if ($savedraft) { - $RCMAIL->display_server_error('errorsaving'); +// Save the message in Drafts/Sent +$saved = $SENDMAIL->save_message($MAIL_MIME); - // start the auto-save timer again - $OUTPUT->command('auto_save_start'); - $OUTPUT->send('iframe'); - } - } -} -// remove temp file -else if ($mailbody_file) { - unlink($mailbody_file); +// raise error if saving failed +if (!$saved && $savedraft) { + $RCMAIL->display_server_error('errorsaving'); + // start the auto-save timer again + $OUTPUT->command('auto_save_start'); + $OUTPUT->send('iframe'); } +$store_target = $SENDMAIL->options['store_target']; +$store_folder = $SENDMAIL->options['store_folder']; + // delete previous saved draft -$old_id = rcube_utils::get_input_value('_draft_saveid', rcube_utils::INPUT_POST); +$drafts_mbox = $RCMAIL->config->get('drafts_mbox'); +$old_id = rcube_utils::get_input_value('_draft_saveid', rcube_utils::INPUT_POST); + if ($old_id && ($sent || $saved)) { $deleted = $RCMAIL->storage->delete_message($old_id, $drafts_mbox); @@ -734,7 +329,10 @@ else { $save_error = true; } else { - rcmail_compose_cleanup($COMPOSE_ID); + $RCMAIL->plugins->exec_hook('attachments_cleanup', array('group' => $COMPOSE_ID)); + $RCMAIL->session->remove('compose_data_' . $COMPOSE_ID); + $_SESSION['last_compose_session'] = $COMPOSE_ID; + $OUTPUT->command('remove_compose_data', $COMPOSE_ID); if ($store_folder) { @@ -748,216 +346,3 @@ else { } $OUTPUT->send('iframe'); - - -/****** message sending functions ********/ - -function rcmail_received_host($host) -{ - $hostname = gethostbyaddr($host); - - $result = rcmail_encrypt_host($hostname); - - if ($host != $hostname) { - $result .= ' (' . rcmail_encrypt_host($host) . ')'; - } - - return $result; -} - -// encrypt host IP or hostname for Received header -function rcmail_encrypt_host($host) -{ - global $RCMAIL; - - if ($RCMAIL->config->get('http_received_header_encrypt')) { - return $RCMAIL->encrypt($host); - } - - if (!preg_match('/[^0-9:.]/', $host)) { - return "[$host]"; - } - - return $host; -} - -// get identity record -function rcmail_get_identity($id) -{ - global $RCMAIL, $message_charset; - - if ($sql_arr = $RCMAIL->user->get_identity($id)) { - $out = $sql_arr; - - if ($message_charset != RCUBE_CHARSET) { - foreach ($out as $k => $v) { - $out[$k] = rcube_charset::convert($v, RCUBE_CHARSET, $message_charset); - } - } - - $out['mailto'] = $sql_arr['email']; - $out['string'] = format_email_recipient($sql_arr['email'], $sql_arr['name']); - - return $out; - } - - return false; -} - -/** - * Extract image attachments from HTML content (data URIs) - */ -function rcmail_extract_inline_images($mime_message, $from) -{ - $body = $mime_message->getHTMLBody(); - $offset = 0; - $list = array(); - $domain = 'localhost'; - $regexp = '#img[^>]+src=[\'"](data:([^;]*);base64,([a-z0-9+/=\r\n]+))([\'"])#i'; - - if (preg_match_all($regexp, $body, $matches, PREG_OFFSET_CAPTURE)) { - // get domain for the Content-ID, must be the same as in Mail_Mime::get() - if (preg_match('#@([0-9a-zA-Z\-\.]+)#', $from, $m)) { - $domain = $m[1]; - } - - foreach ($matches[1] as $idx => $m) { - $data = preg_replace('/\r\n/', '', $matches[3][$idx][0]); - $data = base64_decode($data); - - if (empty($data)) { - continue; - } - - $hash = md5($data) . '@' . $domain; - $mime_type = $matches[2][$idx][0]; - $name = $list[$hash]; - - if (empty($mime_type)) { - $mime_type = rcube_mime::image_content_type($data); - } - - // add the image to the MIME message - if (!$name) { - $ext = preg_replace('#^[^/]+/#', '', $mime_type); - $name = substr($hash, 0, 8) . '.' . $ext; - $list[$hash] = $name; - - $mime_message->addHTMLImage($data, $mime_type, $name, false, $hash); - } - - $body = substr_replace($body, $name, $m[1] + $offset, strlen($m[0])); - $offset += strlen($name) - strlen($m[0]); - } - } - - $mime_message->setHTMLBody($body); -} - -/** - * Parse and cleanup email address input (and count addresses) - * - * @param string Address input - * @param boolean Do count recipients (saved in global $RECIPIENT_COUNT) - * @param boolean Validate addresses (errors saved in global $EMAIL_FORMAT_ERROR) - * @return string Canonical recipients string separated by comma - */ -function rcmail_email_input_format($mailto, $count=false, $check=true) -{ - global $RCMAIL, $EMAIL_FORMAT_ERROR, $RECIPIENT_COUNT; - - // simplified email regexp, supporting quoted local part - $email_regexp = '(\S+|("[^"]+"))@\S+'; - - $delim = ',;'; - $regexp = array("/[$delim]\s*[\r\n]+/", '/[\r\n]+/', "/[$delim]\s*\$/m", '/;/', '/(\S{1})(<'.$email_regexp.'>)/U'); - $replace = array(', ', ', ', '', ',', '\\1 \\2'); - - // replace new lines and strip ending ', ', make address input more valid - $mailto = trim(preg_replace($regexp, $replace, $mailto)); - $items = rcube_utils::explode_quoted_string("[$delim]", $mailto); - $result = array(); - - foreach ($items as $item) { - $item = trim($item); - // address in brackets without name (do nothing) - if (preg_match('/^<'.$email_regexp.'>$/', $item)) { - $item = rcube_utils::idn_to_ascii(trim($item, '<>')); - $result[] = $item; - } - // address without brackets and without name (add brackets) - else if (preg_match('/^'.$email_regexp.'$/', $item)) { - $item = rcube_utils::idn_to_ascii($item); - $result[] = $item; - } - // address with name (handle name) - else if (preg_match('/<*'.$email_regexp.'>*$/', $item, $matches)) { - $address = $matches[0]; - $name = trim(str_replace($address, '', $item)); - if ($name[0] == '"' && $name[count($name)-1] == '"') { - $name = substr($name, 1, -1); - } - $name = stripcslashes($name); - $address = rcube_utils::idn_to_ascii(trim($address, '<>')); - $result[] = format_email_recipient($address, $name); - $item = $address; - } - - // check address format - $item = trim($item, '<>'); - if ($item && $check && !rcube_utils::check_email($item)) { - $EMAIL_FORMAT_ERROR = $item; - return; - } - } - - if ($count) { - $RECIPIENT_COUNT += count($result); - } - - return implode(', ', $result); -} - - -function rcmail_generic_message_footer($isHtml) -{ - global $RCMAIL; - - if ($isHtml && ($file = $RCMAIL->config->get('generic_message_footer_html'))) { - $html_footer = true; - } - else { - $file = $RCMAIL->config->get('generic_message_footer'); - $html_footer = false; - } - - if ($file && realpath($file)) { - // sanity check - if (!preg_match('/\.(php|ini|conf)$/', $file) && strpos($file, '/etc/') === false) { - $footer = file_get_contents($file); - if ($isHtml && !$html_footer) { - $t2h = new rcube_text2html($footer, false); - $footer = $t2h->get_html(); - } - return $footer; - } - } - - return false; -} - -/** - * clear message composing settings - */ -function rcmail_compose_cleanup($id) -{ - if (!isset($_SESSION['compose_data_'.$id])) { - return; - } - - $rcmail = rcmail::get_instance(); - $rcmail->plugins->exec_hook('attachments_cleanup', array('group' => $id)); - $rcmail->session->remove('compose_data_'.$id); - - $_SESSION['last_compose_session'] = $id; -}