mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-09-08 17:08:59 +08:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce937fc4a9 | ||
|
|
5162f0a478 | ||
|
|
e06b7ccbb2 | ||
|
|
24016a0051 | ||
|
|
acd9a8f099 | ||
|
|
148fb09754 | ||
|
|
5951998aad | ||
|
|
01aa97e384 |
@@ -10,6 +10,8 @@
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Frontend| 外观设置新增“自动加载远程内容”开关,默认关闭;关闭后邮件 HTML 中的远程图片默认以占位图显示,并支持单封邮件临时加载远程内容(issue #1073)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |AI 提取| 强化提示词,要求 AI 保持邮件原始链接域名,避免小模型改写验证链接域名导致错误跳转(issue #1072)
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
### Features
|
||||
|
||||
- feat: |Frontend| Add an Appearance toggle for automatically loading remote content, disabled by default; when disabled, HTML mail shows blocked-image placeholders by default and supports temporarily loading remote content for a single mail (issue #1073)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix: |AI Extract| Strengthen the prompt to keep original link domains from the email, preventing small models from rewriting verification-link domains (issue #1072)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
<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";
|
||||
@@ -8,10 +8,39 @@ import { getDownloadEmlUrl } from '../utils/email-parser';
|
||||
import { utcToLocalDate } from '../utils';
|
||||
import { useGlobalState } from '../store';
|
||||
|
||||
const { preferShowTextMail, useIframeShowMail, useUTCDate, isDark } = useGlobalState();
|
||||
const { preferShowTextMail, useIframeShowMail, useUTCDate, isDark, autoLoadRemoteContent } = useGlobalState();
|
||||
|
||||
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,
|
||||
@@ -57,6 +86,15 @@ const showAttachments = ref(false);
|
||||
const curAttachments = ref([]);
|
||||
const attachmentLoding = ref(false);
|
||||
const showFullscreen = ref(false);
|
||||
const loadRemoteContentForCurrentMail = ref(false);
|
||||
const shouldLoadRemoteContent = computed(() => autoLoadRemoteContent.value || loadRemoteContentForCurrentMail.value);
|
||||
const mailHtmlContent = computed(() => shouldLoadRemoteContent.value
|
||||
? (props.mail.message || '')
|
||||
: blockRemoteImages(props.mail.message));
|
||||
|
||||
watch(() => props.mail.id, () => {
|
||||
loadRemoteContentForCurrentMail.value = false;
|
||||
});
|
||||
|
||||
const handleDelete = () => {
|
||||
props.onDelete();
|
||||
@@ -149,6 +187,12 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
</template>
|
||||
{{ t('fullscreen') }}
|
||||
</n-button>
|
||||
|
||||
<n-button v-if="!showTextMail && !shouldLoadRemoteContent" size="small" tertiary type="info"
|
||||
@click="loadRemoteContentForCurrentMail = true">
|
||||
{{ t('loadRemoteContent') }}
|
||||
</n-button>
|
||||
|
||||
</n-space>
|
||||
|
||||
<!-- AI 提取信息 -->
|
||||
@@ -157,9 +201,9 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
<!-- 邮件内容 -->
|
||||
<div class="mail-content" :class="{ 'dark-mode': isDark }">
|
||||
<pre v-if="showTextMail" class="mail-text">{{ mail.text }}</pre>
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mail.message" class="mail-iframe">
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mailHtmlContent" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" :isDark="isDark" class="mail-html" />
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mailHtmlContent" :isDark="isDark" class="mail-html" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -168,9 +212,9 @@ const handleSaveToS3 = async (filename, blob) => {
|
||||
<n-drawer-content :title="mail.subject" closable>
|
||||
<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="mail.message" class="mail-iframe">
|
||||
<iframe v-else-if="useIframeShowMail" :srcdoc="mailHtmlContent" class="mail-iframe">
|
||||
</iframe>
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mail.message" :isDark="isDark" class="mail-html" />
|
||||
<ShadowHtmlComponent v-else :key="mail.id" :htmlContent="mailHtmlContent" :isDark="isDark" class="mail-html" />
|
||||
</div>
|
||||
</n-drawer-content>
|
||||
</n-drawer>
|
||||
|
||||
@@ -178,6 +178,7 @@ export const deMessages = {
|
||||
"views.admin.SenderAccess.disable": "Deaktivieren",
|
||||
"views.Admin.loginViaDisabledCheck": "Passwortprüfung deaktiviert",
|
||||
"views.common.Appearance.preferShowTextMail": "Text-Mail standardmäßig anzeigen",
|
||||
"views.common.Appearance.autoLoadRemoteContent": "Remote-Inhalte automatisch laden",
|
||||
"views.admin.AccountSettings.domain_list": "Domain-Liste (optional)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "Die Domain-Liste filtert nach Empfängeradresse, die Quell-Regex nach Absenderadresse. Für die Weiterleitung müssen beide Bedingungen erfüllt sein (UND-Logik). Leer lassen, um den jeweiligen Filter zu überspringen.",
|
||||
"views.admin.UserManagement.domains": "Domains",
|
||||
@@ -237,6 +238,7 @@ export const deMessages = {
|
||||
"views.admin.AccountSettings.forward_address_required": "Eine Weiterleitungsadresse ist erforderlich",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "Vollbild",
|
||||
"components.MailContentRenderer.loadRemoteContent": "Remote-Inhalte laden",
|
||||
"views.common.Login.generateName": "Zufälligen Namen erzeugen",
|
||||
"views.admin.Telegram.globalMailPushList": "Globale Mail-Push-Chat-ID-Liste",
|
||||
"views.common.Appearance.globalTabplacement": "Globale Tab-Position",
|
||||
|
||||
@@ -178,6 +178,7 @@ export const esMessages = {
|
||||
"views.admin.SenderAccess.disable": "Deshabilitar",
|
||||
"views.Admin.loginViaDisabledCheck": "Comprobación de contraseña deshabilitada",
|
||||
"views.common.Appearance.preferShowTextMail": "Mostrar correo en texto por defecto",
|
||||
"views.common.Appearance.autoLoadRemoteContent": "Cargar contenido remoto automáticamente",
|
||||
"views.admin.AccountSettings.domain_list": "Lista de dominios (opcional)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "La lista de dominios filtra por destinatario y la regex de origen por remitente. Ambas condiciones deben cumplirse para reenviar (lógica AND). Deja alguna vacía para omitirla.",
|
||||
"views.admin.UserManagement.domains": "Dominios",
|
||||
@@ -237,6 +238,7 @@ export const esMessages = {
|
||||
"views.admin.AccountSettings.forward_address_required": "La dirección de reenvío es obligatoria",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "Pantalla completa",
|
||||
"components.MailContentRenderer.loadRemoteContent": "Cargar contenido remoto",
|
||||
"views.common.Login.generateName": "Generar nombre aleatorio",
|
||||
"views.admin.Telegram.globalMailPushList": "Lista global de chat ID para envío de correos",
|
||||
"views.common.Appearance.globalTabplacement": "Posición global de pestañas",
|
||||
|
||||
@@ -178,6 +178,7 @@ export const jaMessages = {
|
||||
"views.admin.SenderAccess.disable": "無効化",
|
||||
"views.Admin.loginViaDisabledCheck": "パスワードチェックを無効化",
|
||||
"views.common.Appearance.preferShowTextMail": "既定でテキストメールを表示",
|
||||
"views.common.Appearance.autoLoadRemoteContent": "リモートコンテンツを自動で読み込む",
|
||||
"views.admin.AccountSettings.domain_list": "ドメイン一覧(任意)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "ドメイン一覧は受信先アドレスで、送信元正規表現は送信者アドレスでフィルタします。転送には両方の条件を満たす必要があります(AND)。どちらかを空欄にするとその条件は無視されます。",
|
||||
"views.admin.UserManagement.domains": "ドメイン",
|
||||
@@ -237,6 +238,7 @@ export const jaMessages = {
|
||||
"views.admin.AccountSettings.forward_address_required": "転送先アドレスは必須です",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "全画面",
|
||||
"components.MailContentRenderer.loadRemoteContent": "リモートコンテンツを読み込む",
|
||||
"views.common.Login.generateName": "ランダム名を生成",
|
||||
"views.admin.Telegram.globalMailPushList": "グローバルメール通知 Chat ID 一覧",
|
||||
"views.common.Appearance.globalTabplacement": "全体タブ位置",
|
||||
|
||||
@@ -178,6 +178,7 @@ export const ptBRMessages = {
|
||||
"views.admin.SenderAccess.disable": "Desativar",
|
||||
"views.Admin.loginViaDisabledCheck": "Verificação de senha desativada",
|
||||
"views.common.Appearance.preferShowTextMail": "Exibir e-mail em texto por padrão",
|
||||
"views.common.Appearance.autoLoadRemoteContent": "Carregar conteúdo remoto automaticamente",
|
||||
"views.admin.AccountSettings.domain_list": "Lista de domínios (opcional)",
|
||||
"views.admin.AccountSettings.source_patterns_tip": "A lista de domínios filtra pelo destinatário e o regex de origem filtra pelo remetente. Ambas as condições precisam corresponder para encaminhar (lógica AND). Deixe qualquer uma vazia para ignorá-la.",
|
||||
"views.admin.UserManagement.domains": "Domínios",
|
||||
@@ -237,6 +238,7 @@ export const ptBRMessages = {
|
||||
"views.admin.AccountSettings.forward_address_required": "O endereço de encaminhamento é obrigatório",
|
||||
"views.admin.AccountSettings.forward_placeholder": "forward@example.com",
|
||||
"components.MailContentRenderer.fullscreen": "Tela cheia",
|
||||
"components.MailContentRenderer.loadRemoteContent": "Carregar conteúdo remoto",
|
||||
"views.common.Login.generateName": "Gerar nome aleatório",
|
||||
"views.admin.Telegram.globalMailPushList": "Lista global de chat ID para envio de e-mails",
|
||||
"views.common.Appearance.globalTabplacement": "Posição global das abas",
|
||||
|
||||
@@ -186,6 +186,10 @@ export const MESSAGE_REGISTRY = {
|
||||
"en": "Fullscreen",
|
||||
"zh": "全屏"
|
||||
},
|
||||
"loadRemoteContent": {
|
||||
"en": "Load Remote Content",
|
||||
"zh": "加载远程内容"
|
||||
},
|
||||
"reply": {
|
||||
"en": "Reply",
|
||||
"zh": "回复"
|
||||
@@ -2114,6 +2118,10 @@ export const MESSAGE_REGISTRY = {
|
||||
"en": "Display text Mail by default",
|
||||
"zh": "默认以文本显示邮件"
|
||||
},
|
||||
"autoLoadRemoteContent": {
|
||||
"en": "Automatically load remote content",
|
||||
"zh": "自动加载远程内容"
|
||||
},
|
||||
"right": {
|
||||
"en": "right",
|
||||
"zh": "右侧"
|
||||
|
||||
@@ -90,6 +90,7 @@ export const useGlobalState = createGlobalState(
|
||||
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
|
||||
const useIframeShowMail = useStorage('useIframeShowMail', false);
|
||||
const preferShowTextMail = useStorage('preferShowTextMail', false);
|
||||
const autoLoadRemoteContent = useStorage('autoLoadRemoteContent', false);
|
||||
const userJwt = useStorage('userJwt', '');
|
||||
const preferredLocale = useStorage('preferredLocale', '');
|
||||
const userTab = useSessionStorage('userTab', 'address_management');
|
||||
@@ -162,6 +163,7 @@ export const useGlobalState = createGlobalState(
|
||||
mailboxSplitSize,
|
||||
useIframeShowMail,
|
||||
preferShowTextMail,
|
||||
autoLoadRemoteContent,
|
||||
userJwt,
|
||||
preferredLocale,
|
||||
userTab,
|
||||
|
||||
@@ -12,7 +12,7 @@ const props = defineProps({
|
||||
|
||||
const {
|
||||
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
|
||||
globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex
|
||||
globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex, autoLoadRemoteContent
|
||||
} = useGlobalState()
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
@@ -43,6 +43,9 @@ const { t } = useScopedI18n('views.common.Appearance')
|
||||
<n-form-item-row :label="t('useIframeShowMail')">
|
||||
<n-switch v-model:value="useIframeShowMail" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('autoLoadRemoteContent')">
|
||||
<n-switch v-model:value="autoLoadRemoteContent" :round="false" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('useUTCDate')">
|
||||
<n-switch v-model:value="useUTCDate" :round="false" />
|
||||
</n-form-item-row>
|
||||
|
||||
Reference in New Issue
Block a user