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 {
|
}) async {
|
||||||
if (query.length < 2) return [];
|
if (query.length < 2) return [];
|
||||||
final pattern = '%${query.toLowerCase()}%';
|
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)
|
final rows = await (_db.select(_db.emails)
|
||||||
..where((t) {
|
..where((t) {
|
||||||
Expression<bool> cond = const Constant(true);
|
Expression<bool> cond = const Constant(true);
|
||||||
@@ -2977,11 +2991,22 @@ class EmailRepositoryImpl implements EmailRepository {
|
|||||||
..limit(100))
|
..limit(100))
|
||||||
.get();
|
.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 seen = <String>{};
|
||||||
final results = <model.EmailAddress>[];
|
final results = <model.EmailAddress>[];
|
||||||
final lowerQuery = query.toLowerCase();
|
final lowerQuery = query.toLowerCase();
|
||||||
for (final row in rows) {
|
for (final row in sortedRows) {
|
||||||
for (final jsonStr in [row.fromJson, row.toAddresses, row.ccJson]) {
|
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>;
|
final list = jsonDecode(jsonStr) as List<dynamic>;
|
||||||
for (final e in list) {
|
for (final e in list) {
|
||||||
final map = e as Map<String, dynamic>;
|
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 ────────────────────────────────────────────────────
|
// ── IMAP method tests ────────────────────────────────────────────────────
|
||||||
|
|
||||||
test(
|
test(
|
||||||
|
|||||||
Reference in New Issue
Block a user