diff --git a/lib/Contracts/IAttachmentService.php b/lib/Contracts/IAttachmentService.php index 38d6ee9a4f..243cdd186a 100644 --- a/lib/Contracts/IAttachmentService.php +++ b/lib/Contracts/IAttachmentService.php @@ -34,4 +34,5 @@ public function getAttachment(string $userId, int $id): array; * @param int $id */ public function deleteAttachment(string $userId, int $id); + } diff --git a/lib/Contracts/IMailManager.php b/lib/Contracts/IMailManager.php index 134871189d..c3f72d1f44 100644 --- a/lib/Contracts/IMailManager.php +++ b/lib/Contracts/IMailManager.php @@ -108,7 +108,7 @@ public function getImapMessage(Horde_Imap_Client_Socket $client, * * @return Message[] */ - public function getThread(Account $account, string $threadRootId): array; + public function getThread(Account $account, string $threadRootId, string $sortOrder = IMailSearch::ORDER_NEWEST_FIRST): array; /** * @param Account $sourceAccount diff --git a/lib/Contracts/IMailSearch.php b/lib/Contracts/IMailSearch.php index 4bee40bca8..db8c8c44ee 100644 --- a/lib/Contracts/IMailSearch.php +++ b/lib/Contracts/IMailSearch.php @@ -42,7 +42,7 @@ public function findMessage(Account $account, * @param string|null $userId * @param string|null $view * - * @return Message[] + * @return Message[][] * * @throws ClientException * @throws ServiceException @@ -59,7 +59,7 @@ public function findMessages(Account $account, /** * Run a search through all mailboxes of a user. * - * @return Message[] + * @return Message[][] * * @throws ClientException * @throws ServiceException diff --git a/lib/Db/Message.php b/lib/Db/Message.php index 0972915b85..e7e1f0a222 100644 --- a/lib/Db/Message.php +++ b/lib/Db/Message.php @@ -142,6 +142,8 @@ class Message extends Entity implements JsonSerializable { /** @var bool */ private $fetchAvatarFromClient = false; + /** @var array */ + private $attachments = []; public function __construct() { $this->from = new AddressList([]); @@ -312,6 +314,14 @@ public function getAvatar(): ?Avatar { return $this->avatar; } + public function setAttachments(array $attachments): void { + $this->attachments = $attachments; + } + + public function getAttachments(): array { + return $this->attachments; + } + #[\Override] #[ReturnTypeWillChange] public function jsonSerialize() { @@ -359,6 +369,7 @@ static function (Tag $tag) { 'mentionsMe' => $this->getMentionsMe(), 'avatar' => $this->avatar?->jsonSerialize(), 'fetchAvatarFromClient' => $this->fetchAvatarFromClient, + 'attachments' => $this->getAttachments(), ]; } } diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 9068cb3771..baa486a6ca 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -756,10 +756,11 @@ public function deleteByUid(Mailbox $mailbox, int ...$uids): void { /** * @param Account $account * @param string $threadRootId + * @param string $sortOrder * * @return Message[] */ - public function findThread(Account $account, string $threadRootId): array { + public function findThread(Account $account, string $threadRootId, string $sortOrder): array { $qb = $this->db->getQueryBuilder(); $qb->select('messages.*') ->from($this->getTableName(), 'messages') @@ -768,7 +769,7 @@ public function findThread(Account $account, string $threadRootId): array { $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('messages.sent_at', 'desc'); + ->orderBy('messages.sent_at', $sortOrder); return $this->findRelatedData($this->findEntities($qb), $account->getUserId()); } @@ -1273,10 +1274,11 @@ public function findByUids(Mailbox $mailbox, array $uids): array { * @param Mailbox $mailbox * @param string $userId * @param int[] $ids + * @param string $sortOrder * * @return Message[] */ - public function findByMailboxAndIds(Mailbox $mailbox, string $userId, array $ids): array { + public function findByMailboxAndIds(Mailbox $mailbox, string $userId, array $ids, string $sortOrder): array { if ($ids === []) { return []; } @@ -1288,7 +1290,7 @@ public function findByMailboxAndIds(Mailbox $mailbox, string $userId, array $ids $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId()), IQueryBuilder::PARAM_INT), $qb->expr()->in('id', $qb->createParameter('ids')) ) - ->orderBy('sent_at', 'desc'); + ->orderBy('sent_at', $sortOrder); $results = []; foreach (array_chunk($ids, 1000) as $chunk) { @@ -1298,6 +1300,50 @@ public function findByMailboxAndIds(Mailbox $mailbox, string $userId, array $ids return array_merge([], ...$results); } + /** + * @param Account $account + * @param Mailbox $mailbox + * @param string $userId + * @param int[] $ids + * @param string $sortOrder + * @param bool $threadingEnabled + * + * @return Message[][] + */ + public function findMessageListsByMailboxAndIds(Account $account, Mailbox $mailbox, string $userId, array $ids, string $sortOrder, bool $threadingEnabled = false): array { + if ($ids === []) { + return []; + } + + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->eq('mailbox_id', $qb->createNamedParameter($mailbox->getId()), IQueryBuilder::PARAM_INT), + $qb->expr()->in('id', $qb->createParameter('ids')) + ) + ->orderBy('sent_at', $sortOrder); + $results = []; + foreach (array_chunk($ids, 1000) as $chunk) { + $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + if ($threadingEnabled) { + $res = $qb->executeQuery(); + while ($row = $res->fetch()) { + $message = $this->mapRowToEntity($row); + if ($message->getThreadRootId() === null) { + $results[] = [$message]; + } else { + $results[] = $this->findThread($account, $message->getThreadRootId(), $sortOrder); + } + } + $res->closeCursor(); + } else { + $results[] = array_map(fn (Message $msg) => [$msg], $this->findRelatedData($this->findEntities($qb), $userId)); + } + } + return $threadingEnabled ? $results : array_merge([], ...$results); + } + /** * @param string $userId * @param int[] $ids @@ -1325,6 +1371,48 @@ public function findByIds(string $userId, array $ids, string $sortOrder): array return array_merge([], ...$results); } + + /** + * @param Account $account + * @param string $userId + * @param int[] $ids + * @param string $sortOrder + * + * @return Message[][] + */ + public function findMessageListsByIds(Account $account, string $userId, array $ids, string $sortOrder, bool $threadingEnabled = false): array { + if ($ids === []) { + return []; + } + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where( + $qb->expr()->in('id', $qb->createParameter('ids')) + ) + ->orderBy('sent_at', $sortOrder); + + $results = []; + foreach (array_chunk($ids, 1000) as $chunk) { + $qb->setParameter('ids', $chunk, IQueryBuilder::PARAM_INT_ARRAY); + if ($threadingEnabled) { + $res = $qb->executeQuery(); + while ($row = $res->fetch()) { + $message = $this->mapRowToEntity($row); + if ($message->getThreadRootId() === null) { + $results[] = [$message]; + } else { + $results[] = $this->findThread($account, $message->getThreadRootId(), $sortOrder); + } + } + $res->closeCursor(); + } else { + $results[] = array_map(fn (Message $msg) => [$msg], $this->findRelatedData($this->findEntities($qb), $userId)); + } + } + return $threadingEnabled ? $results : array_merge([], ...$results); + } + /** * @param Message[] $messages * diff --git a/lib/IMAP/PreviewEnhancer.php b/lib/IMAP/PreviewEnhancer.php index eadc094cac..0021be7cdd 100644 --- a/lib/IMAP/PreviewEnhancer.php +++ b/lib/IMAP/PreviewEnhancer.php @@ -15,6 +15,7 @@ use OCA\Mail\Db\Message; use OCA\Mail\Db\MessageMapper as DbMapper; use OCA\Mail\IMAP\MessageMapper as ImapMapper; +use OCA\Mail\Service\Attachment\AttachmentService; use OCA\Mail\Service\Avatar\Avatar; use OCA\Mail\Service\AvatarService; use Psr\Log\LoggerInterface; @@ -39,11 +40,14 @@ class PreviewEnhancer { /** @var AvatarService */ private $avatarService; - public function __construct(IMAPClientFactory $clientFactory, + public function __construct( + IMAPClientFactory $clientFactory, ImapMapper $imapMapper, DbMapper $dbMapper, LoggerInterface $logger, - AvatarService $avatarService) { + AvatarService $avatarService, + private AttachmentService $attachmentService, + ) { $this->clientFactory = $clientFactory; $this->imapMapper = $imapMapper; $this->mapper = $dbMapper; @@ -52,9 +56,9 @@ public function __construct(IMAPClientFactory $clientFactory, } /** - * @param Message[] $messages + * @param Message[][] $messages * - * @return Message[] + * @return Message[][] */ public function process(Account $account, Mailbox $mailbox, array $messages, bool $preLoadAvatars = false, ?string $userId = null): array { $needAnalyze = array_reduce($messages, static function (array $carry, Message $message) { @@ -65,6 +69,12 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo return array_merge($carry, [$message->getUid()]); }, []); + $client = $this->clientFactory->getClient($account); + + foreach ($messages as $message) { + $attachments = $this->attachmentService->getAttachmentNames($account, $mailbox, $message, $client); + $message->setAttachments($attachments); + } if ($preLoadAvatars) { foreach ($messages as $message) { @@ -87,7 +97,7 @@ public function process(Account $account, Mailbox $mailbox, array $messages, boo return $messages; } - $client = $this->clientFactory->getClient($account); + try { $data = $this->imapMapper->getBodyStructureData( $client, diff --git a/lib/Model/IMAPMessage.php b/lib/Model/IMAPMessage.php index 3a973492f7..003c0add40 100644 --- a/lib/Model/IMAPMessage.php +++ b/lib/Model/IMAPMessage.php @@ -283,6 +283,7 @@ public function getSentDate(): Horde_Imap_Client_DateTime { return $this->imapDate; } + /** * @param int $id * @@ -384,7 +385,7 @@ public function setContent(string $content) { */ #[\Override] public function getAttachments(): array { - throw new Exception('not implemented'); + return $this->attachments; } /** diff --git a/lib/Service/Attachment/AttachmentService.php b/lib/Service/Attachment/AttachmentService.php index 97655a09af..dd3abe9ca5 100644 --- a/lib/Service/Attachment/AttachmentService.php +++ b/lib/Service/Attachment/AttachmentService.php @@ -18,7 +18,10 @@ use OCA\Mail\Db\LocalAttachment; use OCA\Mail\Db\LocalAttachmentMapper; use OCA\Mail\Db\LocalMessage; +use OCA\Mail\Db\Mailbox; +use OCA\Mail\Db\Message; use OCA\Mail\Exception\AttachmentNotFoundException; +use OCA\Mail\Exception\ServiceException; use OCA\Mail\Exception\UploadException; use OCA\Mail\IMAP\MessageMapper; use OCP\AppFramework\Db\DoesNotExistException; @@ -26,6 +29,7 @@ use OCP\Files\Folder; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; +use OCP\ICacheFactory; use Psr\Log\LoggerInterface; class AttachmentService implements IAttachmentService { @@ -51,6 +55,10 @@ class AttachmentService implements IAttachmentService { * @var LoggerInterface */ private $logger; + /** + * @var ICache + */ + private $cache; /** * @param Folder $userFolder @@ -60,6 +68,7 @@ public function __construct($userFolder, AttachmentStorage $storage, IMailManager $mailManager, MessageMapper $imapMessageMapper, + ICacheFactory $cacheFactory, LoggerInterface $logger) { $this->mapper = $mapper; $this->storage = $storage; @@ -67,6 +76,7 @@ public function __construct($userFolder, $this->messageMapper = $imapMessageMapper; $this->userFolder = $userFolder; $this->logger = $logger; + $this->cache = $cacheFactory->createLocal('mail.attachment_names'); } /** @@ -249,6 +259,32 @@ public function handleAttachments(Account $account, array $attachments, \Horde_I return array_values(array_filter($attachmentIds)); } + public function getAttachmentNames(Account $account, Mailbox $mailbox, Message $message, \Horde_Imap_Client_Socket $client): array { + $attachments = []; + $uniqueCacheId = $account->getUserId() . $account->getId() . $mailbox->getId() . $message->getUid(); + $cached = $this->cache->get($uniqueCacheId); + if ($cached) { + return $cached; + } + try { + $imapMessage = $this->mailManager->getImapMessage( + $client, + $account, + $mailbox, + $message->getUid(), + true + ); + $attachments = $imapMessage->getAttachments(); + } catch (ServiceException $e) { + $this->logger->error('Could not get attachment names', ['exception' => $e, 'messageId' => $message->getUid()]); + } + $result = array_map(static function (array $attachment) { + return ['name' => $attachment['fileName'],'mime' => $attachment['mime']]; + }, $attachments); + $this->cache->set($uniqueCacheId, $result); + return $result; + } + /** * Add a message as attachment * diff --git a/lib/Service/MailManager.php b/lib/Service/MailManager.php index 2491fe2239..d956910cba 100644 --- a/lib/Service/MailManager.php +++ b/lib/Service/MailManager.php @@ -17,6 +17,7 @@ use OCA\Mail\Account; use OCA\Mail\Attachment; use OCA\Mail\Contracts\IMailManager; +use OCA\Mail\Contracts\IMailSearch; use OCA\Mail\Db\Mailbox; use OCA\Mail\Db\MailboxMapper; use OCA\Mail\Db\Message; @@ -236,8 +237,8 @@ public function getImapMessagesForScheduleProcessing(Account $account, } #[\Override] - public function getThread(Account $account, string $threadRootId): array { - return $this->dbMessageMapper->findThread($account, $threadRootId); + public function getThread(Account $account, string $threadRootId, string $sortOrder = IMailSearch::ORDER_NEWEST_FIRST): array { + return $this->dbMessageMapper->findThread($account, $threadRootId, $sortOrder); } #[\Override] diff --git a/lib/Service/Search/MailSearch.php b/lib/Service/Search/MailSearch.php index 16f8ada442..aee950a72b 100644 --- a/lib/Service/Search/MailSearch.php +++ b/lib/Service/Search/MailSearch.php @@ -77,7 +77,7 @@ public function findMessage(Account $account, * @param int|null $limit * @param string|null $view * - * @return Message[] + * @return Message[][] * * @throws ClientException * @throws ServiceException @@ -102,8 +102,9 @@ public function findMessages(Account $account, if ($cursor !== null) { $query->setCursor($cursor); } + $threadingEnabled = $view === self::VIEW_THREADED; if ($view !== null) { - $query->setThreaded($view === self::VIEW_THREADED); + $query->setThreaded($threadingEnabled); } // In flagged we don't want anything but flagged messages if ($mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_FLAGGED)) { @@ -113,17 +114,23 @@ public function findMessages(Account $account, if (!$mailbox->isSpecialUse(Horde_Imap_Client::SPECIALUSE_TRASH)) { $query->addFlag(Flag::not(Flag::DELETED)); } - - return $this->previewEnhancer->process( - $account, - $mailbox, - $this->messageMapper->findByIds($account->getUserId(), - $this->getIdsLocally($account, $mailbox, $query, $sortOrder, $limit), - $sortOrder, - ), - true, - $userId + $messages = $this->messageMapper->findMessageListsByIds($account, $account->getUserId(), + $this->getIdsLocally($account, $mailbox, $query, $sortOrder, $limit), + $sortOrder, + $threadingEnabled ); + $processedMessages = []; + foreach ($messages as $messageList) { + $processedMessages[] = $this->previewEnhancer->process( + $account, + $mailbox, + $messageList, + true, + $userId + ); + } + + return $processedMessages; } /** diff --git a/lib/Service/Sync/SyncService.php b/lib/Service/Sync/SyncService.php index 2d1d1181d9..454b3ebf8f 100644 --- a/lib/Service/Sync/SyncService.php +++ b/lib/Service/Sync/SyncService.php @@ -24,6 +24,7 @@ use OCA\Mail\IMAP\Sync\Response; use OCA\Mail\Service\Search\FilterStringParser; use OCA\Mail\Service\Search\SearchQuery; +use OCP\IAppConfig; use Psr\Log\LoggerInterface; use function array_diff; use function array_map; @@ -50,6 +51,9 @@ class SyncService { /** @var MailboxSync */ private $mailboxSync; + /** @var IAppConfig */ + private $config; + public function __construct( IMAPClientFactory $clientFactory, ImapToDbSynchronizer $synchronizer, @@ -57,7 +61,9 @@ public function __construct( MessageMapper $messageMapper, PreviewEnhancer $previewEnhancer, LoggerInterface $logger, - MailboxSync $mailboxSync) { + MailboxSync $mailboxSync, + IAppConfig $config, + ) { $this->clientFactory = $clientFactory; $this->synchronizer = $synchronizer; $this->filterStringParser = $filterStringParser; @@ -65,6 +71,7 @@ public function __construct( $this->previewEnhancer = $previewEnhancer; $this->logger = $logger; $this->mailboxSync = $mailboxSync; + $this->config = $config; } /** @@ -129,6 +136,7 @@ public function syncMailbox(Account $account, $this->mailboxSync->syncStats($client, $mailbox); + $threadingEnabled = $this->config->getValueString('mail', 'layout-message-view', 'threaded') === 'threaded'; $client->logout(); $query = $filter === null ? null : $this->filterStringParser->parse($filter); @@ -138,7 +146,8 @@ public function syncMailbox(Account $account, $knownIds ?? [], $lastMessageTimestamp, $sortOrder, - $query + $query, + $threadingEnabled ); } @@ -147,6 +156,7 @@ public function syncMailbox(Account $account, * @param Mailbox $mailbox * @param int[] $knownIds * @param SearchQuery $query + * @param bool $threadingEnabled * * @return Response * @todo does not work with text token search queries @@ -157,7 +167,8 @@ private function getDatabaseSyncChanges(Account $account, array $knownIds, ?int $lastMessageTimestamp, string $sortOrder, - ?SearchQuery $query): Response { + ?SearchQuery $query, + bool $threadingEnabled): Response { if ($knownIds === []) { $newIds = $this->messageMapper->findAllIds($mailbox); } else { @@ -169,7 +180,13 @@ private function getDatabaseSyncChanges(Account $account, $newUids = $this->messageMapper->findUidsForIds($mailbox, $newIds); $newIds = $this->messageMapper->findIdsByQuery($mailbox, $query, $order, null, $newUids); } - $new = $this->messageMapper->findByMailboxAndIds($mailbox, $account->getUserId(), $newIds); + + $new = $this->messageMapper->findMessageListsByMailboxAndIds($account, $mailbox, $account->getUserId(), $newIds, $sortOrder, $threadingEnabled); + + $newMessages = []; + foreach ($new as $messageList) { + $newMessages[] = $this->previewEnhancer->process($account, $mailbox, $messageList); + } // TODO: $changed = $this->messageMapper->findChanged($account, $mailbox, $uids); if ($query !== null) { @@ -178,7 +195,7 @@ private function getDatabaseSyncChanges(Account $account, } else { $changedIds = $knownIds; } - $changed = $this->messageMapper->findByMailboxAndIds($mailbox, $account->getUserId(), $changedIds); + $changed = $this->messageMapper->findByMailboxAndIds($mailbox, $account->getUserId(), $changedIds, $sortOrder); $stillKnownIds = array_map(static function (Message $msg) { return $msg->getId(); @@ -186,7 +203,7 @@ private function getDatabaseSyncChanges(Account $account, $vanished = array_values(array_diff($knownIds, $stillKnownIds)); return new Response( - $this->previewEnhancer->process($account, $mailbox, $new), + $newMessages, $changed, $vanished, $mailbox->getStats() diff --git a/src/components/AttachmentTag.vue b/src/components/AttachmentTag.vue new file mode 100644 index 0000000000..cf8c7aad1f --- /dev/null +++ b/src/components/AttachmentTag.vue @@ -0,0 +1,43 @@ + + +
+ + + + diff --git a/src/components/Envelope.vue b/src/components/Envelope.vue index 53518fc416..8d54b6dddd 100644 --- a/src/components/Envelope.vue +++ b/src/components/Envelope.vue @@ -332,6 +332,9 @@ {{ translateTagDisplayName(tag) }} +