first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
@@ -0,0 +1,142 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Form to edit handlers.
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
/**
* Form to edit handlers.
*
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_messageinbound_edit_handler_form extends moodleform {
/**
* The form definition
*/
public function definition() {
$mform = $this->_form;
$handler = $this->_customdata['handler'];
// Set up the options for formatting text for descriptions, etc.
$formatoptions = new stdClass();
$formatoptions->trusted = false;
$formatoptions->noclean = false;
$formatoptions->filter = false;
$formatoptions->para = true;
$formatoptions->newlines = false;
$formatoptions->overflowdiv = true;
// General information about the handler.
$mform->addElement('header', 'general', get_string('general'));
$mform->addElement('static', 'name', get_string('name', 'tool_messageinbound'),
$handler->name);
$mform->addElement('static', 'classname', get_string('classname', 'tool_messageinbound'));
$description = format_text($handler->description, FORMAT_MARKDOWN, $formatoptions);
$mform->addElement('static', 'description', get_string('description', 'tool_messageinbound'),
$description);
// Items which can be configured.
$mform->addElement('header', 'configuration', get_string('configuration'));
if ($handler->can_change_defaultexpiration()) {
// Show option to change expiry only if the handler supports it.
$options = array(
HOURSECS => get_string('onehour', 'tool_messageinbound'),
DAYSECS => get_string('oneday', 'tool_messageinbound'),
WEEKSECS => get_string('oneweek', 'tool_messageinbound'),
YEARSECS => get_string('oneyear', 'tool_messageinbound'),
0 => get_string('noexpiry', 'tool_messageinbound'),
);
$mform->addElement('select', 'defaultexpiration', get_string('defaultexpiration', 'tool_messageinbound'), $options);
$mform->addHelpButton('defaultexpiration', 'defaultexpiration', 'tool_messageinbound');
} else {
$text = $this->get_defaultexpiration_text($handler);
$mform->addElement('static', 'defaultexpiration_fake', get_string('defaultexpiration', 'tool_messageinbound'), $text);
$mform->addElement('hidden', 'defaultexpiration');
$mform->addHelpButton('defaultexpiration_fake', 'defaultexpiration', 'tool_messageinbound');
$mform->setType('defaultexpiration', PARAM_INT);
}
if ($handler->can_change_validateaddress()) {
$mform->addElement('checkbox', 'validateaddress', get_string('requirevalidation', 'tool_messageinbound'));
$mform->addHelpButton('validateaddress', 'validateaddress', 'tool_messageinbound');
} else {
if ($handler->validateaddress) {
$text = get_string('yes');
} else {
$text = get_string('no');
}
$mform->addElement('static', 'validateaddress_fake', get_string('requirevalidation', 'tool_messageinbound'), $text);
$mform->addElement('hidden', 'validateaddress');
$mform->addHelpButton('validateaddress_fake', 'fixedvalidateaddress', 'tool_messageinbound');
$mform->setType('validateaddress', PARAM_INT);
}
if ($handler->can_change_enabled()) {
$mform->addElement('checkbox', 'enabled', get_string('enabled', 'tool_messageinbound'));
} else {
if ($handler->enabled) {
$text = get_string('yes');
} else {
$text = get_string('no');
}
$mform->addElement('static', 'enabled_fake', get_string('enabled', 'tool_messageinbound'), $text);
$mform->addHelpButton('enabled', 'fixedenabled', 'tool_messageinbound');
$mform->addElement('hidden', 'enabled');
$mform->setType('enabled', PARAM_INT);
}
$this->add_action_buttons(true, get_string('savechanges'));
}
/**
* Return a text string representing the selected default expiration for the handler.
*
* @param \core\message\inbound\handler $handler handler instance.
*
* @return string localised text string.
*/
protected function get_defaultexpiration_text(\core\message\inbound\handler $handler) {
switch($handler->defaultexpiration) {
case HOURSECS :
return get_string('onehour', 'tool_messageinbound');
case DAYSECS :
return get_string('oneday', 'tool_messageinbound');
case WEEKSECS :
return get_string('oneweek', 'tool_messageinbound');
case YEARSECS :
return get_string('oneyear', 'tool_messageinbound');
case 0:
return get_string('noexpiry', 'tool_messageinbound');
default:
return ''; // Should never happen.
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A Handler to re-process messages which previously failed sender verification.
*
* @package tool_messageinbound
* @category message
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_messageinbound\message\inbound;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/repository/lib.php');
/**
* A Handler to re-process messages which previously failed sender verification.
*
* This may happen if the user did not use their registerd e-mail address,
* the verification hash used had expired, or if some erroneous content was
* introduced into the content hash.
*
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class invalid_recipient_handler extends \core\message\inbound\handler {
/**
* Do not allow changes to the address validation setting.
*/
public function can_change_validateaddress() {
return false;
}
/**
* Return a description for the current handler.
*
* @return string
*/
public function get_description() {
return get_string('invalid_recipient_handler', 'tool_messageinbound');
}
/**
* Return a name for the current handler.
* This appears in the admin pages as a human-readable name.
*
* @return string
*/
public function get_name() {
return get_string('invalid_recipient_handler_name', 'tool_messageinbound');
}
/**
* Process a message received and validated by the Inbound Message processor.
*
* @param \stdClass $record The Inbound Message record
* @param \stdClass $data The message data packet.
* @return bool Whether the message was successfully processed.
* @throws \core\message\inbound\processing_failed_exception when the message can not be found.
*/
public function process_message(\stdClass $record, \stdClass $data) {
global $DB;
if (!$maildata = $DB->get_record('messageinbound_messagelist', array('id' => $record->datavalue))) {
// The message requested couldn't be found. Failing here will alert the user that we failed.
throw new \core\message\inbound\processing_failed_exception('oldmessagenotfound', 'tool_messageinbound');
}
mtrace("=== Request to re-process message {$record->datavalue} from server.");
mtrace("=== Message-Id:\t{$maildata->messageid}");
mtrace("=== Recipient:\t{$maildata->address}");
$manager = new \tool_messageinbound\manager();
return $manager->process_existing_message($maildata);
}
}
@@ -0,0 +1,217 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data provider.
*
* @package tool_messageinbound
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_messageinbound\privacy;
defined('MOODLE_INTERNAL') || die();
use context;
use context_user;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
/**
* Data provider class.
*
* @package tool_messageinbound
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
\core_privacy\local\metadata\provider,
\core_privacy\local\request\core_userlist_provider,
\core_privacy\local\request\plugin\provider {
/**
* Returns metadata.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
$collection->add_database_table('messageinbound_messagelist', [
'messageid' => 'privacy:metadata:messagelist:messageid',
'userid' => 'privacy:metadata:messagelist:userid',
'address' => 'privacy:metadata:messagelist:address',
'timecreated' => 'privacy:metadata:messagelist:timecreated',
], 'privacy:metadata:messagelist');
// Arguably the keys are handled by \core\message\inbound\address_manager and thus could/should be handled by core.
$collection->add_subsystem_link('core_userkey', [], 'privacy:metadata:coreuserkey');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return \contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
$contextlist = new \core_privacy\local\request\contextlist();
// Always add the user context so we're sure we're not dodging user keys, besides it's not costly to do so.
$contextlist->add_user_context($userid);
return $contextlist;
}
/**
* Get the list of users who have data within a context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
global $DB;
$context = $userlist->get_context();
if (!is_a($context, \context_user::class)) {
return;
}
// Add user if any messagelist data exists.
if ($DB->record_exists('messageinbound_messagelist', ['userid' => $context->instanceid])) {
// Only using user context, so instance ID will be the only user ID.
$userlist->add_user($context->instanceid);
}
// Add users based on userkey (since we also delete those).
\core_userkey\privacy\provider::get_user_contexts_with_script($userlist, $context, 'messageinbound_handler');
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
if (!static::approved_contextlist_contains_my_context($contextlist)) {
// We only care about the user's user context.
return;
}
$userid = $contextlist->get_user()->id;
$context = context_user::instance($userid);
$path = [get_string('messageinbound', 'tool_messageinbound')];
// Export user keys.
\core_userkey\privacy\provider::export_userkeys($context, $path, 'messageinbound_handler');
// Export the message list.
$data = [];
$recordset = $DB->get_recordset('messageinbound_messagelist', ['userid' => $userid], 'timecreated, id');
foreach ($recordset as $record) {
$data[] = [
'received_at' => $record->address,
'timecreated' => transform::datetime($record->timecreated),
];
}
$recordset->close();
writer::with_context($context)->export_data($path, (object) ['messages_pending_validation' => $data]);
}
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(context $context) {
global $DB;
if ($context->contextlevel != CONTEXT_USER) {
return;
}
static::delete_user_data($context->instanceid);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
if (!static::approved_contextlist_contains_my_context($contextlist)) {
// We only care about the user's user context.
return;
}
static::delete_user_data($contextlist->get_user()->id);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
$context = $userlist->get_context();
$userids = $userlist->get_userids();
// Since this falls within a user context, only that user should be valid.
if ($context->contextlevel != CONTEXT_USER || count($userids) != 1 || $context->instanceid != $userids[0]) {
return;
}
static::delete_user_data($userids[0]);
}
/**
* Delete a user's data.
*
* @param int $userid The user ID.
* @return void
*/
protected static function delete_user_data($userid) {
global $DB;
$DB->delete_records_select('messageinbound_messagelist', 'userid = :userid', ['userid' => $userid]);
\core_userkey\privacy\provider::delete_userkeys('messageinbound_handler', $userid);
}
/**
* Return whether the contextlist contains our own context.
*
* @param approved_contextlist $contextlist The contextlist
* @return bool
*/
protected static function approved_contextlist_contains_my_context(approved_contextlist $contextlist) {
$userid = $contextlist->get_user()->id;
foreach ($contextlist->get_contexts() as $context) {
if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,55 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A scheduled task to handle cleanup of old, unconfirmed e-mails.
*
* @package tool_messageinbound
* @category task
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_messageinbound\task;
defined('MOODLE_INTERNAL') || die();
/**
* A scheduled task to handle cleanup of old, unconfirmed e-mails.
*
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class cleanup_task extends \core\task\scheduled_task {
/**
* Get a descriptive name for this task (shown to admins).
*
* @return string
*/
public function get_name() {
return get_string('taskcleanup', 'tool_messageinbound');
}
/**
* Execute the main Inbound Message pickup task.
*/
public function execute() {
$manager = new \tool_messageinbound\manager();
$manager->tidy_old_messages();
$manager->tidy_old_verification_failures();
}
}
@@ -0,0 +1,54 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A scheduled task to handle Inbound Message e-mail pickup.
*
* @package tool_messageinbound
* @category task
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_messageinbound\task;
defined('MOODLE_INTERNAL') || die();
/**
* A scheduled task to handle Inbound Message e-mail pickup.
*
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class pickup_task extends \core\task\scheduled_task {
/**
* Get a descriptive name for this task (shown to admins).
*
* @return string
*/
public function get_name() {
return get_string('taskpickup', 'tool_messageinbound');
}
/**
* Execute the main Inbound Message pickup task.
*/
public function execute() {
$manager = new \tool_messageinbound\manager();
return $manager->pickup_messages();
}
}
@@ -0,0 +1,56 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_messageinbound;
/**
* The Mail Pickup Utils.
*
* @package tool_messageinbound
* @copyright 2023 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class utils {
/** @var int Encoding type: 7 bit SMTP semantic data. */
const ENC7BIT = 0;
/** @var int Encoding type: 8 bit SMTP semantic data. */
const ENC8BIT = 1;
/** @var int Encoding type: 8 bit binary data. */
const ENCBINARY = 2;
/** @var int Encoding type: BASE64 encoded data. */
const ENCBASE64 = 3;
/** @var int Encoding type: Human-readable 8-as-7 bit data. */
const ENCQUOTEDPRINTABLE = 4;
/** @var int Encoding type: Unknown. */
const ENCOTHER = 5;
/**
* Get body content encoding.
*
* @return string[] List of body content encoding.
*/
public static function get_body_encoding(): array {
return [
self::ENC7BIT => '7BIT',
self::ENC8BIT => '8BIT',
self::ENCBINARY => 'BINARY',
self::ENCBASE64 => 'BASE64',
self::ENCQUOTEDPRINTABLE => 'QUOTED-PRINTABLE',
self::ENCOTHER => 'X-UNKNOWN',
];
}
}
@@ -0,0 +1,33 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Handlers for tool_messageinbound.
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$handlers = array(
array(
'classname' => '\tool_messageinbound\message\inbound\invalid_recipient_handler',
'enabled' => true,
'validateaddress' => false,
),
);
+49
View File
@@ -0,0 +1,49 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Message Providers for task_messageinbound.
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$messageproviders = array (
// Invalid recipient handler.
'invalidrecipienthandler' => [
'defaults' => [
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED,
],
],
// A generic message processing error.
'messageprocessingerror' => [
'defaults' => [
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED,
],
],
// A generic message processing success message.
'messageprocessingsuccess' => [
'defaults' => [
'popup' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED,
'email' => MESSAGE_PERMITTED + MESSAGE_DEFAULT_ENABLED,
],
],
);
+47
View File
@@ -0,0 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* The Main Manager tasks.
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$tasks = array(
array(
'classname' => '\tool_messageinbound\task\pickup_task',
'blocking' => 0,
'minute' => '*',
'hour' => '*',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
array(
'classname' => '\tool_messageinbound\task\cleanup_task',
'blocking' => 0,
'minute' => '55',
'hour' => '1',
'day' => '*',
'dayofweek' => '*',
'month' => '*'
),
);
+92
View File
@@ -0,0 +1,92 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Inbound Message Settings pages.
*
* @package tool_messageinbound
* @copyright 2014 Andrew NIcols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir.'/adminlib.php');
require_once($CFG->libdir.'/tablelib.php');
admin_externalpage_setup('messageinbound_handlers');
$classname = optional_param('classname', '', PARAM_RAW);
$pageurl = new moodle_url('/admin/tool/messageinbound/index.php');
$PAGE->set_primary_active_tab('siteadminnode');
$PAGE->navbar->add(get_string('message_handlers', 'tool_messageinbound'), $PAGE->url);
if (empty($classname)) {
$renderer = $PAGE->get_renderer('tool_messageinbound');
$records = $DB->get_recordset('messageinbound_handlers', null, 'enabled desc', 'classname');
$instances = array();
foreach ($records as $record) {
$instances[] = \core\message\inbound\manager::get_handler($record->classname);
}
$records->close();
echo $OUTPUT->header();
echo $renderer->messageinbound_handlers_table($instances);
echo $OUTPUT->footer();
} else {
// Retrieve the handler and its record.
$handler = \core\message\inbound\manager::get_handler($classname);
$record = \core\message\inbound\manager::record_from_handler($handler);
$formurl = new moodle_url($PAGE->url, array('classname' => $classname));
$mform = new tool_messageinbound_edit_handler_form($formurl, array(
'handler' => $handler,
));
if ($mform->is_cancelled()) {
redirect($PAGE->url);
} else if ($data = $mform->get_data()) {
// Update the record from the form.
if ($handler->can_change_defaultexpiration()) {
$record->defaultexpiration = (int) $data->defaultexpiration;
}
if ($handler->can_change_validateaddress()) {
$record->validateaddress = !empty($data->validateaddress);
}
if ($handler->can_change_enabled()) {
$record->enabled = !empty($data->enabled);
}
$DB->update_record('messageinbound_handlers', $record);
redirect($PAGE->url);
}
// Add the breadcrumb.
$pageurl->param('classname', $handler->classname);
$PAGE->navbar->add($handler->name, $pageurl);
echo $OUTPUT->header();
echo $OUTPUT->heading(get_string('editinghandler', 'tool_messageinbound', $handler->name));
$mform->set_data($record);
$mform->display();
echo $OUTPUT->footer();
}
@@ -0,0 +1,119 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'tool_messageinbound', language 'en'
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['classname'] = 'Class name';
$string['component'] = 'Component';
$string['configmessageinboundhost'] = 'The address of the server that Moodle should check mail against. To specify a non-default port, use [server]:[port], for example mail.example.com:993. If a port isn\'t specified, the default port for the type of mail server will be used.';
$string['defaultexpiration'] = 'Default address expiry period';
$string['defaultexpiration_help'] = 'When an email address is generated by the handler, it can be set to automatically expire after a period of time, so that it can no longer be used. It is advisable to set an expiry period.';
$string['description'] = 'Description';
$string['domain'] = 'Email domain';
$string['edit'] = 'Edit';
$string['edithandler'] = 'Edit settings for the {$a} handler';
$string['editinghandler'] = 'Editing {$a}';
$string['enabled'] = 'Enabled';
$string['fixedvalidateaddress'] = 'Validate sender address';
$string['fixedvalidateaddress_help'] = 'You cannot change the address validation for this handler. This may be because the handler requires a specific setting.';
$string['fixedenabled_help'] = 'You cannot change the state of this handler. This may be because the handler is required by other handlers.';
$string['handlerdisabled'] = 'The email handler you tried to contact has been disabled. Unable to process message at this time.';
$string['incomingmailconfiguration'] = 'Incoming mail configuration';
$string['incomingmailserversettings'] = 'Incoming mail server settings';
$string['incomingmailserversettings_desc'] = 'Moodle is capable of connecting to appropriately configured IMAP servers. You can specify the settings used to connect to your IMAP server here.';
$string['invalid_recipient_handler'] = 'If a valid message is received but the sender cannot be authenticated, the message is stored on the email server and the user is contacted using the email address in their user profile. The user is given the chance to reply to confirm the authenticity of the original message.
This handler processes those replies.
It is not possible to disable sender verification of this handler because the user may reply from an incorrect email address if their email client configuration is incorrect.';
$string['invalid_recipient_handler_name'] = 'Invalid sender handler';
$string['invalidrecipientdescription'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
$string['invalidrecipientdescriptionhtml'] = 'The message "{$a->subject}" could not be authenticated, since it was sent from a different email address than in your user profile. For the message to be authenticated, you need to reply to this message.';
$string['invalidrecipientfinal'] = 'The message "{$a->subject}" could not be authenticated. Please check that you are sending your message from the same email address as in your profile.';
$string['mailbox'] = 'Mailbox name';
$string['mailboxconfiguration'] = 'Mailbox configuration';
$string['mailboxdescription'] = '[mailbox]+subaddress@[domain]';
$string['mailsettings'] = 'Mail settings';
$string['message_handlers'] = 'Message handlers';
$string['messageprocessingerror'] = 'You recently sent an email "{$a->subject}" but unfortunately it could not be processed.
The details of the error are shown below.
{$a->error}';
$string['messageprocessingerrorhtml'] = '<p>You recently sent an email "{$a->subject}" but unfortunately it could not be processed.</p>
<p>The details of the error are shown below.</p>
<p>{$a->error}</p>';
$string['messageprocessingfailed'] = 'The email "{$a->subject}" could not be processed. The error is as follows: "{$a->message}".';
$string['messageprocessingfailedunknown'] = 'The email "{$a->subject}" could not be processed. Contact your administrator for further information.';
$string['messageprocessingsuccess'] = '{$a->plain}
If you do not wish to receive these notifications in the future, you can edit your personal messaging preferences by opening {$a->messagepreferencesurl} in your browser.';
$string['messageprocessingsuccesshtml'] = '{$a->html}
<p>If you do not wish to receive these notifications in the future, you can <a href="{$a->messagepreferencesurl}">edit your personal messaging preferences</a>.</p>';
$string['messageinbound'] = 'Message Inbound';
$string['messageinboundenabled'] = 'Enable incoming mail processing';
$string['messageinboundenabled_desc'] = 'Incoming mail processing must be enabled in order for messages to be sent with the appropriate information.';
$string['messageinboundgeneralconfiguration'] = 'General configuration';
$string['messageinboundgeneralconfiguration_desc'] = 'Inbound message processing allows you to receive and process email within Moodle. This has applications such as sending email replies to forum posts or adding files to a user\'s private files.';
$string['messageinboundhost'] = 'Incoming Mail Server';
$string['messageinboundhostoauth_help'] = 'OAuth 2 service to use to access the IMAP server, using XOAUTH2 authentication. If the service doesn\'t exist yet, you will need to create it.';
$string['messageinboundhostpass'] = 'Password';
$string['messageinboundhostpass_desc'] = 'This is the password your service provider will have provided to log in to your email account with.';
$string['messageinboundhostssl'] = 'Use SSL';
$string['messageinboundhostssl_desc'] = 'Some mail servers support an additional level of security by encrypting communication between Moodle and your server. We recommend using this SSL encryption if your server supports it.';
$string['messageinboundhosttype'] = 'Server type';
$string['messageinboundhostuser'] = 'Username';
$string['messageinboundhostuser_desc'] = 'This is the username your service provider will have provided to log in to your email account with.';
$string['messageinboundmailboxconfiguration_desc'] = 'When messages are sent out, they fit into the format address+data@example.com. To reliably generate addresses from Moodle, please specify the address that you would normally use before the @ sign, and the domain after the @ sign separately. For example, the Mailbox name in the example would be "address", and the E-mail domain would be "example.com". You should use a dedicated e-mail account for this purpose.';
$string['messageprovider:invalidrecipienthandler'] = 'Message to confirm that an inbound message came from you';
$string['messageprovider:messageprocessingerror'] = 'Warning when an inbound message could not be processed';
$string['messageprovider:messageprocessingsuccess'] = 'Confirmation that a message was successfully processed';
$string['noencryption'] = 'Off - No encryption';
$string['noexpiry'] = 'No expiry';
$string['oldmessagenotfound'] = 'You tried to manually authenticate a message, but the message could not be found. This could be because it has already been processed, or because the message expired.';
$string['oneday'] = 'One day';
$string['onehour'] = 'One hour';
$string['oneweek'] = 'One week';
$string['oneyear'] = 'One year';
$string['pluginname'] = 'Inbound message configuration';
$string['privacy:metadata:coreuserkey'] = 'User\'s keys to validate the email received';
$string['privacy:metadata:messagelist'] = 'A list of message identifiers which failed validation and requires further authorisation';
$string['privacy:metadata:messagelist:address'] = 'The address where the email was sent';
$string['privacy:metadata:messagelist:messageid'] = 'The message ID';
$string['privacy:metadata:messagelist:timecreated'] = 'The time when the record was made';
$string['privacy:metadata:messagelist:userid'] = 'The ID of user who need to approve the message';
$string['replysubjectprefix'] = 'Re:';
$string['requirevalidation'] = 'Validate sender address';
$string['name'] = 'Name';
$string['ssl'] = 'SSL (Auto-detect SSL version)';
$string['sslv2'] = 'SSLv2 (Force SSL Version 2)';
$string['sslv3'] = 'SSLv3 (Force SSL Version 3)';
$string['taskcleanup'] = 'Cleanup of unverified incoming email';
$string['taskpickup'] = 'Incoming email pickup';
$string['tls'] = 'TLS (TLS; started via protocol-level negotiation over unencrypted channel; RECOMMENDED way of initiating secure connection)';
$string['tlsv1'] = 'TLSv1 (direct connection to TLS server version 1.x)';
$string['validateaddress'] = 'Validate sender email address';
$string['validateaddress_help'] = 'When a message is received from a user, Moodle attempts to validate the message by comparing the email address of the sender with the email address in their user profile.
If the sender does not match, then the user is sent a notification to confirm that they really did send the email.
If this setting is disabled, then the email address of the sender is not checked at all.';
+108
View File
@@ -0,0 +1,108 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Output rendering for the plugin.
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Implements the plugin renderer
*
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class tool_messageinbound_renderer extends plugin_renderer_base {
/**
* Render a table listing all of the Inbound Message handlers.
*
* @param array $handlers - list of all messageinbound handlers.
* @return string HTML to output.
*/
public function messageinbound_handlers_table(array $handlers) {
global $CFG;
$table = new html_table();
$handlername = new html_table_cell(get_string('name', 'tool_messageinbound') . "\n" .
html_writer::tag('span', get_string('classname', 'tool_messageinbound'), array('class' => 'handler-function')));
// Prepare some of the rows with additional styling.
$enabled = new html_table_cell(get_string('enabled', 'tool_messageinbound'));
$enabled->attributes['class'] = 'state';
$edit = new html_table_cell(get_string('edit', 'tool_messageinbound'));
$edit->attributes['class'] = 'edit';
$table->head = array(
$handlername,
get_string('description', 'tool_messageinbound'),
$enabled,
$edit,
);
$table->attributes['class'] = 'admintable generaltable messageinboundhandlers';
$yes = get_string('yes');
$no = get_string('no');
$data = array();
// Options for description formatting.
$descriptionoptions = new stdClass();
$descriptionoptions->trusted = false;
$descriptionoptions->noclean = false;
$descriptionoptions->filter = false;
$descriptionoptions->para = true;
$descriptionoptions->newlines = false;
$descriptionoptions->overflowdiv = true;
$editurlbase = new moodle_url('/admin/tool/messageinbound/index.php');
foreach ($handlers as $handler) {
$handlername = new html_table_cell($handler->name . "\n" .
html_writer::tag('span', $handler->classname, array('class' => 'handler-function')));
$handlername->header = true;
$editurl = new moodle_url($editurlbase, array('classname' => $handler->classname));
$editlink = $this->action_icon($editurl, new pix_icon('t/edit',
get_string('edithandler', 'tool_messageinbound', $handler->classname)));
// Prepare some of the rows with additional styling.
$enabled = new html_table_cell($handler->enabled ? $yes : $no);
$enabled->attributes['class'] = 'state';
$edit = new html_table_cell($editlink);
$edit->attributes['class'] = 'edit';
// Add the row.
$row = new html_table_row(array(
$handlername,
format_text($handler->description, FORMAT_MARKDOWN, $descriptionoptions),
$enabled,
$edit,
));
if (!$handler->enabled) {
$row->attributes['class'] = 'disabled';
}
$data[] = $row;
}
$table->data = $data;
return html_writer::table($table);
}
}
@@ -0,0 +1,615 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| Copyright (C) 2000 Edmund Grimley Evans <edmundo@rano.org> |
| |
| 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: |
| Provide charset conversion functionality |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
| Author: Edmund Grimley Evans <edmundo@rano.org> |
+-----------------------------------------------------------------------+
*/
/**
* Character sets conversion functionality
*
* @package Framework
* @subpackage Core
*/
class rcube_charset
{
/**
* Character set aliases (some of them from HTML5 spec.)
*
* @var array
*/
static public $aliases = [
'USASCII' => 'WINDOWS-1252',
'ANSIX31101983' => 'WINDOWS-1252',
'ANSIX341968' => 'WINDOWS-1252',
'UNKNOWN8BIT' => 'ISO-8859-15',
'UNKNOWN' => 'ISO-8859-15',
'USERDEFINED' => 'ISO-8859-15',
'KSC56011987' => 'EUC-KR',
'GB2312' => 'GBK',
'GB231280' => 'GBK',
'UNICODE' => 'UTF-8',
'UTF7IMAP' => 'UTF7-IMAP',
'TIS620' => 'WINDOWS-874',
'ISO88599' => 'WINDOWS-1254',
'ISO885911' => 'WINDOWS-874',
'MACROMAN' => 'MACINTOSH',
'77' => 'MAC',
'128' => 'SHIFT-JIS',
'129' => 'CP949',
'130' => 'CP1361',
'134' => 'GBK',
'136' => 'BIG5',
'161' => 'WINDOWS-1253',
'162' => 'WINDOWS-1254',
'163' => 'WINDOWS-1258',
'177' => 'WINDOWS-1255',
'178' => 'WINDOWS-1256',
'186' => 'WINDOWS-1257',
'204' => 'WINDOWS-1251',
'222' => 'WINDOWS-874',
'238' => 'WINDOWS-1250',
'MS950' => 'CP950',
'WINDOWS31J' => 'CP932',
'WINDOWS949' => 'UHC',
'WINDOWS1257' => 'ISO-8859-13',
'ISO2022JP' => 'ISO-2022-JP-MS',
];
/**
* Windows codepages
*
* @var array
*/
static public $windows_codepages = [
37 => 'IBM037', // IBM EBCDIC US-Canada
437 => 'IBM437', // OEM United States
500 => 'IBM500', // IBM EBCDIC International
708 => 'ASMO-708', // Arabic (ASMO 708)
720 => 'DOS-720', // Arabic (Transparent ASMO); Arabic (DOS)
737 => 'IBM737', // OEM Greek (formerly 437G); Greek (DOS)
775 => 'IBM775', // OEM Baltic; Baltic (DOS)
850 => 'IBM850', // OEM Multilingual Latin 1; Western European (DOS)
852 => 'IBM852', // OEM Latin 2; Central European (DOS)
855 => 'IBM855', // OEM Cyrillic (primarily Russian)
857 => 'IBM857', // OEM Turkish; Turkish (DOS)
858 => 'IBM00858', // OEM Multilingual Latin 1 + Euro symbol
860 => 'IBM860', // OEM Portuguese; Portuguese (DOS)
861 => 'IBM861', // OEM Icelandic; Icelandic (DOS)
862 => 'DOS-862', // OEM Hebrew; Hebrew (DOS)
863 => 'IBM863', // OEM French Canadian; French Canadian (DOS)
864 => 'IBM864', // OEM Arabic; Arabic (864)
865 => 'IBM865', // OEM Nordic; Nordic (DOS)
866 => 'cp866', // OEM Russian; Cyrillic (DOS)
869 => 'IBM869', // OEM Modern Greek; Greek, Modern (DOS)
870 => 'IBM870', // IBM EBCDIC Multilingual/ROECE (Latin 2); IBM EBCDIC Multilingual Latin 2
874 => 'windows-874', // ANSI/OEM Thai (ISO 8859-11); Thai (Windows)
875 => 'cp875', // IBM EBCDIC Greek Modern
932 => 'shift_jis', // ANSI/OEM Japanese; Japanese (Shift-JIS)
936 => 'gb2312', // ANSI/OEM Simplified Chinese (PRC, Singapore); Chinese Simplified (GB2312)
950 => 'big5', // ANSI/OEM Traditional Chinese (Taiwan; Hong Kong SAR, PRC); Chinese Traditional (Big5)
1026 => 'IBM1026', // IBM EBCDIC Turkish (Latin 5)
1047 => 'IBM01047', // IBM EBCDIC Latin 1/Open System
1140 => 'IBM01140', // IBM EBCDIC US-Canada (037 + Euro symbol); IBM EBCDIC (US-Canada-Euro)
1141 => 'IBM01141', // IBM EBCDIC Germany (20273 + Euro symbol); IBM EBCDIC (Germany-Euro)
1142 => 'IBM01142', // IBM EBCDIC Denmark-Norway (20277 + Euro symbol); IBM EBCDIC (Denmark-Norway-Euro)
1143 => 'IBM01143', // IBM EBCDIC Finland-Sweden (20278 + Euro symbol); IBM EBCDIC (Finland-Sweden-Euro)
1144 => 'IBM01144', // IBM EBCDIC Italy (20280 + Euro symbol); IBM EBCDIC (Italy-Euro)
1145 => 'IBM01145', // IBM EBCDIC Latin America-Spain (20284 + Euro symbol); IBM EBCDIC (Spain-Euro)
1146 => 'IBM01146', // IBM EBCDIC United Kingdom (20285 + Euro symbol); IBM EBCDIC (UK-Euro)
1147 => 'IBM01147', // IBM EBCDIC France (20297 + Euro symbol); IBM EBCDIC (France-Euro)
1148 => 'IBM01148', // IBM EBCDIC International (500 + Euro symbol); IBM EBCDIC (International-Euro)
1149 => 'IBM01149', // IBM EBCDIC Icelandic (20871 + Euro symbol); IBM EBCDIC (Icelandic-Euro)
1200 => 'UTF-16', // Unicode UTF-16, little endian byte order (BMP of ISO 10646); available only to managed applications
1201 => 'UTF-16BE', // Unicode UTF-16, big endian byte order; available only to managed applications
1250 => 'windows-1250', // ANSI Central European; Central European (Windows)
1251 => 'windows-1251', // ANSI Cyrillic; Cyrillic (Windows)
1252 => 'windows-1252', // ANSI Latin 1; Western European (Windows)
1253 => 'windows-1253', // ANSI Greek; Greek (Windows)
1254 => 'windows-1254', // ANSI Turkish; Turkish (Windows)
1255 => 'windows-1255', // ANSI Hebrew; Hebrew (Windows)
1256 => 'windows-1256', // ANSI Arabic; Arabic (Windows)
1257 => 'windows-1257', // ANSI Baltic; Baltic (Windows)
1258 => 'windows-1258', // ANSI/OEM Vietnamese; Vietnamese (Windows)
10000 => 'macintosh', // MAC Roman; Western European (Mac)
12000 => 'UTF-32', // Unicode UTF-32, little endian byte order; available only to managed applications
12001 => 'UTF-32BE', // Unicode UTF-32, big endian byte order; available only to managed applications
20127 => 'US-ASCII', // US-ASCII (7-bit)
20273 => 'IBM273', // IBM EBCDIC Germany
20277 => 'IBM277', // IBM EBCDIC Denmark-Norway
20278 => 'IBM278', // IBM EBCDIC Finland-Sweden
20280 => 'IBM280', // IBM EBCDIC Italy
20284 => 'IBM284', // IBM EBCDIC Latin America-Spain
20285 => 'IBM285', // IBM EBCDIC United Kingdom
20290 => 'IBM290', // IBM EBCDIC Japanese Katakana Extended
20297 => 'IBM297', // IBM EBCDIC France
20420 => 'IBM420', // IBM EBCDIC Arabic
20423 => 'IBM423', // IBM EBCDIC Greek
20424 => 'IBM424', // IBM EBCDIC Hebrew
20838 => 'IBM-Thai', // IBM EBCDIC Thai
20866 => 'koi8-r', // Russian (KOI8-R); Cyrillic (KOI8-R)
20871 => 'IBM871', // IBM EBCDIC Icelandic
20880 => 'IBM880', // IBM EBCDIC Cyrillic Russian
20905 => 'IBM905', // IBM EBCDIC Turkish
20924 => 'IBM00924', // IBM EBCDIC Latin 1/Open System (1047 + Euro symbol)
20932 => 'EUC-JP', // Japanese (JIS 0208-1990 and 0212-1990)
20936 => 'cp20936', // Simplified Chinese (GB2312); Chinese Simplified (GB2312-80)
20949 => 'cp20949', // Korean Wansung
21025 => 'cp1025', // IBM EBCDIC Cyrillic Serbian-Bulgarian
21866 => 'koi8-u', // Ukrainian (KOI8-U); Cyrillic (KOI8-U)
28591 => 'iso-8859-1', // ISO 8859-1 Latin 1; Western European (ISO)
28592 => 'iso-8859-2', // ISO 8859-2 Central European; Central European (ISO)
28593 => 'iso-8859-3', // ISO 8859-3 Latin 3
28594 => 'iso-8859-4', // ISO 8859-4 Baltic
28595 => 'iso-8859-5', // ISO 8859-5 Cyrillic
28596 => 'iso-8859-6', // ISO 8859-6 Arabic
28597 => 'iso-8859-7', // ISO 8859-7 Greek
28598 => 'iso-8859-8', // ISO 8859-8 Hebrew; Hebrew (ISO-Visual)
28599 => 'iso-8859-9', // ISO 8859-9 Turkish
28603 => 'iso-8859-13', // ISO 8859-13 Estonian
28605 => 'iso-8859-15', // ISO 8859-15 Latin 9
38598 => 'iso-8859-8-i', // ISO 8859-8 Hebrew; Hebrew (ISO-Logical)
50220 => 'iso-2022-jp', // ISO 2022 Japanese with no halfwidth Katakana; Japanese (JIS)
50221 => 'csISO2022JP', // ISO 2022 Japanese with halfwidth Katakana; Japanese (JIS-Allow 1 byte Kana)
50222 => 'iso-2022-jp', // ISO 2022 Japanese JIS X 0201-1989; Japanese (JIS-Allow 1 byte Kana - SO/SI)
50225 => 'iso-2022-kr', // ISO 2022 Korean
51932 => 'EUC-JP', // EUC Japanese
51936 => 'EUC-CN', // EUC Simplified Chinese; Chinese Simplified (EUC)
51949 => 'EUC-KR', // EUC Korean
52936 => 'hz-gb-2312', // HZ-GB2312 Simplified Chinese; Chinese Simplified (HZ)
54936 => 'GB18030', // Windows XP and later: GB18030 Simplified Chinese (4 byte); Chinese Simplified (GB18030)
65000 => 'UTF-7',
65001 => 'UTF-8',
];
/**
* Validate character set identifier.
*
* @param string $input Character set identifier
*
* @return bool True if valid, False if not valid
*/
public static function is_valid($input)
{
return is_string($input) && preg_match('|^[a-zA-Z0-9_./:#-]{2,32}$|', $input) > 0;
}
/**
* Parse and validate charset name string.
* Sometimes charset string is malformed, there are also charset aliases,
* but we need strict names for charset conversion (specially utf8 class)
*
* @param string $input Input charset name
*
* @return string The validated charset name
*/
public static function parse_charset($input)
{
static $charsets = [];
$charset = strtoupper((string) $input);
if (isset($charsets[$input])) {
return $charsets[$input];
}
$charset = preg_replace([
'/^[^0-9A-Z]+/', // e.g. _ISO-8859-JP$SIO
'/\$.*$/', // e.g. _ISO-8859-JP$SIO
'/UNICODE-1-1-*/', // RFC1641/1642
'/^X-/', // X- prefix (e.g. X-ROMAN8 => ROMAN8)
'/\*.*$/' // lang code according to RFC 2231.5
], '', $charset);
if ($charset == 'BINARY') {
return $charsets[$input] = null;
}
// allow A-Z and 0-9 only
$str = preg_replace('/[^A-Z0-9]/', '', $charset);
$result = $charset;
if (isset(self::$aliases[$str])) {
$result = self::$aliases[$str];
}
// UTF
else if (preg_match('/U[A-Z][A-Z](7|8|16|32)(BE|LE)*/', $str, $m)) {
$result = 'UTF-' . $m[1] . (!empty($m[2]) ? $m[2] : '');
}
// ISO-8859
else if (preg_match('/ISO8859([0-9]{0,2})/', $str, $m)) {
$iso = 'ISO-8859-' . ($m[1] ?: 1);
// some clients sends windows-1252 text as latin1,
// it is safe to use windows-1252 for all latin1
$result = $iso == 'ISO-8859-1' ? 'WINDOWS-1252' : $iso;
}
// handle broken charset names e.g. WINDOWS-1250HTTP-EQUIVCONTENT-TYPE
else if (preg_match('/(WIN|WINDOWS)([0-9]+)/', $str, $m)) {
$result = 'WINDOWS-' . $m[2];
}
// LATIN
else if (preg_match('/LATIN(.*)/', $str, $m)) {
$aliases = ['2' => 2, '3' => 3, '4' => 4, '5' => 9, '6' => 10,
'7' => 13, '8' => 14, '9' => 15, '10' => 16,
'ARABIC' => 6, 'CYRILLIC' => 5, 'GREEK' => 7, 'GREEK1' => 7, 'HEBREW' => 8
];
// some clients sends windows-1252 text as latin1,
// it is safe to use windows-1252 for all latin1
if ($m[1] == 1) {
$result = 'WINDOWS-1252';
}
// we need ISO labels
else if (!empty($aliases[$m[1]])) {
$result = 'ISO-8859-'.$aliases[$m[1]];
}
}
$charsets[$input] = $result;
return $result;
}
/**
* Convert a string from one charset to another.
*
* @param string $str Input string
* @param string $from Suspected charset of the input string
* @param string $to Target charset to convert to; defaults to RCUBE_CHARSET
*
* @return string Converted string
*/
public static function convert($str, $from, $to = null)
{
static $iconv_options;
$to = empty($to) ? RCUBE_CHARSET : self::parse_charset($to);
$from = self::parse_charset($from);
// It is a common case when UTF-16 charset is used with US-ASCII content (#1488654)
// In that case we can just skip the conversion (use UTF-8)
if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) {
$from = 'UTF-8';
}
if ($from == $to || empty($str) || empty($from)) {
return $str;
}
$out = false;
$error_handler = function() { throw new \Exception(); };
// Ignore invalid characters
$mbstring_sc = mb_substitute_character();
mb_substitute_character('none');
// If mbstring reports an illegal character in input via E_WARNING.
// FIXME: Is this really true with substitute character 'none'?
// A warning is thrown in PHP<8 also on unsupported encoding, in PHP>=8 ValueError
// is thrown instead (therefore we catch Throwable below)
set_error_handler($error_handler, E_WARNING);
try {
$out = mb_convert_encoding($str, $to, $from);
}
catch (Throwable $e) {
$out = false;
}
restore_error_handler();
mb_substitute_character($mbstring_sc);
if ($out !== false) {
return $out;
}
if ($iconv_options === null) {
if (function_exists('iconv')) {
// ignore characters not available in output charset
$iconv_options = '//IGNORE';
if (iconv('', $iconv_options, '') === false) {
// iconv implementation does not support options
$iconv_options = '';
}
}
else {
$iconv_options = false;
}
}
// Fallback to iconv module, it is slower, but supports much more charsets than mbstring
if ($iconv_options !== false && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP'
&& $from !== 'ISO-2022-JP'
) {
// If iconv reports an illegal character in input it means that input string
// has been truncated. It's reported as E_NOTICE.
// PHP8 will also throw E_WARNING on unsupported encoding.
set_error_handler($error_handler, E_NOTICE | E_WARNING);
try {
$out = iconv($from, $to . $iconv_options, $str);
}
catch (Throwable $e) {
$out = false;
}
restore_error_handler();
if ($out !== false) {
return $out;
}
}
// return the original string
return $str;
}
/**
* Check if the specified input string matches one of the provided charsets.
* This includes UTF-32, UTF-16, RCUBE_CHARSET and default_charset.
*
* @param string $str Input string
* @param array $charsets Suspected charsets of the input string
*
* @return string|null First matching charset
*/
public static function check($str, $charsets = [])
{
$chunk = strlen($str) > 100 * 1024 ? substr($str, 0, 100 * 1024) : $str;
// Add dehault charset, system charset and easily detectable charset to the list
if (substr($chunk, 0, 4) == "\0\0\xFE\xFF") $charsets[] = 'UTF-32BE';
if (substr($chunk, 0, 4) == "\xFF\xFE\0\0") $charsets[] = 'UTF-32LE';
if (substr($chunk, 0, 2) == "\xFE\xFF") $charsets[] = 'UTF-16BE';
if (substr($chunk, 0, 2) == "\xFF\xFE") $charsets[] = 'UTF-16LE';
// heuristics
if (preg_match('/\x00\x00\x00[^\x00]/', $chunk)) $charsets[] = 'UTF-32BE';
if (preg_match('/[^\x00]\x00\x00\x00/', $chunk)) $charsets[] = 'UTF-32LE';
if (preg_match('/\x00[^\x00]\x00[^\x00]/', $chunk)) $charsets[] = 'UTF-16BE';
if (preg_match('/[^\x00]\x00[^\x00]\x00/', $chunk)) $charsets[] = 'UTF-16LE';
$charsets[] = RCUBE_CHARSET;
$charsets[] = (string) rcube::get_instance()->config->get('default_charset');
$charsets = array_map(['rcube_charset', 'parse_charset'], $charsets);
$charsets = array_unique(array_filter($charsets));
foreach ($charsets as $charset) {
$ret = self::convert($chunk, $charset);
if ($ret === rcube_charset::clean($ret)) {
return $charset;
}
}
}
/**
* Converts string from standard UTF-7 (RFC 2152) to UTF-8.
*
* @param string $str Input string (UTF-7)
*
* @return string Converted string (UTF-8)
* @deprecated use self::convert()
*/
public static function utf7_to_utf8($str)
{
return self::convert($str, 'UTF-7', 'UTF-8');
}
/**
* Converts string from UTF-16 to UTF-8 (helper for utf-7 to utf-8 conversion)
*
* @param string $str Input string
*
* @return string The converted string
* @deprecated use self::convert()
*/
public static function utf16_to_utf8($str)
{
return self::convert($str, 'UTF-16BE', 'UTF-8');
}
/**
* Convert the data ($str) from RFC 2060's UTF-7 to UTF-8.
* If input data is invalid, return the original input string.
* RFC 2060 obviously intends the encoding to be unique (see
* point 5 in section 5.1.3), so we reject any non-canonical
* form, such as &ACY- (instead of &-) or &AMA-&AMA- (instead
* of &AMAAwA-).
*
* @param string $str Input string (UTF7-IMAP)
*
* @return string Output string (UTF-8)
* @deprecated use self::convert()
*/
public static function utf7imap_to_utf8($str)
{
return self::convert($str, 'UTF7-IMAP', 'UTF-8');
}
/**
* Convert the data ($str) from UTF-8 to RFC 2060's UTF-7.
* Unicode characters above U+FFFF are replaced by U+FFFE.
* If input data is invalid, return an empty string.
*
* @param string $str Input string (UTF-8)
*
* @return string Output string (UTF7-IMAP)
* @deprecated use self::convert()
*/
public static function utf8_to_utf7imap($str)
{
return self::convert($str, 'UTF-8', 'UTF7-IMAP');
}
/**
* A method to guess character set of a string.
*
* @param string $string String
* @param string $failover Default result for failover
* @param string $language User language
*
* @return string Charset name
* @deprecated
*/
public static function detect($string, $failover = null, $language = null)
{
if (substr($string, 0, 4) == "\0\0\xFE\xFF") return 'UTF-32BE'; // Big Endian
if (substr($string, 0, 4) == "\xFF\xFE\0\0") return 'UTF-32LE'; // Little Endian
if (substr($string, 0, 2) == "\xFE\xFF") return 'UTF-16BE'; // Big Endian
if (substr($string, 0, 2) == "\xFF\xFE") return 'UTF-16LE'; // Little Endian
if (substr($string, 0, 3) == "\xEF\xBB\xBF") return 'UTF-8';
// heuristics
if (strlen($string) >= 4) {
if ($string[0] == "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-32BE';
if ($string[0] != "\0" && $string[1] == "\0" && $string[2] == "\0" && $string[3] == "\0") return 'UTF-32LE';
if ($string[0] == "\0" && $string[1] != "\0" && $string[2] == "\0" && $string[3] != "\0") return 'UTF-16BE';
if ($string[0] != "\0" && $string[1] == "\0" && $string[2] != "\0" && $string[3] == "\0") return 'UTF-16LE';
}
if (empty($language)) {
$rcube = rcube::get_instance();
$language = $rcube->get_user_language();
}
// Prioritize charsets according to the current language (#1485669)
$prio = null;
switch ($language) {
case 'ja_JP':
$prio = ['ISO-2022-JP', 'JIS', 'UTF-8', 'EUC-JP', 'eucJP-win', 'SJIS'];
break;
case 'zh_CN':
case 'zh_TW':
$prio = ['UTF-8', 'BIG-5', 'EUC-TW', 'GB18030'];
break;
case 'ko_KR':
$prio = ['UTF-8', 'EUC-KR', 'ISO-2022-KR'];
break;
case 'ru_RU':
$prio = ['UTF-8', 'WINDOWS-1251', 'KOI8-R'];
break;
case 'tr_TR':
$prio = ['UTF-8', 'ISO-8859-9', 'WINDOWS-1254'];
break;
}
// mb_detect_encoding() is not reliable for some charsets (#1490135)
// use mb_check_encoding() to make charset priority lists really working
if (!empty($prio) && function_exists('mb_check_encoding')) {
foreach ($prio as $encoding) {
if (mb_check_encoding($string, $encoding)) {
return $encoding;
}
}
}
if (function_exists('mb_detect_encoding')) {
$exclude = 'BASE64,UUENCODE,HTML-ENTITIES,Quoted-Printable,'
. '7bit,8bit,pass,wchar,byte2be,byte2le,byte4be,byte4le,'
. 'UCS-4,UCS-4BE,UCS-4LE,UCS-2,UCS-2BE,UCS-2LE';
if (empty($prio)) {
$prio = [
'UTF-8',
'ISO-8859-1', 'ISO-8859-2', 'ISO-8859-3', 'ISO-8859-4',
'ISO-8859-5', 'ISO-8859-6', 'ISO-8859-7', 'ISO-8859-8', 'ISO-8859-9',
'ISO-8859-10', 'ISO-8859-13', 'ISO-8859-14', 'ISO-8859-15', 'ISO-8859-16',
'WINDOWS-1252', 'WINDOWS-1251', 'WINDOWS-1254',
'EUC-JP', 'EUC-TW', 'KOI8-R', 'BIG-5', 'ISO-2022-KR', 'ISO-2022-JP', 'GB18030',
];
}
// We have to remove unwanted/uncommon encodings from the list.
// This is needed especially on PHP >= 8.1
$all_encodings = array_diff(mb_list_encodings(), explode(',', $exclude));
$encodings = array_unique(array_merge($prio, $all_encodings));
if ($encoding = mb_detect_encoding($string, $encodings, true)) {
return $encoding;
}
}
// No match, check for UTF-8
// from http://w3.org/International/questions/qa-forms-utf-8.html
if (preg_match('/\A(
[\x09\x0A\x0D\x20-\x7E]
| [\xC2-\xDF][\x80-\xBF]
| \xE0[\xA0-\xBF][\x80-\xBF]
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
| \xED[\x80-\x9F][\x80-\xBF]
| \xF0[\x90-\xBF][\x80-\xBF]{2}
| [\xF1-\xF3][\x80-\xBF]{3}
| \xF4[\x80-\x8F][\x80-\xBF]{2}
)*\z/xs', substr($string, 0, 2048))
) {
return 'UTF-8';
}
return $failover;
}
/**
* Removes non-unicode characters from input.
* If the input is an array, both values and keys will be cleaned up.
*
* @param mixed $input String or array.
*
* @return mixed String or array
*/
public static function clean($input)
{
// handle input of type array
if (is_array($input)) {
foreach (array_keys($input) as $key) {
$k = is_string($key) ? self::clean($key) : $key;
$v = self::clean($input[$key]);
if ($k !== $key) {
unset($input[$key]);
if (!array_key_exists($k, $input)) {
$input[$k] = $v;
}
}
else {
$input[$k] = $v;
}
}
return $input;
}
if (!is_string($input) || $input == '') {
return $input;
}
$msch = mb_substitute_character();
mb_substitute_character('none');
$res = mb_convert_encoding($input, 'UTF-8', 'UTF-8');
mb_substitute_character($msch);
return $res;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,430 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| 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: |
| E-mail message headers representation |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Struct representing an e-mail message header
*
* @package Framework
* @subpackage Storage
*/
class rcube_message_header
{
/**
* Message sequence number
*
* @var int
*/
public $id;
/**
* Message unique identifier
*
* @var int
*/
public $uid;
/**
* Message subject
*
* @var string
*/
public $subject;
/**
* Message sender (From)
*
* @var string
*/
public $from;
/**
* Message recipient (To)
*
* @var string
*/
public $to;
/**
* Message additional recipients (Cc)
*
* @var string
*/
public $cc;
/**
* Message hidden recipients (Bcc)
*
* @var string
*/
public $bcc;
/**
* Message Reply-To header
*
* @var string
*/
public $replyto;
/**
* Message In-Reply-To header
*
* @var string
*/
public $in_reply_to;
/**
* Message date (Date)
*
* @var string
*/
public $date;
/**
* Message identifier (Message-ID)
*
* @var string
*/
public $messageID;
/**
* Message size
*
* @var int
*/
public $size;
/**
* Message encoding
*
* @var string
*/
public $encoding;
/**
* Message charset
*
* @var string
*/
public $charset;
/**
* Message Content-type
*
* @var string
*/
public $ctype;
/**
* Message timestamp (based on message date)
*
* @var int
*/
public $timestamp;
/**
* IMAP bodystructure string
*
* @var string
*/
public $bodystructure;
/**
* IMAP body (RFC822.TEXT)
*
* @var string
*/
public $body;
/**
* IMAP part bodies
*
* @var array
*/
public $bodypart = [];
/**
* IMAP internal date
*
* @var string
*/
public $internaldate;
/**
* Message References header
*
* @var string
*/
public $references;
/**
* Message priority (X-Priority)
*
* @var int
*/
public $priority;
/**
* Message receipt recipient
*
* @var string
*/
public $mdn_to;
/**
* IMAP folder this message is stored in
*
* @var string
*/
public $folder;
/**
* Other message headers
*
* @var array
*/
public $others = [];
/**
* Message flags
*
* @var array
*/
public $flags = [];
/**
* Extra flags (for the messages list)
*
* @var array
* @deprecated Use $flags
*/
public $list_flags = [];
/**
* Extra columns content (for the messages list)
*
* @var array
*/
public $list_cols = [];
/**
* Message structure
*
* @var rcube_message_part
*/
public $structure;
/**
* Message thread depth
*
* @var int
*/
public $depth;
/**
* Whether the message has references in the thread
*
* @var bool
*/
public $has_children;
/**
* Number of flagged children (in a thread)
*
* @var int
*/
public $flagged_children;
/**
* Number of unread children (in a thread)
*
* @var int
*/
public $unread_children;
/**
* UID of the message parent (in a thread)
*
* @var int
*/
public $parent_uid;
/**
* IMAP MODSEQ value
*
* @var int
*/
public $modseq;
/**
* IMAP ENVELOPE
*
* @var string
*/
public $envelope;
/**
* Header name to rcube_message_header object property map
*
* @var array
*/
private $obj_headers = [
'date' => 'date',
'from' => 'from',
'to' => 'to',
'subject' => 'subject',
'reply-to' => 'replyto',
'cc' => 'cc',
'bcc' => 'bcc',
'mbox' => 'folder',
'folder' => 'folder',
'content-transfer-encoding' => 'encoding',
'in-reply-to' => 'in_reply_to',
'content-type' => 'ctype',
'charset' => 'charset',
'references' => 'references',
'disposition-notification-to' => 'mdn_to',
'x-confirm-reading-to' => 'mdn_to',
'message-id' => 'messageID',
'x-priority' => 'priority',
];
/**
* Returns header value
*
* @param string $name Header name
* @param bool $decode Decode the header content
*
* @return string|null Header content
*/
public function get($name, $decode = true)
{
$name = strtolower($name);
$value = null;
if (isset($this->obj_headers[$name]) && isset($this->{$this->obj_headers[$name]})) {
$value = $this->{$this->obj_headers[$name]};
}
else if (isset($this->others[$name])) {
$value = $this->others[$name];
}
if ($decode && $value !== null) {
if (is_array($value)) {
foreach ($value as $key => $val) {
$val = rcube_mime::decode_header($val, $this->charset);
$value[$key] = rcube_charset::clean($val);
}
}
else {
$value = rcube_mime::decode_header($value, $this->charset);
$value = rcube_charset::clean($value);
}
}
return $value;
}
/**
* Sets header value
*
* @param string $name Header name
* @param string $value Header content
*/
public function set($name, $value)
{
$name = strtolower($name);
if (isset($this->obj_headers[$name])) {
$this->{$this->obj_headers[$name]} = $value;
}
else {
$this->others[$name] = $value;
}
}
/**
* Factory method to instantiate headers from a data array
*
* @param array $arr Hash array with header values
*
* @return rcube_message_header instance filled with headers values
*/
public static function from_array($arr)
{
$obj = new rcube_message_header;
foreach ($arr as $k => $v) {
$obj->set($k, $v);
}
return $obj;
}
}
/**
* Class for sorting an array of rcube_message_header objects in a predetermined order.
*
* @package Framework
* @subpackage Storage
*/
class rcube_message_header_sorter
{
/** @var array Message UIDs */
private $uids = [];
/**
* Set the predetermined sort order.
*
* @param array $index Numerically indexed array of IMAP UIDs
*/
function set_index($index)
{
$index = array_flip($index);
$this->uids = $index;
}
/**
* Sort the array of header objects
*
* @param array $headers Array of rcube_message_header objects indexed by UID
*/
function sort_headers(&$headers)
{
uksort($headers, [$this, "compare_uids"]);
}
/**
* Sort method called by uksort()
*
* @param int $a Array key (UID)
* @param int $b Array key (UID)
*/
function compare_uids($a, $b)
{
// then find each sequence number in my ordered list
$posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
$posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
// return the relative position as the comparison value
return $posa - $posb;
}
}
@@ -0,0 +1,992 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| 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: |
| MIME message parsing utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for parsing MIME messages
*
* @package Framework
* @subpackage Storage
*/
class rcube_mime
{
private static $default_charset;
/**
* Object constructor.
*/
function __construct($default_charset = null)
{
self::$default_charset = $default_charset;
}
/**
* Returns message/object character set name
*
* @return string Character set name
*/
public static function get_charset()
{
if (self::$default_charset) {
return self::$default_charset;
}
if ($charset = rcube::get_instance()->config->get('default_charset')) {
return $charset;
}
return RCUBE_CHARSET;
}
/**
* Parse the given raw message source and return a structure
* of rcube_message_part objects.
*
* It makes use of the rcube_mime_decode library
*
* @param string $raw_body The message source
*
* @return object rcube_message_part The message structure
*/
public static function parse_message($raw_body)
{
$conf = [
'include_bodies' => true,
'decode_bodies' => true,
'decode_headers' => false,
'default_charset' => self::get_charset(),
];
$mime = new rcube_mime_decode($conf);
return $mime->decode($raw_body);
}
/**
* Split an address list into a structured array list
*
* @param string|array $input Input string (or list of strings)
* @param int $max List only this number of addresses
* @param bool $decode Decode address strings
* @param string $fallback Fallback charset if none specified
* @param bool $addronly Return flat array with e-mail addresses only
*
* @return array Indexed list of addresses
*/
static function decode_address_list($input, $max = null, $decode = true, $fallback = null, $addronly = false)
{
// A common case when the same header is used many times in a mail message
if (is_array($input)) {
$input = implode(', ', $input);
}
$a = self::parse_address_list((string) $input, $decode, $fallback);
$out = [];
$j = 0;
// Special chars as defined by RFC 822 need to in quoted string (or escaped).
$special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
if (!is_array($a)) {
return $out;
}
foreach ($a as $val) {
$j++;
$address = trim($val['address']);
if ($addronly) {
$out[$j] = $address;
}
else {
$name = trim($val['name']);
$string = '';
if ($name && $address && $name != $address) {
$string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
}
else if ($address) {
$string = $address;
}
else if ($name) {
$string = $name;
}
$out[$j] = ['name' => $name, 'mailto' => $address, 'string' => $string];
}
if ($max && $j == $max) {
break;
}
}
return $out;
}
/**
* Decode a message header value
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_header($input, $fallback = null)
{
$str = self::decode_mime_string((string)$input, $fallback);
return $str;
}
/**
* Decode a mime-encoded string to internal charset
*
* @param string $input Header value
* @param string $fallback Fallback charset if none specified
*
* @return string Decoded string
*/
public static function decode_mime_string($input, $fallback = null)
{
$default_charset = $fallback ?: self::get_charset();
// rfc: all line breaks or other characters not found
// in the Base64 Alphabet must be ignored by decoding software
// delete all blanks between MIME-lines, differently we can
// receive unnecessary blanks and broken utf-8 symbols
$input = preg_replace("/\?=\s+=\?/", '?==?', $input);
// encoded-word regexp
$re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
// Find all RFC2047's encoded words
if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
// Initialize variables
$tmp = [];
$out = '';
$start = 0;
foreach ($matches as $idx => $m) {
$pos = $m[0][1];
$charset = $m[1][0];
$encoding = $m[2][0];
$text = $m[3][0];
$length = strlen($m[0][0]);
// Append everything that is before the text to be decoded
if ($start != $pos) {
$substr = substr($input, $start, $pos-$start);
$out .= rcube_charset::convert($substr, $default_charset);
$start = $pos;
}
$start += $length;
// Per RFC2047, each string part "MUST represent an integral number
// of characters . A multi-octet character may not be split across
// adjacent encoded-words." However, some mailers break this, so we
// try to handle characters spanned across parts anyway by iterating
// through and aggregating sequential encoded parts with the same
// character set and encoding, then perform the decoding on the
// aggregation as a whole.
$tmp[] = $text;
if (!empty($matches[$idx+1]) && ($next_match = $matches[$idx+1])) {
if ($next_match[0][1] == $start
&& $next_match[1][0] == $charset
&& $next_match[2][0] == $encoding
) {
continue;
}
}
$count = count($tmp);
$text = '';
// Decode and join encoded-word's chunks
if ($encoding == 'B' || $encoding == 'b') {
$rest = '';
// base64 must be decoded a segment at a time.
// However, there are broken implementations that continue
// in the following word, we'll handle that (#6048)
for ($i=0; $i<$count; $i++) {
$chunk = $rest . $tmp[$i];
$length = strlen($chunk);
if ($length % 4) {
$length = floor($length / 4) * 4;
$rest = substr($chunk, $length);
$chunk = substr($chunk, 0, $length);
}
$text .= base64_decode($chunk);
}
}
else { // if ($encoding == 'Q' || $encoding == 'q') {
// quoted printable can be combined and processed at once
for ($i=0; $i<$count; $i++) {
$text .= $tmp[$i];
}
$text = str_replace('_', ' ', $text);
$text = quoted_printable_decode($text);
}
$out .= rcube_charset::convert($text, $charset);
$tmp = [];
}
// add the last part of the input string
if ($start != strlen($input)) {
$out .= rcube_charset::convert(substr($input, $start), $default_charset);
}
// return the results
return $out;
}
// no encoding information, use fallback
return rcube_charset::convert($input, $default_charset);
}
/**
* Decode a mime part
*
* @param string $input Input string
* @param string $encoding Part encoding
*
* @return string Decoded string
*/
public static function decode($input, $encoding = '7bit')
{
switch (strtolower($encoding)) {
case 'quoted-printable':
return quoted_printable_decode($input);
case 'base64':
return base64_decode($input);
case 'x-uuencode':
case 'x-uue':
case 'uue':
case 'uuencode':
return convert_uudecode($input);
case '7bit':
default:
return $input;
}
}
/**
* Split RFC822 header string into an associative array
*/
public static function parse_headers($headers)
{
$result = [];
$headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
$lines = explode("\n", $headers);
$count = count($lines);
for ($i=0; $i<$count; $i++) {
if ($p = strpos($lines[$i], ': ')) {
$field = strtolower(substr($lines[$i], 0, $p));
$value = trim(substr($lines[$i], $p+1));
if (!empty($value)) {
$result[$field] = $value;
}
}
}
return $result;
}
/**
* E-mail address list parser
*/
private static function parse_address_list($str, $decode = true, $fallback = null)
{
// remove any newlines and carriage returns before
$str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
// extract list items, remove comments
$str = self::explode_header_string(',;', $str, true);
// simplified regexp, supporting quoted local part
$email_rx = '([^\s:]+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
$result = [];
foreach ($str as $key => $val) {
$name = '';
$address = '';
$val = trim($val);
// First token might be a group name, ignore it
$tokens = self::explode_header_string(' ', $val);
if (isset($tokens[0]) && $tokens[0][strlen($tokens[0])-1] == ':') {
$val = substr($val, strlen($tokens[0]));
}
if (preg_match('/(.*)<('.$email_rx.')$/', $val, $m)) {
// Note: There are cases like "Test<test@domain.tld" with no closing bracket,
// therefor we do not include it in the regexp above, but we have to
// remove it later, because $email_rx will catch it (#8164)
$address = rtrim($m[2], '>');
$name = trim($m[1]);
}
else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
$address = $m[1];
$name = '';
}
// special case (#1489092)
else if (preg_match('/(\s*<MAILER-DAEMON>)$/', $val, $m)) {
$address = 'MAILER-DAEMON';
$name = substr($val, 0, -strlen($m[1]));
}
else if (preg_match('/('.$email_rx.')/', $val, $m)) {
$name = $m[1];
}
else {
$name = $val;
}
// unquote and/or decode name
if ($name) {
// An unquoted name ending with colon is a address group name, ignore it
if ($name[strlen($name)-1] == ':') {
$name = '';
}
if (strlen($name) > 1 && $name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
$name = stripslashes($name);
}
if ($decode) {
$name = self::decode_header($name, $fallback);
// some clients encode addressee name with quotes around it
if (strlen($name) > 1 && $name[0] == '"' && $name[strlen($name)-1] == '"') {
$name = substr($name, 1, -1);
}
}
}
if (!$address && $name) {
$address = $name;
$name = '';
}
if ($address) {
$address = self::fix_email($address);
$result[$key] = ['name' => $name, 'address' => $address];
}
}
return $result;
}
/**
* Explodes header (e.g. address-list) string into array of strings
* using specified separator characters with proper handling
* of quoted-strings and comments (RFC2822)
*
* @param string $separator String containing separator characters
* @param string $str Header string
* @param bool $remove_comments Enable to remove comments
*
* @return array Header items
*/
public static function explode_header_string($separator, $str, $remove_comments = false)
{
$length = strlen($str);
$result = [];
$quoted = false;
$comment = 0;
$out = '';
for ($i=0; $i<$length; $i++) {
// we're inside a quoted string
if ($quoted) {
if ($str[$i] == '"') {
$quoted = false;
}
else if ($str[$i] == "\\") {
if ($comment <= 0) {
$out .= "\\";
}
$i++;
}
}
// we are inside a comment string
else if ($comment > 0) {
if ($str[$i] == ')') {
$comment--;
}
else if ($str[$i] == '(') {
$comment++;
}
else if ($str[$i] == "\\") {
$i++;
}
continue;
}
// separator, add to result array
else if (strpos($separator, $str[$i]) !== false) {
if ($out) {
$result[] = $out;
}
$out = '';
continue;
}
// start of quoted string
else if ($str[$i] == '"') {
$quoted = true;
}
// start of comment
else if ($remove_comments && $str[$i] == '(') {
$comment++;
}
if ($comment <= 0) {
$out .= $str[$i];
}
}
if ($out && $comment <= 0) {
$result[] = $out;
}
return $result;
}
/**
* Interpret a format=flowed message body according to RFC 2646
*
* @param string $text Raw body formatted as flowed text
* @param string $mark Mark each flowed line with specified character
* @param bool $delsp Remove the trailing space of each flowed line
*
* @return string Interpreted text with unwrapped lines and stuffed space removed
*/
public static function unfold_flowed($text, $mark = null, $delsp = false)
{
$text = preg_split('/\r?\n/', $text);
$last = -1;
$q_level = 0;
$marks = [];
foreach ($text as $idx => $line) {
if ($q = strspn($line, '>')) {
// remove quote chars
$line = substr($line, $q);
// remove (optional) space-staffing
if (isset($line[0]) && $line[0] === ' ') {
$line = substr($line, 1);
}
// The same paragraph (We join current line with the previous one) when:
// - the same level of quoting
// - previous line was flowed
// - previous line contains more than only one single space (and quote char(s))
if ($q == $q_level
&& isset($text[$last]) && $text[$last][strlen($text[$last])-1] == ' '
&& !preg_match('/^>+ {0,1}$/', $text[$last])
) {
if ($delsp) {
$text[$last] = substr($text[$last], 0, -1);
}
$text[$last] .= $line;
unset($text[$idx]);
if ($mark) {
$marks[$last] = true;
}
}
else {
$last = $idx;
}
}
else {
if ($line == '-- ') {
$last = $idx;
}
else {
// remove space-stuffing
if (isset($line[0]) && $line[0] === ' ') {
$line = substr($line, 1);
}
$last_len = isset($text[$last]) ? strlen($text[$last]) : 0;
if (
$last_len && $line && !$q_level && $text[$last] != '-- '
&& isset($text[$last][$last_len-1]) && $text[$last][$last_len-1] == ' '
) {
if ($delsp) {
$text[$last] = substr($text[$last], 0, -1);
}
$text[$last] .= $line;
unset($text[$idx]);
if ($mark) {
$marks[$last] = true;
}
}
else {
$text[$idx] = $line;
$last = $idx;
}
}
}
$q_level = $q;
}
if (!empty($marks)) {
foreach (array_keys($marks) as $mk) {
$text[$mk] = $mark . $text[$mk];
}
}
return implode("\r\n", $text);
}
/**
* Wrap the given text to comply with RFC 2646
*
* @param string $text Text to wrap
* @param int $length Length
* @param string $charset Character encoding of $text
*
* @return string Wrapped text
*/
public static function format_flowed($text, $length = 72, $charset = null)
{
$text = preg_split('/\r?\n/', $text);
foreach ($text as $idx => $line) {
if ($line != '-- ') {
if ($level = strspn($line, '>')) {
// remove quote chars
$line = substr($line, $level);
// remove (optional) space-staffing and spaces before the line end
$line = rtrim($line, ' ');
if (isset($line[0]) && $line[0] === ' ') {
$line = substr($line, 1);
}
$prefix = str_repeat('>', $level) . ' ';
$line = $prefix . self::wordwrap($line, $length - $level - 2, " \r\n$prefix", false, $charset);
}
else if ($line) {
$line = self::wordwrap(rtrim($line), $length - 2, " \r\n", false, $charset);
// space-stuffing
$line = preg_replace('/(^|\r\n)(From| |>)/', '\\1 \\2', $line);
}
$text[$idx] = $line;
}
}
return implode("\r\n", $text);
}
/**
* Improved wordwrap function with multibyte support.
* The code is based on Zend_Text_MultiByte::wordWrap().
*
* @param string $string Text to wrap
* @param int $width Line width
* @param string $break Line separator
* @param bool $cut Enable to cut word
* @param string $charset Charset of $string
* @param bool $wrap_quoted When enabled quoted lines will not be wrapped
*
* @return string Text
*/
public static function wordwrap($string, $width = 75, $break = "\n", $cut = false, $charset = null, $wrap_quoted = true)
{
// Note: Never try to use iconv instead of mbstring functions here
// Iconv's substr/strlen are 100x slower (#1489113)
if ($charset && $charset != RCUBE_CHARSET) {
$charset = rcube_charset::parse_charset($charset);
mb_internal_encoding($charset);
}
// Convert \r\n to \n, this is our line-separator
$string = str_replace("\r\n", "\n", $string);
$separator = "\n"; // must be 1 character length
$result = [];
while (($stringLength = mb_strlen($string)) > 0) {
$breakPos = mb_strpos($string, $separator, 0);
// quoted line (do not wrap)
if ($wrap_quoted && $string[0] == '>') {
if ($breakPos === $stringLength - 1 || $breakPos === false) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
// next line found and current line is shorter than the limit
else if ($breakPos !== false && $breakPos < $width) {
if ($breakPos === $stringLength - 1) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
$subString = mb_substr($string, 0, $width);
// last line
if ($breakPos === false && $subString === $string) {
$cutLength = null;
}
else {
$nextChar = mb_substr($string, $width, 1);
if ($nextChar === ' ' || $nextChar === $separator) {
$afterNextChar = mb_substr($string, $width + 1, 1);
// Note: mb_substr() does never return False
if ($afterNextChar === false || $afterNextChar === '') {
$subString .= $nextChar;
}
$cutLength = mb_strlen($subString) + 1;
}
else {
$spacePos = mb_strrpos($subString, ' ', 0);
if ($spacePos !== false) {
$subString = mb_substr($subString, 0, $spacePos);
$cutLength = $spacePos + 1;
}
else if ($cut === false) {
$spacePos = mb_strpos($string, ' ', 0);
if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
$subString = mb_substr($string, 0, $spacePos);
$cutLength = $spacePos + 1;
}
else if ($breakPos === false) {
$subString = $string;
$cutLength = null;
}
else {
$subString = mb_substr($string, 0, $breakPos);
$cutLength = $breakPos + 1;
}
}
else {
$cutLength = $width;
}
}
}
}
$result[] = $subString;
if ($cutLength !== null) {
$string = mb_substr($string, $cutLength, ($stringLength - $cutLength));
}
else {
break;
}
}
if ($charset && $charset != RCUBE_CHARSET) {
mb_internal_encoding(RCUBE_CHARSET);
}
return implode($break, $result);
}
/**
* A method to guess the mime_type of an attachment.
*
* @param string $path Path to the file or file contents
* @param string $name File name (with suffix)
* @param string $failover Mime type supplied for failover
* @param bool $is_stream Set to True if $path contains file contents
* @param bool $skip_suffix Set to True if the config/mimetypes.php map should be ignored
*
* @return string
* @author Till Klampaeckel <till@php.net>
* @see http://de2.php.net/manual/en/ref.fileinfo.php
* @see http://de2.php.net/mime_content_type
*/
public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
{
$mime_type = null;
$config = rcube::get_instance()->config;
// Detect mimetype using filename extension
if (!$skip_suffix) {
$mime_type = self::file_ext_type($name);
}
// try fileinfo extension if available
if (!$mime_type && function_exists('finfo_open')) {
$mime_magic = $config->get('mime_magic');
// null as a 2nd argument should be the same as no argument
// this however is not true on all systems/versions
if ($mime_magic) {
$finfo = finfo_open(FILEINFO_MIME, $mime_magic);
}
else {
$finfo = finfo_open(FILEINFO_MIME);
}
if ($finfo) {
$func = $is_stream ? 'finfo_buffer' : 'finfo_file';
$mime_type = $func($finfo, $path, FILEINFO_MIME_TYPE);
finfo_close($finfo);
}
}
// try PHP's mime_content_type
if (!$mime_type && !$is_stream && function_exists('mime_content_type')) {
$mime_type = @mime_content_type($path);
}
// fall back to user-submitted string
if (!$mime_type) {
$mime_type = $failover;
}
return $mime_type;
}
/**
* File type detection based on file name only.
*
* @param string $filename Path to the file or file contents
*
* @return string|null Mimetype label
*/
public static function file_ext_type($filename)
{
static $mime_ext = [];
if (empty($mime_ext)) {
foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
$mime_ext = array_merge($mime_ext, (array) @include($fpath));
}
}
// use file name suffix with hard-coded mime-type map
if (!empty($mime_ext) && $filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if ($ext && !empty($mime_ext[$ext])) {
return $mime_ext[$ext];
}
}
}
/**
* Get mimetype => file extension mapping
*
* @param string $mimetype Mime-Type to get extensions for
*
* @return array List of extensions matching the given mimetype or a hash array
* with ext -> mimetype mappings if $mimetype is not given
*/
public static function get_mime_extensions($mimetype = null)
{
static $mime_types, $mime_extensions;
// return cached data
if (is_array($mime_types)) {
return $mimetype ? (isset($mime_types[$mimetype]) ? $mime_types[$mimetype] : []) : $mime_extensions;
}
// load mapping file
$file_paths = [];
if ($mime_types = rcube::get_instance()->config->get('mime_types')) {
$file_paths[] = $mime_types;
}
// try common locations
if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
$file_paths[] = 'C:/xampp/apache/conf/mime.types';
}
else {
$file_paths[] = '/etc/mime.types';
$file_paths[] = '/etc/httpd/mime.types';
$file_paths[] = '/etc/httpd2/mime.types';
$file_paths[] = '/etc/apache/mime.types';
$file_paths[] = '/etc/apache2/mime.types';
$file_paths[] = '/etc/nginx/mime.types';
$file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
$file_paths[] = '/usr/local/etc/apache/conf/mime.types';
$file_paths[] = '/usr/local/etc/apache24/mime.types';
}
$mime_types = [];
$mime_extensions = [];
$lines = [];
$regex = "/([\w\+\-\.\/]+)\s+([\w\s]+)/i";
foreach ($file_paths as $fp) {
if (@is_readable($fp)) {
$lines = file($fp, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
break;
}
}
foreach ($lines as $line) {
// skip comments or mime types w/o any extensions
if ($line[0] == '#' || !preg_match($regex, $line, $matches)) {
continue;
}
$mime = $matches[1];
foreach (explode(' ', $matches[2]) as $ext) {
$ext = trim($ext);
$mime_types[$mime][] = $ext;
$mime_extensions[$ext] = $mime;
}
}
// fallback to some well-known types most important for daily emails
if (empty($mime_types)) {
foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
$mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
}
foreach ($mime_extensions as $ext => $mime) {
$mime_types[$mime][] = $ext;
}
}
// Add some known aliases that aren't included by some mime.types (#1488891)
// the order is important here so standard extensions have higher prio
$aliases = [
'image/gif' => ['gif'],
'image/png' => ['png'],
'image/x-png' => ['png'],
'image/jpeg' => ['jpg', 'jpeg', 'jpe'],
'image/jpg' => ['jpg', 'jpeg', 'jpe'],
'image/pjpeg' => ['jpg', 'jpeg', 'jpe'],
'image/tiff' => ['tif'],
'image/bmp' => ['bmp'],
'image/x-ms-bmp' => ['bmp'],
'message/rfc822' => ['eml'],
'text/x-mail' => ['eml'],
];
foreach ($aliases as $mime => $exts) {
if (isset($mime_types[$mime])) {
$mime_types[$mime] = array_unique(array_merge((array) $mime_types[$mime], $exts));
}
else {
$mime_types[$mime] = $exts;
}
foreach ($exts as $ext) {
if (!isset($mime_extensions[$ext])) {
$mime_extensions[$ext] = $mime;
}
}
}
if ($mimetype) {
return !empty($mime_types[$mimetype]) ? $mime_types[$mimetype] : [];
}
return $mime_extensions;
}
/**
* Detect image type of the given binary data by checking magic numbers.
*
* @param string $data Binary file content
*
* @return string Detected mime-type or jpeg as fallback
*/
public static function image_content_type($data)
{
$type = 'jpeg';
if (preg_match('/^\x89\x50\x4E\x47/', $data)) $type = 'png';
else if (preg_match('/^\x47\x49\x46\x38/', $data)) $type = 'gif';
else if (preg_match('/^\x00\x00\x01\x00/', $data)) $type = 'ico';
// else if (preg_match('/^\xFF\xD8\xFF\xE0/', $data)) $type = 'jpeg';
return 'image/' . $type;
}
/**
* Try to fix invalid email addresses
*/
public static function fix_email($email)
{
$parts = rcube_utils::explode_quoted_string('@', $email);
foreach ($parts as $idx => $part) {
// remove redundant quoting (#1490040)
if (isset($part[0]) && $part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
$parts[$idx] = $m[1];
}
}
return implode('@', $parts);
}
/**
* Fix mimetype name.
*
* @param string $type Mimetype
*
* @return string Mimetype
*/
public static function fix_mimetype($type)
{
$type = strtolower(trim($type));
$aliases = [
'image/x-ms-bmp' => 'image/bmp', // #4771
'pdf' => 'application/pdf', // #6816
];
if (!empty($aliases[$type])) {
return $aliases[$type];
}
// Some versions of Outlook create garbage Content-Type:
// application/pdf.A520491B_3BF7_494D_8855_7FAC2C6C0608
if (preg_match('/^application\/pdf.+/', $type)) {
return 'application/pdf';
}
// treat image/pjpeg (image/pjpg, image/jpg) as image/jpeg (#4196)
if (preg_match('/^image\/p?jpe?g$/', $type)) {
return 'image/jpeg';
}
return $type;
}
}
@@ -0,0 +1,446 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| 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: |
| SORT/SEARCH/ESEARCH response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for accessing IMAP's SORT/SEARCH/ESEARCH result
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_index
{
public $incomplete = false;
protected $raw_data;
protected $mailbox;
protected $meta = [];
protected $params = [];
protected $order = 'ASC';
const SEPARATOR_ELEMENT = ' ';
/**
* Object constructor.
*/
public function __construct($mailbox = null, $data = null, $order = null)
{
$this->mailbox = $mailbox;
$this->order = $order == 'DESC' ? 'DESC' : 'ASC';
$this->init($data);
}
/**
* Initializes object with SORT command response
*
* @param string $data IMAP response string
*/
public function init($data = null)
{
$this->meta = [];
$data = explode('*', (string)$data);
// ...skip unilateral untagged server responses
for ($i=0, $len=count($data); $i<$len; $i++) {
$data_item = &$data[$i];
if (preg_match('/^ SORT/i', $data_item)) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data_item = substr($data_item, 5);
break;
}
else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data_item = substr($data_item, strlen($m[0]));
if (strtoupper($m[1]) == 'ESEARCH') {
$data_item = trim($data_item);
// remove MODSEQ response
if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) {
$data_item = substr($data_item, 0, -strlen($m[0]));
$this->params['MODSEQ'] = $m[1];
}
// remove TAG response part
if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) {
$data_item = substr($data_item, strlen($m[0]));
}
// remove UID
$data_item = preg_replace('/^UID\s*/i', '', $data_item);
// ESEARCH parameters
while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) {
$param = strtoupper($m[1]);
$value = $m[2];
$this->params[$param] = $value;
$data_item = substr($data_item, strlen($m[0]));
if (in_array($param, ['COUNT', 'MIN', 'MAX'])) {
$this->meta[strtolower($param)] = (int) $value;
}
}
// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ?
// @TODO: work with compressed result?!
if (isset($this->params['ALL'])) {
$data_item = implode(self::SEPARATOR_ELEMENT,
rcube_imap_generic::uncompressMessageSet($this->params['ALL']));
}
}
break;
}
unset($data[$i]);
}
$data = array_filter($data);
if (empty($data)) {
return;
}
$data = array_shift($data);
$data = trim($data);
$data = preg_replace('/[\r\n]/', '', $data);
$data = preg_replace('/\s+/', ' ', $data);
$this->raw_data = $data;
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return $this->raw_data === null;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->raw_data)
&& empty($this->meta['max']) && empty($this->meta['min']) && empty($this->meta['count']);
}
/**
* Returns number of elements in the result
*
* @return int Number of elements
*/
public function count()
{
if (isset($this->meta['count'])) {
return $this->meta['count'];
}
if (empty($this->raw_data)) {
$this->meta['count'] = 0;
$this->meta['length'] = 0;
}
else {
$this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
}
return $this->meta['count'];
}
/**
* Returns number of elements in the result.
* Alias for count() for compatibility with rcube_result_thread
*
* @return int Number of elements
*/
public function count_messages()
{
return $this->count();
}
/**
* Returns maximal message identifier in the result
*
* @return int|null Maximal message identifier
*/
public function max()
{
if ($this->is_empty()) {
return null;
}
if (!isset($this->meta['max'])) {
$this->meta['max'] = null;
$all = $this->get();
if (!empty($all)) {
$this->meta['max'] = (int) max($all);
}
}
return $this->meta['max'];
}
/**
* Returns minimal message identifier in the result
*
* @return int|null Minimal message identifier
*/
public function min()
{
if ($this->is_empty()) {
return null;
}
if (!isset($this->meta['min'])) {
$this->meta['min'] = null;
$all = $this->get();
if (!empty($all)) {
$this->meta['min'] = (int) min($all);
}
}
return $this->meta['min'];
}
/**
* Slices data set.
*
* @param int $offset Offset (as for PHP's array_slice())
* @param int $length Number of elements (as for PHP's array_slice())
*/
public function slice($offset, $length)
{
$data = $this->get();
$data = array_slice($data, $offset, $length);
$this->meta = [];
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Filters data set. Removes elements not listed in $ids list.
*
* @param array $ids List of IDs to remove.
*/
public function filter($ids = [])
{
$data = $this->get();
$data = array_intersect($data, $ids);
$this->meta = [];
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
if (empty($this->raw_data)) {
return;
}
$data = $this->get();
$data = array_reverse($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
$this->meta['pos'] = [];
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
*
* @return mixed False if message ID doesn't exist, True if exists or
* index of the element if $get_index=true
*/
public function exists($msgid, $get_index = false)
{
if (empty($this->raw_data)) {
return false;
}
$msgid = (int) $msgid;
$begin = implode('|', ['^', preg_quote(self::SEPARATOR_ELEMENT, '/')]);
$end = implode('|', ['$', preg_quote(self::SEPARATOR_ELEMENT, '/')]);
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
$get_index ? PREG_OFFSET_CAPTURE : 0)
) {
if ($get_index) {
$idx = 0;
if (!empty($m[0][1])) {
$idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
}
// cache position of this element, so we can use it in get_element()
$this->meta['pos'][$idx] = (int)$m[0][1];
return $idx;
}
return true;
}
return false;
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get()
{
if (empty($this->raw_data)) {
return [];
}
return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
}
/**
* Return all messages in the result.
*
* @return array List of message IDs
*/
public function get_compressed()
{
if (empty($this->raw_data)) {
return '';
}
return rcube_imap_generic::compressMessageSet($this->get());
}
/**
* Return result element at specified index
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int|null Element value
*/
public function get_element($index)
{
if (empty($this->raw_data)) {
return null;
}
$count = $this->count();
// first element
if ($index === 0 || $index === '0' || $index === 'FIRST') {
$pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
if ($pos === false) {
$result = (int) $this->raw_data;
}
else {
$result = (int) substr($this->raw_data, 0, $pos);
}
return $result;
}
// last element
if ($index === 'LAST' || $index == $count-1) {
$pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
if ($pos === false) {
$result = (int) $this->raw_data;
}
else {
$result = (int) substr($this->raw_data, $pos);
}
return $result;
}
// do we know the position of the element or the neighbour of it?
if (!empty($this->meta['pos'])) {
if (isset($this->meta['pos'][$index])) {
$pos = $this->meta['pos'][$index];
}
else if (isset($this->meta['pos'][$index-1])) {
$pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
$this->meta['pos'][$index-1] + 1);
}
else if (isset($this->meta['pos'][$index+1])) {
$pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
$this->meta['pos'][$index+1] - $this->length() - 1);
}
if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, 0, $pos)) {
return (int) $m[1];
}
}
// Finally use less effective method
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
return (int) $data[$index];
}
/**
* Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
* or internal data e.g. MAILBOX, ORDER
*
* @param ?string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param = null)
{
$params = $this->params;
$params['MAILBOX'] = $this->mailbox;
$params['ORDER'] = $this->order;
if ($param !== null) {
return $params[$param] ?? null;
}
return $params;
}
/**
* Returns length of internal data representation
*
* @return int Data length
*/
protected function length()
{
if (!isset($this->meta['length'])) {
$this->meta['length'] = strlen($this->raw_data);
}
return $this->meta['length'];
}
}
@@ -0,0 +1,699 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| Copyright (C) Kolab Systems AG |
| |
| 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: |
| THREAD response handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for accessing IMAP's THREAD result
*
* @package Framework
* @subpackage Storage
*/
class rcube_result_thread
{
public $incomplete = false;
protected $raw_data;
protected $mailbox;
protected $meta = [];
protected $order = 'ASC';
const SEPARATOR_ELEMENT = ' ';
const SEPARATOR_ITEM = '~';
const SEPARATOR_LEVEL = ':';
/**
* Object constructor.
*/
public function __construct($mailbox = null, $data = null)
{
$this->mailbox = $mailbox;
$this->init($data);
}
/**
* Initializes object with IMAP command response
*
* @param string $data IMAP response string
*/
public function init($data = null)
{
$this->meta = [];
$data = explode('*', (string) $data);
// ...skip unilateral untagged server responses
for ($i = 0, $len = count($data); $i < $len; $i++) {
if (preg_match('/^ THREAD/i', $data[$i])) {
// valid response, initialize raw_data for is_error()
$this->raw_data = '';
$data[$i] = substr($data[$i], 7);
break;
}
unset($data[$i]);
}
if (empty($data)) {
return;
}
$data = array_shift($data);
$data = trim($data);
$data = preg_replace('/[\r\n]/', '', $data);
$data = preg_replace('/\s+/', ' ', $data);
$this->raw_data = empty($data) ? '' : $this->parse_thread($data);
}
/**
* Checks the result from IMAP command
*
* @return bool True if the result is an error, False otherwise
*/
public function is_error()
{
return $this->raw_data === null;
}
/**
* Checks if the result is empty
*
* @return bool True if the result is empty, False otherwise
*/
public function is_empty()
{
return empty($this->raw_data);
}
/**
* Returns number of elements (threads) in the result
*
* @return int Number of elements
*/
public function count()
{
if (isset($this->meta['count'])) {
return $this->meta['count'];
}
if (empty($this->raw_data)) {
$this->meta['count'] = 0;
}
else {
$this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
}
if (!$this->meta['count']) {
$this->meta['messages'] = 0;
}
return $this->meta['count'];
}
/**
* Returns number of all messages in the result
*
* @return int Number of elements
*/
public function count_messages()
{
if (isset($this->meta['messages'])) {
return $this->meta['messages'];
}
if (empty($this->raw_data)) {
$this->meta['messages'] = 0;
}
else {
$this->meta['messages'] = 1
+ substr_count($this->raw_data, self::SEPARATOR_ELEMENT)
+ substr_count($this->raw_data, self::SEPARATOR_ITEM);
}
if ($this->meta['messages'] == 0 || $this->meta['messages'] == 1) {
$this->meta['count'] = $this->meta['messages'];
}
return $this->meta['messages'];
}
/**
* Returns maximum message identifier in the result
*
* @return int|null Maximum message identifier
*/
public function max()
{
if ($this->is_empty()) {
return null;
}
if (!isset($this->meta['max'])) {
$this->meta['max'] = (int) @max($this->get());
}
return $this->meta['max'];
}
/**
* Returns minimum message identifier in the result
*
* @return int|null Minimum message identifier
*/
public function min()
{
if ($this->is_empty()) {
return null;
}
if (!isset($this->meta['min'])) {
$this->meta['min'] = (int) @min($this->get());
}
return $this->meta['min'];
}
/**
* Slices data set.
*
* @param int $offset Offset (as for PHP's array_slice())
* @param int $length Number of elements (as for PHP's array_slice())
*/
public function slice($offset, $length)
{
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
$data = array_slice($data, $offset, $length);
$this->meta = [];
$this->meta['count'] = count($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
}
/**
* Filters data set. Removes threads not listed in $roots list.
*
* @param array $roots List of IDs of thread roots.
*/
public function filter($roots)
{
$datalen = strlen($this->raw_data);
$roots = array_flip($roots);
$result = '';
$start = 0;
$this->meta = ['count' => 0];
while ($start < $datalen
&& (($pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) !== false
|| ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
// extract root message ID
if ($npos = strpos($elem, self::SEPARATOR_ITEM)) {
$root = (int) substr($elem, 0, $npos);
}
else {
$root = $elem;
}
if (isset($roots[$root])) {
$this->meta['count']++;
$result .= self::SEPARATOR_ELEMENT . $elem;
}
}
$this->raw_data = ltrim($result, self::SEPARATOR_ELEMENT);
}
/**
* Reverts order of elements in the result
*/
public function revert()
{
$this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
if (empty($this->raw_data)) {
return;
}
$data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
$data = array_reverse($data);
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
$this->meta['pos'] = [];
}
/**
* Check if the given message ID exists in the object
*
* @param int $msgid Message ID
* @param bool $get_index When enabled element's index will be returned.
* Elements are indexed starting with 0
*
* @return bool True on success, False if message ID doesn't exist
*/
public function exists($msgid, $get_index = false)
{
$msgid = (int) $msgid;
$begin = implode('|', [
'^',
preg_quote(self::SEPARATOR_ELEMENT, '/'),
preg_quote(self::SEPARATOR_LEVEL, '/'),
]);
$end = implode('|', [
'$',
preg_quote(self::SEPARATOR_ELEMENT, '/'),
preg_quote(self::SEPARATOR_ITEM, '/'),
]);
if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
$get_index ? PREG_OFFSET_CAPTURE : 0)
) {
if ($get_index) {
$idx = 0;
if ($m[0][1]) {
$idx = substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]+1)
+ substr_count($this->raw_data, self::SEPARATOR_ITEM, 0, $m[0][1]+1);
}
// cache position of this element, so we can use it in get_element()
$this->meta['pos'][$idx] = (int)$m[0][1];
return $idx;
}
return true;
}
return false;
}
/**
* Return IDs of all messages in the result. Threaded data will be flattened.
*
* @return array List of message identifiers
*/
public function get()
{
if (empty($this->raw_data)) {
return [];
}
$regexp = '/(' . preg_quote(self::SEPARATOR_ELEMENT, '/')
. '|' . preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/')
.')/';
return preg_split($regexp, $this->raw_data);
}
/**
* Return all messages in the result.
*
* @return array List of message identifiers
*/
public function get_compressed()
{
if (empty($this->raw_data)) {
return '';
}
return rcube_imap_generic::compressMessageSet($this->get());
}
/**
* Return result element at specified index (all messages, not roots)
*
* @param int|string $index Element's index or "FIRST" or "LAST"
*
* @return int Element value
*/
public function get_element($index)
{
$count = $this->count();
if (!$count) {
return null;
}
// first element
if ($index === 0 || $index === '0' || $index === 'FIRST') {
preg_match('/^([0-9]+)/', $this->raw_data, $m);
$result = (int) $m[1];
return $result;
}
// last element
if ($index === 'LAST' || $index == $count-1) {
preg_match('/([0-9]+)$/', $this->raw_data, $m);
$result = (int) $m[1];
return $result;
}
// do we know the position of the element or the neighbour of it?
if (!empty($this->meta['pos'])) {
$element = preg_quote(self::SEPARATOR_ELEMENT, '/');
$item = preg_quote(self::SEPARATOR_ITEM, '/') . '[0-9]+' . preg_quote(self::SEPARATOR_LEVEL, '/') .'?';
$regexp = '(' . $element . '|' . $item . ')';
if (isset($this->meta['pos'][$index])) {
if (preg_match('/([0-9]+)/', $this->raw_data, $m, null, $this->meta['pos'][$index])) {
$result = $m[1];
}
}
else if (isset($this->meta['pos'][$index-1])) {
// get chunk of data after previous element
$data = substr($this->raw_data, $this->meta['pos'][$index-1]+1, 50);
$data = preg_replace('/^[0-9]+/', '', $data); // remove UID at $index position
$data = preg_replace("/^$regexp/", '', $data); // remove separator
if (preg_match('/^([0-9]+)/', $data, $m)) {
$result = $m[1];
}
}
else if (isset($this->meta['pos'][$index+1])) {
// get chunk of data before next element
$pos = max(0, $this->meta['pos'][$index+1] - 50);
$len = min(50, $this->meta['pos'][$index+1]);
$data = substr($this->raw_data, $pos, $len);
$data = preg_replace("/$regexp\$/", '', $data); // remove separator
if (preg_match('/([0-9]+)$/', $data, $m)) {
$result = $m[1];
}
}
if (isset($result)) {
return (int) $result;
}
}
// Finally use less effective method
$data = $this->get();
return $data[$index] ?? null;
}
/**
* Returns response parameters e.g. MAILBOX, ORDER
*
* @param string $param Parameter name
*
* @return array|string Response parameters or parameter value
*/
public function get_parameters($param=null)
{
$params = [
'MAILBOX' => $this->mailbox,
'ORDER' => $this->order,
];
if ($param !== null) {
return $params[$param];
}
return $params;
}
/**
* THREAD=REFS sorting implementation (based on provided index)
*
* @param rcube_result_index $index Sorted message identifiers
*/
public function sort($index)
{
$this->order = $index->get_parameters('ORDER');
if (empty($this->raw_data)) {
return;
}
// when sorting search result it's good to make the index smaller
if ($index->count() != $this->count_messages()) {
$index->filter($this->get());
}
$result = array_fill_keys($index->get(), null);
$datalen = strlen($this->raw_data);
$start = 0;
// Here we're parsing raw_data twice, we want only one big array
// in memory at a time
// Assign roots
while (
($start < $datalen && ($pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$items = explode(self::SEPARATOR_ITEM, $elem);
$root = (int) array_shift($items);
if ($root) {
$result[$root] = $root;
foreach ($items as $item) {
list($lv, $id) = explode(self::SEPARATOR_LEVEL, $item);
$result[$id] = $root;
}
}
}
// get only unique roots
$result = array_filter($result); // make sure there are no nulls
$result = array_unique($result);
// Re-sort raw data
$result = array_fill_keys($result, null);
$start = 0;
while (
($start < $datalen && ($pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)))
|| ($start < $datalen && ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$start = $pos + 1;
$npos = strpos($elem, self::SEPARATOR_ITEM);
$root = (int) ($npos ? substr($elem, 0, $npos) : $elem);
$result[$root] = $elem;
}
$this->raw_data = implode(self::SEPARATOR_ELEMENT, $result);
}
/**
* Returns data as tree
*
* @return array Data tree
*/
public function get_tree()
{
$datalen = strlen($this->raw_data);
$result = [];
$start = 0;
while ($start < $datalen
&& (($pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT, $start)) !== false
|| ($pos = $datalen))
) {
$len = $pos - $start;
$elem = substr($this->raw_data, $start, $len);
$items = explode(self::SEPARATOR_ITEM, $elem);
$result[array_shift($items)] = $this->build_thread($items);
$start = $pos + 1;
}
return $result;
}
/**
* Returns thread depth and children data
*
* @return array Thread data
*/
public function get_thread_data()
{
$data = $this->get_tree();
$depth = [];
$children = [];
$this->build_thread_data($data, $depth, $children);
return [$depth, $children];
}
/**
* Creates 'depth' and 'children' arrays from stored thread 'tree' data.
*/
protected function build_thread_data($data, &$depth, &$children, $level = 0)
{
foreach ((array)$data as $key => $val) {
$empty = empty($val) || !is_array($val);
$children[$key] = !$empty;
$depth[$key] = $level;
if (!$empty) {
$this->build_thread_data($val, $depth, $children, $level + 1);
}
}
}
/**
* Converts part of the raw thread into an array
*/
protected function build_thread($items, $level = 1, &$pos = 0)
{
$result = [];
for ($len=count($items); $pos < $len; $pos++) {
list($lv, $id) = explode(self::SEPARATOR_LEVEL, $items[$pos]);
if ($level == $lv) {
$pos++;
$result[$id] = $this->build_thread($items, $level+1, $pos);
}
else {
$pos--;
break;
}
}
return $result;
}
/**
* IMAP THREAD response parser
*/
protected function parse_thread($str, $begin = 0, $end = 0, $depth = 0)
{
// Don't be tempted to change $str to pass by reference to speed this up - it will slow it down by about
// 7 times instead :-) See comments on http://uk2.php.net/references and this article:
// http://derickrethans.nl/files/phparch-php-variables-article.pdf
$node = '';
if (!$end) {
$end = strlen($str);
}
// Let's try to store data in max. compacted structure as a string,
// arrays handling is much more expensive
// For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))((11)(12))
// -- 2
// -- 3
// \-- 6
// |-- 4
// | \-- 23
// |
// \-- 44
// \-- 7
// \-- 96
// -- 11
// \-- 12
//
// The output will be: 2 3~1:6~2:4~3:23~2:44~3:7~4:96 11~1:12
// Note: The "11" thread has no root, we use the first message as root
if ($str[$begin] != '(') {
// find next bracket
$stop = $begin + strcspn($str, '()', $begin, $end - $begin);
$messages = explode(' ', trim(substr($str, $begin, $stop - $begin)));
if (empty($messages)) {
return $node;
}
foreach ($messages as $msg) {
if ($msg) {
$node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
if (isset($this->meta['messages'])) {
$this->meta['messages']++;
}
else {
$this->meta['messages'] = 1;
}
$depth++;
}
}
if ($stop < $end) {
$node .= $this->parse_thread($str, $stop, $end, $depth);
}
}
else {
$off = $begin;
while ($off < $end) {
$start = $off;
$off++;
$n = 1;
while ($n > 0) {
$p = strpos($str, ')', $off);
if ($p === false) {
// error, wrong structure, mismatched brackets in IMAP THREAD response
// @TODO: write error to the log or maybe set $this->raw_data = null;
return $node;
}
$p1 = strpos($str, '(', $off);
if ($p1 !== false && $p1 < $p) {
$off = $p1 + 1;
$n++;
}
else {
$off = $p + 1;
$n--;
}
}
// Handle threads with missing parent by using first message as root
if (substr_compare($str, '((', $start, 2) === 0) {
// Extract the current thread, e.g. "((1)(2))"
$thread = substr($str, $start, $off - $start);
// Length of the first token, e.g. "(1)"
$len = strspn($thread, '(0123456789', 1) + 1;
// Extract the token and modify it to look like a thread root
$token = substr($thread, 1, $len);
// Warning: The order is important
$token = str_replace('(', '', $token);
$token = str_replace(' ', ' (', $token);
$token = str_replace(')', ' ', $token);
$thread = substr_replace($thread, $token, 1, $len);
// Parse the thread
$thread = $this->parse_thread($thread, 0, 0, $depth);
}
else {
$thread = $this->parse_thread($str, $start + 1, $off - 1, $depth);
}
if ($thread) {
if (!$depth) {
if ($node) {
$node .= self::SEPARATOR_ELEMENT;
}
}
$node .= $thread;
}
}
}
return $node;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
Description of Roundcube Framework 1.6.6 library import into Moodle
We now use the client part of Roundcube Framework as a library in Moodle.
This library is used to receive emails from Moodle.
This library is not used to send emails.
For more information on this version of Roundcube Framework, check out https://github.com/roundcube/roundcubemail/releases/tag/1.6.6
To upgrade this library:
1. Download the latest release of Roundcube Framework (roundcube-framework-xxx-tar.gz) in https://github.com/roundcube/roundcubemail/releases.
2. Extract the contents of the release archive to a temp folder.
3. Copy the following files from the temp folder to the Moodle folder admin/tool/messageinbound/roundcube:
- rcube_charset.php
- rcube_imap_generic.php
- rcube_message_header.php
- rcube_mime.php
- rcube_result_index.php
- rcube_result_thread.php
- rcube_utils.php
4. Find and replace all array_first() calls with array_shift() in the following files:
- rcube_imap_generic.php
- rcube_result_index.php
- rcube_result_thread.php
5. Update admin/tool/messageinbound/thirdpartylibs.xml.
+104
View File
@@ -0,0 +1,104 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Inbound Message Settings.
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
if ($hassiteconfig) {
// Create a settings page for all of the mail server settings.
$settings = new admin_settingpage('messageinbound_mailsettings',
new lang_string('incomingmailconfiguration', 'tool_messageinbound'));
$settings->add(new admin_setting_heading('messageinbound_generalconfiguration',
new lang_string('messageinboundgeneralconfiguration', 'tool_messageinbound'),
new lang_string('messageinboundgeneralconfiguration_desc', 'tool_messageinbound'), ''));
$settings->add(new admin_setting_configcheckbox('messageinbound_enabled',
new lang_string('messageinboundenabled', 'tool_messageinbound'),
new lang_string('messageinboundenabled_desc', 'tool_messageinbound'), 0));
// These settings are used when generating a Inbound Message address.
$settings->add(new admin_setting_heading('messageinbound_mailboxconfiguration',
new lang_string('mailboxconfiguration', 'tool_messageinbound'),
new lang_string('messageinboundmailboxconfiguration_desc', 'tool_messageinbound'), ''));
$settings->add(new admin_setting_configtext_with_maxlength('messageinbound_mailbox',
new lang_string('mailbox', 'tool_messageinbound'),
null, '', PARAM_RAW, null, 15));
$settings->add(new admin_setting_configtext('messageinbound_domain',
new lang_string('domain', 'tool_messageinbound'),
null, '', PARAM_RAW));
// These settings are used when checking the incoming mailbox for mail.
$settings->add(new admin_setting_heading('messageinbound_serversettings',
new lang_string('incomingmailserversettings', 'tool_messageinbound'),
new lang_string('incomingmailserversettings_desc', 'tool_messageinbound'), ''));
$settings->add(new admin_setting_configtext('messageinbound_host',
new lang_string('messageinboundhost', 'tool_messageinbound'),
new lang_string('configmessageinboundhost', 'tool_messageinbound'), '', PARAM_RAW));
$options = array(
'' => get_string('noencryption', 'tool_messageinbound'),
'ssl' => get_string('ssl', 'tool_messageinbound'),
'sslv2' => get_string('sslv2', 'tool_messageinbound'),
'sslv3' => get_string('sslv3', 'tool_messageinbound'),
'tls' => get_string('tls', 'tool_messageinbound'),
'tlsv1' => get_string('tlsv1', 'tool_messageinbound'),
);
$settings->add(new admin_setting_configselect('messageinbound_hostssl',
new lang_string('messageinboundhostssl', 'tool_messageinbound'),
new lang_string('messageinboundhostssl_desc', 'tool_messageinbound'), 'ssl', $options));
// Get all the issuers.
$issuers = \core\oauth2\api::get_all_issuers();
$oauth2services = [
'' => new lang_string('none', 'admin'),
];
foreach ($issuers as $issuer) {
// Get the enabled issuer only.
if ($issuer->get('enabled')) {
$oauth2services[$issuer->get('id')] = s($issuer->get('name'));
}
}
if (count($oauth2services) > 1) {
$settings->add(new admin_setting_configselect('messageinbound_hostoauth',
new lang_string('issuer', 'auth_oauth2'),
get_string('messageinboundhostoauth_help', 'tool_messageinbound'),
'',
$oauth2services
));
}
$settings->add(new admin_setting_configtext('messageinbound_hostuser',
new lang_string('messageinboundhostuser', 'tool_messageinbound'),
new lang_string('messageinboundhostuser_desc', 'tool_messageinbound'), '', PARAM_NOTAGS));
$settings->add(new admin_setting_configpasswordunmask('messageinbound_hostpass',
new lang_string('messageinboundhostpass', 'tool_messageinbound'),
new lang_string('messageinboundhostpass_desc', 'tool_messageinbound'), ''));
// Add the category to the admin tree.
$ADMIN->add('email', $settings);
// Link to the external page for Inbound Message handler configuration.
$ADMIN->add('email', new admin_externalpage('messageinbound_handlers',
new lang_string('message_handlers', 'tool_messageinbound'),
"$CFG->wwwroot/$CFG->admin/tool/messageinbound/index.php"));
}
+11
View File
@@ -0,0 +1,11 @@
#page-admin-tool-messageinbound-index .handler-function {
display: block;
padding: 0 0.5em;
color: #888;
font-size: 0.75em;
}
#page-admin-tool-messageinbound-index .state,
#page-admin-tool-messageinbound-index .edit {
text-align: center;
}
@@ -0,0 +1,35 @@
@tool @tool_messageinbound
Feature: Incoming mail configuration
In order to receive email from external
As a Moodle administrator
I need to set mail configuration
Background:
Given I log in as "admin"
Scenario: Incoming mail server settings without OAuth 2 service setup yet
Given I navigate to "Server > Email > Incoming mail configuration" in site administration
And "OAuth 2 service" "select" should not exist
Scenario: Incoming mail server settings with OAuth 2 service setup
Given I navigate to "Server > OAuth 2 services" in site administration
And I press "Google"
And I should see "Create new service: Google"
And I set the following fields to these values:
| Name | Testing service |
| Client ID | thisistheclientid |
| Client secret | supersecret |
And I press "Save changes"
When I navigate to "Server > Email > Incoming mail configuration" in site administration
Then "OAuth 2 service" "select" should exist
And I should see "Testing service" in the "OAuth 2 service" "select"
@javascript
Scenario: Check character limitations of mailbox name
When I navigate to "Server > Email > Incoming mail configuration" in site administration
And I set the field "Mailbox name" to "frogfrogfrogfrog"
Then I should see "Maximum of 15 characters"
And the "disabled" attribute of "form#adminsettings button[type='submit']" "css_element" should contain "true"
And I set the field "Mailbox name" to "frogfrogfrogfro"
And I should not see "Maximum of 15 characters"
And the "disabled" attribute of "form#adminsettings button[type='submit']" "css_element" should not be set
@@ -0,0 +1,86 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace tool_messageinbound;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\writer;
use tool_messageinbound\privacy\provider;
/**
* Manager testcase class.
*
* @package tool_messageinbound
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class manager_test extends provider_testcase {
public function setUp(): void {
global $CFG;
$this->resetAfterTest();
// Pretend the system is enabled.
$CFG->messageinbound_enabled = true;
$CFG->messageinbound_mailbox = 'mailbox';
$CFG->messageinbound_domain = 'example.com';
}
public function test_tidy_old_verification_failures(): void {
global $DB;
$now = time();
$stale = $now - DAYSECS - 1; // Make a second older because PHP Unit is too damn fast!!
$this->create_messagelist(['timecreated' => $now]);
$this->create_messagelist(['timecreated' => $now - HOURSECS]);
$this->create_messagelist(['timecreated' => $stale]);
$this->create_messagelist(['timecreated' => $stale - HOURSECS]);
$this->create_messagelist(['timecreated' => $stale - YEARSECS]);
$this->assertEquals(5, $DB->count_records('messageinbound_messagelist', []));
$this->assertEquals(3, $DB->count_records_select('messageinbound_messagelist', 'timecreated < :t', ['t' => $stale + 1]));
$manager = new \tool_messageinbound\manager();
$manager->tidy_old_verification_failures();
$this->assertEquals(2, $DB->count_records('messageinbound_messagelist', []));
$this->assertEquals(0, $DB->count_records_select('messageinbound_messagelist', 'timecreated < :t', ['t' => $stale + 1]));
}
/**
* Create a message to validate.
*
* @param array $params The params.
* @return stdClass
*/
protected function create_messagelist(array $params) {
global $DB, $USER;
$record = (object) array_merge([
'messageid' => 'abc',
'userid' => $USER->id,
'address' => 'text@example.com',
'timecreated' => time(),
], $params);
$record->id = $DB->insert_record('messageinbound_messagelist', $record);
return $record;
}
}
@@ -0,0 +1,301 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Data provider tests.
*
* @package tool_messageinbound
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_messageinbound\privacy;
defined('MOODLE_INTERNAL') || die();
global $CFG;
use core_privacy\tests\provider_testcase;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\approved_userlist;
use core_privacy\local\request\transform;
use core_privacy\local\request\userlist;
use core_privacy\local\request\writer;
use tool_messageinbound\privacy\provider;
/**
* Data provider testcase class.
*
* @package tool_messageinbound
* @category test
* @copyright 2018 Frédéric Massart
* @author Frédéric Massart <fred@branchup.tech>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider_test extends provider_testcase {
public function setUp(): void {
global $CFG;
$this->resetAfterTest();
// Pretend the system is enabled.
$CFG->messageinbound_enabled = true;
$CFG->messageinbound_mailbox = 'mailbox';
$CFG->messageinbound_domain = 'example.com';
}
public function test_get_contexts_for_userid(): void {
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$contexts = provider::get_contexts_for_userid($u1->id)->get_contexts();
$this->assertCount(1, $contexts);
$this->assertEquals($u1ctx->id, $contexts[0]->id);
$contexts = provider::get_contexts_for_userid($u2->id)->get_contexts();
$this->assertCount(1, $contexts);
$this->assertEquals($u2ctx->id, $contexts[0]->id);
}
/**
* Test for provider::test_get_users_in_context().
*/
public function test_get_users_in_context(): void {
$component = 'tool_messageinbound';
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u3 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$u3ctx = \context_user::instance($u3->id);
$addressmanager = new \core\message\inbound\address_manager();
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
$addressmanager->set_data(123);
// Create a user key for user 1.
$addressmanager->generate($u1->id);
// Create a messagelist for user 2.
$this->create_messagelist(['userid' => $u2->id, 'address' => 'u2@example1.com']);
$userlist1 = new userlist($u1ctx, $component);
provider::get_users_in_context($userlist1);
$userlist2 = new userlist($u2ctx, $component);
provider::get_users_in_context($userlist2);
$userlist3 = new userlist($u3ctx, $component);
provider::get_users_in_context($userlist3);
// Ensure user 1 is found from userkey.
$userids = $userlist1->get_userids();
$this->assertCount(1, $userids);
$this->assertEquals($u1->id, $userids[0]);
// Ensure user 2 is found from messagelist.
$userids = $userlist2->get_userids();
$this->assertCount(1, $userids);
$this->assertEquals($u2->id, $userids[0]);
// User 3 has neither, so should not be found.
$userids = $userlist3->get_userids();
$this->assertCount(0, $userids);
}
public function test_delete_data_for_user(): void {
global $DB;
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$addressmanager = new \core\message\inbound\address_manager();
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
$addressmanager->set_data(123);
// Create a user key for both users.
$addressmanager->generate($u1->id);
$addressmanager->generate($u2->id);
// Create a messagelist for both users.
$this->create_messagelist(['userid' => $u1->id]);
$this->create_messagelist(['userid' => $u2->id]);
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
// Passing another user's context does not do anything.
provider::delete_data_for_user(new approved_contextlist($u1, 'tool_messageinbound', [$u2ctx->id]));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
// Deleting user 1.
provider::delete_data_for_user(new approved_contextlist($u1, 'tool_messageinbound', [$u1ctx->id]));
$this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
}
/**
* Test for provider::test_delete_data_for_users().
*/
public function test_delete_data_for_users(): void {
global $DB;
$component = 'tool_messageinbound';
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$addressmanager = new \core\message\inbound\address_manager();
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
$addressmanager->set_data(123);
// Create a user key for both users.
$addressmanager->generate($u1->id);
$addressmanager->generate($u2->id);
// Create a messagelist for both users.
$this->create_messagelist(['userid' => $u1->id]);
$this->create_messagelist(['userid' => $u2->id]);
// Ensure data exists for both users.
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
// Ensure passing another user's ID does not do anything.
$approveduserids = [$u2->id];
$approvedlist = new approved_userlist($u1ctx, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
// Delete u1's data.
$approveduserids = [$u1->id];
$approvedlist = new approved_userlist($u1ctx, $component, $approveduserids);
provider::delete_data_for_users($approvedlist);
// Confirm only u1's data is deleted.
$this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
}
public function test_delete_data_for_all_users_in_context(): void {
global $DB;
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$addressmanager = new \core\message\inbound\address_manager();
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
$addressmanager->set_data(123);
// Create a user key for both users.
$addressmanager->generate($u1->id);
$addressmanager->generate($u2->id);
// Create a messagelist for both users.
$this->create_messagelist(['userid' => $u1->id]);
$this->create_messagelist(['userid' => $u2->id]);
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
// Deleting user 1.
provider::delete_data_for_all_users_in_context($u1ctx);
$this->assertFalse($DB->record_exists('user_private_key', ['userid' => $u1->id, 'script' => 'messageinbound_handler']));
$this->assertTrue($DB->record_exists('user_private_key', ['userid' => $u2->id, 'script' => 'messageinbound_handler']));
$this->assertFalse($DB->record_exists('messageinbound_messagelist', ['userid' => $u1->id]));
$this->assertTrue($DB->record_exists('messageinbound_messagelist', ['userid' => $u2->id]));
}
public function test_export_data_for_user(): void {
$dg = $this->getDataGenerator();
$u1 = $dg->create_user();
$u2 = $dg->create_user();
$u1ctx = \context_user::instance($u1->id);
$u2ctx = \context_user::instance($u2->id);
$addressmanager = new \core\message\inbound\address_manager();
$addressmanager->set_handler('\tool_messageinbound\message\inbound\invalid_recipient_handler');
$addressmanager->set_data(123);
// Create a user key for both users.
$addressmanager->generate($u1->id);
$addressmanager->generate($u2->id);
// Create a messagelist for both users.
$this->create_messagelist(['userid' => $u1->id, 'address' => 'u1@example1.com']);
$this->create_messagelist(['userid' => $u1->id, 'address' => 'u1@example2.com']);
$this->create_messagelist(['userid' => $u2->id, 'address' => 'u2@example1.com']);
// Export for user.
$this->setUser($u1);
provider::export_user_data(new approved_contextlist($u1, 'tool_messageinbound', [$u1ctx->id, $u2ctx->id]));
$data = writer::with_context($u2ctx)->get_data([get_string('messageinbound', 'tool_messageinbound')]);
$this->assertEmpty($data);
$data = writer::with_context($u1ctx)->get_data([get_string('messageinbound', 'tool_messageinbound')]);
$this->assertCount(2, $data->messages_pending_validation);
$this->assertEquals('u1@example1.com', $data->messages_pending_validation[0]['received_at']);
$this->assertEquals('u1@example2.com', $data->messages_pending_validation[1]['received_at']);
$data = writer::with_context($u2ctx)->get_related_data([get_string('messageinbound', 'tool_messageinbound')], 'userkeys');
$this->assertEmpty($data);
$data = writer::with_context($u1ctx)->get_related_data([get_string('messageinbound', 'tool_messageinbound')], 'userkeys');
$this->assertCount(1, $data->keys);
$this->assertEquals('messageinbound_handler', $data->keys[0]->script);
}
/**
* Create a message to validate.
*
* @param array $params The params.
* @return stdClass
*/
protected function create_messagelist(array $params) {
global $DB, $USER;
$record = (object) array_merge([
'messageid' => 'abc',
'userid' => $USER->id,
'address' => 'text@example.com',
'timecreated' => time(),
], $params);
$record->id = $DB->insert_record('messageinbound_messagelist', $record);
return $record;
}
}
@@ -0,0 +1,14 @@
<?xml version="1.0"?>
<libraries>
<library>
<location>roundcube</location>
<name>Roundcube Framework</name>
<license>GPL</license>
<licenseversion>3.0+</licenseversion>
<version>1.6.6</version>
<repository>https://github.com/roundcube/roundcubemail</repository>
<copyrights>
<copyright>The Roundcube Dev Team</copyright>
</copyrights>
</library>
</libraries>
+29
View File
@@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Plugin version info
*
* @package tool_messageinbound
* @copyright 2014 Andrew Nicols
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'tool_messageinbound';