feat: split search query into words for AND semantics

Searching for "foo bar" now matches emails containing both words rather
than the exact phrase. Each whitespace-separated term generates its own
IMAP criterion (OR SUBJECT "w" TEXT "w") — multiple top-level IMAP criteria
are ANDed by the protocol — and its own Drift LIKE clause for the local DB.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas Güttler
2026-04-27 07:27:14 +02:00
co-authored by Claude Sonnet 4.6
parent 0103454e31
commit 2074046bb3
3 changed files with 32 additions and 13 deletions
+11
View File
@@ -6,6 +6,17 @@ Tasks get moved from next.md to done.md
## Tasks
## Multi-word search uses AND semantics
Searching for "foo bar" now returns emails that contain **both** words, not the exact
phrase. Fixed in `email_repository_impl.dart`:
- **IMAP search** (`searchEmails`): query is split on whitespace; each word becomes
`OR SUBJECT "word" TEXT "word"`, joined by spaces. Multiple top-level IMAP criteria
are implicitly ANDed by the protocol.
- **Local DB search** (`searchEmailsGlobal`): each word adds
`& (subject LIKE '%word%' | preview LIKE '%word%')` to the Drift where-clause.
## Navigate back to account list from inside an account
Added an "All accounts" tile (with `Icons.switch_account`) at the top of `FolderDrawer`,
@@ -1726,13 +1726,21 @@ class EmailRepositoryImpl implements EmailRepository {
String accountId,
String query,
) async {
final pattern = '%${query.toLowerCase()}%';
final words = query
.toLowerCase()
.split(RegExp(r'\s+'))
.where((w) => w.isNotEmpty)
.toList();
final rows = await (_db.select(_db.emails)
..where(
(t) =>
t.accountId.equals(accountId) &
(t.subject.like(pattern) | t.preview.like(pattern)),
)
..where((t) {
Expression<bool> condition = t.accountId.equals(accountId);
for (final word in words) {
final pattern = '%$word%';
condition = condition &
(t.subject.like(pattern) | t.preview.like(pattern));
}
return condition;
})
..orderBy([(t) => OrderingTerm.desc(t.receivedAt)])
..limit(50))
.get();
@@ -1770,9 +1778,14 @@ class EmailRepositoryImpl implements EmailRepository {
await _imapConnect(account, _effectiveUsername(account), password);
try {
await client.selectMailboxByPath(mailboxPath);
final escaped = query.replaceAll('"', '\\"');
final terms =
query.split(RegExp(r'\s+')).where((t) => t.isNotEmpty).toList();
final searchCriteria = terms.map((term) {
final escaped = term.replaceAll('"', '\\"');
return 'OR SUBJECT "$escaped" TEXT "$escaped"';
}).join(' ');
final result = await client.uidSearchMessages(
searchCriteria: 'OR SUBJECT "$escaped" TEXT "$escaped"',
searchCriteria: searchCriteria,
);
final uids = result.matchingSequence?.toList() ?? [];
if (uids.isEmpty) return [];
-5
View File
@@ -18,11 +18,6 @@ Then commit.
## Tasks
I opened a mailbox. I search for "foo bar". I want to see all mails containing foo and bar. Not
mails containing "foo bar" exactly.
---
I search for "foo". Now I see all mails containing "foo". I want to easily do the common actions on
the selected mails: Delete, Archive, Move to Folder, Move to Junk, ...