diff options
author | Christoph Wurst <christoph@winzerhof-wurst.at> | 2020-10-15 12:24:38 +0300 |
---|---|---|
committer | Christoph Wurst <christoph@winzerhof-wurst.at> | 2020-10-30 19:16:24 +0300 |
commit | 15867528b25cad3e1262014d0fa4f967a73d64be (patch) | |
tree | 6dd7c46876325acf8aad147b9ef791a73376c102 | |
parent | fe074001833362fa1eb4d18576131b0678907fb5 (diff) |
Store special mailboxes as preference of the account
… instead of using a fragile autodetection every time we need one of
those. We will still try to auto-detect the mailboxes but the users will
have to option to change the destinations.
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
49 files changed, 1133 insertions, 857 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index 74906b801..d5beca95c 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -12,7 +12,7 @@ - **🙈 We’re not reinventing the wheel!** Based on the great [Horde](http://horde.org) libraries. - **📬 Want to host your own mail server?** We don’t have to reimplement this as you could set up [Mail-in-a-Box](https://mailinabox.email)! ]]></description> - <version>1.5.0</version> + <version>1.6.0-alpha.1</version> <licence>agpl</licence> <author>Christoph Wurst</author> <author>Greta Doçi</author> diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index a9eca0249..bf6edfdb7 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -33,27 +33,25 @@ use OCA\Mail\Contracts\IMailSearch; use OCA\Mail\Contracts\IMailTransmission; use OCA\Mail\Contracts\IUserPreferences; use OCA\Mail\Dashboard\MailWidget; -use OCA\Mail\Events\BeforeMessageDeletedEvent; use OCA\Mail\Events\DraftSavedEvent; +use OCA\Mail\Events\MailboxesSynchronizedEvent; use OCA\Mail\Events\SynchronizationEvent; use OCA\Mail\Events\MessageDeletedEvent; use OCA\Mail\Events\MessageFlaggedEvent; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\Events\NewMessagesSynchronized; -use OCA\Mail\Events\SaveDraftEvent; use OCA\Mail\HordeTranslationHandler; use OCA\Mail\Http\Middleware\ErrorMiddleware; use OCA\Mail\Http\Middleware\ProvisioningMiddleware; use OCA\Mail\Listener\AddressCollectionListener; use OCA\Mail\Listener\DeleteDraftListener; -use OCA\Mail\Listener\DraftMailboxCreatorListener; use OCA\Mail\Listener\FlagRepliedMessageListener; use OCA\Mail\Listener\InteractionListener; use OCA\Mail\Listener\AccountSynchronizedThreadUpdaterListener; +use OCA\Mail\Listener\MailboxesSynchronizedSpecialMailboxesUpdater; use OCA\Mail\Listener\MessageCacheUpdaterListener; use OCA\Mail\Listener\NewMessageClassificationListener; use OCA\Mail\Listener\SaveSentMessageListener; -use OCA\Mail\Listener\TrashMailboxCreatorListener; use OCA\Mail\Listener\UserDeletedListener; use OCA\Mail\Search\Provider; use OCA\Mail\Service\Attachment\AttachmentService; @@ -99,8 +97,8 @@ class Application extends App implements IBootstrap { $context->registerServiceAlias(IMailTransmission::class, MailTransmission::class); $context->registerServiceAlias(IUserPreferences::class, UserPreferenceSevice::class); - $context->registerEventListener(BeforeMessageDeletedEvent::class, TrashMailboxCreatorListener::class); $context->registerEventListener(DraftSavedEvent::class, DeleteDraftListener::class); + $context->registerEventListener(MailboxesSynchronizedEvent::class, MailboxesSynchronizedSpecialMailboxesUpdater::class); $context->registerEventListener(MessageFlaggedEvent::class, MessageCacheUpdaterListener::class); $context->registerEventListener(MessageDeletedEvent::class, MessageCacheUpdaterListener::class); $context->registerEventListener(MessageSentEvent::class, AddressCollectionListener::class); @@ -109,7 +107,6 @@ class Application extends App implements IBootstrap { $context->registerEventListener(MessageSentEvent::class, InteractionListener::class); $context->registerEventListener(MessageSentEvent::class, SaveSentMessageListener::class); $context->registerEventListener(NewMessagesSynchronized::class, NewMessageClassificationListener::class); - $context->registerEventListener(SaveDraftEvent::class, DraftMailboxCreatorListener::class); $context->registerEventListener(SynchronizationEvent::class, AccountSynchronizedThreadUpdaterListener::class); $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index 02c3254cc..ca17c0edf 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -145,6 +145,7 @@ interface IMailManager { * @param string $mailboxId * @param int $messageId * + * @throws ClientException * @throws ServiceException */ public function deleteMessage(Account $account, string $mailboxId, int $messageId): void; diff --git a/lib/Contracts/IMailTransmission.php b/lib/Contracts/IMailTransmission.php index f58ae37d6..918556435 100644 --- a/lib/Contracts/IMailTransmission.php +++ b/lib/Contracts/IMailTransmission.php @@ -25,6 +25,8 @@ namespace OCA\Mail\Contracts; use OCA\Mail\Db\Alias; use OCA\Mail\Db\Message; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\SentMailboxNotSetException; use OCA\Mail\Exception\ServiceException; use OCA\Mail\Model\NewMessageData; use OCA\Mail\Model\RepliedMessageData; @@ -39,6 +41,7 @@ interface IMailTransmission { * @param Alias|null $alias * @param Message|null $draft * + * @throws SentMailboxNotSetException * @throws ServiceException */ public function sendMessage(NewMessageData $messageData, @@ -54,6 +57,7 @@ interface IMailTransmission { * * @return array * + * @throws ClientException if no drafts mailbox is configured * @throws ServiceException */ public function saveDraft(NewMessageData $message, Message $previousDraft = null): array; diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index 04e910743..3909c4ca5 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -234,7 +234,10 @@ class AccountsController extends Controller { public function patchAccount(int $id, string $editorMode = null, int $order = null, - bool $showSubscribedOnly = null): JSONResponse { + bool $showSubscribedOnly = null, + int $draftsMailboxId = null, + int $sentMailboxId = null, + int $trashMailboxId = null): JSONResponse { $account = $this->accountService->find($this->currentUserId, $id); $dbAccount = $account->getMailAccount(); @@ -247,6 +250,15 @@ class AccountsController extends Controller { if ($showSubscribedOnly !== null) { $dbAccount->setShowSubscribedOnly($showSubscribedOnly); } + if ($draftsMailboxId !== null) { + $dbAccount->setDraftsMailboxId($draftsMailboxId); + } + if ($sentMailboxId !== null) { + $dbAccount->setSentMailboxId($sentMailboxId); + } + if ($trashMailboxId !== null) { + $dbAccount->setTrashMailboxId($trashMailboxId); + } return new JSONResponse( $this->accountService->save($dbAccount)->toJson() ); @@ -452,7 +464,7 @@ class AccountsController extends Controller { return new JSONResponse([ 'id' => $this->mailManager->getMessageIdForUid($draftsMailbox, $newUID) ]); - } catch (ServiceException $ex) { + } catch (ClientException|ServiceException $ex) { $this->logger->error('Saving draft failed: ' . $ex->getMessage()); throw $ex; } diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index 80b554c46..f5b6a8a27 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -71,6 +71,12 @@ use OCP\AppFramework\Db\Entity; * @method void setShowSubscribedOnly(bool $showSubscribedOnly) * @method string|null getPersonalNamespace() * @method void setPersonalNamespace(string|null $personalNamespace) + * @method void setDraftsMailboxId(?int $id) + * @method int|null getDraftsMailboxId() + * @method void setSentMailboxId(?int $id) + * @method int|null getSentMailboxId() + * @method void setTrashMailboxId(?int $id) + * @method int|null getTrashMailboxId() */ class MailAccount extends Entity { protected $userId; @@ -94,6 +100,15 @@ class MailAccount extends Entity { protected $showSubscribedOnly; protected $personalNamespace; + /** @var int|null */ + protected $draftsMailboxId; + + /** @var int|null */ + protected $sentMailboxId; + + /** @var int|null */ + protected $trashMailboxId; + /** * @param array $params */ @@ -150,6 +165,9 @@ class MailAccount extends Entity { $this->addType('order', 'integer'); $this->addType('showSubscribedOnly', 'boolean'); $this->addType('personalNamespace', 'string'); + $this->addType('draftsMailboxId', 'integer'); + $this->addType('sentMailboxId', 'integer'); + $this->addType('trashMailboxId', 'integer'); } /** @@ -171,6 +189,9 @@ class MailAccount extends Entity { 'provisioned' => $this->getProvisioned(), 'showSubscribedOnly' => $this->getShowSubscribedOnly(), 'personalNamespace' => $this->getPersonalNamespace(), + 'draftsMailboxId' => $this->getDraftsMailboxId(), + 'sentMailboxId' => $this->getSentMailboxId(), + 'trashMailboxId' => $this->getTrashMailboxId(), ]; if (!is_null($this->getOutboundHost())) { diff --git a/lib/Db/MailboxMapper.php b/lib/Db/MailboxMapper.php index 13dd0a445..4d5ee15c2 100644 --- a/lib/Db/MailboxMapper.php +++ b/lib/Db/MailboxMapper.php @@ -142,32 +142,6 @@ class MailboxMapper extends QBMapper { } /** - * @throws DoesNotExistException - */ - public function findSpecial(Account $account, string $specialUse): Mailbox { - $mailboxes = $this->findAll($account); - - // First, let's try to detect by special use attribute - foreach ($mailboxes as $mailbox) { - $specialUses = json_decode($mailbox->getSpecialUse(), true) ?? []; - if (in_array($specialUse, $specialUses, true)) { - return $mailbox; - } - } - - // No luck so far, let's do another round and just guess - foreach ($mailboxes as $mailbox) { - // TODO: also check localized name - if (strtolower($mailbox->getName()) === strtolower($specialUse)) { - return $mailbox; - } - } - - // Give up - throw new DoesNotExistException("Special mailbox $specialUse does not exist"); - } - - /** * @throws MailboxLockedException */ private function lockForSync(Mailbox $mailbox, string $attr, ?int $lock): int { diff --git a/lib/Events/MailboxesSynchronizedEvent.php b/lib/Events/MailboxesSynchronizedEvent.php new file mode 100644 index 000000000..d6e971ad1 --- /dev/null +++ b/lib/Events/MailboxesSynchronizedEvent.php @@ -0,0 +1,47 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Events; + +use OCA\Mail\Account; +use OCP\EventDispatcher\Event; + +/** + * @psalm-immutable + */ +class MailboxesSynchronizedEvent extends Event { + + /** @var Account */ + private $account; + + public function __construct(Account $account) { + parent::__construct(); + $this->account = $account; + } + + public function getAccount(): Account { + return $this->account; + } +} diff --git a/lib/Events/MessageSentEvent.php b/lib/Events/MessageSentEvent.php index a2973cb8c..07bb965ff 100644 --- a/lib/Events/MessageSentEvent.php +++ b/lib/Events/MessageSentEvent.php @@ -33,6 +33,9 @@ use OCA\Mail\Model\NewMessageData; use OCA\Mail\Model\RepliedMessageData; use OCP\EventDispatcher\Event; +/** + * @psalm-immutable + */ class MessageSentEvent extends Event { /** @var Account */ diff --git a/lib/Exception/DraftsMailboxNotSetException.php b/lib/Exception/DraftsMailboxNotSetException.php new file mode 100644 index 000000000..209190b3d --- /dev/null +++ b/lib/Exception/DraftsMailboxNotSetException.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Exception; + +class DraftsMailboxNotSetException extends ClientException { + public function __construct() { + parent::__construct("No drafts mailbox configured"); + } +} diff --git a/lib/Exception/SentMailboxNotSetException.php b/lib/Exception/SentMailboxNotSetException.php new file mode 100644 index 000000000..8c572a59f --- /dev/null +++ b/lib/Exception/SentMailboxNotSetException.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Exception; + +class SentMailboxNotSetException extends ClientException { + public function __construct() { + parent::__construct("No sent mailbox configured"); + } +} diff --git a/lib/Exception/TrashMailboxNotSetException.php b/lib/Exception/TrashMailboxNotSetException.php new file mode 100644 index 000000000..db49c84e8 --- /dev/null +++ b/lib/Exception/TrashMailboxNotSetException.php @@ -0,0 +1,32 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Exception; + +class TrashMailboxNotSetException extends ClientException { + public function __construct() { + parent::__construct("No trash mailbox configured"); + } +} diff --git a/lib/IMAP/MailboxSync.php b/lib/IMAP/MailboxSync.php index 15df59453..343abfa56 100644 --- a/lib/IMAP/MailboxSync.php +++ b/lib/IMAP/MailboxSync.php @@ -29,7 +29,9 @@ use Horde_Imap_Client; use Horde_Imap_Client_Data_Namespace; use Horde_Imap_Client_Exception; use Horde_Imap_Client_Namespace_List; +use OCA\Mail\Events\MailboxesSynchronizedEvent; use OCA\Mail\Exception\ServiceException; +use OCP\EventDispatcher\IEventDispatcher; use Psr\Log\LoggerInterface; use function in_array; use function json_encode; @@ -57,16 +59,21 @@ class MailboxSync { /** @var ITimeFactory */ private $timeFactory; + /** @var IEventDispatcher */ + private $dispatcher; + public function __construct(MailboxMapper $mailboxMapper, FolderMapper $folderMapper, MailAccountMapper $mailAccountMapper, IMAPClientFactory $imapClientFactory, - ITimeFactory $timeFactory) { + ITimeFactory $timeFactory, + IEventDispatcher $dispatcher) { $this->mailboxMapper = $mailboxMapper; $this->folderMapper = $folderMapper; $this->mailAccountMapper = $mailAccountMapper; $this->imapClientFactory = $imapClientFactory; $this->timeFactory = $timeFactory; + $this->dispatcher = $dispatcher; } /** @@ -98,7 +105,7 @@ class MailboxSync { } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( "IMAP error: " . $e->getMessage(), - (int) $e->getCode(), + (int)$e->getCode(), $e ); } @@ -113,6 +120,10 @@ class MailboxSync { ); $this->persist($account, $folders, $indexedOld); + + $this->dispatcher->dispatchTyped( + new MailboxesSynchronizedEvent($account) + ); } private function persist(Account $account, array $folders, array $existing): void { diff --git a/lib/Listener/DeleteDraftListener.php b/lib/Listener/DeleteDraftListener.php index 3f701aa8f..87f46b47a 100644 --- a/lib/Listener/DeleteDraftListener.php +++ b/lib/Listener/DeleteDraftListener.php @@ -34,7 +34,6 @@ use OCA\Mail\Db\Message; use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; use OCA\Mail\IMAP\MessageMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\EventDispatcher\Event; @@ -52,21 +51,16 @@ class DeleteDraftListener implements IEventListener { /** @var MessageMapper */ private $messageMapper; - /** @var MailboxSync */ - private $mailboxSync; - /** @var LoggerInterface */ private $logger; public function __construct(IMAPClientFactory $imapClientFactory, MailboxMapper $mailboxMapper, MessageMapper $messageMapper, - MailboxSync $mailboxSync, LoggerInterface $logger) { $this->imapClientFactory = $imapClientFactory; $this->mailboxMapper = $mailboxMapper; $this->messageMapper = $messageMapper; - $this->mailboxSync = $mailboxSync; $this->logger = $logger; } @@ -84,7 +78,12 @@ class DeleteDraftListener implements IEventListener { */ private function deleteDraft(Account $account, Message $draft): void { $client = $this->imapClientFactory->getClient($account); - $draftsMailbox = $this->getDraftsMailbox($account); + try { + $draftsMailbox = $this->getDraftsMailbox($account); + } catch (DoesNotExistException $e) { + $this->logger->warning("Account has no draft mailbox set, can't delete the draft"); + return; + } try { $this->messageMapper->addFlag( @@ -108,38 +107,14 @@ class DeleteDraftListener implements IEventListener { } } + /** + * @throws DoesNotExistException + */ private function getDraftsMailbox(Account $account): Mailbox { - try { - return $this->mailboxMapper->findSpecial($account, 'drafts'); - } catch (DoesNotExistException $e) { - $this->logger->debug('Creating drafts mailbox'); - $this->createDraftsMailbox($account); - $this->logger->debug('Drafts mailbox created'); - } - - return $this->mailboxMapper->findSpecial($account, 'drafts'); - } - - private function createDraftsMailbox(Account $account): void { - $client = $this->imapClientFactory->getClient($account); - - try { - // TODO: localize mailbox name - $client->createMailbox( - 'Drafts', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_DRAFTS, - ], - ] - ); - } catch (Horde_Imap_Client_Exception $e) { - $this->logger->error('Could not create drafts mailbox', [ - 'exception' => $e, - ]); + $draftsMailboxId = $account->getMailAccount()->getDraftsMailboxId(); + if ($draftsMailboxId === null) { + throw new DoesNotExistException("No drafts mailbox ID set"); } - - // TODO: find a more elegant solution for updating the mailbox cache - $this->mailboxSync->sync($account, $this->logger,true); + return $this->mailboxMapper->findById($draftsMailboxId); } } diff --git a/lib/Listener/DraftMailboxCreatorListener.php b/lib/Listener/DraftMailboxCreatorListener.php deleted file mode 100644 index f4d915e81..000000000 --- a/lib/Listener/DraftMailboxCreatorListener.php +++ /dev/null @@ -1,99 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Listener; - -use Horde_Imap_Client_Exception; -use OCA\Mail\Account; -use OCA\Mail\Db\MailboxMapper; -use OCA\Mail\Events\SaveDraftEvent; -use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventListener; -use Psr\Log\LoggerInterface; - -class DraftMailboxCreatorListener implements IEventListener { - - /** @var MailboxMapper */ - private $mailboxMapper; - - /** @var IMAPClientFactory */ - private $imapClientFactory; - - /** @var MailboxSync */ - private $mailboxSync; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(MailboxMapper $mailboxMapper, - IMAPClientFactory $imapClientFactory, - MailboxSync $mailboxSync, - LoggerInterface $logger) { - $this->mailboxMapper = $mailboxMapper; - $this->imapClientFactory = $imapClientFactory; - $this->mailboxSync = $mailboxSync; - $this->logger = $logger; - } - - public function handle(Event $event): void { - if (!($event instanceof SaveDraftEvent)) { - return; - } - - try { - $this->mailboxMapper->findSpecial($event->getAccount(), 'drafts'); - } catch (DoesNotExistException $e) { - $this->logger->debug('Creating drafts mailbox'); - $this->createDraftsMailbox($event->getAccount()); - $this->logger->debug('Drafts mailbox created'); - } - } - - private function createDraftsMailbox(Account $account): void { - $client = $this->imapClientFactory->getClient($account); - - try { - // TODO: localize mailbox name - $client->createMailbox( - 'Drafts', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_DRAFTS, - ], - ] - ); - } catch (Horde_Imap_Client_Exception $e) { - $this->logger->error('Could not create drafts mailbox', [ - 'exception' => $e, - ]); - } - - // TODO: find a more elegant solution for updating the mailbox cache - $this->mailboxSync->sync($account, $this->logger,true); - } -} diff --git a/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php b/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php new file mode 100644 index 000000000..32c009061 --- /dev/null +++ b/lib/Listener/MailboxesSynchronizedSpecialMailboxesUpdater.php @@ -0,0 +1,122 @@ +<?php + +declare(strict_types=1); + +/* + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Mail\Listener; + +use OCA\Mail\Db\MailAccountMapper; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Events\MailboxesSynchronizedEvent; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; +use function array_combine; +use function array_key_exists; +use function array_map; +use function in_array; +use function json_decode; +use function strtolower; + +class MailboxesSynchronizedSpecialMailboxesUpdater implements IEventListener { + + /** @var MailAccountMapper */ + private $mailAccountMapper; + + /** @var MailboxMapper */ + private $mailboxMapper; + + /** @var LoggerInterface */ + private $logger; + + public function __construct(MailAccountMapper $mailAccountMapper, + MailboxMapper $mailboxMapper, + LoggerInterface $logger) { + $this->mailAccountMapper = $mailAccountMapper; + $this->mailboxMapper = $mailboxMapper; + $this->logger = $logger; + } + + /** + * @param Event $event + */ + public function handle(Event $event): void { + /** @var MailboxesSynchronizedEvent $event */ + $account = $event->getAccount(); + $mailAccount = $account->getMailAccount(); + $mailboxes = $this->indexMailboxes( + $this->mailboxMapper->findAll($account) + ); + + if ($mailAccount->getDraftsMailboxId() === null || !array_key_exists($mailAccount->getDraftsMailboxId(), $mailboxes)) { + $draftsMailbox = $this->findSpecial($mailboxes, 'drafts'); + $mailAccount->setDraftsMailboxId($draftsMailbox->getId()); + } + if ($mailAccount->getSentMailboxId() === null || !array_key_exists($mailAccount->getSentMailboxId(), $mailboxes)) { + $sentMailbox = $this->findSpecial($mailboxes, 'sent'); + $mailAccount->setSentMailboxId($sentMailbox->getId()); + } + if ($mailAccount->getTrashMailboxId() === null || !array_key_exists($mailAccount->getTrashMailboxId(), $mailboxes)) { + $trashMailbox = $this->findSpecial($mailboxes, 'trash'); + $mailAccount->setTrashMailboxId($trashMailbox->getId()); + } + + $this->mailAccountMapper->update($mailAccount); + } + + private function indexMailboxes(array $mailboxes): array { + return array_combine( + array_map(function (Mailbox $mb): int { + return $mb->getId(); + }, $mailboxes), + $mailboxes + ); + } + + /** + * @param Mailbox[] $mailboxes + * @throws DoesNotExistException + */ + private function findSpecial(array $mailboxes, string $specialUse): Mailbox { + // First, let's try to detect by special use attribute + foreach ($mailboxes as $mailbox) { + $specialUses = json_decode($mailbox->getSpecialUse(), true) ?? []; + if (in_array($specialUse, $specialUses, true)) { + return $mailbox; + } + } + + // No luck so far, let's do another round and just guess + foreach ($mailboxes as $mailbox) { + // TODO: also check localized name + if (strtolower($mailbox->getName()) === strtolower($specialUse)) { + return $mailbox; + } + } + + // Give up + throw new DoesNotExistException("Special mailbox $specialUse does not exist"); + } +} diff --git a/lib/Listener/SaveSentMessageListener.php b/lib/Listener/SaveSentMessageListener.php index aa43eb6fc..37eedd93c 100644 --- a/lib/Listener/SaveSentMessageListener.php +++ b/lib/Listener/SaveSentMessageListener.php @@ -26,13 +26,10 @@ declare(strict_types=1); namespace OCA\Mail\Listener; use Horde_Imap_Client_Exception; -use OCA\Mail\Account; -use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\Exception\ServiceException; use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; use OCA\Mail\IMAP\MessageMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\EventDispatcher\Event; @@ -50,21 +47,16 @@ class SaveSentMessageListener implements IEventListener { /** @var MessageMapper */ private $messageMapper; - /** @var MailboxSync */ - private $mailboxSync; - /** @var LoggerInterface */ private $logger; public function __construct(MailboxMapper $mailboxMapper, IMAPClientFactory $imapClientFactory, MessageMapper $messageMapper, - MailboxSync $mailboxSync, LoggerInterface $logger) { $this->mailboxMapper = $mailboxMapper; $this->imapClientFactory = $imapClientFactory; $this->messageMapper = $messageMapper; - $this->mailboxSync = $mailboxSync; $this->logger = $logger; } @@ -73,13 +65,22 @@ class SaveSentMessageListener implements IEventListener { return; } + $sentMailboxId = $event->getAccount()->getMailAccount()->getSentMailboxId(); + if ($sentMailboxId === null) { + $this->logger->warning("No sent mailbox exists, can't save sent message"); + return; + } + // Save the message in the sent mailbox try { - $sentMailbox = $this->mailboxMapper->findSpecial($event->getAccount(), 'sent'); + $sentMailbox = $this->mailboxMapper->findById( + $sentMailboxId + ); } catch (DoesNotExistException $e) { - $this->logger->debug('creating sent mailbox'); - $sentMailbox = $this->createSentMailbox($event->getAccount()); - $this->logger->debug('sent mailbox created'); + $this->logger->error("Sent mailbox could not be found", [ + 'exception' => $e, + ]); + return; } try { @@ -92,35 +93,4 @@ class SaveSentMessageListener implements IEventListener { throw new ServiceException('Could not save sent message on IMAP', 0, $e); } } - - /** - * @throws DoesNotExistException - * @throws ServiceException - */ - private function createSentMailbox(Account $account): Mailbox { - $client = $this->imapClientFactory->getClient($account); - - try { - // TODO: localize mailbox name - $client->createMailbox( - 'Sent', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_SENT, - ], - ] - ); - } catch (Horde_Imap_Client_Exception $e) { - // Let's assume this error is caused because the mailbox already exists, - // caused by concurrent requests or out-of-sync mailbox cache - $this->logger->warning('Could not create sent mailbox: ' . $e->getMessage(), [ - 'exception' => $e, - ]); - } - - // TODO: find a more elegant solution for updating the mailbox cache - $this->mailboxSync->sync($account, $this->logger,true); - - return $this->mailboxMapper->findSpecial($account, 'sent'); - } } diff --git a/lib/Listener/TrashMailboxCreatorListener.php b/lib/Listener/TrashMailboxCreatorListener.php deleted file mode 100644 index 3a57e2b4d..000000000 --- a/lib/Listener/TrashMailboxCreatorListener.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Listener; - -use Horde_Imap_Client_Exception; -use OCA\Mail\Account; -use OCA\Mail\Db\MailboxMapper; -use OCA\Mail\Events\BeforeMessageDeletedEvent; -use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\EventDispatcher\Event; -use OCP\EventDispatcher\IEventListener; -use Psr\Log\LoggerInterface; - -class TrashMailboxCreatorListener implements IEventListener { - - /** @var MailboxMapper */ - private $mailboxMapper; - - /** @var IMAPClientFactory */ - private $imapClientFactory; - - /** @var MailboxSync */ - private $mailboxSync; - - /** @var LoggerInterface */ - private $logger; - - public function __construct(MailboxMapper $mailboxMapper, - IMAPClientFactory $imapClientFactory, - MailboxSync $mailboxSync, - LoggerInterface $logger) { - $this->mailboxMapper = $mailboxMapper; - $this->imapClientFactory = $imapClientFactory; - $this->mailboxSync = $mailboxSync; - $this->logger = $logger; - } - - public function handle(Event $event): void { - if (!($event instanceof BeforeMessageDeletedEvent)) { - return; - } - - try { - $this->mailboxMapper->findSpecial( - $event->getAccount(), - 'trash' - ); - } catch (DoesNotExistException $e) { - $this->logger->debug("Creating trash mailbox"); - $this->createTrash($event->getAccount()); - $this->logger->debug("Trash mailbox created"); - } - } - - private function createTrash(Account $account): void { - $client = $this->imapClientFactory->getClient($account); - - try { - // TODO: localize mailbox name - $client->createMailbox( - 'Trash', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_TRASH, - ], - ] - ); - - // TODO: find a more elegant solution for updating the mailbox cache - $this->mailboxSync->sync($account, $this->logger,true); - } catch (Horde_Imap_Client_Exception $e) { - $this->logger->error('Could not creat trash mailbox', [ - 'exception' => $e, - ]); - } - } -} diff --git a/lib/Migration/Version1060Date20201015084952.php b/lib/Migration/Version1060Date20201015084952.php new file mode 100644 index 000000000..222ab62e5 --- /dev/null +++ b/lib/Migration/Version1060Date20201015084952.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); + +namespace OCA\Mail\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1060Date20201015084952 extends SimpleMigrationStep { + + /** @var IDBConnection */ + protected $connection; + + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $accountsTable = $schema->getTable('mail_accounts'); + $accountsTable->addColumn('drafts_mailbox_id', 'integer', [ + 'notnull' => false, + 'default' => null, + 'length' => 20, + ]); + $accountsTable->addColumn('sent_mailbox_id', 'integer', [ + 'notnull' => false, + 'default' => null, + 'length' => 20, + ]); + $accountsTable->addColumn('trash_mailbox_id', 'integer', [ + 'notnull' => false, + 'default' => null, + 'length' => 20, + ]); + + return $schema; + } + + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options) { + // Force a re-sync, so the values are propagated ASAP + $update = $this->connection->getQueryBuilder(); + $update->update('mail_accounts') + ->set('last_mailbox_sync', $update->createNamedParameter(0)); + $update->execute(); + } +} diff --git a/lib/Service/Classification/ImportanceClassifier.php b/lib/Service/Classification/ImportanceClassifier.php index f237e1f42..c414476e1 100644 --- a/lib/Service/Classification/ImportanceClassifier.php +++ b/lib/Service/Classification/ImportanceClassifier.php @@ -246,8 +246,13 @@ class ImportanceClassifier { */ private function getOutgoingMailboxes(Account $account): array { try { + $sentMailboxId = $account->getMailAccount()->getSentMailboxId(); + if ($sentMailboxId === null) { + return []; + } + return [ - $this->mailboxMapper->findSpecial($account, 'sent') + $this->mailboxMapper->findById($sentMailboxId) ]; } catch (DoesNotExistException $e) { return []; diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 777347a70..10e7cfc8e 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -37,6 +37,7 @@ use OCA\Mail\Events\MessageDeletedEvent; use OCA\Mail\Events\MessageFlaggedEvent; use OCA\Mail\Exception\ClientException; use OCA\Mail\Exception\ServiceException; +use OCA\Mail\Exception\TrashMailboxNotSetException; use OCA\Mail\Folder; use OCA\Mail\IMAP\FolderMapper; use OCA\Mail\IMAP\FolderStats; @@ -268,9 +269,9 @@ class MailManager implements IMailManager { } /** + * @throws ClientException * @throws ServiceException * @todo evaluate if we should sync mailboxes first - * */ public function deleteMessage(Account $account, string $mailboxId, @@ -286,7 +287,11 @@ class MailManager implements IMailManager { throw new ServiceException("Source mailbox $mailboxId does not exist", 0, $e); } try { - $trashMailbox = $this->mailboxMapper->findSpecial($account, 'trash'); + $trashMailboxId = $account->getMailAccount()->getTrashMailboxId(); + if ($trashMailboxId === null) { + throw new TrashMailboxNotSetException(); + } + $trashMailbox = $this->mailboxMapper->findById($trashMailboxId); } catch (DoesNotExistException $e) { throw new ServiceException("No trash folder", 0, $e); } diff --git a/lib/Service/MailTransmission.php b/lib/Service/MailTransmission.php index d2bb318a1..8a82de861 100644 --- a/lib/Service/MailTransmission.php +++ b/lib/Service/MailTransmission.php @@ -42,6 +42,8 @@ use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\Events\SaveDraftEvent; use OCA\Mail\Exception\AttachmentNotFoundException; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\SentMailboxNotSetException; use OCA\Mail\Exception\ServiceException; use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\IMAP\MessageMapper; @@ -107,6 +109,9 @@ class MailTransmission implements IMailTransmission { Alias $alias = null, Message $draft = null): void { $account = $messageData->getAccount(); + if ($account->getMailAccount()->getSentMailboxId() === null) { + throw new SentMailboxNotSetException(); + } if ($replyData !== null) { $message = $this->buildReplyMessage($account, $messageData, $replyData); @@ -180,6 +185,7 @@ class MailTransmission implements IMailTransmission { * * @return array * + * @throws ClientException * @throws ServiceException */ public function saveDraft(NewMessageData $message, Message $previousDraft = null): array { @@ -225,7 +231,11 @@ class MailTransmission implements IMailTransmission { $mail->send($transport, false, false); // save the message in the drafts folder $client = $this->imapClientFactory->getClient($account); - $draftsMailbox = $this->mailboxMapper->findSpecial($account, 'drafts'); + $draftsMailboxId = $account->getMailAccount()->getDraftsMailboxId(); + if ($draftsMailboxId === null) { + throw new ClientException("No drafts mailbox configured"); + } + $draftsMailbox = $this->mailboxMapper->findById($draftsMailboxId); $newUid = $this->messageMapper->save( $client, $draftsMailbox, diff --git a/package-lock.json b/package-lock.json index 882c9c046..615e3e541 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3014,6 +3014,21 @@ "fastq": "^1.6.0" } }, + "@riophae/vue-treeselect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@riophae/vue-treeselect/-/vue-treeselect-0.4.0.tgz", + "integrity": "sha512-J4atYmBqXQmiPFK/0B5sXKjtnGc21mBJEiyKIDZwk0Q9XuynVFX6IJ4EpaLmUgL5Tve7HAS7wkiGGSti6Uaxcg==", + "requires": { + "@babel/runtime": "^7.3.1", + "babel-helper-vue-jsx-merge-props": "^2.0.3", + "easings-css": "^1.0.0", + "fuzzysearch": "^1.0.3", + "is-promise": "^2.1.0", + "lodash": "^4.0.0", + "material-colors": "^1.2.6", + "watch-size": "^2.0.0" + } + }, "@sinonjs/commons": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", @@ -4071,6 +4086,11 @@ "babel-types": "^6.24.1" } }, + "babel-helper-vue-jsx-merge-props": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", + "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==" + }, "babel-loader": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", @@ -6018,7 +6038,7 @@ }, "domelementtype": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "resolved": "http://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" }, "domexception": { @@ -6080,6 +6100,11 @@ "stream-shift": "^1.0.0" } }, + "easings-css": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/easings-css/-/easings-css-1.0.0.tgz", + "integrity": "sha512-7Uq7NdazNfVtr0RNmPAys8it0zKCuaqxJStYKEl72D3j4gbvXhhaM7iWNbqhA4C94ygCye6VuyhzBRQC4szeBg==" + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -7496,6 +7521,11 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "fuzzysearch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", + "integrity": "sha1-3/yA9tawQiPyImqnndGUIxCW0Ag=" + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -8356,6 +8386,11 @@ "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", "dev": true }, + "is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, "is-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", @@ -14497,6 +14532,11 @@ "xml-name-validator": "^3.0.0" } }, + "watch-size": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/watch-size/-/watch-size-2.0.0.tgz", + "integrity": "sha512-M92R89dNoTPWyCD+HuUEDdhaDnh9jxPGOwlDc0u51jAgmjUvzqaEMynXSr3BaWs+QdHYk4KzibPy1TFtjLmOZQ==" + }, "watchpack": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.4.tgz", diff --git a/package.json b/package.json index c6a7a29a6..6d727db31 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@nextcloud/router": "^1.2.0", "@nextcloud/vue": "^2.8.1", "@nextcloud/vue-dashboard": "^1.0.1", + "@riophae/vue-treeselect": "^0.4.0", "@vue/babel-preset-app": "^4.5.8", "color-convert": "^2.0.1", "core-js": "^3.6.5", diff --git a/src/components/AccountDefaultsSettings.vue b/src/components/AccountDefaultsSettings.vue new file mode 100644 index 000000000..df69c9d23 --- /dev/null +++ b/src/components/AccountDefaultsSettings.vue @@ -0,0 +1,176 @@ +<!-- + - @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + - + - @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + - + - @license GNU AGPL version 3 or any later version + - + - This program is free software: you can redistribute it and/or modify + - it under the terms of the GNU Affero General Public License as + - published by the Free Software Foundation, either version 3 of the + - License, or (at your option) any later version. + - + - 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 + - along with this program. If not, see <http://www.gnu.org/licenses/>. + --> + +<template> + <div class="section"> + <h2>{{ t('mail', 'Defaults') }}</h2> + <p class="settings-hint"> + {{ + t('mail', 'Here you can select where Nextcloud Mail stores your drafts as well as sent and deleted messages.') + }} + </p> + <table> + <tr> + <td> + {{ t('mail', 'Draft messages are saved to:') }} + </td> + <td> + <MailboxInlinePicker v-model="draftsMailbox" :account="account" :disabled="saving" /> + </td> + </tr> + <tr> + <td> + {{ t('mail', 'Sent messages are saved to:') }} + </td> + <td> + <MailboxInlinePicker v-model="sentMailbox" :account="account" :disabled="saving" /> + </td> + </tr> + <tr> + <td> + {{ t('mail', 'Deleted messages are moved to:') }} + </td> + <td> + <MailboxInlinePicker v-model="trashMailbox" :account="account" :disabled="saving" /> + </td> + </tr> + </table> + </div> +</template> + +<script> +import logger from '../logger' +import MailboxInlinePicker from './MailboxInlinePicker' + +export default { + name: 'AccountDefaultsSettings', + components: { + MailboxInlinePicker, + }, + props: { + account: { + type: Object, + required: true, + }, + }, + data() { + return { + saving: false, + } + }, + computed: { + draftsMailbox: { + get() { + const mb = this.$store.getters.getMailbox(this.account.draftsMailboxId) + if (!mb) { + return + } + return mb.databaseId + }, + async set(draftsMailboxId) { + logger.debug('setting drafts mailbox to ' + draftsMailboxId) + this.saving = true + try { + await this.$store.dispatch('patchAccount', { + account: this.account, + data: { + draftsMailboxId, + }, + }) + } catch (error) { + logger.error('could not set drafts mailbox', { + error, + }) + } finally { + this.saving = false + } + }, + }, + sentMailbox: { + get() { + const mb = this.$store.getters.getMailbox(this.account.sentMailboxId) + if (!mb) { + return + } + return mb.databaseId + }, + async set(sentMailboxId) { + logger.debug('setting sent mailbox to ' + sentMailboxId) + this.saving = true + try { + await this.$store.dispatch('patchAccount', { + account: this.account, + data: { + sentMailboxId, + }, + }) + } catch (error) { + logger.error('could not set sent mailbox', { + error, + }) + } finally { + this.saving = false + } + }, + }, + trashMailbox: { + get() { + const mb = this.$store.getters.getMailbox(this.account.trashMailboxId) + if (!mb) { + return + } + return mb.databaseId + }, + async set(trashMailboxId) { + logger.debug('setting trash mailbox to ' + trashMailboxId) + this.saving = true + try { + await this.$store.dispatch('patchAccount', { + account: this.account, + data: { + trashMailboxId, + }, + }) + } catch (error) { + logger.error('could not set trash mailbox', { + error, + }) + } finally { + this.saving = false + } + }, + }, + }, +} +</script> + +<style lang="scss" scoped> +.button.icon-rename { + background-color: transparent; + border: none; + opacity: 0.3; + + &:hover, + &:focus { + opacity: 1; + } +} +</style> diff --git a/src/components/Composer.vue b/src/components/Composer.vue index d2b84b376..6736e7415 100644 --- a/src/components/Composer.vue +++ b/src/components/Composer.vue @@ -146,7 +146,8 @@ <ComposerAttachments v-model="attachments" :bus="bus" @upload="onAttachmentsUploading" /> <div class="composer-actions-right"> <p class="composer-actions-draft"> - <span v-if="savingDraft === true" id="draft-status">{{ t('mail', 'Saving draft …') }}</span> + <span v-if="!canSaveDraft" id="draft-status">{{ t('mail', 'Can not save draft because this account does not have a drafts mailbox configured.') }}</span> + <span v-else-if="savingDraft === true" id="draft-status">{{ t('mail', 'Saving draft …') }}</span> <span v-else-if="savingDraft === false" id="draft-status">{{ t('mail', 'Draft saved') }}</span> </p> <Actions> @@ -244,6 +245,11 @@ import { buildReplyBody } from '../ReplyBuilder' import MailvelopeEditor from './MailvelopeEditor' import { getMailvelope } from '../crypto/mailvelope' import { isPgpgMessage } from '../crypto/pgp' +import { matchError } from '../errors/match' +import NoSentMailboxConfiguredError + from '../errors/NoSentMailboxConfiguredError' +import NoDraftsMailboxConfiguredError + from '../errors/NoDraftsMailboxConfiguredError' const debouncedSearch = debouncePromise(findRecipient, 500) @@ -326,6 +332,7 @@ export default { noReply: this.to.some((to) => to.email.startsWith('noreply@') || to.email.startsWith('no-reply@')), draftsPromise: Promise.resolve(), attachmentsPromise: Promise.resolve(), + canSaveDraft: true, savingDraft: undefined, saveDraftDebounced: debounce(700, this.saveDraft), state: STATES.EDITING, @@ -534,7 +541,26 @@ export default { } return this.draft(draftData) }) - .catch(logger.error.bind(logger)) + .then((uid) => { + // It works (again) + this.canSaveDraft = true + + return uid + }) + .catch(async(error) => { + console.error('could not save draft', error) + const canSave = await matchError(error, { + [NoDraftsMailboxConfiguredError.getName()]() { + return false + }, + default() { + return true + }, + }) + if (!canSave) { + this.canSaveDraft = false + } + }) .then((uid) => { this.savingDraft = false return uid @@ -610,11 +636,18 @@ export default { .then((data) => this.send(data)) .then(() => logger.info('message sent')) .then(() => (this.state = STATES.FINISHED)) - .catch((error) => { + .catch(async(error) => { logger.error('could not send message', { error }) - if (error && error.toString) { - this.errorText = error.toString() - } + this.errorText = await matchError(error, { + [NoSentMailboxConfiguredError.getName()]() { + return t('mail', 'No sent mailbox configured. Please pick one in the account settings.') + }, + default(error) { + if (error && error.toString) { + return error.toString() + } + }, + }) this.state = STATES.ERROR }) }, diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index c3325c904..a8c5c4024 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -107,9 +107,14 @@ import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' import Moment from './Moment' import MoveModal from './MoveModal' import importantSvg from '../../img/important.svg' +import { showError } from '@nextcloud/dialogs' import Avatar from './Avatar' import { calculateAccountColor } from '../util/AccountColor' +import { matchError } from '../errors/match' +import NoTrashMailboxConfiguredError + from '../errors/NoTrashMailboxConfiguredError' +import logger from '../logger' export default { name: 'Envelope', @@ -230,15 +235,27 @@ export default { onToggleJunk() { this.$store.dispatch('toggleEnvelopeJunk', this.data) }, - onDelete() { + async onDelete() { // Remove from selection first this.setSelected(false) // Delete this.$emit('delete') - this.$store.dispatch('deleteMessage', { - id: this.data.databaseId, - }) + try { + await this.$store.dispatch('deleteMessage', { + id: this.data.databaseId, + }) + } catch (error) { + showError(await matchError(error, { + [NoTrashMailboxConfiguredError.getName()]() { + return t('mail', 'No trash mailbox configured') + }, + default(error) { + logger.error('could not delete message', error) + return t('mail', 'Could not delete message') + }, + })) + } }, onOpenMoveModal() { this.setSelected(false) diff --git a/src/components/EnvelopeList.vue b/src/components/EnvelopeList.vue index 6235b49b7..068524dda 100644 --- a/src/components/EnvelopeList.vue +++ b/src/components/EnvelopeList.vue @@ -115,10 +115,14 @@ <script> import Actions from '@nextcloud/vue/dist/Components/Actions' import ActionButton from '@nextcloud/vue/dist/Components/ActionButton' +import { showError } from '@nextcloud/dialogs' import Envelope from './Envelope' import logger from '../logger' import MoveModal from './MoveModal' +import { matchError } from '../errors/match' +import NoTrashMailboxConfiguredError + from '../errors/NoTrashMailboxConfiguredError' export default { name: 'EnvelopeList', @@ -213,7 +217,7 @@ export default { this.unselectAll() }, deleteAllSelected() { - this.selection.forEach((envelopeId) => { + this.selection.forEach(async(envelopeId) => { // Navigate if the message being deleted is the one currently viewed if (this.envelopes[envelopeId].databaseId === this.$route.params.threadId) { let next @@ -234,9 +238,21 @@ export default { } } logger.info(`deleting message ${this.envelopes[envelopeId].databaseId}`) - this.$store.dispatch('deleteMessage', { - id: this.envelopes[envelopeId].databaseId, - }) + try { + await this.$store.dispatch('deleteMessage', { + id: this.envelopes[envelopeId].databaseId, + }) + } catch (error) { + showError(await matchError(error, { + [NoTrashMailboxConfiguredError.getName()]() { + return t('mail', 'No trash mailbox configured') + }, + default(error) { + logger.error('could not delete message', error) + return t('mail', 'Could not delete message') + }, + })) + } }) this.unselectAll() }, diff --git a/src/components/Mailbox.vue b/src/components/Mailbox.vue index 042f6a3f6..2202bc076 100644 --- a/src/components/Mailbox.vue +++ b/src/components/Mailbox.vue @@ -54,6 +54,9 @@ import MailboxNotCachedError from '../errors/MailboxNotCachedError' import { matchError } from '../errors/match' import { wait } from '../util/wait' import EmptyMailboxSection from './EmptyMailboxSection' +import { showError } from '@nextcloud/dialogs' +import NoTrashMailboxConfiguredError + from '../errors/NoTrashMailboxConfiguredError' export default { name: 'Mailbox', @@ -272,7 +275,7 @@ export default { this.loadingMore = false } }, - handleShortcut(e) { + async handleShortcut(e) { const envelopes = this.envelopes const currentId = parseInt(this.$route.params.threadId, 10) @@ -318,16 +321,25 @@ export default { case 'del': logger.debug('deleting', { env }) this.onDelete(env.databaseId) - this.$store - .dispatch('deleteMessage', { + try { + await this.$store.dispatch('deleteMessage', { id: env.databaseId, }) - .catch((error) => - logger.error('could not delete envelope', { - env, - error, - }) - ) + } catch (error) { + logger.error('could not delete envelope', { + env, + error, + }) + + showError(await matchError(error, { + [NoTrashMailboxConfiguredError.getName()]() { + return t('mail', 'No trash mailbox configured') + }, + default() { + return t('mail', 'Could not delete message') + }, + })) + } break case 'flag': diff --git a/src/components/MailboxInlinePicker.vue b/src/components/MailboxInlinePicker.vue new file mode 100644 index 000000000..18ff042e7 --- /dev/null +++ b/src/components/MailboxInlinePicker.vue @@ -0,0 +1,85 @@ +<template> + <Treeselect + ref="Treeselect" + v-model="selected" + :options="mailboxes" + :multiple="false" + :clearable="false" + :disabled="disabled" /> +</template> +<script> +import Treeselect from '@riophae/vue-treeselect' +import '@riophae/vue-treeselect/dist/vue-treeselect.css' + +export default { + name: 'MailboxInlinePicker', + components: { + Treeselect, + }, + props: { + account: { + type: Object, + required: true, + }, + disabled: { + type: Boolean, + default: false, + }, + value: { + type: Number, + required: false, + }, + }, + data() { + return { + selected: this.value, + } + }, + computed: { + mailboxes() { + return this.getMailboxes() + }, + }, + watch: { + selected(val) { + if (val !== this.value) { + this.$emit('input', val) + this.selected = val + } + }, + }, + methods: { + getMailboxes(mailboxId) { + let mailboxes = [] + if (!mailboxId) { + mailboxes = this.$store.getters.getMailboxes(this.account.accountId) + } else { + mailboxes = this.$store.getters.getSubMailboxes(mailboxId) + } + return mailboxes.map((mailbox) => { + return { + id: mailbox.databaseId, + label: mailbox.displayName, + children: mailbox.mailboxes.length > 0 ? this.getMailboxes(mailbox.databaseId) : '', + } + }) + }, + }, +} +</script> +<style> +.vue-treeselect__control { + padding: 0; + border: 0; + width: 300px; +} +.vue-treeselect__control-arrow-container { + display: none; +} +.vue-treeselect--searchable .vue-treeselect__input-container { + padding-left: 0; +} +input.vue-treeselect__input { + margin: 0; +} +</style> diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index 61318f895..b8d1a98fb 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -41,7 +41,7 @@ v-show=" !group.isCollapsible || !group.account.collapsed || - SHOW_COLLAPSED.indexOf(item.specialRole) !== -1 + !isCollapsed(group.account, item) " :key="item.databaseId" :account="group.account" @@ -72,8 +72,10 @@ <script> import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation' import AppNavigationNew from '@nextcloud/vue/dist/Components/AppNavigationNew' -import AppNavigationSettings from '@nextcloud/vue/dist/Components/AppNavigationSettings' -import AppNavigationSpacer from '@nextcloud/vue/dist/Components/AppNavigationSpacer' +import AppNavigationSettings + from '@nextcloud/vue/dist/Components/AppNavigationSettings' +import AppNavigationSpacer + from '@nextcloud/vue/dist/Components/AppNavigationSpacer' import logger from '../logger' import NavigationAccount from './NavigationAccount' @@ -82,8 +84,6 @@ import NavigationMailbox from './NavigationMailbox' import AppSettingsMenu from '../components/AppSettingsMenu' -const SHOW_COLLAPSED = Object.seal(['inbox', 'flagged', 'drafts', 'sent']) - export default { name: 'Navigation', components: { @@ -96,17 +96,12 @@ export default { NavigationAccountExpandCollapse, NavigationMailbox, }, - data() { - return { - SHOW_COLLAPSED, - } - }, computed: { menu() { return this.$store.getters.accounts.map((account) => { const mailboxes = this.$store.getters.getMailboxes(account.id) const nonSpecialRoleMailboxes = mailboxes.filter( - (mailbox) => SHOW_COLLAPSED.indexOf(mailbox.specialRole) === -1 + (mailbox) => this.isCollapsed(account, mailbox) ) const isCollapsible = nonSpecialRoleMailboxes.length > 1 @@ -120,6 +115,21 @@ export default { }, }, methods: { + isCollapsed(account, mailbox) { + if (mailbox.specialRole === 'inbox') { + // INBOX is always visible + return false + } + + if (mailbox.databaseId === account.draftsMailboxId + || mailbox.databaseId === account.sentMailboxId + || mailbox.databaseId === account.trashMailboxId) { + // Special folders are always visible + return false + } + + return true + }, onNewMessage() { const accountId = this.$route.params.accountId || this.$store.getters.accounts[0].id diff --git a/src/components/ThreadEnvelope.vue b/src/components/ThreadEnvelope.vue index aca573447..f7c10e9f3 100644 --- a/src/components/ThreadEnvelope.vue +++ b/src/components/ThreadEnvelope.vue @@ -164,6 +164,10 @@ import { generateUrl } from '@nextcloud/router' import Modal from '@nextcloud/vue/dist/Components/Modal' import { Base64 } from 'js-base64' import importantSvg from '../../img/important.svg' +import { matchError } from '../errors/match' +import { showError } from '@nextcloud/dialogs' +import NoTrashMailboxConfiguredError + from '../errors/NoTrashMailboxConfiguredError' export default { name: 'ThreadEnvelope', @@ -327,11 +331,23 @@ export default { onToggleJunk() { this.$store.dispatch('toggleEnvelopeJunk', this.envelope) }, - onDelete() { + async onDelete() { this.$emit('delete', this.envelope.databaseId) - this.$store.dispatch('deleteMessage', { - id: this.envelope.databaseId, - }) + try { + await this.$store.dispatch('deleteMessage', { + id: this.envelope.databaseId, + }) + } catch (error) { + showError(await matchError(error, { + [NoTrashMailboxConfiguredError.getName()]() { + return t('mail', 'No trash mailbox configured') + }, + default(error) { + logger.error('could not delete message', error) + return t('mail', 'Could not delete message') + }, + })) + } }, async onShowSource() { this.sourceLoading = true diff --git a/src/errors/NoDraftsMailboxConfiguredError.js b/src/errors/NoDraftsMailboxConfiguredError.js new file mode 100644 index 000000000..97e99a2eb --- /dev/null +++ b/src/errors/NoDraftsMailboxConfiguredError.js @@ -0,0 +1,34 @@ +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +export default class NoDraftsMailboxConfiguredError extends Error { + + constructor(message) { + super(message) + this.name = NoDraftsMailboxConfiguredError.getName() + this.message = message + } + + static getName() { + return 'NoDraftsMailboxConfiguredError' + } + +} diff --git a/src/errors/NoSentMailboxConfiguredError.js b/src/errors/NoSentMailboxConfiguredError.js new file mode 100644 index 000000000..c6d1cc38b --- /dev/null +++ b/src/errors/NoSentMailboxConfiguredError.js @@ -0,0 +1,34 @@ +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +export default class NoSentMailboxConfiguredError extends Error { + + constructor(message) { + super(message) + this.name = NoSentMailboxConfiguredError.getName() + this.message = message + } + + static getName() { + return 'NoSentMailboxConfiguredError' + } + +} diff --git a/src/errors/NoTrashMailboxConfiguredError.js b/src/errors/NoTrashMailboxConfiguredError.js new file mode 100644 index 000000000..fd81c23cb --- /dev/null +++ b/src/errors/NoTrashMailboxConfiguredError.js @@ -0,0 +1,34 @@ +/** + * @copyright 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @author 2020 Christoph Wurst <christoph@winzerhof-wurst.at> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +export default class NoTrashMailboxConfiguredError extends Error { + + constructor(message) { + super(message) + this.name = NoTrashMailboxConfiguredError.getName() + this.message = message + } + + static getName() { + return 'NoTrashMailboxConfiguredError' + } + +} diff --git a/src/errors/convert.js b/src/errors/convert.js index e8bb40190..a860999a3 100644 --- a/src/errors/convert.js +++ b/src/errors/convert.js @@ -21,10 +21,16 @@ import MailboxLockedError from './MailboxLockedError' import MailboxNotCachedError from './MailboxNotCachedError' +import NoDraftsMailboxConfiguredError from './NoDraftsMailboxConfiguredError' +import NoSentMailboxConfiguredError from './NoSentMailboxConfiguredError' +import NoTrashMailboxConfiguredError from './NoTrashMailboxConfiguredError' const map = { + 'OCA\\Mail\\Exception\\DraftsMailboxNotSetException': NoDraftsMailboxConfiguredError, 'OCA\\Mail\\Exception\\MailboxLockedException': MailboxLockedError, 'OCA\\Mail\\Exception\\MailboxNotCachedException': MailboxNotCachedError, + 'OCA\\Mail\\Exception\\SentMailboxNotSetException': NoSentMailboxConfiguredError, + 'OCA\\Mail\\Exception\\TrashMailboxNotSetException': NoTrashMailboxConfiguredError, } /** diff --git a/src/main.js b/src/main.js index 5db7da7ce..005c2e161 100644 --- a/src/main.js +++ b/src/main.js @@ -24,6 +24,7 @@ import Vue from 'vue' import { getRequestToken } from '@nextcloud/auth' import { sync } from 'vuex-router-sync' import { generateFilePath } from '@nextcloud/router' +import '@nextcloud/dialogs/styles/toast.scss' import VueShortKey from 'vue-shortkey' import VTooltip from 'v-tooltip' diff --git a/src/service/MessageService.js b/src/service/MessageService.js index 541f30462..d1fc0f964 100644 --- a/src/service/MessageService.js +++ b/src/service/MessageService.js @@ -138,23 +138,36 @@ export async function saveDraft(accountId, data) { accountId, }) - return (await axios.post(url, data)).data + try { + return (await axios.post(url, data)).data + } catch (e) { + throw convertAxiosError(e) + } } -export function sendMessage(accountId, data) { +export async function sendMessage(accountId, data) { const url = generateUrl('/apps/mail/api/accounts/{accountId}/send', { accountId, }) - return axios.post(url, data).then((resp) => resp.data) + try { + const resp = await axios.post(url, data) + return resp.data + } catch (e) { + throw convertAxiosError(e) + } } -export function deleteMessage(id) { +export async function deleteMessage(id) { const url = generateUrl('/apps/mail/api/messages/{id}', { id, }) - return axios.delete(url).then((resp) => resp.data) + try { + return (await axios.delete(url)).data + } catch (e) { + throw convertAxiosError(e) + } } export function moveMessage(id, destFolderId) { diff --git a/src/views/AccountSettings.vue b/src/views/AccountSettings.vue index b51a31bc6..2b9e815ff 100644 --- a/src/views/AccountSettings.vue +++ b/src/views/AccountSettings.vue @@ -16,6 +16,7 @@ </div> <SignatureSettings :account="account" /> <EditorSettings :account="account" /> + <AccountDefaultsSettings :account="account" /> <div v-if="!account.provisioned" class="section"> <h2>{{ t('mail', 'Mail server') }}</h2> <div id="mail-settings"> @@ -35,16 +36,18 @@ import AppContent from '@nextcloud/vue/dist/Components/AppContent' import Content from '@nextcloud/vue/dist/Components/Content' +import AccountDefaultsSettings from '../components/AccountDefaultsSettings' import AccountForm from '../components/AccountForm' +import AliasSettings from '../components/AliasSettings' import EditorSettings from '../components/EditorSettings' import Logger from '../logger' import Navigation from '../components/Navigation' import SignatureSettings from '../components/SignatureSettings' -import AliasSettings from '../components/AliasSettings' export default { name: 'AccountSettings', components: { + AccountDefaultsSettings, AccountForm, AliasSettings, AppContent, diff --git a/tests/Integration/Db/MailAccountTest.php b/tests/Integration/Db/MailAccountTest.php index c05f4c342..8bf2af949 100644 --- a/tests/Integration/Db/MailAccountTest.php +++ b/tests/Integration/Db/MailAccountTest.php @@ -66,6 +66,9 @@ class MailAccountTest extends TestCase { 'order' => 13, 'showSubscribedOnly' => null, 'personalNamespace' => null, + 'draftsMailboxId' => null, + 'sentMailboxId' => null, + 'trashMailboxId' => null, ], $a->toJson()); } @@ -89,6 +92,9 @@ class MailAccountTest extends TestCase { 'order' => null, 'showSubscribedOnly' => null, 'personalNamespace' => null, + 'draftsMailboxId' => null, + 'sentMailboxId' => null, + 'trashMailboxId' => null, ]; $a = new MailAccount($expected); // TODO: fix inconsistency diff --git a/tests/Integration/Db/MailboxMapperTest.php b/tests/Integration/Db/MailboxMapperTest.php index d086d993a..9c67c0664 100644 --- a/tests/Integration/Db/MailboxMapperTest.php +++ b/tests/Integration/Db/MailboxMapperTest.php @@ -129,38 +129,4 @@ class MailboxMapperTest extends TestCase { $this->assertSame('INBOX', $result->getName()); } - - public function testNoTrashFound() { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - $account->method('getId')->willReturn(13); - $this->expectException(DoesNotExistException::class); - - $this->mapper->findSpecial($account, 'trash'); - } - - public function testFindTrash() { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - $account->method('getId')->willReturn(13); - $qb = $this->db->getQueryBuilder(); - $insert = $qb->insert($this->mapper->getTableName()) - ->values([ - 'name' => $qb->createNamedParameter('Trash'), - 'account_id' => $qb->createNamedParameter(13, IQueryBuilder::PARAM_INT), - 'sync_new_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), - 'sync_changed_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), - 'sync_vanished_token' => $qb->createNamedParameter('VTEsVjE0Mjg1OTkxNDk='), - 'delimiter' => $qb->createNamedParameter('.'), - 'messages' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'unseen' => $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), - 'selectable' => $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL), - 'special_use' => $qb->createNamedParameter(json_encode(['trash'])), - ]); - $insert->execute(); - - $result = $this->mapper->findSpecial($account, 'trash'); - - $this->assertSame('Trash', $result->getName()); - } } diff --git a/tests/Integration/Service/MailTransmissionIntegrationTest.php b/tests/Integration/Service/MailTransmissionIntegrationTest.php index 097b20313..8540465dc 100644 --- a/tests/Integration/Service/MailTransmissionIntegrationTest.php +++ b/tests/Integration/Service/MailTransmissionIntegrationTest.php @@ -95,6 +95,11 @@ class MailTransmissionIntegrationTest extends TestCase { $this->attachmentService = OC::$server->query(IAttachmentService::class); $userFolder = OC::$server->getUserFolder($this->user->getUID()); + // Make sure the mailbox preferences are set + /** @var MailboxSync $mbSync */ + $mbSync = OC::$server->query(MailboxSync::class); + $mbSync->sync($this->account, new NullLogger(), true); + $this->transmission = new MailTransmission( $userFolder, $this->attachmentService, diff --git a/tests/Unit/IMAP/MailboxSyncTest.php b/tests/Unit/IMAP/MailboxSyncTest.php index 645c9b628..159edcee1 100644 --- a/tests/Unit/IMAP/MailboxSyncTest.php +++ b/tests/Unit/IMAP/MailboxSyncTest.php @@ -33,11 +33,13 @@ use OCA\Mail\Account; use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\MailAccountMapper; use OCA\Mail\Db\MailboxMapper; +use OCA\Mail\Events\MailboxesSynchronizedEvent; use OCA\Mail\Folder; use OCA\Mail\IMAP\FolderMapper; use OCA\Mail\IMAP\IMAPClientFactory; use OCA\Mail\IMAP\MailboxSync; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\NullLogger; @@ -61,6 +63,9 @@ class MailboxSyncTest extends TestCase { /** @var MailboxSync */ private $sync; + /** @var IEventDispatcher|MockObject */ + private $dispatcher; + protected function setUp(): void { parent::setUp(); @@ -69,13 +74,15 @@ class MailboxSyncTest extends TestCase { $this->mailAccountMapper = $this->createMock(MailAccountMapper::class); $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->dispatcher = $this->createMock(IEventDispatcher::class); $this->sync = new MailboxSync( $this->mailboxMapper, $this->folderMapper, $this->mailAccountMapper, $this->imapClientFactory, - $this->timeFactory + $this->timeFactory, + $this->dispatcher ); } @@ -87,6 +94,7 @@ class MailboxSyncTest extends TestCase { $this->timeFactory->method('getTime')->willReturn(100000); $this->imapClientFactory->expects($this->never()) ->method('getClient'); + $this->dispatcher->expects($this->never())->method('dispatchTyped'); $this->sync->sync($account, new NullLogger()); } @@ -116,6 +124,10 @@ class MailboxSyncTest extends TestCase { $this->folderMapper->expects($this->once()) ->method('detectFolderSpecialUse') ->with($folders); + $this->dispatcher + ->expects($this->once()) + ->method('dispatchTyped') + ->with($this->equalTo(new MailboxesSynchronizedEvent($account))); $this->sync->sync($account, new NullLogger()); } diff --git a/tests/Unit/Listener/DeleteDraftListenerTest.php b/tests/Unit/Listener/DeleteDraftListenerTest.php index f262120a2..774c42588 100644 --- a/tests/Unit/Listener/DeleteDraftListenerTest.php +++ b/tests/Unit/Listener/DeleteDraftListenerTest.php @@ -27,13 +27,13 @@ namespace OCA\Mail\Tests\Unit\Listener; use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Account; +use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\Message; use OCA\Mail\Events\DraftSavedEvent; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; use OCA\Mail\IMAP\MessageMapper; use OCA\Mail\Listener\DeleteDraftListener; use OCA\Mail\Model\IMessage; @@ -56,9 +56,6 @@ class DeleteDraftListenerTest extends TestCase { /** @var MessageMapper|MockObject */ private $messageMapper; - /** @var MailboxSync|MockObject */ - private $mailboxSync; - /** @var LoggerInterface|MockObject */ private $logger; @@ -71,14 +68,12 @@ class DeleteDraftListenerTest extends TestCase { $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); $this->mailboxMapper = $this->createMock(MailboxMapper::class); $this->messageMapper = $this->createMock(MessageMapper::class); - $this->mailboxSync = $this->createMock(MailboxSync::class); $this->logger = $this->createMock(LoggerInterface::class); $this->listener = new DeleteDraftListener( $this->imapClientFactory, $this->mailboxMapper, $this->messageMapper, - $this->mailboxSync, $this->logger ); } @@ -109,9 +104,11 @@ class DeleteDraftListenerTest extends TestCase { $this->listener->handle($event); } - public function testHandleDraftSavedEventCreatesDraftsMailbox(): void { + public function testHandleDraftSavedEventNoDraftMailboxSet(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $account->method('getMailAccount')->willReturn($mailAccount); /** @var NewMessageData|MockObject $newMessageData */ $newMessageData = $this->createMock(NewMessageData::class); $draft = new Message(); @@ -130,39 +127,42 @@ class DeleteDraftListenerTest extends TestCase { ->willReturn($client); $mailbox = new Mailbox(); $mailbox->setName('Drafts'); - $this->mailboxMapper->expects($this->exactly(2)) - ->method('findSpecial') - ->with($account, 'drafts') - ->willReturnOnConsecutiveCalls( - $this->throwException(new DoesNotExistException('')), - $mailbox - ); - $client->expects($this->once()) - ->method('createMailbox') - ->with( - 'Drafts', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_DRAFTS, - ], - ] - ); - $this->mailboxSync->expects($this->once()) - ->method('sync') - ->with($account, $this->logger, true); - $this->messageMapper->expects($this->once()) - ->method('addFlag') - ->with( - $client, - $mailbox, - $uid, - \Horde_Imap_Client::FLAG_DELETED - ); - $client->expects($this->once()) - ->method('expunge') - ->with('Drafts'); - $this->logger->expects($this->never()) - ->method('error'); + $this->mailboxMapper->expects($this->never()) + ->method('findById'); + $this->logger->expects($this->once())->method('warning'); + + $this->listener->handle($event); + } + + public function testHandleDraftSavedEventDraftMailboxNotFound(): void { + /** @var Account|MockObject $account */ + $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setDraftsMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); + /** @var NewMessageData|MockObject $newMessageData */ + $newMessageData = $this->createMock(NewMessageData::class); + $draft = new Message(); + $uid = 123; + $draft->setUid($uid); + $event = new DraftSavedEvent( + $account, + $newMessageData, + $draft + ); + /** @var \Horde_Imap_Client_Socket|MockObject $client */ + $client = $this->createMock(\Horde_Imap_Client_Socket::class); + $this->imapClientFactory + ->method('getClient') + ->with($account) + ->willReturn($client); + $mailbox = new Mailbox(); + $mailbox->setName('Drafts'); + $this->mailboxMapper->expects($this->once()) + ->method('findById') + ->with(123) + ->willThrowException(new DoesNotExistException("")); + $this->logger->expects($this->once())->method('warning'); $this->listener->handle($event); } @@ -188,6 +188,9 @@ class DeleteDraftListenerTest extends TestCase { public function testHandleMessageSentEvent(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setDraftsMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); /** @var NewMessageData|MockObject $newMessageData */ $newMessageData = $this->createMock(NewMessageData::class); /** @var RepliedMessageData|MockObject $repliedMessageData */ @@ -216,8 +219,8 @@ class DeleteDraftListenerTest extends TestCase { $mailbox = new Mailbox(); $mailbox->setName('Drafts'); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'drafts') + ->method('findById') + ->with(123) ->willReturn($mailbox); $this->messageMapper->expects($this->once()) ->method('addFlag') diff --git a/tests/Unit/Listener/DraftMailboxCreatorListenerTest.php b/tests/Unit/Listener/DraftMailboxCreatorListenerTest.php deleted file mode 100644 index a7c1bb1dd..000000000 --- a/tests/Unit/Listener/DraftMailboxCreatorListenerTest.php +++ /dev/null @@ -1,156 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Tests\Unit\Listener; - -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Account; -use OCA\Mail\Db\Mailbox; -use OCA\Mail\Db\MailboxMapper; -use OCA\Mail\Db\Message; -use OCA\Mail\Events\SaveDraftEvent; -use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; -use OCA\Mail\Listener\DraftMailboxCreatorListener; -use OCA\Mail\Model\NewMessageData; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\EventDispatcher\Event; -use PHPUnit\Framework\MockObject\MockObject; -use Psr\Log\LoggerInterface; - -class DraftMailboxCreatorListenerTest extends TestCase { - - /** @var MailboxMapper|MockObject */ - private $mailboxMapper; - - /** @var IMAPClientFactory|MockObject */ - private $imapClientFactory; - - /** @var MailboxSync|MockObject */ - private $mailboxSync; - - /** @var LoggerInterface|MockObject */ - private $logger; - - /** @var DraftMailboxCreatorListener */ - private $listener; - - protected function setUp(): void { - parent::setUp(); - - $this->mailboxMapper = $this->createMock(MailboxMapper::class); - $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); - $this->mailboxSync = $this->createMock(MailboxSync::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->listener = new DraftMailboxCreatorListener( - $this->mailboxMapper, - $this->imapClientFactory, - $this->mailboxSync, - $this->logger - ); - } - - public function testHandleUnrelated() { - $event = new Event(); - - $this->listener->handle($event); - - $this->addToAssertionCount(1); - } - - public function testHandleSaveDraftEventMailboxExists(): void { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - /** @var NewMessageData|MockObject $newMessageData */ - $newMessageData = $this->createMock(NewMessageData::class); - $draft = new Message(); - $draft->setUid(123); - $event = new SaveDraftEvent( - $account, - $newMessageData, - $draft - ); - /** @var \Horde_Imap_Client_Socket|MockObject $client */ - $client = $this->createMock(\Horde_Imap_Client_Socket::class); - $this->imapClientFactory - ->method('getClient') - ->with($account) - ->willReturn($client); - $mailbox = new Mailbox(); - $mailbox->setName('Drafts'); - $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'drafts') - ->willReturn($mailbox); - $this->logger->expects($this->never()) - ->method('error'); - - $this->listener->handle($event); - } - - public function testHandleSaveDraftEventCreatesDraftsMailbox(): void { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - /** @var NewMessageData|MockObject $newMessageData */ - $newMessageData = $this->createMock(NewMessageData::class); - $draft = new Message(); - $draft->setUid(123); - $event = new SaveDraftEvent( - $account, - $newMessageData, - $draft - ); - /** @var \Horde_Imap_Client_Socket|MockObject $client */ - $client = $this->createMock(\Horde_Imap_Client_Socket::class); - $this->imapClientFactory - ->method('getClient') - ->with($account) - ->willReturn($client); - $mailbox = new Mailbox(); - $mailbox->setName('Drafts'); - $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'drafts') - ->willThrowException(new DoesNotExistException('')); - $client->expects($this->once()) - ->method('createMailbox') - ->with( - 'Drafts', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_DRAFTS, - ], - ] - ); - $this->mailboxSync->expects($this->once()) - ->method('sync') - ->with($account, $this->logger, true); - $this->logger->expects($this->never()) - ->method('error'); - - $this->listener->handle($event); - } -} diff --git a/tests/Unit/Listener/SaveSentMessageListenerTest.php b/tests/Unit/Listener/SaveSentMessageListenerTest.php index 35b627d43..e8c46e213 100644 --- a/tests/Unit/Listener/SaveSentMessageListenerTest.php +++ b/tests/Unit/Listener/SaveSentMessageListenerTest.php @@ -27,13 +27,13 @@ namespace OCA\Mail\Tests\Unit\Listener; use ChristophWurst\Nextcloud\Testing\TestCase; use OCA\Mail\Account; +use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\Message; use OCA\Mail\Events\MessageSentEvent; use OCA\Mail\Exception\ServiceException; use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; use OCA\Mail\IMAP\MessageMapper; use OCA\Mail\Listener\SaveSentMessageListener; use OCA\Mail\Model\IMessage; @@ -56,9 +56,6 @@ class SaveSentMessageListenerTest extends TestCase { /** @var MessageMapper|MockObject */ private $messageMapper; - /** @var MailboxSync|MockObject */ - private $mailboxSync; - /** @var LoggerInterface|MockObject */ private $logger; @@ -71,14 +68,12 @@ class SaveSentMessageListenerTest extends TestCase { $this->mailboxMapper = $this->createMock(MailboxMapper::class); $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); $this->messageMapper = $this->createMock(MessageMapper::class); - $this->mailboxSync = $this->createMock(MailboxSync::class); $this->logger = $this->createMock(LoggerInterface::class); $this->listener = new SaveSentMessageListener( $this->mailboxMapper, $this->imapClientFactory, $this->messageMapper, - $this->mailboxSync, $this->logger ); } @@ -91,9 +86,11 @@ class SaveSentMessageListenerTest extends TestCase { $this->addToAssertionCount(1); } - public function testHandleMessageSentMailboxDoesNotExistCantCreate(): void { + public function testHandleMessageSentMailboxNotSet(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $account->method('getMailAccount')->willReturn($mailAccount); /** @var NewMessageData|MockObject $newMessageData */ $newMessageData = $this->createMock(NewMessageData::class); /** @var RepliedMessageData|MockObject $repliedMessageData */ @@ -112,42 +109,10 @@ class SaveSentMessageListenerTest extends TestCase { $message, $mail ); - $mailbox = new Mailbox(); - $this->mailboxMapper->expects($this->exactly(2)) - ->method('findSpecial') - ->withConsecutive( - [$account, 'sent'], - [$account, 'sent'] - ) - ->willReturnOnConsecutiveCalls( - $this->throwException(new DoesNotExistException('')), - $mailbox - ); - $client = $this->createMock(\Horde_Imap_Client_Socket::class); - $this->imapClientFactory - ->method('getClient') - ->with($account) - ->willReturn($client); - $exception = new \Horde_Imap_Client_Exception(); - $client->expects($this->once()) - ->method('createMailbox') - ->with( - 'Sent', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_SENT, - ], - ] - ) - ->willThrowException($exception); + $this->mailboxMapper->expects($this->never()) + ->method('findById'); $this->logger->expects($this->once()) - ->method('warning') - ->with( - 'Could not create sent mailbox: ', - $this->equalTo([ - 'exception' => $exception, - ]) - ); + ->method('warning'); $this->listener->handle($event); } @@ -155,6 +120,9 @@ class SaveSentMessageListenerTest extends TestCase { public function testHandleMessageSentMailboxDoesNotExist(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setSentMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); /** @var NewMessageData|MockObject $newMessageData */ $newMessageData = $this->createMock(NewMessageData::class); /** @var RepliedMessageData|MockObject $repliedMessageData */ @@ -173,39 +141,14 @@ class SaveSentMessageListenerTest extends TestCase { $message, $mail ); - $mailbox = new Mailbox(); - $this->mailboxMapper->expects($this->exactly(2)) - ->method('findSpecial') - ->withConsecutive( - [$account, 'sent'], - [$account, 'sent'] - ) - ->willReturnOnConsecutiveCalls( - $this->throwException(new DoesNotExistException('')), - $mailbox - ); - $client = $this->createMock(\Horde_Imap_Client_Socket::class); - $this->imapClientFactory - ->method('getClient') - ->with($account) - ->willReturn($client); - $client->expects($this->once()) - ->method('createMailbox') - ->with( - 'Sent', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_SENT, - ], - ] - ); - $this->messageMapper->expects($this->once()) - ->method('save') - ->with( - $this->anything(), - $mailbox, - $mail - ); + $this->mailboxMapper->expects($this->once()) + ->method('findById') + ->with(123) + ->willThrowException(new DoesNotExistException('')); + $this->messageMapper->expects($this->never()) + ->method('save'); + $this->logger->expects($this->once()) + ->method('error'); $this->listener->handle($event); } @@ -213,6 +156,9 @@ class SaveSentMessageListenerTest extends TestCase { public function testHandleMessageSentSavingError(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setSentMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); /** @var NewMessageData|MockObject $newMessageData */ $newMessageData = $this->createMock(NewMessageData::class); /** @var RepliedMessageData|MockObject $repliedMessageData */ @@ -233,8 +179,8 @@ class SaveSentMessageListenerTest extends TestCase { ); $mailbox = new Mailbox(); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'sent') + ->method('findById') + ->with(123) ->willReturn($mailbox); $this->messageMapper->expects($this->once()) ->method('save') @@ -252,6 +198,9 @@ class SaveSentMessageListenerTest extends TestCase { public function testHandleMessageSent(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setSentMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); /** @var NewMessageData|MockObject $newMessageData */ $newMessageData = $this->createMock(NewMessageData::class); /** @var RepliedMessageData|MockObject $repliedMessageData */ @@ -272,8 +221,8 @@ class SaveSentMessageListenerTest extends TestCase { ); $mailbox = new Mailbox(); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'sent') + ->method('findById') + ->with(123) ->willReturn($mailbox); $this->messageMapper->expects($this->once()) ->method('save') @@ -282,6 +231,8 @@ class SaveSentMessageListenerTest extends TestCase { $mailbox, $mail ); + $this->logger->expects($this->never())->method('warning'); + $this->logger->expects($this->never())->method('error'); $this->listener->handle($event); } diff --git a/tests/Unit/Listener/TrashMailboxCreatorListenerTest.php b/tests/Unit/Listener/TrashMailboxCreatorListenerTest.php deleted file mode 100644 index b54d93fb4..000000000 --- a/tests/Unit/Listener/TrashMailboxCreatorListenerTest.php +++ /dev/null @@ -1,168 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ - -namespace OCA\Mail\Tests\Unit\Listener; - -use ChristophWurst\Nextcloud\Testing\TestCase; -use OCA\Mail\Account; -use OCA\Mail\Db\Mailbox; -use OCA\Mail\Db\MailboxMapper; -use OCA\Mail\Events\BeforeMessageDeletedEvent; -use OCA\Mail\IMAP\IMAPClientFactory; -use OCA\Mail\IMAP\MailboxSync; -use OCA\Mail\Listener\TrashMailboxCreatorListener; -use OCP\AppFramework\Db\DoesNotExistException; -use OCP\EventDispatcher\Event; -use PHPUnit\Framework\MockObject\MockObject; -use Psr\Log\LoggerInterface; - -class TrashMailboxCreatorListenerTest extends TestCase { - - /** @var MailboxMapper|MockObject */ - private $mailboxMapper; - - /** @var IMAPClientFactory|MockObject */ - private $imapClientFactory; - - /** @var MailboxSync|MockObject */ - private $mailboxSync; - - /** @var LoggerInterface|MockObject */ - private $logger; - - /** @var TrashMailboxCreatorListener */ - private $listener; - - protected function setUp(): void { - parent::setUp(); - - $this->mailboxMapper = $this->createMock(MailboxMapper::class); - $this->imapClientFactory = $this->createMock(IMAPClientFactory::class); - $this->mailboxSync = $this->createMock(MailboxSync::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->listener = new TrashMailboxCreatorListener( - $this->mailboxMapper, - $this->imapClientFactory, - $this->mailboxSync, - $this->logger - ); - } - - public function testHandleUnrelated(): void { - $event = new Event(); - - $this->listener->handle($event); - - $this->addToAssertionCount(1); - } - - public function testHandleDoesAlreadyExist(): void { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - $event = new BeforeMessageDeletedEvent( - $account, - 'INBOX', - 123 - ); - $mailbox = new Mailbox(); - $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'trash') - ->willReturn($mailbox); - - $this->listener->handle($event); - } - - public function testHandleDoesNotExistCantCreate(): void { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - $event = new BeforeMessageDeletedEvent( - $account, - 'INBOX', - 123 - ); - $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'trash') - ->willThrowException(new DoesNotExistException('')); - $client = $this->createMock(\Horde_Imap_Client_Socket::class); - $this->imapClientFactory->expects($this->once()) - ->method('getClient') - ->with($account) - ->willReturn($client); - $client->expects($this->once()) - ->method('createMailbox') - ->with( - 'Trash', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_TRASH, - ], - ] - ) - ->willThrowException(new \Horde_Imap_Client_Exception()); - $this->logger->expects($this->once()) - ->method('error'); - - $this->listener->handle($event); - } - - public function testHandleDoesNotExist(): void { - /** @var Account|MockObject $account */ - $account = $this->createMock(Account::class); - $event = new BeforeMessageDeletedEvent( - $account, - 'INBOX', - 123 - ); - $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'trash') - ->willThrowException(new DoesNotExistException('')); - $client = $this->createMock(\Horde_Imap_Client_Socket::class); - $this->imapClientFactory->expects($this->once()) - ->method('getClient') - ->with($account) - ->willReturn($client); - $client->expects($this->once()) - ->method('createMailbox') - ->with( - 'Trash', - [ - 'special_use' => [ - \Horde_Imap_Client::SPECIALUSE_TRASH, - ], - ] - ); - $this->mailboxSync->expects($this->once()) - ->method('sync') - ->with($account, $this->logger, true); - $this->logger->expects($this->never()) - ->method('error'); - - $this->listener->handle($event); - } -} diff --git a/tests/Unit/Service/MailManagerTest.php b/tests/Unit/Service/MailManagerTest.php index d5cece922..e553abb31 100644 --- a/tests/Unit/Service/MailManagerTest.php +++ b/tests/Unit/Service/MailManagerTest.php @@ -26,6 +26,7 @@ namespace OCA\Mail\Tests\Unit\Service; use ChristophWurst\Nextcloud\Testing\TestCase; use Horde_Imap_Client_Socket; use OCA\Mail\Account; +use OCA\Mail\Db\MailAccount; use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\MessageMapper as DbMessageMapper; @@ -188,9 +189,12 @@ class MailManagerTest extends TestCase { ); } - public function testDeleteMessageTrashFolderNotFound(): void { + public function testDeleteMessageTrashMailboxNotFound(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setTrashMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); $this->eventDispatcher->expects($this->once()) ->method('dispatch') ->with( @@ -202,8 +206,8 @@ class MailManagerTest extends TestCase { ->with($account, 'INBOX') ->willReturn($this->createMock(Mailbox::class)); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'trash') + ->method('findById') + ->with(123) ->willThrowException(new DoesNotExistException("")); $this->expectException(ServiceException::class); @@ -217,6 +221,9 @@ class MailManagerTest extends TestCase { public function testDeleteMessage(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setTrashMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); $inbox = new Mailbox(); $inbox->setName('INBOX'); $trash = new Mailbox(); @@ -228,8 +235,8 @@ class MailManagerTest extends TestCase { ->with($account, 'INBOX') ->willReturn($inbox); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'trash') + ->method('findById') + ->with(123) ->willReturn($trash); $client = $this->createMock(Horde_Imap_Client_Socket::class); $this->imapClientFactory->expects($this->once()) @@ -254,6 +261,9 @@ class MailManagerTest extends TestCase { public function testExpungeMessage(): void { /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); + $mailAccount = new MailAccount(); + $mailAccount->setTrashMailboxId(123); + $account->method('getMailAccount')->willReturn($mailAccount); $source = new Mailbox(); $source->setName('Trash'); $trash = new Mailbox(); @@ -265,8 +275,8 @@ class MailManagerTest extends TestCase { ->with($account, 'Trash') ->willReturn($source); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'trash') + ->method('findById') + ->with(123) ->willReturn($trash); $client = $this->createMock(Horde_Imap_Client_Socket::class); $this->imapClientFactory->expects($this->once()) diff --git a/tests/Unit/Service/MailTransmissionTest.php b/tests/Unit/Service/MailTransmissionTest.php index 4aeaf2cda..63cf1587d 100644 --- a/tests/Unit/Service/MailTransmissionTest.php +++ b/tests/Unit/Service/MailTransmissionTest.php @@ -100,6 +100,7 @@ class MailTransmissionTest extends TestCase { public function testSendNewMessage() { $mailAccount = new MailAccount(); $mailAccount->setUserId('testuser'); + $mailAccount->setSentMailboxId(123); /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); $account->method('getMailAccount')->willReturn($mailAccount); @@ -122,6 +123,7 @@ class MailTransmissionTest extends TestCase { public function testSendMessageFromAlias() { $mailAccount = new MailAccount(); $mailAccount->setUserId('testuser'); + $mailAccount->setSentMailboxId(123); /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); $account->method('getMailAccount')->willReturn($mailAccount); @@ -152,6 +154,7 @@ class MailTransmissionTest extends TestCase { public function testSendNewMessageWithCloudAttachments() { $mailAccount = new MailAccount(); $mailAccount->setUserId('testuser'); + $mailAccount->setSentMailboxId(123); /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); $account->method('getMailAccount')->willReturn($mailAccount); @@ -194,6 +197,7 @@ class MailTransmissionTest extends TestCase { public function testReplyToAnExistingMessage() { $mailAccount = new MailAccount(); $mailAccount->setUserId('testuser'); + $mailAccount->setSentMailboxId(123); /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); $account->method('getMailAccount')->willReturn($mailAccount); @@ -222,6 +226,7 @@ class MailTransmissionTest extends TestCase { public function testSaveDraft() { $mailAccount = new MailAccount(); $mailAccount->setUserId('testuser'); + $mailAccount->setDraftsMailboxId(123); /** @var Account|MockObject $account */ $account = $this->createMock(Account::class); $account->method('getMailAccount')->willReturn($mailAccount); @@ -239,8 +244,8 @@ class MailTransmissionTest extends TestCase { ->willReturn($client); $draftsMailbox = new \OCA\Mail\Db\Mailbox(); $this->mailboxMapper->expects($this->once()) - ->method('findSpecial') - ->with($account, 'drafts') + ->method('findById') + ->with(123) ->willReturn($draftsMailbox); $this->messageMapper->expects($this->once()) ->method('save') |