Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F223409
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
324 KB
Referenced Files
None
Subscribers
None
View Options
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
index 3f965024e..ba42e8425 100644
--- a/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
+++ b/plugins/managesieve/lib/Roundcube/rcube_sieve_engine.php
@@ -1,3492 +1,3492 @@
<?php
/**
* Managesieve (Sieve Filters) Engine
*
* Engine part of Managesieve plugin implementing UI and backend access.
*
* Copyright (C) The Roundcube Dev Team
* Copyright (C) Kolab Systems AG
*
* This program 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.
*
* This program 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 this program. If not, see http://www.gnu.org/licenses/.
*/
class rcube_sieve_engine
{
protected $rc;
protected $sieve;
protected $plugin;
protected $errors;
protected $form;
protected $list;
protected $tips = [];
protected $script = [];
protected $exts = [];
protected $active = [];
protected $headers = [];
protected $disabled_actions = [];
protected $addr_headers = [
// Required
"from", "to", "cc", "bcc", "sender", "resent-from", "resent-to",
// Additional (RFC 822 / RFC 2822)
"reply-to", "resent-reply-to", "resent-sender", "resent-cc", "resent-bcc",
// Non-standard (RFC 2076, draft-palme-mailext-headers-08.txt)
"for-approval", "for-handling", "for-comment", "apparently-to", "errors-to",
"delivered-to", "return-receipt-to", "x-admin", "read-receipt-to",
"x-confirm-reading-to", "return-receipt-requested",
"registered-mail-reply-requested-by", "mail-followup-to", "mail-reply-to",
"abuse-reports-to", "x-complaints-to", "x-report-abuse-to",
// Undocumented
"x-beenthere",
];
protected $notify_methods = [
'mailto',
// 'sms',
// 'tel',
];
protected $notify_importance_options = [
3 => 'notifyimportancelow',
2 => 'notifyimportancenormal',
1 => 'notifyimportancehigh'
];
const VERSION = '9.4';
const PROGNAME = 'Roundcube (Managesieve)';
const PORT = 4190;
/**
* Class constructor
*/
function __construct($plugin)
{
$this->rc = rcube::get_instance();
$this->plugin = $plugin;
$this->headers = $this->get_default_headers();
}
/**
* Loads configuration, initializes plugin (including sieve connection)
*/
function start($mode = null)
{
// register UI objects
$this->rc->output->add_handlers([
'filterslist' => [$this, 'filters_list'],
'filtersetslist' => [$this, 'filtersets_list'],
'filterform' => [$this, 'filter_form'],
'filtersetform' => [$this, 'filterset_form'],
'filterseteditraw' => [$this, 'filterset_editraw'],
]);
$this->disabled_actions = (array) $this->rc->config->get('managesieve_disabled_actions');
// connect to managesieve server
$error = $this->connect($_SESSION['username'], $this->rc->decrypt($_SESSION['password']));
$script_name = null;
// load current/active script
if (!$error) {
// Get list of scripts
$list = $this->list_scripts();
// reset current script when entering filters UI (#1489412)
if ($this->rc->action == 'plugin.managesieve') {
$this->rc->session->remove('managesieve_current');
}
if ($mode != 'vacation' && $mode != 'forward') {
if (!empty($_GET['_set']) || !empty($_POST['_set'])) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
}
else if (!empty($_SESSION['managesieve_current'])) {
$script_name = $_SESSION['managesieve_current'];
}
}
$error = $this->load_script($script_name);
}
// finally set script objects
if ($error) {
switch ($error) {
case rcube_sieve::ERROR_CONNECTION:
case rcube_sieve::ERROR_LOGIN:
$this->rc->output->show_message('managesieve.filterconnerror', 'error');
break;
default:
$this->rc->output->show_message('managesieve.filterunknownerror', 'error');
break;
}
// reload interface in case of possible error when specified script wasn't found (#1489412)
if ($script_name !== null && !empty($list) && !in_array($script_name, $list)) {
$this->rc->output->command('reload', 500);
}
// to disable 'Add filter' button set env variable
$this->rc->output->set_env('filterconnerror', true);
$this->script = [];
}
else {
$this->exts = $this->sieve->get_extensions();
$this->init_script();
$this->rc->output->set_env('currentset', $this->sieve->current);
$_SESSION['managesieve_current'] = $this->sieve->current;
}
$this->rc->output->set_env('raw_sieve_editor', $this->rc->config->get('managesieve_raw_editor', true));
$this->rc->output->set_env('managesieve_disabled_actions', $this->disabled_actions);
$this->rc->output->set_env('managesieve_no_set_list', in_array('list_sets', $this->disabled_actions));
return $error;
}
/**
* Connect to configured managesieve server
*
* @param string $username User login
* @param string $password User password
*
* @return int Connection status: 0 on success, >0 on failure
*/
public function connect($username, $password)
{
// Get connection parameters
$host = $this->rc->config->get('managesieve_host', 'localhost');
$port = $this->rc->config->get('managesieve_port');
$tls = $this->rc->config->get('managesieve_usetls', false);
$host = rcube_utils::parse_host($host);
$host = rcube_utils::idn_to_ascii($host);
// remove tls:// prefix, set TLS flag
if (($host = preg_replace('|^tls://|i', '', $host, 1, $cnt)) && $cnt) {
$tls = true;
}
if (empty($port)) {
$port = getservbyname('sieve', 'tcp') ?: self::PORT;
}
$plugin = $this->rc->plugins->exec_hook('managesieve_connect', [
'user' => $username,
'password' => $password,
'host' => $host,
'port' => $port,
'usetls' => $tls,
'auth_type' => $this->rc->config->get('managesieve_auth_type'),
'disabled' => $this->rc->config->get('managesieve_disabled_extensions'),
'debug' => $this->rc->config->get('managesieve_debug', false),
'auth_cid' => $this->rc->config->get('managesieve_auth_cid'),
'auth_pw' => $this->rc->config->get('managesieve_auth_pw'),
'socket_options' => $this->rc->config->get('managesieve_conn_options'),
'gssapi_context' => null,
'gssapi_cn' => null,
]);
// Handle per-host socket options
rcube_utils::parse_socket_options($plugin['socket_options'], $plugin['host']);
// try to connect to managesieve server and to fetch the script
$this->sieve = new rcube_sieve(
$plugin['user'],
$plugin['password'],
$plugin['host'],
$plugin['port'],
$plugin['auth_type'],
$plugin['usetls'],
$plugin['disabled'],
$plugin['debug'],
$plugin['auth_cid'],
$plugin['auth_pw'],
$plugin['socket_options'],
$plugin['gssapi_context'],
$plugin['gssapi_cn']
);
$error = $this->sieve->error();
if ($error) {
rcube::raise_error([
'code' => 403,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Unable to connect to managesieve on $host:$port"
], true, false
);
}
return $error;
}
/**
* Load specified (or active) script
*
* @param string $script_name Optional script name
*
* @return int Connection status: 0 on success, >0 on failure
*/
protected function load_script($script_name = null)
{
// Get list of scripts
$list = $this->list_scripts();
if ($script_name === null || $script_name === '') {
// get (first) active script
if (!empty($this->active)) {
$script_name = $this->active[0];
}
else if ($list) {
$script_name = $list[0];
}
else {
// if script does not exist create one with default content
$this->create_default_script();
}
}
if ($script_name) {
$this->sieve->load($script_name);
}
return $this->sieve->error();
}
/**
* User interface actions handler
*/
function actions()
{
$error = $this->start();
// Handle user requests
if ($action = rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC)) {
$fid = (int) rcube_utils::get_input_value('_fid', rcube_utils::INPUT_POST);
if ($action == 'delete' && !$error) {
if (!in_array('delete_filter', $this->disabled_actions)) {
if (isset($this->script[$fid])) {
$result = false;
if ($this->sieve->script->delete_rule($fid)) {
$result = $this->save_script();
}
if ($result === true) {
$this->rc->output->show_message('managesieve.filterdeleted', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'del', ['id' => $fid]);
}
else {
$this->rc->output->show_message('managesieve.filterdeleteerror', 'error');
}
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'move' && !$error) {
if (isset($this->script[$fid])) {
$to = (int) rcube_utils::get_input_value('_to', rcube_utils::INPUT_POST);
$rule = $this->script[$fid];
// remove rule
unset($this->script[$fid]);
$this->script = array_values($this->script);
// add at target position
if ($to >= count($this->script)) {
$this->script[] = $rule;
}
else {
$script = [];
foreach ($this->script as $idx => $r) {
if ($idx == $to) {
$script[] = $rule;
}
$script[] = $r;
}
$this->script = $script;
}
$this->sieve->script->content = $this->script;
$result = $this->save_script();
if ($result === true) {
$result = $this->list_rules();
$this->rc->output->show_message('managesieve.moved', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'list',
['list' => $result, 'clear' => true, 'set' => $to]);
}
else {
$this->rc->output->show_message('managesieve.moveerror', 'error');
}
}
}
else if ($action == 'act' && !$error) {
if (isset($this->script[$fid])) {
$rule = $this->script[$fid];
$disabled = !empty($rule['disabled']);
$rule['disabled'] = !$disabled;
$result = $this->sieve->script->update_rule($fid, $rule);
if ($result !== false) {
$result = $this->save_script();
}
if ($result === true) {
if ($rule['disabled']) {
$this->rc->output->show_message('managesieve.deactivated', 'confirmation');
}
else {
$this->rc->output->show_message('managesieve.activated', 'confirmation');
}
$this->rc->output->command('managesieve_updatelist', 'update',
['id' => $fid, 'disabled' => $rule['disabled']]);
}
else {
if ($rule['disabled']) {
$this->rc->output->show_message('managesieve.deactivateerror', 'error');
}
else {
$this->rc->output->show_message('managesieve.activateerror', 'error');
}
}
}
}
else if ($action == 'setact' && !$error) {
if (!in_array('enable_disable_set', $this->disabled_actions)) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
$result = $this->activate_script($script_name);
$kep14 = $this->rc->config->get('managesieve_kolab_master');
if ($result === true) {
$this->rc->output->set_env('active_sets', $this->active);
$this->rc->output->show_message('managesieve.setactivated', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'setact',
['name' => $script_name, 'active' => true, 'all' => !$kep14]);
}
else {
$this->rc->output->show_message('managesieve.setactivateerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'deact' && !$error) {
if (!in_array('enable_disable_set', $this->disabled_actions)) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
$result = $this->deactivate_script($script_name);
if ($result === true) {
$this->rc->output->set_env('active_sets', $this->active);
$this->rc->output->show_message('managesieve.setdeactivated', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'setact',
['name' => $script_name, 'active' => false]);
}
else {
$this->rc->output->show_message('managesieve.setdeactivateerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'setdel' && !$error) {
if (!in_array('delete_set', $this->disabled_actions)) {
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST, true);
$result = $this->remove_script($script_name);
if ($result === true) {
$this->rc->output->show_message('managesieve.setdeleted', 'confirmation');
$this->rc->output->command('managesieve_updatelist', 'setdel', ['name' => $script_name]);
$this->rc->session->remove('managesieve_current');
}
else {
$this->rc->output->show_message('managesieve.setdeleteerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
}
}
else if ($action == 'setget') {
if (!in_array('download_set', $this->disabled_actions)) {
$this->rc->request_security_check(rcube_utils::INPUT_GET);
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GPC, true);
$script = $this->sieve->get_script($script_name);
if ($script !== false) {
$this->rc->output->download_headers($script_name . '.txt', ['length' => strlen($script)]);
echo $script;
}
exit;
}
}
else if ($action == 'list') {
$result = $this->list_rules();
$this->rc->output->command('managesieve_updatelist', 'list', ['list' => $result]);
}
else if ($action == 'ruleadd') {
$rid = rcube_utils::get_input_value('_rid', rcube_utils::INPUT_POST);
$id = $this->genid();
$content = $this->rule_div($fid, $id, false, !empty($_SESSION['managesieve-compact-form']));
$this->rc->output->command('managesieve_rulefill', $content, $id, $rid);
}
else if ($action == 'actionadd') {
$aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_POST);
$id = $this->genid();
$content = $this->action_div($fid, $id, false);
$this->rc->output->command('managesieve_actionfill', $content, $id, $aid);
}
else if ($action == 'addresses') {
$aid = rcube_utils::get_input_value('_aid', rcube_utils::INPUT_POST);
$this->rc->output->command('managesieve_vacation_addresses_update', $aid, $this->user_emails());
}
$this->rc->output->send();
}
else if ($this->rc->task == 'mail') {
// Initialize the form
$rules = rcube_utils::get_input_value('r', rcube_utils::INPUT_GET);
if (!empty($rules)) {
$tests = [];
foreach ($rules as $rule) {
list($header, $value) = explode(':', $rule, 2);
$tests[] = [
'type' => 'contains',
'test' => 'header',
'arg1' => $header,
'arg2' => $value,
];
}
$this->form = [
'join' => count($tests) > 1 ? 'allof' : 'anyof',
'name' => '',
'tests' => $tests,
'actions' => [
['type' => 'fileinto'],
['type' => 'stop'],
],
];
}
}
$this->send();
}
function saveraw()
{
// Init plugin and handle managesieve connection
$error = $this->start();
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_POST);
$result = $this->sieve->save_script($script_name, $_POST['rawsetcontent']);
if ($result === false) {
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
$errorLines = $this->sieve->get_error_lines();
if (count($errorLines) > 0) {
$this->rc->output->set_env("sieve_errors", $errorLines);
}
}
else {
$this->rc->output->show_message('managesieve.setupdated', 'confirmation');
$this->rc->output->command('parent.managesieve_updatelist', 'refresh');
}
$this->send();
}
function save()
{
// Init plugin and handle managesieve connection
$error = $this->start();
// get request size limits (#1488648)
$max_post = max([
ini_get('max_input_vars'),
ini_get('suhosin.request.max_vars'),
ini_get('suhosin.post.max_vars'),
]);
$max_depth = max([
ini_get('suhosin.request.max_array_depth'),
ini_get('suhosin.post.max_array_depth'),
]);
// check request size limit
if ($max_post && count($_POST, COUNT_RECURSIVE) >= $max_post) {
rcube::raise_error([
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Request size limit exceeded (one of max_input_vars/suhosin.request.max_vars/suhosin.post.max_vars)"
], true, false
);
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
}
// check request depth limits
else if ($max_depth && count($_POST['_header']) > $max_depth) {
rcube::raise_error([
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Request size limit exceeded (one of suhosin.request.max_array_depth/suhosin.post.max_array_depth)"
], true, false
);
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
}
// filters set add action
else if (!empty($_POST['_newset'])) {
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true);
$copy = rcube_utils::get_input_value('_copy', rcube_utils::INPUT_POST, true);
$from = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
$exceptions = $this->rc->config->get('managesieve_filename_exceptions');
$kolab = $this->rc->config->get('managesieve_kolab_master');
$name_uc = mb_strtolower($name);
$list = $this->list_scripts();
if (in_array('new_set', $this->disabled_actions)) {
$error = 'managesieve.disabledaction';
}
else if (!$name) {
$this->errors['name'] = $this->plugin->gettext('cannotbeempty');
}
else if (mb_strlen($name) > 128) {
$this->errors['name'] = $this->plugin->gettext('nametoolong');
}
else if (!empty($exceptions) && in_array($name, (array)$exceptions)) {
$this->errors['name'] = $this->plugin->gettext('namereserved');
}
else if (!empty($kolab) && in_array($name_uc, ['MASTER', 'USER', 'MANAGEMENT'])) {
$this->errors['name'] = $this->plugin->gettext('namereserved');
}
else if (in_array($name, $list)) {
$this->errors['name'] = $this->plugin->gettext('setexist');
}
else if ($from == 'file') {
// from file
if (is_uploaded_file($_FILES['_file']['tmp_name'])) {
$file = file_get_contents($_FILES['_file']['tmp_name']);
$file = preg_replace('/\r/', '', $file);
// for security don't save script directly
// check syntax before, like this...
$this->sieve->load_script($file);
if (!$this->save_script($name)) {
$this->errors['file'] = $this->plugin->gettext('setcreateerror');
}
}
else {
// upload failed
rcmail_action::upload_error($_FILES['_file']['error']);
}
}
else if (!$this->sieve->copy($name, $from == 'set' ? $copy : '')) {
$error = 'managesieve.setcreateerror';
}
if (!$error && empty($this->errors)) {
// Find position of the new script on the list
$list[] = $name;
asort($list, SORT_LOCALE_STRING);
$list = array_values($list);
$index = array_search($name, $list);
$this->rc->output->show_message('managesieve.setcreated', 'confirmation');
$this->rc->output->command('parent.managesieve_updatelist', 'setadd',
['name' => $name, 'index' => $index]);
}
else if (!empty($msg)) {
$this->rc->output->command('display_message', $msg, 'error');
}
else if ($error) {
$this->rc->output->show_message($error, 'error');
}
}
// filter add/edit action
else if (isset($_POST['_name'])) {
$name = trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true));
$fid = trim(rcube_utils::get_input_value('_fid', rcube_utils::INPUT_POST));
$join = trim(rcube_utils::get_input_value('_join', rcube_utils::INPUT_POST));
// and arrays
$headers = rcube_utils::get_input_value('_header', rcube_utils::INPUT_POST);
$cust_headers = rcube_utils::get_input_value('_custom_header', rcube_utils::INPUT_POST);
$cust_vars = rcube_utils::get_input_value('_custom_var', rcube_utils::INPUT_POST);
$ops = rcube_utils::get_input_value('_rule_op', rcube_utils::INPUT_POST);
$sizeops = rcube_utils::get_input_value('_rule_size_op', rcube_utils::INPUT_POST);
$sizeitems = rcube_utils::get_input_value('_rule_size_item', rcube_utils::INPUT_POST);
$sizetargets = rcube_utils::get_input_value('_rule_size_target', rcube_utils::INPUT_POST);
$spamtestops = rcube_utils::get_input_value('_rule_spamtest_op', rcube_utils::INPUT_POST);
$spamtesttargets = rcube_utils::get_input_value('_rule_spamtest_target', rcube_utils::INPUT_POST);
$targets = rcube_utils::get_input_value('_rule_target', rcube_utils::INPUT_POST, true);
$mods = rcube_utils::get_input_value('_rule_mod', rcube_utils::INPUT_POST);
$mod_types = rcube_utils::get_input_value('_rule_mod_type', rcube_utils::INPUT_POST);
$body_trans = rcube_utils::get_input_value('_rule_trans', rcube_utils::INPUT_POST);
$body_types = rcube_utils::get_input_value('_rule_trans_type', rcube_utils::INPUT_POST, true);
$comparators = rcube_utils::get_input_value('_rule_comp', rcube_utils::INPUT_POST);
$indexes = rcube_utils::get_input_value('_rule_index', rcube_utils::INPUT_POST);
$lastindexes = rcube_utils::get_input_value('_rule_index_last', rcube_utils::INPUT_POST);
$dateheaders = rcube_utils::get_input_value('_rule_date_header', rcube_utils::INPUT_POST);
$dateparts = rcube_utils::get_input_value('_rule_date_part', rcube_utils::INPUT_POST);
$mime_parts = rcube_utils::get_input_value('_rule_mime_part', rcube_utils::INPUT_POST);
$mime_types = rcube_utils::get_input_value('_rule_mime_type', rcube_utils::INPUT_POST);
$mime_params = rcube_utils::get_input_value('_rule_mime_param', rcube_utils::INPUT_POST, true);
$message = rcube_utils::get_input_value('_rule_message', rcube_utils::INPUT_POST);
$dup_handles = rcube_utils::get_input_value('_rule_duplicate_handle', rcube_utils::INPUT_POST, true);
$dup_headers = rcube_utils::get_input_value('_rule_duplicate_header', rcube_utils::INPUT_POST, true);
$dup_uniqueids = rcube_utils::get_input_value('_rule_duplicate_uniqueid', rcube_utils::INPUT_POST, true);
$dup_seconds = rcube_utils::get_input_value('_rule_duplicate_seconds', rcube_utils::INPUT_POST);
$dup_lasts = rcube_utils::get_input_value('_rule_duplicate_last', rcube_utils::INPUT_POST);
$act_types = rcube_utils::get_input_value('_action_type', rcube_utils::INPUT_POST, true);
$mailboxes = rcube_utils::get_input_value('_action_mailbox', rcube_utils::INPUT_POST, true);
$act_targets = rcube_utils::get_input_value('_action_target', rcube_utils::INPUT_POST, true);
$domain_targets = rcube_utils::get_input_value('_action_target_domain', rcube_utils::INPUT_POST);
$area_targets = rcube_utils::get_input_value('_action_target_area', rcube_utils::INPUT_POST, true);
$reasons = rcube_utils::get_input_value('_action_reason', rcube_utils::INPUT_POST, true);
$addresses = rcube_utils::get_input_value('_action_addresses', rcube_utils::INPUT_POST, true);
$intervals = rcube_utils::get_input_value('_action_interval', rcube_utils::INPUT_POST);
$interval_types = rcube_utils::get_input_value('_action_interval_type', rcube_utils::INPUT_POST);
$from = rcube_utils::get_input_value('_action_from', rcube_utils::INPUT_POST, true);
$subject = rcube_utils::get_input_value('_action_subject', rcube_utils::INPUT_POST, true);
$flags = rcube_utils::get_input_value('_action_flags', rcube_utils::INPUT_POST);
$varnames = rcube_utils::get_input_value('_action_varname', rcube_utils::INPUT_POST);
$varvalues = rcube_utils::get_input_value('_action_varvalue', rcube_utils::INPUT_POST);
$varmods = rcube_utils::get_input_value('_action_varmods', rcube_utils::INPUT_POST);
$notifymethods = rcube_utils::get_input_value('_action_notifymethod', rcube_utils::INPUT_POST);
$notifytargets = rcube_utils::get_input_value('_action_notifytarget', rcube_utils::INPUT_POST, true);
$notifyoptions = rcube_utils::get_input_value('_action_notifyoption', rcube_utils::INPUT_POST, true);
$notifymessages = rcube_utils::get_input_value('_action_notifymessage', rcube_utils::INPUT_POST, true);
$notifyfrom = rcube_utils::get_input_value('_action_notifyfrom', rcube_utils::INPUT_POST);
$notifyimp = rcube_utils::get_input_value('_action_notifyimportance', rcube_utils::INPUT_POST);
$addheader_name = rcube_utils::get_input_value('_action_addheader_name', rcube_utils::INPUT_POST);
$addheader_value = rcube_utils::get_input_value('_action_addheader_value', rcube_utils::INPUT_POST, true);
$addheader_pos = rcube_utils::get_input_value('_action_addheader_pos', rcube_utils::INPUT_POST);
$delheader_name = rcube_utils::get_input_value('_action_delheader_name', rcube_utils::INPUT_POST);
$delheader_value = rcube_utils::get_input_value('_action_delheader_value', rcube_utils::INPUT_POST, true);
$delheader_pos = rcube_utils::get_input_value('_action_delheader_pos', rcube_utils::INPUT_POST);
$delheader_index = rcube_utils::get_input_value('_action_delheader_index', rcube_utils::INPUT_POST);
$delheader_op = rcube_utils::get_input_value('_action_delheader_op', rcube_utils::INPUT_POST);
$delheader_comp = rcube_utils::get_input_value('_action_delheader_comp', rcube_utils::INPUT_POST);
$this->form['disabled'] = empty($_POST['_enabled']);
$this->form['join'] = $join == 'allof';
$this->form['name'] = $name;
$this->form['tests'] = [];
$this->form['actions'] = [];
if ($name == '') {
$this->errors['name'] = $this->plugin->gettext('cannotbeempty');
}
else {
foreach ($this->script as $idx => $rule)
if ($rule['name'] == $name && $idx != $fid) {
$this->errors['name'] = $this->plugin->gettext('ruleexist');
break;
}
}
$i = 0;
// rules
if ($join == 'any') {
$this->form['tests'][0]['test'] = 'true';
}
else {
foreach ($headers as $idx => $header) {
// targets are indexed differently (assume form order)
$target = $this->strip_value($targets[$idx], true);
$header = $this->strip_value($header);
$operator = $this->strip_value($ops[$idx]);
$comparator = $this->strip_value($comparators[$idx]);
if ($header == 'size') {
$sizeop = $this->strip_value($sizeops[$idx]);
$sizeitem = $this->strip_value($sizeitems[$idx]);
$sizetarget = $this->strip_value($sizetargets[$idx]);
$this->form['tests'][$i]['test'] = 'size';
$this->form['tests'][$i]['type'] = $sizeop;
$this->form['tests'][$i]['arg'] = $sizetarget;
if ($sizetarget == '') {
$this->errors['tests'][$i]['sizetarget'] = $this->plugin->gettext('cannotbeempty');
}
else if (!preg_match('/^[0-9]+(K|M|G)?$/i', $sizetarget.$sizeitem, $m)) {
$this->errors['tests'][$i]['sizetarget'] = $this->plugin->gettext('forbiddenchars');
$this->form['tests'][$i]['item'] = $sizeitem;
}
else {
$this->form['tests'][$i]['arg'] .= $m[1];
}
}
else if ($header == 'spamtest') {
$spamtestop = $this->strip_value($spamtestops[$idx]);
$spamtesttarget = $this->strip_value($spamtesttargets[$idx]);
$comparator = 'i;ascii-numeric';
if (!$spamtestop) {
$spamtestop = 'value-eq';
$spamtesttarget = '0';
}
$this->form['tests'][$i]['test'] = 'spamtest';
$this->form['tests'][$i]['type'] = $spamtestop;
$this->form['tests'][$i]['arg'] = $spamtesttarget;
if ($spamtesttarget === '') {
$this->errors['tests'][$i]['spamtesttarget'] = $this->plugin->gettext('cannotbeempty');
}
else if (!preg_match('/^([0-9]|10)$/i', $spamtesttarget)) {
$this->errors['tests'][$i]['spamtesttarget'] = $this->plugin->gettext('forbiddenchars');
}
}
else if ($header == 'currentdate') {
$datepart = $this->strip_value($dateparts[$idx]);
$type = preg_replace('/^not/', '', $operator);
$this->form['tests'][$i]['test'] = 'currentdate';
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['part'] = $datepart;
$this->form['tests'][$i]['arg'] = $target;
$this->form['tests'][$i]['not'] = preg_match('/^not/', $operator) === 1;
if ($type == 'exists') {
$this->errors['tests'][$i]['op'] = true;
}
if ($type != 'exists') {
if (empty($target)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (strpos($type, 'count-') === 0) {
foreach ($target as $arg) {
if (preg_match('/[^0-9]/', $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
else if (strpos($type, 'value-') === 0) {
// Some date/time formats do not support i;ascii-numeric comparator
if ($comparator == 'i;ascii-numeric' && in_array($datepart, ['date', 'time', 'iso8601', 'std11'])) {
$comparator = '';
}
}
if (!preg_match('/^(regex|matches|count-)/', $type) && !empty($target)) {
foreach ($target as $arg) {
if (!$this->validate_date_part($datepart, $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('invaliddateformat');
break;
}
}
}
}
}
else if ($header == 'date') {
$datepart = $this->strip_value($dateparts[$idx]);
$dateheader = $this->strip_value($dateheaders[$idx]);
$index = $this->strip_value($indexes[$idx]);
$indexlast = $this->strip_value($lastindexes[$idx]);
$mod = $this->strip_value($mods[$idx]);
$type = preg_replace('/^not/', '', $operator);
if ($type == 'exists') {
$this->errors['tests'][$i]['op'] = true;
}
if (!empty($index) && $mod != 'envelope') {
$this->form['tests'][$i]['index'] = intval($index);
$this->form['tests'][$i]['last'] = !empty($indexlast);
}
if (empty($dateheader)) {
$dateheader = 'Date';
}
else if (!preg_match('/^[\x21-\x39\x41-\x7E]+$/i', $dateheader)) {
$this->errors['tests'][$i]['dateheader'] = $this->plugin->gettext('forbiddenchars');
}
$this->form['tests'][$i]['test'] = 'date';
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['part'] = $datepart;
$this->form['tests'][$i]['arg'] = $target;
$this->form['tests'][$i]['header'] = $dateheader;
$this->form['tests'][$i]['not'] = preg_match('/^not/', $operator) === 1;
if ($type != 'exists') {
if (empty($target)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (strpos($type, 'count-') === 0) {
foreach ($target as $arg) {
if (preg_match('/[^0-9]/', $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
else if (strpos($type, 'value-') === 0) {
// Some date/time formats do not support i;ascii-numeric comparator
if ($comparator == 'i;ascii-numeric' && in_array($datepart, ['date', 'time', 'iso8601', 'std11'])) {
$comparator = '';
}
}
if (!empty($target) && !preg_match('/^(regex|matches|count-)/', $type)) {
foreach ($target as $arg) {
if (!$this->validate_date_part($datepart, $arg)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('invaliddateformat');
break;
}
}
}
}
}
else if ($header == 'body') {
$trans = $this->strip_value($body_trans[$idx]);
$trans_type = $this->strip_value($body_types[$idx], true);
$type = preg_replace('/^not/', '', $operator);
$this->form['tests'][$i]['test'] = 'body';
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['arg'] = $target;
$this->form['tests'][$i]['not'] = preg_match('/^not/', $operator) === 1;
$this->form['tests'][$i]['part'] = $trans;
if ($trans == 'content') {
$this->form['tests'][$i]['content'] = $trans_type;
}
if ($type == 'exists') {
$this->errors['tests'][$i]['op'] = true;
}
if (empty($target) && $type != 'exists') {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (preg_match('/^(value|count)-/', $type)) {
foreach ($target as $target_value) {
if (preg_match('/[^0-9]/', $target_value)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
}
else if ($header == 'message') {
$test = $this->strip_value($message[$idx]);
if (preg_match('/^not/', $test)) {
$this->form['tests'][$i]['not'] = true;
$test = substr($test, 3);
}
$this->form['tests'][$i]['test'] = $test;
if ($test == 'duplicate') {
$this->form['tests'][$i]['last'] = !empty($dup_lasts[$idx]);
$this->form['tests'][$i]['handle'] = trim($dup_handles[$idx]);
$this->form['tests'][$i]['header'] = trim($dup_headers[$idx]);
$this->form['tests'][$i]['uniqueid'] = trim($dup_uniqueids[$idx]);
$this->form['tests'][$i]['seconds'] = trim($dup_seconds[$idx]);
if ($this->form['tests'][$i]['seconds']
&& preg_match('/[^0-9]/', $this->form['tests'][$i]['seconds'])
) {
$this->errors['tests'][$i]['duplicate_seconds'] = $this->plugin->gettext('forbiddenchars');
}
if ($this->form['tests'][$i]['header'] && $this->form['tests'][$i]['uniqueid']) {
$this->errors['tests'][$i]['duplicate_uniqueid'] = $this->plugin->gettext('duplicate.conflict.err');
}
}
}
else {
$cust_header = $headers = $this->strip_value($cust_headers[$idx]);
$mod = $this->strip_value($mods[$idx]);
$mod_type = $this->strip_value($mod_types[$idx]);
$index = isset($indexes[$idx]) ? $this->strip_value($indexes[$idx]) : null;
$indexlast = isset($lastindexes[$idx]) ? $this->strip_value($lastindexes[$idx]) : null;
$mime_param = isset($mime_params[$idx]) ? $this->strip_value($mime_params[$idx]) : null;
$mime_type = isset($mime_types[$idx]) ? $mime_types[$idx] : null;
$mime_part = isset($mime_parts[$idx]) ? $mime_parts[$idx] : null;
$cust_var = null;
if ($header == 'string') {
$cust_var = $headers = $this->strip_value($cust_vars[$idx]);
}
$this->form['tests'][$i]['not'] = preg_match('/^not/', $operator) === 1;
$type = preg_replace('/^not/', '', $operator);
if (!empty($index) && $mod != 'envelope') {
$this->form['tests'][$i]['index'] = intval($index);
$this->form['tests'][$i]['last'] = !empty($indexlast);
}
if ($header == '...' || $header == 'string') {
if (!count($headers)) {
$this->errors['tests'][$i]['header'] = $this->plugin->gettext('cannotbeempty');
}
else if ($header == '...') {
foreach ($headers as $hr) {
// RFC2822: printable ASCII except colon
if (!preg_match('/^[\x21-\x39\x41-\x7E]+$/i', $hr)) {
$this->errors['tests'][$i]['header'] = $this->plugin->gettext('forbiddenchars');
}
}
}
if (empty($this->errors['tests'][$i]['header'])) {
$cust_header = $cust_var = (is_array($headers) && count($headers) == 1) ? $headers[0] : $headers;
}
}
$test = $header == 'string' ? 'string' : 'header';
$header = $header == 'string' ? $cust_var : $header;
$header = $header == '...' ? $cust_header : $header;
if (is_array($header)) {
foreach ($header as $h_index => $val) {
if (isset($this->headers[$val])) {
$header[$h_index] = $this->headers[$val];
}
}
}
if ($type == 'exists') {
$this->form['tests'][$i]['test'] = 'exists';
$this->form['tests'][$i]['arg'] = $header;
}
else {
if ($mod == 'address' || $mod == 'envelope') {
$found = false;
if (empty($this->errors['tests'][$i]['header'])) {
foreach ((array)$header as $hdr) {
if (!in_array(strtolower(trim($hdr)), $this->addr_headers)) {
$found = true;
}
}
}
if (!$found) {
$test = $mod;
}
}
$this->form['tests'][$i]['type'] = $type;
$this->form['tests'][$i]['test'] = $test;
$this->form['tests'][$i]['arg1'] = $header;
$this->form['tests'][$i]['arg2'] = $target;
if (empty($target)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (preg_match('/^(value|count)-/', $type)) {
foreach ($target as $target_value) {
if (preg_match('/[^0-9]/', $target_value)) {
$this->errors['tests'][$i]['target'] = $this->plugin->gettext('forbiddenchars');
}
}
}
if ($mod) {
$this->form['tests'][$i]['part'] = $mod_type;
}
}
if ($test == 'header') {
if (in_array($mime_type, ['type', 'subtype', 'contenttype', 'param'])) {
$this->form['tests'][$i]['mime-' . $mime_type] = true;
if ($mime_type == 'param') {
if (empty($mime_param)) {
$this->errors['tests'][$i]['mime-param'] = $this->plugin->gettext('cannotbeempty');
}
$this->form['tests'][$i]['mime-param'] = $mime_param;
}
}
if ($mime_part == 'anychild') {
$this->form['tests'][$i]['mime-anychild'] = true;
}
}
}
if ($header != 'size' && $comparator) {
$this->form['tests'][$i]['comparator'] = $comparator;
}
$i++;
}
}
$i = 0;
// actions
foreach ($act_types as $idx => $type) {
$type = $this->strip_value($type);
switch ($type) {
case 'fileinto':
case 'fileinto_copy':
$mailbox = $this->strip_value($mailboxes[$idx], false, false);
$this->form['actions'][$i]['target'] = $this->mod_mailbox($mailbox, 'in');
if ($type == 'fileinto_copy') {
$type = 'fileinto';
$this->form['actions'][$i]['copy'] = true;
}
break;
case 'reject':
case 'ereject':
$target = $this->strip_value($area_targets[$idx]);
$this->form['actions'][$i]['target'] = str_replace("\r\n", "\n", $target);
// if ($target == '')
// $this->errors['actions'][$i]['targetarea'] = $this->plugin->gettext('cannotbeempty');
break;
case 'redirect':
case 'redirect_copy':
$target = $this->strip_value($act_targets[$idx]);
$domain = $this->strip_value($domain_targets[$idx]);
// force one of the configured domains
$domains = (array) $this->rc->config->get('managesieve_domains');
if (!empty($domains) && !empty($target)) {
if (!$domain || !in_array($domain, $domains)) {
$domain = $domains[0];
}
$target .= '@' . $domain;
}
$this->form['actions'][$i]['target'] = $target;
if ($target == '') {
$this->errors['actions'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
else if (!rcube_utils::check_email($target)) {
$this->errors['actions'][$i]['target'] = $this->plugin->gettext(!empty($domains) ? 'forbiddenchars' : 'noemailwarning');
}
if ($type == 'redirect_copy') {
$type = 'redirect';
$this->form['actions'][$i]['copy'] = true;
}
break;
case 'addflag':
case 'setflag':
case 'removeflag':
$this->form['actions'][$i]['target'] = $this->strip_value($flags[$idx]);
if (empty($this->form['actions'][$i]['target'])) {
$this->errors['actions'][$i]['flag'] = $this->plugin->gettext('noflagset');
}
break;
case 'addheader':
case 'deleteheader':
$this->form['actions'][$i]['name'] = trim($type == 'addheader' ? $addheader_name[$idx] : $delheader_name[$idx]);
$this->form['actions'][$i]['value'] = $type == 'addheader' ? $addheader_value[$idx] : $delheader_value[$idx];
$this->form['actions'][$i]['last'] = ($type == 'addheader' ? $addheader_pos[$idx] : $delheader_pos[$idx]) == 'last';
if (empty($this->form['actions'][$i]['name'])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('cannotbeempty');
}
else if (!preg_match('/^[0-9a-z_-]+$/i', $this->form['actions'][$i]['name'])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('forbiddenchars');
}
if ($type == 'deleteheader') {
foreach ((array) $this->form['actions'][$i]['value'] as $pidx => $pattern) {
if (empty($pattern)) {
unset($this->form['actions'][$i]['value'][$pidx]);
}
}
$this->form['actions'][$i]['match-type'] = $delheader_op[$idx];
$this->form['actions'][$i]['comparator'] = $delheader_comp[$idx];
$this->form['actions'][$i]['index'] = $delheader_index[$idx];
if (empty($this->form['actions'][$i]['index'])) {
if (!empty($this->form['actions'][$i]['last'])) {
$this->errors['actions'][$i]['index'] = $this->plugin->gettext('lastindexempty');
}
}
else if (!preg_match('/^[0-9]+$/i', $this->form['actions'][$i]['index'])) {
$this->errors['actions'][$i]['index'] = $this->plugin->gettext('forbiddenchars');
}
}
else {
if (empty($this->form['actions'][$i]['value'])) {
$this->errors['actions'][$i]['value'] = $this->plugin->gettext('cannotbeempty');
}
}
break;
case 'vacation':
$reason = $this->strip_value($reasons[$idx], true);
$interval_type = $interval_types[$idx] == 'seconds' ? 'seconds' : 'days';
$this->form['actions'][$i]['reason'] = str_replace("\r\n", "\n", $reason);
$this->form['actions'][$i]['from'] = $from[$idx];
$this->form['actions'][$i]['subject'] = $subject[$idx];
$this->form['actions'][$i]['addresses'] = $addresses[$idx];
$this->form['actions'][$i][$interval_type] = $intervals[$idx];
// @TODO: vacation :mime, :handle
foreach ((array)$this->form['actions'][$i]['addresses'] as $aidx => $address) {
$this->form['actions'][$i]['addresses'][$aidx] = $address = trim($address);
if (empty($address)) {
unset($this->form['actions'][$i]['addresses'][$aidx]);
}
else if (!rcube_utils::check_email($address)) {
$this->errors['actions'][$i]['addresses'] = $this->plugin->gettext('noemailwarning');
break;
}
}
if (!empty($this->form['actions'][$i]['from'])) {
// According to RFC5230 the :from string must specify a valid [RFC2822] mailbox-list
// we'll try to extract addresses and validate them separately
$from = rcube_mime::decode_address_list($this->form['actions'][$i]['from'], null, true, RCUBE_CHARSET);
foreach ((array) $from as $idx => $addr) {
if (empty($addr['mailto']) || !rcube_utils::check_email($addr['mailto'])) {
$this->errors['actions'][$i]['from'] = $this->plugin->gettext('noemailwarning');
break;
}
else {
$from[$idx] = format_email_recipient($addr['mailto'], $addr['name']);
}
}
// Only one address is allowed (at least on cyrus imap)
if (is_array($from) && count($from) > 1) {
$this->errors['actions'][$i]['from'] = $this->plugin->gettext('noemailwarning');
}
// Then we convert it back to RFC2822 format
if (empty($this->errors['actions'][$i]['from']) && !empty($from)) {
$this->form['actions'][$i]['from'] = Mail_mimePart::encodeHeader(
'From', implode(', ', $from), RCUBE_CHARSET, 'base64', '');
}
}
if ($this->form['actions'][$i]['reason'] == '') {
$this->errors['actions'][$i]['reason'] = $this->plugin->gettext('cannotbeempty');
}
if ($this->form['actions'][$i][$interval_type] && !preg_match('/^[0-9]+$/', $this->form['actions'][$i][$interval_type])) {
$this->errors['actions'][$i]['interval'] = $this->plugin->gettext('forbiddenchars');
}
break;
case 'set':
$this->form['actions'][$i]['name'] = $varnames[$idx];
$this->form['actions'][$i]['value'] = $varvalues[$idx];
foreach ((array)$varmods[$idx] as $v_m) {
$this->form['actions'][$i][$v_m] = true;
}
if (empty($varnames[$idx])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('cannotbeempty');
}
else if (!preg_match('/^[0-9a-z_]+$/i', $varnames[$idx])) {
$this->errors['actions'][$i]['name'] = $this->plugin->gettext('forbiddenchars');
}
if (!isset($varvalues[$idx]) || $varvalues[$idx] === '') {
$this->errors['actions'][$i]['value'] = $this->plugin->gettext('cannotbeempty');
}
break;
case 'notify':
if (empty($notifymethods[$idx])) {
$this->errors['actions'][$i]['method'] = $this->plugin->gettext('cannotbeempty');
}
if (empty($notifytargets[$idx])) {
$this->errors['actions'][$i]['target'] = $this->plugin->gettext('cannotbeempty');
}
if (!empty($notifyfrom[$idx]) && !rcube_utils::check_email($notifyfrom[$idx])) {
$this->errors['actions'][$i]['from'] = $this->plugin->gettext('noemailwarning');
}
// skip empty options
foreach ((array)$notifyoptions[$idx] as $opt_idx => $opt) {
if (!strlen(trim($opt))) {
unset($notifyoptions[$idx][$opt_idx]);
}
}
$this->form['actions'][$i]['method'] = $notifymethods[$idx] . ':' . $notifytargets[$idx];
$this->form['actions'][$i]['options'] = $notifyoptions[$idx];
$this->form['actions'][$i]['message'] = $notifymessages[$idx];
$this->form['actions'][$i]['from'] = $notifyfrom[$idx];
$this->form['actions'][$i]['importance'] = $notifyimp[$idx];
break;
}
$this->form['actions'][$i]['type'] = $type;
$i++;
}
if (!$this->errors && !$error) {
// save the script
if (!isset($this->script[$fid])) {
$fid = $this->sieve->script->add_rule($this->form);
$new = true;
}
else {
$fid = $this->sieve->script->update_rule($fid, $this->form);
}
if ($fid !== false) {
$save = $this->save_script();
}
if (!empty($save) && $fid !== false) {
$this->rc->output->show_message('managesieve.filtersaved', 'confirmation');
if ($this->rc->task != 'mail') {
$args = [
'name' => $this->form['name'],
'id' => $fid,
'disabled' => $this->form['disabled']
];
$this->rc->output->command('parent.managesieve_updatelist', isset($new) ? 'add' : 'update', $args);
}
else {
$this->rc->output->command('managesieve_dialog_close');
$this->rc->output->send('iframe');
}
}
else {
$this->rc->output->show_message('managesieve.filtersaveerror', 'error');
}
}
else {
$this->rc->output->show_message('managesieve.filterformerror', 'warning');
}
}
$this->send();
}
protected function send()
{
// Handle form action
if (isset($_GET['_framed']) || isset($_POST['_framed'])) {
if (isset($_GET['_newset']) || isset($_POST['_newset'])) {
$this->rc->output->send('managesieve.setedit');
}
else if (isset($_GET['_seteditraw']) || isset($_POST['_seteditraw'])) {
$this->rc->output->send('managesieve.seteditraw');
}
else {
$this->rc->output->send('managesieve.filteredit');
}
}
else {
$this->rc->output->set_pagetitle($this->plugin->gettext('filters'));
$this->rc->output->send('managesieve.managesieve');
}
}
/**
* Return the filters list as HTML table
*/
function filters_list($attrib)
{
// add id to message list table if not specified
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmfilterslist';
}
// define list of cols to be displayed
$a_show_cols = ['name'];
$result = $this->list_rules();
// create the table
$out = rcmail_action::table_output($attrib, $result, $a_show_cols, 'id');
// set client env
$this->rc->output->add_gui_object('filterslist', $attrib['id']);
$this->rc->output->include_script('list.js');
// add some labels to client
$this->rc->output->add_label('managesieve.filterdeleteconfirm');
return $out;
}
/**
* Return the filters list as <SELECT>
*/
function filtersets_list($attrib, $no_env = false)
{
// add id to message list table if not specified
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmfiltersetslist';
}
$list = $this->list_scripts();
if ($list) {
asort($list, SORT_LOCALE_STRING);
}
if (!empty($attrib['type']) && $attrib['type'] == 'list') {
// define list of cols to be displayed
$a_show_cols = ['name'];
$result = [];
$scripts = [];
if ($list) {
foreach ($list as $idx => $set) {
$scripts['S' . $idx] = $set;
$result[] = [
'name' => $set,
'id' => 'S' . $idx,
'class' => !in_array($set, $this->active) ? 'disabled' : '',
];
}
}
// create XHTML table
$out = $this->rc->table_output($attrib, $result, $a_show_cols, 'id');
$this->rc->output->set_env('filtersets', $scripts);
$this->rc->output->include_script('list.js');
}
else {
$select = new html_select([
'name' => '_set',
'id' => $attrib['id'],
'class' => 'custom-select',
'onchange' => $this->rc->task != 'mail' ? 'rcmail.managesieve_set()' : ''
]);
if ($list) {
foreach ($list as $set) {
$select->add($set, $set);
}
}
$out = $select->show($this->sieve->current);
}
// set client env
if (!$no_env) {
$this->rc->output->add_gui_object('filtersetslist', $attrib['id']);
$this->rc->output->add_label('managesieve.setdeleteconfirm');
}
return $out;
}
function filterset_editraw($attrib)
{
$script_name = rcube_utils::get_input_value('_set', rcube_utils::INPUT_GP);
$script = $this->sieve->get_script($script_name);
$script_post = !empty($_POST['rawsetcontent']) ? $_POST['rawsetcontent'] : null;
$framed = !empty($_POST['_framed']) || !empty($_GET['_framed']);
$hiddenfields = new html_hiddenfield();
$hiddenfields->add(['name' => '_task', 'value' => $this->rc->task]);
$hiddenfields->add(['name' => '_action', 'value' => 'plugin.managesieve-saveraw']);
$hiddenfields->add(['name' => '_set', 'value' => $script_name]);
$hiddenfields->add(['name' => '_seteditraw', 'value' => 1]);
$hiddenfields->add(['name' => '_framed', 'value' => $framed ? 1 : 0]);
$out = $hiddenfields->show();
$txtarea = new html_textarea([
'id' => 'rawfiltersettxt',
'name' => 'rawsetcontent',
'class' => 'form-control',
'rows' => '15'
]);
$out .= $txtarea->show($script_post !== null ? $script_post : ($script !== false ? rtrim($script) : ''));
$this->rc->output->add_gui_object('sievesetrawform', 'filtersetrawform');
$this->plugin->include_stylesheet('codemirror/lib/codemirror.css');
$this->plugin->include_script('codemirror/lib/codemirror.js');
$this->plugin->include_script('codemirror/addon/selection/active-line.js');
$this->plugin->include_script('codemirror/mode/sieve/sieve.js');
if ($script === false) {
$this->rc->output->show_message('managesieve.filterunknownerror', 'error');
}
$out = html::tag('form', $attrib + [
'id' => 'filtersetrawform',
'name' => 'filtersetrawform',
'action' => './',
'method' => 'post',
'enctype' => 'multipart/form-data',
], $out
);
return str_replace('</form>', '', $out);
}
function filterset_form($attrib)
{
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmfiltersetform';
}
$framed = !empty($_POST['_framed']) || !empty($_GET['_framed']);
$table = new html_table(['cols' => 2, 'class' => 'propform']);
$hiddenfields = new html_hiddenfield(['name' => '_task', 'value' => $this->rc->task]);
$hiddenfields->add(['name' => '_action', 'value' => 'plugin.managesieve-save']);
$hiddenfields->add(['name' => '_framed', 'value' => $framed ? 1 : 0]);
$hiddenfields->add(['name' => '_newset', 'value' => 1]);
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST);
$copy = rcube_utils::get_input_value('_copy', rcube_utils::INPUT_POST);
$selected = rcube_utils::get_input_value('_from', rcube_utils::INPUT_POST);
// filter set name input
$input_name = new html_inputfield([
'name' => '_name',
'id' => '_name',
'size' => 30,
'class' => !empty($this->errors['name']) ? 'error form-control' : 'form-control'
]);
$table->add('title', html::label('_name', rcube::Q($this->plugin->gettext('filtersetname'))));
$table->add(null, $input_name->show($name));
$filters = '<ul class="proplist">';
$filters .= '<li>' . html::label('from_none', html::tag('input', [
'type' => 'radio',
'id' => 'from_none',
'name' => '_from',
'value' => 'none',
'checked' => !$selected || $selected == 'none'
]) . rcube::Q($this->plugin->gettext('none'))) . '</li>';
// filters set list
$list = $this->list_scripts();
$select = new html_select(['name' => '_copy', 'id' => '_copy', 'class' => 'custom-select']);
if (is_array($list)) {
asort($list, SORT_LOCALE_STRING);
if (!$copy && isset($_SESSION['managesieve_current'])) {
$copy = $_SESSION['managesieve_current'];
}
foreach ($list as $set) {
$select->add($set, $set);
}
$filters .= '<li>' . html::label('from_set', html::tag('input', [
'type' => 'radio',
'id' => 'from_set',
'name' => '_from',
'value' => 'set',
'checked' => $selected == 'set',
]) . rcube::Q($this->plugin->gettext('fromset')) . ' ' . $select->show($copy)) . '</li>';
}
// script upload box
$upload = new html_inputfield([
'name' => '_file',
'id' => '_file',
'size' => 30,
'type' => 'file',
'class' => !empty($this->errors['file']) ? 'error form-control' : 'form-control'
]);
$filters .= '<li>' . html::label('from_file', html::tag('input', [
'type' => 'radio',
'id' => 'from_file',
'name' => '_from',
'value' => 'file',
'checked' => $selected == 'file',
]) . rcube::Q($this->plugin->gettext('fromfile')) . ' ' . $upload->show()) . '</li>';
$filters .= '</ul>';
$table->add('title', html::label('from_none', rcube::Q($this->plugin->gettext('filters'))));
$table->add('', $filters);
$out = '<form name="filtersetform" action="./" method="post" enctype="multipart/form-data">'
. "\n" . $hiddenfields->show() . "\n" . $table->show();
$this->rc->output->add_gui_object('sieveform', 'filtersetform');
if (!empty($this->errors['name'])) {
$this->add_tip('_name', $this->errors['name'], true);
}
if (!empty($this->errors['file'])) {
$this->add_tip('_file', $this->errors['file'], true);
}
$this->print_tips();
return $out;
}
/**
* Filter form object for templates engine
*/
function filter_form($attrib)
{
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmfilterform';
}
$fid = rcube_utils::get_input_value('_fid', rcube_utils::INPUT_GPC);
$scr = isset($this->form) ? $this->form : (array_key_exists($fid, $this->script) ? $this->script[$fid] : null);
$compact = !empty($attrib['compact-form']);
$framed = !empty($_POST['_framed']) || !empty($_GET['_framed']);
$_SESSION['managesieve-compact-form'] = $compact;
// do not allow creation of new filters
if ($fid === null && in_array('new_filter', $this->disabled_actions)) {
$this->rc->output->show_message('managesieve.disabledaction', 'error');
return;
}
$hiddenfields = new html_hiddenfield(['name' => '_task', 'value' => $this->rc->task]);
$hiddenfields->add(['name' => '_action', 'value' => 'plugin.managesieve-save']);
$hiddenfields->add(['name' => '_framed', 'value' => $framed ? 1 : 0]);
$hiddenfields->add(['name' => '_fid', 'value' => $fid]);
$out = $hiddenfields->show();
// 'any' flag
$any = (
(!isset($this->form) && !empty($scr) && empty($scr['tests']))
|| (!empty($scr['tests']) && count($scr['tests']) == 1
&& $scr['tests'][0]['test'] == 'true' && empty($scr['tests'][0]['not'])
)
);
// filter name input
$input_name = new html_inputfield([
'name' => '_name',
'id' => '_name',
'size' => 30,
'class' => !empty($this->errors['name']) ? 'form-control error' : 'form-control'
]);
if (!empty($this->errors['name'])) {
$this->add_tip('_name', $this->errors['name'], true);
}
$input_name = $input_name->show(isset($scr) ? $scr['name'] : '');
$out .= sprintf("\n" . '<div class="form-group row">'
. '<label for="_name" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8">%s</div></div>',
rcube::Q($this->plugin->gettext('filtername')), $input_name
);
// filter set selector
if ($this->rc->task == 'mail') {
$out .= sprintf("\n" . '<div class="form-group row">'
. '<label for="%s" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8">%s</div></div>',
'sievescriptname',
rcube::Q($this->plugin->gettext('filterset')),
$this->filtersets_list(['id' => 'sievescriptname'], true)
);
}
$out .= sprintf("\n" . '<div class="form-group row form-check">'
. '<label for="fenabled" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8 form-check">'
. '<input type="checkbox" id="fenabled" name="_enabled" value="1"' . (empty($scr['disabled']) ? ' checked' : '') . ' />'
. '</div></div>',
rcube::Q($this->plugin->gettext('filterenabled'))
);
if ($compact) {
$select = new html_select(['name' => '_join', 'id' => '_join', 'class' => 'custom-select',
'onchange' => 'rule_join_radio(this.value)']);
foreach (['allof', 'anyof', 'any'] as $val) {
$select->add($this->plugin->gettext('filter' . $val), $val);
}
$join = $any ? 'any' : 'allof';
if (isset($scr) && !$any) {
$join = !empty($scr['join']) ? 'allof' : 'anyof';
}
$out .= sprintf("\n" . '<div class="form-group row">'
. '<label for="_join" class="col-sm-4 col-form-label">%s</label>'
. '<div class="col-sm-8">%s</div></div>',
rcube::Q($this->plugin->gettext('scope')), $select->show($join)
);
$out .= '<div id="rules"'.($any ? ' style="display: none"' : '').'>';
$out .= "\n<fieldset><legend>" . rcube::Q($this->plugin->gettext('rules')) . "</legend>\n";
}
else {
$out .= '<br><fieldset><legend>' . rcube::Q($this->plugin->gettext('messagesrules')) . "</legend>\n";
// any, allof, anyof radio buttons
$field_id = '_allof';
$input_join = new html_radiobutton(['name' => '_join', 'id' => $field_id, 'value' => 'allof',
'onclick' => 'rule_join_radio(\'allof\')', 'class' => 'radio']);
if (isset($scr) && !$any) {
$input_join = $input_join->show($scr['join'] ? 'allof' : '');
}
else {
$input_join = $input_join->show();
}
$out .= $input_join . html::label($field_id, rcube::Q($this->plugin->gettext('filterallof')));
$field_id = '_anyof';
$input_join = new html_radiobutton(['name' => '_join', 'id' => $field_id, 'value' => 'anyof',
'onclick' => 'rule_join_radio(\'anyof\')', 'class' => 'radio']);
if (isset($scr) && !$any) {
$input_join = $input_join->show($scr['join'] ? '' : 'anyof');
}
else {
$input_join = $input_join->show('anyof'); // default
}
$out .= $input_join . html::label($field_id, rcube::Q($this->plugin->gettext('filteranyof')));
$field_id = '_any';
$input_join = new html_radiobutton(['name' => '_join', 'id' => $field_id, 'value' => 'any',
'onclick' => 'rule_join_radio(\'any\')', 'class' => 'radio']);
$input_join = $input_join->show($any ? 'any' : '');
$out .= $input_join . html::label($field_id, rcube::Q($this->plugin->gettext('filterany')));
$out .= '<div id="rules"'.($any ? ' style="display: none"' : '').'>';
}
$rows_num = !empty($scr['tests']) ? count($scr['tests']) : 1;
for ($x=0; $x<$rows_num; $x++) {
$out .= $this->rule_div($fid, $x, true, $compact);
}
$out .= $compact ? "</fieldset>\n</div>\n" : "</div>\n</fieldset>\n";
// actions
$label = $this->plugin->gettext($compact ? 'actions' : 'messagesactions');
$out .= '<fieldset><legend>' . rcube::Q($label) . "</legend>\n";
$rows_num = isset($scr) ? count($scr['actions']) : 1;
$out .= '<div id="actions">';
for ($x=0; $x<$rows_num; $x++) {
$out .= $this->action_div($fid, $x);
}
$out .= "</div>\n";
$out .= "</fieldset>\n";
$this->print_tips();
$this->rc->output->add_label(
'managesieve.ruledeleteconfirm',
'managesieve.actiondeleteconfirm'
);
$this->rc->output->set_env('rule_disabled', !empty($scr['disabled']));
$this->rc->output->add_gui_object('sieveform', 'filterform');
$attrib['name'] = 'filterform';
$attrib['action'] = './';
$attrib['method'] = 'post';
$out = html::tag('form', $attrib, $out, ['name', 'action', 'method', 'class']);
if (!$compact) {
$out = str_replace('</form>', '', $out);
}
return $out;
}
function rule_div($fid, $id, $div = true, $compact = false)
{
if (isset($id) && isset($this->form)) {
$rule = $this->form['tests'][$id];
}
else if (isset($id) && isset($this->script[$fid]['tests'][$id])) {
$rule = $this->script[$fid]['tests'][$id];
}
else {
$rule = ['test' => null];
}
if (isset($this->form['tests'])) {
$rows_num = count($this->form['tests']);
}
else if (isset($this->script[$fid]['tests'])) {
$rows_num = count($this->script[$fid]['tests']);
}
else {
$rows_num = 0;
}
// headers select
$select_header = new html_select(['name' => "_header[$id]", 'id' => 'header'.$id,
'onchange' => 'rule_header_select(' .$id .')', 'class' => 'custom-select']);
foreach ($this->headers as $index => $header) {
$header = $this->rc->text_exists($index) ? $this->plugin->gettext($index) : $header;
$select_header->add($header, $index);
}
$select_header->add($this->plugin->gettext('...'), '...');
if (in_array('body', $this->exts)) {
$select_header->add($this->plugin->gettext('body'), 'body');
}
$select_header->add($this->plugin->gettext('size'), 'size');
if (in_array('spamtest', $this->exts)) {
$select_header->add($this->plugin->gettext('spamtest'), 'spamtest');
}
if (in_array('date', $this->exts)) {
$select_header->add($this->plugin->gettext('datetest'), 'date');
$select_header->add($this->plugin->gettext('currdate'), 'currentdate');
}
if (in_array('variables', $this->exts)) {
$select_header->add($this->plugin->gettext('string'), 'string');
}
if (in_array('duplicate', $this->exts)) {
$select_header->add($this->plugin->gettext('message'), 'message');
}
$test = null;
if (isset($rule['test'])) {
if (in_array($rule['test'], ['header', 'address', 'envelope'])) {
if (is_array($rule['arg1']) && count($rule['arg1']) == 1) {
$rule['arg1'] = $rule['arg1'][0];
}
$header = !is_array($rule['arg1']) ? strtolower($rule['arg1']) : null;
$matches = !is_array($rule['arg1']) && $header && isset($this->headers[$header]);
$test = $matches ? $header : '...';
}
else if ($rule['test'] == 'exists') {
if (is_array($rule['arg']) && count($rule['arg']) == 1) {
$rule['arg'] = $rule['arg'][0];
}
$header = !is_array($rule['arg']) ? strtolower($rule['arg']) : null;
$matches = !is_array($rule['arg']) && $header && isset($this->headers[$header]);
$test = $matches ? $header : '...';
}
else if (in_array($rule['test'], ['size', 'spamtest', 'body', 'date', 'currentdate', 'string'])) {
$test = $rule['test'];
}
else if (in_array($rule['test'], ['duplicate'])) {
$test = 'message';
}
else if ($rule['test'] != 'true') {
$test = '...';
}
}
$tout = '<div class="flexbox">';
$aout = $select_header->show($test);
$custom = null;
$customv = null;
// custom headers input
if (isset($rule['test']) && in_array($rule['test'], ['header', 'address', 'envelope'])) {
$custom = (array) $rule['arg1'];
if (count($custom) == 1 && isset($this->headers[strtolower($custom[0])])) {
$custom = null;
}
}
else if (isset($rule['test']) && $rule['test'] == 'string') {
$customv = (array) $rule['arg1'];
if (count($customv) == 1 && isset($this->headers[strtolower($customv[0])])) {
$customv = null;
}
}
else if (isset($rule['test']) && $rule['test'] == 'exists') {
$custom = (array) $rule['arg'];
if (count($custom) == 1 && isset($this->headers[strtolower($custom[0])])) {
$custom = null;
}
}
// custom header and variable inputs
$aout .= $this->list_input($id, 'custom_header', $custom, 15, false, [
'disabled' => !isset($custom),
'class' => $this->error_class($id, 'test', 'header', 'custom_header'),
'placeholder' => $this->plugin->gettext('headername'),
'title' => $this->plugin->gettext('headername'),
]) . "\n";
$aout .= $this->list_input($id, 'custom_var', $customv, 15, false, [
'disabled' => !isset($customv),
'class' => $this->error_class($id, 'test', 'header', 'custom_var')
]) . "\n";
$test = self::rule_test($rule);
$target = '';
$sizetarget = null;
$sizeitem = null;
// target(s) input
if (in_array($rule['test'], ['header', 'address', 'envelope', 'string'])) {
$target = $rule['arg2'];
}
else if (in_array($rule['test'], ['body', 'date', 'currentdate', 'spamtest'])) {
$target = $rule['arg'];
}
else if ($rule['test'] == 'size') {
if (preg_match('/^([0-9]+)(K|M|G)?$/', $rule['arg'], $matches)) {
$sizetarget = $matches[1];
$sizeitem = $matches[2];
}
else {
$sizetarget = $rule['arg'];
$sizeitem = $rule['item'];
}
}
// (current)date part select
if (in_array('date', $this->exts) || in_array('currentdate', $this->exts)) {
$date_parts = ['date', 'iso8601', 'std11', 'julian', 'time',
'year', 'month', 'day', 'hour', 'minute', 'second', 'weekday', 'zone'];
$select_dp = new html_select([
'name' => "_rule_date_part[$id]",
'id' => 'rule_date_part'.$id,
'style' => in_array($rule['test'], ['currentdate', 'date']) && !preg_match('/^(notcount|count)-/', $test) ? '' : 'display:none',
'class' => 'datepart_selector custom-select',
]);
foreach ($date_parts as $part) {
$select_dp->add(rcube::Q($this->plugin->gettext($part)), $part);
}
$aout .= $select_dp->show($rule['test'] == 'currentdate' || $rule['test'] == 'date' ? $rule['part'] : '');
}
// message test select (e.g. duplicate)
if (in_array('duplicate', $this->exts)) {
$select_msg = new html_select([
'name' => "_rule_message[$id]",
'id' => 'rule_message'.$id,
'style' => in_array($rule['test'], ['duplicate']) ? '' : 'display:none',
'class' => 'message_selector custom-select',
]);
$select_msg->add(rcube::Q($this->plugin->gettext('duplicate')), 'duplicate');
$select_msg->add(rcube::Q($this->plugin->gettext('notduplicate')), 'notduplicate');
$tout .= $select_msg->show($test);
}
$tout .= $this->match_type_selector('rule_op', $id, $test, $rule['test']);
$tout .= $this->list_input($id, 'rule_target', $target, null, false, [
'disabled' => in_array($rule['test'], ['size', 'exists', 'duplicate', 'spamtest']),
'class' => $this->error_class($id, 'test', 'target', 'rule_target')
]) . "\n";
$select_size_op = new html_select([
'name' => "_rule_size_op[$id]",
'id' => 'rule_size_op'.$id,
'class' => 'input-group-prepend custom-select'
]);
$select_size_op->add(rcube::Q($this->plugin->gettext('filterover')), 'over');
$select_size_op->add(rcube::Q($this->plugin->gettext('filterunder')), 'under');
$select_size_item = new html_select([
'name' => "_rule_size_item[$id]",
'id' => 'rule_size_item'.$id,
'class' => 'input-group-append custom-select'
]);
foreach (['', 'K', 'M', 'G'] as $unit) {
$select_size_item->add($this->plugin->gettext($unit . 'B'), $unit);
}
$tout .= '<div id="rule_size' .$id. '" class="input-group" style="display:' . ($rule['test']=='size' ? 'inline' : 'none') .'">';
$tout .= $select_size_op->show($rule['test']=='size' ? $rule['type'] : '');
$tout .= html::tag('input', [
'type' => 'text',
'name' => "_rule_size_target[$id]",
'id' => 'rule_size_i'.$id,
'value' => $sizetarget,
'size' => 10,
'class' => $this->error_class($id, 'test', 'sizetarget', 'rule_size_i'),
]);
$tout .= "\n" . $select_size_item->show($sizeitem);
$tout .= '</div>';
$tout .= '</div>';
if (in_array('relational', $this->exts)) {
$select_spamtest_op = new html_select([
'name' => "_rule_spamtest_op[$id]",
'id' => 'rule_spamtest_op' . $id,
'class' => 'input-group-prepend custom-select',
'onchange' => 'rule_spamtest_select(' . $id .')'
]);
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestisunknown')), '');
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestisgreaterthan')), 'value-gt');
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestisgreaterthanequal')), 'value-ge');
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestislessthan')), 'value-lt');
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestislessthanequal')), 'value-le');
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestequals')), 'value-eq');
$select_spamtest_op->add(rcube::Q($this->plugin->gettext('spamtestnotequals')), 'value-ne');
$select_spamtest_target = new html_select([
'name' => "_rule_spamtest_target[$id]",
'id' => 'rule_spamtest_target' . $id,
'class' => 'input-group-append custom-select'
]);
$select_spamtest_target->add(rcube::Q("0%"), '1');
$select_spamtest_target->add(rcube::Q("20%"), '2');
$select_spamtest_target->add(rcube::Q("30%"), '3');
$select_spamtest_target->add(rcube::Q("40%"), '4');
$select_spamtest_target->add(rcube::Q("50%"), '5');
$select_spamtest_target->add(rcube::Q("60%"), '6');
$select_spamtest_target->add(rcube::Q("70%"), '7');
$select_spamtest_target->add(rcube::Q("80%"), '8');
$select_spamtest_target->add(rcube::Q("90%"), '9');
$select_spamtest_target->add(rcube::Q("100%"), '10');
$tout .= '<div id="rule_spamtest' . $id . '" class="input-group" style="display:' . ($rule['test'] == 'spamtest' ? 'inline' : 'none') .'">';
$tout .= $select_spamtest_op->show($rule['test'] == 'spamtest' && $target > 0 ? $rule['type'] : '');
$tout .= $select_spamtest_target->show($rule['test'] == 'spamtest' ? $target : '');
$tout .= '</div>';
}
// Advanced modifiers (address, envelope)
$select_mod = new html_select([
'name' => "_rule_mod[$id]",
'id' => 'rule_mod_op' . $id,
'class' => 'custom-select',
'onchange' => 'rule_mod_select(' .$id .')'
]);
$select_mod->add(rcube::Q($this->plugin->gettext('none')), '');
$select_mod->add(rcube::Q($this->plugin->gettext('address')), 'address');
if (in_array('envelope', $this->exts)) {
$select_mod->add(rcube::Q($this->plugin->gettext('envelope')), 'envelope');
}
$select_type = new html_select([
'name' => "_rule_mod_type[$id]",
'id' => 'rule_mod_type' . $id,
'class' => 'custom-select',
]);
$select_type->add(rcube::Q($this->plugin->gettext('allparts')), 'all');
$select_type->add(rcube::Q($this->plugin->gettext('domain')), 'domain');
$select_type->add(rcube::Q($this->plugin->gettext('localpart')), 'localpart');
if (in_array('subaddress', $this->exts)) {
$select_type->add(rcube::Q($this->plugin->gettext('user')), 'user');
$select_type->add(rcube::Q($this->plugin->gettext('detail')), 'detail');
}
$need_mod = !in_array($rule['test'], ['size', 'spamtest', 'body', 'date', 'currentdate', 'duplicate', 'string']);
$mout = '<div id="rule_mod' .$id. '" class="adv input-group"' . (!$need_mod ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('modifier'))));
$mout .= $select_mod->show($rule['test']);
$mout .= '</div>';
$mout .= '<div id="rule_mod_type' . $id . '" class="adv input-group"';
$mout .= (!in_array($rule['test'], ['address', 'envelope']) ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('modtype'))));
$mout .= $select_type->show(isset($rule['part']) ? $rule['part'] : null);
$mout .= '</div>';
// Advanced modifiers (comparators)
$need_comp = $rule['test'] != 'size' && $rule['test'] != 'spamtest' && $rule['test'] != 'duplicate';
$mout .= '<div id="rule_comp' .$id. '" class="adv input-group"' . (!$need_comp ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('comparator'))));
$mout .= $this->comparator_selector(isset($rule['comparator']) ? $rule['comparator'] : null, 'rule_comp', $id);
$mout .= '</div>';
// Advanced modifiers (mime)
if (in_array('mime', $this->exts)) {
$need_mime = !$rule || in_array($rule['test'], ['header', 'address', 'exists']);
$mime_type = '';
$select_mime = new html_select([
'name' => "_rule_mime_type[$id]",
'id' => 'rule_mime_type' . $id,
'style' => 'min-width:8em', 'onchange' => 'rule_mime_select(' . $id . ')',
'class' => 'custom-select',
]);
$select_mime->add('-', '');
foreach (['contenttype', 'type', 'subtype', 'param'] as $val) {
if (isset($rule['mime-' . $val])) {
$mime_type = $val;
}
$select_mime->add(rcube::Q($this->plugin->gettext('mime-' . $val)), $val);
}
$select_mime_part = new html_select([
'name' => "_rule_mime_part[$id]",
'id' => 'rule_mime_part' . $id,
'class' => 'custom-select',
]);
$select_mime_part->add(rcube::Q($this->plugin->gettext('mime-message')), '');
$select_mime_part->add(rcube::Q($this->plugin->gettext('mime-anychild')), 'anychild');
$mout .= '<div id="rule_mime_part' .$id. '" class="adv input-group"' . (!$need_mime ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('mimepart'))));
$mout .= $select_mime_part->show(!empty($rule['mime-anychild']) ? 'anychild' : '');
$mout .= '</div>';
$mout .= '<div id="rule_mime' .$id. '" class="adv input-group"' . (!$need_mime ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('mime'))));
$mout .= $select_mime->show($mime_type);
$mout .= $this->list_input($id, 'rule_mime_param', isset($rule['mime-param']) ? $rule['mime-param'] : null,
30, $mime_type != 'param', ['class' => $this->error_class($id, 'test', 'mime_param', 'rule_mime_param')]
);
$mout .= '</div>';
}
// Advanced modifiers (body transformations)
$select_mod = new html_select([
'name' => "_rule_trans[$id]",
'id' => 'rule_trans_op' . $id,
'class' => 'custom-select',
'onchange' => 'rule_trans_select(' .$id .')'
]);
$select_mod->add(rcube::Q($this->plugin->gettext('text')), 'text');
$select_mod->add(rcube::Q($this->plugin->gettext('undecoded')), 'raw');
$select_mod->add(rcube::Q($this->plugin->gettext('contenttype')), 'content');
$rule_content = '';
if (isset($rule['content'])) {
$rule_content = is_array($rule['content']) ? implode(',', $rule['content']) : $rule['content'];
}
$mout .= '<div id="rule_trans' .$id. '" class="adv input-group"' . ($rule['test'] != 'body' ? ' style="display:none"' : '') . '>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('modifier'))));
$mout .= $select_mod->show(isset($rule['part']) ? $rule['part'] : null);
$mout .= html::tag('input', [
'type' => 'text',
'name' => "_rule_trans_type[$id]",
'id' => 'rule_trans_type'.$id,
'value' => $rule_content,
'size' => 20,
'style' => !isset($rule['part']) || $rule['part'] != 'content' ? 'display:none' : '',
'class' => $this->error_class($id, 'test', 'part', 'rule_trans_type'),
]);
$mout .= '</div>';
// Date header
if (in_array('date', $this->exts)) {
$mout .= '<div id="rule_date_header_div' .$id. '" class="adv input-group"'. ($rule['test'] != 'date' ? ' style="display:none"' : '') .'>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('dateheader'))));
$mout .= html::tag('input', [
'type' => 'text',
'name' => "_rule_date_header[$id]",
'id' => 'rule_date_header' . $id,
'value' => $rule['test'] == 'date' ? $rule['header'] : '',
'size' => 15,
'class' => $this->error_class($id, 'test', 'dateheader', 'rule_date_header'),
]);
$mout .= '</div>';
}
// Index
if (in_array('index', $this->exts)) {
$need_index = in_array($rule['test'], ['header', ', address', 'date']);
$mout .= '<div id="rule_index_div' .$id. '" class="adv input-group"'. (!$need_index ? ' style="display:none"' : '') .'>';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('index'))));
$mout .= html::tag('input', [
'type' => 'text',
'name' => "_rule_index[$id]",
'id' => 'rule_index' . $id,
'value' => !empty($rule['index']) ? intval($rule['index']) : '',
'size' => 3,
'class' => $this->error_class($id, 'test', 'index', 'rule_index'),
]);
$mout .= html::label('input-group-append',
html::tag('input', [
'type' => 'checkbox',
'name' => "_rule_index_last[$id]",
'id' => 'rule_index_last' . $id,
'value' => 1,
'checked' => !empty($rule['last']),
]) . rcube::Q($this->plugin->gettext('indexlast')));
$mout .= '</div>';
}
// Duplicate
if (in_array('duplicate', $this->exts)) {
$need_duplicate = $rule['test'] == 'duplicate';
$mout .= '<div id="rule_duplicate_div' .$id. '" class="adv"'. (!$need_duplicate ? ' style="display:none"' : '') .'>';
foreach (['handle', 'header', 'uniqueid'] as $unit) {
$mout .= '<div class="input-group">';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('duplicate.' . $unit))));
$mout .= html::tag('input', [
'type' => 'text',
'name' => '_rule_duplicate_' . $unit . "[$id]",
'id' => 'rule_duplicate_' . $unit . $id,
'value' => isset($rule[$unit]) ? $rule[$unit] : '',
'size' => 30,
'class' => $this->error_class($id, 'test', 'duplicate_' . $unit, 'rule_duplicate_' . $unit),
]);
$mout .= '</div>';
}
$mout .= '<div class="input-group">';
$mout .= html::span('label input-group-prepend', html::span('input-group-text', rcube::Q($this->plugin->gettext('duplicate.seconds'))));
$mout .= html::tag('input', [
'type' => 'text',
'name' => "_rule_duplicate_seconds[$id]",
'id' => 'rule_duplicate_seconds' . $id,
'value' => isset($rule['seconds']) ? $rule['seconds'] : '',
'size' => 6,
'class' => $this->error_class($id, 'test', 'duplicate_seconds', 'rule_duplicate_seconds'),
]);
$mout .= html::label('input-group-append',
html::tag('input', [
'type' => 'checkbox',
'name' => "_rule_duplicate_last[$id]",
'id' => 'rule_duplicate_last' . $id,
'value' => 1,
'checked' => !empty($rule['last']),
]) . rcube::Q($this->plugin->gettext('duplicate.last')));
$mout .= '</div>';
$mout .= '</div>';
}
$add_title = rcube::Q($this->plugin->gettext('add'));
$del_title = rcube::Q($this->plugin->gettext('del'));
$adv_title = rcube::Q($this->plugin->gettext('advancedopts'));
// Build output table
$out = $div ? '<div class="rulerow" id="rulerow' .$id .'">'."\n" : '';
$out .= '<table class="compact-table"><tr>';
if (!$compact) {
$out .= '<td class="advbutton">';
$out .= sprintf('<a href="#" id="ruleadv%s" title="%s" onclick="rule_adv_switch(%s, this); return false" class="show">'
. '<span class="inner">%s</span></a>', $id, $adv_title, $id, $adv_title);
$out .= '</td>';
}
$out .= '<td class="rowactions"><div class="flexbox">' . $aout . '</div></td>';
$out .= '<td class="rowtargets">' . $tout . "\n";
$out .= '<div id="rule_advanced' .$id. '" style="display:none" class="advanced">' . $mout . '</div>';
$out .= '</td>';
$out .= '<td class="rowbuttons">';
if ($compact) {
$out .= sprintf('<a href="#" id="ruleadv%s" title="%s" onclick="rule_adv_switch(%s, this); return false" class="advanced show">'
. '<span class="inner">%s</span></a>', $id, $adv_title, $id, $adv_title);
}
$out .= sprintf('<a href="#" id="ruleadd%s" title="%s" onclick="rcmail.managesieve_ruleadd(\'%s\'); return false" class="button create add">'
. '<span class="inner">%s</span></a>', $id, $add_title, $id, $add_title);
$out .= sprintf('<a href="#" id="ruledel%s" title="%s" onclick="rcmail.managesieve_ruledel(\'%s\'); return false" class="button delete del%s">'
. '<span class="inner">%s</span></a>', $id, $del_title, $id, ($rows_num < 2 ? ' disabled' : ''), $del_title);
$out .= '</td>';
$out .= '</tr></table>';
$out .= $div ? "</div>\n" : '';
return $out;
}
private static function rule_test(&$rule)
{
// first modify value/count tests with 'not' keyword
// we'll revert the meaning of operators
if (!empty($rule['not']) && !empty($rule['type'])
&& preg_match('/^(count|value)-([gteqnl]{2})/', $rule['type'], $m)
) {
$rule['not'] = false;
switch ($m[2]) {
case 'gt': $rule['type'] = $m[1] . '-le'; break;
case 'ge': $rule['type'] = $m[1] . '-lt'; break;
case 'lt': $rule['type'] = $m[1] . '-ge'; break;
case 'le': $rule['type'] = $m[1] . '-gt'; break;
case 'eq': $rule['type'] = $m[1] . '-ne'; break;
case 'ne': $rule['type'] = $m[1] . '-eq'; break;
}
}
else if (!empty($rule['not']) && !empty($rule['test']) && $rule['test'] == 'size') {
$rule['not'] = false;
$rule['type'] = $rule['type'] == 'over' ? 'under' : 'over';
}
$set = ['header', 'address', 'envelope', 'body', 'date', 'currentdate', 'string'];
$test = null;
// build test string supported by select element
if (!empty($rule['size'])) {
$test = $rule['type'];
}
else if (!empty($rule['test']) && in_array($rule['test'], $set)) {
$test = (!empty($rule['not']) ? 'not' : '') . ($rule['type'] ?: 'is');
}
else if (!empty($rule['test'])) {
$test = (!empty($rule['not']) ? 'not' : '') . $rule['test'];
}
return $test;
}
function action_div($fid, $id, $div = true)
{
if (isset($id) && isset($this->form)) {
$action = $this->form['actions'][$id];
}
else if (isset($id) && isset($this->script[$fid]['actions'][$id])) {
$action = $this->script[$fid]['actions'][$id];
}
else {
$action = ['type' => null];
}
if (isset($this->form['actions'])) {
$rows_num = count($this->form['actions']);
}
else if (isset($this->script[$fid]['actions'])) {
$rows_num = count($this->script[$fid]['actions']);
}
else {
$rows_num = 0;
}
$out = $div ? '<div class="actionrow" id="actionrow' .$id .'">'."\n" : '';
$out .= '<table class="compact-table"><tr><td class="rowactions">';
// action select
$select_action = new html_select([
'name' => "_action_type[$id]",
'id' => 'action_type' . $id,
'class' => 'custom-select',
'onchange' => "action_type_select($id)"
]);
if (in_array('fileinto', $this->exts)) {
$select_action->add($this->plugin->gettext('messagemoveto'), 'fileinto');
}
if (in_array('fileinto', $this->exts) && in_array('copy', $this->exts)) {
$select_action->add($this->plugin->gettext('messagecopyto'), 'fileinto_copy');
}
if ($action['type'] == 'redirect' || !in_array('redirect', $this->disabled_actions)) {
$select_action->add($this->plugin->gettext('messageredirect'), 'redirect');
if (in_array('copy', $this->exts)) {
$select_action->add($this->plugin->gettext('messagesendcopy'), 'redirect_copy');
}
}
if (in_array('reject', $this->exts)) {
$select_action->add($this->plugin->gettext('messagediscard'), 'reject');
}
else if (in_array('ereject', $this->exts)) {
$select_action->add($this->plugin->gettext('messagediscard'), 'ereject');
}
if (in_array('vacation', $this->exts)) {
$select_action->add($this->plugin->gettext('messagereply'), 'vacation');
}
$select_action->add($this->plugin->gettext('messagedelete'), 'discard');
if (in_array('imapflags', $this->exts) || in_array('imap4flags', $this->exts)) {
$select_action->add($this->plugin->gettext('setflags'), 'setflag');
$select_action->add($this->plugin->gettext('addflags'), 'addflag');
$select_action->add($this->plugin->gettext('removeflags'), 'removeflag');
}
if (in_array('editheader', $this->exts)) {
$select_action->add($this->plugin->gettext('addheader'), 'addheader');
$select_action->add($this->plugin->gettext('deleteheader'), 'deleteheader');
}
if (in_array('variables', $this->exts)) {
$select_action->add($this->plugin->gettext('setvariable'), 'set');
}
if (in_array('enotify', $this->exts) || in_array('notify', $this->exts)) {
$select_action->add($this->plugin->gettext('notify'), 'notify');
}
$select_action->add($this->plugin->gettext('messagekeep'), 'keep');
$select_action->add($this->plugin->gettext('rulestop'), 'stop');
$select_type = $action['type'];
if (in_array($action['type'], ['fileinto', 'redirect']) && !empty($action['copy'])) {
$select_type .= '_copy';
}
$out .= $select_action->show($select_type);
$out .= '</td>';
// actions target inputs
$out .= '<td class="rowtargets">';
// force domain selection in redirect email input
$domains = (array) $this->rc->config->get('managesieve_domains');
if (!empty($domains)) {
sort($domains);
$domain_select = new html_select([
'name' => "_action_target_domain[$id]",
'id' => 'action_target_domain' . $id,
'class' => 'custom-select',
]);
$domain_select->add(array_combine($domains, $domains));
if ($action['type'] == 'redirect') {
$parts = explode('@', $action['target']);
if (!empty($parts)) {
$action['domain'] = array_pop($parts);
$action['target'] = implode('@', $parts);
}
}
}
// redirect target
$out .= '<span id="redirect_target' . $id . '" class="input-group" style="white-space:nowrap;'
. ' display:' . ($action['type'] == 'redirect' ? '' : 'none') . '">'
. html::tag('input', [
'type' => 'text',
'name' => "_action_target[$id]",
'id' => 'action_target' . $id,
'value' => $action['type'] == 'redirect' ? $action['target'] : '',
'size' => !empty($domains) ? 20 : 35,
'class' => $this->error_class($id, 'action', 'target', 'action_target'),
]);
$out .= isset($domain_select) ? '<span class="input-group-append input-group-prepend">'
. ' <span class="input-group-text">@</span> </span>'
. $domain_select->show(!empty($action['domain']) ? $action['domain'] : '') : '';
$out .= '</span>';
// (e)reject target
$out .= html::tag('textarea', [
'name' => '_action_target_area[' . $id . ']',
'id' => 'action_target_area' . $id,
'rows' => 3,
'cols' => 35,
'class' => $this->error_class($id, 'action', 'targetarea', 'action_target_area'),
'style' => 'display:' . (in_array($action['type'], ['reject', 'ereject']) ? 'inline' : 'none'),
],
(in_array($action['type'], ['reject', 'ereject']) ? rcube::Q($action['target'], 'strict', false) : '')
);
// vacation
$vsec = in_array('vacation-seconds', $this->exts);
$auto_addr = $this->rc->config->get('managesieve_vacation_addresses_init');
$from_addr = $this->rc->config->get('managesieve_vacation_from_init');
if (empty($action)) {
if ($auto_addr) {
$action['addresses'] = $this->user_emails();
}
if ($from_addr) {
$default_identity = $this->rc->user->list_emails(true);
$action['from'] = format_email_recipient($default_identity['email'], $default_identity['name']);
}
}
else if (!empty($action['from'])) {
$from = rcube_mime::decode_address_list($action['from'], null, true, RCUBE_CHARSET);
foreach ((array) $from as $idx => $addr) {
$from[$idx] = format_email_recipient($addr['mailto'], $addr['name']);
}
if (!empty($from)) {
$action['from'] = implode(', ', $from);
}
}
$action_subject = '';
if (isset($action['subject'])) {
$action_subject = is_array($action['subject']) ? implode(', ', $action['subject']) : $action['subject'];
}
$out .= '<div id="action_vacation' .$id.'" style="display:' .($action['type'] == 'vacation' ? 'inline' : 'none') .'" class="composite">';
$out .= '<span class="label">'. rcube::Q($this->plugin->gettext('vacationreason')) .'</span><br>';
$out .= html::tag('textarea', [
'name' => "_action_reason[$id]",
'id' => 'action_reason' . $id,
'rows' => 3,
'cols' => 35,
'class' => $this->error_class($id, 'action', 'reason', 'action_reason'),
], rcube::Q(isset($action['reason']) ? $action['reason'] : '', 'strict', false));
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('vacationsubject')) . '</span><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_subject[$id]",
'id' => 'action_subject' . $id,
'value' => $action_subject,
'size' => 35,
'class' => $this->error_class($id, 'action', 'subject', 'action_subject'),
]);
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('vacationfrom')) . '</span><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_from[$id]",
'id' => 'action_from' . $id,
'value' => isset($action['from']) ? $action['from'] : '',
'size' => 35,
'class' => $this->error_class($id, 'action', 'from', 'action_from'),
]);
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('vacationaddr')) . '</span><br>';
$out .= $this->list_input($id, 'action_addresses', isset($action['addresses']) ? $action['addresses'] : null,
30, false, ['class' => $this->error_class($id, 'action', 'addresses', 'action_addresses')]
)
. html::a(['href' => '#', 'onclick' => rcmail_output::JS_OBJECT_NAME . ".managesieve_vacation_addresses($id)"],
rcube::Q($this->plugin->gettext('filladdresses')));
$out .= '<br><span class="label">' . rcube::Q($this->plugin->gettext('vacationinterval')) . '</span><br>';
$out .= '<div class="input-group">' . html::tag('input', [
'type' => 'text',
'name' => "_action_interval[$id]",
'id' => 'action_interval' . $id,
'value' => rcube_sieve_vacation::vacation_interval($action, $this->exts),
'size' => 2,
'class' => $this->error_class($id, 'action', 'interval', 'action_interval'),
]);
if ($vsec) {
$interval_select = new html_select([
'name' => "_action_interval_type[$id]",
'class' => 'input-group-append custom-select'
]);
$interval_select->add($this->plugin->gettext('days'), 'days');
$interval_select->add($this->plugin->gettext('seconds'), 'seconds');
$out .= $interval_select->show(isset($action['seconds']) ? 'seconds' : 'days');
}
else {
$out .= "\n" . html::span('input-group-append', html::span('input-group-text', $this->plugin->gettext('days')));
}
$out .= '</div></div>';
// flags
$flags = [
'read' => '\\Seen',
'answered' => '\\Answered',
'flagged' => '\\Flagged',
'deleted' => '\\Deleted',
'draft' => '\\Draft',
];
$flags_target = isset($action['target']) ? (array) $action['target'] : [];
$custom_flags = [];
$is_flag_action = preg_match('/^(set|add|remove)flag$/', $action['type']);
if ($is_flag_action) {
$custom_flags = array_filter($flags_target, function($v) use($flags) {
return !in_array_nocase($v, $flags);
});
}
$flout = '';
foreach ($flags as $fidx => $flag) {
$flout .= html::label(null, html::tag('input', [
'type' => 'checkbox',
'name' => "_action_flags[$id][]",
'value' => $flag,
'checked' => $is_flag_action && in_array_nocase($flag, $flags_target),
])
. rcube::Q($this->plugin->gettext('flag'.$fidx))) . '<br>';
}
$flout .= $this->list_input($id, 'action_flags', $custom_flags, null, false, [
'class' => $this->error_class($id, 'action', 'flag', 'action_flags_flag'),
'id' => "action_flags_flag{$id}"
]);
$out .= html::div([
'id' => 'action_flags' . $id,
'style' => 'display:' . ($is_flag_action ? 'inline' : 'none'),
'class' => trim('checklist ' . $this->error_class($id, 'action', 'flags', 'action_flags')),
], $flout);
// set variable
$set_modifiers = [
'lower',
'upper',
'lowerfirst',
'upperfirst',
'quotewildcard',
'length'
];
$out .= '<div id="action_set' .$id.'" style="display:' .($action['type'] == 'set' ? 'inline' : 'none') .'">';
foreach (['name', 'value'] as $unit) {
$out .= '<span class="label">' .rcube::Q($this->plugin->gettext('setvar' . $unit)) . '</span><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => '_action_var' . $unit . '[' . $id . ']',
'id' => 'action_var' . $unit . $id,
'value' => isset($action[$unit]) ? $action[$unit] : '',
'size' => 35,
'class' => $this->error_class($id, 'action', $unit, 'action_var' . $unit),
]);
$out .= '<br>';
}
$out .= '<span class="label">' .rcube::Q($this->plugin->gettext('setvarmodifiers')) . '</span>';
foreach ($set_modifiers as $s_m) {
$s_m_id = 'action_varmods' . $id . $s_m;
$out .= '<br>' . html::tag('input', [
'type' => 'checkbox',
'name' => "_action_varmods[$id][]",
'value' => $s_m,
'id' => $s_m_id,
'checked' => array_key_exists($s_m, (array)$action) && !empty($action[$s_m]),
])
.rcube::Q($this->plugin->gettext('var' . $s_m));
}
$out .= '</div>';
// notify
$notify_methods = (array) $this->rc->config->get('managesieve_notify_methods');
$importance_options = $this->notify_importance_options;
if (empty($notify_methods)) {
$notify_methods = $this->notify_methods;
}
$method = $target = '';
if (!empty($action['method'])) {
list($method, $target) = explode(':', $action['method'], 2);
$method = strtolower($method);
}
if ($method && !in_array($method, $notify_methods)) {
$notify_methods[] = $method;
}
$select_method = new html_select([
'name' => "_action_notifymethod[$id]",
'id' => "_action_notifymethod$id",
'class' => 'input-group-prepend custom-select ' . $this->error_class($id, 'action', 'method', 'action_notifymethod'),
]);
foreach ($notify_methods as $m_n) {
$select_method->add(rcube::Q($this->rc->text_exists('managesieve.notifymethod'.$m_n) ? $this->plugin->gettext('managesieve.notifymethod'.$m_n) : $m_n), $m_n);
}
$select_importance = new html_select([
'name' => "_action_notifyimportance[$id]",
'id' => "_action_notifyimportance$id",
'class' => 'custom-select ' . $this->error_class($id, 'action', 'importance', 'action_notifyimportance')
]);
foreach ($importance_options as $io_v => $io_n) {
$select_importance->add(rcube::Q($this->plugin->gettext($io_n)), $io_v);
}
// @TODO: nice UI for mailto: (other methods too) URI parameters
$out .= '<div id="action_notify' .$id.'" style="display:' .($action['type'] == 'notify' ? 'inline' : 'none') .'" class="composite">';
$out .= '<span class="label">' .rcube::Q($this->plugin->gettext('notifytarget')) . '</span><br>';
$out .= '<div class="input-group">';
$out .= $select_method->show($method);
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_notifytarget[$id]",
'id' => 'action_notifytarget' . $id,
'value' => $target,
'size' => 25,
'class' => $this->error_class($id, 'action', 'target', 'action_notifytarget'),
]);
$out .= '</div>';
$out .= '<br><span class="label">'. rcube::Q($this->plugin->gettext('notifymessage')) .'</span><br>';
$out .= html::tag('textarea', [
'name' => "_action_notifymessage[$id]",
'id' => 'action_notifymessage' . $id,
'rows' => 3,
'cols' => 35,
'class' => $this->error_class($id, 'action', 'message', 'action_notifymessage'),
], isset($action['message']) ? rcube::Q($action['message'], 'strict', false) : ''
);
if (in_array('enotify', $this->exts)) {
$out .= '<br><span class="label">' .rcube::Q($this->plugin->gettext('notifyfrom')) . '</span><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_notifyfrom[$id]",
'id' => 'action_notifyfrom' . $id,
'value' => isset($action['from']) ? $action['from'] : '',
'size' => 35,
'class' => $this->error_class($id, 'action', 'from', 'action_notifyfrom'),
]);
}
$out .= '<br><span class="label">' . rcube::Q($this->plugin->gettext('notifyimportance')) . '</span><br>';
$out .= $select_importance->show(!empty($action['importance']) ? (int) $action['importance'] : 2);
$out .= '<div id="action_notifyoption_div' . $id . '">'
. '<span class="label">' . rcube::Q($this->plugin->gettext('notifyoptions')) . '</span><br>'
. $this->list_input($id, 'action_notifyoption', !empty($action['options']) ? (array) $action['options'] : [],
30, false, ['class' => $this->error_class($id, 'action', 'options', 'action_notifyoption')]
) . '</div>';
$out .= '</div>';
if (in_array('editheader', $this->exts)) {
$action['pos'] = !empty($action['last']) ? 'last' : '';
$pos1_selector = new html_select([
'name' => "_action_addheader_pos[$id]",
'id' => "action_addheader_pos$id",
'class' => 'custom-select ' . $this->error_class($id, 'action', 'pos', 'action_addheader_pos')
]);
$pos1_selector->add($this->plugin->gettext('headeratstart'), '');
$pos1_selector->add($this->plugin->gettext('headeratend'), 'last');
$pos2_selector = new html_select([
'name' => "_action_delheader_pos[$id]",
'id' => "action_delheader_pos$id",
'class' => 'custom-select ' . $this->error_class($id, 'action', 'pos', 'action_delheader_pos')
]);
$pos2_selector->add($this->plugin->gettext('headerfromstart'), '');
$pos2_selector->add($this->plugin->gettext('headerfromend'), 'last');
// addheader
$out .= '<div id="action_addheader' .$id.'" style="display:' .($action['type'] == 'addheader' ? 'inline' : 'none') .'" class="composite">';
$out .= '<label class="label" for="action_addheader_name' . $id .'">' .rcube::Q($this->plugin->gettext('headername')) . '</label><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_addheader_name[$id]",
'id' => "action_addheader_name{$id}",
'value' => isset($action['name']) ? $action['name'] : '',
'size' => 35,
'class' => $this->error_class($id, 'action', 'name', 'action_addheader_name'),
]);
$out .= '<br><label class="label" for="action_addheader_value' . $id .'">'. rcube::Q($this->plugin->gettext('headervalue')) .'</label><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_addheader_value[$id]",
'id' => "action_addheader_value{$id}",
'value' => isset($action['value']) ? $action['value'] : '',
'size' => 35,
'class' => $this->error_class($id, 'action', 'value', 'action_addheader_value'),
]);
$out .= '<br><label class="label" for="action_addheader_pos' . $id .'">'. rcube::Q($this->plugin->gettext('headerpos')) .'</label><br>';
$out .= $pos1_selector->show($action['pos']);
$out .= '</div>';
// deleteheader
$out .= '<div id="action_deleteheader' .$id.'" style="display:' .($action['type'] == 'deleteheader' ? 'inline' : 'none') .'" class="composite">';
$out .= '<label class="label" for="action_delheader_name' . $id .'">' .rcube::Q($this->plugin->gettext('headername')) . '</label><br>';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_delheader_name[$id]",
'id' => "action_delheader_name{$id}",
'value' => isset($action['name']) ? $action['name'] : '',
'size' => 35,
'class' => $this->error_class($id, 'action', 'name', 'action_delheader_name'),
]);
$out .= '<br><label class="label" for="action_delheader_value' . $id .'">'. rcube::Q($this->plugin->gettext('headerpatterns')) .'</label><br>';
$out .= $this->list_input($id, 'action_delheader_value', isset($action['value']) ? $action['value'] : null,
null, false, ['class' => $this->error_class($id, 'action', 'value', 'action_delheader_value')]) . "\n";
$out .= '<br><div class="adv input-group">';
$out .= html::span('label input-group-prepend', html::label([
'class' => 'input-group-text', 'for' => 'action_delheader_op'.$id
], rcube::Q($this->plugin->gettext('headermatchtype'))));
$out .= $this->match_type_selector('action_delheader_op', $id, isset($action['match-type']) ? $action['match-type'] : null, null, 'basic');
$out .= '</div>';
$out .= '<div class="adv input-group">';
$out .= html::span('label input-group-prepend', html::label([
'class' => 'input-group-text', 'for' => 'action_delheader_comp_op'.$id
], rcube::Q($this->plugin->gettext('comparator'))));
$out .= $this->comparator_selector(isset($action['comparator']) ? $action['comparator'] : null, 'action_delheader_comp', $id);
$out .= '</div>';
$out .= '<br><label class="label" for="action_delheader_index' . $id .'">'. rcube::Q($this->plugin->gettext('headeroccurrence')) .'</label><br>';
$out .= '<div class="input-group">';
$out .= html::tag('input', [
'type' => 'text',
'name' => "_action_delheader_index[$id]",
'id' => "action_delheader_index{$id}",
'value' => !empty($action['index']) ? intval($action['index']) : '',
'size' => 5,
'class' => $this->error_class($id, 'action', 'index', 'action_delheader_index'),
]);
$out .= ' ' . $pos2_selector->show($action['pos']);
$out .= '</div></div>';
}
// mailbox select
$additional = [];
if ($action['type'] == 'fileinto' && isset($action['target'])) {
// make sure non-existing (or unsubscribed) mailbox is listed (#1489956)
if ($mailbox = $this->mod_mailbox($action['target'], 'out')) {
$additional = [$mailbox];
}
}
else {
$mailbox = '';
}
$select = rcmail_action::folder_selector([
'maxlength' => 100,
'name' => "_action_mailbox[$id]",
'id' => "action_mailbox{$id}",
'style' => 'display:'.(empty($action['type']) || $action['type'] == 'fileinto' ? 'inline' : 'none'),
'additional' => $additional,
]);
$out .= $select->show($mailbox);
$out .= '</td>';
// add/del buttons
$add_label = rcube::Q($this->plugin->gettext('add'));
$del_label = rcube::Q($this->plugin->gettext('del'));
$out .= '<td class="rowbuttons">';
$out .= sprintf('<a href="#" id="actionadd%s" title="%s" onclick="rcmail.managesieve_actionadd(%s)" class="button create add">'
. '<span class="inner">%s</span></a>', $id, $add_label, $id, $add_label);
$out .= sprintf('<a href="#" id="actiondel%s" title="%s" onclick="rcmail.managesieve_actiondel(%s)" class="button delete del%s">'
. '<span class="inner">%s</span></a>', $id, $del_label, $id, ($rows_num < 2 ? ' disabled' : ''), $del_label);
$out .= '</td>';
$out .= '</tr></table>';
$out .= $div ? "</div>\n" : '';
return $out;
}
/**
* Generates a numeric identifier for a filter
*/
protected function genid()
{
return preg_replace('/[^0-9]/', '', microtime(true));
}
/**
* Trims and makes safe an input value
*
* @param string|array $str Input value
* @param bool $allow_html Allow HTML tags in the value
* @param bool $trim Trim the value
*
* @return string|array
*/
protected function strip_value($str, $allow_html = false, $trim = true)
{
if (is_array($str)) {
foreach ($str as $idx => $val) {
$str[$idx] = $this->strip_value($val, $allow_html, $trim);
if ($str[$idx] === '') {
unset($str[$idx]);
}
}
return $str;
}
if (!$allow_html) {
$str = strip_tags($str);
}
return $trim ? trim($str) : $str;
}
/**
* Returns error class, if there's a form error "registered"
*/
protected function error_class($id, $type, $target, $elem_prefix = '')
{
// TODO: tooltips
if (
($type == 'test' && !empty($this->errors['tests'][$id][$target]))
|| ($type == 'action' && !empty($this->errors['actions'][$id][$target]))
) {
$str = $this->errors[$type == 'test' ? 'tests' : 'actions'][$id][$target];
$this->add_tip($elem_prefix . $id, $str, true);
return 'error';
}
return '';
}
protected function add_tip($id, $str, $error = false)
{
$class = $error ? 'sieve error' : '';
$this->tips[] = [$id, $class, $str];
}
protected function print_tips()
{
if (empty($this->tips)) {
return;
}
$script = rcmail_output::JS_OBJECT_NAME.'.managesieve_tip_register('.json_encode($this->tips).');';
$this->rc->output->add_script($script, 'docready');
}
protected function list_input($id, $name, $value, $size = null, $hidden = false, $attrib = [])
{
$value = (array) $value;
$value = array_map(['rcube', 'Q'], $value);
$value = implode("\n", $value);
$attrib = array_merge($attrib, [
'data-type' => 'list',
'data-size' => $size,
'data-hidden' => $hidden ?: null,
'name' => '_' . $name . '[' . $id . ']',
'style' => 'display:none',
]);
if (empty($attrib['id'])) {
$attrib['id'] = $name . $id;
}
return html::tag('textarea', $attrib, $value);
}
/**
* Validate input for date part elements
*/
protected function validate_date_part($type, $value)
{
// we do simple validation of date/part format
switch ($type) {
case 'date': // yyyy-mm-dd
return preg_match('/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/', $value);
case 'iso8601':
return preg_match('/^[0-9: .,ZWT+-]+$/', $value);
case 'std11':
return preg_match('/^((Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s+)?[0-9]{1,2}\s+'
. '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+[0-9]{2,4}\s+'
. '[0-9]{2}:[0-9]{2}(:[0-9]{2})?\s+([+-]*[0-9]{4}|[A-Z]{1,3})$/', $value);
case 'julian':
return preg_match('/^[0-9]+$/', $value);
case 'time': // hh:mm:ss
return preg_match('/^[0-9]{2}:[0-9]{2}:[0-9]{2}$/', $value);
case 'year':
return preg_match('/^[0-9]{4}$/', $value);
case 'month':
return preg_match('/^[0-9]{2}$/', $value) && $value > 0 && $value < 13;
case 'day':
return preg_match('/^[0-9]{2}$/', $value) && $value > 0 && $value < 32;
case 'hour':
return preg_match('/^[0-9]{2}$/', $value) && $value < 24;
case 'minute':
return preg_match('/^[0-9]{2}$/', $value) && $value < 60;
case 'second':
// According to RFC5260, seconds can be from 00 to 60
return preg_match('/^[0-9]{2}$/', $value) && $value < 61;
case 'weekday':
return preg_match('/^[0-9]$/', $value) && $value < 7;
case 'zone':
return preg_match('/^[+-][0-9]{4}$/', $value);
}
}
/**
* Converts mailbox name from/to UTF7-IMAP from/to internal Sieve encoding
* with delimiter replacement.
*
* @param string $mailbox Mailbox name
* @param string $mode Conversion direction ('in'|'out')
*
* @return string Mailbox name
*/
protected function mod_mailbox($mailbox, $mode = 'out')
{
$delimiter = $_SESSION['imap_delimiter'];
$replace_delimiter = $this->rc->config->get('managesieve_replace_delimiter');
$mbox_encoding = $this->rc->config->get('managesieve_mbox_encoding', 'UTF7-IMAP');
if ($mode == 'out') {
$mailbox = rcube_charset::convert($mailbox, $mbox_encoding, 'UTF7-IMAP');
if ($replace_delimiter && $replace_delimiter != $delimiter) {
$mailbox = str_replace($replace_delimiter, $delimiter, $mailbox);
}
}
else {
$mailbox = rcube_charset::convert($mailbox, 'UTF7-IMAP', $mbox_encoding);
if ($replace_delimiter && $replace_delimiter != $delimiter) {
$mailbox = str_replace($delimiter, $replace_delimiter, $mailbox);
}
}
return $mailbox;
}
/**
* List sieve scripts
*
* @return array Scripts list
*/
public function list_scripts()
{
if ($this->list !== null) {
return $this->list;
}
$this->list = $this->sieve->get_scripts();
// Handle active script(s) and list of scripts according to Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
// Skip protected names
foreach ((array) $this->list as $idx => $name) {
$_name = strtoupper($name);
if ($_name == 'MASTER') {
$master_script = $name;
}
else if ($_name == 'MANAGEMENT') {
$management_script = $name;
}
else if ($_name == 'USER') {
$user_script = $name;
}
else {
continue;
}
unset($this->list[$idx]);
}
// get active script(s), read USER script
if (!empty($user_script)) {
$extension = $this->rc->config->get('managesieve_filename_extension', '.sieve');
$filename_regex = '/'.preg_quote($extension, '/').'$/';
$_SESSION['managesieve_user_script'] = $user_script;
$this->sieve->load($user_script);
foreach ($this->sieve->script->as_array() as $rules) {
foreach ($rules['actions'] as $action) {
if ($action['type'] == 'include' && empty($action['global'])) {
$name = preg_replace($filename_regex, '', $action['target']);
// make sure the script exist
if (in_array($name, $this->list)) {
$this->active[] = $name;
}
}
}
}
}
// create USER script if it doesn't exist
else {
$content = "# USER Management Script\n"
."#\n"
."# This script includes the various active sieve scripts\n"
."# it is AUTOMATICALLY GENERATED. DO NOT EDIT MANUALLY!\n"
."#\n"
."# For more information, see http://wiki.kolab.org/KEP:14#USER\n"
."#\n";
if ($this->sieve->save_script('USER', $content)) {
$_SESSION['managesieve_user_script'] = 'USER';
if (empty($this->master_file)) {
$this->sieve->activate('USER');
}
}
}
}
else if (!empty($this->list)) {
// Get active script name
if ($active = $this->sieve->get_active()) {
$this->active = [$active];
}
// Hide scripts from config
$exceptions = $this->rc->config->get('managesieve_filename_exceptions');
if (!empty($exceptions)) {
$this->list = array_diff($this->list, (array)$exceptions);
}
}
// When no script listing allowed limit the list to the defined script
if (in_array('list_sets', $this->disabled_actions)) {
$script_name = $this->rc->config->get('managesieve_script_name', 'roundcube');
$this->list = array_intersect($this->list, [$script_name]);
$this->active = null;
if (in_array($script_name, $this->list)) {
// Because its the only allowed script make sure its active
$this->activate_script($script_name);
}
}
// reindex
if (!empty($this->list)) {
$this->list = array_values($this->list);
}
return $this->list;
}
/**
* Removes sieve script
*
* @param string $name Script name
*
* @return bool True on success, False on failure
*/
public function remove_script($name)
{
$result = $this->sieve->remove($name);
// Kolab's KEP:14
if ($result && $this->rc->config->get('managesieve_kolab_master')) {
$this->deactivate_script($name);
}
return $result;
}
/**
* Activates sieve script
*
* @param string $name Script name
*
* @return bool True on success, False on failure
*/
public function activate_script($name)
{
// Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
$extension = $this->rc->config->get('managesieve_filename_extension', '.sieve');
$user_script = $_SESSION['managesieve_user_script'];
$result = false;
// if the script is not active...
if ($user_script && array_search($name, (array) $this->active) === false) {
// ...rewrite USER file adding appropriate include command
if ($this->sieve->load($user_script)) {
$script = $this->sieve->script->as_array();
$list = [];
$regexp = '/' . preg_quote($extension, '/') . '$/';
// Create new include entry
$rule = [
'actions' => [
[
'target' => $name . $extension,
'type' => 'include',
'personal' => true,
]
]
];
// get all active scripts for sorting
foreach ($script as $rid => $rules) {
foreach ($rules['actions'] as $action) {
if ($action['type'] == 'include' && empty($action['global'])) {
$target = $extension ? preg_replace($regexp, '', $action['target']) : $action['target'];
$list[] = $target;
}
}
}
$list[] = $name;
// Sort and find current script position
asort($list, SORT_LOCALE_STRING);
$list = array_values($list);
$index = array_search($name, $list);
// add rule at the end of the script
if ($index === false || $index == count($list)-1) {
$this->sieve->script->add_rule($rule);
}
// add rule at index position
else {
$script2 = [];
foreach ($script as $rid => $rules) {
if ($rid == $index) {
$script2[] = $rule;
}
$script2[] = $rules;
}
$this->sieve->script->content = $script2;
}
$result = $this->sieve->save();
if ($result) {
$this->active[] = $name;
}
}
}
}
else {
$result = $this->sieve->activate($name);
if ($result) {
$this->active = [$name];
}
}
return $result;
}
/**
* Deactivates sieve script
*
* @param string $name Script name
*
* @return bool True on success, False on failure
*/
public function deactivate_script($name)
{
// Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
$extension = $this->rc->config->get('managesieve_filename_extension', '.sieve');
$user_script = $_SESSION['managesieve_user_script'];
$result = false;
// if the script is active...
if ($user_script && ($key = array_search($name, $this->active)) !== false) {
// ...rewrite USER file removing appropriate include command
if ($this->sieve->load($user_script)) {
$script = $this->sieve->script->as_array();
$name = $name.$extension;
$rid = 0;
foreach ($script as $rid => $rules) {
foreach ($rules['actions'] as $action) {
if ($action['type'] == 'include' && empty($action['global'])
&& $action['target'] == $name
) {
break 2;
}
}
}
// Entry found
if ($rid < count($script)) {
$this->sieve->script->delete_rule($rid);
$result = $this->sieve->save();
if ($result) {
unset($this->active[$key]);
}
}
}
}
}
else {
$result = $this->sieve->deactivate();
if ($result)
$this->active = [];
}
return $result;
}
/**
* Saves current script (adding some variables)
*/
public function save_script($name = null)
{
// Kolab's KEP:14
if ($this->rc->config->get('managesieve_kolab_master')) {
$this->sieve->script->set_var('EDITOR', self::PROGNAME);
$this->sieve->script->set_var('EDITOR_VERSION', self::VERSION);
}
return $this->sieve->save($name);
}
/**
* Returns list of rules from the current script
*
* @return array List of rules
*/
public function list_rules()
{
$result = [];
$i = 1;
foreach ($this->script as $idx => $filter) {
if (empty($filter['actions'])) {
continue;
}
$fname = !empty($filter['name']) ? $filter['name'] : "#$i";
$result[] = [
'id' => $idx,
'name' => $fname,
'class' => !empty($filter['disabled']) ? 'disabled' : '',
];
$i++;
}
return $result;
}
/**
* Initializes internal script data
*/
protected function init_script()
{
if (!$this->sieve->script) {
return;
}
$this->script = $this->sieve->script->as_array();
$headers = [];
$exceptions = ['date', 'currentdate', 'size', 'spamtest', 'body'];
// find common headers used in script, will be added to the list
// of available (predefined) headers (#1489271)
foreach ($this->script as $rule) {
- foreach ((array) $rule['tests'] as $test) {
+ foreach ((array) ($rule['tests'] ?? []) as $test) {
if ($test['test'] == 'header') {
foreach ((array) $test['arg1'] as $header) {
$lc_header = strtolower($header);
// skip special names to not confuse UI
if (in_array($lc_header, $exceptions)) {
continue;
}
if (!isset($this->headers[$lc_header]) && !isset($headers[$lc_header])) {
$headers[$lc_header] = $header;
}
}
}
}
}
ksort($headers);
$this->headers += $headers;
}
/**
* Get all e-mail addresses of the user
*/
protected function user_emails()
{
$addresses = $this->rc->user->list_emails();
foreach ($addresses as $idx => $email) {
$addresses[$idx] = $email['email'];
}
$addresses = array_unique($addresses);
sort($addresses);
return $addresses;
}
/**
* Convert configured default headers into internal format
*/
protected function get_default_headers()
{
$default = ['Subject', 'From', 'To'];
$headers = (array) $this->rc->config->get('managesieve_default_headers', $default);
$keys = array_map('strtolower', $headers);
$headers = array_combine($keys, $headers);
// make sure there's no Date header
unset($headers['date']);
return $headers;
}
/**
* Match type selector
*/
protected function match_type_selector($name, $id, $test, $rule = null, $mode = 'all')
{
// matching type select (operator)
$select_op = new html_select([
'name' => "_{$name}[$id]",
'id' => "{$name}{$id}",
'style' => 'display:' . (!in_array($rule, ['size', 'duplicate', 'spamtest']) ? 'inline' : 'none'),
'class' => 'operator_selector col-6 custom-select',
'onchange' => "{$name}_select(this, '{$id}')",
]);
$select_op->add(rcube::Q($this->plugin->gettext('filtercontains')), 'contains');
$select_op->add(rcube::Q($this->plugin->gettext('filternotcontains')), 'notcontains');
$select_op->add(rcube::Q($this->plugin->gettext('filteris')), 'is');
$select_op->add(rcube::Q($this->plugin->gettext('filterisnot')), 'notis');
if ($mode == 'all') {
$select_op->add(rcube::Q($this->plugin->gettext('filterexists')), 'exists');
$select_op->add(rcube::Q($this->plugin->gettext('filternotexists')), 'notexists');
}
$select_op->add(rcube::Q($this->plugin->gettext('filtermatches')), 'matches');
$select_op->add(rcube::Q($this->plugin->gettext('filternotmatches')), 'notmatches');
if (in_array('regex', $this->exts)) {
$select_op->add(rcube::Q($this->plugin->gettext('filterregex')), 'regex');
$select_op->add(rcube::Q($this->plugin->gettext('filternotregex')), 'notregex');
}
if ($mode == 'all' && in_array('relational', $this->exts)) {
$select_op->add(rcube::Q($this->plugin->gettext('countisgreaterthan')), 'count-gt');
$select_op->add(rcube::Q($this->plugin->gettext('countisgreaterthanequal')), 'count-ge');
$select_op->add(rcube::Q($this->plugin->gettext('countislessthan')), 'count-lt');
$select_op->add(rcube::Q($this->plugin->gettext('countislessthanequal')), 'count-le');
$select_op->add(rcube::Q($this->plugin->gettext('countequals')), 'count-eq');
$select_op->add(rcube::Q($this->plugin->gettext('countnotequals')), 'count-ne');
$select_op->add(rcube::Q($this->plugin->gettext('valueisgreaterthan')), 'value-gt');
$select_op->add(rcube::Q($this->plugin->gettext('valueisgreaterthanequal')), 'value-ge');
$select_op->add(rcube::Q($this->plugin->gettext('valueislessthan')), 'value-lt');
$select_op->add(rcube::Q($this->plugin->gettext('valueislessthanequal')), 'value-le');
$select_op->add(rcube::Q($this->plugin->gettext('valueequals')), 'value-eq');
$select_op->add(rcube::Q($this->plugin->gettext('valuenotequals')), 'value-ne');
}
return $select_op->show($test);
}
protected function comparator_selector($comparator, $name, $id)
{
$select_comp = new html_select([
'name' => "_{$name}[$id]",
'id' => "{$name}_op{$id}",
'class' => 'custom-select'
]);
$select_comp->add(rcube::Q($this->plugin->gettext('default')), '');
$select_comp->add(rcube::Q($this->plugin->gettext('octet')), 'i;octet');
$select_comp->add(rcube::Q($this->plugin->gettext('asciicasemap')), 'i;ascii-casemap');
if (in_array('comparator-i;ascii-numeric', $this->exts)) {
$select_comp->add(rcube::Q($this->plugin->gettext('asciinumeric')), 'i;ascii-numeric');
}
return $select_comp->show($comparator);
}
/**
* Merge a rule into the script
*/
protected function merge_rule($rule, $existing, &$script_name = null)
{
// if script does not exist create a new one
if ($script_name === null || $script_name === false) {
$script_name = $this->create_default_script();
$this->sieve->load($script_name);
$this->init_script();
}
if (!$this->sieve->script) {
return false;
}
$script_active = in_array($script_name, $this->active);
$rule_active = empty($rule['disabled']);
$rule_index = 0;
$activate_script = false;
// If the script is not active, but the rule is,
// put the rule in an active script if there is one
if (!$script_active && $rule_active && !empty($this->active)) {
// Remove the rule from current (inactive) script
if (isset($existing['idx'])) {
unset($this->script[$existing['idx']]);
$this->sieve->script->content = $this->script;
$this->save_script($script_name);
}
// Load and init the active script, add the rule there
$this->sieve->load($script_name = $this->active[0]);
$this->init_script();
array_unshift($this->script, $rule);
}
// update original forward rule/script
else {
// re-order rules if needed
if (isset($rule['after']) && $rule['after'] !== '') {
// unset the original rule
if (isset($existing['idx'])) {
$this->script[$existing['idx']] = null;
}
// add at target position
if ($rule['after'] >= count($this->script) - 1) {
$this->script[] = $rule;
$this->script = array_values(array_filter($this->script));
$rule_index = count($this->script);
}
else {
$script = [];
foreach ($this->script as $idx => $r) {
if ($r) {
$script[] = $r;
}
if ($idx == $rule['after']) {
$script[] = $rule;
$rule_index = count($script);
}
}
$this->script = $script;
}
}
// rule exists, update it "in place"
else if (isset($existing['idx'])) {
$this->script[$existing['idx']] = $rule;
$rule_index = $existing['idx'];
}
// otherwise put the rule on top
else {
array_unshift($this->script, $rule);
$rule_index = 0;
}
// if the script is not active, but the rule is, we need to de-activate
// all rules except the forward rule
if (!$script_active && $rule_active) {
$activate_script = true;
foreach ($this->script as $idx => $r) {
if ($idx !== $rule_index) {
$this->script[$idx]['disabled'] = true;
}
}
}
}
$this->sieve->script->content = $this->script;
// save the script
$saved = $this->save_script($script_name);
// activate the script
if ($saved && $activate_script) {
$this->activate_script($script_name);
}
return $saved;
}
/**
* Create default script
*/
protected function create_default_script()
{
// if script not exists build default script contents
$script_file = $this->rc->config->get('managesieve_default');
$script_name = $this->rc->config->get('managesieve_script_name');
$kolab_master = $this->rc->config->get('managesieve_kolab_master');
$content = '';
if (empty($script_name)) {
$script_name = 'roundcube';
}
if ($script_file && !$kolab_master && is_readable($script_file) && !is_dir($script_file)) {
$content = file_get_contents($script_file);
}
// add script and set it active
if ($this->sieve->save_script($script_name, $content)) {
$this->activate_script($script_name);
$this->list[] = $script_name;
}
return $script_name;
}
}
diff --git a/program/actions/contacts/search.php b/program/actions/contacts/search.php
index 1637cbfad..ea92fcefa 100644
--- a/program/actions/contacts/search.php
+++ b/program/actions/contacts/search.php
@@ -1,313 +1,313 @@
<?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: |
| Search action (and form) for address book contacts |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_search extends rcmail_action_contacts_index
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
if (empty($_GET['_form'])) {
self::contact_search();
}
$rcmail->output->add_handler('searchform', [$this, 'contact_search_form']);
$rcmail->output->send('contactsearch');
}
public static function contact_search()
{
$rcmail = rcmail::get_instance();
$adv = isset($_POST['_adv']);
$sid = rcube_utils::get_input_value('_sid', rcube_utils::INPUT_GET);
$search = null;
// get search criteria from saved search
if ($sid && ($search = $rcmail->user->get_search($sid))) {
$fields = $search['data']['fields'];
$search = $search['data']['search'];
}
// get fields/values from advanced search form
else if ($adv) {
foreach (array_keys($_POST) as $key) {
$s = trim(rcube_utils::get_input_value($key, rcube_utils::INPUT_POST, true));
if (strlen($s) && preg_match('/^_search_([a-zA-Z0-9_-]+)$/', $key, $m)) {
$search[] = $s;
$fields[] = $m[1];
}
}
if (empty($fields)) {
// do nothing, show the form again
return;
}
}
// quick-search
else {
$search = trim(rcube_utils::get_input_value('_q', rcube_utils::INPUT_GET, true));
$fields = rcube_utils::get_input_value('_headers', rcube_utils::INPUT_GET);
if (empty($fields)) {
$fields = array_keys(self::$SEARCH_MODS_DEFAULT);
}
else {
$fields = array_filter(explode(',', $fields));
}
// update search_mods setting
$old_mods = $rcmail->config->get('addressbook_search_mods');
$search_mods = array_fill_keys($fields, 1);
if ($old_mods != $search_mods) {
$rcmail->user->save_prefs(['addressbook_search_mods' => $search_mods]);
}
if (in_array('*', $fields)) {
$fields = '*';
}
}
// Values matching mode
$mode = (int) $rcmail->config->get('addressbook_search_mode');
$mode |= rcube_addressbook::SEARCH_GROUPS;
// get sources list
$sources = $rcmail->get_address_sources();
$sort_col = $rcmail->config->get('addressbook_sort_col', 'name');
$afields = $rcmail->config->get('contactlist_fields');
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
$search_set = [];
$records = [];
foreach ($sources as $s) {
$source = $rcmail->get_address_book($s['id']);
// check if search fields are supported....
if (is_array($fields)) {
- $cols = $source->coltypes[0] ? array_flip($source->coltypes) : $source->coltypes;
+ $cols = !empty($source->coltypes[0]) ? array_flip($source->coltypes) : $source->coltypes;
$supported = 0;
foreach ($fields as $f) {
if (array_key_exists($f, $cols)) {
$supported ++;
}
}
// in advanced search we require all fields (AND operator)
// in quick search we require at least one field (OR operator)
if (($adv && $supported < count($fields)) || (!$adv && !$supported)) {
continue;
}
}
// reset page
$source->set_page(1);
$source->set_pagesize(9999);
// get contacts count
$result = $source->search($fields, $search, $mode, false);
if (empty($result) || !$result->count) {
continue;
}
// get records
$result = $source->list_records($afields);
while ($row = $result->next()) {
$row['sourceid'] = $s['id'];
$key = rcube_addressbook::compose_contact_key($row, $sort_col);
$records[$key] = $row;
}
unset($result);
$search_set[$s['id']] = $source->get_search_set();
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
// cut first-page records
if ($page_size < $count) {
$records = array_slice($records, 0, $page_size);
}
$result->records = array_values($records);
// search request ID
$search_request = md5('addr'
. (is_array($fields) ? implode(',', $fields) : $fields)
. (is_array($search) ? implode(',', $search) : $search)
);
// save search settings in session
$_SESSION['contact_search'][$search_request] = $search_set;
$_SESSION['contact_search_params'] = ['id' => $search_request, 'data' => [$fields, $search]];
$_SESSION['page'] = 1;
if ($adv) {
$rcmail->output->command('list_contacts_clear');
}
if ($result->count > 0) {
// create javascript list
self::js_contacts_list($result);
$rcmail->output->show_message('contactsearchsuccessful', 'confirmation', ['nr' => $result->count]);
}
else {
$rcmail->output->show_message('nocontactsfound', 'notice');
}
// update message count display
$rcmail->output->set_env('search_request', $search_request);
$rcmail->output->set_env('pagecount', ceil($result->count / $page_size));
$rcmail->output->command('set_rowcount', self::get_rowcount_text($result));
// Re-set current source
$rcmail->output->set_env('search_id', $sid);
$rcmail->output->set_env('source', '');
$rcmail->output->set_env('group', '');
// Re-set list header
$rcmail->output->command('set_group_prop', null);
if (!$sid) {
// unselect currently selected directory/group
$rcmail->output->command('unselect_directory');
// enable "Save search" command
$rcmail->output->command('enable_command', 'search-create', true);
}
$rcmail->output->command('update_group_commands');
// send response
$rcmail->output->send();
}
public static function contact_search_form($attrib)
{
$rcmail = rcmail::get_instance();
$i_size = !empty($attrib['size']) ? $attrib['size'] : 30;
$short_labels = self::get_bool_attr($attrib, 'short-legend-labels');
$form = [
'main' => [
'name' => $rcmail->gettext('properties'),
'content' => [],
],
'personal' => [
'name' => $rcmail->gettext($short_labels ? 'personal' : 'personalinfo'),
'content' => [],
],
'other' => [
'name' => $rcmail->gettext('other'),
'content' => [],
],
];
// get supported coltypes from all address sources
$sources = $rcmail->get_address_sources();
$coltypes = [];
foreach ($sources as $s) {
$CONTACTS = $rcmail->get_address_book($s['id']);
if (!empty($CONTACTS->coltypes)) {
$contact_cols = isset($CONTACTS->coltypes[0]) ? array_flip($CONTACTS->coltypes) : $CONTACTS->coltypes;
$coltypes = array_merge($coltypes, $contact_cols);
}
}
// merge supported coltypes with global coltypes
foreach ($coltypes as $col => $colprop) {
if (!empty(rcmail_action_contacts_index::$CONTACT_COLTYPES[$col])) {
$coltypes[$col] = array_merge(rcmail_action_contacts_index::$CONTACT_COLTYPES[$col], (array) $colprop);
}
else {
$coltypes[$col] = (array) $colprop;
}
}
// build form fields list
foreach ($coltypes as $col => $colprop) {
if (!isset($colprop['type'])) {
$colprop['type'] = 'text';
}
if ($colprop['type'] != 'image' && empty($colprop['nosearch'])) {
$ftype = $colprop['type'] == 'select' ? 'select' : 'text';
$label = isset($colprop['label']) ? $colprop['label'] : $rcmail->gettext($col);
$category = !empty($colprop['category']) ? $colprop['category'] : 'other';
// load jquery UI datepicker for date fields
if ($colprop['type'] == 'date') {
$colprop['class'] = (!empty($colprop['class']) ? $colprop['class'] . ' ' : '') . 'datepicker';
}
else if ($ftype == 'text') {
$colprop['size'] = $i_size;
}
$colprop['id'] = '_search_' . $col;
$content = html::div('row',
html::label(['class' => 'contactfieldlabel label', 'for' => $colprop['id']], rcube::Q($label))
. html::div('contactfieldcontent', rcube_output::get_edit_field('search_' . $col, '', $colprop, $ftype))
);
$form[$category]['content'][] = $content;
}
}
$hiddenfields = new html_hiddenfield();
$hiddenfields->add(['name' => '_adv', 'value' => 1]);
$out = $rcmail->output->request_form([
'name' => 'form',
'method' => 'post',
'task' => $rcmail->task,
'action' => 'search',
'noclose' => true,
] + $attrib, $hiddenfields->show()
);
$rcmail->output->add_gui_object('editform', $attrib['id']);
unset($attrib['name']);
unset($attrib['id']);
foreach ($form as $f) {
if (!empty($f['content'])) {
$content = html::div('contactfieldgroup', join("\n", $f['content']));
$legend = html::tag('legend', null, rcube::Q($f['name']));
$out .= html::tag('fieldset', $attrib, $legend . $content) . "\n";
}
}
return $out . '</form>';
}
}
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index d4e1c7222..47c3fad85 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -1,4686 +1,4686 @@
<?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: |
| IMAP Storage Engine |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Interface class for accessing an IMAP server
*
* @package Framework
* @subpackage Storage
*/
class rcube_imap extends rcube_storage
{
/**
* Instance of rcube_imap_generic
*
* @var rcube_imap_generic
*/
public $conn;
/**
* Instance of rcube_imap_cache
*
* @var rcube_imap_cache
*/
protected $mcache;
/**
* Instance of rcube_cache
*
* @var rcube_cache
*/
protected $cache;
protected $plugins;
protected $delimiter;
protected $namespace;
protected $struct_charset;
protected $search_set;
protected $search_string = '';
protected $search_charset = '';
protected $search_sort_field = '';
protected $search_threads = false;
protected $search_sorted = false;
protected $sort_field = '';
protected $sort_order = 'DESC';
protected $options = ['auth_type' => 'check', 'skip_deleted' => false];
protected $caching = false;
protected $messages_caching = false;
protected $threading = false;
protected $connect_done = false;
protected $list_excludes = [];
protected $list_root;
protected $msg_uid;
protected $sort_folder_collator;
/**
* Object constructor.
*/
public function __construct()
{
$this->conn = new rcube_imap_generic();
$this->plugins = rcube::get_instance()->plugins;
// Set namespace and delimiter from session,
// so some methods would work before connection
if (isset($_SESSION['imap_namespace'])) {
$this->namespace = $_SESSION['imap_namespace'];
}
if (isset($_SESSION['imap_delimiter'])) {
$this->delimiter = $_SESSION['imap_delimiter'];
}
if (!empty($_SESSION['imap_list_conf'])) {
list($this->list_root, $this->list_excludes) = $_SESSION['imap_list_conf'];
}
}
/**
* Magic getter for backward compat.
*
* @deprecated
*/
public function __get($name)
{
if (isset($this->{$name})) {
return $this->{$name};
}
}
/**
* Connect to an IMAP server
*
* @param string $host Host to connect
* @param string $user Username for IMAP account
* @param string $pass Password for IMAP account
* @param integer $port Port to connect to
* @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
*
* @return bool True on success, False on failure
*/
public function connect($host, $user, $pass, $port = 143, $use_ssl = null)
{
// check for OpenSSL support in PHP build
if ($use_ssl && extension_loaded('openssl')) {
$this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
}
else if ($use_ssl) {
rcube::raise_error([
'code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "OpenSSL not available"
], true, false);
$port = 143;
}
$this->options['port'] = $port;
if (!empty($this->options['debug'])) {
$this->set_debug(true);
$this->options['ident'] = [
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
'command' => $_SERVER['REQUEST_URI'],
];
}
$attempt = 0;
do {
$data = [
'host' => $host,
'user' => $user,
'attempt' => ++$attempt,
'retry' => false
];
$data = $this->plugins->exec_hook('storage_connect', array_merge($this->options, $data));
if ($attempt > 1 && !$data['retry']) {
$break;
}
if (!empty($data['pass'])) {
$pass = $data['pass'];
}
// Handle per-host socket options
if (isset($data['socket_options'])) {
rcube_utils::parse_socket_options($data['socket_options'], $data['host']);
}
$this->conn->connect($data['host'], $data['user'], $pass, $data);
} while(!$this->conn->connected() && $data['retry']);
$config = [
'host' => $data['host'],
'user' => $data['user'],
'password' => $pass,
'port' => $port,
'ssl' => $use_ssl,
];
$this->options = array_merge($this->options, $config);
$this->connect_done = true;
if ($this->conn->connected()) {
// check for session identifier
$session = null;
if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) {
$session = $m[1];
}
// get namespace and delimiter
$this->set_env();
// trigger post-connect hook
$this->plugins->exec_hook('storage_connected', [
'host' => $host, 'user' => $user, 'session' => $session
]);
return true;
}
// write error log
else if ($this->conn->error) {
if ($pass && $user) {
$message = sprintf("Login failed for %s against %s from %s. %s",
$user, $host, rcube_utils::remote_ip(), $this->conn->error);
rcube::raise_error([
'code' => 403, 'type' => 'imap',
'file' => __FILE__, 'line' => __LINE__,
'message' => $message
], true, false);
}
}
return false;
}
/**
* Close IMAP connection.
* Usually done on script shutdown
*/
public function close()
{
$this->connect_done = false;
$this->conn->closeConnection();
if ($this->mcache) {
$this->mcache->close();
}
}
/**
* Check connection state, connect if not connected.
*
* @return bool Connection state.
*/
public function check_connection()
{
// Establish connection if it wasn't done yet
if (!$this->connect_done && !empty($this->options['user'])) {
return $this->connect(
$this->options['host'],
$this->options['user'],
$this->options['password'],
$this->options['port'],
$this->options['ssl']
);
}
return $this->is_connected();
}
/**
* Checks IMAP connection.
*
* @return bool True on success, False on failure
*/
public function is_connected()
{
return $this->conn->connected();
}
/**
* Returns code of last error
*
* @return int Error code
*/
public function get_error_code()
{
return $this->conn->errornum;
}
/**
* Returns text of last error
*
* @return string Error string
*/
public function get_error_str()
{
return $this->conn->error;
}
/**
* Returns code of last command response
*
* @return int Response code
*/
public function get_response_code()
{
switch ($this->conn->resultcode) {
case 'NOPERM':
return self::NOPERM;
case 'READ-ONLY':
return self::READONLY;
case 'TRYCREATE':
return self::TRYCREATE;
case 'INUSE':
return self::INUSE;
case 'OVERQUOTA':
return self::OVERQUOTA;
case 'ALREADYEXISTS':
return self::ALREADYEXISTS;
case 'NONEXISTENT':
return self::NONEXISTENT;
case 'CONTACTADMIN':
return self::CONTACTADMIN;
default:
return self::UNKNOWN;
}
}
/**
* Activate/deactivate debug mode
*
* @param bool $dbg True if IMAP conversation should be logged
*/
public function set_debug($dbg = true)
{
$this->options['debug'] = $dbg;
$this->conn->setDebug($dbg, [$this, 'debug_handler']);
}
/**
* Set internal folder reference.
* All operations will be performed on this folder.
*
* @param string $folder Folder name
*/
public function set_folder($folder)
{
$this->folder = $folder;
}
/**
* Save a search result for future message listing methods
*
* @param array $set Search set, result from rcube_imap::get_search_set():
* 0 - searching criteria, string
* 1 - search result, rcube_result_index|rcube_result_thread
* 2 - searching character set, string
* 3 - sorting field, string
* 4 - true if sorted, bool
*/
public function set_search_set($set)
{
$set = (array) $set;
$this->search_string = isset($set[0]) ? $set[0] : null;
$this->search_set = isset($set[1]) ? $set[1] : null;
$this->search_charset = isset($set[2]) ? $set[2] : null;
$this->search_sort_field = isset($set[3]) ? $set[3] : null;
$this->search_sorted = isset($set[4]) ? $set[4] : null;
$this->search_threads = is_a($this->search_set, 'rcube_result_thread');
if (is_a($this->search_set, 'rcube_result_multifolder')) {
$this->set_threading(false);
}
}
/**
* Return the saved search set as hash array
*
* @return array|null Search set
*/
public function get_search_set()
{
if (empty($this->search_set)) {
return null;
}
return [
$this->search_string,
$this->search_set,
$this->search_charset,
$this->search_sort_field,
$this->search_sorted,
];
}
/**
* Returns the IMAP server's capability.
*
* @param string $cap Capability name
*
* @return mixed Capability value or TRUE if supported, FALSE if not
*/
public function get_capability($cap)
{
$cap = strtoupper($cap);
$sess_key = "STORAGE_$cap";
if (!isset($_SESSION[$sess_key])) {
if (!$this->check_connection()) {
return false;
}
if ($cap == rcube_storage::DUAL_USE_FOLDERS) {
$_SESSION[$sess_key] = $this->detect_dual_use_folders();
}
else {
$_SESSION[$sess_key] = $this->conn->getCapability($cap);
}
}
return $_SESSION[$sess_key];
}
/**
* Checks the PERMANENTFLAGS capability of the current folder
* and returns true if the given flag is supported by the IMAP server
*
* @param string $flag Permanentflag name
*
* @return bool True if this flag is supported
*/
public function check_permflag($flag)
{
$flag = strtoupper($flag);
$perm_flags = $this->get_permflags($this->folder);
$imap_flag = $this->conn->flags[$flag];
return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
}
/**
* Returns PERMANENTFLAGS of the specified folder
*
* @param string $folder Folder name
*
* @return array Flags
*/
public function get_permflags($folder)
{
if (!strlen($folder)) {
return [];
}
if (!$this->check_connection()) {
return [];
}
if ($this->conn->select($folder)) {
$permflags = $this->conn->data['PERMANENTFLAGS'];
}
else {
return [];
}
if (!isset($permflags) || !is_array($permflags)) {
$permflags = [];
}
return $permflags;
}
/**
* Returns the delimiter that is used by the IMAP server for folder separation
*
* @return string Delimiter string
*/
public function get_hierarchy_delimiter()
{
return $this->delimiter;
}
/**
* Get namespace
*
* @param string $name Namespace array index: personal, other, shared, prefix
*
* @return array Namespace data
*/
public function get_namespace($name = null)
{
$ns = $this->namespace;
if ($name) {
// an alias for BC
if ($name == 'prefix') {
$name = 'prefix_in';
}
return isset($ns[$name]) ? $ns[$name] : null;
}
unset($ns['prefix_in'], $ns['prefix_out']);
return $ns;
}
/**
* Sets delimiter and namespaces
*/
protected function set_env()
{
if ($this->delimiter !== null && $this->namespace !== null) {
return;
}
$config = rcube::get_instance()->config;
$imap_personal = $config->get('imap_ns_personal');
$imap_other = $config->get('imap_ns_other');
$imap_shared = $config->get('imap_ns_shared');
$imap_delimiter = $config->get('imap_delimiter');
if (!$this->check_connection()) {
return;
}
$ns = $this->conn->getNamespace();
// Set namespaces (NAMESPACE supported)
if (is_array($ns)) {
$this->namespace = $ns;
}
else {
$this->namespace = [
'personal' => null,
'other' => null,
'shared' => null,
];
}
if ($imap_delimiter) {
$this->delimiter = $imap_delimiter;
}
if (empty($this->delimiter) && !empty($this->namespace['personal'][0][1])) {
$this->delimiter = $this->namespace['personal'][0][1];
}
if (empty($this->delimiter)) {
$this->delimiter = $this->conn->getHierarchyDelimiter();
}
if (empty($this->delimiter)) {
$this->delimiter = '/';
}
$this->list_root = null;
$this->list_excludes = [];
// Overwrite namespaces
if ($imap_personal !== null) {
$this->namespace['personal'] = null;
foreach ((array) $imap_personal as $dir) {
$this->namespace['personal'][] = [$dir, $this->delimiter];
}
}
if ($imap_other === false) {
foreach ((array) $this->namespace['other'] as $dir) {
if (is_array($dir) && !empty($dir[0])) {
$this->list_excludes[] = $dir[0];
}
}
$this->namespace['other'] = null;
}
else if ($imap_other !== null) {
$this->namespace['other'] = null;
foreach ((array) $imap_other as $dir) {
if ($dir) {
$this->namespace['other'][] = [$dir, $this->delimiter];
}
}
}
if ($imap_shared === false) {
foreach ((array) $this->namespace['shared'] as $dir) {
if (is_array($dir) && !empty($dir[0])) {
$this->list_excludes[] = $dir[0];
}
}
$this->namespace['shared'] = null;
}
else if ($imap_shared !== null) {
$this->namespace['shared'] = null;
foreach ((array) $imap_shared as $dir) {
if ($dir) {
$this->namespace['shared'][] = [$dir, $this->delimiter];
}
}
}
// Performance optimization for case where we have no shared/other namespace
// and personal namespace has one prefix (#5073)
// In such a case we can tell the server to return only content of the
// specified folder in LIST/LSUB, no post-filtering
if (empty($this->namespace['other']) && empty($this->namespace['shared'])
&& !empty($this->namespace['personal']) && count($this->namespace['personal']) === 1
&& strlen($this->namespace['personal'][0][0]) > 1
) {
$this->list_root = $this->namespace['personal'][0][0];
$this->list_excludes = [];
}
// Find personal namespace prefix(es) for self::mod_folder()
if (!empty($this->namespace['personal']) && is_array($this->namespace['personal'])) {
// There can be more than one namespace root,
// - for prefix_out get the first one but only
// if there is only one root
// - for prefix_in get the first one but only
// if there is no non-prefixed namespace root (#5403)
$roots = [];
foreach ($this->namespace['personal'] as $ns) {
$roots[] = $ns[0];
}
if (!in_array('', $roots)) {
$this->namespace['prefix_in'] = $roots[0];
}
if (count($roots) == 1) {
$this->namespace['prefix_out'] = $roots[0];
}
}
$_SESSION['imap_namespace'] = $this->namespace;
$_SESSION['imap_delimiter'] = $this->delimiter;
$_SESSION['imap_list_conf'] = [$this->list_root, $this->list_excludes];
}
/**
* Returns IMAP server vendor name
*
* @return string Vendor name
* @since 1.2
*/
public function get_vendor()
{
if (isset($_SESSION['imap_vendor'])) {
return $_SESSION['imap_vendor'];
}
$config = rcube::get_instance()->config;
$imap_vendor = $config->get('imap_vendor');
if ($imap_vendor) {
return $imap_vendor;
}
if (!$this->check_connection()) {
return;
}
if (isset($this->conn->data['ID'])) {
$ident = $this->conn->data['ID'];
}
else if ($this->get_capability('ID')) {
$ident = $this->conn->id([
'name' => 'Roundcube',
'version' => RCUBE_VERSION,
'php' => PHP_VERSION,
'os' => PHP_OS,
]);
}
else {
$ident = null;
}
$vendor = (string) (!empty($ident) ? $ident['name'] : '');
$ident = strtolower($vendor . ' ' . $this->conn->data['GREETING']);
$vendors = ['cyrus', 'dovecot', 'uw-imap', 'gimap', 'hmail', 'greenmail'];
foreach ($vendors as $v) {
if (strpos($ident, $v) !== false) {
$vendor = $v;
break;
}
}
return $_SESSION['imap_vendor'] = $vendor;
}
/**
* Get message count for a specific folder
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param bool $force Force reading from server and update cache
* @param bool $status Enables storing folder status info (max UID/count),
* required for folder_status()
*
* @return int Number of messages
*/
public function count($folder = '', $mode = 'ALL', $force = false, $status = true)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->countmessages($folder, $mode, $force, $status);
}
/**
* Protected method for getting number of messages
*
* @param string $folder Folder name
* @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
* @param bool $force Force reading from server and update cache
* @param bool $status Enables storing folder status info (max UID/count),
* required for folder_status()
* @param bool $no_search Ignore current search result
*
* @return int Number of messages
* @see rcube_imap::count()
*/
protected function countmessages($folder, $mode = 'ALL', $force = false, $status = true, $no_search = false)
{
$mode = strtoupper($mode);
// Count search set, assume search set is always up-to-date (don't check $force flag)
// @TODO: this could be handled in more reliable way, e.g. a separate method
// maybe in rcube_imap_search
if (!$no_search && $this->search_string && $folder == $this->folder) {
if ($mode == 'ALL') {
return $this->search_set->count_messages();
}
if ($mode == 'THREADS') {
return $this->search_set->count();
}
}
// EXISTS is a special alias for ALL, it allows to get the number
// of all messages in a folder also when search is active and with
// any skip_deleted setting
$a_folder_cache = $this->get_cache('messagecount');
// return cached value
if (!$force && isset($a_folder_cache[$folder][$mode])) {
return $a_folder_cache[$folder][$mode];
}
if (!isset($a_folder_cache[$folder]) || !is_array($a_folder_cache[$folder])) {
$a_folder_cache[$folder] = [];
}
if ($mode == 'THREADS') {
$res = $this->threads($folder);
$count = $res->count();
if ($status) {
$msg_count = $res->count_messages();
$this->set_folder_stats($folder, 'cnt', $msg_count);
$this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0);
}
}
// Need connection here
else if (!$this->check_connection()) {
return 0;
}
// RECENT count is fetched a bit different
else if ($mode == 'RECENT') {
$count = $this->conn->countRecent($folder);
}
// use SEARCH for message counting
else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) {
$search_str = "ALL UNDELETED";
$keys = ['COUNT'];
if ($mode == 'UNSEEN') {
$search_str .= " UNSEEN";
}
else {
if ($this->messages_caching) {
$keys[] = 'ALL';
}
if ($status) {
$keys[] = 'MAX';
}
}
// @TODO: if $mode == 'ALL' we could try to use cache index here
// get message count using (E)SEARCH
// not very performant but more precise (using UNDELETED)
$index = $this->conn->search($folder, $search_str, true, $keys);
$count = $index->count();
if ($mode == 'ALL') {
// Cache index data, will be used in index_direct()
$this->icache['undeleted_idx'] = $index;
if ($status) {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $index->max());
}
}
}
else {
if ($mode == 'UNSEEN') {
$count = $this->conn->countUnseen($folder);
}
else {
$count = $this->conn->countMessages($folder);
if ($status && $mode == 'ALL') {
$this->set_folder_stats($folder, 'cnt', $count);
$this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0);
}
}
}
$count = (int) $count;
if (!isset($a_folder_cache[$folder][$mode]) || $a_folder_cache[$folder][$mode] !== $count) {
$a_folder_cache[$folder][$mode] = $count;
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
}
return $count;
}
/**
* Public method for listing message flags
*
* @param string $folder Folder name
* @param array $uids Message UIDs
* @param int $mod_seq Optional MODSEQ value (of last flag update)
*
* @return array Indexed array with message flags
*/
public function list_flags($folder, $uids, $mod_seq = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return [];
}
// @TODO: when cache was synchronized in this request
// we might already have asked for flag updates, use it.
$flags = $this->conn->fetch($folder, $uids, true, ['FLAGS'], $mod_seq);
$result = [];
if (!empty($flags)) {
foreach ($flags as $message) {
$result[$message->uid] = $message->flags;
}
}
return $result;
}
/**
* Public method for listing headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
*/
public function list_messages($folder = '', $page = null, $sort_field = null, $sort_order = null, $slice = 0)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
}
/**
* protected method for listing message headers
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param string $sort_field Header field to sort by
* @param string $sort_order Sort order [ASC|DESC]
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function _list_messages($folder = '', $page = null, $sort_field = null, $sort_order = null, $slice = 0)
{
if (!strlen($folder)) {
return [];
}
$this->set_sort_order($sort_field, $sort_order);
$page = $page ?: $this->list_page;
// use saved message set
if ($this->search_string) {
return $this->list_search_messages($folder, $page, $slice);
}
if ($this->threading) {
return $this->list_thread_messages($folder, $page, $slice);
}
// get UIDs of all messages in the folder, sorted
$index = $this->index($folder, $this->sort_field, $this->sort_order);
if ($index->is_empty()) {
return [];
}
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$index->slice($from, $to - $from);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch requested messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
/**
* protected method for listing message headers using threads
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_messages
*/
protected function list_thread_messages($folder, $page, $slice = 0)
{
// get all threads (not sorted)
if ($mcache = $this->get_mcache_engine()) {
$threads = $mcache->get_thread($folder);
}
else {
$threads = $this->threads($folder);
}
return $this->fetch_thread_headers($folder, $threads, $page, $slice);
}
/**
* Method for fetching threads data
*
* @param string $folder Folder name
*
* @return rcube_result_thread Thread data object
*/
function threads($folder)
{
if ($mcache = $this->get_mcache_engine()) {
// don't store in self's internal cache, cache has it's own internal cache
return $mcache->get_thread($folder);
}
if (!empty($this->icache['threads'])) {
if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
return $this->icache['threads'];
}
}
// get all threads
$result = $this->threads_direct($folder);
// add to internal (fast) cache
return $this->icache['threads'] = $result;
}
/**
* Method for direct fetching of threads data
*
* @param string $folder Folder name
*
* @return rcube_result_thread Thread data object
*/
function threads_direct($folder)
{
if (!$this->check_connection()) {
return new rcube_result_thread();
}
// get all threads
return $this->conn->thread($folder, $this->threading,
$this->options['skip_deleted'] ? 'UNDELETED' : '', true);
}
/**
* protected method for fetching threaded messages headers
*
* @param string $folder Folder name
* @param rcube_result_thread $threads Threads data object
* @param int $page List page number
* @param int $slice Number of threads to slice
*
* @return array Messages headers
*/
protected function fetch_thread_headers($folder, $threads, $page, $slice = 0)
{
// Sort thread structure
$this->sort_threads($threads);
$from = ($page-1) * $this->page_size;
$to = $from + $this->page_size;
$threads->slice($from, $to - $from);
if ($slice) {
$threads->slice(-$slice, $slice);
}
// Get UIDs of all messages in all threads
$a_index = $threads->get();
// fetch requested headers from server
$a_msg_headers = $this->fetch_headers($folder, $a_index);
unset($a_index);
// Set depth, has_children and unread_children fields in headers
$this->set_thread_flags($a_msg_headers, $threads);
return array_values($a_msg_headers);
}
/**
* protected method for setting threaded messages flags:
* depth, has_children, unread_children, flagged_children
*
* @param array $headers Reference to headers array indexed by message UID
* @param rcube_result_thread $threads Threads data object
*
* @return array Message headers array indexed by message UID
*/
protected function set_thread_flags(&$headers, $threads)
{
$parents = [];
list($msg_depth, $msg_children) = $threads->get_thread_data();
foreach ($headers as $uid => $header) {
$depth = $msg_depth[$uid];
$parents = array_slice($parents, 0, $depth);
if (!empty($parents)) {
$headers[$uid]->parent_uid = end($parents);
if (empty($header->flags['SEEN'])) {
$headers[$parents[0]]->unread_children++;
}
if (!empty($header->flags['FLAGGED'])) {
$headers[$parents[0]]->flagged_children++;
}
}
array_push($parents, $uid);
$headers[$uid]->depth = $depth;
$headers[$uid]->has_children = $msg_children[$uid];
$headers[$uid]->unread_children = 0;
$headers[$uid]->flagged_children = 0;
}
}
/**
* A protected method for listing a set of message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from the result array
*
* @return array Indexed array with message header objects
*/
protected function list_search_messages($folder, $page, $slice = 0)
{
if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
return [];
}
$from = ($page-1) * $this->page_size;
// gather messages from a multi-folder search
if (!empty($this->search_set->multi)) {
$page_size = $this->page_size;
$sort_field = $this->sort_field;
$search_set = $this->search_set;
// fetch resultset headers, sort and slice them
if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) {
$this->sort_field = null;
$this->page_size = 1000; // fetch up to 1000 matching messages per folder
$this->threading = false;
$a_msg_headers = [];
foreach ($search_set->sets as $resultset) {
if (!$resultset->is_empty()) {
$this->search_set = $resultset;
$this->search_threads = $resultset instanceof rcube_result_thread;
$a_headers = $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1);
$a_msg_headers = array_merge($a_msg_headers, $a_headers);
unset($a_headers);
}
}
// sort headers
if (!empty($a_msg_headers)) {
$a_msg_headers = rcube_imap_generic::sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
}
// store (sorted) message index
$search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
// only return the requested part of the set
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $page_size);
}
else {
if ($this->sort_order != $search_set->get_parameters('ORDER')) {
$search_set->revert();
}
// slice resultset first...
$index = array_slice($search_set->get(), $from, $page_size);
$fetch = [];
foreach ($index as $msg_id) {
list($uid, $folder) = explode('-', $msg_id, 2);
$fetch[$folder][] = $uid;
}
// ... and fetch the requested set of headers
$a_msg_headers = [];
foreach ($fetch as $folder => $a_index) {
$a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
}
// Re-sort the result according to the original search set order
usort($a_msg_headers, function($a, $b) use ($index) {
return array_search($a->uid . '-' . $a->folder, $index) - array_search($b->uid . '-' . $b->folder, $index);
});
}
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
// restore members
$this->sort_field = $sort_field;
$this->page_size = $page_size;
$this->search_set = $search_set;
return $a_msg_headers;
}
// use saved messages from searching
if ($this->threading) {
return $this->list_search_thread_messages($folder, $page, $slice);
}
// search set is threaded, we need a new one
if ($this->search_threads) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
$index = clone $this->search_set;
// return empty array if no messages found
if ($index->is_empty()) {
return [];
}
// quickest method (default sorting)
if (!$this->search_sort_field && !$this->sort_field) {
$got_index = true;
}
// sorted messages, so we can first slice array and then fetch only wanted headers
else if ($this->search_sorted) { // SORT searching result
$got_index = true;
// reset search set if sorting field has been changed
if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
$index = clone $this->search_set;
// return empty array if no messages found
if ($index->is_empty()) {
return [];
}
}
}
if (!empty($got_index)) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
// get messages uids for one page
$index->slice($from, $this->page_size);
if ($slice) {
$index->slice(-$slice, $slice);
}
// fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
// SEARCH result, need sorting
$cnt = $index->count();
// 300: experimental value for best result
if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
// use memory less expensive (and quick) method for big result set
$index = clone $this->index('', $this->sort_field, $this->sort_order);
// get messages uids for one page...
$index->slice($from, $this->page_size);
if ($slice) {
$index->slice(-$slice, $slice);
}
// ...and fetch headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index);
return array_values($a_msg_headers);
}
else {
// for small result set we can fetch all messages headers
$a_index = $index->get();
$a_msg_headers = $this->fetch_headers($folder, $a_index, false);
// return empty array if no messages found
if (!is_array($a_msg_headers) || empty($a_msg_headers)) {
return [];
}
// if not already sorted
$a_msg_headers = rcube_imap_generic::sortHeaders(
$a_msg_headers, $this->sort_field, $this->sort_order);
$a_msg_headers = array_slice(array_values($a_msg_headers), $from, $this->page_size);
if ($slice) {
$a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
}
return $a_msg_headers;
}
}
/**
* protected method for listing a set of threaded message headers (search results)
*
* @param string $folder Folder name
* @param int $page Current page to list
* @param int $slice Number of slice items to extract from result array
*
* @return array Indexed array with message header objects
* @see rcube_imap::list_search_messages()
*/
protected function list_search_thread_messages($folder, $page, $slice = 0)
{
// update search_set if previous data was fetched with disabled threading
if (!$this->search_threads) {
if ($this->search_set->is_empty()) {
return [];
}
$this->search('', $this->search_string, $this->search_charset, $this->sort_field);
}
return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
}
/**
* Fetches messages headers (by UID)
*
* @param string $folder Folder name
* @param array $msgs Message UIDs
* @param bool $sort Enables result sorting by $msgs
* @param bool $force Disables cache use
*
* @return array Messages headers indexed by UID
*/
function fetch_headers($folder, $msgs, $sort = true, $force = false)
{
if (empty($msgs)) {
return [];
}
if (!$force && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_messages($folder, $msgs);
}
else if (!$this->check_connection()) {
return [];
}
else {
// fetch requested headers from server
$headers = $this->conn->fetchHeaders(
$folder, $msgs, true, false, $this->get_fetch_headers());
}
if (empty($headers)) {
return [];
}
$msg_headers = [];
foreach ($headers as $h) {
$h->folder = $folder;
$msg_headers[$h->uid] = $h;
}
if ($sort) {
// use this class for message sorting
$sorter = new rcube_message_header_sorter();
$sorter->set_index($msgs);
$sorter->sort_headers($msg_headers);
}
return $msg_headers;
}
/**
* Returns current status of a folder (compared to the last time use)
*
* We compare the maximum UID to determine the number of
* new messages because the RECENT flag is not reliable.
*
* @param string $folder Folder name
* @param array $diff Difference data
*
* @return int Folder status
*/
public function folder_status($folder = null, &$diff = [])
{
if (!strlen($folder)) {
$folder = $this->folder;
}
$old = $this->get_folder_stats($folder);
// refresh message count -> will update
$this->countmessages($folder, 'ALL', true, true, true);
$result = 0;
if (empty($old)) {
return $result;
}
$new = $this->get_folder_stats($folder);
// got new messages
if ($new['maxuid'] > $old['maxuid']) {
$result += 1;
// get new message UIDs range, that can be used for example
// to get the data of these messages
$diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid'];
}
// some messages has been deleted
if ($new['cnt'] < $old['cnt']) {
$result += 2;
}
// @TODO: optional checking for messages flags changes (?)
// @TODO: UIDVALIDITY checking
return $result;
}
/**
* Stores folder statistic data in session
* @TODO: move to separate DB table (cache?)
*
* @param string $folder Folder name
* @param string $name Data name
* @param mixed $data Data value
*/
protected function set_folder_stats($folder, $name, $data)
{
$_SESSION['folders'][$folder][$name] = $data;
}
/**
* Gets folder statistic data
*
* @param string $folder Folder name
*
* @return array Stats data
*/
protected function get_folder_stats($folder)
{
if (isset($_SESSION['folders'][$folder])) {
return (array) $_SESSION['folders'][$folder];
}
return [];
}
/**
* Return sorted list of message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param bool $no_threads Get not threaded index
* @param bool $no_search Get index not limited to search result (optionally)
*
* @return rcube_result_index|rcube_result_thread List of messages (UIDs)
*/
public function index($folder = '', $sort_field = null, $sort_order = null,
$no_threads = false, $no_search = false
) {
if (!$no_threads && $this->threading) {
return $this->thread_index($folder, $sort_field, $sort_order);
}
$this->set_sort_order($sort_field, $sort_order);
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string) {
if ($this->search_set->is_empty()) {
return new rcube_result_index($folder, '* SORT');
}
if ($this->search_set instanceof rcube_result_multifolder) {
$index = $this->search_set;
$index->folder = $folder;
// TODO: handle changed sorting
}
// search result is an index with the same sorting?
else if (($this->search_set instanceof rcube_result_index)
&& ((!$this->sort_field && !$this->search_sorted) ||
($this->search_sorted && $this->search_sort_field == $this->sort_field))
) {
$index = $this->search_set;
}
// $no_search is enabled when we are not interested in
// fetching index for search result, e.g. to sort
// threaded search result we can use full mailbox index.
// This makes possible to use index from cache
else if (!$no_search) {
if (!$this->sort_field) {
// No sorting needed, just build index from the search result
// @TODO: do we need to sort by UID here?
$search = $this->search_set->get_compressed();
$index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
}
else {
$index = $this->index_direct($folder, $this->sort_field, $this->sort_order, $this->search_set);
}
}
if (isset($index)) {
if ($this->sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
}
// check local cache
if ($mcache = $this->get_mcache_engine()) {
return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
}
// fetch from IMAP server
return $this->index_direct($folder, $this->sort_field, $this->sort_order);
}
/**
* Return sorted list of message UIDs ignoring current search settings.
* Doesn't uses cache by default.
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
* @param rcube_result_* $search Optional messages set to limit the result
*
* @return rcube_result_index Sorted list of message UIDs
*/
public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
{
if (!empty($search)) {
$search = $search->get_compressed();
}
// use message index sort as default sorting
if (!$sort_field) {
// use search result from count() if possible
if (empty($search) && $this->options['skip_deleted']
&& !empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('ALL') !== null
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$index = $this->icache['undeleted_idx'];
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
else {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->search($folder, $query, true);
}
}
else if (!$this->check_connection()) {
return new rcube_result_index();
}
// fetch complete message index
else {
if ($this->get_capability('SORT')) {
$query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
if ($search) {
$query = trim($query . ' UID ' . $search);
}
$index = $this->conn->sort($folder, $sort_field, $query, true);
}
if (empty($index) || $index->is_error()) {
$index = $this->conn->index($folder, $search ? $search : "1:*",
$sort_field, $this->options['skip_deleted'],
$search ? true : false, true);
}
}
if ($sort_order != $index->get_parameters('ORDER')) {
$index->revert();
}
return $index;
}
/**
* Return index of threaded message UIDs
*
* @param string $folder Folder to get index from
* @param string $sort_field Sort column
* @param string $sort_order Sort order [ASC, DESC]
*
* @return rcube_result_thread Message UIDs
*/
public function thread_index($folder = '', $sort_field = null, $sort_order = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// we have a saved search result, get index from there
if ($this->search_string && $this->search_threads && $folder == $this->folder) {
$threads = $this->search_set;
}
else {
// get all threads (default sort order)
$threads = $this->threads($folder);
}
$this->set_sort_order($sort_field, $sort_order);
$this->sort_threads($threads);
return $threads;
}
/**
* Sort threaded result, using THREAD=REFS method if available.
* If not, use any method and re-sort the result in THREAD=REFS way.
*
* @param rcube_result_thread $threads Threads result set
*/
protected function sort_threads($threads)
{
if ($threads->is_empty()) {
return;
}
// THREAD=ORDEREDSUBJECT: sorting by sent date of root message
// THREAD=REFERENCES: sorting by sent date of root message
// THREAD=REFS: sorting by the most recent date in each thread
if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
$sortby = $this->sort_field ?: 'date';
$index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
if (!$index->is_empty()) {
$threads->sort($index);
}
}
else if ($this->sort_order != $threads->get_parameters('ORDER')) {
$threads->revert();
}
}
/**
* Invoke search request to IMAP server
*
* @param string $folder Folder name to search in
* @param string $search Search criteria
* @param string $charset Search charset
* @param string $sort_field Header field to sort by
*
* @return rcube_result_index Search result object
* @todo: Search criteria should be provided in non-IMAP format, e.g. array
*/
public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
{
if (!$search) {
$search = 'ALL';
}
if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
$folder = $this->folder;
}
$plugin = $this->plugins->exec_hook('imap_search_before', [
'folder' => $folder,
'search' => $search,
'charset' => $charset,
'sort_field' => $sort_field,
'threading' => $this->threading,
'result' => null,
]);
$folder = $plugin['folder'];
$search = $plugin['search'];
$charset = $plugin['charset'];
$sort_field = $plugin['sort_field'];
$results = $plugin['result'];
// multi-folder search
if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
// connect IMAP to have all the required classes and settings loaded
$this->check_connection();
// disable threading
$this->threading = false;
$searcher = new rcube_imap_search($this->options, $this->conn);
// set limit to not exceed the client's request timeout
$searcher->set_timelimit(60);
// continue existing incomplete search
if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) {
$searcher->set_results($this->search_set);
}
// execute the search
$results = $searcher->exec(
$folder,
$search,
$charset ? $charset : $this->default_charset,
$sort_field && $this->get_capability('SORT') ? $sort_field : null,
$this->threading
);
}
else if (!$results) {
$folder = is_array($folder) ? $folder[0] : $folder;
$search = is_array($search) ? $search[$folder] : $search;
$results = $this->search_index($folder, $search, $charset, $sort_field);
}
$sorted = $this->threading || $this->search_sorted || !empty($plugin['search_sorted']);
$this->set_search_set([$search, $results, $charset, $sort_field, $sorted]);
return $results;
}
/**
* Direct (real and simple) SEARCH request (without result sorting and caching).
*
* @param string $mailbox Mailbox name to search in
* @param string $str Search string
*
* @return rcube_result_index Search result (UIDs)
*/
public function search_once($folder = null, $str = 'ALL')
{
if (!$this->check_connection()) {
return new rcube_result_index();
}
if (!$str) {
$str = 'ALL';
}
// multi-folder search
if (is_array($folder) && count($folder) > 1) {
$searcher = new rcube_imap_search($this->options, $this->conn);
$index = $searcher->exec($folder, $str, $this->default_charset);
}
else {
$folder = is_array($folder) ? $folder[0] : $folder;
if (!strlen($folder)) {
$folder = $this->folder;
}
$index = $this->conn->search($folder, $str, true);
}
return $index;
}
/**
* protected search method
*
* @param string $folder Folder name
* @param string $criteria Search criteria
* @param string $charset Charset
* @param string $sort_field Sorting field
*
* @return rcube_result_index|rcube_result_thread Search results (UIDs)
* @see rcube_imap::search()
*/
protected function search_index($folder, $criteria = 'ALL', $charset = null, $sort_field = null)
{
if (!$this->check_connection()) {
if ($this->threading) {
return new rcube_result_thread();
}
else {
return new rcube_result_index();
}
}
if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
$criteria = 'UNDELETED '.$criteria;
}
// unset CHARSET if criteria string is ASCII, this way
// SEARCH won't be re-sent after "unsupported charset" response
if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
$charset = 'US-ASCII';
}
if ($this->threading) {
$threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen that Courier doesn't support UTF-8)
if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
$threads = $this->conn->thread($folder, $this->threading,
self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
return $threads;
}
if ($sort_field && $this->get_capability('SORT')) {
$charset = $charset ?: $this->default_charset;
$messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
// Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
// but I've seen Courier with disabled UTF-8 support)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->sort($folder, $sort_field,
self::convert_criteria($criteria, $charset), true, 'US-ASCII');
}
if (!$messages->is_error()) {
$this->search_sorted = true;
return $messages;
}
}
$messages = $this->conn->search($folder,
($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
// Error, try with US-ASCII (some servers may support only US-ASCII)
if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
$messages = $this->conn->search($folder, self::convert_criteria($criteria, $charset), true);
}
$this->search_sorted = false;
return $messages;
}
/**
* Converts charset of search criteria string
*
* @param string $str Search string
* @param string $charset Original charset
* @param string $dest_charset Destination charset (default US-ASCII)
*
* @return string Search string
*/
public static function convert_criteria($str, $charset, $dest_charset = 'US-ASCII')
{
// convert strings to US_ASCII
if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
$last = 0;
$res = '';
foreach ($matches[1] as $m) {
$string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
$string = substr($str, $string_offset - 1, $m[0]);
$string = rcube_charset::convert($string, $charset, $dest_charset);
if ($string === false || !strlen($string)) {
continue;
}
$res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
$last = $m[0] + $string_offset - 1;
}
if ($last < strlen($str)) {
$res .= substr($str, $last, strlen($str)-$last);
}
}
// strings for conversion not found
else {
$res = $str;
}
return $res;
}
/**
* Refresh saved search set
*
* @return array Current search set
*/
public function refresh_search()
{
if (!empty($this->search_string)) {
$this->search(
is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
$this->search_string,
$this->search_charset,
$this->search_sort_field
);
}
return $this->get_search_set();
}
/**
* Flag certain result subsets as 'incomplete'.
* For subsequent refresh_search() calls to only refresh the updated parts.
*/
protected function set_search_dirty($folder)
{
if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
if ($subset = $this->search_set->get_set($folder)) {
$subset->incomplete = $this->search_set->incomplete = true;
}
}
}
/**
* Return message headers object of a specific message
*
* @param int $id Message UID
* @param string $folder Folder to read from
* @param bool $force True to skip cache
*
* @return rcube_message_header Message headers
*/
public function get_message_headers($uid, $folder = null, $force = false)
{
// decode combined UID-folder identifier
if (preg_match('/^\d+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
if (!strlen($folder)) {
$folder = $this->folder;
}
// get cached headers
if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
$headers = $mcache->get_message($folder, $uid);
}
else if (!$this->check_connection()) {
$headers = false;
}
else {
$headers = $this->conn->fetchHeader(
$folder, $uid, true, true, $this->get_fetch_headers());
if (is_object($headers)) {
$headers->folder = $folder;
}
}
return $headers;
}
/**
* Fetch message headers and body structure from the IMAP server and build
* an object structure.
*
* @param int $uid Message UID to fetch
* @param string $folder Folder to read from
*
* @return object rcube_message_header Message data
*/
public function get_message($uid, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
// decode combined UID-folder identifier
if (preg_match('/^\d+-.+/', $uid)) {
list($uid, $folder) = explode('-', $uid, 2);
}
// Check internal cache
if (!empty($this->icache['message']) && ($headers = $this->icache['message'])) {
// Make sure the folder and UID is what we expect.
// In case when the same process works with folders that are personal
// and shared two folders can contain the same UIDs.
if ($headers->uid == $uid && $headers->folder == $folder) {
return $headers;
}
}
$headers = $this->get_message_headers($uid, $folder);
// message doesn't exist?
if (empty($headers)) {
return null;
}
// structure might be cached
if (!empty($headers->structure)) {
return $headers;
}
$this->msg_uid = $uid;
if (!$this->check_connection()) {
return $headers;
}
if (empty($headers->bodystructure)) {
$headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
}
$structure = $headers->bodystructure;
if (empty($structure)) {
return $headers;
}
// set message charset from message headers
if ($headers->charset) {
$this->struct_charset = $headers->charset;
}
else {
$this->struct_charset = $this->structure_charset($structure);
}
$headers->ctype = @strtolower($headers->ctype);
// Here we can recognize malformed BODYSTRUCTURE and
// 1. [@TODO] parse the message in other way to create our own message structure
// 2. or just show the raw message body.
// Example of structure for malformed MIME message:
// ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
&& strtolower($structure[0].'/'.$structure[1]) == 'text/plain'
) {
// A special known case "Content-type: text" (#1488968)
if ($headers->ctype == 'text') {
$structure[1] = 'plain';
$headers->ctype = 'text/plain';
}
// we can handle single-part messages, by simple fix in structure (#1486898)
else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
$structure[0] = $m[1];
$structure[1] = $m[2];
}
else {
// Try to parse the message using rcube_mime_decode.
// We need a better solution, it parses message
// in memory, which wouldn't work for very big messages,
// (it uses up to 10x more memory than the message size)
// it's also buggy and not actively developed
if ($headers->size && rcube_utils::mem_check($headers->size * 10)) {
$raw_msg = $this->get_raw_body($uid);
$struct = rcube_mime::parse_message($raw_msg);
}
else {
return $headers;
}
}
}
if (empty($struct)) {
$struct = $this->structure_part($structure, 0, '', $headers);
}
// some workarounds on simple messages...
if (empty($struct->parts)) {
// ...don't trust given content-type
if (!empty($headers->ctype)) {
$struct->mime_id = '1';
$struct->mimetype = strtolower($headers->ctype);
list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
}
// ...and charset (there's a case described in #1488968 where invalid content-type
// results in invalid charset in BODYSTRUCTURE)
if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) {
$struct->charset = $headers->charset;
$struct->ctype_parameters['charset'] = $headers->charset;
}
}
$headers->structure = $struct;
return $this->icache['message'] = $headers;
}
/**
* Build message part object
*
* @param array $part
* @param int $count
* @param string $parent
*/
protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null)
{
$struct = new rcube_message_part;
$struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
// multipart
if (is_array($part[0])) {
$struct->ctype_primary = 'multipart';
/* RFC3501: BODYSTRUCTURE fields of multipart part
part1 array
part2 array
part3 array
....
1. subtype
2. parameters (optional)
3. description (optional)
4. language (optional)
5. location (optional)
*/
// find first non-array entry
for ($i=1; $i<count($part); $i++) {
if (!is_array($part[$i])) {
$struct->ctype_secondary = strtolower($part[$i]);
// read content type parameters
if (is_array($part[$i+1])) {
$struct->ctype_parameters = [];
for ($j=0; $j<count($part[$i+1]); $j+=2) {
$param = strtolower($part[$i+1][$j]);
$struct->ctype_parameters[$param] = $part[$i+1][$j+1];
}
}
break;
}
}
$struct->mimetype = 'multipart/'.$struct->ctype_secondary;
// build parts list for headers pre-fetching
for ($i=0; $i<count($part); $i++) {
// fetch message headers if message/rfc822 or named part
if (is_array($part[$i]) && !is_array($part[$i][0])) {
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
$mime_part_headers[] = $tmp_part_id;
}
else if ($this->is_attachment_part($part[$i])) {
$mime_part_headers[] = $tmp_part_id;
}
}
}
// pre-fetch headers of all parts (in one command for better performance)
// @TODO: we could do this before _structure_part() call, to fetch
// headers for parts on all levels
if (!empty($mime_part_headers)) {
$mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder,
$this->msg_uid, $mime_part_headers);
}
$struct->parts = [];
for ($i=0, $count=0; $i<count($part); $i++) {
if (!is_array($part[$i])) {
break;
}
$tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
$struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id,
!empty($mime_part_headers[$tmp_part_id]) ? $mime_part_headers[$tmp_part_id] : null);
}
return $struct;
}
/* RFC3501: BODYSTRUCTURE fields of non-multipart part
0. type
1. subtype
2. parameters
3. id
4. description
5. encoding
6. size
-- text
7. lines
-- message/rfc822
7. envelope structure
8. body structure
9. lines
--
x. md5 (optional)
x. disposition (optional)
x. language (optional)
x. location (optional)
*/
// regular part
$struct->ctype_primary = strtolower($part[0]);
$struct->ctype_secondary = strtolower($part[1]);
$struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
// read content type parameters
if (is_array($part[2])) {
$struct->ctype_parameters = [];
for ($i=0; $i<count($part[2]); $i+=2) {
$struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
}
if (isset($struct->ctype_parameters['charset'])) {
$struct->charset = $struct->ctype_parameters['charset'];
}
}
// #1487700: workaround for lack of charset in malformed structure
if (empty($struct->charset) && !empty($mime_headers) && !empty($mime_headers->charset)) {
$struct->charset = $mime_headers->charset;
}
// read content encoding
if (!empty($part[5])) {
$struct->encoding = strtolower($part[5]);
$struct->headers['content-transfer-encoding'] = $struct->encoding;
}
// get part size
if (!empty($part[6])) {
$struct->size = intval($part[6]);
}
// read part disposition
$di = 8;
if ($struct->ctype_primary == 'text') {
$di += 1;
}
else if ($struct->mimetype == 'message/rfc822') {
$di += 3;
}
if (isset($part[$di]) && is_array($part[$di]) && count($part[$di]) == 2) {
$struct->disposition = strtolower($part[$di][0]);
if ($struct->disposition && $struct->disposition !== 'inline' && $struct->disposition !== 'attachment') {
// RFC2183, Section 2.8 - unrecognized type should be treated as "attachment"
$struct->disposition = 'attachment';
}
if (is_array($part[$di][1])) {
for ($n=0; $n<count($part[$di][1]); $n+=2) {
$struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
}
}
}
// get message/rfc822's child-parts
if (isset($part[8]) && is_array($part[8]) && $di != 8) {
$struct->parts = [];
for ($i=0; $i<count($part[8]); $i++) {
if (!is_array($part[8][$i])) {
break;
}
$subpart_id = $struct->mime_id ? $struct->mime_id . '.' . ($i+1) : $i+1;
if ($this->is_attachment_part($part[8][$i])) {
$mime_part_headers[] = $subpart_id;
}
$struct->parts[$subpart_id] = $part[8][$i];
}
// Fetch attachment parts' headers in one go
if (!empty($mime_part_headers)) {
$mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder, $this->msg_uid, $mime_part_headers);
}
$count = 0;
foreach ($struct->parts as $idx => $subpart) {
$struct->parts[$idx] = $this->structure_part($subpart, ++$count, $struct->mime_id,
!empty($mime_part_headers[$idx]) ? $mime_part_headers[$idx] : null);
}
$struct->parts = array_values($struct->parts);
}
// get part ID
if (!empty($part[3])) {
$struct->content_id = $struct->headers['content-id'] = trim($part[3]);
if (empty($struct->disposition)) {
$struct->disposition = 'inline';
}
}
// fetch message headers if message/rfc822 or named part (could contain Content-Location header)
if (
$struct->ctype_primary == 'message'
|| (!empty($struct->ctype_parameters['name']) && !empty($struct->content_id))
) {
if (empty($mime_headers)) {
$mime_headers = $this->conn->fetchPartHeader($this->folder, $this->msg_uid, true, $struct->mime_id);
}
if (is_string($mime_headers)) {
$struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
}
else if (is_object($mime_headers)) {
$struct->headers = get_object_vars($mime_headers) + $struct->headers;
}
// get real content-type of message/rfc822
if ($struct->mimetype == 'message/rfc822') {
// single-part
if (!is_array($part[8][0])) {
$struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
}
// multi-part
else {
for ($n=0; $n<count($part[8]); $n++) {
if (!is_array($part[8][$n])) {
break;
}
}
$struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
}
}
if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
if (is_array($part[8]) && $di != 8) {
$struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id);
}
}
}
// normalize filename property
$this->set_part_filename($struct, $mime_headers);
return $struct;
}
/**
* Check if the mail structure part is an attachment part and requires
* fetching the MIME headers for further processing.
*/
protected function is_attachment_part($part)
{
if (!empty($part[2]) && is_array($part[2]) && empty($part[3])) {
$params = array_map('strtolower', (array) $part[2]);
$find = ['name', 'filename', 'name*', 'filename*', 'name*0', 'filename*0', 'name*0*', 'filename*0*'];
// In case of malformed header check disposition. E.g. some servers for
// "Content-Type: PDF; name=test.pdf" may return text/plain and ignore name argument
return count(array_intersect($params, $find)) > 0
|| (isset($part[9]) && is_array($part[9]) && stripos($part[9][0], 'attachment') === 0);
}
return false;
}
/**
* Set attachment filename from message part structure
*
* @param rcube_message_part $part Part object
* @param string $headers Part's raw headers
*/
protected function set_part_filename(&$part, $headers = null)
{
// Some IMAP servers do not support RFC2231, if we have
// part headers we'll get attachment name from them, not the BODYSTRUCTURE
$rfc2231_params = [];
if (!empty($headers) || !empty($part->headers)) {
if (is_object($headers)) {
$headers = get_object_vars($headers);
}
else {
$headers = !empty($headers) ? rcube_mime::parse_headers($headers) : $part->headers;
}
$ctype = isset($headers['content-type']) ? $headers['content-type'] : '';
$disposition = isset($headers['content-disposition']) ? $headers['content-disposition'] : '';
$tokens = preg_split('/;[\s\r\n\t]*/', $ctype. ';' . $disposition);
foreach ($tokens as $token) {
// TODO: Use order defined by the parameter name not order of occurrence in the header
if (preg_match('/^(name|filename)\*([0-9]*)\*?="*([^"]+)"*/i', $token, $matches)) {
$key = strtolower($matches[1]);
$rfc2231_params[$key] = (isset($rfc2231_params[$key]) ? $rfc2231_params[$key] : '') . $matches[3];
}
}
}
if (isset($rfc2231_params['name'])) {
$filename_encoded = $rfc2231_params['name'];
}
else if (isset($rfc2231_params['filename'])) {
$filename_encoded = $rfc2231_params['filename'];
}
else if (isset($part->d_parameters['filename*'])) {
$filename_encoded = $part->d_parameters['filename*'];
}
else if (isset($part->ctype_parameters['name*'])) {
$filename_encoded = $part->ctype_parameters['name*'];
}
else if (!empty($part->d_parameters['filename'])) {
$filename_mime = $part->d_parameters['filename'];
}
// read 'name' after rfc2231 parameters as it may contain truncated filename (from Thunderbird)
else if (!empty($part->ctype_parameters['name'])) {
$filename_mime = $part->ctype_parameters['name'];
}
else if (!empty($part->headers['content-description'])) {
$filename_mime = $part->headers['content-description'];
}
else {
return;
}
// decode filename
if (isset($filename_mime)) {
if (!empty($part->charset)) {
$charset = $part->charset;
}
else if (!empty($this->struct_charset)) {
$charset = $this->struct_charset;
}
else {
$charset = rcube_charset::detect($filename_mime, $this->default_charset);
}
$part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
}
else if (isset($filename_encoded)) {
// decode filename according to RFC 2231, Section 4
if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
$filename_charset = $fmatches[1];
$filename_encoded = $fmatches[2];
}
$part->filename = rawurldecode($filename_encoded);
if (!empty($filename_charset)) {
$part->filename = rcube_charset::convert($part->filename, $filename_charset);
}
}
// Workaround for invalid Content-Type (#6816)
// Some servers for "Content-Type: PDF; name=test.pdf" may return text/plain and ignore name argument
if ($part->mimetype == 'text/plain' && !empty($headers['content-type'])) {
$tokens = preg_split('/;[\s\r\n\t]*/', $headers['content-type']);
$type = rcube_mime::fix_mimetype($tokens[0]);
if ($type != $part->mimetype) {
$part->mimetype = $type;
list($part->ctype_primary, $part->ctype_secondary) = explode('/', $part->mimetype);
}
}
}
/**
* Get charset name from message structure (first part)
*
* @param array $structure Message structure
*
* @return string Charset name
*/
protected function structure_charset($structure)
{
while (is_array($structure)) {
if (is_array($structure[2]) && $structure[2][0] == 'charset') {
return $structure[2][1];
}
$structure = $structure[0];
}
}
/**
* Fetch message body of a specific message from the server
*
* @param int $uid Message UID
* @param string $part Part number
* @param rcube_message_part $o_part Part object created by get_structure()
* @param mixed $print True to print part, resource to write part contents in
* @param resource $fp File pointer to save the message part
* @param bool $skip_charset_conv Disables charset conversion
* @param int $max_bytes Only read this number of bytes
* @param bool $formatted Enables formatting of text/* parts bodies
*
* @return string Message/part body if not printed
*/
public function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null,
$skip_charset_conv = false, $max_bytes = 0, $formatted = true)
{
if (!$this->check_connection()) {
return null;
}
// get part data if not provided
if (!is_object($o_part)) {
$structure = $this->conn->getStructure($this->folder, $uid, true);
$part_data = rcube_imap_generic::getStructurePartData($structure, $part);
$o_part = new rcube_message_part;
$o_part->ctype_primary = $part_data['type'];
$o_part->ctype_secondary = $part_data['subtype'];
- $o_part->encoding = $part_data['encoding'];
- $o_part->charset = $part_data['charset'];
- $o_part->size = $part_data['size'];
+ $o_part->encoding = $part_data['encoding'] ?? null;
+ $o_part->charset = $part_data['charset'] ?? null;
+ $o_part->size = $part_data['size'] ?? null;
}
$body = '';
// Note: multipart/* parts will have size=0, we don't want to ignore them
if ($o_part && ($o_part->size || $o_part->ctype_primary == 'multipart')) {
$formatted = $formatted && $o_part->ctype_primary == 'text';
$body = $this->conn->handlePartBody($this->folder, $uid, true,
$part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
}
if ($fp || $print) {
return true;
}
// convert charset (if text or message part)
if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
// Remove NULL characters if any (#1486189)
if ($formatted && strpos($body, "\x00") !== false) {
$body = str_replace("\x00", '', $body);
}
if (!$skip_charset_conv) {
if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
// try to extract charset information from HTML meta tag (#1488125)
if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
$o_part->charset = strtoupper($m[1]);
}
else {
$o_part->charset = $this->default_charset;
}
}
$body = rcube_charset::convert($body, $o_part->charset);
}
}
return $body;
}
/**
* Returns the whole message source as string (or saves to a file)
*
* @param int $uid Message UID
* @param resource $fp File pointer to save the message
* @param string $part Optional message part ID
*
* @return string Message source string
*/
public function get_raw_body($uid, $fp = null, $part = null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->handlePartBody($this->folder, $uid,
true, $part, null, false, $fp);
}
/**
* Returns the message headers as string
*
* @param int $uid Message UID
* @param string $part Optional message part ID
*
* @return string Message headers string
*/
public function get_raw_headers($uid, $part = null)
{
if (!$this->check_connection()) {
return null;
}
return $this->conn->fetchPartHeader($this->folder, $uid, true, $part);
}
/**
* Sends the whole message source to stdout
*
* @param int $uid Message UID
* @param bool $formatted Enables line-ending formatting
*/
public function print_raw_body($uid, $formatted = true)
{
if (!$this->check_connection()) {
return;
}
$this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
}
/**
* Set message flag to one or several messages
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
* @param string $folder Folder name
* @param bool $skip_cache True to skip message cache clean up
*
* @return bool Operation status
*/
public function set_flag($uids, $flag, $folder = null, $skip_cache = false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
$flag = strtoupper($flag);
list($uids, $all_mode) = $this->parse_uids($uids);
if (strpos($flag, 'UN') === 0) {
$result = $this->conn->unflag($folder, $uids, substr($flag, 2));
}
else {
$result = $this->conn->flag($folder, $uids, $flag);
}
if ($result && !$skip_cache) {
// reload message headers if cached
// update flags instead removing from cache
if ($mcache = $this->get_mcache_engine()) {
$status = strpos($flag, 'UN') !== 0;
$mflag = preg_replace('/^UN/', '', $flag);
$mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
$mflag, $status);
}
// clear cached counters
if ($flag == 'SEEN' || $flag == 'UNSEEN') {
$this->clear_messagecount($folder, ['SEEN', 'UNSEEN']);
}
else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
$this->clear_messagecount($folder, ['ALL', 'THREADS']);
if ($this->options['skip_deleted']) {
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
}
$this->set_search_dirty($folder);
}
return $result;
}
/**
* Append a mail message (source) to a specific folder
*
* @param string $folder Target folder
* @param string|array $message The message source string or filename
* or array (of strings and file pointers)
* @param string $headers Headers string if $message contains only the body
* @param bool $is_file True if $message is a filename
* @param array $flags Message flags
* @param mixed $date Message internal date
* @param bool $binary Enables BINARY append
*
* @return int|bool Appended message UID or True on success, False on error
*/
public function save_message($folder, &$message, $headers = '', $is_file = false, $flags = [], $date = null, $binary = false)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// make sure folder exists
if (!$this->folder_exists($folder)) {
return false;
}
$date = $this->date_format($date);
if ($is_file) {
$saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary);
}
else {
$saved = $this->conn->append($folder, $message, $flags, $date, $binary);
}
if ($saved) {
// increase messagecount of the target folder
$this->set_messagecount($folder, 'ALL', 1);
$this->plugins->exec_hook('message_saved', [
'folder' => $folder,
'message' => $message,
'headers' => $headers,
'is_file' => $is_file,
'flags' => $flags,
'date' => $date,
'binary' => $binary,
'result' => $saved,
]);
}
return $saved;
}
/**
* Move a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return bool True on success, False on error
*/
public function move_message($uids, $to_mbox, $from_mbox = '')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
if ($to_mbox === $from_mbox) {
return false;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$config = rcube::get_instance()->config;
$to_trash = $to_mbox == $config->get('trash_mbox');
// flag messages as read before moving them
if ($to_trash && $config->get('read_when_deleted')) {
// don't flush cache (4th argument)
$this->set_flag($uids, 'SEEN', $from_mbox, true);
}
// move messages
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
// when moving to Trash we make sure the folder exists
// as it's uncommon scenario we do this when MOVE fails, not before
if (!$moved && $to_trash && $this->get_response_code() == rcube_storage::TRYCREATE) {
if ($this->create_folder($to_mbox, true, 'trash')) {
$moved = $this->conn->move($uids, $from_mbox, $to_mbox);
}
}
if ($moved) {
$this->clear_messagecount($from_mbox);
$this->clear_messagecount($to_mbox);
$this->set_search_dirty($from_mbox);
$this->set_search_dirty($to_mbox);
// unset threads internal cache
unset($this->icache['threads']);
// remove message ids from search set
if ($this->search_set && $from_mbox == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids), $this->folder);
}
}
// remove cached messages
// @TODO: do cache update instead of clearing it
$this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
}
return $moved;
}
/**
* Copy a message from one folder to another
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $to_mbox Target folder
* @param string $from_mbox Source folder
*
* @return bool True on success, False on error
*/
public function copy_message($uids, $to_mbox, $from_mbox = '')
{
if (!strlen($from_mbox)) {
$from_mbox = $this->folder;
}
list($uids, ) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
// copy messages
$copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
if ($copied) {
$this->clear_messagecount($to_mbox);
}
return $copied;
}
/**
* Mark messages as deleted and expunge them
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Source folder
*
* @return bool True on success, False on error
*/
public function delete_message($uids, $folder = '')
{
if (!strlen($folder)) {
$folder = $this->folder;
}
list($uids, $all_mode) = $this->parse_uids($uids);
// exit if no message uids are specified
if (empty($uids)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$deleted = $this->conn->flag($folder, $uids, 'DELETED');
if ($deleted) {
// send expunge command in order to have the deleted message
// really deleted from the folder
$this->expunge_message($uids, $folder, false);
$this->clear_messagecount($folder);
// unset threads internal cache
unset($this->icache['threads']);
$this->set_search_dirty($folder);
// remove message ids from search set
if ($this->search_set && $folder == $this->folder) {
// threads are too complicated to just remove messages from set
if ($this->search_threads || $all_mode) {
$this->refresh_search();
}
else if (!$this->search_set->incomplete) {
$this->search_set->filter(explode(',', $uids));
}
}
// remove cached messages
$this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
}
return $deleted;
}
/**
* Send IMAP expunge command and clear cache
*
* @param mixed $uids Message UIDs as array or comma-separated string, or '*'
* @param string $folder Folder name
* @param bool $clear_cache False if cache should not be cleared
*
* @return bool True on success, False on failure
*/
public function expunge_message($uids, $folder = null, $clear_cache = true)
{
if ($uids && $this->get_capability('UIDPLUS')) {
list($uids, $all_mode) = $this->parse_uids($uids);
}
else {
$uids = null;
}
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return false;
}
// force folder selection and check if folder is writeable
// to prevent a situation when CLOSE is executed on closed
// or EXPUNGE on read-only folder
$result = $this->conn->select($folder);
if (!$result) {
return false;
}
if (!$this->conn->data['READ-WRITE']) {
$this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
return false;
}
// CLOSE(+SELECT) should be faster than EXPUNGE
if (empty($uids) || !empty($all_mode)) {
$result = $this->conn->close();
}
else {
$result = $this->conn->expunge($folder, $uids);
}
if ($result && $clear_cache) {
$this->clear_message_cache($folder, !empty($all_mode) ? null : explode(',', $uids));
$this->clear_messagecount($folder);
}
return $result;
}
/* --------------------------------
* folder management
* --------------------------------*/
/**
* Public method for listing subscribed folders.
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
* @param string $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array List of folders
*/
public function list_folders_subscribed($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false)
{
$cache_key = rcube_cache::key_name('mailboxes', [$root, $name, $filter, $rights]);
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
$data = $this->plugins->exec_hook('storage_folders',
['root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB']);
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
$a_mboxes = $this->list_folders_subscribed_direct($root, $name);
}
if (!is_array($a_mboxes)) {
return [];
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// INBOX should always be available
if (in_array_nocase($root . $name, ['*', '%', 'INBOX', 'INBOX*'])
&& (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
) {
array_unshift($a_mboxes, 'INBOX');
}
// sort folders (always sort for cache)
if (!$skip_sort || $this->cache) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LSUB)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of subscribed folders
* @see rcube_imap::list_folders_subscribed()
*/
public function list_folders_subscribed_direct($root = '', $name = '*')
{
if (!$this->check_connection()) {
return null;
}
$config = rcube::get_instance()->config;
$list_root = $root === '' && $this->list_root ? $this->list_root : $root;
// Server supports LIST-EXTENDED, we can use selection options
// #1486225: Some dovecot versions return wrong result using LIST-EXTENDED
$list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
if ($list_extended) {
// This will also set folder options, LSUB doesn't do that
$result = $this->conn->listMailboxes($list_root, $name, null, ['SUBSCRIBED']);
}
else {
// retrieve list of folders from IMAP server using LSUB
$result = $this->conn->listSubscribed($list_root, $name);
}
if (!is_array($result)) {
return [];
}
// Add/Remove folders according to some configuration options
$this->list_folders_filter($result, $root . $name, ($list_extended ? 'ext-' : '') . 'subscribed');
// Save the last command state, so we can ignore errors on any following UNSUBSCRIBE calls
$state = $this->save_conn_state();
if ($list_extended) {
// unsubscribe non-existent folders, remove from the list
if ($name == '*' && !empty($this->conn->data['LIST'])) {
foreach ($result as $idx => $folder) {
if (($opts = $this->conn->data['LIST'][$folder])
&& in_array_nocase('\\NonExistent', $opts)
) {
$this->conn->unsubscribe($folder);
unset($result[$idx]);
}
}
}
}
else {
// unsubscribe non-existent folders, remove them from the list
if (!empty($result) && $name == '*') {
$existing = $this->list_folders($root, $name);
// Try to make sure the list of existing folders is not malformed,
// we don't want to unsubscribe existing folders on error
if (is_array($existing) && (!empty($root) || count($existing) > 1)) {
$nonexisting = array_diff($result, $existing);
$result = array_diff($result, $nonexisting);
foreach ($nonexisting as $folder) {
$this->conn->unsubscribe($folder);
}
}
}
}
$this->restore_conn_state($state);
return $result;
}
/**
* Get a list of all folders available on the server
*
* @param string $root IMAP root dir
* @param string $name Optional name pattern
* @param mixed $filter Optional filter
* @param string $rights Optional ACL requirements
* @param bool $skip_sort Enable to return unsorted list (for better performance)
*
* @return array Indexed array with folder names
*/
public function list_folders($root = '', $name = '*', $filter = null, $rights = null, $skip_sort = false)
{
$cache_key = rcube_cache::key_name('mailboxes.list', [$root, $name, $filter, $rights]);
// get cached folder list
$a_mboxes = $this->get_cache($cache_key);
if (is_array($a_mboxes)) {
return $a_mboxes;
}
// Give plugins a chance to provide a list of folders
$data = $this->plugins->exec_hook('storage_folders',
['root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST']);
if (isset($data['folders'])) {
$a_mboxes = $data['folders'];
}
else {
// retrieve list of folders from IMAP server
$a_mboxes = $this->list_folders_direct($root, $name);
}
if (!is_array($a_mboxes)) {
$a_mboxes = [];
}
// INBOX should always be available
if (in_array_nocase($root . $name, ['*', '%', 'INBOX', 'INBOX*'])
&& (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
) {
array_unshift($a_mboxes, 'INBOX');
}
// cache folder attributes
if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
$this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
}
// filter folders list according to rights requirements
if ($rights && $this->get_capability('ACL')) {
$a_mboxes = $this->filter_rights($a_mboxes, $rights);
}
// filter folders and sort them
if (!$skip_sort) {
$a_mboxes = $this->sort_folder_list($a_mboxes);
}
// write folders list to cache
$this->update_cache($cache_key, $a_mboxes);
return $a_mboxes;
}
/**
* Method for direct folders listing (LIST)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of folders
* @see rcube_imap::list_folders()
*/
public function list_folders_direct($root = '', $name = '*')
{
if (!$this->check_connection()) {
return null;
}
$list_root = $root === '' && $this->list_root ? $this->list_root : $root;
$result = $this->conn->listMailboxes($list_root, $name);
if (!is_array($result)) {
return [];
}
// Add/Remove folders according to some configuration options
$this->list_folders_filter($result, $root . $name);
return $result;
}
/**
* Apply configured filters on folders list
*/
protected function list_folders_filter(&$result, $root, $update_type = null)
{
$config = rcube::get_instance()->config;
// #1486796: some server configurations doesn't return folders in all namespaces
if ($root === '*' && $config->get('imap_force_ns')) {
$this->list_folders_update($result, $update_type);
}
// Remove hidden folders
if ($config->get('imap_skip_hidden_folders')) {
$result = array_filter($result, function($v) { return $v[0] != '.'; });
}
// Remove folders in shared namespaces (if configured, see self::set_env())
if ($root === '*' && !empty($this->list_excludes)) {
$result = array_filter($result, function($v) {
foreach ($this->list_excludes as $prefix) {
if (strpos($v, $prefix) === 0) {
return false;
}
}
return true;
});
}
}
/**
* Fix folders list by adding folders from other namespaces.
* Needed on some servers e.g. Courier IMAP
*
* @param array $result Reference to folders list
* @param string $type Listing type (ext-subscribed, subscribed or all)
*/
protected function list_folders_update(&$result, $type = null)
{
$namespace = $this->get_namespace();
$search = [];
// build list of namespace prefixes
foreach ((array)$namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $ns_data) {
if (strlen($ns_data[0])) {
$search[] = $ns_data[0];
}
}
}
}
if (!empty($search)) {
// go through all folders detecting namespace usage
foreach ($result as $folder) {
foreach ($search as $idx => $prefix) {
if (strpos($folder, $prefix) === 0) {
unset($search[$idx]);
}
}
if (empty($search)) {
break;
}
}
// get folders in hidden namespaces and add to the result
foreach ($search as $prefix) {
if ($type == 'ext-subscribed') {
$list = $this->conn->listMailboxes('', $prefix . '*', null, ['SUBSCRIBED']);
}
else if ($type == 'subscribed') {
$list = $this->conn->listSubscribed('', $prefix . '*');
}
else {
$list = $this->conn->listMailboxes('', $prefix . '*');
}
if (!empty($list)) {
$result = array_merge($result, $list);
}
}
}
}
/**
* Filter the given list of folders according to access rights
*
* For performance reasons we assume user has full rights
* on all personal folders.
*/
protected function filter_rights($a_folders, $rights)
{
$regex = '/('.$rights.')/';
foreach ($a_folders as $idx => $folder) {
if ($this->folder_namespace($folder) == 'personal') {
continue;
}
$myrights = implode('', (array)$this->my_rights($folder));
if ($myrights !== null && !preg_match($regex, $myrights)) {
unset($a_folders[$idx]);
}
}
return $a_folders;
}
/**
* Get mailbox quota information
*
* @param string $folder Folder name
*
* @return mixed Quota info or False if not supported
*/
public function get_quota($folder = null)
{
if ($this->get_capability('QUOTA') && $this->check_connection()) {
return $this->conn->getQuota($folder);
}
return false;
}
/**
* Get folder size (size of all messages in a folder)
*
* @param string $folder Folder name
*
* @return int Folder size in bytes, False on error
*/
public function folder_size($folder)
{
if (!strlen($folder)) {
return false;
}
if (!$this->check_connection()) {
return 0;
}
if ($this->get_capability('STATUS=SIZE')) {
$status = $this->conn->status($folder, ['SIZE']);
if (is_array($status) && array_key_exists('SIZE', $status)) {
return (int) $status['SIZE'];
}
}
// On Cyrus we can use special folder annotation, which should be much faster
if ($this->get_vendor() == 'cyrus') {
$idx = '/shared/vendor/cmu/cyrus-imapd/size';
$result = $this->get_metadata($folder, $idx, [], true);
if (!empty($result) && isset($result[$folder][$idx]) && is_numeric($result[$folder][$idx])) {
return $result[$folder][$idx];
}
}
// @TODO: could we try to use QUOTA here?
$result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
if (is_array($result)) {
$result = array_sum($result);
}
return $result;
}
/**
* Subscribe to a specific folder(s)
*
* @param array $folders Folder name(s)
*
* @return bool True on success, False on failure
*/
public function subscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'subscribe');
}
/**
* Unsubscribe folder(s)
*
* @param array $a_mboxes Folder name(s)
*
* @return bool True on success, False on failure
*/
public function unsubscribe($folders)
{
// let this common function do the main work
return $this->change_subscription($folders, 'unsubscribe');
}
/**
* Create a new folder on the server and register it in local cache
*
* @param string $folder New folder name
* @param bool $subscribe True if the new folder should be subscribed
* @param string $type Optional folder type (junk, trash, drafts, sent, archive)
* @param bool $noselect Make the folder a \NoSelect folder by adding hierarchy
* separator at the end (useful for server that do not support
* both folders and messages as folder children)
*
* @return bool True on success, False on failure
*/
public function create_folder($folder, $subscribe = false, $type = null, $noselect = false)
{
if (!$this->check_connection()) {
return false;
}
if ($noselect) {
$folder .= $this->delimiter;
}
$result = $this->conn->createFolder($folder, $type ? ["\\" . ucfirst($type)] : null);
// Folder creation may fail when specific special-use flag is not supported.
// Try to create it anyway with no flag specified (#7147)
if (!$result && $type) {
$result = $this->conn->createFolder($folder);
}
// try to subscribe it
if ($result) {
// clear cache
$this->clear_cache('mailboxes', true);
if ($subscribe && !$noselect) {
$this->subscribe($folder);
}
}
return $result;
}
/**
* Set a new name to an existing folder
*
* @param string $folder Folder to rename
* @param string $new_name New folder name
*
* @return bool True on success, False on failure
*/
public function rename_folder($folder, $new_name)
{
if (!strlen($new_name)) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of subscribed folders
if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
$a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
$subscribed = $this->folder_exists($folder, true);
}
else {
$a_subscribed = $this->list_folders_subscribed();
$subscribed = in_array($folder, $a_subscribed);
}
$result = $this->conn->renameFolder($folder, $new_name);
if ($result) {
// unsubscribe the old folder, subscribe the new one
if ($subscribed) {
$this->conn->unsubscribe($folder);
$this->conn->subscribe($new_name);
}
// check if folder children are subscribed
foreach ($a_subscribed as $c_subscribed) {
if (strpos($c_subscribed, $folder.$delm) === 0) {
$this->conn->unsubscribe($c_subscribed);
$this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
$new_name, $c_subscribed));
// clear cache
$this->clear_message_cache($c_subscribed);
}
}
// clear cache
$this->clear_message_cache($folder);
$this->clear_cache('mailboxes', true);
}
return $result;
}
/**
* Remove folder (with subfolders) from the server
*
* @param string $folder Folder name
*
* @return bool True on success, False on failure
*/
public function delete_folder($folder)
{
if (!$this->check_connection()) {
return false;
}
$delm = $this->get_hierarchy_delimiter();
// get list of sub-folders or all folders
// if folder name contains special characters
$path = strspn($folder, '%*') > 0 ? ($folder . $delm) : '';
$sub_mboxes = $this->list_folders('', $path . '*');
// According to RFC3501 deleting a \Noselect folder
// with subfolders may fail. To workaround this we delete
// subfolders first (in reverse order) (#5466)
if (!empty($sub_mboxes)) {
foreach (array_reverse($sub_mboxes) as $mbox) {
if (strpos($mbox, $folder . $delm) === 0) {
if ($this->conn->deleteFolder($mbox)) {
$this->conn->unsubscribe($mbox);
$this->clear_message_cache($mbox);
}
}
}
}
// delete the folder
if ($result = $this->conn->deleteFolder($folder)) {
// and unsubscribe it
$this->conn->unsubscribe($folder);
$this->clear_message_cache($folder);
}
$this->clear_cache('mailboxes', true);
return $result;
}
/**
* Detect special folder associations stored in storage backend
*/
public function get_special_folders($forced = false)
{
$result = parent::get_special_folders();
$rcube = rcube::get_instance();
// Lock SPECIAL-USE after user preferences change (#4782)
if ($rcube->config->get('lock_special_folders')) {
return $result;
}
if (isset($this->icache['special-use'])) {
return array_merge($result, $this->icache['special-use']);
}
if (!$forced || !$this->get_capability('SPECIAL-USE')) {
return $result;
}
if (!$this->check_connection()) {
return $result;
}
$types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
$special = [];
// request \Subscribed flag in LIST response as performance improvement for folder_exists()
$folders = $this->conn->listMailboxes('', '*', ['SUBSCRIBED'], ['SPECIAL-USE']);
if (!empty($folders)) {
foreach ($folders as $idx => $folder) {
if (is_array($folder)) {
$folder = $idx;
}
if (!empty($this->conn->data['LIST'][$folder])) {
$flags = $this->conn->data['LIST'][$folder];
foreach ($types as $type) {
if (in_array($type, $flags)) {
$type = strtolower(substr($type, 1));
$special[$type] = $folder;
}
}
}
}
}
$this->icache['special-use'] = $special;
unset($this->icache['special-folders']);
return array_merge($result, $special);
}
/**
* Set special folder associations stored in storage backend
*/
public function set_special_folders($specials)
{
if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$folders = $this->get_special_folders(true);
$old = isset($this->icache['special-use']) ? (array) $this->icache['special-use'] : [];
foreach ($specials as $type => $folder) {
if (in_array($type, rcube_storage::$folder_types)) {
$old_folder = isset($old[$type]) ? $old[$type] : null;
if ($old_folder !== $folder) {
// unset old-folder metadata
if ($old_folder !== null) {
$this->delete_metadata($old_folder, ['/private/specialuse']);
}
// set new folder metadata
if ($folder) {
$this->set_metadata($folder, ['/private/specialuse' => "\\" . ucfirst($type)]);
}
}
}
}
$this->icache['special-use'] = $specials;
unset($this->icache['special-folders']);
return true;
}
/**
* Checks if folder exists and is subscribed
*
* @param string $folder Folder name
* @param bool $subscription Enable subscription checking
*
* @return bool True or False
*/
public function folder_exists($folder, $subscription = false)
{
if ($folder == 'INBOX') {
return true;
}
$key = $subscription ? 'subscribed' : 'existing';
if (!empty($this->icache[$key]) && in_array($folder, (array) $this->icache[$key])) {
return true;
}
if (!$this->check_connection()) {
return false;
}
if ($subscription) {
// It's possible we already called LIST command, check LIST data
if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
&& in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
) {
$a_folders = [$folder];
}
else {
$a_folders = $this->conn->listSubscribed('', $folder);
}
}
else {
// It's possible we already called LIST command, check LIST data
if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
$a_folders = [$folder];
}
else {
$a_folders = $this->conn->listMailboxes('', $folder);
}
}
if (is_array($a_folders) && in_array($folder, $a_folders)) {
$this->icache[$key][] = $folder;
return true;
}
return false;
}
/**
* Returns the namespace where the folder is in
*
* @param string $folder Folder name
*
* @return string One of 'personal', 'other' or 'shared'
*/
public function folder_namespace($folder)
{
if ($folder == 'INBOX') {
return 'personal';
}
foreach ($this->namespace as $type => $namespace) {
if (is_array($namespace)) {
foreach ($namespace as $ns) {
if ($len = strlen($ns[0])) {
if (($len > 1 && $folder == substr($ns[0], 0, -1))
|| strpos($folder, $ns[0]) === 0
) {
return $type;
}
}
}
}
}
return 'personal';
}
/**
* Modify folder name according to personal namespace prefix.
* For output it removes prefix of the personal namespace if it's possible.
* For input it adds the prefix. Use it before creating a folder in root
* of the folders tree.
*
* @param string $folder Folder name
* @param string $mode Mode name (out/in)
*
* @return string Folder name
*/
public function mod_folder($folder, $mode = 'out')
{
$prefix = isset($this->namespace['prefix_' . $mode]) ? $this->namespace['prefix_' . $mode] : null;
if ($prefix === null || $prefix === ''
|| !($prefix_len = strlen($prefix)) || !strlen($folder)
) {
return $folder;
}
// remove prefix for output
if ($mode == 'out') {
if (substr($folder, 0, $prefix_len) === $prefix) {
return substr($folder, $prefix_len);
}
return $folder;
}
// add prefix for input (e.g. folder creation)
return $prefix . $folder;
}
/**
* Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
*
* @param string $folder Folder name
* @param bool $force Set to True if attributes should be refreshed
*
* @return array Options list
*/
public function folder_attributes($folder, $force = false)
{
// get attributes directly from LIST command
if (!empty($this->conn->data['LIST'])
&& isset($this->conn->data['LIST'][$folder])
&& is_array($this->conn->data['LIST'][$folder])
) {
$opts = $this->conn->data['LIST'][$folder];
}
// get cached folder attributes
else if (!$force) {
$opts = $this->get_cache('mailboxes.attributes');
if ($opts && isset($opts[$folder])) {
$opts = $opts[$folder];
}
}
if (!isset($opts) || !is_array($opts)) {
if (!$this->check_connection()) {
return [];
}
$this->conn->listMailboxes('', $folder);
if (isset($this->conn->data['LIST'][$folder])) {
$opts = $this->conn->data['LIST'][$folder];
}
}
return isset($opts) && is_array($opts) ? $opts : [];
}
/**
* Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
* PERMANENTFLAGS, UIDNEXT, UNSEEN
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_data($folder)
{
if (!strlen($folder)) {
$folder = $this->folder !== null ? $this->folder : 'INBOX';
}
if ($this->conn->selected != $folder) {
if (!$this->check_connection()) {
return [];
}
if ($this->conn->select($folder)) {
$this->folder = $folder;
}
else {
return null;
}
}
$data = $this->conn->data;
// add (E)SEARCH result for ALL UNDELETED query
if (!empty($this->icache['undeleted_idx'])
&& $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
) {
$data['UNDELETED'] = $this->icache['undeleted_idx'];
}
// dovecot does not return HIGHESTMODSEQ until requested, we use it though in our caching system
// calling STATUS is needed only once, after first use mod-seq db will be maintained
if (!isset($data['HIGHESTMODSEQ']) && empty($data['NOMODSEQ'])
&& ($this->get_capability('QRESYNC') || $this->get_capability('CONDSTORE'))
) {
if ($add_data = $this->conn->status($folder, ['HIGHESTMODSEQ'])) {
$data = array_merge($data, $add_data);
}
}
return $data;
}
/**
* Returns extended information about the folder
*
* @param string $folder Folder name
*
* @return array Data
*/
public function folder_info($folder)
{
if (!empty($this->icache['options']) && $this->icache['options']['name'] == $folder) {
return $this->icache['options'];
}
// get cached metadata
$cache_key = rcube_cache::key_name('mailboxes.folder-info', [$folder]);
$cached = $this->get_cache($cache_key);
if (is_array($cached)) {
return $cached;
}
$acl = $this->get_capability('ACL');
$namespace = $this->get_namespace();
$options = ['is_root' => false];
// check if the folder is a namespace prefix
if (!empty($namespace)) {
$mbox = $folder . $this->delimiter;
foreach ($namespace as $ns) {
if (!empty($ns)) {
foreach ($ns as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break 2;
}
}
}
}
}
// check if the folder is other user virtual-root
if ($options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
$parts = explode($this->delimiter, $folder);
if (count($parts) == 2) {
$mbox = $parts[0] . $this->delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $mbox) {
$options['is_root'] = true;
break;
}
}
}
}
$options['name'] = $folder;
$options['attributes'] = $this->folder_attributes($folder, true);
$options['namespace'] = $this->folder_namespace($folder);
$options['special'] = $this->is_special_folder($folder);
$options['noselect'] = false;
// Set 'noselect' flag
if (is_array($options['attributes'])) {
foreach ($options['attributes'] as $attrib) {
$attrib = strtolower($attrib);
if ($attrib == '\noselect' || $attrib == '\nonexistent') {
$options['noselect'] = true;
}
}
}
else {
$options['noselect'] = true;
}
// Get folder rights (MYRIGHTS)
if ($acl && ($rights = $this->my_rights($folder))) {
$options['rights'] = $rights;
}
// Set 'norename' flag
if (!empty($options['rights'])) {
$rfc_4314 = is_array($this->get_capability('RIGHTS'));
$options['norename'] = ($rfc_4314 && !in_array('x', $options['rights']))
|| (!$rfc_4314 && !in_array('d', $options['rights']));
if (!$options['noselect']) {
$options['noselect'] = !in_array('r', $options['rights']);
}
}
else {
$options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
}
// update caches
$this->icache['options'] = $options;
$this->update_cache($cache_key, $options);
return $options;
}
/**
* Synchronizes messages cache.
*
* @param string $folder Folder name
*/
public function folder_sync($folder)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->synchronize($folder);
}
}
/**
* Check if the folder name is valid
*
* @param string $folder Folder name (UTF-8)
* @param string &$char First forbidden character found
*
* @return bool True if the name is valid, False otherwise
*/
public function folder_validate($folder, &$char = null)
{
if (parent::folder_validate($folder, $char)) {
$vendor = $this->get_vendor();
$regexp = '\\x00-\\x1F\\x7F%*';
if ($vendor == 'cyrus') {
// List based on testing Kolab's Cyrus-IMAP 2.5
$regexp .= '!`@(){}|\\?<;"';
}
if (!preg_match("/[$regexp]/", $folder, $m)) {
return true;
}
$char = $m[0];
}
return false;
}
/**
* Get message header names for rcube_imap_generic::fetchHeader(s)
*
* @return string Space-separated list of header names
*/
protected function get_fetch_headers()
{
if (!empty($this->options['fetch_headers'])) {
$headers = explode(' ', $this->options['fetch_headers']);
}
else {
$headers = [];
}
if ($this->messages_caching || !empty($this->options['all_headers'])) {
$headers = array_merge($headers, $this->all_headers);
}
return $headers;
}
/* -----------------------------------------
* ACL and METADATA/ANNOTATEMORE methods
* ----------------------------------------*/
/**
* Changes the ACL on the specified folder (SETACL)
*
* @param string $folder Folder name
* @param string $user User name
* @param string $acl ACL string
*
* @return bool True on success, False on failure
* @since 0.5-beta
*/
public function set_acl($folder, $user, $acl)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
$this->clear_cache(rcube_cache::key_name('mailboxes.folder-info', [$folder]));
return $this->conn->setACL($folder, $user, $acl);
}
/**
* Removes any <identifier,rights> pair for the
* specified user from the ACL for the specified
* folder (DELETEACL)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return bool True on success, False on failure
* @since 0.5-beta
*/
public function delete_acl($folder, $user)
{
if (!$this->get_capability('ACL')) {
return false;
}
if (!$this->check_connection()) {
return false;
}
return $this->conn->deleteACL($folder, $user);
}
/**
* Returns the access control list for folder (GETACL)
*
* @param string $folder Folder name
*
* @return array User-rights array on success, NULL on error
* @since 0.5-beta
*/
public function get_acl($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->getACL($folder);
}
/**
* Returns information about what rights can be granted to the
* user (identifier) in the ACL for the folder (LISTRIGHTS)
*
* @param string $folder Folder name
* @param string $user User name
*
* @return array List of user rights
* @since 0.5-beta
*/
public function list_rights($folder, $user)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->listRights($folder, $user);
}
/**
* Returns the set of rights that the current user has to
* folder (MYRIGHTS)
*
* @param string $folder Folder name
*
* @return array MYRIGHTS response on success, NULL on error
* @since 0.5-beta
*/
public function my_rights($folder)
{
if (!$this->get_capability('ACL')) {
return null;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->myRights($folder);
}
/**
* Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry-value array (use NULL value as NIL)
*
* @return bool True on success, False on failure
* @since 0.5-beta
*/
public function set_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->setMetadata($folder, $entries);
}
if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $entry => $value) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$entry] = [$ent, $attr, $value];
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entry names array
*
* @return bool True on success, False on failure
* @since 0.5-beta
*/
public function delete_metadata($folder, $entries)
{
if (!$this->check_connection()) {
return false;
}
$this->clear_cache('mailboxes.metadata.', true);
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
return $this->conn->deleteMetadata($folder, $entries);
}
if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
foreach ((array)$entries as $idx => $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$entries[$idx] = [$ent, $attr, null];
}
return $this->conn->setAnnotation($folder, $entries);
}
return false;
}
/**
* Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
*
* @param string $folder Folder name (empty for server metadata)
* @param array $entries Entries
* @param array $options Command options (with MAXSIZE and DEPTH keys)
* @param bool $force Disables cache use
*
* @return array Metadata entry-value hash array on success, NULL on error
* @since 0.5-beta
*/
public function get_metadata($folder, $entries, $options = [], $force = false)
{
$entries = (array) $entries;
if (!$force) {
$cache_key = rcube_cache::key_name('mailboxes.metadata', [$folder, $options, $entries]);
// get cached data
$cached_data = $this->get_cache($cache_key);
if (is_array($cached_data)) {
return $cached_data;
}
}
if (!$this->check_connection()) {
return null;
}
if ($this->get_capability('METADATA') ||
(!strlen($folder) && $this->get_capability('METADATA-SERVER'))
) {
$res = $this->conn->getMetadata($folder, $entries, $options);
}
else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
$queries = [];
$res = [];
// Convert entry names
foreach ($entries as $entry) {
list($ent, $attr) = $this->md2annotate($entry);
$queries[$attr][] = $ent;
}
// @TODO: Honor MAXSIZE and DEPTH options
foreach ($queries as $attrib => $entry) {
$result = $this->conn->getAnnotation($folder, $entry, $attrib);
// an error, invalidate any previous getAnnotation() results
if (!is_array($result)) {
return null;
}
foreach ($result as $fldr => $data) {
$res[$fldr] = array_merge((array) $res[$fldr], $data);
}
}
}
if (isset($res)) {
if (!$force && !empty($cache_key)) {
$this->update_cache($cache_key, $res);
}
return $res;
}
}
/**
* Converts the METADATA extension entry name into the correct
* entry-attrib names for older ANNOTATEMORE version.
*
* @param string $entry Entry name
*
* @return array Entry-attribute list, NULL if not supported (?)
*/
protected function md2annotate($entry)
{
if (substr($entry, 0, 7) == '/shared') {
return [substr($entry, 7), 'value.shared'];
}
else if (substr($entry, 0, 8) == '/private') {
return [substr($entry, 8), 'value.priv'];
}
// @TODO: log error
}
/* --------------------------------
* internal caching methods
* --------------------------------*/
/**
* Enable or disable indexes caching
*
* @param string $type Cache type (@see rcube::get_cache)
*/
public function set_caching($type)
{
if ($type) {
$this->caching = $type;
}
else {
if ($this->cache) {
$this->cache->close();
}
$this->cache = null;
$this->caching = false;
}
}
/**
* Getter for IMAP cache object
*/
protected function get_cache_engine()
{
if ($this->caching && !$this->cache) {
$rcube = rcube::get_instance();
$ttl = $rcube->config->get('imap_cache_ttl', '10d');
$this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
}
return $this->cache;
}
/**
* Returns cached value
*
* @param string $key Cache key
*
* @return mixed
*/
public function get_cache($key)
{
if ($cache = $this->get_cache_engine()) {
return $cache->get($key);
}
}
/**
* Update cache
*
* @param string $key Cache key
* @param mixed $data Data
*/
public function update_cache($key, $data)
{
if ($cache = $this->get_cache_engine()) {
$cache->set($key, $data);
}
}
/**
* Clears the cache.
*
* @param string $key Cache key name or pattern
* @param bool $prefix_mode Enable it to clear all keys starting
* with prefix specified in $key
*/
public function clear_cache($key = null, $prefix_mode = false)
{
if ($cache = $this->get_cache_engine()) {
$cache->remove($key, $prefix_mode);
}
}
/* --------------------------------
* message caching methods
* --------------------------------*/
/**
* Enable or disable messages caching
*
* @param bool $set Flag
* @param int $mode Cache mode
*/
public function set_messages_caching($set, $mode = null)
{
if ($set) {
$this->messages_caching = true;
if ($mode && ($cache = $this->get_mcache_engine())) {
$cache->set_mode($mode);
}
}
else {
if ($this->mcache) {
$this->mcache->close();
}
$this->mcache = null;
$this->messages_caching = false;
}
}
/**
* Getter for messages cache object
*/
protected function get_mcache_engine()
{
if ($this->messages_caching && !$this->mcache) {
$rcube = rcube::get_instance();
if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
$ttl = $rcube->config->get('messages_cache_ttl', '10d');
$threshold = $rcube->config->get('messages_cache_threshold', 50);
$this->mcache = new rcube_imap_cache(
$dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
}
}
return $this->mcache;
}
/**
* Clears the messages cache.
*
* @param string $folder Folder name
* @param array $uids Optional message UIDs to remove from cache
*/
protected function clear_message_cache($folder = null, $uids = null)
{
if ($mcache = $this->get_mcache_engine()) {
$mcache->clear($folder, $uids);
}
}
/**
* Delete outdated cache entries
*/
function cache_gc()
{
rcube_imap_cache::gc();
}
/* --------------------------------
* protected methods
* --------------------------------*/
/**
* Determines if server supports dual use folders (those can
* contain both sub-folders and messages).
*
* @return bool
*/
protected function detect_dual_use_folders()
{
$val = rcube::get_instance()->config->get('imap_dual_use_folders');
if ($val !== null) {
return (bool) $val;
}
if (!$this->check_connection()) {
return false;
}
$folder = str_replace('.', '', 'foldertest' . microtime(true));
$folder = $this->mod_folder($folder, 'in');
$subfolder = $folder . $this->delimiter . 'foldertest';
if ($this->conn->createFolder($folder)) {
if ($created = $this->conn->createFolder($subfolder)) {
$this->conn->deleteFolder($subfolder);
}
$this->conn->deleteFolder($folder);
return $created;
}
return false;
}
/**
* Validate the given input and save to local properties
*
* @param string $sort_field Sort column
* @param string $sort_order Sort order
*/
protected function set_sort_order($sort_field, $sort_order)
{
if ($sort_field != null) {
$this->sort_field = asciiwords($sort_field);
}
if ($sort_order != null) {
$this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
}
}
/**
* Sort folders in alphabetical order. Optionally put special folders
* first and other-users/shared namespaces last.
*
* @param array $a_folders Folders list
* @param bool $skip_special Skip special folders handling
*
* @return array Sorted list
*/
public function sort_folder_list($a_folders, $skip_special = false)
{
$folders = [];
// convert names to UTF-8
foreach ($a_folders as $folder) {
// for better performance skip encoding conversion
// if the string does not look like UTF7-IMAP
$folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
}
// sort folders
// asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
uasort($folders, [$this, 'sort_folder_comparator']);
$folders = array_keys($folders);
if ($skip_special || empty($folders)) {
return $folders;
}
// Collect special folders and non-personal namespace roots
$specials = array_merge(['INBOX'], array_values($this->get_special_folders()));
$ns_roots = [];
foreach (['other', 'shared'] as $ns_name) {
if ($ns = $this->get_namespace($ns_name)) {
foreach ($ns as $root) {
if (isset($root[0]) && strlen($root[0])) {
$ns_roots[rtrim($root[0], $root[1])] = $root[0];
}
}
}
}
// Force the type of folder name variable (#1485527)
$folders = array_map('strval', $folders);
$out = [];
// Put special folders on top...
$specials = array_unique(array_intersect($specials, $folders));
$folders = array_merge($specials, array_diff($folders, $specials));
// ... and rebuild the list to move their subfolders where they belong
$this->sort_folder_specials(null, $folders, $specials, $out);
// Put other-user/shared namespaces at the end
if (!empty($ns_roots)) {
$folders = [];
foreach ($out as $folder) {
foreach ($ns_roots as $root => $prefix) {
if ($folder === $root || strpos($folder, $prefix) === 0) {
$folders[] = $folder;
}
}
}
if (!empty($folders)) {
$out = array_merge(array_diff($out, $folders), $folders);
}
}
return $out;
}
/**
* Recursive function to put subfolders of special folders in place
*/
protected function sort_folder_specials($folder, &$list, &$specials, &$out)
{
$count = count($list);
for ($i = 0; $i < $count; $i++) {
$name = $list[$i];
if ($name === null) {
continue;
}
if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
$out[] = $name;
$list[$i] = null;
if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
unset($specials[$found]);
$this->sort_folder_specials($name, $list, $specials, $out);
}
}
}
}
/**
* Callback for uasort() that implements correct
* locale-aware case-sensitive sorting
*/
protected function sort_folder_comparator($str1, $str2)
{
if ($this->sort_folder_collator === null) {
$this->sort_folder_collator = false;
// strcoll() does not work with UTF8 locale on Windows,
// use Collator from the intl extension
if (stripos(PHP_OS, 'win') === 0 && function_exists('collator_compare')) {
$locale = $this->options['language'] ?: 'en_US';
$this->sort_folder_collator = collator_create($locale) ?: false;
}
}
$path1 = explode($this->delimiter, $str1);
$path2 = explode($this->delimiter, $str2);
foreach ($path1 as $idx => $folder1) {
$folder2 = isset($path2[$idx]) ? $path2[$idx] : '';
if ($folder1 === $folder2) {
continue;
}
if ($this->sort_folder_collator) {
return collator_compare($this->sort_folder_collator, $folder1, $folder2);
}
return strcoll($folder1, $folder2);
}
}
/**
* Find UID of the specified message sequence ID
*
* @param int $id Message (sequence) ID
* @param string $folder Folder name
*
* @return int Message UID
*/
public function id2uid($id, $folder = null)
{
if (!strlen($folder)) {
$folder = $this->folder;
}
if (!$this->check_connection()) {
return null;
}
return $this->conn->ID2UID($folder, $id);
}
/**
* Subscribe/unsubscribe a list of folders and update local cache
*/
protected function change_subscription($folders, $mode)
{
$updated = 0;
$folders = (array) $folders;
if (!empty($folders)) {
if (!$this->check_connection()) {
return false;
}
foreach ($folders as $folder) {
$updated += (int) $this->conn->{$mode}($folder);
}
}
// clear cached folders list(s)
if ($updated) {
$this->clear_cache('mailboxes', true);
}
return $updated == count($folders);
}
/**
* Increase/decrease messagecount for a specific folder
*/
protected function set_messagecount($folder, $mode, $increment)
{
if (!is_numeric($increment)) {
return false;
}
$mode = strtoupper($mode);
$a_folder_cache = $this->get_cache('messagecount');
if (
!isset($a_folder_cache[$folder])
|| !is_array($a_folder_cache[$folder])
|| !isset($a_folder_cache[$folder][$mode])
) {
return false;
}
// add incremental value to messagecount
$a_folder_cache[$folder][$mode] += $increment;
// there's something wrong, delete from cache
if ($a_folder_cache[$folder][$mode] < 0) {
unset($a_folder_cache[$folder][$mode]);
}
// write back to cache
$this->update_cache('messagecount', $a_folder_cache);
return true;
}
/**
* Remove messagecount of a specific folder from cache
*/
protected function clear_messagecount($folder, $mode = [])
{
$a_folder_cache = $this->get_cache('messagecount');
if (isset($a_folder_cache[$folder]) && is_array($a_folder_cache[$folder])) {
if (!empty($mode)) {
foreach ((array) $mode as $key) {
unset($a_folder_cache[$folder][$key]);
}
}
else {
unset($a_folder_cache[$folder]);
}
$this->update_cache('messagecount', $a_folder_cache);
}
}
/**
* Converts date string/object into IMAP date/time format
*/
protected function date_format($date)
{
if (empty($date)) {
return null;
}
if (!is_object($date) || !is_a($date, 'DateTime')) {
try {
$timestamp = rcube_utils::strtotime($date);
$date = new DateTime("@".$timestamp);
}
catch (Exception $e) {
return null;
}
}
return $date->format('d-M-Y H:i:s O');
}
/**
* Remember state of the IMAP connection (last IMAP command).
* Use e.g. if you want to execute more commands and ignore results of these.
*
* @return array Connection state
*/
protected function save_conn_state()
{
return [
$this->conn->error,
$this->conn->errornum,
$this->conn->resultcode,
];
}
/**
* Restore saved connection state.
*
* @param array $state Connection result
*/
protected function restore_conn_state($state)
{
list($this->conn->error, $this->conn->errornum, $this->conn->resultcode) = $state;
}
/**
* This is our own debug handler for the IMAP connection
*/
public function debug_handler($imap, $message)
{
rcube::write_log('imap', $message);
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Mar 1, 3:27 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
165655
Default Alt Text
(324 KB)
Attached To
Mode
R3 roundcubemail
Attached
Detach File
Event Timeline
Log In to Comment