mirror of
https://github.com/roundcube/roundcubemail.git
synced 2026-03-04 07:14:02 +01:00
This implements rendering mime-types with content-type 'text/markdown' and 'text/x-markdown' into HTML in the preview and show views (if not "dispositioned" as "attachment"), but not in the get view for attached files (the one opening attachments in an external window.)
1734 lines
66 KiB
PHP
1734 lines
66 KiB
PHP
<?php
|
|
|
|
/*
|
|
+-----------------------------------------------------------------------+
|
|
| This file is part of the Roundcube Webmail client |
|
|
| |
|
|
| Copyright (C) The Roundcube Dev Team |
|
|
| |
|
|
| Licensed under the GNU General Public License version 3 or |
|
|
| any later version with exceptions for skins & plugins. |
|
|
| See the README file for a full license statement. |
|
|
| |
|
|
| PURPOSE: |
|
|
| Compose a new mail message with all headers and attachments |
|
|
+-----------------------------------------------------------------------+
|
|
| Author: Thomas Bruederli <roundcube@gmail.com> |
|
|
+-----------------------------------------------------------------------+
|
|
*/
|
|
|
|
class rcmail_action_mail_compose extends rcmail_action_mail_index
|
|
{
|
|
protected static $COMPOSE_ID;
|
|
protected static $COMPOSE;
|
|
|
|
/** @var rcube_message|stdClass|null Mail message */
|
|
protected static $MESSAGE;
|
|
protected static $MESSAGE_BODY;
|
|
protected static $CID_MAP = [];
|
|
protected static $HTML_MODE = false;
|
|
protected static $SENDMAIL;
|
|
|
|
/**
|
|
* Request handler.
|
|
*
|
|
* @param array $args Arguments from the previous step(s)
|
|
*/
|
|
#[Override]
|
|
public function run($args = [])
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
self::$COMPOSE_ID = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GET);
|
|
self::$COMPOSE = null;
|
|
|
|
if (self::$COMPOSE_ID && !empty($_SESSION['compose_data_' . self::$COMPOSE_ID])) {
|
|
self::$COMPOSE = &$_SESSION['compose_data_' . self::$COMPOSE_ID];
|
|
}
|
|
|
|
// give replicated session storage some time to synchronize
|
|
$retries = 0;
|
|
while (self::$COMPOSE_ID && !is_array(self::$COMPOSE) && $rcmail->db->is_replicated() && $retries++ < 5) {
|
|
usleep(500000);
|
|
$rcmail->session->reload();
|
|
if ($_SESSION['compose_data_' . self::$COMPOSE_ID]) {
|
|
self::$COMPOSE = &$_SESSION['compose_data_' . self::$COMPOSE_ID];
|
|
}
|
|
}
|
|
|
|
// Nothing below is called during message composition, only at "new/forward/reply/draft" initialization or
|
|
// if a compose-ID is given (i.e. when the compose step is opened in a new window/tab).
|
|
if (!is_array(self::$COMPOSE)) {
|
|
// Infinite redirect prevention in case of broken session (#1487028)
|
|
if (self::$COMPOSE_ID) {
|
|
// if we know the message with specified ID was already sent
|
|
// we can ignore the error and compose a new message (#1490009)
|
|
if (
|
|
!isset($_SESSION['last_compose_session'])
|
|
|| self::$COMPOSE_ID != $_SESSION['last_compose_session']
|
|
) {
|
|
rcube::raise_error(450, false, true);
|
|
}
|
|
}
|
|
|
|
self::$COMPOSE_ID = uniqid(mt_rand());
|
|
$params = rcube_utils::request2param(rcube_utils::INPUT_GET, 'task|action', true);
|
|
|
|
$_SESSION['compose_data_' . self::$COMPOSE_ID] = [
|
|
'id' => self::$COMPOSE_ID,
|
|
'param' => $params,
|
|
'mailbox' => isset($params['mbox']) && strlen($params['mbox'])
|
|
? $params['mbox'] : $rcmail->storage->get_folder(),
|
|
];
|
|
|
|
self::$COMPOSE = &$_SESSION['compose_data_' . self::$COMPOSE_ID];
|
|
self::process_compose_params(self::$COMPOSE);
|
|
|
|
// check if folder for saving sent messages exists and is subscribed (#1486802)
|
|
if (!empty(self::$COMPOSE['param']['sent_mbox'])) {
|
|
$sent_folder = self::$COMPOSE['param']['sent_mbox'];
|
|
rcmail_sendmail::check_sent_folder($sent_folder, true);
|
|
}
|
|
|
|
// redirect to a unique URL with all parameters stored in session
|
|
$rcmail->output->redirect([
|
|
'_action' => 'compose',
|
|
'_id' => self::$COMPOSE['id'],
|
|
'_search' => !empty($_REQUEST['_search']) ? $_REQUEST['_search'] : null,
|
|
]);
|
|
}
|
|
|
|
// add some labels to client
|
|
$rcmail->output->add_label('notuploadedwarning', 'savingmessage', 'siginserted', 'responseinserted',
|
|
'messagesaved', 'converting', 'editorwarning', 'discard',
|
|
'fileuploaderror', 'sendmessage', 'newresponse', 'responsename', 'responsetext', 'save',
|
|
'savingresponse', 'restoresavedcomposedata', 'restoremessage', 'delete', 'restore', 'ignore',
|
|
'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender',
|
|
'encryptnoattachments', 'encryptedsendialog', 'searchpubkeyservers', 'importpubkeys',
|
|
'encryptpubkeysfound', 'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
|
|
'keyrevoked', 'keyimportsuccess', 'keyservererror', 'attaching', 'namex', 'attachmentrename'
|
|
);
|
|
|
|
$rcmail->output->set_pagetitle($rcmail->gettext('compose'));
|
|
|
|
$rcmail->output->set_env('compose_id', self::$COMPOSE['id']);
|
|
$rcmail->output->set_env('session_id', session_id());
|
|
$rcmail->output->set_env('mailbox', $rcmail->storage->get_folder());
|
|
$rcmail->output->set_env('top_posting', intval($rcmail->config->get('reply_mode')) > 0);
|
|
$rcmail->output->set_env('sig_below', $rcmail->config->get('sig_below'));
|
|
$rcmail->output->set_env('save_localstorage', (bool) $rcmail->config->get('compose_save_localstorage'));
|
|
$rcmail->output->set_env('is_sent', false);
|
|
$rcmail->output->set_env('mimetypes', self::supported_mimetypes());
|
|
$rcmail->output->set_env('keyservers', $rcmail->config->keyservers());
|
|
$rcmail->output->set_env('mailvelope_main_keyring', $rcmail->config->get('mailvelope_main_keyring'));
|
|
|
|
$drafts_mbox = $rcmail->config->get('drafts_mbox');
|
|
$config_show_sig = $rcmail->config->get('show_sig', 1);
|
|
|
|
// add config parameters to client script
|
|
if (strlen($drafts_mbox)) {
|
|
$rcmail->output->set_env('drafts_mailbox', $drafts_mbox);
|
|
$rcmail->output->set_env('draft_autosave', $rcmail->config->get('draft_autosave'));
|
|
}
|
|
|
|
// default font for HTML editor
|
|
$font = self::font_defs($rcmail->config->get('default_font'));
|
|
if ($font && !is_array($font)) {
|
|
$rcmail->output->set_env('default_font', $font);
|
|
}
|
|
|
|
// default font size for HTML editor
|
|
$font_size = self::fontsize_defs($rcmail->config->get('default_font_size'));
|
|
if ($font_size && !is_array($font_size)) {
|
|
$rcmail->output->set_env('default_font_size', $font_size);
|
|
}
|
|
|
|
$compose_mode = null;
|
|
$msg_uid = null;
|
|
$options = [];
|
|
|
|
// get reference message and set compose mode
|
|
if (!empty(self::$COMPOSE['param']['draft_uid'])) {
|
|
$msg_uid = self::$COMPOSE['param']['draft_uid'];
|
|
$compose_mode = rcmail_sendmail::MODE_DRAFT;
|
|
$rcmail->output->set_env('draft_id', $msg_uid);
|
|
$rcmail->storage->set_folder($drafts_mbox);
|
|
} elseif (!empty(self::$COMPOSE['param']['reply_uid'])) {
|
|
$msg_uid = self::$COMPOSE['param']['reply_uid'];
|
|
$compose_mode = rcmail_sendmail::MODE_REPLY;
|
|
} elseif (!empty(self::$COMPOSE['param']['forward_uid'])) {
|
|
$msg_uid = self::$COMPOSE['param']['forward_uid'];
|
|
$compose_mode = rcmail_sendmail::MODE_FORWARD;
|
|
self::$COMPOSE['forward_uid'] = $msg_uid;
|
|
self::$COMPOSE['as_attachment'] = !empty(self::$COMPOSE['param']['attachment']);
|
|
} elseif (!empty(self::$COMPOSE['param']['uid'])) {
|
|
$msg_uid = self::$COMPOSE['param']['uid'];
|
|
$compose_mode = rcmail_sendmail::MODE_EDIT;
|
|
}
|
|
|
|
self::$COMPOSE['mode'] = $compose_mode;
|
|
|
|
if ($compose_mode) {
|
|
$rcmail->output->set_env('compose_mode', $compose_mode);
|
|
}
|
|
|
|
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) {
|
|
$rcmail->output->set_env('show_sig_later', true);
|
|
}
|
|
} elseif ($config_show_sig == 1) {
|
|
$rcmail->output->set_env('show_sig', true);
|
|
} elseif ($config_show_sig == 2 && empty($compose_mode)) {
|
|
$rcmail->output->set_env('show_sig', true);
|
|
} elseif (
|
|
$config_show_sig == 3
|
|
&& ($compose_mode == rcmail_sendmail::MODE_REPLY || $compose_mode == rcmail_sendmail::MODE_FORWARD)
|
|
) {
|
|
$rcmail->output->set_env('show_sig', true);
|
|
}
|
|
|
|
if (!empty($msg_uid) && (empty(self::$COMPOSE['as_attachment']) || $compose_mode == rcmail_sendmail::MODE_DRAFT)) {
|
|
$mbox_name = $rcmail->storage->get_folder();
|
|
|
|
// set format before rcube_message construction
|
|
// use the same format as for the message view
|
|
if (isset($_SESSION['msg_formats'][$mbox_name . ':' . $msg_uid])) {
|
|
$rcmail->config->set('prefer_html', $_SESSION['msg_formats'][$mbox_name . ':' . $msg_uid]);
|
|
} else {
|
|
$prefer_html = $rcmail->config->get('prefer_html')
|
|
|| $rcmail->config->get('htmleditor')
|
|
|| $compose_mode == rcmail_sendmail::MODE_DRAFT
|
|
|| $compose_mode == rcmail_sendmail::MODE_EDIT;
|
|
|
|
$rcmail->config->set('prefer_html', $prefer_html);
|
|
}
|
|
|
|
self::$MESSAGE = new rcube_message($msg_uid);
|
|
|
|
// make sure message is marked as read
|
|
if (
|
|
!empty(self::$MESSAGE->headers)
|
|
&& self::$MESSAGE->context === null
|
|
&& empty(self::$MESSAGE->headers->flags['SEEN'])
|
|
) {
|
|
$rcmail->storage->set_flag($msg_uid, 'SEEN');
|
|
}
|
|
|
|
if (!empty(self::$MESSAGE->headers->charset)) {
|
|
$rcmail->storage->set_charset(self::$MESSAGE->headers->charset);
|
|
}
|
|
|
|
if (empty(self::$MESSAGE->headers)) {
|
|
// error
|
|
} elseif ($compose_mode == rcmail_sendmail::MODE_FORWARD || $compose_mode == rcmail_sendmail::MODE_REPLY) {
|
|
if ($compose_mode == rcmail_sendmail::MODE_REPLY) {
|
|
self::$COMPOSE['reply_uid'] = self::$MESSAGE->context === null ? $msg_uid : null;
|
|
|
|
if (!empty(self::$COMPOSE['param']['all'])) {
|
|
self::$COMPOSE['reply_all'] = self::$COMPOSE['param']['all'];
|
|
}
|
|
} else {
|
|
self::$COMPOSE['forward_uid'] = $msg_uid;
|
|
}
|
|
|
|
self::$COMPOSE['reply_msgid'] = self::$MESSAGE->headers->messageID;
|
|
self::$COMPOSE['references'] = trim(self::$MESSAGE->headers->references . ' ' . self::$MESSAGE->headers->messageID);
|
|
|
|
// Save the sent message in the same folder of the message being replied to
|
|
if (
|
|
$rcmail->config->get('reply_same_folder')
|
|
&& ($sent_folder = self::$COMPOSE['mailbox'])
|
|
&& rcmail_sendmail::check_sent_folder($sent_folder, false)
|
|
) {
|
|
self::$COMPOSE['param']['sent_mbox'] = $sent_folder;
|
|
}
|
|
}
|
|
// @phpstan-ignore-next-line
|
|
elseif ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) {
|
|
$info = self::$MESSAGE->headers->get('x-draft-info');
|
|
$info = $info ? rcmail_sendmail::draftinfo_decode($info) : [];
|
|
|
|
if (!empty($info['dsn']) && $info['dsn'] === 'on') {
|
|
$options['dsn_enabled'] = true;
|
|
}
|
|
|
|
if (!empty($info['keep_formatting']) && $info['keep_formatting'] === 'on') {
|
|
$options['keep_formatting_enabled'] = true;
|
|
}
|
|
|
|
if ($compose_mode == rcmail_sendmail::MODE_DRAFT) {
|
|
// get reply_uid/forward_uid to flag the original message when sending
|
|
if (!empty($info['type'])) {
|
|
if ($info['type'] == 'reply') {
|
|
self::$COMPOSE['reply_uid'] = $info['uid'];
|
|
} elseif ($info['type'] == 'forward') {
|
|
self::$COMPOSE['forward_uid'] = $info['uid'];
|
|
}
|
|
}
|
|
|
|
self::$COMPOSE['mailbox'] = $info['folder'] ?? null;
|
|
|
|
// Save the sent message in the same folder of the message being replied to
|
|
if (
|
|
$rcmail->config->get('reply_same_folder')
|
|
&& ($sent_folder = self::$COMPOSE['mailbox'])
|
|
&& rcmail_sendmail::check_sent_folder($sent_folder, false)
|
|
) {
|
|
self::$COMPOSE['param']['sent_mbox'] = $sent_folder;
|
|
}
|
|
|
|
if (($msgid = self::$MESSAGE->headers->get('message-id')) && !preg_match('/^mid:[0-9]+$/', $msgid)) {
|
|
self::$COMPOSE['param']['message-id'] = $msgid;
|
|
}
|
|
|
|
// use message UID as draft_id
|
|
$rcmail->output->set_env('draft_id', $msg_uid);
|
|
}
|
|
|
|
if ($in_reply_to = self::$MESSAGE->headers->get('in-reply-to')) {
|
|
self::$COMPOSE['reply_msgid'] = '<' . $in_reply_to . '>';
|
|
}
|
|
|
|
self::$COMPOSE['references'] = self::$MESSAGE->headers->references;
|
|
}
|
|
} else {
|
|
self::$MESSAGE = new stdClass();
|
|
|
|
// apply mailto: URL parameters
|
|
if (!empty(self::$COMPOSE['param']['in-reply-to'])) {
|
|
self::$COMPOSE['reply_msgid'] = '<' . trim(self::$COMPOSE['param']['in-reply-to'], '<> ') . '>';
|
|
}
|
|
|
|
if (!empty(self::$COMPOSE['param']['references'])) {
|
|
self::$COMPOSE['references'] = self::$COMPOSE['param']['references'];
|
|
}
|
|
}
|
|
|
|
if (!empty(self::$COMPOSE['reply_msgid'])) {
|
|
$rcmail->output->set_env('reply_msgid', self::$COMPOSE['reply_msgid']);
|
|
}
|
|
|
|
$options['message'] = self::$MESSAGE;
|
|
|
|
// Initialize helper class to build the UI
|
|
self::$SENDMAIL = new rcmail_sendmail(self::$COMPOSE, $options);
|
|
|
|
// process self::$MESSAGE body/attachments, set self::$MESSAGE_BODY/$HTML_MODE vars and some session data
|
|
self::$MESSAGE_BODY = self::prepare_message_body();
|
|
|
|
// register UI objects (Note: some objects are registered by rcmail_sendmail above)
|
|
$rcmail->output->add_handlers([
|
|
'composebody' => [$this, 'compose_body'],
|
|
'composeobjects' => [$this, 'compose_objects'],
|
|
'composeattachmentlist' => [$this, 'compose_attachment_list'],
|
|
'composeattachmentform' => [$this, 'compose_attachment_form'],
|
|
'composeattachment' => [$this, 'compose_attachment_field'],
|
|
'filedroparea' => [$this, 'compose_file_drop_area'],
|
|
'editorselector' => [$this, 'editor_selector'],
|
|
'addressbooks' => [$this, 'addressbook_list'],
|
|
'addresslist' => [$this, 'contacts_list'],
|
|
'responseslist' => [$this, 'compose_responses_list'],
|
|
]);
|
|
|
|
$rcmail->output->include_script('publickey.js');
|
|
|
|
self::spellchecker_init();
|
|
|
|
$rcmail->output->send('compose');
|
|
}
|
|
|
|
/**
|
|
* process compose request parameters
|
|
*/
|
|
public static function process_compose_params(&$COMPOSE)
|
|
{
|
|
if (!empty($COMPOSE['param']['to'])) {
|
|
$mailto = explode('?', $COMPOSE['param']['to'], 2);
|
|
|
|
// #1486037: remove "mailto:" prefix
|
|
$COMPOSE['param']['to'] = preg_replace('/^mailto:/i', '', $mailto[0]);
|
|
// #1490346: decode the recipient address
|
|
// #1490510: use raw encoding for correct "+" character handling as specified in RFC6068
|
|
$COMPOSE['param']['to'] = rawurldecode($COMPOSE['param']['to']);
|
|
|
|
// Supported case-insensitive tokens in mailto URL
|
|
$url_tokens = ['to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'references', 'subject', 'body'];
|
|
|
|
if (!empty($mailto[1])) {
|
|
parse_str($mailto[1], $query);
|
|
foreach ($query as $f => $val) {
|
|
if (($key = array_search(strtolower($f), $url_tokens)) !== false) {
|
|
$f = $url_tokens[$key];
|
|
}
|
|
|
|
// merge mailto: addresses with addresses from 'to' parameter
|
|
if ($f == 'to' && !empty($COMPOSE['param']['to'])) {
|
|
$to_addresses = rcube_mime::decode_address_list($COMPOSE['param']['to'], null, true, null, true);
|
|
$add_addresses = rcube_mime::decode_address_list($val, null, true);
|
|
|
|
foreach ($add_addresses as $addr) {
|
|
if (!in_array($addr['mailto'], $to_addresses)) {
|
|
$to_addresses[] = $addr['mailto'];
|
|
$COMPOSE['param']['to'] .= ', ' . $addr['string'];
|
|
}
|
|
}
|
|
} else {
|
|
$COMPOSE['param'][$f] = $val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// resolve _forward_uid=* to an absolute list of messages from a search result
|
|
if (
|
|
!empty($COMPOSE['param']['forward_uid'])
|
|
&& $COMPOSE['param']['forward_uid'] == '*'
|
|
&& !empty($_SESSION['search'][1])
|
|
&& is_object($_SESSION['search'][1])
|
|
) {
|
|
$COMPOSE['param']['forward_uid'] = $_SESSION['search'][1]->get();
|
|
}
|
|
|
|
// clean HTML message body which can be submitted by URL
|
|
if (!empty($COMPOSE['param']['body'])) {
|
|
if ($COMPOSE['param']['html'] = strpos($COMPOSE['param']['body'], '<') !== false) {
|
|
$wash_params = ['safe' => false];
|
|
$COMPOSE['param']['body'] = self::prepare_html_body($COMPOSE['param']['body'], $wash_params);
|
|
}
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// select folder where to save the sent message
|
|
$COMPOSE['param']['sent_mbox'] = $rcmail->config->get('sent_mbox');
|
|
|
|
// pipe compose parameters thru plugins
|
|
$plugin = $rcmail->plugins->exec_hook('message_compose', $COMPOSE);
|
|
|
|
$COMPOSE['param'] = array_merge($COMPOSE['param'], $plugin['param']);
|
|
|
|
// add attachments listed by message_compose hook
|
|
if (!empty($plugin['attachments'])) {
|
|
foreach ($plugin['attachments'] as $attach) {
|
|
// we have structured data
|
|
if (is_array($attach)) {
|
|
$attachment = $attach + ['group' => self::$COMPOSE_ID];
|
|
}
|
|
// only a file path is given
|
|
else {
|
|
$filename = basename($attach);
|
|
$attachment = [
|
|
'group' => self::$COMPOSE_ID,
|
|
'name' => $filename,
|
|
'mimetype' => rcube_mime::file_content_type($attach, $filename),
|
|
'size' => filesize($attach),
|
|
'path' => $attach,
|
|
];
|
|
}
|
|
|
|
// save attachment if valid
|
|
if (
|
|
(!empty($attachment['data']) && !empty($attachment['name']))
|
|
|| (!empty($attachment['path']) && file_exists($attachment['path']))
|
|
) {
|
|
$rcmail->insert_uploaded_file($attachment, 'attachment_save');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decide whether to use HTML or plain text
|
|
*/
|
|
public static function compose_editor_mode()
|
|
{
|
|
static $useHtml;
|
|
|
|
if ($useHtml !== null) {
|
|
return $useHtml;
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$html_editor = intval($rcmail->config->get('htmleditor'));
|
|
$compose_mode = self::$COMPOSE['mode'];
|
|
|
|
if (isset(self::$COMPOSE['param']['html']) && is_bool(self::$COMPOSE['param']['html'])) {
|
|
$useHtml = self::$COMPOSE['param']['html'];
|
|
} elseif (isset($_POST['_is_html'])) {
|
|
$useHtml = !empty($_POST['_is_html']);
|
|
} elseif ($compose_mode == rcmail_sendmail::MODE_DRAFT || $compose_mode == rcmail_sendmail::MODE_EDIT) {
|
|
$useHtml = self::message_is_html();
|
|
} elseif ($compose_mode == rcmail_sendmail::MODE_REPLY) {
|
|
$useHtml = $html_editor == 1 || ($html_editor >= 2 && self::message_is_html());
|
|
} elseif ($compose_mode == rcmail_sendmail::MODE_FORWARD) {
|
|
$useHtml = $html_editor == 1 || $html_editor == 4
|
|
|| ($html_editor == 3 && self::message_is_html());
|
|
} else {
|
|
$useHtml = $html_editor == 1 || $html_editor == 4;
|
|
}
|
|
|
|
return $useHtml;
|
|
}
|
|
|
|
/**
|
|
* Check whether user prefers HTML and the message has HTML content
|
|
*/
|
|
public static function message_is_html()
|
|
{
|
|
return rcmail::get_instance()->config->get('prefer_html')
|
|
&& (self::$MESSAGE instanceof rcube_message)
|
|
&& self::$MESSAGE->has_html_part(true);
|
|
}
|
|
|
|
/**
|
|
* Initialize spellchecker
|
|
*/
|
|
public static function spellchecker_init()
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
if (!$rcmail->config->get('enable_spellcheck')) {
|
|
return;
|
|
}
|
|
|
|
$spellchecker = new rcube_spellchecker();
|
|
|
|
// Set language list
|
|
$spellcheck_langs = $spellchecker->languages();
|
|
|
|
if (empty($spellcheck_langs)) {
|
|
if ($err = $spellchecker->error()) {
|
|
rcube::raise_error('Spell check engine error: ' . trim($err), true);
|
|
}
|
|
} else {
|
|
$dictionary = (bool) $rcmail->config->get('spellcheck_dictionary');
|
|
$lang = $_SESSION['language'];
|
|
|
|
// if not found in the list, try with two-letter code
|
|
if (empty($spellcheck_langs[$lang])) {
|
|
$lang = strtolower(substr($lang, 0, 2));
|
|
}
|
|
|
|
if (empty($spellcheck_langs[$lang])) {
|
|
$lang = 'en';
|
|
}
|
|
|
|
// include GoogieSpell
|
|
$rcmail->output->include_script('googiespell.js');
|
|
$rcmail->output->add_script(sprintf(
|
|
"var googie = new GoogieSpell('%s/images/googiespell/','%s&lang=', %s);\n"
|
|
. "googie.lang_chck_spell = \"%s\";\n"
|
|
. "googie.lang_rsm_edt = \"%s\";\n"
|
|
. "googie.lang_close = \"%s\";\n"
|
|
. "googie.lang_revert = \"%s\";\n"
|
|
. "googie.lang_no_error_found = \"%s\";\n"
|
|
. "googie.lang_learn_word = \"%s\";\n"
|
|
. "googie.setLanguages(%s);\n"
|
|
. "googie.setCurrentLanguage('%s');\n"
|
|
. "googie.setDecoration(false);\n"
|
|
. "googie.decorateTextarea(rcmail.env.composebody);\n",
|
|
$rcmail->output->asset_url($rcmail->output->get_skin_path()),
|
|
$rcmail->url(['_task' => 'utils', '_action' => 'spell', '_remote' => 1]),
|
|
!empty($dictionary) ? 'true' : 'false',
|
|
rcube::JQ(rcube::Q($rcmail->gettext('checkspelling'))),
|
|
rcube::JQ(rcube::Q($rcmail->gettext('resumeediting'))),
|
|
rcube::JQ(rcube::Q($rcmail->gettext('close'))),
|
|
rcube::JQ(rcube::Q($rcmail->gettext('revertto'))),
|
|
rcube::JQ(rcube::Q($rcmail->gettext('nospellerrors'))),
|
|
rcube::JQ(rcube::Q($rcmail->gettext('addtodict'))),
|
|
rcube_output::json_serialize($spellcheck_langs),
|
|
$lang
|
|
), 'foot');
|
|
|
|
$rcmail->output->add_label('checking');
|
|
$rcmail->output->set_env('spell_langs', $spellcheck_langs);
|
|
$rcmail->output->set_env('spell_lang', $lang);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Prepare composed message body
|
|
*/
|
|
public static function prepare_message_body()
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$body = '';
|
|
|
|
// use posted message body
|
|
if (!empty($_POST['_message'])) {
|
|
$body = rcube_utils::get_input_string('_message', rcube_utils::INPUT_POST, true);
|
|
$isHtml = (bool) rcube_utils::get_input_string('_is_html', rcube_utils::INPUT_POST);
|
|
} elseif (!empty(self::$COMPOSE['param']['body'])) {
|
|
$body = self::$COMPOSE['param']['body'];
|
|
$isHtml = !empty(self::$COMPOSE['param']['html']);
|
|
}
|
|
// forward as attachment
|
|
elseif (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD && !empty(self::$COMPOSE['as_attachment'])) {
|
|
$isHtml = self::compose_editor_mode();
|
|
|
|
self::write_forward_attachments();
|
|
}
|
|
// reply/edit/draft/forward
|
|
elseif (!empty(self::$COMPOSE['mode'])
|
|
&& (self::$COMPOSE['mode'] != rcmail_sendmail::MODE_REPLY || intval($rcmail->config->get('reply_mode')) != -1)
|
|
) {
|
|
$isHtml = self::compose_editor_mode();
|
|
$messages = [];
|
|
|
|
// Create a (fake) image attachments map. We need it before we handle
|
|
// the message body. After that we'll go throughout the list and check
|
|
// which images were used in the body and attach them for real or skip.
|
|
if ($isHtml) {
|
|
self::$CID_MAP = self::cid_map(self::$MESSAGE);
|
|
}
|
|
|
|
// set is_safe flag (before HTML body washing)
|
|
if (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT) {
|
|
self::$MESSAGE->is_safe = true;
|
|
} else {
|
|
self::check_safe(self::$MESSAGE);
|
|
}
|
|
|
|
if (!empty(self::$MESSAGE->parts)) {
|
|
// collect IDs of message/rfc822 parts
|
|
foreach (self::$MESSAGE->mime_parts() as $part) {
|
|
if ($part->mimetype == 'message/rfc822') {
|
|
$messages[] = $part->mime_id;
|
|
}
|
|
}
|
|
|
|
foreach (self::$MESSAGE->parts as $part) {
|
|
if (!empty($part->realtype) && $part->realtype == 'multipart/encrypted') {
|
|
// find the encrypted message payload part
|
|
if ($pgp_mime_part = self::$MESSAGE->get_multipart_encrypted_part()) {
|
|
$rcmail->output->set_env('pgp_mime_message', [
|
|
'_mbox' => $rcmail->storage->get_folder(),
|
|
'_uid' => self::$MESSAGE->uid,
|
|
'_part' => $pgp_mime_part->mime_id,
|
|
]);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// skip no-content and attachment parts (#1488557)
|
|
if ($part->type != 'content' || !$part->size || self::$MESSAGE->is_attachment($part)) {
|
|
continue;
|
|
}
|
|
|
|
// skip all content parts inside the message/rfc822 part
|
|
foreach ($messages as $mimeid) {
|
|
if (str_starts_with($part->mime_id, $mimeid . '.')) {
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
if ($part_body = self::compose_part_body($part, $isHtml)) {
|
|
$body .= ($body ? ($isHtml ? '<br/>' : "\n") : '') . $part_body;
|
|
}
|
|
}
|
|
}
|
|
|
|
// compose reply-body
|
|
if (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
|
|
$body = self::create_reply_body($body, $isHtml);
|
|
|
|
if (!empty(self::$MESSAGE->pgp_mime)) {
|
|
$rcmail->output->set_env('compose_reply_header', self::get_reply_header(self::$MESSAGE));
|
|
}
|
|
}
|
|
// forward message body inline
|
|
elseif (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_FORWARD) {
|
|
$body = self::create_forward_body($body, $isHtml);
|
|
}
|
|
// load draft message body
|
|
elseif (
|
|
self::$COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT
|
|
|| self::$COMPOSE['mode'] == rcmail_sendmail::MODE_EDIT
|
|
) {
|
|
$body = self::create_draft_body($body, $isHtml);
|
|
}
|
|
|
|
// Save forwarded files (or inline images) as attachments
|
|
// This will also update inline images location in the body
|
|
self::write_compose_attachments(self::$MESSAGE, $isHtml, $body);
|
|
}
|
|
// new message
|
|
else {
|
|
$isHtml = self::compose_editor_mode();
|
|
}
|
|
|
|
$plugin = $rcmail->plugins->exec_hook('message_compose_body', [
|
|
'body' => $body,
|
|
'html' => $isHtml,
|
|
'mode' => self::$COMPOSE['mode'],
|
|
'message' => self::$MESSAGE,
|
|
]);
|
|
|
|
$body = $plugin['body'];
|
|
unset($plugin);
|
|
|
|
// add blocked.gif attachment (#1486516)
|
|
$regexp = '/ src="' . preg_quote($rcmail->output->asset_url('program/resources/blocked.gif'), '/') . '"/';
|
|
if ($isHtml && preg_match($regexp, $body)) {
|
|
$content = self::get_resource_content('blocked.gif');
|
|
|
|
if ($content && ($attachment = self::save_image('blocked.gif', 'image/gif', $content))) {
|
|
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
|
|
$rcmail->comm_path, self::$COMPOSE['id'], $attachment['id']);
|
|
$body = preg_replace($regexp, ' src="' . $url . '"', $body);
|
|
}
|
|
}
|
|
|
|
self::$HTML_MODE = $isHtml;
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Prepare message part body for composition
|
|
*
|
|
* @param rcube_message_part $part Message part
|
|
* @param bool $isHtml Use HTML mode
|
|
*
|
|
* @return string Message body text
|
|
*/
|
|
public static function compose_part_body($part, $isHtml = false)
|
|
{
|
|
// Check if we have enough memory to handle the message in it
|
|
// #1487424: we need up to 10x more memory than the body
|
|
if (!rcube_utils::mem_check($part->size * 10)) {
|
|
return '';
|
|
}
|
|
|
|
// fetch part if not available
|
|
$body = self::$MESSAGE->get_part_body($part->mime_id, true);
|
|
|
|
// message is cached but not exists (#1485443), or other error
|
|
if ($body === false) {
|
|
return '';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// register this part as pgp encrypted
|
|
if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) {
|
|
self::$MESSAGE->pgp_mime = true;
|
|
$rcmail->output->set_env('pgp_mime_message', [
|
|
'_mbox' => $rcmail->storage->get_folder(),
|
|
'_uid' => self::$MESSAGE->uid,
|
|
'_part' => $part->mime_id,
|
|
]);
|
|
}
|
|
|
|
$strip_signature = self::$COMPOSE['mode'] != rcmail_sendmail::MODE_DRAFT
|
|
&& self::$COMPOSE['mode'] != rcmail_sendmail::MODE_EDIT
|
|
&& $rcmail->config->get('strip_existing_sig', true);
|
|
|
|
$flowed = !empty($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed';
|
|
$delsp = $flowed && !empty($part->ctype_parameters['delsp']) && $part->ctype_parameters['delsp'] == 'yes';
|
|
|
|
if ($isHtml) {
|
|
if ($part->ctype_secondary == 'html') {
|
|
$body = self::prepare_html_body($body);
|
|
} elseif ($part->ctype_secondary == 'enriched') {
|
|
$body = rcube_enriched::to_html($body);
|
|
} elseif ($part->ctype_secondary === 'markdown' || $part->ctype_secondary === 'x-markdown') {
|
|
$body = rcube_markdown::to_html($body);
|
|
} else {
|
|
// try to remove the signature
|
|
if ($strip_signature) {
|
|
$body = self::remove_signature($body);
|
|
}
|
|
|
|
// add HTML formatting
|
|
$body = self::plain_body($body, $flowed, $delsp);
|
|
}
|
|
} else {
|
|
if ($part->ctype_secondary == 'enriched') {
|
|
$body = rcube_enriched::to_html($body);
|
|
$part->ctype_secondary = 'html';
|
|
} elseif ($part->ctype_secondary === 'markdown' || $part->ctype_secondary === 'x-markdown') {
|
|
$body = rcube_markdown::to_html($body);
|
|
$part->ctype_secondary = 'html';
|
|
}
|
|
|
|
if ($part->ctype_secondary == 'html') {
|
|
// set line length for body wrapping
|
|
$line_length = $rcmail->config->get('line_length', 72);
|
|
|
|
// use html part if it has been used for message (pre)viewing
|
|
// decrease line length for quoting
|
|
$len = self::$COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY ? $line_length - 2 : $line_length;
|
|
$body = $rcmail->html2text($body, ['width' => $len]);
|
|
} else {
|
|
if ($part->ctype_secondary == 'plain' && $flowed) {
|
|
$body = rcube_mime::unfold_flowed($body, null, $delsp);
|
|
}
|
|
|
|
// try to remove the signature
|
|
if ($strip_signature) {
|
|
$body = self::remove_signature($body);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Template object for the message body composition element
|
|
*/
|
|
public static function compose_body($attrib)
|
|
{
|
|
[$form_start, $form_end] = self::$SENDMAIL->form_tags($attrib);
|
|
unset($attrib['form']);
|
|
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'rcmComposeBody';
|
|
}
|
|
|
|
// If desired, set this textarea to be editable by TinyMCE
|
|
$attrib['data-html-editor'] = true;
|
|
if (self::$HTML_MODE) {
|
|
$attrib['class'] = trim(($attrib['class'] ?? '') . ' mce_editor');
|
|
$attrib['data-html-editor-content-element'] = $attrib['id'] . '-content';
|
|
}
|
|
|
|
$attrib['name'] = '_message';
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$textarea = new html_textarea($attrib);
|
|
$hidden = new html_hiddenfield();
|
|
|
|
$hidden->add(['name' => '_draft_saveid', 'value' => $rcmail->output->get_env('draft_id')]);
|
|
$hidden->add(['name' => '_draft', 'value' => '']);
|
|
$hidden->add(['name' => '_is_html', 'value' => self::$HTML_MODE ? '1' : '0']);
|
|
$hidden->add(['name' => '_framed', 'value' => '1']);
|
|
|
|
$rcmail->output->set_env('composebody', $attrib['id']);
|
|
|
|
$content = $hidden->show() . "\n";
|
|
|
|
// We're adding a hidden textarea with the HTML content to workaround browsers' performance
|
|
// issues with rendering/loading long content. It will be copied to the main editor (#8108)
|
|
if (self::$HTML_MODE && strlen(self::$MESSAGE_BODY) > 50 * 1024) {
|
|
$contentArea = new html_textarea(['style' => 'display:none', 'id' => $attrib['id'] . '-content']);
|
|
$content .= $contentArea->show(self::$MESSAGE_BODY) . "\n" . $textarea->show();
|
|
} else {
|
|
$content .= $textarea->show(self::$MESSAGE_BODY);
|
|
}
|
|
|
|
// include HTML editor
|
|
self::html_editor();
|
|
|
|
return "{$form_start}\n{$content}\n{$form_end}\n";
|
|
}
|
|
|
|
/**
|
|
* Prepare the message body in reply mode
|
|
*/
|
|
public static function create_reply_body($body, $bodyIsHtml)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$reply_mode = (int) $rcmail->config->get('reply_mode');
|
|
$reply_indent = $reply_mode != 2;
|
|
|
|
// In top-posting without quoting it's better to use multi-line header
|
|
if ($reply_mode == 2) {
|
|
$prefix = self::get_forward_header(self::$MESSAGE, $bodyIsHtml, false);
|
|
} else {
|
|
$prefix = self::get_reply_header(self::$MESSAGE);
|
|
if ($bodyIsHtml) {
|
|
$prefix = '<p id="reply-intro">' . rcube::Q($prefix) . '</p>';
|
|
} else {
|
|
$prefix .= "\n";
|
|
}
|
|
}
|
|
|
|
if (!$bodyIsHtml) {
|
|
// quote the message text
|
|
if ($reply_indent) {
|
|
$body = self::quote_text($body);
|
|
}
|
|
|
|
if ($reply_mode > 0) { // top-posting
|
|
$prefix = "\n\n\n" . $prefix;
|
|
$suffix = '';
|
|
} else {
|
|
$suffix = "\n";
|
|
}
|
|
} else {
|
|
$suffix = '';
|
|
|
|
if ($reply_indent) {
|
|
$prefix .= '<blockquote>';
|
|
$suffix .= '</blockquote>';
|
|
}
|
|
|
|
if ($reply_mode == 2) {
|
|
// top-posting, no indent
|
|
} elseif ($reply_mode > 0) {
|
|
// top-posting
|
|
$prefix = '<br>' . $prefix;
|
|
} else {
|
|
$suffix .= '<p><br/></p>';
|
|
}
|
|
}
|
|
|
|
return $prefix . $body . $suffix;
|
|
}
|
|
|
|
/**
|
|
* Get reply header for the composed message body
|
|
*
|
|
* @param rcube_message $message Mail message
|
|
*/
|
|
public static function get_reply_header($message)
|
|
{
|
|
if (empty($message->headers)) {
|
|
return '';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$list = rcube_mime::decode_address_list($message->get_header('from'), 1, false, $message->headers->charset);
|
|
$from = array_pop($list);
|
|
|
|
return $rcmail->gettext([
|
|
'name' => 'mailreplyintro',
|
|
'vars' => [
|
|
'date' => $rcmail->format_date($message->get_header('date'), $rcmail->config->get('date_long')),
|
|
'sender' => !empty($from['name']) ? $from['name'] : rcube_utils::idn_to_utf8($from['mailto']),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Prepare the message body in forward mode
|
|
*/
|
|
public static function create_forward_body($body, $bodyIsHtml)
|
|
{
|
|
return self::get_forward_header(self::$MESSAGE, $bodyIsHtml) . trim($body, "\r\n");
|
|
}
|
|
|
|
/**
|
|
* Get forward header for the composed message body
|
|
*
|
|
* @param rcube_message $message Mail message
|
|
*/
|
|
public static function get_forward_header($message, $bodyIsHtml = false, $extended = true)
|
|
{
|
|
if (empty($message->headers)) {
|
|
return '';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$date = $rcmail->format_date($message->get_header('date'), $rcmail->config->get('date_long'));
|
|
|
|
if (!$bodyIsHtml) {
|
|
$prefix = "\n\n\n-------- " . $rcmail->gettext('originalmessage') . " --------\n";
|
|
$prefix .= $rcmail->gettext('subject') . ': ' . $message->subject . "\n";
|
|
$prefix .= $rcmail->gettext('date') . ': ' . $date . "\n";
|
|
$prefix .= $rcmail->gettext('from') . ': ' . $message->get_header('from') . "\n";
|
|
$prefix .= $rcmail->gettext('to') . ': ' . $message->get_header('to') . "\n";
|
|
|
|
if ($extended && ($cc = $message->get_header('cc'))) {
|
|
$prefix .= $rcmail->gettext('cc') . ': ' . $cc . "\n";
|
|
}
|
|
|
|
if ($extended && ($replyto = $message->get_header('reply-to')) && $replyto != $message->get_header('from')) {
|
|
$prefix .= $rcmail->gettext('replyto') . ': ' . $replyto . "\n";
|
|
}
|
|
|
|
$prefix .= "\n";
|
|
} else {
|
|
$prefix = sprintf(
|
|
'<br /><p>-------- ' . $rcmail->gettext('originalmessage') . ' --------</p>'
|
|
. '<table border="0" cellpadding="0" cellspacing="0"><tbody>'
|
|
. '<tr><th align="right" nowrap="nowrap" valign="baseline">%s: </th><td>%s</td></tr>'
|
|
. '<tr><th align="right" nowrap="nowrap" valign="baseline">%s: </th><td>%s</td></tr>'
|
|
. '<tr><th align="right" nowrap="nowrap" valign="baseline">%s: </th><td>%s</td></tr>'
|
|
. '<tr><th align="right" nowrap="nowrap" valign="baseline">%s: </th><td>%s</td></tr>',
|
|
$rcmail->gettext('subject'), rcube::Q($message->subject),
|
|
$rcmail->gettext('date'), rcube::Q($date),
|
|
$rcmail->gettext('from'), rcube::Q($message->get_header('from'), 'replace'),
|
|
$rcmail->gettext('to'), rcube::Q($message->get_header('to'), 'replace')
|
|
);
|
|
|
|
if ($extended && ($cc = $message->get_header('cc'))) {
|
|
$prefix .= sprintf('<tr><th align="right" nowrap="nowrap" valign="baseline">%s: </th><td>%s</td></tr>',
|
|
$rcmail->gettext('cc'), rcube::Q($cc, 'replace'));
|
|
}
|
|
|
|
if ($extended && ($replyto = $message->get_header('reply-to')) && $replyto != $message->get_header('from')) {
|
|
$prefix .= sprintf('<tr><th align="right" nowrap="nowrap" valign="baseline">%s: </th><td>%s</td></tr>',
|
|
$rcmail->gettext('replyto'), rcube::Q($replyto, 'replace'));
|
|
}
|
|
|
|
$prefix .= '</tbody></table><br>';
|
|
}
|
|
|
|
return $prefix;
|
|
}
|
|
|
|
/**
|
|
* Prepare the message body in draft mode
|
|
*/
|
|
public static function create_draft_body($body, $bodyIsHtml)
|
|
{
|
|
// Return the draft body as-is
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Clean up HTML content of Draft/Reply/Forward (part of the message)
|
|
*/
|
|
public static function prepare_html_body($body, $wash_params = [])
|
|
{
|
|
static $part_no;
|
|
|
|
// Set attributes of the part container
|
|
$container_id = self::$COMPOSE['mode'] . 'body' . (++$part_no);
|
|
|
|
$wash_params += [
|
|
'safe' => self::$MESSAGE->is_safe,
|
|
'css_prefix' => 'v' . $part_no,
|
|
'add_comments' => false,
|
|
];
|
|
|
|
if (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_DRAFT) {
|
|
// convert TinyMCE's empty-line sequence (#1490463)
|
|
$body = preg_replace('/<p>\xC2\xA0<\/p>/', '<p><br /></p>', $body);
|
|
// remove <body> tags (not their content)
|
|
$wash_params['ignore_elements'] = ['body'];
|
|
} else {
|
|
$wash_params['container_id'] = $container_id;
|
|
}
|
|
|
|
// Make the HTML content safe and clean
|
|
return self::wash_html($body, $wash_params, self::$CID_MAP);
|
|
}
|
|
|
|
/**
|
|
* Removes signature from the message body
|
|
*/
|
|
public static function remove_signature($body)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$body = str_replace("\r\n", "\n", $body);
|
|
$len = strlen($body);
|
|
$sig_max_lines = $rcmail->config->get('sig_max_lines', 15);
|
|
|
|
while (($sp = strrpos($body, "-- \n", !empty($sp) ? -$len + $sp - 1 : 0)) !== false) {
|
|
if ($sp == 0 || $body[$sp - 1] == "\n") {
|
|
// do not touch blocks with more that X lines
|
|
if (substr_count($body, "\n", $sp) < $sig_max_lines) {
|
|
$body = substr($body, 0, max(0, $sp - 1));
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Handle inline attachments in the message body
|
|
*
|
|
* @param rcube_message $message Mail message
|
|
*/
|
|
public static function write_compose_attachments(&$message, $bodyIsHtml, &$message_body)
|
|
{
|
|
if (!empty($message->pgp_mime)) {
|
|
return;
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$has_html = $message->has_html_part();
|
|
$messages = [];
|
|
$loaded = [];
|
|
|
|
foreach ($rcmail->list_uploaded_files(self::$COMPOSE_ID) as $attachment) {
|
|
$loaded[$attachment['name'] . $attachment['mimetype']] = $attachment;
|
|
}
|
|
|
|
foreach ((array) $message->mime_parts() as $pid => $part) {
|
|
if ($part->mimetype == 'message/rfc822') {
|
|
$messages[] = $part->mime_id;
|
|
}
|
|
|
|
if (
|
|
$part->disposition == 'attachment'
|
|
|| ($part->disposition == 'inline' && $bodyIsHtml)
|
|
|| $part->filename
|
|
|| $part->mimetype == 'message/rfc822'
|
|
) {
|
|
// skip parts that aren't valid attachments
|
|
if ($part->ctype_primary == 'multipart' || $part->mimetype == 'application/ms-tnef') {
|
|
continue;
|
|
}
|
|
|
|
// skip message attachments in reply mode
|
|
if ($part->ctype_primary == 'message' && self::$COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
|
|
continue;
|
|
}
|
|
|
|
// skip version.txt parts of multipart/encrypted messages
|
|
if (!empty($message->pgp_mime) && $part->mimetype == 'application/pgp-encrypted' && $part->filename == 'version.txt') {
|
|
continue;
|
|
}
|
|
|
|
// skip attachments included in message/rfc822 attachment (#1486487, #1490607)
|
|
foreach ($messages as $mimeid) {
|
|
if (str_starts_with($part->mime_id, $mimeid . '.')) {
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
$replace = null;
|
|
|
|
// Skip inline images when not used in the body
|
|
// Note: Apple Mail sends PDF files marked as inline (#7382)
|
|
// Note: Apple clients send inline images even if there's no HTML body (#7414)
|
|
if ($has_html && $part->disposition == 'inline' && $part->mimetype != 'application/pdf') {
|
|
if (!$bodyIsHtml) {
|
|
continue;
|
|
}
|
|
|
|
$idx = $part->content_id ? ('cid:' . $part->content_id) : $part->content_location ?? null;
|
|
|
|
if ($idx && isset(self::$CID_MAP[$idx]) && strpos($message_body, self::$CID_MAP[$idx]) !== false) {
|
|
$replace = self::$CID_MAP[$idx];
|
|
} else {
|
|
continue;
|
|
}
|
|
}
|
|
// skip any other attachment on Reply
|
|
elseif (self::$COMPOSE['mode'] == rcmail_sendmail::MODE_REPLY) {
|
|
continue;
|
|
}
|
|
|
|
$key = self::attachment_name($part) . $part->mimetype;
|
|
|
|
if (!empty($loaded[$key])) {
|
|
$attachment = $loaded[$key];
|
|
} else {
|
|
$attachment = self::save_attachment($message, $pid, self::$COMPOSE['id']);
|
|
}
|
|
|
|
if ($attachment) {
|
|
if ($replace) {
|
|
$url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
|
|
$rcmail->comm_path, self::$COMPOSE['id'], $attachment['id']);
|
|
|
|
$message_body = str_replace($replace, $url, $message_body);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create a map of attachment content-id/content-locations
|
|
*
|
|
* @param rcube_message $message Mail message
|
|
*/
|
|
public static function cid_map($message)
|
|
{
|
|
if (!empty($message->pgp_mime)) {
|
|
return [];
|
|
}
|
|
|
|
$messages = [];
|
|
$map = [];
|
|
|
|
foreach ($message->mime_parts() as $pid => $part) {
|
|
if ($part->mimetype == 'message/rfc822') {
|
|
$messages[] = $part->mime_id;
|
|
}
|
|
|
|
if (!empty($part->content_id) || !empty($part->content_location)) {
|
|
// skip attachments included in message/rfc822 attachment (#1486487, #1490607)
|
|
foreach ($messages as $mimeid) {
|
|
if (str_starts_with($part->mime_id, $mimeid . '.')) {
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
$url = sprintf('RCMAP%s', md5($message->folder . '/' . $message->uid . '/' . $pid));
|
|
$idx = !empty($part->content_id) ? ('cid:' . $part->content_id) : $part->content_location;
|
|
|
|
$map[$idx] = $url;
|
|
}
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
/**
|
|
* Creates attachment(s) from the forwarded message(s)
|
|
*/
|
|
public static function write_forward_attachments()
|
|
{
|
|
if (!empty(self::$MESSAGE->pgp_mime)) {
|
|
return;
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$storage = $rcmail->get_storage();
|
|
$size_errors = 0;
|
|
$size_limit = parse_bytes($rcmail->config->get('max_message_size'));
|
|
$total_size = 10 * 1024; // size of message body, to start with
|
|
$names = [];
|
|
$refs = [];
|
|
$loaded = [];
|
|
|
|
foreach ($rcmail->list_uploaded_files(self::$COMPOSE_ID) as $attachment) {
|
|
$loaded[$attachment['name'] . $attachment['mimetype']] = $attachment;
|
|
$total_size += $attachment['size'];
|
|
}
|
|
|
|
if (self::$COMPOSE['forward_uid'] == '*') {
|
|
$index = $storage->index(null, self::sort_column(), self::sort_order());
|
|
self::$COMPOSE['forward_uid'] = $index->get();
|
|
} elseif (!is_array(self::$COMPOSE['forward_uid']) && strpos(self::$COMPOSE['forward_uid'], ':')) {
|
|
self::$COMPOSE['forward_uid'] = rcube_imap_generic::uncompressMessageSet(self::$COMPOSE['forward_uid']);
|
|
} elseif (is_string(self::$COMPOSE['forward_uid'])) {
|
|
self::$COMPOSE['forward_uid'] = explode(',', self::$COMPOSE['forward_uid']);
|
|
}
|
|
|
|
foreach ((array) self::$COMPOSE['forward_uid'] as $uid) {
|
|
$message = new rcube_message($uid);
|
|
|
|
if (empty($message->headers)) {
|
|
continue;
|
|
}
|
|
|
|
if (!empty($message->headers->charset)) {
|
|
$storage->set_charset($message->headers->charset);
|
|
}
|
|
|
|
if (empty(self::$MESSAGE->subject)) {
|
|
self::$MESSAGE->subject = $message->subject;
|
|
}
|
|
|
|
if ($message->headers->get('bcc', false) || $message->headers->get('resent-bcc', false)) {
|
|
self::$COMPOSE['has_bcc'] = true;
|
|
}
|
|
|
|
// generate (unique) attachment name
|
|
$name = strlen($message->subject) ? mb_substr($message->subject, 0, 64) : 'message_rfc822';
|
|
if (!empty($names[$name])) {
|
|
$names[$name]++;
|
|
$name .= '_' . $names[$name];
|
|
}
|
|
$names[$name] = 1;
|
|
$name .= '.eml';
|
|
|
|
if (!empty($loaded[$name . 'message/rfc822'])) {
|
|
continue;
|
|
}
|
|
|
|
if ($size_limit && $size_limit < $total_size + $message->headers->size) {
|
|
$size_errors++;
|
|
continue;
|
|
}
|
|
|
|
$total_size += $message->headers->size;
|
|
|
|
self::save_attachment($message, null, self::$COMPOSE['id'], ['filename' => $name]);
|
|
|
|
if ($message->headers->messageID) {
|
|
$refs[] = $message->headers->messageID;
|
|
}
|
|
}
|
|
|
|
// set In-Reply-To and References headers
|
|
if (count($refs) == 1) {
|
|
self::$COMPOSE['reply_msgid'] = $refs[0];
|
|
}
|
|
|
|
if (!empty($refs)) {
|
|
self::$COMPOSE['references'] = implode(' ', $refs);
|
|
}
|
|
|
|
if ($size_errors) {
|
|
$limit = self::show_bytes($size_limit);
|
|
$error = $rcmail->gettext([
|
|
'name' => 'msgsizeerrorfwd',
|
|
'vars' => ['num' => $size_errors, 'size' => $limit],
|
|
]);
|
|
$script = sprintf("%s.display_message('%s', 'error');", rcmail_output::JS_OBJECT_NAME, rcube::JQ($error));
|
|
$rcmail->output->add_script($script, 'docready');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Saves an image as attachment
|
|
*/
|
|
public static function save_image($path, $mimetype = '', $data = null)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$is_file = false;
|
|
|
|
// handle attachments in memory
|
|
if (empty($data)) {
|
|
$data = file_get_contents($path);
|
|
$is_file = true;
|
|
}
|
|
|
|
$name = self::basename($path);
|
|
|
|
if (empty($mimetype)) {
|
|
if ($is_file) {
|
|
$mimetype = rcube_mime::file_content_type($path, $name);
|
|
} else {
|
|
$mimetype = rcube_mime::file_content_type($data, $name, 'application/octet-stream', true);
|
|
}
|
|
}
|
|
|
|
$attachment = [
|
|
'group' => self::$COMPOSE['id'],
|
|
'name' => $name,
|
|
'mimetype' => $mimetype,
|
|
'data' => $data,
|
|
'size' => strlen($data),
|
|
];
|
|
|
|
if ($rcmail->insert_uploaded_file($attachment, 'attachment_save')) {
|
|
return $attachment;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Unicode-safe basename()
|
|
*/
|
|
public static function basename($filename)
|
|
{
|
|
// basename() is not unicode safe and locale dependent
|
|
if (stristr(\PHP_OS, 'win') || stristr(\PHP_OS, 'netware')) {
|
|
return preg_replace('/^.*[\\\\\/]/', '', $filename);
|
|
}
|
|
|
|
return preg_replace('/^.*[\/]/', '', $filename);
|
|
}
|
|
|
|
/**
|
|
* Handler for template object 'composeObjects'
|
|
*
|
|
* @param array $attrib HTML attributes
|
|
*
|
|
* @return string HTML content
|
|
*/
|
|
public static function compose_objects($attrib)
|
|
{
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'compose-objects';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$content = [];
|
|
|
|
// Add a warning about Bcc recipients
|
|
if (!empty(self::$COMPOSE['has_bcc'])) {
|
|
$msg = html::span(null, rcube::Q($rcmail->gettext('bccemail')));
|
|
$msg_attrib = ['id' => 'bcc-warning', 'class' => 'boxwarning'];
|
|
$content[] = html::div($msg_attrib, $msg);
|
|
}
|
|
|
|
$plugin = $rcmail->plugins->exec_hook('compose_objects',
|
|
['content' => $content, 'message' => self::$MESSAGE]);
|
|
|
|
$content = implode("\n", $plugin['content']);
|
|
|
|
return $content ? html::div($attrib, $content) : '';
|
|
}
|
|
|
|
/**
|
|
* Attachments list object for templates
|
|
*/
|
|
public static function compose_attachment_list($attrib)
|
|
{
|
|
// add ID if not given
|
|
if (empty($attrib['id'])) {
|
|
$attrib['id'] = 'rcmAttachmentList';
|
|
}
|
|
|
|
$rcmail = rcmail::get_instance();
|
|
$out = '';
|
|
$button = '';
|
|
$jslist = [];
|
|
|
|
if (!empty($attrib['icon_pos']) && $attrib['icon_pos'] == 'left') {
|
|
self::$COMPOSE['icon_pos'] = 'left';
|
|
}
|
|
|
|
$icon_pos = self::$COMPOSE['icon_pos'] ?? null;
|
|
|
|
$attachments = $rcmail->list_uploaded_files(self::$COMPOSE_ID);
|
|
|
|
if (!empty($attachments)) {
|
|
if (!empty($attrib['deleteicon'])) {
|
|
$button = html::img([
|
|
'src' => $rcmail->output->asset_url($attrib['deleteicon'], true),
|
|
'alt' => $rcmail->gettext('delete'),
|
|
]);
|
|
} elseif (self::get_bool_attr($attrib, 'textbuttons')) {
|
|
$button = rcube::Q($rcmail->gettext('delete'));
|
|
}
|
|
|
|
foreach ($attachments as $id => $a_prop) {
|
|
if (empty($a_prop)) {
|
|
continue;
|
|
}
|
|
|
|
$id = $a_prop['id'] ?? $id;
|
|
|
|
$link_content = sprintf(
|
|
'<span class="attachment-name" onmouseover="rcube_webmail.long_subject_title_ex(this)">%s</span>'
|
|
. ' <span class="attachment-size">(%s)</span>',
|
|
rcube::Q($a_prop['name']),
|
|
self::show_bytes($a_prop['size'])
|
|
);
|
|
|
|
$content_link = html::a([
|
|
'href' => '#load',
|
|
'class' => 'filename',
|
|
'onclick' => sprintf(
|
|
"return %s.command('load-attachment','rcmfile%s', this, event)",
|
|
rcmail_output::JS_OBJECT_NAME,
|
|
$id
|
|
),
|
|
'tabindex' => !empty($attrib['tabindex']) ? $attrib['tabindex'] : '0',
|
|
],
|
|
$link_content
|
|
);
|
|
|
|
$delete_link = html::a([
|
|
'href' => '#delete',
|
|
'title' => $rcmail->gettext('delete'),
|
|
'onclick' => sprintf(
|
|
"return %s.command('remove-attachment','rcmfile%s', this, event)",
|
|
rcmail_output::JS_OBJECT_NAME,
|
|
$id
|
|
),
|
|
'class' => 'delete',
|
|
'tabindex' => !empty($attrib['tabindex']) ? $attrib['tabindex'] : '0',
|
|
'aria-label' => $rcmail->gettext('delete') . ' ' . $a_prop['name'],
|
|
],
|
|
$button
|
|
);
|
|
|
|
$out .= html::tag('li', [
|
|
'id' => 'rcmfile' . $id,
|
|
'class' => rcube_utils::file2class($a_prop['mimetype'], $a_prop['name']),
|
|
],
|
|
$icon_pos == 'left' ? $delete_link . $content_link : $content_link . $delete_link
|
|
);
|
|
|
|
$jslist['rcmfile' . $id] = [
|
|
'name' => $a_prop['name'],
|
|
'mimetype' => $a_prop['mimetype'],
|
|
'complete' => true,
|
|
];
|
|
}
|
|
}
|
|
|
|
if (!empty($attrib['deleteicon'])) {
|
|
self::$COMPOSE['deleteicon'] = $rcmail->output->asset_url($attrib['deleteicon'], true);
|
|
} elseif (self::get_bool_attr($attrib, 'textbuttons')) {
|
|
self::$COMPOSE['textbuttons'] = true;
|
|
}
|
|
if (!empty($attrib['cancelicon'])) {
|
|
$rcmail->output->set_env('cancelicon', $rcmail->output->asset_url($attrib['cancelicon'], true));
|
|
}
|
|
if (!empty($attrib['loadingicon'])) {
|
|
$rcmail->output->set_env('loadingicon', $rcmail->output->asset_url($attrib['loadingicon'], true));
|
|
}
|
|
|
|
$rcmail->output->set_env('attachments', $jslist);
|
|
$rcmail->output->add_gui_object('attachmentlist', $attrib['id']);
|
|
|
|
// put tabindex value into data-tabindex attribute
|
|
if (isset($attrib['tabindex'])) {
|
|
$attrib['data-tabindex'] = $attrib['tabindex'];
|
|
unset($attrib['tabindex']);
|
|
}
|
|
|
|
return html::tag('ul', $attrib, $out, html::$common_attrib);
|
|
}
|
|
|
|
/**
|
|
* Attachment upload form object for templates
|
|
*/
|
|
public static function compose_attachment_form($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// Limit attachment size according to message size limit
|
|
$limit = parse_bytes($rcmail->config->get('max_message_size')) / 1.33;
|
|
|
|
return self::upload_form($attrib, 'uploadform', 'send-attachment', ['multiple' => true], $limit);
|
|
}
|
|
|
|
/**
|
|
* Register a certain container as active area to drop files onto
|
|
*/
|
|
public static function compose_file_drop_area($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
if (!empty($attrib['id'])) {
|
|
$rcmail->output->add_gui_object('filedrop', $attrib['id']);
|
|
$rcmail->output->set_env('filedrop', ['action' => 'upload', 'fieldname' => '_attachments']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Editor mode selector object for templates
|
|
*/
|
|
public static function editor_selector($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
// determine whether HTML or plain text should be checked
|
|
$useHtml = self::compose_editor_mode();
|
|
|
|
if (empty($attrib['editorid'])) {
|
|
$attrib['editorid'] = 'rcmComposeBody';
|
|
}
|
|
|
|
if (empty($attrib['name'])) {
|
|
$attrib['name'] = 'editorSelect';
|
|
}
|
|
|
|
$attrib['onchange'] = "return rcmail.command('toggle-editor', {id: '" . $attrib['editorid'] . "', html: this.value == 'html'}, '', event)";
|
|
|
|
$select = new html_select($attrib);
|
|
|
|
$select->add(rcube::Q($rcmail->gettext('htmltoggle')), 'html');
|
|
$select->add(rcube::Q($rcmail->gettext('plaintoggle')), 'plain');
|
|
|
|
return $select->show($useHtml ? 'html' : 'plain');
|
|
}
|
|
|
|
/**
|
|
* Addressbooks list object for templates
|
|
*/
|
|
public static function addressbook_list($attrib = [])
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
$attrib += ['id' => 'rcmdirectorylist'];
|
|
|
|
$line_templ = html::tag('li',
|
|
['id' => 'rcmli%s', 'class' => '%s'],
|
|
html::a([
|
|
'href' => '#list',
|
|
'rel' => '%s',
|
|
'onclick' => 'return ' . rcmail_output::JS_OBJECT_NAME . ".command('list-addresses','%s',this)",
|
|
],
|
|
'%s'
|
|
)
|
|
);
|
|
|
|
$out = '';
|
|
|
|
foreach ($rcmail->get_address_sources(false, true) as $j => $source) {
|
|
$id = strval(strlen($source['id']) ? $source['id'] : $j);
|
|
$js_id = rcube::JQ($id);
|
|
|
|
// set class name(s)
|
|
$class_name = 'addressbook';
|
|
if (!empty($source['class_name'])) {
|
|
$class_name .= ' ' . $source['class_name'];
|
|
}
|
|
|
|
$out .= sprintf($line_templ,
|
|
rcube_utils::html_identifier($id, true),
|
|
$class_name,
|
|
$source['id'],
|
|
$js_id,
|
|
!empty($source['name']) ? $source['name'] : $id
|
|
);
|
|
}
|
|
|
|
$rcmail->output->add_gui_object('addressbookslist', $attrib['id']);
|
|
|
|
return html::tag('ul', $attrib, $out, html::$common_attrib);
|
|
}
|
|
|
|
/**
|
|
* Contacts list object for templates
|
|
*/
|
|
public static function contacts_list($attrib = [])
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
$attrib += ['id' => 'rcmAddressList'];
|
|
|
|
// set client env
|
|
$rcmail->output->add_gui_object('contactslist', $attrib['id']);
|
|
$rcmail->output->set_env('pagecount', 0);
|
|
$rcmail->output->set_env('current_page', 0);
|
|
$rcmail->output->include_script('list.js');
|
|
|
|
return rcmail_action::table_output($attrib, [], ['name'], 'ID');
|
|
}
|
|
|
|
/**
|
|
* Responses list object for templates
|
|
*
|
|
* @param array $attrib Object attributes
|
|
*
|
|
* @return string HTML content
|
|
*/
|
|
public static function compose_responses_list($attrib)
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
|
|
$attrib += ['id' => 'rcmresponseslist', 'tagname' => 'ul', 'cols' => 1, 'itemclass' => ''];
|
|
|
|
$list = new html_table($attrib);
|
|
|
|
foreach ($rcmail->get_compose_responses() as $response) {
|
|
$item = html::a([
|
|
'href' => '#response-' . urlencode($response['id']),
|
|
'class' => rtrim('insertresponse ' . $attrib['itemclass']),
|
|
'unselectable' => 'on',
|
|
'tabindex' => '0',
|
|
'onclick' => sprintf(
|
|
"return %s.command('insert-response', '%s', this, event)",
|
|
rcmail_output::JS_OBJECT_NAME,
|
|
rcube::JQ($response['id'])
|
|
),
|
|
],
|
|
rcube::Q($response['name'])
|
|
);
|
|
|
|
$list->add([], $item);
|
|
}
|
|
|
|
// add placeholder text when there are no responses available
|
|
if (!empty($attrib['list-placeholder']) && $list->size() == 0) {
|
|
$list->add([], html::a([
|
|
'href' => '#',
|
|
'class' => rtrim('insertresponse placeholder disabled'),
|
|
'unselectable' => 'on',
|
|
'tabindex' => '0',
|
|
'aria-disabled' => 'true',
|
|
],
|
|
rcube::Q($rcmail->gettext($attrib['list-placeholder']))
|
|
));
|
|
}
|
|
|
|
$rcmail->output->add_gui_object('responseslist', $attrib['id']);
|
|
|
|
return $list->show();
|
|
}
|
|
|
|
/**
|
|
* Save the specified message (or message part) as an attachment
|
|
*/
|
|
public static function save_attachment($message, $pid, $compose_id, $params = [])
|
|
{
|
|
$rcmail = rcmail::get_instance();
|
|
$storage = $rcmail->get_storage();
|
|
|
|
if ($pid) {
|
|
// attachment requested
|
|
$part = $message->mime_parts[$pid];
|
|
$size = $part->size;
|
|
$mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
|
|
$filename = !empty($params['filename']) ? $params['filename'] : self::attachment_name($part);
|
|
} elseif ($message instanceof rcube_message) {
|
|
// the whole message requested
|
|
$size = $message->size ?? null; // @phpstan-ignore-line
|
|
$mimetype = 'message/rfc822';
|
|
$filename = !empty($params['filename']) ? $params['filename'] : 'message_rfc822.eml';
|
|
} elseif (is_string($message)) {
|
|
// the whole message requested
|
|
$size = strlen($message);
|
|
$data = $message;
|
|
$mimetype = $params['mimetype'];
|
|
$filename = $params['filename'];
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
if (!isset($data)) {
|
|
$data = null;
|
|
$path = null;
|
|
|
|
// don't load too big attachments into memory
|
|
if (!rcube_utils::mem_check($size)) {
|
|
$path = rcube_utils::temp_filename('attmnt');
|
|
|
|
if ($fp = fopen($path, 'w')) {
|
|
if ($pid) {
|
|
// part body
|
|
$message->get_part_body($pid, false, 0, $fp);
|
|
} else {
|
|
// complete message
|
|
$storage->get_raw_body($message->uid, $fp);
|
|
}
|
|
|
|
fclose($fp);
|
|
} else {
|
|
return false;
|
|
}
|
|
} elseif ($pid) {
|
|
// part body
|
|
$data = $message->get_part_body($pid);
|
|
} else {
|
|
// complete message
|
|
$data = $storage->get_raw_body($message->uid);
|
|
}
|
|
}
|
|
|
|
$attachment = [
|
|
'group' => $compose_id,
|
|
'name' => $filename,
|
|
'mimetype' => $mimetype,
|
|
'content_id' => !empty($part) && isset($part->content_id) ? $part->content_id : null,
|
|
'data' => $data,
|
|
'path' => $path ?? null,
|
|
'size' => isset($path) ? filesize($path) : strlen($data),
|
|
'charset' => !empty($part) ? $part->charset : ($params['charset'] ?? null),
|
|
];
|
|
|
|
if ($rcmail->insert_uploaded_file($attachment, 'attachment_save')) {
|
|
return $attachment;
|
|
} elseif (!empty($path)) {
|
|
@unlink($path);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Add quotation (>) to a replied message text.
|
|
*
|
|
* @param string $text Text to quote
|
|
*
|
|
* @return string The quoted text
|
|
*/
|
|
public static function quote_text($text)
|
|
{
|
|
$lines = preg_split('/\r?\n/', trim($text));
|
|
$out = '';
|
|
|
|
foreach ($lines as $line) {
|
|
$quoted = isset($line[0]) && $line[0] == '>';
|
|
$out .= '>' . ($quoted ? '' : ' ') . $line . "\n";
|
|
}
|
|
|
|
return rtrim($out, "\n");
|
|
}
|
|
}
|