Compare commits

..

11 Commits
v1.9.0 ... main

Author SHA1 Message Date
Dream Hunter
565bb839db fix: hide mobile preview line setting (#1080) 2026-07-08 14:41:49 +08:00
YewFence
dbd1f8706d feat: 增加全宽列表视图功能 (#1079)
* feat(mailbox): add list view mode

- Add a toggleable list view for the mailbox, with a settings option and back button.
- deselect mail on second click in list view
- set current mail on row click in multi-action mode

* feat(mailbox): add configurable body preview line clamp

Allow users to set the number of preview lines (0–5) for mail body in the list view via a slider in Appearance settings. Includes i18n support for the new option and its "Off" state.

* chore: clarify some i18n message in settings

include the following changes:
- The original "Mailbox Split Size" to "Left list width in two-column mailbox view"
- The description of new feature "Full-width mailbox list view"
sync all languages with the updated message

* docs: update changelog with recent UI improvements

- Added mailbox full-width list view and body preview lines settings
- Extended left panel width ratio range to 0
- Included English changelog translations

* docs: fix CHANGELOG improvements types

* fix: enable mail list preview line clamp settings on mobile
2026-07-08 14:21:10 +08:00
Dream Hunter
3f1d800e90 fix: validate AI extracted link domains (#1075)
* fix: validate AI extracted link domains

* fix: validate extracted links against full email content

* refactor: simplify AI link domain guard

* refactor: keep AI domain fix prompt-only
2026-07-04 16:56:12 +08:00
Dream Hunter
70b30c2494 chore: upgrade Twisted to stable 26.4.0 (#1071)
chore: upgrade twisted to stable 26.4.0
2026-06-25 00:23:17 +08:00
Dream Hunter
1a1dd720c8 chore: upgrade smtp proxy and e2e dependencies (#1069) 2026-06-24 23:59:16 +08:00
Dream Hunter
2d501d82cf chore: upgrade dependencies (#1068) 2026-06-24 23:39:10 +08:00
Chánh Niệm
7c57592742 docs: add Resend DNS-only proxy warning to prevent #515-style verification failures (#1062)
docs: add Resend DNS-only proxy warning to send-mail config

Resend domain verification CNAME records must use DNS-only (gray
cloud) on Cloudflare. Proxied (orange cloud) records prevent
verification, and a single failed attempt can take hours before
retry. This is a recurring issue (#515) that the Resend setup
docs did not warn about.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
2026-06-19 15:49:03 +08:00
凉心
41105ed803 fix: add page header padding for mobile layout (#1056)
* fix: add page header padding for mobile layout

* fix: limit page header padding to mobile layout

* docs: update changelog for mobile header fix

---------

Co-authored-by: dreamhunter2333 <dreamhunter2333@gmail.com>
2026-06-13 11:39:51 +08:00
Dream Hunter
c924b71a5c fix: compact HTML before AI email extraction (#1057)
fix: compact html before ai extraction
2026-06-12 00:41:19 +08:00
Dream Hunter
4f0b44de2e chore: upgrade Vitest to v4 (#1052)
chore: upgrade vitest to v4
2026-06-03 23:29:56 +08:00
Dream Hunter
f6fcbe793c feat: upgrade version to v1.10.0 (#1051)
- Update version number to 1.10.0 in all package.json files

- Add v1.10.0 placeholder in CHANGELOG.md
2026-06-03 22:14:44 +08:00
27 changed files with 2318 additions and 2046 deletions

View File

@@ -28,6 +28,7 @@ Upgrade the version number of the cloudflare_temp_email project.
4. Update the `VERSION` constant in `worker/src/constants.ts`. 4. Update the `VERSION` constant in `worker/src/constants.ts`.
5. Insert a new version placeholder at the top of `CHANGELOG.md`. 5. Insert a new version placeholder at the top of `CHANGELOG.md`.
6. Insert a new version placeholder at the top of `CHANGELOG_EN.md`. 6. Insert a new version placeholder at the top of `CHANGELOG_EN.md`.
7. Rename the previous version heading in both changelogs from `## v{OLD_VERSION}(main)` to `## v{OLD_VERSION}` so only the new active development version keeps `(main)`.
## CHANGELOG format ## CHANGELOG format
@@ -44,7 +45,14 @@ In `CHANGELOG.md`, insert before the existing `## v{OLD_VERSION}(main)` line (i.
``` ```
`CHANGELOG_EN.md` uses the same format. After inserting the new placeholder, update the old heading:
```diff
-## v{OLD_VERSION}(main)
+## v{OLD_VERSION}
```
`CHANGELOG_EN.md` uses the same format and must receive the same old-heading update.
## Commit message format ## Commit message format
@@ -53,4 +61,5 @@ feat: upgrade version to v{VERSION}
- Update version number to {VERSION} in all package.json files - Update version number to {VERSION} in all package.json files
- Add v{VERSION} placeholder in CHANGELOG.md - Add v{VERSION} placeholder in CHANGELOG.md
- Move (main) marker from v{OLD_VERSION} to v{VERSION}
``` ```

View File

@@ -6,7 +6,24 @@
<a href="CHANGELOG_EN.md">English</a> <a href="CHANGELOG_EN.md">English</a>
</p> </p>
## v1.9.0(main) ## v1.10.0(main)
### Features
- feat: |Frontend| 邮箱新增「邮箱全宽列表视图」开关(在外观设置中控制),开启后默认全宽列表展示邮件标题与正文预览,点击单封邮件再展开为双栏,再次点击同一封邮件可回到列表视图;多选模式下点击邮件会同步切换勾选状态与右侧预览,并禁用同邮件点击收回列表,展开时双栏左侧列表宽度仍遵循「邮箱双栏视图左侧列表宽度占比」配置;默认关闭,保留原有双栏行为
- feat: |Frontend| 邮箱全宽列表视图新增「正文预览行数」配置(在外观设置中控制),可设置邮件正文预览的最大行数,默认 2 行0 表示关闭预览
### Bug Fixes
- fix: |AI 提取| 强化提示词,要求 AI 保持邮件原始链接域名避免小模型改写验证链接域名导致错误跳转issue #1072
- fix: |AI 提取| HTML-only 邮件在发送给 Workers AI 前会先压缩为可读文本,避免样式模板过长导致验证码位于 4000 字截断之后而无法识别
- fix: |Frontend| 移动端 Header 增加页头内边距,避免标题、菜单按钮与屏幕边缘过近
### Improvements
- feat: |Frontend| 「邮箱双栏视图左侧列表宽度占比」最小值由 0.25 放宽至 0左侧列表可完全折叠使正文近乎全屏刻度增加 0 点;收件箱与发件箱的双栏拆分同步生效,并优化外观设置文案以明确该比例控制左侧邮件列表宽度
## v1.9.0
### Features ### Features

View File

@@ -6,7 +6,24 @@
<a href="CHANGELOG_EN.md">English</a> <a href="CHANGELOG_EN.md">English</a>
</p> </p>
## v1.9.0(main) ## v1.10.0(main)
### Features
- feat: |Frontend| Add a "Full-width mailbox list view" toggle in Appearance settings. When enabled, the mailbox shows a full-width list of subjects and body previews by default; clicking a mail expands it into the two-pane split view, clicking the same mail again returns to the list view; in multi-select mode, clicking a mail updates both its checked state and the right-side preview while disabling same-mail collapse, and the split width still follows the "Left list width in two-column mailbox view" setting. Defaults to off, preserving the original two-pane behavior
- feat: |Frontend| Add "Body Preview Lines" in Appearance settings for the full-width mailbox list view, allowing runtime control over the body-preview clamp. It defaults to 2 lines, and 0 disables previews
### 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)
- fix: |AI Extract| Convert HTML-only mail bodies into compact readable text before sending them to Workers AI, preventing long templates from pushing verification codes past the 4000-character truncation window
- fix: |Frontend| Add mobile Header page padding so the title and menu button no longer sit too close to the screen edge
### Improvements
- feat: |Frontend| Lower the "Left list width in two-column mailbox view" minimum from 0.25 to 0 so the left list pane can fully collapse for a near-fullscreen content view, with a 0 mark added; applies to both the inbox and send-box two-pane splits, and clarifies the Appearance setting label so it is clear the value controls the left mail list width
## v1.9.0
### Features ### Features

76
e2e/package-lock.json generated
View File

@@ -6,8 +6,8 @@
"": { "": {
"name": "cloudflare-temp-email-e2e", "name": "cloudflare-temp-email-e2e",
"dependencies": { "dependencies": {
"imapflow": "^1.3.1", "imapflow": "^1.4.2",
"nodemailer": "^8.0.5" "nodemailer": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.58.2", "@playwright/test": "1.58.2",
@@ -69,13 +69,13 @@
} }
}, },
"node_modules/@zone-eu/mailsplit": { "node_modules/@zone-eu/mailsplit": {
"version": "5.4.8", "version": "5.4.12",
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz", "resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.12.tgz",
"integrity": "sha512-eEyACj4JZ7sjzRvy26QhLgKEMWwQbsw1+QZnlLX+/gihcNH07lVPOcnwf5U6UAL7gkc//J3jVd76o/WS+taUiA==", "integrity": "sha512-w7Gy+NvjZ0MiXm8F6zfjImAqcTONKDImgWVBjDKQVFUXWuz3VFM5levNArkL2M877ajql5+bkS2pDV56injlmg==",
"license": "(MIT OR EUPL-1.1+)", "license": "(MIT OR EUPL-1.1+)",
"dependencies": { "dependencies": {
"libbase64": "1.3.0", "libbase64": "1.3.0",
"libmime": "5.3.7", "libmime": "5.3.8",
"libqp": "2.1.1" "libqp": "2.1.1"
} }
}, },
@@ -129,38 +129,26 @@
} }
}, },
"node_modules/imapflow": { "node_modules/imapflow": {
"version": "1.3.1", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.3.1.tgz", "resolved": "https://registry.npmjs.org/imapflow/-/imapflow-1.4.2.tgz",
"integrity": "sha512-DKwpMDR1EWXpV5T7adqQAccN7n684AX3poEZ5F3YoPlm2MyGeKavpRgNr3qptdEQaK+x5SlZ9jigT+cMs4geBA==", "integrity": "sha512-73CGfb5+W0FkZ5CY4GfSdsoXyQ+17wdKkpMN2vwJHdLtOOFQWxv0ilG7KYY79XHBYg5njjqxXYB2FPw5Tl81zQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@zone-eu/mailsplit": "5.4.8", "@zone-eu/mailsplit": "5.4.12",
"encoding-japanese": "2.2.0", "encoding-japanese": "2.2.0",
"iconv-lite": "0.7.2", "iconv-lite": "0.7.2",
"libbase64": "1.3.0", "libbase64": "1.3.0",
"libmime": "5.3.8", "libmime": "5.3.8",
"libqp": "2.1.1", "libqp": "2.1.1",
"nodemailer": "8.0.5", "nodemailer": "9.0.1",
"pino": "10.3.1", "pino": "10.3.1",
"socks": "2.8.7" "socks": "2.8.9"
}
},
"node_modules/imapflow/node_modules/libmime": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz",
"integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==",
"license": "MIT",
"dependencies": {
"encoding-japanese": "2.2.0",
"iconv-lite": "0.7.2",
"libbase64": "1.3.0",
"libqp": "2.1.1"
} }
}, },
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "10.1.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 12" "node": ">= 12"
@@ -173,29 +161,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/libmime": { "node_modules/libmime": {
"version": "5.3.7", "version": "5.3.8",
"resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.7.tgz", "resolved": "https://registry.npmjs.org/libmime/-/libmime-5.3.8.tgz",
"integrity": "sha512-FlDb3Wtha8P01kTL3P9M+ZDNDWPKPmKHWaU/cG/lg5pfuAwdflVpZE+wm9m7pKmC5ww6s+zTxBKS1p6yl3KpSw==", "integrity": "sha512-ZrCY+Q66mPvasAfjsQ/IgahzoBvfE1VdtGRpo1hwRB1oK3wJKxhKA3GOcd2a6j7AH5eMFccxK9fBoCpRZTf8ng==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"encoding-japanese": "2.2.0", "encoding-japanese": "2.2.0",
"iconv-lite": "0.6.3", "iconv-lite": "0.7.2",
"libbase64": "1.3.0", "libbase64": "1.3.0",
"libqp": "2.1.1" "libqp": "2.1.1"
} }
}, },
"node_modules/libmime/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/libqp": { "node_modules/libqp": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz", "resolved": "https://registry.npmjs.org/libqp/-/libqp-2.1.1.tgz",
@@ -203,9 +179,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nodemailer": { "node_modules/nodemailer": {
"version": "8.0.5", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz",
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==",
"license": "MIT-0", "license": "MIT-0",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -346,12 +322,12 @@
} }
}, },
"node_modules/socks": { "node_modules/socks": {
"version": "2.8.7", "version": "2.8.9",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz",
"integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ip-address": "^10.0.1", "ip-address": "^10.1.1",
"smart-buffer": "^4.2.0" "smart-buffer": "^4.2.0"
}, },
"engines": { "engines": {

View File

@@ -13,7 +13,7 @@
"ws": "^8.18.0" "ws": "^8.18.0"
}, },
"dependencies": { "dependencies": {
"imapflow": "^1.3.1", "imapflow": "^1.4.2",
"nodemailer": "^8.0.5" "nodemailer": "^9.0.1"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "cloudflare_temp_email", "name": "cloudflare_temp_email",
"version": "1.9.0", "version": "1.10.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -28,16 +28,16 @@
"@vueuse/core": "^14.3.0", "@vueuse/core": "^14.3.0",
"@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.16.1", "axios": "^1.18.1",
"dompurify": "^3.4.7", "dompurify": "^3.4.11",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"mail-parser-wasm": "^0.2.2", "mail-parser-wasm": "^0.2.2",
"naive-ui": "^2.44.1", "naive-ui": "^2.44.1",
"postal-mime": "^2.7.4", "postal-mime": "^2.7.4",
"vooks": "^0.2.12", "vooks": "^0.2.12",
"vue": "^3.5.35", "vue": "^3.5.38",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-i18n": "^11.4.4", "vue-i18n": "^11.4.6",
"vue-router": "^4.6.4" "vue-router": "^4.6.4"
}, },
"devDependencies": { "devDependencies": {
@@ -50,10 +50,10 @@
"vite": "^7.3.5", "vite": "^7.3.5",
"vite-plugin-pwa": "^1.3.0", "vite-plugin-pwa": "^1.3.0",
"vite-plugin-wasm": "^3.6.0", "vite-plugin-wasm": "^3.6.0",
"vitest": "^3.2.6", "vitest": "^4.1.9",
"workbox-build": "^7.4.1", "workbox-build": "^7.4.1",
"workbox-window": "^7.4.1", "workbox-window": "^7.4.1",
"wrangler": "^4.96.0" "wrangler": "^4.104.0"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }

1500
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -60,7 +60,7 @@ const props = defineProps({
const localFilterKeyword = ref('') const localFilterKeyword = ref('')
const { const {
isDark, mailboxSplitSize, indexTab, loading, useUTCDate, isDark, mailboxSplitSize, mailListView, mailListPreviewLineClamp, indexTab, loading, useUTCDate,
autoRefresh, configAutoRefreshInterval, sendMailModel autoRefresh, configAutoRefreshInterval, sendMailModel
} = useGlobalState() } = useGlobalState()
const autoRefreshInterval = ref(configAutoRefreshInterval.value) const autoRefreshInterval = ref(configAutoRefreshInterval.value)
@@ -71,6 +71,12 @@ const count = ref(0)
const page = ref(1) const page = ref(1)
const pageSize = ref(20) const pageSize = ref(20)
const mailListPreviewLineClampValue = computed(() => {
const value = Number(mailListPreviewLineClamp.value)
if (!Number.isFinite(value)) return 0
return Math.min(5, Math.max(0, Math.round(value)))
})
// Computed property for filtered data (only filter current page) // Computed property for filtered data (only filter current page)
const data = computed(() => { const data = computed(() => {
if (!localFilterKeyword.value || localFilterKeyword.value.trim() === '') { if (!localFilterKeyword.value || localFilterKeyword.value.trim() === '') {
@@ -183,7 +189,7 @@ const refresh = async () => {
count.value = totalCount; count.value = totalCount;
} }
curMail.value = null; curMail.value = null;
if (!isMobile.value && data.value.length > 0) { if (!isMobile.value && !mailListView.value && data.value.length > 0) {
curMail.value = data.value[0]; curMail.value = data.value[0];
} }
} catch (error) { } catch (error) {
@@ -202,6 +208,11 @@ const backFirstPageAndRefresh = async () => {
const clickRow = async (row) => { const clickRow = async (row) => {
if (multiActionMode.value) { if (multiActionMode.value) {
row.checked = !row.checked; row.checked = !row.checked;
curMail.value = row;
return;
}
if (mailListView.value && curMail.value?.id === row.id) {
curMail.value = null;
return; return;
} }
curMail.value = row; curMail.value = row;
@@ -375,8 +386,13 @@ onBeforeUnmount(() => {
clearable /> clearable />
</n-space> </n-space>
</div> </div>
<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" :resize-trigger-size="8"
:on-update:size="onSpiltSizeChange"> :default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange" v-if="!mailListView || curMail">
<template #resize-trigger>
<div class="split-handle">
<div class="split-handle__grip" />
</div>
</template>
<template #1> <template #1>
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;"> <div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
<n-list hoverable clickable> <n-list hoverable clickable>
@@ -412,15 +428,25 @@ onBeforeUnmount(() => {
</template> </template>
<template #2> <template #2>
<div v-if="curMail" style="margin: 8px;"> <div v-if="curMail" style="margin: 8px;">
<n-flex justify="space-between"> <n-flex justify="space-between" align="center">
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small"> <n-space :wrap="false" align="center">
<template #icon> <n-button v-if="mailListView" @click="curMail = null" text size="small">
<n-icon> <template #icon>
<ArrowBackIosNewFilled /> <n-icon>
</n-icon> <ArrowBackIosNewFilled />
</template> </n-icon>
{{ t('prevMail') }} </template>
</n-button> {{ t('backToList') }}
</n-button>
<n-button @click="prevMail" :disabled="!canGoPrevMail" text size="small">
<template #icon>
<n-icon>
<ArrowBackIosNewFilled />
</n-icon>
</template>
{{ t('prevMail') }}
</n-button>
</n-space>
<n-button @click="nextMail" :disabled="!canGoNextMail" text size="small" icon-placement="right"> <n-button @click="nextMail" :disabled="!canGoNextMail" text size="small" icon-placement="right">
<template #icon> <template #icon>
<n-icon> <n-icon>
@@ -446,6 +472,48 @@ onBeforeUnmount(() => {
</n-card> </n-card>
</template> </template>
</n-split> </n-split>
<div v-else class="mail-list-scroll">
<n-list hoverable clickable>
<n-list-item v-for="row in data" v-bind:key="row.id" @click="() => clickRow(row)"
:class="mailItemClass(row)">
<template #prefix v-if="multiActionMode">
<n-checkbox v-model:checked="row.checked" />
</template>
<n-thing class="mail-list-thing">
<template #header>
<n-ellipsis class="mail-list-title">
{{ row.subject }}
</n-ellipsis>
</template>
<template #description>
<div class="mail-list-meta">
<n-tag type="info">
ID: {{ row.id }}
</n-tag>
<n-tag type="info">
{{ utcToLocalDate(row.created_at, useUTCDate) }}
</n-tag>
<n-tag type="info">
<n-ellipsis class="mail-list-meta-text">
{{ showEMailTo ? "FROM: " + row.source : row.source }}
</n-ellipsis>
</n-tag>
<n-tag v-if="showEMailTo" type="info">
<n-ellipsis class="mail-list-meta-text">
TO: {{ row.address }}
</n-ellipsis>
</n-tag>
<AiExtractInfo :metadata="row.metadata" compact />
</div>
</template>
<n-ellipsis v-if="row.text && mailListPreviewLineClampValue > 0"
:line-clamp="mailListPreviewLineClampValue" class="mail-list-preview" :tooltip="false">
{{ row.text }}
</n-ellipsis>
</n-thing>
</n-list-item>
</n-list>
</div>
</div> </div>
<div class="left" v-else> <div class="left" v-else>
<n-space justify="space-around" align="center" :wrap="false" style="display: flex; align-items: center;"> <n-space justify="space-around" align="center" :wrap="false" style="display: flex; align-items: center;">
@@ -555,8 +623,80 @@ onBeforeUnmount(() => {
height: 100%; height: 100%;
} }
.mail-list-scroll {
overflow-y: auto;
overflow-x: hidden;
min-height: 60vh;
max-height: 100vh;
}
.mail-list-thing,
.mail-list-title,
.mail-list-preview {
min-width: 0;
max-width: 100%;
}
.mail-list-thing,
.mail-list-preview {
width: 100%;
}
.mail-list-thing :deep(.n-thing-main),
.mail-list-thing :deep(.n-thing-header),
.mail-list-thing :deep(.n-thing-header__title),
.mail-list-thing :deep(.n-thing-main__description),
.mail-list-thing :deep(.n-thing-main__content) {
min-width: 0;
}
.mail-list-meta {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-width: 0;
max-width: 100%;
}
.mail-list-meta :deep(.n-tag) {
max-width: 100%;
}
.mail-list-meta-text {
max-width: min(240px, 100%);
}
.mail-list-preview {
display: -webkit-box;
overflow-wrap: anywhere;
opacity: 0.7;
}
.mail-list-scroll :deep(.n-list-item__main) {
min-width: 0;
}
pre { pre {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
.split-handle {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.split-handle__grip {
width: 4px;
height: 32px;
border-radius: 2px;
background-color: var(--n-resize-trigger-color);
transition: background-color 0.2s;
}
.split-handle:hover .split-handle__grip {
background-color: var(--n-resize-trigger-color-hover);
}
</style> </style>

View File

@@ -210,8 +210,13 @@ onMounted(async () => {
</n-button> </n-button>
</n-space> </n-space>
</div> </div>
<n-split direction="horizontal" :max="0.75" :min="0.25" :default-size="mailboxSplitSize" <n-split direction="horizontal" :max="0.75" :min="0" :resize-trigger-size="8"
:on-update:size="onSpiltSizeChange"> :default-size="mailboxSplitSize" :on-update:size="onSpiltSizeChange">
<template #resize-trigger>
<div class="split-handle">
<div class="split-handle__grip" />
</div>
</template>
<template #1> <template #1>
<div style="overflow: auto; min-height: 60vh; max-height: 100vh;"> <div style="overflow: auto; min-height: 60vh; max-height: 100vh;">
<n-list hoverable clickable> <n-list hoverable clickable>
@@ -379,4 +384,23 @@ pre {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: break-word; word-wrap: break-word;
} }
.split-handle {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.split-handle__grip {
width: 4px;
height: 32px;
border-radius: 2px;
background-color: var(--n-resize-trigger-color);
transition: background-color 0.2s;
}
.split-handle:hover .split-handle__grip {
background-color: var(--n-resize-trigger-color-hover);
}
</style> </style>

View File

@@ -74,6 +74,7 @@ export const deMessages = {
"components.AiExtractInfo.authLink": "Authentifizierungslink", "components.AiExtractInfo.authLink": "Authentifizierungslink",
"views.admin.Maintenance.autoCleanup": "Automatische Bereinigung", "views.admin.Maintenance.autoCleanup": "Automatische Bereinigung",
"components.MailBox.autoRefresh": "Automatische Aktualisierung", "components.MailBox.autoRefresh": "Automatische Aktualisierung",
"components.MailBox.backToList": "Zurück zur Liste",
"views.common.Appearance.autoRefreshInterval": "Automatisches Aktualisierungsintervall (s)", "views.common.Appearance.autoRefreshInterval": "Automatisches Aktualisierungsintervall (s)",
"views.Index.auto_reply": "Automatische Antwort", "views.Index.auto_reply": "Automatische Antwort",
"views.index.AutoReply.autoReply": "Automatische Antwort", "views.index.AutoReply.autoReply": "Automatische Antwort",
@@ -299,7 +300,10 @@ export const deMessages = {
"views.index.SimpleIndex.deleteSuccess": "E-Mail erfolgreich gelöscht", "views.index.SimpleIndex.deleteSuccess": "E-Mail erfolgreich gelöscht",
"views.user.UserLogin.cannotForgotPassword": "E-Mail-Verifizierung oder Registrierung ist deaktiviert; das Passwort kann nicht zurückgesetzt werden. Bitte den Administrator kontaktieren.", "views.user.UserLogin.cannotForgotPassword": "E-Mail-Verifizierung oder Registrierung ist deaktiviert; das Passwort kann nicht zurückgesetzt werden. Bitte den Administrator kontaktieren.",
"views.Admin.mailWebhook": "Mail-Webhook", "views.Admin.mailWebhook": "Mail-Webhook",
"views.common.Appearance.mailboxSplitSize": "Größe der Mailbox-Aufteilung", "views.common.Appearance.mailboxSplitSize": "Breite der linken Liste in der zweispaltigen Postfachansicht",
"views.common.Appearance.mailListView": "Postfach-Listenansicht in voller Breite",
"views.common.Appearance.mailListPreviewLineClamp": "Zeilen der Textvorschau",
"views.common.Appearance.off": "Aus",
"views.index.SimpleIndex.refreshSuccess": "E-Mails erfolgreich aktualisiert", "views.index.SimpleIndex.refreshSuccess": "E-Mails erfolgreich aktualisiert",
"views.Admin.unknow": "E-Mails mit unbekanntem Empfänger", "views.Admin.unknow": "E-Mails mit unbekanntem Empfänger",
"views.Admin.maintenance": "Wartung", "views.Admin.maintenance": "Wartung",

View File

@@ -74,6 +74,7 @@ export const esMessages = {
"components.AiExtractInfo.authLink": "Enlace de autenticación", "components.AiExtractInfo.authLink": "Enlace de autenticación",
"views.admin.Maintenance.autoCleanup": "Limpieza automática", "views.admin.Maintenance.autoCleanup": "Limpieza automática",
"components.MailBox.autoRefresh": "Actualización automática", "components.MailBox.autoRefresh": "Actualización automática",
"components.MailBox.backToList": "Volver a la lista",
"views.common.Appearance.autoRefreshInterval": "Intervalo de actualización automática (s)", "views.common.Appearance.autoRefreshInterval": "Intervalo de actualización automática (s)",
"views.Index.auto_reply": "Respuesta automática", "views.Index.auto_reply": "Respuesta automática",
"views.index.AutoReply.autoReply": "Respuesta automática", "views.index.AutoReply.autoReply": "Respuesta automática",
@@ -299,7 +300,10 @@ export const esMessages = {
"views.index.SimpleIndex.deleteSuccess": "Correo eliminado correctamente", "views.index.SimpleIndex.deleteSuccess": "Correo eliminado correctamente",
"views.user.UserLogin.cannotForgotPassword": "La verificación por correo o el registro está desactivado; no se puede restablecer la contraseña. Contacta con el administrador.", "views.user.UserLogin.cannotForgotPassword": "La verificación por correo o el registro está desactivado; no se puede restablecer la contraseña. Contacta con el administrador.",
"views.Admin.mailWebhook": "Webhook de correo", "views.Admin.mailWebhook": "Webhook de correo",
"views.common.Appearance.mailboxSplitSize": "Tamaño de división del buzón", "views.common.Appearance.mailboxSplitSize": "Ancho de la lista izquierda en la vista de buzón de dos columnas",
"views.common.Appearance.mailListView": "Vista de lista del buzón de ancho completo",
"views.common.Appearance.mailListPreviewLineClamp": "Líneas de vista previa del cuerpo",
"views.common.Appearance.off": "Desactivado",
"views.index.SimpleIndex.refreshSuccess": "Correos actualizados correctamente", "views.index.SimpleIndex.refreshSuccess": "Correos actualizados correctamente",
"views.Admin.unknow": "Correos con destinatario desconocido", "views.Admin.unknow": "Correos con destinatario desconocido",
"views.Admin.maintenance": "Mantenimiento", "views.Admin.maintenance": "Mantenimiento",

View File

@@ -74,6 +74,7 @@ export const jaMessages = {
"components.AiExtractInfo.authLink": "認証リンク", "components.AiExtractInfo.authLink": "認証リンク",
"views.admin.Maintenance.autoCleanup": "自動クリーンアップ", "views.admin.Maintenance.autoCleanup": "自動クリーンアップ",
"components.MailBox.autoRefresh": "自動更新", "components.MailBox.autoRefresh": "自動更新",
"components.MailBox.backToList": "リストに戻る",
"views.common.Appearance.autoRefreshInterval": "自動更新間隔 (秒)", "views.common.Appearance.autoRefreshInterval": "自動更新間隔 (秒)",
"views.Index.auto_reply": "自動返信", "views.Index.auto_reply": "自動返信",
"views.index.AutoReply.autoReply": "自動返信", "views.index.AutoReply.autoReply": "自動返信",
@@ -299,7 +300,10 @@ export const jaMessages = {
"views.index.SimpleIndex.deleteSuccess": "メールを削除しました", "views.index.SimpleIndex.deleteSuccess": "メールを削除しました",
"views.user.UserLogin.cannotForgotPassword": "メール検証または登録が無効のため、パスワードを再設定できません。管理者へ連絡してください。", "views.user.UserLogin.cannotForgotPassword": "メール検証または登録が無効のため、パスワードを再設定できません。管理者へ連絡してください。",
"views.Admin.mailWebhook": "メールWebhook", "views.Admin.mailWebhook": "メールWebhook",
"views.common.Appearance.mailboxSplitSize": "メールボックス分割サイズ", "views.common.Appearance.mailboxSplitSize": "メールボックス2カラム表示の左側リスト幅",
"views.common.Appearance.mailListView": "メールボックス全幅リスト表示",
"views.common.Appearance.mailListPreviewLineClamp": "本文プレビュー行数",
"views.common.Appearance.off": "オフ",
"views.index.SimpleIndex.refreshSuccess": "メールを更新しました", "views.index.SimpleIndex.refreshSuccess": "メールを更新しました",
"views.Admin.unknow": "受信者不明のメール", "views.Admin.unknow": "受信者不明のメール",
"views.Admin.maintenance": "メンテナンス", "views.Admin.maintenance": "メンテナンス",

View File

@@ -74,6 +74,7 @@ export const ptBRMessages = {
"components.AiExtractInfo.authLink": "Link de autenticação", "components.AiExtractInfo.authLink": "Link de autenticação",
"views.admin.Maintenance.autoCleanup": "Limpeza automática", "views.admin.Maintenance.autoCleanup": "Limpeza automática",
"components.MailBox.autoRefresh": "Atualização automática", "components.MailBox.autoRefresh": "Atualização automática",
"components.MailBox.backToList": "Voltar para a lista",
"views.common.Appearance.autoRefreshInterval": "Intervalo de atualização automática (s)", "views.common.Appearance.autoRefreshInterval": "Intervalo de atualização automática (s)",
"views.Index.auto_reply": "Resposta automática", "views.Index.auto_reply": "Resposta automática",
"views.index.AutoReply.autoReply": "Resposta automática", "views.index.AutoReply.autoReply": "Resposta automática",
@@ -299,7 +300,10 @@ export const ptBRMessages = {
"views.index.SimpleIndex.deleteSuccess": "E-mail excluído com sucesso", "views.index.SimpleIndex.deleteSuccess": "E-mail excluído com sucesso",
"views.user.UserLogin.cannotForgotPassword": "A verificação por e-mail ou o registro está desativado; não é possível redefinir a senha. Entre em contato com o administrador.", "views.user.UserLogin.cannotForgotPassword": "A verificação por e-mail ou o registro está desativado; não é possível redefinir a senha. Entre em contato com o administrador.",
"views.Admin.mailWebhook": "Webhook de e-mail", "views.Admin.mailWebhook": "Webhook de e-mail",
"views.common.Appearance.mailboxSplitSize": "Tamanho da divisão da caixa de correio", "views.common.Appearance.mailboxSplitSize": "Largura da lista esquerda na visualização de duas colunas da caixa de correio",
"views.common.Appearance.mailListView": "Visualização de lista da caixa de correio em largura total",
"views.common.Appearance.mailListPreviewLineClamp": "Linhas da prévia do corpo",
"views.common.Appearance.off": "Desativado",
"views.index.SimpleIndex.refreshSuccess": "E-mails atualizados com sucesso", "views.index.SimpleIndex.refreshSuccess": "E-mails atualizados com sucesso",
"views.Admin.unknow": "E-mails com destinatário desconhecido", "views.Admin.unknow": "E-mails com destinatário desconhecido",
"views.Admin.maintenance": "Manutenção", "views.Admin.maintenance": "Manutenção",

View File

@@ -42,6 +42,10 @@ export const MESSAGE_REGISTRY = {
"en": "Auto Refresh", "en": "Auto Refresh",
"zh": "自动刷新" "zh": "自动刷新"
}, },
"backToList": {
"en": "Back to List",
"zh": "返回列表"
},
"cancelMultiAction": { "cancelMultiAction": {
"en": "Cancel Multi Action", "en": "Cancel Multi Action",
"zh": "取消多选" "zh": "取消多选"
@@ -2107,8 +2111,20 @@ export const MESSAGE_REGISTRY = {
"zh": "左侧" "zh": "左侧"
}, },
"mailboxSplitSize": { "mailboxSplitSize": {
"en": "Mailbox Split Size", "en": "Left list width in two-column mailbox view",
"zh": "邮箱界面分栏大小" "zh": "邮箱双栏视图左侧列表宽度占比"
},
"mailListView": {
"en": "Full-width mailbox list view",
"zh": "邮箱全宽列表视图"
},
"mailListPreviewLineClamp": {
"en": "Body Preview Lines",
"zh": "正文预览行数"
},
"off": {
"en": "Off",
"zh": "关闭"
}, },
"preferShowTextMail": { "preferShowTextMail": {
"en": "Display text Mail by default", "en": "Display text Mail by default",

View File

@@ -88,6 +88,8 @@ export const useGlobalState = createGlobalState(
const adminMailTabAddress = ref(""); const adminMailTabAddress = ref("");
const adminSendBoxTabAddress = ref(""); const adminSendBoxTabAddress = ref("");
const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25); const mailboxSplitSize = useStorage('mailboxSplitSize', 0.25);
const mailListView = useStorage('mailListView', false);
const mailListPreviewLineClamp = useStorage('mailListPreviewLineClamp', 2);
const useIframeShowMail = useStorage('useIframeShowMail', false); const useIframeShowMail = useStorage('useIframeShowMail', false);
const preferShowTextMail = useStorage('preferShowTextMail', false); const preferShowTextMail = useStorage('preferShowTextMail', false);
const userJwt = useStorage('userJwt', ''); const userJwt = useStorage('userJwt', '');
@@ -160,6 +162,8 @@ export const useGlobalState = createGlobalState(
adminMailTabAddress, adminMailTabAddress,
adminSendBoxTabAddress, adminSendBoxTabAddress,
mailboxSplitSize, mailboxSplitSize,
mailListView,
mailListPreviewLineClamp,
useIframeShowMail, useIframeShowMail,
preferShowTextMail, preferShowTextMail,
userJwt, userJwt,

View File

@@ -435,6 +435,10 @@ onMounted(async () => {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
:deep(.n-page-header) {
padding: 10px;
}
:deep(.n-page-header__title) { :deep(.n-page-header__title) {
min-width: 0; min-width: 0;
} }

View File

@@ -11,7 +11,7 @@ const props = defineProps({
}) })
const { const {
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval, mailboxSplitSize, mailListView, mailListPreviewLineClamp, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex globalTabplacement, useSideMargin, useUTCDate, useSimpleIndex
} = useGlobalState() } = useGlobalState()
const isMobile = useIsMobile() const isMobile = useIsMobile()
@@ -23,12 +23,26 @@ const { t } = useScopedI18n('views.common.Appearance')
<div class="center"> <div class="center">
<n-card :bordered="false" embedded> <n-card :bordered="false" embedded>
<n-form-item-row v-if="!isMobile" :label="t('mailboxSplitSize')"> <n-form-item-row v-if="!isMobile" :label="t('mailboxSplitSize')">
<n-slider v-model:value="mailboxSplitSize" :min="0.25" :max="0.75" :step="0.01" :marks="{ <n-slider v-model:value="mailboxSplitSize" :min="0" :max="0.75" :step="0.01" :marks="{
0: '0',
0.25: '0.25', 0.25: '0.25',
0.5: '0.5', 0.5: '0.5',
0.75: '0.75' 0.75: '0.75'
}" /> }" />
</n-form-item-row> </n-form-item-row>
<n-form-item-row v-if="!isMobile" :label="t('mailListView')">
<n-switch v-model:value="mailListView" :round="false" />
</n-form-item-row>
<n-form-item-row v-if="!isMobile" :label="t('mailListPreviewLineClamp')">
<n-slider v-model:value="mailListPreviewLineClamp" :min="0" :max="5" :step="1" :marks="{
0: t('off'),
1: '1',
2: '2',
3: '3',
4: '4',
5: '5'
}" />
</n-form-item-row>
<n-form-item-row :label="t('autoRefreshInterval')"> <n-form-item-row :label="t('autoRefreshInterval')">
<n-slider v-model:value="configAutoRefreshInterval" :min="30" :max="300" :step="1" :marks="{ <n-slider v-model:value="configAutoRefreshInterval" :min="30" :max="300" :step="1" :marks="{
60: '60', 120: '120', 180: '180', 240: '240' 60: '60', 120: '120', 180: '180', 240: '240'

View File

@@ -1,6 +1,6 @@
{ {
"name": "temp-email-pages", "name": "temp-email-pages",
"version": "1.9.0", "version": "1.10.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -11,7 +11,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"wrangler": "^4.96.0" "wrangler": "^4.104.0"
}, },
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
} }

View File

@@ -1,6 +1,6 @@
aiosmtpd==1.4.6 aiosmtpd==1.4.6
pydantic-settings==2.13.1 pydantic-settings==2.14.2
Twisted==25.5.0 Twisted==26.4.0
httpx==0.28.1 httpx==0.28.1
pyOpenSSL==26.0.0 pyOpenSSL==26.0.0
service-identity==24.2.0 service-identity==24.2.0

View File

@@ -48,6 +48,12 @@ No additional env var is required.
Register at `https://resend.com/domains` and add DNS records according to the instructions. Register at `https://resend.com/domains` and add DNS records according to the instructions.
> [!WARNING] DNS record proxy status on Cloudflare
> Resend domain verification CNAME records **must be set to DNS-only** (gray cloud) in
> the Cloudflare DNS dashboard. Proxied (orange cloud) records will prevent Resend from
> completing verification, and a single failed attempt can take several hours before
> Retry becomes available. See [#515](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/515).
Create an `api key` on the `API KEYS` page. Create an `api key` on the `API KEYS` page.
Then execute the following command to add `RESEND_TOKEN` to secrets: Then execute the following command to add `RESEND_TOKEN` to secrets:

View File

@@ -48,6 +48,12 @@ send_email = [
注册 `https://resend.com/domains` 根据提示添加 DNS 记录, 注册 `https://resend.com/domains` 根据提示添加 DNS 记录,
> [!WARNING] Cloudflare 上 DNS 记录的代理状态
> Resend 域名验证的 CNAME 记录**必须设置为 仅 DNS**(灰云),在
> Cloudflare DNS 控制面板中代理(橙云)记录会阻止 Resend
> 完成验证,且一次失败的尝试可能需要数小时才能重试。
> 参见 [#515](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/515)。
`API KEYS` 页面创建 `api key` `API KEYS` 页面创建 `api key`
然后执行下面的命令,将 `RESEND_TOKEN` 添加到 secrets 中 然后执行下面的命令,将 `RESEND_TOKEN` 添加到 secrets 中

View File

@@ -1,12 +1,12 @@
{ {
"name": "temp-mail-docs", "name": "temp-mail-docs",
"private": true, "private": true,
"version": "1.9.0", "version": "1.10.0",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@types/node": "^25.9.1", "@types/node": "^26.0.0",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"wrangler": "^4.96.0" "wrangler": "^4.104.0"
}, },
"scripts": { "scripts": {
"dev": "vitepress dev docs", "dev": "vitepress dev docs",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "cloudflare_temp_email", "name": "cloudflare_temp_email",
"version": "1.9.0", "version": "1.10.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -11,23 +11,23 @@
"build": "wrangler deploy --dry-run --outdir dist --minify" "build": "wrangler deploy --dry-run --outdir dist --minify"
}, },
"devDependencies": { "devDependencies": {
"@cloudflare/workers-types": "^4.20260602.1", "@cloudflare/workers-types": "^4.20260624.1",
"@eslint/js": "9.39.1", "@eslint/js": "9.39.1",
"@types/node": "^25.9.1", "@types/node": "^25.9.4",
"eslint": "9.39.1", "eslint": "9.39.1",
"globals": "^16.5.0", "globals": "^16.5.0",
"typescript-eslint": "^8.60.1", "typescript-eslint": "^8.62.0",
"wrangler": "^4.96.0" "wrangler": "^4.104.0"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.888.0", "@aws-sdk/client-s3": "3.888.0",
"@aws-sdk/s3-request-presigner": "3.888.0", "@aws-sdk/s3-request-presigner": "3.888.0",
"@simplewebauthn/server": "^13.3.1", "@simplewebauthn/server": "^13.3.2",
"hono": "^4.12.23", "hono": "^4.12.27",
"jsonpath-plus": "^10.4.0", "jsonpath-plus": "^10.4.0",
"mimetext": "^3.0.28", "mimetext": "^3.0.28",
"postal-mime": "^2.7.4", "postal-mime": "^2.7.4",
"resend": "^6.12.4", "resend": "^6.14.0",
"telegraf": "4.16.3", "telegraf": "4.16.3",
"worker-mailer": "^1.2.1" "worker-mailer": "^1.2.1"
}, },

1381
worker/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
export const CONSTANTS = { export const CONSTANTS = {
VERSION: 'v' + '1.9.0', VERSION: 'v' + '1.10.0',
// DB Version // DB Version
DB_VERSION_KEY: 'db_version', DB_VERSION_KEY: 'db_version',

View File

@@ -75,7 +75,8 @@ If the extracted content is in markdown link format [text](url):
2. **Single Selection**: Choose ONLY ONE type based on the highest priority match 2. **Single Selection**: Choose ONLY ONE type based on the highest priority match
3. **Real Data Only**: Never invent, guess, or fabricate content 3. **Real Data Only**: Never invent, guess, or fabricate content
4. **Complete URLs**: Links must be full, valid URLs as they appear in the email 4. **Complete URLs**: Links must be full, valid URLs as they appear in the email
5. **Clean Extraction**: Return only the raw extracted content, no extra text 5. **No Domain Modification**: Never modify, rewrite, or substitute URL domains. If the exact URL domain is uncertain, return none
6. **Clean Extraction**: Return only the raw extracted content, no extra text
# Output Format (JSON only) # Output Format (JSON only)
{ {
@@ -166,6 +167,65 @@ async function saveExtractMetadata(
} }
} }
function decodeHtmlEntities(text: string): string {
const entities: Record<string, string> = {
amp: '&',
lt: '<',
gt: '>',
quot: '"',
apos: "'",
nbsp: ' ',
};
const decodeCodePoint = (value: number, fallback: string) => {
if (!Number.isFinite(value) || value < 0 || value > 0x10ffff) {
return fallback;
}
return String.fromCodePoint(value);
};
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z][a-z0-9]+);/gi, (match, entity) => {
const normalized = entity.toLowerCase();
if (normalized.startsWith('#x')) {
const value = Number.parseInt(normalized.slice(2), 16);
return decodeCodePoint(value, match);
}
if (normalized.startsWith('#')) {
const value = Number.parseInt(normalized.slice(1), 10);
return decodeCodePoint(value, match);
}
return entities[normalized] ?? match;
});
}
function htmlToTextForAi(html: string): string {
return decodeHtmlEntities(
html
.replace(/<\s*(script|style|head|svg)[^>]*>[\s\S]*?<\s*\/\s*\1\s*>/gi, ' ')
.replace(/<!--[\s\S]*?-->/g, ' ')
.replace(/<a\b[^>]*\bhref=(["'])(.*?)\1[^>]*>([\s\S]*?)<\/a>/gi, ' $3 $2 ')
.replace(/<\s*br\s*\/?>/gi, '\n')
.replace(/<\/\s*(p|div|tr|td|th|li|table|section|article|header|footer|h[1-6])\s*>/gi, '\n')
.replace(/<[^>]+>/g, ' ')
)
.replace(/[ \t\r\f\v]+/g, ' ')
.replace(/\n\s+/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function getEmailContentForExtract(parsedEmail: Awaited<ReturnType<typeof commonParseMail>>): string {
if (parsedEmail?.text) {
return parsedEmail.text;
}
if (!parsedEmail?.html) {
return "";
}
return htmlToTextForAi(parsedEmail.html) || parsedEmail.html;
}
/** /**
* Main extraction function * Main extraction function
* Checks if extraction is enabled, processes the email content, and saves to database. * Checks if extraction is enabled, processes the email content, and saves to database.
@@ -219,7 +279,7 @@ export async function extractEmailInfo(
// Parse email to get content (shared by the AI path and the regex fallback) // Parse email to get content (shared by the AI path and the regex fallback)
const parsedEmail = await commonParseMail(parsedEmailContext); const parsedEmail = await commonParseMail(parsedEmailContext);
const emailContent = parsedEmail?.text || parsedEmail?.html || ""; const emailContent = getEmailContentForExtract(parsedEmail);
if (!emailContent) { if (!emailContent) {
return null; return null;