Import Ruty

This commit is contained in:
2024-03-11 00:58:34 +01:00
parent 34a31bb184
commit 985f1ab418
618 changed files with 225414 additions and 0 deletions
@@ -0,0 +1,150 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Copy a contact record from one directory to another |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_copy extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$cids = self::get_cids();
$target = rcube_utils::get_input_string('_to', rcube_utils::INPUT_POST);
$target_group = rcube_utils::get_input_string('_togid', rcube_utils::INPUT_POST);
$success = 0;
$errormsg = 'copyerror';
$maxnum = $rcmail->config->get('max_group_members', 0);
foreach ($cids as $source => $cid) {
// Something wrong, target not specified
if (!strlen($target)) {
break;
}
// It might happen when copying records from search result
// Do nothing, go to next source
if ((string) $target == (string) $source) {
continue;
}
$CONTACTS = $rcmail->get_address_book($source);
$TARGET = $rcmail->get_address_book($target);
if (!$TARGET || !$TARGET->ready || $TARGET->readonly) {
break;
}
$ids = [];
foreach ($cid as $cid) {
$a_record = $CONTACTS->get_record($cid, true);
// avoid copying groups
if (isset($a_record['_type']) && $a_record['_type'] == 'group') {
continue;
}
// Check if contact exists, if so, we'll need it's ID
// Note: Some addressbooks allows empty email address field
// @TODO: should we check all email addresses?
$email = $CONTACTS->get_col_values('email', $a_record, true);
if (!empty($email)) {
$result = $TARGET->search('email', $email[0], 1, true, true);
}
else if (!empty($a_record['name'])) {
$result = $TARGET->search('name', $a_record['name'], 1, true, true);
}
else {
$result = new rcube_result_set();
}
// insert contact record
if (!$result->count) {
$plugin = $rcmail->plugins->exec_hook('contact_create', [
'record' => $a_record,
'source' => $target,
'group' => $target_group
]);
if (!$plugin['abort']) {
if ($insert_id = $TARGET->insert($plugin['record'], false)) {
$ids[] = $insert_id;
$success++;
}
}
else if ($plugin['result']) {
$ids = array_merge($ids, $plugin['result']);
$success++;
}
}
else {
$record = $result->first();
$ids[] = $record['ID'];
$errormsg = empty($email) ? 'contactnameexists' : 'contactexists';
}
}
// assign to group
if ($target_group && $TARGET->groups && !empty($ids)) {
$plugin = $rcmail->plugins->exec_hook('group_addmembers', [
'group_id' => $target_group,
'ids' => $ids,
'source' => $target
]);
if (!$plugin['abort']) {
$TARGET->reset();
$TARGET->set_group($target_group);
if ($maxnum && ($TARGET->count()->count + count($plugin['ids']) > $maxnum)) {
$rcmail->output->show_message('maxgroupmembersreached', 'warning', ['max' => $maxnum]);
$rcmail->output->send();
}
if (($cnt = $TARGET->add_to_group($target_group, $plugin['ids'])) && $cnt > $success) {
$success = $cnt;
}
}
else if (!empty($plugin['result'])) {
$success = $plugin['result'];
}
$errormsg = !empty($plugin['message']) ? $plugin['message'] : 'copyerror';
}
}
if (!$success) {
$rcmail->output->show_message($errormsg, 'error');
}
else {
$rcmail->output->show_message('copysuccess', 'confirmation', ['nr' => $success]);
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,172 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delete the submitted contacts (CIDs) from the users address book |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_delete extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$cids = self::get_cids(null, rcube_utils::INPUT_POST);
$delcnt = 0;
// remove previous deletes
$undo_time = $rcmail->config->get('undo_timeout', 0);
$rcmail->session->remove('contact_undo');
foreach ($cids as $source => $cid) {
$CONTACTS = self::contact_source($source);
if ($CONTACTS->readonly && empty($CONTACTS->deletable)) {
// more sources? do nothing, probably we have search results from
// more than one source, some of these sources can be readonly
if (count($cids) == 1) {
$rcmail->output->show_message('contactdelerror', 'error');
$rcmail->output->command('list_contacts');
$rcmail->output->send();
}
continue;
}
$plugin = $rcmail->plugins->exec_hook('contact_delete', [
'id' => $cid,
'source' => $source
]);
$deleted = !$plugin['abort'] ? $CONTACTS->delete($cid, $undo_time < 1) : $plugin['result'];
if (!$deleted) {
if (!empty($plugin['message'])) {
$error = $plugin['message'];
}
else if (($error = $CONTACTS->get_error()) && !empty($error['message'])) {
$error = $error['message'];
}
else {
$error = 'contactdelerror';
}
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GP);
$group = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_GP);
$rcmail->output->show_message($error, 'error');
$rcmail->output->command('list_contacts', $source, $group);
$rcmail->output->send();
}
else {
$delcnt += $deleted;
// store deleted contacts IDs in session for undo action
if ($undo_time > 0 && $CONTACTS->undelete) {
$_SESSION['contact_undo']['data'][$source] = $cid;
}
}
}
if (!empty($_SESSION['contact_undo'])) {
$_SESSION['contact_undo']['ts'] = time();
$msg = html::span(null, $rcmail->gettext('contactdeleted'))
. ' ' . html::a(
['onclick' => rcmail_output::JS_OBJECT_NAME.".command('undo', '', this)"],
$rcmail->gettext('undo')
);
$rcmail->output->show_message($msg, 'confirmation', null, true, $undo_time);
}
else {
$rcmail->output->show_message('contactdeleted', 'confirmation');
}
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
$page = $_SESSION['page'] ?? 1;
// update saved search after data changed
if (($records = self::search_update(true)) !== false) {
// create resultset object
$count = count($records);
$first = ($page-1) * $page_size;
$result = new rcube_result_set($count, $first);
$pages = ceil((count($records) + $delcnt) / $page_size);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($page_size * ($page - 1))) {
$rcmail->output->command('list_page', 'prev');
$rowcount = $rcmail->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
// sort the records
ksort($records, SORT_LOCALE_STRING);
$first += $page_size;
// create resultset object
$res = new rcube_result_set($count, $first - $delcnt);
if ($page_size < $count) {
$records = array_slice($records, $first - $delcnt, $delcnt);
}
$res->records = array_values($records);
$records = $res;
}
else {
unset($records);
}
}
else if (isset($CONTACTS)) {
// count contacts for this user
$result = $CONTACTS->count();
$pages = ceil(($result->count + $delcnt) / $page_size);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($page_size * ($page - 1))) {
$rcmail->output->command('list_page', 'prev');
$rowcount = $rcmail->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
$CONTACTS->set_page($page);
$records = $CONTACTS->list_records(null, -$delcnt);
}
}
if (!isset($rowcount)) {
$rowcount = isset($result) ? self::get_rowcount_text($result) : '';
}
// update message count display
$rcmail->output->set_env('pagecount', isset($result) ? ceil($result->count / $page_size) : 0);
$rcmail->output->command('set_rowcount', $rowcount);
// add new rows from next page (if any)
if (!empty($records)) {
self::js_contacts_list($records);
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,269 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for a contact entry |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_edit 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 ($rcmail->action == 'edit') {
// Get contact ID and source ID from request
$cids = self::get_cids();
$source = key($cids);
$cid = array_first($cids[$source]);
// Initialize addressbook
$CONTACTS = self::contact_source($source, true);
// Contact edit
if ($cid && (self::$contact = $CONTACTS->get_record($cid, true))) {
$rcmail->output->set_env('cid', self::$contact['ID']);
}
// editing not allowed here
if ($CONTACTS->readonly || !empty(self::$contact['readonly'])) {
$rcmail->output->show_message('sourceisreadonly');
$rcmail->overwrite_action('show');
return;
}
if (empty(self::$contact)) {
$rcmail->output->show_message('contactnotfound', 'error');
}
}
else {
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
if (strlen($source)) {
$CONTACTS = $rcmail->get_address_book($source, true);
}
if (empty($CONTACTS) || $CONTACTS->readonly) {
$CONTACTS = $rcmail->get_address_book(rcube_addressbook::TYPE_DEFAULT, true);
$source = $rcmail->get_address_book_id($CONTACTS);
}
// Initialize addressbook
$CONTACTS = self::contact_source($source, true);
}
self::$SOURCE_ID = $source;
self::$CONTACTS = $CONTACTS;
self::set_sourcename($CONTACTS);
// check if we have a valid result
if (!empty($args['contact'])) {
self::$contact = $args['contact'];
}
$rcmail->output->add_handlers([
'contactedithead' => [$this, 'contact_edithead'],
'contacteditform' => [$this, 'contact_editform'],
'contactphoto' => [$this, 'contact_photo'],
'photouploadform' => [$this, 'upload_photo_form'],
'sourceselector' => [$this, 'source_selector'],
'filedroparea' => [$this, 'photo_drop_area'],
]);
$rcmail->output->set_pagetitle($rcmail->gettext(($rcmail->action == 'add' ? 'addcontact' : 'editcontact')));
if ($rcmail->action == 'add' && $rcmail->output->template_exists('contactadd')) {
$rcmail->output->send('contactadd');
}
// this will be executed if no template for addcontact exists
$rcmail->output->send('contactedit');
}
public static function contact_edithead($attrib)
{
$rcmail = rcmail::get_instance();
$business_mode = $rcmail->config->get('contact_form_mode') === 'business';
// check if we have a valid result
$i_size = !empty($attrib['size']) ? $attrib['size'] : 20;
$form = [
'head' => [
'name' => $rcmail->gettext('contactnameandorg'),
'content' => [
'source' => ['id' => '_source', 'label' => $rcmail->gettext('addressbook')],
'prefix' => ['size' => $i_size],
'firstname' => ['size' => $i_size, 'visible' => true],
'middlename' => ['size' => $i_size],
'surname' => ['size' => $i_size, 'visible' => true],
'suffix' => ['size' => $i_size],
'name' => ['size' => $i_size * 2],
'nickname' => ['size' => $i_size * 2],
'organization' => ['size' => $i_size * 2, 'visible' => $business_mode],
'department' => ['size' => $i_size * 2, 'visible' => $business_mode],
'jobtitle' => ['size' => $i_size * 2, 'visible' => $business_mode],
]
]
];
list($form_start, $form_end) = self::get_form_tags($attrib);
unset($attrib['form'], $attrib['name'], $attrib['size']);
// return the address edit form
$out = self::contact_form($form, self::$contact, $attrib);
return $form_start . $out . $form_end;
}
public static function contact_editform($attrib)
{
$rcmail = rcmail::get_instance();
$addr_tpl = $rcmail->config->get('address_template', '');
// copy (parsed) address template to client
if (preg_match_all('/\{([a-z0-9]+)\}([^{]*)/i', $addr_tpl, $templ, PREG_SET_ORDER)) {
$rcmail->output->set_env('address_template', $templ);
}
$i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
$t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 10;
$t_cols = !empty($attrib['textareacols']) ? $attrib['textareacols'] : 40;
$short_labels = self::get_bool_attr($attrib, 'short-legend-labels');
$form = [
'contact' => [
'name' => $rcmail->gettext('properties'),
'content' => [
'email' => ['size' => $i_size, 'maxlength' => 254, 'visible' => true],
'phone' => ['size' => $i_size, 'visible' => true],
'address' => ['visible' => true],
'website' => ['size' => $i_size],
'im' => ['size' => $i_size],
],
],
'personal' => [
'name' => $rcmail->gettext($short_labels ? 'personal' : 'personalinfo'),
'content' => [
'gender' => ['visible' => true],
'maidenname' => ['size' => $i_size],
'birthday' => ['visible' => true],
'anniversary' => [],
'manager' => ['size' => $i_size],
'assistant' => ['size' => $i_size],
'spouse' => ['size' => $i_size],
],
],
];
if (isset(self::$CONTACT_COLTYPES['notes'])) {
$form['notes'] = [
'name' => $rcmail->gettext('notes'),
'single' => true,
'content' => [
'notes' => ['size' => $t_cols, 'rows' => $t_rows, 'label' => false, 'visible' => true, 'limit' => 1],
],
];
}
list($form_start, $form_end) = self::get_form_tags($attrib);
unset($attrib['form']);
// return the complete address edit form as table
$out = self::contact_form($form, self::$contact, $attrib);
return $form_start . $out . $form_end;
}
public static function upload_photo_form($attrib)
{
$rcmail = rcmail::get_instance();
$hidden = new html_hiddenfield(['name' => '_cid', 'value' => $rcmail->output->get_env('cid')]);
$attrib['prefix'] = $hidden->show();
$input_attr = ['name' => '_photo', 'accept' => 'image/*'];
$rcmail->output->add_label('addphoto','replacephoto');
return self::upload_form($attrib, 'uploadform', 'upload-photo', $input_attr);
}
/**
* similar function as in /steps/settings/edit_identity.inc
* @todo: Use rcmail_action::get_form_tags()
*/
public static function get_form_tags($attrib, $action = null, $id = null, $hidden = null)
{
static $edit_form;
$rcmail = rcmail::get_instance();
$form_start = $form_end = '';
if (empty($edit_form)) {
$hiddenfields = new html_hiddenfield();
if ($rcmail->action == 'edit') {
$hiddenfields->add(['name' => '_source', 'value' => self::$SOURCE_ID]);
}
$hiddenfields->add(['name' => '_gid', 'value' => self::$CONTACTS->group_id]);
$hiddenfields->add(['name' => '_search', 'value' => rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC)]);
if ($cid = $rcmail->output->get_env('cid')) {
$hiddenfields->add(['name' => '_cid', 'value' => $cid]);
}
$form_attrib = [
'name' => 'form',
'method' => 'post',
'task' => $rcmail->task,
'action' => 'save',
'request' => 'save.' . intval($cid),
'noclose' => true,
];
$form_start = $rcmail->output->request_form($form_attrib + $attrib, $hiddenfields->show());
$form_end = empty($attrib['form']) ? '</form>' : '';
$edit_form = !empty($attrib['form']) ? $attrib['form'] : 'form';
$rcmail->output->add_gui_object('editform', $edit_form);
}
return [$form_start, $form_end];
}
/**
* Register container as active area to drop photos onto
*/
public static function photo_drop_area($attrib)
{
$rcmail = rcmail::get_instance();
if (!empty($attrib['id'])) {
$rcmail->output->add_gui_object('filedrop', $attrib['id']);
$rcmail->output->set_env('filedrop', [
'action' => 'upload-photo',
'fieldname' => '_photo',
'single' => 1,
'filter' => '^image/.+'
]);
}
}
}
@@ -0,0 +1,192 @@
<?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: |
| Export the selected address book as vCard file |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_export extends rcmail_action_contacts_index
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->request_security_check(rcube_utils::INPUT_GET);
$sort_col = $rcmail->config->get('addressbook_sort_col', 'name');
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['contact_search'][$_REQUEST['_search']])) {
$search = (array) $_SESSION['contact_search'][$_REQUEST['_search']];
$records = [];
// Get records from all sources
foreach ($search as $s => $set) {
$source = $rcmail->get_address_book($s);
// reset page
$source->set_page(1);
$source->set_pagesize(99999);
$source->set_search_set($set);
// get records
$result = $source->list_records();
while ($record = $result->next()) {
// because vcard_map is per-source we need to create vcard here
self::prepare_for_export($record, $source);
$record['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($record, $sort_col);
$records[$key] = $record;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
$result->records = array_values($records);
}
// selected contacts
else if (!empty($_REQUEST['_cid'])) {
$records = [];
// Selected contact IDs (with multi-source support)
$cids = self::get_cids();
foreach ($cids as $s => $ids) {
$source = $rcmail->get_address_book($s);
// reset page and page size (#6103)
$source->set_page(1);
$source->set_pagesize(count($ids));
$result = $source->search('ID', $ids, 1, true, true);
while ($record = $result->next()) {
// because vcard_map is per-source we need to create vcard here
self::prepare_for_export($record, $source);
$record['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($record, $sort_col);
$records[$key] = $record;
}
}
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
$result->records = array_values($records);
}
// selected directory/group
else {
$CONTACTS = self::contact_source(null, true);
// get contacts for this user
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(99999);
$result = $CONTACTS->list_records(null, 0, true);
}
// Give plugins a possibility to implement other output formats or modify the result
$plugin = $rcmail->plugins->exec_hook('addressbook_export', ['result' => $result]);
$result = $plugin['result'];
if ($plugin['abort']) {
$rcmail->output->sendExit();
}
// send download headers
$rcmail->output->header('Content-Type: text/vcard; charset=' . RCUBE_CHARSET);
$rcmail->output->header('Content-Disposition: attachment; filename="contacts.vcf"');
while ($result && ($row = $result->next())) {
if (!empty($CONTACTS)) {
self::prepare_for_export($row, $CONTACTS);
}
// fix folding and end-of-line chars
$row['vcard'] = preg_replace('/\r|\n\s+/', '', $row['vcard']);
$row['vcard'] = preg_replace('/\n/', rcube_vcard::$eol, $row['vcard']);
echo rcube_vcard::rfc2425_fold($row['vcard']) . rcube_vcard::$eol;
}
$rcmail->output->sendExit();
}
/**
* Copy contact record properties into a vcard object
*/
public static function prepare_for_export(&$record, $source = null)
{
$groups = $source && $source->groups && $source->export_groups ? $source->get_record_groups($record['ID']) : null;
$fieldmap = $source ? $source->vcard_map : null;
if (empty($record['vcard'])) {
$vcard = new rcube_vcard(null, RCUBE_CHARSET, false, $fieldmap);
$vcard->reset();
foreach ($record as $key => $values) {
list($field, $section) = rcube_utils::explode(':', $key);
// avoid unwanted casting of DateTime objects to an array
// (same as in rcube_contacts::convert_save_data())
if (is_object($values) && is_a($values, 'DateTime')) {
$values = [$values];
}
foreach ((array) $values as $value) {
if (is_array($value) || is_a($value, 'DateTime') || @strlen($value)) {
$vcard->set($field, $value, $section ? strtoupper($section) : '');
}
}
}
// append group names
if ($groups) {
$vcard->set('groups', join(',', $groups), null);
}
$record['vcard'] = $vcard->export();
}
// patch categories to already existing vcard block
else {
$vcard = new rcube_vcard($record['vcard'], RCUBE_CHARSET, false, $fieldmap);
// unset CATEGORIES entry, it might be not up-to-date (#1490277)
$vcard->set('groups', null);
$record['vcard'] = $vcard->export();
if (!empty($groups)) {
$vgroups = 'CATEGORIES:' . rcube_vcard::vcard_quote($groups, ',') . rcube_vcard::$eol;
$record['vcard'] = str_replace('END:VCARD', $vgroups . 'END:VCARD', $record['vcard']);
}
}
}
}
@@ -0,0 +1,86 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handle adding members to a contact group |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_group_addmembers extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$contacts = self::contact_source($source);
if ($contacts->readonly || !$contacts->groups) {
$rcmail->output->show_message('sourceisreadonly', 'warning');
$rcmail->output->send();
}
$gid = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_POST);
$ids = self::get_cids($source);
$result = false;
if ($gid && $ids) {
$plugin = $rcmail->plugins->exec_hook('group_addmembers', [
'group_id' => $gid,
'ids' => $ids,
'source' => $source,
]);
$contacts->set_group($gid);
$num2add = count($plugin['ids']);
if (empty($plugin['abort'])) {
if (
($maxnum = $rcmail->config->get('max_group_members'))
&& ($contacts->count()->count + $num2add > $maxnum)
) {
$rcmail->output->show_message('maxgroupmembersreached', 'warning', ['max' => $maxnum]);
$rcmail->output->send();
}
$result = $contacts->add_to_group($gid, $plugin['ids']);
}
else {
$result = $plugin['result'];
}
}
if ($result) {
$rcmail->output->show_message('contactaddedtogroup', 'confirmation');
}
else if (!empty($plugin['abort']) || $contacts->get_error()) {
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error');
}
else {
$message = !empty($plugin['message']) ? $plugin['message'] : 'nogroupassignmentschanged';
$rcmail->output->show_message($message);
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,67 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for contact groups creation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_group_create extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$contacts = self::contact_source($source);
if ($contacts->readonly || !$contacts->groups) {
$rcmail->output->show_message('sourceisreadonly', 'warning');
$rcmail->output->send();
}
if ($name = trim(rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST, true))) {
$plugin = $rcmail->plugins->exec_hook('group_create', [
'name' => $name,
'source' => $source,
]);
if (empty($plugin['abort'])) {
$created = $contacts->create_group($plugin['name']);
}
else {
$created = $plugin['result'];
}
}
if (!empty($created)) {
$rcmail->output->show_message('groupcreated', 'confirmation');
$rcmail->output->command('insert_contact_group', ['source' => $source] + $created);
}
else if (empty($created)) {
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error');
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,67 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for contact group delete action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_group_delete extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$contacts = self::contact_source($source);
if ($contacts->readonly || !$contacts->groups) {
$rcmail->output->show_message('sourceisreadonly', 'warning');
$rcmail->output->send();
}
if ($gid = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_POST)) {
$plugin = $rcmail->plugins->exec_hook('group_delete', [
'group_id' => $gid,
'source' => $source,
]);
if (empty($plugin['abort'])) {
$deleted = $contacts->delete_group($gid);
}
else {
$deleted = $plugin['result'];
}
}
if (!empty($deleted)) {
$rcmail->output->show_message('groupdeleted', 'confirmation');
$rcmail->output->command('remove_group_item', ['source' => $source, 'id' => $gid]);
}
else {
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error');
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,71 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Removing members from a contact group |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_group_delmembers extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$contacts = self::contact_source($source);
if ($contacts->readonly || !$contacts->groups) {
$rcmail->output->show_message('sourceisreadonly', 'warning');
$rcmail->output->send();
}
$gid = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_POST);
$ids = self::get_cids($source);
if ($gid && $ids) {
$plugin = $rcmail->plugins->exec_hook('group_delmembers', [
'group_id' => $gid,
'ids' => $ids,
'source' => $source,
]);
if (empty($plugin['abort'])) {
$result = $contacts->remove_from_group($gid, $plugin['ids']);
}
else {
$result = $plugin['result'];
}
}
if (!empty($result)) {
$rcmail->output->show_message('contactremovedfromgroup', 'confirmation');
$rcmail->output->command('remove_group_contacts', ['source' => $source, 'gid' => $gid]);
}
else {
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error');
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,77 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for contact groups rename action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_group_rename extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$contacts = self::contact_source($source);
if ($contacts->readonly || !$contacts->groups) {
$rcmail->output->show_message('sourceisreadonly', 'warning');
$rcmail->output->send();
}
if (
($gid = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_POST))
&& ($name = trim(rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST, true)))
) {
$newgid = null;
$plugin = $rcmail->plugins->exec_hook('group_rename', [
'group_id' => $gid,
'name' => $name,
'source' => $source,
]);
if (empty($plugin['abort'])) {
$newname = $contacts->rename_group($gid, $plugin['name'], $newgid);
}
else {
$newname = $plugin['result'];
}
}
if (!empty($newname)) {
$rcmail->output->show_message('grouprenamed', 'confirmation');
$rcmail->output->command('update_contact_group', [
'source' => $source,
'id' => $gid,
'name' => $newname,
'newid' => $newgid ?? null
]);
}
else {
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error');
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,486 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Import contacts from a vCard or CSV file |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_import extends rcmail_action_contacts_index
{
const UPLOAD_ERR_CSV_FIELDS = 101;
protected static $stats;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$importstep = 'import_form';
$has_map = isset($_POST['_map']) && is_array($_POST['_map']);
if ($has_map || (isset($_FILES['_file']) && is_array($_FILES['_file']))) {
$replace = (bool) rcube_utils::get_input_string('_replace', rcube_utils::INPUT_GPC);
$target = rcube_utils::get_input_string('_target', rcube_utils::INPUT_GPC);
$with_groups = (int) rcube_utils::get_input_string('_groups', rcube_utils::INPUT_GPC);
// reload params for CSV field mapping screen
if ($has_map && !empty($_SESSION['contactcsvimport']['params'])) {
$params = $_SESSION['contactcsvimport']['params'];
$replace = $params['replace'];
$target = $params['target'];
$with_groups = $params['with_groups'];
}
$vcards = [];
$csvs = [];
$map = [];
$upload_error = null;
$CONTACTS = $rcmail->get_address_book($target, true);
if (!$CONTACTS->groups) {
$with_groups = false;
}
if ($CONTACTS->readonly) {
$rcmail->output->show_message('addresswriterror', 'error');
}
else {
$filepaths = [];
if ($has_map) {
$filepaths = $_SESSION['contactcsvimport']['files'];
}
else {
foreach ((array) $_FILES['_file']['tmp_name'] as $i => $filepath) {
// Process uploaded file if there is no error
$err = $_FILES['_file']['error'][$i];
if ($err) {
$upload_error = $err;
}
else {
$filepaths[] = $filepath;
}
}
}
foreach ($filepaths as $filepath) {
$file_content = file_get_contents($filepath);
// let rcube_vcard do the hard work :-)
$vcard_o = new rcube_vcard();
$vcard_o->extend_fieldmap($CONTACTS->vcard_map);
$v_list = $vcard_o->import($file_content);
if (!empty($v_list)) {
$vcards = array_merge($vcards, $v_list);
continue;
}
// no vCards found, try CSV
$csv = new rcube_csv2vcard($_SESSION['language']);
if ($has_map) {
$skip_head = isset($_POST['_skip_header']);
$map = rcube_utils::get_input_value('_map', rcube_utils::INPUT_GPC);
$map = array_filter($map);
$csv->set_map($map);
$csv->import($file_content, false, $skip_head);
unlink($filepath);
}
else {
// save uploaded file for the real import in the next step
$temp_csv = rcube_utils::temp_filename('csvimpt');
if (move_uploaded_file($filepath, $temp_csv) && file_exists($temp_csv)) {
$fields = $csv->get_fields();
$last_map = $map;
$map = $csv->import($file_content, true);
// when multiple CSV files are uploaded check they all have the same structure
if ($last_map && $last_map !== $map) {
$csvs = [];
$upload_error = self::UPLOAD_ERR_CSV_FIELDS;
break;
}
$csvs[] = $temp_csv;
}
else {
$upload_error = UPLOAD_ERR_CANT_WRITE;
}
continue;
}
$v_list = $csv->export();
if (!empty($v_list)) {
$vcards = array_merge($vcards, $v_list);
}
}
}
if (count($csvs) > 0) {
// csv import, show field mapping options
$importstep = 'import_map';
$_SESSION['contactcsvimport']['files'] = $csvs;
$_SESSION['contactcsvimport']['params'] = [
'replace' => $replace,
'target' => $target,
'with_groups' => $with_groups,
'fields' => !empty($fields) ? $fields : [],
];
// Stored separately due to nested array limitations in session
$_SESSION['contactcsvimport']['map'] = $map;
// Re-enable the import button
$rcmail->output->command('parent.import_state_set', 'error');
}
elseif (count($vcards) > 0) {
// import vcards
self::$stats = new stdClass;
self::$stats->names = [];
self::$stats->skipped_names = [];
self::$stats->count = count($vcards);
self::$stats->inserted = 0;
self::$stats->skipped = 0;
self::$stats->invalid = 0;
self::$stats->errors = 0;
if ($replace) {
$CONTACTS->delete_all($CONTACTS->groups && $with_groups < 2);
}
if ($with_groups) {
$import_groups = $CONTACTS->list_groups();
}
foreach ($vcards as $vcard) {
$a_record = $vcard->get_assoc();
// Generate contact's display name (must be before validation), the same we do in save.inc
if (empty($a_record['name'])) {
$a_record['name'] = rcube_addressbook::compose_display_name($a_record, true);
// Reset it if equals to email address (from compose_display_name())
if ($a_record['name'] == ($a_record['email'][0] ?? null)) {
$a_record['name'] = '';
}
}
// skip invalid (incomplete) entries
if (!$CONTACTS->validate($a_record, true)) {
self::$stats->invalid++;
continue;
}
// We're using UTF8 internally
$email = null;
if (isset($vcard->email[0])) {
$email = $vcard->email[0];
$email = rcube_utils::idn_to_utf8($email);
}
if (!$replace) {
$existing = null;
// compare e-mail address
if ($email) {
$existing = $CONTACTS->search('email', $email, 1, false);
}
// compare display name if email not found
if ((!$existing || !$existing->count) && $vcard->displayname) {
$existing = $CONTACTS->search('name', $vcard->displayname, 1, false);
}
if ($existing && $existing->count) {
self::$stats->skipped++;
self::$stats->skipped_names[] = $vcard->displayname ?: $email;
continue;
}
}
$a_record['vcard'] = $vcard->export();
$plugin = $rcmail->plugins->exec_hook('contact_create', ['record' => $a_record, 'source' => null]);
$a_record = $plugin['record'];
// insert record and send response
if (empty($plugin['abort'])) {
$success = $CONTACTS->insert($a_record);
}
else {
$success = $plugin['result'];
}
if ($success) {
// assign groups for this contact (if enabled)
if ($with_groups && !empty($a_record['groups'])) {
foreach (explode(',', $a_record['groups'][0]) as $group_name) {
if ($group_id = self::import_group_id($group_name, $CONTACTS, $with_groups == 1, $import_groups)) {
$CONTACTS->add_to_group($group_id, $success);
}
}
}
self::$stats->inserted++;
self::$stats->names[] = $a_record['name'] ?: $email;
}
else {
self::$stats->errors++;
}
}
$importstep = 'import_confirm';
$_SESSION['contactcsvimport'] = null;
$rcmail->output->command('parent.import_state_set', self::$stats->inserted ? 'reload' : 'ok');
}
else {
if ($upload_error == self::UPLOAD_ERR_CSV_FIELDS) {
$rcmail->output->show_message('csvfilemismatch', 'error');
}
else {
self::upload_error($upload_error);
}
$rcmail->output->command('parent.import_state_set', 'error');
}
}
$rcmail->output->set_pagetitle($rcmail->gettext('importcontacts'));
$rcmail->output->add_handlers([
'importstep' => [$this, $importstep],
]);
// render page
if ($rcmail->output->template_exists('contactimport')) {
$rcmail->output->send('contactimport');
}
else {
$rcmail->output->send('importcontacts'); // deprecated
}
}
/**
* Handler function to display the import/upload form
*/
public static function import_form($attrib)
{
$rcmail = rcmail::get_instance();
$target = rcube_utils::get_input_string('_target', rcube_utils::INPUT_GPC);
$attrib += ['id' => 'rcmImportForm'];
$writable_books = $rcmail->get_address_sources(true, true);
$max_filesize = self::upload_init();
$form = '';
$hint = $rcmail->gettext(['id' => 'importfile', 'name' => 'maxuploadsize', 'vars' => ['size' => $max_filesize]]);
$table = new html_table(['cols' => 2]);
$upload = new html_inputfield([
'type' => 'file',
'name' => '_file[]',
'id' => 'rcmimportfile',
'size' => 40,
'multiple' => 'multiple',
'class' => 'form-control-file',
]);
$table->add('title', html::label('rcmimportfile', $rcmail->gettext('importfromfile')));
$table->add(null, $upload->show() . html::div('hint', $hint));
// addressbook selector
if (count($writable_books) > 1) {
$select = new html_select([
'name' => '_target',
'id' => 'rcmimporttarget',
'is_escaped' => true,
'class' => 'custom-select'
]);
foreach ($writable_books as $book) {
$select->add($book['name'], $book['id']);
}
$table->add('title', html::label('rcmimporttarget', $rcmail->gettext('importtarget')));
$table->add(null, $select->show($target));
}
else {
$abook = new html_hiddenfield(['name' => '_target', 'value' => key($writable_books)]);
$form .= $abook->show();
}
$form .= html::tag('input', ['type' => 'hidden', 'name' => '_unlock', 'value' => '']);
// selector for group import options
if (count($writable_books) >= 1 || $writable_books[0]->groups) {
$select = new html_select([
'name' => '_groups',
'id' => 'rcmimportgroups',
'is_escaped' => true,
'class' => 'custom-select'
]);
$select->add($rcmail->gettext('none'), '0');
$select->add($rcmail->gettext('importgroupsall'), '1');
$select->add($rcmail->gettext('importgroupsexisting'), '2');
$table->add('title', html::label('rcmimportgroups', $rcmail->gettext('importgroups')));
$table->add(null, $select->show(rcube_utils::get_input_value('_groups', rcube_utils::INPUT_GPC)));
}
// checkbox to replace the entire address book
$check_replace = new html_checkbox(['name' => '_replace', 'value' => 1, 'id' => 'rcmimportreplace']);
$table->add('title', html::label('rcmimportreplace', $rcmail->gettext('importreplace')));
$table->add(null, $check_replace->show(rcube_utils::get_input_string('_replace', rcube_utils::INPUT_GPC)));
$form .= $table->show(['id' => null] + $attrib);
// remove any info left over info from previous import attempts
$_SESSION['contactcsvimport'] = null;
$rcmail->output->set_env('writable_source', !empty($writable_books));
$rcmail->output->add_label('selectimportfile','importwait');
$rcmail->output->add_gui_object('importform', $attrib['id']);
$attrib = [
'action' => $rcmail->url('import'),
'method' => 'post',
'enctype' => 'multipart/form-data'
] + $attrib;
return html::p(null, rcube::Q($rcmail->gettext('importdesc'), 'show'))
. $rcmail->output->form_tag($attrib, $form);
}
/**
* Render the field mapping page for the CSV import process
*/
public static function import_map($attrib)
{
$rcmail = rcmail::get_instance();
$params = $_SESSION['contactcsvimport']['params'];
// hide groups field from list when group import disabled
if (empty($params['with_groups'])) {
unset($params['fields']['groups']);
}
$fieldlist = new html_select(['name' => '_map[]']);
$fieldlist->add($rcmail->gettext('fieldnotmapped'), '');
foreach ($params['fields'] as $id => $name) {
$fieldlist->add($name, $id);
}
$field_table = new html_table(['cols' => 2] + $attrib);
if ($classes = $attrib['table-header-class']) {
$field_table->set_header_attribs($classes);
}
$field_table->add_header($attrib['table-col-source-class'] ?: null, $rcmail->gettext('source'));
$field_table->add_header($attrib['table-col-destination-class'] ?: null, $rcmail->gettext('destination'));
$map = $_SESSION['contactcsvimport']['map'];
foreach ($map['source'] as $i => $name) {
$field_table->add('title', html::label('rcmimportmap' . $i, rcube::Q($name)));
$field_table->add(null, $fieldlist->show(array_key_exists($i, $map['destination']) ? $map['destination'][$i] : '', ['id' => 'rcmimportmap' . $i]));
}
$form = '';
$form .= html::tag('input', ['type' => 'hidden', 'name' => '_unlock', 'value' => '']);
// show option to import data from first line of the file
$check_header = new html_checkbox(['name' => '_skip_header', 'value' => 1, 'id' => 'rcmskipheader']);
$form .= html::p(null, html::label('rcmskipheader', $check_header->show(1) . $rcmail->gettext('skipheader')));
$form .= $field_table->show();
$attrib = ['action' => $rcmail->url('import'), 'method' => 'post'] + $attrib + ['id' => 'rcmImportFormMap'];
$rcmail->output->add_gui_object('importformmap', $attrib['id']);
return html::p(null, rcube::Q($rcmail->gettext('importmapdesc'), 'show'))
. $rcmail->output->form_tag($attrib, $form);
}
/**
* Render the confirmation page for the import process
*/
public static function import_confirm($attrib)
{
$rcmail = rcmail::get_instance();
$vars = get_object_vars(self::$stats);
$vars['names'] = $vars['skipped_names'] = '';
$content = html::p(null, $rcmail->gettext([
'name' => 'importconfirm',
'nr' => self::$stats->inserted,
'vars' => $vars,
]) . (self::$stats->names ? ':' : '.')
);
if (self::$stats->names) {
$content .= html::p('em', join(', ', array_map(['rcube', 'Q'], self::$stats->names)));
}
if (self::$stats->skipped) {
$content .= html::p(null, $rcmail->gettext([
'name' => 'importconfirmskipped',
'nr' => self::$stats->skipped,
'vars' => $vars,
]) . ':')
. html::p('em', join(', ', array_map(['rcube', 'Q'], self::$stats->skipped_names)));
}
return html::div($attrib, $content);
}
/**
* Returns the matching group id. If group doesn't exist, it'll be created if allowed.
*/
public static function import_group_id($group_name, $contacts, $create, &$import_groups)
{
$group_id = 0;
foreach ($import_groups as $group) {
if (strtolower($group['name']) === strtolower($group_name)) {
$group_id = $group['ID'];
break;
}
}
// create a new group
if (!$group_id && $create) {
$new_group = $contacts->create_group($group_name);
if (empty($new_group['ID'])) {
$new_group['ID'] = $new_group['id'];
}
$import_groups[] = $new_group;
$group_id = $new_group['ID'];
}
return $group_id;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send contacts list to client (as remote response) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_list extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
if (!empty($_GET['_page'])) {
$page = intval($_GET['_page']);
}
else {
$page = !empty($_SESSION['page']) ? $_SESSION['page'] : 1;
}
$_SESSION['page'] = $page;
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
$group_data = null;
// Use search result
if (($records = self::search_update(true)) !== false) {
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$first = ($page-1) * $page_size;
$result = new rcube_result_set($count, $first);
// we need only records for current page
if ($page_size < $count) {
$records = array_slice($records, $first, $page_size);
}
$result->records = array_values($records);
}
// List selected directory
else {
$afields = $rcmail->config->get('contactlist_fields');
$contacts = self::contact_source(null, true);
// get contacts for this user
$result = $contacts->list_records($afields);
if (!$result->count && $result->searchonly) {
$rcmail->output->show_message('contactsearchonly', 'notice');
// Don't invoke advanced search dialog automatically from here (#6679)
}
if (!empty($contacts->group_id)) {
$group_data = ['ID' => $contacts->group_id]
+ array_intersect_key((array) $contacts->get_group($contacts->group_id), ['name' => 1,'email' => 1]);
}
}
$rcmail->output->command('set_group_prop', $group_data);
// update message count display
$rcmail->output->set_env('pagecount', ceil($result->count / $page_size));
$rcmail->output->command('set_rowcount', self::get_rowcount_text($result));
// create javascript list
self::js_contacts_list($result);
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,91 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Compose a recipient list with all selected contacts |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_mailto extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$cids = self::get_cids();
$mailto = [];
$sources = [];
foreach ($cids as $source => $cid) {
$contacts = $rcmail->get_address_book($source);
if ($contacts->ready) {
$contacts->set_page(1);
$contacts->set_pagesize(count($cid) + 2); // +2 to skip counting query
$sources[] = $contacts->search($contacts->primary_key, $cid, 0, true, true, 'email');
}
}
if (!empty($_REQUEST['_gid']) && isset($_REQUEST['_source'])) {
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GP);
$group_id = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_GP);
$contacts = $rcmail->get_address_book($source);
$group_data = $contacts->get_group($group_id);
// group has an email address assigned: use that
if (!empty($group_data['email'])) {
$mailto[] = format_email_recipient($group_data['email'][0], $group_data['name']);
}
else if ($contacts->ready) {
$maxnum = (int) $rcmail->config->get('max_group_members');
$contacts->set_group($group_id);
$contacts->set_page(1);
$contacts->set_pagesize($maxnum ?: 999);
$sources[] = $contacts->list_records();
}
}
foreach ($sources as $source) {
while (is_object($source) && ($rec = $source->iterate())) {
$emails = rcube_addressbook::get_col_values('email', $rec, true);
if (!empty($emails)) {
$mailto[] = format_email_recipient($emails[0], $rec['name']);
}
}
}
if (!empty($mailto)) {
$mailto_str = join(', ', $mailto);
$mailto_id = substr(md5($mailto_str), 0, 16);
$_SESSION['mailto'][$mailto_id] = urlencode($mailto_str);
$rcmail->output->command('open_compose_step', ['_mailto' => $mailto_id]);
}
else {
$rcmail->output->show_message('nocontactsfound', 'warning');
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,243 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Move a contact record from one directory to another |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_move extends rcmail_action_contacts_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$cids = self::get_cids();
$target = rcube_utils::get_input_string('_to', rcube_utils::INPUT_POST);
$target_group = rcube_utils::get_input_string('_togid', rcube_utils::INPUT_POST);
$rcmail = rcmail::get_instance();
$all = 0;
$deleted = 0;
$success = 0;
$errormsg = 'moveerror';
$maxnum = $rcmail->config->get('max_group_members', 0);
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
$page = !empty($_SESSION['page']) ? $_SESSION['page'] : 1;
foreach ($cids as $source => $source_cids) {
// Something wrong, target not specified
if (!strlen($target)) {
break;
}
// It might happen when moving records from search result
// Do nothing, go to the next source
if ((string) $target === (string) $source) {
continue;
}
$CONTACTS = $rcmail->get_address_book($source);
$TARGET = $rcmail->get_address_book($target);
if (!$TARGET || !$TARGET->ready || $TARGET->readonly) {
break;
}
if (!$CONTACTS || !$CONTACTS->ready || ($CONTACTS->readonly && empty($CONTACTS->deletable))) {
continue;
}
$ids = [];
foreach ($source_cids as $idx => $cid) {
$record = $CONTACTS->get_record($cid, true);
// avoid moving groups
if (isset($record['_type']) && $record['_type'] == 'group') {
unset($source_cids[$idx]);
continue;
}
// Check if contact exists, if so, we'll need it's ID
// Note: Some addressbooks allows empty email address field
// @TODO: should we check all email addresses?
$email = $CONTACTS->get_col_values('email', $record, true);
if (!empty($email)) {
$result = $TARGET->search('email', $email[0], 1, true, true);
}
else if (!empty($record['name'])) {
$result = $TARGET->search('name', $record['name'], 1, true, true);
}
else {
$result = new rcube_result_set();
}
// insert contact record
if (!$result->count) {
$plugin = $rcmail->plugins->exec_hook('contact_create', [
'record' => $record,
'source' => $target,
'group' => $target_group
]);
if (empty($plugin['abort'])) {
if ($insert_id = $TARGET->insert($plugin['record'], false)) {
$ids[] = $insert_id;
$success++;
}
}
else if (!empty($plugin['result'])) {
$ids = array_merge($ids, $plugin['result']);
$success++;
}
}
else {
$record = $result->first();
$ids[] = $record['ID'];
$errormsg = empty($email) ? 'contactnameexists' : 'contactexists';
}
}
// remove source contacts
if ($success && !empty($source_cids)) {
$all += count($source_cids);
$plugin = $rcmail->plugins->exec_hook('contact_delete', [
'id' => $source_cids,
'source' => $source
]);
$del_status = !$plugin['abort'] ? $CONTACTS->delete($source_cids) : $plugin['result'];
if ($del_status) {
$deleted += $del_status;
}
}
// assign to group
if ($target_group && $TARGET->groups && !empty($ids)) {
$plugin = $rcmail->plugins->exec_hook('group_addmembers', [
'group_id' => $target_group,
'ids' => $ids,
'source' => $target
]);
if (empty($plugin['abort'])) {
$TARGET->reset();
$TARGET->set_group($target_group);
if ($maxnum && ($TARGET->count()->count + count($plugin['ids']) > $maxnum)) {
$rcmail->output->show_message('maxgroupmembersreached', 'warning', ['max' => $maxnum]);
$rcmail->output->send();
}
if (($cnt = $TARGET->add_to_group($target_group, $plugin['ids'])) && $cnt > $success) {
$success = $cnt;
}
}
else if ($plugin['result']) {
$success = $plugin['result'];
}
$errormsg = !empty($plugin['message']) ? $plugin['message'] : 'moveerror';
}
}
if (!$deleted || $deleted != $all) {
$rcmail->output->command('list_contacts');
}
else {
// update saved search after data changed
if (($records = self::search_update(true)) !== false) {
// create resultset object
$count = count($records);
$first = ($page-1) * $page_size;
$result = new rcube_result_set($count, $first);
$pages = ceil((count($records) + $deleted) / $page_size);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($page_size * ($page - 1))) {
$rcmail->output->command('list_page', 'prev');
$rowcount = $rcmail->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
// sort the records
ksort($records, SORT_LOCALE_STRING);
$first += $page_size;
// create resultset object
$res = new rcube_result_set($count, $first - $deleted);
if ($page_size < $count) {
$records = array_slice($records, $first - $deleted, $deleted);
}
$res->records = array_values($records);
$records = $res;
}
else {
unset($records);
}
}
else if (isset($CONTACTS)) {
// count contacts for this user
$result = $CONTACTS->count();
$pages = ceil(($result->count + $deleted) / $page_size);
// last page and it's empty, display previous one
if ($result->count && $result->count <= ($page_size * ($page - 1))) {
$rcmail->output->command('list_page', 'prev');
$rowcount = $rcmail->gettext('loading');
}
// get records from the next page to add to the list
else if ($pages > 1 && $page < $pages) {
$CONTACTS->set_page($page);
$records = $CONTACTS->list_records(null, -$deleted);
}
}
if (!isset($rowcount)) {
$rowcount = isset($result) ? self::get_rowcount_text($result) : 0;
}
// update message count display
$rcmail->output->set_env('pagecount', isset($result) ? ceil($result->count / $page_size) : 0);
$rcmail->output->command('set_rowcount', $rowcount);
// add new rows from next page (if any)
if (!empty($records)) {
self::js_contacts_list($records);
}
}
if (!$success) {
$rcmail->output->show_message($errormsg, 'error');
}
else {
$rcmail->output->show_message('movesuccess', 'confirmation', ['nr' => $success]);
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,118 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact photo |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_photo extends rcmail_action_contacts_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// Get contact ID and source ID from request
$cids = self::get_cids();
$source = key($cids);
$cid = $cids ? array_first($cids[$source]) : null;
$file_id = rcube_utils::get_input_string('_photo', rcube_utils::INPUT_GPC);
// read the referenced file
if ($file_id && !empty($_SESSION['contacts']['files'][$file_id])) {
$tempfile = $_SESSION['contacts']['files'][$file_id];
$tempfile = $rcmail->plugins->exec_hook('attachment_display', $tempfile);
if (!empty($tempfile['status'])) {
if (!empty($tempfile['data'])) {
$data = $tempfile['data'];
}
else if ($tempfile['path']) {
$data = file_get_contents($tempfile['path']);
}
}
}
else {
// by email, search for contact first
if ($email = rcube_utils::get_input_string('_email', rcube_utils::INPUT_GPC)) {
foreach ($rcmail->get_address_sources() as $s) {
$abook = $rcmail->get_address_book($s['id']);
$result = $abook->search(['email'], $email, 1, true, true, 'photo');
while ($result && ($record = $result->iterate())) {
if (!empty($record['photo'])) {
break 2;
}
}
}
}
// by contact id
if (empty($record) && $cid) {
// Initialize addressbook source
$CONTACTS = self::contact_source($source, true);
// read contact record
$record = $CONTACTS->get_record($cid, true);
}
if (!empty($record['photo'])) {
$data = is_array($record['photo']) ? $record['photo'][0] : $record['photo'];
if (!preg_match('![^a-z0-9/=+-]!i', $data)) {
$data = base64_decode($data, true);
}
}
}
// let plugins do fancy things with contact photos
$plugin = $rcmail->plugins->exec_hook('contact_photo', [
'record' => $record ?? null,
'email' => $email ?? null,
'data' => $data ?? null,
]);
// redirect to url provided by a plugin
if (!empty($plugin['url'])) {
$rcmail->output->redirect($plugin['url']);
}
$data = $plugin['data'];
// detect if photo data is a URL
if ($data && strlen($data) < 1024 && filter_var($data, FILTER_VALIDATE_URL)) {
$rcmail->output->redirect($data);
}
// cache for one day if requested by email
if (!$cid && !empty($email)) {
$rcmail->output->future_expire_header(86400);
}
if ($data) {
$rcmail->output->sendExit($data, ['Content-Type: ' . rcube_mime::image_content_type($data)]);
}
if (!empty($_GET['_error'])) {
$rcmail->output->sendExit('', ['HTTP/1.0 204 Photo not found']);
}
$rcmail->output->sendExit(base64_decode(rcmail_output::BLANK_GIF), ['Content-Type: image/gif']);
}
}
@@ -0,0 +1,147 @@
<?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: |
| Print contact details |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_print extends rcmail_action_contacts_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// Get contact ID and source ID from request
$cids = self::get_cids();
$source = key($cids);
$cid = $cids ? array_first($cids[$source]) : null;
// Initialize addressbook source
self::$CONTACTS = self::contact_source($source, true);
self::$SOURCE_ID = $source;
// read contact record
if ($cid && self::$CONTACTS) {
self::$contact = self::$CONTACTS->get_record($cid, true);
}
$rcmail->output->add_handlers([
'contacthead' => [$this, 'contact_head'],
'contactdetails' => [$this, 'contact_details'],
'contactphoto' => [$this, 'contact_photo'],
]);
$rcmail->output->send('contactprint');
}
public static function contact_head($attrib)
{
$rcmail = rcmail::get_instance();
// check if we have a valid result
if (!self::$contact) {
$rcmail->output->show_message('contactnotfound', 'error');
return false;
}
$form = [
'head' => [ // section 'head' is magic!
'name' => $rcmail->gettext('contactnameandorg'),
'content' => [
'prefix' => [],
'name' => [],
'firstname' => [],
'middlename' => [],
'surname' => [],
'suffix' => [],
],
],
];
unset($attrib['name']);
return self::contact_form($form, self::$contact, $attrib);
}
public static function contact_details($attrib)
{
// check if we have a valid result
if (!self::$contact) {
return false;
}
$rcmail = rcmail::get_instance();
$form = [
'contact' => [
'name' => $rcmail->gettext('properties'),
'content' => [
'organization' => [],
'department' => [],
'jobtitle' => [],
'email' => [],
'phone' => [],
'address' => [],
'website' => [],
'im' => [],
'groups' => [],
],
],
'personal' => [
'name' => $rcmail->gettext('personalinfo'),
'content' => [
'nickname' => [],
'gender' => [],
'maidenname' => [],
'birthday' => [],
'anniversary' => [],
'manager' => [],
'assistant' => [],
'spouse' => [],
],
],
];
if (isset(rcmail_action_contacts_index::$CONTACT_COLTYPES['notes'])) {
$form['notes'] = [
'name' => $rcmail->gettext('notes'),
'content' => [
'notes' => ['type' => 'textarea', 'label' => false],
],
];
}
if (self::$CONTACTS->groups) {
$groups = self::$CONTACTS->get_record_groups(self::$contact['ID']);
if (!empty($groups)) {
$form['contact']['content']['groups'] = [
'value' => rcube::Q(implode(', ', $groups)),
'label' => $rcmail->gettext('groups')
];
}
}
return self::contact_form($form, self::$contact, $attrib);
}
}
@@ -0,0 +1,128 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact data as QR code |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_qrcode extends rcmail_action_contacts_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
// Get contact ID and source ID from request
$cids = self::get_cids();
$source = key($cids);
$cid = $cids ? array_first($cids[$source]) : null;
$rcmail = rcmail::get_instance();
// read contact record
$abook = self::contact_source($source, true);
$contact = $abook->get_record($cid, true);
// generate QR code image
if ($data = self::contact_qrcode($contact)) {
$headers = [
'Content-Type: ' . self::check_support(),
'Content-Length: ' . strlen($data)
];
$rcmail->output->sendExit($data, $headers);
}
$rcmail->output->sendExit('', ['HTTP/1.0 404 Contact not found']);
}
/**
* Generate a QR-code image for a contact
*
* @param array $contact Contact record
*
* @return string|null Image content, Null on error or missing PHP extensions
*/
public static function contact_qrcode($contact)
{
if (empty($contact)) {
return null;
}
$type = self::check_support();
if (empty($type)) {
return null;
}
$vcard = new rcube_vcard();
// QR code input is limited, use only common fields
$fields = ['name', 'firstname', 'surname', 'middlename', 'nickname',
'organization', 'phone', 'email', 'jobtitle', 'prefix', 'suffix'];
foreach ($contact as $field => $value) {
if (strpos($field, ':') !== false) {
list($field, $section) = explode(':', $field, 2);
}
else {
$section = null;
}
if (in_array($field, $fields)) {
foreach ((array) $value as $v) {
$vcard->set($field, $v, $section);
}
}
}
$data = $vcard->export();
if (empty($data)) {
return null;
}
$renderer_style = new BaconQrCode\Renderer\RendererStyle\RendererStyle(300, 1);
$renderer_image = $type == 'image/png'
? new BaconQrCode\Renderer\Image\ImagickImageBackEnd()
: new BaconQrCode\Renderer\Image\SvgImageBackEnd();
$renderer = new BaconQrCode\Renderer\ImageRenderer($renderer_style, $renderer_image);
$writer = new BaconQrCode\Writer($renderer);
return $writer->writeString($data, RCUBE_CHARSET);
}
/**
* Check required extensions and classes for QR code generation
*
* @return string|null Content-type of the image result
*/
public static function check_support()
{
if (extension_loaded('iconv') && class_exists('BaconQrCode\Renderer\ImageRenderer')) {
if (extension_loaded('xmlwriter')) {
return 'image/svg+xml';
}
if (extension_loaded('imagick')) {
return 'image/png';
}
}
}
}
@@ -0,0 +1,323 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save a contact entry or to add a new one |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_save extends rcmail_action_contacts_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$contacts = self::contact_source(null, true);
$cid = rcube_utils::get_input_string('_cid', rcube_utils::INPUT_POST);
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$return_action = empty($cid) ? 'add' : 'edit';
// Source changed, display the form again
if (!empty($_GET['_reload'])) {
$rcmail->overwrite_action($return_action);
return;
}
// cannot edit record
if (!$contacts || $contacts->readonly) {
$rcmail->output->show_message('contactreadonly', 'error');
$rcmail->overwrite_action($return_action);
return;
}
// read POST values into hash array
$a_record = self::process_input();
// do input checks (delegated to $contacts instance)
if (!$contacts->validate($a_record)) {
$err = (array) $contacts->get_error();
$rcmail->output->show_message(!empty($err['message']) ? rcube::Q($err['message']) : 'formincomplete', 'warning');
$rcmail->overwrite_action($return_action, ['contact' => $a_record]);
return;
}
// get raw photo data if changed
if (isset($a_record['photo'])) {
if ($a_record['photo'] == '-del-') {
$a_record['photo'] = '';
}
else if (!empty($_SESSION['contacts']['files'][$a_record['photo']])) {
$tempfile = $_SESSION['contacts']['files'][$a_record['photo']];
$tempfile = $rcmail->plugins->exec_hook('attachment_get', $tempfile);
if ($tempfile['status']) {
$a_record['photo'] = $tempfile['data'] ?: @file_get_contents($tempfile['path']);
}
}
else {
unset($a_record['photo']);
}
// cleanup session data
$rcmail->plugins->exec_hook('attachments_cleanup', ['group' => 'contact']);
$rcmail->session->remove('contacts');
}
// update an existing contact
if (!empty($cid)) {
$plugin = $rcmail->plugins->exec_hook('contact_update', [
'id' => $cid,
'record' => $a_record,
'source' => $source
]);
$a_record = $plugin['record'];
if (!$plugin['abort']) {
$result = $contacts->update($cid, $a_record);
}
else {
$result = $plugin['result'];
}
if ($result) {
// show confirmation
$rcmail->output->show_message('successfullysaved', 'confirmation', null, false);
// in search mode, just reload the list (#1490015)
if (!empty($_REQUEST['_search'])) {
$rcmail->output->command('parent.command', 'list');
$rcmail->output->send('iframe');
}
$newcid = null;
// LDAP DN change
if (is_string($result) && strlen($result) > 1) {
$newcid = $result;
// change cid in POST for 'show' action
$_POST['_cid'] = $newcid;
}
// refresh contact data for list update and 'show' action
$contact = $contacts->get_record($newcid ?: $cid, true);
// Plugins can decide to remove the contact on edit, e.g. automatic_addressbook
// Best we can do is to refresh the list (#5522)
if (empty($contact)) {
$rcmail->output->command('parent.command', 'list');
$rcmail->output->send('iframe');
}
// Update contacts list
$a_js_cols = [];
$record = $contact;
$record['email'] = array_first($contacts->get_col_values('email', $record, true));
$record['name'] = rcube_addressbook::compose_list_name($record);
foreach (['name'] as $col) {
$a_js_cols[] = rcube::Q((string) $record[$col]);
}
// performance: unset some big data items we don't need here
$record = array_intersect_key($record, ['ID' => 1,'email' => 1,'name' => 1]);
$record['_type'] = 'person';
// update the changed col in list
$rcmail->output->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source, $record);
$rcmail->overwrite_action('show', ['contact' => $contact]);
}
else {
// show error message
$error = self::error_str($contacts, $plugin);
$rcmail->output->show_message($error, 'error', null, false);
$rcmail->overwrite_action('show');
}
}
// insert a new contact
else {
// Name of the addressbook already selected on the list
$orig_source = rcube_utils::get_input_string('_orig_source', rcube_utils::INPUT_GPC);
if (!strlen($source)) {
$source = $orig_source;
}
// show notice if existing contacts with same e-mail are found
foreach ($contacts->get_col_values('email', $a_record, true) as $email) {
if ($email && ($res = $contacts->search('email', $email, 1, false, true)) && $res->count) {
$rcmail->output->show_message('contactexists', 'notice', null, false);
break;
}
}
$plugin = $rcmail->plugins->exec_hook('contact_create', [
'record' => $a_record,
'source' => $source
]);
$a_record = $plugin['record'];
// insert record and send response
if (!$plugin['abort']) {
$insert_id = $contacts->insert($a_record);
}
else {
$insert_id = $plugin['result'];
}
if ($insert_id) {
$contacts->reset();
// add new contact to the specified group
if ($contacts->groups && $contacts->group_id) {
$plugin = $rcmail->plugins->exec_hook('group_addmembers', [
'group_id' => $contacts->group_id,
'ids' => $insert_id,
'source' => $source
]);
if (!$plugin['abort']) {
if (($maxnum = $rcmail->config->get('max_group_members', 0)) && ($contacts->count()->count + 1 > $maxnum)) {
// @FIXME: should we remove the contact?
$msgtext = $rcmail->gettext(['name' => 'maxgroupmembersreached', 'vars' => ['max' => $maxnum]]);
$rcmail->output->command('parent.display_message', $msgtext, 'warning');
}
else {
$contacts->add_to_group($plugin['group_id'], $plugin['ids']);
}
}
}
// show confirmation
$rcmail->output->show_message('successfullysaved', 'confirmation', null, false);
$rcmail->output->command('parent.set_rowcount', $rcmail->gettext('loading'));
$rcmail->output->command('parent.list_contacts');
$rcmail->output->send('iframe');
}
else {
// show error message
$error = self::error_str($contacts, $plugin);
$rcmail->output->show_message($error, 'error', null, false);
$rcmail->overwrite_action('add');
}
}
}
public static function process_input()
{
$record = [];
foreach (rcmail_action_contacts_index::$CONTACT_COLTYPES as $col => $colprop) {
if (!empty($colprop['composite'])) {
continue;
}
$fname = '_' . $col;
// gather form data of composite fields
if (!empty($colprop['childs'])) {
$values = [];
foreach ($colprop['childs'] as $childcol => $cp) {
$vals = rcube_utils::get_input_value('_' . $childcol, rcube_utils::INPUT_POST, true);
foreach ((array) $vals as $i => $val) {
$values[$i][$childcol] = $val;
}
}
if (isset($_REQUEST['_subtype_' . $col])) {
$subtypes = (array) rcube_utils::get_input_value('_subtype_' . $col, rcube_utils::INPUT_POST);
}
else {
$subtypes = [''];
}
foreach ($subtypes as $i => $subtype) {
$suffix = $subtype ? ":$subtype" : '';
if ($values[$i]) {
$record[$col . $suffix][] = $values[$i];
}
}
}
// assign values and subtypes
else if (isset($_POST[$fname]) && is_array($_POST[$fname])) {
$values = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
$subtypes = rcube_utils::get_input_value('_subtype_' . $col, rcube_utils::INPUT_POST);
foreach ($values as $i => $val) {
if ($col == 'email') {
// extract email from full address specification, e.g. "Name" <addr@domain.tld>
$addr = rcube_mime::decode_address_list($val, 1, false);
if (!empty($addr) && ($addr = array_pop($addr)) && $addr['mailto']) {
$val = $addr['mailto'];
}
}
$subtype = $subtypes[$i] ? ':'.$subtypes[$i] : '';
$record[$col.$subtype][] = $val;
}
}
else if (isset($_POST[$fname])) {
$record[$col] = rcube_utils::get_input_value($fname, rcube_utils::INPUT_POST, true);
// normalize the submitted date strings
if ($colprop['type'] == 'date') {
if ($record[$col] && ($dt = rcube_utils::anytodatetime($record[$col]))) {
$record[$col] = $dt->format('Y-m-d');
}
else {
unset($record[$col]);
}
}
}
}
// Generate contact's display name (must be before validation)
if (empty($record['name'])) {
$record['name'] = rcube_addressbook::compose_display_name($record, true);
// Reset it if equals to email address (from compose_display_name())
$email = rcube_addressbook::get_col_values('email', $record, true);
if (isset($email[0]) && $record['name'] == $email[0]) {
$record['name'] = '';
}
}
return $record;
}
public static function error_str($contacts, $plugin)
{
if (!empty($plugin['message'])) {
return $plugin['message'];
}
$err = $contacts->get_error();
if (!empty($err['message'])) {
return $err['message'];
}
return 'errorsaving';
}
}
@@ -0,0 +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_string('_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_string($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_string('_q', rcube_utils::INPUT_GET, true));
$fields = rcube_utils::get_input_string('_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 = !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 = $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>';
}
}
@@ -0,0 +1,73 @@
<?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: |
| Create saved search |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_search_create extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$id = rcube_utils::get_input_value('_search', rcube_utils::INPUT_POST);
$name = rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST, true);
if (
!empty($_SESSION['contact_search_params'])
&& ($params = $_SESSION['contact_search_params'])
&& $params['id'] == $id
) {
$data = [
'type' => rcube_user::SEARCH_ADDRESSBOOK,
'name' => $name,
'data' => [
'fields' => $params['data'][0],
'search' => $params['data'][1],
],
];
$plugin = $rcmail->plugins->exec_hook('saved_search_create', ['data' => $data]);
if (empty($plugin['abort'])) {
$result = $rcmail->user->insert_search($plugin['data']);
}
else {
$result = $plugin['result'];
}
}
if (!empty($result)) {
$rcmail->output->show_message('savedsearchcreated', 'confirmation');
$rcmail->output->command('insert_saved_search', rcube::Q($name), rcube::Q($result));
}
else {
$error = !empty($plugin['message']) ? $plugin['message'] : 'savedsearchcreateerror';
$rcmail->output->show_message($error, 'error');
}
$rcmail->output->send();
}
}
@@ -0,0 +1,63 @@
<?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: |
| Delete saved search |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_search_delete extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$id = rcube_utils::get_input_string('_sid', rcube_utils::INPUT_POST);
$result = false;
if (!empty($id)) {
$plugin = $rcmail->plugins->exec_hook('saved_search_delete', ['id' => $id]);
if (empty($plugin['abort'])) {
$result = $rcmail->user->delete_search($id);
}
else {
$result = $plugin['result'];
}
}
if ($result) {
$rcmail->output->show_message('savedsearchdeleted', 'confirmation');
$rcmail->output->command('remove_search_item', rcube::Q($id));
// contact list will be cleared, clear also page counter
$rcmail->output->command('set_rowcount', $rcmail->gettext('nocontactsfound'));
$rcmail->output->set_env('pagecount', 0);
}
else {
$error = !empty($plugin['message']) ? $plugin['message'] : 'savedsearchdeleteerror';
$rcmail->output->show_message($error, 'error');
}
$rcmail->output->send();
}
}
@@ -0,0 +1,243 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show contact details |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_show extends rcmail_action_contacts_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// Get contact ID and source ID from request
$cids = self::get_cids();
$source = key($cids);
$cid = $cids ? array_first($cids[$source]) : null;
// Initialize addressbook source
self::$CONTACTS = self::contact_source($source, true);
self::$SOURCE_ID = $source;
// read contact record (or get the one defined in 'save' action)
if (!empty($args['contact'])) {
self::$contact = $args['contact'];
}
else if ($cid) {
self::$contact = self::$CONTACTS->get_record($cid, true);
}
if ($cid && self::$contact) {
$rcmail->output->set_env('readonly', self::$CONTACTS->readonly || !empty(self::$contact['readonly']));
$rcmail->output->set_env('cid', self::$contact['ID']);
// remember current search request ID (if in search mode)
if ($search = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GET)) {
$rcmail->output->set_env('search_request', $search);
}
}
// get address book name (for display)
self::set_sourcename(self::$CONTACTS);
$rcmail->output->add_handlers([
'contacthead' => [$this, 'contact_head'],
'contactdetails' => [$this, 'contact_details'],
'contactphoto' => [$this, 'contact_photo'],
]);
$rcmail->output->send('contact');
}
public static function contact_head($attrib)
{
$rcmail = rcmail::get_instance();
// check if we have a valid result
if (!self::$contact) {
$rcmail->output->show_message('contactnotfound', 'error');
return false;
}
$form = [
'head' => [ // section 'head' is magic!
'name' => $rcmail->gettext('contactnameandorg'),
'content' => [
'source' => ['type' => 'text'],
'prefix' => ['type' => 'text'],
'firstname' => ['type' => 'text'],
'middlename' => ['type' => 'text'],
'surname' => ['type' => 'text'],
'suffix' => ['type' => 'text'],
'name' => ['type' => 'text'],
'nickname' => ['type' => 'text'],
'organization' => ['type' => 'text'],
'department' => ['type' => 'text'],
'jobtitle' => ['type' => 'text'],
],
],
];
unset($attrib['name']);
return self::contact_form($form, self::$contact, $attrib);
}
public static function contact_details($attrib)
{
$rcmail = rcmail::get_instance();
// check if we have a valid result
if (!self::$contact) {
return false;
}
$i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
$short_labels = self::get_bool_attr($attrib, 'short-legend-labels');
$form = [
'contact' => [
'name' => $rcmail->gettext('properties'),
'content' => [
'email' => ['size' => $i_size, 'render_func' => 'rcmail_action_contacts_show::render_email_value'],
'phone' => ['size' => $i_size, 'render_func' => 'rcmail_action_contacts_show::render_phone_value'],
'address' => [],
'website' => ['size' => $i_size, 'render_func' => 'rcmail_action_contacts_show::render_url_value'],
'im' => ['size' => $i_size],
],
],
'personal' => [
'name' => $rcmail->gettext($short_labels ? 'personal' : 'personalinfo'),
'content' => [
'gender' => ['size' => $i_size],
'maidenname' => ['size' => $i_size],
'birthday' => ['size' => $i_size],
'anniversary' => ['size' => $i_size],
'manager' => ['size' => $i_size],
'assistant' => ['size' => $i_size],
'spouse' => ['size' => $i_size],
],
],
];
if (isset(rcmail_action_contacts_index::$CONTACT_COLTYPES['notes'])) {
$form['notes'] = [
'name' => $rcmail->gettext('notes'),
'content' => [
'notes' => ['type' => 'textarea', 'label' => false],
],
];
}
if (self::$CONTACTS->groups) {
$form['groups'] = [
'name' => $rcmail->gettext('groups'),
'content' => self::contact_record_groups(self::$contact['ID']),
];
}
return self::contact_form($form, self::$contact, $attrib);
}
public static function render_email_value($email)
{
$rcmail = rcmail::get_instance();
return html::a([
'href' => 'mailto:' . $email,
'onclick' => sprintf(
"return %s.command('compose','%s',this)",
rcmail_output::JS_OBJECT_NAME,
rcube::JQ($email)
),
'title' => $rcmail->gettext('composeto'),
'class' => 'email',
],
rcube::Q($email)
);
}
public static function render_phone_value($phone)
{
$attrs = [
'href' => 'tel:' . preg_replace('/[^0-9+,;-]/', '', $phone),
'class' => 'phone',
];
return html::a($attrs, rcube::Q($phone));
}
public static function render_url_value($url)
{
$prefix = preg_match('!^(http|ftp)s?://!', $url) ? '' : 'http://';
return html::a([
'href' => $prefix . $url,
'target' => '_blank',
'class' => 'url',
],
rcube::Q($url)
);
}
public static function contact_record_groups($contact_id)
{
$groups = self::$CONTACTS->list_groups();
if (empty($groups)) {
return '';
}
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$members = self::$CONTACTS->get_record_groups($contact_id);
$table = new html_table(['tagname' => 'ul', 'cols' => 1, 'class' => 'proplist simplelist']);
$checkbox = new html_checkbox(['name' => '_gid[]', 'class' => 'groupmember', 'disabled' => self::$CONTACTS->readonly]);
foreach ($groups as $group) {
$gid = $group['ID'];
$input = $checkbox->show(!empty($members[$gid]) ? $gid : null, ['value' => $gid]);
$table->add(null, html::label(null, $input . rcube::Q($group['name'])));
}
$hiddenfields = new html_hiddenfield(['name' => '_source', 'value' => $source]);
$hiddenfields->add(['name' => '_cid', 'value' => $contact_id]);
$form_attrs = [
'name' => 'form',
'method' => 'post',
'task' => $rcmail->task,
'action' => 'save',
'request' => 'save.' . intval($contact_id),
'noclose' => true,
];
$form_start = $rcmail->output->request_form($form_attrs, $hiddenfields->show());
$form_end = '</form>';
$rcmail->output->add_gui_object('editform', 'form');
$rcmail->output->add_label('addingmember', 'removingmember');
return $form_start . html::tag('fieldset', 'contactfieldgroup contactgroups', $table->show()) . $form_end;
}
}
@@ -0,0 +1,68 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Undelete contacts (CIDs) from last delete action |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_undo extends rcmail_action_contacts_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$delcnt = 0;
if (!empty($_SESSION['contact_undo']) && !empty($_SESSION['contact_undo']['data'])) {
foreach ((array) $_SESSION['contact_undo']['data'] as $source => $cid) {
$contacts = self::contact_source($source);
$plugin = $rcmail->plugins->exec_hook('contact_undelete', [
'id' => $cid,
'source' => $source
]);
$restored = empty($plugin['abort']) ? $contacts->undelete($cid) : $plugin['result'];
if (!$restored) {
$error = !empty($plugin['message']) ? $plugin['message'] : 'contactrestoreerror';
$rcmail->output->show_message($error, 'error');
$rcmail->output->command('list_contacts');
$rcmail->output->send();
}
else {
$delcnt += $restored;
}
}
}
$rcmail->session->remove('contact_undo');
if ($delcnt) {
$rcmail->output->show_message('contactrestored', 'confirmation');
$rcmail->output->command('list_contacts');
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,94 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handles contact photo uploads |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_contacts_upload_photo extends rcmail_action_contacts_index
{
/**
* Supported image format types
* ImageMagick works with other non-image types (e.g. pdf), we don't want that here
*
* @var array
*/
public static $IMAGE_TYPES = ['jpeg','jpg','jp2','tiff','tif','bmp','eps','gif','png','png8','png24','png32','svg','ico'];
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// clear all stored output properties (like scripts and env vars)
$rcmail->output->reset();
if (!empty($_FILES['_photo']['tmp_name'])) {
$filepath = $_FILES['_photo']['tmp_name'];
// check file type and resize image
$image = new rcube_image($_FILES['_photo']['tmp_name']);
$imageprop = $image->props();
if (
in_array(strtolower($imageprop['type']), self::$IMAGE_TYPES)
&& $imageprop['width']
&& $imageprop['height']
) {
$maxsize = intval($rcmail->config->get('contact_photo_size', 160));
$tmpfname = rcube_utils::temp_filename('imgconvert');
$save_hook = 'attachment_upload';
// scale image to a maximum size
if (($imageprop['width'] > $maxsize || $imageprop['height'] > $maxsize) && $image->resize($maxsize, $tmpfname)) {
$filepath = $tmpfname;
$save_hook = 'attachment_save';
}
// save uploaded file in storage backend
$attachment = $rcmail->plugins->exec_hook($save_hook, [
'path' => $filepath,
'size' => $_FILES['_photo']['size'],
'name' => $_FILES['_photo']['name'],
'mimetype' => 'image/' . $imageprop['type'],
'group' => 'contact',
]);
}
else {
$attachment = ['error' => $rcmail->gettext('invalidimageformat')];
}
if (!empty($attachment['status']) && empty($attachment['abort'])) {
$file_id = $attachment['id'];
$_SESSION['contacts']['files'][$file_id] = $attachment;
$rcmail->output->command('replace_contact_photo', $file_id);
}
else {
// upload failed
self::upload_error($_FILES['_photo']['error'], $attachment);
}
}
else {
self::upload_failure();
}
$rcmail->output->command('photo_upload_end');
$rcmail->output->send('iframe');
}
}
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Perform OAuth2 user login |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_login_oauth extends rcmail_action
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$auth_code = rcube_utils::get_input_string('code', rcube_utils::INPUT_GET);
$auth_error = rcube_utils::get_input_string('error', rcube_utils::INPUT_GET);
$auth_state = rcube_utils::get_input_string('state', rcube_utils::INPUT_GET);
// auth code return from oauth login
if (!empty($auth_code)) {
$auth = $rcmail->oauth->request_access_token($auth_code, $auth_state);
// oauth success
if ($auth && isset($auth['username'], $auth['authorization'], $auth['token'])) {
// enforce XOAUTH2 auth type
$rcmail->config->set('imap_auth_type', 'XOAUTH2');
$rcmail->config->set('login_password_maxlen', strlen($auth['authorization']));
// use access_token and user info for IMAP login
$storage_host = $rcmail->autoselect_host();
if ($rcmail->login($auth['username'], $auth['authorization'], $storage_host, true)) {
// replicate post-login tasks from index.php
$rcmail->session->remove('temp');
$rcmail->session->regenerate_id(false);
// send auth cookie if necessary
$rcmail->session->set_auth_cookie();
// save OAuth token in session
$_SESSION['oauth_token'] = $auth['token'];
// log successful login
$rcmail->log_login();
// allow plugins to control the redirect url after login success
$redir = $rcmail->plugins->exec_hook('login_after', ['_task' => 'mail']);
unset($redir['abort'], $redir['_err']);
// send redirect
header('Location: ' . $rcmail->url($redir, true, false));
exit;
}
else {
$rcmail->output->show_message('loginfailed', 'warning');
// log failed login
$error_code = $rcmail->login_error();
$rcmail->log_login($auth['username'], true, $error_code);
$rcmail->plugins->exec_hook('login_failed', [
'code' => $error_code,
'host' => $storage_host,
'user' => $auth['username'],
]);
$rcmail->kill_session();
// fall through -> login page
}
}
else {
$rcmail->output->show_message('oauthloginfailed', 'warning');
}
}
// error return from oauth login
else if (!empty($auth_error)) {
$error_message = rcube_utils::get_input_string('error_description', rcube_utils::INPUT_GET) ?: $auth_error;
$rcmail->output->show_message($error_message, 'warning');
}
// login action: redirect to `oauth_auth_uri`
else if ($rcmail->task === 'login') {
// this will always exit() the process
$rcmail->oauth->login_redirect();
}
}
}
@@ -0,0 +1,98 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Add the submitted contact to the user's address book |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_addcontact extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$address = rcube_utils::get_input_string('_address', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_POST);
// Get the default addressbook
$CONTACTS = null;
$SENDERS = null;
$type = 0;
if ($source != rcube_addressbook::TYPE_TRUSTED_SENDER) {
$CONTACTS = $rcmail->get_address_book(rcube_addressbook::TYPE_DEFAULT, true);
$type = rcube_addressbook::TYPE_DEFAULT;
}
// Get the trusted senders addressbook
if (!empty($_POST['_reload']) || $source == rcube_addressbook::TYPE_TRUSTED_SENDER) {
$collected_senders = $rcmail->config->get('collected_senders');
if (strlen($collected_senders)) {
$type |= rcube_addressbook::TYPE_TRUSTED_SENDER;
$SENDERS = $rcmail->get_address_book($collected_senders);
if ($CONTACTS == $SENDERS) {
$SENDERS = null;
}
}
}
$contact = rcube_mime::decode_address_list($address, 1, false);
if (empty($contact[1]['mailto'])) {
$rcmail->output->show_message('errorsavingcontact', 'error', null, false);
$rcmail->output->send();
}
$contact = [
'email' => $contact[1]['mailto'],
'name' => $contact[1]['name'],
];
$email = rcube_utils::idn_to_ascii($contact['email']);
if (!rcube_utils::check_email($email, false)) {
$rcmail->output->show_message('emailformaterror', 'error', ['email' => $contact['email']], false);
$rcmail->output->send();
}
if ($rcmail->contact_exists($contact['email'], $type)) {
$rcmail->output->show_message('contactexists', 'warning');
$rcmail->output->send();
}
$done = $rcmail->contact_create($contact, $SENDERS ?: $CONTACTS, $error);
if ($done) {
$rcmail->output->show_message('addedsuccessfully', 'confirmation');
if (!empty($_POST['_reload'])) {
$rcmail->output->command('command', 'load-remote');
}
}
else {
$rcmail->output->show_message($error ?: 'errorsavingcontact', 'error', null, false);
}
$rcmail->output->send();
}
}
@@ -0,0 +1,48 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delete attachments from compose form |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_attachment_delete extends rcmail_action_mail_attachment_upload
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
self::init();
$rcmail = rcmail::get_instance();
$attachment = self::get_attachment();
if (is_array($attachment)) {
$attachment = $rcmail->plugins->exec_hook('attachment_delete', $attachment);
if (!empty($attachment['status'])) {
$rcmail->session->remove(self::$SESSION_KEY . '.attachments.' . self::$file_id);
$rcmail->output->command('remove_from_attachment_list', 'rcmfile' . self::$file_id);
}
}
$rcmail->output->send();
}
}
@@ -0,0 +1,35 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display attachments in compose form |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_attachment_display extends rcmail_action_mail_attachment_upload
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
self::init();
self::display_uploaded_file(self::get_attachment());
exit;
}
}
@@ -0,0 +1,54 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Rename attachments in compose form |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_attachment_rename extends rcmail_action_mail_attachment_upload
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
self::init();
$filename = rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST);
$filename = trim($filename);
if (
strlen($filename)
&& ($attachment = self::get_attachment())
&& is_array($attachment)
) {
$attachment['name'] = $filename;
$rcmail->session->remove(self::$SESSION_KEY . '.attachments. ' . self::$file_id);
$rcmail->session->append(self::$SESSION_KEY . '.attachments', $attachment['id'], $attachment);
$rcmail->output->command('rename_attachment_handler', 'rcmfile' . self::$file_id, $filename);
}
$rcmail->output->send();
}
}
@@ -0,0 +1,290 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Attachment uploads handler for the compose form |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_attachment_upload extends rcmail_action_mail_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
protected static $SESSION_KEY;
protected static $COMPOSE;
protected static $COMPOSE_ID;
protected static $file_id;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
self::init();
// clear all stored output properties (like scripts and env vars)
$rcmail->output->reset();
$uploadid = rcube_utils::get_input_string('_uploadid', rcube_utils::INPUT_GPC);
$uri = rcube_utils::get_input_string('_uri', rcube_utils::INPUT_POST);
// handle dropping a reference to an attachment part of some message
if ($uri) {
$attachment = null;
$url = parse_url($uri);
if (!empty($url['query'])) {
parse_str($url['query'], $params);
}
if (
!empty($params) && isset($params['_mbox']) && strlen($params['_mbox'])
&& !empty($params['_uid']) && !empty($params['_part'])
) {
// @TODO: at some point we might support drag-n-drop between
// two different accounts on the same server, for now make sure
// this is the same server and the same user
list($host, $port) = rcube_utils::explode(':', $_SERVER['HTTP_HOST']);
if (
$host == $url['host']
&& $port == $url['port']
&& $rcmail->get_user_name() == rawurldecode($url['user'])
) {
$message = new rcube_message($params['_uid'], $params['_mbox']);
if ($message && !empty($message->headers)) {
$attachment = rcmail_action_mail_compose::save_attachment($message, $params['_part'], self::$COMPOSE_ID);
}
}
}
$plugin = $rcmail->plugins->exec_hook('attachment_from_uri', [
'attachment' => $attachment,
'uri' => $uri,
'compose_id' => self::$COMPOSE_ID
]);
if ($plugin['attachment']) {
self::attachment_success($plugin['attachment'], $uploadid);
}
else {
$rcmail->output->command('display_message', $rcmail->gettext('filelinkerror'), 'error');
$rcmail->output->command('remove_from_attachment_list', $uploadid);
}
$rcmail->output->send();
}
// handle file(s) upload
if (is_array($_FILES['_attachments']['tmp_name'])) {
$multiple = count($_FILES['_attachments']['tmp_name']) > 1;
$errors = [];
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_attachments']['error'][$i];
if (!$err) {
$filename = $_FILES['_attachments']['name'][$i];
$filesize = $_FILES['_attachments']['size'][$i];
$filetype = rcube_mime::file_content_type($filepath, $filename, $_FILES['_attachments']['type'][$i]);
if ($err = self::check_message_size($filesize, $filetype)) {
if (!in_array($err, $errors)) {
$rcmail->output->command('display_message', $err, 'error');
$rcmail->output->command('remove_from_attachment_list', $uploadid);
$errors[] = $err;
}
continue;
}
$attachment = $rcmail->plugins->exec_hook('attachment_upload', [
'path' => $filepath,
'name' => $filename,
'size' => $filesize,
'mimetype' => $filetype,
'group' => self::$COMPOSE_ID,
]);
}
if (!$err && !empty($attachment['status']) && empty($attachment['abort'])) {
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$rcmail->session->append(self::$SESSION_KEY . '.attachments', $attachment['id'], $attachment);
self::attachment_success($attachment, $uploadid);
}
else { // upload failed
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$size = self::show_bytes(rcube_utils::max_upload_size());
$msg = $rcmail->gettext(['name' => 'filesizeerror', 'vars' => ['size' => $size]]);
}
else if (!empty($attachment['error'])) {
$msg = $attachment['error'];
}
else {
$msg = $rcmail->gettext('fileuploaderror');
}
if (!empty($attachment['error']) || $err != UPLOAD_ERR_NO_FILE) {
if (!in_array($msg, $errors)) {
$rcmail->output->command('display_message', $msg, 'error');
$rcmail->output->command('remove_from_attachment_list', $uploadid);
$errors[] = $msg;
}
}
}
}
}
else if (self::upload_failure()) {
$rcmail->output->command('remove_from_attachment_list', $uploadid);
}
// send html page with JS calls as response
$rcmail->output->command('auto_save_start', false);
$rcmail->output->send('iframe');
}
public static function init()
{
self::$COMPOSE_ID = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GPC);
self::$COMPOSE = null;
self::$SESSION_KEY = 'compose_data_' . self::$COMPOSE_ID;
if (self::$COMPOSE_ID && !empty($_SESSION[self::$SESSION_KEY])) {
self::$COMPOSE =& $_SESSION[self::$SESSION_KEY];
}
if (!self::$COMPOSE) {
die("Invalid session var!");
}
self::$file_id = rcube_utils::get_input_string('_file', rcube_utils::INPUT_GPC);
self::$file_id = preg_replace('/^rcmfile/', '', self::$file_id) ?: 'unknown';
}
public static function get_attachment()
{
return self::$COMPOSE['attachments'][self::$file_id];
}
public static function attachment_success($attachment, $uploadid)
{
$rcmail = rcmail::get_instance();
$id = $attachment['id'];
if (!empty(self::$COMPOSE['deleteicon']) && is_file(self::$COMPOSE['deleteicon'])) {
$button = html::img([
'src' => self::$COMPOSE['deleteicon'],
'alt' => $rcmail->gettext('delete')
]);
}
else if (!empty(self::$COMPOSE['textbuttons'])) {
$button = rcube::Q($rcmail->gettext('delete'));
}
else {
$button = '';
}
$link_content = sprintf(
'<span class="attachment-name">%s</span><span class="attachment-size">(%s)</span>',
rcube::Q($attachment['name']), self::show_bytes($attachment['size'])
);
$content_link = html::a([
'href' => "#load",
'class' => 'filename',
'onclick' => sprintf(
"return %s.command('load-attachment','rcmfile%s', this, event)",
rcmail_output::JS_OBJECT_NAME,
$id
),
], $link_content);
$delete_link = html::a([
'href' => "#delete",
'onclick' => sprintf(
"return %s.command('remove-attachment','rcmfile%s', this, event)",
rcmail_output::JS_OBJECT_NAME,
$id
),
'title' => $rcmail->gettext('delete'),
'class' => 'delete',
'aria-label' => $rcmail->gettext('delete') . ' ' . $attachment['name'],
], $button);
if (!empty(self::$COMPOSE['icon_pos']) && self::$COMPOSE['icon_pos'] == 'left') {
$content = $delete_link . $content_link;
}
else {
$content = $content_link . $delete_link;
}
$rcmail->output->command('add2attachment_list', "rcmfile$id", [
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true
],
$uploadid
);
}
/**
* Checks if the attached file will fit in message size limit.
* Calculates size of all attachments and compares with the limit.
*
* @param int $filesize File size
* @param string $filetype File mimetype
*
* @return string Error message if the limit is exceeded
*/
public static function check_message_size($filesize, $filetype)
{
$rcmail = rcmail::get_instance();
$limit = parse_bytes($rcmail->config->get('max_message_size'));
$size = 10 * 1024; // size of message body
if (!$limit) {
return;
}
// add size of already attached files
if (!empty(self::$COMPOSE['attachments'])) {
foreach ((array) self::$COMPOSE['attachments'] as $att) {
// All attachments are base64-encoded except message/rfc822 (see sendmail.inc)
$multip = $att['mimetype'] == 'message/rfc822' ? 1 : 1.33;
$size += $att['size'] * $multip;
}
}
// add size of the new attachment
$multip = $filetype == 'message/rfc822' ? 1 : 1.33;
$size += $filesize * $multip;
if ($size > $limit) {
$limit = self::show_bytes($limit);
return $rcmail->gettext(['name' => 'msgsizeerror', 'vars' => ['size' => $limit]]);
}
}
}
@@ -0,0 +1,214 @@
<?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: |
| Perform a search on configured address books for the email |
| address autocompletion |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_autocomplete extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$MAXNUM = (int) $rcmail->config->get('autocomplete_max', 15);
$mode = (int) $rcmail->config->get('addressbook_search_mode');
$single = (bool) $rcmail->config->get('autocomplete_single');
$search = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC, true);
$reqid = rcube_utils::get_input_string('_reqid', rcube_utils::INPUT_GPC);
$contacts = [];
if (strlen($search) && ($book_types = self::autocomplete_addressbooks())) {
$sort_keys = [];
$books_num = count($book_types);
$search_lc = mb_strtolower($search);
$mode |= rcube_addressbook::SEARCH_GROUPS;
$fields = $rcmail->config->get('contactlist_fields');
foreach ($book_types as $abook_id) {
$abook = $rcmail->get_address_book($abook_id);
$abook->set_pagesize($MAXNUM);
if ($result = $abook->search($fields, $search, $mode, true, true, 'email')) {
while ($record = $result->iterate()) {
// Contact can have more than one e-mail address
$email_arr = (array) $abook->get_col_values('email', $record, true);
$email_cnt = count($email_arr);
$idx = 0;
foreach ($email_arr as $email) {
if (empty($email)) {
continue;
}
$name = rcube_addressbook::compose_list_name($record);
$contact = format_email_recipient($email, $name);
// skip entries that don't match
if ($email_cnt > 1 && strpos(mb_strtolower($contact), $search_lc) === false) {
continue;
}
$index = $contact;
// skip duplicates
if (empty($contacts[$index])) {
$contact = [
'name' => $contact,
'type' => $record['_type'] ?? null,
'id' => $record['ID'],
'source' => $abook_id,
];
$display = rcube_addressbook::compose_search_name($record, $email, $name);
if ($display && $display != $contact['name']) {
$contact['display'] = $display;
}
// groups with defined email address will not be expanded to its members' addresses
if ($contact['type'] == 'group') {
$contact['email'] = $email;
}
$name = !empty($contact['display']) ? $contact['display'] : $name;
$contacts[$index] = $contact;
$sort_keys[$index] = sprintf('%s %03d', $name, $idx++);
if (count($contacts) >= $MAXNUM) {
break 2;
}
}
// skip redundant entries (show only first email address)
if ($single) {
break;
}
}
}
}
// also list matching contact groups
if ($abook->groups && count($contacts) < $MAXNUM) {
foreach ($abook->list_groups($search, $mode) as $group) {
$abook->reset();
$abook->set_group($group['ID']);
$group_prop = $abook->get_group($group['ID']);
// group (distribution list) with email address(es)
if (!empty($group_prop['email'])) {
$idx = 0;
foreach ((array) $group_prop['email'] as $email) {
$index = format_email_recipient($email, $group['name']);
if (empty($contacts[$index])) {
$sort_keys[$index] = sprintf('%s %03d', $group['name'] , $idx++);
$contacts[$index] = [
'name' => $index,
'email' => $email,
'type' => 'group',
'id' => $group['ID'],
'source' => $abook_id,
];
if (count($contacts) >= $MAXNUM) {
break 3;
}
}
}
}
// show group with count
else if (($result = $abook->count()) && $result->count) {
if (empty($contacts[$group['name']])) {
$sort_keys[$group['name']] = $group['name'];
$contacts[$group['name']] = [
'name' => $group['name'] . ' (' . intval($result->count) . ')',
'type' => 'group',
'id' => $group['ID'],
'source' => $abook_id,
];
if (count($contacts) >= $MAXNUM) {
break 2;
}
}
}
}
}
}
if (count($contacts)) {
// sort contacts index
asort($sort_keys, SORT_LOCALE_STRING);
// re-sort contacts according to index
foreach ($sort_keys as $idx => $val) {
$sort_keys[$idx] = $contacts[$idx];
}
$contacts = array_values($sort_keys);
}
}
// Allow autocomplete result optimization via plugin
$plugin = $rcmail->plugins->exec_hook('contacts_autocomplete_after', [
'search' => $search,
// Provide already-found contacts to plugin if they are required
'contacts' => $contacts,
]);
$contacts = $plugin['contacts'];
$rcmail->output->command('ksearch_query_results', $contacts, $search, $reqid);
$rcmail->output->send();
}
/**
* Collect addressbook sources used for autocompletion
*/
public static function autocomplete_addressbooks()
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
if (strlen($source)) {
$book_types = [$source];
}
else {
$book_types = (array) $rcmail->config->get('autocomplete_addressbooks', 'sql');
}
$collected_recipients = $rcmail->config->get('collected_recipients');
$collected_senders = $rcmail->config->get('collected_senders');
if (strlen($collected_recipients) && !in_array($collected_recipients, $book_types)) {
$book_types[] = $collected_recipients;
}
if (strlen($collected_senders) && !in_array($collected_senders, $book_types)) {
$book_types[] = $collected_senders;
}
return !empty($book_types) ? $book_types : null;
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Bounce/resend an email message |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_bounce extends rcmail_action
{
protected static $MESSAGE;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$msg_uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GP);
$msg_folder = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_GP, true);
$MESSAGE = new rcube_message($msg_uid, $msg_folder);
self::$MESSAGE = $MESSAGE;
if (!$MESSAGE->headers) {
$rcmail->output->show_message('messageopenerror', 'error');
$rcmail->output->send('iframe');
}
// Display Bounce form
if (empty($_POST)) {
if (!empty($MESSAGE->headers->charset)) {
$rcmail->storage->set_charset($MESSAGE->headers->charset);
}
// Initialize helper class to build the UI
$SENDMAIL = new rcmail_sendmail(
['mode' => rcmail_sendmail::MODE_FORWARD],
['message' => $MESSAGE]
);
$rcmail->output->set_env('mailbox', $msg_folder);
$rcmail->output->set_env('uid', $msg_uid);
$rcmail->output->add_handler('bounceobjects', [$this, 'bounce_objects']);
$rcmail->output->send('bounce');
}
// Initialize helper class to send the message
$SENDMAIL = new rcmail_sendmail(
['mode' => rcmail_sendmail::MODE_FORWARD],
[
'sendmail' => true,
'error_handler' => function(...$args) use ($rcmail) {
call_user_func_array([$rcmail->output, 'show_message'], $args);
$rcmail->output->send('iframe');
}
]
);
// Handle the form input
$input_headers = $SENDMAIL->headers_input();
// Set Resent-* headers, these will be added on top of the bounced message
$headers = [];
foreach (['From', 'To', 'Cc', 'Bcc', 'Date', 'Message-ID'] as $name) {
if (!empty($input_headers[$name])) {
$headers['Resent-' . $name] = $input_headers[$name];
}
}
// Create the bounce message
$BOUNCE = new rcmail_resend_mail([
'bounce_message' => $MESSAGE,
'bounce_headers' => $headers,
]);
// Send the bounce message
$SENDMAIL->deliver_message($BOUNCE);
// Save in Sent (if requested)
$saved = $SENDMAIL->save_message($BOUNCE);
if (!$saved && strlen($SENDMAIL->options['store_target'])) {
self::display_server_error('errorsaving');
}
$rcmail->output->show_message('messagesent', 'confirmation', null, false);
$rcmail->output->send('iframe');
}
/**
* Handler for template object 'bounceObjects'
*
* @param array $attrib HTML attributes
*
* @return string HTML content
*/
public static function bounce_objects($attrib)
{
if (empty($attrib['id'])) {
$attrib['id'] = 'bounce-objects';
}
$rcmail = rcmail::get_instance();
$content = [];
// Always display a hint about the bounce feature behavior
$msg = html::span(null, rcube::Q($rcmail->gettext('bouncehint')));
$msg_attrib = ['id' => 'bounce-hint', 'class' => 'boxinformation'];
$content[] = html::div($msg_attrib, $msg);
// Add a warning about Bcc recipients
if (self::$MESSAGE->headers->get('bcc', false) || self::$MESSAGE->headers->get('resent-bcc', false)) {
$msg = html::span(null, rcube::Q($rcmail->gettext('bccemail')));
$msg_attrib = ['id' => 'bcc-warning', 'class' => 'boxwarning'];
$content[] = html::div($msg_attrib, $msg);
}
$plugin = $rcmail->plugins->exec_hook('bounce_objects',
['content' => $content, 'message' => self::$MESSAGE]);
$content = implode("\n", $plugin['content']);
return $content ? html::div($attrib, $content) : '';
}
}
@@ -0,0 +1,211 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Check for recent messages, in all mailboxes |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_check_recent extends rcmail_action_mail_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// If there's no folder or messages list, there's nothing to update
// This can happen on 'refresh' request
if (empty($_POST['_folderlist']) && empty($_POST['_list'])) {
return;
}
$trash = $rcmail->config->get('trash_mbox');
$current = $rcmail->storage->get_folder();
$check_all = $rcmail->action != 'refresh' || (bool) $rcmail->config->get('check_all_folders');
$page = $rcmail->storage->get_page();
$page_size = $rcmail->storage->get_pagesize();
$search_request = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC);
if ($search_request && $_SESSION['search_request'] != $search_request) {
$search_request = null;
}
// list of folders to check
if ($check_all) {
$a_mailboxes = $rcmail->storage->list_folders_subscribed('', '*', 'mail');
}
else if ($search_request && isset($_SESSION['search'][1]) && is_object($_SESSION['search'][1])) {
$a_mailboxes = (array) $_SESSION['search'][1]->get_parameters('MAILBOX');
}
else {
$a_mailboxes = (array) $current;
if ($current != 'INBOX') {
$a_mailboxes[] = 'INBOX';
}
}
// Control folders list from a plugin
$plugin = $rcmail->plugins->exec_hook('check_recent', ['folders' => $a_mailboxes, 'all' => $check_all]);
$a_mailboxes = $plugin['folders'];
$list_cleared = false;
self::storage_fatal_error();
// check recent/unseen counts
foreach ($a_mailboxes as $mbox_name) {
$is_current = $mbox_name == $current
|| (
!empty($search_request)
&& isset($_SESSION['search'][1])
&& is_object($_SESSION['search'][1])
&& in_array($mbox_name, (array)$_SESSION['search'][1]->get_parameters('MAILBOX'))
);
if ($is_current) {
// Synchronize mailbox cache, handle flag changes
$rcmail->storage->folder_sync($mbox_name);
}
// Get mailbox status
$status = $rcmail->storage->folder_status($mbox_name, $diff);
if ($is_current) {
self::storage_fatal_error();
}
if ($status & 1) {
// trigger plugin hook
$rcmail->plugins->exec_hook('new_messages', [
'mailbox' => $mbox_name,
'is_current' => $is_current,
'diff' => $diff
]);
}
self::send_unread_count($mbox_name, true, null, (!$is_current && ($status & 1)) ? 'recent' : '');
if ($status && $is_current) {
// refresh saved search set
if (!empty($search_request) && isset($_SESSION['search'])) {
unset($search_request); // only do this once
$_SESSION['search'] = $rcmail->storage->refresh_search();
if (!empty($_SESSION['search'][1]->multi)) {
$mbox_name = '';
}
}
if (!empty($_POST['_quota'])) {
$rcmail->output->command('set_quota', self::quota_content(null, $mbox_name));
}
$rcmail->output->set_env('exists', $rcmail->storage->count($mbox_name, 'EXISTS', true));
// "No-list" mode, don't get messages
if (empty($_POST['_list'])) {
continue;
}
// get overall message count; allow caching because rcube_storage::folder_status()
// did a refresh but only in list mode
$list_mode = $rcmail->storage->get_threading() ? 'THREADS' : 'ALL';
$all_count = $rcmail->storage->count($mbox_name, $list_mode, $list_mode == 'THREADS', false);
// check current page if we're not on the first page
if ($all_count && $page > 1) {
$remaining = $all_count - $page_size * ($page - 1);
if ($remaining <= 0) {
$page -= 1;
$rcmail->storage->set_page($page);
$_SESSION['page'] = $page;
}
}
$rcmail->output->set_env('messagecount', $all_count);
$rcmail->output->set_env('pagecount', ceil($all_count/$page_size));
$rcmail->output->command('set_rowcount', self::get_messagecount_text($all_count), $mbox_name);
$rcmail->output->set_env('current_page', $all_count ? $page : 1);
// remove old rows (and clear selection if new list is empty)
$rcmail->output->command('message_list.clear', $all_count ? false : true);
if ($all_count) {
$a_headers = $rcmail->storage->list_messages($mbox_name, null, self::sort_column(), self::sort_order());
// add message rows
self::js_message_list($a_headers, false);
// remove messages that don't exists from list selection array
$rcmail->output->command('update_selection');
}
$list_cleared = true;
}
// set trash folder state
if ($mbox_name === $trash) {
$rcmail->output->command('set_trash_count', $rcmail->storage->count($mbox_name, 'EXISTS', true));
}
}
// handle flag updates
if (!$list_cleared) {
$uids = rcube_utils::get_input_value('_uids', rcube_utils::INPUT_POST);
$uids = self::get_uids($uids, null, $multifolder);
$recent_flags = [];
foreach ($uids as $mbox_name => $set) {
$get_flags = true;
$modseq = null;
if ($mbox_name == $current) {
$data = $rcmail->storage->folder_data($mbox_name);
$modseq = !empty($_SESSION['list_mod_seq']) ? $_SESSION['list_mod_seq'] : null;
$get_flags = empty($modseq) || empty($data['HIGHESTMODSEQ']) || $modseq != $data['HIGHESTMODSEQ'];
// remember last HIGHESTMODSEQ value (if supported)
if (!empty($data['HIGHESTMODSEQ'])) {
$_SESSION['list_mod_seq'] = $data['HIGHESTMODSEQ'];
}
}
// TODO: Consider HIGHESTMODSEQ for all folders in multifolder search, otherwise
// flags for all messages in a set are requested on every refresh
if ($get_flags) {
$flags = $rcmail->storage->list_flags($mbox_name, $set, $modseq);
foreach ($flags as $idx => $row) {
if ($multifolder) {
$idx .= '-' . $mbox_name;
}
$recent_flags[$idx] = array_change_key_case(array_map('intval', $row));
}
}
$rcmail->output->set_env('recent_flags', $recent_flags);
}
}
// trigger refresh hook
$rcmail->plugins->exec_hook('refresh', []);
$rcmail->output->send();
}
}
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Copy the submitted messages to a specific mailbox |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_copy extends rcmail_action_mail_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// copy messages
if (empty($_POST['_uid']) || !isset($_POST['_target_mbox']) || !strlen($_POST['_target_mbox'])) {
$rcmail->output->show_message('internalerror', 'error');
}
$uids = self::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST);
$target = rcube_utils::get_input_string('_target_mbox', rcube_utils::INPUT_POST, true);
$sources = [];
$copied = false;
foreach ($uids as $mbox => $uids) {
if ($mbox === $target) {
$copied++;
}
else {
$copied += (int) $rcmail->storage->copy_message($uids, $target, $mbox);
$sources[] = $mbox;
}
}
if (!$copied) {
self::display_server_error('errorcopying');
}
else {
$rcmail->output->show_message('messagecopied', 'confirmation');
self::send_unread_count($target, true);
$rcmail->output->command('set_quota', self::quota_content(null, $multifolder ? $sources[0] : 'INBOX'));
}
$rcmail->output->send();
}
}
+140
View File
@@ -0,0 +1,140 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handler for mail delete operation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_delete extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// count messages before changing anything
$threading = (bool) $rcmail->storage->get_threading();
$trash = $rcmail->config->get('trash_mbox');
$sources = [];
$old_count = 0;
$deleted = 0;
$count = 0;
if (empty($_POST['_from']) || $_POST['_from'] != 'show') {
$old_count = $rcmail->storage->count(null, $threading ? 'THREADS' : 'ALL');
}
if (empty($_POST['_uid'])) {
$rcmail->output->show_message('internalerror', 'error');
$rcmail->output->send();
}
foreach (rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST) as $mbox => $uids) {
$deleted += (int) $rcmail->storage->delete_message($uids, $mbox);
$count += is_array($uids) ? count($uids) : 1;
$sources[] = $mbox;
}
if (empty($deleted)) {
// send error message
if ($_POST['_from'] != 'show') {
$rcmail->output->command('list_mailbox');
}
self::display_server_error('errordeleting');
$rcmail->output->send();
}
else {
$rcmail->output->show_message('messagedeleted', 'confirmation');
}
$search_request = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC);
// refresh saved search set after moving some messages
if ($search_request && $rcmail->storage->get_search_set()) {
$_SESSION['search'] = $rcmail->storage->refresh_search();
}
if (!empty($_POST['_from']) && $_POST['_from'] == 'show') {
if ($next = rcube_utils::get_input_string('_next_uid', rcube_utils::INPUT_GPC)) {
$rcmail->output->command('show_message', $next);
}
else {
$rcmail->output->command('command', 'list');
}
$rcmail->output->send();
}
$mbox = $rcmail->storage->get_folder();
$msg_count = $rcmail->storage->count(null, $threading ? 'THREADS' : 'ALL');
$exists = $rcmail->storage->count($mbox, 'EXISTS', true);
$page_size = $rcmail->storage->get_pagesize();
$page = $rcmail->storage->get_page();
$pages = ceil($msg_count / $page_size);
$nextpage_count = $old_count - $page_size * $page;
$remaining = $msg_count - $page_size * ($page - 1);
$jump_back = false;
// jump back one page (user removed the whole last page)
if ($page > 1 && $remaining == 0) {
$page -= 1;
$rcmail->storage->set_page($page);
$_SESSION['page'] = $page;
$jump_back = true;
}
// update unseen messages counts for all involved folders
foreach ($sources as $source) {
self::send_unread_count($source, true);
}
// update message count display
$rcmail->output->set_env('messagecount', $msg_count);
$rcmail->output->set_env('current_page', $page);
$rcmail->output->set_env('pagecount', $pages);
$rcmail->output->set_env('exists', $exists);
$rcmail->output->command('set_quota', self::quota_content(null, $multifolder ? $sources[0] : 'INBOX'));
$rcmail->output->command('set_rowcount', self::get_messagecount_text($msg_count), $mbox);
if ($threading) {
$count = rcube_utils::get_input_string('_count', rcube_utils::INPUT_POST);
}
// add new rows from next page (if any)
if (!empty($count) && $_POST['_uid'] != '*' && ($jump_back || $nextpage_count > 0)) {
// #5862: Don't add more rows than it was on the next page
$count = $jump_back ? null : min($nextpage_count, $count);
$a_headers = $rcmail->storage->list_messages($mbox, null, self::sort_column(), self::sort_order(), $count);
self::js_message_list($a_headers, false);
}
// set trash folder state
if ($mbox === $trash) {
$rcmail->output->command('set_trash_count', $exists);
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,54 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Implement folder EXPUNGE request |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_folder_expunge extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
$success = $rcmail->storage->expunge_folder($mbox);
// reload message list if current mailbox
if ($success) {
$rcmail->output->show_message('folderexpunged', 'confirmation');
if (!empty($_REQUEST['_reload'])) {
$rcmail->output->command('set_quota', self::quota_content(null, $mbox));
$rcmail->output->command('message_list.clear');
$rcmail->action = 'list';
return;
}
}
else {
self::display_server_error();
}
$rcmail->output->send();
}
}
@@ -0,0 +1,82 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Implement folder PURGE request |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_folder_purge extends rcmail_action_mail_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
$trash_mbox = $rcmail->config->get('trash_mbox');
$trash_regexp = '/^' . preg_quote($trash_mbox . $delimiter, '/') . '/';
// we should only be purging trash (or their subfolders)
if (!strlen($trash_mbox) || $mbox === $trash_mbox || preg_match($trash_regexp, $mbox)) {
$success = $storage->delete_message('*', $mbox);
$delete = true;
}
// move to Trash
else {
$success = $storage->move_message('1:*', $trash_mbox, $mbox);
$delete = false;
}
if ($success) {
$rcmail->output->show_message('folderpurged', 'confirmation');
$rcmail->output->command('set_unread_count', $mbox, 0);
self::set_unseen_count($mbox, 0);
// set trash folder state
if ($mbox === $trash_mbox) {
$rcmail->output->command('set_trash_count', 0);
}
else if (strlen($trash_mbox)) {
$rcmail->output->command('set_trash_count', $rcmail->storage->count($trash_mbox, 'EXISTS'));
}
if (!$delete && strlen($trash_mbox)) {
self::send_unread_count($trash_mbox, true);
}
if (!empty($_REQUEST['_reload'])) {
$rcmail->output->set_env('messagecount', 0);
$rcmail->output->set_env('pagecount', 0);
$rcmail->output->set_env('exists', 0);
$rcmail->output->command('message_list.clear');
$rcmail->output->command('set_rowcount', self::get_messagecount_text(), $mbox);
$rcmail->output->command('set_quota', self::quota_content(null, $mbox));
}
}
else {
self::display_server_error();
}
$rcmail->output->send();
}
}
+372
View File
@@ -0,0 +1,372 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delivering a specific uploaded file or mail message attachment |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_get extends rcmail_action_mail_index
{
protected static $attachment;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// This resets X-Frame-Options for framed output (#6688)
$rcmail->output->page_headers();
// show loading page
if (!empty($_GET['_preload'])) {
unset($_GET['_preload']);
unset($_GET['_safe']);
$url = $rcmail->url($_GET + ['_mimewarning' => 1, '_embed' => 1]);
$message = $rcmail->gettext('loadingdata');
header('Content-Type: text/html; charset=' . RCUBE_CHARSET);
print "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url='.rcube::Q($url).'">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset=' . RCUBE_CHARSET . '">' . "\n"
. "</head>\n<body>\n$message\n</body>\n</html>";
exit;
}
$attachment = new rcmail_attachment_handler;
$mimetype = $attachment->mimetype;
$filename = $attachment->filename;
self::$attachment = $attachment;
// show part page
if (!empty($_GET['_frame'])) {
$rcmail->output->set_pagetitle($filename);
// register UI objects
$rcmail->output->add_handlers([
'messagepartframe' => [$this, 'message_part_frame'],
'messagepartcontrols' => [$this, 'message_part_controls'],
]);
$part_id = rcube_utils::get_input_string('_part', rcube_utils::INPUT_GET);
$uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET);
// message/rfc822 preview (Note: handle also multipart/ parts, they can
// come from Enigma, which replaces message/rfc822 with real mimetype)
if ($part_id && ($mimetype == 'message/rfc822' || strpos($mimetype, 'multipart/') === 0)) {
$uid = preg_replace('/\.[0-9.]+/', '', $uid);
$uid .= '.' . $part_id;
$rcmail->output->set_env('is_message', true);
}
$rcmail->output->set_env('mailbox', $rcmail->storage->get_folder());
$rcmail->output->set_env('uid', $uid);
$rcmail->output->set_env('part', $part_id);
$rcmail->output->set_env('filename', $filename);
$rcmail->output->set_env('mimetype', $mimetype);
$rcmail->output->send('messagepart');
}
// render thumbnail of an image attachment
if (!empty($_GET['_thumb']) && $attachment->is_valid()) {
$thumbnail_size = $rcmail->config->get('image_thumbnail_size', 240);
$file_ident = $attachment->ident;
$thumb_name = 'thumb' . md5($file_ident . ':' . $rcmail->user->ID . ':' . $thumbnail_size);
$cache_file = rcube_utils::temp_filename($thumb_name, false, false);
// render thumbnail image if not done yet
if (!is_file($cache_file) && $attachment->body_to_file($orig_name = rcube_utils::temp_filename('attmnt'))) {
$image = new rcube_image($orig_name);
if ($imgtype = $image->resize($thumbnail_size, $cache_file, true)) {
$mimetype = 'image/' . $imgtype;
}
else {
// Resize failed, we need to check the file mimetype
// So, we do not exit here, but goto generic file body handler below
$_GET['_thumb'] = 0;
$_REQUEST['_embed'] = 1;
}
}
if (!empty($_GET['_thumb'])) {
if (is_file($cache_file)) {
$rcmail->output->future_expire_header(3600);
header('Content-Type: ' . $mimetype);
header('Content-Length: ' . filesize($cache_file));
readfile($cache_file);
}
exit;
}
}
// Handle attachment body (display or download)
if (empty($_GET['_thumb']) && $attachment->is_valid()) {
// require CSRF protected url for downloads
if (!empty($_GET['_download'])) {
$rcmail->request_security_check(rcube_utils::INPUT_GET);
}
$extensions = rcube_mime::get_mime_extensions($mimetype);
// compare file mimetype with the stated content-type headers and file extension to avoid malicious operations
if (!empty($_REQUEST['_embed']) && empty($_REQUEST['_nocheck'])) {
$file_extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// 1. compare filename suffix with expected suffix derived from mimetype
$valid = $file_extension && in_array($file_extension, (array)$extensions)
|| empty($extensions)
|| !empty($_REQUEST['_mimeclass']);
// 2. detect the real mimetype of the attachment part and compare it with the stated mimetype and filename extension
if ($valid || !$file_extension || $mimetype == 'application/octet-stream' || stripos($mimetype, 'text/') === 0) {
$tmp_body = $attachment->body(2048);
// detect message part mimetype
$real_mimetype = rcube_mime::file_content_type($tmp_body, $filename, $mimetype, true, true);
list($real_ctype_primary, $real_ctype_secondary) = explode('/', $real_mimetype);
// accept text/plain with any extension
if ($real_mimetype == 'text/plain' && self::mimetype_compare($real_mimetype, $mimetype)) {
$valid_extension = true;
}
// ignore differences in text/* mimetypes. Filetype detection isn't very reliable here
else if ($real_ctype_primary == 'text' && strpos($mimetype, $real_ctype_primary) === 0) {
$real_mimetype = $mimetype;
$valid_extension = true;
}
// ignore filename extension if mimeclass matches (#1489029)
else if (!empty($_REQUEST['_mimeclass']) && $real_ctype_primary == $_REQUEST['_mimeclass']) {
$valid_extension = true;
}
else {
// get valid file extensions
$extensions = rcube_mime::get_mime_extensions($real_mimetype);
$valid_extension = !$file_extension || empty($extensions) || in_array($file_extension, (array)$extensions);
}
if (
// fix mimetype for files wrongly declared as octet-stream
($mimetype == 'application/octet-stream' && $valid_extension)
// force detected mimetype for images (#8158)
|| (strpos($real_mimetype, 'image/') === 0)
) {
$mimetype = $real_mimetype;
}
// "fix" real mimetype the same way the original is before comparison
$real_mimetype = rcube_mime::fix_mimetype($real_mimetype);
$valid = $valid_extension && self::mimetype_compare($real_mimetype, $mimetype);
}
else {
$real_mimetype = $mimetype;
}
// show warning if validity checks failed
if (!$valid) {
// send blocked.gif for expected images
if (empty($_REQUEST['_mimewarning']) && strpos($mimetype, 'image/') === 0) {
// Do not cache. Failure might be the result of a misconfiguration,
// thus real content should be returned once fixed.
$content = self::get_resource_content('blocked.gif');
$rcmail->output->nocacheing_headers();
header("Content-Type: image/gif");
header("Content-Transfer-Encoding: binary");
header("Content-Length: " . strlen($content));
echo $content;
}
// html warning with a button to load the file anyway
else {
$rcmail->output = new rcmail_html_page();
$rcmail->output->register_inline_warning(
$rcmail->gettext([
'name' => 'attachmentvalidationerror',
'vars' => [
'expected' => $mimetype . (!empty($file_extension) ? rcube::Q(" (.{$file_extension})") : ''),
'detected' => $real_mimetype . (!empty($extensions[0]) ? " (.{$extensions[0]})" : ''),
]
]),
$rcmail->gettext('showanyway'),
$rcmail->url(array_merge($_GET, ['_nocheck' => 1]))
);
$rcmail->output->write();
}
exit;
}
}
// TIFF/WEBP to JPEG conversion, if needed
foreach (['tiff', 'webp'] as $type) {
$img_support = !empty($_SESSION['browser_caps']) && !empty($_SESSION['browser_caps'][$type]);
if (
!empty($_REQUEST['_embed'])
&& !$img_support
&& $attachment->image_type() == 'image/' . $type
&& rcube_image::is_convertable('image/' . $type)
) {
$convert2jpeg = true;
$mimetype = 'image/jpeg';
break;
}
}
// deliver part content
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$rcmail->output = new rcmail_html_page();
$out = '';
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (!rcube_utils::mem_check($attachment->size * 10)) {
$rcmail->output->register_inline_warning(
$rcmail->gettext('messagetoobig'),
$rcmail->gettext('download'),
$rcmail->url(array_merge($_GET, ['_download' => 1]))
);
}
else {
// render HTML body
$out = $attachment->html();
// insert remote objects warning into HTML body
if (self::$REMOTE_OBJECTS) {
$rcmail->output->register_inline_warning(
$rcmail->gettext('blockedresources'),
$rcmail->gettext('allow'),
$rcmail->url(array_merge($_GET, ['_safe' => 1]))
);
}
}
$rcmail->output->write($out);
exit;
}
// add filename extension if missing
if (!pathinfo($filename, PATHINFO_EXTENSION) && ($extensions = rcube_mime::get_mime_extensions($mimetype))) {
$filename .= '.' . $extensions[0];
}
$rcmail->output->download_headers($filename, [
'type' => $mimetype,
'type_charset' => $attachment->charset,
'disposition' => !empty($_GET['_download']) ? 'attachment' : 'inline',
]);
// handle tiff to jpeg conversion
if (!empty($convert2jpeg)) {
$file_path = rcube_utils::temp_filename('attmnt');
// convert image to jpeg and send it to the browser
if ($attachment->body_to_file($file_path)) {
$image = new rcube_image($file_path);
if ($image->convert(rcube_image::TYPE_JPG, $file_path)) {
header("Content-Length: " . filesize($file_path));
readfile($file_path);
}
}
}
else {
$attachment->output($mimetype);
}
exit;
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Compares two mimetype strings with making sure that
* e.g. image/bmp and image/x-ms-bmp are treated as equal.
*/
public static function mimetype_compare($type1, $type2)
{
$regexp = '~/(x-ms-|x-)~';
$type1 = preg_replace($regexp, '/', $type1);
$type2 = preg_replace($regexp, '/', $type2);
return $type1 === $type2;
}
/**
* Attachment properties table
*/
public static function message_part_controls($attrib)
{
if (!self::$attachment->is_valid()) {
return '';
}
$rcmail = rcmail::get_instance();
$table = new html_table(['cols' => 2]);
$table->add('title', rcube::Q($rcmail->gettext('namex')).':');
$table->add('header', rcube::Q(self::$attachment->filename));
$table->add('title', rcube::Q($rcmail->gettext('type')).':');
$table->add('header', rcube::Q(self::$attachment->mimetype));
$table->add('title', rcube::Q($rcmail->gettext('size')).':');
$table->add('header', rcube::Q(self::$attachment->size()));
return $table->show($attrib);
}
/**
* Attachment preview frame
*/
public static function message_part_frame($attrib)
{
$rcmail = rcmail::get_instance();
if ($rcmail->output->get_env('is_message')) {
$url = [
'task' => 'mail',
'action' => 'preview',
'uid' => $rcmail->output->get_env('uid'),
'mbox' => $rcmail->output->get_env('mailbox'),
];
}
else {
$mimetype = $rcmail->output->get_env('mimetype');
$url = $_GET;
$url[strpos($mimetype, 'text/') === 0 ? '_embed' : '_preload'] = 1;
unset($url['_frame']);
}
$url['_framed'] = 1; // For proper X-Frame-Options:deny handling
$attrib['src'] = $rcmail->url($url);
$rcmail->output->add_gui_object('messagepartframe', $attrib['id']);
return html::iframe($attrib);
}
}
@@ -0,0 +1,69 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Check all mailboxes for unread messages and update GUI |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_getunread extends rcmail_action_mail_index
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$a_folders = $rcmail->storage->list_folders_subscribed('', '*', 'mail');
if (!empty($a_folders)) {
$current = $rcmail->storage->get_folder();
$inbox = $current == 'INBOX';
$trash = $rcmail->config->get('trash_mbox');
$check_all = (bool) $rcmail->config->get('check_all_folders');
foreach ($a_folders as $mbox) {
$unseen_old = self::get_unseen_count($mbox);
if (!$check_all && $unseen_old !== null && $mbox != $current) {
$unseen = $unseen_old;
}
else {
$unseen = $rcmail->storage->count($mbox, 'UNSEEN', $unseen_old === null);
}
// call it always for current folder, so it can update counter
// after possible message status change when opening a message
// not in preview frame
if ($unseen || $unseen_old === null || $mbox == $current) {
$rcmail->output->command('set_unread_count', $mbox, $unseen, $inbox && $mbox == 'INBOX');
}
self::set_unseen_count($mbox, $unseen);
// set trash folder state
if ($mbox === $trash) {
$rcmail->output->command('set_trash_count', $rcmail->storage->count($mbox, 'EXISTS'));
}
}
}
$rcmail->output->send();
}
}
@@ -0,0 +1,56 @@
<?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: |
| Expand addressbook group into list of email addresses |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_group_expand extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$gid = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_GET);
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$abook = $rcmail->get_address_book($source);
if ($gid && $abook) {
$abook->set_group($gid);
$abook->set_pagesize(9999); // TODO: limit number of group members by config?
$result = $abook->list_records($rcmail->config->get('contactlist_fields'));
$members = [];
while ($result && ($record = $result->iterate())) {
$email = array_first((array) $abook->get_col_values('email', $record, true));
if (!empty($email)) {
$members[] = format_email_recipient($email, rcube_addressbook::compose_list_name($record));
}
}
$rcmail->output->command('replace_group_recipients', $gid, join(', ', array_unique($members)));
}
$rcmail->output->send();
}
}
@@ -0,0 +1,87 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Fetch message headers in raw format for display |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_headers extends rcmail_action_mail_index
{
protected static $source;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GP);
$inline = $rcmail->output instanceof rcmail_output_html;
if (!$uid) {
exit;
}
if ($pos = strpos($uid, '.')) {
$message = new rcube_message($uid);
$source = $message->get_part_body(substr($uid, $pos + 1));
$source = substr($source, 0, strpos($source, "\r\n\r\n"));
}
else {
$source = $rcmail->storage->get_raw_headers($uid);
}
if ($source !== false) {
$source = trim(rcube_charset::clean($source));
$source = htmlspecialchars($source, ENT_COMPAT | ENT_HTML401, RCUBE_CHARSET);
$source = preg_replace(
[
'/\n[\t\s]+/',
'/^([a-z0-9_:-]+)/im',
'/\r?\n/'
],
[
"\n&nbsp;&nbsp;&nbsp;&nbsp;",
'<font class="bold">\1</font>',
'<br />'
],
$source
);
self::$source = $source;
$rcmail->output->add_handlers(['dialogcontent' => [$this, 'headers_output']]);
if ($inline) {
$rcmail->output->set_env('dialog_class', 'text-nowrap');
}
else {
$rcmail->output->command('set_headers', $source);
}
}
else if (!$inline) {
$rcmail->output->show_message('messageopenerror', 'error');
}
$rcmail->output->send($inline ? 'dialog' : null);
}
public static function headers_output()
{
return self::$source;
}
}
+200
View File
@@ -0,0 +1,200 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save the uploaded file(s) as messages to the current IMAP folder |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_import extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// clear all stored output properties (like scripts and env vars)
$rcmail->output->reset();
if (!empty($_FILES['_file']) && is_array($_FILES['_file'])) {
$imported = 0;
$folder = $rcmail->storage->get_folder();
foreach ((array) $_FILES['_file']['tmp_name'] as $i => $filepath) {
// Process uploaded file if there is no error
$err = $_FILES['_file']['error'][$i];
if (!$err) {
// check file content type first
$ctype = rcube_mime::file_content_type($filepath, $_FILES['_file']['name'][$i], $_FILES['_file']['type'][$i]);
list($mtype_primary, $mtype_secondary) = explode('/', $ctype);
if (in_array($ctype, ['application/zip', 'application/x-zip'])) {
$filepath = self::zip_extract($filepath);
if (empty($filepath)) {
continue;
}
}
else if (!in_array($mtype_primary, ['text', 'message'])) {
continue;
}
foreach ((array) $filepath as $file) {
// read the first few lines to detect header-like structure
$fp = fopen($file, 'r');
do {
$line = fgets($fp);
}
while ($line !== false && trim($line) == '');
if (!preg_match('/^From .+/', $line) && !preg_match('/^[a-z-_]+:\s+.+/i', $line)) {
continue;
}
$message = $lastline = '';
fseek($fp, 0);
while (($line = fgets($fp)) !== false) {
// importing mbox file, split by From - lines
if ($lastline === '' && strncmp($line, 'From ', 5) === 0 && strlen($line) > 5) {
if (!empty($message)) {
$imported += (int) self::save_message($folder, $message);
}
$message = $line;
$lastline = '';
continue;
}
$message .= $line;
$lastline = rtrim($line);
}
if (!empty($message)) {
$imported += (int) self::save_message($folder, $message);
}
// remove temp files extracted from zip
if (is_array($filepath)) {
unlink($file);
}
}
}
else {
self::upload_error($err);
}
}
if ($imported) {
$rcmail->output->show_message($rcmail->gettext(['name' => 'importmessagesuccess', 'nr' => $imported, 'vars' => ['nr' => $imported]]), 'confirmation');
$rcmail->output->command('command', 'list');
}
else {
$rcmail->output->show_message('importmessageerror', 'error');
}
}
else {
self::upload_failure();
}
// send html page with JS calls as response
$rcmail->output->send('iframe');
}
public static function zip_extract($path)
{
if (!class_exists('ZipArchive', false)) {
return;
}
$zip = new ZipArchive;
$files = [];
if ($zip->open($path)) {
for ($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->getNameIndex($i);
$tmpfname = rcube_utils::temp_filename('zipimport');
if (copy("zip://$path#$entry", $tmpfname)) {
$ctype = rcube_mime::file_content_type($tmpfname, $entry);
list($mtype_primary, ) = explode('/', $ctype);
if (in_array($mtype_primary, ['text', 'message'])) {
$files[] = $tmpfname;
}
else {
unlink($tmpfname);
}
}
}
$zip->close();
}
return $files;
}
public static function save_message($folder, &$message)
{
$date = null;
if (strncmp($message, 'From ', 5) === 0) {
// Extract the mbox from_line
$pos = strpos($message, "\n");
$from = substr($message, 0, $pos);
$message = substr($message, $pos + 1);
// Read the received date, support only known date formats
// RFC4155: "Sat Jan 3 01:05:34 1996"
$mboxdate_rx = '/^([a-z]{3} [a-z]{3} [0-9 ][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4})/i';
// Roundcube/Zipdownload: "12-Dec-2016 10:56:33 +0100"
$imapdate_rx = '/^([0-9]{1,2}-[a-z]{3}-[0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9+-]{5})/i';
if (
($pos = strpos($from, ' ', 6))
&& ($dt_str = substr($from, $pos + 1))
&& (preg_match($mboxdate_rx, $dt_str, $m) || preg_match($imapdate_rx, $dt_str, $m))
) {
try {
$date = new DateTime($m[0], new DateTimeZone('UTC'));
}
catch (Exception $e) {
// ignore
}
}
}
// unquote ">From " lines in message body
$message = preg_replace('/\n>([>]*)From /', "\n\\1From ", $message);
$message = rtrim($message);
$rcmail = rcmail::get_instance();
if ($rcmail->storage->save_message($folder, $message, '', false, [], $date)) {
return true;
}
rcube::raise_error("Failed to import message to $folder", true, false);
return false;
}
}
File diff suppressed because it is too large Load Diff
+162
View File
@@ -0,0 +1,162 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send message list to client (as remote response) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_list extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$save_arr = [];
$dont_override = (array) $rcmail->config->get('dont_override');
$cols = null;
// is there a sort type for this request?
$sort = rcube_utils::get_input_string('_sort', rcube_utils::INPUT_GET);
if ($sort && preg_match('/^[a-zA-Z_-]+$/', $sort)) {
// yes, so set the sort vars
list($sort_col, $sort_order) = explode('_', $sort);
// set session vars for sort (so next page and task switch know how to sort)
if (!in_array('message_sort_col', $dont_override)) {
$_SESSION['sort_col'] = $save_arr['message_sort_col'] = $sort_col;
}
if (!in_array('message_sort_order', $dont_override)) {
$_SESSION['sort_order'] = $save_arr['message_sort_order'] = $sort_order;
}
}
// is there a set of columns for this request?
if ($cols = rcube_utils::get_input_string('_cols', rcube_utils::INPUT_GET)) {
$_SESSION['list_attrib']['columns'] = explode(',', $cols);
if (!in_array('list_cols', $dont_override)) {
$save_arr['list_cols'] = explode(',', $cols);
}
}
// register layout change
if ($layout = rcube_utils::get_input_string('_layout', rcube_utils::INPUT_GET)) {
$rcmail->output->set_env('layout', $layout);
$save_arr['layout'] = $layout;
// force header replace on layout change
if (!empty($_SESSION['list_attrib']['columns'])) {
$cols = $_SESSION['list_attrib']['columns'];
}
}
if (!empty($save_arr)) {
$rcmail->user->save_prefs($save_arr);
}
$mbox_name = $rcmail->storage->get_folder();
$threading = (bool) $rcmail->storage->get_threading();
// Synchronize mailbox cache, handle flag changes
$rcmail->storage->folder_sync($mbox_name);
// fetch message headers
$a_headers = [];
if ($count = $rcmail->storage->count($mbox_name, $threading ? 'THREADS' : 'ALL', !empty($_REQUEST['_refresh']))) {
$a_headers = $rcmail->storage->list_messages($mbox_name, null, self::sort_column(), self::sort_order());
}
// update search set (possible change of threading mode)
if (!empty($_REQUEST['_search']) && isset($_SESSION['search'])
&& $_SESSION['search_request'] == $_REQUEST['_search']
) {
$search_request = $_REQUEST['_search'];
$_SESSION['search'] = $rcmail->storage->get_search_set();
$multifolder = !empty($_SESSION['search']) && !empty($_SESSION['search'][1]->multi);
}
// remove old search data
else if (empty($_REQUEST['_search']) && isset($_SESSION['search'])) {
$rcmail->session->remove('search');
}
self::list_pagetitle();
// update mailboxlist
if (empty($search_request)) {
self::send_unread_count($mbox_name, !empty($_REQUEST['_refresh']), empty($a_headers) ? 0 : null);
}
// update message count display
$pages = ceil($count / $rcmail->storage->get_pagesize());
$page = $count ? $rcmail->storage->get_page() : 1;
$exists = $rcmail->storage->count($mbox_name, 'EXISTS', true);
$rcmail->output->set_env('messagecount', $count);
$rcmail->output->set_env('pagecount', $pages);
$rcmail->output->set_env('threading', $threading);
$rcmail->output->set_env('current_page', $page);
$rcmail->output->set_env('exists', $exists);
$rcmail->output->command('set_rowcount', self::get_messagecount_text($count), $mbox_name);
// remove old message rows if commanded by the client
if (!empty($_REQUEST['_clear'])) {
$rcmail->output->command('clear_message_list');
}
// add message rows
self::js_message_list($a_headers, false, $cols);
if (!empty($a_headers)) {
if (!empty($search_request)) {
$rcmail->output->show_message('searchsuccessful', 'confirmation', ['nr' => $count]);
}
// remember last HIGHESTMODSEQ value (if supported)
// we need it for flag updates in check-recent
$data = $rcmail->storage->folder_data($mbox_name);
if (!empty($data['HIGHESTMODSEQ'])) {
$_SESSION['list_mod_seq'] = $data['HIGHESTMODSEQ'];
}
}
else {
// handle IMAP errors (e.g. #1486905)
if ($err_code = $rcmail->storage->get_error_code()) {
self::display_server_error();
}
else if (!empty($search_request)) {
$rcmail->output->show_message('searchnomatch', 'notice');
}
else {
$rcmail->output->show_message('nomessagesfound', 'notice');
}
}
// set trash folder state
if ($mbox_name === $rcmail->config->get('trash_mbox')) {
$rcmail->output->command('set_trash_count', $exists);
}
if ($page == 1) {
$rcmail->output->command('set_quota', self::quota_content(null, !empty($multifolder) ? 'INBOX' : $mbox_name));
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,204 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send contacts list to client (as remote response) |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_list_contacts extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$source = rcube_utils::get_input_string('_source', rcube_utils::INPUT_GPC);
$afields = $rcmail->config->get('contactlist_fields');
$addr_sort_col = $rcmail->config->get('addressbook_sort_col', 'name');
$page_size = $rcmail->config->get('addressbook_pagesize', $rcmail->config->get('pagesize', 50));
$list_page = max(1, $_GET['_page'] ?? 0);
$jsresult = [];
// Use search result
if (!empty($_REQUEST['_search']) && isset($_SESSION['contact_search'][$_REQUEST['_search']])) {
$search = (array) $_SESSION['contact_search'][$_REQUEST['_search']];
$sparam = $_SESSION['contact_search_params']['id'] == $_REQUEST['_search'] ? $_SESSION['contact_search_params']['data'] : [];
$mode = (int) $rcmail->config->get('addressbook_search_mode');
$records = [];
// get records from all sources
foreach ($search as $s => $set) {
$CONTACTS = $rcmail->get_address_book($s);
// list matching groups of this source (on page one)
if ($sparam[1] && $CONTACTS->groups && $list_page == 1) {
$jsresult += self::compose_contact_groups($CONTACTS, $s, $sparam[1], $mode);
}
// reset page
$CONTACTS->set_page(1);
$CONTACTS->set_pagesize(9999);
$CONTACTS->set_search_set($set);
// get records
$result = $CONTACTS->list_records($afields);
while ($row = $result->next()) {
$row['sourceid'] = $s;
$key = rcube_addressbook::compose_contact_key($row, $addr_sort_col);
$records[$key] = $row;
}
unset($result);
}
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$first = ($list_page-1) * $page_size;
$result = new rcube_result_set($count, $first);
// we need only records for current page
if ($page_size < $count) {
$records = array_slice($records, $first, $page_size);
}
$result->records = array_values($records);
}
// list contacts from selected source
else {
$CONTACTS = $rcmail->get_address_book($source);
if ($CONTACTS && $CONTACTS->ready) {
// set list properties
$CONTACTS->set_pagesize($page_size);
$CONTACTS->set_page($list_page);
if ($group_id = rcube_utils::get_input_string('_gid', rcube_utils::INPUT_GET)) {
$CONTACTS->set_group($group_id);
}
// list groups of this source (on page one)
else if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
$jsresult = self::compose_contact_groups($CONTACTS, $source);
}
// get contacts for this user
$result = $CONTACTS->list_records($afields);
}
}
if (!empty($result) && !$result->count && $result->searchonly) {
$rcmail->output->show_message('contactsearchonly', 'notice');
}
else if (!empty($result) && $result->count > 0) {
// create javascript list
while ($row = $result->next()) {
$name = rcube_addressbook::compose_list_name($row);
// add record for every email address of the contact
$emails = rcube_addressbook::get_col_values('email', $row, true);
foreach ($emails as $i => $email) {
$source = !empty($row['sourceid']) ? $row['sourceid'] : $source;
$row_id = $source.'-'.$row['ID'].'-'.$i;
$is_group = isset($row['_type']) && $row['_type'] == 'group';
$classname = $is_group ? 'group' : 'person';
$keyname = $is_group ? 'contactgroup' : 'contact';
$jsresult[$row_id] = format_email_recipient($email, $name);
$rcmail->output->command('add_contact_row', $row_id, [
$keyname => html::a(
['title' => $email],
rcube::Q($name ?: $email)
. ($name && count($emails) > 1 ? '&nbsp;' . html::span('email', rcube::Q($email)) : '')
)
],
$classname
);
}
}
}
// update env
$rcmail->output->set_env('contactdata', $jsresult);
$rcmail->output->set_env('pagecount', isset($result) ? ceil($result->count / $page_size) : 1);
$rcmail->output->command('set_page_buttons');
// send response
$rcmail->output->send();
}
/**
* Add groups from the given address source to the address book widget
*/
public static function compose_contact_groups($abook, $source_id, $search = null, $search_mode = 0)
{
$rcmail = rcmail::get_instance();
$jsresult = [];
foreach ($abook->list_groups($search, $search_mode) as $group) {
$abook->reset();
$abook->set_group($group['ID']);
// group (distribution list) with email address(es)
if (!empty($group['email'])) {
foreach ((array) $group['email'] as $email) {
$row_id = 'G'.$group['ID'];
$jsresult[$row_id] = format_email_recipient($email, $group['name']);
$rcmail->output->command('add_contact_row', $row_id, [
'contactgroup' => html::span(['title' => $email], rcube::Q($group['name']))
], 'group');
}
}
// make virtual groups clickable to list their members
else if (!empty($group['virtual'])) {
$row_id = 'G'.$group['ID'];
$rcmail->output->command('add_contact_row', $row_id, [
'contactgroup' => html::a([
'href' => '#list',
'rel' => $group['ID'],
'title' => $rcmail->gettext('listgroup'),
'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)",
rcmail_output::JS_OBJECT_NAME, $source_id, $group['ID']),
],
rcube::Q($group['name']) . '&nbsp;' . html::span('action', '&raquo;')
)],
'group',
['ID' => $group['ID'], 'name' => $group['name'], 'virtual' => true]
);
}
// show group with count
else if (($result = $abook->count()) && $result->count) {
$row_id = 'E'.$group['ID'];
$jsresult[$row_id] = ['name' => $group['name'], 'source' => $source_id];
$rcmail->output->command('add_contact_row', $row_id, [
'contactgroup' => rcube::Q($group['name'] . ' (' . intval($result->count) . ')')
], 'group');
}
}
$abook->reset();
$abook->set_group(0);
return $jsresult;
}
}
+196
View File
@@ -0,0 +1,196 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Mark the submitted messages with the specified flag |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_mark extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$_uids = rcube_utils::get_input_value('_uid', rcube_utils::INPUT_POST);
$flag = rcube_utils::get_input_string('_flag', rcube_utils::INPUT_POST);
$folders = rcube_utils::get_input_string('_folders', rcube_utils::INPUT_POST);
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST);
if (empty($_uids) || empty($flag)) {
$rcmail->output->show_message('internalerror', 'error');
$rcmail->output->send();
}
$rcmail = rcmail::get_instance();
$threading = (bool) $rcmail->storage->get_threading();
$skip_deleted = (bool) $rcmail->config->get('skip_deleted');
$read_deleted = (bool) $rcmail->config->get('read_when_deleted');
$flag = self::imap_flag($flag);
$old_count = 0;
$from = $_POST['_from'] ?? null;
if ($flag == 'DELETED' && $skip_deleted && $from != 'show') {
// count messages before changing anything
$old_count = $rcmail->storage->count(null, $threading ? 'THREADS' : 'ALL');
}
if ($folders == 'all') {
$mboxes = $rcmail->storage->list_folders_subscribed('', '*', 'mail');
$input = array_combine($mboxes, array_fill(0, count($mboxes), '*'));
}
else if ($folders == 'sub') {
$delim = $rcmail->storage->get_hierarchy_delimiter();
$mboxes = $rcmail->storage->list_folders_subscribed($mbox . $delim, '*', 'mail');
array_unshift($mboxes, $mbox);
$input = array_combine($mboxes, array_fill(0, count($mboxes), '*'));
}
else if ($folders == 'cur') {
$input = [$mbox => '*'];
}
else {
$input = self::get_uids(null, null, $dummy, rcube_utils::INPUT_POST);
}
$marked = 0;
$count = 0;
$read = 0;
foreach ($input as $mbox => $uids) {
$marked += (int) $rcmail->storage->set_flag($uids, $flag, $mbox);
$count += is_array($uids) ? count($uids) : 1;
}
if (!$marked) {
// send error message
if ($from != 'show') {
$rcmail->output->command('list_mailbox');
}
self::display_server_error('errormarking');
$rcmail->output->send();
}
else if (empty($_POST['_quiet'])) {
$rcmail->output->show_message('messagemarked', 'confirmation');
}
if ($flag == 'DELETED' && $read_deleted && !empty($_POST['_ruid'])) {
if ($ruids = rcube_utils::get_input_value('_ruid', rcube_utils::INPUT_POST)) {
foreach (self::get_uids($ruids) as $mbox => $uids) {
$read += (int) $rcmail->storage->set_flag($uids, 'SEEN', $mbox);
}
}
if ($read && !$skip_deleted) {
$rcmail->output->command('flag_deleted_as_read', $ruids);
}
}
if ($flag == 'SEEN' || $flag == 'UNSEEN' || ($flag == 'DELETED' && !$skip_deleted)) {
foreach ($input as $mbox => $uids) {
self::send_unread_count($mbox);
}
$rcmail->output->set_env('last_flag', $flag);
}
else if ($flag == 'DELETED' && $skip_deleted) {
if ($from == 'show') {
if ($next = rcube_utils::get_input_value('_next_uid', rcube_utils::INPUT_GPC)) {
$rcmail->output->command('show_message', $next);
}
else {
$rcmail->output->command('command', 'list');
}
}
else {
$search_request = rcube_utils::get_input_value('_search', rcube_utils::INPUT_GPC);
// refresh saved search set after moving some messages
if ($search_request && $rcmail->storage->get_search_set()) {
$_SESSION['search'] = $rcmail->storage->refresh_search();
}
$msg_count = $rcmail->storage->count(NULL, $threading ? 'THREADS' : 'ALL');
$page_size = $rcmail->storage->get_pagesize();
$page = $rcmail->storage->get_page();
$pages = ceil($msg_count / $page_size);
$nextpage_count = $old_count - $page_size * $page;
$remaining = $msg_count - $page_size * ($page - 1);
$jump_back = false;
// jump back one page (user removed the whole last page)
if ($page > 1 && $remaining == 0) {
$page -= 1;
$rcmail->storage->set_page($page);
$_SESSION['page'] = $page;
$jump_back = true;
}
foreach ($input as $mbox => $uids) {
self::send_unread_count($mbox, true);
}
// update message count display
$rcmail->output->set_env('messagecount', $msg_count);
$rcmail->output->set_env('current_page', $page);
$rcmail->output->set_env('pagecount', $pages);
$rcmail->output->command('set_rowcount', self::get_messagecount_text($msg_count), $mbox);
if ($threading) {
$count = rcube_utils::get_input_value('_count', rcube_utils::INPUT_POST);
}
// add new rows from next page (if any)
if ($old_count && $_uids != '*' && ($jump_back || $nextpage_count > 0)) {
// #5862: Don't add more rows than it was on the next page
$count = $jump_back ? null : min($nextpage_count, $count);
$a_headers = $rcmail->storage->list_messages($mbox, null,
self::sort_column(), self::sort_order(), $count);
self::js_message_list($a_headers, false);
}
}
}
$rcmail->output->send();
}
/**
* Map Roundcube UI's flag label into IMAP flag
*
* @param string $flag Flag label
*
* @return string Uppercase IMAP flag
*/
public static function imap_flag($flag)
{
$flags_map = [
'undelete' => 'UNDELETED',
'delete' => 'DELETED',
'read' => 'SEEN',
'unread' => 'UNSEEN',
'flagged' => 'FLAGGED',
'unflagged' => 'UNFLAGGED',
];
return !empty($flags_map[$flag]) ? $flags_map[$flag] : strtoupper($flag);
}
}
+164
View File
@@ -0,0 +1,164 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handler for mail move operation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_move extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// count messages before changing anything
$threading = (bool) $rcmail->storage->get_threading();
$trash = $rcmail->config->get('trash_mbox');
$old_count = 0;
if (empty($_POST['_from']) || $_POST['_from'] != 'show') {
$old_count = $rcmail->storage->count(null, $threading ? 'THREADS' : 'ALL');
}
$target = rcube_utils::get_input_string('_target_mbox', rcube_utils::INPUT_POST, true);
if (empty($_POST['_uid']) || !strlen($target)) {
$rcmail->output->show_message('internalerror', 'error');
$rcmail->output->send();
}
$success = true;
$addrows = false;
$count = 0;
$sources = [];
foreach (rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST) as $mbox => $uids) {
if ($mbox === $target) {
$count += is_array($uids) ? count($uids) : 1;
}
else if ($rcmail->storage->move_message($uids, $target, $mbox)) {
$count += is_array($uids) ? count($uids) : 1;
$sources[] = $mbox;
}
else {
$success = false;
}
}
if (!$success) {
// send error message
if (empty($_POST['_from']) || $_POST['_from'] != 'show') {
$rcmail->output->command('list_mailbox');
}
self::display_server_error('errormoving', null, $target == $trash ? 'delete' : '');
$rcmail->output->send();
}
else {
$rcmail->output->show_message($target == $trash ? 'messagemovedtotrash' : 'messagemoved', 'confirmation');
}
if (!empty($_POST['_refresh'])) {
// FIXME: send updated message rows instead of reloading the entire list
$rcmail->output->command('refresh_list');
}
else {
$addrows = true;
}
$search_request = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC);
// refresh saved search set after moving some messages
if ($search_request && $rcmail->storage->get_search_set()) {
$_SESSION['search'] = $rcmail->storage->refresh_search();
}
if (!empty($_POST['_from']) && $_POST['_from'] == 'show') {
if ($next = rcube_utils::get_input_string('_next_uid', rcube_utils::INPUT_GPC)) {
$rcmail->output->command('show_message', $next);
}
else {
$rcmail->output->command('command', 'list');
}
$rcmail->output->send();
}
$mbox = $rcmail->storage->get_folder();
$msg_count = $rcmail->storage->count(null, $threading ? 'THREADS' : 'ALL');
$exists = $rcmail->storage->count($mbox, 'EXISTS', true);
$page_size = $rcmail->storage->get_pagesize();
$page = $rcmail->storage->get_page();
$pages = ceil($msg_count / $page_size);
$nextpage_count = $old_count - $page_size * $page;
$remaining = $msg_count - $page_size * ($page - 1);
// jump back one page (user removed the whole last page)
if ($page > 1 && $remaining == 0) {
$page -= 1;
$rcmail->storage->set_page($page);
$_SESSION['page'] = $page;
$jump_back = true;
}
// update unseen messages counts for all involved folders
foreach ($sources as $source) {
self::send_unread_count($source, true);
}
self::send_unread_count($target, true);
// update message count display
$rcmail->output->set_env('messagecount', $msg_count);
$rcmail->output->set_env('current_page', $page);
$rcmail->output->set_env('pagecount', $pages);
$rcmail->output->set_env('exists', $exists);
$rcmail->output->command('set_quota', self::quota_content(null, $multifolder ? $sources[0] : 'INBOX'));
$rcmail->output->command('set_rowcount', self::get_messagecount_text($msg_count), $mbox);
if ($threading) {
$count = rcube_utils::get_input_string('_count', rcube_utils::INPUT_POST);
}
// add new rows from next page (if any)
if ($addrows && $count && $_POST['_uid'] != '*' && (!empty($jump_back) || $nextpage_count > 0)) {
// #5862: Don't add more rows than it was on the next page
$count = !empty($jump_back) ? null : min($nextpage_count, $count);
$a_headers = $rcmail->storage->list_messages($mbox, NULL,
self::sort_column(), self::sort_order(), $count);
self::js_message_list($a_headers, false);
}
// set trash folder state
if ($mbox === $trash) {
$rcmail->output->command('set_trash_count', $exists);
}
else if ($target === $trash) {
$rcmail->output->command('set_trash_count', $rcmail->storage->count($trash, 'EXISTS', true));
}
// send response
$rcmail->output->send();
}
}
@@ -0,0 +1,78 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Updates message page navigation controls |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_pagenav extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET);
$index = $rcmail->storage->index(null, self::sort_column(), self::sort_order());
$cnt = $index->count_messages();
if ($cnt && ($pos = $index->exists($uid, true)) !== false) {
$prev = $pos ? $index->get_element($pos-1) : 0;
$first = $pos ? $index->get_element('FIRST') : 0;
$next = $pos < $cnt-1 ? $index->get_element($pos+1) : 0;
$last = $pos < $cnt-1 ? $index->get_element('LAST') : 0;
}
else {
// error, this will at least disable page navigation
$rcmail->output->command('set_rowcount', '');
$rcmail->output->send();
}
// Set UIDs and activate navigation buttons
if (!empty($prev)) {
$rcmail->output->set_env('prev_uid', $prev);
$rcmail->output->command('enable_command', 'previousmessage', 'firstmessage', true);
}
if (!empty($next)) {
$rcmail->output->set_env('next_uid', $next);
$rcmail->output->command('enable_command', 'nextmessage', 'lastmessage', true);
}
if (!empty($first)) {
$rcmail->output->set_env('first_uid', $first);
}
if (!empty($last)) {
$rcmail->output->set_env('last_uid', $last);
}
// Don't need a real messages count value
$rcmail->output->set_env('messagecount', 1);
// Set rowcount text
$rcmail->output->command('set_rowcount', $rcmail->gettext([
'name' => 'messagenrof',
'vars' => ['nr' => ($pos ?? 0) + 1, 'count' => $cnt]
]));
$rcmail->output->send();
}
}
+287
View File
@@ -0,0 +1,287 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Mail messages search action |
+-----------------------------------------------------------------------+
| Author: Benjamin Smith <defitro@gmail.com> |
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_search extends rcmail_action_mail_index
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
@set_time_limit(170); // extend default max_execution_time to ~3 minutes
// reset list_page and old search results
$rcmail->storage->set_page(1);
$rcmail->storage->set_search_set(null);
$_SESSION['page'] = 1;
// get search string
$str = rcube_utils::get_input_string('_q', rcube_utils::INPUT_GET, true);
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_GET, true);
$filter = rcube_utils::get_input_string('_filter', rcube_utils::INPUT_GET);
$headers = rcube_utils::get_input_string('_headers', rcube_utils::INPUT_GET);
$scope = rcube_utils::get_input_string('_scope', rcube_utils::INPUT_GET);
$interval = rcube_utils::get_input_string('_interval', rcube_utils::INPUT_GET);
$continue = rcube_utils::get_input_string('_continue', rcube_utils::INPUT_GET);
$filter = trim((string) $filter);
$search_request = md5($mbox . $scope . $interval . $filter . $str);
// Parse input
list($subject, $search) = self::search_input($str, $headers, $scope, $mbox);
// add list filter string
$search_str = $filter && $filter != 'ALL' ? $filter : '';
if ($search_interval = self::search_interval_criteria($interval)) {
$search_str .= ' ' . $search_interval;
}
if (!empty($subject)) {
$search_str .= str_repeat(' OR', count($subject)-1);
foreach ($subject as $sub) {
$search_str .= ' ' . $sub . ' ' . rcube_imap_generic::escape($search);
}
}
$search_str = trim($search_str);
$sort_column = self::sort_column();
$sort_order = self::sort_order();
// set message set for already stored (but incomplete) search request
if (!empty($continue) && isset($_SESSION['search']) && $_SESSION['search_request'] == $continue) {
$rcmail->storage->set_search_set($_SESSION['search']);
$search_str = $_SESSION['search'][0];
}
// execute IMAP search
if ($search_str) {
$mboxes = [];
// search all, current or subfolders folders
if ($scope == 'all') {
$mboxes = $rcmail->storage->list_folders_subscribed('', '*', 'mail', null, true);
// we want natural alphabetic sorting of folders in the result set
natcasesort($mboxes);
}
else if ($scope == 'sub') {
$delim = $rcmail->storage->get_hierarchy_delimiter();
$mboxes = $rcmail->storage->list_folders_subscribed($mbox . $delim, '*', 'mail');
array_unshift($mboxes, $mbox);
}
if ($scope != 'all') {
// Remember current folder, it can change in meantime (plugins)
// but we need it to e.g. recognize Sent folder to handle From/To column later
$rcmail->output->set_env('mailbox', $mbox);
}
$result = $rcmail->storage->search($mboxes, $search_str, RCUBE_CHARSET, $sort_column);
}
// save search results in session
if (!isset($_SESSION['search']) || !is_array($_SESSION['search'])) {
$_SESSION['search'] = [];
}
if ($search_str) {
$_SESSION['search'] = $rcmail->storage->get_search_set();
$_SESSION['last_text_search'] = $str;
}
$_SESSION['search_request'] = $search_request;
$_SESSION['search_scope'] = $scope;
$_SESSION['search_interval'] = $interval;
$_SESSION['search_filter'] = $filter;
// Get the headers
if (!isset($result) || empty($result->incomplete)) {
$result_h = $rcmail->storage->list_messages($mbox, 1, $sort_column, $sort_order);
}
// Make sure we got the headers
if (!empty($result_h)) {
$count = $rcmail->storage->count($mbox, $rcmail->storage->get_threading() ? 'THREADS' : 'ALL');
self::js_message_list($result_h, false);
if ($search_str) {
$all_count = $rcmail->storage->count(null, 'ALL');
$rcmail->output->show_message('searchsuccessful', 'confirmation', ['nr' => $all_count]);
}
// remember last HIGHESTMODSEQ value (if supported)
// we need it for flag updates in check-recent
if ($mbox !== null) {
$data = $rcmail->storage->folder_data($mbox);
if (!empty($data['HIGHESTMODSEQ'])) {
$_SESSION['list_mod_seq'] = $data['HIGHESTMODSEQ'];
}
}
}
// handle IMAP errors (e.g. #1486905)
else if ($err_code = $rcmail->storage->get_error_code()) {
$count = 0;
self::display_server_error();
}
// advice the client to re-send the (cross-folder) search request
else if (!empty($result) && !empty($result->incomplete)) {
$count = 0; // keep UI locked
$rcmail->output->command('continue_search', $search_request);
}
else {
$count = 0;
$rcmail->output->show_message('searchnomatch', 'notice');
$rcmail->output->set_env('multifolder_listing', isset($result) ? !empty($result->multi) : false);
if (isset($result) && !empty($result->multi) && $scope == 'all') {
$rcmail->output->command('select_folder', '');
}
}
// update message count display
$rcmail->output->set_env('search_request', $search_str ? $search_request : '');
$rcmail->output->set_env('search_filter', $_SESSION['search_filter']);
$rcmail->output->set_env('messagecount', $count);
$rcmail->output->set_env('pagecount', ceil($count / $rcmail->storage->get_pagesize()));
$rcmail->output->set_env('exists', $mbox === null ? 0 : $rcmail->storage->count($mbox, 'EXISTS'));
$rcmail->output->command('set_rowcount', self::get_messagecount_text($count, 1), $mbox);
self::list_pagetitle();
// update unseen messages count
if ($search_str === '') {
self::send_unread_count($mbox, false, empty($result_h) ? 0 : null);
}
if (isset($result) && empty($result->incomplete)) {
$rcmail->output->command('set_quota', self::quota_content(null, !empty($result->multi) ? 'INBOX' : $mbox));
}
$rcmail->output->send();
}
/**
* Creates BEFORE/SINCE search criteria from the specified interval
* Interval can be: 1W, 1M, 1Y, -1W, -1M, -1Y
*/
public static function search_interval_criteria($interval)
{
if (empty($interval)) {
return;
}
if ($interval[0] == '-') {
$search = 'BEFORE';
$interval = substr($interval, 1);
}
else {
$search = 'SINCE';
}
$date = new DateTime('now');
$interval = new DateInterval('P' . $interval);
$date->sub($interval);
return $search . ' ' . $date->format('j-M-Y');
}
/**
* Parse search input.
*
* @param string $str Search string
* @param string $headers Comma-separated list of headers/fields to search in
* @param string $scope Search scope (all | base | sub)
* @param string $mbox Folder name
*
* @return array Search criteria (1st element) and search value (2nd element)
*/
public static function search_input($str, $headers, $scope, $mbox)
{
$rcmail = rcmail::get_instance();
$subject = [];
$srch = null;
$supported = ['subject', 'from', 'to', 'cc', 'bcc'];
// Check the search string for type of search
if (preg_match("/^(from|to|reply-to|cc|bcc|subject):.*/i", $str, $m)) {
list(, $srch) = explode(":", $str);
$subject[$m[1]] = 'HEADER ' . strtoupper($m[1]);
}
else if (preg_match("/^body:.*/i", $str)) {
list(, $srch) = explode(":", $str);
$subject['body'] = 'BODY';
}
else if (strlen(trim($str))) {
if ($headers) {
foreach (explode(',', $headers) as $header) {
switch ($header) {
case 'text':
// #1488208: get rid of other headers when searching by "TEXT"
$subject = ['text' => 'TEXT'];
break 2;
case 'body':
$subject['body'] = 'BODY';
break;
case 'replyto':
case 'reply-to':
$subject['reply-to'] = 'HEADER REPLY-TO';
$subject['mail-reply-to'] = 'HEADER MAIL-REPLY-TO';
break;
case 'followupto':
case 'followup-to':
$subject['followup-to'] = 'HEADER FOLLOWUP-TO';
$subject['mail-followup-to'] = 'HEADER MAIL-FOLLOWUP-TO';
break;
default:
if (in_array_nocase($header, $supported)) {
$subject[$header] = 'HEADER ' . strtoupper($header);
}
}
}
// save search modifiers for the current folder to user prefs
if ($scope != 'all') {
$search_mods = self::search_mods();
$search_mods_value = array_fill_keys(array_keys($subject), 1);
if (!isset($search_mods[$mbox]) || $search_mods[$mbox] != $search_mods_value) {
$search_mods[$mbox] = $search_mods_value;
$rcmail->user->save_prefs(['search_mods' => $search_mods]);
}
}
}
else {
// search in subject by default
$subject['subject'] = 'HEADER SUBJECT';
}
}
return [$subject, isset($srch) ? trim($srch) : trim($str)];
}
}
@@ -0,0 +1,139 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Search contacts from the address book widget |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_search_contacts extends rcmail_action_mail_list_contacts
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$search = rcube_utils::get_input_string('_q', rcube_utils::INPUT_GPC, true);
$sources = $rcmail->get_address_sources();
$search_mode = (int) $rcmail->config->get('addressbook_search_mode');
$addr_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));
$records = [];
$search_set = [];
$jsresult = [];
$search_mode |= rcube_addressbook::SEARCH_GROUPS;
foreach ($sources as $s) {
$source = $rcmail->get_address_book($s['id']);
$source->set_page(1);
$source->set_pagesize(9999);
// list matching groups of this source
if ($source->groups) {
$jsresult += self::compose_contact_groups($source, $s['id'], $search, $search_mode);
}
// get contacts count
$result = $source->search($afields, $search, $search_mode, true, true, 'email');
if (!$result->count) {
continue;
}
while ($row = $result->next()) {
$row['sourceid'] = $s['id'];
$key = rcube_addressbook::compose_contact_key($row, $addr_sort_col);
$records[$key] = $row;
}
$search_set[$s['id']] = $source->get_search_set();
unset($result);
}
$group_count = count($jsresult);
// sort the records
ksort($records, SORT_LOCALE_STRING);
// create resultset object
$count = count($records);
$result = new rcube_result_set($count);
// select the requested page
if ($page_size < $count) {
$records = array_slice($records, $result->first, $page_size);
}
$result->records = array_values($records);
if (!empty($result) && $result->count > 0) {
// create javascript list
while ($row = $result->next()) {
$name = rcube_addressbook::compose_list_name($row);
$is_group = isset($row['_type']) && $row['_type'] == 'group';
$classname = $is_group ? 'group' : 'person';
$keyname = $is_group ? 'contactgroup' : 'contact';
// add record for every email address of the contact
// (same as in list_contacts.inc)
$emails = rcube_addressbook::get_col_values('email', $row, true);
foreach ($emails as $i => $email) {
$row_id = $row['sourceid'].'-'.$row['ID'].'-'.$i;
$jsresult[$row_id] = format_email_recipient($email, $name);
$title = rcube_addressbook::compose_search_name($row, $email, $name);
$link_content = rcube::Q($name ?: $email);
if ($name && count($emails) > 1) {
$link_content .= '&nbsp;' . html::span('email', rcube::Q($email));
}
$link = html::a(['title' => $title], $link_content);
$rcmail->output->command('add_contact_row', $row_id, [$keyname => $link], $classname);
}
}
// search request ID
$search_request = md5('composeaddr' . $search);
// save search settings in session
$_SESSION['contact_search'][$search_request] = $search_set;
$_SESSION['contact_search_params'] = ['id' => $search_request, 'data' => [$afields, $search]];
$rcmail->output->show_message('contactsearchsuccessful', 'confirmation', ['nr' => $result->count]);
$rcmail->output->set_env('search_request', $search_request);
$rcmail->output->set_env('source', '');
$rcmail->output->command('unselect_directory');
}
else if (!$group_count) {
$rcmail->output->show_message('nocontactsfound', 'notice');
}
// update env
$rcmail->output->set_env('contactdata', $jsresult);
$rcmail->output->set_env('pagecount', ceil($result->count / $page_size));
$rcmail->output->command('set_page_buttons');
// send response
$rcmail->output->send();
}
}
+424
View File
@@ -0,0 +1,424 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Compose a new mail message and send it or store as draft |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_send extends rcmail_action
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// remove all scripts and act as called in frame
$rcmail->output->reset();
$rcmail->output->framed = true;
$COMPOSE_ID = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GPC);
$COMPOSE =& $_SESSION['compose_data_'.$COMPOSE_ID];
// Sanity checks
if (!isset($COMPOSE['id'])) {
rcube::raise_error([
'code' => 500,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Invalid compose ID"
], true, false
);
$rcmail->output->show_message('internalerror', 'error');
$rcmail->output->send('iframe');
}
$saveonly = !empty($_GET['_saveonly']);
$savedraft = !empty($_POST['_draft']) && !$saveonly;
$SENDMAIL = new rcmail_sendmail($COMPOSE, [
'sendmail' => true,
'saveonly' => $saveonly,
'savedraft' => $savedraft,
'error_handler' => function(...$args) use ($rcmail) {
call_user_func_array([$rcmail->output, 'show_message'], $args);
$rcmail->output->send('iframe');
},
'keepformatting' => !empty($_POST['_keepformatting']),
]);
if (!isset($COMPOSE['attachments'])) {
$COMPOSE['attachments'] = [];
}
// Collect input for message headers
$headers = $SENDMAIL->headers_input();
$COMPOSE['param']['message-id'] = $headers['Message-ID'];
$message_id = $headers['Message-ID'];
$message_charset = $SENDMAIL->options['charset'];
$message_body = rcube_utils::get_input_string('_message', rcube_utils::INPUT_POST, true, $message_charset);
$isHtml = (bool) rcube_utils::get_input_string('_is_html', rcube_utils::INPUT_POST);
// Reset message body and attachments in Mailvelope mode
if (isset($_POST['_pgpmime'])) {
$pgp_mime = rcube_utils::get_input_string('_pgpmime', rcube_utils::INPUT_POST);
$isHtml = false;
$message_body = '';
// clear unencrypted attachments
if (!empty($COMPOSE['attachments'])) {
foreach ((array) $COMPOSE['attachments'] as $attach) {
$rcmail->plugins->exec_hook('attachment_delete', $attach);
}
}
$COMPOSE['attachments'] = [];
}
if ($isHtml) {
$bstyle = [];
if ($font_size = $rcmail->config->get('default_font_size')) {
$bstyle[] = 'font-size: ' . $font_size;
}
if ($font_family = $rcmail->config->get('default_font')) {
$bstyle[] = 'font-family: ' . self::font_defs($font_family);
}
// append doctype and html/body wrappers
$bstyle = !empty($bstyle) ? (" style='" . implode('; ', $bstyle) . "'") : '';
$message_body = '<html><head>'
. '<meta http-equiv="Content-Type" content="text/html; charset='
. ($message_charset ?: RCUBE_CHARSET) . '" /></head>'
. "<body" . $bstyle . ">\r\n" . $message_body;
}
if (!$savedraft) {
if ($isHtml) {
$b_style = 'padding: 0 0.4em; border-left: #1010ff 2px solid; margin: 0';
$pre_style = 'margin: 0; padding: 0; font-family: monospace';
$message_body = preg_replace(
[
// remove empty signature div
'/<div id="_rc_sig">(&nbsp;)?<\/div>[\s\r\n]*$/',
// replace signature's div ID (#6073)
'/ id="_rc_sig"/',
// add inline css for blockquotes and container
'/<blockquote>/',
'/<div class="pre">/',
// convert TinyMCE's new-line sequences (#1490463)
'/<p>&nbsp;<\/p>/',
],
[
'',
' id="signature"',
'<blockquote type="cite" style="'.$b_style.'">',
'<div class="pre" style="'.$pre_style.'">',
'<p><br /></p>',
],
$message_body
);
rcube_utils::preg_error([
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not format HTML!"
], true);
}
// Check spelling before send
if (
$rcmail->config->get('spellcheck_before_send')
&& $rcmail->config->get('enable_spellcheck')
&& empty($COMPOSE['spell_checked'])
&& !empty($message_body)
) {
$language = rcube_utils::get_input_string('_lang', rcube_utils::INPUT_GPC);
$message_body = str_replace("\r\n", "\n", $message_body);
$spellchecker = new rcube_spellchecker($language);
$spell_result = $spellchecker->check($message_body, $isHtml);
if ($error = $spellchecker->error()) {
rcube::raise_error([
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Spellcheck error: " . $error
],
true, false
);
}
else {
$COMPOSE['spell_checked'] = true;
if (!$spell_result) {
if ($isHtml) {
$result['words'] = $spellchecker->get();
$result['dictionary'] = (bool) $rcmail->config->get('spellcheck_dictionary');
}
else {
$result = $spellchecker->get_xml();
}
$rcmail->output->show_message('mispellingsfound', 'error');
$rcmail->output->command('spellcheck_resume', $result);
$rcmail->output->send('iframe');
}
}
}
// generic footer for all messages
if ($footer = $SENDMAIL->generic_message_footer($isHtml)) {
$message_body .= "\r\n" . $footer;
}
}
if ($isHtml) {
$message_body .= "\r\n</body></html>\r\n";
}
// sort attachments to make sure the order is the same as in the UI (#1488423)
if ($files = rcube_utils::get_input_string('_attachments', rcube_utils::INPUT_POST)) {
$files = explode(',', $files);
$files = array_flip($files);
foreach ($files as $idx => $val) {
if (!empty($COMPOSE['attachments'][$idx])) {
$files[$idx] = $COMPOSE['attachments'][$idx];
unset($COMPOSE['attachments'][$idx]);
}
}
$COMPOSE['attachments'] = array_merge(array_filter($files), (array) $COMPOSE['attachments']);
}
// Since we can handle big messages with disk usage, we need more time to work
@set_time_limit(360);
// create PEAR::Mail_mime instance, set headers, body and params
$MAIL_MIME = $SENDMAIL->create_message($headers, $message_body, $isHtml, $COMPOSE['attachments']);
// add stored attachments, if any
if (is_array($COMPOSE['attachments'])) {
self::add_attachments($SENDMAIL, $MAIL_MIME, $COMPOSE['attachments'], $isHtml);
}
// compose PGP/Mime message
if (!empty($pgp_mime)) {
$MAIL_MIME->addAttachment(new Mail_mimePart('Version: 1', [
'content_type' => 'application/pgp-encrypted',
'description' => 'PGP/MIME version identification',
]));
$MAIL_MIME->addAttachment(new Mail_mimePart($pgp_mime, [
'content_type' => 'application/octet-stream',
'filename' => 'encrypted.asc',
'disposition' => 'inline',
]));
$MAIL_MIME->setContentType('multipart/encrypted', ['protocol' => 'application/pgp-encrypted']);
$MAIL_MIME->setParam('preamble', 'This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)');
}
// This hook allows to modify the message before send or save action
$plugin = $rcmail->plugins->exec_hook('message_ready', ['message' => $MAIL_MIME]);
$MAIL_MIME = $plugin['message'];
// Deliver the message over SMTP
if (!$savedraft && !$saveonly) {
$sent = $SENDMAIL->deliver_message($MAIL_MIME);
}
// Save the message in Drafts/Sent
$saved = $SENDMAIL->save_message($MAIL_MIME);
// raise error if saving failed
if (!$saved && $savedraft) {
self::display_server_error('errorsaving');
// start the auto-save timer again
$rcmail->output->command('auto_save_start');
$rcmail->output->send('iframe');
}
$store_target = $SENDMAIL->options['store_target'];
$store_folder = $SENDMAIL->options['store_folder'];
// delete previous saved draft
$drafts_mbox = $rcmail->config->get('drafts_mbox');
$old_id = rcube_utils::get_input_string('_draft_saveid', rcube_utils::INPUT_POST);
if ($old_id && (!empty($sent) || $saved)) {
$deleted = $rcmail->storage->delete_message($old_id, $drafts_mbox);
// raise error if deletion of old draft failed
if (!$deleted) {
rcube::raise_error([
'code' => 800,
'type' => 'imap',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not delete message from $drafts_mbox"
],
true, false
);
}
}
if ($savedraft) {
// remember new draft-uid ($saved could be an UID or true/false here)
if ($saved && is_bool($saved)) {
$index = $rcmail->storage->search_once($drafts_mbox, 'HEADER Message-ID ' . $message_id);
$saved = $index->max();
}
if ($saved) {
$plugin = $rcmail->plugins->exec_hook('message_draftsaved', [
'msgid' => $message_id,
'uid' => $saved,
'folder' => $store_target
]);
// display success
$rcmail->output->show_message(!empty($plugin['message']) ? $plugin['message'] : 'messagesaved', 'confirmation');
// update "_draft_saveid" and the "cmp_hash" to prevent "Unsaved changes" warning
$COMPOSE['param']['draft_uid'] = $plugin['uid'];
$rcmail->output->command('set_draft_id', $plugin['uid']);
$rcmail->output->command('compose_field_hash', true);
}
// start the auto-save timer again
$rcmail->output->command('auto_save_start');
}
else {
// Collect folders which could contain the composed message,
// we'll refresh the list if currently opened folder is one of them (#1490238)
$folders = [];
$save_error = false;
if (!$saveonly) {
if (in_array($COMPOSE['mode'], ['reply', 'forward', 'draft'])) {
$folders[] = $COMPOSE['mailbox'];
}
if (!empty($COMPOSE['param']['draft_uid']) && $drafts_mbox) {
$folders[] = $drafts_mbox;
}
}
if ($store_folder && !$saved) {
$params = $saveonly ? null : ['prefix' => true];
self::display_server_error('errorsavingsent', null, null, $params);
if ($saveonly) {
$rcmail->output->send('iframe');
}
$save_error = true;
}
else {
$rcmail->plugins->exec_hook('attachments_cleanup', ['group' => $COMPOSE_ID]);
$rcmail->session->remove('compose_data_' . $COMPOSE_ID);
$_SESSION['last_compose_session'] = $COMPOSE_ID;
$rcmail->output->command('remove_compose_data', $COMPOSE_ID);
if ($store_folder) {
$folders[] = $store_target;
}
}
$msg = $rcmail->gettext($saveonly ? 'successfullysaved' : 'messagesent');
$rcmail->output->command('sent_successfully', 'confirmation', $msg, $folders, $save_error);
}
$rcmail->output->send('iframe');
}
public static function add_attachments($SENDMAIL, $message, $attachments, $isHtml)
{
$rcmail = rcmail::get_instance();
foreach ($attachments as $id => $attachment) {
// This hook retrieves the attachment contents from the file storage backend
$attachment = $rcmail->plugins->exec_hook('attachment_get', $attachment);
$is_inline = false;
$dispurl = null;
if ($isHtml) {
$dispurl = '/[\'"]\S+display-attachment\S+file=rcmfile' . preg_quote($attachment['id']) . '[\'"]/';
$message_body = $message->getHTMLBody();
$is_inline = preg_match($dispurl, $message_body);
}
$ctype = isset($attachment['mimetype']) ? $attachment['mimetype'] : '';
$ctype = str_replace('image/pjpeg', 'image/jpeg', $ctype); // #1484914
// inline image
if ($is_inline) {
// Mail_Mime does not support many inline attachments with the same name (#1489406)
// we'll generate cid: urls here to workaround this
$cid = preg_replace('/[^0-9a-zA-Z]/', '', uniqid(time(), true));
if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $SENDMAIL->options['from'], $matches)) {
$cid .= $matches[1];
}
else {
$cid .= '@localhost';
}
if ($dispurl && !empty($message_body)) {
$message_body = preg_replace($dispurl, '"cid:' . $cid . '"', $message_body);
rcube_utils::preg_error([
'line' => __LINE__,
'file' => __FILE__,
'message' => "Could not replace an image reference!"
], true
);
$message->setHTMLBody($message_body);
}
if (!empty($attachment['data'])) {
$message->addHTMLImage($attachment['data'], $ctype, $attachment['name'], false, $cid);
}
else {
$message->addHTMLImage($attachment['path'], $ctype, $attachment['name'], true, $cid);
}
}
else {
$file = !empty($attachment['data']) ? $attachment['data'] : $attachment['path'];
$folding = (int) $rcmail->config->get('mime_param_folding');
$message->addAttachment($file,
$ctype,
$attachment['name'],
empty($attachment['data']),
$ctype == 'message/rfc822' ? '8bit' : 'base64',
'attachment',
$attachment['charset'] ?? null,
'', '',
$folding ? 'quoted-printable' : null,
$folding == 2 ? 'quoted-printable' : null,
'', RCUBE_CHARSET
);
}
}
}
}
+150
View File
@@ -0,0 +1,150 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Send a message disposition notification for a specific mail |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_sendmdn extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
if ($uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_POST)) {
$sent = self::send_mdn($uid, $smtp_error);
}
// show either confirm or error message
if (!empty($sent)) {
$rcmail->output->set_env('mdn_request', false);
$rcmail->output->show_message('receiptsent', 'confirmation');
}
else if (!empty($smtp_error) && is_string($smtp_error)) {
$rcmail->output->show_message($smtp_error, 'error');
}
else if (!empty($smtp_error) && !empty($smtp_error['label'])) {
$rcmail->output->show_message($smtp_error['label'], 'error', $smtp_error['vars']);
}
else {
$rcmail->output->show_message('errorsendingreceipt', 'error');
}
// Redirect to 'addcontact' action to save the sender address
if (!empty($_POST['_save'])) {
if ($_POST['_save'] == 5) {
$_POST['_source'] = rcube_addressbook::TYPE_TRUSTED_SENDER;
}
$rcmail->action = 'addcontact';
return;
}
$rcmail->output->send();
}
/**
* Send the MDN response
*
* @param mixed $message Original message object (rcube_message) or UID
* @param array|string $smtp_error SMTP error array or (deprecated) string
*
* @return boolean Send status
*/
public static function send_mdn($message, &$smtp_error)
{
$rcmail = rcmail::get_instance();
if (!is_object($message) || !is_a($message, 'rcube_message')) {
$message = new rcube_message($message);
}
if ($message->headers->mdn_to && empty($message->headers->flags['MDNSENT']) &&
($rcmail->storage->check_permflag('MDNSENT') || $rcmail->storage->check_permflag('*'))
) {
$charset = $message->headers->charset;
$identity = rcmail_sendmail::identity_select($message);
$sender = format_email_recipient($identity['email'], $identity['name']);
$recipient = array_first(rcube_mime::decode_address_list($message->headers->mdn_to, 1, true, $charset));
$mailto = $recipient['mailto'];
$compose = new Mail_mime("\r\n");
$compose->setParam('text_encoding', 'quoted-printable');
$compose->setParam('html_encoding', 'quoted-printable');
$compose->setParam('head_encoding', 'quoted-printable');
$compose->setParam('head_charset', RCUBE_CHARSET);
$compose->setParam('html_charset', RCUBE_CHARSET);
$compose->setParam('text_charset', RCUBE_CHARSET);
// compose headers array
$headers = [
'Date' => $rcmail->user_date(),
'From' => $sender,
'To' => $message->headers->mdn_to,
'Subject' => $rcmail->gettext('receiptread') . ': ' . $message->subject,
'Message-ID' => $rcmail->gen_message_id($identity['email']),
'X-Sender' => $identity['email'],
'References' => trim($message->headers->references . ' ' . $message->headers->messageID),
'In-Reply-To' => $message->headers->messageID,
];
$report = "Final-Recipient: rfc822; {$identity['email']}\r\n"
. "Original-Message-ID: {$message->headers->messageID}\r\n"
. "Disposition: manual-action/MDN-sent-manually; displayed\r\n";
if ($message->headers->to) {
$report .= "Original-Recipient: {$message->headers->to}\r\n";
}
if ($agent = $rcmail->config->get('useragent')) {
$headers['User-Agent'] = $agent;
$report .= "Reporting-UA: $agent\r\n";
}
$to = rcube_mime::decode_mime_string($message->headers->to, $charset);
$date = $rcmail->format_date($message->headers->date, $rcmail->config->get('date_long'));
$body = $rcmail->gettext("yourmessage") . "\r\n\r\n" .
"\t" . $rcmail->gettext("to") . ": {$to}\r\n" .
"\t" . $rcmail->gettext("subject") . ": {$message->subject}\r\n" .
"\t" . $rcmail->gettext("date") . ": {$date}\r\n" .
"\r\n" . $rcmail->gettext("receiptnote");
$compose->headers(array_filter($headers));
$compose->setContentType('multipart/report', ['report-type'=> 'disposition-notification']);
$compose->setTXTBody(rcube_mime::wordwrap($body, 75, "\r\n"));
$compose->addAttachment($report, 'message/disposition-notification', 'MDNPart2.txt', false, '7bit', 'inline');
// SMTP options
$options = ['mdn_use_from' => (bool) $rcmail->config->get('mdn_use_from')];
$sent = $rcmail->deliver_message($compose, $identity['email'], $mailto, $smtp_error, $body_file, $options, true);
if ($sent) {
$rcmail->storage->set_flag($message->uid, 'MDNSENT');
return true;
}
}
return false;
}
}
+953
View File
@@ -0,0 +1,953 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display a mail message similar as a usual mail application does |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_show extends rcmail_action_mail_index
{
protected static $MESSAGE;
protected static $CLIENT_MIMETYPES = [];
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
self::$PRINT_MODE = $rcmail->action == 'print';
// Read browser capabilities and store them in session
if ($caps = rcube_utils::get_input_string('_caps', rcube_utils::INPUT_GET)) {
$browser_caps = [];
foreach (explode(',', $caps) as $cap) {
$cap = explode('=', $cap);
$browser_caps[$cap[0]] = $cap[1];
}
$_SESSION['browser_caps'] = $browser_caps;
}
$msg_id = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET);
$uid = preg_replace('/\.[0-9.]+$/', '', $msg_id);
$mbox_name = $rcmail->storage->get_folder();
// similar code as in program/steps/mail/get.inc
if ($uid) {
// set message format (need to be done before rcube_message construction)
if (!empty($_GET['_format'])) {
$prefer_html = $_GET['_format'] == 'html';
$rcmail->config->set('prefer_html', $prefer_html);
$_SESSION['msg_formats'][$mbox_name.':'.$uid] = $prefer_html;
}
else if (isset($_SESSION['msg_formats'][$mbox_name.':'.$uid])) {
$rcmail->config->set('prefer_html', $_SESSION['msg_formats'][$mbox_name.':'.$uid]);
}
$MESSAGE = new rcube_message($msg_id, $mbox_name, !empty($_GET['_safe']));
self::$MESSAGE = $MESSAGE;
// if message not found (wrong UID)...
if (empty($MESSAGE->headers)) {
self::message_error();
}
self::$CLIENT_MIMETYPES = self::supported_mimetypes();
// show images?
self::check_safe($MESSAGE);
// set message charset as default
if (!empty($MESSAGE->headers->charset)) {
$rcmail->storage->set_charset($MESSAGE->headers->charset);
}
if (!isset($_SESSION['writeable_abook'])) {
$_SESSION['writeable_abook'] = $rcmail->get_address_sources(true) ? true : false;
}
$rcmail->output->set_pagetitle(abbreviate_string($MESSAGE->subject, 128, '...', true));
// set environment
$rcmail->output->set_env('uid', $msg_id);
$rcmail->output->set_env('safemode', $MESSAGE->is_safe);
$rcmail->output->set_env('message_context', $MESSAGE->context);
$rcmail->output->set_env('message_flags', array_keys(array_change_key_case((array) $MESSAGE->headers->flags)));
$rcmail->output->set_env('sender', !empty($MESSAGE->sender) ? $MESSAGE->sender['string'] : '');
$rcmail->output->set_env('mailbox', $mbox_name);
$rcmail->output->set_env('username', $rcmail->get_user_name());
$rcmail->output->set_env('permaurl', $rcmail->url(['_action' => 'show', '_uid' => $msg_id, '_mbox' => $mbox_name]));
$rcmail->output->set_env('has_writeable_addressbook', $_SESSION['writeable_abook']);
$rcmail->output->set_env('delimiter', $rcmail->storage->get_hierarchy_delimiter());
$rcmail->output->set_env('mimetypes', self::$CLIENT_MIMETYPES);
if ($MESSAGE->headers->get('list-post', false)) {
$rcmail->output->set_env('list_post', true);
}
// set configuration
self::set_env_config(['delete_junk', 'flag_for_deletion', 'read_when_deleted',
'skip_deleted', 'display_next', 'forward_attachment', 'mailvelope_main_keyring']);
// set special folders
foreach (['drafts', 'trash', 'junk'] as $mbox) {
if ($folder = $rcmail->config->get($mbox . '_mbox')) {
$rcmail->output->set_env($mbox . '_mailbox', $folder);
}
}
if ($MESSAGE->has_html_part()) {
$prefer_html = $rcmail->config->get('prefer_html');
$rcmail->output->set_env('optional_format', $prefer_html ? 'text' : 'html');
}
$rcmail->output->add_label('checkingmail', 'deletemessage', 'movemessagetotrash',
'movingmessage', 'deletingmessage', 'markingmessage', 'replyall', 'replylist',
'bounce', 'bouncemsg', 'sendingmessage');
// check for unset disposition notification
self::mdn_request_handler($MESSAGE);
if (empty($MESSAGE->headers->flags['SEEN']) && $MESSAGE->context === null) {
$v = intval($rcmail->config->get('mail_read_time'));
if ($v > 0) {
$rcmail->output->set_env('mail_read_time', $v);
}
else if ($v == 0) {
$rcmail->output->command('set_unread_message', $MESSAGE->uid, $mbox_name);
$rcmail->plugins->exec_hook('message_read', [
'uid' => $MESSAGE->uid,
'mailbox' => $mbox_name,
'message' => $MESSAGE,
]);
$set_seen_flag = true;
}
}
}
$rcmail->output->add_handlers([
'mailboxname' => [$this, 'mailbox_name_display'],
'messageattachments' => [$this, 'message_attachments'],
'messageobjects' => [$this, 'message_objects'],
'messagesummary' => [$this, 'message_summary'],
'messageheaders' => [$this, 'message_headers'],
'messagefullheaders' => [$this, 'message_full_headers'],
'messagebody' => [$this, 'message_body'],
'contactphoto' => [$this, 'message_contactphoto'],
]);
if ($rcmail->action == 'print' && $rcmail->output->template_exists('messageprint')) {
$rcmail->output->send('messageprint', false);
}
else if ($rcmail->action == 'preview' && $rcmail->output->template_exists('messagepreview')) {
$rcmail->output->send('messagepreview', false);
}
else {
$rcmail->output->send('message', false);
}
// mark message as read
if (!empty($set_seen_flag)) {
if ($rcmail->storage->set_flag(self::$MESSAGE->uid, 'SEEN', $mbox_name)) {
if ($count = self::get_unseen_count($mbox_name)) {
self::set_unseen_count($mbox_name, $count - 1);
}
}
}
exit;
}
/**
* Handler for the template object 'messageattachments'.
*
* @param array $attrib Named parameters
*
* @return string HTML content showing the message attachments list
*/
public static function message_attachments($attrib)
{
if (empty(self::$MESSAGE->attachments)) {
return '';
}
$rcmail = rcmail::get_instance();
$out =
$ol = '';
$attachments = [];
foreach (self::$MESSAGE->attachments as $attach_prop) {
$filename = self::attachment_name($attach_prop, true);
$filesize = self::message_part_size($attach_prop);
$mimetype = rcube_mime::fix_mimetype($attach_prop->mimetype);
$class = rcube_utils::file2class($mimetype, $filename);
$id = 'attach' . $attach_prop->mime_id;
if ($mimetype == 'application/octet-stream' && ($type = rcube_mime::file_ext_type($filename))) {
$mimetype = $type;
}
// Skip inline images
if (strpos($mimetype, 'image/') === 0 && !self::is_attachment(self::$MESSAGE, $attach_prop)) {
continue;
}
if (!empty($attrib['maxlength']) && mb_strlen($filename) > $attrib['maxlength']) {
$title = $filename;
$filename = abbreviate_string($filename, $attrib['maxlength']);
}
else {
$title = '';
}
$item = html::span('attachment-name', rcube::Q($filename))
. html::span('attachment-size', '(' . rcube::Q($filesize) . ')');
$li_class = $class;
if (!self::$PRINT_MODE) {
$link_attrs = [
'href' => self::$MESSAGE->get_part_url($attach_prop->mime_id, false),
'onclick' => sprintf('%s.command(\'load-attachment\',\'%s\',this); return false',
rcmail_output::JS_OBJECT_NAME, $attach_prop->mime_id),
'onmouseover' => $title ? '' : 'rcube_webmail.long_subject_title_ex(this, 0)',
'title' => $title,
'class' => 'filename',
];
if ($mimetype != 'message/rfc822' && empty($attach_prop->size)) {
$li_class .= ' no-menu';
$link_attrs['onclick'] = sprintf('%s.alert_dialog(%s.get_label(\'emptyattachment\')); return false',
rcmail_output::JS_OBJECT_NAME, rcmail_output::JS_OBJECT_NAME);
$rcmail->output->add_label('emptyattachment');
}
$item = html::a($link_attrs, $item);
$attachments[$attach_prop->mime_id] = $mimetype;
}
$ol .= html::tag('li', ['class' => $li_class, 'id' => $id], $item);
}
$out = html::tag('ul', $attrib, $ol, html::$common_attrib);
$rcmail->output->set_env('attachments', $attachments);
$rcmail->output->add_gui_object('attachments', $attrib['id']);
return $out;
}
public static function remote_objects_msg()
{
$rcmail = rcmail::get_instance();
$attrib['id'] = 'remote-objects-message';
$attrib['class'] = 'notice';
$attrib['style'] = 'display: none';
$msg = html::span(null, rcube::Q($rcmail->gettext('blockedresources')));
$buttons = html::a([
'href' => "#loadremote",
'onclick' => rcmail_output::JS_OBJECT_NAME . ".command('load-remote')"
],
rcube::Q($rcmail->gettext('allow'))
);
// add link to save sender in addressbook and reload message
$show_images = $rcmail->config->get('show_images');
if (!empty(self::$MESSAGE->sender['mailto']) && ($show_images == 1 || $show_images == 3)) {
$arg = $show_images == 3 ? rcube_addressbook::TYPE_TRUSTED_SENDER : 'true';
$buttons .= ' ' . html::a([
'href' => "#loadremotealways",
'onclick' => rcmail_output::JS_OBJECT_NAME . ".command('load-remote', $arg)",
'style' => "white-space:nowrap"
],
rcube::Q($rcmail->gettext(['name' => 'alwaysallow', 'vars' => ['sender' => self::$MESSAGE->sender['mailto']]]))
);
}
$rcmail->output->add_gui_object('remoteobjectsmsg', $attrib['id']);
return html::div($attrib, $msg . '&nbsp;' . html::span('boxbuttons', $buttons));
}
/**
* Display a warning whenever a suspicious email address has been found in the message.
*
* @return string HTML content of the warning element
*/
public static function suspicious_content_warning()
{
if (empty(self::$SUSPICIOUS_EMAIL)) {
return '';
}
$rcmail = rcmail::get_instance();
$attrib = [
'id' => 'suspicious-content-message',
'class' => 'notice',
];
$msg = html::span(null, rcube::Q($rcmail->gettext('suspiciousemail')));
return html::div($attrib, $msg);
}
public static function message_buttons()
{
$rcmail = rcmail::get_instance();
$delim = $rcmail->storage->get_hierarchy_delimiter();
$dbox = $rcmail->config->get('drafts_mbox');
// the message is not a draft
if (!empty(self::$MESSAGE->context)
|| (
!empty(self::$MESSAGE->folder)
&& (self::$MESSAGE->folder != $dbox && strpos(self::$MESSAGE->folder, $dbox.$delim) !== 0)
)
) {
return '';
}
$attrib['id'] = 'message-buttons';
$attrib['class'] = 'information notice';
$msg = html::span(null, rcube::Q($rcmail->gettext('isdraft')))
. '&nbsp;'
. html::a([
'href' => "#edit",
'onclick' => rcmail_output::JS_OBJECT_NAME.".command('edit')"
],
rcube::Q($rcmail->gettext('edit'))
);
return html::div($attrib, $msg);
}
/**
* Handler for the template object 'messageobjects' that contains
* warning/info boxes, buttons, etc. related to the displayed message.
*
* @param array $attrib Named parameters
*
* @return string HTML content showing the message objects
*/
public static function message_objects($attrib)
{
if (empty($attrib['id'])) {
$attrib['id'] = 'message-objects';
}
$rcmail = rcmail::get_instance();
$content = [
self::message_buttons(),
self::remote_objects_msg(),
self::suspicious_content_warning(),
];
$plugin = $rcmail->plugins->exec_hook('message_objects',
['content' => $content, 'message' => self::$MESSAGE]);
$content = implode("\n", $plugin['content']);
return html::div($attrib, $content);
}
/**
* Handler for the template object 'contactphoto'.
*
* @param array $attrib Named parameters
*
* @return string HTML content for the IMG tag
*/
public static function message_contactphoto($attrib)
{
$rcmail = rcmail::get_instance();
$error_handler = false;
$placeholder = 'data:image/gif;base64,' . rcmail_output::BLANK_GIF;
if (!empty($attrib['placeholder'])) {
$placeholder = $rcmail->output->abs_url($attrib['placeholder'], true);
$placeholder = $rcmail->output->asset_url($placeholder);
// set error handler on <img>
$error_handler = true;
$attrib['onerror'] = "this.onerror = null; this.src = '$placeholder';";
}
if (!empty(self::$MESSAGE->sender)) {
$photo_img = $rcmail->url([
'_task' => 'addressbook',
'_action' => 'photo',
'_email' => self::$MESSAGE->sender['mailto'],
'_error' => $error_handler ? 1 : null,
'_bgcolor' => $attrib['bg-color'] ?? null
]);
}
else {
$photo_img = $placeholder;
}
return html::img(['src' => $photo_img, 'alt' => $rcmail->gettext('contactphoto')] + $attrib);
}
/**
* Returns table with message headers
*/
public static function message_headers($attrib, $headers = null)
{
static $sa_attrib;
// keep header table attrib
if (is_array($attrib) && !$sa_attrib && empty($attrib['valueof'])) {
$sa_attrib = $attrib;
}
else if (!is_array($attrib) && is_array($sa_attrib)) {
$attrib = $sa_attrib;
}
if (!isset(self::$MESSAGE)) {
return false;
}
$rcmail = rcmail::get_instance();
// get associative array of headers object
if (!$headers) {
$headers_obj = self::$MESSAGE->headers;
$headers = get_object_vars(self::$MESSAGE->headers);
}
else if (is_object($headers)) {
$headers_obj = $headers;
$headers = get_object_vars($headers_obj);
}
else {
$headers_obj = rcube_message_header::from_array($headers);
}
// show these headers
$standard_headers = ['subject', 'from', 'sender', 'to', 'cc', 'bcc', 'replyto',
'mail-reply-to', 'mail-followup-to', 'date', 'priority'];
$exclude_headers = !empty($attrib['exclude']) ? explode(',', $attrib['exclude']) : [];
$output_headers = [];
$attr_max = $attrib['max'] ?? null;
$attr_addicon = $attrib['addicon'] ?? null;
$charset = !empty($headers['charset']) ? $headers['charset'] : null;
foreach ($standard_headers as $hkey) {
$value = null;
if (!empty($headers[$hkey])) {
$value = $headers[$hkey];
}
else if (!empty($headers['others'][$hkey])) {
$value = $headers['others'][$hkey];
}
else if (empty($attrib['valueof'])) {
continue;
}
if (in_array($hkey, $exclude_headers)) {
continue;
}
$ishtml = false;
$header_title = $rcmail->gettext(preg_replace('/(^mail-|-)/', '', $hkey));
$header_value = null;
if ($hkey == 'date') {
$header_value = $rcmail->format_date($value,
self::$PRINT_MODE ? $rcmail->config->get('date_long', 'x') : null);
}
else if ($hkey == 'priority') {
$header_value = html::span('prio' . $value, rcube::Q(self::localized_priority($value)));
$ishtml = true;
}
else if ($hkey == 'replyto') {
if ($value != $headers['from']) {
$header_value = self::address_string($value, $attr_max, true, $attr_addicon, $charset, $header_title);
$ishtml = true;
}
}
else if ($hkey == 'mail-reply-to') {
if ((!isset($headers['replyto']) || $value != $headers['replyto']) && $value != $headers['from']) {
$header_value = self::address_string($value, $attr_max, true, $attr_addicon, $charset, $header_title);
$ishtml = true;
}
}
else if ($hkey == 'sender') {
if ($value != $headers['from']) {
$header_value = self::address_string($value, $attr_max, true, $attr_addicon, $charset, $header_title);
$ishtml = true;
}
}
else if ($hkey == 'mail-followup-to') {
$header_value = self::address_string($value, $attr_max, true, $attr_addicon, $charset, $header_title);
$ishtml = true;
}
else if (in_array($hkey, ['from', 'to', 'cc', 'bcc'])) {
$header_value = self::address_string($value, $attr_max, true, $attr_addicon, $charset, $header_title);
$ishtml = true;
}
else if ($hkey == 'subject' && empty($value)) {
$header_value = $rcmail->gettext('nosubject');
}
else {
$value = is_array($value) ? implode(' ', $value) : $value;
$header_value = trim(rcube_mime::decode_header($value, $charset));
}
if (empty($header_value)) {
continue;
}
$output_headers[$hkey] = [
'title' => $header_title,
'value' => $header_value,
'raw' => $value,
'html' => $ishtml,
];
}
$plugin = $rcmail->plugins->exec_hook('message_headers_output', [
'output' => $output_headers,
'headers' => $headers_obj,
'exclude' => $exclude_headers, // readonly
'folder' => self::$MESSAGE->folder, // readonly
'uid' => self::$MESSAGE->uid, // readonly
]);
// single header value is requested
if (!empty($attrib['valueof'])) {
if (empty($plugin['output'][$attrib['valueof']])) {
return '';
}
$row = $plugin['output'][$attrib['valueof']];
return !empty($row['html']) ? $row['value'] : rcube::SQ($row['value']);
}
// compose html table
$table = new html_table(['cols' => 2]);
foreach ($plugin['output'] as $hkey => $row) {
$val = !empty($row['html']) ? $row['value'] : rcube::SQ($row['value']);
$table->add(['class' => 'header-title'], rcube::SQ($row['title']));
$table->add(['class' => 'header ' . $hkey], $val);
}
return $table->show($attrib);
}
/**
* Returns element with "From|To <sender|recipient> on <date>"
*/
public static function message_summary($attrib)
{
if (!isset(self::$MESSAGE) || empty(self::$MESSAGE->headers)) {
return;
}
$rcmail = rcmail::get_instance();
$header = self::$MESSAGE->context ? 'from' : self::message_list_smart_column_name();
$label = 'shortheader' . $header;
$date = $rcmail->format_date(self::$MESSAGE->headers->date, $rcmail->config->get('date_long', 'x'));
$user = self::$MESSAGE->headers->$header;
if (!$user && $header == 'to' && !empty(self::$MESSAGE->headers->cc)) {
$user = self::$MESSAGE->headers->cc;
}
if (!$user && $header == 'to' && !empty(self::$MESSAGE->headers->bcc)) {
$user = self::$MESSAGE->headers->bcc;
}
$vars[$header] = self::address_string($user, 1, true, $attrib['addicon'], self::$MESSAGE->headers->charset);
$vars['date'] = html::span('text-nowrap', $date);
if (empty($user)) {
$label = 'shortheaderdate';
}
$out = html::span(null, $rcmail->gettext(['name' => $label, 'vars' => $vars]));
return html::div($attrib, $out);
}
/**
* Convert Priority header value into a localized string
*/
public static function localized_priority($value)
{
$labels_map = [
'1' => 'highest',
'2' => 'high',
'3' => 'normal',
'4' => 'low',
'5' => 'lowest',
];
if ($value && !empty($labels_map[$value])) {
return rcmail::get_instance()->gettext($labels_map[$value]);
}
return '';
}
/**
* Returns block to show full message headers
*/
public static function message_full_headers($attrib)
{
$rcmail = rcmail::get_instance();
$html = html::div(['id' => "all-headers", 'class' => "all", 'style' => 'display:none'],
html::div(['id' => 'headers-source'], ''));
$html .= html::div([
'class' => "more-headers show-headers",
'onclick' => "return ".rcmail_output::JS_OBJECT_NAME.".command('show-headers','',this)",
'title' => $rcmail->gettext('togglefullheaders')
], '');
$rcmail->output->add_gui_object('all_headers_row', 'all-headers');
$rcmail->output->add_gui_object('all_headers_box', 'headers-source');
return html::div($attrib, $html);
}
/**
* Handler for the 'messagebody' GUI object
*
* @param array $attrib Named parameters
*
* @return string HTML content showing the message body
*/
public static function message_body($attrib)
{
if (
empty(self::$MESSAGE)
|| (!is_array(self::$MESSAGE->parts) && empty(self::$MESSAGE->body))
) {
return '';
}
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmailMsgBody';
}
$rcmail = rcmail::get_instance();
$safe_mode = self::$MESSAGE->is_safe || !empty($_GET['_safe']);
$out = '';
$part_no = 0;
$header_attrib = [];
foreach ($attrib as $attr => $value) {
if (preg_match('/^headertable([a-z]+)$/i', $attr, $regs)) {
$header_attrib[$regs[1]] = $value;
}
}
if (!empty(self::$MESSAGE->parts)) {
foreach (self::$MESSAGE->parts as $part) {
if ($part->type == 'headers') {
$out .= html::div('message-partheaders', self::message_headers(count($header_attrib) ? $header_attrib : null, $part->headers));
}
else if ($part->type == 'content') {
// unsupported (e.g. encrypted)
if (!empty($part->realtype)) {
if ($part->realtype == 'multipart/encrypted' || $part->realtype == 'application/pkcs7-mime') {
if (
!empty($_SESSION['browser_caps']['pgpmime'])
&& ($pgp_mime_part = self::$MESSAGE->get_multipart_encrypted_part())
) {
$out .= html::span('part-notice', $rcmail->gettext('externalmessagedecryption'));
$rcmail->output->set_env('pgp_mime_part', $pgp_mime_part->mime_id);
$rcmail->output->set_env('pgp_mime_container', '#' . $attrib['id']);
$rcmail->output->add_label('loadingdata');
}
if (!self::$MESSAGE->encrypted_part) {
$out .= html::span('part-notice', $rcmail->gettext('encryptedmessage'));
}
}
continue;
}
else if (!$part->size) {
continue;
}
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
else if (!rcube_utils::mem_check($part->size * 10)) {
$out .= self::part_too_big_message(self::$MESSAGE, $part->mime_id);
continue;
}
// fetch part body
$body = self::$MESSAGE->get_part_body($part->mime_id, true);
// message is cached but not exists (#1485443), or other error
if ($body === false) {
// Don't bail out if it is only one-of-many part of the message (#6854)
if (strlen($out)) {
$out .= html::span('part-notice', $rcmail->gettext('messageopenerror'));
continue;
}
self::message_error();
}
$plugin = $rcmail->plugins->exec_hook('message_body_prefix',
['part' => $part, 'prefix' => '', 'message' => self::$MESSAGE]);
// Set attributes of the part container
$container_class = $part->ctype_secondary == 'html' ? 'message-htmlpart' : 'message-part';
$container_id = $container_class . (++$part_no);
$container_attrib = ['class' => $container_class, 'id' => $container_id];
$body_args = [
'safe' => $safe_mode,
'plain' => !$rcmail->config->get('prefer_html'),
'css_prefix' => 'v' . $part_no,
'body_class' => 'rcmBody',
'container_id' => $container_id,
'container_attrib' => $container_attrib,
];
// Parse the part content for display
$body = self::print_body($body, $part, $body_args);
// check if the message body is PGP encrypted
if (strpos($body, '-----BEGIN PGP MESSAGE-----') !== false) {
$rcmail->output->set_env('is_pgp_content', '#' . $container_id);
}
if ($part->ctype_secondary == 'html') {
$body = self::html4inline($body, $body_args);
}
$out .= html::div($body_args['container_attrib'], $plugin['prefix'] . $body);
}
}
}
else {
// Check if we have enough memory to handle the message in it
// #1487424: we need up to 10x more memory than the body
if (isset(self::$MESSAGE->body) && !rcube_utils::mem_check(strlen(self::$MESSAGE->body) * 10)) {
$out .= self::part_too_big_message(self::$MESSAGE, 0);
}
else {
$plugin = $rcmail->plugins->exec_hook('message_body_prefix',
['part' => self::$MESSAGE, 'prefix' => '']);
$out .= html::div('message-part',
$plugin['prefix'] . self::plain_body(self::$MESSAGE->body));
}
}
// list images after mail body
if ($rcmail->config->get('inline_images', true) && !empty(self::$MESSAGE->attachments)) {
$thumbnail_size = $rcmail->config->get('image_thumbnail_size', 240);
$show_label = rcube::Q($rcmail->gettext('showattachment'));
$download_label = rcube::Q($rcmail->gettext('download'));
foreach (self::$MESSAGE->attachments as $attach_prop) {
// Content-Type: image/*...
if ($mimetype = self::part_image_type($attach_prop)) {
// Skip inline images
if (!self::is_attachment(self::$MESSAGE, $attach_prop)) {
continue;
}
// display thumbnails
if ($thumbnail_size) {
$supported = in_array($mimetype, self::$CLIENT_MIMETYPES);
$show_link_attr = [
'href' => self::$MESSAGE->get_part_url($attach_prop->mime_id, false),
'onclick' => sprintf(
'%s.command(\'load-attachment\',\'%s\',this); return false',
rcmail_output::JS_OBJECT_NAME,
$attach_prop->mime_id
)
];
$download_link_attr = [
'href' => $show_link_attr['href'] . '&_download=1',
];
$show_link = html::a($show_link_attr + ['class' => 'open'], $show_label);
$download_link = html::a($download_link_attr + ['class' => 'download'], $download_label);
$out .= html::p(['class' => 'image-attachment', 'style' => $supported ? '' : 'display:none'],
html::a($show_link_attr + ['class' => 'image-link', 'style' => sprintf('width:%dpx', $thumbnail_size)],
html::img([
'class' => 'image-thumbnail',
'src' => self::$MESSAGE->get_part_url($attach_prop->mime_id, 'image') . '&_thumb=1',
'title' => $attach_prop->filename,
'alt' => $attach_prop->filename,
'style' => sprintf('max-width:%dpx; max-height:%dpx', $thumbnail_size, $thumbnail_size),
'onload' => $supported ? '' : '$(this).parents(\'p.image-attachment\').show()',
])
) .
html::span('image-filename', rcube::Q($attach_prop->filename)) .
html::span('image-filesize', rcube::Q(self::message_part_size($attach_prop))) .
html::span('attachment-links', ($supported ? $show_link . '&nbsp;' : '') . $download_link) .
html::br(['style' => 'clear:both'])
);
}
else {
$out .= html::tag('fieldset', 'image-attachment',
html::tag('legend', 'image-filename', rcube::Q($attach_prop->filename)) .
html::p(['align' => 'center'],
html::img([
'src' => self::$MESSAGE->get_part_url($attach_prop->mime_id, 'image'),
'title' => $attach_prop->filename,
'alt' => $attach_prop->filename,
])
)
);
}
}
}
}
// tell client that there are blocked remote objects
if (self::$REMOTE_OBJECTS && !$safe_mode) {
$rcmail->output->set_env('blockedobjects', true);
}
$rcmail->output->add_gui_object('messagebody', $attrib['id']);
return html::div($attrib, $out);
}
/**
* Returns a HTML notice element for too big message parts
*
* @param rcube_message $message Email message object
* @param string $part_id Message part identifier
*
* @return string HTML content
*/
public static function part_too_big_message($message, $part_id)
{
$rcmail = rcmail::get_instance();
$token = $rcmail->get_request_token();
$url = $rcmail->url([
'task' => 'mail',
'action' => 'get',
'download' => 1,
'uid' => $message->uid,
'part' => $part_id,
'mbox' => $message->folder,
'token' => $token,
]);
return html::span('part-notice', $rcmail->gettext('messagetoobig')
. '&nbsp;' . html::a($url, $rcmail->gettext('download')));
}
/**
* Handle disposition notification requests
*
* @param rcube_message $message Email message object
*/
public static function mdn_request_handler($message)
{
$rcmail = rcmail::get_instance();
if ($message->headers->mdn_to
&& $message->context === null
&& !empty($message->sender['mailto'])
&& empty($message->headers->flags['MDNSENT'])
&& empty($message->headers->flags['SEEN'])
&& ($rcmail->storage->check_permflag('MDNSENT') || $rcmail->storage->check_permflag('*'))
&& $message->folder != $rcmail->config->get('drafts_mbox')
&& $message->folder != $rcmail->config->get('sent_mbox')
) {
$mdn_cfg = intval($rcmail->config->get('mdn_requests'));
$exists = $mdn_cfg == 1;
// Check sender existence in contacts
// 3 and 4 = my contacts, 5 and 6 = trusted senders
if ($mdn_cfg == 3 || $mdn_cfg == 4 || $mdn_cfg == 5 || $mdn_cfg == 6) {
$type = rcube_addressbook::TYPE_TRUSTED_SENDER;
if ($mdn_cfg == 3 || $mdn_cfg == 4) {
$type |= rcube_addressbook::TYPE_WRITEABLE | rcube_addressbook::TYPE_RECIPIENT;
}
if ($rcmail->contact_exists($message->sender['mailto'], $type)) {
$exists = 1;
}
}
if ($exists) {
// Send MDN
if (rcmail_action_mail_sendmdn::send_mdn($message, $smtp_error)) {
$rcmail->output->show_message('receiptsent', 'confirmation');
}
else if ($smtp_error && is_string($smtp_error)) {
$rcmail->output->show_message($smtp_error, 'error');
}
else if ($smtp_error && !empty($smtp_error['label'])) {
$rcmail->output->show_message($smtp_error['label'], 'error', $smtp_error['vars']);
}
else {
$rcmail->output->show_message('errorsendingreceipt', 'error');
}
}
else if ($mdn_cfg != 2 && $mdn_cfg != 4 && $mdn_cfg != 6) {
// Ask the user
$rcmail->output->add_label('sendreceipt', 'mdnrequest', 'send', 'sendalwaysto', 'ignore');
$rcmail->output->set_env('mdn_request_save', $mdn_cfg == 3 || $mdn_cfg == 5 ? $mdn_cfg : 0);
$rcmail->output->set_env('mdn_request_sender', $message->sender);
$rcmail->output->set_env('mdn_request', true);
}
}
}
/**
* Check whether the message part is a normal attachment
*
* @param rcube_message $message Message object
* @param rcube_message_part $part Message part
*
* @return bool
*/
protected static function is_attachment($message, $part)
{
// Inline attachment with Content-Id specified
if (!empty($part->content_id) && $part->disposition == 'inline') {
return false;
}
// Any image attached to multipart/related message (#7184)
$parent_id = preg_replace('/\.[0-9]+$/', '', $part->mime_id);
$parent = $message->mime_parts[$parent_id] ?? null;
if ($parent && $parent->mimetype == 'multipart/related') {
return false;
}
return true;
}
}
@@ -0,0 +1,98 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display a mail message similar as a usual mail application does |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_mail_viewsource extends rcmail_action
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
if (!empty($_GET['_save'])) {
$rcmail->request_security_check(rcube_utils::INPUT_GET);
}
ob_end_clean();
// similar code as in program/steps/mail/get.inc
if ($uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET)) {
if ($pos = strpos($uid, '.')) {
$message = new rcube_message($uid);
$headers = $message->headers;
$part_id = substr($uid, $pos + 1);
}
else {
$headers = $rcmail->storage->get_message_headers($uid);
}
$charset = $headers->charset ?: $rcmail->config->get('default_charset', RCUBE_CHARSET);
if (!empty($_GET['_save'])) {
$subject = rcube_mime::decode_header($headers->subject, $headers->charset);
$filename = self::filename_from_subject(mb_substr($subject, 0, 128));
$filename = ($filename ?: $uid) . '.eml';
$rcmail->output->download_headers($filename, [
'length' => $headers->size,
'type' => 'text/plain',
'type_charset' => $charset,
]);
}
else {
// Make sure it works in an iframe (#9084)
$rcmail->output->page_headers();
header("Content-Type: text/plain; charset={$charset}");
}
if (isset($part_id) && isset($message)) {
$message->get_part_body($part_id, empty($_GET['_save']), 0, -1);
}
else {
$rcmail->storage->print_raw_body($uid, empty($_GET['_save']));
}
}
else {
rcube::raise_error([
'code' => 500,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Message UID $uid not found"
],
true, true
);
}
exit;
}
/**
* Helper function to convert message subject into filename
*/
public static function filename_from_subject($str)
{
$str = preg_replace('/[:\t\n\r\0\x0B\/]+\s*/', ' ', $str);
return trim($str, " \t\n\r\0\x0B./_");
}
}
@@ -0,0 +1,147 @@
<?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: |
| Display license information about program and enabled plugins |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_about extends rcmail_action
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('about'));
$rcmail->output->add_handlers([
'supportlink' => [$this, 'supportlink'],
'pluginlist' => [$this, 'plugins_list'],
'copyright' => function() {
return 'Copyright &copy; 2005-2022, The Roundcube Dev Team';
},
'license' => function() {
return 'This program is free software; you can redistribute it and/or modify it under the terms '
. 'of the <a href="http://www.gnu.org/licenses/gpl.html" target="_blank">GNU General Public License</a> '
. 'as published by the Free Software Foundation, either version 3 of the License, '
. 'or (at your option) any later version.<br/>'
. 'Some <a href="https://roundcube.net/license" target="_blank">exceptions</a> '
. 'for skins &amp; plugins apply.';
},
]);
$rcmail->output->send('about');
}
public static function supportlink($attrib)
{
$rcmail = rcmail::get_instance();
if ($url = $rcmail->config->get('support_url')) {
$label = !empty($attrib['label']) ? $attrib['label'] : 'support';
$attrib['href'] = $url;
return html::a($attrib, $rcmail->gettext($label));
}
}
public static function plugins_list($attrib)
{
$rcmail = rcmail::get_instance();
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmpluginlist';
}
$plugins = array_filter($rcmail->plugins->active_plugins);
$plugin_info = [];
foreach ($plugins as $name) {
if ($info = $rcmail->plugins->get_info($name)) {
$plugin_info[$name] = $info;
}
}
// load info from required plugins, too
foreach ($plugin_info as $name => $info) {
if (!empty($info['require']) && is_array($info['require'])) {
foreach ($info['require'] as $req_name) {
if (!isset($plugin_info[$req_name]) && ($req_info = $rcmail->plugins->get_info($req_name))) {
$plugin_info[$req_name] = $req_info;
}
}
}
}
if (empty($plugin_info)) {
return '';
}
ksort($plugin_info, SORT_LOCALE_STRING);
$table = new html_table($attrib);
// add table header
$table->add_header('name', $rcmail->gettext('plugin'));
$table->add_header('version', $rcmail->gettext('version'));
$table->add_header('license', $rcmail->gettext('license'));
$table->add_header('source', $rcmail->gettext('source'));
foreach ($plugin_info as $name => $data) {
$uri = !empty($data['src_uri']) ? $data['src_uri'] : ($data['uri'] ?? '');
if ($uri && stripos($uri, 'http') !== 0) {
$uri = 'http://' . $uri;
}
if ($uri) {
$uri = html::a([
'target' => '_blank',
'href' => rcube::Q($uri)
],
rcube::Q($rcmail->gettext('download'))
);
}
$license = isset($data['license']) ? $data['license'] : '';
if (!empty($data['license_uri'])) {
$license = html::a([
'target' => '_blank',
'href' => rcube::Q($data['license_uri'])
],
rcube::Q($data['license'])
);
}
else {
$license = rcube::Q($license);
}
$table->add_row();
$table->add('name', rcube::Q(!empty($data['name']) ? $data['name'] : $name));
$table->add('version', !empty($data['version']) ? rcube::Q($data['version']) : '');
$table->add('license', $license);
$table->add('source', $uri);
}
return $table->show();
}
}
@@ -0,0 +1,22 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality to create a folder |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_create extends rcmail_action_settings_folder_edit
{
}
@@ -0,0 +1,66 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality to delete a folder |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_delete extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$plugin = $rcmail->plugins->exec_hook('folder_delete', ['name' => $mbox]);
if (empty($plugin['abort'])) {
$deleted = $storage->delete_folder($plugin['name']);
}
else {
$deleted = $plugin['result'];
}
// #1488692: update session
if ($deleted && isset($_SESSION['mbox']) && $_SESSION['mbox'] === $mbox) {
$rcmail->session->remove('mbox');
}
}
if (!empty($deleted)) {
// Remove folder and subfolders rows
$rcmail->output->command('remove_folder_row', $mbox);
$rcmail->output->show_message('folderdeleted', 'confirmation');
// Clear content frame
$rcmail->output->command('subscription_select');
$rcmail->output->command('set_quota', self::quota_content());
}
else {
self::display_server_error('errorsaving');
}
$rcmail->output->send();
}
}
@@ -0,0 +1,347 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality to edit a folder |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_edit extends rcmail_action_settings_folders
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->add_handlers([
'folderdetails' => [$this, 'folder_form'],
]);
$rcmail->output->add_label('nonamewarning');
$rcmail->output->send('folderedit');
}
public static function folder_form($attrib)
{
// WARNING: folder names in UI are encoded with RCUBE_CHARSET
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
// edited folder name (empty in create-folder mode)
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_GPC, true);
// predefined path for new folder
$parent = rcube_utils::get_input_string('_path', rcube_utils::INPUT_GPC, true);
$threading_supported = $storage->get_capability('THREAD');
$dual_use_supported = $storage->get_capability(rcube_storage::DUAL_USE_FOLDERS);
$delimiter = $storage->get_hierarchy_delimiter();
// Get mailbox parameters
if (strlen($mbox)) {
$options = self::folder_options($mbox);
$namespace = $storage->get_namespace();
$path = explode($delimiter, $mbox);
$folder = array_pop($path);
$path = implode($delimiter, $path);
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$hidden_fields = ['name' => '_mbox', 'value' => $mbox];
}
else {
$options = [];
$path = $parent;
$folder = '';
$hidden_fields = [];
// allow creating subfolders of INBOX folder
if ($path == 'INBOX') {
$path = $storage->mod_folder($path, 'in');
}
}
// remove personal namespace prefix
$path_id = null;
if (strlen($path)) {
$path_id = $path;
$path = $storage->mod_folder($path . $delimiter);
if (($path[strlen($path)-1] ?? '') == $delimiter) {
$path = substr($path, 0, -1);
}
}
$form = [];
// General tab
$form['props'] = [
'name' => $rcmail->gettext('properties'),
];
// Location (name)
if (!empty($options['protected'])) {
$foldername = str_replace($delimiter, ' &raquo; ', rcube::Q(self::localize_foldername($mbox, false, true)));
}
else if (!empty($options['norename'])) {
$foldername = rcube::Q($folder);
}
else {
if (isset($_POST['_name'])) {
$folder = trim(rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST, true));
}
$foldername = new html_inputfield(['name' => '_name', 'id' => '_name', 'size' => 30, 'class' => 'form-control']);
$foldername = '<span class="input-group">' . $foldername->show($folder);
if (!empty($options['special']) && ($sname = self::localize_foldername($mbox, false, true)) != $folder) {
$foldername .= ' <span class="input-group-append"><span class="input-group-text">(' . rcube::Q($sname) .')</span></span>';
}
$foldername .= '</span>';
}
$form['props']['fieldsets']['location'] = [
'name' => $rcmail->gettext('location'),
'content' => [
'name' => [
'label' => $rcmail->gettext('foldername'),
'value' => $foldername,
],
],
];
if (!empty($options) && (!empty($options['norename']) || !empty($options['protected']))) {
// prevent user from moving folder
$hidden_path = new html_hiddenfield(['name' => '_parent', 'value' => $path]);
$form['props']['fieldsets']['location']['content']['name']['value'] .= $hidden_path->show();
}
else {
$selected = $_POST['_parent'] ?? $path_id;
$exceptions = [$mbox];
// Exclude 'prefix' namespace from parent folders list (#1488349)
// If INBOX. namespace exists, folders created as INBOX subfolders
// will be listed at the same level - selecting INBOX as a parent does nothing
if ($prefix = $storage->get_namespace('prefix')) {
$exceptions[] = substr($prefix, 0, -1);
}
$select = self::folder_selector([
'id' => '_parent',
'name' => '_parent',
'noselection' => '---',
'maxlength' => 150,
'unsubscribed' => true,
'skip_noinferiors' => true,
'exceptions' => $exceptions,
'additional' => is_string($selected) && strlen($selected) ? [$selected] : null,
]);
$form['props']['fieldsets']['location']['content']['parent'] = [
'label' => $rcmail->gettext('parentfolder'),
'value' => $select->show($selected),
];
}
// Settings
$form['props']['fieldsets']['settings'] = [
'name' => $rcmail->gettext('settings'),
];
// For servers that do not support both sub-folders and messages in a folder
if (!$dual_use_supported) {
if (!strlen($mbox)) {
$select = new html_select(['name' => '_type', 'id' => '_type']);
$select->add($rcmail->gettext('dualusemail'), 'mail');
$select->add($rcmail->gettext('dualusefolder'), 'folder');
$value = rcube_utils::get_input_string('_type', rcube_utils::INPUT_POST);
$value = $select->show($value ?: 'mail');
}
else {
$value = $options['noselect'] ? 'folder' : 'mail';
$value = $rcmail->gettext('dualuse' . $value);
}
$form['props']['fieldsets']['settings']['content']['type'] = [
'label' => $rcmail->gettext('dualuselabel'),
'value' => $value,
];
}
// Settings: threading
if ($threading_supported && ($mbox == 'INBOX' || (empty($options['noselect']) && empty($options['is_root'])))) {
$value = 0;
$select = new html_select(['name' => '_viewmode', 'id' => '_viewmode']);
$select->add($rcmail->gettext('list'), 0);
$select->add($rcmail->gettext('threads'), 1);
if (isset($_POST['_viewmode'])) {
$value = (int) $_POST['_viewmode'];
}
else if (strlen($mbox)) {
$a_threaded = $rcmail->config->get('message_threading', []);
$default_mode = $rcmail->config->get('default_list_mode', 'list');
$value = (int) ($a_threaded[$mbox] ?? $default_mode == 'threads');
}
$form['props']['fieldsets']['settings']['content']['viewmode'] = [
'label' => $rcmail->gettext('listmode'),
'value' => $select->show($value),
];
}
$msgcount = 0;
// Information (count, size) - Edit mode
if (strlen($mbox)) {
// Number of messages
$form['props']['fieldsets']['info'] = [
'name' => $rcmail->gettext('info'),
'content' => []
];
if ((!$options['noselect'] && !$options['is_root']) || $mbox == 'INBOX') {
$msgcount = (int) $storage->count($mbox, 'ALL', true, false);
if ($msgcount) {
// Get the size on servers with supposed-to-be-fast method for that
if ($storage->get_capability('STATUS=SIZE')) {
$size = $storage->folder_size($mbox);
if ($size !== false) {
$size = self::show_bytes($size);
}
}
// create link with folder-size command
if (!isset($size) || $size === false) {
$onclick = sprintf("return %s.command('folder-size', '%s', this)",
rcmail_output::JS_OBJECT_NAME, rcube::JQ($mbox));
$attr = ['href' => '#', 'onclick' => $onclick, 'id' => 'folder-size'];
$size = html::a($attr, $rcmail->gettext('getfoldersize'));
}
}
else {
// no messages -> zero size
$size = 0;
}
$form['props']['fieldsets']['info']['content']['count'] = [
'label' => $rcmail->gettext('messagecount'),
'value' => $msgcount
];
$form['props']['fieldsets']['info']['content']['size'] = [
'label' => $rcmail->gettext('size'),
'value' => $size,
];
}
// show folder type only if we have non-private namespaces
if (!empty($namespace['shared']) || !empty($namespace['others'])) {
$form['props']['fieldsets']['info']['content']['foldertype'] = [
'label' => $rcmail->gettext('foldertype'),
'value' => $rcmail->gettext($options['namespace'] . 'folder')
];
}
}
// Allow plugins to modify folder form content
$plugin = $rcmail->plugins->exec_hook('folder_form', [
'form' => $form,
'options' => $options,
'name' => $mbox,
'parent_name' => $parent
]);
$form = $plugin['form'];
// Set form tags and hidden fields
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-folder', null, $hidden_fields);
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
// Create form output
foreach ($form as $idx => $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = self::get_form_part($fieldset, $attrib);
if ($subcontent) {
$subcontent = html::tag('legend', null, rcube::Q($fieldset['name'])) . $subcontent;
$content .= html::tag('fieldset', null, $subcontent) ."\n";
}
}
}
else {
$content = self::get_form_part($tab, $attrib);
}
if ($idx != 'props') {
$out .= html::tag('fieldset', null, html::tag('legend', null, rcube::Q($tab['name'])) . $content) ."\n";
}
else {
$out .= $content ."\n";
}
}
$out .= "\n$form_end";
$rcmail->output->set_env('messagecount', $msgcount);
$rcmail->output->set_env('folder', $mbox);
if ($mbox !== null && empty($_POST)) {
$rcmail->output->command('parent.set_quota', self::quota_content(null, $mbox));
}
return $out;
}
public static function get_form_part($form, $attrib = [])
{
$rcmail = rcmail::get_instance();
$content = '';
if (!empty($form['content']) && is_array($form['content'])) {
$table = new html_table(['cols' => 2]);
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_' . $col;
$label = !empty($colprop['label']) ? $colprop['label'] : $rcmail->gettext($col);
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show($attrib);
}
else if (isset($form['content'])) {
$content = $form['content'];
}
return $content;
}
}
@@ -0,0 +1,70 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality of folder purge |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_purge extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
$delimiter = $storage->get_hierarchy_delimiter();
$trash_mbox = $rcmail->config->get('trash_mbox');
$trash_regexp = '/^' . preg_quote($trash_mbox . $delimiter, '/') . '/';
// we should only be purging trash (or their subfolders)
if (!strlen($trash_mbox) || $mbox === $trash_mbox || preg_match($trash_regexp, $mbox)) {
$success = $storage->delete_message('*', $mbox);
$delete = true;
}
// move to Trash
else {
$success = $storage->move_message('1:*', $trash_mbox, $mbox);
$delete = false;
}
if (!empty($success)) {
$rcmail->output->set_env('messagecount', 0);
if ($delete) {
$rcmail->output->show_message('folderpurged', 'confirmation');
$rcmail->output->command('set_quota', self::quota_content(null, $mbox));
}
else {
$rcmail->output->show_message('messagemoved', 'confirmation');
}
$_SESSION['unseen_count'][$mbox] = 0;
$rcmail->output->command('show_folder', $mbox, null, true);
}
else {
self::display_server_error('errorsaving');
}
$rcmail->output->send();
}
}
@@ -0,0 +1,94 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality of folder rename |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_rename extends rcmail_action_settings_folders
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$name = trim(rcube_utils::get_input_string('_folder_newname', rcube_utils::INPUT_POST, true));
$oldname = rcube_utils::get_input_string('_folder_oldname', rcube_utils::INPUT_POST, true);
if (strlen($name) && strlen($oldname)) {
$rename = self::rename_folder($oldname, $name);
}
if (!empty($rename)) {
self::update_folder_row($name, $oldname);
}
else {
self::display_server_error('errorsaving');
}
$rcmail->output->send();
}
public static function rename_folder($oldname, $newname)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$plugin = $rcmail->plugins->exec_hook('folder_rename', [
'oldname' => $oldname, 'newname' => $newname]);
if (empty($plugin['abort'])) {
$renamed = $storage->rename_folder($oldname, $newname);
}
else {
$renamed = $plugin['result'];
}
// update per-folder options for modified folder and its subfolders
if ($renamed) {
$delimiter = $storage->get_hierarchy_delimiter();
$a_threaded = (array) $rcmail->config->get('message_threading', []);
$oldprefix = '/^' . preg_quote($oldname . $delimiter, '/') . '/';
foreach ($a_threaded as $key => $val) {
if ($key == $oldname) {
unset($a_threaded[$key]);
$a_threaded[$newname] = $val;
}
else if (preg_match($oldprefix, $key)) {
unset($a_threaded[$key]);
$a_threaded[preg_replace($oldprefix, $newname . $delimiter, $key)] = $val;
}
}
$rcmail->user->save_prefs(['message_threading' => $a_threaded]);
// #1488692: update session
if (isset($_SESSION['mbox']) && $_SESSION['mbox'] === $oldname) {
$_SESSION['mbox'] = $newname;
}
return true;
}
return false;
}
}
@@ -0,0 +1,235 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handler for saving the create/edit folder form |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_save extends rcmail_action_settings_folder_edit
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
// WARNING: folder names in UI are encoded with RCUBE_CHARSET
$name = trim(rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST, true));
$path = rcube_utils::get_input_string('_parent', rcube_utils::INPUT_POST, true);
$old_imap = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
$type = rcube_utils::get_input_string('_type', rcube_utils::INPUT_POST);
$name_imap = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP');
// $path is in UTF7-IMAP already
// init IMAP connection
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$options = strlen($old_imap) ? self::folder_options($old_imap) : [];
$char = null;
// Folder name checks
if (!empty($options['protected']) || !empty($options['norename'])) {
// do nothing
}
else if (!strlen($name)) {
$error = $rcmail->gettext('namecannotbeempty');
}
else if (mb_strlen($name) > 128) {
$error = $rcmail->gettext('nametoolong');
}
else if ($name[0] == '.' && $rcmail->config->get('imap_skip_hidden_folders')) {
$error = $rcmail->gettext('namedotforbidden');
}
else if (!$storage->folder_validate($name, $char)) {
$error = $rcmail->gettext('forbiddencharacter') . " ($char)";
}
if (!empty($error)) {
$rcmail->output->command('display_message', $error, 'error');
}
else {
if (!empty($options['protected']) || !empty($options['norename'])) {
$name_imap = $old_imap;
}
else if (strlen($path)) {
$name_imap = $path . $delimiter . $name_imap;
}
else {
$name_imap = $storage->mod_folder($name_imap, 'in');
}
}
$dual_use_supported = $storage->get_capability(rcube_storage::DUAL_USE_FOLDERS);
$acl_supported = $storage->get_capability('ACL');
// Check access rights to the parent folder
if (empty($error) && $acl_supported && strlen($path) && (!strlen($old_imap) || $old_imap != $name_imap)) {
$parent_opts = $storage->folder_info($path);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
$error = $rcmail->gettext('parentnotwritable');
}
}
if (!empty($error)) {
$rcmail->output->command('display_message', $error, 'error');
$folder = null;
}
else {
$folder = [
'name' => $name_imap,
'oldname' => $old_imap,
'class' => '',
'options' => $options,
'settings' => [
// List view mode: 0-list, 1-threads
'view_mode' => (int) rcube_utils::get_input_string('_viewmode', rcube_utils::INPUT_POST),
'sort_column' => rcube_utils::get_input_string('_sortcol', rcube_utils::INPUT_POST),
'sort_order' => rcube_utils::get_input_string('_sortord', rcube_utils::INPUT_POST),
],
'subscribe' => false,
'noselect' => false,
];
}
// create a new mailbox
if (empty($error) && !strlen($old_imap)) {
$folder['subscribe'] = true;
// Server does not support both sub-folders and messages in a folder
// For folders that are supposed to contain other folders we will:
// - disable subscription
// - add a separator at the end to make them \NoSelect
if (!$dual_use_supported && $type == 'folder') {
$folder['subscribe'] = false;
$folder['noselect'] = true;
}
$plugin = $rcmail->plugins->exec_hook('folder_create', ['record' => $folder]);
$folder = $plugin['record'];
if (!$plugin['abort']) {
$created = $storage->create_folder($folder['name'], $folder['subscribe'], null, $folder['noselect']);
}
else {
$created = $plugin['result'];
}
if ($created) {
// Save folder settings
if (isset($_POST['_viewmode'])) {
$a_threaded = (array) $rcmail->config->get('message_threading', []);
$a_threaded[$folder['name']] = (bool) $_POST['_viewmode'];
$rcmail->user->save_prefs(['message_threading' => $a_threaded]);
}
self::update_folder_row($folder['name'], null, $folder['subscribe'], $folder['class']);
$rcmail->output->show_message('foldercreated', 'confirmation');
// reset folder preview frame
$rcmail->output->command('subscription_select');
$rcmail->output->send('iframe');
}
else {
// show error message
if (!empty($plugin['message'])) {
$rcmail->output->show_message($plugin['message'], 'error', null, false);
}
else {
self::display_server_error('errorsaving');
}
}
}
// update a mailbox
else if (empty($error)) {
$plugin = $rcmail->plugins->exec_hook('folder_update', ['record' => $folder]);
$folder = $plugin['record'];
$rename = $folder['oldname'] != $folder['name'];
if (!$plugin['abort']) {
if ($rename) {
$updated = $storage->rename_folder($folder['oldname'], $folder['name']);
}
else {
$updated = true;
}
}
else {
$updated = $plugin['result'];
}
if ($updated) {
// Update folder settings,
if (isset($_POST['_viewmode'])) {
$a_threaded = (array) $rcmail->config->get('message_threading', []);
// In case of name change update names of children in settings
if ($rename) {
$oldprefix = '/^' . preg_quote($folder['oldname'] . $delimiter, '/') . '/';
foreach ($a_threaded as $key => $val) {
if ($key == $folder['oldname']) {
unset($a_threaded[$key]);
}
else if (preg_match($oldprefix, $key)) {
unset($a_threaded[$key]);
$a_threaded[preg_replace($oldprefix, $folder['name'].$delimiter, $key)] = $val;
}
}
}
$a_threaded[$folder['name']] = (bool) $_POST['_viewmode'];
$rcmail->user->save_prefs(['message_threading' => $a_threaded]);
}
$rcmail->output->show_message('folderupdated', 'confirmation');
$rcmail->output->set_env('folder', $folder['name']);
if ($rename) {
// #1488692: update session
if (isset($_SESSION['mbox']) && $_SESSION['mbox'] === $folder['oldname']) {
$_SESSION['mbox'] = $folder['name'];
}
self::update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']);
$rcmail->output->send('iframe');
}
else if (!empty($folder['class'])) {
self::update_folder_row($folder['name'], $folder['oldname'], $folder['subscribe'], $folder['class']);
}
}
else {
// show error message
if (!empty($plugin['message'])) {
$rcmail->output->show_message($plugin['message'], 'error', null, false);
}
else {
self::display_server_error('errorsaving');
}
}
}
$rcmail->overwrite_action('edit-folder');
}
}
@@ -0,0 +1,49 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality of getting folder size |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_size extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$name = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
$size = $storage->folder_size($name);
// @TODO: check quota and show percentage usage of specified mailbox?
if ($size !== false) {
$rcmail->output->command('folder_size_update', self::show_bytes($size));
}
else {
self::display_server_error();
}
$rcmail->output->send();
}
}
@@ -0,0 +1,67 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handler for folder subscribe action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_subscribe extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$result = $storage->subscribe([$mbox]);
// Handle virtual (non-existing) folders
if (
!$result
&& $storage->get_error_code() == -1
&& $storage->get_response_code() == rcube_storage::TRYCREATE
) {
$result = $storage->create_folder($mbox, true);
if ($result) {
// @TODO: remove 'virtual' class of folder's row
}
}
}
if (!empty($result)) {
// Handle subscription of protected folder (#1487656)
if ($rcmail->config->get('protect_default_folders') && $storage->is_special_folder($mbox)) {
$rcmail->output->command('disable_subscription', $mbox);
}
$rcmail->output->show_message('foldersubscribed', 'confirmation');
}
else {
self::display_server_error('errorsaving');
$rcmail->output->command('reset_subscription', $mbox, false);
}
$rcmail->output->send();
}
}
@@ -0,0 +1,50 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handler for folder unsubscribe action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folder_unsubscribe extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$mbox = rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true);
if (strlen($mbox)) {
$result = $storage->unsubscribe([$mbox]);
}
if (!empty($result)) {
$rcmail->output->show_message('folderunsubscribed', 'confirmation');
}
else {
self::display_server_error('errorsaving');
$rcmail->output->command('reset_subscription', $mbox, true);
}
$rcmail->output->send();
}
}
@@ -0,0 +1,377 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality of folders listing |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_folders extends rcmail_action_settings_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$rcmail->output->set_pagetitle($rcmail->gettext('folders'));
$rcmail->output->set_env('prefix_ns', $storage->get_namespace('prefix'));
$rcmail->output->set_env('quota', (bool) $storage->get_capability('QUOTA'));
$rcmail->output->include_script('treelist.js');
// add some labels to client
$rcmail->output->add_label('deletefolderconfirm', 'purgefolderconfirm', 'movefolderconfirm',
'folderdeleting', 'foldermoving', 'foldersubscribing', 'folderunsubscribing',
'move', 'quota');
// register UI objects
$rcmail->output->add_handlers([
'foldersubscription' => [$this, 'folder_subscriptions'],
'folderfilter' => [$this, 'folder_filter'],
'quotadisplay' => [$rcmail, 'quota_display'],
'searchform' => [$rcmail->output, 'search_form'],
]);
$rcmail->output->send('folders');
}
// build table with all folders listed by server
public static function folder_subscriptions($attrib)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmSubscriptionlist';
}
// get folders from server
$storage->clear_cache('mailboxes', true);
$a_unsubscribed = $storage->list_folders();
$a_subscribed = $storage->list_folders_subscribed('', '*', null, null, true); // unsorted
$delimiter = $storage->get_hierarchy_delimiter();
$namespace = $storage->get_namespace();
$special_folders = array_flip(array_merge(['inbox' => 'INBOX'], $storage->get_special_folders()));
$protect_default = $rcmail->config->get('protect_default_folders');
$seen = [];
$list_folders = [];
// pre-process folders list
foreach ($a_unsubscribed as $i => $folder) {
$folder_id = $folder;
$folder = $storage->mod_folder($folder);
$foldersplit = explode($delimiter, $folder);
$name = rcube_charset::convert(array_pop($foldersplit), 'UTF7-IMAP');
$is_special = isset($special_folders[$folder_id]);
$parent_folder = $is_special ? '' : join($delimiter, $foldersplit);
$level = $is_special ? 0 : count($foldersplit);
// add any necessary "virtual" parent folders
if ($parent_folder && empty($seen[$parent_folder])) {
for ($i = 1; $i <= $level; $i++) {
$ancestor_folder = join($delimiter, array_slice($foldersplit, 0, $i));
if ($ancestor_folder) {
if (empty($seen[$ancestor_folder])) {
$seen[$ancestor_folder] = true;
$ancestor_name = rcube_charset::convert($foldersplit[$i-1], 'UTF7-IMAP');
$list_folders[] = [
'id' => $ancestor_folder,
'name' => $ancestor_name,
'level' => $i-1,
'virtual' => true,
];
}
}
}
}
// Handle properly INBOX.INBOX situation
if (isset($seen[$folder])) {
continue;
}
$seen[$folder] = true;
$list_folders[] = [
'id' => $folder_id,
'name' => $name,
'level' => $level,
];
}
unset($seen);
$checkbox_subscribe = new html_checkbox([
'name' => '_subscribed[]',
'title' => $rcmail->gettext('changesubscription'),
'onclick' => rcmail_output::JS_OBJECT_NAME.".command(this.checked?'subscribe':'unsubscribe',this.value)",
]);
$js_folders = [];
$folders = [];
$collapsed = (string) $rcmail->config->get('collapsed_folders');
// create list of available folders
foreach ($list_folders as $i => $folder) {
$sub_key = array_search($folder['id'], $a_subscribed);
$is_subscribed = $sub_key !== false;
$is_special = isset($special_folders[$folder['id']]);
$is_protected = $folder['id'] == 'INBOX' || ($protect_default && $is_special);
$noselect = false;
$classes = [];
$folder_utf8 = rcube_charset::convert($folder['id'], 'UTF7-IMAP');
$display_folder = rcube::Q($is_special ? self::localize_foldername($folder['id'], false, true) : $folder['name']);
if (!empty($folder['virtual'])) {
$classes[] = 'virtual';
}
// Check \Noselect flag (of existing folder)
if (!$is_protected && in_array($folder['id'], $a_unsubscribed)) {
$attrs = $storage->folder_attributes($folder['id']);
$noselect = in_array_nocase('\\Noselect', $attrs);
}
$is_disabled = (($is_protected && $is_subscribed) || $noselect);
// Below we will disable subscription option for "virtual" folders
// according to namespaces, but only if they aren't already subscribed.
// User should be able to unsubscribe from the folder
// even if it doesn't exists or is not accessible (OTRS:1000059)
if (!$is_subscribed && !$is_disabled && !empty($namespace) && !empty($folder['virtual'])) {
// check if the folder is a namespace prefix, then disable subscription option on it
if (!$is_disabled && $folder['level'] == 0) {
$fname = $folder['id'] . $delimiter;
foreach ($namespace as $ns) {
if (is_array($ns)) {
foreach ($ns as $item) {
if ($item[0] === $fname) {
$is_disabled = true;
break 2;
}
}
}
}
}
// check if the folder is an other users virtual-root folder, then disable subscription option on it
if (!$is_disabled && $folder['level'] == 1 && !empty($namespace['other'])) {
$parts = explode($delimiter, $folder['id']);
$fname = $parts[0] . $delimiter;
foreach ($namespace['other'] as $item) {
if ($item[0] === $fname) {
$is_disabled = true;
break;
}
}
}
// check if the folder is shared, then disable subscription option on it (if not subscribed already)
if (!$is_disabled) {
$tmp_ns = array_merge((array)$namespace['other'], (array)$namespace['shared']);
foreach ($tmp_ns as $item) {
if (strlen($item[0]) && strpos($folder['id'], $item[0]) === 0) {
$is_disabled = true;
break;
}
}
}
}
$is_collapsed = strpos($collapsed, '&'.rawurlencode($folder['id']).'&') !== false;
$folder_id = rcube_utils::html_identifier($folder['id'], true);
if ($folder_class = self::folder_classname($folder['id'])) {
$classes[] = $folder_class;
}
$folders[$folder['id']] = [
'idx' => $folder_id,
'folder_imap' => $folder['id'],
'folder' => $folder_utf8,
'display' => $display_folder,
'protected' => $is_protected || !empty($folder['virtual']),
'class' => join(' ', $classes),
'subscribed' => $is_subscribed,
'level' => $folder['level'],
'collapsed' => $is_collapsed,
'content' => html::a(['href' => '#'], $display_folder)
. $checkbox_subscribe->show(($is_subscribed ? $folder['id'] : ''),
['value' => $folder['id'], 'disabled' => $is_disabled ? 'disabled' : ''])
];
}
$plugin = $rcmail->plugins->exec_hook('folders_list', ['list' => $folders]);
// add drop-target representing 'root'
$root = [
'idx' => rcube_utils::html_identifier('*', true),
'folder_imap' => '*',
'folder' => '',
'display' => '',
'protected' => true,
'class' => 'root',
'content' => '<span>&nbsp;</span>',
];
$folders = [];
$plugin['list'] = array_values($plugin['list']);
array_unshift($plugin['list'], $root);
for ($i = 0, $length = count($plugin['list']); $i<$length; $i++) {
$folders[] = self::folder_tree_element($plugin['list'], $i, $js_folders);
}
$rcmail->output->add_gui_object('subscriptionlist', $attrib['id']);
$rcmail->output->set_env('subscriptionrows', $js_folders);
$rcmail->output->set_env('defaultfolders', array_keys($special_folders));
$rcmail->output->set_env('collapsed_folders', $collapsed);
$rcmail->output->set_env('delimiter', $delimiter);
return html::tag('ul', $attrib, implode('', $folders), html::$common_attrib);
}
public static function folder_tree_element($folders, &$key, &$js_folders)
{
$data = $folders[$key];
$idx = 'rcmli' . $data['idx'];
$js_folders[$data['folder_imap']] = [$data['folder'], $data['display'], $data['protected']];
$content = $data['content'];
$attribs = [
'id' => $idx,
'class' => trim($data['class'] . ' mailbox')
];
if (!isset($data['level'])) {
$data['level'] = 0;
}
$children = [];
while (!empty($folders[$key+1]) && ($folders[$key+1]['level'] > $data['level'])) {
$key++;
$children[] = self::folder_tree_element($folders, $key, $js_folders);
}
if (!empty($children)) {
$content .= html::div('treetoggle ' . (!empty($data['collapsed']) ? 'collapsed' : 'expanded'), '&nbsp;')
. html::tag('ul', ['style' => !empty($data['collapsed']) ? "display:none" : null],
implode("\n", $children));
}
return html::tag('li', $attribs, $content);
}
public static function folder_filter($attrib)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$namespace = $storage->get_namespace();
if (empty($namespace['personal']) && empty($namespace['shared']) && empty($namespace['other'])) {
return '';
}
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmfolderfilter';
}
if (!self::get_bool_attr($attrib, 'noevent')) {
$attrib['onchange'] = rcmail_output::JS_OBJECT_NAME . '.folder_filter(this.value)';
}
$roots = [];
$select = new html_select($attrib);
$select->add($rcmail->gettext('all'), '---');
foreach (array_keys($namespace) as $type) {
foreach ((array)$namespace[$type] as $ns) {
$root = rtrim($ns[0], $ns[1]);
$label = $rcmail->gettext('namespace.' . $type);
if (count($namespace[$type]) > 1) {
$label .= ' (' . rcube_charset::convert($root, 'UTF7-IMAP', RCUBE_CHARSET) . ')';
}
$select->add($label, $root);
if (strlen($root)) {
$roots[] = $root;
}
}
}
$rcmail->output->add_gui_object('foldersfilter', $attrib['id']);
$rcmail->output->set_env('ns_roots', $roots);
return $select->show();
}
public static function folder_options($mailbox)
{
$rcmail = rcmail::get_instance();
$options = $rcmail->get_storage()->folder_info($mailbox);
$options['protected'] = !empty($options['is_root'])
|| strtoupper($mailbox) === 'INBOX'
|| (!empty($options['special']) && $rcmail->config->get('protect_default_folders'));
return $options;
}
/**
* Updates (or creates) folder row in the subscriptions table
*
* @param string $name Folder name
* @param string $oldname Old folder name (for update)
* @param bool $subscribe Checks subscription checkbox
* @param string $class CSS class name for folder row
*/
public static function update_folder_row($name, $oldname = null, $subscribe = false, $class_name = null)
{
$rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$options = self::folder_options($name);
$name_utf8 = rcube_charset::convert($name, 'UTF7-IMAP');
$foldersplit = explode($delimiter, $storage->mod_folder($name));
$level = count($foldersplit) - 1;
$class_name = trim($class_name . ' mailbox');
if (!empty($options['protected'])) {
$display_name = self::localize_foldername($name);
}
else {
$display_name = rcube_charset::convert($foldersplit[$level], 'UTF7-IMAP');
}
$protected = !empty($options['protected']) || !empty($options['noselect']);
if ($oldname === null) {
$rcmail->output->command('add_folder_row', $name, $name_utf8, $display_name,
$protected, $subscribe, $class_name);
}
else {
$rcmail->output->command('replace_folder_row', $oldname, $name, $name_utf8, $display_name,
$protected, $class_name);
}
}
}
@@ -0,0 +1,73 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Manage identities of a user account |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identities extends rcmail_action
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('identities'));
$rcmail->output->include_script('list.js');
$rcmail->output->set_env('identities_level', (int) $rcmail->config->get('identities_level', 0));
$rcmail->output->add_label('deleteidentityconfirm');
$rcmail->output->add_handlers([
'identitieslist' => [$this, 'identities_list'],
]);
$rcmail->output->send('identities');
}
public static function identities_list($attrib)
{
$rcmail = rcmail::get_instance();
// add id to message list table if not specified
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmIdentitiesList';
}
// get identities list and define 'mail' column
$list = $rcmail->user->list_emails();
foreach ($list as $idx => $row) {
$list[$idx]['mail'] = trim($row['name'] . ' <' . rcube_utils::idn_to_utf8($row['email']) . '>');
}
// get all identities from DB and define list of cols to be displayed
$plugin = $rcmail->plugins->exec_hook('identities_list', [
'list' => $list,
'cols' => ['mail']
]);
// @TODO: use <UL> instead of <TABLE> for identities list
$out = self::table_output($attrib, $plugin['list'], $plugin['cols'], 'identity_id');
// set client env
$rcmail->output->add_gui_object('identitieslist', $attrib['id']);
return $out;
}
}
@@ -0,0 +1,22 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide identity create form |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_create extends rcmail_action_settings_identity_edit
{
}
@@ -0,0 +1,52 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for identity delete action |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_delete extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$iid = rcube_utils::get_input_string('_iid', rcube_utils::INPUT_POST);
$deleted = 0;
if ($iid && preg_match('/^[0-9]+(,[0-9]+)*$/', $iid)) {
$plugin = $rcmail->plugins->exec_hook('identity_delete', ['id' => $iid]);
$deleted = !$plugin['abort'] ? $rcmail->user->delete_identity($iid) : $plugin['result'];
}
if ($deleted > 0 && $deleted !== false) {
$rcmail->output->show_message('deletedsuccessfully', 'confirmation', null, false);
$rcmail->output->command('remove_identity', $iid);
}
else {
$msg = !empty($plugin['message']) ? $plugin['message'] : ($deleted < 0 ? 'nodeletelastidentity' : 'errorsaving');
$rcmail->output->show_message($msg, 'error', null, false);
}
$rcmail->output->send();
}
}
@@ -0,0 +1,240 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for an identity record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_edit extends rcmail_action
{
protected static $mode = self::MODE_HTTP;
protected static $record;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$IDENTITIES_LEVEL = intval($rcmail->config->get('identities_level', 0));
// edit-identity
if ($rcmail->action == 'edit-identity'
&& ($id = rcube_utils::get_input_string('_iid', rcube_utils::INPUT_GPC))
) {
self::$record = $rcmail->user->get_identity($id);
if (!is_array(self::$record)) {
$rcmail->output->show_message('dberror', 'error');
$rcmail->output->send('iframe');
}
$rcmail->output->set_env('iid', self::$record['identity_id']);
$rcmail->output->set_env('mailvelope_main_keyring', $rcmail->config->get('mailvelope_main_keyring'));
$rcmail->output->set_env('mailvelope_keysize', $rcmail->config->get('mailvelope_keysize'));
}
// add-identity
else {
if ($IDENTITIES_LEVEL > 1) {
$rcmail->output->show_message('opnotpermitted', 'error');
// go to identities page
$rcmail->overwrite_action('identities');
return;
}
if ($IDENTITIES_LEVEL == 1) {
self::$record['email'] = $rcmail->get_user_email();
}
}
$rcmail->output->add_handler('identityform', [$this, 'identity_form']);
$rcmail->output->set_env('identities_level', $IDENTITIES_LEVEL);
$rcmail->output->add_label('deleteidentityconfirm', 'generate',
'encryptioncreatekey', 'openmailvelopesettings', 'encryptionprivkeysinmailvelope',
'encryptionnoprivkeysinmailvelope', 'keypaircreatesuccess');
$rcmail->output->set_pagetitle($rcmail->gettext(($rcmail->action == 'add-identity' ? 'addidentity' : 'editidentity')));
if ($rcmail->action == 'add-identity' && $rcmail->output->template_exists('identityadd')) {
$rcmail->output->send('identityadd');
}
$rcmail->output->send('identityedit');
}
public static function identity_form($attrib)
{
$rcmail = rcmail::get_instance();
$IDENTITIES_LEVEL = intval($rcmail->config->get('identities_level', 0));
// Add HTML editor script(s)
self::html_editor('identity', 'rcmfd_signature');
// add some labels to client
$rcmail->output->add_label('noemailwarning', 'converting', 'editorwarning');
$i_size = !empty($attrib['size']) ? $attrib['size'] : 40;
$t_rows = !empty($attrib['textarearows']) ? $attrib['textarearows'] : 6;
$t_cols = !empty($attrib['textareacols']) ? $attrib['textareacols'] : 40;
// list of available cols
$form = [
'addressing' => [
'name' => $rcmail->gettext('settings'),
'content' => [
'name' => ['type' => 'text', 'size' => $i_size],
'email' => ['type' => 'text', 'size' => $i_size],
'organization' => ['type' => 'text', 'size' => $i_size],
'reply-to' => ['type' => 'text', 'size' => $i_size],
'bcc' => ['type' => 'text', 'size' => $i_size],
'standard' => ['type' => 'checkbox', 'label' => $rcmail->gettext('setdefault')],
]
],
'signature' => [
'name' => $rcmail->gettext('signature'),
'content' => [
'signature' => [
'type' => 'textarea',
'size' => $t_cols,
'rows' => $t_rows,
'spellcheck' => true,
'data-html-editor' => true
],
'html_signature' => [
'type' => 'checkbox',
'label' => $rcmail->gettext('htmlsignature'),
'onclick' => "return rcmail.command('toggle-editor', {id: 'rcmfd_signature', html: this.checked}, '', event)"
],
]
],
'encryption' => [
'name' => $rcmail->gettext('identityencryption'),
'attrs' => ['class' => 'identity-encryption', 'style' => 'display:none'],
'content' => html::div('identity-encryption-block', '')
]
];
// Enable TinyMCE editor
if (!empty(self::$record['html_signature'])) {
$form['signature']['content']['signature']['class'] = 'mce_editor';
$form['signature']['content']['signature']['is_escaped'] = true;
// Correctly handle HTML entities in HTML editor (#1488483)
self::$record['signature'] = htmlspecialchars(self::$record['signature'], ENT_NOQUOTES, RCUBE_CHARSET);
}
// hide "default" checkbox if only one identity is allowed
if ($IDENTITIES_LEVEL > 1) {
unset($form['addressing']['content']['standard']);
}
// disable some field according to access level
if ($IDENTITIES_LEVEL == 1 || $IDENTITIES_LEVEL == 3) {
$form['addressing']['content']['email']['disabled'] = true;
$form['addressing']['content']['email']['class'] = 'disabled';
}
if ($IDENTITIES_LEVEL == 4) {
foreach ($form['addressing']['content'] as $formfield => $value){
$form['addressing']['content'][$formfield]['disabled'] = true;
$form['addressing']['content'][$formfield]['class'] = 'disabled';
}
}
if (!empty(self::$record['email'])) {
self::$record['email'] = rcube_utils::idn_to_utf8(self::$record['email']);
}
// Allow plugins to modify identity form content
$plugin = $rcmail->plugins->exec_hook('identity_form', [
'form' => $form,
'record' => self::$record
]);
$form = $plugin['form'];
self::$record = $plugin['record'];
// Set form tags and hidden fields
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-identity',
intval(self::$record['identity_id'] ?? 0),
['name' => '_iid', 'value' => self::$record['identity_id'] ?? 0]
);
unset($plugin);
unset($attrib['form'], $attrib['id']);
// return the complete edit form as table
$out = "$form_start\n";
foreach ($form as $fieldset) {
if (empty($fieldset['content'])) {
continue;
}
$content = '';
if (is_array($fieldset['content'])) {
$table = new html_table(['cols' => 2]);
foreach ($fieldset['content'] as $col => $colprop) {
$colprop['id'] = 'rcmfd_'.$col;
if (!empty($colprop['label'])) {
$label = $colprop['label'];
}
else {
$label = $rcmail->gettext(str_replace('-', '', $col));
}
if (!empty($colprop['value'])) {
$value = $colprop['value'];
}
else {
$val = self::$record[$col] ?? '';
$value = rcube_output::get_edit_field($col, $val, $colprop, $colprop['type']);
}
$table->add('title', html::label($colprop['id'], rcube::Q($label)));
$table->add(null, $value);
}
$content = $table->show($attrib);
}
else {
$content = $fieldset['content'];
}
$content = html::tag('legend', null, rcube::Q($fieldset['name'])) . $content;
$out .= html::tag('fieldset', !empty($fieldset['attrs']) ? $fieldset['attrs'] : [], $content) . "\n";
}
$out .= $form_end;
// add image upload form
$max_size = self::upload_init($rcmail->config->get('identity_image_size', 64) * 1024);
$form_id = 'identityImageUpload';
$out .= '<form id="' . $form_id . '" style="display: none">'
. html::div('hint', $rcmail->gettext(['name' => 'maxuploadsize', 'vars' => ['size' => $max_size]]))
. '</form>';
$rcmail->output->add_gui_object('uploadform', $form_id);
return $out;
}
}
@@ -0,0 +1,212 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save an identity record or to add a new one |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_identity_save extends rcmail_action_settings_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$IDENTITIES_LEVEL = intval($rcmail->config->get('identities_level', 0));
$a_save_cols = ['name', 'email', 'organization', 'reply-to', 'bcc', 'standard', 'signature', 'html_signature'];
$a_bool_cols = ['standard', 'html_signature'];
$updated = false;
// check input
if (empty($_POST['_email']) && ($IDENTITIES_LEVEL == 0 || $IDENTITIES_LEVEL == 2)) {
$rcmail->output->show_message('noemailwarning', 'warning');
$rcmail->overwrite_action('edit-identity');
return;
}
$save_data = [];
foreach ($a_save_cols as $col) {
$fname = '_'.$col;
if (isset($_POST[$fname])) {
$save_data[$col] = rcube_utils::get_input_string($fname, rcube_utils::INPUT_POST, true);
}
}
// set "off" values for checkboxes that were not checked, and therefore
// not included in the POST body.
foreach ($a_bool_cols as $col) {
$fname = '_' . $col;
if (!isset($_POST[$fname])) {
$save_data[$col] = 0;
}
}
// make the identity a "default" if only one identity is allowed
if ($IDENTITIES_LEVEL > 1) {
$save_data['standard'] = 1;
}
// unset email address if user has no rights to change it
if ($IDENTITIES_LEVEL == 1 || $IDENTITIES_LEVEL == 3) {
unset($save_data['email']);
}
// unset all fields except signature
else if ($IDENTITIES_LEVEL == 4) {
foreach ($save_data as $idx => $value) {
if ($idx != 'signature' && $idx != 'html_signature') {
unset($save_data[$idx]);
}
}
}
// Validate e-mail addresses
$email_checks = !empty($save_data['email']) ? [rcube_utils::idn_to_ascii($save_data['email'])] : [];
foreach (['reply-to', 'bcc'] as $item) {
if (!empty($save_data[$item])) {
foreach (rcube_mime::decode_address_list($save_data[$item], null, false) as $rcpt) {
$email_checks[] = rcube_utils::idn_to_ascii($rcpt['mailto']);
}
}
}
foreach ($email_checks as $email) {
if ($email && !rcube_utils::check_email($email)) {
// show error message
$rcmail->output->show_message('emailformaterror', 'error', ['email' => rcube_utils::idn_to_utf8($email)], false);
$rcmail->overwrite_action('edit-identity');
return;
}
}
if (!empty($save_data['signature']) && !empty($save_data['html_signature'])) {
// replace uploaded images with data URIs
$save_data['signature'] = self::attach_images($save_data['signature'], 'identity');
// XSS protection in HTML signature (#1489251)
$save_data['signature'] = self::wash_html($save_data['signature']);
// clear POST data of signature, we want to use safe content
// when the form is displayed again
unset($_POST['_signature']);
}
// update an existing identity
if (!empty($_POST['_iid'])) {
$iid = rcube_utils::get_input_string('_iid', rcube_utils::INPUT_POST);
if (in_array($IDENTITIES_LEVEL, [1, 3, 4])) {
// merge with old identity data, fixes #1488834
$identity = $rcmail->user->get_identity($iid);
$save_data = array_merge($identity, $save_data);
unset($save_data['changed'], $save_data['del'], $save_data['user_id'], $save_data['identity_id']);
}
$plugin = $rcmail->plugins->exec_hook('identity_update', ['id' => $iid, 'record' => $save_data]);
$save_data = $plugin['record'];
if ($save_data['email']) {
$save_data['email'] = rcube_utils::idn_to_ascii($save_data['email']);
}
if (!$plugin['abort']) {
$updated = $rcmail->user->update_identity($iid, $save_data);
}
else {
$updated = $plugin['result'];
}
if ($updated) {
$rcmail->output->show_message('successfullysaved', 'confirmation');
if (!empty($save_data['standard'])) {
$default_id = $iid;
}
// update the changed col in list
$name = $save_data['name'] . ' <' . rcube_utils::idn_to_utf8($save_data['email']) .'>';
$rcmail->output->command('parent.update_identity_row', $iid, rcube::Q(trim($name)));
}
else {
// show error message
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error', null, false);
$rcmail->overwrite_action('edit-identity');
return;
}
}
// insert a new identity record
else if ($IDENTITIES_LEVEL < 2) {
if ($IDENTITIES_LEVEL == 1) {
$save_data['email'] = $rcmail->get_user_email();
}
$plugin = $rcmail->plugins->exec_hook('identity_create', ['record' => $save_data]);
$save_data = $plugin['record'];
if ($save_data['email']) {
$save_data['email'] = rcube_utils::idn_to_ascii($save_data['email']);
}
if (!$plugin['abort']) {
$insert_id = $save_data['email'] ? $rcmail->user->insert_identity($save_data) : null;
}
else {
$insert_id = $plugin['result'];
}
if ($insert_id) {
$rcmail->plugins->exec_hook('identity_create_after', ['id' => $insert_id, 'record' => $save_data]);
$rcmail->output->show_message('successfullysaved', 'confirmation', null, false);
$_GET['_iid'] = $insert_id;
if (!empty($save_data['standard'])) {
$default_id = $insert_id;
}
// add a new row to the list
$name = $save_data['name'] . ' <' . rcube_utils::idn_to_utf8($save_data['email']) .'>';
$rcmail->output->command('parent.update_identity_row', $insert_id, rcube::Q(trim($name)), true);
}
else {
// show error message
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error', null, false);
$rcmail->overwrite_action('edit-identity');
return;
}
}
else {
$rcmail->output->show_message('opnotpermitted', 'error');
}
// mark all other identities as 'not-default'
if (!empty($default_id)) {
$rcmail->user->set_default($default_id);
}
// go to next step
$rcmail->overwrite_action('edit-identity');
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,96 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Provide functionality for user's settings & preferences |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_prefs_edit extends rcmail_action_settings_index
{
protected static $mode = self::MODE_HTTP;
protected static $section;
protected static $sections;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('preferences'));
self::$section = rcube_utils::get_input_string('_section', rcube_utils::INPUT_GPC);
list(self::$sections,) = self::user_prefs(self::$section);
// register UI objects
$rcmail->output->add_handlers([
'userprefs' => [$this, 'user_prefs_form'],
'sectionname' => [$this, 'prefs_section_name'],
]);
$rcmail->output->send('settingsedit');
}
public static function user_prefs_form($attrib)
{
$rcmail = rcmail::get_instance();
// add some labels to client
$rcmail->output->add_label('nopagesizewarning', 'nosupporterror');
unset($attrib['form']);
$hidden = ['name' => '_section', 'value' => self::$section];
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-prefs', null, $hidden);
$out = $form_start;
if (!empty(self::$sections[self::$section]['header'])) {
$div_attr = ['id' => 'preferences-header', 'class' =>'boxcontent'];
$out .= html::div($div_attr, self::$sections[self::$section]['header']);
}
foreach (self::$sections[self::$section]['blocks'] as $class => $block) {
if (!empty($block['options'])) {
$table = new html_table(['cols' => 2]);
foreach ($block['options'] as $option) {
if (isset($option['title'])) {
$table->add('title', $option['title']);
$table->add(null, $option['content']);
}
else {
$table->add(['colspan' => 2], $option['content']);
}
}
$out .= html::tag('fieldset', $class, html::tag('legend', null, $block['name']) . $table->show($attrib));
}
else if (!empty($block['content'])) {
$out .= html::tag('fieldset', null, html::tag('legend', null, $block['name']) . $block['content']);
}
}
return $out . $form_end;
}
public static function prefs_section_name()
{
return self::$sections[self::$section]['section'];
}
}
@@ -0,0 +1,298 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save user preferences to DB and to the current session |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_prefs_save extends rcmail_action
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$CURR_SECTION = rcube_utils::get_input_string('_section', rcube_utils::INPUT_POST);
$dont_override = (array) $rcmail->config->get('dont_override');
$a_user_prefs = [];
// set options for specified section
switch ($CURR_SECTION) {
case 'general':
$a_user_prefs = [
'language' => self::prefs_input('language', '/^[a-zA-Z0-9_-]+$/'),
'timezone' => self::prefs_input('timezone', '/^[a-zA-Z_\/-]+$/'),
'date_format' => self::prefs_input('date_format', '/^[a-zA-Z_.\/ -]+$/'),
'time_format' => self::prefs_input('time_format', '/^[a-zA-Z0-9: ]+$/'),
'prettydate' => isset($_POST['_pretty_date']),
'display_next' => isset($_POST['_display_next']),
'refresh_interval' => self::prefs_input_int('refresh_interval') * 60,
'standard_windows' => isset($_POST['_standard_windows']),
'skin' => self::prefs_input('skin', '/^[a-zA-Z0-9_.-]+$/'),
];
// compose derived date/time format strings
if (
(isset($_POST['_date_format']) || isset($_POST['_time_format']))
&& !empty($a_user_prefs['date_format'])
&& !empty($a_user_prefs['time_format'])
) {
$a_user_prefs['date_short'] = 'D ' . $a_user_prefs['time_format'];
$a_user_prefs['date_long'] = $a_user_prefs['date_format'] . ' ' . $a_user_prefs['time_format'];
}
break;
case 'mailbox':
$a_user_prefs = [
'layout' => self::prefs_input('layout', '/^[a-z]+$/'),
'mail_read_time' => self::prefs_input_int('mail_read_time'),
'autoexpand_threads' => self::prefs_input_int('autoexpand_threads'),
'check_all_folders' => isset($_POST['_check_all_folders']),
'mail_pagesize' => max(2, self::prefs_input_int('mail_pagesize')),
];
break;
case 'mailview':
$a_user_prefs = [
'message_extwin' => self::prefs_input_int('message_extwin'),
'message_show_email' => isset($_POST['_message_show_email']),
'prefer_html' => isset($_POST['_prefer_html']),
'inline_images' => isset($_POST['_inline_images']),
'show_images' => self::prefs_input_int('show_images'),
'mdn_requests' => self::prefs_input_int('mdn_requests'),
'default_charset' => self::prefs_input('default_charset', '/^[a-zA-Z0-9-]+$/'),
];
break;
case 'compose':
$a_user_prefs = [
'compose_extwin' => self::prefs_input_int('compose_extwin'),
'htmleditor' => self::prefs_input_int('htmleditor'),
'draft_autosave' => self::prefs_input_int('draft_autosave'),
'mime_param_folding' => self::prefs_input_int('mime_param_folding'),
'force_7bit' => isset($_POST['_force_7bit']),
'mdn_default' => isset($_POST['_mdn_default']),
'dsn_default' => isset($_POST['_dsn_default']),
'reply_same_folder' => isset($_POST['_reply_same_folder']),
'spellcheck_before_send' => isset($_POST['_spellcheck_before_send']),
'spellcheck_ignore_syms' => isset($_POST['_spellcheck_ignore_syms']),
'spellcheck_ignore_nums' => isset($_POST['_spellcheck_ignore_nums']),
'spellcheck_ignore_caps' => isset($_POST['_spellcheck_ignore_caps']),
'show_sig' => self::prefs_input_int('show_sig'),
'reply_mode' => self::prefs_input_int('reply_mode'),
'sig_below' => isset($_POST['_sig_below']),
'strip_existing_sig' => isset($_POST['_strip_existing_sig']),
'sig_separator' => isset($_POST['_sig_separator']),
'default_font' => self::prefs_input('default_font', '/^[a-zA-Z ]+$/'),
'default_font_size' => self::prefs_input('default_font_size', '/^[0-9]+pt$/'),
'reply_all_mode' => self::prefs_input_int('reply_all_mode'),
'forward_attachment' => !empty($_POST['_forward_attachment']),
'compose_save_localstorage' => self::prefs_input_int('compose_save_localstorage'),
];
break;
case 'addressbook':
$a_user_prefs = [
'default_addressbook' => rcube_utils::get_input_string('_default_addressbook', rcube_utils::INPUT_POST, true),
'collected_recipients' => rcube_utils::get_input_string('_collected_recipients', rcube_utils::INPUT_POST, true),
'collected_senders' => rcube_utils::get_input_string('_collected_senders', rcube_utils::INPUT_POST, true),
'autocomplete_single' => isset($_POST['_autocomplete_single']),
'addressbook_sort_col' => self::prefs_input('addressbook_sort_col', '/^[a-z_]+$/'),
'addressbook_name_listing' => self::prefs_input_int('addressbook_name_listing'),
'addressbook_pagesize' => max(2, self::prefs_input_int('addressbook_pagesize')),
'contact_form_mode' => self::prefs_input('contact_form_mode', '/^(private|business)$/'),
];
break;
case 'server':
$a_user_prefs = [
'read_when_deleted' => isset($_POST['_read_when_deleted']),
'skip_deleted' => isset($_POST['_skip_deleted']),
'flag_for_deletion' => isset($_POST['_flag_for_deletion']),
'delete_junk' => isset($_POST['_delete_junk']),
'logout_purge' => self::prefs_input('logout_purge', '/^(all|never|30|60|90)$/'),
'logout_expunge' => isset($_POST['_logout_expunge']),
];
break;
case 'folders':
$a_user_prefs = [
'show_real_foldernames' => isset($_POST['_show_real_foldernames']),
// stop using SPECIAL-USE (#4782)
'lock_special_folders' => !in_array('lock_special_folders', $dont_override),
];
foreach (rcube_storage::$folder_types as $type) {
$a_user_prefs[$type . '_mbox'] = rcube_utils::get_input_string('_' . $type . '_mbox', rcube_utils::INPUT_POST, true);
};
break;
case 'encryption':
$a_user_prefs = [
'mailvelope_main_keyring' => isset($_POST['_mailvelope_main_keyring']),
];
break;
}
$plugin = rcmail::get_instance()->plugins->exec_hook('preferences_save',
['prefs' => $a_user_prefs, 'section' => $CURR_SECTION]);
$a_user_prefs = $plugin['prefs'];
// don't override these parameters
foreach ($dont_override as $p) {
$a_user_prefs[$p] = $rcmail->config->get($p);
}
// verify some options
switch ($CURR_SECTION) {
case 'general':
// switch UI language
if (isset($_POST['_language']) && $a_user_prefs['language'] != $_SESSION['language']) {
$rcmail->load_language($a_user_prefs['language']);
$rcmail->output->command('reload', 500);
}
// switch skin (if valid, otherwise unset the pref and fall back to default)
if (!empty($a_user_prefs['skin'])) {
if (!$rcmail->output->check_skin($a_user_prefs['skin'])) {
unset($a_user_prefs['skin']);
}
else if ($rcmail->config->get('skin') != $a_user_prefs['skin']) {
$rcmail->output->command('reload', 500);
}
}
$a_user_prefs['timezone'] = (string) $a_user_prefs['timezone'];
$min_refresh_interval = (int) $rcmail->config->get('min_refresh_interval');
if (!empty($a_user_prefs['refresh_interval']) && $min_refresh_interval) {
if ($a_user_prefs['refresh_interval'] < $min_refresh_interval) {
$a_user_prefs['refresh_interval'] = $min_refresh_interval;
}
}
break;
case 'mailbox':
// force min size
if ($a_user_prefs['mail_pagesize'] < 1) {
$a_user_prefs['mail_pagesize'] = 10;
}
$max_pagesize = (int) $rcmail->config->get('max_pagesize');
if ($max_pagesize && ($a_user_prefs['mail_pagesize'] > $max_pagesize)) {
$a_user_prefs['mail_pagesize'] = $max_pagesize;
}
break;
case 'addressbook':
// force min size
if ($a_user_prefs['addressbook_pagesize'] < 1) {
$a_user_prefs['addressbook_pagesize'] = 10;
}
$max_pagesize = (int) $rcmail->config->get('max_pagesize');
if ($max_pagesize && ($a_user_prefs['addressbook_pagesize'] > $max_pagesize)) {
$a_user_prefs['addressbook_pagesize'] = $max_pagesize;
}
break;
case 'folders':
$storage = $rcmail->get_storage();
$specials = [];
foreach (rcube_storage::$folder_types as $type) {
$specials[$type] = $a_user_prefs[$type . '_mbox'];
}
$storage->set_special_folders($specials);
break;
case 'server':
if (isset($a_user_prefs['logout_purge']) && !is_numeric($a_user_prefs['logout_purge'])) {
$a_user_prefs['logout_purge'] = $a_user_prefs['logout_purge'] !== 'never';
}
break;
}
// Save preferences
if (empty($plugin['abort'])) {
$saved = $rcmail->user->save_prefs($a_user_prefs);
}
else {
$saved = $plugin['result'];
}
if ($saved) {
$rcmail->output->show_message('successfullysaved', 'confirmation');
}
else {
$rcmail->output->show_message(!empty($plugin['message']) ? $plugin['message'] : 'errorsaving', 'error');
}
// display the form again
$rcmail->overwrite_action('edit-prefs');
}
/**
* Get option value from POST and validate with a regex
*/
public static function prefs_input($name, $regex)
{
$rcmail = rcmail::get_instance();
$value = rcube_utils::get_input_value('_' . $name, rcube_utils::INPUT_POST);
if (!is_string($value)) {
$value = null;
}
if ($value !== null && strlen($value) && !preg_match($regex, $value)) {
$value = $rcmail->config->get($name);
}
return $value;
}
/**
* Get integer option value from POST
*/
public static function prefs_input_int($name)
{
$rcmail = rcmail::get_instance();
$value = rcube_utils::get_input_value('_' . $name, rcube_utils::INPUT_POST);
return (int) $value;
}
}
@@ -0,0 +1,22 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for a canned response creation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_create extends rcmail_action_settings_response_edit
{
}
@@ -0,0 +1,50 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for canned response deletion |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_delete extends rcmail_action
{
static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
if ($id = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GP)) {
$plugin = $rcmail->plugins->exec_hook('response_delete', ['id' => $id]);
$deleted = !$plugin['abort'] ? $rcmail->user->delete_response($id) : $plugin['result'];
if (!empty($deleted)) {
$rcmail->output->command('display_message', $rcmail->gettext('deletedsuccessfully'), 'confirmation');
$rcmail->output->command('remove_response', $id);
}
else {
$msg = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($msg, 'error');
}
}
$rcmail->output->send();
}
}
@@ -0,0 +1,124 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Show edit form for a canned response record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_edit extends rcmail_action_settings_responses
{
protected static $mode = self::MODE_HTTP;
protected static $response;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$title = $rcmail->gettext($rcmail->action == 'add-response' ? 'addresponse' : 'editresponse');
if (!empty($args['post'])) {
self::$response = $args['post'];
}
else if ($id = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GP)) {
self::$response = $rcmail->get_compose_response($id);
if (!is_array(self::$response)) {
$rcmail->output->show_message('dberror', 'error');
$rcmail->output->send('iframe');
}
}
$rcmail->output->set_pagetitle($title);
$rcmail->output->set_env('readonly', !empty(self::$response['static']));
$rcmail->output->add_handler('responseform', [$this, 'response_form']);
$rcmail->output->send('responseedit');
}
/**
* Get content of a response editing/adding form
*
* @param array $attrib Template object attributes
*
* @return string HTML content
*/
public static function response_form($attrib)
{
$rcmail = rcmail::get_instance();
// add some labels to client
$rcmail->output->add_label('converting', 'editorwarning');
// Set form tags and hidden fields
$readonly = !empty(self::$response['static']);
$is_html = self::$response['is_html'] ?? false;
$id = self::$response['id'] ?? '';
$hidden = ['name' => '_id', 'value' => $id];
list($form_start, $form_end) = self::get_form_tags($attrib, 'save-response', $id, $hidden);
unset($attrib['form'], $attrib['id']);
$name_attr = [
'id' => 'ffname',
'size' => $attrib['size'] ?? null,
'readonly' => $readonly,
'required' => true,
];
$text_attr = [
'id' => 'fftext',
'size' => $attrib['textareacols'] ?? null,
'rows' => $attrib['textarearows'] ?? null,
'readonly' => $readonly,
'spellcheck' => true,
'data-html-editor' => true
];
$chk_attr = [
'id' => 'ffis_html',
'disabled' => $readonly,
'onclick' => "return rcmail.command('toggle-editor', {id: 'fftext', html: this.checked}, '', event)"
];
// Add HTML editor script(s)
self::html_editor('response', 'fftext');
// Enable TinyMCE editor
if ($is_html) {
$text_attr['class'] = 'mce_editor';
$text_attr['is_escaped'] = true;
// Correctly handle HTML entities in HTML editor (#1488483)
self::$response['data'] = htmlspecialchars(self::$response['data'], ENT_NOQUOTES, RCUBE_CHARSET);
}
$table = new html_table(['cols' => 2]);
$table->add('title', html::label('ffname', rcube::Q($rcmail->gettext('responsename'))));
$table->add(null, rcube_output::get_edit_field('name', self::$response['name'] ?? '', $name_attr, 'text'));
$table->add('title', html::label('fftext', rcube::Q($rcmail->gettext('responsetext'))));
$table->add(null, rcube_output::get_edit_field('text', self::$response['data'] ?? '', $text_attr, 'textarea'));
$table->add('title', html::label('ffis_html', rcube::Q($rcmail->gettext('htmltoggle'))));
$table->add(null, rcube_output::get_edit_field('is_html', $is_html, $chk_attr, 'checkbox'));
// return the complete edit form as table
return "$form_start\n" . $table->show($attrib) . $form_end;
}
}
@@ -0,0 +1,59 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for fetching a canned response content |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_get extends rcmail_action
{
static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$id = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GET);
if ($id && ($response = $rcmail->get_compose_response($id))) {
$is_html = (bool) rcube_utils::get_input_string('_is_html', rcube_utils::INPUT_GET);
if ($is_html && empty($response['is_html'])) {
$converter = new rcube_text2html($response['data'], false, ['wrap' => true]);
$response['data'] = $converter->get_html();
$response['is_html'] = true;
}
else if (!$is_html && !empty($response['is_html'])) {
$params = [
'width' => $rcmail->config->get('line_length', 72),
'links' => false,
];
$response['data'] = $rcmail->html2text($response['data'], $params);
$response['is_html'] = false;
}
$rcmail->output->command('insert_response', $response);
}
$rcmail->output->send();
}
}
@@ -0,0 +1,112 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| A handler for saving a canned response record |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_response_save extends rcmail_action_settings_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$id = trim(rcube_utils::get_input_string('_id', rcube_utils::INPUT_POST));
$name = trim(rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST));
$text = trim(rcube_utils::get_input_string('_text', rcube_utils::INPUT_POST, true));
$is_html = (bool) rcube_utils::get_input_string('_is_html', rcube_utils::INPUT_POST);
$response = [
'id' => $id,
'name' => $name,
'data' => $text,
'is_html' => $is_html,
];
if (!empty($text) && $is_html) {
// replace uploaded images with data URIs
$text = self::attach_images($text, 'response');
// XSS protection in HTML signature (#1489251)
$text = self::wash_html($text);
$response['data'] = $text;
}
if (empty($name) || empty($text)) {
// TODO: error
$rcmail->output->show_message('formincomplete', 'error');
$rcmail->overwrite_action('edit-response', ['post' => $response]);
return;
}
if (!empty($id) && is_numeric($id)) {
$plugin = $rcmail->plugins->exec_hook('response_update', ['id' => $id, 'record' => $response]);
$response = $plugin['record'];
if (!$plugin['abort']) {
$updated = $rcmail->user->update_response($id, $response);
}
else {
$updated = $plugin['result'];
}
if ($updated) {
$rcmail->output->show_message('successfullysaved', 'confirmation');
$rcmail->output->command('parent.update_response_row', $id, rcube::Q($response['name']));
}
else {
// show error message
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error', null, false);
}
}
else {
$plugin = $rcmail->plugins->exec_hook('response_create', ['record' => $response]);
$response = $plugin['record'];
if (!$plugin['abort']) {
$insert_id = $rcmail->user->insert_response($response);
}
else {
$insert_id = $plugin['result'];
}
if ($insert_id) {
$rcmail->output->show_message('successfullysaved', 'confirmation');
$response['id'] = $_GET['_id'] = $insert_id;
// add a new row to the list
$rcmail->output->command('parent.update_response_row', $insert_id, rcube::Q($response['name']), true);
}
else {
$error = !empty($plugin['message']) ? $plugin['message'] : 'errorsaving';
$rcmail->output->show_message($error, 'error', null, false);
$rcmail->overwrite_action('add-response');
return;
}
}
// display the form again
$rcmail->overwrite_action('edit-response', ['post' => $response]);
}
}
@@ -0,0 +1,73 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Listing of canned responses, and quick insert action handler |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_responses extends rcmail_action_settings_index
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$rcmail->output->set_pagetitle($rcmail->gettext('responses'));
$rcmail->output->include_script('list.js');
$rcmail->output->add_label('deleteresponseconfirm');
$rcmail->output->add_handlers(['responseslist' => [$this, 'responses_list']]);
$rcmail->output->send('responses');
}
/**
* Create template object 'responseslist'
*
* @param array $attrib Object attributes
*
* @return string HTML table output
*/
public static function responses_list($attrib)
{
$rcmail = rcmail::get_instance();
$attrib += ['id' => 'rcmresponseslist', 'tagname' => 'table'];
$plugin = $rcmail->plugins->exec_hook('responses_list', [
'list' => $rcmail->get_compose_responses(),
'cols' => ['name']
]);
$out = self::table_output($attrib, $plugin['list'], $plugin['cols'], 'id');
$readonly_responses = [];
foreach ($plugin['list'] as $item) {
if (!empty($item['static'])) {
$readonly_responses[] = $item['id'];
}
}
// set client env
$rcmail->output->add_gui_object('responseslist', $attrib['id']);
$rcmail->output->set_env('readonly_responses', $readonly_responses);
return $out;
}
}
@@ -0,0 +1,122 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Handles image uploads |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_upload extends rcmail_action
{
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$from = rcube_utils::get_input_string('_from', rcube_utils::INPUT_GET);
$type = preg_replace('/(add|edit)-/', '', $from);
// Plugins in Settings may use this file for some uploads (#5694)
// Make sure it does not contain a dot, which is a special character
// when using rcube_session::append() below
$type = str_replace('.', '-', $type);
// Supported image format types
$IMAGE_TYPES = explode(',', 'jpeg,jpg,jp2,tiff,tif,bmp,eps,gif,png,png8,png24,png32,svg,ico');
// clear all stored output properties (like scripts and env vars)
$rcmail->output->reset();
$max_size = $rcmail->config->get($type . '_image_size', 64) * 1024;
$uploadid = rcube_utils::get_input_string('_uploadid', rcube_utils::INPUT_GET);
if (!empty($_FILES['_file']['tmp_name']) && is_array($_FILES['_file']['tmp_name'])) {
$multiple = count($_FILES['_file']['tmp_name']) > 1;
foreach ($_FILES['_file']['tmp_name'] as $i => $filepath) {
$err = $_FILES['_file']['error'][$i];
$imageprop = null;
$attachment = null;
// Process uploaded attachment if there is no error
if (!$err) {
if ($max_size < $_FILES['_file']['size'][$i]) {
$err = 'size_error';
}
// check image file type
else {
$image = new rcube_image($filepath);
$imageprop = $image->props();
if (!in_array(strtolower($imageprop['type']), $IMAGE_TYPES)) {
$err = 'type_error';
}
}
}
// save uploaded image in storage backend
if (!empty($imageprop)) {
$attachment = $rcmail->plugins->exec_hook('attachment_upload', [
'path' => $filepath,
'size' => $_FILES['_file']['size'][$i],
'name' => $_FILES['_file']['name'][$i],
'mimetype' => 'image/' . $imageprop['type'],
'group' => $type,
]);
}
if (!$err && !empty($attachment['status']) && empty($attachment['abort'])) {
$id = $attachment['id'];
// store new file in session
unset($attachment['status'], $attachment['abort']);
$rcmail->session->append($type . '.files', $id, $attachment);
$content = rcube::Q($attachment['name']);
$rcmail->output->command('add2attachment_list', "rcmfile$id", [
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
'complete' => true
],
$uploadid
);
}
else {
$error_label = null;
if ($err == 'type_error') {
$error_label = 'invalidimageformat';
}
else if ($err == 'size_error') {
$error_label = ['name' => 'filesizeerror', 'vars' => ['size' => self::show_bytes($max_size)]];
}
self::upload_error($err, $attachment, $error_label);
}
}
}
else if (self::upload_failure()) {
$rcmail->output->command('remove_from_attachment_list', $uploadid);
}
$rcmail->output->send('iframe');
}
}
@@ -0,0 +1,49 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Displaying uploaded images |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_settings_upload_display extends rcmail_action
{
protected static $mode = self::MODE_HTTP;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$from = rcube_utils::get_input_string('_from', rcube_utils::INPUT_GET);
$type = preg_replace('/(add|edit)-/', '', $from);
// Plugins in Settings may use this file for some uploads (#5694)
// Make sure it does not contain a dot, which is a special character
// when using rcube_session::append() below
$type = str_replace('.', '-', $type);
$id = 'undefined';
if (preg_match('/^rcmfile(\w+)$/', $_GET['_file'], $regs)) {
$id = $regs[1];
}
self::display_uploaded_file($_SESSION[$type]['files'][$id]);
exit;
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Display error message page |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_error extends rcmail_action
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$ERROR_CODE = !empty($args['code']) ? $args['code'] : 500;
$ERROR_MESSAGE = !empty($args['message']) ? $args['message'] : null;
// authorization error
if ($ERROR_CODE == 401) {
$error_title = $rcmail->gettext('errauthorizationfailed');
$error_text = nl2br($rcmail->gettext('errunauthorizedexplain')
. "\n" . $rcmail->gettext('errcontactserveradmin'));
}
// forbidden due to request check
else if ($ERROR_CODE == 403) {
if ($_SERVER['REQUEST_METHOD'] == 'GET' && $rcmail->request_status == rcube::REQUEST_ERROR_URL) {
$url = $rcmail->url($_GET, true, false, true);
$add = html::a($url, $rcmail->gettext('clicktoresumesession'));
}
else {
$add = $rcmail->gettext('errcontactserveradmin');
}
$error_title = $rcmail->gettext('errrequestcheckfailed');
$error_text = nl2br($rcmail->gettext('errcsrfprotectionexplain')) . '<p>' . $add . '</p>';
}
// failed request (wrong step in URL)
else if ($ERROR_CODE == 404) {
$request_url = htmlentities($_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']);
$error_title = $rcmail->gettext('errnotfound');
$error_text = nl2br($rcmail->gettext('errnotfoundexplain')
. "\n" . $rcmail->gettext('errcontactserveradmin'));
$error_text .= '<p><i>' . $rcmail->gettext('errfailedrequest') . ": $request_url</i></p>";
}
// Gone, e.g. message cached but not in the storage
else if ($ERROR_CODE == 410) {
$error_title = $rcmail->gettext('servererror');
$error_text = $rcmail->gettext('messageopenerror');
}
// invalid compose ID
else if ($ERROR_CODE == 450 && $_SERVER['REQUEST_METHOD'] == 'GET' && $rcmail->action == 'compose') {
$url = $rcmail->url('compose');
$error_title = $rcmail->gettext('errcomposesession');
$error_text = nl2br($rcmail->gettext('errcomposesessionexplain'))
. '<p>' . html::a($url, $rcmail->gettext('clicktocompose')) . '</p>';
}
// database connection error
else if ($ERROR_CODE == 601) {
$error_title = "Configuration error";
$error_text = nl2br($ERROR_MESSAGE) . "<br />Please read the INSTALL instructions!";
}
// database connection error
else if ($ERROR_CODE == 603) {
$error_title = $rcmail->gettext('dberror');
$error_text = nl2br($rcmail->gettext('dbconnerror') . "\n" . $rcmail->gettext('errcontactserveradmin'));
}
// system error
else {
$error_title = $rcmail->gettext('servererror');
$error_text = sprintf('Error No. [%s]', $ERROR_CODE);
}
// inform plugins
if ($rcmail->plugins) {
$plugin = $rcmail->plugins->exec_hook('error_page', [
'code' => $ERROR_CODE,
'title' => $error_title,
'text' => $error_text,
]);
if (!empty($plugin['title'])) {
$error_title = $plugin['title'];
}
if (!empty($plugin['text'])) {
$error_text = $plugin['text'];
}
}
$HTTP_ERR_CODE = $ERROR_CODE && $ERROR_CODE < 600 ? $ERROR_CODE : 500;
// Ajax request
if ($rcmail->output && $rcmail->output->type == 'js') {
$rcmail->output->sendExit('', ["HTTP/1.0 $HTTP_ERR_CODE $error_title"]);
}
// compose page content
$page_content = '<div class="boxerror">'
.'<h3 class="error-title">' . mb_strtoupper($error_title) . '</h3>'
.'<div class="error-text">' . $error_text . '</div>'
.'</div>';
if ($rcmail->output && $rcmail->output->template_exists('error')) {
$GLOBALS['__page_content'] = $page_content;
$task = empty($rcmail->user) || empty($rcmail->user->ID) ? '-login' : '';
$rcmail->output->reset();
$rcmail->output->set_env('error_task', 'error' . $task);
$rcmail->output->set_env('server_error', $ERROR_CODE);
$rcmail->output->set_env('comm_path', $rcmail->comm_path);
$rcmail->output->send('error');
}
$skin = $rcmail->config->get('skin', 'default');
$product = $rcmail->config->get('product_name', 'Roundcube Webmail');
$output = '<!doctype html><html><head>'
. '<title>' . $product . ':: ERROR</title>'
. '<link rel="stylesheet" type="text/css" href="skins/$skin/common.css" />'
. '</head><body>'
. '<table border="0" cellspacing="0" cellpadding="0" width="100%" height="80%">'
. '<tr><td align="center">' . $page_content . '</td></tr>'
. '</table>'
. '</body></html>';
$rcmail->output->sendExit($output);
}
}
@@ -0,0 +1,41 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Convert HTML message to plain text |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_html2text extends rcmail_action
{
public static $source = 'php://input';
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$html = file_get_contents(self::$source);
$params['links'] = (bool) rcube_utils::get_input_value('_do_links', rcube_utils::INPUT_GET);
$params['width'] = (int) rcube_utils::get_input_value('_width', rcube_utils::INPUT_GET);
$rcmail = rcmail::get_instance();
$text = $rcmail->html2text($html, $params);
$rcmail->output->sendExit($text, ['Content-Type: text/plain; charset=' . RCUBE_CHARSET]);
}
}
@@ -0,0 +1,66 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Delete rows from cache tables |
+-----------------------------------------------------------------------+
| Author: Dennis P. Nikolaenko <dennis@nikolaenko.ru> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_killcache extends rcmail_action
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
// don't allow public access if not in devel_mode
if (!$rcmail->config->get('devel_mode')) {
header("HTTP/1.0 401 Access denied");
die("Access denied!");
}
// @TODO: transaction here (if supported by DB) would be a good thing
$res = $rcmail->db->query("DELETE FROM " . $rcmail->db->table_name('cache', true));
if ($err = $rcmail->db->is_error($res)) {
exit($err);
}
$res = $rcmail->db->query("DELETE FROM " . $rcmail->db->table_name('cache_shared', true));
if ($err = $rcmail->db->is_error($res)) {
exit($err);
}
$res = $rcmail->db->query("DELETE FROM " . $rcmail->db->table_name('cache_messages', true));
if ($err = $rcmail->db->is_error($res)) {
exit($err);
}
$res = $rcmail->db->query("DELETE FROM " . $rcmail->db->table_name('cache_index', true));
if ($err = $rcmail->db->is_error($res)) {
exit($err);
}
$res = $rcmail->db->query("DELETE FROM " . $rcmail->db->table_name('cache_thread', true));
if ($err = $rcmail->db->is_error($res)) {
exit($err);
}
echo "Cache cleared\n";
exit;
}
}
@@ -0,0 +1,73 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Modify CSS source from a URL |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_modcss extends rcmail_action
{
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$url = preg_replace('![^a-z0-9.-]!i', '', $_GET['_u']);
if ($url === null || !($realurl = $_SESSION['modcssurls'][$url])) {
header('HTTP/1.1 403 Forbidden');
exit("Unauthorized request");
}
// don't allow any other connections than http(s)
if (!preg_match('~^https?://~i', $realurl, $matches)) {
header('HTTP/1.1 403 Forbidden');
exit("Invalid URL");
}
$source = false;
$ctype = null;
try {
$client = rcube::get_instance()->get_http_client();
$response = $client->get($realurl);
if (!empty($response)) {
$ctype = $response->getHeader('Content-Type');
$ctype = !empty($ctype) ? $ctype[0] : '';
$source = $response->getBody();
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
}
$ctype_regexp = '~^text/(css|plain)~i';
$container_id = isset($_GET['_c']) ? preg_replace('/[^a-z0-9]/i', '', $_GET['_c']) : '';
$css_prefix = isset($_GET['_p']) ? preg_replace('/[^a-z0-9]/i', '', $_GET['_p']) : '';
if ($source !== false && $ctype && preg_match($ctype_regexp, $ctype)) {
header('Content-Type: text/css');
echo rcube_utils::mod_css_styles($source, $container_id, false, $css_prefix);
exit;
}
header('HTTP/1.0 404 Not Found');
exit("Invalid response returned by server");
}
}
@@ -0,0 +1,90 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Save preferences setting in database |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_save_pref extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$name = rcube_utils::get_input_string('_name', rcube_utils::INPUT_POST);
$value = rcube_utils::get_input_value('_value', rcube_utils::INPUT_POST);
$sessname = rcube_utils::get_input_string('_session', rcube_utils::INPUT_POST);
// Whitelisted preferences and session variables, others
// can be added by plugins
$whitelist = [
'list_cols',
'collapsed_folders',
'collapsed_abooks',
];
$whitelist_sess = [
'list_attrib/columns',
];
$whitelist = array_merge($whitelist, $rcmail->plugins->allowed_prefs);
$whitelist_sess = array_merge($whitelist_sess, $rcmail->plugins->allowed_session_prefs);
if (!in_array($name, $whitelist) || ($sessname && !in_array($sessname, $whitelist_sess))) {
rcube::raise_error([
'code' => 500,
'file' => __FILE__,
'line' => __LINE__,
'message' => sprintf("Hack attempt detected (user: %s)", $rcmail->get_user_name())
],
true,
false
);
$rcmail->output->reset();
$rcmail->output->send();
}
// save preference value
$rcmail->user->save_prefs([$name => $value]);
// update also session if requested
if ($sessname) {
// Support multidimensional arrays...
$vars = explode('/', $sessname);
// ... up to 3 levels
if (count($vars) == 1) {
$_SESSION[$vars[0]] = $value;
}
else if (count($vars) == 2) {
$_SESSION[$vars[0]][$vars[1]] = $value;
}
else if (count($vars) == 3) {
$_SESSION[$vars[0]][$vars[1]][$vars[2]] = $value;
}
}
$rcmail->output->reset();
$rcmail->output->send();
}
}
@@ -0,0 +1,81 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Invoke the configured or default spell checking engine. |
+-----------------------------------------------------------------------+
| Author: Kris Steinhoff <steinhof@umich.edu> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_spell extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
// read input
$lang = rcube_utils::get_input_string('lang', rcube_utils::INPUT_GET);
$data = file_get_contents('php://input');
$learn_word = strpos($data, '<learnword>');
// Get data string
$left = strpos($data, '<text>');
$right = strrpos($data, '</text>');
$data = substr($data, $left+6, $right-($left+6));
$data = html_entity_decode($data, ENT_QUOTES, RCUBE_CHARSET);
$spellchecker = new rcube_spellchecker($lang);
if ($learn_word) {
$spellchecker->add_word($data);
$result = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><learnwordresult></learnwordresult>';
}
else if (empty($data)) {
$result = '<?xml version="1.0" encoding="'.RCUBE_CHARSET.'"?><spellresult charschecked="0"></spellresult>';
}
else {
$spellchecker->check($data);
$result = $spellchecker->get_xml();
}
if ($error = $spellchecker->error()) {
rcube::raise_error([
'code' => 500,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Spellcheck error: " . $error
],
true,
false
);
header("HTTP/1.0 500 Internal Server Error");
exit;
}
// set response length
header("Content-Length: " . strlen($result));
// Don't use server's default Content-Type charset (#1486406)
header("Content-Type: text/xml; charset=" . RCUBE_CHARSET);
print $result;
exit;
}
}
@@ -0,0 +1,76 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Spellchecker for TinyMCE |
+-----------------------------------------------------------------------+
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_spell_html extends rcmail_action
{
// only process ajax requests
protected static $mode = self::MODE_AJAX;
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$rcmail = rcmail::get_instance();
$method = rcube_utils::get_input_string('method', rcube_utils::INPUT_POST);
$lang = rcube_utils::get_input_string('lang', rcube_utils::INPUT_POST);
$result = [];
$spellchecker = new rcube_spellchecker($lang);
if ($method == 'addToDictionary') {
$data = rcube_utils::get_input_string('word', rcube_utils::INPUT_POST);
$spellchecker->add_word($data);
$result['result'] = true;
}
else {
$data = rcube_utils::get_input_string('text', rcube_utils::INPUT_POST, true);
$data = html_entity_decode($data, ENT_QUOTES, RCUBE_CHARSET);
if ($data && !$spellchecker->check($data)) {
$result['words'] = $spellchecker->get();
$result['dictionary'] = (bool) $rcmail->config->get('spellcheck_dictionary');
}
}
header("Content-Type: application/json; charset=" . RCUBE_CHARSET);
if ($error = $spellchecker->error()) {
rcube::raise_error([
'code' => 500,
'file' => __FILE__,
'line' => __LINE__,
'message' => "Spellcheck error: " . $error
],
true,
false
);
echo json_encode(['error' => $rcmail->gettext('internalerror')]);
exit;
}
// send output
echo json_encode($result);
exit;
}
}
@@ -0,0 +1,41 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Convert plain text to HTML |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
class rcmail_action_utils_text2html extends rcmail_action
{
public static $source = 'php://input';
/**
* Request handler.
*
* @param array $args Arguments from the previous step(s)
*/
public function run($args = [])
{
$text = file_get_contents(self::$source);
$converter = new rcube_text2html($text, false, ['wrap' => true]);
$rcmail = rcmail::get_instance();
$html = $converter->get_html();
$rcmail->output->sendExit($html, ['Content-Type: text/html; charset=' . RCUBE_CHARSET]);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Setup the command line environment and provide some utility |
| functions. |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (php_sapi_name() != 'cli') {
die('Not on the "shell" (php-cli).');
}
require_once INSTALL_PATH . 'program/include/iniset.php';
// Unset max. execution time limit, set to 120 seconds in iniset.php
@set_time_limit(0);
$rcmail = rcmail::get_instance();
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Setup the application environment required to process |
| any request. |
+-----------------------------------------------------------------------+
| Author: Till Klampaeckel <till@php.net> |
| Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
if (PHP_VERSION_ID < 70300) {
die("Unsupported PHP version. Required PHP >= 7.3.");
}
// application constants
define('RCMAIL_VERSION', '1.6.3');
define('RCMAIL_START', microtime(true));
if (!defined('INSTALL_PATH')) {
define('INSTALL_PATH', dirname($_SERVER['SCRIPT_FILENAME']).'/');
}
if (!defined('RCMAIL_CONFIG_DIR')) {
define('RCMAIL_CONFIG_DIR', getenv('ROUNDCUBE_CONFIG_DIR') ?: (INSTALL_PATH . 'config'));
}
if (!defined('RCUBE_LOCALIZATION_DIR')) {
define('RCUBE_LOCALIZATION_DIR', INSTALL_PATH . 'program/localization/');
}
define('RCUBE_INSTALL_PATH', INSTALL_PATH);
define('RCUBE_CONFIG_DIR', RCMAIL_CONFIG_DIR.'/');
// Show basic error message on fatal PHP error
register_shutdown_function('rcmail_error_handler');
// RC include folders MUST be included FIRST to avoid other
// possible not compatible libraries (i.e PEAR) to be included
// instead the ones provided by RC
$include_path = INSTALL_PATH . 'program/lib' . PATH_SEPARATOR;
$include_path.= ini_get('include_path');
if (set_include_path($include_path) === false) {
die("Fatal error: ini_set/set_include_path does not work.");
}
// increase maximum execution time for php scripts
// (does not work in safe mode)
@set_time_limit(120);
// include composer autoloader (if available)
if (@file_exists(INSTALL_PATH . 'vendor/autoload.php')) {
require INSTALL_PATH . 'vendor/autoload.php';
}
// translate PATH_INFO to _task and _action GET parameters
if (!empty($_SERVER['PATH_INFO']) && preg_match('!^/([a-z]+)/([a-z]+)$!', $_SERVER['PATH_INFO'], $m)) {
if (!isset($_GET['_task'])) {
$_GET['_task'] = $m[1];
}
if (!isset($_GET['_action'])) {
$_GET['_action'] = $m[2];
}
}
// include Roundcube Framework
require_once 'Roundcube/bootstrap.php';
// register autoloader for rcmail app classes
spl_autoload_register('rcmail_autoload');
/**
* PHP5 autoloader routine for dynamic class loading
*/
function rcmail_autoload($classname)
{
if (strpos($classname, 'rcmail') === 0) {
if (preg_match('/^rcmail_action_([^_]+)_(.*)$/', $classname, $matches)) {
$filepath = INSTALL_PATH . "program/actions/{$matches[1]}/{$matches[2]}.php";
}
else {
$filepath = INSTALL_PATH . "program/include/$classname.php";
}
if (is_readable($filepath)) {
include_once $filepath;
return true;
}
}
return false;
}
/**
* Show a generic error message on fatal PHP error
*/
function rcmail_error_handler()
{
$error = error_get_last();
if ($error && ($error['type'] === E_ERROR || $error['type'] === E_PARSE)) {
rcmail_fatal_error();
}
}
/**
* Raise a generic error message on error
*/
function rcmail_fatal_error()
{
if (php_sapi_name() === 'cli') {
echo "Fatal error: Please check the Roundcube error log and/or server error logs for more information.\n";
}
elseif (!empty($_REQUEST['_remote'])) {
// Ajax request from UI
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(['code' => 500, 'message' => "Internal Server Error"]);
}
else {
if (!defined('RCUBE_FATAL_ERROR_MSG')) {
define('RCUBE_FATAL_ERROR_MSG', INSTALL_PATH . 'program/resources/error.html');
}
echo file_get_contents(RCUBE_FATAL_ERROR_MSG);
}
exit;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,393 @@
<?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: |
| Unified access to attachment properties and body |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Unified access to attachment properties and body
* Unified for message parts as well as uploaded attachments
*
* @package Webmail
*/
class rcmail_attachment_handler
{
public $filename;
public $size;
public $mimetype;
public $ident;
public $charset = RCUBE_CHARSET;
private $message;
private $part;
private $upload;
private $body;
private $body_file;
private $download = false;
/**
* Class constructor.
* Reads request parameters and initializes attachment/part props.
*/
public function __construct()
{
ob_end_clean();
$part_id = rcube_utils::get_input_string('_part', rcube_utils::INPUT_GET);
$file_id = rcube_utils::get_input_string('_file', rcube_utils::INPUT_GET);
$compose_id = rcube_utils::get_input_string('_id', rcube_utils::INPUT_GET);
$uid = rcube_utils::get_input_string('_uid', rcube_utils::INPUT_GET);
$rcube = rcube::get_instance();
$this->download = !empty($_GET['_download']);
// similar code as in program/steps/mail/show.inc
if (!empty($uid)) {
$rcube->config->set('prefer_html', true);
$this->message = new rcube_message($uid, null, !empty($_GET['_safe']));
if ($this->part = $this->message->mime_parts[$part_id]) {
$this->filename = rcmail_action_mail_index::attachment_name($this->part);
$this->mimetype = $this->part->mimetype;
$this->size = $this->part->size;
$this->ident = $this->message->headers->messageID . ':' . $this->part->mime_id . ':' . $this->size . ':' . $this->mimetype;
$this->charset = $this->part->charset ?: RCUBE_CHARSET;
if (empty($_GET['_frame'])) {
// allow post-processing of the attachment body
$plugin = $rcube->plugins->exec_hook('message_part_get', [
'uid' => $uid,
'id' => $this->part->mime_id,
'mimetype' => $this->mimetype,
'part' => $this->part,
'download' => $this->download,
]);
if ($plugin['abort']) {
exit;
}
// overwrite modified vars from plugin
$this->mimetype = $plugin['mimetype'];
if (!empty($plugin['body'])) {
$this->body = $plugin['body'];
$this->size = strlen($this->body);
}
}
}
}
else if ($file_id && $compose_id) {
$file_id = preg_replace('/^rcmfile/', '', $file_id);
if (($compose = $_SESSION['compose_data_' . $compose_id])
&& ($this->upload = $compose['attachments'][$file_id])
) {
$this->filename = $this->upload['name'];
$this->mimetype = $this->upload['mimetype'];
$this->size = $this->upload['size'];
$this->ident = sprintf('%s:%s%s', $compose_id, $file_id, $this->size);
$this->charset = !empty($this->upload['charset']) ? $this->upload['charset'] : RCUBE_CHARSET;
}
}
if (empty($this->part) && empty($this->upload)) {
header('HTTP/1.1 404 Not Found');
exit;
}
// check connection status
self::check_storage_status();
$this->mimetype = rcube_mime::fix_mimetype($this->mimetype);
}
/**
* Remove temp files, etc.
*/
public function __destruct()
{
if ($this->body_file) {
@unlink($this->body_file);
}
}
/**
* Check if the object is a message part not uploaded file
*
* @return bool True if the object is a message part
*/
public function is_message_part()
{
return !empty($this->message);
}
/**
* Object/request status
*
* @return bool Status
*/
public function is_valid()
{
return !empty($this->part) || !empty($this->upload);
}
/**
* Return attachment/part mimetype if this is an image
* of supported type.
*
* @return string Image mimetype
*/
public function image_type()
{
$part = (object) [
'filename' => $this->filename,
'mimetype' => $this->mimetype,
];
return rcmail_action_mail_index::part_image_type($part);
}
/**
* Formatted attachment/part size (with units)
*
* @return string Attachment/part size (with units)
*/
public function size()
{
$part = $this->part ?: ((object) ['size' => $this->size, 'exact_size' => true]);
return rcmail_action::message_part_size($part);
}
/**
* Returns, prints or saves the attachment/part body
*/
public function body($size = null, $fp = null)
{
// we may have the body in memory or file already
if ($this->body !== null) {
if ($fp == -1) {
echo $size ? substr($this->body, 0, $size) : $this->body;
}
else if ($fp) {
$result = fwrite($fp, $size ? substr($this->body, $size) : $this->body) !== false;
}
else {
$result = $size ? substr($this->body, 0, $size) : $this->body;
}
}
else if ($this->body_file) {
if ($size) {
$result = file_get_contents($this->body_file, false, null, 0, $size);
}
else {
$result = file_get_contents($this->body_file);
}
if ($fp == -1) {
echo $result;
}
else if ($fp) {
$result = fwrite($fp, $result) !== false;
}
}
else if ($this->message) {
$result = $this->message->get_part_body($this->part->mime_id, false, 0, $fp);
// check connection status
if (!$fp && $this->size && empty($result)) {
self::check_storage_status();
}
}
else if ($this->upload) {
// This hook retrieves the attachment contents from the file storage backend
$attachment = rcube::get_instance()->plugins->exec_hook('attachment_get', $this->upload);
if ($fp && $fp != -1) {
if ($attachment['data']) {
$result = fwrite($fp, $size ? substr($attachment['data'], 0, $size) : $attachment['data']) !== false;
}
else if ($attachment['path']) {
if ($fh = fopen($attachment['path'], 'rb')) {
$result = stream_copy_to_stream($fh, $fp, $size ? $size : -1);
}
}
}
else {
$data = $attachment['data'] ?? '';
if (!$data && $attachment['path']) {
$data = file_get_contents($attachment['path']);
}
if ($fp == -1) {
echo $size ? substr($data, 0, $size) : $data;
}
else {
$result = $size ? substr($data, 0, $size) : $data;
}
}
}
return $result ?? null;
}
/**
* Save the body to a file
*
* @param string $filename File name with path
*
* @return bool True on success, False on failure
*/
public function body_to_file($filename)
{
if ($filename && $this->size && ($fp = fopen($filename, 'w'))) {
$this->body(0, $fp);
$this->body_file = $filename;
fclose($fp);
@chmod($filename, 0600);
return true;
}
return false;
}
/**
* Output attachment body with content filtering
*/
public function output($mimetype)
{
if (!$this->size) {
return false;
}
$secure = stripos($mimetype, 'image/') === false || $this->download;
// Remove <script> in SVG images
if (!$secure && stripos($mimetype, 'image/svg') === 0) {
if (!$this->body) {
$this->body = $this->body();
if (empty($this->body)) {
return false;
}
}
echo self::svg_filter($this->body);
return true;
}
if ($this->body !== null && !$this->download) {
header("Content-Length: " . strlen($this->body));
echo $this->body;
return true;
}
// Don't be tempted to set Content-Length to $part->d_parameters['size'] (#1490482)
// RFC2183 says "The size parameter indicates an approximate size"
return $this->body(0, -1);
}
/**
* Returns formatted HTML if the attachment is HTML
*/
public function html()
{
list($type, $subtype) = explode('/', $this->mimetype);
$part = (object) [
'charset' => $this->charset,
'ctype_secondary' => $subtype,
];
// get part body if not available
// fix formatting and charset
$body = rcube_message::format_part_body($this->body(), $part);
// show images?
$is_safe = $this->is_safe();
return rcmail_action_mail_index::wash_html($body, ['safe' => $is_safe, 'inline_html' => false]);
}
/**
* Remove <script> in SVG images
*/
public static function svg_filter($body)
{
// clean SVG with washtml
$wash_opts = [
'show_washed' => false,
'allow_remote' => false,
'charset' => RCUBE_CHARSET,
'html_elements' => ['title'],
];
// initialize HTML washer
$washer = new rcube_washtml($wash_opts);
// allow CSS styles, will be sanitized by rcmail_washtml_callback()
$washer->add_callback('style', 'rcmail_action_mail_index::washtml_callback');
return $washer->wash($body);
}
/**
* Handles nicely storage connection errors
*/
public static function check_storage_status()
{
$error = rcmail::get_instance()->storage->get_error_code();
// Check if we have a connection error
if ($error == rcube_imap_generic::ERROR_BAD) {
ob_end_clean();
// Get action is often executed simultaneously.
// Some servers have MAXPERIP or other limits.
// To workaround this we'll wait for some time
// and try again (once).
// Note: Random sleep interval is used to minimize concurrency
// in getting message parts
if (!isset($_GET['_redirected'])) {
usleep(rand(10, 30) * 100000); // 1-3 sec.
header('Location: ' . $_SERVER['REQUEST_URI'] . '&_redirected=1');
}
else {
rcube::raise_error([
'code' => 500, 'file' => __FILE__, 'line' => __LINE__,
'message' => 'Unable to get/display message part. IMAP connection error'
],
true, true
);
}
// Don't kill session, just quit (#1486995)
exit;
}
}
public function is_safe()
{
if ($this->message) {
return rcmail_action_mail_index::check_safe($this->message);
}
return !empty($_GET['_safe']);
}
}
@@ -0,0 +1,87 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Render a simple HTML page with the given contents |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class to create an empty HTML page with some default styles
*
* @package Webmail
* @subpackage View
*/
class rcmail_html_page extends rcmail_output_html
{
protected $inline_warning;
/**
* Process the page content and write to stdOut
*
* @param string $contents HTML page content
*/
public function write($contents = '')
{
self::reset(true);
// load embed.css from skin folder (if exists)
$embed_css = $this->config->get('embed_css_location', '/embed.css');
if ($embed_css = $this->get_skin_file($embed_css, $path, null, true)) {
$this->include_css($embed_css);
}
else { // set default styles for warning blocks inside the attachment part frame
$this->add_header(html::tag('style', ['type' => 'text/css'],
".rcmail-inline-message { font-family: sans-serif; border:2px solid #ffdf0e;"
. "background:#fef893; padding:0.6em 1em; margin-bottom:0.6em }\n" .
".rcmail-inline-buttons { margin-bottom:0 }"
));
}
if (empty($contents)) {
$contents = '<html><body></body></html>';
}
if ($this->inline_warning) {
$body_start = 0;
if ($body_pos = strpos($contents, '<body')) {
$body_start = strpos($contents, '>', $body_pos) + 1;
}
$contents = substr_replace($contents, $this->inline_warning, $body_start, 0);
}
parent::write($contents);
}
/**
* Add inline warning with optional button
*
* @param string $text Warning content
* @param string $button_label Button label
* @param string $button_url Button URL
*/
public function register_inline_warning($text, $button_label = null, $button_url = null)
{
$text = html::span(null, $text);
if ($button_label) {
$onclick = "location.href = '$button_url'";
$button = html::tag('button', ['onclick' => $onclick], rcube::Q($button_label));
$text .= html::p(['class' => 'rcmail-inline-buttons'], $button);
}
$this->inline_warning = html::div(['class' => 'rcmail-inline-message rcmail-inline-warning'], $text);
}
}
@@ -0,0 +1,932 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| PURPOSE: |
| Roundcube Installer |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class to control the installation process of the Roundcube Webmail package
*
* @category Install
* @package Webmail
*/
class rcmail_install
{
public $step;
public $last_error;
public $is_post = false;
public $failures = 0;
public $config = [];
public $defaults = [];
public $comments = [];
public $configured = false;
public $legacy_config = false;
public $email_pattern = '([a-z0-9][a-z0-9\-\.\+\_]*@[a-z0-9]([a-z0-9\-][.]?)*[a-z0-9])';
public $bool_config_props = ['ip_check', 'enable_spellcheck', 'auto_create_user', 'smtp_log', 'prefer_html'];
public $local_config = ['db_dsnw', 'imap_host', 'support_url', 'des_key', 'plugins'];
public $obsolete_config = ['db_backend', 'db_max_length', 'double_auth', 'preview_pane', 'debug_level', 'referer_check'];
public $replaced_config = [
'skin_path' => 'skin',
'locale_string' => 'language',
'multiple_identities' => 'identities_level',
'addrbook_show_images' => 'show_images',
'imap_root' => 'imap_ns_personal',
'pagesize' => 'mail_pagesize',
'top_posting' => 'reply_mode',
'keep_alive' => 'refresh_interval',
'min_keep_alive' => 'min_refresh_interval',
'default_host' => 'imap_host',
'smtp_server' => 'smtp_host',
];
// List of configuration options supported by the Installer
public $supported_config = [
'product_name', 'support_url', 'temp_dir', 'des_key', 'ip_check', 'enable_spellcheck',
'spellcheck_engine', 'identities_level', 'log_driver', 'log_dir', 'syslog_id',
'syslog_facility', 'db_dsnw', 'db_prefix', 'imap_host', 'username_domain',
'auto_create_user', 'sent_mbox', 'trash_mbox', 'drafts_mbox', 'junk_mbox',
'smtp_host', 'smtp_user', 'smtp_pass', 'smtp_log', 'language', 'skin', 'mail_pagesize',
'addressbook_pagesize', 'prefer_html', 'htmleditor', 'draft_autosave', 'mdn_requests',
'mime_param_folding', 'plugins',
];
// list of supported database drivers
public $supported_dbs = [
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
'SQLite (v2)' => 'pdo_sqlite2',
'SQL Server (SQLSRV)' => 'pdo_sqlsrv',
'SQL Server (DBLIB)' => 'pdo_dblib',
'Oracle' => 'oci8',
];
/** @var array List of config options with default value change per-release */
public $defaults_changes = [
'1.4.0' => ['skin', 'smtp_port', 'smtp_user', 'smtp_pass'],
'1.4.1' => ['jquery_ui_skin_map'],
];
/**
* Constructor
*/
public function __construct()
{
$this->step = isset($_REQUEST['_step']) ? intval($_REQUEST['_step']) : 0;
$this->is_post = isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'POST';
}
/**
* Singleton getter
*/
public static function get_instance()
{
static $inst;
if (!$inst) {
$inst = new rcmail_install();
}
return $inst;
}
/**
* Read the local config files and store properties
*/
public function load_config()
{
if ($this->configured) {
return;
}
// defaults
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'defaults.inc.php')) {
$this->config = (array) $config;
$this->defaults = $this->config;
}
$config = null;
// config
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'config.inc.php')) {
$this->config = array_merge($this->config, $config);
}
else {
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'main.inc.php')) {
$this->config = array_merge($this->config, $config);
$this->legacy_config = true;
}
if ($config = $this->load_config_file(RCUBE_CONFIG_DIR . 'db.inc.php')) {
$this->config = array_merge($this->config, $config);
$this->legacy_config = true;
}
}
$this->configured = !empty($config);
}
/**
* Read the default config file and store properties
*
* @param string $file File name with path
*/
public function load_config_file($file)
{
if (!is_readable($file)) {
return;
}
$config = [];
$rcmail_config = []; // deprecated var name
include $file;
// read comments from config file
if (function_exists('token_get_all')) {
$tokens = token_get_all(file_get_contents($file));
$in_config = false;
$buffer = '';
for ($i = 0; $i < count($tokens); $i++) {
$token = $tokens[$i];
if ($token[0] == T_VARIABLE && ($token[1] == '$config' || $token[1] == '$rcmail_config')) {
$in_config = true;
if ($buffer && $tokens[$i+1] == '[' && $tokens[$i+2][0] == T_CONSTANT_ENCAPSED_STRING) {
$propname = trim($tokens[$i+2][1], "'\"");
$this->comments[$propname] = preg_replace('/\n\n/', "\n", $buffer);
$buffer = '';
$i += 3;
}
}
else if ($in_config && $token[0] == T_COMMENT) {
$buffer .= strtr($token[1], ['\n' => "\n"]) . "\n";
}
}
}
return array_merge((array) $rcmail_config, (array) $config);
}
/**
* Getter for a certain config property
*
* @param string $name Property name
* @param string $default Default value
*
* @return mixed The property value
*/
public function getprop($name, $default = '')
{
$value = $this->config[$name] ?? null;
if ($name == 'des_key' && !$this->configured && !isset($_REQUEST["_$name"])) {
$value = rcube_utils::random_bytes(24);
}
return $value !== null && $value !== '' ? $value : $default;
}
/**
* Create configuration file that contains parameters
* that differ from default values.
*
* @param bool $use_post Use POSTed configuration values (of supported options)
*
* @return string The complete config file content
*/
public function create_config($use_post = true)
{
$config = [];
foreach ($this->config as $prop => $default) {
$post_value = $_POST["_$prop"] ?? null;
$value = $default;
if ($use_post && in_array($prop, $this->supported_config)
&& ($post_value !== null || in_array($prop, $this->bool_config_props))
) {
$value = $post_value;
}
// always disable installer
if ($prop == 'enable_installer') {
$value = false;
}
// generate new encryption key, never use the default value
if ($prop == 'des_key' && $value == $this->defaults[$prop]) {
$value = rcube_utils::random_bytes(24);
}
// convert some form data
if ($prop == 'db_dsnw' && !empty($_POST['_dbtype'])) {
if ($_POST['_dbtype'] == 'sqlite') {
$value = sprintf('%s://%s?mode=0646', $_POST['_dbtype'],
$_POST['_dbname'][0] == '/' ? '/' . $_POST['_dbname'] : $_POST['_dbname']);
}
else if ($_POST['_dbtype']) {
$value = sprintf('%s://%s:%s@%s/%s', $_POST['_dbtype'],
rawurlencode($_POST['_dbuser']), rawurlencode($_POST['_dbpass']), $_POST['_dbhost'], $_POST['_dbname']);
}
}
else if ($prop == 'imap_host' && is_array($value)) {
$value = self::_clean_array($value);
if (count($value) <= 1) {
$value = $value[0];
}
}
else if ($prop == 'mail_pagesize' || $prop == 'addressbook_pagesize') {
$value = max(2, intval($value));
}
else if ($prop == 'smtp_user' && !empty($_POST['_smtp_user_u'])) {
$value = '%u';
}
else if ($prop == 'smtp_pass' && !empty($_POST['_smtp_user_u'])) {
$value = '%p';
}
else if (is_bool($default)) {
$value = (bool) $value;
}
else if (is_numeric($value)) {
$value = intval($value);
}
else if ($prop == 'plugins' && !empty($_POST['submit'])) {
$value = [];
foreach (array_keys($_POST) as $key) {
if (preg_match('/^_plugins_*/', $key)) {
array_push($value, $_POST[$key]);
}
}
}
// skip this property
if ($value == ($this->defaults[$prop] ?? null)
&& (!in_array($prop, $this->local_config)
|| in_array($prop, array_merge($this->obsolete_config, array_keys($this->replaced_config)))
|| preg_match('/^db_(table|sequence)_/', $prop)
)
) {
continue;
}
// save change
$this->config[$prop] = $value;
$config[$prop] = $value;
}
$out = "<?php\n\n";
$out .= "/* Local configuration for Roundcube Webmail */\n\n";
foreach ($config as $prop => $value) {
// copy option descriptions from existing config or defaults.inc.php
$out .= $this->comments[$prop] ?? '';
$out .= "\$config['$prop'] = " . self::_dump_var($value, $prop) . ";\n\n";
}
return $out;
}
/**
* save generated config file in RCUBE_CONFIG_DIR
*
* @return boolean True if the file was saved successfully, false if not
*/
public function save_configfile($config)
{
if (is_writable(RCUBE_CONFIG_DIR)) {
return file_put_contents(RCUBE_CONFIG_DIR . 'config.inc.php', $config);
}
return false;
}
/**
* Check the current configuration for missing properties
* and deprecated or obsolete settings
*
* @param string $version Previous version on upgrade
*
* @return array List with problems detected
*/
public function check_config($version = null)
{
$this->load_config();
if (!$this->configured) {
return;
}
$out = $seen = [];
// iterate over the current configuration
foreach (array_keys($this->config) as $prop) {
if (!empty($this->replaced_config[$prop])) {
$replacement = $this->replaced_config[$prop];
$out['replaced'][] = ['prop' => $prop, 'replacement' => $replacement];
$seen[$replacement] = true;
}
else if (empty($seen[$prop]) && in_array($prop, $this->obsolete_config)) {
$out['obsolete'][] = ['prop' => $prop];
$seen[$prop] = true;
}
}
// the old default mime_magic reference is obsolete
if ($this->config['mime_magic'] == '/usr/share/misc/magic') {
$out['obsolete'][] = [
'prop' => 'mime_magic',
'explain' => "Set value to null in order to use system default"
];
}
// check config dependencies and contradictions
if (!empty($this->config['enable_spellcheck']) && $this->config['spellcheck_engine'] == 'pspell') {
if (!extension_loaded('pspell')) {
$out['dependencies'][] = [
'prop' => 'spellcheck_engine',
'explain' => "This requires the <tt>pspell</tt> extension which could not be loaded."
];
}
else if (!empty($this->config['spellcheck_languages'])) {
foreach ($this->config['spellcheck_languages'] as $lang => $descr) {
if (!@pspell_new($lang)) {
$out['dependencies'][] = [
'prop' => 'spellcheck_languages',
'explain' => "You are missing pspell support for language $lang ($descr)"
];
}
}
}
}
if ($this->config['log_driver'] == 'syslog') {
if (!function_exists('openlog')) {
$out['dependencies'][] = [
'prop' => 'log_driver',
'explain' => "This requires the <tt>syslog</tt> extension which could not be loaded."
];
}
if (empty($this->config['syslog_id'])) {
$out['dependencies'][] = [
'prop' => 'syslog_id',
'explain' => "Using <tt>syslog</tt> for logging requires a syslog ID to be configured"
];
}
}
// check ldap_public sources having global_search enabled
if (is_array($this->config['ldap_public']) && !is_array($this->config['autocomplete_addressbooks'])) {
foreach ($this->config['ldap_public'] as $ldap_public) {
if ($ldap_public['global_search']) {
$out['replaced'][] = [
'prop' => 'ldap_public::global_search',
'replacement' => 'autocomplete_addressbooks'
];
break;
}
}
}
if ($version) {
$out['defaults'] = [];
foreach ($this->defaults_changes as $v => $opts) {
if (version_compare($v, $version, '>')) {
$out['defaults'] = array_merge($out['defaults'], $opts);
}
}
$out['defaults'] = array_unique($out['defaults']);
}
return $out;
}
/**
* Merge the current configuration with the defaults
* and copy replaced values to the new options.
*/
public function merge_config()
{
$current = $this->config;
$this->config = [];
foreach ($this->replaced_config as $prop => $replacement) {
if (isset($current[$prop])) {
if ($prop == 'skin_path') {
$this->config[$replacement] = preg_replace('#skins/(\w+)/?$#', '\\1', $current[$prop]);
}
else if ($prop == 'multiple_identities') {
$this->config[$replacement] = $current[$prop] ? 2 : 0;
}
else {
$this->config[$replacement] = $current[$prop];
}
unset($current[$prop]);
unset($current[$replacement]);
}
}
// Merge old *_port options into the new *_host options, where possible
foreach (['default' => 'imap', 'smtp' => 'smtp'] as $prop => $type) {
$old_prop = "{$prop}_port";
$new_prop = "{$type}_host";
if (!empty($current[$old_prop]) && !empty($this->config[$new_prop])
&& is_string($this->config[$new_prop])
&& !preg_match('/:[0-9]+$/', $this->config[$new_prop])
) {
$this->config[$new_prop] .= ':' . $current[$old_prop];
}
unset($current[$old_prop]);
}
foreach ($this->obsolete_config as $prop) {
unset($current[$prop]);
}
// add all ldap_public sources having global_search enabled to autocomplete_addressbooks
if (!empty($current['ldap_public']) && is_array($current['ldap_public'])) {
foreach ($current['ldap_public'] as $key => $ldap_public) {
if (!empty($ldap_public['global_search'])) {
$this->config['autocomplete_addressbooks'][] = $key;
unset($current['ldap_public'][$key]['global_search']);
}
}
}
$this->config = array_merge($this->config, $current);
}
/**
* Compare the local database schema with the reference schema
* required for this version of Roundcube
*
* @param rcube_db $db Database object
*
* @return bool True if the schema is up-to-date, false if not or an error occurred
*/
public function db_schema_check($db)
{
if (!$this->configured) {
return false;
}
// read reference schema from mysql.initial.sql
$engine = $db->db_provider;
$db_schema = $this->db_read_schema(INSTALL_PATH . "SQL/$engine.initial.sql", $schema_version);
$errors = [];
// Just check the version
if ($schema_version) {
$version = rcmail_utils::db_version();
if (empty($version)) {
$errors[] = "Schema version not found";
}
else if ($schema_version != $version) {
$errors[] = "Schema version: {$version} (required: {$schema_version})";
}
return !empty($errors) ? $errors : false;
}
// check list of tables
$existing_tables = $db->list_tables();
foreach ($db_schema as $table => $cols) {
$table = $this->config['db_prefix'] . $table;
if (!in_array($table, $existing_tables)) {
$errors[] = "Missing table '".$table."'";
}
else { // compare cols
$db_cols = $db->list_cols($table);
$diff = array_diff(array_keys($cols), $db_cols);
if (!empty($diff)) {
$errors[] = "Missing columns in table '$table': " . implode(',', $diff);
}
}
}
return !empty($errors) ? $errors : false;
}
/**
* Utility function to read database schema from an .sql file
*/
private function db_read_schema($schemafile, &$version = null)
{
$lines = file($schemafile);
$schema = [];
$keywords = ['PRIMARY','KEY','INDEX','UNIQUE','CONSTRAINT','REFERENCES','FOREIGN'];
$table_name = null;
foreach ($lines as $line) {
if (preg_match('/^\s*create table ([\S]+)/i', $line, $m)) {
$table_name = explode('.', $m[1]);
$table_name = end($table_name);
$table_name = preg_replace('/[`"\[\]]/', '', $table_name);
}
else if (preg_match('/insert into/i', $line) && preg_match('/\'roundcube-version\',\s*\'([0-9]+)\'/', $line, $m)) {
$version = $m[1];
}
else if ($table_name && ($line = trim($line))) {
if ($line == 'GO' || $line[0] == ')' || $line[strlen($line)-1] == ';') {
$table_name = null;
}
else {
$items = explode(' ', $line);
$col = $items[0];
$col = preg_replace('/[`"\[\]]/', '', $col);
if (!in_array(strtoupper($col), $keywords)) {
$type = strtolower($items[1]);
$type = preg_replace('/[^a-zA-Z0-9()]/', '', $type);
$schema[$table_name][$col] = $type;
}
}
}
}
return $schema;
}
/**
* Try to detect some file's mimetypes to test the correct behavior of fileinfo
*/
public function check_mime_detection()
{
$errors = [];
$files = [
'program/resources/tinymce/video.png' => 'image/png',
'program/resources/blank.tiff' => 'image/tiff',
'program/resources/blocked.gif' => 'image/gif',
];
foreach ($files as $path => $expected) {
$mimetype = rcube_mime::file_content_type(INSTALL_PATH . $path, basename($path));
if ($mimetype != $expected) {
$errors[] = [$path, $mimetype, $expected];
}
}
return $errors;
}
/**
* Check the correct configuration of the 'mime_types' mapping option
*/
public function check_mime_extensions()
{
$errors = [];
$types = [
'application/zip' => 'zip',
'text/css' => 'css',
'application/pdf' => 'pdf',
'image/gif' => 'gif',
'image/svg+xml' => 'svg',
];
foreach ($types as $mimetype => $expected) {
$ext = rcube_mime::get_mime_extensions($mimetype);
if (!in_array($expected, (array) $ext)) {
$errors[] = [$mimetype, $ext, $expected];
}
}
return $errors;
}
/**
* Getter for the last error message
*
* @return string Error message or null if none exists
*/
public function get_error()
{
return $this->last_error['message'] ?? null;
}
/**
* Return a list with all imap/smtp hosts configured
*
* @return array Clean list with imap/smtp hosts
*/
public function get_hostlist($prop = 'imap_host')
{
$hosts = (array) $this->getprop($prop);
$out = [];
$imap_host = '';
if ($prop == 'smtp_host') {
// Set the imap host name for the %h macro
$default_hosts = $this->get_hostlist();
$imap_host = !empty($default_hosts) ? $default_hosts[0] : '';
}
foreach ($hosts as $key => $name) {
if (!empty($name)) {
if ($prop == 'smtp_host') {
// SMTP host array uses `IMAP host => SMTP host` format
$host = $name;
}
else {
$host = is_numeric($key) ? $name : $key;
}
$out[] = rcube_utils::parse_host($host, $imap_host);
}
}
return $out;
}
/**
* Create a HTML dropdown to select a previous version of Roundcube
*/
public function versions_select($attrib = [])
{
$select = new html_select($attrib);
$select->add([
'0.1-stable', '0.1.1',
'0.2-alpha', '0.2-beta', '0.2-stable',
'0.3-stable', '0.3.1',
'0.4-beta', '0.4.2',
'0.5-beta', '0.5', '0.5.1', '0.5.2', '0.5.3', '0.5.4',
'0.6-beta', '0.6',
'0.7-beta', '0.7', '0.7.1', '0.7.2', '0.7.3', '0.7.4',
'0.8-beta', '0.8-rc', '0.8.0', '0.8.1', '0.8.2', '0.8.3', '0.8.4', '0.8.5', '0.8.6',
'0.9-beta', '0.9-rc', '0.9-rc2',
// Note: Do not add newer versions here
]);
return $select;
}
/**
* Return a list with available subfolders of the skin directory
*
* @return array List of available skins
*/
public function list_skins()
{
$skins = [];
$skindir = INSTALL_PATH . 'skins/';
foreach (glob($skindir . '*') as $path) {
if (is_dir($path) && is_readable($path)) {
$skins[] = substr($path, strlen($skindir));
}
}
return $skins;
}
/**
* Return a list with available subfolders of the plugins directory
* (with their associated description in composer.json)
*
* @return array List of available plugins
*/
public function list_plugins()
{
$plugins = [];
$plugin_dir = INSTALL_PATH . 'plugins/';
$enabled = isset($this->config['plugins']) ? (array) $this->config['plugins'] : [];
foreach (glob($plugin_dir . '*') as $path) {
if (!is_dir($path)) {
continue;
}
if (is_readable($path . '/composer.json')) {
$file_json = json_decode(file_get_contents($path . '/composer.json'));
$plugin_desc = $file_json->description ?: 'N/A';
}
else {
$plugin_desc = 'N/A';
}
$name = substr($path, strlen($plugin_dir));
$plugins[] = [
'name' => $name,
'desc' => $plugin_desc,
'enabled' => in_array($name, $enabled)
];
}
return $plugins;
}
/**
* Display OK status
*
* @param string $name Test name
* @param string $message Confirm message
*/
public function pass($name, $message = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="success">OK</span>';
$this->_showhint($message);
}
/**
* Display an error status and increase failure count
*
* @param string $name Test name
* @param string $message Error message
* @param string $url URL for details
* @param bool $optional Do not count this failure
*/
public function fail($name, $message = '', $url = '', $optional = false)
{
if (!$optional) {
$this->failures++;
}
echo rcube::Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
$this->_showhint($message, $url);
}
/**
* Display an error status for optional settings/features
*
* @param string $name Test name
* @param string $message Error message
* @param string $url URL for details
*/
public function optfail($name, $message = '', $url = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="na">NOT OK</span>';
$this->_showhint($message, $url);
}
/**
* Display warning status
*
* @param string $name Test name
* @param string $message Warning message
* @param string $url URL for details
*/
public function na($name, $message = '', $url = '')
{
echo rcube::Q($name) . ':&nbsp; <span class="na">NOT AVAILABLE</span>';
$this->_showhint($message, $url);
}
private function _showhint($message, $url = '')
{
$hint = rcube::Q($message);
if ($url) {
$hint .= ($hint ? '; ' : '') . 'See <a href="' . rcube::Q($url) . '" target="_blank">' . rcube::Q($url) . '</a>';
}
if ($hint) {
echo '<span class="indent">(' . $hint . ')</span>';
}
}
private static function _clean_array($arr)
{
$out = [];
foreach (array_unique($arr) as $k => $val) {
if (!empty($val)) {
if (is_numeric($k)) {
$out[] = $val;
}
else {
$out[$k] = $val;
}
}
}
return $out;
}
private static function _dump_var($var, $name = null)
{
// special values
switch ($name) {
case 'syslog_facility':
$list = [
32 => 'LOG_AUTH', 80 => 'LOG_AUTHPRIV', 72 => ' LOG_CRON',
24 => 'LOG_DAEMON', 0 => 'LOG_KERN', 128 => 'LOG_LOCAL0',
136 => 'LOG_LOCAL1', 144 => 'LOG_LOCAL2', 152 => 'LOG_LOCAL3',
160 => 'LOG_LOCAL4', 168 => 'LOG_LOCAL5', 176 => 'LOG_LOCAL6',
184 => 'LOG_LOCAL7', 48 => 'LOG_LPR', 16 => 'LOG_MAIL',
56 => 'LOG_NEWS', 40 => 'LOG_SYSLOG', 8 => 'LOG_USER', 64 => 'LOG_UUCP'
];
if (!empty($list[$var])) {
return $list[$var];
}
break;
}
if (is_array($var)) {
if (empty($var)) {
return '[]';
}
// check if all keys are numeric
$isnum = true;
foreach (array_keys($var) as $key) {
if (!is_numeric($key)) {
$isnum = false;
break;
}
}
if ($isnum) {
return '[' . implode(', ', array_map(['rcmail_install', '_dump_var'], $var)) . ']';
}
}
return var_export($var, true);
}
/**
* Initialize the database with the according schema
*
* @param rcube_db $db Database connection
*
* @return bool True on success, False on error
*/
public function init_db($db)
{
$engine = $db->db_provider;
// read schema file from /SQL/*
$fname = INSTALL_PATH . "SQL/$engine.initial.sql";
if ($sql = @file_get_contents($fname)) {
$db->set_option('table_prefix', $this->config['db_prefix']);
$db->exec_script($sql);
}
else {
$this->fail('DB Schema', "Cannot read the schema file: $fname");
return false;
}
if ($err = $this->get_error()) {
$this->fail('DB Schema', "Error creating database schema: $err");
return false;
}
return true;
}
/**
* Update database schema
*
* @param string $version Version to update from
*
* @return boolean True on success, False on error
*/
public function update_db($version)
{
return rcmail_utils::db_update(INSTALL_PATH . 'SQL', 'roundcube', $version, ['quiet' => true]);
}
/**
* Handler for Roundcube errors
*/
public function raise_error($p)
{
$this->last_error = $p;
}
/**
* Check if vendor/autoload.php was created by Roundcube and left untouched
*
* @param string $target_dir The target installation dir
* @return string
*/
public static function vendor_dir_untouched($target_dir)
{
system('grep -q "generated by Roundcube" ' . escapeshellarg($target_dir . '/vendor/autoload.php') . ' 2>/dev/null', $exit_code);
return $exit_code === 0;
}
}
+588
View File
@@ -0,0 +1,588 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Roundcube OAuth2 utilities |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
use GuzzleHttp\Client;
use GuzzleHttp\MessageFormatter;
use GuzzleHttp\Exception\RequestException;
/**
* Roundcube OAuth2 utilities
*
* @package Webmail
* @subpackage Utils
*/
class rcmail_oauth
{
/** @var rcmail */
protected $rcmail;
/** @var array */
protected $options = [];
/** @var string */
protected $last_error = null;
/** @var boolean */
protected $no_redirect = false;
/** @var rcmail_oauth */
static protected $instance;
/**
* Singleton factory
*
* @return rcmail_oauth The one and only instance
*/
static function get_instance($options = [])
{
if (!self::$instance) {
self::$instance = new rcmail_oauth($options);
self::$instance->init();
}
return self::$instance;
}
/**
* Object constructor
*
* @param array $options Config options:
*/
public function __construct($options = [])
{
$this->rcmail = rcmail::get_instance();
$this->options = (array) $options + [
'provider' => $this->rcmail->config->get('oauth_provider'),
'auth_uri' => $this->rcmail->config->get('oauth_auth_uri'),
'token_uri' => $this->rcmail->config->get('oauth_token_uri'),
'client_id' => $this->rcmail->config->get('oauth_client_id'),
'client_secret' => $this->rcmail->config->get('oauth_client_secret'),
'identity_uri' => $this->rcmail->config->get('oauth_identity_uri'),
'identity_fields' => $this->rcmail->config->get('oauth_identity_fields', ['email']),
'scope' => $this->rcmail->config->get('oauth_scope'),
'verify_peer' => $this->rcmail->config->get('oauth_verify_peer', true),
'auth_parameters' => $this->rcmail->config->get('oauth_auth_parameters', []),
'login_redirect' => $this->rcmail->config->get('oauth_login_redirect', false),
];
}
/**
* Initialize this instance
*
* @return void
*/
protected function init()
{
// subscribe to storage and smtp init events
if ($this->is_enabled()) {
$this->rcmail->plugins->register_hook('storage_init', [$this, 'storage_init']);
$this->rcmail->plugins->register_hook('smtp_connect', [$this, 'smtp_connect']);
$this->rcmail->plugins->register_hook('managesieve_connect', [$this, 'managesieve_connect']);
$this->rcmail->plugins->register_hook('logout_after', [$this, 'logout_after']);
$this->rcmail->plugins->register_hook('login_failed', [$this, 'login_failed']);
$this->rcmail->plugins->register_hook('unauthenticated', [$this, 'unauthenticated']);
$this->rcmail->plugins->register_hook('refresh', [$this, 'refresh']);
}
}
/**
* Check if OAuth is generally enabled in config
*
* @return boolean
*/
public function is_enabled()
{
return !empty($this->options['provider']) &&
!empty($this->options['token_uri']) &&
!empty($this->options['client_id']);
}
/**
* Compose a fully qualified redirect URI for auth requests
*
* @return string
*/
public function get_redirect_uri()
{
$url = $this->rcmail->url([], true, true);
// rewrite redirect URL to not contain query parameters because some providers do not support this
$url = preg_replace('/\?.*/', '', $url);
return slashify($url) . 'index.php/login/oauth';
}
/**
* Getter for the last error occurred
*
* @return mixed
*/
public function get_last_error()
{
return $this->last_error;
}
/**
* Helper method to decode a JWT
*
* @param string $jwt
* @return array Hash array with decoded body
*/
public function jwt_decode($jwt)
{
list($headb64, $bodyb64, $cryptob64) = explode('.', strtr($jwt, '-_', '+/'));
$header = json_decode(base64_decode($headb64), true);
$body = json_decode(base64_decode($bodyb64), true);
if (isset($body['azp']) && $body['azp'] !== $this->options['client_id']) {
throw new RuntimeException('Failed to validate JWT: invalid azp value');
}
else if (isset($body['aud']) && !in_array($this->options['client_id'], (array) $body['aud'])) {
throw new RuntimeException('Failed to validate JWT: invalid aud value');
}
else if (!isset($body['azp']) && !isset($body['aud'])) {
throw new RuntimeException('Failed to validate JWT: missing aud/azp value');
}
return $body;
}
/**
* Login action: redirect to `oauth_auth_uri`
*
* @return void
*/
public function login_redirect()
{
if (!empty($this->options['auth_uri']) && !empty($this->options['client_id'])) {
// create a secret string
$_SESSION['oauth_state'] = rcube_utils::random_bytes(12);
// compose full oauth login uri
$delimiter = strpos($this->options['auth_uri'], '?') > 0 ? '&' : '?';
$query = http_build_query([
'response_type' => 'code',
'client_id' => $this->options['client_id'],
'scope' => $this->options['scope'],
'redirect_uri' => $this->get_redirect_uri(),
'state' => $_SESSION['oauth_state'],
] + (array) $this->options['auth_parameters']);
$this->rcmail->output->redirect($this->options['auth_uri'] . $delimiter . $query); // exit
}
else {
// log error about missing config options
rcube::raise_error([
'message' => "Missing required OAuth config options 'oauth_auth_uri', 'oauth_client_id'",
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
}
}
/**
* Request access token with auth code returned from oauth login
*
* @param string $auth_code
* @param string $state
*
* @return array Authorization data as hash array with entries
* `username` as the authentication user name
* `authorization` as the oauth authorization string "<type> <access-token>"
* `token` as the complete oauth response to be stored in session
*/
public function request_access_token($auth_code, $state = null)
{
$oauth_token_uri = $this->options['token_uri'];
$oauth_client_id = $this->options['client_id'];
$oauth_client_secret = $this->options['client_secret'];
$oauth_identity_uri = $this->options['identity_uri'];
if (!empty($oauth_token_uri) && !empty($oauth_client_secret)) {
try {
// validate state parameter against $_SESSION['oauth_state']
if (!empty($_SESSION['oauth_state']) && $_SESSION['oauth_state'] !== $state) {
throw new RuntimeException('Invalid state parameter');
}
// send token request to get a real access token for the given auth code
$client = new Client([
'timeout' => 10.0,
'verify' => $this->options['verify_peer'],
]);
$response = $client->post($oauth_token_uri, [
'form_params' => [
'code' => $auth_code,
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'redirect_uri' => $this->get_redirect_uri(),
'grant_type' => 'authorization_code',
],
]);
$data = \GuzzleHttp\json_decode($response->getBody(), true);
// auth success
if (!empty($data['access_token'])) {
$username = null;
$identity = null;
$authorization = sprintf('%s %s', $data['token_type'], $data['access_token']);
// decode JWT id_token if provided
if (!empty($data['id_token'])) {
try {
$identity = $this->jwt_decode($data['id_token']);
foreach ($this->options['identity_fields'] as $field) {
if (isset($identity[$field])) {
$username = $identity[$field];
break;
}
}
} catch (\Exception $e) {
// log error
rcube::raise_error([
'message' => $e->getMessage(),
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
}
}
// request user identity (email)
if (empty($username) && !empty($oauth_identity_uri)) {
$identity_response = $client->get($oauth_identity_uri, [
'headers' => [
'Authorization' => $authorization,
'Accept' => 'application/json',
],
]);
$identity = \GuzzleHttp\json_decode($identity_response->getBody(), true);
foreach ($this->options['identity_fields'] as $field) {
if (isset($identity[$field])) {
$username = $identity[$field];
break;
}
}
}
$data['identity'] = $username;
$this->mask_auth_data($data);
$this->rcmail->session->remove('oauth_state');
$this->rcmail->plugins->exec_hook('oauth_login', array_merge($data, [
'username' => $username,
'identity' => $identity,
]));
// remove some data we don't want to store in session
unset($data['id_token']);
// return auth data
return [
'username' => $username,
'authorization' => $authorization,
'token' => $data,
];
}
else {
throw new Exception('Unexpected response from OAuth service');
}
}
catch (RequestException $e) {
$this->last_error = "OAuth token request failed: " . $e->getMessage();
$this->no_redirect = true;
$formatter = new MessageFormatter();
rcube::raise_error([
'message' => $this->last_error . '; ' . $formatter->format($e->getRequest(), $e->getResponse()),
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
return false;
}
catch (Exception $e) {
$this->last_error = "OAuth token request failed: " . $e->getMessage();
$this->no_redirect = true;
rcube::raise_error([
'message' => $this->last_error,
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
return false;
}
}
else {
$this->last_error = "Missing required OAuth config options 'oauth_token_uri', 'oauth_client_id', 'oauth_client_secret'";
rcube::raise_error([
'message' => $this->last_error,
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
return false;
}
}
/**
* Obtain a new access token using the refresh_token grant type
*
* If successful, this will update the `oauth_token` entry in
* session data.
*
* @param array $token
*
* @return array Updated authorization data
*/
public function refresh_access_token(array $token)
{
$oauth_token_uri = $this->options['token_uri'];
$oauth_client_id = $this->options['client_id'];
$oauth_client_secret = $this->options['client_secret'];
// send token request to get a real access token for the given auth code
try {
$client = new Client([
'timeout' => 10.0,
'verify' => $this->options['verify_peer'],
]);
$response = $client->post($oauth_token_uri, [
'form_params' => [
'client_id' => $oauth_client_id,
'client_secret' => $oauth_client_secret,
'refresh_token' => $this->rcmail->decrypt($token['refresh_token']),
'grant_type' => 'refresh_token',
],
]);
$data = \GuzzleHttp\json_decode($response->getBody(), true);
// auth success
if (!empty($data['access_token'])) {
// update access token stored as password
$authorization = sprintf('%s %s', $data['token_type'], $data['access_token']);
$_SESSION['password'] = $this->rcmail->encrypt($authorization);
$this->mask_auth_data($data);
// update session data
$_SESSION['oauth_token'] = array_merge($token, $data);
$this->rcmail->plugins->exec_hook('oauth_refresh_token', $data);
return [
'token' => $data,
'authorization' => $authorization,
];
}
}
catch (RequestException $e) {
$this->last_error = "OAuth refresh token request failed: " . $e->getMessage();
$formatter = new MessageFormatter();
rcube::raise_error([
'message' => $this->last_error . '; ' . $formatter->format($e->getRequest(), $e->getResponse()),
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
// refrehsing token failed, mark session as expired
if ($e->getCode() >= 400 && $e->getCode() < 500) {
$this->rcmail->kill_session();
}
return false;
}
catch (Exception $e) {
$this->last_error = "OAuth refresh token request failed: " . $e->getMessage();
rcube::raise_error([
'message' => $this->last_error,
'file' => __FILE__,
'line' => __LINE__,
], true, false
);
return false;
}
}
/**
* Modify some properties of the received auth response
*
* @param array $token
* @return void
*/
protected function mask_auth_data(&$data)
{
// compute absolute token expiration date
$data['expires'] = time() + $data['expires_in'] - 10;
// encrypt refresh token if provided
if (isset($data['refresh_token'])) {
$data['refresh_token'] = $this->rcmail->encrypt($data['refresh_token']);
}
}
/**
* Check the given access token data if still valid
*
* ... and attempt to refresh if possible.
*
* @param array $token
* @return boolean
*/
protected function check_token_validity($token)
{
if ($token['expires'] < time() && isset($token['refresh_token']) && empty($this->last_error)) {
return $this->refresh_access_token($token) !== false;
}
return false;
}
/**
* Callback for 'storage_init' hook
*
* @param array $options
* @return array
*/
public function storage_init($options)
{
if (isset($_SESSION['oauth_token']) && $options['driver'] === 'imap') {
// check token validity
if ($this->check_token_validity($_SESSION['oauth_token'])) {
$options['password'] = $this->rcmail->decrypt($_SESSION['password']);
}
// enforce XOAUTH2 authorization type
$options['auth_type'] = 'XOAUTH2';
}
return $options;
}
/**
* Callback for 'smtp_connect' hook
*
* @param array $options
* @return array
*/
public function smtp_connect($options)
{
if (isset($_SESSION['oauth_token'])) {
// check token validity
$this->check_token_validity($_SESSION['oauth_token']);
// enforce XOAUTH2 authorization type
$options['smtp_user'] = '%u';
$options['smtp_pass'] = '%p';
$options['smtp_auth_type'] = 'XOAUTH2';
}
return $options;
}
/**
* Callback for 'managesieve_connect' hook
*
* @param array $options
* @return array
*/
public function managesieve_connect($options)
{
if (isset($_SESSION['oauth_token'])) {
// check token validity
$this->check_token_validity($_SESSION['oauth_token']);
// enforce XOAUTH2 authorization type
$options['auth_type'] = 'XOAUTH2';
}
return $options;
}
/**
* Callback for 'logout_after' hook
*
* @param array $options
* @return array
*/
public function logout_after($options)
{
$this->no_redirect = true;
}
/**
* Callback for 'login_failed' hook
*
* @param array $options
* @return array
*/
public function login_failed($options)
{
// no redirect on imap login failures
$this->no_redirect = true;
return $options;
}
/**
* Callback for 'unauthenticated' hook
*
* @param array $options
* @return array
*/
public function unauthenticated($options)
{
if (
$this->options['login_redirect']
&& !$this->rcmail->output->ajax_call
&& !$this->no_redirect
&& empty($options['error'])
&& $options['http_code'] === 200
) {
$this->login_redirect();
}
return $options;
}
/**
* Callback for 'refresh' hook
*
* @param array $options
* @return void
*/
public function refresh($options)
{
if (isset($_SESSION['oauth_token'])) {
$this->check_token_validity($_SESSION['oauth_token']);
}
}
}
@@ -0,0 +1,150 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Abstract class for output generation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
| Author: Aleksander Machniak <alec@alec.pl> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Webmail
* @subpackage View
*/
abstract class rcmail_output extends rcube_output
{
const JS_OBJECT_NAME = 'rcmail';
const BLANK_GIF = 'R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7';
public $type = 'html';
public $ajax_call = false;
public $framed = false;
protected $pagetitle = '';
protected $object_handlers = [];
protected $devel_mode = false;
/**
* Object constructor
*/
public function __construct()
{
parent::__construct();
$this->devel_mode = (bool) $this->config->get('devel_mode');
}
/**
* Setter for page title
*
* @param string $title Page title
*/
public function set_pagetitle($title)
{
$this->pagetitle = $title;
}
/**
* Getter for the current skin path property
*/
public function get_skin_path()
{
return $this->config->get('skin_path');
}
/**
* Delete all stored env variables and commands
*/
public function reset()
{
parent::reset();
$this->object_handlers = [];
$this->pagetitle = '';
}
/**
* Call a client method
*
* @param string $cmd Method to call
* @param mixed ...$args Method arguments
*/
abstract function command($cmd, ...$args);
/**
* Add a localized label(s) to the client environment
*
* @param mixed ...$args Labels (an array of strings, or many string arguments)
*/
abstract function add_label(...$args);
/**
* Register a template object handler
*
* @param string $name Object name
* @param callable $func Function name to call
*
* @return void
*/
public function add_handler($name, $func)
{
$this->object_handlers[$name] = $func;
}
/**
* Register a list of template object handlers
*
* @param array $handlers Hash array with object=>handler pairs
*
* @return void
*/
public function add_handlers($handlers)
{
$this->object_handlers = array_merge($this->object_handlers, $handlers);
}
/**
* A wrapper for header() function, so it can be replaced for automated tests
*
* @param string $header The header string
* @param bool $replace Replace previously set header?
*
* @return void
*/
public function header($header, $replace = true)
{
header($header, $replace);
}
/**
* A helper to send output to the browser and exit
*
* @param string $body The output body
* @param array $headers Headers
*
* @return void
*/
public function sendExit($body = '', $headers = [])
{
foreach ($headers as $header) {
header($header);
}
print $body;
exit;
}
}
@@ -0,0 +1,82 @@
<?php
/**
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
| |
| Copyright (C) The Roundcube Dev Team |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
| See the README file for a full license statement. |
| |
| CONTENTS: |
| Abstract class for output generation |
+-----------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-----------------------------------------------------------------------+
*/
/**
* Class for output generation
*
* @package Webmail
* @subpackage View
*/
class rcmail_output_cli extends rcmail_output
{
public $type = 'cli';
/**
* Call a client method
*
* @see rcube_output::command()
*/
function command($cmd, ...$args)
{
// NOP
}
/**
* Add a localized label to the client environment
*
* @see rcube_output::add_label()
*/
function add_label(...$args)
{
// NOP
}
/**
* Invoke display_message command
*
* @see rcube_output::show_message()
*/
function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0)
{
if ($this->app->text_exists($message)) {
$message = $this->app->gettext(['name' => $message, 'vars' => $vars]);
}
printf("[%s] %s\n", strtoupper($type), $message);
}
/**
* Redirect to a certain url.
*
* @see rcube_output::redirect()
*/
function redirect($p = [], $delay = 1)
{
// NOP
}
/**
* Send output to the client.
*/
function send()
{
// NOP
}
}

Some files were not shown because too many files have changed in this diff Show More