diff options
author | Daniel Kesselberg <mail@danielkesselberg.de> | 2021-06-01 21:53:19 +0300 |
---|---|---|
committer | Christoph Wurst <christoph@winzerhof-wurst.at> | 2021-06-10 12:27:59 +0300 |
commit | 0da32c97f64e0cdf950368257447ec8fa6fe7fea (patch) | |
tree | e8caddaefcb971e17a83b32e60726d8cdeac568d /lib | |
parent | c149af8e36d5491b2a9bb0133495f289e00d6945 (diff) |
Show each thread once per message list
Signed-off-by: Daniel Kesselberg <mail@danielkesselberg.de>
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Diffstat (limited to 'lib')
-rw-r--r-- | lib/Contracts/IMailManager.php | 25 | ||||
-rwxr-xr-x | lib/Controller/MessagesController.php | 2 | ||||
-rwxr-xr-x | lib/Controller/ThreadController.php | 123 | ||||
-rw-r--r-- | lib/Db/MessageMapper.php | 206 | ||||
-rw-r--r-- | lib/Db/ThreadMapper.php | 73 | ||||
-rw-r--r-- | lib/Service/MailManager.php | 95 |
6 files changed, 417 insertions, 107 deletions
diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index 76eb2f1d5..1eafd7b32 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -110,11 +110,11 @@ interface IMailManager { /** * @param Account $account - * @param int $messageId database message ID + * @param string $threadRootId thread root id * * @return Message[] */ - public function getThread(Account $account, int $messageId): array; + public function getThread(Account $account, string $threadRootId): array; /** * @param Account $sourceAccount @@ -263,4 +263,25 @@ interface IMailManager { * @throws ClientException if the given tag does not exist */ public function updateTag(int $id, string $displayName, string $color, string $userId): Tag; + + /** + * @param Account $srcAccount + * @param Mailbox $srcMailbox + * @param Account $dstAccount + * @param Mailbox $dstMailbox + * @param string $threadRootId + * @return void + * @throws ServiceException + */ + public function moveThread(Account $srcAccount, Mailbox $srcMailbox, Account $dstAccount, Mailbox $dstMailbox, string $threadRootId): void; + + /** + * @param Account $account + * @param Mailbox $mailbox + * @param string $threadRootId + * @return void + * @throws ClientException + * @throws ServiceException + */ + public function deleteThread(Account $account, Mailbox $mailbox, string $threadRootId): void; } diff --git a/lib/Controller/MessagesController.php b/lib/Controller/MessagesController.php index a39de7001..e3cc7a3e7 100755 --- a/lib/Controller/MessagesController.php +++ b/lib/Controller/MessagesController.php @@ -307,7 +307,7 @@ class MessagesController extends Controller { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - return new JSONResponse($this->mailManager->getThread($account, $id)); + return new JSONResponse($this->mailManager->getThread($account, $message->getThreadRootId())); } /** diff --git a/lib/Controller/ThreadController.php b/lib/Controller/ThreadController.php new file mode 100755 index 000000000..eecc245a0 --- /dev/null +++ b/lib/Controller/ThreadController.php @@ -0,0 +1,123 @@ +<?php + +declare(strict_types=1); + +/** + * @author Daniel Kesselberg <mail@danielkesselberg.de> + * + * Mail + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\Mail\Controller; + +use OCA\Mail\Contracts\IMailManager; +use OCA\Mail\Exception\ClientException; +use OCA\Mail\Exception\ServiceException; +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; +use Psr\Log\LoggerInterface; + +class ThreadController extends Controller { + + /** @var string */ + private $currentUserId; + + /** @var LoggerInterface */ + private $logger; + + /** @var AccountService */ + private $accountService; + + /** @var IMailManager */ + private $mailManager; + + public function __construct(string $appName, + IRequest $request, + string $UserId, + AccountService $accountService, + IMailManager $mailManager) { + parent::__construct($appName, $request); + + $this->currentUserId = $UserId; + $this->accountService = $accountService; + $this->mailManager = $mailManager; + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id + * @param int $destMailboxId + * + * @return JSONResponse + * @throws ClientException + * @throws ServiceException + */ + public function move(int $id, int $destMailboxId): JSONResponse { + try { + $message = $this->mailManager->getMessage($this->currentUserId, $id); + $srcMailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); + $srcAccount = $this->accountService->find($this->currentUserId, $srcMailbox->getAccountId()); + $dstMailbox = $this->mailManager->getMailbox($this->currentUserId, $destMailboxId); + $dstAccount = $this->accountService->find($this->currentUserId, $dstMailbox->getAccountId()); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + + $this->mailManager->moveThread( + $srcAccount, + $srcMailbox, + $dstAccount, + $dstMailbox, + $message->getThreadRootId() + ); + + return new JSONResponse(); + } + + /** + * @NoAdminRequired + * @TrapError + * + * @param int $id + * + * @return JSONResponse + * @throws ClientException + * @throws ServiceException + */ + public function delete(int $id): JSONResponse { + try { + $message = $this->mailManager->getMessage($this->currentUserId, $id); + $mailbox = $this->mailManager->getMailbox($this->currentUserId, $message->getMailboxId()); + $account = $this->accountService->find($this->currentUserId, $mailbox->getAccountId()); + } catch (DoesNotExistException $e) { + return new JSONResponse([], Http::STATUS_FORBIDDEN); + } + + $this->mailManager->deleteThread( + $account, + $mailbox, + $message->getThreadRootId() + ); + + return new JSONResponse(); + } +} diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 6b79fb38a..203c877e6 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -25,29 +25,29 @@ declare(strict_types=1); namespace OCA\Mail\Db; -use OCP\IUser; -use function ltrim; use OCA\Mail\Account; use OCA\Mail\Address; -use RuntimeException; -use OCP\IDBConnection; -use function array_map; -use function get_class; -use function mb_strcut; -use function array_keys; -use function array_combine; -use function array_udiff; use OCA\Mail\AddressList; +use OCA\Mail\IMAP\Threading\DatabaseMessage; use OCA\Mail\Service\Search\Flag; -use OCP\AppFramework\Db\QBMapper; -use OCP\DB\QueryBuilder\IQueryBuilder; -use OCA\Mail\Service\Search\SearchQuery; -use OCP\AppFramework\Utility\ITimeFactory; use OCA\Mail\Service\Search\FlagExpression; -use OCA\Mail\IMAP\Threading\DatabaseMessage; +use OCA\Mail\Service\Search\SearchQuery; use OCA\Mail\Support\PerformanceLogger; use OCP\AppFramework\Db\DoesNotExistException; -use function \OCA\Mail\array_flat_map; +use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\IUser; +use RuntimeException; +use function array_combine; +use function array_keys; +use function array_map; +use function array_udiff; +use function get_class; +use function ltrim; +use function mb_strcut; +use function OCA\Mail\array_flat_map; /** * @template-extends QBMapper<Message> @@ -442,6 +442,7 @@ class MessageMapper extends QBMapper { return $ids; } + /** * @param Message ...$messages * @@ -552,34 +553,22 @@ class MessageMapper extends QBMapper { /** * @param Account $account - * @param int $messageId + * @param string $threadRootId * * @return Message[] */ - public function findThread(Account $account, int $messageId): array { + public function findThread(Account $account, string $threadRootId): array { $qb = $this->db->getQueryBuilder(); - $subQb1 = $this->db->getQueryBuilder(); - - $mailboxIdsQuery = $subQb1 - ->select('id') - ->from('mail_mailboxes') - ->where($qb->expr()->eq('account_id', $qb->createNamedParameter($account->getId(), IQueryBuilder::PARAM_INT))); - - /** - * Select the message with the given ID or any that has the same thread ID - */ - $selectMessages = $qb - ->select('m2.*') - ->from($this->getTableName(), 'm1') - ->leftJoin('m1', $this->getTableName(), 'm2', $qb->expr()->eq('m1.thread_root_id', 'm2.thread_root_id')) + $qb->select('messages.*') + ->from($this->getTableName(), 'messages') + ->join('messages', 'mail_mailboxes', 'mailboxes', $qb->expr()->eq('messages.mailbox_id', 'mailboxes.id', IQueryBuilder::PARAM_INT)) ->where( - $qb->expr()->in('m1.mailbox_id', $qb->createFunction($mailboxIdsQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY), - $qb->expr()->in('m2.mailbox_id', $qb->createFunction($mailboxIdsQuery->getSQL()), IQueryBuilder::PARAM_INT_ARRAY), - $qb->expr()->eq('m1.id', $qb->createNamedParameter($messageId, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), - $qb->expr()->isNotNull('m1.thread_root_id') + $qb->expr()->eq('mailboxes.account_id', $qb->createNamedParameter($account->getId(), IQueryBuilder::PARAM_INT)), + $qb->expr()->eq('messages.thread_root_id', $qb->createNamedParameter($threadRootId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR) ) - ->orderBy('sent_at', 'desc'); - return $this->findRelatedData($this->findEntities($selectMessages), $account->getUserId()); + ->orderBy('messages.sent_at', 'desc'); + + return $this->findRelatedData($this->findEntities($qb), $account->getUserId()); } /** @@ -593,10 +582,14 @@ class MessageMapper extends QBMapper { public function findIdsByQuery(Mailbox $mailbox, SearchQuery $query, ?int $limit, array $uids = null): array { $qb = $this->db->getQueryBuilder(); - $select = $qb - ->selectDistinct('m.id') - ->addSelect('m.sent_at') - ->from($this->getTableName(), 'm'); + if ($this->needDistinct($query)) { + $select = $qb->selectDistinct(['m.id', 'm.sent_at']); + } else { + $select = $qb->select(['m.id', 'm.sent_at']); + } + + $select->from($this->getTableName(), 'm') + ->leftJoin('m', $this->getTableName(), 'm2', 'm.mailbox_id = m2.mailbox_id and m.thread_root_id = m2.thread_root_id and m.sent_at < m2.sent_at'); if (!empty($query->getFrom())) { $select->innerJoin('m', 'mail_recipients', 'r0', 'm.id = r0.message_id'); @@ -612,7 +605,7 @@ class MessageMapper extends QBMapper { } $select->where( - $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId()), IQueryBuilder::PARAM_INT) + $qb->expr()->eq('m.mailbox_id', $qb->createNamedParameter($mailbox->getId()), IQueryBuilder::PARAM_INT) ); if (!empty($query->getFrom())) { @@ -641,7 +634,7 @@ class MessageMapper extends QBMapper { $qb->expr()->orX( ...array_map(function (string $subject) use ($qb) { return $qb->expr()->iLike( - 'subject', + 'm.subject', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($subject) . '%', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR ); @@ -652,31 +645,32 @@ class MessageMapper extends QBMapper { if ($query->getCursor() !== null) { $select->andWhere( - $qb->expr()->lt('sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT)) + $qb->expr()->lt('m.sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT)) ); } // createParameter if ($uids !== null) { $select->andWhere( - $qb->expr()->in('uid', $qb->createParameter('uids')) + $qb->expr()->in('m.uid', $qb->createParameter('uids')) ); } foreach ($query->getFlags() as $flag) { - $select->andWhere($qb->expr()->eq($this->flagToColumnName($flag), $qb->createNamedParameter($flag->isSet(), IQueryBuilder::PARAM_BOOL))); + $select->andWhere($qb->expr()->eq('m.' . $this->flagToColumnName($flag), $qb->createNamedParameter($flag->isSet(), IQueryBuilder::PARAM_BOOL))); } if (!empty($query->getFlagExpressions())) { $select->andWhere( ...array_map(function (FlagExpression $expr) use ($select) { - return $this->flagExpressionToQuery($expr, $select); + return $this->flagExpressionToQuery($expr, $select, 'm'); }, $query->getFlagExpressions()) ); } - $select = $select - ->orderBy('sent_at', 'desc'); + $select->andWhere($qb->expr()->isNull('m2.id')); + + $select->orderBy('m.sent_at', 'desc'); if ($limit !== null) { - $select = $select->setMaxResults($limit); + $select->setMaxResults($limit); } if ($uids !== null) { @@ -697,10 +691,14 @@ class MessageMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qbMailboxes = $this->db->getQueryBuilder(); - $select = $qb - ->selectDistinct('m.id') - ->addSelect('m.sent_at') - ->from($this->getTableName(), 'm'); + if ($this->needDistinct($query)) { + $select = $qb->selectDistinct(['m.id', 'm.sent_at']); + } else { + $select = $qb->select(['m.id', 'm.sent_at']); + } + + $select->from($this->getTableName(), 'm') + ->leftJoin('m', $this->getTableName(), 'm2', 'm.mailbox_id = m2.mailbox_id and m.thread_root_id = m2.thread_root_id and m.sent_at < m2.sent_at'); if (!empty($query->getFrom())) { $select->innerJoin('m', 'mail_recipients', 'r0', 'm.id = r0.message_id'); @@ -720,7 +718,7 @@ class MessageMapper extends QBMapper { ->join('mb', 'mail_accounts', 'a', $qb->expr()->eq('a.id', 'mb.account_id', IQueryBuilder::PARAM_INT)) ->where($qb->expr()->eq('a.user_id', $qb->createNamedParameter($user->getUID()))); $select->where( - $qb->expr()->in('mailbox_id', $qb->createFunction($selectMailboxIds->getSQL()), IQueryBuilder::PARAM_INT_ARRAY) + $qb->expr()->in('m.mailbox_id', $qb->createFunction($selectMailboxIds->getSQL()), IQueryBuilder::PARAM_INT_ARRAY) ); if (!empty($query->getFrom())) { @@ -749,7 +747,7 @@ class MessageMapper extends QBMapper { $qb->expr()->orX( ...array_map(function (string $subject) use ($qb) { return $qb->expr()->iLike( - 'subject', + 'm.subject', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($subject) . '%', IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR ); @@ -760,30 +758,31 @@ class MessageMapper extends QBMapper { if ($query->getCursor() !== null) { $select->andWhere( - $qb->expr()->lt('sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT)) + $qb->expr()->lt('m.sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT)) ); } if ($uids !== null) { $select->andWhere( - $qb->expr()->in('uid', $qb->createParameter('uids')) + $qb->expr()->in('m.uid', $qb->createParameter('uids')) ); } foreach ($query->getFlags() as $flag) { - $select->andWhere($qb->expr()->eq($this->flagToColumnName($flag), $qb->createNamedParameter($flag->isSet(), IQueryBuilder::PARAM_BOOL))); + $select->andWhere($qb->expr()->eq('m.' . $this->flagToColumnName($flag), $qb->createNamedParameter($flag->isSet(), IQueryBuilder::PARAM_BOOL))); } if (!empty($query->getFlagExpressions())) { $select->andWhere( ...array_map(function (FlagExpression $expr) use ($select) { - return $this->flagExpressionToQuery($expr, $select); + return $this->flagExpressionToQuery($expr, $select, 'm'); }, $query->getFlagExpressions()) ); } - $select = $select - ->orderBy('sent_at', 'desc'); + $select->andWhere($qb->expr()->isNull('m2.id')); + + $select->orderBy('m.sent_at', 'desc'); if ($limit !== null) { - $select = $select->setMaxResults($limit); + $select->setMaxResults($limit); } if ($uids !== null) { @@ -800,17 +799,44 @@ class MessageMapper extends QBMapper { }, $this->findEntities($select)); } - private function flagExpressionToQuery(FlagExpression $expr, IQueryBuilder $qb): string { - $operands = array_map(function (object $operand) use ($qb) { + /** + * Return true when a distinct query is required. + * + * For the threaded message list it's necessary to self-join + * the mail_messages table to figure out if we are the latest message + * of a thread. + * + * Unfortunately a self-join on a larger table has a significant + * performance impact. An database index (e.g. on thread_root_id) + * could improve the query performance but adding an index is blocked by + * - https://github.com/nextcloud/server/pull/25471 + * - https://github.com/nextcloud/mail/issues/4735 + * + * We noticed a better query performance without distinct. As distinct is + * only necessary when a search query is present (e.g. search for mail with + * two recipients) it's reasonable to use distinct only for those requests. + * + * @param SearchQuery $query + * @return bool + */ + private function needDistinct(SearchQuery $query): bool { + return !empty($query->getFrom()) + || !empty($query->getTo()) + || !empty($query->getCc()) + || !empty($query->getBcc()); + } + + private function flagExpressionToQuery(FlagExpression $expr, IQueryBuilder $qb, string $tableAlias): string { + $operands = array_map(function (object $operand) use ($qb, $tableAlias) { if ($operand instanceof Flag) { return $qb->expr()->eq( - $this->flagToColumnName($operand), + $tableAlias . '.' . $this->flagToColumnName($operand), $qb->createNamedParameter($operand->isSet(), IQueryBuilder::PARAM_BOOL), IQueryBuilder::PARAM_BOOL ); } if ($operand instanceof FlagExpression) { - return $this->flagExpressionToQuery($operand, $qb); + return $this->flagExpressionToQuery($operand, $qb, $tableAlias); } throw new RuntimeException('Invalid operand type ' . get_class($operand)); @@ -954,30 +980,38 @@ class MessageMapper extends QBMapper { * @return int[] */ public function findNewIds(Mailbox $mailbox, array $ids): array { - $sub = $this->db->getQueryBuilder(); - $qb = $this->db->getQueryBuilder(); + $select = $this->db->getQueryBuilder(); + $subSelect = $this->db->getQueryBuilder(); + + $inExpression = []; + $notInExpression = []; + + foreach (array_chunk($ids, 1000) as $chunk) { + $inExpression[] = $subSelect->expr()->in('id', $select->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)); + $notInExpression[] = $subSelect->expr()->notIn('m.id', $select->createNamedParameter($chunk, IQueryBuilder::PARAM_INT_ARRAY)); + } - $subSelect = $sub - ->select($sub->func()->max('uid')) + $subSelect + ->select($subSelect->func()->min('sent_at')) ->from($this->getTableName()) ->where( - $sub->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)), - $sub->expr()->in('id', $qb->createParameter('ids')) + $subSelect->expr()->eq('mailbox_id', $select->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)), + $subSelect->expr()->orX(...$inExpression) ); - $qb->select('id') - ->from($this->getTableName()) + $select + ->select('m.id') + ->from($this->getTableName(), 'm') + ->leftJoin('m', $this->getTableName(), 'm2', 'm.mailbox_id = m2.mailbox_id and m.thread_root_id = m2.thread_root_id and m.sent_at < m2.sent_at') ->where( - $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)) - ); + $select->expr()->eq('m.mailbox_id', $select->createNamedParameter($mailbox->getId(), IQueryBuilder::PARAM_INT)), + $select->expr()->andX(...$notInExpression), + $select->expr()->gt('m.sent_at', $select->createFunction('(' . $subSelect->getSQL() . ')'), IQueryBuilder::PARAM_INT), + $select->expr()->isNull('m2.id') + ) + ->orderBy('m.sent_at', 'desc'); - return array_flat_map(function (array $chunk) use ($qb, $subSelect) { - $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); - $select = $qb->andWhere( - $qb->expr()->gt('uid', $qb->createFunction('(' . $subSelect->getSQL() . ')'), IQueryBuilder::PARAM_INT) - ); - return $this->findIds($select); - }, array_chunk($ids, 1000)); + return $this->findIds($select); } /** @@ -1073,7 +1107,7 @@ class MessageMapper extends QBMapper { if (empty($rows)) { return null; } - return (int) $rows[0]['id']; + return (int)$rows[0]['id']; } /** diff --git a/lib/Db/ThreadMapper.php b/lib/Db/ThreadMapper.php new file mode 100644 index 000000000..991f46573 --- /dev/null +++ b/lib/Db/ThreadMapper.php @@ -0,0 +1,73 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2021 Daniel Kesselberg <mail@danielkesselberg.de> + * + * @author 2021 Daniel Kesselberg <mail@danielkesselberg.de> + * + * @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<Message> + */ +class ThreadMapper extends QBMapper { + public function __construct(IDBConnection $db) { + parent::__construct($db, 'mail_messages'); + } + + /** + * @return array<array-key, array{mailboxName: string, messageUid: int}> + */ + public function findMessageUidsAndMailboxNamesByAccountAndThreadRoot(MailAccount $mailAccount, string $threadRootId, bool $trash): array { + $qb = $this->db->getQueryBuilder(); + $qb->select('messages.uid', 'mailboxes.name') + ->from($this->tableName, 'messages') + ->join('messages', 'mail_mailboxes', 'mailboxes', 'messages.mailbox_id = mailboxes.id') + ->where( + $qb->expr()->eq('messages.thread_root_id', $qb->createNamedParameter($threadRootId, IQueryBuilder::PARAM_STR)), + $qb->expr()->eq('mailboxes.account_id', $qb->createNamedParameter($mailAccount->getId(), IQueryBuilder::PARAM_INT)) + ); + + $trashMailboxId = $mailAccount->getTrashMailboxId(); + if ($trashMailboxId !== null) { + if ($trash) { + $qb->andWhere($qb->expr()->eq('mailboxes.id', $qb->createNamedParameter($trashMailboxId, IQueryBuilder::PARAM_INT))); + } else { + $qb->andWhere($qb->expr()->neq('mailboxes.id', $qb->createNamedParameter($trashMailboxId, IQueryBuilder::PARAM_INT))); + } + } + + $result = $qb->execute(); + $rows = array_map(static function (array $row) { + return [ + 'messageUid' => (int)$row[0], + 'mailboxName' => (string)$row[1] + ]; + }, $result->fetchAll(\PDO::FETCH_NUM)); + $result->closeCursor(); + + return $rows; + } +} diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index dd283f9f1..238b87900 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -35,6 +35,7 @@ use OCA\Mail\Db\Message; use OCA\Mail\Db\MessageMapper as DbMessageMapper; use OCA\Mail\Db\Tag; use OCA\Mail\Db\TagMapper; +use OCA\Mail\Db\ThreadMapper; use OCA\Mail\Events\BeforeMessageDeletedEvent; use OCA\Mail\Events\MessageDeletedEvent; use OCA\Mail\Events\MessageFlaggedEvent; @@ -87,17 +88,18 @@ class MailManager implements IMailManager { /** @var DbMessageMapper */ private $dbMessageMapper; - /** @var TagMapper */ - private $tagMapper; - /** @var IEventDispatcher */ private $eventDispatcher; - /** - * @var LoggerInterface - */ + /** @var LoggerInterface */ private $logger; + /** @var TagMapper */ + private $tagMapper; + + /** @var ThreadMapper */ + private $threadMapper; + public function __construct(IMAPClientFactory $imapClientFactory, MailboxMapper $mailboxMapper, MailboxSync $mailboxSync, @@ -106,7 +108,8 @@ class MailManager implements IMailManager { DbMessageMapper $dbMessageMapper, IEventDispatcher $eventDispatcher, LoggerInterface $logger, - TagMapper $tagMapper) { + TagMapper $tagMapper, + ThreadMapper $threadMapper) { $this->imapClientFactory = $imapClientFactory; $this->mailboxMapper = $mailboxMapper; $this->mailboxSync = $mailboxSync; @@ -116,6 +119,7 @@ class MailManager implements IMailManager { $this->eventDispatcher = $eventDispatcher; $this->logger = $logger; $this->tagMapper = $tagMapper; + $this->threadMapper = $threadMapper; } public function getMailbox(string $uid, int $id): Mailbox { @@ -155,13 +159,13 @@ class MailManager implements IMailManager { throw new ServiceException( "Could not get mailbox status: " . $e->getMessage(), - (int) $e->getCode(), + (int)$e->getCode(), $e ); } $this->folderMapper->detectFolderSpecialUse([$folder]); - $this->mailboxSync->sync($account, $this->logger,true); + $this->mailboxSync->sync($account, $this->logger, true); return $this->mailboxMapper->find($account, $name); } @@ -182,14 +186,14 @@ class MailManager implements IMailManager { } catch (Horde_Imap_Client_Exception | DoesNotExistException $e) { throw new ServiceException( "Could not load message", - (int) $e->getCode(), + (int)$e->getCode(), $e ); } } - public function getThread(Account $account, int $messageId): array { - return $this->dbMessageMapper->findThread($account, $messageId); + public function getThread(Account $account, string $threadRootId): array { + return $this->dbMessageMapper->findThread($account, $threadRootId); } public function getMessageIdForUid(Mailbox $mailbox, $uid): ?int { @@ -349,7 +353,7 @@ class MailManager implements IMailManager { } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( "Could not set subscription status for mailbox " . $mailbox->getId() . " on IMAP: " . $e->getMessage(), - (int) $e->getCode(), + (int)$e->getCode(), $e ); } @@ -357,7 +361,7 @@ class MailManager implements IMailManager { /** * 2. Pull changes into the mailbox database cache */ - $this->mailboxSync->sync($account, $this->logger,true); + $this->mailboxSync->sync($account, $this->logger, true); /** * 3. Return the updated object @@ -396,7 +400,7 @@ class MailManager implements IMailManager { } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( "Could not set message flag on IMAP: " . $e->getMessage(), - (int) $e->getCode(), + (int)$e->getCode(), $e ); } @@ -445,7 +449,7 @@ class MailManager implements IMailManager { } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( "Could not set message keyword on IMAP: " . $e->getMessage(), - (int) $e->getCode(), + (int)$e->getCode(), $e ); } @@ -520,7 +524,7 @@ class MailManager implements IMailManager { /** * 2. Get the IMAP changes into our database cache */ - $this->mailboxSync->sync($account, $this->logger,true); + $this->mailboxSync->sync($account, $this->logger, true); /** * 3. Return the cached object with the new ID @@ -605,7 +609,7 @@ class MailManager implements IMailManager { } catch (Horde_Imap_Client_Exception $e) { throw new ServiceException( "Could not get message flag options from IMAP: " . $e->getMessage(), - (int) $e->getCode(), + (int)$e->getCode(), $e ); } @@ -649,4 +653,59 @@ class MailManager implements IMailManager { return $this->tagMapper->update($tag); } + + public function moveThread(Account $srcAccount, Mailbox $srcMailbox, Account $dstAccount, Mailbox $dstMailbox, string $threadRootId): void { + $mailAccount = $srcAccount->getMailAccount(); + $messageInTrash = $srcMailbox->getId() === $mailAccount->getTrashMailboxId(); + + $messages = $this->threadMapper->findMessageUidsAndMailboxNamesByAccountAndThreadRoot( + $mailAccount, + $threadRootId, + $messageInTrash + ); + + foreach ($messages as $message) { + $this->logger->debug('move message', [ + 'messageId' => $message['messageUid'], + 'srcMailboxId' => $srcMailbox->getId(), + 'dstMailboxId' => $dstMailbox->getId() + ]); + + $this->moveMessage( + $srcAccount, + $message['mailboxName'], + $message['messageUid'], + $dstAccount, + $dstMailbox->getName() + ); + } + } + + /** + * @throws ClientException + * @throws ServiceException + */ + public function deleteThread(Account $account, Mailbox $mailbox, string $threadRootId): void { + $mailAccount = $account->getMailAccount(); + $messageInTrash = $mailbox->getId() === $mailAccount->getTrashMailboxId(); + + $messages = $this->threadMapper->findMessageUidsAndMailboxNamesByAccountAndThreadRoot( + $mailAccount, + $threadRootId, + $messageInTrash + ); + + foreach ($messages as $message) { + $this->logger->debug('deleting message', [ + 'messageId' => $message['messageUid'], + 'mailboxId' => $mailbox->getId(), + ]); + + $this->deleteMessage( + $account, + $message['mailboxName'], + $message['messageUid'] + ); + } + } } |