Skip to content

Commit

Permalink
feat: delete tags
Browse files Browse the repository at this point in the history
Signed-off-by: hamza221 <[email protected]>
  • Loading branch information
hamza221 committed Oct 26, 2023
1 parent 03ba490 commit ad66a0a
Show file tree
Hide file tree
Showing 13 changed files with 391 additions and 33 deletions.
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
13 changes: 13 additions & 0 deletions lib/Contracts/IMailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 25 additions & 1 deletion lib/Controller/TagsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,29 @@
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;

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;
}

/**
Expand Down Expand Up @@ -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);

Check failure on line 105 in lib/Controller/TagsController.php

View workflow job for this annotation

GitHub Actions / Nextcloud dev-master

InvalidArgument

lib/Controller/TagsController.php:105:27: InvalidArgument: Argument 1 of OCP\AppFramework\Http\JSONResponse::__construct expects array<array-key, mixed>|object, but int provided (see https://psalm.dev/004)

Check failure on line 105 in lib/Controller/TagsController.php

View workflow job for this annotation

GitHub Actions / Nextcloud dev-stable27

InvalidArgument

lib/Controller/TagsController.php:105:27: InvalidArgument: Argument 1 of OCP\AppFramework\Http\JSONResponse::__construct expects array<array-key, mixed>|object, but int provided (see https://psalm.dev/004)

Check failure on line 105 in lib/Controller/TagsController.php

View workflow job for this annotation

GitHub Actions / Nextcloud dev-stable26

InvalidArgument

lib/Controller/TagsController.php:105:27: InvalidArgument: Argument 1 of OCP\AppFramework\Http\JSONResponse::__construct expects array<array-key, mixed>|object, but int provided (see https://psalm.dev/004)
}

/**
* @throws ClientException
Expand Down
54 changes: 54 additions & 0 deletions lib/Db/MessageTags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

/**
* @copyright 2023 Hamza Mahjoubi <[email protected]>
*
* @author 2023 Hamza Mahjoubi <[email protected]>
*
* @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\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(),
];
}
}
52 changes: 52 additions & 0 deletions lib/Db/MessageTagsMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

/**
* @copyright 2023 Hamza Mahjoubi <[email protected]>
*
* @author 2023 Hamza Mahjoubi <[email protected]>
*
* @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\Db;

use OCP\AppFramework\Db\QBMapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/**
* @template-extends QBMapper<MessageTags>
*/
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);
}

}
80 changes: 80 additions & 0 deletions lib/Service/MailManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +100,9 @@ class MailManager implements IMailManager {
/** @var TagMapper */
private $tagMapper;

/** @var MessageTagsMapper */
private $messageTagsMapper;

/** @var ThreadMapper */
private $threadMapper;

Expand All @@ -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;
Expand All @@ -121,6 +126,7 @@ public function __construct(IMAPClientFactory $imapClientFactory,
$this->eventDispatcher = $eventDispatcher;
$this->logger = $logger;
$this->tagMapper = $tagMapper;
$this->messageTagsMapper = $messageTagsMapper;
$this->threadMapper = $threadMapper;
}

Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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) {

Check failure on line 874 in lib/Service/MailManager.php

View workflow job for this annotation

GitHub Actions / Nextcloud dev-master

NamedArgumentNotAllowed

lib/Service/MailManager.php:874:32: NamedArgumentNotAllowed: Method array_merge called with named unpacked array array<array-key, array<array-key, OCA\Mail\Db\Message>> (array with string keys) (see https://psalm.dev/268)

Check failure on line 874 in lib/Service/MailManager.php

View workflow job for this annotation

GitHub Actions / Nextcloud dev-stable27

NamedArgumentNotAllowed

lib/Service/MailManager.php:874:32: NamedArgumentNotAllowed: Method array_merge called with named unpacked array array<array-key, array<array-key, OCA\Mail\Db\Message>> (array with string keys) (see https://psalm.dev/268)

Check failure on line 874 in lib/Service/MailManager.php

View workflow job for this annotation

GitHub Actions / Nextcloud dev-stable26

NamedArgumentNotAllowed

lib/Service/MailManager.php:874:32: NamedArgumentNotAllowed: Method array_merge called with named unpacked array array<array-key, array<array-key, OCA\Mail\Db\Message>> (array with string keys) (see https://psalm.dev/268)
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();
Expand Down
72 changes: 72 additions & 0 deletions src/components/DeleteTagModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!--
- @copyright 2023 Hamza Mahjoubi <hamzamahjoubi221@proton.me>
-
- @author 2023 Hamza Mahjoubi <[email protected]>
-
- @license AGPL-3.0-or-later
-
- 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>
<ConfirmationModal title="Delete tag"
@confirm="deleteTag"
@cancel="onClose">
{{ t('mail','The tag will be deleted from all messages.') }}
</ConfirmationModal>
</template>
<script>
import { showSuccess, showInfo } from '@nextcloud/dialogs'
import ConfirmationModal from './ConfirmationModal'
export default {
name: 'DeleteTagModal',
components: {
ConfirmationModal,
},
props: {
tag: {
type: Object,
required: true,
},
accountId: {
type: Number,
required: true,
},
},
data() {
return {
deleting: false,
}
},
methods: {
onClose() {
this.$emit('close')
},
async deleteTag() {
this.deleting = true
try {
await this.$store.dispatch('deleteTag', {
tag: this.tag,
accountId: this.accountId,
})
showSuccess(t('mail', 'Tag: {name} deleted', { name: this.tag.displayName }))
} catch (error) {
showInfo(t('mail', 'An error occurred, unable to delete the tag.'))
}
this.$emit('close')

},
},
}

</script>
Loading

0 comments on commit ad66a0a

Please sign in to comment.