feat: |Doc| use shadow DOM render mail html (#604)

This commit is contained in:
Dream Hunter
2025-03-08 10:53:45 +08:00
committed by GitHub
parent 97d24b2087
commit 908fc0cc86
13 changed files with 1086 additions and 971 deletions

3
.flake8 Normal file
View File

@@ -0,0 +1,3 @@
[flake8]
max-line-length = 180
exclude = .git,__pycache__,build,dist

View File

@@ -4,6 +4,7 @@
## main(v0.9.1)
- feat: |UI| support google ads
- feat: |UI| 使用 shadow DOM 防止样式污染
## v0.9.0

View File

@@ -21,10 +21,10 @@
"dependencies": {
"@simplewebauthn/browser": "10.0.0",
"@unhead/vue": "^1.11.20",
"@vueuse/core": "^12.7.0",
"@vueuse/core": "^12.8.2",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.8.1",
"axios": "^1.8.2",
"jszip": "^3.10.1",
"mail-parser-wasm": "^0.2.1",
"naive-ui": "^2.41.0",
@@ -32,21 +32,21 @@
"vooks": "^0.2.12",
"vue": "^3.5.13",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^11.1.1",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vicons/fa": "^0.13.0",
"@vicons/material": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.1",
"unplugin-auto-import": "^19.1.0",
"unplugin-vue-components": "^28.4.0",
"vite": "^6.2.0",
"unplugin-auto-import": "^19.1.1",
"unplugin-vue-components": "^28.4.1",
"vite": "^6.2.1",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-top-level-await": "^1.5.0",
"vite-plugin-wasm": "^3.4.1",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0",
"wrangler": "^3.110.0"
"wrangler": "^3.114.0"
}
}

747
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material
import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
import { utcToLocalDate } from '../utils';
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
const message = useMessage()
const isMobile = useIsMobile()
@@ -171,7 +172,7 @@ const refresh = async () => {
}
};
const backFirstPageAndRefresh = async () =>{
const backFirstPageAndRefresh = async () => {
page.value = 1;
await refresh();
}
@@ -380,7 +381,7 @@ onBeforeUnmount(() => {
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
:on-update:size="onSpiltSizeChange">
<template #1>
<div style="overflow: auto; height: 80vh;">
<div style="overflow: auto; min-height: 50vh; max-height: 100vh;">
<n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
:class="mailItemClass(row)">
@@ -396,10 +397,14 @@ onBeforeUnmount(() => {
{{ utcToLocalDate(row.created_at, useUTCDate) }}
</n-tag>
<n-tag type="info">
FROM: {{ row.source }}
<n-ellipsis style="max-width: 240px;">
{{ showEMailTo ? "FROM: " + row.source : row.source }}
</n-ellipsis>
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
<n-ellipsis style="max-width: 240px;">
TO: {{ row.address }}
</n-ellipsis>
</n-tag>
</template>
</n-thing>
@@ -460,7 +465,7 @@ onBeforeUnmount(() => {
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
<ShadowHtmlComponent v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
</n-card>
<n-card :bordered="false" embedded class="mail-item" v-else>
<n-result status="info" :title="t('pleaseSelectMail')">
@@ -498,7 +503,7 @@ onBeforeUnmount(() => {
{{ utcToLocalDate(row.created_at, useUTCDate) }}
</n-tag>
<n-tag type="info">
FROM: {{ row.source }}
{{ showEMailTo ? "FROM: " + row.source : row.source }}
</n-tag>
<n-tag v-if="showEMailTo" type="info">
TO: {{ row.address }}
@@ -560,7 +565,7 @@ onBeforeUnmount(() => {
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
<ShadowHtmlComponent :key="curMail.id" v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
</n-card>
</n-drawer-content>
</n-drawer>

View File

@@ -0,0 +1,75 @@
<template>
<div v-if="useFallback" v-html="htmlContent"></div>
<div v-else ref="shadowHost"></div>
</template>
<script setup>
import { ref, watch, onMounted, onBeforeUnmount, defineProps } from 'vue';
const props = defineProps({
htmlContent: {
type: String,
required: true,
},
});
const shadowHost = ref(null);
let shadowRoot = null;
const useFallback = ref(false);
/**
* Renders content into Shadow DOM with fallback to v-html
*/
const renderShadowDom = () => {
if (!shadowHost.value && !useFallback.value) return;
try {
// Don't attempt to use Shadow DOM if already in fallback mode
if (useFallback.value) return;
// Initialize Shadow DOM if not already created
if (!shadowRoot && shadowHost.value) {
try {
shadowRoot = shadowHost.value.attachShadow({ mode: 'open' });
} catch (error) {
console.warn('Shadow DOM not supported, falling back to v-html:', error);
useFallback.value = true;
return;
}
}
// Update content if Shadow DOM exists
if (shadowRoot) {
shadowRoot.innerHTML = props.htmlContent;
}
} catch (error) {
console.error('Failed to render Shadow DOM, falling back to v-html:', error);
useFallback.value = true;
}
};
// Initial render when component is mounted
onMounted(() => {
// Check if Shadow DOM is supported in this browser
if (typeof Element.prototype.attachShadow !== 'function') {
console.warn('Shadow DOM is not supported in this browser, using v-html fallback');
useFallback.value = true;
return;
}
renderShadowDom();
});
// Clean up resources when component is unmounted
onBeforeUnmount(() => {
if (shadowRoot) {
shadowRoot.innerHTML = '';
}
shadowRoot = null;
});
// Update Shadow DOM when htmlContent changes
watch(() => props.htmlContent, () => {
renderShadowDom();
}, { flush: 'post' });
</script>

View File

@@ -11,6 +11,6 @@
"author": "",
"license": "ISC",
"devDependencies": {
"wrangler": "^3.110.0"
"wrangler": "^3.114.0"
}
}

View File

@@ -135,7 +135,7 @@ class SimpleMailbox:
self.message_count = res.json()["count"]
return [
(start + uid, SimpleMessage(start + uid, parse_email(item["raw"])))
for uid, item in enumerate(reversed(res.json()["results"]))
for uid, item in enumerate(res.json()["results"])
]
def fetchSENT(self, messages):

View File

@@ -48,17 +48,14 @@ def generate_email_model(item: dict) -> EmailModel:
email_json = json.loads(item["raw"])
message = MIMEMultipart()
if email_json.get("version") == "v2":
message['From'] = f"{email_json["from_name"]} <{item["address"]}>" if email_json.get(
"from_name") else item["address"]
message['To'] = f"{email_json["to_name"]} <{email_json["to_mail"]}>" if email_json.get(
"to_name") else email_json["to_mail"]
message['From'] = f'{email_json["from_name"]} <{item["address"]}>' if email_json.get("from_name") else item["address"]
message['To'] = f'{email_json["to_name"]} <{email_json["to_mail"]}>' if email_json.get("to_name") else email_json["to_mail"]
message.attach(MIMEText(
email_json["content"],
"html" if email_json.get("is_html") else "plain"
))
else:
message['From'] = f"{email_json["from"]['name']} <{
email_json["from"]['email']}>"
message['From'] = f'{email_json["from"]["name"]} <{email_json["from"]["email"]}>'
message['To'] = ", ".join(
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
message.attach(MIMEText(

View File

@@ -4,9 +4,9 @@
"version": "0.9.1",
"type": "module",
"devDependencies": {
"@types/node": "^22.13.5",
"@types/node": "^22.13.9",
"vitepress": "^1.6.3",
"wrangler": "^3.110.0"
"wrangler": "^3.114.0"
},
"scripts": {
"dev": "vitepress dev docs",

File diff suppressed because it is too large Load Diff

View File

@@ -11,19 +11,19 @@
"build": "wrangler deploy --dry-run --outdir dist --minify"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250224.0",
"@cloudflare/workers-types": "^4.20250303.0",
"@eslint/js": "9.18.0",
"@simplewebauthn/types": "10.0.0",
"eslint": "9.18.0",
"globals": "^15.15.0",
"typescript-eslint": "^8.25.0",
"wrangler": "^3.110.0"
"typescript-eslint": "^8.26.0",
"wrangler": "^3.114.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.750.0",
"@aws-sdk/s3-request-presigner": "^3.750.0",
"@aws-sdk/client-s3": "^3.758.0",
"@aws-sdk/s3-request-presigner": "^3.758.0",
"@simplewebauthn/server": "10.0.1",
"hono": "^4.7.2",
"hono": "^4.7.4",
"mimetext": "^3.0.27",
"postal-mime": "^2.4.3",
"resend": "^4.1.2",

541
worker/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff