feat(advanced-search): allow date and recipient search
Signed-off-by: Johannes Merkel <mail@johannesgge.de>
This commit is contained in:
Родитель
b415472563
Коммит
8e12c633dd
|
@ -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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче