feat: prioritise sent-folder addresses in To/Cc/Bcc autocomplete (#380)
## What changed `searchAddresses` (used by the To/Cc/Bcc autocomplete) now runs two passes over the candidate email rows: 1. **Sent-folder rows first** — the mailboxes table is queried for mailboxes with `role='sent'`; any email row whose `mailboxPath` matches gets processed before inbox/other rows. Within this group addresses are ordered by `receivedAt` DESC as before. 2. **All other rows** — processed after sent rows, also by `receivedAt` DESC. Within sent-folder rows, `toAddresses` and `ccJson` are checked before `fromJson` (the sender in a sent email is our own address, not a useful suggestion). For non-sent rows the original order (`fromJson`, `toAddresses`, `ccJson`) is kept. This means: if you wrote to `info@foo.de` yesterday and received spam from `info@spam.de` today, typing "i" surfaces `info@foo.de` first. ## How verified - All 492 unit tests pass (`task test`). - Added a dedicated test `searchAddresses prioritises sent-folder addresses over newer received` that inserts an older sent email and a newer received email matching the same query prefix and asserts the sent-folder address is returned first. Closes #375 Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de> Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/380
This commit was merged in pull request #380.
This commit is contained in:
committed by
guettli
co-authored by
guettli
Thomas SharedInbox
parent
87244de7da
commit
5e029a1365
@@ -2963,6 +2963,20 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
}) async {
|
||||
if (query.length < 2) return [];
|
||||
final pattern = '%${query.toLowerCase()}%';
|
||||
|
||||
// Addresses we deliberately wrote to (sent folder) should appear before
|
||||
// addresses that happened to email us (inbox/other folders).
|
||||
final sentMailboxes = await (_db.select(_db.mailboxes)
|
||||
..where((t) {
|
||||
Expression<bool> cond = t.role.equals('sent');
|
||||
if (accountId != null) {
|
||||
cond = t.accountId.equals(accountId) & cond;
|
||||
}
|
||||
return cond;
|
||||
}))
|
||||
.get();
|
||||
final sentPaths = {for (final m in sentMailboxes) m.path};
|
||||
|
||||
final rows = await (_db.select(_db.emails)
|
||||
..where((t) {
|
||||
Expression<bool> cond = const Constant(true);
|
||||
@@ -2977,11 +2991,22 @@ class EmailRepositoryImpl implements EmailRepository {
|
||||
..limit(100))
|
||||
.get();
|
||||
|
||||
// Two passes: sent-folder rows first (prioritise recipients we chose),
|
||||
// then other rows (senders who contacted us).
|
||||
final sortedRows = [
|
||||
...rows.where((r) => sentPaths.contains(r.mailboxPath)),
|
||||
...rows.where((r) => !sentPaths.contains(r.mailboxPath)),
|
||||
];
|
||||
|
||||
final seen = <String>{};
|
||||
final results = <model.EmailAddress>[];
|
||||
final lowerQuery = query.toLowerCase();
|
||||
for (final row in rows) {
|
||||
for (final jsonStr in [row.fromJson, row.toAddresses, row.ccJson]) {
|
||||
for (final row in sortedRows) {
|
||||
final isSent = sentPaths.contains(row.mailboxPath);
|
||||
final fields = isSent
|
||||
? [row.toAddresses, row.ccJson, row.fromJson]
|
||||
: [row.fromJson, row.toAddresses, row.ccJson];
|
||||
for (final jsonStr in fields) {
|
||||
final list = jsonDecode(jsonStr) as List<dynamic>;
|
||||
for (final e in list) {
|
||||
final map = e as Map<String, dynamic>;
|
||||
|
||||
@@ -497,6 +497,60 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'searchAddresses prioritises sent-folder addresses over newer received',
|
||||
() async {
|
||||
final r = _makeRepos();
|
||||
await r.accounts.addAccount(_account, 'pw');
|
||||
|
||||
// Register the Sent mailbox so searchAddresses knows its role.
|
||||
await r.db.into(r.db.mailboxes).insert(
|
||||
MailboxesCompanion.insert(
|
||||
id: 'acc-1:Sent',
|
||||
accountId: 'acc-1',
|
||||
path: 'Sent',
|
||||
name: 'Sent',
|
||||
role: const Value('sent'),
|
||||
),
|
||||
);
|
||||
|
||||
// Older sent email: user deliberately wrote to info@foo.de.
|
||||
await r.db.into(r.db.emails).insert(
|
||||
EmailsCompanion.insert(
|
||||
id: 'acc-1:sent-1',
|
||||
accountId: 'acc-1',
|
||||
mailboxPath: 'Sent',
|
||||
uid: 1,
|
||||
receivedAt: DateTime(2025),
|
||||
toAddresses: const Value(
|
||||
'[{"name":"Foo","email":"info@foo.de"}]',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Newer received email: spam arrived today from info@spam.de.
|
||||
await r.db.into(r.db.emails).insert(
|
||||
EmailsCompanion.insert(
|
||||
id: 'acc-1:inbox-1',
|
||||
accountId: 'acc-1',
|
||||
mailboxPath: 'INBOX',
|
||||
uid: 2,
|
||||
receivedAt: DateTime(2026),
|
||||
fromJson: const Value(
|
||||
'[{"name":"Spam","email":"info@spam.de"}]',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Even though spam is newer, the sent-folder address should win.
|
||||
final results = await r.emails.searchAddresses(null, 'info');
|
||||
expect(results.map((a) => a.email).toList(), [
|
||||
'info@foo.de',
|
||||
'info@spam.de',
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
// ── IMAP method tests ────────────────────────────────────────────────────
|
||||
|
||||
test(
|
||||
|
||||
Reference in New Issue
Block a user