diff --git a/ruty/mails/plugins/acl/acl.js b/ruty/mails/plugins/acl/acl.js new file mode 100644 index 0000000..531248d --- /dev/null +++ b/ruty/mails/plugins/acl/acl.js @@ -0,0 +1,400 @@ +/** + * ACL plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (c) The Roundcube Dev Team + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ + +if (window.rcmail) { + rcmail.addEventListener('init', function() { + if (rcmail.gui_objects.acltable) { + rcmail.acl_list_init(); + // enable autocomplete on user input + if (rcmail.env.acl_users_source) { + var inst = rcmail.is_framed() ? parent.rcmail : rcmail; + inst.init_address_input_events($('#acluser'), {action:'settings/plugin.acl-autocomplete'}); + + // pass config settings and localized texts to autocomplete context + inst.set_env({ autocomplete_max:rcmail.env.autocomplete_max, autocomplete_min_length:rcmail.env.autocomplete_min_length }); + inst.add_label('autocompletechars', rcmail.labels.autocompletechars); + inst.add_label('autocompletemore', rcmail.labels.autocompletemore); + + // fix inserted value + inst.addEventListener('autocomplete_insert', function(e) { + if (e.field.id != 'acluser') + return; + + e.field.value = e.insert.replace(/[ ,;]+$/, ''); + }); + } + } + + rcmail.enable_command('acl-create', 'acl-save', 'acl-cancel', 'acl-mode-switch', true); + rcmail.enable_command('acl-delete', 'acl-edit', false); + + if (rcmail.env.acl_advanced) + $('#acl-switch').addClass('selected').find('input').prop('checked', true); + }); +} + +// Display new-entry form +rcube_webmail.prototype.acl_create = function() +{ + this.acl_init_form(); +} + +// Display ACL edit form +rcube_webmail.prototype.acl_edit = function() +{ + // @TODO: multi-row edition + var id = this.acl_list.get_single_selection(); + if (id) + this.acl_init_form(id); +} + +// ACL entry delete +rcube_webmail.prototype.acl_delete = function() +{ + var users = this.acl_get_usernames(); + + if (users && users.length) { + this.confirm_dialog(this.get_label('acl.deleteconfirm'), 'delete', function(e, ref) { + ref.http_post('settings/plugin.acl', { + _act: 'delete', + _user: users.join(','), + _mbox: rcmail.env.mailbox + }, ref.set_busy(true, 'acl.deleting')); + }); + } +} + +// Save ACL data +rcube_webmail.prototype.acl_save = function() +{ + var data, type, rights = '', user = $('#acluser', this.acl_form).val(); + + $((this.env.acl_advanced ? '#advancedrights :checkbox' : '#simplerights :checkbox'), this.acl_form).map(function() { + if (this.checked) + rights += this.value; + }); + + if (type = $('input:checked[name=usertype]', this.acl_form).val()) { + if (type != 'user') + user = type; + } + + if (!user) { + this.alert_dialog(this.get_label('acl.nouser')); + return; + } + if (!rights) { + this.alert_dialog(this.get_label('acl.norights')); + return; + } + + data = { + _act: 'save', + _user: user, + _acl: rights, + _mbox: this.env.mailbox + } + + if (this.acl_id) { + data._old = this.acl_id; + } + + this.http_post('settings/plugin.acl', data, this.set_busy(true, 'acl.saving')); +} + +// Cancel/Hide form +rcube_webmail.prototype.acl_cancel = function() +{ + this.ksearch_blur(); + this.acl_popup.dialog('close'); +} + +// Update data after save (and hide form) +rcube_webmail.prototype.acl_update = function(o) +{ + // delete old row + if (o.old) + this.acl_remove_row(o.old); + // make sure the same ID doesn't exist + else if (this.env.acl[o.id]) + this.acl_remove_row(o.id); + + // add new row + this.acl_add_row(o, true); + // hide autocomplete popup + this.ksearch_blur(); + // hide form + this.acl_popup.dialog('close'); +} + +// Switch table display mode +rcube_webmail.prototype.acl_mode_switch = function(elem) +{ + this.env.acl_advanced = !this.env.acl_advanced; + this.enable_command('acl-delete', 'acl-edit', false); + this.http_request('settings/plugin.acl', '_act=list' + + '&_mode='+(this.env.acl_advanced ? 'advanced' : 'simple') + + '&_mbox='+urlencode(this.env.mailbox), + this.set_busy(true, 'loading')); +} + +// ACL table initialization +rcube_webmail.prototype.acl_list_init = function() +{ + var method = this.env.acl_advanced ? 'addClass' : 'removeClass'; + + $('#acl-switch')[method]('selected'); + $(this.gui_objects.acltable)[method]('advanced'); + + this.acl_list = new rcube_list_widget(this.gui_objects.acltable, + {multiselect: true, draggable: false, keyboard: true}); + this.acl_list.addEventListener('select', function(o) { rcmail.acl_list_select(o); }) + .addEventListener('dblclick', function(o) { rcmail.acl_list_dblclick(o); }) + .addEventListener('keypress', function(o) { rcmail.acl_list_keypress(o); }) + .init(); +} + +// ACL table row selection handler +rcube_webmail.prototype.acl_list_select = function(list) +{ + rcmail.enable_command('acl-delete', list.get_selection().length > 0); + rcmail.enable_command('acl-edit', list.get_selection().length == 1); + list.focus(); +} + +// ACL table double-click handler +rcube_webmail.prototype.acl_list_dblclick = function(list) +{ + this.acl_edit(); +} + +// ACL table keypress handler +rcube_webmail.prototype.acl_list_keypress = function(list) +{ + if (list.key_pressed == list.ENTER_KEY) + this.command('acl-edit'); + else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY) + if (!this.acl_form || !this.acl_form.is(':visible')) + this.command('acl-delete'); +} + +// Reloads ACL table +rcube_webmail.prototype.acl_list_update = function(html) +{ + $(this.gui_objects.acltable).html(html); + this.acl_list_init(); +} + +// Returns names of users in selected rows +rcube_webmail.prototype.acl_get_usernames = function() +{ + var users = [], n, len, id, row, + list = this.acl_list, + selection = list.get_selection(); + + for (n=0, len=selection.length; n= 0) { + users.push(selection[n]); + } + else if ((row = list.rows[selection[n]]) && (id = $(row.obj).data('userid'))) { + users.push(id); + } + } + + return users; +} + +// Removes ACL table row +rcube_webmail.prototype.acl_remove_row = function(id) +{ + var list = this.acl_list; + + list.remove_row(id); + list.clear_selection(); + + // we don't need it anymore (remove id conflict) + $('#rcmrow'+id).remove(); + this.env.acl[id] = null; + + this.enable_command('acl-delete', list.get_selection().length > 0); + this.enable_command('acl-edit', list.get_selection().length == 1); +} + +// Adds ACL table row +rcube_webmail.prototype.acl_add_row = function(o, sel) +{ + var n, len, ids = [], spec = [], id = o.id, list = this.acl_list, + items = this.env.acl_advanced ? [] : this.env.acl_items, + table = this.gui_objects.acltable, + row = $('thead > tr', table).clone(); + + // Update new row + $('th', row).map(function() { + var td = $(''), + title = $(this).attr('title'), + cl = this.className.replace(/^acl/, ''); + + if (title) + td.attr('title', title); + + if (items && items[cl]) + cl = items[cl]; + + if (cl == 'user') + td.addClass(cl).attr('title', o.title).append($('').text(o.display)); + else + td.addClass(this.className + ' ' + rcmail.acl_class(o.acl, cl)).html(''); + + $(this).replaceWith(td); + }); + + row = row.attr({id: 'rcmrow' + id, 'data-userid': o.username}).get(0); + + this.env.acl[id] = o.acl; + + // sorting... (create an array of user identifiers, then sort it) + for (n in this.env.acl) { + if (this.env.acl[n]) { + if (this.env.acl_specials.length && $.inArray(n, this.env.acl_specials) >= 0) + spec.push(n); + else + ids.push(n); + } + } + ids.sort(); + // specials on the top + ids = spec.concat(ids); + + // find current id + for (n=0, len=ids.length; n -1) + found++; + + if (found == len) + return 'enabled'; + else if (found) + return 'partial'; + + return 'disabled'; +} diff --git a/ruty/mails/plugins/acl/acl.min.js b/ruty/mails/plugins/acl/acl.min.js new file mode 100644 index 0000000..0c77347 --- /dev/null +++ b/ruty/mails/plugins/acl/acl.min.js @@ -0,0 +1,17 @@ +/** + * ACL plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (c) The Roundcube Dev Team + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ +window.rcmail&&rcmail.addEventListener("init",function(){var e;rcmail.gui_objects.acltable&&(rcmail.acl_list_init(),rcmail.env.acl_users_source&&((e=rcmail.is_framed()?parent.rcmail:rcmail).init_address_input_events($("#acluser"),{action:"settings/plugin.acl-autocomplete"}),e.set_env({autocomplete_max:rcmail.env.autocomplete_max,autocomplete_min_length:rcmail.env.autocomplete_min_length}),e.add_label("autocompletechars",rcmail.labels.autocompletechars),e.add_label("autocompletemore",rcmail.labels.autocompletemore),e.addEventListener("autocomplete_insert",function(e){"acluser"==e.field.id&&(e.field.value=e.insert.replace(/[ ,;]+$/,""))}))),rcmail.enable_command("acl-create","acl-save","acl-cancel","acl-mode-switch",!0),rcmail.enable_command("acl-delete","acl-edit",!1),rcmail.env.acl_advanced&&$("#acl-switch").addClass("selected").find("input").prop("checked",!0)}),rcube_webmail.prototype.acl_create=function(){this.acl_init_form()},rcube_webmail.prototype.acl_edit=function(){var e=this.acl_list.get_single_selection();e&&this.acl_init_form(e)},rcube_webmail.prototype.acl_delete=function(){var a=this.acl_get_usernames();a&&a.length&&this.confirm_dialog(this.get_label("acl.deleteconfirm"),"delete",function(e,t){t.http_post("settings/plugin.acl",{_act:"delete",_user:a.join(","),_mbox:rcmail.env.mailbox},t.set_busy(!0,"acl.deleting"))})},rcube_webmail.prototype.acl_save=function(){var e,t="",a=$("#acluser",this.acl_form).val();$(this.env.acl_advanced?"#advancedrights :checkbox":"#simplerights :checkbox",this.acl_form).map(function(){this.checked&&(t+=this.value)}),(a=(e=$("input:checked[name=usertype]",this.acl_form).val())&&"user"!=e?e:a)?t?(a={_act:"save",_user:a,_acl:t,_mbox:this.env.mailbox},this.acl_id&&(a._old=this.acl_id),this.http_post("settings/plugin.acl",a,this.set_busy(!0,"acl.saving"))):this.alert_dialog(this.get_label("acl.norights")):this.alert_dialog(this.get_label("acl.nouser"))},rcube_webmail.prototype.acl_cancel=function(){this.ksearch_blur(),this.acl_popup.dialog("close")},rcube_webmail.prototype.acl_update=function(e){e.old?this.acl_remove_row(e.old):this.env.acl[e.id]&&this.acl_remove_row(e.id),this.acl_add_row(e,!0),this.ksearch_blur(),this.acl_popup.dialog("close")},rcube_webmail.prototype.acl_mode_switch=function(e){this.env.acl_advanced=!this.env.acl_advanced,this.enable_command("acl-delete","acl-edit",!1),this.http_request("settings/plugin.acl","_act=list&_mode="+(this.env.acl_advanced?"advanced":"simple")+"&_mbox="+urlencode(this.env.mailbox),this.set_busy(!0,"loading"))},rcube_webmail.prototype.acl_list_init=function(){var e=this.env.acl_advanced?"addClass":"removeClass";$("#acl-switch")[e]("selected"),$(this.gui_objects.acltable)[e]("advanced"),this.acl_list=new rcube_list_widget(this.gui_objects.acltable,{multiselect:!0,draggable:!1,keyboard:!0}),this.acl_list.addEventListener("select",function(e){rcmail.acl_list_select(e)}).addEventListener("dblclick",function(e){rcmail.acl_list_dblclick(e)}).addEventListener("keypress",function(e){rcmail.acl_list_keypress(e)}).init()},rcube_webmail.prototype.acl_list_select=function(e){rcmail.enable_command("acl-delete",0 tr",r).clone();for(t in $("th",r).map(function(){var e=$(""),t=$(this).attr("title"),a=this.className.replace(/^acl/,"");t&&e.attr("title",t),"user"==(a=o&&o[a]?o[a]:a)?e.addClass(a).attr("title",l.title).append($("").text(l.display)):e.addClass(this.className+" "+rcmail.acl_class(l.acl,a)).html(""),$(this).replaceWith(e)}),r=r.attr({id:"rcmrow"+s,"data-userid":l.username}).get(0),this.env.acl[s]=l.acl,this.env.acl)this.env.acl[t]&&(this.env.acl_specials.length&&0<=$.inArray(t,this.env.acl_specials)?i:c).push(t);for(c.sort(),t=0,a=(c=i.concat(c)).length;t + * + * Copyright (C) Kolab Systems AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +class acl extends rcube_plugin +{ + public $task = 'settings'; + + private $rc; + private $supported = null; + private $mbox; + private $ldap; + private $specials = ['anyone', 'anonymous']; + + /** + * Plugin initialization + */ + function init() + { + $this->rc = rcmail::get_instance(); + + // Register hooks + $this->add_hook('folder_form', [$this, 'folder_form']); + + // Plugin actions + $this->register_action('plugin.acl', [$this, 'acl_actions']); + $this->register_action('plugin.acl-autocomplete', [$this, 'acl_autocomplete']); + } + + /** + * Handler for plugin actions (AJAX) + */ + function acl_actions() + { + $action = trim(rcube_utils::get_input_string('_act', rcube_utils::INPUT_GPC)); + + // Connect to IMAP + $this->rc->storage_init(); + + // Load localization and configuration + $this->add_texts('localization/'); + $this->load_config(); + + if ($action == 'save') { + $this->action_save(); + } + else if ($action == 'delete') { + $this->action_delete(); + } + else if ($action == 'list') { + $this->action_list(); + } + + // Only AJAX actions + $this->rc->output->send(); + } + + /** + * Handler for user login autocomplete request + */ + function acl_autocomplete() + { + $this->load_config(); + + $search = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC, true); + $reqid = rcube_utils::get_input_string('_reqid', rcube_utils::INPUT_GPC); + $users = []; + $keys = []; + + if ($this->init_ldap()) { + $max = (int) $this->rc->config->get('autocomplete_max', 15); + $mode = (int) $this->rc->config->get('addressbook_search_mode'); + + $this->ldap->set_pagesize($max); + $result = $this->ldap->search('*', $search, $mode); + + foreach ($result->records as $record) { + $user = $record['uid']; + + if (is_array($user) && !empty($user)) { + $user = array_filter($user); + $user = $user[0]; + } + + if ($user) { + $display = rcube_addressbook::compose_search_name($record); + $user = ['name' => $user, 'display' => $display]; + $users[] = $user; + $keys[] = $display ?: $user['name']; + } + } + + if ($this->rc->config->get('acl_groups')) { + $prefix = $this->rc->config->get('acl_group_prefix'); + $group_field = $this->rc->config->get('acl_group_field', 'name'); + $result = $this->ldap->list_groups($search, $mode); + + foreach ($result as $record) { + $group = $record['name']; + $group_id = is_array($record[$group_field]) ? $record[$group_field][0] : $record[$group_field]; + + if ($group) { + $users[] = ['name' => ($prefix ?: '') . $group_id, 'display' => $group, 'type' => 'group']; + $keys[] = $group; + } + } + } + } + + if (count($users)) { + // sort users index + asort($keys, SORT_LOCALE_STRING); + // re-sort users according to index + foreach ($keys as $idx => $val) { + $keys[$idx] = $users[$idx]; + } + $users = array_values($keys); + } + + $this->rc->output->command('ksearch_query_results', $users, $search, $reqid); + $this->rc->output->send(); + } + + /** + * Handler for 'folder_form' hook + * + * @param array $args Hook arguments array (form data) + * + * @return array Hook arguments array + */ + function folder_form($args) + { + $mbox_imap = $args['options']['name'] ?? ''; + $myrights = $args['options']['rights'] ?? ''; + + // Edited folder name (empty in create-folder mode) + if (!strlen($mbox_imap)) { + return $args; + } +/* + // Do nothing on protected folders (?) + if (!empty($args['options']['protected'])) { + return $args; + } +*/ + // Get MYRIGHTS + if (empty($myrights)) { + return $args; + } + + // Load localization and include scripts + $this->load_config(); + $this->specials = $this->rc->config->get('acl_specials', $this->specials); + $this->add_texts('localization/', ['deleteconfirm', 'norights', + 'nouser', 'deleting', 'saving', 'newuser', 'editperms']); + $this->rc->output->add_label('save', 'cancel'); + $this->include_script('acl.js'); + $this->rc->output->include_script('list.js'); + $this->include_stylesheet($this->local_skin_path() . '/acl.css'); + + // add Info fieldset if it doesn't exist + if (!isset($args['form']['props']['fieldsets']['info'])) + $args['form']['props']['fieldsets']['info'] = [ + 'name' => $this->rc->gettext('info'), + 'content' => [] + ]; + + // Display folder rights to 'Info' fieldset + $args['form']['props']['fieldsets']['info']['content']['myrights'] = [ + 'label' => rcube::Q($this->gettext('myrights')), + 'value' => $this->acl2text($myrights) + ]; + + // Return if not folder admin + if (!in_array('a', $myrights)) { + return $args; + } + + // The 'Sharing' tab + $this->mbox = $mbox_imap; + $this->rc->output->set_env('acl_users_source', (bool) $this->rc->config->get('acl_users_source')); + $this->rc->output->set_env('mailbox', $mbox_imap); + $this->rc->output->add_handlers([ + 'acltable' => [$this, 'templ_table'], + 'acluser' => [$this, 'templ_user'], + 'aclrights' => [$this, 'templ_rights'], + ]); + + $this->rc->output->set_env('autocomplete_max', (int) $this->rc->config->get('autocomplete_max', 15)); + $this->rc->output->set_env('autocomplete_min_length', $this->rc->config->get('autocomplete_min_length')); + $this->rc->output->add_label('autocompletechars', 'autocompletemore'); + + $args['form']['sharing'] = [ + 'name' => rcube::Q($this->gettext('sharing')), + 'content' => $this->rc->output->parse('acl.table', false, false), + ]; + + return $args; + } + + /** + * Creates ACL rights table + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + function templ_table($attrib) + { + if (empty($attrib['id'])) { + $attrib['id'] = 'acl-table'; + } + + $out = $this->list_rights($attrib); + + $this->rc->output->add_gui_object('acltable', $attrib['id']); + + return $out; + } + + /** + * Creates ACL rights form (rights list part) + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + function templ_rights($attrib) + { + // Get supported rights + $supported = $this->rights_supported(); + + // give plugins the opportunity to adjust this list + $data = $this->rc->plugins->exec_hook('acl_rights_supported', + ['rights' => $supported, 'folder' => $this->mbox, 'labels' => []] + ); + $supported = $data['rights']; + + // depending on server capability either use 'te' or 'd' for deleting msgs + $deleteright = implode(array_intersect(str_split('ted'), $supported)); + + $out = ''; + $ul = ''; + $input = new html_checkbox(); + + // Advanced rights + $attrib['id'] = 'advancedrights'; + foreach ($supported as $key => $val) { + $id = "acl$val"; + $ul .= html::tag('li', null, + $input->show('', ['name' => "acl[$val]", 'value' => $val, 'id' => $id]) + . html::label(['for' => $id, 'title' => $this->gettext('longacl'.$val)], $this->gettext('acl'.$val)) + ); + } + + $out = html::tag('ul', $attrib, $ul, html::$common_attrib); + + // Simple rights + $ul = ''; + $attrib['id'] = 'simplerights'; + $items = [ + 'read' => 'lrs', + 'write' => 'wi', + 'delete' => $deleteright, + 'other' => preg_replace('/[lrswi'.$deleteright.']/', '', implode($supported)), + ]; + + // give plugins the opportunity to adjust this list + $data = $this->rc->plugins->exec_hook('acl_rights_simple', + ['rights' => $items, 'folder' => $this->mbox, 'labels' => [], 'titles' => []] + ); + + foreach ($data['rights'] as $key => $val) { + $id = "acl$key"; + $title = !empty($data['titles'][$key]) ? $data['titles'][$key] : $this->gettext('longacl'.$key); + $label = !empty($data['labels'][$key]) ? $data['labels'][$key] : $this->gettext('acl'.$key); + $ul .= html::tag('li', null, + $input->show('', ['name' => "acl[$val]", 'value' => $val, 'id' => $id]) + . html::label(['for' => $id, 'title' => $title], $label) + ); + } + + $out .= "\n" . html::tag('ul', $attrib, $ul, html::$common_attrib); + + $this->rc->output->set_env('acl_items', $data['rights']); + + return $out; + } + + /** + * Creates ACL rights form (user part) + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + function templ_user($attrib) + { + // Create username input + $class = !empty($attrib['class']) ? $attrib['class'] : ''; + $attrib['name'] = 'acluser'; + $attrib['class'] = 'form-control'; + + $textfield = new html_inputfield($attrib); + + $label = html::label(['for' => $attrib['id'], 'class' => 'input-group-text'], $this->gettext('username')); + $fields['user'] = html::div('input-group', + html::span('input-group-prepend', $label) . ' ' . $textfield->show() + ); + + // Add special entries + if (!empty($this->specials)) { + foreach ($this->specials as $key) { + $fields[$key] = html::label(['for' => 'id' . $key], $this->gettext($key)); + } + } + + $this->rc->output->set_env('acl_specials', $this->specials); + + // Create list with radio buttons + if (count($fields) > 1) { + $ul = ''; + foreach ($fields as $key => $val) { + $radio = new html_radiobutton(['name' => 'usertype']); + $radio = $radio->show($key == 'user' ? 'user' : '', ['value' => $key, 'id' => 'id' . $key]); + $ul .= html::tag('li', null, $radio . $val); + } + + $out = html::tag('ul', ['id' => 'usertype', 'class' => $class], $ul, html::$common_attrib); + } + // Display text input alone + else { + $out = html::div($class, $fields['user']); + } + + return $out; + } + + /** + * Creates ACL rights table + * + * @param array $attrib Template object attributes + * + * @return string HTML Content + */ + private function list_rights($attrib = []) + { + // Get ACL for the folder + $acl = $this->rc->storage->get_acl($this->mbox); + + if (!is_array($acl)) { + $acl = []; + } + + // Keep special entries (anyone/anonymous) on top of the list + if (!empty($this->specials) && !empty($acl)) { + foreach ($this->specials as $key) { + if (isset($acl[$key])) { + $acl_special[$key] = $acl[$key]; + unset($acl[$key]); + } + } + } + + // Sort the list by username + uksort($acl, 'strnatcasecmp'); + + if (!empty($acl_special)) { + $acl = array_merge($acl_special, $acl); + } + + // Get supported rights and build column names + $supported = $this->rights_supported(); + + // give plugins the opportunity to adjust this list + $data = $this->rc->plugins->exec_hook('acl_rights_supported', + ['rights' => $supported, 'folder' => $this->mbox, 'labels' => []] + ); + $supported = $data['rights']; + + // depending on server capability either use 'te' or 'd' for deleting msgs + $deleteright = implode(array_intersect(str_split('ted'), $supported)); + + // Use advanced or simple (grouped) rights + $advanced = $this->rc->config->get('acl_advanced_mode'); + + if ($advanced) { + $items = []; + foreach ($supported as $sup) { + $items[$sup] = $sup; + } + } + else { + $items = [ + 'read' => 'lrs', + 'write' => 'wi', + 'delete' => $deleteright, + 'other' => preg_replace('/[lrswi'.$deleteright.']/', '', implode($supported)), + ]; + + // give plugins the opportunity to adjust this list + $data = $this->rc->plugins->exec_hook('acl_rights_simple', + ['rights' => $items, 'folder' => $this->mbox, 'labels' => []] + ); + $items = $data['rights']; + } + + // Create the table + $attrib['noheader'] = true; + $table = new html_table($attrib); + $self = $this->rc->get_user_name(); + $js_table = []; + + // Create table header + $table->add_header('user', $this->gettext('identifier')); + foreach (array_keys($items) as $key) { + $label = !empty($data['labels'][$key]) ? $data['labels'][$key] : $this->gettext('shortacl' . $key); + $table->add_header(['class' => 'acl' . $key, 'title' => $label], $label); + } + + foreach ($acl as $user => $rights) { + if ($user === $self) { + continue; + } + + // filter out virtual rights (c or d) the server may return + $userrights = array_intersect($rights, $supported); + $userid = rcube_utils::html_identifier($user); + $title = null; + + if (!empty($this->specials) && in_array($user, $this->specials)) { + $username = $this->gettext($user); + } + else { + $username = $this->resolve_acl_identifier($user, $title); + } + + $table->add_row(['id' => 'rcmrow' . $userid, 'data-userid' => $user]); + $table->add(['class' => 'user text-nowrap', 'title' => $title], + html::a(['id' => 'rcmlinkrow' . $userid], rcube::Q($username)) + ); + + foreach ($items as $key => $right) { + $in = $this->acl_compare($userrights, $right); + switch ($in) { + case 2: $class = 'enabled'; break; + case 1: $class = 'partial'; break; + default: $class = 'disabled'; break; + } + $table->add('acl' . $key . ' ' . $class, ''); + } + + $js_table[$userid] = implode($userrights); + } + + $this->rc->output->set_env('acl', $js_table); + $this->rc->output->set_env('acl_advanced', $advanced); + + $out = $table->show(); + + return $out; + } + + /** + * Handler for ACL update/create action + */ + private function action_save() + { + $mbox = trim(rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true)); // UTF7-IMAP + $user = trim(rcube_utils::get_input_string('_user', rcube_utils::INPUT_POST)); + $acl = trim(rcube_utils::get_input_string('_acl', rcube_utils::INPUT_POST)); + $oldid = trim(rcube_utils::get_input_string('_old', rcube_utils::INPUT_POST)); + + $acl = array_intersect(str_split($acl), $this->rights_supported()); + $users = $oldid ? [$user] : explode(',', $user); + $result = 0; + $self = $this->rc->get_user_name(); + + foreach ($users as $user) { + $user = trim($user); + $username = ''; + $prefix = $this->rc->config->get('acl_groups') ? $this->rc->config->get('acl_group_prefix') : ''; + + if ($prefix && strpos($user, $prefix) === 0) { + $username = $user; + } + else if (!empty($this->specials) && in_array($user, $this->specials)) { + $username = $this->gettext($user); + } + else if (!empty($user)) { + if (!strpos($user, '@') && ($realm = $this->get_realm())) { + $user .= '@' . rcube_utils::idn_to_ascii(preg_replace('/^@/', '', $realm)); + } + + // Make sure it's valid email address to prevent from "disappearing folder" + // issue in Cyrus IMAP e.g. when the acl user identifier contains spaces inside. + if (strpos($user, '@') && !rcube_utils::check_email($user, false)) { + $user = null; + } + + $username = $user; + } + + if (!$acl || !$user || !strlen($mbox)) { + continue; + } + + $user = $this->mod_login($user); + $username = $this->mod_login($username); + + if ($user != $self && $username != $self) { + if ($this->rc->storage->set_acl($mbox, $user, $acl)) { + $display = $this->resolve_acl_identifier($username, $title); + $this->rc->output->command('acl_update', [ + 'id' => rcube_utils::html_identifier($user), + 'username' => $username, + 'title' => $title, + 'display' => $display, + 'acl' => implode($acl), + 'old' => $oldid + ]); + $result++; + } + } + } + + if ($result) { + $this->rc->output->show_message($oldid ? 'acl.updatesuccess' : 'acl.createsuccess', 'confirmation'); + } + else { + $this->rc->output->show_message($oldid ? 'acl.updateerror' : 'acl.createerror', 'error'); + } + } + + /** + * Handler for ACL delete action + */ + private function action_delete() + { + $mbox = trim(rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_POST, true)); //UTF7-IMAP + $user = trim(rcube_utils::get_input_string('_user', rcube_utils::INPUT_POST)); + + $user = explode(',', $user); + + foreach ($user as $u) { + $u = trim($u); + if ($this->rc->storage->delete_acl($mbox, $u)) { + $this->rc->output->command('acl_remove_row', rcube_utils::html_identifier($u)); + } + else { + $error = true; + } + } + + if (empty($error)) { + $this->rc->output->show_message('acl.deletesuccess', 'confirmation'); + } + else { + $this->rc->output->show_message('acl.deleteerror', 'error'); + } + } + + /** + * Handler for ACL list update action (with display mode change) + */ + private function action_list() + { + if (in_array('acl_advanced_mode', (array)$this->rc->config->get('dont_override'))) { + return; + } + + $this->mbox = trim(rcube_utils::get_input_string('_mbox', rcube_utils::INPUT_GPC, true)); // UTF7-IMAP + $advanced = trim(rcube_utils::get_input_string('_mode', rcube_utils::INPUT_GPC)); + $advanced = $advanced == 'advanced'; + + // Save state in user preferences + $this->rc->user->save_prefs(['acl_advanced_mode' => $advanced]); + + $out = $this->list_rights(); + + $out = preg_replace(['/^]+>/', '/<\/table>$/'], '', $out); + + $this->rc->output->command('acl_list_update', $out); + } + + /** + * Creates
    list with descriptive access rights + * + * @param array $rights MYRIGHTS result + * + * @return string HTML content + */ + function acl2text($rights) + { + if (empty($rights)) { + return ''; + } + + $supported = $this->rights_supported(); + $list = []; + $attrib = [ + 'name' => 'rcmyrights', + 'style' => 'margin:0; padding:0 15px;', + ]; + + foreach ($supported as $right) { + if (in_array($right, $rights)) { + $list[] = html::tag('li', null, rcube::Q($this->gettext('acl' . $right))); + } + } + + if (count($list) == count($supported)) { + return rcube::Q($this->gettext('aclfull')); + } + + return html::tag('ul', $attrib, implode("\n", $list)); + } + + /** + * Compares two ACLs (according to supported rights) + * + * @param array $acl1 ACL rights array (or string) + * @param array $acl2 ACL rights array (or string) + * + * @param int Comparison result, 2 - full match, 1 - partial match, 0 - no match + */ + function acl_compare($acl1, $acl2) + { + if (!is_array($acl1)) $acl1 = str_split($acl1); + if (!is_array($acl2)) $acl2 = str_split($acl2); + + $rights = $this->rights_supported(); + + $acl1 = array_intersect($acl1, $rights); + $acl2 = array_intersect($acl2, $rights); + $res = array_intersect($acl1, $acl2); + + $cnt1 = count($res); + $cnt2 = count($acl2); + + if ($cnt1 == $cnt2) { + return 2; + } + + if ($cnt1) { + return 1; + } + + return 0; + } + + /** + * Get list of supported access rights (according to RIGHTS capability) + * + * @return array List of supported access rights abbreviations + */ + function rights_supported() + { + if ($this->supported !== null) { + return $this->supported; + } + + $capa = $this->rc->storage->get_capability('RIGHTS'); + + if (is_array($capa) && !empty($capa)) { + $rights = strtolower($capa[0]); + } + else { + $rights = 'cd'; + } + + return $this->supported = str_split('lrswi' . $rights . 'pa'); + } + + /** + * Username realm detection. + * + * @return string Username realm (domain) + */ + private function get_realm() + { + // When user enters a username without domain part, realm + // allows to add it to the username (and display correct username in the table) + + if (isset($_SESSION['acl_username_realm'])) { + return $_SESSION['acl_username_realm']; + } + + $self = $this->rc->get_user_name(); + + // find realm in username of logged user (?) + list($name, $domain) = rcube_utils::explode('@', $self); + + // Use (always existent) ACL entry on the INBOX for the user to determine + // whether or not the user ID in ACL entries need to be qualified and how + // they would need to be qualified. + if (empty($domain)) { + $acl = $this->rc->storage->get_acl('INBOX'); + if (is_array($acl)) { + $regexp = '/^' . preg_quote($self, '/') . '@(.*)$/'; + foreach (array_keys($acl) as $name) { + if (preg_match($regexp, $name, $matches)) { + $domain = $matches[1]; + break; + } + } + } + } + + return $_SESSION['acl_username_realm'] = $domain; + } + + /** + * Initializes autocomplete LDAP backend + */ + protected function init_ldap() + { + if ($this->ldap) { + return $this->ldap->ready; + } + + // get LDAP config + $config = $this->rc->config->get('acl_users_source'); + + if (empty($config)) { + return false; + } + + // not an array, use configured ldap_public source + if (!is_array($config)) { + $ldap_config = (array) $this->rc->config->get('ldap_public'); + $config = $ldap_config[$config]; + } + + $uid_field = $this->rc->config->get('acl_users_field', 'mail'); + $filter = $this->rc->config->get('acl_users_filter'); + + if (empty($uid_field) || empty($config)) { + return false; + } + + // get name attribute + if (!empty($config['fieldmap'])) { + $name_field = $config['fieldmap']['name']; + } + // ... no fieldmap, use the old method + if (empty($name_field)) { + $name_field = $config['name_field']; + } + + // add UID field to fieldmap, so it will be returned in a record with name + $config['fieldmap']['name'] = $name_field; + $config['fieldmap']['uid'] = $uid_field; + + // search in UID and name fields + // $name_field can be in a form of : (#1490591) + $name_field = preg_replace('/:.*$/', '', $name_field); + $search = array_unique([$name_field, $uid_field]); + + $config['search_fields'] = $search; + $config['required_fields'] = [$uid_field]; + + // set search filter + if ($filter) { + $config['filter'] = $filter; + } + + // disable vlv + $config['vlv'] = false; + + // Initialize LDAP connection + $this->ldap = new rcube_ldap( + $config, + $this->rc->config->get('ldap_debug'), + $this->rc->config->mail_domain($_SESSION['imap_host']) + ); + + return $this->ldap->ready; + } + + /** + * Modify user login according to 'login_lc' setting + */ + protected function mod_login($user) + { + $login_lc = $this->rc->config->get('login_lc'); + + if ($login_lc === true || $login_lc == 2) { + $user = mb_strtolower($user); + } + // lowercase domain name + else if ($login_lc && strpos($user, '@')) { + list($local, $domain) = explode('@', $user); + $user = $local . '@' . mb_strtolower($domain); + } + + return $user; + } + + /** + * Resolve acl identifier to user/group name + */ + protected function resolve_acl_identifier($id, &$title = null) + { + if ($this->init_ldap()) { + $groups = $this->rc->config->get('acl_groups'); + $prefix = $this->rc->config->get('acl_group_prefix'); + $group_field = $this->rc->config->get('acl_group_field', 'name'); + + // Unfortunately this works only if group_field=name, + // list_groups() allows searching by group name only + if ($groups && $prefix && $group_field === 'name' && strpos($id, $prefix) === 0) { + $gid = substr($id, strlen($prefix)); + $result = $this->ldap->list_groups($gid, rcube_addressbook::SEARCH_STRICT); + + if (count($result) === 1 && ($record = $result[0])) { + if (isset($record[$group_field]) && $record[$group_field] === $gid) { + $display = $record['name']; + if ($display != $gid) { + $title = sprintf('%s (%s)', $display, $gid); + } + + return $display; + } + } + + return $id; + } + + $this->ldap->set_pagesize('2'); + // Note: 'uid' works here because we overwrite fieldmap in init_ldap() above + $result = $this->ldap->search('uid', $id, rcube_addressbook::SEARCH_STRICT); + + if ($result->count === 1 && ($record = $result->first())) { + if ($record['uid'] === $id) { + $title = rcube_addressbook::compose_search_name($record); + $display = rcube_addressbook::compose_list_name($record); + + return $display; + } + } + } + + return $id; + } +} diff --git a/ruty/mails/plugins/acl/composer.json b/ruty/mails/plugins/acl/composer.json new file mode 100644 index 0000000..888641d --- /dev/null +++ b/ruty/mails/plugins/acl/composer.json @@ -0,0 +1,24 @@ +{ + "name": "roundcube/acl", + "type": "roundcube-plugin", + "description": "IMAP Folders Access Control Lists Management (RFC4314, RFC2086).", + "license": "GPL-3.0-or-later", + "version": "1.8", + "authors": [ + { + "name": "Aleksander Machniak", + "email": "alec@alec.pl", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/acl/config.inc.php.dist b/ruty/mails/plugins/acl/config.inc.php.dist new file mode 100644 index 0000000..e7c9e4b --- /dev/null +++ b/ruty/mails/plugins/acl/config.inc.php.dist @@ -0,0 +1,33 @@ + +
    +

    + +
    + + + + + +
    +

    +
    + + +
    +
    + + +
    +
    diff --git a/ruty/mails/plugins/additional_message_headers/additional_message_headers.php b/ruty/mails/plugins/additional_message_headers/additional_message_headers.php new file mode 100644 index 0000000..b20f9bc --- /dev/null +++ b/ruty/mails/plugins/additional_message_headers/additional_message_headers.php @@ -0,0 +1,47 @@ + 'My-Very-Own-Webmail']; + * + * @author Ziba Scott + * @website http://roundcube.net + */ +class additional_message_headers extends rcube_plugin +{ + /** + * Plugin initialization + */ + function init() + { + $this->add_hook('message_before_send', [$this, 'message_headers']); + } + + /** + * 'message_before_send' hook handler + * + * @param array $args Hook arguments + * + * @return array Modified hook arguments + */ + function message_headers($args) + { + $this->load_config(); + + $rcube = rcube::get_instance(); + + // additional email headers + $additional_headers = $rcube->config->get('additional_message_headers', []); + + if (!empty($additional_headers)) { + $args['message']->headers($additional_headers, true); + } + + return $args; + } +} diff --git a/ruty/mails/plugins/additional_message_headers/composer.json b/ruty/mails/plugins/additional_message_headers/composer.json new file mode 100644 index 0000000..08f422d --- /dev/null +++ b/ruty/mails/plugins/additional_message_headers/composer.json @@ -0,0 +1,24 @@ +{ + "name": "roundcube/additional_message_headers", + "type": "roundcube-plugin", + "description": "Very simple plugin which will add additional headers to or remove them from outgoing messages.", + "license": "GPL-3.0-or-later", + "version": "1.2.1", + "authors": [ + { + "name": "Ziba Scott", + "email": "email@example.org", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/additional_message_headers/config.inc.php.dist b/ruty/mails/plugins/additional_message_headers/config.inc.php.dist new file mode 100644 index 0000000..9046813 --- /dev/null +++ b/ruty/mails/plugins/additional_message_headers/config.inc.php.dist @@ -0,0 +1,14 @@ + diff --git a/ruty/mails/plugins/archive/archive.js b/ruty/mails/plugins/archive/archive.js new file mode 100644 index 0000000..0975dff --- /dev/null +++ b/ruty/mails/plugins/archive/archive.js @@ -0,0 +1,81 @@ +/** + * Archive plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (c) The Roundcube Dev Team + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ + +function rcmail_archive(prop) +{ + if (rcmail_is_archive()) + return; + + var post_data = rcmail.selection_post_data(); + + // exit if selection is empty + if (!post_data._uid) + return; + + // Disable message command buttons until a message is selected + rcmail.enable_command(rcmail.env.message_commands, false); + rcmail.enable_command('plugin.archive', false); + + // let the server sort the messages to the according subfolders + rcmail.with_selected_messages('move', post_data, null, 'plugin.move2archive'); + + // Reset preview (must be after with_selected_messages() call) + rcmail.show_contentframe(false); +} + +function rcmail_is_archive() +{ + // check if current folder is an archive folder or one of its children + return rcmail.env.mailbox == rcmail.env.archive_folder + || rcmail.env.mailbox.startsWith(rcmail.env.archive_folder + rcmail.env.delimiter); +} + +// callback for app-onload event +if (window.rcmail) { + rcmail.addEventListener('init', function(evt) { + // register command (directly enable in message view mode) + rcmail.register_command('plugin.archive', rcmail_archive, rcmail.env.uid && !rcmail_is_archive()); + + // add event-listener to message list + if (rcmail.message_list) + rcmail.message_list.addEventListener('select', function(list) { + rcmail.enable_command('plugin.archive', list.get_selection().length > 0 && !rcmail_is_archive()); + }); + + // set css style for archive folder + var li; + if (rcmail.env.archive_folder) { + // in Settings > Folders + if (rcmail.subscription_list) + li = rcmail.subscription_list.get_item(rcmail.env.archive_folder); + // in folders list + else + li = rcmail.get_folder_li(rcmail.env.archive_folder, '', true); + + if (li) + $(li).addClass('archive'); + + // in folder selector popup + rcmail.addEventListener('menu-open', function(p) { + if (p.name == 'folder-selector') { + var search = rcmail.env.archive_folder; + $('a', p.obj).filter(function() { return $(this).data('id') == search; }).parent().addClass('archive'); + } + }); + } + }); +} diff --git a/ruty/mails/plugins/archive/archive.min.js b/ruty/mails/plugins/archive/archive.min.js new file mode 100644 index 0000000..bb25abb --- /dev/null +++ b/ruty/mails/plugins/archive/archive.min.js @@ -0,0 +1,17 @@ +/** + * Archive plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (c) The Roundcube Dev Team + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ +function rcmail_archive(i){var e;rcmail_is_archive()||(e=rcmail.selection_post_data())._uid&&(rcmail.enable_command(rcmail.env.message_commands,!1),rcmail.enable_command("plugin.archive",!1),rcmail.with_selected_messages("move",e,null,"plugin.move2archive"),rcmail.show_contentframe(!1))}function rcmail_is_archive(){return rcmail.env.mailbox==rcmail.env.archive_folder||rcmail.env.mailbox.startsWith(rcmail.env.archive_folder+rcmail.env.delimiter)}window.rcmail&&rcmail.addEventListener("init",function(i){var e;rcmail.register_command("plugin.archive",rcmail_archive,rcmail.env.uid&&!rcmail_is_archive()),rcmail.message_list&&rcmail.message_list.addEventListener("select",function(i){rcmail.enable_command("plugin.archive",0archive_folder = $rcmail->config->get('archive_mbox'); + + if ($rcmail->task == 'mail' && ($rcmail->action == '' || $rcmail->action == 'show') && $this->archive_folder) { + $this->include_stylesheet($this->local_skin_path() . '/archive.css'); + $this->include_script('archive.js'); + $this->add_texts('localization', true); + $this->add_button( + [ + 'type' => 'link', + 'label' => 'buttontext', + 'command' => 'plugin.archive', + 'class' => 'button buttonPas archive disabled', + 'classact' => 'button archive', + 'width' => 32, + 'height' => 32, + 'title' => 'buttontitle', + 'domain' => $this->ID, + 'innerclass' => 'inner', + ], + 'toolbar'); + + // register hook to localize the archive folder + $this->add_hook('render_mailboxlist', [$this, 'render_mailboxlist']); + + // set env variables for client + $rcmail->output->set_env('archive_folder', $this->archive_folder); + $rcmail->output->set_env('archive_type', $rcmail->config->get('archive_type','')); + } + else if ($rcmail->task == 'mail') { + // handler for ajax request + $this->register_action('plugin.move2archive', [$this, 'move_messages']); + } + else if ($rcmail->task == 'settings') { + $this->add_hook('preferences_list', [$this, 'prefs_table']); + $this->add_hook('preferences_save', [$this, 'prefs_save']); + + if ($rcmail->action == 'folders' && $this->archive_folder) { + $this->include_stylesheet($this->local_skin_path() . '/archive.css'); + $this->include_script('archive.js'); + // set env variables for client + $rcmail->output->set_env('archive_folder', $this->archive_folder); + } + } + } + + /** + * Hook to give the archive folder a localized name in the mailbox list + */ + function render_mailboxlist($p) + { + // set localized name for the configured archive folder + if ($this->archive_folder && !rcmail::get_instance()->config->get('show_real_foldernames')) { + if (isset($p['list'][$this->archive_folder])) { + $p['list'][$this->archive_folder]['name'] = $this->gettext('archivefolder'); + } + else { + // search in subfolders + $this->_mod_folder_name($p['list'], $this->archive_folder, $this->gettext('archivefolder')); + } + } + + return $p; + } + + /** + * Helper method to find the archive folder in the mailbox tree + */ + private function _mod_folder_name(&$list, $folder, $new_name) + { + foreach ($list as $idx => $item) { + if ($item['id'] == $folder) { + $list[$idx]['name'] = $new_name; + return true; + } + else if (!empty($item['folders'])) { + if ($this->_mod_folder_name($list[$idx]['folders'], $folder, $new_name)) { + return true; + } + } + } + + return false; + } + + /** + * Plugin action to move the submitted list of messages to the archive subfolders + * according to the user settings and their headers. + */ + function move_messages() + { + $rcmail = rcmail::get_instance(); + + // only process ajax requests + if (!$rcmail->output->ajax_call) { + return; + } + + $this->add_texts('localization'); + + $storage = $rcmail->get_storage(); + $delimiter = $storage->get_hierarchy_delimiter(); + $threading = (bool) $storage->get_threading(); + $read_on_move = (bool) $rcmail->config->get('read_on_archive'); + $archive_type = $rcmail->config->get('archive_type', ''); + $archive_prefix = $this->archive_folder . $delimiter; + $search_request = rcube_utils::get_input_string('_search', rcube_utils::INPUT_GPC); + $from_show_action = !empty($_POST['_from']) && $_POST['_from'] == 'show'; + + // count messages before changing anything + $old_count = 0; + if (!$from_show_action) { + $old_count = $storage->count(null, $threading ? 'THREADS' : 'ALL'); + } + + $sort_col = rcmail_action_mail_index::sort_column(); + $sort_ord = rcmail_action_mail_index::sort_order(); + $count = 0; + $uids = null; + + // this way response handler for 'move' action will be executed + $rcmail->action = 'move'; + $this->result = [ + 'reload' => false, + 'error' => false, + 'sources' => [], + 'destinations' => [], + ]; + + foreach (rcmail::get_uids(null, null, $multifolder, rcube_utils::INPUT_POST) as $mbox => $uids) { + if (!$this->archive_folder || $mbox === $this->archive_folder || strpos($mbox, $archive_prefix) === 0) { + $count = count($uids); + continue; + } + else if (!$archive_type || $archive_type == 'folder') { + $folder = $this->archive_folder; + + if ($archive_type == 'folder') { + // compose full folder path + $folder .= $delimiter . $mbox; + } + + // create archive subfolder if it doesn't yet exist + $this->subfolder_worker($folder); + + $count += $this->move_messages_worker($uids, $mbox, $folder, $read_on_move); + } + else { + if ($uids == '*') { + $index = $storage->index(null, $sort_col, $sort_ord); + $uids = $index->get(); + } + + $messages = $storage->fetch_headers($mbox, $uids); + $execute = []; + + foreach ($messages as $message) { + $subfolder = null; + switch ($archive_type) { + case 'year': + $subfolder = $rcmail->format_date($message->timestamp, 'Y'); + break; + + case 'month': + $subfolder = $rcmail->format_date($message->timestamp, 'Y') + . $delimiter . $rcmail->format_date($message->timestamp, 'm'); + break; + + case 'tbmonth': + $subfolder = $rcmail->format_date($message->timestamp, 'Y') + . $delimiter . $rcmail->format_date($message->timestamp, 'Y') + . '-' . $rcmail->format_date($message->timestamp, 'm'); + break; + + case 'sender': + $subfolder = $this->sender_subfolder($message->get('from')); + break; + + case 'folderyear': + $subfolder = $rcmail->format_date($message->timestamp, 'Y') + . $delimiter . $mbox; + break; + + case 'foldermonth': + $subfolder = $rcmail->format_date($message->timestamp, 'Y') + . $delimiter . $rcmail->format_date($message->timestamp, 'm') + . $delimiter . $mbox; + break; + } + + // compose full folder path + $folder = $this->archive_folder . ($subfolder ? $delimiter . $subfolder : ''); + + $execute[$folder][] = $message->uid; + } + + foreach ($execute as $folder => $uids) { + // create archive subfolder if it doesn't yet exist + $this->subfolder_worker($folder); + + $count += $this->move_messages_worker($uids, $mbox, $folder, $read_on_move); + } + } + } + + if ($this->result['error']) { + if (!$from_show_action) { + $rcmail->output->command('list_mailbox'); + } + + $rcmail->output->show_message($this->gettext('archiveerror'), 'warning'); + $rcmail->output->send(); + } + + if (!empty($_POST['_refresh'])) { + // FIXME: send updated message rows instead of reloading the entire list + $rcmail->output->command('refresh_list'); + $addrows = false; + } + else { + $addrows = true; + } + + // refresh saved search set after moving some messages + if ($search_request && $rcmail->storage->get_search_set()) { + $_SESSION['search'] = $rcmail->storage->refresh_search(); + } + + if ($from_show_action) { + 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 = $storage->get_folder(); + $msg_count = $storage->count(null, $threading ? 'THREADS' : 'ALL'); + $exists = $storage->count($mbox, 'EXISTS', true); + $page_size = $storage->get_pagesize(); + $page = $storage->get_page(); + $pages = ceil($msg_count / $page_size); + $nextpage_count = $old_count - $page_size * $page; + $remaining = $msg_count - $page_size * ($page - 1); + $quota_root = $multifolder ? $this->result['sources'][0] : 'INBOX'; + $jump_back = false; + + // jump back one page (user removed the whole last page) + if ($page > 1 && $remaining == 0) { + $page -= 1; + $storage->set_page($page); + $_SESSION['page'] = $page; + $jump_back = true; + } + + // update unread messages counts for all involved folders + foreach ($this->result['sources'] as $folder) { + rcmail_action_mail_index::send_unread_count($folder, 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', rcmail_action::quota_content(null, $quota_root)); + $rcmail->output->command('set_rowcount', rcmail_action_mail_index::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 && $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 = $storage->list_messages($mbox, null, $sort_col, $sort_ord, $count); + + rcmail_action_mail_index::js_message_list($a_headers, false); + } + + if ($this->result['reload']) { + $rcmail->output->show_message($this->gettext('archivedreload'), 'confirmation'); + } + else { + $rcmail->output->show_message($this->gettext('archived'), 'confirmation'); + + if (!$read_on_move) { + foreach ($this->result['destinations'] as $folder) { + rcmail_action_mail_index::send_unread_count($folder, true); + } + } + } + + // send response + $rcmail->output->send(); + } + + /** + * Move messages from one folder to another and mark as read if needed + */ + private function move_messages_worker($uids, $from_mbox, $to_mbox, $read_on_move) + { + $storage = rcmail::get_instance()->get_storage(); + + if ($read_on_move) { + // don't flush cache (4th argument) + $storage->set_flag($uids, 'SEEN', $from_mbox, true); + } + + // move message to target folder + if ($storage->move_message($uids, $to_mbox, $from_mbox)) { + if (!in_array($from_mbox, $this->result['sources'])) { + $this->result['sources'][] = $from_mbox; + } + if (!in_array($to_mbox, $this->result['destinations'])) { + $this->result['destinations'][] = $to_mbox; + } + + return count($uids); + } + + $this->result['error'] = true; + } + + /** + * Create archive subfolder if it doesn't yet exist + */ + private function subfolder_worker($folder) + { + $storage = rcmail::get_instance()->get_storage(); + $delimiter = $storage->get_hierarchy_delimiter(); + + if ($this->folders === null) { + $this->folders = $storage->list_folders('', $this->archive_folder . '*', 'mail', null, true); + } + + if (!in_array($folder, $this->folders)) { + $path = explode($delimiter, $folder); + + // we'll create all folders in the path + for ($i=0; $ifolders)) { + if ($storage->create_folder($_folder, true)) { + $this->result['reload'] = true; + $this->folders[] = $_folder; + } + } + } + } + } + + /** + * Hook to inject plugin-specific user settings + * + * @param array $args Hook arguments + * + * @return array Modified hook arguments + */ + function prefs_table($args) + { + $this->add_texts('localization'); + + $rcmail = rcmail::get_instance(); + $dont_override = $rcmail->config->get('dont_override', []); + + if ($args['section'] == 'folders' && !in_array('archive_mbox', $dont_override)) { + $mbox = $rcmail->config->get('archive_mbox'); + $type = $rcmail->config->get('archive_type'); + + // load folders list when needed + if ($args['current']) { + $select = rcmail_action::folder_selector([ + 'noselection' => '---', + 'realnames' => true, + 'maxlength' => 30, + 'folder_filter' => 'mail', + 'folder_rights' => 'w', + 'onchange' => "if ($(this).val() == 'INBOX') $(this).val('')", + 'class' => 'custom-select', + ]); + } + else { + $select = new html_select(); + } + + $args['blocks']['main']['options']['archive_mbox'] = [ + 'title' => html::label('_archive_mbox', rcube::Q($this->gettext('archivefolder'))), + 'content' => $select->show($mbox, ['id' => '_archive_mbox', 'name' => '_archive_mbox']) + ]; + + // If the server supports only either messages or folders in a folder + // we do not allow archive splitting, for simplicity (#5057) + if ($rcmail->get_storage()->get_capability(rcube_storage::DUAL_USE_FOLDERS)) { + // add option for structuring the archive folder + $archive_type = new html_select(['name' => '_archive_type', 'id' => 'ff_archive_type', 'class' => 'custom-select']); + $archive_type->add($this->gettext('none'), ''); + $archive_type->add($this->gettext('archivetypeyear'), 'year'); + $archive_type->add($this->gettext('archivetypemonth'), 'month'); + $archive_type->add($this->gettext('archivetypetbmonth'), 'tbmonth'); + $archive_type->add($this->gettext('archivetypesender'), 'sender'); + $archive_type->add($this->gettext('archivetypefolder'), 'folder'); + $archive_type->add($this->gettext('archivetypefolderyear'), 'folderyear'); + $archive_type->add($this->gettext('archivetypefoldermonth'), 'foldermonth'); + + $args['blocks']['archive'] = [ + 'name' => rcube::Q($this->gettext('settingstitle')), + 'options' => [ + 'archive_type' => [ + 'title' => html::label('ff_archive_type', rcube::Q($this->gettext('archivetype'))), + 'content' => $archive_type->show($type) + ] + ] + ]; + } + } + else if ($args['section'] == 'server' && !in_array('read_on_archive', $dont_override)) { + $chbox = new html_checkbox(['name' => '_read_on_archive', 'id' => 'ff_read_on_archive', 'value' => 1]); + $args['blocks']['main']['options']['read_on_archive'] = [ + 'title' => html::label('ff_read_on_archive', rcube::Q($this->gettext('readonarchive'))), + 'content' => $chbox->show($rcmail->config->get('read_on_archive') ? 1 : 0) + ]; + } + + return $args; + } + + /** + * Hook to save plugin-specific user settings + * + * @param array $args Hook arguments + * + * @return array Modified hook arguments + */ + function prefs_save($args) + { + $rcmail = rcmail::get_instance(); + $dont_override = $rcmail->config->get('dont_override', []); + + if ($args['section'] == 'folders' && !in_array('archive_mbox', $dont_override)) { + $args['prefs']['archive_type'] = rcube_utils::get_input_string('_archive_type', rcube_utils::INPUT_POST); + } + else if ($args['section'] == 'server' && !in_array('read_on_archive', $dont_override)) { + $args['prefs']['read_on_archive'] = (bool) rcube_utils::get_input_value('_read_on_archive', rcube_utils::INPUT_POST); + } + + return $args; + } + + /** + * Create folder name from the message sender address + */ + protected function sender_subfolder($from) + { + static $delim; + static $vendor; + static $skip_hidden; + + preg_match('/[\b<](.+@.+)[\b>]/i', $from, $m); + + if (empty($m[1])) { + return $this->gettext('unkownsender'); + } + + if ($delim === null) { + $rcmail = rcmail::get_instance(); + $storage = $rcmail->get_storage(); + $delim = $storage->get_hierarchy_delimiter(); + $vendor = $storage->get_vendor(); + $skip_hidden = $rcmail->config->get('imap_skip_hidden_folders'); + } + + // Remove some forbidden characters + $regexp = '\\x00-\\x1F\\x7F%*'; + + if ($vendor == 'cyrus') { + // List based on testing Kolab's Cyrus-IMAP 2.5 + $regexp .= '!`(){}|\\?<;"'; + } + + $folder_name = preg_replace("/[$regexp]/", '', $m[1]); + + if ($skip_hidden && $folder_name[0] == '.') { + $folder_name = substr($folder_name, 1); + } + + $replace = $delim == '-' ? '_' : '-'; + $replacements = [$delim => $replace]; + + // Cyrus-IMAP does not allow @ character in folder name + if ($vendor == 'cyrus') { + $replacements['@'] = $replace; + } + + // replace reserved characters in folder name + return strtr($folder_name, $replacements); + } +} diff --git a/ruty/mails/plugins/archive/composer.json b/ruty/mails/plugins/archive/composer.json new file mode 100644 index 0000000..9ef4cb1 --- /dev/null +++ b/ruty/mails/plugins/archive/composer.json @@ -0,0 +1,29 @@ +{ + "name": "roundcube/archive", + "type": "roundcube-plugin", + "description": "This adds a button to move the selected messages to an archive folder. The folder (and the optional structure of subfolders) can be selected in the settings panel.", + "license": "GPL-3.0-or-later", + "version": "3.5", + "authors": [ + { + "name": "Thomas Bruederli", + "email": "roundcube@gmail.com", + "role": "Lead" + }, + { + "name": "Aleksander Machniak", + "email": "alec@alec.pl", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/archive/localization/ar.inc b/ruty/mails/plugins/archive/localization/ar.inc new file mode 100644 index 0000000..c14ba12 --- /dev/null +++ b/ruty/mails/plugins/archive/localization/ar.inc @@ -0,0 +1,24 @@ +]*>(.|[\r\n])*<\/blockquote>/gmi, '') + .replace(/<[^>]+>/gm, ' ') + .replace(/ /g, ' '); + } + else { + // Remove quoted content + msg = msg.replace(/^>.*$/gmi, ''); + } + + return msg; +}; + +function rcmail_check_message(msg) +{ + var i, rx, keywords = rcmail.get_label('keywords', 'attachment_reminder').split(",").concat([".doc", ".pdf"]); + + keywords = $.map(keywords, function(n) { return RegExp.escape(n); }); + rx = new RegExp('(' + keywords.join('|') + ')', 'i'); + + return msg.search(rx) != -1; +}; + +function rcmail_have_attachments() +{ + return rcmail.env.attachments && $('li', rcmail.gui_objects.attachmentlist).length; +}; + +function rcmail_attachment_reminder_dialog() +{ + var buttons = {}; + + buttons[rcmail.get_label('addattachment')] = function() { + $(this).remove(); + $('#messagetoolbar a.attach, .toolbar a.attach').first().click(); + }; + buttons[rcmail.get_label('send')] = function(e) { + $(this).remove(); + rcmail.env.attachment_reminder = true; + rcmail.command('send', '', e); + }; + + rcmail.env.attachment_reminder = false; + rcmail.show_popup_dialog( + rcmail.get_label('attachment_reminder.forgotattachment'), + rcmail.get_label('attachment_reminder.missingattachment'), + buttons, + {button_classes: ['mainaction attach', 'send']} + ); +}; + + +if (window.rcmail) { + rcmail.addEventListener('beforesend', function(evt) { + var msg = rcmail_get_compose_message(), + subject = $('#compose-subject').val(); + + if (!rcmail.env.attachment_reminder && !rcmail_have_attachments() + && (rcmail_check_message(msg) || rcmail_check_message(subject)) + ) { + rcmail_attachment_reminder_dialog(); + return false; + } + }); +} diff --git a/ruty/mails/plugins/attachment_reminder/attachment_reminder.min.js b/ruty/mails/plugins/attachment_reminder/attachment_reminder.min.js new file mode 100644 index 0000000..7a92173 --- /dev/null +++ b/ruty/mails/plugins/attachment_reminder/attachment_reminder.min.js @@ -0,0 +1,17 @@ +/** + * Attachment Reminder plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (c) The Roundcube Dev Team + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ +function rcmail_get_compose_message(){var e=rcmail.editor.get_content({nosig:!0});return e=rcmail.editor.is_html()?e.replace(/]*>(.|[\r\n])*<\/blockquote>/gim,"").replace(/<[^>]+>/gm," ").replace(/ /g," "):e.replace(/^>.*$/gim,"")}function rcmail_check_message(e){var a=rcmail.get_label("keywords","attachment_reminder").split(",").concat([".doc",".pdf"]),a=$.map(a,function(e){return RegExp.escape(e)}),a=new RegExp("("+a.join("|")+")","i");return-1!=e.search(a)}function rcmail_have_attachments(){return rcmail.env.attachments&&$("li",rcmail.gui_objects.attachmentlist).length}function rcmail_attachment_reminder_dialog(){var e={};e[rcmail.get_label("addattachment")]=function(){$(this).remove(),$("#messagetoolbar a.attach, .toolbar a.attach").first().click()},e[rcmail.get_label("send")]=function(e){$(this).remove(),rcmail.env.attachment_reminder=!0,rcmail.command("send","",e)},rcmail.env.attachment_reminder=!1,rcmail.show_popup_dialog(rcmail.get_label("attachment_reminder.forgotattachment"),rcmail.get_label("attachment_reminder.missingattachment"),e,{button_classes:["mainaction attach","send"]})}window.rcmail&&rcmail.addEventListener("beforesend",function(e){var a=rcmail_get_compose_message(),t=$("#compose-subject").val();if(!rcmail.env.attachment_reminder&&!rcmail_have_attachments()&&(rcmail_check_message(a)||rcmail_check_message(t)))return rcmail_attachment_reminder_dialog(),!1}); diff --git a/ruty/mails/plugins/attachment_reminder/attachment_reminder.php b/ruty/mails/plugins/attachment_reminder/attachment_reminder.php new file mode 100644 index 0000000..82783fb --- /dev/null +++ b/ruty/mails/plugins/attachment_reminder/attachment_reminder.php @@ -0,0 +1,100 @@ + + * + * Copyright (C) 2013 Thomas Yu - Sian, Liu + * Copyright (C) Kolab Systems AG + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + */ + +class attachment_reminder extends rcube_plugin +{ + public $task = 'mail|settings'; + public $noajax = true; + + + /** + * Plugin initialization + */ + function init() + { + $rcmail = rcube::get_instance(); + + if ($rcmail->task == 'mail' && $rcmail->action == 'compose') { + if ($rcmail->config->get('attachment_reminder')) { + $this->include_script('attachment_reminder.js'); + $this->add_texts('localization/', ['keywords', 'forgotattachment', 'missingattachment']); + $rcmail->output->add_label('addattachment', 'send'); + } + } + + if ($rcmail->task == 'settings') { + $dont_override = $rcmail->config->get('dont_override', []); + + if (!in_array('attachment_reminder', $dont_override)) { + $this->add_hook('preferences_list', [$this, 'prefs_list']); + $this->add_hook('preferences_save', [$this, 'prefs_save']); + } + } + } + + /** + * 'preferences_list' hook handler + * + * @param array $args Hook arguments + * + * @return array Hook arguments + */ + function prefs_list($args) + { + if ($args['section'] == 'compose') { + $this->add_texts('localization/'); + $reminder = rcube::get_instance()->config->get('attachment_reminder'); + $field_id = 'rcmfd_attachment_reminder'; + $checkbox = new html_checkbox(['name' => '_attachment_reminder', 'id' => $field_id, 'value' => 1]); + + $args['blocks']['main']['options']['attachment_reminder'] = [ + 'title' => html::label($field_id, rcube::Q($this->gettext('reminderoption'))), + 'content' => $checkbox->show($reminder ? 1 : 0), + ]; + } + + return $args; + } + + /** + * 'preferences_save' hook handler + * + * @param array $args Hook arguments + * + * @return array Hook arguments + */ + function prefs_save($args) + { + if ($args['section'] == 'compose') { + $dont_override = rcube::get_instance()->config->get('dont_override', []); + if (!in_array('attachment_reminder', $dont_override)) { + $args['prefs']['attachment_reminder'] = !empty($_POST['_attachment_reminder']); + } + } + + return $args; + } +} diff --git a/ruty/mails/plugins/attachment_reminder/composer.json b/ruty/mails/plugins/attachment_reminder/composer.json new file mode 100644 index 0000000..0a0e884 --- /dev/null +++ b/ruty/mails/plugins/attachment_reminder/composer.json @@ -0,0 +1,29 @@ +{ + "name": "roundcube/attachment_reminder", + "type": "roundcube-plugin", + "description": "This Roundcube plugin reminds the user to attach a file if the composed message text indicates that there should be any.", + "license": "GPL-3.0-or-later", + "version": "1.1", + "authors": [ + { + "name": "Aleksander Machniak", + "email": "alec@alec.pl", + "role": "Lead" + }, + { + "name": "Thomas Yu - Sian, Liu", + "email": "", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/attachment_reminder/localization/ar.inc b/ruty/mails/plugins/attachment_reminder/localization/ar.inc new file mode 100644 index 0000000..8f78077 --- /dev/null +++ b/ruty/mails/plugins/attachment_reminder/localization/ar.inc @@ -0,0 +1,18 @@ +add_hook('startup', [$this, 'startup']); + $this->add_hook('authenticate', [$this, 'authenticate']); + } + + /** + * 'startup' hook handler + * + * @param array $args Hook arguments + * + * @return array Hook arguments + */ + function startup($args) + { + // change action to login + if (empty($_SESSION['user_id']) && !empty($_GET['_autologin']) && $this->is_localhost()) { + $args['action'] = 'login'; + } + + return $args; + } + + /** + * 'authenticate' hook handler + * + * @param array $args Hook arguments + * + * @return array Hook arguments + */ + function authenticate($args) + { + if (!empty($_GET['_autologin']) && $this->is_localhost()) { + $args['user'] = 'me'; + $args['pass'] = '******'; + $args['host'] = 'localhost'; + $args['cookiecheck'] = false; + $args['valid'] = true; + } + + return $args; + } + + /** + * Checks if the request comes from localhost + * + * @return bool + */ + private function is_localhost() + { + return $_SERVER['REMOTE_ADDR'] == '::1' || $_SERVER['REMOTE_ADDR'] == '127.0.0.1'; + } +} diff --git a/ruty/mails/plugins/autologon/composer.json b/ruty/mails/plugins/autologon/composer.json new file mode 100644 index 0000000..5797fb9 --- /dev/null +++ b/ruty/mails/plugins/autologon/composer.json @@ -0,0 +1,24 @@ +{ + "name": "roundcube/autologon", + "type": "roundcube-plugin", + "description": "Sample plugin to try out some hooks", + "license": "GPL-3.0-or-later", + "version": "1.0", + "authors": [ + { + "name": "Thomas Bruederli", + "email": "roundcube@gmail.com", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/autologout/autologout.php b/ruty/mails/plugins/autologout/autologout.php new file mode 100644 index 0000000..e8f792c --- /dev/null +++ b/ruty/mails/plugins/autologout/autologout.php @@ -0,0 +1,69 @@ + + * + * + * + * + * + * + * This plugin won't work if the POST request is made using CURL or other + * methods. It will only work if the POST request is made by submitting a + * form similar to the one from above. The form can be hidden and it can + * be sent automatically using JavaScript or JQuery (for example by using: + * $("#loSubmitButton").click();) + */ + +class autologout extends rcube_plugin +{ + public $task = 'logout'; + + /** + * Plugin initialization + */ + public function init() + { + $this->add_hook('startup', [$this, 'startup']); + } + + /** + * Request handler + */ + public function startup($args) + { + $rcmail = rcmail::get_instance(); + + // Change task and action to logout + if (!empty($_SESSION['user_id']) && !empty($_POST['_autologout']) && $this->known_client()) { + $rcmail->logout_actions(); + $rcmail->kill_session(); + } + + return $args; + } + + /** + * Checks if the request came from an allowed client IP + */ + private function known_client() + { + /* + * If you want to restrict the use of this plugin to specific + * remote clients, you can verify the remote client's IP like this: + * + * return in_array(rcube_utils::remote_addr(), ['123.123.123.123', '124.124.124.124']); + */ + + return true; + } +} diff --git a/ruty/mails/plugins/autologout/composer.json b/ruty/mails/plugins/autologout/composer.json new file mode 100644 index 0000000..98ab1d9 --- /dev/null +++ b/ruty/mails/plugins/autologout/composer.json @@ -0,0 +1,17 @@ +{ + "name": "roundcube/autologout", + "type": "roundcube-plugin", + "description": "Plugin to auto log out users with a POST request sent from an external site.", + "license": "GPLv3+", + "version": "1.0", + "authors": [ + { + "name": "Cover Tower LLC", + "email": "contact@covertower.com" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/database_attachments/composer.json b/ruty/mails/plugins/database_attachments/composer.json new file mode 100644 index 0000000..16b3311 --- /dev/null +++ b/ruty/mails/plugins/database_attachments/composer.json @@ -0,0 +1,30 @@ +{ + "name": "roundcube/database_attachments", + "type": "roundcube-plugin", + "description": "This plugin which provides database backed storage for temporary attachment file handling. The primary advantage of this plugin is its compatibility with round-robin dns multi-server Roundcube installations.", + "license": "GPL-3.0-or-later", + "version": "1.2", + "authors": [ + { + "name": "Aleksander Machniak", + "email": "alec@alec.pl", + "role": "Lead" + }, + { + "name": "Ziba Scott", + "email": "ziba@umich.edu", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3", + "roundcube/filesystem_attachments": ">=1.0.0" + } +} diff --git a/ruty/mails/plugins/database_attachments/config.inc.php.dist b/ruty/mails/plugins/database_attachments/config.inc.php.dist new file mode 100644 index 0000000..dd3ad7d --- /dev/null +++ b/ruty/mails/plugins/database_attachments/config.inc.php.dist @@ -0,0 +1,14 @@ + + * @author Aleksander Machniak + * + * Copyright (C) The Roundcube Dev Team + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +require_once INSTALL_PATH . 'plugins/filesystem_attachments/filesystem_attachments.php'; + +class database_attachments extends filesystem_attachments +{ + // Cache object + protected $cache; + + // A prefix for the cache key used in the session and in the key field of the cache table + const PREFIX = "ATTACH"; + + /** + * Save a newly uploaded attachment + */ + function upload($args) + { + $args['status'] = false; + + $cache = $this->get_cache(); + $key = $this->_key($args); + $data = file_get_contents($args['path']); + + if ($data === false) { + return $args; + } + + $data = base64_encode($data); + $status = $cache->set($key, $data); + + if ($status) { + $args['id'] = $key; + $args['status'] = true; + $args['path'] = null; + } + + return $args; + } + + /** + * Save an attachment from a non-upload source (draft or forward) + */ + function save($args) + { + $args['status'] = false; + + $cache = $this->get_cache(); + $key = $this->_key($args); + + if (!empty($args['path'])) { + $args['data'] = file_get_contents($args['path']); + + if ($args['data'] === false) { + return $args; + } + + $args['path'] = null; + } + + $data = base64_encode($args['data']); + $status = $cache->set($key, $data); + + if ($status) { + $args['id'] = $key; + $args['status'] = true; + } + + return $args; + } + + /** + * Remove an attachment from storage + * This is triggered by the remove attachment button on the compose screen + */ + function remove($args) + { + $cache = $this->get_cache(); + $status = $cache->remove($args['id']); + + $args['status'] = true; + + return $args; + } + + /** + * When composing an html message, image attachments may be shown + * For this plugin, $this->get() will check the file and + * return it's contents + */ + function display($args) + { + return $this->get($args); + } + + /** + * When displaying or sending the attachment the file contents are fetched + * using this method. This is also called by the attachment_display hook. + */ + function get($args) + { + $cache = $this->get_cache(); + $data = $cache->get($args['id']); + + if ($data !== null && $data !== false) { + $args['data'] = base64_decode($data); + $args['status'] = true; + } + else { + $args['status'] = false; + } + + return $args; + } + + /** + * Delete all temp files associated with this user + */ + function cleanup($args) + { + // check if cache object exist, it may be empty on session_destroy (#1489726) + if ($cache = $this->get_cache()) { + $cache->remove($args['group'] ?? null, true); + } + } + + /** + * Helper method to generate a unique key for the given attachment file + */ + protected function _key($args) + { + $uname = !empty($args['path']) ? $args['path'] : $args['name']; + return $args['group'] . md5(microtime() . $uname . $_SESSION['user_id']); + } + + /** + * Initialize and return cache object + */ + protected function get_cache() + { + if (!$this->cache) { + $this->load_config(); + + $rcmail = rcube::get_instance(); + $ttl = 12 * 60 * 60; // default: 12 hours + $ttl = $rcmail->config->get('database_attachments_cache_ttl', $ttl); + $type = $rcmail->config->get('database_attachments_cache', 'db'); + $prefix = self::PREFIX; + + // Add session identifier to the prefix to prevent from removing attachments + // in other sessions of the same user (#1490542) + if ($id = session_id()) { + $prefix .= $id; + } + + // Init SQL cache (disable cache data serialization) + $this->cache = $rcmail->get_cache($prefix, $type, $ttl, false, true); + } + + return $this->cache; + } +} diff --git a/ruty/mails/plugins/debug_logger/composer.json b/ruty/mails/plugins/debug_logger/composer.json new file mode 100644 index 0000000..bdd516c --- /dev/null +++ b/ruty/mails/plugins/debug_logger/composer.json @@ -0,0 +1,24 @@ +{ + "name": "roundcube/debug_logger", + "type": "roundcube-plugin", + "description": "Enhanced logging for debugging purposes. It is not recommended to be enabled on production systems without testing because of the somewhat increased memory, cpu and disk i/o overhead.", + "license": "GPL-3.0-or-later", + "version": "1.0", + "authors": [ + { + "name": "Ziba Scott", + "email": "ziba@umich.edu", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/debug_logger/debug_logger.php b/ruty/mails/plugins/debug_logger/debug_logger.php new file mode 100644 index 0000000..c31dd06 --- /dev/null +++ b/ruty/mails/plugins/debug_logger/debug_logger.php @@ -0,0 +1,159 @@ +plugins->init()): + * + * rcube::console("my test","start"); + * rcube::console("my message"); + * rcube::console("my sql calls","start"); + * rcube::console("cp -r * /dev/null","shell exec"); + * rcube::console("select * from example","sql"); + * rcube::console("select * from example","sql"); + * rcube::console("select * from example","sql"); + * rcube::console("end"); + * rcube::console("end"); + * + * logs/master (after reloading the main page): + * + * [17-Feb-2009 16:51:37 -0500] start: Task: mail. + * [17-Feb-2009 16:51:37 -0500] start: my test + * [17-Feb-2009 16:51:37 -0500] my message + * [17-Feb-2009 16:51:37 -0500] shell exec: cp -r * /dev/null + * [17-Feb-2009 16:51:37 -0500] start: my sql calls + * [17-Feb-2009 16:51:37 -0500] sql: select * from example + * [17-Feb-2009 16:51:37 -0500] sql: select * from example + * [17-Feb-2009 16:51:37 -0500] sql: select * from example + * [17-Feb-2009 16:51:37 -0500] end: my sql calls - 0.0018 seconds shell exec: 1, sql: 3, + * [17-Feb-2009 16:51:37 -0500] end: my test - 0.0055 seconds shell exec: 1, sql: 3, + * [17-Feb-2009 16:51:38 -0500] end: Task: mail. - 0.8854 seconds shell exec: 1, sql: 3, + * + * logs/sql (after reloading the main page): + * + * [17-Feb-2009 16:51:37 -0500] sql: select * from example + * [17-Feb-2009 16:51:37 -0500] sql: select * from example + * [17-Feb-2009 16:51:37 -0500] sql: select * from example + */ +class debug_logger extends rcube_plugin +{ + protected $runlog; + + function init() + { + require_once(__DIR__ . '/runlog/runlog.php'); + + $this->runlog = new runlog(); + + if (!rcmail::get_instance()->config->get('log_dir')) { + rcmail::get_instance()->config->set('log_dir', INSTALL_PATH . 'logs'); + } + + $log_config = rcmail::get_instance()->config->get('debug_logger', []); + $log_dir = rcmail::get_instance()->config->get('log_dir'); + + foreach ($log_config as $type => $file) { + $this->runlog->set_file($log_dir . '/' . $file, $type); + } + + $start_string = ''; + $action = rcmail::get_instance()->action; + $task = rcmail::get_instance()->task; + + if ($action) { + $start_string .= "Action: {$action}. "; + } + + if ($task) { + $start_string .= "Task: {$task}. "; + } + + $this->runlog->start($start_string); + + $this->add_hook('console', [$this, 'console']); + $this->add_hook('authenticate', [$this, 'authenticate']); + } + + function authenticate($args) + { + $this->runlog->note('Authenticating '.$args['user'].'@'.$args['host']); + return $args; + } + + function console($args) + { + $note = $args['args'][0]; + + if (!empty($args['args'][1])) { + $type = $args['args'][1]; + } + else { + // This could be extended to detect types based on the + // file which called console. For now only rcube_imap/rcube_storage is supported + $bt = debug_backtrace(); + $file = count($bt) >= 2 ? $bt[2]['file'] : ''; + + switch (basename($file)) { + case 'rcube_imap.php': + $type = 'imap'; + break; + case 'rcube_storage.php': + $type = 'storage'; + break; + default: + $type = false; + break; + } + } + + switch ($note) { + case 'end': + $type = 'end'; + break; + } + + switch ($type) { + case 'start': + $this->runlog->start($note); + break; + case 'end': + $this->runlog->end(); + break; + default: + $this->runlog->note($note, $type); + break; + } + + return $args; + } + + function __destruct() + { + if ($this->runlog) { + $this->runlog->end(); + } + } +} diff --git a/ruty/mails/plugins/debug_logger/runlog/runlog.php b/ruty/mails/plugins/debug_logger/runlog/runlog.php new file mode 100644 index 0000000..f24b4cb --- /dev/null +++ b/ruty/mails/plugins/debug_logger/runlog/runlog.php @@ -0,0 +1,210 @@ + + */ +class runlog { + + private $start_time = false; + private $parent_stack = []; + private $file_handles = []; + private $debug_messages = []; + private $indent = 0; + private $run_log = []; + + public $print_to_console = false; + public $threshold = 0; + public $tag_count = []; + public $timestamp = "d-M-Y H:i:s O"; + public $max_line_size = 150; + + function __construct() + { + $this->start_time = microtime(true); + } + + public function start($name, $tag = false) + { + $this->run_log[] = [ + 'type' => 'start', + 'tag' => $tag, + 'index' => count($this->run_log), + 'value' => $name, + 'time' => microtime(true), + 'parents' => $this->parent_stack, + 'ended' => false, + ]; + + $this->parent_stack[] = $name; + + $this->print_to_console("start: ".$name, $tag); + $this->print_to_file("start: ".$name, $tag); + $this->indent++; + } + + public function end() + { + $name = array_pop($this->parent_stack); + $lastk = 0; + + foreach ($this->run_log as $k => $entry) { + if ($entry['value'] == $name && $entry['type'] == 'start' && !$entry['ended']) { + $lastk = $k; + } + } + + $start = $this->run_log[$lastk]['time']; + $this->run_log[$lastk]['duration'] = microtime(true) - $start; + $this->run_log[$lastk]['ended'] = true; + $this->run_log[] = [ + 'type' => 'end', + 'tag' => $this->run_log[$lastk]['tag'], + 'index' => $lastk, + 'value' => $name, + 'time' => microtime(true), + 'duration' => microtime(true) - $start, + 'parents' => $this->parent_stack, + ]; + + $this->indent--; + if ($this->run_log[$lastk]['duration'] >= $this->threshold) { + $tag_report = ""; + foreach ($this->tag_count as $tag => $count){ + $tag_report .= "$tag: $count, "; + } + $end_txt = sprintf("end: $name - %0.4f seconds $tag_report", $this->run_log[$lastk]['duration']); + $this->print_to_console($end_txt, $this->run_log[$lastk]['tag']); + $this->print_to_file($end_txt, $this->run_log[$lastk]['tag']); + } + } + + public function increase_tag_count($tag) + { + if (!isset($this->tag_count[$tag])) { + $this->tag_count[$tag] = 0; + } + + $this->tag_count[$tag]++; + } + + public function get_text() + { + $text = ""; + foreach ($this->run_log as $entry){ + $text .= str_repeat(" ", count($entry['parents'])); + if ($entry['tag'] != 'text') { + $text .= $entry['tag'] . ': '; + } + $text .= $entry['value']; + + if ($entry['tag'] == 'end') { + $text .= sprintf(" - %0.4f seconds", $entry['duration']); + } + + $text .= "\n"; + } + + return $text; + } + + public function set_file($filename, $tag = 'master') + { + if (!isset($this->file_handles[$tag])) { + $this->file_handles[$tag] = fopen($filename, 'a'); + if (!$this->file_handles[$tag]) { + trigger_error("Could not open file for writing: $filename"); + } + } + } + + public function note($msg, $tag = false) + { + if ($tag) { + $this->increase_tag_count($tag); + } + if (is_array($msg)) { + $msg = '
    ' . print_r($msg, true) . '
    '; + } + $this->debug_messages[] = $msg; + $this->run_log[] = [ + 'type' => 'note', + 'tag' => $tag ?: 'text', + 'value' => htmlentities($msg), + 'time' => microtime(true), + 'parents' => $this->parent_stack, + ]; + + $this->print_to_file($msg, $tag); + $this->print_to_console($msg, $tag); + } + + public function print_to_file($msg, $tag = false) + { + $file_handle_tag = $tag ?: 'master'; + + if ($file_handle_tag != 'master' && isset($this->file_handles[$file_handle_tag])) { + $buffer = $this->get_indent(); + $buffer .= "$msg\n"; + if (!empty($this->timestamp)) { + $buffer = sprintf("[%s] %s", date($this->timestamp, time()), $buffer); + } + fwrite($this->file_handles[$file_handle_tag], wordwrap($buffer, $this->max_line_size, "\n ")); + } + + if (isset($this->file_handles['master']) && $this->file_handles['master']) { + $buffer = $this->get_indent(); + if ($tag) { + $buffer .= "$tag: "; + } + $msg = str_replace("\n", "", $msg); + $buffer .= "$msg"; + if (!empty($this->timestamp)) { + $buffer = sprintf("[%s] %s", date($this->timestamp, time()), $buffer); + } + if(strlen($buffer) > $this->max_line_size){ + $buffer = substr($buffer,0,$this->max_line_size - 3) . "..."; + } + fwrite($this->file_handles['master'], $buffer."\n"); + } + } + + public function print_to_console($msg, $tag = false) + { + if ($this->print_to_console) { + if (is_array($this->print_to_console)) { + if (in_array($tag, $this->print_to_console)) { + echo $this->get_indent(); + if ($tag) { + echo "$tag: "; + } + echo "$msg\n"; + } + } + else { + echo $this->get_indent(); + if ($tag) { + echo "$tag: "; + } + echo "$msg\n"; + } + } + } + + private function get_indent() + { + $buf = ""; + for ($i = 0; $i < $this->indent; $i++) { + $buf .= " "; + } + return $buf; + } + + function __destruct() + { + foreach ($this->file_handles as $handle) { + fclose($handle); + } + } +} diff --git a/ruty/mails/plugins/emoticons/composer.json b/ruty/mails/plugins/emoticons/composer.json new file mode 100644 index 0000000..3b9faa9 --- /dev/null +++ b/ruty/mails/plugins/emoticons/composer.json @@ -0,0 +1,29 @@ +{ + "name": "roundcube/emoticons", + "type": "roundcube-plugin", + "description": "Plugin that adds emoticons support.", + "license": "GPL-3.0-or-later", + "version": "3.0", + "authors": [ + { + "name": "Thomas Bruederli", + "email": "roundcube@gmail.com", + "role": "Lead" + }, + { + "name": "Aleksander Machniak", + "email": "alec@alec.pl", + "role": "Developer" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": ">=0.1.3" + } +} diff --git a/ruty/mails/plugins/emoticons/config.inc.php.dist b/ruty/mails/plugins/emoticons/config.inc.php.dist new file mode 100644 index 0000000..1af6f67 --- /dev/null +++ b/ruty/mails/plugins/emoticons/config.inc.php.dist @@ -0,0 +1,7 @@ +add_hook('message_part_after', [$this, 'message_part_after']); + $this->add_hook('html_editor', [$this, 'html_editor']); + + if ($rcube->task == 'settings') { + $this->add_hook('preferences_list', [$this, 'preferences_list']); + $this->add_hook('preferences_save', [$this, 'preferences_save']); + } + } + + /** + * 'message_part_after' hook handler to replace common + * plain text emoticons with emoji + */ + function message_part_after($args) + { + if ($args['type'] == 'plain') { + $this->load_config(); + + $rcube = rcube::get_instance(); + if (!$rcube->config->get('emoticons_display', false)) { + return $args; + } + + $args['body'] = self::text2icons($args['body']); + } + + return $args; + } + + /** + * 'html_editor' hook handler, where we enable emoticons in TinyMCE + */ + function html_editor($args) + { + $rcube = rcube::get_instance(); + + $this->load_config(); + + if ($rcube->config->get('emoticons_compose', true)) { + $args['extra_plugins'][] = 'emoticons'; + $args['extra_buttons'][] = 'emoticons'; + } + + return $args; + } + + /** + * 'preferences_list' hook handler + */ + function preferences_list($args) + { + $rcube = rcube::get_instance(); + $dont_override = $rcube->config->get('dont_override', []); + + if ($args['section'] == 'mailview' && !in_array('emoticons_display', $dont_override)) { + $this->load_config(); + $this->add_texts('localization'); + + $field_id = 'emoticons_display'; + $checkbox = new html_checkbox(['name' => '_' . $field_id, 'id' => $field_id, 'value' => 1]); + + $args['blocks']['main']['options']['emoticons_display'] = [ + 'title' => html::label($field_id, $this->gettext('emoticonsdisplay')), + 'content' => $checkbox->show(intval($rcube->config->get('emoticons_display', false))) + ]; + } + else if ($args['section'] == 'compose' && !in_array('emoticons_compose', $dont_override)) { + $this->load_config(); + $this->add_texts('localization'); + + $field_id = 'emoticons_compose'; + $checkbox = new html_checkbox(['name' => '_' . $field_id, 'id' => $field_id, 'value' => 1]); + + $args['blocks']['main']['options']['emoticons_compose'] = [ + 'title' => html::label($field_id, $this->gettext('emoticonscompose')), + 'content' => $checkbox->show(intval($rcube->config->get('emoticons_compose', true))) + ]; + } + + return $args; + } + + /** + * 'preferences_save' hook handler + */ + function preferences_save($args) + { + if ($args['section'] == 'mailview') { + $args['prefs']['emoticons_display'] = (bool) rcube_utils::get_input_value('_emoticons_display', rcube_utils::INPUT_POST); + } + else if ($args['section'] == 'compose') { + $args['prefs']['emoticons_compose'] = (bool) rcube_utils::get_input_value('_emoticons_compose', rcube_utils::INPUT_POST); + } + + return $args; + } + + /** + * Replace common plain text emoticons with emoji + * + * @param string $text Text + * + * @return string Converted text + */ + protected static function text2icons($text) + { + // This is a lookbehind assertion which will exclude html entities + // E.g. situation when ";)" in "")" shouldn't be replaced by the icon + // It's so long because of assertion format restrictions + $entity = '(? self::ico_tag('1f603', ':D' ), // laugh + '/:-?\(/' => self::ico_tag('1f626', ':(' ), // frown + '/'.$entity.';-?\)/' => self::ico_tag('1f609', ';)' ), // wink + '/8-?\)/' => self::ico_tag('1f60e', '8)' ), // cool + '/(? self::ico_tag('1f62e', ':O' ), // surprised + '/(? self::ico_tag('1f61b', ':P' ), // tongue out + '/(? self::ico_tag('1f631', ':-@' ), // yell + '/O:-?\)/i' => self::ico_tag('1f607', 'O:-)' ), // innocent + '/(? self::ico_tag('1f60a', ':-)' ), // smile + '/(? self::ico_tag('1f633', ':-$' ), // embarrassed + '/(? self::ico_tag('1f48b', ':-*' ), // kiss + '/(? self::ico_tag('1f615', ':-S' ), // undecided + ]; + + return preg_replace(array_keys($map), array_values($map), $text); + } + + protected static function ico_tag($ico, $title) + { + return html::span(['title' => $title], "&#x{$ico};"); + } +} diff --git a/ruty/mails/plugins/emoticons/localization/ar.inc b/ruty/mails/plugins/emoticons/localization/ar.inc new file mode 100644 index 0000000..1c3725d --- /dev/null +++ b/ruty/mails/plugins/emoticons/localization/ar.inc @@ -0,0 +1,18 @@ += 2.0 and <= 2.1.12. +Note: for server use GnuPG developers still recommend version 1.4. diff --git a/ruty/mails/plugins/enigma/bin/import_keys.sh b/ruty/mails/plugins/enigma/bin/import_keys.sh new file mode 100644 index 0000000..7caf066 --- /dev/null +++ b/ruty/mails/plugins/enigma/bin/import_keys.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env php + | + +-----------------------------------------------------------------------+ +*/ + +define('INSTALL_PATH', realpath(__DIR__ . '/../../../') . '/'); + +require INSTALL_PATH . 'program/include/clisetup.php'; + +$rcmail = rcube::get_instance(); + +// get arguments +$args = rcube_utils::get_opt([ + 'u' => 'user', + 'h' => 'host', + 'd' => 'dir', + 'x' => 'dry-run', +]); + +if (!empty($_SERVER['argv'][1]) && $_SERVER['argv'][1] == 'help') { + print_usage(); + exit; +} + +if (empty($args['dir'])) { + rcube::raise_error("--dir argument is required", true); +} + +$host = get_host($args); +$dirs = []; + +// Read the homedir and iterate over all subfolders (as users) +if (empty($args['user'])) { + if ($dh = opendir($args['dir'])) { + while (($dir = readdir($dh)) !== false) { + if ($dir != '.' && $dir != '..') { + $dirs[$args['dir'] . '/' . $dir] = $dir; + } + } + closedir($dh); + } +} +// a single user +else { + $dirs = [$args['dir'] => $args['user']]; +} + +foreach ($dirs as $dir => $user) { + echo "Importing keys from $dir\n"; + + if ($user_id = get_user_id($user, $host)) { + reset_state($user_id, !empty($args['dry-run'])); + import_dir($user_id, $dir, !empty($args['dry-run'])); + } +} + + +function print_usage() +{ + print "Usage: import.sh [options]\n"; + print "Options:\n"; + print " --user=username User, if not set --dir subfolders will be iterated\n"; + print " --host=host The IMAP hostname or IP the given user is related to\n"; + print " --dir=path Location of the gpg homedir\n"; + print " --dry-run Do nothing, just list found user/files\n"; +} + +function get_host($args) +{ + global $rcmail; + + if (empty($args['host'])) { + $hosts = $rcmail->config->get('imap_host', ''); + if (is_string($hosts)) { + $args['host'] = $hosts; + } + else if (is_array($hosts) && count($hosts) == 1) { + $args['host'] = reset($hosts); + } + else { + rcube::raise_error("Specify a host name", true); + } + + // host can be a URL like tls://192.168.12.44 + $host_url = parse_url($args['host']); + if (!empty($host_url['host'])) { + $args['host'] = $host_url['host']; + } + } + + return $args['host']; +} + +function get_user_id($username, $host) +{ + global $rcmail; + + $db = $rcmail->get_dbh(); + + // find user in local database + $user = rcube_user::query($username, $host); + + if (empty($user)) { + rcube::raise_error("User does not exist: $username"); + } + + return $user->ID; +} + +function reset_state($user_id, $dry_run = false) +{ + global $rcmail; + + if ($dry_run) { + return; + } + + $db = $rcmail->get_dbh(); + + $db->query("DELETE FROM " . $db->table_name('filestore', true) + . " WHERE `user_id` = ? AND `context` = ?", + $user_id, 'enigma'); +} + +function import_dir($user_id, $dir, $dry_run = false) +{ + global $rcmail; + + $db = $rcmail->get_dbh(); + $table = $db->table_name('filestore', true); + $db_files = ['pubring.gpg', 'secring.gpg', 'pubring.kbx']; + $maxsize = min($db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000; + + foreach (glob("$dir/private-keys-v1.d/*.key") as $file) { + $db_files[] = substr($file, strlen($dir) + 1); + } + + foreach ($db_files as $file) { + if ($mtime = @filemtime("$dir/$file")) { + $data = file_get_contents("$dir/$file"); + $data = base64_encode($data); + $datasize = strlen($data); + + if ($datasize > $maxsize) { + rcube::raise_error([ + 'code' => 605, 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Enigma: Failed to save $file. Size exceeds max_allowed_packet." + ], true, false); + + continue; + } + + echo "* $file\n"; + + if ($dry_run) { + continue; + } + + $result = $db->query( + "INSERT INTO $table (`user_id`, `context`, `filename`, `mtime`, `data`)" + . " VALUES(?, 'enigma', ?, ?, ?)", + $user_id, $file, $mtime, $data); + + if ($db->is_error($result)) { + rcube::raise_error([ + 'code' => 605, 'line' => __LINE__, 'file' => __FILE__, + 'message' => "Enigma: Failed to save $file into database." + ], true, false); + } + } + } +} diff --git a/ruty/mails/plugins/enigma/composer.json b/ruty/mails/plugins/enigma/composer.json new file mode 100644 index 0000000..f86c03d --- /dev/null +++ b/ruty/mails/plugins/enigma/composer.json @@ -0,0 +1,25 @@ +{ + "name": "roundcube/enigma", + "type": "roundcube-plugin", + "description": "Server-side PGP Encryption for Roundcube", + "license": "GPL-3.0-or-later", + "version": "0.9", + "authors": [ + { + "name": "Aleksander Machniak", + "email": "alec@alec.pl", + "role": "Lead" + } + ], + "repositories": [ + { + "type": "composer", + "url": "https://plugins.roundcube.net" + } + ], + "require": { + "php": ">=7.3.0", + "roundcube/plugin-installer": "~0.1.6", + "pear/crypt_gpg": "~1.6.3" + } +} diff --git a/ruty/mails/plugins/enigma/config.inc.php.dist b/ruty/mails/plugins/enigma/config.inc.php.dist new file mode 100644 index 0000000..a5a5233 --- /dev/null +++ b/ruty/mails/plugins/enigma/config.inc.php.dist @@ -0,0 +1,80 @@ += 2.1. +$config['enigma_pgp_gpgconf'] = ''; + +// Name of the PGP symmetric cipher algorithm. +// Run gpg --version to see the list of supported algorithms +$config['enigma_pgp_cipher_algo'] = null; + +// Name of the PGP digest (hash) algorithm. +// Run gpg --version to see the list of supported algorithms +$config['enigma_pgp_digest_algo'] = null; + +// Enables multi-host environments support. +// Enable it if you have more than one HTTP server. +// Make sure all servers run the same GnuPG version and have time in sync. +// Keys will be stored in SQL database (make sure max_allowed_packet +// is big enough). +$config['enigma_multihost'] = false; + +// Enables signatures verification feature. +$config['enigma_signatures'] = true; + +// Enables messages decryption feature. +$config['enigma_decryption'] = true; + +// Enables messages encryption and signing feature. +$config['enigma_encryption'] = true; + +// Enable signing all messages by default +$config['enigma_sign_all'] = false; + +// Enable encrypting all messages by default +$config['enigma_encrypt_all'] = false; + +// Enable attaching a public key to all messages by default +$config['enigma_attach_pubkey'] = false; + +// Default for how long to store private key passwords (in minutes). +// When set to 0 passwords will be stored for the whole session. +$config['enigma_password_time'] = 5; + +// Enable support for private keys without passwords. +$config['enigma_passwordless'] = false; + +// With this option you can lock composing options +// of the plugin forcing the user to use configured settings. +// The array accepts: 'sign', 'encrypt', 'pubkey'. +// +// For example, to force your users to sign every email, +// you should set: +// - enigma_sign_all = true +// - enigma_options_lock = ['sign'] +// - dont_override = ['enigma_sign_all'] +$config['enigma_options_lock'] = []; diff --git a/ruty/mails/plugins/enigma/enigma.js b/ruty/mails/plugins/enigma/enigma.js new file mode 100644 index 0000000..647b22b --- /dev/null +++ b/ruty/mails/plugins/enigma/enigma.js @@ -0,0 +1,712 @@ +/** + * Enigma plugin script + * + * @licstart The following is the entire license notice for the + * JavaScript code in this file. + * + * Copyright (c) The Roundcube Dev Team + * + * The JavaScript code in this page is free software: you can redistribute it + * and/or modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * @licend The above is the entire license notice + * for the JavaScript code in this file. + */ + +window.rcmail && rcmail.addEventListener('init', function(evt) { + if (rcmail.env.task == 'settings') { + if (rcmail.gui_objects.keyslist) { + rcmail.keys_list = new rcube_list_widget(rcmail.gui_objects.keyslist, + {multiselect:true, draggable:false, keyboard:true}); + rcmail.keys_list + .addEventListener('select', function(o) { rcmail.enigma_keylist_select(o); }) + .addEventListener('keypress', function(o) { rcmail.list_keypress(o, {del: 'plugin.enigma-key-delete'}); }) + .init() + .focus(); + + rcmail.enigma_list(); + + rcmail.register_command('firstpage', function(props) { return rcmail.enigma_list_page('first'); }); + rcmail.register_command('previouspage', function(props) { return rcmail.enigma_list_page('previous'); }); + rcmail.register_command('nextpage', function(props) { return rcmail.enigma_list_page('next'); }); + rcmail.register_command('lastpage', function(props) { return rcmail.enigma_list_page('last'); }); + } + + if (rcmail.env.action == 'plugin.enigmakeys') { + rcmail.register_command('search', function(props) {return rcmail.enigma_search(props); }, true); + rcmail.register_command('reset-search', function(props) {return rcmail.enigma_search_reset(props); }, true); + rcmail.register_command('plugin.enigma-import', function() { rcmail.enigma_import(); }, true); + rcmail.register_command('plugin.enigma-import-search', function() { rcmail.enigma_import_search(); }, true); + rcmail.register_command('plugin.enigma-key-export', function() { rcmail.enigma_export(); }); + rcmail.register_command('plugin.enigma-key-export-selected', function() { rcmail.enigma_export(true); }); + rcmail.register_command('plugin.enigma-key-import', function() { rcmail.enigma_key_import(); }, true); + rcmail.register_command('plugin.enigma-key-import-search', function() { rcmail.enigma_key_import_search(); }, true); + rcmail.register_command('plugin.enigma-key-delete', function(props) { return rcmail.enigma_delete(); }); + rcmail.register_command('plugin.enigma-key-create', function(props) { return rcmail.enigma_key_create(); }, true); + rcmail.register_command('plugin.enigma-key-save', function(props) { return rcmail.enigma_key_create_save(); }, true); + + rcmail.addEventListener('responseafterplugin.enigmakeys', function() { + rcmail.enable_command('plugin.enigma-key-export', rcmail.env.rowcount > 0); + rcmail.triggerEvent('listupdate', {list: rcmail.keys_list, rowcount: rcmail.env.rowcount}); + }); + + if (rcmail.gui_objects.importform) { + // make sure Enter key in search input starts searching + // instead of submitting the form + $('#rcmimportsearch').keydown(function(e) { + if (e.which == 13) { + rcmail.enigma_import_search(); + return false; + } + }); + } + } + } + else if (rcmail.env.task == 'mail') { + if (rcmail.env.action == 'compose') { + rcmail.addEventListener('beforesend', function(props) { rcmail.enigma_beforesend_handler(props); }) + .addEventListener('beforesavedraft', function(props) { rcmail.enigma_beforesavedraft_handler(props); }); + + $('#enigmamenu').find('input,label').mouseup(function(e) { + // don't close the menu on mouse click inside + e.stopPropagation(); + }); + + $('a.button.enigma').prop('tabindex', $('#messagetoolbar > a').first().prop('tabindex')); + + $.each(['encrypt', 'sign'], function() { + var opt = this, input = $('#enigma' + opt + 'opt'); + + if (rcmail.env['enigma_force_' + opt]) { + input.prop('checked', true); + } + + // Compose status bar in Elastic + if (window.UI && UI.compose_status) { + input.on('change', function() { UI.compose_status(opt, this.checked); }); + } + + // As the options might have been initially enabled we have to + // trigger onchange event, so all handlers can update the state + input.trigger('change'); + }); + } + + if (rcmail.env.enigma_password_request) { + rcmail.enigma_password_request(rcmail.env.enigma_password_request); + } + } +}); + + +/*********************************************************/ +/********* Enigma Settings/Keys/Certs UI *********/ +/*********************************************************/ + +// Display key(s) import form +rcube_webmail.prototype.enigma_key_import = function() +{ + var dialog = $('