diff --git a/CHANGELOG b/CHANGELOG index 59b915f70..49c39906a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ CHANGELOG Roundcube Webmail =========================== +- vcard_attachments: Add possibility to attach contact vCard to composed message (#4997) - Preserve message internal/received date on import in mbox format (#5559) - Zipdownload: Fix date format in mbox "From line" - Possibility to display QR code for contacts data (#5030) diff --git a/plugins/vcard_attachments/composer.json b/plugins/vcard_attachments/composer.json index 07105bdf6..504d8a330 100644 --- a/plugins/vcard_attachments/composer.json +++ b/plugins/vcard_attachments/composer.json @@ -1,9 +1,9 @@ { "name": "roundcube/vcard_attachments", "type": "roundcube-plugin", - "description": "This plugin detects vCard attachments/bodies and shows a button(s) to add them to address book", + "description": "Detects vCard attachments and allows to add them to address book. Also allows to attach vCards of your contacts to composed messages", "license": "GPLv3+", - "version": "3.2", + "version": "4.0", "authors": [ { "name": "Thomas Bruederli", diff --git a/plugins/vcard_attachments/localization/en_US.inc b/plugins/vcard_attachments/localization/en_US.inc index a52a93228..65ab8ac24 100644 --- a/plugins/vcard_attachments/localization/en_US.inc +++ b/plugins/vcard_attachments/localization/en_US.inc @@ -19,5 +19,7 @@ $labels = array(); $labels['addvcardmsg'] = 'Add vCard to addressbook'; $labels['vcardsavefailed'] = 'Unable to save vCard'; +$labels['attachvcard'] = 'Attach vCard'; +$labels['vcard'] = 'vCard'; ?> \ No newline at end of file diff --git a/plugins/vcard_attachments/skins/classic/style.css b/plugins/vcard_attachments/skins/classic/style.css index 044d3983e..7c361441f 100644 --- a/plugins/vcard_attachments/skins/classic/style.css +++ b/plugins/vcard_attachments/skins/classic/style.css @@ -15,3 +15,15 @@ p.vcardattachment a { padding: 0.7em 0.5em 0.3em 42px; height: 22px; } + +#abookactions a.vcard span { + text-indent: -5000px; + display: inline-block; + height: 22px; + width: 15px; + background: url(../../../../skins/classic/images/messageicons.png) 0 -168px no-repeat; +} + +#abookactions a.vcard.disabled span { + opacity: 0.5; +} diff --git a/plugins/vcard_attachments/skins/larry/style.css b/plugins/vcard_attachments/skins/larry/style.css index 4f9f61b81..3115f69db 100644 --- a/plugins/vcard_attachments/skins/larry/style.css +++ b/plugins/vcard_attachments/skins/larry/style.css @@ -14,3 +14,8 @@ p.vcardattachment a { background: url(vcard_add_contact.png) 6px 2px no-repeat; padding: 1.2em 0.5em 0.7em 46px; } + +a.listbutton.vcard .inner +{ + background-position: center -2107px; +} diff --git a/plugins/vcard_attachments/vcard_attachments.php b/plugins/vcard_attachments/vcard_attachments.php index ebc494e45..177cb6f8e 100644 --- a/plugins/vcard_attachments/vcard_attachments.php +++ b/plugins/vcard_attachments/vcard_attachments.php @@ -1,7 +1,8 @@ action == 'show' || $rcmail->action == 'preview') { $this->add_hook('message_load', array($this, 'message_load')); $this->add_hook('template_object_messagebody', array($this, 'html_output')); } + else if ($rcmail->action == 'upload') { + $this->add_hook('attachment_from_uri', array($this, 'attach_vcard')); + } + else if ($rcmail->action == 'compose' && !$rcmail->output->framed) { + $skin_path = $this->local_skin_path(); + $btn_class = strpos($skin_path, 'classic') ? 'button' : 'listbutton'; + + $this->add_texts('localization', true); + $this->include_stylesheet($skin_path . '/style.css'); + $this->include_script('vcardattach.js'); + $this->add_button( + array( + 'type' => 'link', + 'label' => 'vcard_attachments.vcard', + 'command' => 'attach-vcard', + 'class' => $btn_class . ' vcard disabled', + 'classact' => $btn_class . ' vcard', + 'title' => 'vcard_attachments.attachvcard', + 'innerclass' => 'inner', + ), + 'compose-contacts-toolbar'); + } else if (!$rcmail->output->framed && (!$rcmail->action || $rcmail->action == 'list')) { $icon = 'plugins/vcard_attachments/' .$this->local_skin_path(). '/vcard.png'; $rcmail->output->set_env('vcard_icon', $icon); @@ -46,13 +70,14 @@ class vcard_attachments extends rcube_plugin // the same with message bodies foreach ((array)$this->message->parts as $part) { if ($this->is_vcard($part)) { - $this->vcard_parts[] = $part->mime_id; + $this->vcard_parts[] = $part->mime_id; $this->vcard_bodies[] = $part->mime_id; } } - if ($this->vcard_parts) + if ($this->vcard_parts) { $this->add_texts('localization'); + } } /** @@ -87,9 +112,9 @@ class vcard_attachments extends rcube_plugin // add box below message body $p['content'] .= html::p(array('class' => 'vcardattachment'), html::a(array( - 'href' => "#", + 'href' => "#", 'onclick' => "return plugin_vcard_save_contact('" . rcube::JQ($part.':'.$idx) . "')", - 'title' => $this->gettext('addvcardmsg'), + 'title' => $this->gettext('addvcardmsg'), ), html::span(null, rcube::Q($display))) ); @@ -222,4 +247,81 @@ class vcard_attachments extends rcube_plugin return $this->abook = $CONTACTS; } + + /** + * Attaches a contact vcard to composed mail + */ + public function attach_vcard($args) + { + if (preg_match('|^vcard://(.+)$|', $args['uri'], $m)) { + list($cid, $source) = explode('-', $m[1]); + + $vcard = $this->get_contact_vcard($source, $cid, $filename); + $params = array( + 'filename' => $filename, + 'mimetype' => 'text/vcard', + ); + + if ($vcard) { + $args['attachment'] = rcmail_save_attachment($vcard, null, $args['compose_id'], $params); + } + } + + return $args; + } + + /** + * Get vcard data for specified contact + */ + private function get_contact_vcard($source, $cid, &$filename = null) + { + $rcmail = rcmail::get_instance(); + $source = $rcmail->get_address_book($source); + $contact = $source->get_record($cid, true); + + if ($contact) { + $fieldmap = $source ? $source->vcard_map : null; + + if (empty($contact['vcard'])) { + $vcard = new rcube_vcard('', RCUBE_CHARSET, false, $fieldmap); + $vcard->reset(); + + foreach ($contact as $key => $values) { + list($field, $section) = explode(':', $key); + // avoid unwanted casting of DateTime objects to an array + // (same as in rcube_contacts::convert_save_data()) + if (is_object($values) && is_a($values, 'DateTime')) { + $values = array($values); + } + + foreach ((array) $values as $value) { + if (is_array($value) || is_a($value, 'DateTime') || @strlen($value)) { + $vcard->set($field, $value, strtoupper($section)); + } + } + } + + $contact['vcard'] = $vcard->export(); + } + + $name = rcube_addressbook::compose_list_name($contact); + $filename = (self::parse_filename($name) ?: 'contact') . '.vcf'; + + // fix folding and end-of-line chars + $vcard = preg_replace('/\r|\n\s+/', '', $contact['vcard']); + $vcard = preg_replace('/\n/', rcube_vcard::$eol, $vcard); + + return rcube_vcard::rfc2425_fold($vcard) . rcube_vcard::$eol; + } + } + + /** + * Helper function to convert contact name into filename + */ + static private function parse_filename($str) + { + $str = preg_replace('/[\t\n\r\0\x0B:\/]+\s*/', ' ', $str); + + return trim($str, " ./_"); + } } diff --git a/plugins/vcard_attachments/vcardattach.js b/plugins/vcard_attachments/vcardattach.js index 400966231..fe505c8f2 100644 --- a/plugins/vcard_attachments/vcardattach.js +++ b/plugins/vcard_attachments/vcardattach.js @@ -4,7 +4,7 @@ * @licstart The following is the entire license notice for the * JavaScript code in this file. * - * Copyright (c) 2012-2014, The Roundcube Dev Team + * Copyright (c) 2012-2016, 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 @@ -33,6 +33,40 @@ function plugin_vcard_insertrow(data) } } -if (window.rcmail && rcmail.gui_objects.messagelist) { - rcmail.addEventListener('insertrow', function(data, evt) { plugin_vcard_insertrow(data); }); +function plugin_vcard_attach() +{ + var id, n, contacts = [], + ts = new Date().getTime(), + args = {_uploadid: ts, _id: rcmail.env.compose_id}; + + for (n=0; n < rcmail.contact_list.selection.length; n++) { + id = rcmail.contact_list.selection[n]; + if (id && id.charAt(0) != 'E' && rcmail.env.contactdata[id]) + contacts.push(id); + } + + if (!contacts.length) + return false; + + args._uri = 'vcard://' + contacts.join(','); + + // add to attachments list + if (!rcmail.add2attachment_list(ts, {name: '', html: rcmail.get_label('attaching'), classname: 'uploading', complete: false})) + rcmail.file_upload_id = rcmail.set_busy(true, 'attaching'); + + rcmail.http_post('upload', args); } + +window.rcmail && rcmail.addEventListener('init', function(evt) { + if (rcmail.gui_objects.messagelist) + rcmail.addEventListener('insertrow', function(data, evt) { plugin_vcard_insertrow(data); }); + + if (rcmail.env.action == 'compose' && rcmail.gui_objects.contactslist) { + rcmail.env.compose_commands.push('attach-vcard'); + rcmail.register_command('attach-vcard', function() { plugin_vcard_attach(); }); + rcmail.contact_list.addEventListener('select', function(list) { + // TODO: support attaching more than one at once + rcmail.enable_command('attach-vcard', list.selection.length == 1 && rcmail.contact_list.selection[0].charAt(0) != 'E'); + }); + } +}); diff --git a/program/steps/addressbook/export.inc b/program/steps/addressbook/export.inc index b056a3e74..ecec53d73 100644 --- a/program/steps/addressbook/export.inc +++ b/program/steps/addressbook/export.inc @@ -109,7 +109,7 @@ if ($plugin['abort']) { } // send downlaod headers -header('Content-Type: text/x-vcard; charset='.RCUBE_CHARSET); +header('Content-Type: text/vcard; charset=' . RCUBE_CHARSET); header('Content-Disposition: attachment; filename="contacts.vcf"'); while ($result && ($row = $result->next())) { diff --git a/program/steps/mail/attachments.inc b/program/steps/mail/attachments.inc index b620cd8f7..21f220871 100644 --- a/program/steps/mail/attachments.inc +++ b/program/steps/mail/attachments.inc @@ -102,13 +102,18 @@ if ($uri) { && $RCMAIL->get_user_name() == rawurldecode($url['user']) ) { $message = new rcube_message($params['_uid'], $params['_mbox']); + + if ($message && !empty($message->headers)) { + $attachment = rcmail_save_attachment($message, $params['_part'], $COMPOSE_ID); + } } } - if ($message && !empty($message->headers) - && ($attachment = rcmail_save_attachment($message, $params['_part'], $COMPOSE_ID)) - ) { - rcmail_attachment_success($attachment, $uploadid); + $plugin = $RCMAIL->plugins->exec_hook('attachment_from_uri', array( + 'attachment' => $attachment, 'uri' => $uri, 'compose_id' => $COMPOSE_ID)); + + if ($plugin['attachment']) { + rcmail_attachment_success($plugin['attachment'], $uploadid); } else { $OUTPUT->command('display_message', $RCMAIL->gettext('filelinkerror'), 'error'); diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc index 19d7e7a8a..2c6a83854 100644 --- a/program/steps/mail/func.inc +++ b/program/steps/mail/func.inc @@ -2248,42 +2248,51 @@ function rcmail_save_attachment($message, $pid, $compose_id, $params = array()) $mimetype = $part->ctype_primary . '/' . $part->ctype_secondary; $filename = $params['filename'] ?: rcmail_attachment_name($part); } - else { + else if (is_object($message)) { // the whole message requested - $size = $message->size; + $size = $message->size; $mimetype = 'message/rfc822'; $filename = $params['filename'] ?: 'message_rfc822.eml'; } + else if (is_string($message)) { + // the whole message requested + $size = strlen($message); + $data = $message; + $mimetype = $params['mimetype']; + $filename = $params['filename']; + } + + if (!isset($data)) { + // don't load too big attachments into memory + if (!rcube_utils::mem_check($size)) { + $temp_dir = unslashify($rcmail->config->get('temp_dir')); + $path = tempnam($temp_dir, 'rcmAttmnt'); + + if ($fp = fopen($path, 'w')) { + if ($pid) { + // part body + $message->get_part_body($pid, false, 0, $fp); + } + else { + // complete message + $storage->get_raw_body($message->uid, $fp); + } - // don't load too big attachments into memory - if (!rcube_utils::mem_check($size)) { - $temp_dir = unslashify($rcmail->config->get('temp_dir')); - $path = tempnam($temp_dir, 'rcmAttmnt'); - - if ($fp = fopen($path, 'w')) { - if ($pid) { - // part body - $message->get_part_body($pid, false, 0, $fp); + fclose($fp); } else { - // complete message - $storage->get_raw_body($message->uid, $fp); + return false; } - - fclose($fp); + } + else if ($pid) { + // part body + $data = $message->get_part_body($pid); } else { - return false; + // complete message + $data = $storage->get_raw_body($message->uid); } } - else if ($pid) { - // part body - $data = $message->get_part_body($pid); - } - else { - // complete message - $data = $storage->get_raw_body($message->uid); - } $attachment = array( 'group' => $compose_id, @@ -2293,7 +2302,7 @@ function rcmail_save_attachment($message, $pid, $compose_id, $params = array()) 'data' => $data, 'path' => $path, 'size' => $path ? filesize($path) : strlen($data), - 'charset' => $part ? $part->charset : null, + 'charset' => $part ? $part->charset : $params['charset'], ); $attachment = $rcmail->plugins->exec_hook('attachment_save', $attachment); diff --git a/skins/classic/common.css b/skins/classic/common.css index 01ed26845..5aa7258c1 100644 --- a/skins/classic/common.css +++ b/skins/classic/common.css @@ -710,6 +710,11 @@ table.records-table tr.selected td background-color: #CC3333; } +table.records-table tr.selected td a +{ + color: #FFFFFF; +} + table.records-table tr.focused td { } diff --git a/skins/classic/mail.css b/skins/classic/mail.css index d275c054a..a45545434 100644 --- a/skins/classic/mail.css +++ b/skins/classic/mail.css @@ -1707,6 +1707,7 @@ input.from_address position: absolute; margin-right: 5px; right: 0; + top: 0; } #abookactions diff --git a/skins/classic/templates/compose.html b/skins/classic/templates/compose.html index ab64e739d..97b39c498 100644 --- a/skins/classic/templates/compose.html +++ b/skins/classic/templates/compose.html @@ -49,7 +49,16 @@