From 1a95848521b63dd6bb648b3b8ba9c86e459f1ea6 Mon Sep 17 00:00:00 2001 From: hamza221 Date: Sun, 17 Sep 2023 22:51:40 +0200 Subject: [PATCH] feat: delete tags Signed-off-by: hamza221 --- appinfo/routes.php | 5 ++ lib/Contracts/IMailManager.php | 13 +++++ lib/Controller/TagsController.php | 26 ++++++++- lib/Db/MessageTags.php | 54 +++++++++++++++++ lib/Db/MessageTagsMapper.php | 52 +++++++++++++++++ lib/Service/MailManager.php | 80 ++++++++++++++++++++++++++ src/components/DeleteTagModal.vue | 75 ++++++++++++++++++++++++ src/components/TagItem.vue | 19 ++++-- src/components/TagModal.vue | 76 +++++++++++++++--------- src/service/MessageService.js | 8 +++ src/store/actions.js | 8 +++ src/store/mutations.js | 4 ++ tests/Unit/Service/MailManagerTest.php | 8 +++ 13 files changed, 395 insertions(+), 33 deletions(-) create mode 100644 lib/Db/MessageTags.php create mode 100644 lib/Db/MessageTagsMapper.php create mode 100644 src/components/DeleteTagModal.vue diff --git a/appinfo/routes.php b/appinfo/routes.php index c78844ea5e..688279f9bf 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -135,6 +135,11 @@ 'url' => '/api/tags/{id}', 'verb' => 'PUT' ], + [ + 'name' => 'tags#delete', + 'url' => '/api/tags/{accountId}/delete/{id}', + 'verb' => 'DELETE' + ], [ 'name' => 'aliases#updateSignature', 'url' => '/api/accounts/{accountId}/aliases/{id}/signature', diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index e02b3e70ca..79239a0844 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -317,6 +317,19 @@ public function createTag(string $displayName, string $color, string $userId): T */ public function updateTag(int $id, string $displayName, string $color, string $userId): Tag; + /** + * Delete a mail tag + * + * @throws ClientException + */ + public function deleteTag(int $id, string $userId, array $accounts): Tag; + + /** + * Delete message Tags and untagged messages on Imap + * + * @throws ClientException + */ + public function deleteTagForAccount(int $id, string $userId, Tag $tag, Account $account): void; /** * @param Account $srcAccount * @param Mailbox $srcMailbox diff --git a/lib/Controller/TagsController.php b/lib/Controller/TagsController.php index e7f44b54aa..e45d2cf331 100644 --- a/lib/Controller/TagsController.php +++ b/lib/Controller/TagsController.php @@ -27,7 +27,10 @@ use OCA\Mail\Contracts\IMailManager; use OCA\Mail\Exception\ClientException; use OCA\Mail\Http\TrapError; +use OCA\Mail\Service\AccountService; use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; use OCP\AppFramework\Http\JSONResponse; use OCP\IRequest; @@ -35,13 +38,18 @@ class TagsController extends Controller { private string $currentUserId; private IMailManager $mailManager; + private AccountService $accountService; + + public function __construct(IRequest $request, string $UserId, - IMailManager $mailManager + IMailManager $mailManager, + AccountService $accountService, ) { parent::__construct(Application::APP_ID, $request); $this->currentUserId = $UserId; $this->mailManager = $mailManager; + $this->accountService = $accountService; } /** @@ -80,6 +88,22 @@ public function update(int $id, string $displayName, string $color): JSONRespons $tag = $this->mailManager->updateTag($id, $displayName, $color, $this->currentUserId); return new JSONResponse($tag); } + /** + * @NoAdminRequired + * + * @throws ClientException + */ + #[TrapError] + public function delete(int $id, int $accountId): JSONResponse { + try { + $accounts = $this->accountService->findByUserId($this->currentUserId); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + $this->mailManager->deleteTag($id, $this->currentUserId, $accounts); + + return new JSONResponse([$id]); + } /** * @throws ClientException diff --git a/lib/Db/MessageTags.php b/lib/Db/MessageTags.php new file mode 100644 index 0000000000..8203c154cd --- /dev/null +++ b/lib/Db/MessageTags.php @@ -0,0 +1,54 @@ + + * + * @author 2023 Hamza Mahjoubi + * + * @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 . + */ + +namespace OCA\Mail\Db; + +use JsonSerializable; +use OCP\AppFramework\Db\Entity; +use ReturnTypeWillChange; + +/** + * @method string getImapMessageId() + * @method void setImapMessageId(string $imapMessageId) + * @method int getTagId() + * @method void setTagId(int $tagId) + */ +class MessageTags extends Entity implements JsonSerializable { + protected $imapMessageId; + protected $tagId; + + public function __construct() { + $this->addType('tagId', 'integer'); + } + + #[ReturnTypeWillChange] + public function jsonSerialize() { + return [ + 'id' => $this->getId(), + 'imapMessageId' => $this->getImapMessageId(), + 'tagId' => $this->getTagId(), + ]; + } +} diff --git a/lib/Db/MessageTagsMapper.php b/lib/Db/MessageTagsMapper.php new file mode 100644 index 0000000000..b5012e157c --- /dev/null +++ b/lib/Db/MessageTagsMapper.php @@ -0,0 +1,52 @@ + + * + * @author 2023 Hamza Mahjoubi + * + * @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 . + */ + +namespace OCA\Mail\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @template-extends QBMapper + */ +class MessageTagsMapper extends QBMapper { + + public function __construct(IDBConnection $db) { + parent::__construct($db, 'mail_message_tags'); + } + + public function getMessagesByTag(int $id): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('tag_id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)), + + ); + return $this->findEntities($qb); + } + +} diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 52ca214452..e320f77d33 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -36,6 +36,7 @@ use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\Message; use OCA\Mail\Db\MessageMapper as DbMessageMapper; +use OCA\Mail\Db\MessageTagsMapper; use OCA\Mail\Db\Tag; use OCA\Mail\Db\TagMapper; use OCA\Mail\Db\ThreadMapper; @@ -99,6 +100,9 @@ class MailManager implements IMailManager { /** @var TagMapper */ private $tagMapper; + /** @var MessageTagsMapper */ + private $messageTagsMapper; + /** @var ThreadMapper */ private $threadMapper; @@ -111,6 +115,7 @@ public function __construct(IMAPClientFactory $imapClientFactory, IEventDispatcher $eventDispatcher, LoggerInterface $logger, TagMapper $tagMapper, + MessageTagsMapper $messageTagsMapper, ThreadMapper $threadMapper) { $this->imapClientFactory = $imapClientFactory; $this->mailboxMapper = $mailboxMapper; @@ -121,6 +126,7 @@ public function __construct(IMAPClientFactory $imapClientFactory, $this->eventDispatcher = $eventDispatcher; $this->logger = $logger; $this->tagMapper = $tagMapper; + $this->messageTagsMapper = $messageTagsMapper; $this->threadMapper = $threadMapper; } @@ -493,7 +499,37 @@ public function flagMessage(Account $account, string $mailbox, int $uid, string ) ); } + public function tagMessageWithClient(Horde_Imap_Client_Socket $client, Account $account, Mailbox $mailbox, array $messages, Tag $tag, bool $value):void { + if ($this->isPermflagsEnabled($client, $account, $mailbox->getName()) === true) { + $messageIds = array_map(static function (Message $message) { + return $message->getUid(); + }, $messages); + try { + if ($value) { + // imap keywords and flags work the same way + $this->imapMessageMapper->addFlag($client, $mailbox, $messageIds, $tag->getImapLabel()); + } else { + $this->imapMessageMapper->removeFlag($client, $mailbox, $messageIds, $tag->getImapLabel()); + } + } catch (Horde_Imap_Client_Exception $e) { + throw new ServiceException( + "Could not set message keyword on IMAP: " . $e->getMessage(), + $e->getCode(), + $e + ); + } + if ($value) { + foreach ($messages as $message) { + $this->tagMapper->tagMessage($tag, $message->getMessageId(), $account->getUserId()); + } + } else { + foreach ($messages as $message) { + $this->tagMapper->untagMessage($tag, $message->getMessageId()); + } + } + } + } /** * Tag (flag) a message on IMAP * @@ -819,6 +855,50 @@ public function updateTag(int $id, string $displayName, string $color, string $u return $this->tagMapper->update($tag); } + public function deleteTag(int $id, string $userId, array $accounts) :Tag { + try { + $tag = $this->tagMapper->getTagForUser($id, $userId); + } catch (DoesNotExistException $e) { + throw new ClientException('Tag not found', 0, $e); + } + + foreach ($accounts as $account) { + $this->deleteTagForAccount($id, $userId, $tag, $account); + } + return $this->tagMapper->delete($tag); + } + + public function deleteTagForAccount(int $id, string $userId, Tag $tag, Account $account) :void { + try { + $messageTags = $this->messageTagsMapper->getMessagesByTag($id); + $messages = array_merge(... array_map(function ($messageTag) use ($account) { + return $this->getByMessageId($account, $messageTag->getImapMessageId()); + }, [...$messageTags])); + } catch (DoesNotExistException $e) { + throw new ClientException('Messages not found', 0, $e); + } + + $client = $this->imapClientFactory->getClient($account); + + foreach ($messageTags as $messageTag) { + $this->messageTagsMapper->delete($messageTag); + } + $groupedMessages = []; + foreach ($messages as $message) { + $mailboxId = $message->getMailboxId(); + if (array_key_exists($mailboxId, $groupedMessages)) { + $groupedMessages[$mailboxId][] = $message; + } else { + $groupedMessages[$mailboxId] = [$message]; + } + } + + foreach ($groupedMessages as $mailboxId => $messages) { + $mailbox = $this->getMailbox($userId, $mailboxId); + $this->tagMessageWithClient($client, $account, $mailbox, $messages, $tag, false); + } + $client->logout(); + } public function moveThread(Account $srcAccount, Mailbox $srcMailbox, Account $dstAccount, Mailbox $dstMailbox, string $threadRootId): array { $mailAccount = $srcAccount->getMailAccount(); $messageInTrash = $srcMailbox->getId() === $mailAccount->getTrashMailboxId(); diff --git a/src/components/DeleteTagModal.vue b/src/components/DeleteTagModal.vue new file mode 100644 index 0000000000..0b66cfca8a --- /dev/null +++ b/src/components/DeleteTagModal.vue @@ -0,0 +1,75 @@ + + + diff --git a/src/components/TagItem.vue b/src/components/TagItem.vue index 5c30259b37..2f02f54ab3 100644 --- a/src/components/TagItem.vue +++ b/src/components/TagItem.vue @@ -12,6 +12,14 @@ @click="openEditTag"> {{ t('mail','Edit name or color') }} + + + {{ t('mail','Delete tag') }} + + @@ -46,6 +54,8 @@