Feature: recipient autocomplete from Sent, with on-demand server search
Compose recipient fields only suggested existing contacts and directory users, so people you had emailed before but never saved as a contact never came up. This adds an Outlook-Web-style suggestion flow. On startup the Sent folder is read once (metadata only) to build a cache of addresses you have written to; those are merged into the autocomplete after contacts and directory principals, deduped, contacts winning. When the recipient is not in the cache, the dropdown offers a "search the server" row that queries the Sent folder on demand. That lookup fetches only the to/cc fields (no subject, body or attachments) and returns the matching addresses, deduped. New strings are added to all 20 locales.
This commit is contained in:
@@ -209,6 +209,23 @@ export class DemoJMAPClient implements IJMAPClient {
|
||||
return { emails, hasMore: position + limit < total, total };
|
||||
}
|
||||
|
||||
async searchSentRecipients(query: string, _sentMailboxId: string, _accountId?: string, _limit: number = 60): Promise<Array<{ name: string; email: string }>> {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
const byEmail = new Map<string, { name: string; email: string }>();
|
||||
for (const email of this.data.emails) {
|
||||
for (const r of [...(email.to || []), ...(email.cc || [])]) {
|
||||
if (!r.email) continue;
|
||||
const key = r.email.toLowerCase();
|
||||
if (byEmail.has(key)) continue;
|
||||
if (key.includes(q) || (r.name && r.name.toLowerCase().includes(q))) {
|
||||
byEmail.set(key, { name: r.name || '', email: r.email });
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(byEmail.values());
|
||||
}
|
||||
|
||||
// ── Email mutations ───────────────────────────────────────────
|
||||
|
||||
async markAsRead(emailId: string, read: boolean = true): Promise<void> {
|
||||
|
||||
@@ -89,6 +89,13 @@ export interface IJMAPClient {
|
||||
limit?: number,
|
||||
position?: number,
|
||||
): Promise<{ emails: Email[]; hasMore: boolean; total: number }>;
|
||||
/**
|
||||
* Lean recipient search for compose autocomplete ("search the server" action):
|
||||
* finds messages in `sentMailboxId` whose to/cc matches `query` and returns
|
||||
* only the matching addresses (fetches just the `to`/`cc` properties - no
|
||||
* bodies or attachments), deduped.
|
||||
*/
|
||||
searchSentRecipients(query: string, sentMailboxId: string, accountId?: string, limit?: number): Promise<Array<{ name: string; email: string }>>;
|
||||
|
||||
// ── Email mutations ───────────────────────────────────────────
|
||||
markAsRead(emailId: string, read?: boolean, accountId?: string): Promise<void>;
|
||||
|
||||
@@ -1887,6 +1887,53 @@ export class JMAPClient implements IJMAPClient {
|
||||
}
|
||||
}
|
||||
|
||||
async searchSentRecipients(query: string, sentMailboxId: string, accountId?: string, limit: number = 60): Promise<Array<{ name: string; email: string }>> {
|
||||
const q = query.trim();
|
||||
if (!q || !sentMailboxId) return [];
|
||||
try {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
const response = await this.request([
|
||||
["Email/query", {
|
||||
accountId: targetAccountId,
|
||||
filter: {
|
||||
operator: "AND",
|
||||
conditions: [
|
||||
{ inMailbox: sentMailboxId },
|
||||
{ operator: "OR", conditions: [{ to: q }, { cc: q }] },
|
||||
],
|
||||
},
|
||||
sort: [{ property: "receivedAt", isAscending: false }],
|
||||
limit,
|
||||
}, "0"],
|
||||
// Fetch ONLY the recipient fields - no subject/preview/body/attachments.
|
||||
["Email/get", {
|
||||
accountId: targetAccountId,
|
||||
"#ids": { resultOf: "0", name: "Email/query", path: "/ids" },
|
||||
properties: ["to", "cc"],
|
||||
}, "1"],
|
||||
]);
|
||||
const emails = (response.methodResponses?.[1]?.[1]?.list || []) as Email[];
|
||||
const lower = q.toLowerCase();
|
||||
const byEmail = new Map<string, { name: string; email: string }>();
|
||||
for (const email of emails) {
|
||||
for (const r of [...(email.to || []), ...(email.cc || [])]) {
|
||||
if (!r.email) continue;
|
||||
const key = r.email.toLowerCase().trim();
|
||||
if (!key || byEmail.has(key)) continue;
|
||||
// The query matched *some* recipient of the message; keep only the
|
||||
// addresses that actually match, not every co-recipient.
|
||||
if (key.includes(lower) || (r.name && r.name.toLowerCase().includes(lower))) {
|
||||
byEmail.set(key, { name: (r.name || "").trim(), email: r.email });
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(byEmail.values());
|
||||
} catch (error) {
|
||||
console.error('Recipient search failed:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getThread(threadId: string, accountId?: string): Promise<Thread | null> {
|
||||
try {
|
||||
const targetAccountId = accountId || this.accountId;
|
||||
|
||||
Reference in New Issue
Block a user