Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2518245
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
58 KB
Referenced Files
None
Subscribers
None
View Options
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');
+ $('<input type="hidden" name="_unlock" />').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<len; n++)
+ if (folders[n] == data.realname)
+ break;
+
+ // add row
+ if (n && n < len) {
+ // find the row before
+ name = folders[n-1];
+ for (n in sources)
+ if (sources[n].realname && sources[n].realname == name) {
+ row.insertAfter('#rcmli'+n);
+ break;
+ }
+ }
+ else if (olddata) {
+ row.insertBefore(refrow);
+ }
+ else {
+ row.appendTo(list);
+ }
+
+ if (olddata) {
+ // remove old row (just after the new row has been inserted)
+ refrow.remove();
+ old += '-';
+ level = olddata.realname.split(this.env.delimiter).length - data.realname.split(this.env.delimiter).length;
+ // update (realname and ID of) subfolders
+ for (n in sources) {
+ if (n.indexOf(old) == 0) {
+ // new ID
+ id = data.id + '-' + n.substr(old.length);
+ name = sources[n].name;
+ realname = data.realname + sources[n].realname.substr(olddata.realname.length);
+
+ // update display name
+ if (level > 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 @@
+<?php
+
+/**
+ * Kolab address book UI
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * 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('<label for="%s">%s</label>', $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']) ? '</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
@@ -1,950 +1,1072 @@
<?php
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
*
* @author Thomas Bruederli
* @see rcube_addressbook
*/
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),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => null),
'phone' => array(),
'address' => array('limit' => 2, 'subtypes' => array('home','business')),
'officelocation' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
'website' => array('limit' => 1, 'subtypes' => null),
'im' => array('limit' => 1, 'subtypes' => null),
'gender' => array('limit' => 1),
'initials' => array('type' => 'text', 'size' => 6, 'limit' => 1,
'label' => 'kolab_addressbook.initials', 'category' => 'personal'),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
'manager' => array('limit' => 1),
'assistant' => array('limit' => 1),
'spouse' => array('limit' => 1),
'children' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.children', 'category' => 'personal'),
'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'),
'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'),
'notes' => array(),
'photo' => array(),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude
);
private $gid;
- private $imap;
- private $kolab;
- private $folder;
private $contactstorage;
private $liststorage;
private $contacts;
private $distlists;
private $groupmembers;
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');
private $addresstypemap = array('work' => 'business');
private $fieldmap = array(
// kolab => roundcube
'full-name' => 'name',
'given-name' => 'firstname',
'middle-names' => 'middlename',
'last-name' => 'surname',
'prefix' => 'prefix',
'suffix' => 'suffix',
'nick-name' => 'nickname',
'organization' => 'organization',
'department' => 'department',
'job-title' => 'jobtitle',
'initials' => 'initials',
'birthday' => 'birthday',
'anniversary' => 'anniversary',
'im-address' => 'im',
'web-page' => 'website',
'office-location' => 'officelocation',
'profession' => 'profession',
'manager-name' => 'manager',
'assistant' => 'assistant',
'spouse-name' => 'spouse',
'children' => 'children',
'body' => 'notes',
'pgp-publickey' => 'pgppublickey',
'free-busy-url' => 'freebusyurl',
);
public function __construct($imap_folder = null)
{
if ($imap_folder)
$this->imap_folder = $imap_folder;
// extend coltypes configuration
$format = rcube_kolab::get_format('contact');
$this->coltypes['phone']['subtypes'] = $format->_phone_types;
$this->coltypes['address']['subtypes'] = $format->_address_types;
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (is_string($prop['label']))
$this->coltypes[$col]['label'] = rcube_label($prop['label']);
}
// fetch objects from the given IMAP folder
$this->contactstorage = rcube_kolab::get_storage($this->imap_folder);
$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;
+ }
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
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';
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(strtolower($group['last-name']), strtolower($search)))
$groups[] = array('ID' => $group['ID'], 'name' => $group['last-name']);
}
return $groups;
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols=null, $subset=0)
{
$this->result = $this->count();
// list member of the selected group
if ($this->gid) {
$seen = array();
$this->result->count = 0;
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
continue;
if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++)
$this->result->count++;
}
$ids = array_keys($seen);
}
else
$ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
// fill contact data into the current result set
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, count($ids));
for ($i = $start_row; $i < $last_row; $i++) {
if ($id = $ids[$i])
$this->result->add($this->contacts[$id]);
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param boolean $strict True for strict (=), False for partial (LIKE) matching
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
{
$this->_fetch_contacts();
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = array_keys($this->coltypes);
}
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else
$value = mb_strtolower($value);
$scount = count($fields);
// build key name regexp
$regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'strict' => $strict, 'ids' => array());
// search be iterating over all records in memory
foreach ($this->contacts as $id => $contact) {
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f)
if (empty($contact[$f]))
continue 2;
}
$found = array();
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
if ($advanced) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $value[array_search($colname, $fields)];
}
else {
$search = $value;
}
foreach ((array)$contact[$col] as $val) {
// composite field, e.g. address
if (is_array($val)) {
$val = implode($val);
}
$val = mb_strtolower($val);
if (($strict && $val == $search) || (!$strict && strpos($val, $search) !== false)) {
if (!$advanced) {
$this->filter['ids'][] = $id;
break 2;
}
else {
$found[$colname] = true;
}
}
}
}
if (count($found) >= $scount) // && $advanced
$this->filter['ids'][] = $id;
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter)
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['strict']);
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
$this->_fetch_contacts();
$this->_fetch_groups();
$count = $this->gid ? count($this->distlists[$this->gid]['member']) : (is_array($this->filter['ids']) ? count($this->filter['ids']) : count($this->contacts));
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @param boolean True to return record as associative array, otherwise a result set is returned
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc=false)
{
$this->_fetch_contacts();
if ($this->contacts[$id]) {
$this->result = new rcube_result_set(1);
$this->result->add($this->contacts[$id]);
return $assoc ? $this->contacts[$id] : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
foreach ((array)$this->groupmembers[$id] as $gid) {
if ($group = $this->distlists[$gid])
$out[$gid] = $group['last-name'];
}
return $out;
}
/**
* Create a new contact record
*
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$object['uid'] = $this->contactstorage->generateUID();
$saved = $this->contactstorage->save($object);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$contact = $this->_to_rcube_contact($object);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $object['uid'];
$insert_id = $id;
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @return boolean True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
$this->_fetch_contacts();
if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
$old = $this->contactstorage->getObject($uid);
$object = array_merge($old, $this->_from_rcube_contact($save_data));
$saved = $this->contactstorage->save($object, $uid);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$this->contacts[$id] = $this->_to_rcube_contact($object);
$updated = true;
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
*/
public function delete($ids)
{
$this->_fetch_contacts();
$this->_fetch_groups();
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
foreach ($ids as $id) {
if ($uid = $this->id2uid[$id]) {
$deleted = $this->contactstorage->delete($uid);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else {
// remove from distribution lists
foreach ((array)$this->groupmembers[$id] as $gid)
$this->remove_from_group($gid, $id);
// clear internal cache
unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
$count++;
}
}
}
return $count;
}
/**
* Remove all records from the database
*/
public function delete_all()
{
if (!PEAR::isError($this->contactstorage->deleteAll())) {
$this->contacts = array();
$this->id2uid = array();
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
rcube_kolab::shutdown();
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'uid' => $this->liststorage->generateUID(),
'last-name' => $name,
'member' => array(),
);
$saved = $this->liststorage->save($list);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
else {
$id = md5($list['uid']);
$this->distlists[$record['ID']] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid])
$deleted = $this->liststorage->delete($list['uid']);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else
$result = true;
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['last-name']) {
$list['last-name'] = $newname;
$saved = $this->liststorage->save($list, $list['uid']);
}
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$added = 0;
$exists = array();
$this->_fetch_groups();
$this->_fetch_contacts();
$list = $this->distlists[$gid];
foreach ((array)$list['member'] as $i => $member)
$exists[] = $member['ID'];
// substract existing assignments from list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
if ($uid = $this->id2uid[$contact_id]) {
$contact = $this->contacts[$contact_id];
foreach ($this->get_col_values('email', $contact, true) as $email) {
$list['member'][] = array(
'uid' => $uid,
'display-name' => $contact['name'],
'smtp-address' => $email,
);
}
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added)
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
true, false);
$added = false;
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$this->_fetch_groups();
if (!($list = $this->distlists[$gid]))
return false;
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids))
$new_member[] = $member;
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Simply fetch all records and store them in private member vars
*/
private function _fetch_contacts()
{
if (!isset($this->contacts)) {
// read contacts
$this->contacts = $this->id2uid = array();
foreach ((array)$this->contactstorage->getObjects() as $record) {
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $record['uid'];
}
// sort data arrays according to desired list sorting
uasort($this->contacts, array($this, '_sort_contacts_comp'));
}
}
/**
* Callback function for sorting contacts
*/
private function _sort_contacts_comp($a, $b)
{
return strcasecmp($a['name'], $b['name']);
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups()
{
if (!isset($this->distlists)) {
$this->distlists = $this->groupmembers = array();
foreach ((array)$this->liststorage->getObjects() as $record) {
// FIXME: folders without any distribution-list objects return contacts instead ?!
if ($record['__type'] != 'Group')
continue;
$record['ID'] = md5($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = md5($member['uid']);
$record['member'][$i]['ID'] = $mid;
$this->groupmembers[$mid][] = $record['ID'];
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$out = array(
'ID' => md5($record['uid']),
'email' => array(),
'phone' => array(),
);
foreach ($this->fieldmap as $kolab => $rcube) {
if (strlen($record[$kolab]))
$out[$rcube] = $record[$kolab];
}
if (isset($record['gender']))
$out['gender'] = $this->gender_map[$record['gender']];
foreach ((array)$record['email'] as $i => $email)
$out['email'][] = $email['smtp-address'];
if (!$record['email'] && $record['emails'])
$out['email'] = preg_split('/,\s*/', $record['emails']);
foreach ((array)$record['phone'] as $i => $phone)
$out['phone:'.$phone['type']][] = $phone['number'];
if (is_array($record['address'])) {
foreach ($record['address'] as $i => $adr) {
$key = 'address:' . $adr['type'];
$out[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['postal-code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
$out['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
}
// remove empty fields
return array_filter($out);
}
private function _from_rcube_contact($contact)
{
$object = array();
foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
if (isset($contact[$rcube]))
$object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
else if ($values = $this->get_col_values($rcube, $contact, true))
$object[$kolab] = is_array($values) ? $values[0] : $values;
}
// format dates
if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
$object['birthday'] = date('Y-m-d', $date);
if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
$object['anniversary'] = date('Y-m-d', $date);
$gendermap = array_flip($this->gender_map);
if (isset($contact['gender']))
$object['gender'] = $gendermap[$contact['gender']];
$emails = $this->get_col_values('email', $contact, true);
$object['emails'] = join(', ', array_filter($emails));
// overwrite 'email' field
$object['email'] = '';
foreach ($this->get_col_values('phone', $contact) as $type => $values) {
if ($this->phonetypemap[$type])
$type = $this->phonetypemap[$type];
foreach ((array)$values as $phone) {
if (!empty($phone)) {
$object['phone-' . $type] = $phone;
$object['phone'][] = array('number' => $phone, 'type' => $type);
}
}
}
foreach ($this->get_col_values('address', $contact) as $type => $values) {
if ($this->addresstypemap[$type])
$type = $this->addresstypemap[$type];
$basekey = 'addr-' . $type . '-';
foreach ((array)$values as $adr) {
// switch type if slot is already taken
if (isset($object[$basekey . 'type'])) {
$type = $type == 'home' ? 'business' : 'home';
$basekey = 'addr-' . $type . '-';
}
if (!isset($object[$basekey . 'type'])) {
$object[$basekey . 'type'] = $type;
$object[$basekey . 'street'] = $adr['street'];
$object[$basekey . 'locality'] = $adr['locality'];
$object[$basekey . 'postal-code'] = $adr['zipcode'];
$object[$basekey . 'region'] = $adr['region'];
$object[$basekey . 'country'] = $adr['country'];
}
else {
$object['address'][] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'postal-code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
}
// save new photo as attachment
if ($contact['photo']) {
$attkey = 'photo.attachment';
$object['_attachments'][$attkey] = array(
'type' => rc_image_content_type($contact['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
);
$object['picture'] = $attkey;
}
return $object;
}
}
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 00000000..cd6228a7
Binary files /dev/null and b/plugins/kolab_addressbook/skins/default/kolab_folders.png differ
diff --git a/plugins/kolab_addressbook/skins/default/templates/bookedit.html b/plugins/kolab_addressbook/skins/default/templates/bookedit.html
new file mode 100644
index 00000000..717ed8c4
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/default/templates/bookedit.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+<link rel="stylesheet" type="text/css" href="/settings.css" />
+<script type="text/javascript" src="/functions.js"></script>
+</head>
+<body class="iframe">
+
+<div id="folder-title" class="boxtitle"><roundcube:label name="kolab_addressbook.bookproperties" /></div>
+
+<div id="folder-details" class="boxcontent">
+ <roundcube:object name="bookdetails" />
+ <p>
+<!--
+ <roundcube:if condition="!strlen(request:_source)" />
+ <input type="button" value="<roundcube:label name="cancel" />" class="button" onclick="history.back()" />
+ <roundcube:endif />
+-->
+ <roundcube:button command="book-save" type="input" class="button mainaction" label="save" />
+ </p>
+</div>
+<script type="text/javascript">rcube_init_tabs('folder-details > form')</script>
+
+</body>
+</html>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Dec 18, 10:58 AM (51 m, 44 s)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
418751
Default Alt Text
(58 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment