PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/emailLibrary/utils.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:00:32 UTC 10,005 bytes, 229 lines download raw bytes

1// static/js/emailLibrary/utils.js
2//
3// Pure helpers extracted from emailLibrary.js. No DOM state, no fetch,
4// no shared mutable references — safe to import anywhere.
5
6// ── Talon-inspired multilingual quote-detection regexes ───────────
7// Borrowed (loosely) from Mailgun's `talon` library. These are partial
8// regex source strings — combined with surrounding patterns by callers.
9// Multilingual on purpose: a typed "wrote:" line is locale-bound, and
10// people forward / reply across language settings all the time.
11
12export const _TALON_WROTE = '(?:wrote|écrit|escribió|scrisse|schrieb|skrev|schreef|napisał|написал|napsal|написа|έγραψε|katselivat|napisao|написав|napisała|napisali|hat geschrieben|kirjoitti|написала|escreveu|napisao|написа|написала)';
13
14export const _TALON_FROM = '(?:From|Från|Von|De|Da|От|Od|Van|差出人|发件人|寄件人|Ut|Frá|Lähettäjä|Avsender|Pošiljatelj|Од|Від|Posiljatelj|Frå)';
15export const _TALON_SENT = '(?:Sent|Skickat|Gesendet|Envoy[ée]|Inviato|Enviado|Verzonden|Отправлено|Wysłane|Date|送信日時|发送时间|寄件日期|Sendt|Lähetetty|Tarih|Datum|Data|Datum)';
16export const _TALON_SUBJ = '(?:Subject|Ämne|Betreff|Objet|Oggetto|Asunto|Onderwerp|Тема|Temat|件名|主题|主旨|Emne|Aihe|Onderwerp|Konu)';
17export const _TALON_TO   = '(?:To|Till|An|À|A|Voor|Para|Naar|Кому|Do|宛先|收件人|Emri|Komu)';
18export const _TALON_ORIG_RE = /(?:^|\n)[\s>]*[-_=]{3,}\s*(?:Original\s+Message|Forwarded\s+message|Ursprüngliche\s+Nachricht|Mensaje\s+original|Messaggio\s+originale|Message\s+d['’]origine|Oorspronkelijk\s+bericht|Original\s+meddelande|Vor[ ]asal[a]\s+meddelande|原文|原始邮件|転送)\s*[-_=]{3,}/i;
19
20// Minimum plain-text length of a "signature" before we bother folding it.
21// Short closings ("Cheers, John") stay inline — folding them would add
22// a click for two bytes of saving.
23export const _SIG_BLOAT_MIN_CHARS = 200;
24
25// HTML-escape a string by round-tripping through a detached div. Cheap
26// and correct (handles all the entities that matter for innerHTML).
27export function _esc(text) {
28  const div = document.createElement('div');
29  div.textContent = text || '';
30  return div.innerHTML;
31}
32
33function _attrEsc(text) {
34  return String(text ?? '')
35    .replace(/"/g, '"')
36    .replace(/'/g, ''')
37    .replace(/</g, '&lt;')
38    .replace(/>/g, '&gt;')
39    .replace(/`/g, '&#96;');
40}
41
42function _compactUrlSchemeValue(value) {
43  return String(value || '').replace(/[\u0000-\u0020\u007f-\u009f]+/g, '').toLowerCase();
44}
45
46function _isDangerousUrl(value) {
47  const compact = _compactUrlSchemeValue(value);
48  return compact.startsWith('javascript:') || compact.startsWith('vbscript:') || compact.startsWith('data:');
49}
50
51function _isDangerousSrcset(value) {
52  return String(value || '').split(',').some(candidate => _isDangerousUrl(candidate));
53}
54
55// Escape + linkify URLs and email addresses. Returns innerHTML-safe markup.
56export function _escLinkify(text) {
57  const escaped = _esc(text);
58  // URLs: http(s)://... or www....
59  const urlRe = /\b((?:https?:\/\/|www\.)[^\s<>"']+[^\s<>"'.,;:!?)\]])/g;
60  const mailRe = /\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g;
61  return escaped
62    .replace(urlRe, (m) => {
63      const href = m.startsWith('www.') ? `https://${m}` : m;
64      return `<a href="${_attrEsc(href)}" target="_blank" rel="noopener noreferrer">${m}</a>`;
65    })
66    .replace(mailRe, (m) => `<a href="${_attrEsc(`mailto:${m}`)}">${m}</a>`);
67}
68
69// Pull display name out of "Name <email@x>"; fallback to local-part of
70// the email; final fallback to the input string.
71export function _extractName(addr) {
72  const m = addr.match(/^"?([^"<]+?)"?\s*<([^>]+)>\s*$/);
73  if (m) return m[1].trim();
74  const localPart = addr.split('@')[0];
75  return localPart || addr;
76}
77
78// Parse the "Author <email> · Date" metadata string emitted by the
79// server-side thread parser.
80export function _parseTurnMeta(meta) {
81  if (!meta) return { author: '', email: '', date: '' };
82  const m = String(meta);
83  const eMatch = m.match(/<([^<>\s]+@[^<>\s]+)>/) ||
84                 m.match(/\b([\w.+-]+@[\w.-]+\.[A-Za-z]{2,})\b/);
85  const email = eMatch ? eMatch[1].toLowerCase().trim() : '';
86  const parts = m.split(/\s+[·•]\s+/);
87  let author = '', date = '';
88  if (parts.length >= 2) {
89    author = parts[0].replace(/<[^>]+>/g, '').trim();
90    date = parts.slice(1).join(' · ').trim();
91  } else {
92    author = m.replace(/<[^>]+>/g, '').trim();
93  }
94  return { author, email, date };
95}
96
97// Short, locale-aware display string for a chat-bubble timestamp.
98// Returns '' for invalid / empty input.
99export function _formatBubbleDate(iso) {
100  if (!iso) return '';
101  const d = new Date(iso);
102  if (!d || isNaN(d.getTime())) return '';
103  try {
104    return d.toLocaleString(undefined, {
105      month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit',
106    });
107  } catch (_) { return ''; }
108}
109
110// Format a raw "to" address string ("Foo <[email protected]>, [email protected]") into a
111// short, readable list — display names when present, just the local part
112// of the email otherwise, and ", +N" once there are more than 2 recipients.
113export function _formatRecipients(raw) {
114  if (!raw) return '';
115  const addrs = String(raw).split(',').map(s => s.trim()).filter(Boolean);
116  if (!addrs.length) return '';
117  const friendly = addrs.map(a => {
118    const m = a.match(/^\s*"?([^"<]+?)"?\s*<[^>]+>\s*$/);
119    if (m && m[1].trim()) return m[1].trim();
120    const em = a.replace(/[<>]/g, '').trim();
121    return em.split('@')[0] || em;
122  });
123  if (friendly.length === 1) return friendly[0];
124  if (friendly.length === 2) return friendly.join(', ');
125  return friendly.slice(0, 2).join(', ') + ' +' + (friendly.length - 2);
126}
127
128// Deterministic per-sender colour. Same hashing as
129// emailInbox.js#_senderColor so a sender's avatar / name colour matches
130// across the list view and the bubble reader.
131export function _senderColor(name) {
132  if (!name) return 'hsl(220, 55%, 65%)';
133  const key = String(name).toLowerCase();
134  let hash = 0;
135  for (let i = 0; i < key.length; i++) {
136    hash = ((hash << 5) - hash + key.charCodeAt(i)) | 0;
137  }
138  const hue = ((hash % 360) + 360) % 360;
139  return `hsl(${hue}, 55%, 65%)`;
140}
141
142// 1- or 2-letter initials for an avatar bubble. Unicode-friendly.
143export function _initials(s) {
144  if (!s) return '?';
145  const clean = String(s).replace(/<[^>]+>/g, '').replace(/[^\p{L}\s]/gu, ' ').trim();
146  const parts = clean.split(/\s+/).filter(Boolean);
147  if (!parts.length) return '?';
148  const first = parts[0][0] || '';
149  const last = parts.length > 1 ? parts[parts.length - 1][0] : '';
150  return (first + last).toUpperCase();
151}
152
153// HTML sanitizer for rendering remote email bodies. Strips script/iframe/
154// form/style/etc., kills `on*` handlers, blocks `javascript:`/`vbscript:`/
155// `data:` URLs on every known URL attribute, scrubs inline colour/font/
156// position styles so the theme can take over, and wraps highlight-bearing
157// inline tags in <mark> so they render legibly across themes.
158function _sanitizeHtmlOnce(html) {
159  const doc = new DOMParser().parseFromString(html, 'text/html');
160  doc.querySelectorAll(
161    'script, iframe, object, embed, form, style, link, ' +
162    'svg, math, base, meta, noscript, frame, frameset, applet, portal'
163  ).forEach(el => el.remove());
164
165  const URL_ATTRS = ['href', 'src', 'xlink:href', 'srcset', 'action', 'formaction', 'background', 'poster', 'data'];
166
167  const STRIP_CSS_PROPS = ['color', 'background', 'background-color',
168                           'font-family', 'font', '-webkit-text-fill-color',
169                           'position', 'z-index'];
170  const HIGHLIGHT_INLINE_TAGS = new Set(['SPAN', 'FONT', 'EM', 'B', 'I',
171                                         'STRONG', 'SMALL', 'U']);
172  const HAS_BG_COLOR = /background(?:-color)?\s*:\s*(?!\s*(?:transparent|none|inherit|initial)\b)[^;]+/i;
173  const _markedForHighlight = [];
174
175  doc.querySelectorAll('*').forEach(el => {
176    for (const attr of [...el.attributes]) {
177      const name = attr.name.toLowerCase();
178      if (name.startsWith('on')) { el.removeAttribute(attr.name); continue; }
179      if (name === 'srcdoc') { el.removeAttribute(attr.name); continue; }
180      if (URL_ATTRS.includes(name) && (name === 'srcset' ? _isDangerousSrcset(attr.value) : _isDangerousUrl(attr.value))) {
181        el.removeAttribute(attr.name);
182        continue;
183      }
184    }
185    el.removeAttribute('color');
186    const bgcolor = el.getAttribute('bgcolor');
187    el.removeAttribute('bgcolor');
188    el.removeAttribute('face');
189    const style = el.getAttribute('style');
190    const hadHighlight =
191      HIGHLIGHT_INLINE_TAGS.has(el.tagName) &&
192      ((style && HAS_BG_COLOR.test(style)) || (bgcolor && bgcolor !== 'transparent'));
193    if (hadHighlight) _markedForHighlight.push(el);
194    if (style) {
195      const kept = style.split(';').map(s => s.trim()).filter(decl => {
196        if (!decl) return false;
197        const lower = _compactUrlSchemeValue(decl);
198        if (lower.includes('javascript:') || lower.includes('vbscript:') || lower.includes('data:') || lower.includes('expression(')) return false;
199        const prop = decl.split(':', 1)[0].trim().toLowerCase();
200        return !STRIP_CSS_PROPS.includes(prop);
201      });
202      if (kept.length) el.setAttribute('style', kept.join('; '));
203      else el.removeAttribute('style');
204    }
205    if (el.tagName === 'A') {
206      el.setAttribute('target', '_blank');
207      el.setAttribute('rel', 'noopener noreferrer');
208    }
209  });
210
211  _markedForHighlight.forEach(el => {
212    if (el.tagName === 'MARK' || !el.firstChild) return;
213    const mark = doc.createElement('mark');
214    while (el.firstChild) mark.appendChild(el.firstChild);
215    el.appendChild(mark);
216  });
217
218  return doc.body.innerHTML;
219}
220
221export function _sanitizeHtml(html) {
222  let out = String(html ?? '');
223  for (let i = 0; i < 4; i++) {
224    const next = _sanitizeHtmlOnce(out);
225    if (next === out) break;
226    out = next;
227  }
228  return out;
229}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.