Skip to content

Commit 2fd5134

Browse files
authored
Merge pull request #11002 from nextcloud/bug/8849/hide-trash-widget
fix: don't show important or unread emails in trash
2 parents c96ec6e + ce2f949 commit 2fd5134

File tree

10 files changed

+348
-106
lines changed

10 files changed

+348
-106
lines changed

lib/Contracts/IMailSearch.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use OCA\Mail\Db\Message;
1515
use OCA\Mail\Exception\ClientException;
1616
use OCA\Mail\Exception\ServiceException;
17+
use OCA\Mail\Service\Search\SearchQuery;
1718
use OCP\AppFramework\Db\DoesNotExistException;
1819
use OCP\IUser;
1920

@@ -50,14 +51,12 @@ public function findMessages(Account $account,
5051
?int $limit): array;
5152

5253
/**
53-
* @param IUser $user
54-
* @param string|null $filter
55-
* @param int|null $cursor
54+
* Run a search through all mailboxes of a user.
5655
*
5756
* @return Message[]
5857
*
5958
* @throws ClientException
6059
* @throws ServiceException
6160
*/
62-
public function findMessagesGlobally(IUser $user, ?string $filter, ?int $cursor, ?int $limit): array;
61+
public function findMessagesGlobally(IUser $user, SearchQuery $query, ?int $limit): array;
6362
}

lib/Dashboard/ImportantMailWidget.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
namespace OCA\Mail\Dashboard;
1111

12+
use OCA\Mail\Service\Search\Flag;
13+
use OCA\Mail\Service\Search\GlobalSearchQuery;
14+
use OCA\Mail\Service\Search\SearchQuery;
15+
1216
class ImportantMailWidget extends MailWidget {
1317
/**
1418
* @inheritDoc
@@ -24,10 +28,10 @@ public function getTitle(): string {
2428
return $this->l10n->t('Important mail');
2529
}
2630

27-
/**
28-
* @inheritDoc
29-
*/
30-
public function getSearchFilter(): string {
31-
return 'is:important';
31+
public function getSearchQuery(string $userId): SearchQuery {
32+
$query = new GlobalSearchQuery();
33+
$query->addFlag(Flag::is(Flag::IMPORTANT));
34+
$query->setExcludeMailboxIds($this->getMailboxIdsToExclude($userId));
35+
return $query;
3236
}
3337
}

lib/Dashboard/MailWidget.php

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use OCA\Mail\Exception\ClientException;
1616
use OCA\Mail\Exception\ServiceException;
1717
use OCA\Mail\Service\AccountService;
18+
use OCA\Mail\Service\Search\SearchQuery;
1819
use OCP\AppFramework\Services\IInitialState;
1920
use OCP\Dashboard\IAPIWidget;
2021
use OCP\Dashboard\IAPIWidgetV2;
@@ -91,11 +92,7 @@ public function load(): void {
9192
// No assets need to be loaded anymore as the widget is rendered from the API
9293
}
9394

94-
/**
95-
* Get widget-specific search filter
96-
* @return string
97-
*/
98-
abstract public function getSearchFilter(): string;
95+
abstract public function getSearchQuery(string $userId): SearchQuery;
9996

10097
/**
10198
* @param string $userId
@@ -110,16 +107,24 @@ protected function getEmails(string $userId, ?int $minTimestamp, int $limit = 7)
110107
if ($user === null) {
111108
return [];
112109
}
113-
$filter = $this->getSearchFilter();
114-
$emails = $this->mailSearch->findMessagesGlobally($user, $filter, null, $limit);
115110

111+
$query = $this->getSearchQuery($userId);
116112
if ($minTimestamp !== null) {
117-
return array_filter($emails, static function (Message $email) use ($minTimestamp) {
118-
return $email->getSentAt() > $minTimestamp;
119-
});
113+
$query->setStart((string)$minTimestamp);
114+
}
115+
116+
return $this->mailSearch->findMessagesGlobally($user, $query, $limit);
117+
}
118+
119+
protected function getMailboxIdsToExclude(string $userId): array {
120+
$mailboxIdsToExclude = [];
121+
122+
foreach ($this->accountService->findByUserId($userId) as $account) {
123+
$mailboxIdsToExclude[] = $account->getMailAccount()->getJunkMailboxId();
124+
$mailboxIdsToExclude[] = $account->getMailAccount()->getTrashMailboxId();
120125
}
121126

122-
return $emails;
127+
return array_values(array_filter($mailboxIdsToExclude));
123128
}
124129

125130
/**

lib/Dashboard/UnreadMailWidget.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99

1010
namespace OCA\Mail\Dashboard;
1111

12+
use OCA\Mail\Service\Search\Flag;
13+
use OCA\Mail\Service\Search\GlobalSearchQuery;
14+
use OCA\Mail\Service\Search\SearchQuery;
15+
1216
class UnreadMailWidget extends MailWidget {
1317
/**
1418
* @inheritDoc
@@ -24,10 +28,10 @@ public function getTitle(): string {
2428
return $this->l10n->t('Unread mail');
2529
}
2630

27-
/**
28-
* @inheritDoc
29-
*/
30-
public function getSearchFilter(): string {
31-
return 'is:unread';
31+
public function getSearchQuery(string $userId): SearchQuery {
32+
$query = new GlobalSearchQuery();
33+
$query->addFlag(Flag::not(Flag::SEEN));
34+
$query->setExcludeMailboxIds($this->getMailboxIdsToExclude($userId));
35+
return $query;
3236
}
3337
}

lib/Db/MessageMapper.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use OCA\Mail\IMAP\Threading\DatabaseMessage;
1717
use OCA\Mail\Service\Search\Flag;
1818
use OCA\Mail\Service\Search\FlagExpression;
19+
use OCA\Mail\Service\Search\GlobalSearchQuery;
1920
use OCA\Mail\Service\Search\SearchQuery;
2021
use OCA\Mail\Support\PerformanceLogger;
2122
use OCA\Mail\Support\PerformanceLoggerTask;
@@ -1076,6 +1077,16 @@ public function findIdsGloballyByQuery(IUser $user, SearchQuery $query, ?int $li
10761077
->from('mail_mailboxes', 'mb')
10771078
->join('mb', 'mail_accounts', 'a', $qb->expr()->eq('a.id', 'mb.account_id', IQueryBuilder::PARAM_INT))
10781079
->where($qb->expr()->eq('a.user_id', $qb->createNamedParameter($user->getUID())));
1080+
1081+
if ($query instanceof GlobalSearchQuery) {
1082+
$excludeMailboxIds = $query->getExcludeMailboxIds();
1083+
if (count($excludeMailboxIds) > 0) {
1084+
$selectMailboxIds->andWhere(
1085+
$qb->expr()->notIn('mb.id', $qb->createNamedParameter($excludeMailboxIds, IQueryBuilder::PARAM_INT_ARRAY))
1086+
);
1087+
}
1088+
}
1089+
10791090
$select->where(
10801091
$qb->expr()->in('m.mailbox_id', $qb->createFunction($selectMailboxIds->getSQL()), IQueryBuilder::PARAM_INT_ARRAY)
10811092
);

lib/Search/Provider.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use OCA\Mail\AppInfo\Application;
1313
use OCA\Mail\Contracts\IMailSearch;
1414
use OCA\Mail\Db\Message;
15+
use OCA\Mail\Service\Search\FilterStringParser;
1516
use OCP\IDateTimeFormatter;
1617
use OCP\IL10N;
1718
use OCP\IURLGenerator;
@@ -35,10 +36,13 @@ class Provider implements IProvider {
3536
/** @var IURLGenerator */
3637
private $urlGenerator;
3738

38-
public function __construct(IMailSearch $mailSearch,
39+
public function __construct(
40+
IMailSearch $mailSearch,
3941
IL10N $l10n,
4042
IDateTimeFormatter $dateTimeFormatter,
41-
IURLGenerator $urlGenerator) {
43+
IURLGenerator $urlGenerator,
44+
private FilterStringParser $filterStringParser,
45+
) {
4246
$this->mailSearch = $mailSearch;
4347
$this->l10n = $l10n;
4448
$this->dateTimeFormatter = $dateTimeFormatter;
@@ -67,11 +71,16 @@ public function search(IUser $user, ISearchQuery $query): SearchResult {
6771
}
6872

6973
protected function searchByFilter(IUser $user, ISearchQuery $query, string $filter): SearchResult {
74+
$mailQuery = $this->filterStringParser->parse($filter);
75+
7076
$cursor = $query->getCursor();
77+
if ($cursor !== null) {
78+
$mailQuery->setCursor((int)$cursor);
79+
}
80+
7181
$messages = $this->mailSearch->findMessagesGlobally(
7282
$user,
73-
$filter,
74-
empty($cursor) ? null : ((int)$cursor),
83+
$mailQuery,
7584
$query->getLimit()
7685
);
7786

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Mail\Service\Search;
11+
12+
class GlobalSearchQuery extends SearchQuery {
13+
/** @var int[] */
14+
private array $excludeMailboxIds = [];
15+
16+
/**
17+
* @return int[]
18+
*/
19+
public function getExcludeMailboxIds(): array {
20+
return $this->excludeMailboxIds;
21+
}
22+
23+
/**
24+
* @param int[] $mailboxIds
25+
*/
26+
public function setExcludeMailboxIds(array $mailboxIds): void {
27+
$this->excludeMailboxIds = $mailboxIds;
28+
}
29+
}

lib/Service/Search/MailSearch.php

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -123,24 +123,16 @@ public function findMessages(Account $account,
123123
}
124124

125125
/**
126-
* @param IUser $user
127-
* @param string|null $filter
128-
* @param int|null $cursor
126+
* Find messages across all mailboxes for a user
129127
*
130128
* @return Message[]
131129
*
132-
* @throws ClientException
133130
* @throws ServiceException
134131
*/
135-
public function findMessagesGlobally(IUser $user,
136-
?string $filter,
137-
?int $cursor,
132+
public function findMessagesGlobally(
133+
IUser $user,
134+
SearchQuery $query,
138135
?int $limit): array {
139-
$query = $this->filterStringParser->parse($filter);
140-
if ($cursor !== null) {
141-
$query->setCursor($cursor);
142-
}
143-
144136
return $this->messageMapper->findByIds($user->getUID(),
145137
$this->getIdsGlobally($user, $query, $limit),
146138
'DESC'
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace Unit\Dashboard;
11+
12+
use ChristophWurst\Nextcloud\Testing\TestCase;
13+
use OCA\Mail\Account;
14+
use OCA\Mail\Dashboard\ImportantMailWidget;
15+
use OCA\Mail\Db\MailAccount;
16+
use OCA\Mail\Db\Message;
17+
use OCA\Mail\Service\AccountService;
18+
use OCA\Mail\Service\Search\GlobalSearchQuery;
19+
use OCA\Mail\Service\Search\MailSearch;
20+
use OCA\Mail\Service\Search\SearchQuery as MailSearchQuery;
21+
use OCP\AppFramework\Services\IInitialState;
22+
use OCP\IL10N;
23+
use OCP\IURLGenerator;
24+
use OCP\IUser;
25+
use OCP\IUserManager;
26+
use PHPUnit\Framework\MockObject\MockObject;
27+
28+
class ImportantMailWidgetTest extends TestCase {
29+
30+
private IL10N&MockObject $l10n;
31+
private IURLGenerator&MockObject $urlGenerator;
32+
private IUserManager&MockObject $userManager;
33+
private AccountService&MockObject $accountService;
34+
private MailSearch&MockObject $mailSearch;
35+
private IInitialState&MockObject $initialState;
36+
private ImportantMailWidget $widget;
37+
38+
protected function setUp(): void {
39+
parent::setUp();
40+
41+
$this->l10n = $this->createMock(IL10N::class);
42+
$this->urlGenerator = $this->createMock(IURLGenerator::class);
43+
$this->userManager = $this->createMock(IUserManager::class);
44+
$this->accountService = $this->createMock(AccountService::class);
45+
$this->mailSearch = $this->createMock(MailSearch::class);
46+
$this->initialState = $this->createMock(IInitialState::class);
47+
48+
$this->widget = new ImportantMailWidget(
49+
$this->l10n,
50+
$this->urlGenerator,
51+
$this->userManager,
52+
$this->accountService,
53+
$this->mailSearch,
54+
$this->initialState,
55+
'bob'
56+
);
57+
}
58+
59+
public function testGetItems(): void {
60+
$user = $this->createMock(IUser::class);
61+
$this->userManager->expects($this->once())
62+
->method('get')
63+
->willReturn($user);
64+
$message1 = new Message();
65+
$message1->setSubject('Important');
66+
$message1->setMailboxId(1);
67+
$message2 = new Message();
68+
$message2->setSubject('Also important');
69+
$message2->setMailboxId(2);
70+
$this->mailSearch->expects($this->once())
71+
->method('findMessagesGlobally')
72+
->with($user, $this->callback(function (GlobalSearchQuery $query) {
73+
self::assertCount(1, $query->getFlags());
74+
self::assertNull($query->getStart());
75+
return true;
76+
}))
77+
->willReturn([$message1, $message2]);
78+
$mailAccount = new MailAccount();
79+
$account = new Account($mailAccount);
80+
$this->accountService->expects($this->once())
81+
->method('findByUserId')
82+
->willReturn([$account]);
83+
84+
$items = $this->widget->getItems('bob', null, 7);
85+
86+
$this->assertCount(2, $items);
87+
}
88+
89+
public function testGetItemsWithSince(): void {
90+
$user = $this->createMock(IUser::class);
91+
$this->userManager->expects($this->once())
92+
->method('get')
93+
->willReturn($user);
94+
$message1 = new Message();
95+
$message1->setSubject('Important');
96+
$message1->setMailboxId(1);
97+
$message2 = new Message();
98+
$message2->setSubject('Also important');
99+
$message2->setMailboxId(2);
100+
$this->mailSearch->expects($this->once())
101+
->method('findMessagesGlobally')
102+
->with($user, $this->callback(function (MailSearchQuery $query) {
103+
self::assertCount(1, $query->getFlags());
104+
self::assertNotNull($query->getStart());
105+
return true;
106+
}))
107+
->willReturn([$message1, $message2]);
108+
$mailAccount = new MailAccount();
109+
$account = new Account($mailAccount);
110+
$this->accountService->expects($this->once())
111+
->method('findByUserId')
112+
->willReturn([$account]);
113+
114+
$items = $this->widget->getItems('bob', '1745340000', 7);
115+
116+
$this->assertCount(2, $items);
117+
}
118+
}

0 commit comments

Comments
 (0)