From d5609160657829b0077fbb0db3b76b6096033652 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Thu, 08 Oct 2015 10:55:43 -0400
Subject: [PATCH] Added possibility to drag-n-drop attachments from mail preview to compose window

---
 CHANGELOG                               |    1 
 program/steps/mail/attachments.inc      |  108 ++++++++++++-----
 program/steps/mail/compose.inc          |   99 ---------------
 program/localization/en_US/messages.inc |    2 
 program/steps/mail/func.inc             |   74 ++++++++++++
 program/steps/mail/show.inc             |    2 
 program/js/app.js                       |   48 +++++++-
 7 files changed, 199 insertions(+), 135 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index 444447d..040abb9 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,7 @@
 CHANGELOG Roundcube Webmail
 ===========================
 
+- Added possibility to drag-n-drop attachments from mail preview to compose window
 - Implemented mail messages searching with predefined date interval
 - PGP encryption support via Mailvelope integration
 - PGP encryption support via Enigma plugin
diff --git a/program/js/app.js b/program/js/app.js
index 46e0857..b8713a7 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -6,8 +6,8 @@
  * @licstart  The following is the entire license notice for the
  * JavaScript code in this file.
  *
- * Copyright (C) 2005-2014, The Roundcube Dev Team
- * Copyright (C) 2011-2014, Kolab Systems AG
+ * Copyright (C) 2005-2015, The Roundcube Dev Team
+ * Copyright (C) 2011-2015, Kolab Systems AG
  *
  * The JavaScript code in this page is free software: you can
  * redistribute it and/or modify it under the terms of the GNU
@@ -274,6 +274,23 @@
             this.enable_command('compose', 'add-contact', false);
             parent.rcmail.show_contentframe(true);
           }
+
+          // initialize drag-n-drop on attachments, so they can e.g.
+          // be dropped into mail compose attachments in another window
+          if (this.gui_objects.attachments)
+            $('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) {
+              var n, href = this.href, dt = e.originalEvent.dataTransfer;
+              if (dt) {
+                // inject username to the uri
+                href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'});
+                // cleanup the node to get filename without the size test
+                n = $(this).clone();
+                n.children().remove();
+
+                dt.setData('roundcube-uri', href);
+                dt.setData('roundcube-name', $.trim(n.text()));
+              }
+            });
         }
         else if (this.env.action == 'compose') {
           this.env.address_group_stack = [];
@@ -8410,15 +8427,32 @@
     this.file_drag_hover(e, false);
 
     // prepare multipart form data composition
-    var files = e.target.files || e.dataTransfer.files,
+    var uri, files = e.target.files || e.dataTransfer.files,
       formdata = window.FormData ? new FormData() : null,
       fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
       boundary = '------multipartformboundary' + (new Date).getTime(),
       dashdash = '--', crlf = '\r\n',
-      multipart = dashdash + boundary + crlf;
+      multipart = dashdash + boundary + crlf,
+      args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
 
-    if (!files || !files.length)
+    if (!files || !files.length) {
+      // Roundcube attachment, pass its uri to the backend and attach
+      if (uri = e.dataTransfer.getData('roundcube-uri')) {
+        var ts = new Date().getTime(),
+          // jQuery way to escape filename (#1490530)
+          content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.gettext('attaching')).html();
+
+        args._uri = uri;
+        args._uploadid = ts;
+
+        // add to attachments list
+        if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
+          this.file_upload_id = this.set_busy(true, 'attaching');
+
+        this.http_post(this.env.filedrop.action || 'upload', args);
+      }
       return;
+    }
 
     // inline function to submit the files to the server
     var submit_data = function() {
@@ -8434,10 +8468,12 @@
       // complete multipart content and post request
       multipart += dashdash + boundary + dashdash + crlf;
 
+      args._uploadid = ts;
+
       $.ajax({
         type: 'POST',
         dataType: 'json',
-        url: ref.url(ref.env.filedrop.action || 'upload', {_id: ref.env.compose_id||ref.env.cid||'', _uploadid: ts, _remote: 1, _from: ref.env.action}),
+        url: ref.url(ref.env.filedrop.action || 'upload', args),
         contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
         processData: false,
         timeout: 0, // disable default timeout set in ajaxSetup()
diff --git a/program/localization/en_US/messages.inc b/program/localization/en_US/messages.inc
index cc8d5ea..313d6ae 100644
--- a/program/localization/en_US/messages.inc
+++ b/program/localization/en_US/messages.inc
@@ -41,6 +41,7 @@
 $messages['refreshing'] = 'Refreshing...';
 $messages['loading'] = 'Loading...';
 $messages['uploading'] = 'Uploading file...';
+$messages['attaching'] = 'Attaching file...';
 $messages['uploadingmany'] = 'Uploading files...';
 $messages['loadingdata'] = 'Loading data...';
 $messages['checkingmail'] = 'Checking for new messages...';
@@ -114,6 +115,7 @@
 $messages['deletedsuccessfully'] = 'Successfully deleted.';
 $messages['converting'] = 'Removing formatting...';
 $messages['messageopenerror'] = 'Could not load message from server.';
+$messages['filelinkerror'] = 'Attaching the file failed.';
 $messages['fileuploaderror'] = 'File upload failed.';
 $messages['filesizeerror'] = 'The uploaded file exceeds the maximum size of $size.';
 $messages['copysuccess'] = 'Successfully copied $nr contacts.';
diff --git a/program/steps/mail/attachments.inc b/program/steps/mail/attachments.inc
index fe5684c..c39b25c 100644
--- a/program/steps/mail/attachments.inc
+++ b/program/steps/mail/attachments.inc
@@ -68,7 +68,6 @@
     }
 
     $RCMAIL->display_uploaded_file($COMPOSE['attachments'][$id]);
-
     exit;
 }
 
@@ -77,8 +76,41 @@
 // clear all stored output properties (like scripts and env vars)
 $OUTPUT->reset();
 
-$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GET);
+$uploadid = rcube_utils::get_input_value('_uploadid', rcube_utils::INPUT_GPC);
+$uri      = rcube_utils::get_input_value('_uri', rcube_utils::INPUT_POST);
 
+// handle dropping a reference to an attachment part of some message
+if ($uri) {
+    $url = parse_url($uri);
+    parse_str($url['query'], $params);
+
+    if (strlen($params['_mbox']) && $params['_uid'] && $params['_part']) {
+        // @TODO: at some point we might support drag-n-drop between
+        // two different accounts on the same server, for now make sure
+        // this is the same server and the same user
+        list($host, $port) = explode(':', $_SERVER['HTTP_HOST']);
+        if ($host == $url['host'] && $port == $url['port']
+            && $RCMAIL->get_user_name() == rawurldecode($url['user'])
+        ) {
+            $message = new rcube_message($params['_uid'], $params['_mbox']);
+        }
+    }
+
+    if ($message && !empty($message->headers)
+        && ($attachment = rcmail_save_attachment($message, $params['_part'], $COMPOSE_ID))
+    ) {
+        rcmail_attachment_success($attachment, $uploadid);
+    }
+    else {
+        $OUTPUT->command('display_message', $RCMAIL->gettext('filelinkerror'), 'error');
+        $OUTPUT->command('remove_from_attachment_list', $uploadid);
+    }
+
+    $OUTPUT->send();
+    return;
+}
+
+// handle file(s) upload
 if (is_array($_FILES['_attachments']['tmp_name'])) {
     $multiple = count($_FILES['_attachments']['tmp_name']) > 1;
 
@@ -97,41 +129,11 @@
         }
 
         if (!$err && $attachment['status'] && !$attachment['abort']) {
-            $id = $attachment['id'];
-
             // store new attachment in session
             unset($attachment['status'], $attachment['abort']);
-            $RCMAIL->session->append($SESSION_KEY.'.attachments', $id, $attachment);
+            $RCMAIL->session->append($SESSION_KEY . '.attachments', $attachment['id'], $attachment);
 
-            if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) {
-                $button = html::img(array(
-                    'src' => $icon,
-                    'alt' => $RCMAIL->gettext('delete')
-                ));
-            }
-            else if ($COMPOSE['textbuttons']) {
-                $button = rcube::Q($RCMAIL->gettext('delete'));
-            }
-            else {
-                $button = '';
-            }
-
-            $content = html::a(array(
-                'href'    => "#delete",
-                'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", rcmail_output::JS_OBJECT_NAME, $id),
-                'title'   => $RCMAIL->gettext('delete'),
-                'class'   => 'delete',
-                'aria-label' => $RCMAIL->gettext('delete') . ' ' . $attachment['name'],
-            ), $button);
-
-            $content .= rcube::Q($attachment['name']);
-
-            $OUTPUT->command('add2attachment_list', "rcmfile$id", array(
-                'html'      => $content,
-                'name'      => $attachment['name'],
-                'mimetype'  => $attachment['mimetype'],
-                'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
-                'complete'  => true), $uploadid);
+            rcmail_attachment_success($attachment, $uploadid);
         }
         else {  // upload failed
             if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
@@ -172,3 +174,41 @@
 // send html page with JS calls as response
 $OUTPUT->command('auto_save_start', false);
 $OUTPUT->send('iframe');
+
+
+function rcmail_attachment_success($attachment, $uploadid)
+{
+    global $RCMAIL, $COMPOSE;
+
+    $id = $attachment['id'];
+
+    if (($icon = $COMPOSE['deleteicon']) && is_file($icon)) {
+        $button = html::img(array(
+            'src' => $icon,
+            'alt' => $RCMAIL->gettext('delete')
+        ));
+    }
+    else if ($COMPOSE['textbuttons']) {
+        $button = rcube::Q($RCMAIL->gettext('delete'));
+    }
+    else {
+        $button = '';
+    }
+
+    $content = html::a(array(
+        'href'    => "#delete",
+        'onclick' => sprintf("return %s.command('remove-attachment','rcmfile%s', this)", rcmail_output::JS_OBJECT_NAME, $id),
+        'title'   => $RCMAIL->gettext('delete'),
+        'class'   => 'delete',
+        'aria-label' => $RCMAIL->gettext('delete') . ' ' . $attachment['name'],
+    ), $button);
+
+    $content .= rcube::Q($attachment['name']);
+
+    $RCMAIL->output->command('add2attachment_list', "rcmfile$id", array(
+        'html'      => $content,
+        'name'      => $attachment['name'],
+        'mimetype'  => $attachment['mimetype'],
+        'classname' => rcube_utils::file2class($attachment['mimetype'], $attachment['name']),
+        'complete'  => true), $uploadid);
+}
diff --git a/program/steps/mail/compose.inc b/program/steps/mail/compose.inc
index 0b047d4..0e61e26 100644
--- a/program/steps/mail/compose.inc
+++ b/program/steps/mail/compose.inc
@@ -88,7 +88,7 @@
     'selectimportfile', 'messageissent', 'loadingdata', 'nopubkeyfor', 'nopubkeyforsender',
     'encryptnoattachments','encryptedsendialog','searchpubkeyservers', 'importpubkeys',
     'encryptpubkeysfound',  'search', 'close', 'import', 'keyid', 'keylength', 'keyexpired',
-    'keyrevoked', 'keyimportsuccess', 'keyservererror');
+    'keyrevoked', 'keyimportsuccess', 'keyservererror', 'attaching');
 
 $OUTPUT->set_pagetitle($RCMAIL->gettext('compose'));
 
@@ -1294,10 +1294,8 @@
             }
 
             if (($attachment = $loaded_attachments[rcmail_attachment_name($part) . $part->mimetype])
-                || ($attachment = rcmail_save_attachment($message, $pid))
+                || ($attachment = rcmail_save_attachment($message, $pid, $COMPOSE['id']))
             ) {
-                $COMPOSE['attachments'][$attachment['id']] = $attachment;
-
                 if ($bodyIsHtml && ($part->content_id || $part->content_location)) {
                     $url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
                         $RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
@@ -1329,8 +1327,7 @@
 
     foreach ((array)$message->mime_parts as $pid => $part) {
         if (($part->content_id || $part->content_location) && $part->filename) {
-            if ($attachment = rcmail_save_attachment($message, $pid)) {
-                $COMPOSE['attachments'][$attachment['id']] = $attachment;
+            if ($attachment = rcmail_save_attachment($message, $pid, $COMPOSE['id'])) {
                 $url = sprintf('%s&_id=%s&_action=display-attachment&_file=rcmfile%s',
                     $RCMAIL->comm_path, $COMPOSE['id'], $attachment['id']);
 
@@ -1398,46 +1395,11 @@
         $names[$name] = 1;
         $name .= '.eml';
 
-        $data = $path = null;
-
         if (!empty($loaded_attachments[$name . 'message/rfc822'])) {
             continue;
         }
 
-        // don't load too big attachments into memory
-        if (!rcube_utils::mem_check($message->size)) {
-            $temp_dir = unslashify($RCMAIL->config->get('temp_dir'));
-            $path     = tempnam($temp_dir, 'rcmAttmnt');
-            if ($fp = fopen($path, 'w')) {
-                $storage->get_raw_body($message->uid, $fp);
-                fclose($fp);
-            }
-            else {
-                return false;
-            }
-        }
-        else {
-            $data = $storage->get_raw_body($message->uid);
-        }
-
-        $attachment = array(
-            'group'    => $COMPOSE['id'],
-            'name'     => $name,
-            'mimetype' => 'message/rfc822',
-            'data'     => $data,
-            'path'     => $path,
-            'size'     => $path ? filesize($path) : strlen($data),
-        );
-
-        $attachment = $RCMAIL->plugins->exec_hook('attachment_save', $attachment);
-
-        if ($attachment['status']) {
-            unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']);
-            $COMPOSE['attachments'][$attachment['id']] = $attachment;
-        }
-        else if ($path) {
-            @unlink($path);
-        }
+        rcmail_save_attachment($message, null, $COMPOSE['id'], array('filename' => $name));
 
         if ($message->headers->messageID) {
             $refs[] = $message->headers->messageID;
@@ -1451,59 +1413,6 @@
     if (!empty($refs)) {
         $COMPOSE['references'] = implode(' ', $refs);
     }
-}
-
-
-function rcmail_save_attachment(&$message, $pid)
-{
-    global $COMPOSE;
-
-    $rcmail = rcmail::get_instance();
-    $part   = $message->mime_parts[$pid];
-    $data   = $path = null;
-
-    // don't load too big attachments into memory
-    if (!rcube_utils::mem_check($part->size)) {
-        $temp_dir = unslashify($rcmail->config->get('temp_dir'));
-        $path     = tempnam($temp_dir, 'rcmAttmnt');
-
-        if ($fp = fopen($path, 'w')) {
-            $message->get_part_body($pid, false, 0, $fp);
-            fclose($fp);
-        }
-        else {
-            return false;
-        }
-    }
-    else {
-        $data = $message->get_part_body($pid);
-    }
-
-    $mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
-    $filename = rcmail_attachment_name($part);
-
-    $attachment = array(
-        'group'      => $COMPOSE['id'],
-        'name'       => $filename,
-        'mimetype'   => $mimetype,
-        'content_id' => $part->content_id,
-        'data'       => $data,
-        'path'       => $path,
-        'size'       => $path ? filesize($path) : strlen($data),
-        'charset'    => $part->charset,
-    );
-
-    $attachment = $rcmail->plugins->exec_hook('attachment_save', $attachment);
-
-    if ($attachment['status']) {
-        unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']);
-        return $attachment;
-    }
-    else if ($path) {
-        @unlink($path);
-    }
-
-    return false;
 }
 
 function rcmail_save_image($path, $mimetype = '', $data = null)
diff --git a/program/steps/mail/func.inc b/program/steps/mail/func.inc
index d949cf6..80b618c 100644
--- a/program/steps/mail/func.inc
+++ b/program/steps/mail/func.inc
@@ -2199,3 +2199,77 @@
 
     return $jsresult;
 }
+
+function rcmail_save_attachment($message, $pid, $compose_id, $params = array())
+{
+    $rcmail  = rcmail::get_instance();
+    $storage = $rcmail->get_storage();
+
+    if ($pid) {
+        // attachment requested
+        $part     = $message->mime_parts[$pid];
+        $size     = $part->size;
+        $mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
+        $filename = $params['filename'] ?: rcmail_attachment_name($part);
+    }
+    else {
+        // the whole message requested
+        $size = $message->size;
+        $mimetype = 'message/rfc822';
+        $filename = $params['filename'] ?: 'message_rfc822.eml';
+    }
+
+    // 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);
+            }
+
+            fclose($fp);
+        }
+        else {
+            return false;
+        }
+    }
+    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,
+        'name'       => $filename,
+        'mimetype'   => $mimetype,
+        'content_id' => $part ? $part->content_id : null,
+        'data'       => $data,
+        'path'       => $path,
+        'size'       => $path ? filesize($path) : strlen($data),
+        'charset'    => $part ? $part->charset : null,
+    );
+
+    $attachment = $rcmail->plugins->exec_hook('attachment_save', $attachment);
+
+    if ($attachment['status']) {
+        unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']);
+        $rcmail->session->append('compose_data_' . $compose_id . '.attachments', $attachment['id'], $attachment);
+        return $attachment;
+    }
+    else if ($path) {
+        @unlink($path);
+    }
+
+    return false;
+}
diff --git a/program/steps/mail/show.inc b/program/steps/mail/show.inc
index e5bfcf0..82bcb41 100644
--- a/program/steps/mail/show.inc
+++ b/program/steps/mail/show.inc
@@ -68,6 +68,7 @@
     $OUTPUT->set_env('safemode', $MESSAGE->is_safe);
     $OUTPUT->set_env('sender', $MESSAGE->sender['string']);
     $OUTPUT->set_env('mailbox', $mbox_name);
+    $OUTPUT->set_env('username', $RCMAIL->get_user_name());
     $OUTPUT->set_env('permaurl', $RCMAIL->url(array('_action' => 'show', '_uid' => $MESSAGE->uid, '_mbox' => $mbox_name)));
 
     if ($MESSAGE->headers->get('list-post', false)) {
@@ -240,6 +241,7 @@
         $out = html::tag('ul', $attrib, $ol, html::$common_attrib);
 
         $RCMAIL->output->set_env('attachments', $attachments);
+        $RCMAIL->output->add_gui_object('attachments', $attrib['id']);
     }
 
     return $out;

--
Gitblit v1.9.1