Menu
Public repository / master
AkurAI-Mail / src/lib/webmail/recipients.js
Latest change 10d19fe0941c186ec244d1f0e2af6b19200dcb0c - OLI-248: pure webmail logic as plain ES modules with JSDoc types; inline icon module by Ólafur Búi Ólafsson
/** @typedef {{ name?: string, email: string }} Contact */
/** Recipient field parsing, recent-recipient memory and autocomplete suggestions. */
export const RECENT_RECIPIENTS_KEY = "akurai-recent-recipients";
export const RECENT_RECIPIENTS_MAX = 20;
export const SUGGESTION_LIMIT = 8;
/**
* @param {string} value
*/
export function splitRecipients(value) {
return value.split(",").map((part) => part.trim()).filter(Boolean);
}
/**
* Puts newly used addresses first, case-insensitively de-duplicated, capped at 20.
* @param {string[]} recent
* @param {string} used
*/
export function mergeRecent(recent, used) {
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.
* @param {Contact[]} contacts
* @param {string[]} recent
* @returns {Contact[]}
*/
export function recipientSuggestions(contacts, recent) {
const isContact = (/** @type {string} */ email) => 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.
* @param {string} value
*/
export function currentTerm(value) {
return splitRecipients(value).length ? value.slice(value.lastIndexOf(",") + 1).trim() : value.trim();
}
/**
* @param {string} value
* @param {Contact[]} suggestions
*/
export function matchSuggestions(value, suggestions) {
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.
* @param {string} value
* @param {string} email
*/
export function completeRecipient(value, email) {
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>`.
* @param {string} value
*/
export function extractEmail(value) {
return (EMAIL_PATTERN.exec(value || "")?.[0] ?? "").toLowerCase();
}