mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
feat: use rust mail-parser (#104)
* feat: imp worker v2 * feat: add rust mail-parser * feat: imp frontend v2 * feat: imp frontend v2 * feat: update doc * feat: add mailV1Alert * feat: remove unused
This commit is contained in:
20
CHANGELOG
20
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`
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
@@ -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
|
||||
|
||||
8
db/2024-04-09-patch.sql
Normal file
8
db/2024-04-09-patch.sql
Normal file
@@ -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
|
||||
);
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
184
frontend/pnpm-lock.yaml
generated
184
frontend/pnpm-lock.yaml
generated
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -14,6 +14,7 @@ export const useGlobalState = createGlobalState(
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
has_v1_mails: false,
|
||||
address: '',
|
||||
auto_reply: {
|
||||
subject: '',
|
||||
|
||||
71
frontend/src/utils/email-parser.js
Normal file
71
frontend/src/utils/email-parser.js
Normal file
@@ -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' }
|
||||
))
|
||||
}
|
||||
@@ -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 () => {
|
||||
<div>{{ t('auth') }}</div>
|
||||
</template>
|
||||
<p>{{ t('authTip') }}</p>
|
||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{
|
||||
minRows: 3
|
||||
}" />
|
||||
<n-input v-model:value="adminAuth" type="textarea" :autosize="{ minRows: 3 }" />
|
||||
<template #action>
|
||||
<n-button @click="authFunc" size="small" tertiary round type="primary">
|
||||
{{ t('auth') }}
|
||||
|
||||
@@ -29,8 +29,8 @@ const emailDomain = ref("")
|
||||
|
||||
const login = async () => {
|
||||
try {
|
||||
await api.getSettings()
|
||||
jwt.value = password.value;
|
||||
await api.getSettings()
|
||||
location.reload()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -85,6 +85,7 @@ const { t } = useI18n({
|
||||
copied: 'Copied',
|
||||
showPassword: 'Show Password',
|
||||
fetchAddressError: 'Fetch address error, maybe your jwt is invalid or network error.',
|
||||
mailV1Alert: 'You have some mails in v1, please click here to login and visit your history mails.',
|
||||
},
|
||||
zh: {
|
||||
title: 'Cloudflare 临时邮件',
|
||||
@@ -114,6 +115,7 @@ const { t } = useI18n({
|
||||
copied: '已复制',
|
||||
showPassword: '查看密码',
|
||||
fetchAddressError: '获取地址失败, 请检查你的 jwt 是否有效 或 网络是否正常。',
|
||||
mailV1Alert: '你有一些 v1 版本的邮件,请点击此处登录查看。',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -351,14 +353,24 @@ onMounted(async () => {
|
||||
<n-card v-if="!settings.fetched">
|
||||
<n-skeleton style="height: 50vh" />
|
||||
</n-card>
|
||||
<n-alert v-else-if="settings.address" type="info" show-icon>
|
||||
<span>
|
||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
<div v-else-if="settings.address">
|
||||
<n-alert v-if="settings.has_v1_mails" type="warning" show-icon closable>
|
||||
<span>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small"
|
||||
href="https://temp-email-v1.dreamhunter2333.xyz/">
|
||||
<b>{{ t('mailV1Alert') }} </b>
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
<n-alert type="info" show-icon>
|
||||
<span>
|
||||
<b>{{ t('yourAddress') }} <b>{{ settings.address }}</b></b>
|
||||
<n-button style="margin-left: 10px" @click="copy" size="small" tertiary round type="primary">
|
||||
<n-icon :component="Copy" /> {{ t('copy') }}
|
||||
</n-button>
|
||||
</span>
|
||||
</n-alert>
|
||||
</div>
|
||||
<n-card v-else>
|
||||
<n-result status="info" :description="t('pleaseGetNewEmail')">
|
||||
<template #footer>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
import { CloudDownloadRound } from '@vicons/material'
|
||||
import { useIsMobile } from '../utils/composables'
|
||||
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
|
||||
|
||||
const message = useMessage()
|
||||
const isMobile = useIsMobile()
|
||||
@@ -30,11 +31,13 @@ const { t } = useI18n({
|
||||
autoRefresh: 'Auto Refresh',
|
||||
refresh: 'Refresh',
|
||||
attachments: 'Show Attachments',
|
||||
downloadMail: 'Download Mail',
|
||||
pleaseSelectMail: "Please select a mail to view."
|
||||
},
|
||||
zh: {
|
||||
autoRefresh: '自动刷新',
|
||||
refresh: '刷新',
|
||||
downloadMail: '下载邮件',
|
||||
attachments: '查看附件',
|
||||
pleaseSelectMail: "请选择一封邮件查看。"
|
||||
}
|
||||
@@ -72,12 +75,14 @@ const refresh = async () => {
|
||||
+ `?limit=${pageSize.value}`
|
||||
+ `&offset=${(page.value - 1) * pageSize.value}`
|
||||
);
|
||||
data.value = results;
|
||||
data.value = await Promise.all(results.map(async (item) => {
|
||||
return await processItem(item);
|
||||
}));
|
||||
if (totalCount > 0) {
|
||||
count.value = totalCount;
|
||||
}
|
||||
if (!isMobile.value && !curMail.value && data.value.length > 0) {
|
||||
curMail.value = results[0];
|
||||
curMail.value = data.value[0];
|
||||
}
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
@@ -89,29 +94,9 @@ const clickRow = async (row) => {
|
||||
curMail.value = row;
|
||||
};
|
||||
|
||||
const getAttachments = async (attachment_id) => {
|
||||
try {
|
||||
const res = await api.fetch(
|
||||
`/api/attachment/${attachment_id}`
|
||||
);
|
||||
curAttachments.value = res
|
||||
.filter((item) => item?.content?.data)
|
||||
.map((item) => {
|
||||
return {
|
||||
id: item.contentId || Math.random().toString(36).substring(2, 15),
|
||||
filename: item.filename || "",
|
||||
size: item.size,
|
||||
url: URL.createObjectURL(
|
||||
new Blob(
|
||||
[new Uint8Array(item.content.data)],
|
||||
{ type: item.contentType || 'application/octet-stream' }
|
||||
))
|
||||
}
|
||||
});
|
||||
showAttachments.value = true;
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
const getAttachments = (attachments) => {
|
||||
curAttachments.value = attachments;
|
||||
showAttachments.value = true;
|
||||
};
|
||||
|
||||
const mailItemClass = (row) => {
|
||||
@@ -177,12 +162,17 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-button v-if="curMail.attachment_id" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachment_id)">
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail.raw)">
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
{{ t('downloadMail') }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
<div v-html="curMail.message" style="max-height: 100vh;"></div>
|
||||
<div v-html="curMail.message" style="margin-top: 10px;max-height: 100vh;"></div>
|
||||
</n-card>
|
||||
<n-card class="mail-item" v-else>
|
||||
<n-result status="info" :title="t('pleaseSelectMail')">
|
||||
@@ -238,10 +228,15 @@ onMounted(async () => {
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-button v-if="curMail.attachment_id" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachment_id)">
|
||||
<n-button v-if="curMail.attachments && curMail.attachments.length > 0" size="small" tertiary type="info"
|
||||
@click="getAttachments(curMail.attachments)">
|
||||
{{ t('attachments') }}
|
||||
</n-button>
|
||||
<n-button tag="a" target="_blank" tertiary type="info" size="small'" :download="curMail.id + '.eml'"
|
||||
:href="getDownloadEmlUrl(curMail)">
|
||||
{{ t('downloadMail') }}
|
||||
<n-icon :component="CloudDownloadRound" />
|
||||
</n-button>
|
||||
</n-space>
|
||||
<div v-html="curMail.message" style="max-height: 100vh;"></div>
|
||||
</n-card>
|
||||
|
||||
@@ -7,6 +7,8 @@ import { splitVendorChunkPlugin } from 'vite';
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
@@ -15,6 +17,8 @@ export default defineConfig({
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
splitVendorChunkPlugin(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
|
||||
14
mail-parser-wasm/.gitignore
vendored
Normal file
14
mail-parser-wasm/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
13
mail-parser-wasm/Cargo.toml
Normal file
13
mail-parser-wasm/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "mail-parser-wasm"
|
||||
version = "0.1.6"
|
||||
edition = "2021"
|
||||
description = "A simple mail parser for wasm"
|
||||
license = "MIT"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
mail-parser = "0.9.3"
|
||||
wasm-bindgen = "0.2.92"
|
||||
16
mail-parser-wasm/README.md
Normal file
16
mail-parser-wasm/README.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# mail-parser-wasm
|
||||
|
||||
## usage
|
||||
|
||||
```js
|
||||
import { parse_message } from 'mail-parser-wasm'
|
||||
|
||||
const parsedEmail = parse_message(item.raw);
|
||||
```
|
||||
|
||||
## build
|
||||
|
||||
```bash
|
||||
wasm-pack build --release
|
||||
wasm-pack publish
|
||||
```
|
||||
159
mail-parser-wasm/src/lib.rs
Normal file
159
mail-parser-wasm/src/lib.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
extern crate wasm_bindgen;
|
||||
|
||||
use mail_parser::{MessageParser, MimeHeaders};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
#[wasm_bindgen]
|
||||
pub struct AttachmentResult {
|
||||
content_id: String,
|
||||
content_type: String,
|
||||
filename: String,
|
||||
content: Vec<u8>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl AttachmentResult {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn content_id(&self) -> String {
|
||||
self.content_id.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn content_type(&self) -> String {
|
||||
self.content_type.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn filename(&self) -> String {
|
||||
self.filename.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn content(&self) -> Vec<u8> {
|
||||
self.content.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct MessageResult {
|
||||
sender: String,
|
||||
subject: String,
|
||||
body_html: String,
|
||||
text: String,
|
||||
attachments: Vec<AttachmentResult>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl MessageResult {
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn sender(&self) -> String {
|
||||
self.sender.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn subject(&self) -> String {
|
||||
self.subject.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn body_html(&self) -> String {
|
||||
self.body_html.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn text(&self) -> String {
|
||||
self.text.clone()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(getter)]
|
||||
pub fn attachments(&self) -> Vec<AttachmentResult> {
|
||||
self.attachments.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_attachment(message: &mail_parser::Message) -> Vec<AttachmentResult> {
|
||||
let mut attachments: Vec<AttachmentResult> = Vec::new();
|
||||
for attachment in message.attachments() {
|
||||
if !attachment.is_message() {
|
||||
attachments.push(AttachmentResult {
|
||||
content_id: attachment
|
||||
.content_id()
|
||||
.map(|id| id.to_owned())
|
||||
.unwrap_or(String::new()),
|
||||
content_type: attachment
|
||||
.content_type()
|
||||
.map(|ct| {
|
||||
let c_type = ct.c_type.clone().into_owned();
|
||||
let c_subtype = ct.c_subtype.clone();
|
||||
if c_subtype.is_none() {
|
||||
return c_type;
|
||||
} else {
|
||||
return format!("{}/{}", c_type, c_subtype.unwrap());
|
||||
}
|
||||
})
|
||||
.unwrap_or(String::new()),
|
||||
filename: attachment
|
||||
.attachment_name()
|
||||
.map(|name| name.to_owned())
|
||||
.unwrap_or(String::new()),
|
||||
content: attachment.contents().to_vec(),
|
||||
});
|
||||
} else {
|
||||
attachments.append(
|
||||
&mut attachment
|
||||
.message()
|
||||
.map(|msg| parse_attachment(msg))
|
||||
.unwrap_or(Vec::new()),
|
||||
);
|
||||
}
|
||||
}
|
||||
attachments
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_message(raw_message: &str) -> MessageResult {
|
||||
// check if the message is valid
|
||||
let res = MessageParser::default().parse(raw_message);
|
||||
if res.is_none() {
|
||||
return MessageResult {
|
||||
sender: String::new(),
|
||||
subject: String::new(),
|
||||
body_html: String::new(),
|
||||
text: String::new(),
|
||||
attachments: Vec::new(),
|
||||
};
|
||||
}
|
||||
let message = res.unwrap();
|
||||
|
||||
MessageResult {
|
||||
sender: message
|
||||
.from()
|
||||
.and_then(|from| from.first())
|
||||
.map(|addr| {
|
||||
if addr.name().is_some() {
|
||||
return format!(
|
||||
"{} <{}>",
|
||||
addr.name().unwrap(),
|
||||
addr.address().unwrap_or("")
|
||||
);
|
||||
} else {
|
||||
return addr.address().unwrap_or("").to_owned();
|
||||
}
|
||||
})
|
||||
.unwrap_or(String::new()),
|
||||
subject: message
|
||||
.subject()
|
||||
.map(|subject| subject.to_owned())
|
||||
.unwrap_or(String::new()),
|
||||
body_html: message
|
||||
.body_html(0)
|
||||
.map(|html| html.into_owned())
|
||||
.unwrap_or(String::new()),
|
||||
text: message
|
||||
.body_text(0)
|
||||
.map(|text| text.into_owned())
|
||||
.unwrap_or(String::new()),
|
||||
attachments: parse_attachment(&message),
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.2.2",
|
||||
"mailparser": "^3.6.9",
|
||||
"mimetext": "^3.0.24",
|
||||
"postal-mime": "^2.2.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"mailparser@3.6.9": "patches/mailparser@3.6.9.patch"
|
||||
}
|
||||
"mimetext": "^3.0.24"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
diff --git a/lib/stream-hash.js b/lib/stream-hash.js
|
||||
index 3f9b44133766c04866ab2ab178c061f35dbf8f42..368ed6d94da4401909b7eddc87d18354947daf33 100644
|
||||
--- a/lib/stream-hash.js
|
||||
+++ b/lib/stream-hash.js
|
||||
@@ -7,19 +7,15 @@ class StreamHash extends Transform {
|
||||
constructor(attachment, algo) {
|
||||
super();
|
||||
this.attachment = attachment;
|
||||
- this.algo = (algo || 'md5').toLowerCase();
|
||||
- this.hash = crypto.createHash(algo);
|
||||
this.byteCount = 0;
|
||||
}
|
||||
|
||||
_transform(chunk, encoding, done) {
|
||||
- this.hash.update(chunk);
|
||||
this.byteCount += chunk.length;
|
||||
done(null, chunk);
|
||||
}
|
||||
|
||||
_flush(done) {
|
||||
- this.attachment.checksum = this.hash.digest('hex');
|
||||
this.attachment.size = this.byteCount;
|
||||
done();
|
||||
}
|
||||
204
worker/pnpm-lock.yaml
generated
204
worker/pnpm-lock.yaml
generated
@@ -4,24 +4,13 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
patchedDependencies:
|
||||
mailparser@3.6.9:
|
||||
hash: vtv6mupuxeqjidadcgidi322su
|
||||
path: patches/mailparser@3.6.9.patch
|
||||
|
||||
dependencies:
|
||||
hono:
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
mailparser:
|
||||
specifier: ^3.6.9
|
||||
version: 3.6.9(patch_hash=vtv6mupuxeqjidadcgidi322su)
|
||||
mimetext:
|
||||
specifier: ^3.0.24
|
||||
version: 3.0.24
|
||||
postal-mime:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1
|
||||
|
||||
devDependencies:
|
||||
wrangler:
|
||||
@@ -340,13 +329,6 @@ packages:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@selderee/plugin-htmlparser2@0.11.0:
|
||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||
dependencies:
|
||||
domhandler: 5.0.3
|
||||
selderee: 0.11.0
|
||||
dev: false
|
||||
|
||||
/@types/node-forge@1.3.11:
|
||||
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
|
||||
dependencies:
|
||||
@@ -450,48 +432,6 @@ packages:
|
||||
ms: 2.1.2
|
||||
dev: true
|
||||
|
||||
/deepmerge@4.3.1:
|
||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: false
|
||||
|
||||
/dom-serializer@2.0.0:
|
||||
resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==}
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
entities: 4.5.0
|
||||
dev: false
|
||||
|
||||
/domelementtype@2.3.0:
|
||||
resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==}
|
||||
dev: false
|
||||
|
||||
/domhandler@5.0.3:
|
||||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
dev: false
|
||||
|
||||
/domutils@3.1.0:
|
||||
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
dev: false
|
||||
|
||||
/encoding-japanese@2.0.0:
|
||||
resolution: {integrity: sha512-++P0RhebUC8MJAwJOsT93dT+5oc5oPImp1HubZpAuCZ5kTLnhuuBhKHj2jJeO/Gj93idPBWmIuQ9QWMe5rX3pQ==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
dev: false
|
||||
|
||||
/entities@4.5.0:
|
||||
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
|
||||
engines: {node: '>=0.12'}
|
||||
dev: false
|
||||
|
||||
/esbuild@0.17.19:
|
||||
resolution: {integrity: sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -580,43 +520,11 @@ packages:
|
||||
function-bind: 1.1.2
|
||||
dev: true
|
||||
|
||||
/he@1.2.0:
|
||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/hono@4.2.2:
|
||||
resolution: {integrity: sha512-mDmjBHF6uBNN3TASdAbDCFsN9FLbrlgXyFZkhLEkU7hUgk0+T9hcsUrL/nho4qV+Xk0RDHx7gop4Q1gelZZVRw==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
dev: false
|
||||
|
||||
/html-to-text@9.0.5:
|
||||
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
'@selderee/plugin-htmlparser2': 0.11.0
|
||||
deepmerge: 4.3.1
|
||||
dom-serializer: 2.0.0
|
||||
htmlparser2: 8.0.2
|
||||
selderee: 0.11.0
|
||||
dev: false
|
||||
|
||||
/htmlparser2@8.0.2:
|
||||
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
|
||||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
domutils: 3.1.0
|
||||
entities: 4.5.0
|
||||
dev: false
|
||||
|
||||
/iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
dev: false
|
||||
|
||||
/is-binary-path@2.1.0:
|
||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -651,80 +559,12 @@ packages:
|
||||
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||
dev: false
|
||||
|
||||
/leac@0.6.0:
|
||||
resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==}
|
||||
dev: false
|
||||
|
||||
/libbase64@1.2.1:
|
||||
resolution: {integrity: sha512-l+nePcPbIG1fNlqMzrh68MLkX/gTxk/+vdvAb388Ssi7UuUN31MI44w4Yf33mM3Cm4xDfw48mdf3rkdHszLNew==}
|
||||
dev: false
|
||||
|
||||
/libbase64@1.3.0:
|
||||
resolution: {integrity: sha512-GgOXd0Eo6phYgh0DJtjQ2tO8dc0IVINtZJeARPeiIJqge+HdsWSuaDTe8ztQ7j/cONByDZ3zeB325AHiv5O0dg==}
|
||||
dev: false
|
||||
|
||||
/libmime@5.2.0:
|
||||
resolution: {integrity: sha512-X2U5Wx0YmK0rXFbk67ASMeqYIkZ6E5vY7pNWRKtnNzqjvdYYG8xtPDpCnuUEnPU9vlgNev+JoSrcaKSUaNvfsw==}
|
||||
dependencies:
|
||||
encoding-japanese: 2.0.0
|
||||
iconv-lite: 0.6.3
|
||||
libbase64: 1.2.1
|
||||
libqp: 2.0.1
|
||||
dev: false
|
||||
|
||||
/libmime@5.3.4:
|
||||
resolution: {integrity: sha512-TsqPdercr6DHrnoQx1F0nS2Y4yPT+fWuOjEP2rqzvV77hMYWomTe/rpm0u9JORQ/FavEXybAGcBJsQbLr9+hjA==}
|
||||
dependencies:
|
||||
encoding-japanese: 2.0.0
|
||||
iconv-lite: 0.6.3
|
||||
libbase64: 1.3.0
|
||||
libqp: 2.1.0
|
||||
dev: false
|
||||
|
||||
/libqp@2.0.1:
|
||||
resolution: {integrity: sha512-Ka0eC5LkF3IPNQHJmYBWljJsw0UvM6j+QdKRbWyCdTmYwvIDE6a7bCm0UkTAL/K+3KXK5qXT/ClcInU01OpdLg==}
|
||||
dev: false
|
||||
|
||||
/libqp@2.1.0:
|
||||
resolution: {integrity: sha512-O6O6/fsG5jiUVbvdgT7YX3xY3uIadR6wEZ7+vy9u7PKHAlSEB6blvC1o5pHBjgsi95Uo0aiBBdkyFecj6jtb7A==}
|
||||
dev: false
|
||||
|
||||
/linkify-it@5.0.0:
|
||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||
dependencies:
|
||||
uc.micro: 2.1.0
|
||||
dev: false
|
||||
|
||||
/magic-string@0.25.9:
|
||||
resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==}
|
||||
dependencies:
|
||||
sourcemap-codec: 1.4.8
|
||||
dev: true
|
||||
|
||||
/mailparser@3.6.9(patch_hash=vtv6mupuxeqjidadcgidi322su):
|
||||
resolution: {integrity: sha512-1fIDZlgN1NnuzmTSEUxkaViquXYkw5NbQehVc+kz55QRy98QgLdTtRSKv289Jy4NrCiDchRx6zAijB4HrPsvkA==}
|
||||
dependencies:
|
||||
encoding-japanese: 2.0.0
|
||||
he: 1.2.0
|
||||
html-to-text: 9.0.5
|
||||
iconv-lite: 0.6.3
|
||||
libmime: 5.3.4
|
||||
linkify-it: 5.0.0
|
||||
mailsplit: 5.4.0
|
||||
nodemailer: 6.9.11
|
||||
punycode: 2.3.1
|
||||
tlds: 1.250.0
|
||||
dev: false
|
||||
patched: true
|
||||
|
||||
/mailsplit@5.4.0:
|
||||
resolution: {integrity: sha512-wnYxX5D5qymGIPYLwnp6h8n1+6P6vz/MJn5AzGjZ8pwICWssL+CCQjWBIToOVHASmATot4ktvlLo6CyLfOXWYA==}
|
||||
dependencies:
|
||||
libbase64: 1.2.1
|
||||
libmime: 5.2.0
|
||||
libqp: 2.0.1
|
||||
dev: false
|
||||
|
||||
/mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -795,23 +635,11 @@ packages:
|
||||
engines: {node: '>= 6.13.0'}
|
||||
dev: true
|
||||
|
||||
/nodemailer@6.9.11:
|
||||
resolution: {integrity: sha512-UiAkgiERuG94kl/3bKfE8o10epvDnl0vokNEtZDPTq9BWzIl6EFT9336SbIT4oaTBD8NmmUTLsQyXHV82eXSWg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
dev: false
|
||||
|
||||
/normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/parseley@0.12.1:
|
||||
resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==}
|
||||
dependencies:
|
||||
leac: 0.6.0
|
||||
peberminta: 0.9.0
|
||||
dev: false
|
||||
|
||||
/path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
dev: true
|
||||
@@ -820,28 +648,15 @@ packages:
|
||||
resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==}
|
||||
dev: true
|
||||
|
||||
/peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
dev: false
|
||||
|
||||
/picomatch@2.3.1:
|
||||
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
|
||||
engines: {node: '>=8.6'}
|
||||
dev: true
|
||||
|
||||
/postal-mime@2.2.1:
|
||||
resolution: {integrity: sha512-YqGeFmiKXUxv32hOy2t47VX67mYydC47CTCc7+HKd3xlNKPDhivnO/ZovN3iWXxvyyL2TRTxusuuq3etWeCKsw==}
|
||||
dev: false
|
||||
|
||||
/printable-characters@1.0.42:
|
||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||
dev: true
|
||||
|
||||
/punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/readdirp@3.6.0:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
@@ -888,16 +703,6 @@ packages:
|
||||
estree-walker: 0.6.1
|
||||
dev: true
|
||||
|
||||
/safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
dev: false
|
||||
|
||||
/selderee@0.11.0:
|
||||
resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==}
|
||||
dependencies:
|
||||
parseley: 0.12.1
|
||||
dev: false
|
||||
|
||||
/selfsigned@2.4.1:
|
||||
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -933,11 +738,6 @@ packages:
|
||||
engines: {node: '>= 0.4'}
|
||||
dev: true
|
||||
|
||||
/tlds@1.250.0:
|
||||
resolution: {integrity: sha512-rWsBfFCWKrjM/o2Q1TTUeYQv6tHSd/umUutDjVs6taTuEgRDIreVYIBgWRWW4ot7jp6n0UVUuxhTLWBtUmPu/w==}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/to-regex-range@5.0.1:
|
||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||
engines: {node: '>=8.0'}
|
||||
@@ -949,10 +749,6 @@ packages:
|
||||
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
|
||||
dev: true
|
||||
|
||||
/uc.micro@2.1.0:
|
||||
resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
|
||||
dev: false
|
||||
|
||||
/undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
dev: true
|
||||
|
||||
158
worker/src/admin_api.js
Normal file
158
worker/src/admin_api.js
Normal file
@@ -0,0 +1,158 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/admin/address', async (c) => {
|
||||
const { limit, offset, query } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
if (query) {
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT * FROM address where concat('${c.env.PREFIX}', name) like ? order by id desc limit ? offset ? `
|
||||
).bind(`%${query}%`, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: addressCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address where concat('${c.env.PREFIX}', name) like ?`
|
||||
).bind(`%${query}%`).first();
|
||||
count = addressCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results.map((r) => {
|
||||
r.name = c.env.PREFIX + r.name;
|
||||
return r;
|
||||
}),
|
||||
count: count
|
||||
})
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT * FROM address order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: addressCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address`
|
||||
).first();
|
||||
count = addressCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results.map((r) => {
|
||||
r.name = c.env.PREFIX + r.name;
|
||||
return r;
|
||||
}),
|
||||
count: count
|
||||
})
|
||||
})
|
||||
|
||||
api.delete('/admin/delete_address/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE id = ? `
|
||||
).bind(id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to delete address", 500)
|
||||
}
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM mails WHERE address IN
|
||||
(select concat('${c.env.PREFIX}', name) from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/show_password/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const name = await c.env.DB.prepare(
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(id).first("name");
|
||||
// compute address
|
||||
const emailAddress = c.env.PREFIX + name
|
||||
const jwt = await Jwt.sign({
|
||||
address: emailAddress,
|
||||
address_id: id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
password: jwt
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
api.get('/admin/mails', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?`
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM raw_mails where address = ? `
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/admin/mails_unknow', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(`
|
||||
SELECT id, source, raw, created_at FROM raw_mails
|
||||
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
|
||||
order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM raw_mails
|
||||
where address NOT IN
|
||||
(select concat('${c.env.PREFIX}', name) from address)`
|
||||
).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/admin/statistics', async (c) => {
|
||||
const { count: mailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails`
|
||||
).first();
|
||||
const { count: addressCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM address`
|
||||
).first();
|
||||
const { count: activeUserCount7days } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
||||
).first();
|
||||
return c.json({
|
||||
mailCount: mailCount,
|
||||
userCount: addressCount,
|
||||
activeUserCount7days: activeUserCount7days
|
||||
})
|
||||
});
|
||||
|
||||
export { api }
|
||||
114
worker/src/api_v1.js
Normal file
114
worker/src/api_v1.js
Normal file
@@ -0,0 +1,114 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
// api v1 is deprecated
|
||||
const api = new Hono()
|
||||
|
||||
api.get('/admin/v1/mails', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ? `
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ? `
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/admin/v1/mails_unknow', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(`
|
||||
SELECT id, source, subject, message FROM mails
|
||||
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
|
||||
order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails
|
||||
where address NOT IN
|
||||
(select concat('${c.env.PREFIX}', name) from address)`
|
||||
).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/api/v1/mails', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, subject, message, message_id, created_at FROM mails where address = ? order by id desc limit ? offset ?`
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ?`
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
// add attachments
|
||||
let attachmentResults = [];
|
||||
const message_ids = results.map((r) => r.message_id).filter((r) => r);
|
||||
if (message_ids && message_ids.length > 0) {
|
||||
const { results: innerAttachmentResults } = await c.env.DB.prepare(
|
||||
`SELECT id, message_id FROM attachments where message_id in (${message_ids.map((id) => `'${id}'`).join(",")})`
|
||||
).all();
|
||||
attachmentResults = innerAttachmentResults || [];
|
||||
}
|
||||
results.forEach((r) => {
|
||||
const attachment_id = attachmentResults.filter((ar) => ar.message_id == r.message_id).map((ar) => ar.id);
|
||||
if (attachment_id && attachment_id.length > 0) {
|
||||
r.attachment_id = attachment_id[0];
|
||||
}
|
||||
delete r.message_id;
|
||||
})
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
})
|
||||
|
||||
// attachments
|
||||
api.get("/api/v1/attachment/:attachment_id", async (c) => {
|
||||
const { attachment_id } = c.req.param();
|
||||
const { data } = await c.env.DB.prepare(
|
||||
`SELECT data FROM attachments where id = ? `
|
||||
).bind(attachment_id).first();
|
||||
if (!data) {
|
||||
return c.text("Not found", 404)
|
||||
}
|
||||
return c.json(JSON.parse(data))
|
||||
})
|
||||
|
||||
export { api }
|
||||
@@ -1,8 +1,5 @@
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { EmailMessage } from "cloudflare:email";
|
||||
import { simpleParser } from 'mailparser';
|
||||
import PostalMime from 'postal-mime';
|
||||
global.setImmediate = (callback) => callback();
|
||||
|
||||
async function email(message, env, ctx) {
|
||||
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => message.from.includes(word))) {
|
||||
@@ -23,34 +20,17 @@ async function email(message, env, ctx) {
|
||||
}
|
||||
const message_id = message.headers.get("Message-ID");
|
||||
|
||||
let parsedEmail = {};
|
||||
// todo fix this
|
||||
if (!message.from.endsWith("@mega.nz")) {
|
||||
try {
|
||||
parsedEmail = await simpleParser(rawEmail)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsedEmail.html && !parsedEmail.textAsHtml && !parsedEmail.text) {
|
||||
console.log("Failed parse email, try postal-mime");
|
||||
parsedEmail = await PostalMime.parse(rawEmail);
|
||||
}
|
||||
|
||||
// process email
|
||||
// save email
|
||||
const { success } = await env.DB.prepare(
|
||||
`INSERT INTO mails (source, address, subject, message, message_id) VALUES (?, ?, ?, ?, ?)`
|
||||
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
message.from, message.to,
|
||||
parsedEmail.subject || "",
|
||||
parsedEmail.html || parsedEmail.textAsHtml || parsedEmail.text || "",
|
||||
message_id
|
||||
message.from, message.to, rawEmail, message_id
|
||||
).run();
|
||||
if (!success) {
|
||||
message.setReject(`Failed save message to ${message.to}`);
|
||||
console.log(`Failed save message from ${message.from} to ${message.to}`);
|
||||
}
|
||||
|
||||
// auto reply email
|
||||
try {
|
||||
const results = await env.DB.prepare(
|
||||
@@ -80,28 +60,6 @@ async function email(message, env, ctx) {
|
||||
} catch (error) {
|
||||
console.log("reply email error", error);
|
||||
}
|
||||
// process attachments
|
||||
try {
|
||||
if (
|
||||
env.ENABLE_ATTACHMENT
|
||||
&& parsedEmail.attachments
|
||||
&& parsedEmail.attachments.length > 0
|
||||
) {
|
||||
const { success } = await env.DB.prepare(
|
||||
`INSERT INTO attachments (source, address, message_id, data) VALUES (?, ?, ?, ?)`
|
||||
).bind(
|
||||
message.from, message.to, message_id,
|
||||
JSON.stringify(parsedEmail.attachments)
|
||||
).run();
|
||||
if (!success) {
|
||||
message.setReject(`Failed save attachment to ${message.to}`);
|
||||
console.log(`Failed save attachment from ${message.from} to ${message.to}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.log("save attachment error", error);
|
||||
}
|
||||
} else {
|
||||
message.setReject(`Unknown address ${message.to}`);
|
||||
console.log(`Unknown address ${message.to}`);
|
||||
|
||||
@@ -16,31 +16,15 @@ api.get('/api/mails', async (c) => {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, subject, message, message_id, created_at FROM mails where address = ? order by id desc limit ? offset ?`
|
||||
`SELECT id, source, raw, created_at FROM raw_mails where address = ? order by id desc limit ? offset ?`
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ?`
|
||||
`SELECT count(*) as count FROM raw_mails where address = ?`
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
// add attachments
|
||||
let attachmentResults = [];
|
||||
const message_ids = results.map((r) => r.message_id).filter((r) => r);
|
||||
if (message_ids && message_ids.length > 0) {
|
||||
const { results: innerAttachmentResults } = await c.env.DB.prepare(
|
||||
`SELECT id, message_id FROM attachments where message_id in (${message_ids.map((id) => `'${id}'`).join(",")})`
|
||||
).all();
|
||||
attachmentResults = innerAttachmentResults || [];
|
||||
}
|
||||
results.forEach((r) => {
|
||||
const attachment_id = attachmentResults.filter((ar) => ar.message_id == r.message_id).map((ar) => ar.id);
|
||||
if (attachment_id && attachment_id.length > 0) {
|
||||
r.attachment_id = attachment_id[0];
|
||||
}
|
||||
delete r.message_id;
|
||||
})
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
@@ -84,28 +68,29 @@ api.get('/api/settings', async (c) => {
|
||||
console.warn("Failed to update address")
|
||||
}
|
||||
}
|
||||
let auto_reply = {};
|
||||
const results = await c.env.DB.prepare(
|
||||
`SELECT * FROM auto_reply_mails where address = ? `
|
||||
).bind(address).first();
|
||||
if (!results) {
|
||||
return c.json({
|
||||
auto_reply: {},
|
||||
address: address
|
||||
});
|
||||
}
|
||||
return c.json({
|
||||
auto_reply: {
|
||||
if (results) {
|
||||
auto_reply = {
|
||||
subject: results.subject,
|
||||
message: results.message,
|
||||
enabled: results.enabled == 1,
|
||||
source_prefix: results.source_prefix,
|
||||
name: results.name,
|
||||
},
|
||||
address: address
|
||||
}
|
||||
}
|
||||
const { count: mailCountV1 } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ?`
|
||||
).bind(address).first();
|
||||
return c.json({
|
||||
auto_reply: auto_reply,
|
||||
address: address,
|
||||
has_v1_mails: mailCountV1 > 0
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
api.post('/api/settings', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { auto_reply } = await c.req.json();
|
||||
@@ -211,169 +196,4 @@ api.delete('/api/delete_address', async (c) => {
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/address', async (c) => {
|
||||
const { limit, offset, query } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
if (query) {
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT * FROM address where concat('${c.env.PREFIX}', name) like ? order by id desc limit ? offset ? `
|
||||
).bind(`%${query}%`, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: addressCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address where concat('${c.env.PREFIX}', name) like ?`
|
||||
).bind(`%${query}%`).first();
|
||||
count = addressCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results.map((r) => {
|
||||
r.name = c.env.PREFIX + r.name;
|
||||
return r;
|
||||
}),
|
||||
count: count
|
||||
})
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT * FROM address order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: addressCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM address`
|
||||
).first();
|
||||
count = addressCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results.map((r) => {
|
||||
r.name = c.env.PREFIX + r.name;
|
||||
return r;
|
||||
}),
|
||||
count: count
|
||||
})
|
||||
})
|
||||
|
||||
api.delete('/admin/delete_address/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE id = ? `
|
||||
).bind(id).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to delete address", 500)
|
||||
}
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM mails WHERE address IN
|
||||
(select concat('${c.env.PREFIX}', name) from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
}
|
||||
return c.json({
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
api.get('/admin/show_password/:id', async (c) => {
|
||||
const { id } = c.req.param();
|
||||
const name = await c.env.DB.prepare(
|
||||
`SELECT name FROM address WHERE id = ? `
|
||||
).bind(id).first("name");
|
||||
// compute address
|
||||
const emailAddress = c.env.PREFIX + name
|
||||
const jwt = await Jwt.sign({
|
||||
address: emailAddress,
|
||||
address_id: id
|
||||
}, c.env.JWT_SECRET)
|
||||
return c.json({
|
||||
password: jwt
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
api.get('/admin/mails', async (c) => {
|
||||
const { address, limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(
|
||||
`SELECT id, source, subject, message FROM mails where address = ? order by id desc limit ? offset ? `
|
||||
).bind(address, limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(
|
||||
`SELECT count(*) as count FROM mails where address = ? `
|
||||
).bind(address).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
api.get('/admin/mails_unknow', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const { results } = await c.env.DB.prepare(`
|
||||
SELECT id, source, subject, message FROM mails
|
||||
where address NOT IN(select concat('${c.env.PREFIX}', name) from address)
|
||||
order by id desc limit ? offset ? `
|
||||
).bind(limit, offset).all();
|
||||
let count = 0;
|
||||
if (offset == 0) {
|
||||
const { count: mailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails
|
||||
where address NOT IN
|
||||
(select concat('${c.env.PREFIX}', name) from address)`
|
||||
).first();
|
||||
count = mailCount;
|
||||
}
|
||||
return c.json({
|
||||
results: results,
|
||||
count: count
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
api.get('/admin/statistics', async (c) => {
|
||||
const { count: mailCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM mails`
|
||||
).first();
|
||||
const { count: addressCount } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM address`
|
||||
).first();
|
||||
const { count: activeUserCount7days } = await c.env.DB.prepare(`
|
||||
SELECT count(*) as count FROM address where updated_at > datetime('now', '-7 day')`
|
||||
).first();
|
||||
return c.json({
|
||||
mailCount: mailCount,
|
||||
userCount: addressCount,
|
||||
activeUserCount7days: activeUserCount7days
|
||||
})
|
||||
});
|
||||
|
||||
// attachments
|
||||
api.get("/api/attachment/:attachment_id", async (c) => {
|
||||
const { attachment_id } = c.req.param();
|
||||
const { data } = await c.env.DB.prepare(
|
||||
`SELECT data FROM attachments where id = ? `
|
||||
).bind(attachment_id).first();
|
||||
if (!data) {
|
||||
return c.text("Not found", 404)
|
||||
}
|
||||
return c.json(JSON.parse(data))
|
||||
})
|
||||
|
||||
export { api }
|
||||
|
||||
@@ -3,6 +3,8 @@ import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
|
||||
import { api } from './router';
|
||||
import { api as adminApi } from './admin_api';
|
||||
import { api as apiV1 } from './api_v1';
|
||||
import { email } from './email';
|
||||
|
||||
const app = new Hono()
|
||||
@@ -36,8 +38,10 @@ app.use('/admin/*', async (c, next) => {
|
||||
|
||||
|
||||
app.route('/', api)
|
||||
app.route('/', adminApi)
|
||||
app.route('/', apiV1)
|
||||
|
||||
app.all('/*', async c => c.html(`<h1>Hello World</h1>`))
|
||||
app.all('/*', async c => c.text("Not Found", 404))
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
name = "cloudflare_temp_email"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2023-08-14"
|
||||
compatibility_date = "2023-12-01"
|
||||
node_compat = true
|
||||
|
||||
[vars]
|
||||
@@ -12,8 +12,6 @@ PREFIX = "tmp"
|
||||
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
|
||||
JWT_SECRET = "xxx"
|
||||
BLACK_LIST = ""
|
||||
# IF YOU WANT DISABLE ATTACHMENT, SET IT TO false or COMMENT IT
|
||||
ENABLE_ATTACHMENT = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
|
||||
Reference in New Issue
Block a user