mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-07-10 23:13:12 +08:00
feat: |Doc| use shadow DOM render mail html (#604)
This commit is contained in:
3
.flake8
Normal file
3
.flake8
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length = 180
|
||||||
|
exclude = .git,__pycache__,build,dist
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
## main(v0.9.1)
|
## main(v0.9.1)
|
||||||
|
|
||||||
- feat: |UI| support google ads
|
- feat: |UI| support google ads
|
||||||
|
- feat: |UI| 使用 shadow DOM 防止样式污染
|
||||||
|
|
||||||
## v0.9.0
|
## v0.9.0
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@simplewebauthn/browser": "10.0.0",
|
"@simplewebauthn/browser": "10.0.0",
|
||||||
"@unhead/vue": "^1.11.20",
|
"@unhead/vue": "^1.11.20",
|
||||||
"@vueuse/core": "^12.7.0",
|
"@vueuse/core": "^12.8.2",
|
||||||
"@wangeditor/editor": "^5.1.23",
|
"@wangeditor/editor": "^5.1.23",
|
||||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||||
"axios": "^1.8.1",
|
"axios": "^1.8.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"mail-parser-wasm": "^0.2.1",
|
"mail-parser-wasm": "^0.2.1",
|
||||||
"naive-ui": "^2.41.0",
|
"naive-ui": "^2.41.0",
|
||||||
@@ -32,21 +32,21 @@
|
|||||||
"vooks": "^0.2.12",
|
"vooks": "^0.2.12",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-clipboard3": "^2.0.0",
|
"vue-clipboard3": "^2.0.0",
|
||||||
"vue-i18n": "^11.1.1",
|
"vue-i18n": "^11.1.2",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vicons/fa": "^0.13.0",
|
"@vicons/fa": "^0.13.0",
|
||||||
"@vicons/material": "^0.13.0",
|
"@vicons/material": "^0.13.0",
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"unplugin-auto-import": "^19.1.0",
|
"unplugin-auto-import": "^19.1.1",
|
||||||
"unplugin-vue-components": "^28.4.0",
|
"unplugin-vue-components": "^28.4.1",
|
||||||
"vite": "^6.2.0",
|
"vite": "^6.2.1",
|
||||||
"vite-plugin-pwa": "^0.21.1",
|
"vite-plugin-pwa": "^0.21.1",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-plugin-wasm": "^3.4.1",
|
"vite-plugin-wasm": "^3.4.1",
|
||||||
"workbox-build": "^7.3.0",
|
"workbox-build": "^7.3.0",
|
||||||
"workbox-window": "^7.3.0",
|
"workbox-window": "^7.3.0",
|
||||||
"wrangler": "^3.110.0"
|
"wrangler": "^3.114.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
747
frontend/pnpm-lock.yaml
generated
747
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material
|
|||||||
import { useIsMobile } from '../utils/composables'
|
import { useIsMobile } from '../utils/composables'
|
||||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||||
import { utcToLocalDate } from '../utils';
|
import { utcToLocalDate } from '../utils';
|
||||||
|
import ShadowHtmlComponent from "./ShadowHtmlComponent.vue";
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const isMobile = useIsMobile()
|
const isMobile = useIsMobile()
|
||||||
@@ -171,7 +172,7 @@ const refresh = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const backFirstPageAndRefresh = async () =>{
|
const backFirstPageAndRefresh = async () => {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
await refresh();
|
await refresh();
|
||||||
}
|
}
|
||||||
@@ -380,7 +381,7 @@ onBeforeUnmount(() => {
|
|||||||
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
<n-split class="left" direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize"
|
||||||
:on-update:size="onSpiltSizeChange">
|
:on-update:size="onSpiltSizeChange">
|
||||||
<template #1>
|
<template #1>
|
||||||
<div style="overflow: auto; height: 80vh;">
|
<div style="overflow: auto; min-height: 50vh; max-height: 100vh;">
|
||||||
<n-list hoverable clickable>
|
<n-list hoverable clickable>
|
||||||
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
|
||||||
:class="mailItemClass(row)">
|
:class="mailItemClass(row)">
|
||||||
@@ -396,10 +397,14 @@ onBeforeUnmount(() => {
|
|||||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<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>
|
||||||
<n-tag v-if="showEMailTo" type="info">
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
TO: {{ row.address }}
|
<n-ellipsis style="max-width: 240px;">
|
||||||
|
TO: {{ row.address }}
|
||||||
|
</n-ellipsis>
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</template>
|
</template>
|
||||||
</n-thing>
|
</n-thing>
|
||||||
@@ -460,7 +465,7 @@ onBeforeUnmount(() => {
|
|||||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||||
</iframe>
|
</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>
|
||||||
<n-card :bordered="false" embedded class="mail-item" v-else>
|
<n-card :bordered="false" embedded class="mail-item" v-else>
|
||||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||||
@@ -498,7 +503,7 @@ onBeforeUnmount(() => {
|
|||||||
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
{{ utcToLocalDate(row.created_at, useUTCDate) }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag type="info">
|
<n-tag type="info">
|
||||||
FROM: {{ row.source }}
|
{{ showEMailTo ? "FROM: " + row.source : row.source }}
|
||||||
</n-tag>
|
</n-tag>
|
||||||
<n-tag v-if="showEMailTo" type="info">
|
<n-tag v-if="showEMailTo" type="info">
|
||||||
TO: {{ row.address }}
|
TO: {{ row.address }}
|
||||||
@@ -560,7 +565,7 @@ onBeforeUnmount(() => {
|
|||||||
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
<iframe v-else-if="useIframeShowMail" :srcdoc="curMail.message"
|
||||||
style="margin-top: 10px;width: 100%; height: 100%;">
|
style="margin-top: 10px;width: 100%; height: 100%;">
|
||||||
</iframe>
|
</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-card>
|
||||||
</n-drawer-content>
|
</n-drawer-content>
|
||||||
</n-drawer>
|
</n-drawer>
|
||||||
|
|||||||
75
frontend/src/components/ShadowHtmlComponent.vue
Normal file
75
frontend/src/components/ShadowHtmlComponent.vue
Normal 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>
|
||||||
@@ -11,6 +11,6 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wrangler": "^3.110.0"
|
"wrangler": "^3.114.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class SimpleMailbox:
|
|||||||
self.message_count = res.json()["count"]
|
self.message_count = res.json()["count"]
|
||||||
return [
|
return [
|
||||||
(start + uid, SimpleMessage(start + uid, parse_email(item["raw"])))
|
(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):
|
def fetchSENT(self, messages):
|
||||||
|
|||||||
@@ -48,17 +48,14 @@ def generate_email_model(item: dict) -> EmailModel:
|
|||||||
email_json = json.loads(item["raw"])
|
email_json = json.loads(item["raw"])
|
||||||
message = MIMEMultipart()
|
message = MIMEMultipart()
|
||||||
if email_json.get("version") == "v2":
|
if email_json.get("version") == "v2":
|
||||||
message['From'] = f"{email_json["from_name"]} <{item["address"]}>" if email_json.get(
|
message['From'] = f'{email_json["from_name"]} <{item["address"]}>' if email_json.get("from_name") else item["address"]
|
||||||
"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['To'] = f"{email_json["to_name"]} <{email_json["to_mail"]}>" if email_json.get(
|
|
||||||
"to_name") else email_json["to_mail"]
|
|
||||||
message.attach(MIMEText(
|
message.attach(MIMEText(
|
||||||
email_json["content"],
|
email_json["content"],
|
||||||
"html" if email_json.get("is_html") else "plain"
|
"html" if email_json.get("is_html") else "plain"
|
||||||
))
|
))
|
||||||
else:
|
else:
|
||||||
message['From'] = f"{email_json["from"]['name']} <{
|
message['From'] = f'{email_json["from"]["name"]} <{email_json["from"]["email"]}>'
|
||||||
email_json["from"]['email']}>"
|
|
||||||
message['To'] = ", ".join(
|
message['To'] = ", ".join(
|
||||||
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
[f"{to['name']} <{to['email']}>" for to in email_json["personalizations"][0]["to"]])
|
||||||
message.attach(MIMEText(
|
message.attach(MIMEText(
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"version": "0.9.1",
|
"version": "0.9.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.9",
|
||||||
"vitepress": "^1.6.3",
|
"vitepress": "^1.6.3",
|
||||||
"wrangler": "^3.110.0"
|
"wrangler": "^3.114.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vitepress dev docs",
|
"dev": "vitepress dev docs",
|
||||||
|
|||||||
628
vitepress-docs/pnpm-lock.yaml
generated
628
vitepress-docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -11,19 +11,19 @@
|
|||||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@cloudflare/workers-types": "^4.20250224.0",
|
"@cloudflare/workers-types": "^4.20250303.0",
|
||||||
"@eslint/js": "9.18.0",
|
"@eslint/js": "9.18.0",
|
||||||
"@simplewebauthn/types": "10.0.0",
|
"@simplewebauthn/types": "10.0.0",
|
||||||
"eslint": "9.18.0",
|
"eslint": "9.18.0",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"typescript-eslint": "^8.25.0",
|
"typescript-eslint": "^8.26.0",
|
||||||
"wrangler": "^3.110.0"
|
"wrangler": "^3.114.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.750.0",
|
"@aws-sdk/client-s3": "^3.758.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.750.0",
|
"@aws-sdk/s3-request-presigner": "^3.758.0",
|
||||||
"@simplewebauthn/server": "10.0.1",
|
"@simplewebauthn/server": "10.0.1",
|
||||||
"hono": "^4.7.2",
|
"hono": "^4.7.4",
|
||||||
"mimetext": "^3.0.27",
|
"mimetext": "^3.0.27",
|
||||||
"postal-mime": "^2.4.3",
|
"postal-mime": "^2.4.3",
|
||||||
"resend": "^4.1.2",
|
"resend": "^4.1.2",
|
||||||
|
|||||||
541
worker/pnpm-lock.yaml
generated
541
worker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user