Skip to content

Commit 908fc0c

Browse files
feat: |Doc| use shadow DOM render mail html (#604)
1 parent 97d24b2 commit 908fc0c

13 files changed

+1087
-972
lines changed

.flake8

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

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
## main(v0.9.1)
55

66
- feat: |UI| support google ads
7+
- feat: |UI| 使用 shadow DOM 防止样式污染
78

89
## v0.9.0
910

frontend/package.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,32 @@
2121
"dependencies": {
2222
"@simplewebauthn/browser": "10.0.0",
2323
"@unhead/vue": "^1.11.20",
24-
"@vueuse/core": "^12.7.0",
24+
"@vueuse/core": "^12.8.2",
2525
"@wangeditor/editor": "^5.1.23",
2626
"@wangeditor/editor-for-vue": "^5.1.12",
27-
"axios": "^1.8.1",
27+
"axios": "^1.8.2",
2828
"jszip": "^3.10.1",
2929
"mail-parser-wasm": "^0.2.1",
3030
"naive-ui": "^2.41.0",
3131
"postal-mime": "^2.4.3",
3232
"vooks": "^0.2.12",
3333
"vue": "^3.5.13",
3434
"vue-clipboard3": "^2.0.0",
35-
"vue-i18n": "^11.1.1",
35+
"vue-i18n": "^11.1.2",
3636
"vue-router": "^4.5.0"
3737
},
3838
"devDependencies": {
3939
"@vicons/fa": "^0.13.0",
4040
"@vicons/material": "^0.13.0",
4141
"@vitejs/plugin-vue": "^5.2.1",
42-
"unplugin-auto-import": "^19.1.0",
43-
"unplugin-vue-components": "^28.4.0",
44-
"vite": "^6.2.0",
42+
"unplugin-auto-import": "^19.1.1",
43+
"unplugin-vue-components": "^28.4.1",
44+
"vite": "^6.2.1",
4545
"vite-plugin-pwa": "^0.21.1",
4646
"vite-plugin-top-level-await": "^1.5.0",
4747
"vite-plugin-wasm": "^3.4.1",
4848
"workbox-build": "^7.3.0",
4949
"workbox-window": "^7.3.0",
50-
"wrangler": "^3.110.0"
50+
"wrangler": "^3.114.0"
5151
}
5252
}

frontend/pnpm-lock.yaml

+395-352
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/components/MailBox.vue

+12-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material
77
import { useIsMobile } from '../utils/composables'
88
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
99
import { utcToLocalDate } from '../utils';
10+
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
1011
1112
const message = useMessage()
1213
const isMobile = useIsMobile()
@@ -171,7 +172,7 @@ const refresh = async () => {
171172
}
172173
};
173174
174-
const backFirstPageAndRefresh = async () =>{
175+
const backFirstPageAndRefresh = async () => {
175176
page.value = 1;
176177
await refresh();
177178
}
@@ -380,7 +381,7 @@ onBeforeUnmount(() => {
380381
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
381382
:on-update:size="onSpiltSizeChange">
382383
<template #1>
383-
<div style="overflow: auto; height: 80vh;">
384+
<div style="overflow: auto; min-height: 50vh; max-height: 100vh;">
384385
<n-list hoverable clickable>
385386
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
386387
:class="mailItemClass(row)">
@@ -396,10 +397,14 @@ onBeforeUnmount(() => {
396397
{{ utcToLocalDate(row.created_at, useUTCDate) }}
397398
</n-tag>
398399
<n-tag type="info">
399-
FROM: {{ row.source }}
400+
<n-ellipsis style="max-width: 240px;">
401+
{{ showEMailTo ? "FROM: " + row.source : row.source }}
402+
</n-ellipsis>
400403
</n-tag>
401404
<n-tag v-if="showEMailTo" type="info">
402-
TO: {{ row.address }}
405+
<n-ellipsis style="max-width: 240px;">
406+
TO: {{ row.address }}
407+
</n-ellipsis>
403408
</n-tag>
404409
</template>
405410
</n-thing>
@@ -460,7 +465,7 @@ onBeforeUnmount(() => {
460465
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
461466
style="margin-top: 10px;width: 100%; height: 100%;">
462467
</iframe>
463-
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
468+
<ShadowHtmlComponent v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
464469
</n-card>
465470
<n-card :bordered="false" embedded class="mail-item" v-else>
466471
<n-result status="info" :title="t('pleaseSelectMail')">
@@ -498,7 +503,7 @@ onBeforeUnmount(() => {
498503
{{ utcToLocalDate(row.created_at, useUTCDate) }}
499504
</n-tag>
500505
<n-tag type="info">
501-
FROM: {{ row.source }}
506+
{{ showEMailTo ? "FROM: " + row.source : row.source }}
502507
</n-tag>
503508
<n-tag v-if="showEMailTo" type="info">
504509
TO: {{ row.address }}
@@ -560,7 +565,7 @@ onBeforeUnmount(() => {
560565
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
561566
style="margin-top: 10px;width: 100%; height: 100%;">
562567
</iframe>
563-
<div v-else v-html="curMail.message" style="margin-top: 10px;"></div>
568+
<ShadowHtmlComponent :key="curMail.id" v-else :htmlContent="curMail.message" style="margin-top: 10px;" />
564569
</n-card>
565570
</n-drawer-content>
566571
</n-drawer>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<template>
2+
<div v-if="useFallback" v-html="htmlContent"></div>
3+
<div v-else ref="shadowHost"></div>
4+
</template>
5+
6+
<script setup>
7+
import { ref, watch, onMounted, onBeforeUnmount, defineProps } from 'vue';
8+
9+
const props = defineProps({
10+
htmlContent: {
11+
type: String,
12+
required: true,
13+
},
14+
});
15+
16+
const shadowHost = ref(null);
17+
let shadowRoot = null;
18+
const useFallback = ref(false);
19+
20+
/**
21+
* Renders content into Shadow DOM with fallback to v-html
22+
*/
23+
const renderShadowDom = () => {
24+
if (!shadowHost.value && !useFallback.value) return;
25+
26+
try {
27+
// Don't attempt to use Shadow DOM if already in fallback mode
28+
if (useFallback.value) return;
29+
30+
// Initialize Shadow DOM if not already created
31+
if (!shadowRoot && shadowHost.value) {
32+
try {
33+
shadowRoot = shadowHost.value.attachShadow({ mode: 'open' });
34+
} catch (error) {
35+
console.warn('Shadow DOM not supported, falling back to v-html:', error);
36+
useFallback.value = true;
37+
return;
38+
}
39+
}
40+
41+
// Update content if Shadow DOM exists
42+
if (shadowRoot) {
43+
shadowRoot.innerHTML = props.htmlContent;
44+
}
45+
} catch (error) {
46+
console.error('Failed to render Shadow DOM, falling back to v-html:', error);
47+
useFallback.value = true;
48+
}
49+
};
50+
51+
// Initial render when component is mounted
52+
onMounted(() => {
53+
// Check if Shadow DOM is supported in this browser
54+
if (typeof Element.prototype.attachShadow !== 'function') {
55+
console.warn('Shadow DOM is not supported in this browser, using v-html fallback');
56+
useFallback.value = true;
57+
return;
58+
}
59+
60+
renderShadowDom();
61+
});
62+
63+
// Clean up resources when component is unmounted
64+
onBeforeUnmount(() => {
65+
if (shadowRoot) {
66+
shadowRoot.innerHTML = '';
67+
}
68+
shadowRoot = null;
69+
});
70+
71+
// Update Shadow DOM when htmlContent changes
72+
watch(() => props.htmlContent, () => {
73+
renderShadowDom();
74+
}, { flush: 'post' });
75+
</script>

pages/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
"author": "",
1212
"license": "ISC",
1313
"devDependencies": {
14-
"wrangler": "^3.110.0"
14+
"wrangler": "^3.114.0"
1515
}
1616
}

smtp_proxy_server/imap_server.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ def fetchINBOX(self, messages):
135135
self.message_count = res.json()["count"]
136136
return [
137137
(start + uid, SimpleMessage(start + uid, parse_email(item["raw"])))
138-
for uid, item in enumerate(reversed(res.json()["results"]))
138+
for uid, item in enumerate(res.json()["results"])
139139
]
140140

141141
def fetchSENT(self, messages):

smtp_proxy_server/parse_email.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,14 @@ def generate_email_model(item: dict) -> EmailModel:
4848
email_json = json.loads(item["raw"])
4949
message = MIMEMultipart()
5050
if email_json.get("version") == "v2":
51-
message['From'] = f"{email_json["from_name"]} <{item["address"]}>" if email_json.get(
52-
"from_name") else item["address"]
53-
message['To'] = f"{email_json["to_name"]} <{email_json["to_mail"]}>" if email_json.get(
54-
"to_name") else email_json["to_mail"]
51+
message['From'] = f'{email_json["from_name"]} <{item["address"]}>' if email_json.get("from_name") else item["address"]
52+
message['To'] = f'{email_json["to_name"]} <{email_json["to_mail"]}>' if email_json.get("to_name") else email_json["to_mail"]
5553
message.attach(MIMEText(
5654
email_json["content"],
5755
"html" if email_json.get("is_html") else "plain"
5856
))
5957
else:
60-
message['From'] = f"{email_json["from"]['name']} <{
61-
email_json["from"]['email']}>"
58+
message['From'] = f'{email_json["from"]["name"]} <{email_json["from"]["email"]}>'
6259
message['To'] = ", ".join(
6360
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
6461
message.attach(MIMEText(

vitepress-docs/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44
"version": "0.9.1",
55
"type": "module",
66
"devDependencies": {
7-
"@types/node": "^22.13.5",
7+
"@types/node": "^22.13.9",
88
"vitepress": "^1.6.3",
9-
"wrangler": "^3.110.0"
9+
"wrangler": "^3.114.0"
1010
},
1111
"scripts": {
1212
"dev": "vitepress dev docs",

0 commit comments

Comments
 (0)