diff --git a/appinfo/routes.php b/appinfo/routes.php index 48d21a1f20..b9be6cc940 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -45,6 +45,10 @@ 'name' => 'messages#setFlags', 'url' => '/accounts/{accountId}/folders/{folderId}/messages/{messageId}/flags', 'verb' => 'PUT'], + [ + 'name' => 'messages#move', + 'url' => '/accounts/{accountId}/folders/{folderId}/messages/{id}/move', + 'verb' => 'POST'], [ 'name' => 'proxy#redirect', 'url' => '/redirect', diff --git a/css/mail.css b/css/mail.css index 4cfafb6930..dc7014d5e7 100755 --- a/css/mail.css +++ b/css/mail.css @@ -26,6 +26,10 @@ border: none; } +.mail_folders > .ui-droppable-active { + background-color: rgb(240, 240, 240); +} + /* SETUP */ #setup { diff --git a/js/controller/messagecontroller.js b/js/controller/messagecontroller.js index 5ea595a2ed..302c178528 100644 --- a/js/controller/messagecontroller.js +++ b/js/controller/messagecontroller.js @@ -32,6 +32,7 @@ define(function(require) { }); Radio.message.on('forward', openForwardComposer); Radio.message.on('flag', flagMessage); + Radio.message.on('move', moveMessage); /** * @param {Account} account @@ -189,6 +190,28 @@ define(function(require) { }); } + function moveMessage(sourceAccount, sourceFolder, message, destAccount, + destFolder) { + if (sourceAccount.get('accountId') === destAccount.get('accountId') + && sourceFolder.get('id') === destFolder.get('id')) { + // Nothing to move + return; + } + + var moving = Radio.message.request('move', sourceAccount, sourceFolder, message, destAccount, destFolder); + + sourceFolder.messages.remove(message); + destFolder.addMessage(message); + + $.when(moving).done(function() { + // TODO: update counters + }); + $.when(moving).fail(function() { + Radio.ui.trigger('error:show', t('mail', 'Could not move message.')); + sourceFolder.addMessage(message); + }); + } + /** * @param {Account} account * @param {Folder} folder diff --git a/js/models/account.js b/js/models/account.js index f10557b3e6..5874906ab9 100644 --- a/js/models/account.js +++ b/js/models/account.js @@ -58,8 +58,8 @@ define(function(require) { * @returns {undefined} */ addFolder: function(folder) { - folder = this.folders.add(folder); folder.account = this; + this.folders.add(folder); }, getFolderById: function(folderId) { if (!this.folders) { diff --git a/js/models/folder.js b/js/models/folder.js index 84fd0e1c0e..94ede0dff7 100644 --- a/js/models/folder.js +++ b/js/models/folder.js @@ -30,7 +30,13 @@ define(function(require) { initialize: function() { var FolderCollection = require('models/foldercollection'); var MessageCollection = require('models/messagecollection'); + this.account = this.get('account'); + this.unset('account'); this.folders = new FolderCollection(this.get('folders') || []); + this.folders.forEach(_.bind(function(folder) { + folder.account = this.account; + }, this)); + this.unset('folders'); this.messages = new MessageCollection(); }, toggleOpen: function() { @@ -41,8 +47,8 @@ define(function(require) { * @returns {undefined} */ addMessage: function(message) { - this.messages.add(message); message.folder = this; + this.messages.add(message); }, /** * @param {Array} message @@ -59,14 +65,11 @@ define(function(require) { * @returns {undefined} */ addFolder: function(folder) { - folder = this.folder.add(folder); + folder = this.folders.add(folder); folder.account = this.account; }, toJSON: function() { var data = Backbone.Model.prototype.toJSON.call(this); - if (data.folders && data.folders.toJSON) { - data.folders = data.folders.toJSON(); - } if (!data.id) { data.id = this.cid; } diff --git a/js/models/message.js b/js/models/message.js index 4fb4768fcf..322f40fac6 100644 --- a/js/models/message.js +++ b/js/models/message.js @@ -18,12 +18,18 @@ define(function(require) { * @class Message */ var Message = Backbone.Model.extend({ + folder: undefined, defaults: { flags: [], active: false }, initialize: function() { this.set('flags', new MessageFlags(this.get('flags'))); + if (this.get('folder')) { + // Folder should be a simple property + this.folder = this.get('folder'); + this.unset('folder'); + } this.listenTo(this.get('flags'), 'change', this._transformEvent); }, _transformEvent: function() { diff --git a/js/service/folderservice.js b/js/service/folderservice.js index a5be63ae6d..facc31ed0b 100644 --- a/js/service/folderservice.js +++ b/js/service/folderservice.js @@ -12,6 +12,7 @@ define(function(require) { 'use strict'; var $ = require('jquery'); + var _ = require('underscore'); var OC = require('OC'); /** @@ -32,7 +33,7 @@ define(function(require) { for (var prop in data) { if (prop === 'folders') { account.folders.reset(); - account.addFolder(data.folders); + _.each(data.folders, account.addFolder, account); } else { account.set(prop, data[prop]); } diff --git a/js/service/messageservice.js b/js/service/messageservice.js index dd28fdce45..76a19f1de5 100644 --- a/js/service/messageservice.js +++ b/js/service/messageservice.js @@ -29,6 +29,7 @@ define(function(require) { Radio.message.reply('entity', getMessageEntity); Radio.message.reply('bodies', fetchMessageBodies); Radio.message.reply('flag', flagMessage); + Radio.message.reply('move', moveMessage); Radio.message.reply('send', sendMessage); Radio.message.reply('draft', saveDraft); Radio.message.reply('delete', deleteMessage); @@ -206,6 +207,40 @@ define(function(require) { return defer.promise(); } + /** + * @param {Account} sourceAccount + * @param {Folder} sourceFolder + * @param {Message} message + * @param {Account} destAccount + * @param {Folder} destFolder + * @returns {Deferred} + */ + function moveMessage(sourceAccount, sourceFolder, message, destAccount, + destFolder) { + var defer = $.Deferred(); + + var url = OC.generateUrl('apps/mail/accounts/{accountId}/folders/{folderId}/messages/{messageId}/move', { + accountId: sourceAccount.get('accountId'), + folderId: sourceFolder.get('id'), + messageId: message.get('id') + }); + $.ajax(url, { + type: 'POST', + data: { + destAccountId: destAccount.get('accountId'), + destFolderId: destFolder.get('id') + }, + success: function() { + defer.resolve(); + }, + error: function() { + defer.reject(); + } + }); + + return defer.promise(); + } + /** * @param {Account} account * @param {object} message diff --git a/js/views/account.js b/js/views/accountview.js similarity index 98% rename from js/views/account.js rename to js/views/accountview.js index 1bc2509039..8e2d362eea 100644 --- a/js/views/account.js +++ b/js/views/accountview.js @@ -15,7 +15,7 @@ define(function(require) { var Marionette = require('marionette'); var OC = require('OC'); var Radio = require('radio'); - var FolderView = require('views/folder'); + var FolderView = require('views/folderview'); var AccountTemplate = require('text!templates/account.html'); var SHOW_COLLAPSED = Object.seal([ diff --git a/js/views/folder.js b/js/views/folderview.js similarity index 73% rename from js/views/folder.js rename to js/views/folderview.js index c81fa88210..1e467e9a17 100644 --- a/js/views/folder.js +++ b/js/views/folderview.js @@ -12,6 +12,7 @@ define(function(require) { 'use strict'; var $ = require('jquery'); + var _ = require('underscore'); var Backbone = require('backbone'); var Handlebars = require('handlebars'); var OC = require('OC'); @@ -34,9 +35,12 @@ define(function(require) { folderId: this.model.get('id') }); + var folders = this.model.folders.length > 0 ? this.model.folders.toJSON() : undefined; + return { count: count, - url: url + url: url, + folders: folders }; }, events: { @@ -72,6 +76,21 @@ define(function(require) { // nesting elements during re-render. this.$el.unwrap(); this.setElement(this.$el); + + // Make non search fodler folders droppable + if (!(/\/FLAGGED$/.test(atob(this.model.get('id'))))) { + var dropScope = 'folder-' + this.model.account.get('accountId'); + this.$el.droppable({ + scope: dropScope, + drop: _.bind(function(event, ui) { + var account = require('state').currentAccount; + var sourceFolder = account.getFolderById(ui.helper.data('folderId')); + var message = sourceFolder.messages.get(ui.helper.data('messageId')); + Radio.message.trigger('move', account, sourceFolder, message, account, this.model); + }, this), + hoverClass: 'ui-droppable-active' + }); + } } }); }); diff --git a/js/views/messagesitem.js b/js/views/messagesitem.js index a1089ea550..7510316fbb 100644 --- a/js/views/messagesitem.js +++ b/js/views/messagesitem.js @@ -50,6 +50,24 @@ define(function(require) { }); } + var _this = this; + var dragScope = 'folder-' + this.model.folder.account.get('accountId'); + this.$el.draggable({ + appendTo: '#content-wrapper', + scope: dragScope, + helper: function() { + var el = $('
'); + el.data('folderId', require('state').currentFolder.get('id')); + el.data('messageId', _this.model.get('id')); + return el; + }, + cursorAt: { + top: -5, + left: -5 + }, + revert: 'invalid' + }); + $('.action.delete').tooltip({placement: 'left'}); }, toggleMessageStar: function(event) { diff --git a/js/views/navigation-accounts.js b/js/views/navigation-accounts.js index 1c1706e89c..ca6e67a72d 100644 --- a/js/views/navigation-accounts.js +++ b/js/views/navigation-accounts.js @@ -14,7 +14,7 @@ define(function(require) { var _ = require('underscore'); var $ = require('jquery'); var Marionette = require('marionette'); - var AccountView = require('views/account'); + var AccountView = require('views/accountview'); var Radio = require('radio'); /** diff --git a/lib/Account.php b/lib/Account.php index b9d2488355..8d4ace8634 100644 --- a/lib/Account.php +++ b/lib/Account.php @@ -518,6 +518,13 @@ public function deleteMessage($sourceFolderId, $messageId) { } } + public function moveMessage($sourceFolderId, $messageId, $destFolderId) { + $this->getImapConnection()->copy($sourceFolderId, $destFolderId, [ + 'ids' => new \Horde_Imap_Client_Ids($messageId), + 'move' => true, + ]); + } + /** * * @param int $messageId diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index 72951665f6..13990b9944 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -28,6 +28,7 @@ namespace OCA\Mail\Controller; +use OCA\Mail\Exception\ServiceException; use OCA\Mail\Http\AttachmentDownloadResponse; use OCA\Mail\Http\HtmlResponse; use OCA\Mail\Service\AccountService; @@ -203,6 +204,28 @@ public function show($accountId, $folderId, $id) { return new JSONResponse($json); } + /** + * @NoAdminRequired + * @NoCSRFRequired + * + * @param int $accountId + * @param string $folderId + * @param int $id + * @param int $destAccountId + * @param string $destFolderId + * @return JSONResponse + */ + public function move($accountId, $folderId, $id, $destAccountId, $destFolderId) { + try { + $this->accountService->moveMessage($accountId, $folderId, $id, $destAccountId, $destFolderId, $this->currentUserId); + } catch (ServiceException $ex) { + return new JSONResponse([ + 'error' => $ex->getMessage(), + ], 500); + } + return new JSONResponse(); + } + /** * @NoAdminRequired * @NoCSRFRequired diff --git a/lib/Service/AccountService.php b/lib/Service/AccountService.php index 808c493d33..d1931ea840 100644 --- a/lib/Service/AccountService.php +++ b/lib/Service/AccountService.php @@ -23,12 +23,14 @@ use Exception; use OCA\Mail\Account; +use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Exception\ServiceException; use OCP\IL10N; class AccountService { - /** @var \OCA\Mail\Db\MailAccountMapper */ + /** @var MailAccountMapper */ private $mapper; /** @@ -90,6 +92,23 @@ public function find($currentUserId, $accountId) { return new Account($this->mapper->find($currentUserId, $accountId)); } + private function moveMessageOnSameAccount(IAccount $account, $sourceFolderId, + $destFolderId, $messageId) { + $account->moveMessage(base64_decode($sourceFolderId), $messageId, base64_decode($destFolderId)); + } + + public function moveMessage($accountId, $folderId, $id, $destAccountId, + $destFolderId, $userId) { + $sourceAccount = $this->find($userId, $accountId); + $destAccount = $this->find($userId, $destAccountId); + + if ($sourceAccount->getId() === $destAccount->getId()) { + $this->moveMessageOnSameAccount($sourceAccount, $folderId, $destFolderId, $id); + } else { + throw new ServiceException('It is not possible to move across accounts yet'); + } + } + /** * @param int $accountId */ @@ -103,7 +122,7 @@ public function delete($currentUserId, $accountId) { /** * @param $newAccount - * @return \OCA\Mail\Db\MailAccount + * @return MailAccount */ public function save($newAccount) { return $this->mapper->save($newAccount); diff --git a/lib/Service/IAccount.php b/lib/Service/IAccount.php index 2515f4663c..10cf956b92 100644 --- a/lib/Service/IAccount.php +++ b/lib/Service/IAccount.php @@ -61,6 +61,13 @@ public function saveDraft(IMessage $message, $previousUID); */ public function deleteMessage($folderId, $messageId); + /** + * @param string $sourceFolderId + * @param int $messageId + * @param string $destFolderId + */ + public function moveMessage($sourceFolderId, $messageId, $destFolderId); + /** * @param string[] $query * @return array diff --git a/lib/Service/UnifiedAccount.php b/lib/Service/UnifiedAccount.php index fd1c51a1ed..5afe776bab 100644 --- a/lib/Service/UnifiedAccount.php +++ b/lib/Service/UnifiedAccount.php @@ -21,8 +21,11 @@ */ namespace OCA\Mail\Service; -use OCP\IL10N; +use Exception; +use Horde_Mail_Rfc822_List; +use OCA\Mail\Exception\ServiceException; use OCA\Mail\Model\IMessage; +use OCP\IL10N; class UnifiedAccount implements IAccount { @@ -126,7 +129,7 @@ public function getMailbox($folderId) { public function getEmail() { if ($this->email === null) { $allAccounts = $this->accountService->findByUserId($this->userId); - $addressesList = new \Horde_Mail_Rfc822_List(); + $addressesList = new Horde_Mail_Rfc822_List(); foreach ($allAccounts as $account) { $inbox = $account->getInbox(); if (is_null($inbox)) { @@ -169,6 +172,10 @@ public function deleteMessage($folderId, $messageId) { $account->deleteMessage($inbox->getFolderId(), $messageId); } + public function moveMessage($sourceFolderId, $messageId, $destFolderId) { + throw new ServiceException('Not implemented'); + } + /** * @param string[] $query * @return array diff --git a/lib/exception/serviceexception.php b/lib/exception/serviceexception.php new file mode 100644 index 0000000000..c7e09a9136 --- /dev/null +++ b/lib/exception/serviceexception.php @@ -0,0 +1,28 @@ + + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Mail\Exception; + +use Exception; + +class ServiceException extends Exception { + +}