Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/mail.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorDaniel Kesselberg <mail@danielkesselberg.de>2021-06-01 21:53:19 +0300
committerChristoph Wurst <christoph@winzerhof-wurst.at>2021-06-10 12:27:59 +0300
commit0da32c97f64e0cdf950368257447ec8fa6fe7fea (patch)
treee8caddaefcb971e17a83b32e60726d8cdeac568d /lib
parentc149af8e36d5491b2a9bb0133495f289e00d6945 (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.php25
-rwxr-xr-xlib/Controller/MessagesController.php2
-rwxr-xr-xlib/Controller/ThreadController.php123
-rw-r--r--lib/Db/MessageMapper.php206
-rw-r--r--lib/Db/ThreadMapper.php73
-rw-r--r--lib/Service/MailManager.php95
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']
+ );
+ }
+ }
}