AkurAI Build
Menu

Public repository / 4f0b1d374fab4075f2b552b5f250bca5f28afee9

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();
}