From 17e5d1b7c3eecfebf0abfd10f790124affcea9e5 Mon Sep 17 00:00:00 2001 From: "Aleksander Machniak (Kolab Systems)" Date: Fri, 24 Jun 2011 20:13:43 +0200 Subject: [PATCH] Added addressbook directories list styling with readonly and namespace icons. Improved addressbook folders naming/structure representation on the list. Added addressbook folders management features including (delete/create/rename). --- .../kolab_addressbook/kolab_addressbook.js | 256 ++++++++++++++++ .../lib/kolab_addressbook_ui.php | 273 ++++++++++++++++++ .../{ => lib}/rcube_kolab_contacts.php | 138 ++++++++- .../skins/default/kolab_addressbook.css | 35 +++ .../skins/default/kolab_folders.png | Bin 0 -> 2481 bytes .../skins/default/templates/bookedit.html | 27 ++ 6 files changed, 721 insertions(+), 8 deletions(-) create mode 100644 plugins/kolab_addressbook/kolab_addressbook.js create mode 100644 plugins/kolab_addressbook/lib/kolab_addressbook_ui.php rename plugins/kolab_addressbook/{ => lib}/rcube_kolab_contacts.php (88%) create mode 100644 plugins/kolab_addressbook/skins/default/kolab_addressbook.css create mode 100644 plugins/kolab_addressbook/skins/default/kolab_folders.png create mode 100644 plugins/kolab_addressbook/skins/default/templates/bookedit.html diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js new file mode 100644 index 00000000..dbb35004 --- /dev/null +++ b/plugins/kolab_addressbook/kolab_addressbook.js @@ -0,0 +1,256 @@ +if (window.rcmail) { + rcmail.addEventListener('init', function() { + rcmail.set_book_actions(); + if (rcmail.gui_objects.editform && rcmail.env.action.match(/^plugin\.book/)) { + rcmail.enable_command('book-save', true); + } + }); + rcmail.addEventListener('listupdate', function() { + rcmail.set_book_actions(); + }); +} + +// (De-)activates address book management commands +rcube_webmail.prototype.set_book_actions = function() +{ + var source = this.env.source, + sources = this.env.address_sources; + + this.enable_command('book-create', true); + this.enable_command('book-edit', 'book-delete', source && sources[source] && sources[source].kolab && sources[source].editable); +}; + +rcube_webmail.prototype.book_create = function() +{ + this.book_show_contentframe('create'); +}; + +rcube_webmail.prototype.book_edit = function() +{ + this.book_show_contentframe('edit'); +}; + +rcube_webmail.prototype.book_delete = function() +{ + if (this.env.source != '' && confirm(this.get_label('kolab_addressbook.bookdeleteconfirm'))) { + var lock = this.set_busy(true, 'kolab_addressbook.bookdeleting'); + this.http_request('plugin.book', '_act=delete&_source='+urlencode(this.book_realname()), lock); + } +}; + +// displays page with book edit/create form +rcube_webmail.prototype.book_show_contentframe = function(action, framed) +{ + var add_url = '', target = window; + + // unselect contact + this.contact_list.clear_selection(); + this.enable_command('edit', 'delete', 'compose', false); + + if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) { + add_url = '&_framed=1'; + target = window.frames[this.env.contentframe]; + this.show_contentframe(true); + } + else if (framed) + return false; + + if (action) { + this.set_busy(true); + this.location_href(this.env.comm_path+'&_action=plugin.book&_act='+action + +'&_source='+urlencode(this.book_realname()) + +add_url, target); + } + + return true; +}; + +// submits book create/update form +rcube_webmail.prototype.book_save = function() +{ + var form = this.gui_objects.editform, + input = $("input[name='_name']", form) + + if (input.length && input.val() == '') { + alert(this.get_label('kolab_addressbook.nobooknamewarning')); + input.focus(); + return; + } + + input = this.display_message(this.get_label('kolab_addressbook.booksaving'), 'loading'); + $('').val(input).appendTo(form); + + form.submit(); +}; + +// action executed after book delete +rcube_webmail.prototype.book_delete_done = function(id) +{ + var n, g, li = this.get_folder_li(id), groups = this.env.contactgroups; + + // remove folder and its groups rows + for (n in groups) + if (groups[n].source == id && (g = this.get_folder_li(n))) { + $(g).remove(); + delete this.env.contactgroups[n]; + } + $(li).remove(); + + delete this.env.address_sources[id]; + delete this.env.contactfolders[id]; +}; + +// action executed after book create/update +rcube_webmail.prototype.book_update = function(data, old) +{ + var n, i, id, len, row, refrow, olddata, name = '', realname = '', sources, level, + folders = [], class_name = 'addressbook', + list = this.gui_objects.folderlist, + groups = this.env.contactgroups; + + this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data; + this.show_contentframe(false); + + // update + if (old && old != data.id) { + olddata = this.env.address_sources[old]; + delete this.env.address_sources[old]; + delete this.env.contactfolders[old]; + + // update source ID in groups + for (n in groups) + if (groups[n].source == old) + this.env.contactgroups[n].source = data.id; + + refrow = $('#rcmli'+old); + } + // create + else if (!old) { + refrow = $('li', list).get(0); + + // this shouldn't happen + if (!refrow) + this.redirect(this.get_task_url('addressbook')); + } + + if (!refrow) + return; + + sources = this.env.address_sources; + + // clone a table row if there are existing rows + row = $(refrow).clone(); + + // set row attributes + if (data.readonly) + class_name += ' readonly'; + if (data.class) + class_name += ' '+data.class; + // updated currently selected book + if (this.env.source != '' && this.env.source == old) { + class_name += ' selected'; + this.env.source = data.id; + } + + row.attr({id: 'rcmli'+data.id, 'class': class_name}); + $('a', row).html(data.name).attr({onclick: '', rel: data.id, href: '#'}) + .click({id: data.id}, function(e) { return rcmail.command('list', e.data.id, this); }); + + // sort kolab folders, to put the new one in order + for (n in sources) + if (sources[n].kolab && (name = sources[n].realname)) + folders.push(name); + folders.sort(); + + // find current id + for (n=0, len=folders.length; n 0) { + for (i=level; i>0; i--) + name = name.replace(/^  /, ''); + } + else if (level < 0) { + for (i=level; i<0; i++) + name = '  ' + name; + } + + // update existing row + refrow = $('#rcmli'+n); + refrow.remove().attr({id: 'rcmli'+id}); + $('a', refrow).html(name).attr({onclick: '', rel: id, href: '#'}) + .click({id: id}, function(e) { return rcmail.command('list', e.data.id, this); }); + + // move the row to the new place + refrow.insertAfter(row); + row = refrow; + + // update list data + sources[n].id = id; + sources[n].name = name; + sources[n].realname = realname; + this.env.address_sources[id] = this.env.contactfolders[id] = sources[n]; + delete this.env.address_sources[n]; + delete this.env.contactfolders[n]; + + // update groups + for (i in groups) { + if (groups[i].source == n) { + // update existing row + refrow = $('#rcmli'+i); + refrow.remove().attr({id: 'rcmliG'+id+groups[i].id}); + $('a', refrow).attr('onclick', '') + .click({source: id, id: groups[i].id}, function(e) { + return rcmail.command('listgroup', {'source': e.data.source, 'id': e.data.id}, this); }); + refrow.insertAfter(row); + row = refrow; + + // update group data + groups[i].source = id; + this.env.contactgroups['G'+id+groups[i].id] = groups[i]; + delete this.env.contactgroups[i]; + } + } + } + } + } +}; + +// returns real IMAP folder name +rcube_webmail.prototype.book_realname = function() +{ + var source = this.env.source, sources = this.env.address_sources; + return source != '' && sources[source] && sources[source].realname ? sources[source].realname : ''; +}; diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php new file mode 100644 index 00000000..6bac21b1 --- /dev/null +++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php @@ -0,0 +1,273 @@ + + * + * Copyright (C) 2011, 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 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. + */ +class kolab_addressbook_ui +{ + private $plugin; + private $rc; + + /** + * Class constructor + * + * @param kolab_addressbook $plugin Plugin object + */ + public function __construct($plugin) + { + $this->rc = rcmail::get_instance(); + $this->plugin = $plugin; + + $this->init_ui(); + } + + /** + * Adds folders management functionality to Addressbook UI + */ + private function init_ui() + { + if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action)) { + return; + } + + // Include script + $this->plugin->include_script('kolab_addressbook.js'); + + if (empty($this->rc->action)) { + // Include stylesheet (for directorylist) + $this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css'); + + // Add actions on address books + $options = array('book-create', 'book-edit', 'book-delete'); + $idx = 0; + + foreach ($options as $command) { + $content = html::tag('li', $idx ? null : array('class' => 'separator_above'), + $this->plugin->api->output->button(array( + 'label' => 'kolab_addressbook.'.str_replace('-', '', $command), + 'domain' => $this->ID, + 'classact' => 'active', + 'command' => $command + ))); + $this->plugin->api->add_content($content, 'groupoptions'); + $idx++; + } + + $this->rc->output->add_label('kolab_addressbook.bookdeleteconfirm', + 'kolab_addressbook.bookdeleting'); + } + // book create/edit form + else { + $this->rc->output->add_label('kolab_addressbook.nobooknamewarning', + 'kolab_addressbook.booksaving'); + } + } + + + /** + * Handler for address book create/edit action + */ + public function book_edit() + { + $this->rc->output->add_handler('bookdetails', array($this, 'book_form')); + $this->rc->output->send('kolab_addressbook.bookedit'); + } + + + /** + * Handler for 'bookdetails' object returning form content for book create/edit + * + * @param array $attr Object attributes + * + * @return string HTML output + */ + public function book_form($attrib) + { + $action = trim(get_input_value('_act', RCUBE_INPUT_GPC)); + $folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true)); // UTF8 + $name = trim(get_input_value('_name', RCUBE_INPUT_GPC, true)); // UTF8 + $old = trim(get_input_value('_oldname', RCUBE_INPUT_GPC, true)); // UTF7-IMAP + $path_imap = trim(get_input_value('_parent', RCUBE_INPUT_GPC, true)); // UTF7-IMAP + + $hidden_fields[] = array('name' => '_source', 'value' => $folder); + + $folder = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP'); + $delim = $_SESSION['imap_delimiter']; + $form = array(); + + if ($this->rc->action == 'plugin.book-save') { + // save error + $path_imap = $folder; + $hidden_fields[] = array('name' => '_oldname', 'value' => $old); + + if (strlen($old)) { + $this->rc->imap_connect(); + $options = $this->rc->imap->mailbox_info($old); + } + } + else if ($action == 'edit') { + $path_imap = explode($delim, $folder); + $name = rcube_charset_convert(array_pop($path_imap), 'UTF7-IMAP'); + $path_imap = implode($path_imap, $delim); + + $this->rc->imap_connect(); + $options = $this->rc->imap->mailbox_info($folder); + + $hidden_fields[] = array('name' => '_oldname', 'value' => $folder); + } + else { + $path_imap = $folder; + $name = ''; + } + + // General tab + $form['props'] = array( + 'name' => $this->rc->gettext('properties'), + ); + + $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30)); + $foldername = $foldername->show($name); + + $form['props']['fieldsets']['location'] = array( + 'name' => $this->rc->gettext('location'), + 'content' => array( + 'name' => array( + 'label' => $this->plugin->gettext('bookname'), + 'value' => $foldername, + ), + ), + ); + + if (strlen($path_imap)) { + $path = rcube_charset_convert($path_imap, 'UTF7-IMAP'); + // @TODO: $options + if (!empty($options) && ($options['norename'] || $options['namespace'] != 'personal')) { + // prevent user from moving folder + $hidden_fields[] = array('name' => '_parent', 'value' => $path_imap); + } + else { + $radio1 = new html_radiobutton(array('name' => '_parent', 'value' => '')); + $radio2 = new html_radiobutton(array('name' => '_parent', 'value' => $path_imap)); + + $html_path = str_replace($delim, ' » ', $path); + + $folderpath = $radio1->show($path_imap) . Q(rcube_label('none')) . ' ' + .$radio2->show($path_imap) . Q($html_path); + + $form['props']['fieldsets']['location']['content']['path'] = array( + 'label' => $this->plugin->gettext('parentbook'), + 'value' => $folderpath, + ); + } + } + + // Allow plugins to modify address book form content (e.g. with ACL form) + $plugin = $this->rc->plugins->exec_hook('addressbook_form', + array('form' => $form, 'options' => $options, 'name' => $folder)); + + $form = $plugin['form']; + + // Set form tags and hidden fields + list($form_start, $form_end) = $this->get_form_tags($attrib, 'plugin.book-save', null, $hidden_fields); + + unset($attrib['form']); + + // return the complete edit form as table + $out = "$form_start\n"; + + // Create form output + foreach ($form as $tab) { + if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) { + $content = ''; + foreach ($tab['fieldsets'] as $fieldset) { + $subcontent = $this->get_form_part($fieldset); + if ($subcontent) { + $content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n"; + } + } + } + else { + $content = $this->get_form_part($tab); + } + + if ($content) { + $out .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n"; + } + } + + $out .= "\n$form_end"; + + return $out; + } + + + private function get_form_part($form) + { + $content = ''; + + if (is_array($form['content']) && !empty($form['content'])) { + $table = new html_table(array('cols' => 2)); + foreach ($form['content'] as $col => $colprop) { + $colprop['id'] = '_'.$col; + $label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col); + + $table->add('title', sprintf('', $colprop['id'], Q($label))); + $table->add(null, $colprop['value']); + } + $content = $table->show(); + } + else { + $content = $form['content']; + } + + return $content; + } + + + private function get_form_tags($attrib, $action, $id = null, $hidden = null) + { + $form_start = $form_end = ''; + + $request_key = $action . (isset($id) ? '.'.$id : ''); + $form_start = $this->rc->output->request_form(array( + 'name' => 'form', + 'method' => 'post', + 'task' => $this->rc->task, + 'action' => $action, + 'request' => $request_key, + 'noclose' => true, + ) + $attrib); + + if (is_array($hidden)) { + foreach ($hidden as $field) { + $hiddenfield = new html_hiddenfield($field); + $form_start .= $hiddenfield->show(); + } + } + + $form_end = !strlen($attrib['form']) ? '' : ''; + + $EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form'; + $this->rc->output->add_gui_object('editform', $EDIT_FORM); + + return array($form_start, $form_end); + } + +} diff --git a/plugins/kolab_addressbook/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php similarity index 88% rename from plugins/kolab_addressbook/rcube_kolab_contacts.php rename to plugins/kolab_addressbook/lib/rcube_kolab_contacts.php index af3daa8d..3c578e6d 100644 --- a/plugins/kolab_addressbook/rcube_kolab_contacts.php +++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php @@ -13,8 +13,9 @@ class rcube_kolab_contacts extends rcube_addressbook { public $primary_key = 'ID'; - public $readonly = false; - public $groups = true; + public $readonly = true; + public $editable = false; + public $groups = false; public $coltypes = array( 'name' => array('limit' => 1), 'firstname' => array('limit' => 1), @@ -55,9 +56,6 @@ class rcube_kolab_contacts extends rcube_addressbook ); private $gid; - private $imap; - private $kolab; - private $folder; private $contactstorage; private $liststorage; private $contacts; @@ -66,6 +64,7 @@ class rcube_kolab_contacts extends rcube_addressbook private $id2uid; private $filter; private $result; + private $namespace; private $imap_folder = 'INBOX/Contacts'; private $gender_map = array(0 => 'male', 1 => 'female'); private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax'); @@ -120,6 +119,25 @@ class rcube_kolab_contacts extends rcube_addressbook $this->liststorage = rcube_kolab::get_storage($this->imap_folder, 'distributionlist'); $this->ready = !PEAR::isError($this->contactstorage) && !PEAR::isError($this->liststorage); + + // Set readonly and editable flags according to folder permissions + if ($this->ready) { + if ($this->get_owner() == $_SESSION['username']) { + $this->editable = true; + $this->readonly = false; + } + else { + $acl = $this->contactstorage->_folder->getACL(); + $acl = $acl[$_SESSION['username']]; + if (strpos($acl, 'i') !== false) + $this->readonly = false; + if (strpos($acl, 'a') !== false || strpos($acl, 'x') !== false) + $this->editable = true; + } + + if (!$this->readonly) + $this->groups = true; + } } @@ -130,9 +148,113 @@ class rcube_kolab_contacts extends rcube_addressbook */ public function get_name() { - $folder = rcube_charset_convert($this->imap_folder, 'UTF7-IMAP'); - // @TODO: use namespace prefixes - return strtr(preg_replace('!^(INBOX|user)/!i', '', $folder), '/', ':'); + $folder = $this->imap_folder; + $namespace = $_SESSION['imap_namespace']; // from rcube_imap class + $found = false; + + if (!empty($namespace['shared'])) { + foreach ($namespace['shared'] as $ns) { + if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { + $prefix = ''; + $folder = substr($folder, strlen($ns[0])); + $delim = $ns[1]; + $found = true; + $this->namespace = 'shared'; + break; + } + } + } + if (!$found && !empty($namespace['other'])) { + foreach ($namespace['other'] as $ns) { + if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { + // remove namespace prefix + $folder = substr($folder, strlen($ns[0])); + $delim = $ns[1]; + // get username + $pos = strpos($folder, $delim); + $prefix = '('.substr($folder, 0, $pos).') '; + $found = true; + $this->namespace = 'other'; + break; + } + } + } + if (!$found && !empty($namespace['personal'])) { + foreach ($namespace['personal'] as $ns) { + if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { + // remove namespace prefix + $folder = substr($folder, strlen($ns[0])); + $prefix = ''; + $delim = $ns[1]; + $found = true; + $this->namespace = 'personal'; + break; + } + } + } + + if (empty($delim)) + $delim = $_SESSION['imap_delimiter']; // from rcube_imap class + + $folder = rcube_charset_convert($folder, 'UTF7-IMAP'); + $folder = str_replace($delim, ' » ', $folder); + + if ($prefix) + $folder = $prefix . ' ' . $folder; + + return $folder; + } + + + /** + * Getter for the IMAP folder name + * + * @return string Name of the IMAP folder + */ + public function get_realname() + { + return $this->imap_folder; + } + + + /** + * Getter for the IMAP folder owner + * + * @return string Name of the folder owner + */ + public function get_owner() + { + return $this->contactstorage->_folder->getOwner(); + } + + + /** + * Getter for the name of the namespace to which the IMAP folder belongs + * + * @return string Name of the namespace (personal, other, shared) + */ + public function get_namespace() + { + if ($this->namespace) { + return $this->namespace; + } + + $folder = $this->imap_folder; + $namespace = $_SESSION['imap_namespace']; // from rcube_imap class + + if (!empty($namespace)) { + foreach ($namespace as $nsname => $nsvalue) { + if (in_array($nsname, array('personal', 'other', 'shared')) && !empty($nsvalue)) { + foreach ($nsvalue as $ns) { + if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { + return $this->namespace = $nsname; + } + } + } + } + } + + return $this->namespace = 'personal'; } diff --git a/plugins/kolab_addressbook/skins/default/kolab_addressbook.css b/plugins/kolab_addressbook/skins/default/kolab_addressbook.css new file mode 100644 index 00000000..da3abed2 --- /dev/null +++ b/plugins/kolab_addressbook/skins/default/kolab_addressbook.css @@ -0,0 +1,35 @@ +#directorylist li.addressbook.readonly +{ + /* don't use 'background' to not reset background color */ + background-image: url(kolab_folders.png); + background-position: 5px 0px; + background-repeat: no-repeat; +} + +#directorylist li.addressbook.shared +{ + background-image: url(kolab_folders.png); + background-position: 5px -54px; + background-repeat: no-repeat; +} + +#directorylist li.addressbook.shared.readonly +{ + background-image: url(kolab_folders.png); + background-position: 5px -72px; + background-repeat: no-repeat; +} + +#directorylist li.addressbook.other +{ + background-image: url(kolab_folders.png); + background-position: 5px -18px; + background-repeat: no-repeat; +} + +#directorylist li.addressbook.other.readonly +{ + background-image: url(kolab_folders.png); + background-position: 5px -36px; + background-repeat: no-repeat; +} diff --git a/plugins/kolab_addressbook/skins/default/kolab_folders.png b/plugins/kolab_addressbook/skins/default/kolab_folders.png new file mode 100644 index 0000000000000000000000000000000000000000..cd6228a77aafec51ed9a9fc639ad8b993a2cbe32 GIT binary patch literal 2481 zcmV;i2~PHjP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2ipb} z2r>|&egW(N00}WkL_t(o!|j-Ra8%_PhoA49y(HOWlaPA?xse122w1rUf`S()7L=RR z5o@QVj+Lp-*v>ehGo9%u+RL;>lxnrDT1P8lMXgjRf`EVs;UW+&fkepW!Y0}5Zuacu z?4DcyNWcwBbgKQs8Qz)iukW1S%zM6hpLxCmU$X?jx~HGaD=sd6J}WzGPBa>&VSmGB zC8XT_$3HyL^OXYL|6ohA+wC6t?`d}=oW#yDq;318-UVQGvDpCQTD>{dmt4i)^-=NnA(RWN;us!6H`jZvaEpxwbkW}$u8r~liMc(*bX;x|GbqiTj*Pb5%1#a z`aGhMZW6lAQ%`O}A2tWew9qY+gsLKHDiBEx*$^05?ZS%eA|CM&33sD}dJ*<=YDd-3 zXQeS~=1kHvGI{#%uOr!H+_o}gdv*bUO*a6u%|d-s7*QWuR3i~ffNr3wd92^~Dig+! zCoexAS+*m|HeOT=4##Xh0Glb{Ilkv@5m(2dsv%<0Fp+42gq9#Zvw*_F5SC@3>pJmx zf2eREn2%FV^3jO6; zvSdlZ>kD8SIw`3s2w6f1IVr%x)MadT8&RhZGbLJ(98kDn!-n5`JRV-!@D2{A6IphE zkPsGFmW5&HXhOzjcc4w$P27K~i+!I3d@9TG{MreVH!c6^eUugDA|x4EmIt;C4Bf;q z3^YwgIn#mNmC^)=3m?$|OuS*<;(d}NBP0odKn!e~mIattriE!*XbF{F?{At0927}Y z=>STBVn7C@ONP)ISSHW`v;qmf{WSteT;G(RMffLk7W?6E&|p6{y8DXFA( z?AQScSzk`1fvpLvR;{Y`cs%^&r42N-wDupL*&i|#k)(lu7Fm{;&6+)zR98B^-Cn|d zej*`1;gFwjpP#U9TO@oZgOzv5>h+x3ahdVx?zHTZG zo1Fvy{2Ti5ojB#%&xkY-Fy^6$9=g4MoZrBnnt{`4$KTTh%?I%GH4|0B6lA&ap8u3B z=2<=t3x9G8K3SI6&AnwIuJlYK(LV!9^D)MbEJ2n8N>Cvp#pqKM9Hy5sh2HGs0C~lV z6;za!p~h9BY8*||IPviwTuwV@PoJZ7>eb9ya5GocRUh7Sz%Yr2dRe!vOF7@y`Jj|cq&f2!k(QA~ zc~vbXrInatW^t&|S4JjfMY%XEldyd_BS+SfF?tcN2d`znvyNTL1cLq$ zY0e^%Q8{bbWr?IXE0}frFEPzDLScwSEkb=!)M$iAUxl5TiB~U6BxiYj)zx!; z_KVE?(W5LmlXy%c67r+;wp&5(serHTqwU7I&A$;t4txI0ELXaiAQIs*hM}hzx~A%S zqRmu;O~iUSfEZUC>D%B)A;UQ|k%n@lSY#Nq4^5;Y9Z6Ob)Ql`A5{{w-6&%Ct14N24 zrch*md=})_w(`l-FE+Hu88>#m{Xt@inYDy-%48NVeVlz8-=(IkfW{+DRMt=8`q|Ub zjWFliJMs4hZQUVd(-k6JA<{QOq$`f}4LFjN5%Ql*87dJBhNSxbr;#}@Lj_vL5N66&B^+kt^w+aO2! z-O2|Tk?X=HNhm>us2n30QV4gn@y^;`V~_Z;_P@c1+!ma2s_{8TD$hzEjt-G(JAfIDLnkX7o%#^xuXsSwKK_O+O!|u;hdgweKAskYOYL)~X97H53yNX68;)&-{HC@lreIL)6iW+NJ+ua9dsEc zi7wn?#d^2sv zD4IHR5eY#srj%X?Fsl5 z(*FvSHcd)(8K$(vABHalN;pDO^R9T$_LY}7k|SrFBO|+B5~9L1O{ZySnxUyZmNP5O vJ#0j=h2+rUk(e6sH4-@ezJL>78z}z;x + + +<roundcube:object name="pagetitle" /> + + + + + + +
+ +
+ +

+ + +

+
+ + + +