Menu
Public repository / 275d4fa11a39ecc30e2caf604d5a0d499910e8db
AkurAI-Mail / src/lib/webmail/recipients.ts
Latest change 22202d1bde69be4ace8a299dffd91f8ff24378df - Recover deployed webmail frontend (frontend parity with EC2 a6afffc) by Ólafur Búi Ólafsson
/** Recipient field parsing, recent-recipient memory and autocomplete suggestions. */
export interface Contact { name?: string; email: string }
export const RECENT_RECIPIENTS_KEY = "akurai-recent-recipients";
export const RECENT_RECIPIENTS_MAX = 20;
export const SUGGESTION_LIMIT = 8;
export function splitRecipients(value: string) {
return value.split(",").map((part) => part.trim()).filter(Boolean);
}
/** Puts newly used addresses first, case-insensitively de-duplicated, capped at 20. */
export function mergeRecent(recent: string[], used: string) {
const fresh = splitRecipients(used);
if (!fresh.length) return recent;
const known = new Set(recent.map((entry) => entry.toLowerCase()));
return [...fresh.filter((entry) => !known.has(entry.toLowerCase())), ...recent].slice(0, RECENT_RECIPIENTS_MAX);
}
/** Saved contacts followed by recent addresses that are not already contacts. */
export function recipientSuggestions(contacts: Contact[], recent: string[]): Contact[] {
const isContact = (email: string) => contacts.some((contact) => contact.email.toLowerCase() === email.toLowerCase());
return [...contacts, ...recent.filter((email) => !isContact(email)).map((email) => ({ email }))];
}
/** The text after the last comma: what the user is currently typing. */
export function currentTerm(value: string) {
return splitRecipients(value).length ? value.slice(value.lastIndexOf(",") + 1).trim() : value.trim();
}
export function matchSuggestions(value: string, suggestions: Contact[]) {
const term = currentTerm(value).toLowerCase();
if (!term) return [];
const chosen = new Set(splitRecipients(value).slice(0, -1).map((entry) => entry.toLowerCase()));
return suggestions
.filter((entry) => entry.email.toLowerCase().includes(term) && !chosen.has(entry.email.toLowerCase()))
.slice(0, SUGGESTION_LIMIT);
}
/** Replaces the term being typed with `email` and leaves a trailing ", " for the next one. */
export function completeRecipient(value: string, email: string) {
const parts = value.split(",");
parts[parts.length - 1] = ` ${email}`;
return `${parts.map((part) => part.trim()).filter(Boolean).join(", ")}, `;
}
const EMAIL_PATTERN = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/i;
/** Pulls the bare lowercase address out of `"Name" <a@b.c>`. */
export function extractEmail(value: string) {
return (EMAIL_PATTERN.exec(value || "")?.[0] ?? "").toLowerCase();
}