diff --git a/static/index.html b/static/index.html
index 34195ca38..c175558e9 100644
--- a/static/index.html
+++ b/static/index.html
@@ -219,8 +219,8 @@
}, { once: true });
})();
-
-
+
+
@@ -2512,7 +2512,7 @@
-
+
diff --git a/static/js/document.js b/static/js/document.js
index c4c97e841..65599bc71 100644
--- a/static/js/document.js
+++ b/static/js/document.js
@@ -2588,9 +2588,47 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js';
return -1;
}
+ function _emailQuoteStartOffset(text) {
+ const original = String(text || '');
+ if (!original) return -1;
+ const boundary = String.raw`(?:^|\n|
|<\/(?:p|div|blockquote|li|tr|h[1-6])>)`;
+ const patterns = [
+ new RegExp(`${boundary}\\s*(?:[-_=–—\\s]| ){3,}(?:previous|original|forwarded)\\s+(?:message|email|mail)(?:[-_=–—\\s]| ){3,}`, 'i'),
+ new RegExp(`${boundary}\\s*On\\s+.{1,700}?\\s+wrote:\\s*`, 'i'),
+ new RegExp(`${boundary}\\s*-{2,}\\s*Original Message\\s*-{2,}`, 'i'),
+ ];
+ let best = -1;
+ for (const re of patterns) {
+ const m = re.exec(original);
+ if (!m) continue;
+ let idx = m.index;
+ const prefix = m[0].match(/^(?:\n|
|<\/(?:p|div|blockquote|li|tr|h[1-6])>)/i);
+ if (prefix) idx += prefix[0].length;
+ if (best < 0 || idx < best) best = idx;
+ }
+ const fromRe = new RegExp(`${boundary}\\s*From:\\s*\\S`, 'i');
+ const fromMatch = fromRe.exec(original);
+ if (fromMatch) {
+ let idx = fromMatch.index;
+ const prefix = fromMatch[0].match(/^(?:\n|
|<\/(?:p|div|blockquote|li|tr|h[1-6])>)/i);
+ if (prefix) idx += prefix[0].length;
+ const nearby = original.slice(idx, idx + 1200);
+ if (/(?:^|\n|
|<\/(?:p|div|blockquote|li|tr|h[1-6])>)\s*(?:To|Subject):\s*/i.test(nearby)) {
+ if (best < 0 || idx < best) best = idx;
+ }
+ }
+ return best;
+ }
+
function _splitEmailReplyQuote(text) {
const original = String(text || '');
if (!original) return { body: '', quote: '', stripped: false };
+ const htmlQuoteOffset = _emailQuoteStartOffset(original);
+ if (htmlQuoteOffset >= 0) {
+ const body = original.slice(0, htmlQuoteOffset).trim();
+ const quote = original.slice(htmlQuoteOffset).trim();
+ return { body, quote, stripped: true };
+ }
const lines = original.split('\n');
const quoteIdx = _emailQuoteStartIndex(lines);
if (quoteIdx < 0) return { body: original.trim(), quote: '', stripped: false };
diff --git a/static/sw.js b/static/sw.js
index 3df123bce..18beb4456 100644
--- a/static/sw.js
+++ b/static/sw.js
@@ -7,7 +7,7 @@
// - Other static assets (images/fonts/libs): cache-first with bg refresh.
// - API / non-GET: never cached.
// Bump CACHE_NAME whenever the precache list or SW logic changes.
-const CACHE_NAME = 'odysseus-v334';
+const CACHE_NAME = 'odysseus-v335';
// Core shell precached on install so repeat opens are instant without any
// network wait. Keep this list in sync with the