feat(advanced-search): allow date and recipient search

Signed-off-by: Johannes Merkel <mail@johannesgge.de>
This commit is contained in:
Johannes Merkel 2023-10-25 17:19:39 +02:00 коммит произвёл Christoph Wurst
Родитель b415472563
Коммит 8e12c633dd
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: CC42AC2A7F0E56D8
7 изменённых файлов: 272 добавлений и 3 удалений

Просмотреть файл

@ -70,6 +70,7 @@ use OCA\Mail\Listener\SaveSentMessageListener;
use OCA\Mail\Listener\SpamReportListener;
use OCA\Mail\Listener\UserDeletedListener;
use OCA\Mail\Notification\Notifier;
use OCA\Mail\Search\FilteringProvider;
use OCA\Mail\Search\Provider;
use OCA\Mail\Service\Attachment\AttachmentService;
use OCA\Mail\Service\AvatarService;
@ -86,9 +87,11 @@ use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\Dashboard\IAPIWidgetV2;
use OCP\IServerContainer;
use OCP\Search\IFilteringProvider;
use OCP\User\Events\UserDeletedEvent;
use OCP\Util;
use Psr\Container\ContainerInterface;
use function interface_exists;
include_once __DIR__ . '/../../vendor/autoload.php';
@ -150,7 +153,11 @@ class Application extends App implements IBootstrap {
$context->registerDashboardWidget(UnreadMailWidget::class);
}
$context->registerSearchProvider(Provider::class);
if (interface_exists(IFilteringProvider::class)) {
$context->registerSearchProvider(FilteringProvider::class);
} else {
$context->registerSearchProvider(Provider::class);
}
$context->registerNotifierService(Notifier::class);

Просмотреть файл

@ -966,6 +966,18 @@ class MessageMapper extends QBMapper {
);
}
if (!empty($query->getStart())) {
$select->andWhere(
$qb->expr()->gte('m.sent_at', $qb->createNamedParameter($query->getStart()), IQueryBuilder::PARAM_INT)
);
}
if (!empty($query->getEnd())) {
$select->andWhere(
$qb->expr()->lte('m.sent_at', $qb->createNamedParameter($query->getEnd()), IQueryBuilder::PARAM_INT)
);
}
if ($query->getCursor() !== null) {
$select->andWhere(
$qb->expr()->lt('m.sent_at', $qb->createNamedParameter($query->getCursor(), IQueryBuilder::PARAM_INT))

Просмотреть файл

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
/*
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Search;
use DateTimeImmutable;
use OCP\IUser;
use OCP\Search\IFilteringProvider;
use OCP\Search\ISearchQuery;
use OCP\Search\SearchResult;
use function implode;
class FilteringProvider extends Provider implements IFilteringProvider {
public function search(IUser $user, ISearchQuery $query): SearchResult {
$filters = [];
if ($term = $query->getFilter('term')?->get()) {
if (is_string($term)) {
$filters[] = "subject:$term";
}
}
if ($since = $query->getFilter('since')?->get()) {
if ($since instanceof DateTimeImmutable) {
$ts = $since->getTimestamp();
$filters[] = "start:$ts";
}
}
if ($until = $query->getFilter('until')?->get()) {
if ($until instanceof DateTimeImmutable) {
$ts = $until->getTimestamp();
$filters[] = "end:$ts";
}
}
if ($userFilter = $query->getFilter('person')?->get()) {
if ($userFilter instanceof IUser) {
$email = $userFilter->getEMailAddress();
if ($email !== null) {
$filters[] = "from:$email";
$filters[] = "to:$email";
$filters[] = "cc:$email";
}
}
}
if (count($filters) === 0) {
return SearchResult::complete(
$this->getName(),
[]
);
}
return $this->searchByFilter($user, $query, implode(' ', $filters));
}
public function getSupportedFilters(): array {
return [
'term',
'since',
'until',
'person',
];
}
public function getAlternateIds(): array {
return [];
}
public function getCustomFilters(): array {
return [];
}
}

Просмотреть файл

@ -79,11 +79,15 @@ class Provider implements IProvider {
}
public function search(IUser $user, ISearchQuery $query): SearchResult {
return $this->searchByFilter($user, $query, $query->getTerm());
}
protected function searchByFilter(IUser $user, ISearchQuery $query, string $filter): SearchResult {
$cursor = $query->getCursor();
$messages = $this->mailSearch->findMessagesGlobally(
$user,
$query->getTerm(),
empty($cursor) ? null : ((int) $cursor),
$filter,
empty($cursor) ? null : ((int)$cursor),
$query->getLimit()
);

Просмотреть файл

@ -42,6 +42,7 @@
<referencedClass name="Symfony\Component\Console\Input\InputInterface" />
<referencedClass name="Symfony\Component\Console\Input\InputOption" />
<referencedClass name="Symfony\Component\Console\Output\OutputInterface" />
<referencedClass name="OCP\Search\IFilteringProvider" /><!-- 28+ -->
<referencedClass name="OCP\TextProcessing\IManager" />
<referencedClass name="OCP\TextProcessing\SummaryTaskType" />
<referencedClass name="OCP\TextProcessing\Task" />

Просмотреть файл

@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
/*
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
*
* @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\Tests\Unit\Search;
use ChristophWurst\Nextcloud\Testing\ServiceMockObject;
use ChristophWurst\Nextcloud\Testing\TestCase;
use OCA\Mail\AddressList;
use OCA\Mail\Db\Message;
use OCA\Mail\Search\FilteringProvider;
use OCP\IUser;
use OCP\Search\IFilter;
use OCP\Search\IFilteringProvider;
use OCP\Search\ISearchQuery;
use function interface_exists;
/**
* @covers \OCA\Mail\Search\FilteringProvider
*/
class FilteringProviderTest extends TestCase {
private ServiceMockObject $serviceMock;
private FilteringProvider $provider;
protected function setUp(): void {
parent::setUp();
if (!interface_exists(IFilteringProvider::class)) {
$this->markTestSkipped('Base class missing');
}
$this->serviceMock = $this->createServiceMock(FilteringProvider::class);
$this->provider = $this->serviceMock->getService();
}
public function testSearchForTerm(): void {
$term = 'spam';
$user = $this->createMock(IUser::class);
$query = $this->createMock(ISearchQuery::class);
$termFilter = $this->createMock(IFilter::class);
$termFilter->method('get')->willReturn($term);
$query->method('getFilter')->willReturnCallback(function ($filter) use ($termFilter) {
return match ($filter) {
'term' => $termFilter,
default => null,
};
});
$message1 = new Message();
$message1->setSubject('This is not spam');
$message1->setFrom(AddressList::parse('Sender <sender@domain.tld>'));
$this->serviceMock->getParameter('mailSearch')
->expects(self::once())
->method('findMessagesGlobally')
->with(
$user,
'subject:spam'
)
->willReturn([
$message1,
]);
$result = $this->provider->search(
$user,
$query,
);
self::assertNotEmpty($result->jsonSerialize()['entries'] ?? []);
}
public function testSearchForUserNoEmail(): void {
$user = $this->createMock(IUser::class);
$otherUser = $this->createMock(IUser::class);
$query = $this->createMock(ISearchQuery::class);
$termFilter = $this->createMock(IFilter::class);
$termFilter->method('get')->willReturn($otherUser);
$query->method('getFilter')->willReturnCallback(function ($filter) use ($termFilter) {
return match ($filter) {
'person' => $termFilter,
default => null,
};
});
$this->serviceMock->getParameter('mailSearch')
->expects(self::never())
->method('findMessagesGlobally');
$result = $this->provider->search(
$user,
$query,
);
self::assertEmpty($result->jsonSerialize()['entries'] ?? []);
}
public function testSearchForUser(): void {
$user = $this->createMock(IUser::class);
$otherUser = $this->createMock(IUser::class);
$otherUser->method('getEMailAddress')->willReturn('other@domain.tld');
$query = $this->createMock(ISearchQuery::class);
$userFilter = $this->createMock(IFilter::class);
$userFilter->method('get')->willReturn($otherUser);
$query->method('getFilter')->willReturnCallback(function ($filter) use ($userFilter) {
return match ($filter) {
'person' => $userFilter,
default => null,
};
});
$message1 = new Message();
$message1->setSubject('This is not spam');
$message1->setFrom(AddressList::parse('Other <other@domain.tld>'));
$this->serviceMock->getParameter('mailSearch')
->expects(self::once())
->method('findMessagesGlobally')
->with(
$user,
"from:other@domain.tld to:other@domain.tld cc:other@domain.tld"
)
->willReturn([
$message1,
]);
$result = $this->provider->search(
$user,
$query,
);
self::assertNotEmpty($result->jsonSerialize()['entries'] ?? []);
}
}

Просмотреть файл

@ -2,6 +2,7 @@
<files psalm-version="5.14.1@b9d355e0829c397b9b3b47d0c0ed042a8a70284d">
<file src="lib/AppInfo/Application.php">
<MissingDependency>
<code>FilteringProvider</code>
<code>ImportantMailWidgetV2</code>
<code>UnreadMailWidgetV2</code>
</MissingDependency>