diff --git a/CHANGELOG b/CHANGELOG index 53c0db1e..9e7d90db 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,20 @@ # CHANGE LOG -## 2024-01-13 +## 2024-04-10 v0.0.1 + +Breaking changes: + +- remove `ENABLE_ATTACHMENT` config +- use rust wasm to parse email in frontend +- deprecated api moved to `/api/v1` DB changes -- `db/2024-01-13-patch.sql` +- `db/2024-04-09-patch.sql` + +## 2024-04-09 v0.0.0 + +release v0.0.0 ## 2024-04-03 @@ -16,3 +26,9 @@ Changes: - add delete account - add admin panel search + +## 2024-01-13 + +DB changes + +- `db/2024-01-13-patch.sql` diff --git a/README.md b/README.md index fe5c9ed9..e2f65286 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ - [x] 增加访问授权,可作为私人站点 - [x] 增加自动回复功能 - [x] 增加查看附件功能 -- [ ] 免费版附件过大会造成 Exceeded CPU Limit 错误 +- [x] 使用 rust wasm 解析邮件 --- @@ -137,8 +137,6 @@ PREFIX = "tmp" # 要处理的邮箱名称前缀 DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名 JWT_SECRET = "xxx" # 用于生成 jwt 的密钥 BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔 -# 免费版附件过大会造成 Exceeded CPU Limit 错误,如果不需要附件功能,可以关闭 -ENABLE_ATTACHMENT = true [[d1_databases]] binding = "DB" diff --git a/README_EN.md b/README_EN.md index 99f9d63e..dd3fee1b 100644 --- a/README_EN.md +++ b/README_EN.md @@ -20,7 +20,7 @@ This is a temporary email service that uses Cloudflare Workers to create a tempo - [x] Add access authorization, which can be used as a private site - [x] Add auto reply feature - [x] Add attachment viewing function -- [ ] Exceeded CPU Limit error caused by the free version of the attachment +- [x] use rust wasm to parse email ![demo](readme_assets/demo.png) @@ -56,8 +56,6 @@ pnpm install # DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] you domain name # JWT_SECRET = "xxx" # BLACK_LIST = "" -# free version attachment too large will cause Exceeded CPU Limit error, if you don't need attachment function, you can close -# ENABLE_ATTACHMENT = true cp wrangler.toml.template wrangler.toml # deploy pnpm run deploy diff --git a/db/2024-04-09-patch.sql b/db/2024-04-09-patch.sql new file mode 100644 index 00000000..27999d3d --- /dev/null +++ b/db/2024-04-09-patch.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS raw_mails ( + id INTEGER PRIMARY KEY, + message_id TEXT, + source TEXT, + address TEXT, + raw TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/db/schema.sql b/db/schema.sql index 5b70df5f..2d70d177 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -8,6 +8,15 @@ CREATE TABLE IF NOT EXISTS mails ( created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE IF NOT EXISTS raw_mails ( + id INTEGER PRIMARY KEY, + message_id TEXT, + source TEXT, + address TEXT, + raw TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + CREATE TABLE IF NOT EXISTS address ( id INTEGER PRIMARY KEY, name TEXT UNIQUE, diff --git a/frontend/package.json b/frontend/package.json index ca6ec184..c44574b8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,7 +13,9 @@ "@vicons/material": "^0.12.0", "@vueuse/core": "^10.9.0", "axios": "^1.6.8", + "mail-parser-wasm": "^0.1.6", "naive-ui": "^2.38.1", + "postal-mime": "^2.2.1", "vooks": "^0.2.12", "vue": "^3.4.21", "vue-clipboard3": "^2.0.0", @@ -27,6 +29,8 @@ "unplugin-vue-components": "^0.26.0", "vite": "^5.2.6", "vite-plugin-pwa": "^0.19.7", + "vite-plugin-top-level-await": "^1.4.1", + "vite-plugin-wasm": "^3.3.0", "workbox-window": "^7.0.0" } } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 2a526ee1..f965e458 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -14,9 +14,15 @@ dependencies: axios: specifier: ^1.6.8 version: 1.6.8 + mail-parser-wasm: + specifier: ^0.1.6 + version: 0.1.6 naive-ui: specifier: ^2.38.1 version: 2.38.1(vue@3.4.21) + postal-mime: + specifier: ^2.2.1 + version: 2.2.1 vooks: specifier: ^0.2.12 version: 0.2.12(vue@3.4.21) @@ -52,6 +58,12 @@ devDependencies: vite-plugin-pwa: specifier: ^0.19.7 version: 0.19.7(vite@5.2.6)(workbox-build@7.0.0)(workbox-window@7.0.0) + vite-plugin-top-level-await: + specifier: ^1.4.1 + version: 1.4.1(rollup@2.79.1)(vite@5.2.6) + vite-plugin-wasm: + specifier: ^3.3.0 + version: 3.3.0(vite@5.2.6) workbox-window: specifier: ^7.0.0 version: 7.0.0 @@ -1593,6 +1605,18 @@ packages: rollup: 2.79.1 dev: true + /@rollup/plugin-virtual@3.0.2(rollup@2.79.1): + resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + rollup: 2.79.1 + dev: true + /@rollup/pluginutils@3.1.0(rollup@2.79.1): resolution: {integrity: sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==} engines: {node: '>= 8.0.0'} @@ -1741,6 +1765,131 @@ packages: string.prototype.matchall: 4.0.11 dev: true + /@swc/core-darwin-arm64@1.4.12: + resolution: {integrity: sha512-BZUUq91LGJsLI2BQrhYL3yARkcdN4TS3YGNS6aRYUtyeWrGCTKHL90erF2BMU2rEwZLLkOC/U899R4o4oiSHfA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-darwin-x64@1.4.12: + resolution: {integrity: sha512-Wkk8rq1RwCOgg5ybTlfVtOYXLZATZ+QjgiBNM7pIn03A5/zZicokNTYd8L26/mifly2e74Dz34tlIZBT4aTGDA==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm-gnueabihf@1.4.12: + resolution: {integrity: sha512-8jb/SN67oTQ5KSThWlKLchhU6xnlAlnmnLCCOKK1xGtFS6vD+By9uL+qeEY2krV98UCRTf68WSmC0SLZhVoz5A==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-gnu@1.4.12: + resolution: {integrity: sha512-DhW47DQEZKCdSq92v5F03rqdpjRXdDMqxfu4uAlZ9Uo1wJEGvY23e1SNmhji2sVHsZbBjSvoXoBLk0v00nSG8w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-arm64-musl@1.4.12: + resolution: {integrity: sha512-PR57pT3TssnCRvdsaKNsxZy9N8rFg9AKA1U7W+LxbZ/7Z7PHc5PjxF0GgZpE/aLmU6xOn5VyQTlzjoamVkt05g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-gnu@1.4.12: + resolution: {integrity: sha512-HLZIWNHWuFIlH+LEmXr1lBiwGQeCshKOGcqbJyz7xpqTh7m2IPAxPWEhr/qmMTMsjluGxeIsLrcsgreTyXtgNA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-linux-x64-musl@1.4.12: + resolution: {integrity: sha512-M5fBAtoOcpz2YQAFtNemrPod5BqmzAJc8pYtT3dVTn1MJllhmLHlphU8BQytvoGr1PHgJL8ZJBlBGdt70LQ7Mw==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-arm64-msvc@1.4.12: + resolution: {integrity: sha512-K8LjjgZ7VQFtM+eXqjfAJ0z+TKVDng3r59QYn7CL6cyxZI2brLU3lNknZcUFSouZD+gsghZI/Zb8tQjVk7aKDQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-ia32-msvc@1.4.12: + resolution: {integrity: sha512-hflO5LCxozngoOmiQbDPyvt6ODc5Cu9AwTJP9uH/BSMPdEQ6PCnefuUOJLAKew2q9o+NmDORuJk+vgqQz9Uzpg==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core-win32-x64-msvc@1.4.12: + resolution: {integrity: sha512-3A4qMtddBDbtprV5edTB/SgJn9L+X5TL7RGgS3eWtEgn/NG8gA80X/scjf1v2MMeOsrcxiYhnemI2gXCKuQN2g==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@swc/core@1.4.12: + resolution: {integrity: sha512-QljRxTaUajSLB9ui93cZ38/lmThwIw/BPxjn+TphrYN6LPU3vu9/ykjgHtlpmaXDDcngL4K5i396E7iwwEUxYg==} + engines: {node: '>=10'} + requiresBuild: true + peerDependencies: + '@swc/helpers': ^0.5.0 + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.6 + optionalDependencies: + '@swc/core-darwin-arm64': 1.4.12 + '@swc/core-darwin-x64': 1.4.12 + '@swc/core-linux-arm-gnueabihf': 1.4.12 + '@swc/core-linux-arm64-gnu': 1.4.12 + '@swc/core-linux-arm64-musl': 1.4.12 + '@swc/core-linux-x64-gnu': 1.4.12 + '@swc/core-linux-x64-musl': 1.4.12 + '@swc/core-win32-arm64-msvc': 1.4.12 + '@swc/core-win32-ia32-msvc': 1.4.12 + '@swc/core-win32-x64-msvc': 1.4.12 + dev: true + + /@swc/counter@0.1.3: + resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + dev: true + + /@swc/types@0.1.6: + resolution: {integrity: sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==} + dependencies: + '@swc/counter': 0.1.3 + dev: true + /@types/estree@0.0.39: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true @@ -2969,6 +3118,10 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + /mail-parser-wasm@0.1.6: + resolution: {integrity: sha512-RoPPXqpGcCe4BcnXmxH4Cl5u0AH8y0JUNutksg2xzK0qFGEVE3xipx90JHzUUZ3MuMxo7doQTRktcABTIb3aeg==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -3131,6 +3284,10 @@ packages: engines: {node: '>= 0.4'} dev: true + /postal-mime@2.2.1: + resolution: {integrity: sha512-YqGeFmiKXUxv32hOy2t47VX67mYydC47CTCc7+HKd3xlNKPDhivnO/ZovN3iWXxvyyL2TRTxusuuq3etWeCKsw==} + dev: false + /postcss@8.4.38: resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} engines: {node: ^10 || ^12 || >=14} @@ -3742,6 +3899,11 @@ packages: punycode: 2.3.1 dev: true + /uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + dev: true + /vdirs@0.1.8(vue@3.4.21): resolution: {integrity: sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==} peerDependencies: @@ -3773,6 +3935,28 @@ packages: - supports-color dev: true + /vite-plugin-top-level-await@1.4.1(rollup@2.79.1)(vite@5.2.6): + resolution: {integrity: sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==} + peerDependencies: + vite: '>=2.8' + dependencies: + '@rollup/plugin-virtual': 3.0.2(rollup@2.79.1) + '@swc/core': 1.4.12 + uuid: 9.0.1 + vite: 5.2.6 + transitivePeerDependencies: + - '@swc/helpers' + - rollup + dev: true + + /vite-plugin-wasm@3.3.0(vite@5.2.6): + resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 + dependencies: + vite: 5.2.6 + dev: true + /vite@5.2.6: resolution: {integrity: sha512-FPtnxFlSIKYjZ2eosBQamz4CbyrTizbZ3hnGJlh/wMtCrlp1Hah6AzBLjGI5I2urTfNnpovpHdrL6YRuBOPnCA==} engines: {node: ^18.0.0 || >=20.0.0} diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 02fe0da8..bb545997 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -75,7 +75,8 @@ const getSettings = async () => { const res = await apiFetch("/api/settings");; settings.value = { address: res["address"], - auto_reply: res["auto_reply"] + auto_reply: res["auto_reply"], + has_v1_mails: res["has_v1_mails"], }; } finally { settings.value.fetched = true; diff --git a/frontend/src/store/index.js b/frontend/src/store/index.js index a12253c3..072ff7c5 100644 --- a/frontend/src/store/index.js +++ b/frontend/src/store/index.js @@ -14,6 +14,7 @@ export const useGlobalState = createGlobalState( }) const settings = ref({ fetched: false, + has_v1_mails: false, address: '', auto_reply: { subject: '', diff --git a/frontend/src/utils/email-parser.js b/frontend/src/utils/email-parser.js new file mode 100644 index 00000000..ec7426ab --- /dev/null +++ b/frontend/src/utils/email-parser.js @@ -0,0 +1,71 @@ +import PostalMime from 'postal-mime'; +import { parse_message } from 'mail-parser-wasm' + +export async function processItem(item) { + // Try to parse the email using mail-parser-wasm + try { + const parsedEmail = parse_message(item.raw); + item.source = parsedEmail.sender || item.source; + item.subject = parsedEmail.subject || ''; + item.message = parsedEmail.body_html || parsedEmail.text || ''; + item.attachments = parsedEmail.attachments?.map((a_item) => { + const blob_url = URL.createObjectURL( + new Blob( + [a_item.content], + { type: a_item.content_type || 'application/octet-stream' } + )) + if (a_item.content_id && a_item.content_id.length > 0) { + item.message = item.message.replace(`cid:${a_item.content_id}`, blob_url); + } + return { + id: a_item.content_id || Math.random().toString(36).substring(2, 15), + filename: a_item.filename || a_item.content_id || "", + size: a_item.content?.length || 0, + url: blob_url + } + }) || []; + } catch (error) { + console.log('Error parsing email with mail-parser-wasm'); + console.error(error); + } + if (item.subject && item.subject.length > 0 && item.message && item.message.length > 0) { + return item; + } + // Fallback to PostalMime + try { + const parsedEmail = await PostalMime.parse(item.raw); + item.source = parsedEmail.from.address || item.source; + if (parsedEmail.from.address && parsedEmail.from.name) { + item.source = `${parsedEmail.from.name} <${parsedEmail.from.address}>`; + } + item.subject = parsedEmail.subject || 'No Subject'; + item.message = parsedEmail.html || parsedEmail.text || item.raw; + item.attachments = parsedEmail.attachments?.map((a_item) => { + const blob_url = URL.createObjectURL( + new Blob( + [a_item.content], + { type: a_item.mimeType || 'application/octet-stream' } + )) + if (a_item.contentId && a_item.contentId.length > 0) { + item.message = item.message.replace(`cid:${a_item.contentId}`, blob_url); + } + return { + id: a_item.contentId || Math.random().toString(36).substring(2, 15), + filename: a_item.filename || a_item.contentId || "", + size: a_item.content?.length || 0, + url: blob_url + } + }) || []; + } catch (error) { + console.log('Error parsing email with PostalMime'); + console.error(error); + item.subject = 'No Subject'; + item.message = item.raw; + } +} + +export function getDownloadEmlUrl(raw) { + return URL.createObjectURL( + new Blob([raw], { type: 'text/plain' } + )) +} diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue index 16957c90..a8e78845 100644 --- a/frontend/src/views/Admin.vue +++ b/frontend/src/views/Admin.vue @@ -6,6 +6,7 @@ import { User, UserCheck, MailBulk } from '@vicons/fa' import { useGlobalState } from '../store' import { api } from '../api' +import { processItem, getDownloadEmlUrl } from '../utils/email-parser' const { localeCache, adminAuth, showAdminAuth } = useGlobalState() const router = useRouter() @@ -222,7 +223,9 @@ const fetchMailData = async () => { + `&limit=${mailPageSize.value}` + `&offset=${(mailPage.value - 1) * mailPageSize.value}` ); - mailData.value = results; + mailData.value = await Promise.all(results.map(async (item) => { + return await processItem(item); + })); if (count > 0) { mailCount.value = count; } @@ -249,7 +252,9 @@ const fetchMailUnknowData = async () => { + `?limit=${mailPageSize.value}` + `&offset=${(mailPage.value - 1) * mailPageSize.value}` ); - mailUnknowData.value = results; + mailUnknowData.value = await Promise.all(results.map(async (item) => { + return await processItem(item); + })); if (count > 0) { mailUnknowCount.value = count; } @@ -268,9 +273,7 @@ const fetchMailUnknowData = async () => {
{{ t('auth') }}

{{ t('authTip') }}

- +