hide outlook/hotmail quoted text correctly and fix html email whitespace

This commit is contained in:
Abhinav Raut
2026-05-18 00:30:13 +05:30
parent e048ba5716
commit bae4c23b01
5 changed files with 102 additions and 6 deletions
@@ -77,7 +77,7 @@
:html="sanitizedContent"
:allowedSchemas="['cid', 'https', 'http', 'mailto']"
:allowed-css-properties="extendedCssProperties"
class="mb-1 native-html whitespace-pre-wrap break-words"
class="mb-1 native-html break-words"
:class="{ 'mb-3': message.attachments.length > 0 }"
/>
</div>
@@ -205,6 +205,7 @@ import BubbleAttachmentPreview from '@main/features/conversation/message/attachm
import MessageEnvelope from './MessageEnvelope.vue'
import CSATResponseDisplay from './CSATResponseDisplay.vue'
import api from '@main/api'
import { containsQuoteMarkers } from '@shared-ui/utils/quotedContent.js'
const extendedCssProperties = [...allowedCssProperties, 'transform', 'transform-origin']
@@ -305,7 +306,7 @@ const retryMessage = (msg) => {
const showQuotedText = ref(false)
const hasQuotedContent = computed(
() => !isOutgoing.value && sanitizedContent.value.includes('<blockquote')
() => !isOutgoing.value && containsQuoteMarkers(sanitizedContent.value)
)
const toggleQuote = () => {
showQuotedText.value = !showQuotedText.value
@@ -0,0 +1,63 @@
import { describe, test, expect } from 'vitest'
import { containsQuoteMarkers } from '@shared-ui/utils/quotedContent.js'
describe('containsQuoteMarkers', () => {
test('returns false for null', () => {
expect(containsQuoteMarkers(null)).toBe(false)
})
test('returns false for undefined', () => {
expect(containsQuoteMarkers(undefined)).toBe(false)
})
test('returns false for empty string', () => {
expect(containsQuoteMarkers('')).toBe(false)
})
test('returns false for plain HTML with no quote', () => {
expect(containsQuoteMarkers('<p>Hello world</p><div>thanks</div>')).toBe(false)
})
test('detects <blockquote> (Gmail/Apple/Thunderbird)', () => {
expect(containsQuoteMarkers('<blockquote>previous</blockquote>')).toBe(true)
})
test('detects <blockquote> with attributes', () => {
expect(containsQuoteMarkers('<blockquote type="cite" class="gmail_quote">x</blockquote>'))
.toBe(true)
})
test('detects Outlook Web appendonsend marker', () => {
expect(containsQuoteMarkers('<div id="appendonsend"></div>')).toBe(true)
})
test('detects Outlook divRplyFwdMsg marker', () => {
expect(containsQuoteMarkers('<div id="divRplyFwdMsg" dir="ltr">From:</div>')).toBe(true)
})
test('detects Outlook for Mac OLK_SRC_BODY_SECTION marker', () => {
expect(containsQuoteMarkers('<div id="OLK_SRC_BODY_SECTION">body</div>')).toBe(true)
})
test('detects legacy Outlook OutlookMessageHeader class', () => {
expect(containsQuoteMarkers('<div class="OutlookMessageHeader">x</div>')).toBe(true)
})
test('detects real Hotmail reply payload', () => {
const html = `<div class="elementToProof">Quoted reply!</div>
<div id="appendonsend"></div>
<hr style="display:inline-block;width:98%" tabindex="-1">
<div id="divRplyFwdMsg" dir="ltr"><font face="Calibri, sans-serif">From: Libredesk
Sent: 17 May 2026 18:12</font></div>
<div>Original message body</div>`
expect(containsQuoteMarkers(html)).toBe(true)
})
test('does not match marker word in plain prose', () => {
expect(containsQuoteMarkers('I love the appendonsend feature in Outlook')).toBe(false)
})
test('does not match if id uses single quotes (edge case - Outlook always emits double)', () => {
expect(containsQuoteMarkers(`<div id='appendonsend'></div>`)).toBe(false)
})
})
@@ -160,6 +160,7 @@ import MessageAttachment from './MessageAttachment.vue'
import CSATMessageBubble from './CSATMessageBubble.vue'
import { TypingIndicator } from '@shared-ui/components/TypingIndicator'
import { Spinner } from '@shared-ui/components/ui/spinner'
import { containsQuoteMarkers } from '@shared-ui/utils/quotedContent.js'
const extendedCssProperties = [...allowedCssProperties, 'transform', 'transform-origin']
@@ -187,9 +188,7 @@ const getMessageTime = (timestamp) => {
return useRelativeTime(new Date(timestamp)).value
}
const hasQuotedContent = (content) => {
return content && content.includes('<blockquote')
}
const hasQuotedContent = (content) => containsQuoteMarkers(content)
const isQuotedTextVisible = (messageUuid) => {
return quotedTextState.value[messageUuid] || false
+15 -1
View File
@@ -296,7 +296,21 @@
}
.hide-quoted-text {
blockquote {
// Selectors below use attribute-suffix matching because vue-letter
// prefixes every id/class with a random "msg_XYZ_" at render time, so
// raw "#appendonsend" would never match.
// Outlook/Hotmail emit a marker with the quoted history as trailing
// siblings (no wrapping element), so we hide the marker AND everything
// after it. Outlook always top-posts, so trailing siblings are quote.
blockquote,
[id$="_divRplyFwdMsg"],
[id$="_divRplyFwdMsg"] ~ *,
[id$="_appendonsend"],
[id$="_appendonsend"] ~ *,
[id$="_OLK_SRC_BODY_SECTION"],
[id$="_OLK_SRC_BODY_SECTION"] ~ *,
[class*="_OutlookMessageHeader"],
[class*="_OutlookMessageHeader"] ~ * {
@apply hidden;
}
}
+19
View File
@@ -0,0 +1,19 @@
// Runs against the RAW HTML stored on the message - before vue-letter
// renders. vue-letter prefixes every id/class with a random "msg_XYZ_" at
// render time, so any CSS that needs to match the rendered DOM must use
// attribute-suffix selectors (see .hide-quoted-text in main.scss).
//
// Scope: <blockquote> covers Gmail/Apple/Thunderbird. The id/class markers
// below cover the Microsoft variants (Outlook desktop/web/mac, Hotmail).
export const QUOTE_MARKERS = [
'<blockquote',
'id="divRplyFwdMsg"',
'id="appendonsend"',
'id="OLK_SRC_BODY_SECTION"',
'class="OutlookMessageHeader"'
]
export const containsQuoteMarkers = (html) => {
if (!html) return false
return QUOTE_MARKERS.some((marker) => html.includes(marker))
}