Skip to content

Commit

Permalink
Merge pull request #8897 from nextcloud/feat/search-body
Browse files Browse the repository at this point in the history
Feat: Search mail bodies
  • Loading branch information
ChristophWurst authored Oct 25, 2023
2 parents fe783a6 + eb93d46 commit 7cb233d
Show file tree
Hide file tree
Showing 14 changed files with 225 additions and 43 deletions.
6 changes: 5 additions & 1 deletion lib/Controller/AccountsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)
);
Expand Down
7 changes: 7 additions & 0 deletions lib/Db/MailAccount.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -190,6 +192,9 @@ class MailAccount extends Entity {

protected ?int $junkMailboxId = null;

/** @var bool */
protected $searchBody = false;

/**
* @param array $params
*/
Expand Down Expand Up @@ -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');
}

/**
Expand Down Expand Up @@ -298,6 +304,7 @@ public function toJson() {
'quotaPercentage' => $this->getQuotaPercentage(),
'trashRetentionDays' => $this->getTrashRetentionDays(),
'junkMailboxId' => $this->getJunkMailboxId(),
'searchBody' => $this->getSearchBody(),
];

if (!is_null($this->getOutboundHost())) {
Expand Down
14 changes: 11 additions & 3 deletions lib/Db/MessageMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
Expand Down
4 changes: 2 additions & 2 deletions lib/IMAP/Search/Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
55 changes: 55 additions & 0 deletions lib/Migration/Version3500Date20231009102414.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

/**
* @author 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\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;
}
}
10 changes: 4 additions & 6 deletions lib/Service/Search/FilterStringParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion lib/Service/Search/MailSearch.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
15 changes: 7 additions & 8 deletions lib/Service/Search/SearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ class SearchQuery {
/** @var string[] */
private $subjects = [];

/** @var string[] */
private $bodies = [];

/** @var string[] */
private $textTokens = [];

Expand Down Expand Up @@ -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;
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/components/AccountSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@
:account="account" />
</div>
</AppSettingsSection>
<AppSettingsSection id="mailbox_search" :title="t('mail', 'Mailbox search')">
<SearchSettings :account="account" />
</AppSettingsSection>
</AppSettingsDialog>
</template>

Expand All @@ -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'

Expand All @@ -146,6 +150,7 @@ export default {
OutOfOfficeForm,
CertificateSettings,
TrashRetentionSettings,
SearchSettings,
},
props: {
account: {
Expand Down
1 change: 1 addition & 0 deletions src/components/MailboxThread.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
:class="{ header__button: !showThread || !isMobile }">
<SearchMessages v-if="!showThread || !isMobile"
:mailbox="mailbox"
:account-id="account.accountId"
@search-changed="onUpdateSearchQuery" />
<AppContentList
v-infinite-scroll="onScroll"
Expand Down
60 changes: 41 additions & 19 deletions src/components/SearchMessages.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,29 @@
</h2>
<div class="modal-inner--content">
<div class="modal-inner--field">
<label class="modal-inner--label" for="fromId">
{{ t('mail','Search term') }}
<label class="modal-inner--label" for="subjectId">
{{ t('mail','Subject') }}
</label>
<div class="modal-inner--container">
<input
v-model="modalQuery"
id="subjectId"
v-model="searchInSubject"
type="text"
class="search-input"
:placeholder="t('mail', 'Search in mailbox')">
:placeholder="t('mail', 'Search subject')">
</div>
</div>
<div class="modal-inner--field">
<label class="modal-inner--label" for="bodyId">
{{ t('mail','Body') }}
</label>
<div class="modal-inner--container">
<input
id="bodyId"
v-model="searchInMessageBody"
type="text"
class="search-input"
:placeholder="t('mail', 'Search body')">
</div>
</div>
<div class="modal-inner--field">
Expand Down Expand Up @@ -316,10 +330,13 @@ export default {
type: Object,
required: true,
},
accountId: {
type: Number,
required: true,
},
},
data() {
return {
modalQuery: '',
query: '',
debouncedSearchQuery: debouncePromise(this.sendQueryEvent, 700),
autocompleteRecipients: [],
Expand All @@ -329,8 +346,8 @@ export default {
searchInTo: null,
searchInCc: null,
searchInBcc: null,
searchInSubject: true,
searchInMessageBody: false,
searchInSubject: null,
searchInMessageBody: null,
searchFlags: [],
hasAttachments: false,
startDate: null,
Expand Down Expand Up @@ -360,13 +377,17 @@ export default {
return val !== ''
}).length > 0
},
searchBody() {
return this.$store.getters.getAccount(this.accountId)?.searchBody
},
filterData() {
return {
to: this.searchInTo !== null && this.searchInTo.length > 0 ? this.searchInTo[0].email : '',
from: this.searchInFrom !== null && this.searchInFrom.length > 0 ? this.searchInFrom[0].email : '',
cc: this.searchInCc !== null && this.searchInCc.length > 0 ? this.searchInCc[0].email : '',
bcc: this.searchInBcc !== null && this.searchInBcc.length > 0 ? this.searchInBcc[0].email : '',
subject: this.searchInSubject && this.query.length > 1 ? this.query : '',
subject: this.searchInSubject !== null && this.searchInSubject.length > 1 ? this.searchInSubject : '',
body: this.searchInMessageBody !== null && this.searchInMessageBody.length > 1 ? this.searchInMessageBody : '',
tags: this.selectedTags.length > 0 ? this.selectedTags.map(item => item.id) : '',
flags: this.searchFlags.length > 0 ? this.searchFlags.map(item => item) : '',
start: this.prepareStart(),
Expand All @@ -377,7 +398,13 @@ export default {
searchQuery() {
let _search = ''
Object.entries(this.filterData).filter(([key, val]) => {
if (val !== '' && val !== null) {
if (key === 'body') {
val.split(' ').forEach((word) => {
if (word !== '' && val !== null) {
_search += `${key}:${encodeURI(word)} `
}
})
} else if (val !== '' && val !== null) {
_search += `${key}:${encodeURI(val)} `
}
return val
Expand All @@ -388,9 +415,8 @@ export default {
},
watch: {
query() {
if (this.query !== this.modalQuery) {
this.modalQuery = this.query
}
this.searchInMessageBody = this.searchBody ? this.query : null
this.searchInSubject = this.query
this.debouncedSearchQuery()
},
},
Expand All @@ -410,11 +436,7 @@ export default {
closeSearchModal() {
this.moreSearchActions = false
this.$nextTick(() => {
if (this.query !== this.modalQuery) {
this.query = this.modalQuery
} else {
this.sendQueryEvent()
}
this.sendQueryEvent()
})
},
Expand All @@ -440,8 +462,8 @@ export default {
this.searchInTo = null
this.searchInCc = null
this.searchInBcc = null
this.searchInSubject = true
this.searchInMessageBody = false
this.searchInSubject = null
this.searchInMessageBody = null
this.searchFlags = []
this.startDate = null
this.endDate = null
Expand Down
Loading

0 comments on commit 7cb233d

Please sign in to comment.