Squash Odysseus development history

This commit is contained in:
pewdiepie-archdaemon
2026-09-11 06:04:19 +00:00
parent e5c99a5eee
commit 6ee6502010
2050 changed files with 538359 additions and 57745 deletions
+27
View File
@@ -0,0 +1,27 @@
// static/js/emailLibrary/replyRecipients.js
//
// Pure helpers for building reply-all recipient lists. No DOM, no fetch,
// no shared state — safe to import anywhere and to unit-test under node.
// Extract the bare email from "Name <email@x>" or a plain "email@x".
export function extractEmail(addr) {
const m = (addr || '').match(/<([^>]+)>/);
return (m ? m[1] : (addr || '')).trim().toLowerCase();
}
// Reply-all CC = everyone on the original To + Cc, minus ourselves, with the
// original "Name <email>" form preserved.
//
// `mine` is a single address or a list of the user's own addresses (a
// multi-account user has more than one). Empty/unknown ⇒ no exclusion.
// Comparing by exact extracted email (not a substring `includes`) is what
// fixes issue #360: an empty self address made `"...".includes("")` true for
// every recipient, so reply-all dropped the entire Cc list.
export function buildReplyAllCc(data, mine) {
const list = Array.isArray(mine) ? mine : [mine];
const me = new Set(list.map((a) => (a || '').toLowerCase()).filter(Boolean));
const split = (s) => (typeof s === 'string' ? s : '').split(',').map((x) => x.trim()).filter(Boolean);
return [...split(data && data.to), ...split(data && data.cc)]
.filter((addr) => !me.has(extractEmail(addr)))
.join(', ');
}
+20 -8
View File
@@ -110,13 +110,18 @@ export function _foldSummary(label, iconSvg, meta) {
subMeta = '';
}
}
// `meta` is derived from _extractQuoteMeta, which strips tags but then
// un-escapes entities (to recover `<foo@bar.com>` for bubble alignment) —
// so it can carry attacker-controlled angle brackets from a quoted block.
// This summary is built into innerHTML, so escape both parts to stop a
// crafted quote (e.g. `From: <img src=x onerror=...>`) from running script.
const metaSpan = subMeta
? `<span class="email-fold-summary-meta">${subMeta}</span>`
? `<span class="email-fold-summary-meta">${_esc(subMeta)}</span>`
: '';
return (
'<summary class="email-fold-summary">'
+ iconSvg
+ `<span class="email-fold-summary-name">${primary}</span>`
+ `<span class="email-fold-summary-name">${_esc(primary)}</span>`
+ metaSpan
+ '<svg class="email-summary-chevron" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="margin-left:auto;transition:transform .15s ease;"><polyline points="6 9 12 15 18 9"/></svg>'
+ '</summary>'
@@ -128,7 +133,7 @@ export function _foldSummary(label, iconSvg, meta) {
// "On <date>, <addr> wrote:". Returns a display string like
// "Jane Doe · Mon, Apr 18, 2026 at 9:31 AM" or `''`.
export function _extractQuoteMeta(html) {
if (!html) return '';
if (typeof html !== 'string' || !html) return '';
const txt = html
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
@@ -149,7 +154,11 @@ export function _extractQuoteMeta(html) {
let date = sentMatch ? sentMatch[1].trim() : '';
if (!from && !date) {
const gmail = txt.match(/On\s+([^,]+?,[^,]+?\d{4}[^,]*),?\s+(.+?)\s+wrote\s*:/i);
// The date may carry up to three commas before the year: the standard
// US Gmail attribution is "On Mon, Apr 18, 2026 at 9:31 AM, Jane wrote:"
// (weekday and day-of-month each add one). A single-comma pattern never
// reached the year there, so the fold lost its sender/date headline.
const gmail = txt.match(/On\s+((?:[^,]*,){0,3}?[^,]*?\d{4}[^,]*),?\s+(.+?)\s+wrote\s*:/i);
if (gmail) { date = gmail[1].trim(); from = gmail[2].trim(); }
}
@@ -158,9 +167,12 @@ export function _extractQuoteMeta(html) {
if (from.length > 60) from = from.slice(0, 57) + '…';
if (date.length > 28) date = date.slice(0, 25) + '…';
if (from && date) return `${_esc(from)} · ${_esc(date)}`;
if (from) return _esc(from);
if (date) return _esc(date);
// Return the raw sender/date text; `_foldSummary` is the single sink that
// builds these into HTML, so it owns escaping. Escaping here too would
// double-encode (e.g. "Ben & Jerry" -> "Ben &amp;amp; Jerry").
if (from && date) return `${from} · ${date}`;
if (from) return from;
if (date) return date;
return '';
}
@@ -290,7 +302,7 @@ export function _foldSignature(html, hintSig) {
m = html.match(/<div[^>]*id=["'](?:Signature|signature|divRplyFwdMsg)["'][\s\S]*$/i);
if (m) return wrap(html.slice(0, html.length - m[0].length), '', m[0]);
m = html.match(/(<br>|\n)\s*--\s*(<br>|\n)([\s\S]*)$/i);
m = html.match(/(<br\s*\/?>|\n)\s*--\s*(<br\s*\/?>|\n)([\s\S]*)$/i);
if (m) {
const idx = html.lastIndexOf(m[0]);
return wrap(html.slice(0, idx), m[1], m[3]);
+5
View File
@@ -20,11 +20,16 @@ export const state = {
_libFolders: [],
_libAccountId: null, // null = backend default account
_libAccounts: [], // list of accounts for the chip strip
_libAutoReplyActive: false, // active account currently has an away reply
_libPendingExpandUid: null,
_libSearch: '',
_libFilter: 'all', // all, unread, unanswered
_libSort: 'recent', // recent, unread, favorites
_libHasAttachments: false,
_libShowTags: localStorage.getItem('odysseus.email.showTags') !== '0',
// Embedded MIME images are shown by default; Email Settings can opt a
// mailbox back into the manual-load behavior.
_libViewInlineImages: true,
_libLoading: false,
_docModule: null,
_onEmailClick: null,
+59 -13
View File
@@ -15,7 +15,7 @@ export const _TALON_FROM = '(?:From|Från|Von|De|Da|От|Od|Van|差出人|发件
export const _TALON_SENT = '(?:Sent|Skickat|Gesendet|Envoy[ée]|Inviato|Enviado|Verzonden|Отправлено|Wysłane|Date|送信日時|发送时间|寄件日期|Sendt|Lähetetty|Tarih|Datum|Data|Datum)';
export const _TALON_SUBJ = '(?:Subject|Ämne|Betreff|Objet|Oggetto|Asunto|Onderwerp|Тема|Temat|件名|主题|主旨|Emne|Aihe|Onderwerp|Konu)';
export const _TALON_TO = '(?:To|Till|An|À|A|Voor|Para|Naar|Кому|Do|宛先|收件人|Emri|Komu)';
export const _TALON_ORIG_RE = /(?:^|\n)[\s>]*[-_=]{3,}\s*(?:Original\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;
export 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;
// Minimum plain-text length of a "signature" before we bother folding it.
// Short closings ("Cheers, John") stay inline — folding them would add
@@ -30,6 +30,47 @@ export function _esc(text) {
return div.innerHTML;
}
const _EMAIL_SUMMARY_ERROR_MESSAGES = Object.freeze({
email_summary_missing_body: 'No email body to summarize',
email_summary_not_configured: 'No model configured for email summaries',
email_summary_empty: 'The model returned an empty summary',
email_summary_unavailable: 'Failed to summarize',
});
export function _emailSummaryErrorMessage(result) {
const code = String(result?.error_code || '');
return _EMAIL_SUMMARY_ERROR_MESSAGES[code] || 'Failed to summarize';
}
export function _renderEmailSummaryError(container, result) {
const message = container.ownerDocument.createElement('span');
message.style.color = 'var(--red)';
message.textContent = _emailSummaryErrorMessage(result);
container.replaceChildren(message);
}
function _attrEsc(text) {
return String(text ?? '')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/`/g, '&#96;');
}
function _compactUrlSchemeValue(value) {
return String(value || '').replace(/[\u0000-\u0020\u007f-\u009f]+/g, '').toLowerCase();
}
function _isDangerousUrl(value) {
const compact = _compactUrlSchemeValue(value);
return compact.startsWith('javascript:') || compact.startsWith('vbscript:') || compact.startsWith('data:');
}
function _isDangerousSrcset(value) {
return String(value || '').split(',').some(candidate => _isDangerousUrl(candidate));
}
// Escape + linkify URLs and email addresses. Returns innerHTML-safe markup.
export function _escLinkify(text) {
const escaped = _esc(text);
@@ -39,9 +80,9 @@ export function _escLinkify(text) {
return escaped
.replace(urlRe, (m) => {
const href = m.startsWith('www.') ? `https://${m}` : m;
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${m}</a>`;
return `<a href="${_attrEsc(href)}" target="_blank" rel="noopener noreferrer">${m}</a>`;
})
.replace(mailRe, (m) => `<a href="mailto:${m}">${m}</a>`);
.replace(mailRe, (m) => `<a href="${_attrEsc(`mailto:${m}`)}">${m}</a>`);
}
// Pull display name out of "Name <email@x>"; fallback to local-part of
@@ -133,19 +174,14 @@ export function _initials(s) {
// `data:` URLs on every known URL attribute, scrubs inline colour/font/
// position styles so the theme can take over, and wraps highlight-bearing
// inline tags in <mark> so they render legibly across themes.
export function _sanitizeHtml(html) {
function _sanitizeHtmlOnce(html) {
const doc = new DOMParser().parseFromString(html, 'text/html');
doc.querySelectorAll(
'script, iframe, object, embed, form, style, link, ' +
'svg, math, base, meta, noscript, frame, frameset, applet, portal'
).forEach(el => el.remove());
const URL_ATTRS = ['href', 'src', 'srcset', 'action', 'formaction', 'background', 'poster', 'data'];
const isDangerousUrl = (val) => {
if (!val) return false;
const v = val.trim().toLowerCase();
return v.startsWith('javascript:') || v.startsWith('vbscript:') || v.startsWith('data:');
};
const URL_ATTRS = ['href', 'src', 'xlink:href', 'srcset', 'action', 'formaction', 'background', 'poster', 'data'];
const STRIP_CSS_PROPS = ['color', 'background', 'background-color',
'font-family', 'font', '-webkit-text-fill-color',
@@ -160,7 +196,7 @@ export function _sanitizeHtml(html) {
const name = attr.name.toLowerCase();
if (name.startsWith('on')) { el.removeAttribute(attr.name); continue; }
if (name === 'srcdoc') { el.removeAttribute(attr.name); continue; }
if (URL_ATTRS.includes(name) && isDangerousUrl(attr.value)) {
if (URL_ATTRS.includes(name) && (name === 'srcset' ? _isDangerousSrcset(attr.value) : _isDangerousUrl(attr.value))) {
el.removeAttribute(attr.name);
continue;
}
@@ -177,8 +213,8 @@ export function _sanitizeHtml(html) {
if (style) {
const kept = style.split(';').map(s => s.trim()).filter(decl => {
if (!decl) return false;
const lower = decl.toLowerCase();
if (lower.includes('javascript:') || lower.includes('expression(')) return false;
const lower = _compactUrlSchemeValue(decl);
if (lower.includes('javascript:') || lower.includes('vbscript:') || lower.includes('data:') || lower.includes('expression(')) return false;
const prop = decl.split(':', 1)[0].trim().toLowerCase();
return !STRIP_CSS_PROPS.includes(prop);
});
@@ -200,3 +236,13 @@ export function _sanitizeHtml(html) {
return doc.body.innerHTML;
}
export function _sanitizeHtml(html) {
let out = String(html ?? '');
for (let i = 0; i < 4; i++) {
const next = _sanitizeHtmlOnce(out);
if (next === out) break;
out = next;
}
return out;
}