From eb93d46102f3cf0f8ca1b93b8f2ff039a6e5414d Mon Sep 17 00:00:00 2001 From: hamza221 Date: Wed, 27 Sep 2023 15:50:23 +0200 Subject: [PATCH] Feat: Search mail bodies Signed-off-by: hamza221 --- lib/Controller/AccountsController.php | 6 +- lib/Db/MailAccount.php | 7 ++ lib/Db/MessageMapper.php | 14 +++- lib/IMAP/Search/Provider.php | 4 +- .../Version3500Date20231009102414.php | 55 +++++++++++++ lib/Service/Search/FilterStringParser.php | 10 +-- lib/Service/Search/MailSearch.php | 2 +- lib/Service/Search/SearchQuery.php | 15 ++-- src/components/AccountSettings.vue | 5 ++ src/components/MailboxThread.vue | 1 + src/components/SearchMessages.vue | 60 +++++++++----- src/components/SearchSettings.vue | 81 +++++++++++++++++++ tests/Integration/Db/MailAccountTest.php | 4 +- tests/Unit/Service/Search/MailSearchTest.php | 4 +- 14 files changed, 225 insertions(+), 43 deletions(-) create mode 100644 lib/Migration/Version3500Date20231009102414.php create mode 100644 src/components/SearchSettings.vue diff --git a/lib/Controller/AccountsController.php b/lib/Controller/AccountsController.php index b3b37c8065..0ddcefc168 100644 --- a/lib/Controller/AccountsController.php +++ b/lib/Controller/AccountsController.php @@ -245,7 +245,8 @@ public function patchAccount(int $id, int $snoozeMailboxId = null, bool $signatureAboveQuote = null, int $trashRetentionDays = null, - int $junkMailboxId = null): JSONResponse { + int $junkMailboxId = null, + bool $searchBody = null): JSONResponse { $account = $this->accountService->find($this->currentUserId, $id); $dbAccount = $account->getMailAccount(); @@ -290,6 +291,9 @@ public function patchAccount(int $id, $this->mailManager->getMailbox($this->currentUserId, $junkMailboxId); $dbAccount->setJunkMailboxId($junkMailboxId); } + if($searchBody !== null) { + $dbAccount->setSearchBody($searchBody); + } return new JSONResponse( $this->accountService->save($dbAccount) ); diff --git a/lib/Db/MailAccount.php b/lib/Db/MailAccount.php index 683234c79c..3744b1414a 100644 --- a/lib/Db/MailAccount.php +++ b/lib/Db/MailAccount.php @@ -114,6 +114,8 @@ * @method void setTrashRetentionDays(int|null $trashRetentionDays) * @method int|null getJunkMailboxId() * @method void setJunkMailboxId(?int $id) + * @method bool getSearchBody() + * @method void setSearchBody(bool $searchBody) */ class MailAccount extends Entity { public const SIGNATURE_MODE_PLAIN = 0; @@ -190,6 +192,9 @@ class MailAccount extends Entity { protected ?int $junkMailboxId = null; + /** @var bool */ + protected $searchBody = false; + /** * @param array $params */ @@ -265,6 +270,7 @@ public function __construct(array $params = []) { $this->addType('quotaPercentage', 'integer'); $this->addType('trashRetentionDays', 'integer'); $this->addType('junkMailboxId', 'integer'); + $this->addType('searchBody', 'boolean'); } /** @@ -298,6 +304,7 @@ public function toJson() { 'quotaPercentage' => $this->getQuotaPercentage(), 'trashRetentionDays' => $this->getTrashRetentionDays(), 'junkMailboxId' => $this->getJunkMailboxId(), + 'searchBody' => $this->getSearchBody(), ]; if (!is_null($this->getOutboundHost())) { diff --git a/lib/Db/MessageMapper.php b/lib/Db/MessageMapper.php index 086686aa72..4336bf6484 100644 --- a/lib/Db/MessageMapper.php +++ b/lib/Db/MessageMapper.php @@ -836,9 +836,17 @@ public function findIdsByQuery(Mailbox $mailbox, SearchQuery $query, ?int $limit // createParameter if ($uids !== null) { - $select->andWhere( - $qb->expr()->in('m.uid', $qb->createParameter('uids')) - ); + // In the case of body+subject search we need a combination of both results, + // thus the orWhere in every other case andWhere should do the job. + if(!empty($query->getSubjects())) { + $select->orWhere( + $qb->expr()->in('m.uid', $qb->createParameter('uids')) + ); + } else { + $select->andWhere( + $qb->expr()->in('m.uid', $qb->createParameter('uids')) + ); + } } foreach ($query->getFlags() as $flag) { $select->andWhere($qb->expr()->eq('m.' . $this->flagToColumnName($flag), $qb->createNamedParameter($flag->isSet(), IQueryBuilder::PARAM_BOOL))); diff --git a/lib/IMAP/Search/Provider.php b/lib/IMAP/Search/Provider.php index 0648d81f0e..bd4fd92476 100644 --- a/lib/IMAP/Search/Provider.php +++ b/lib/IMAP/Search/Provider.php @@ -73,9 +73,9 @@ public function findMatches(Account $account, */ private function convertMailQueryToHordeQuery(SearchQuery $searchQuery): Horde_Imap_Client_Search_Query { return array_reduce( - $searchQuery->getTextTokens(), + $searchQuery->getBodies(), static function (Horde_Imap_Client_Search_Query $query, string $textToken) { - $query->text($textToken, false); + $query->text($textToken, true); return $query; }, new Horde_Imap_Client_Search_Query() diff --git a/lib/Migration/Version3500Date20231009102414.php b/lib/Migration/Version3500Date20231009102414.php new file mode 100644 index 0000000000..5f27f9e35f --- /dev/null +++ b/lib/Migration/Version3500Date20231009102414.php @@ -0,0 +1,55 @@ + + * + * @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\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version3500Date20231009102414 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $mailAccountsTable = $schema->getTable('mail_accounts'); + if (!$mailAccountsTable->hasColumn('search_body')) { + $mailAccountsTable->addColumn('search_body', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + } + + return $schema; + } +} diff --git a/lib/Service/Search/FilterStringParser.php b/lib/Service/Search/FilterStringParser.php index 99b4a1efa0..55242db6be 100644 --- a/lib/Service/Search/FilterStringParser.php +++ b/lib/Service/Search/FilterStringParser.php @@ -35,12 +35,7 @@ public function parse(?string $filter): SearchQuery { } $tokens = explode(' ', $filter); foreach ($tokens as $token) { - if (!$this->parseFilterToken($query, $token)) { - $query->addTextToken($token); - - // Always look into the subject as well - $query->addSubject($token); - } + $this->parseFilterToken($query, $token); } return $query; @@ -107,6 +102,9 @@ private function parseFilterToken(SearchQuery $query, string $token): bool { case 'subject': $query->addSubject($param); return true; + case 'body': + $query->addBody($param); + return true; case 'tags': $tags = explode(',', $param); $query->setTags($tags); diff --git a/lib/Service/Search/MailSearch.php b/lib/Service/Search/MailSearch.php index c6e25f653f..c4068efaee 100644 --- a/lib/Service/Search/MailSearch.php +++ b/lib/Service/Search/MailSearch.php @@ -165,7 +165,7 @@ public function findMessagesGlobally(IUser $user, * @throws ServiceException */ private function getIdsLocally(Account $account, Mailbox $mailbox, SearchQuery $query, ?int $limit): array { - if (empty($query->getTextTokens())) { + if (empty($query->getBodies())) { return $this->messageMapper->findIdsByQuery($mailbox, $query, $limit); } diff --git a/lib/Service/Search/SearchQuery.php b/lib/Service/Search/SearchQuery.php index 7e56497a34..2450950699 100644 --- a/lib/Service/Search/SearchQuery.php +++ b/lib/Service/Search/SearchQuery.php @@ -50,6 +50,9 @@ class SearchQuery { /** @var string[] */ private $subjects = []; + /** @var string[] */ + private $bodies = []; + /** @var string[] */ private $textTokens = []; @@ -154,16 +157,12 @@ public function getSubjects(): array { public function addSubject(string $subject): void { $this->subjects[] = $subject; } - - /** - * @return string[] - */ - public function getTextTokens(): array { - return $this->textTokens; + public function getBodies(): array { + return $this->bodies; } - public function addTextToken(string $textToken): void { - $this->textTokens[] = $textToken; + public function addBody(string $body): void { + $this->bodies[] = $body; } /** diff --git a/src/components/AccountSettings.vue b/src/components/AccountSettings.vue index fb11574b7f..0dc4c30cbc 100644 --- a/src/components/AccountSettings.vue +++ b/src/components/AccountSettings.vue @@ -112,6 +112,9 @@ :account="account" /> + + + @@ -127,6 +130,7 @@ import SieveAccountForm from './SieveAccountForm' import SieveFilterForm from './SieveFilterForm' import OutOfOfficeForm from './OutOfOfficeForm' import CertificateSettings from './CertificateSettings' +import SearchSettings from './SearchSettings' import TrashRetentionSettings from './TrashRetentionSettings' import logger from '../logger' @@ -146,6 +150,7 @@ export default { OutOfOfficeForm, CertificateSettings, TrashRetentionSettings, + SearchSettings, }, props: { account: { diff --git a/src/components/MailboxThread.vue b/src/components/MailboxThread.vue index 81fb3af76d..26a9314694 100644 --- a/src/components/MailboxThread.vue +++ b/src/components/MailboxThread.vue @@ -4,6 +4,7 @@ :class="{ header__button: !showThread || !isMobile }">