mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-08-06 05:53:36 +08:00
refactor: simplify remote content handling
This commit is contained in:
@@ -1,11 +1,10 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { useScopedI18n } from '@/i18n/app'
|
||||
import { CloudDownloadRound, ReplyFilled, ForwardFilled, FullscreenRound } from '@vicons/material'
|
||||
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||
import AiExtractInfo from "./AiExtractInfo.vue";
|
||||
import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||
import { applyRemoteContentPolicy } from '../utils/mail-html';
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
@@ -13,6 +12,35 @@ const { preferShowTextMail, useIframeShowMail, useUTCDate, isDark, autoLoadRemot
|
||||
|
||||
const { t } = useScopedI18n('components.MailContentRenderer')
|
||||
|
||||
const blockedRemoteContentPlaceholder = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80" viewBox="0 0 320 80">
|
||||
<rect width="320" height="80" rx="8" fill="#f3f4f6"/>
|
||||
<text x="160" y="45" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#6b7280">Remote content blocked</text>
|
||||
</svg>
|
||||
`)}`;
|
||||
|
||||
const hasRemoteUrl = (value: string | null) => /(?:^|,)\s*(?:https?:)?\/\//i.test(value || '');
|
||||
|
||||
const blockRemoteImages = (html?: string | null) => {
|
||||
if (!html || typeof DOMParser !== 'function') return html || '';
|
||||
|
||||
const hasDocumentShell = /<!doctype|<html[\s>]/i.test(html);
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
|
||||
for (const image of doc.querySelectorAll('img')) {
|
||||
const src = image.getAttribute('src');
|
||||
const srcset = image.getAttribute('srcset');
|
||||
if (!hasRemoteUrl(src) && !hasRemoteUrl(srcset)) continue;
|
||||
|
||||
if (src) image.setAttribute('data-blocked-src', src);
|
||||
if (srcset) image.setAttribute('data-blocked-srcset', srcset);
|
||||
image.removeAttribute('srcset');
|
||||
image.setAttribute('src', blockedRemoteContentPlaceholder);
|
||||
}
|
||||
|
||||
return hasDocumentShell ? `<!doctype html>\n${doc.documentElement.outerHTML}` : doc.body.innerHTML;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
mail: {
|
||||
type: Object,
|
||||
@@ -60,7 +88,9 @@ const attachmentLoding = ref(false);
|
||||
const showFullscreen = ref(false);
|
||||
const loadRemoteContentForCurrentMail = ref(false);
|
||||
const shouldLoadRemoteContent = computed(() => autoLoadRemoteContent.value || loadRemoteContentForCurrentMail.value);
|
||||
const mailHtmlContent = computed(() => applyRemoteContentPolicy(props.mail.message, shouldLoadRemoteContent.value));
|
||||
const mailHtmlContent = computed(() => shouldLoadRemoteContent.value
|
||||
? (props.mail.message || '')
|
||||
: blockRemoteImages(props.mail.message));
|
||||
|
||||
watch(() => props.mail.id, () => {
|
||||
loadRemoteContentForCurrentMail.value = false;
|
||||
@@ -180,12 +210,6 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
<n-drawer v-model:show="showFullscreen" width="100%" placement="bottom" :trap-focus="false" :block-scroll="false"
|
||||
style="height: 100vh;">
|
||||
<n-drawer-content :title="mail.subject" closable>
|
||||
<n-space v-if="!showTextMail && !shouldLoadRemoteContent" class="fullscreen-actions">
|
||||
<n-button size="small" tertiary type="info" @click="loadRemoteContentForCurrentMail = true">
|
||||
{{ t('loadRemoteContent') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
|
||||
<div class="fullscreen-mail-content" :class="{ 'dark-mode': isDark }">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mailHtmlContent" class="mail-iframe">
|
||||
@@ -279,10 +303,6 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.fullscreen-actions {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.fullscreen-mail-content .mail-iframe {
|
||||
min-height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
const REMOTE_CONTENT_PLACEHOLDER = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(`
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80" viewBox="0 0 320 80">
|
||||
<rect width="320" height="80" rx="8" fill="#f3f4f6"/>
|
||||
<text x="160" y="45" text-anchor="middle" font-family="sans-serif" font-size="14" fill="#6b7280">Remote content blocked</text>
|
||||
</svg>
|
||||
`)}`;
|
||||
|
||||
const REMOTE_URL_PATTERN = /^(?:https?:)?\/\//i;
|
||||
const DOCUMENT_SHELL_PATTERN = /<!doctype|<html[\s>]/i;
|
||||
|
||||
const isRemoteUrl = (value: string | null) => {
|
||||
return REMOTE_URL_PATTERN.test(value?.trim() || '');
|
||||
};
|
||||
|
||||
const hasRemoteSrcset = (value: string | null) => {
|
||||
if (!value) return false;
|
||||
|
||||
return value.split(',')
|
||||
.some((candidate) => isRemoteUrl(candidate.trim().split(/\s+/)[0] || null));
|
||||
};
|
||||
|
||||
const shouldBlockImage = (image: HTMLImageElement) => {
|
||||
return isRemoteUrl(image.getAttribute('src'))
|
||||
|| hasRemoteSrcset(image.getAttribute('srcset'));
|
||||
};
|
||||
|
||||
const blockImage = (image: HTMLImageElement) => {
|
||||
const src = image.getAttribute('src');
|
||||
const srcset = image.getAttribute('srcset');
|
||||
|
||||
if (src) image.setAttribute('data-blocked-src', src);
|
||||
if (srcset) image.setAttribute('data-blocked-srcset', srcset);
|
||||
|
||||
image.removeAttribute('srcset');
|
||||
image.setAttribute('src', REMOTE_CONTENT_PLACEHOLDER);
|
||||
image.setAttribute('loading', 'lazy');
|
||||
image.style.maxWidth = '100%';
|
||||
image.style.height = 'auto';
|
||||
image.style.border = '1px solid #d1d5db';
|
||||
image.style.borderRadius = '8px';
|
||||
};
|
||||
|
||||
const serializeMailHtml = (doc: Document, hasDocumentShell: boolean) => {
|
||||
if (hasDocumentShell) {
|
||||
return `<!doctype html>\n${doc.documentElement.outerHTML}`;
|
||||
}
|
||||
|
||||
return doc.body.innerHTML;
|
||||
};
|
||||
|
||||
export const applyRemoteContentPolicy = (html: string | null | undefined, loadRemoteContent: boolean) => {
|
||||
const mailHtml = html || '';
|
||||
if (loadRemoteContent || !mailHtml) return mailHtml;
|
||||
if (typeof DOMParser !== 'function') return mailHtml;
|
||||
|
||||
const hasDocumentShell = DOCUMENT_SHELL_PATTERN.test(mailHtml);
|
||||
const doc = new DOMParser().parseFromString(mailHtml, 'text/html');
|
||||
|
||||
for (const image of doc.querySelectorAll('img')) {
|
||||
if (shouldBlockImage(image)) {
|
||||
blockImage(image);
|
||||
}
|
||||
}
|
||||
|
||||
return serializeMailHtml(doc, hasDocumentShell);
|
||||
};
|
||||
Reference in New Issue
Block a user