Compare commits

...

45 Commits

Author SHA1 Message Date
Dream Hunter
6ae3b0d85e feat: update docs (#574) 2025-01-24 17:36:59 +08:00
Dream Hunter
01e6cb1075 feat: |worker| health_check add JWT_SECRET and DOMAINS (#573) 2025-01-24 15:00:50 +08:00
Dream Hunter
814f6fada2 feat: |UI| admin worker config page add overflow: auto (#572) 2025-01-22 23:34:49 +08:00
Dream Hunter
31901aacc5 feat: update docs (#571) 2025-01-22 23:25:40 +08:00
Dream Hunter
fb9b9f6ae4 feat: update CHANGE LOG (#570) 2025-01-22 23:19:53 +08:00
Dream Hunter
095951ab45 feat: update docs (#569) 2025-01-22 23:14:38 +08:00
Dream Hunter
37614ce6fa feat: footer support html (#567) 2025-01-21 10:24:13 +08:00
Dream Hunter
3f81fbee6d feat: announcement support html (#566)
* feat: announcement support html

* feat: update dependencies
2025-01-20 13:53:40 +08:00
Dream Hunter
cf13236e7b fix: telegram mail page use iframe show email (#564) 2025-01-18 14:59:08 +08:00
Dream Hunter
36e9c611e6 feat: |Worker| add REMOVE_ALL_ATTACHMENT and REMOVE_EXCEED_SIZE_ATTAC… (#563)
feat: |Worker| add REMOVE_ALL_ATTACHMENT and REMOVE_EXCEED_SIZE_ATTACHMENT
2025-01-18 14:43:09 +08:00
Dream Hunter
047200c1c2 feat: |Worker| add REMOVE_ALL_ATTACHMENT and REMOVE_EXCEED_SIZE_ATTAC… (#562)
feat: |Worker| add REMOVE_ALL_ATTACHMENT and REMOVE_EXCEED_SIZE_ATTACHMENT
2025-01-18 14:12:01 +08:00
Dream Hunter
a22add0e14 fix: telegram mail page use iframe show email (#561) 2025-01-18 13:52:09 +08:00
Dream Hunter
7b1c4cc72a fix: mail-parser-wasm parsedEmailContext cache (#560) 2025-01-18 13:26:09 +08:00
刘志聪
3870727a08 fix: rpc headers covert & typo (#559)
Co-authored-by: liuzhicong <liuzhicong@dhgate.com>
2025-01-16 00:20:02 +08:00
Dream Hunter
2bb033964c feat: update doc (#557) 2025-01-11 18:56:36 +08:00
Dream Hunter
9db5a00b35 feat: v0.8.5 && update dependencies && fix deprecated warning for `… (#556)
feat: v0.8.5 && update dependencies && fix `deprecated` warning for `mail-parser-wasm-worker`
2025-01-11 18:46:46 +08:00
Dream Hunter
e161eb5d14 fix: telegram query email do not pass parsedEmailContext (#555) 2025-01-11 18:14:34 +08:00
Dream Hunter
b604f56d56 feat: |Github Action| Deploy Backend add DEBUG_MODE for logging && BA… (#554)
feat: |Github Action| Deploy Backend add DEBUG_MODE for logging && BACKEND_USE_MAIL_WASM_PARSER to enable mail-parser-wasm-worker
2025-01-11 18:04:53 +08:00
Dream Hunter
52caf811f5 feat: add JUNK_MAIL_CHECK_LIST for check exits and passed item && add ParsedEmailContext to cache the parsed Email (#553)
* feat: Junk mail only check JUNK_MAIL_FORCE_PASS_LIST

* feat: add `JUNK_MAIL_CHECK_LIST` for check exits and passed item && add `ParsedEmailContext` to cache the parsed Email
2025-01-11 17:42:20 +08:00
Dream Hunter
ee3884914b Update CHANGELOG.md 2025-01-09 22:50:22 +08:00
Dream Hunter
844fc52bbc feat: |UI| add configAutoRefreshInterval && autoRefresh useStorage (#549)
* feat: |UI| add configAutoRefreshInterval && autoRefresh useStorage

* Update MailBox.vue

* Update MailBox.vue
2025-01-09 22:49:25 +08:00
Dream Hunter
b87b49f09d Update CHANGELOG.md 2025-01-08 20:04:11 +08:00
刘志聪
5bfa588f70 feat: trigger another worker (#547) 2025-01-08 20:02:48 +08:00
Dream Hunter
92620cdedb feat: add DISABLE_ANONYMOUS_USER_CREATE_EMAIL which only allow logi… (#545)
feat: add `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` which only allow login user create email address
2025-01-05 18:51:48 +08:00
Dream Hunter
e9748be9fe Update vite.config.js (#544) 2025-01-05 02:05:40 +08:00
Dream Hunter
479322c430 feat: |Telegram Bot| add new command to clean invalid jwts (#543) 2025-01-05 01:52:07 +08:00
Dream Hunter
934e58e23b fix: |UI| admin mails unknown page call wrong api (#542) 2025-01-05 01:14:36 +08:00
Dream Hunter
c964d77a59 feat: |UI| add JUNK_MAIL_FORCE_PASS_LIST (#539) 2024-12-30 18:38:33 +08:00
Dream Hunter
8a03d3e57f feat: |UI| admin portal user oauth config support delete (#538) 2024-12-30 18:08:17 +08:00
Dream Hunter
6caba7c863 feat: add docs (#537) 2024-12-28 13:52:08 +08:00
Dream Hunter
43e5bdc764 feat: update dependencies (#536) 2024-12-28 00:32:07 +08:00
Dream Hunter
7bec0daba4 feat: update docs (#534) 2024-12-27 10:47:14 +08:00
Dream Hunter
13e5adef17 feat: update docs (#533) 2024-12-26 22:23:41 +08:00
Dream Hunter
440238133e feat: |Github Action| add upstream sync and auto deploy frontend&&bac… (#528)
feat: |Github Action| add upstream sync and auto deploy frontend&&backend
2024-12-23 22:55:10 +08:00
Dream Hunter
4a881e2d2b feat: upgrade dependencies (#527) 2024-12-23 21:10:45 +08:00
Dream Hunter
b0bf7a5f13 feat: add NO_LIMIT_SEND_ADDRESS_LIST_KEY in admin account settings page (#525) 2024-12-22 15:52:53 +08:00
Dream Hunter
a9bb8785ba feat: support send mail from admin portal(no balance limit) (#524) 2024-12-22 15:40:26 +08:00
Dream Hunter
0b48baff6d fix: frontend github actions cannot use branch param to deploy (#520)
* Update frontend_deploy.yaml

* Update frontend_deploy.yaml
2024-12-19 17:58:54 +08:00
Dream Hunter
e0b5e80efd feat: |doc| update doc (#510) 2024-12-04 00:56:52 +08:00
Dream Hunter
b0e36ac2aa feat: |doc| update Telegram Bot doc (#509) 2024-12-04 00:33:47 +08:00
Dream Hunter
51db19c85b feat: |UI| add tip for multiple tag (#508) 2024-12-04 00:29:01 +08:00
Dream Hunter
e52b010aa4 feat: |doc| update doc (#507) 2024-12-03 22:04:46 +08:00
Dream Hunter
8f6793402c feat: |UI| add forward in mail page (#502) 2024-11-30 15:53:48 +08:00
Dream Hunter
e86c530116 feat: |UI| hide ID for user (#501) 2024-11-30 15:11:37 +08:00
Dream Hunter
0308f518da feat: upgrade dependencies && |doc| update ui install worker doc (#494) 2024-11-22 14:42:35 +08:00
83 changed files with 4893 additions and 3941 deletions

View File

@@ -0,0 +1,44 @@
diff --git a/worker/src/common.ts b/worker/src/common.ts
index bd9bcc9..e7e2748 100644
--- a/worker/src/common.ts
+++ b/worker/src/common.ts
@@ -273,23 +273,23 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
}
const raw_mail = parsedEmailContext.rawEmail;
// TODO: WASM parse email
- // try {
- // const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
+ try {
+ const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
- // const parsedEmail = parse_message_wrapper(raw_mail);
- // parsedEmailContext.parsedEmail = {
- // sender: parsedEmail.sender || "",
- // subject: parsedEmail.subject || "",
- // text: parsedEmail.text || "",
- // headers: parsedEmail.headers?.map(
- // (header) => ({ key: header.key, value: header.value })
- // ) || [],
- // html: parsedEmail.body_html || "",
- // };
- // return parsedEmailContext.parsedEmail;
- // } catch (e) {
- // console.error("Failed use mail-parser-wasm-worker to parse email", e);
- // }
+ const parsedEmail = parse_message_wrapper(raw_mail);
+ parsedEmailContext.parsedEmail = {
+ sender: parsedEmail.sender || "",
+ subject: parsedEmail.subject || "",
+ text: parsedEmail.text || "",
+ headers: parsedEmail.headers?.map(
+ (header) => ({ key: header.key, value: header.value })
+ ) || [],
+ html: parsedEmail.body_html || "",
+ };
+ return parsedEmailContext.parsedEmail;
+ } catch (e) {
+ console.error("Failed use mail-parser-wasm-worker to parse email", e);
+ }
try {
const { default: PostalMime } = await import('postal-mime');
const parsedEmail = await PostalMime.parse(raw_mail);

View File

@@ -1,6 +1,9 @@
name: Deploy Backend Production
name: Deploy Backend
on:
workflow_run:
workflows: [Upstream Sync]
types: [completed]
push:
tags:
- "*"
@@ -29,14 +32,28 @@ jobs:
- name: Deploy Backend for ${{ github.ref_name }}
run: |
export debug_mode=${{ secrets.DEBUG_MODE }}
export use_mail_wasm_parser=${{ secrets.BACKEND_USE_MAIL_WASM_PARSER }}
cd worker/
echo '${{ secrets.BACKEND_TOML }}' > wrangler.toml
pnpm install --no-frozen-lockfile
output=$(pnpm run deploy 2>&1)
if [ $? -ne 0 ]; then
code=$?
echo "Command failed with exit code $code"
exit $code
if [ -n "$use_mail_wasm_parser" ]; then
echo "Using mail-parser-wasm-worker"
pnpm add mail-parser-wasm-worker
git apply ../.github/config/mail-parser-wasm-worker.patch
echo "Applied mail-parser-wasm-worker patch"
fi
if [ -n "$debug_mode" ]; then
pnpm run deploy
else
output=$(pnpm run deploy 2>&1)
if [ $? -ne 0 ]; then
code=$?
echo "Command failed with exit code $code"
exit $code
fi
fi
echo "Deployed for tag ${{ github.ref_name }}"
env:

View File

@@ -1,9 +1,10 @@
name: Deploy Frontend
on:
workflow_run:
workflows: [Upstream Sync]
types: [completed]
push:
paths:
- "frontend/**"
tags:
- "*"
workflow_dispatch:
@@ -38,12 +39,12 @@ jobs:
export frontend_branch=${{ secrets.FRONTEND_BRANCH }}
if [ -n "$frontend_branch" ]; then
echo "Deploying branch $frontend_branch"
pnpm run deploy:actions --project-name=$project_name
pnpm run deploy:actions --project-name=$project_name --branch $frontend_branch
else
echo "Deploying branch prodcution"
echo "Deploying branch production"
pnpm run deploy --project-name=$project_name
fi
echo "Deploying prodcution for ${{ github.ref_name }}"
echo "Deploying production for ${{ github.ref_name }}"
echo "Deployed for tag ${{ github.ref_name }}"
export tg_mini_app_project_name=${{ secrets.TG_FRONTEND_NAME }}
@@ -51,9 +52,9 @@ jobs:
echo "Deploying telegram mini app $tg_mini_app_project_name"
if [ -n "$frontend_branch" ]; then
echo "Deploying telegram mini app branch $frontend_branch"
pnpm run deploy:actions:telegram --project-name=$tg_mini_app_project_name
pnpm run deploy:actions:telegram --project-name=$tg_mini_app_project_name --branch $frontend_branch
else
echo "Deploying telegram mini app branch prodcution"
echo "Deploying telegram mini app branch production"
pnpm run deploy:telegram --project-name=$tg_mini_app_project_name
fi
echo "Deployed telegram mini app for ${{ github.ref_name }}"

25
.github/workflows/sync.yaml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Upstream Sync
on:
schedule:
- cron: "0 0 * * 1"
workflow_dispatch:
jobs:
sync_latest_from_upstream:
name: Sync latest commits from upstream repo
runs-on: ubuntu-latest
if: ${{ github.event.repository.fork }}
steps:
- uses: actions/checkout@v4
- name: Sync upstream changes
id: sync
uses: aormsby/Fork-Sync-With-Upstream-action@v3.4
with:
upstream_sync_repo: dreamhunter2333/cloudflare_temp_email
upstream_sync_branch: main
target_sync_branch: main
target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set
test_mode: false

View File

@@ -1,7 +1,50 @@
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
# CHANGE LOG
## main(v0.8.0)
## main(v0.8.6)
- feat: |UI| 公告支持 html 格式
- feat: |UI| `COPYRIGHT` 支持 html 格式
- feat: |Doc| 优化部署文档,补充了 `Github Actions 部署文档`,增加了 `Worker 变量说明`
## v0.8.5
- feat: |mail-parser-wasm-worker| 修复 `initSync` 函数调用时的 `deprecated` 参数警告
- feat: rpc headers covert & typo (#559)
- fix: telegram mail page use iframe show email (#561)
- feat: |Worker| 增加 `REMOVE_ALL_ATTACHMENT``REMOVE_EXCEED_SIZE_ATTACHMENT` 用于移除邮件附件,由于是解析邮件的一些信息会丢失,比如图片等.
## v0.8.4
- fix: |UI| 修复 admin portal 无收件人邮箱删除调用api 错误
- feat: |Telegram Bot| 增加 telegram bot 清理无效地址凭证命令
- feat: 增加 worker 配置 `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` 禁用匿名用户创建邮箱地址,只允许登录用户创建邮箱地址
- feat: 增加 worker 配置 `ENABLE_ANOTHER_WORKER``ANOTHER_WORKER_LIST` ,用于调用其他 worker 的 rpc 接口 (#547)
- feat: |UI| 自动刷新配置保存到浏览器,可配置刷新间隔
- feat: 垃圾邮件检测增加存在时才检查的列表 `JUNK_MAIL_CHECK_LIST` 配置
- feat: | Worker | 增加 `ParsedEmailContext` 类用于缓存解析后的邮件内容,减少解析次数
- feat: |Github Action| Worker 部署增加 `DEBUG_MODE` 输出日志, `BACKEND_USE_MAIL_WASM_PARSER` 配置是否使用 wasm 解析邮件
## v0.8.3
- feat: |Github Action| 增加自动更新并部署功能
- feat: |UI| admin 用户设置,支持 oauth2 配置的删除
- feat: 增加垃圾邮件检测必须通过的列表 `JUNK_MAIL_FORCE_PASS_LIST` 配置
## v0.8.2
- fix: |Doc| 修复文档中的一些错误
- fix: |Github Action| 修复 frontend 部署分支错误的问题
- feat: admin 发送邮件功能
- feat: admin 后台,账号配置页面添加无限发送邮件的地址列表
## v0.8.1
- feat: |Doc| 更新 UI 安装的文档
- feat: |UI| 对用户隐藏邮箱账号的 ID
- feat: |UI| 增加邮件详情页的 `转发` 按钮
## v0.8.0
- feat: |UI| 随机生成地址时不超过最大长度
- feat: |UI| 邮件时间显示浏览器时区,可在设置中切换显示为 UTC 时间
@@ -9,6 +52,14 @@
## v0.7.6
### Breaking Changes
UI 部署 worker 需要点击 Settings -> Runtime, 修改 Compatibility flags, 增加 `nodejs_compat`
![worker-runtime](vitepress-docs/docs/public/ui_install/worker-runtime.png)
### Changes
- feat: 支持提前设置 bot info, 降低 telegram 回调延迟 (#441)
- feat: 增加 telegram mini app 的 build 压缩包
- feat: 增加是否启用垃圾邮件检查 `ENABLE_CHECK_JUNK_MAIL` 配置

View File

@@ -31,9 +31,9 @@
## [查看部署文档](https://temp-mail-docs.awsl.uk)
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/github-action.html)
[Github Action 部署文档](https://temp-mail-docs.awsl.uk/zh/guide/actions/github-action.html)
[English Docs](https://temp-mail-docs.awsl.uk/en/)

View File

@@ -1,6 +1,6 @@
{
"name": "cloudflare_temp_email",
"version": "0.8.0",
"version": "0.8.6",
"private": true,
"type": "module",
"scripts": {
@@ -19,33 +19,34 @@
"deploy:actions": "npm run build && wrangler pages deploy ./dist"
},
"dependencies": {
"@simplewebauthn/browser": "^10.0.0",
"@unhead/vue": "^1.11.11",
"@vicons/material": "^0.12.0",
"@vueuse/core": "^10.11.1",
"@simplewebauthn/browser": "10.0.0",
"@unhead/vue": "^1.11.18",
"@vueuse/core": "^12.5.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"axios": "^1.7.7",
"axios": "^1.7.9",
"jszip": "^3.10.1",
"mail-parser-wasm": "^0.1.8",
"naive-ui": "^2.40.1",
"postal-mime": "^2.3.2",
"mail-parser-wasm": "^0.2.1",
"naive-ui": "^2.41.0",
"postal-mime": "^2.4.1",
"vooks": "^0.2.12",
"vue": "^3.5.12",
"vue": "^3.5.13",
"vue-clipboard3": "^2.0.0",
"vue-i18n": "^9.14.1",
"vue-router": "^4.4.5"
"vue-i18n": "^11.0.1",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vicons/fa": "^0.12.0",
"@vitejs/plugin-vue": "^5.1.4",
"unplugin-auto-import": "^0.18.3",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.10",
"vite-plugin-pwa": "^0.19.8",
"@vicons/fa": "^0.13.0",
"@vicons/material": "^0.13.0",
"@vitejs/plugin-vue": "^5.2.1",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vite": "^6.0.11",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0",
"vite-plugin-wasm": "^3.4.1",
"workbox-build": "^7.3.0",
"workbox-window": "^7.3.0",
"wrangler": "^3.84.1"
"wrangler": "^3.104.0"
}
}

3067
frontend/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -61,24 +61,26 @@ onMounted(async () => {
<n-config-provider :locale="localeConfig" :theme="theme">
<n-global-style />
<n-spin description="loading..." :show="loading">
<n-message-provider container-style="margin-top: 20px;">
<n-grid x-gap="12" :cols="12">
<n-gi v-if="showSideMargin" span="1"></n-gi>
<n-gi :span="!showSideMargin ? 12 : 10">
<div class="main">
<n-space vertical>
<n-layout style="min-height: 80vh;">
<Header />
<router-view></router-view>
</n-layout>
<Footer />
</n-space>
</div>
</n-gi>
<n-gi v-if="showSideMargin" span="1"></n-gi>
</n-grid>
<n-back-top />
</n-message-provider>
<n-notification-provider container-style="margin-top: 60px;">
<n-message-provider container-style="margin-top: 20px;">
<n-grid x-gap="12" :cols="12">
<n-gi v-if="showSideMargin" span="1"></n-gi>
<n-gi :span="!showSideMargin ? 12 : 10">
<div class="main">
<n-space vertical>
<n-layout style="min-height: 80vh;">
<Header />
<router-view></router-view>
</n-layout>
<Footer />
</n-space>
</div>
</n-gi>
<n-gi v-if="showSideMargin" span="1"></n-gi>
</n-grid>
<n-back-top />
</n-message-provider>
</n-notification-provider>
</n-spin>
</n-config-provider>
</template>

View File

@@ -1,4 +1,5 @@
import { useGlobalState } from '../store'
import { h } from 'vue'
import axios from 'axios'
const API_BASE = import.meta.env.VITE_API_BASE || "";
@@ -52,7 +53,7 @@ const apiFetch = async (path, options = {}) => {
}
}
const getOpenSettings = async (message) => {
const getOpenSettings = async (message, notification) => {
try {
const res = await api.fetch("/open_api/settings");
const domainLabels = res["domainLabels"] || [];
@@ -75,6 +76,7 @@ const getOpenSettings = async (message) => {
}),
adminContact: res["adminContact"] || "",
enableUserCreateEmail: res["enableUserCreateEmail"] || false,
disableAnonymousUserCreateEmail: res["disableAnonymousUserCreateEmail"] || false,
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
enableAutoReply: res["enableAutoReply"] || false,
enableIndexAbout: res["enableIndexAbout"] || false,
@@ -88,10 +90,12 @@ const getOpenSettings = async (message) => {
}
if (openSettings.value.announcement && openSettings.value.announcement != announcement.value) {
announcement.value = openSettings.value.announcement;
message.info(announcement.value, {
showIcon: false,
duration: 0,
closable: true
notification.info({
content: () => {
return h("div", {
innerHTML: announcement.value
});
}
});
}
} catch (error) {

View File

@@ -3,7 +3,7 @@ import { watch, onMounted, ref, onBeforeUnmount } from "vue";
import { useMessage } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../store'
import { CloudDownloadRound, ReplyFilled } from '@vicons/material'
import { CloudDownloadRound, ReplyFilled, ForwardFilled } from '@vicons/material'
import { useIsMobile } from '../utils/composables'
import { processItem, getDownloadEmlUrl } from '../utils/email-parser'
import { utcToLocalDate } from '../utils';
@@ -50,11 +50,10 @@ const props = defineProps({
})
const {
isDark, mailboxSplitSize, indexTab, loading, useUTCDate,
isDark, mailboxSplitSize, indexTab, loading, useUTCDate, autoRefresh, configAutoRefreshInterval,
useIframeShowMail, sendMailModel, preferShowTextMail
} = useGlobalState()
const autoRefresh = ref(false)
const autoRefreshInterval = ref(30)
const autoRefreshInterval = ref(configAutoRefreshInterval.value)
const data = ref([])
const timer = ref(null)
@@ -86,6 +85,7 @@ const { t } = useI18n({
delete: 'Delete',
deleteMailTip: 'Are you sure you want to delete mail?',
reply: 'Reply',
forwardMail: 'Forward',
showTextMail: 'Show Text Mail',
showHtmlMail: 'Show Html Mail',
saveToS3: 'Save to S3',
@@ -105,6 +105,7 @@ const { t } = useI18n({
delete: '删除',
deleteMailTip: '确定要删除邮件吗?',
reply: '回复',
forwardMail: '转发',
showTextMail: '显示纯文本邮件',
showHtmlMail: '显示HTML邮件',
saveToS3: '保存到S3',
@@ -117,14 +118,16 @@ const { t } = useI18n({
});
const setupAutoRefresh = async (autoRefresh) => {
// auto refresh every 30 seconds
autoRefreshInterval.value = 30;
// auto refresh every configAutoRefreshInterval seconds
autoRefreshInterval.value = configAutoRefreshInterval.value;
if (autoRefresh) {
clearInterval(timer.value);
timer.value = setInterval(async () => {
if (loading.value) return;
autoRefreshInterval.value--;
if (autoRefreshInterval.value <= 0) {
autoRefreshInterval.value = 30;
await refresh();
autoRefreshInterval.value = configAutoRefreshInterval.value;
await backFirstPageAndRefresh();
}
}, 1000)
} else {
@@ -135,7 +138,7 @@ const setupAutoRefresh = async (autoRefresh) => {
watch(autoRefresh, async (autoRefresh, old) => {
setupAutoRefresh(autoRefresh)
})
}, { immediate: true })
watch([page, pageSize], async ([page, pageSize], [oldPage, oldPageSize]) => {
if (page !== oldPage || pageSize !== oldPageSize) {
@@ -168,6 +171,11 @@ const refresh = async () => {
}
};
const backFirstPageAndRefresh = async () =>{
page.value = 1;
await refresh();
}
const clickRow = async (row) => {
if (multiActionMode.value) {
row.checked = !row.checked;
@@ -215,6 +223,15 @@ const replyMail = async () => {
indexTab.value = 'sendmail';
};
const forwardMail = async () => {
Object.assign(sendMailModel.value, {
subject: `${t('forwardMail')}: ${curMail.value.subject}`,
contentType: curMail.value.message ? 'html' : 'text',
content: curMail.value.message || curMail.value.text,
});
indexTab.value = 'sendmail';
};
const onSpiltSizeChange = (size) => {
mailboxSplitSize.value = size;
}
@@ -355,7 +372,7 @@ onBeforeUnmount(() => {
{{ t('autoRefresh') }}
</template>
</n-switch>
<n-button @click="refresh" type="primary" tertiary>
<n-button @click="backFirstPageAndRefresh" type="primary" tertiary>
{{ t('refresh') }}
</n-button>
</n-space>
@@ -429,6 +446,12 @@ onBeforeUnmount(() => {
</template>
{{ t('reply') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
<template #icon>
<n-icon :component="ForwardFilled" />
</template>
{{ t('forwardMail') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>
@@ -459,7 +482,7 @@ onBeforeUnmount(() => {
{{ t('autoRefresh') }}
</template>
</n-switch>
<n-button @click="refresh" tertiary size="small" type="primary">
<n-button @click="backFirstPageAndRefresh" tertiary size="small" type="primary">
{{ t('refresh') }}
</n-button>
</n-space>
@@ -523,6 +546,12 @@ onBeforeUnmount(() => {
</template>
{{ t('reply') }}
</n-button>
<n-button v-if="showReply" size="small" tertiary type="info" @click="forwardMail">
<template #icon>
<n-icon :component="ForwardFilled" />
</template>
{{ t('forwardMail') }}
</n-button>
<n-button size="small" tertiary type="info" @click="showTextMail = !showTextMail">
{{ showTextMail ? t('showHtmlMail') : t('showTextMail') }}
</n-button>

View File

@@ -19,6 +19,7 @@ export const useGlobalState = createGlobalState(
needAuth: false,
adminContact: '',
enableUserCreateEmail: false,
disableAnonymousUserCreateEmail: false,
enableUserDeleteEmail: false,
enableAutoReply: false,
enableIndexAbout: false,
@@ -71,6 +72,8 @@ export const useGlobalState = createGlobalState(
const globalTabplacement = useStorage('globalTabplacement', 'top');
const useSideMargin = useStorage('useSideMargin', true);
const useUTCDate = useStorage('useUTCDate', false);
const autoRefresh = useStorage('autoRefresh', false);
const configAutoRefreshInterval = useStorage("configAutoRefreshInterval", 60);
const userOpenSettings = ref({
fetched: false,
enable: false,
@@ -129,6 +132,8 @@ export const useGlobalState = createGlobalState(
globalTabplacement,
useSideMargin,
useUTCDate,
autoRefresh,
configAutoRefreshInterval,
telegramApp,
isTelegram,
showAdminPage,

View File

@@ -23,6 +23,7 @@ import Telegram from './admin/Telegram.vue';
import Webhook from './admin/Webhook.vue';
import MailWebhook from './admin/MailWebhook.vue';
import WorkerConfig from './admin/WorkerConfig.vue';
import SendMail from './admin/SendMail.vue';
const {
adminAuth, showAdminAuth, adminTab, loading,
@@ -45,6 +46,7 @@ const { t } = useI18n({
accessHeader: 'Admin Password',
accessTip: 'Please enter the admin password',
mails: 'Emails',
sendMail: 'Send Mail',
qucickSetup: 'Quick Setup',
account: 'Account',
account_create: 'Create Account',
@@ -70,6 +72,7 @@ const { t } = useI18n({
accessHeader: 'Admin 密码',
accessTip: '请输入 Admin 密码',
mails: '邮件',
sendMail: '发送邮件',
qucickSetup: '快速设置',
account: '账号',
account_create: '创建账号',
@@ -172,6 +175,9 @@ onMounted(async () => {
<n-tab-pane name="sendBox" :tab="t('sendBox')">
<SendBox />
</n-tab-pane>
<n-tab-pane name="sendMail" :tab="t('sendMail')">
<SendMail />
</n-tab-pane>
<n-tab-pane name="mailWebhook" :tab="t('mailWebhook')">
<MailWebhook />
</n-tab-pane>

View File

@@ -21,9 +21,14 @@ const { t } = useI18n({
<div>
<n-divider class="footer-divider" />
<div style="text-align: center; padding: 20px">
<n-text depth="3">
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }} {{ openSettings.copyright }}
</n-text>
<n-space justify="center">
<n-text depth="3">
{{ t('copyright') }} © 2023-{{ new Date().getFullYear() }}
</n-text>
<n-text depth="3">
<div v-html="openSettings.copyright"></div>
</n-text>
</n-space>
</div>
</div>
</template>

View File

@@ -15,6 +15,7 @@ import { api } from '../api'
import { getRouterPathWithLang } from '../utils'
const message = useMessage()
const notification = useNotification()
const {
toggleDark, isDark, isTelegram, showAdminPage,
@@ -223,7 +224,7 @@ const logoClick = async () => {
}
onMounted(async () => {
await api.getOpenSettings(message);
await api.getOpenSettings(message, notification);
// make sure user_id is fetched
if (!userSettings.value.user_id) await api.getUserSettings(message);
});

View File

@@ -11,22 +11,24 @@ const message = useMessage()
const { t } = useI18n({
messages: {
en: {
tip: 'You can manually input the following multiple select input',
tip: 'You can manually input the following multiple select input and enter',
save: 'Save',
successTip: 'Save Success',
address_block_list: 'Address Block Keywords for Users(Admin can skip)',
address_block_list_placeholder: 'Please enter the keywords you want to block',
send_address_block_list: 'Address Block Keywords for send email',
noLimitSendAddressList: 'No Balance Limit Send Address List',
verified_address_list: 'Verified Address List(Can send email by cf internal api)',
fromBlockList: 'Block Keywords for receive email',
},
zh: {
tip: '您可以手动输入以下多选输入框',
tip: '您可以手动输入以下多选输入框, 回车增加',
save: '保存',
successTip: '保存成功',
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
send_address_block_list: '发送邮件地址屏蔽关键词',
noLimitSendAddressList: '无余额限制发送地址列表',
verified_address_list: '已验证地址列表(可通过 cf 内部 api 发送邮件)',
fromBlockList: '接收邮件地址屏蔽关键词',
}
@@ -35,6 +37,7 @@ const { t } = useI18n({
const addressBlockList = ref([])
const sendAddressBlockList = ref([])
const noLimitSendAddressList = ref([])
const verifiedAddressList = ref([])
const fromBlockList = ref([])
@@ -45,6 +48,7 @@ const fetchData = async () => {
sendAddressBlockList.value = res.sendBlockList || []
verifiedAddressList.value = res.verifiedAddressList || []
fromBlockList.value = res.fromBlockList || []
noLimitSendAddressList.value = res.noLimitSendAddressList || []
} catch (error) {
message.error(error.message || "error");
}
@@ -59,6 +63,7 @@ const save = async () => {
sendBlockList: sendAddressBlockList.value || [],
verifiedAddressList: verifiedAddressList.value || [],
fromBlockList: fromBlockList.value || [],
noLimitSendAddressList: noLimitSendAddressList.value || [],
})
})
message.success(t('successTip'))
@@ -76,7 +81,7 @@ onMounted(async () => {
<template>
<div class="center">
<n-card :bordered="false" embedded style="max-width: 600px;">
<n-alert :show-icon="false" style="margin-bottom: 10px;">
<n-alert :show-icon="false" type="warning" style="margin-bottom: 10px;">
{{ t("tip") }}
</n-alert>
<n-form-item-row :label="t('address_block_list')">
@@ -87,6 +92,10 @@ onMounted(async () => {
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
:placeholder="t('address_block_list_placeholder')" />
</n-form-item-row>
<n-form-item-row :label="t('noLimitSendAddressList')">
<n-select v-model:value="noLimitSendAddressList" filterable multiple tag
:placeholder="t('noLimitSendAddressList')" />
</n-form-item-row>
<n-form-item-row :label="t('verified_address_list')">
<n-select v-model:value="verifiedAddressList" filterable multiple tag
:placeholder="t('verified_address_list')" />

View File

@@ -11,7 +11,7 @@ const fetchMailUnknowData = async (limit, offset) => {
}
const deleteMail = async (curMailId) => {
await api.fetch(`/api/mails/${curMailId}`, { method: 'DELETE' });
await api.fetch(`/admin/mails/${curMailId}`, { method: 'DELETE' });
};
</script>

View File

@@ -0,0 +1,199 @@
<script setup>
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { useI18n } from 'vue-i18n'
import { onBeforeUnmount, ref, shallowRef } from 'vue'
import { useSessionStorage } from '@vueuse/core'
import { api } from '../../api'
const message = useMessage()
const isPreview = ref(false)
const editorRef = shallowRef()
const sendMailModel = useSessionStorage('sendMailByAdminModel', {
fromName: "",
fromMail: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
});
const { t } = useI18n({
locale: 'zh',
messages: {
en: {
successSend: 'Please check your sendbox. If failed, please try again later.',
fromName: 'Your Name and Address, leave Name blank to use email address',
toName: 'Recipient Name and Address, leave Name blank to use email address',
subject: 'Subject',
options: 'Options',
edit: 'Edit',
preview: 'Preview',
content: 'Content',
send: 'Send',
text: 'Text',
html: 'HTML',
'rich text': 'Rich Text',
tooLarge: 'Too large file, please upload file less than 1MB.',
},
zh: {
successSend: '请查看您的发件箱, 如果失败, 请检查稍后重试。',
fromName: '你的名称和地址,名称不填写则使用邮箱地址',
toName: '收件人名称和地址,名称不填写则使用邮箱地址',
subject: '主题',
options: '选项',
edit: '编辑',
preview: '预览',
content: '内容',
send: '发送',
text: '文本',
html: 'HTML',
'rich text': '富文本',
tooLarge: '文件过大, 请上传小于1MB的文件。',
}
}
});
const contentTypes = [
{ label: t('text'), value: 'text' },
{ label: t('html'), value: 'html' },
{ label: t('rich text'), value: 'rich' },
]
const send = async () => {
try {
await api.fetch(`/admin/send_mail`,
{
method: 'POST',
body:
JSON.stringify({
from_name: sendMailModel.value.fromName,
from_mail: sendMailModel.value.fromMail,
to_name: sendMailModel.value.toName,
to_mail: sendMailModel.value.toMail,
subject: sendMailModel.value.subject,
is_html: sendMailModel.value.contentType != 'text',
content: sendMailModel.value.content,
})
})
sendMailModel.value = {
fromName: "",
fromMail: "",
toName: "",
toMail: "",
subject: "",
contentType: 'text',
content: "",
}
} catch (error) {
message.error(error.message || "error");
} finally {
message.success(t("successSend"));
}
}
const toolbarConfig = {
excludeKeys: ["uploadVideo"]
}
const editorConfig = {
MENU_CONF: {
'uploadImage': {
async customUpload() {
message.error(t('tooLarge'))
},
maxFileSize: 1 * 1024 * 1024,
base64LimitSize: 1 * 1024 * 1024,
}
}
}
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor;
}
</script>
<template>
<div class="center">
<n-card :bordered="false" embedded>
<n-flex justify="end">
<n-button type="primary" @click="send">{{ t('send') }}</n-button>
</n-flex>
<div class="left">
<n-form :model="sendMailModel">
<n-form-item :label="t('fromName')" label-placement="top">
<n-input-group>
<n-input v-model:value="sendMailModel.fromName" />
<n-input v-model:value="sendMailModel.fromMail" />
</n-input-group>
</n-form-item>
<n-form-item :label="t('toName')" label-placement="top">
<n-input-group>
<n-input v-model:value="sendMailModel.toName" />
<n-input v-model:value="sendMailModel.toMail" />
</n-input-group>
</n-form-item>
<n-form-item :label="t('subject')" label-placement="top">
<n-input v-model:value="sendMailModel.subject" />
</n-form-item>
<n-form-item :label="t('options')" label-placement="top">
<n-radio-group v-model:value="sendMailModel.contentType">
<n-radio-button v-for="option in contentTypes" :key="option.value" :value="option.value"
:label="option.label" />
</n-radio-group>
<n-button v-if="sendMailModel.contentType != 'text'" @click="isPreview = !isPreview"
style="margin-left: 10px;">
{{ isPreview ? t('edit') : t('preview') }}
</n-button>
</n-form-item>
<n-form-item :label="t('content')" label-placement="top">
<n-card :bordered="false" embedded v-if="isPreview">
<div v-html="sendMailModel.content" />
</n-card>
<div v-else-if="sendMailModel.contentType == 'rich'" style="border: 1px solid #ccc">
<Toolbar style="border-bottom: 1px solid #ccc" :defaultConfig="toolbarConfig"
:editor="editorRef" mode="default" />
<Editor style="height: 500px; overflow-y: hidden;" v-model="sendMailModel.content"
:defaultConfig="editorConfig" mode="default" @onCreated="handleCreated" />
</div>
<n-input v-else type="textarea" v-model:value="sendMailModel.content" :autosize="{
minRows: 3
}" />
</n-form-item>
</n-form>
</div>
</n-card>
</div>
</template>
<style scoped>
.n-card {
max-width: 800px;
}
.n-button {
text-align: left;
margin-right: 10px;
}
.center {
display: flex;
text-align: center;
place-items: center;
justify-content: center;
}
.left {
text-align: left;
place-items: left;
justify-content: left;
}
</style>

View File

@@ -17,7 +17,7 @@ const { t } = useI18n({
status: 'Check Status',
enableTelegramAllowList: 'Enable Telegram Allow List(Manually input user ID)',
enable: 'Enable',
telegramAllowList: 'Telegram Allow List',
telegramAllowList: 'Telegram Allow List(Manually input telegram user ID)',
save: 'Save',
miniAppUrl: 'Telegram Mini App URL',
enableGlobalMailPush: 'Enable Global Mail Push(Manually input telegram user ID)',
@@ -27,12 +27,12 @@ const { t } = useI18n({
init: '初始化',
successTip: '成功',
status: '查看状态',
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID)',
enableTelegramAllowList: '启用 Telegram 白名单(手动输入用户 ID, 回车增加)',
enable: '启用',
telegramAllowList: 'Telegram 白名单',
telegramAllowList: 'Telegram 白名单(手动输入用户 ID, 回车增加)',
save: '保存',
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID)',
enableGlobalMailPush: '启用全局邮件推送(手动输入邮箱管理员的 telegram 用户 ID, 回车增加)',
globalMailPushList: '全局邮件推送用户列表',
}
}

View File

@@ -17,6 +17,7 @@ const { t } = useI18n({
messages: {
en: {
save: 'Save',
delete: 'Delete',
successTip: 'Save Success',
enable: 'Enable',
enableMailAllowList: 'Enable Mail Address Allow List(Manually enterable)',
@@ -28,9 +29,10 @@ const { t } = useI18n({
},
zh: {
save: '保存',
delete: '删除',
successTip: '保存成功',
enable: '启用',
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
mailAllowList: '邮件地址白名单',
addOauth2: '添加 Oauth2',
name: '名称',
@@ -193,8 +195,19 @@ onMounted(async () => {
{{ t('save') }}
</n-button>
</n-flex>
<n-collapse default-expanded-names="1" accordion>
<n-divider />
<n-collapse default-expanded-names="1" accordion :trigger-areas="['main', 'arrow']">
<n-collapse-item v-for="(item, index) in userOauth2Settings" :key="index" :title="item.name">
<template #header-extra>
<n-popconfirm @positive-click="userOauth2Settings.splice(index, 1)">
<template #trigger>
<n-button tertiary type="error">
{{ t('delete') }}
</n-button>
</template>
{{ t('delete') }}
</n-popconfirm>
</template>
<n-form :model="item">
<n-form-item-row :label="t('name')" required>
<n-input v-model:value="item.name" />

View File

@@ -28,7 +28,7 @@ const { t } = useI18n({
enableUserRegister: "允许用户注册",
enableMailVerify: '启用邮件验证(发送地址必须是系统中能有余额且能正常发送邮件的地址)',
verifyMailSender: '验证邮件发送地址',
enableMailAllowList: '启用邮件地址白名单(可手动输入)',
enableMailAllowList: '启用邮件地址白名单(可手动输入, 回车增加)',
mailAllowList: '邮件地址白名单',
maxAddressCount: '可绑定最大邮箱地址数量',
}

View File

@@ -13,13 +13,13 @@ const { t } = useI18n({
messages: {
en: {
successTip: 'Success',
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook)',
webhookAllowList: 'Webhook Allow List(Enter the address that is allowed to use webhook and enter)',
save: 'Save',
notEnabled: 'Webhook is not enabled',
},
zh: {
successTip: '成功',
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址)',
webhookAllowList: 'Webhook 白名单(请输入允许使用webhook 的地址, 回车增加)',
save: '保存',
notEnabled: 'Webhook 未开启',
}

View File

@@ -26,7 +26,7 @@ onMounted(async () => {
<template>
<div class="center">
<n-card :bordered="false" embedded style="max-width: 600px;">
<n-card :bordered="false" embedded style="max-width: 600px; overflow: auto;">
<pre>{{ JSON.stringify(settings, null, 2) }}</pre>
</n-card>
</div>

View File

@@ -1,10 +1,13 @@
<script setup>
import { GithubAlt, Discord, Telegram } from '@vicons/fa'
import { useGlobalState } from '../../store'
const { announcement } = useGlobalState()
</script>
<template>
<div class="center">
<n-card :bordered="false" embedded>
<div v-html="announcement"></div>
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
<template #icon>
<n-icon :component="GithubAlt" />

View File

@@ -5,7 +5,7 @@ import { useIsMobile } from '../../utils/composables'
import { useGlobalState } from '../../store'
const {
mailboxSplitSize, useIframeShowMail, preferShowTextMail,
mailboxSplitSize, useIframeShowMail, preferShowTextMail, configAutoRefreshInterval,
globalTabplacement, useSideMargin, useUTCDate
} = useGlobalState()
const isMobile = useIsMobile()
@@ -23,6 +23,7 @@ const { t } = useI18n({
right: 'right',
bottom: 'bottom',
useUTCDate: 'Use UTC Date',
autoRefreshInterval: 'Auto Refresh Interval(Sec)',
},
zh: {
mailboxSplitSize: '邮箱界面分栏大小',
@@ -35,6 +36,7 @@ const { t } = useI18n({
right: '右侧',
bottom: '底部',
useUTCDate: '使用 UTC 时间',
autoRefreshInterval: '自动刷新间隔(秒)',
}
}
});
@@ -50,6 +52,11 @@ const { t } = useI18n({
0.75: '0.75'
}" />
</n-form-item-row>
<n-form-item-row :label="t('autoRefreshInterval')">
<n-slider v-model:value="configAutoRefreshInterval" :min="30" :max="300" :step="1" :marks="{
60: '60', 120: '120', 180: '180', 240: '240'
}" />
</n-form-item-row>
<n-form-item-row :label="t('preferShowTextMail')">
<n-switch v-model:value="preferShowTextMail" :round="false" />
</n-form-item-row>

View File

@@ -34,6 +34,7 @@ const props = defineProps({
})
const message = useMessage()
const notification = useNotification()
const router = useRouter()
const {
@@ -184,9 +185,18 @@ const domainsOptions = computed(() => {
});
});
const showNewAddressTab = computed(() => {
if (openSettings.value.disableAnonymousUserCreateEmail
&& !userSettings.value.user_email
) {
return false;
}
return openSettings.value.enableUserCreateEmail;
});
onMounted(async () => {
if (!openSettings.value.domains || openSettings.value.domains.length === 0) {
await api.getOpenSettings();
await api.getOpenSettings(message, notification);
}
emailDomain.value = domainsOptions.value ? domainsOptions.value[0]?.value : "";
});
@@ -209,8 +219,7 @@ onMounted(async () => {
</template>
{{ t('login') }}
</n-button>
<n-button v-if="openSettings.enableUserCreateEmail" @click="tabValue = 'register'" block secondary
strong>
<n-button v-if="showNewAddressTab" @click="tabValue = 'register'" block secondary strong>
<template #icon>
<n-icon :component="NewLabelOutlined" />
</template>
@@ -218,7 +227,7 @@ onMounted(async () => {
</n-button>
</n-form>
</n-tab-pane>
<n-tab-pane v-if="openSettings.enableUserCreateEmail" name="register" :tab="t('getNewEmail')">
<n-tab-pane v-if="showNewAddressTab" name="register" :tab="t('getNewEmail')">
<n-spin :show="generateNameLoading">
<n-form>
<span>

View File

@@ -46,7 +46,7 @@ onMounted(async () => {
<template>
<div class="center">
<n-card :bordered="false" embedded v-if="curMail.message" style="max-width: 800px; overflow: auto;">
<n-card :bordered="false" embedded v-if="curMail.message" style="max-width: 800px; height: 100%;">
<n-tag type="info">
ID: {{ curMail.id }}
</n-tag>
@@ -59,7 +59,8 @@ onMounted(async () => {
<n-tag v-if="showEMailTo" type="info">
TO: {{ curMail.address }}
</n-tag>
<div v-html="curMail.message" style="margin-top: 10px;"></div>
<iframe :srcdoc="curMail.message" style="margin-top: 10px;width: 100%; height: 100%;">
</iframe>
</n-card>
</div>
</template>
@@ -71,5 +72,6 @@ onMounted(async () => {
text-align: left;
place-items: center;
justify-content: center;
height: 80vh;
}
</style>

View File

@@ -125,10 +125,6 @@ const fetchData = async () => {
}
const columns = [
{
title: "ID",
key: "id"
},
{
title: t('name'),
key: "name"

View File

@@ -24,6 +24,7 @@ export default defineConfig({
{
'naive-ui': [
'useMessage',
'useNotification',
'NButton',
'NPopconfirm',
'NIcon',
@@ -37,7 +38,7 @@ export default defineConfig({
VitePWA({
registerType: null,
devOptions: {
enabled: true
enabled: false
},
workbox: {
disableDevLogs: true,
@@ -68,5 +69,10 @@ export default defineConfig({
},
define: {
'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version),
},
esbuild: {
supported: {
'top-level-await': true
},
}
})

View File

@@ -1,6 +1,6 @@
[package]
name = "mail-parser-wasm"
version = "0.1.8"
version = "0.2.1"
edition = "2021"
description = "A simple mail parser for wasm"
license = "MIT"
@@ -9,5 +9,5 @@ license = "MIT"
crate-type = ["cdylib"]
[dependencies]
mail-parser = "0.9.3"
wasm-bindgen = "0.2.92"
mail-parser = "0.9.4"
wasm-bindgen = "0.2.99"

View File

@@ -35,10 +35,31 @@ impl AttachmentResult {
}
}
#[derive(Clone)]
#[wasm_bindgen]
pub struct MessageHeader {
key: String,
value: String,
}
#[wasm_bindgen]
impl MessageHeader {
#[wasm_bindgen(getter)]
pub fn key(&self) -> String {
self.key.clone()
}
#[wasm_bindgen(getter)]
pub fn value(&self) -> String {
self.value.clone()
}
}
#[wasm_bindgen]
pub struct MessageResult {
sender: String,
subject: String,
headers: Vec<MessageHeader>,
body_html: String,
text: String,
attachments: Vec<AttachmentResult>,
@@ -56,6 +77,11 @@ impl MessageResult {
self.subject.clone()
}
#[wasm_bindgen(getter)]
pub fn headers(&self) -> Vec<MessageHeader> {
self.headers.clone()
}
#[wasm_bindgen(getter)]
pub fn body_html(&self) -> String {
self.body_html.clone()
@@ -119,6 +145,7 @@ pub fn parse_message(raw_message: &str) -> MessageResult {
return MessageResult {
sender: String::new(),
subject: String::new(),
headers: Vec::new(),
body_html: String::new(),
text: String::new(),
attachments: Vec::new(),
@@ -146,6 +173,14 @@ pub fn parse_message(raw_message: &str) -> MessageResult {
.subject()
.map(|subject| subject.to_owned())
.unwrap_or(String::new()),
headers: message
.headers()
.iter()
.map(|header| MessageHeader {
key: header.name().to_owned(),
value: header.value().as_text().unwrap_or("").to_owned(),
})
.collect(),
body_html: message
.body_html(0)
.map(|html| html.into_owned())

View File

@@ -1,12 +1,12 @@
import initAsync, { initSync, parse_message } from './mail_parser_wasm';
import MODULE from './mail_parser_wasm_bg.wasm';
initSync(MODULE);
initSync({ module: MODULE });
export { initAsync, MODULE };
export * from './mail_parser_wasm';
export const parse_message_wrapper = (raw_message) => {
initSync(MODULE);
initSync({ module: MODULE });
return parse_message(raw_message);
}

View File

@@ -7,7 +7,7 @@
"url": "https://github.com/dreamhunter2333/cloudflare_temp_email",
"directory": "mail-parser-wasm"
},
"version": "0.1.8",
"version": "0.2.1",
"license": "MIT",
"files": [
"mail_parser_wasm_bg.wasm",

View File

@@ -1,6 +1,6 @@
{
"name": "temp-email-pages",
"version": "0.8.0",
"version": "0.8.6",
"description": "",
"main": "index.js",
"scripts": {
@@ -11,6 +11,6 @@
"author": "",
"license": "ISC",
"devDependencies": {
"wrangler": "^3.85.0"
"wrangler": "^3.104.0"
}
}

15
scripts/update-dependencies.sh Executable file
View File

@@ -0,0 +1,15 @@
cd frontend/
pnpm up
cd ..
cd worker/
pnpm up
cd ..
cd pages/
pnpm up
cd ..
cd vitepress-docs/
pnpm up
cd ..

View File

@@ -119,9 +119,21 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
},
{
text: '通过 Github Actions 部署',
collapsed: true,
items: [
{ text: 'D1 数据库', link: 'actions/d1' },
{ text: 'Github Actions 配置', link: 'actions/github-action' },
{ text: '配置邮件转发', link: 'email-routing.md' },
{ text: '配置发送邮件', link: 'config-send-mail' },
{ text: '自动更新配置', link: 'actions/auto-update' },
]
},
{
text: '通用',
collapsed: false,
items: [
{ text: '通过 Github Actions 部署', link: 'github-action' },
{ text: 'worker变量说明', link: 'worker-vars' },
{ text: '常见问题', link: 'common-issues' },
]
},
{
@@ -138,6 +150,7 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
{ text: '配置 webhook', link: 'feature/webhook' },
{ text: '新建邮箱地址 API', link: 'feature/new-address-api' },
{ text: 'Oauth2 第三方登录', link: 'feature/user-oauth2' },
{ text: '配置其他worker增强', link: 'feature/another-worker-enhanced' },
]
},
{

View File

@@ -77,13 +77,13 @@ compatibility_flags = [ "nodejs_compat" ]
# TITLE = "Custom Title" # The title of the site
PREFIX = "tmp" # The mailbox name prefix to be processed
# (min, max) length of the adderss, if not set, the default is (1, 30)
# MIN_ADDRESS_LEN = 1
# MAX_ADDRESS_LEN = 30
# ANNOUNCEMENT = "Custom Announcement"
# address check REGEX, if not set, will not check
# ADDRESS_CHECK_REGEX = "^(?!.*admin).*"
# address name replace REGEX, if not set, the default is [^a-z0-9]
# ADDRESS_REGEX = "[^a-z0-9]"
# MIN_ADDRESS_LEN = 1
# MAX_ADDRESS_LEN = 30
# If you want your site to be private, uncomment below and change your password
# PASSWORDS = ["123", "456"]
# admin console password, if not configured, access to the console is not allowed
@@ -107,6 +107,8 @@ JWT_SECRET = "xxx" # Key used to generate jwt
BLACK_LIST = "" # Blacklist, used to filter senders, comma separated
# Allow users to create email addresses
ENABLE_USER_CREATE_EMAIL = true
# Disable anonymous user create email, if set true, users can only create email addresses after logging in
# DISABLE_ANONYMOUS_USER_CREATE_EMAIL = true
# Allow users to delete messages
ENABLE_USER_DELETE_EMAIL = true
# Allow automatic replies to emails
@@ -132,6 +134,14 @@ ENABLE_AUTO_REPLY = false
# FRONTEND_URL = "https://xxxx.xxx"
# Enable check junk mail
# ENABLE_CHECK_JUNK_MAIL = false
# junk mail check list, if status exists and status is not pass, will be marked as junk mail
# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"]
# junk mail force check pass list, if no status or status is not pass, will be marked as junk mail
# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"]
# remove attachment if size exceed 2MB, mail maybe mising some information due to parsing
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
# remove all attachment, mail maybe mising some information due to parsing
# REMOVE_ALL_ATTACHMENT = true
[[d1_databases]]
binding = "DB"

View File

@@ -15,14 +15,17 @@ hero:
- theme: alt
text: 通过用户界面部署
link: /zh/guide/quick-start
- theme: alt
text: 通过 Github Actions 部署
link: /zh/guide/quick-start
features:
- title: 免费托管在 CloudFlare无需服务器
details: Cloudflare D1 数据库Cloudflare Pages 前端Cloudflare Workers 后端, Cloudflare Email Routing
- title: 仅需域名即可私有部署
details: 支持 password 登录邮箱,使用访问密码可作为私人站点,支持附件功能
- title: 仅需域名即可私有部署, 免费托管在 CloudFlare无需服务器
details: 支持 password 登录邮箱, 用户注册,使用访问密码可作为私人站点,支持附件功能。
- title: 使用 rust wasm 解析邮件
details: 使用 rust wasm 解析邮件支持邮件各种RFC标准支持附件, 速度极快
- title: 支持 Telegram Bot 和 Webhook
details: 邮件可转发到 Telegram 或者 webhook, Telegram Bot 支持绑定邮箱,查看邮件, Telegram 小程序
- title: 支持发送邮件(UI/API/SMTP)
details: 支持通过域名邮箱发送 txt 或者 html 邮件,支持 DKIM 签名, UI/API/SMTP 发送邮件
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

View File

@@ -0,0 +1,10 @@
# Github Actions 部署如何配置自动更新
::: warning 注意
有问题请通过 `Github Issues` 反馈,感谢。
自动更新不会执行 D1 数据库的 sql 文件,当数据库 schema 变动时,需要手动执行。
:::
1. 打开仓库的 `Actions` 页面,找到 `Upstream Sync`,点击 `enable workflow` 启用 `workflow`
2. 如果 `Upstream Sync` 运行失败,到仓库主页点击 `Sync` 手动同步即可
3. 修改 `Upstream Sync``schedule` 配置可自定义更新间隔,参考 [cron 表达式](https://crontab.guru/)

View File

@@ -0,0 +1,3 @@
# 初始化/更新 D1 数据库
参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1)

View File

@@ -0,0 +1,31 @@
# 通过 Github Actions 部署
::: warning 注意
目前只支持 worker 和 pages 的部署。
有问题请通过 `Github Issues` 反馈,感谢。
:::
## 部署步骤
1. 在 GitHub fork 本仓库
2. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production``Deploy Frontend`,点击 `enable workflow` 启用 `workflow`
3. 然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`:
- `CLOUDFLARE_ACCOUNT_ID`: Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id)
- `CLOUDFLARE_API_TOKEN`: Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token)
- `BACKEND_TOML`: 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件)
- `FRONTEND_ENV`: 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html)
- `FRONTEND_NAME`: 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建
- `FRONTEND_BRANCH`: (可选) pages 部署的分支,可不配置,默认 `production`
- `TG_FRONTEND_NAME`: (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写
- `DEBUG_MODE`: (可选) 是否开启调试模式,配置为 `true` 开启, 默认 worker 部署日志不会输出到 Github Actions 页面,开启后会输出
- `BACKEND_USE_MAIL_WASM_PARSER`: (可选) 是否使用 wasm 解析邮件,配置为 `true` 开启, 功能参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
4. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production``Deploy Frontend`,点击 `Run workflow` 选择分支手动部署
## 如何配置自动更新
1. 打开仓库的 `Actions` 页面,找到 `Upstream Sync`,点击 `enable workflow` 启用 `workflow`
2. 如果 `Upstream Sync` 运行失败,到仓库主页点击 `Sync` 手动同步即可

View File

@@ -22,6 +22,9 @@ wrangler kv:namespace create DEV
## 修改 `wrangler.toml` 配置文件
> [!NOTE] 注意
> 更多变量的配置请查看 [worker变量说明](/zh/guide/worker-vars)
```toml
name = "cloudflare_temp_email"
main = "src/worker.ts"
@@ -32,7 +35,6 @@ compatibility_flags = [ "nodejs_compat" ]
# routes = [
# { pattern = "temp-email-api.xxxxx.xyz", custom_domain = true },
# ]
node_compat = true
# 如果你想要使用定时任务清理邮件,取消下面的注释,并修改 cron 表达式
# [triggers]
@@ -44,67 +46,20 @@ node_compat = true
# ]
[vars]
# TITLE = "Custom Title" # 自定义网站标题
PREFIX = "tmp" # 要处理的邮箱名称前缀,不需要后缀可配置为空字符串
# (min, max) adderss的长度如果不设置默认为(1, 30)
# ANNOUNCEMENT = "Custom Announcement" # 自定义公告
# address name 的正则表达式, 只用于检查,符合条件将通过检查
# ADDRESS_CHECK_REGEX = "^(?!.*admin).*"
# address name 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 [^a-z0-9], 需谨慎使用, 有些符号可能导致无法收件
# ADDRESS_REGEX = "[^a-z0-9]"
# MIN_ADDRESS_LEN = 1
# MAX_ADDRESS_LEN = 30
# 如果你想要你的网站私有,取消下面的注释,并修改密码
# PASSWORDS = ["123", "456"]
# 邮箱名称前缀,不需要后缀可配置为空字符串或者不配置
PREFIX = "tmp"
# 用于临时邮箱的所有域名, 支持多个域名
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"]
# 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
JWT_SECRET = "xxx"
# admin 控制台密码, 不配置则不允许访问控制台
# ADMIN_PASSWORDS = ["123", "456"]
# 警告: 管理员控制台没有密码或用户检查
# DISABLE_ADMIN_PASSWORD_CHECK = false
# admin 联系方式,不配置则不显示,可配置任意字符串
# ADMIN_CONTACT = "xx@xx.xxx"
# DEFAULT_DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 默认用户可用的域名(未登录或未分配角色的用户)
DOMAINS = ["xxx.xxx1" , "xxx.xxx2"] # 你的域名, 支持多个域名
# 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称
# DOMAIN_LABELS = ["中文.xxx", "xxx.xxx2"]
# 新用户默认角色, 仅在启用邮件验证时有效
# USER_DEFAULT_ROLE = "vip"
# admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台
# ADMIN_USER_ROLE = "admin" # the role which can access admin panel
# 用户角色配置, 如果 domains 为空将使用 default_domains
# 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
# USER_ROLES = [
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "vip", prefix = "vip" },
# { domains = ["xxx.xxx1" , "xxx.xxx2"], role = "admin", prefix = "" },
# ]
JWT_SECRET = "xxx" # 用于生成 jwt 的密钥, jwt 用于给用户登录以及鉴权
BLACK_LIST = "" # 黑名单,用于过滤发件人,逗号分隔
# 是否允许用户创建邮件, 不配置则不允许
ENABLE_USER_CREATE_EMAIL = true
# 允许用户删除邮件, 不配置则不允许
ENABLE_USER_DELETE_EMAIL = true
# 允许自动回复邮件
ENABLE_AUTO_REPLY = false
# 是否启用 webhook
# ENABLE_WEBHOOK = true
# 前端界面页脚文本
# COPYRIGHT = "Dream Hunter"
# DISABLE_SHOW_GITHUB = true # 是否显示 GitHub 链接
# 默认发送邮件余额,如果不设置,将为 0
# DEFAULT_SEND_BALANCE = 1
# NO_LIMIT_SEND_ROLE = "vip" # 可以无限发送邮件的角色
# Turnstile 人机验证配置
# CF_TURNSTILE_SITE_KEY = ""
# CF_TURNSTILE_SECRET_KEY = ""
# telegram bot 最多绑定邮箱数量
# TG_MAX_ADDRESS = 5
# telegram BOT_INFO预定义的 BOT_INFO 可以降低 webhook 的延迟
# TG_BOT_INFO = "{}"
# 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
# 前端地址,用于发送 webhook 的邮件 url
# FRONTEND_URL = "https://xxxx.xxx"
# 是否启用垃圾邮件检查
# ENABLE_CHECK_JUNK_MAIL = false
# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看
[[d1_databases]]
@@ -124,6 +79,11 @@ database_id = "xxx" # D1 数据库 ID
# namespace_id = "1001"
# # 10 requests per minute
# simple = { limit = 10, period = 60 }
# 绑定其他 worker 处理邮件,例如通过 auth-inbox ai 能力解析验证码或激活链接
# [[services]]
# binding = "AUTH_INBOX"
# service = "auth-inbox"
```
## Telegram Bot 配置

View File

@@ -0,0 +1,41 @@
# 常见问题
> [!NOTE] 注意
> 如果你的问题没有在这里找到解决方案,请到 `Github Issues` 中搜索或者提问, 或者到 Telegram 群组中提问。
## 通用
| 问题 | 解决方案 |
| ---------------------------------------------- | ------------------------------------------------------------------------------- |
| 使用 Cloudflare Workers 给已认证的邮箱发送邮件 | 使用 cf 的 API 进行发送,只支持绑定到 CF 上的收件地址,即 CF EMAIL 转发目的地址 |
| 绑定多个域名 | 每个域名都需要设置 email 转发到 worker |
## worker 相关
| 问题 | 解决方案 |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| `Uncaught Error: No such module "path". imported from "worker.js"` | [参考](/zh/guide/ui/worker) |
| `No such module "node:stream". imported from "worker.js"` | [参考](/zh/guide/ui/worker) |
| `二级域名无法发送邮件` | [参考](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/515) |
| `Failed to send verify code: No balance` | admin 后台设置无限制邮件或者发件权限页面增加额度 |
| `Github OAuth无法获取到邮箱 400 Failed to get user email` | 需要 github 用户设置公开邮箱 |
| `Cannot read properties of undefined (reading 'map')` | worker 变量没有设置成功 |
## pages 相关
| 问题 | 解决方案 |
| --------------- | ---------------------------------------- |
| `network error` | 使用无痕模式或者清空浏览器缓存DNS 缓存 |
## telegram bot
| 问题 | 解决方案 |
| -------------------------------------------------------------- | -------------------------------------------------- |
| `Telgram Bot获取邮件失败400Bad Request:BUTTON_URL_INVALID` | tg mini app 的 URL 填写错误,需要填写 pages 的 URL |
| `Telegram bot bind error: bind adress count reach the limit` | 需要设置 worker 变量 `TG_MAX_ADDRESS` |
## Github Actions
| 问题 | 解决方案 |
| ------------------------------------------ | --------------------------------------------------------------------------------- |
| Github Action部署后cf里始终是preview分支 | 到 cf pages 页面的设置中确认 前端的分支 和 Github Action 的 前端部署分支 是否相同 |

View File

@@ -11,9 +11,16 @@ admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送
`API KEYS` 页面创建 `api key`
使用 cli 或者直接添加到 `wrangler.toml``vars`,或者在 cloudflare worker 页面的变量中添加 `RESEND_TOKEN`
然后执行下面的命令,将 `RESEND_TOKEN` 添加到 secrets 中
> [!NOTE]
> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做
如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面
```bash
# 切换到 worker 目录
cd worker
wrangler secret put RESEND_TOKEN
```

View File

@@ -1,9 +1,9 @@
# Cloudflare Email Routing
1. 配置对应域名的 `电子邮件 DNS 记录`, 如果是多个域名,需要配置多个域名的 `电子邮件 DNS 记录`
1. 在 CF 控制台网页的对应域名的 `Email Routing` 下,配置 `电子邮件 DNS 记录`, 如果是多个域名,需要配置多个域名的 `电子邮件 DNS 记录`
2. 在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址。
2. 在将电子邮件地址绑定到您的 Worker 之前,您需要启用电子邮件路由并拥有至少一个经过验证的电子邮件地址(目标地址)
3. 配置每个域名的 `Cloudflare Email Routing` catch-all 发送到 `worker`
3. 配置每个域名的 `Email Routing` 的路由规则中的 `Catch-all 地址` 发送到 `worker`
![email](/readme_assets/email.png)

View File

@@ -0,0 +1,144 @@
# 通过其他 worker 增强
> 临时邮箱的核心能力在邮件的管理,通过其他 worker 可以增强临时邮箱的功能,例如通过 auth-inbox ai 能力解析验证码或激活链接
> 该功能仅触发其他 worker ,在 webhook 后执行
> [!NOTE]
> 如果要使用 worker 增强,请提前创建可以 rpc 调用的 worker具体下文详述
> 参考:
> - https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/
> - https://developers.cloudflare.com/workers/runtime-apis/rpc/
> - auth-inbox 项目https://github.com/TooonyChen/AuthInbox
## 创建其他 worker以 auth-inbox 项目ai解析验证码为例子
### worker 改造为继承 WorkerEntrypoint
一个简单,作为被调用方,提供 rpc 方法调用的worker代码如下rpcEmail 方法为样例)
(使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork
src/index.ts 文件
```js
import { WorkerEntrypoint } from "cloudflare:workers";
interface Env {
DB: D1Database;
// ...
}
export default class extends WorkerEntrypoint<Env> {
async fetch(request: Request): Promise<Response> {
console.log("原本fetch接口入参是request,env,ctx");
console.log("修改为WorkerEntrypoint风格后只有一个入参request获取环境变量和上下文有小改动");
// 环境变量及上下文改动详见:
// https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#bindings-env
// https://developers.cloudflare.com/workers/runtime-apis/bindings/service-bindings/rpc/#lifecycle-methods-ctx
const env: Env = this.env;
const ctx: ExecutionContext = this.ctx;
console.log("后续逻辑不变");
return new Response('ok', { status: 200 });
}
// 主要功能
async email(message: ForwardableEmailMessage): Promise<void> {
console.log("原本fetch接口入参是message,env,ctx");
console.log("修改为WorkerEntrypoint风格后只有一个入参message获取环境变量和上下文和fetch方法一样");
const env: Env = this.env;
const ctx: ExecutionContext = this.ctx;
console.log("接受email routing请求后后续逻辑不变");
}
// 暴露rpc接口处理来自其他worker的邮件请求
async rpcEmail(requestBody: string): Promise<void> {
console.log(`接受其他worker临时邮件服务cloudflare_temp_email的请求request body: ${requestBody}`);
// requestBody json 格式,由临时邮件服务发送,格式如下
// type RPCEmailMessage = {
// from: string | undefined | null,
// to: string | undefined | null,
// rawEmail: string | undefined | null,
// headers: Map<string, string>,
// }
// ... todo ...
}
}
```
### 部署其他 worker
修改好或者使用 以auth-inbox 为例,部署到 cloudflare worker 上,详见 https://github.com/TooonyChen/AuthInbox ,或者使用已经修改好的项目 https://github.com/oneisall8955/AuthInbox-fork
## 配置临时邮件服务,使用指定其他 worker 增强
## 绑定服务
### 通过 wrangler.toml 配置
```toml
[[services]]
binding = "AUTH_INBOX"
service = "auth-inbox"
```
这里的 `binding = "AUTH_INBOX"` 可以自定义,可以是任何字符串,`service = "auth-inbox"` 是部署好的提供rpc接口调用的worker名称。
### 用户界面配置
在设置-绑定,添加绑定,选择绑定服务。
变量名称填写自定义的名称,可以任意字符串 ,例如 `AUTH_INBOX`
服务绑定选择上一步创建好的服务,例如 `auth-inbox`
![another-worker-enhanced-01.png](/feature/another-worker-enhanced-01.png)
![another-worker-enhanced-02.png](/feature/another-worker-enhanced-02.png)
## 环境变量配置
### 通过 wrangler.toml 配置
```toml
ENABLE_ANOTHER_WORKER = true
ANOTHER_WORKER_LIST ="""
[
{
"binding":"AUTH_INBOX",
"method":"rpcEmail",
"keywords":[
"","","","","","","","","","","","","","","","","","",
"account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
]
}
]
"""
```
环境变量解释:
- ENABLE_ANOTHER_WORKER = true默认为falsetrue则开启其他 worker 处理邮件
- ANOTHER_WORKER_LIST 是一个JOSN数组每个对象包3个字段
- binding: *必填必须与services部分指定的 binding = "XXX" 保持一致*,例子中为 AUTH_INBOX
- method: 可选,默认 rpcEmail指的是调用这个 worker 的哪一个 rpc 方法处理
- keywords: 关键词数组,忽略大小写。用于过滤,如果*解析后邮件文本*匹配到这些关键词,触发这个 worker并且调用这个 worker 的 `method` 方法
### 用户界面配置
在设置-环境变量,添加环境变量
- ENABLE_ANOTHER_WORKER = true
- ANOTHER_WORKER_LIST 为上面提及的JSON数组字符串不再复述详细介绍看上文
```json
[
{
"binding":"AUTH_INBOX",
"method":"rpcEmail",
"keywords":[
"验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
"account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
]
}
]
```
![another-worker-enhanced-03.png](/feature/another-worker-enhanced-03.png)
## 测试
发送一个邮件到临时邮箱观察worker日志到或者到 auth-inbox 提供的面板上查看验证码
![another-worker-enhanced-04.png](/feature/another-worker-enhanced-04.png)

View File

@@ -32,6 +32,7 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
sender: parsedEmail.sender || "",
subject: parsedEmail.subject || "",
text: parsedEmail.text || "",
headers: parsedEmail.headers || [],
html: parsedEmail.body_html || "",
};
} catch (e) {

View File

@@ -1,7 +1,9 @@
# 配置子域名邮箱
::: warning
::: warning 注意
子域名邮箱发送邮件可能无法发送邮件,建议使用主域名邮箱发送邮件,子域名邮箱仅用于接收邮件。
mail channel 已不被支持,下面参考中仅限收件部分。
:::
参考

View File

@@ -1,17 +1,30 @@
# 配置 Telegram Bot
试用地址:[@cf_temp_mail_bot](https://t.me/cf_temp_mail_bot)
::: warning 注意
worker 默认的 `worker.dev` 域名的证书是不被 telegram 支持的,配置 Telegram Bot 请使用自定义域名
:::
> [!NOTE]
> 如果要使用 Telegram Bot, 请先绑定 `KV`
>
> 如果不需要 Telegram Bot, 可跳过此步骤
>
> 如果你想 Telegram 的解析邮件能力更强,参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
## Telegram Bot 配置
请先创建一个 Telegram Bot然后获取 `token`,然后执行下面的命令,将 `token` 添加到 secrets 中
你也可以在 Cloudflare 的 UI 界面中添加 `secrets`
> [!NOTE]
> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做
如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面
```bash
# 切换到 worker 目录
cd worker
pnpm wrangler secret put TELEGRAM_BOT_TOKEN
```
@@ -48,8 +61,6 @@ cp .env.example .env.prod
pnpm run deploy:telegram --project-name=<你的项目名称>
```
部署完成后,请在 admin 后台的 `设置` -> `电报小程序` 页面 `电报小程序 URL`
请在 `@BotFather` 处执行 `/setmenubutton`,然后输入你的网页地址,设置左下角的 `Open App` 按钮。
你也可以在 `@BotFather` 处执行 `/newapp` 新建 app 来获得 mini app 的链接
- 部署完成后,请在 admin 后台的 `设置` -> `电报小程序` 页面 `电报小程序 URL` 中填写网页 URL
- 请在 `@BotFather` 处执行 `/setmenubutton`,然后输入你的网页地址,设置左下角的 `Open App` 按钮。
- 请在 `@BotFather` 处执行 `/newapp` 新建 app 来注册 mini app。

View File

@@ -1,23 +0,0 @@
# 通过 Github Actions 部署
::: warning
有问题请通过 `Github Issues` 反馈,感谢。
:::
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
1. 点击按钮 fork 本仓库 或者直接 fork 本仓库
2. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production``Deploy Frontend`,点击 `enable workflow` 启用 `workflow`
3. 然后在仓库页面 `Settings` -> `Secrets and variables` -> `Actions` -> `Repository secrets`, 添加以下 `secrets`:
- `CLOUDFLARE_ACCOUNT_ID`: Cloudflare 账户 ID, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#cloudflare-account-id)
- `CLOUDFLARE_API_TOKEN`: Cloudflare API Token, [参考文档](https://developers.cloudflare.com/workers/wrangler/ci-cd/#api-token)
- `BACKEND_TOML`: 后端配置文件,[参考此处](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件)
- `FRONTEND_ENV`: 前端配置文件,请复制 `frontend/.env.example` 的内容,[并参考此处修改](/zh/guide/cli/pages.html)
- `FRONTEND_NAME`: 你在 Cloudflare Pages 创建的项目名称,可通过 [用户界面](https://temp-mail-docs.awsl.uk/zh/guide/ui/pages.html) 或者 [命令行](https://temp-mail-docs.awsl.uk/zh/guide/cli/pages.html) 创建
- `FRONTEND_BRANCH`: (可选) pages 部署的分支,可不配置,默认 `production`
- `TG_FRONTEND_NAME`: (可选) 你在 Cloudflare Pages 创建的项目名称,同 `FRONTEND_NAME`,如果需要 Telegram Mini App 功能,请填写
1. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production``Deploy Frontend`,点击 `Run workflow` 选择分支手动部署

View File

@@ -1,11 +1,18 @@
# 快速开始
- 良好的网络环境
- cloudflare 账号
## 开始之前
打开 [cloudflare控制台](https://dash.cloudflare.com/)
需要 `良好的网络环境``cloudflare 账号` 打开 [cloudflare控制台](https://dash.cloudflare.com/)
查看通过 [命令行部署](/zh/guide/cli/pre-requisite) 或者 [用户界面部署](/zh/guide/ui/d1)
选择下面三种方式之一进行部署
- [通过命令行部署](/zh/guide/cli/pre-requisite)
- [通过用户界面部署](/zh/guide/ui/d1)
- [通过Github Actions 部署](/zh/guide/actions/github-action)
### 也可以参考网友提供的详细的小白教程
- [【教程】小白也能看懂的自建Cloudflare临时邮箱教程域名邮箱](https://linux.do/t/topic/316819/1)
## 升级流程
@@ -18,14 +25,18 @@
然后参考下面的文档使用 `CLI` 或者 `UI` 覆盖部署之前的 `worker``pages` 即可
CLI 部署
### CLI 部署
- [命令行更新 d1](/zh/guide/cli/d1)
- [命令行部署 worker](/zh/guide/cli/worker)
- [命令行部署 pages](/zh/guide/cli/worker)
UI 部署
### UI 部署
- [用户界面更新 d1](/zh/guide/ui/d1)
- [用户界面部署 worker](/zh/guide/ui/worker)
- [用户界面部署 pages](/zh/guide/ui/pages)
### Github Actions 部署
- [Github Actions 部署如何配置自动更新](/zh/guide/actions/auto-update)

View File

@@ -8,7 +8,7 @@
![worker1](/ui_install/worker-1.png)
3. 回到 `Overview`,找到刚刚创建的 worker点击 `Settings` -> `Runtime`, 修改 `Compatibility flags`, 增加 `nodejs_compat`
3. 回到 `Overview`,找到刚刚创建的 worker点击 `Settings` -> `Runtime`, 修改 `Compatibility flags`, 增加 `nodejs_compat`, 兼容日期也需要大于图片中的日期。
![worker-runtime](/ui_install/worker-runtime.png)
@@ -26,7 +26,36 @@
![worker2](/ui_install/worker-2.png)
![worker-upload](/ui_install/worker-upload.png)
6. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。
6. 点击 `Settings` -> `Variables`, 如图所示添加变量
![worker-var](/ui_install/worker-var.png)
> [!NOTE] 注意
> 更多变量的配置请查看 [worker变量说明](/zh/guide/worker-vars)
>
> 注意字符串格式的变量的最外层的引号是不需要的
>
> 对于 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`
建议配置的变量列表
| 变量名 | 类型 | 说明 | 示例 |
| -------------------------- | ----------- | ------------------------------------------ | ------------------------------------ |
| `PREFIX` | 文本 | 新建邮箱名称默认前缀,不需要前缀可不配置 | `tmp` |
| `DOMAINS` | JSON | 用于临时邮箱的所有域名, 支持多个域名 | `["awsl.uk", "dreamhunter2333.xyz"]` |
| `JWT_SECRET` | 文本/Secret | 用于生成 jwt 的密钥, jwt 用于登录以及鉴权 | `xxx` |
| `ADMIN_PASSWORDS` | JSON | admin 控制台密码, 不配置则不允许访问控制台 | `["123", "456"]` |
| `ENABLE_USER_CREATE_EMAIL` | 文本/JSON | 是否允许用户创建邮箱, 不配置则不允许 | `true` |
| `ENABLE_USER_DELETE_EMAIL` | 文本/JSON | 是否允许用户删除邮件, 不配置则不允许 | `true` |
7. 点击 `Settings` -> `Variables`, 下拉找到 `D1 Database`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Deploy`
> [!NOTE] 重要
> 注意此处 `D1 Database` 的绑定名称必须为 `DB`
![worker-d1](/ui_install/worker-d1.png)
8. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。
> [!NOTE]
> 打开 `worker` 的 `url`,如果显示 `OK` 说明部署成功
@@ -35,22 +64,12 @@
![worker3](/ui_install/worker-3.png)
7. 点击 `Settings` -> `Variables`, 如图所示添加变量,参考 [修改 wrangler.toml 配置文件](/zh/guide/cli/worker.html#修改-wrangler-toml-配置文件) 中的 `[vars]` 部分
> [!NOTE]
> 注意字符串格式的变量的最外层的引号是不需要的
>
> - 对于 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`
![worker-var](/ui_install/worker-var.png)
8. 点击 `Settings` -> `Variables`, 下拉找到 `D1 Database`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Deploy`
![worker-d1](/ui_install/worker-d1.png)
9. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤,点击 `Workers & Pages` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace`,然后在 `Settings` -> `Variables`, 下拉找到 `KV`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 `KV` 缓存,点击 `Deploy`
> [!NOTE]
> [!NOTE] 重要
> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
>
> 注意此处 `KV` 的绑定名称必须为 `KV`
![worker-kv](/ui_install/worker-kv.png)
![worker-kv-bind](/ui_install/worker-kv-bind.png)
@@ -61,3 +80,8 @@
> 如果不需要 Telegram Bot, 可跳过此步骤
请先创建一个 Telegram Bot然后获取 `token`,然后执行下面的命令,将 `token` 添加到 `Variables` 中, Name: `TELEGRAM_BOT_TOKEN`
11. 如果你想要使用 admin 页面中的定时任务清理邮件,需要到 `Settings` -> `Triggers` -> `Cron Triggers` 中添加定时任务.
> [!NOTE]
> 选择 `cron` 表达式,输入 `0 0 * * *`(此表达式表示每天午夜运行),点击 `Add` 增加。请根据您的需求调整此表达式。

View File

@@ -0,0 +1,138 @@
# Worker 变量说明
> [!NOTE] 注意
> 通过 CLI 部署时的写法请参考 `worker/wrangler.toml.template`
## 必填变量
| 变量名 | 类型 | 说明 | 示例 |
| -------------------------- | ----------- | ------------------------------------------ | ------------------------------------ |
| `DOMAINS` | JSON | 用于临时邮箱的所有域名, 支持多个域名 | `["awsl.uk", "dreamhunter2333.xyz"]` |
| `JWT_SECRET` | 文本/Secret | 用于生成 jwt 的密钥, jwt 用于登录以及鉴权 | `xxx` |
| `ADMIN_PASSWORDS` | JSON | admin 控制台密码, 不配置则不允许访问控制台 | `["123", "456"]` |
| `ENABLE_USER_CREATE_EMAIL` | 文本/JSON | 是否允许用户创建邮箱, 不配置则不允许 | `true` |
| `ENABLE_USER_DELETE_EMAIL` | 文本/JSON | 是否允许用户删除邮件, 不配置则不允许 | `true` |
## 后台相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ------------------------------ | --------- | ------------------------------------ | ---------------- |
| `PASSWORDS` | JSON | 网站私有密码, 配置后需要密码才能访问 | `["123", "456"]` |
| `DISABLE_ADMIN_PASSWORD_CHECK` | 文本/JSON | 警告: 管理员控制台没有密码或用户检查 | `false` |
## 邮箱相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ---------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- |
| `PREFIX` | 文本 | 新建 `邮箱名称` 的默认前缀,不需要前缀可不配置 | `tmp` |
| `MIN_ADDRESS_LEN` | 数字 | `邮箱名称` 的最小长度 | `1` |
| `MAX_ADDRESS_LEN` | 数字 | `邮箱名称` 的最大长度 | `30` |
| `ADDRESS_CHECK_REGEX` | 文本 | `邮箱名称` 的正则表达式, 只用于检查 | `^(?!.*admin).*` |
| `ADDRESS_REGEX` | 文本 | `邮箱名称` 替换非法符号的正则表达式, 不在其中的符号将被替换,如果不设置,默认为 `[^a-z0-9]`, 需谨慎使用, 有些符号可能导致无法收件 | `[^a-z0-9]` |
| `DEFAULT_DOMAINS` | JSON | 默认用户可用的域名(未登录或未分配角色的用户) | `["awsl.uk", "dreamhunter2333.xyz"]` |
| `DOMAIN_LABELS` | JSON | 对于中文域名,可以使用 DOMAIN_LABELS 显示域名的中文展示名称 | `["中文.awsl.uk", "dreamhunter2333.xyz"]` |
| `ENABLE_AUTO_REPLY` | 文本/JSON | 允许自动回复邮件 | `true` |
| `DEFAULT_SEND_BALANCE` | 文本/JSON | 默认发送邮件余额,如果不设置,将为 0 | `1` |
## 接受邮件相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ------------------------------- | --------- | -------------------------------------------------------------------------- | -------------------------- |
| `BLACK_LIST` | 文本 | 黑名单,用于过滤发件人,逗号分隔 | `gov.cn,edu.cn` |
| `ENABLE_CHECK_JUNK_MAIL` | 文本/JSON | 是否启用垃圾邮件检查,配合下列两个列表使用 | `false` |
| `JUNK_MAIL_CHECK_LIST` | JSON | 垃圾邮件检查配置, 任何一项 `存在``不通过` 则被判定为垃圾邮件 | `["spf", "dkim", "dmarc"]` |
| `JUNK_MAIL_FORCE_PASS_LIST` | JSON | 垃圾邮件检查配置, 任何一项 `不存在` 或者 `不通过` 则被判定为垃圾邮件 | `["spf", "dkim", "dmarc"]` |
| `FORWARD_ADDRESS_LIST` | JSON | 全局转发地址列表,如果不配置则不启用,启用后所有邮件都会转发到列表中的地址 | `["xxx@xxx.com"]` |
| `REMOVE_EXCEED_SIZE_ATTACHMENT` | 文本/JSON | 如果附件大小超过 2MB则删除附件邮件可能由于解析而丢失一些信息 | `true` |
| `REMOVE_ALL_ATTACHMENT` | 文本/JSON | 移除所有附件,邮件可能由于解析而丢失一些信息 | `true` |
> [!NOTE]
> `垃圾邮件检查` 和 `移除附件功能` 需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时
>
> 如果你想解析邮件能力更强
>
> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
## webhook 相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ---------------- | --------- | ------------------------------------- | ------------------ |
| `ENABLE_WEBHOOK` | 文本/JSON | 是否启用 webhook | `true` |
| `FRONTEND_URL` | 文本 | 前端地址,用于发送 webhook 的邮件 url | `https://xxxx.xxx` |
> [!NOTE]
> webhook 功能需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时
>
> 如果你想解析邮件能力更强
>
> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
## 用户相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ------------------------------------- | --------- | ------------------------------------------------------------------------ | ------- |
| `USER_DEFAULT_ROLE` | 文本 | 新用户默认角色, 仅在启用邮件验证时有效 | `vip` |
| `ADMIN_USER_ROLE` | 文本 | admin 角色配置, 如果用户角色等于 ADMIN_USER_ROLE 则可以访问 admin 控制台 | `admin` |
| `USER_ROLES` | JSON | - | 见下方 |
| `DISABLE_ANONYMOUS_USER_CREATE_EMAIL` | 文本/JSON | 禁用匿名用户创建邮箱,如果设置为 true则用户只能在登录后创建邮箱地址 | `true` |
| `NO_LIMIT_SEND_ROLE` | 文本 | 可以无限发送邮件的角色 | `vip` |
> [!NOTE] USER_ROLES 用户角色配置说明
>
> - 如果 `domains` 为空将使用 `DEFAULT_DOMAINS`
> - 如果 prefix 为 null 将使用默认前缀, 如果 prefix 为空字符串将不使用前缀
>
> 通过用户界面部署时 `USER_ROLES` 请配置为此格式 `[{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"vip","prefix":"vip"},{"domains":["awsl.uk","dreamhunter2333.xyz"],"role":"admin","prefix":""}]`
>
> CLI 部署时 `USER_ROLES` 请参考 `worker/wrangler.toml.template` 配置为此格式 `[{ domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "vip", prefix = "vip" }, { domains = ["awsl.uk", "dreamhunter2333.xyz"], role = "admin", prefix = "" }]`
## 网页相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ------------------------- | ----------- | ------------------------------------------------ | --------------------- |
| `TITLE` | 文本 | 自定义前端页面网站标题,支持 html | `Custom Title` |
| `ANNOUNCEMENT` | 文本 | 自定义前端页面公告,支持 html | `Custom Announcement` |
| `COPYRIGHT` | 文本 | 自定义前端界面页脚文本,支持 html | `Dream Hunter` |
| `ADMIN_CONTACT` | 文本 | admin 联系方式,可配置任意字符串, 不配置则不显示 | `xxx@gmail.com` |
| `DISABLE_SHOW_GITHUB` | 文本/JSON | 是否显示 GitHub 链接 | `true` |
| `CF_TURNSTILE_SITE_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
| `CF_TURNSTILE_SECRET_KEY` | 文本/Secret | Turnstile 人机验证配置 | `xxx` |
## Telegram Bot 相关变量
| 变量名 | 类型 | 说明 | 示例 |
| ---------------- | ---- | ---------------------------------------------------------------------- | ---- |
| `TG_MAX_ADDRESS` | 数字 | telegram bot 最多绑定邮箱数量 | `5` |
| `TG_BOT_INFO` | 文本 | 可不配置telegram BOT_INFO预定义的 BOT_INFO 可以降低 webhook 的延迟 | `{}` |
> [!NOTE]
> Telegram 功能需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时
>
> 如果你想解析邮件能力更强
>
> 参考 [配置 worker 使用 wasm 解析邮件](/zh/guide/feature/mail_parser_wasm_worker)
## 其他变量
| 变量名 | 类型 | 说明 | 示例 |
| ----------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `ENABLE_ANOTHER_WORKER` | 文本/JSON | 是否开启其他 worker 处理邮件 | `false` |
| `ANOTHER_WORKER_LIST` | JSON | - 其他 worker 处理邮件的配置,可以配置多个其他 worker <br/> - 通过关键词筛选,调用对应绑定的 worker 的方法(默认方法名为 rpcEmail<br/> - keywords必填否则 worker 将不会被触发 | 见下方 |
> [!NOTE]
> `ANOTHER_WORKER_LIST` 的配置示例
>
> ```toml
> #ANOTHER_WORKER_LIST ="""
> #[
> # {
> # "binding":"AUTH_INBOX",
> # "method":"rpcEmail",
> # "keywords":[
> # "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
> # "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
> # ]
> # }
> #]
> #
> ```

View File

@@ -1,12 +1,12 @@
{
"name": "temp-mail-docs",
"private": true,
"version": "0.8.0",
"version": "0.8.6",
"type": "module",
"devDependencies": {
"@types/node": "^22.9.0",
"vitepress": "^1.5.0",
"wrangler": "^3.85.0"
"@types/node": "^22.10.7",
"vitepress": "^1.6.2",
"wrangler": "^3.104.0"
},
"scripts": {
"dev": "vitepress dev docs",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "cloudflare_temp_email",
"version": "0.8.0",
"version": "0.8.6",
"private": true,
"type": "module",
"scripts": {
@@ -11,22 +11,22 @@
"build": "wrangler deploy --dry-run --outdir dist --minify"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20241106.0",
"@eslint/js": "8.56.0",
"@simplewebauthn/types": "^10.0.0",
"eslint": "8.56.0",
"globals": "^15.12.0",
"typescript-eslint": "^7.18.0",
"wrangler": "^3.85.0"
"@cloudflare/workers-types": "^4.20250121.0",
"@eslint/js": "9.18.0",
"@simplewebauthn/types": "10.0.0",
"eslint": "9.18.0",
"globals": "^15.14.0",
"typescript-eslint": "^8.21.0",
"wrangler": "^3.104.0"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.687.0",
"@aws-sdk/s3-request-presigner": "^3.687.0",
"@simplewebauthn/server": "^10.0.1",
"hono": "^4.6.9",
"mimetext": "^3.0.24",
"postal-mime": "^2.3.2",
"resend": "^3.5.0",
"@aws-sdk/client-s3": "^3.732.0",
"@aws-sdk/s3-request-presigner": "^3.732.0",
"@simplewebauthn/server": "10.0.1",
"hono": "^4.6.17",
"mimetext": "^3.0.27",
"postal-mime": "^2.4.1",
"resend": "^4.1.1",
"telegraf": "4.16.3"
},
"pnpm": {

2597
worker/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,6 +11,7 @@ import webhook_settings from './webhook_settings'
import mail_webhook_settings from './mail_webhook_settings'
import oauth2_settings from './oauth2_settings'
import worker_config from './worker_config'
import { sendMailbyAdmin } from './send_mail'
export const api = new Hono<HonoCustomType>()
@@ -256,11 +257,13 @@ api.get('/admin/account_settings', async (c) => {
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY);
const fromBlockList = c.env.KV ? await c.env.KV.get<string[]>(CONSTANTS.EMAIL_KV_BLACK_LIST, 'json') : [];
const noLimitSendAddressList = await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY);
return c.json({
blockList: blockList || [],
sendBlockList: sendBlockList || [],
verifiedAddressList: verifiedAddressList || [],
fromBlockList: fromBlockList || []
fromBlockList: fromBlockList || [],
noLimitSendAddressList: noLimitSendAddressList || []
})
} catch (error) {
console.error(error);
@@ -270,7 +273,10 @@ api.get('/admin/account_settings', async (c) => {
api.post('/admin/account_settings', async (c) => {
/** @type {{ blockList: Array<string>, sendBlockList: Array<string> }} */
const { blockList, sendBlockList, verifiedAddressList, fromBlockList } = await c.req.json();
const {
blockList, sendBlockList, noLimitSendAddressList,
verifiedAddressList, fromBlockList
} = await c.req.json();
if (!blockList || !sendBlockList || !verifiedAddressList) {
return c.text("Invalid blockList or sendBlockList", 400)
}
@@ -295,6 +301,10 @@ api.post('/admin/account_settings', async (c) => {
if (fromBlockList) {
await c.env.KV.put(CONSTANTS.EMAIL_KV_BLACK_LIST, JSON.stringify(fromBlockList || []))
}
await saveSetting(
c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY,
JSON.stringify(noLimitSendAddressList || [])
)
return c.json({
success: true
})
@@ -330,3 +340,6 @@ api.post("/admin/mail_webhook/test", mail_webhook_settings.testWebhookSettings);
// worker config
api.get("/admin/worker/configs", worker_config.getConfig);
// send mail by admin
api.post("/admin/send_mail", sendMailbyAdmin);

View File

@@ -1,5 +1,5 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { HonoCustomType, ParsedEmailContext } from "../types";
import { CONSTANTS } from "../constants";
import { WebhookSettings } from "../models";
import { commonParseMail, sendWebhook } from "../common";
@@ -25,8 +25,8 @@ async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response
const { id: mailId, raw } = await c.env.DB.prepare(
`SELECT id, raw FROM raw_mails ORDER BY RANDOM() LIMIT 1`
).first<{ id: string, raw: string }>() || {};
const parsedEmail = await commonParseMail(raw);
const parsedEmailContext: ParsedEmailContext = { rawEmail: raw || "" };
const parsedEmail = await commonParseMail(parsedEmailContext);
const res = await sendWebhook(settings, {
id: mailId || "0",
url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "",

View File

@@ -0,0 +1,22 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { sendMail } from "../mails_api/send_mail_api";
export const sendMailbyAdmin = async (c: Context<HonoCustomType>) => {
const {
from_name, from_mail,
to_mail, to_name,
subject, content, is_html
} = await c.req.json();
await sendMail(c, from_mail, {
from_name: from_name,
to_name: to_name,
to_mail: to_mail,
subject: subject,
content: content,
is_html: is_html,
}, {
isAdmin: true
})
return c.json({ status: "ok" });
}

View File

@@ -1,7 +1,7 @@
import { Context } from 'hono';
import { HonoCustomType } from '../types';
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles } from '../utils';
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList } from '../utils';
import { CONSTANTS } from '../constants';
import { isS3Enabled } from '../mails_api/s3_attachment';
@@ -13,7 +13,7 @@ export default {
"HAS_ADMIN_PASSWORDS": getAdminPasswords(c).length,
"ANNOUNCEMENT": getStringValue(c.env.ANNOUNCEMENT),
"PREFIX": c.env.PREFIX,
"PREFIX": getStringValue(c.env.PREFIX),
"ADDRESS_CHECK_REGEX": getStringValue(c.env.ADDRESS_CHECK_REGEX),
"ADDRESS_REGEX": getStringValue(c.env.ADDRESS_REGEX),
"MIN_ADDRESS_LEN": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
@@ -33,6 +33,7 @@ export default {
"ADMIN_CONTACT": c.env.ADMIN_CONTACT,
"ENABLE_USER_CREATE_EMAIL": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
"DISABLE_ANONYMOUS_USER_CREATE_EMAIL": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
"ENABLE_USER_DELETE_EMAIL": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
"ENABLE_AUTO_REPLY": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
"COPYRIGHT": c.env.COPYRIGHT,
@@ -42,6 +43,14 @@ export default {
"DISABLE_SHOW_GITHUB": !getBooleanValue(c.env.DISABLE_SHOW_GITHUB),
"DISABLE_ADMIN_PASSWORD_CHECK": getBooleanValue(c.env.DISABLE_ADMIN_PASSWORD_CHECK),
"ENABLE_CHECK_JUNK_MAIL": getBooleanValue(c.env.ENABLE_CHECK_JUNK_MAIL),
"JUNK_MAIL_CHECK_LIST": getStringArray(c.env.JUNK_MAIL_CHECK_LIST),
"JUNK_MAIL_FORCE_PASS_LIST": getStringArray(c.env.JUNK_MAIL_FORCE_PASS_LIST),
"REMOVE_EXCEED_SIZE_ATTACHMENT": getBooleanValue(c.env.REMOVE_EXCEED_SIZE_ATTACHMENT),
"REMOVE_ALL_ATTACHMENT": getBooleanValue(c.env.REMOVE_ALL_ATTACHMENT),
"ENABLE_ANOTHER_WORKER": getBooleanValue(c.env.ENABLE_ANOTHER_WORKER),
"ANOTHER_WORKER_LIST": getAnotherWorkerList(c),
})
}
}

View File

@@ -18,7 +18,7 @@ api.get('/open_api/settings', async (c) => {
return c.json({
"title": c.env.TITLE,
"announcement": getStringValue(c.env.ANNOUNCEMENT),
"prefix": c.env.PREFIX,
"prefix": getStringValue(c.env.PREFIX),
"addressRegex": getStringValue(c.env.ADDRESS_REGEX),
"minAddressLen": getIntValue(c.env.MIN_ADDRESS_LEN, 1),
"maxAddressLen": getIntValue(c.env.MAX_ADDRESS_LEN, 30),
@@ -28,6 +28,7 @@ api.get('/open_api/settings', async (c) => {
"needAuth": needAuth,
"adminContact": c.env.ADMIN_CONTACT,
"enableUserCreateEmail": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
"disableAnonymousUserCreateEmail": getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL),
"enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
"enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
"enableIndexAbout": getBooleanValue(c.env.ENABLE_INDEX_ABOUT),

View File

@@ -1,8 +1,8 @@
import { Context } from 'hono';
import { Jwt } from 'hono/utils/jwt'
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting } from './utils';
import { HonoCustomType, UserRole } from './types';
import { getBooleanValue, getDomains, getStringValue, getIntValue, getUserRoles, getDefaultDomains, getJsonSetting, getAnotherWorkerList } from './utils';
import { HonoCustomType, UserRole, AnotherWorker, RPCEmailMessage, ParsedEmailContext } from './types';
import { unbindTelegramByAddress } from './telegram_api/common';
import { CONSTANTS } from './constants';
import { AdminWebhookSettings, WebhookMail, WebhookSettings } from './models';
@@ -256,40 +256,51 @@ export const handleListQuery = async (
}
export const commonParseMail = async (raw_mail: string | undefined | null): Promise<{
export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): Promise<{
sender: string,
subject: string,
text: string,
html: string,
headers?: Record<string, string>[]
} | undefined> => {
if (!raw_mail) {
// check parsed email context is valid
if (!parsedEmailContext || !parsedEmailContext.rawEmail) {
return undefined;
}
// return parsed email if already parsed
if (parsedEmailContext.parsedEmail) {
return parsedEmailContext.parsedEmail;
}
const raw_mail = parsedEmailContext.rawEmail;
// TODO: WASM parse email
// try {
// const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
// const parsedEmail = parse_message_wrapper(raw_mail);
// return {
// parsedEmailContext.parsedEmail = {
// sender: parsedEmail.sender || "",
// subject: parsedEmail.subject || "",
// text: parsedEmail.text || "",
// headers: parsedEmail.headers?.map(
// (header) => ({ key: header.key, value: header.value })
// ) || [],
// html: parsedEmail.body_html || "",
// };
// return parsedEmailContext.parsedEmail;
// } catch (e) {
// console.error("Failed use mail-parser-wasm-worker to parse email", e);
// }
try {
const { default: PostalMime } = await import('postal-mime');
const parsedEmail = await PostalMime.parse(raw_mail);
return {
parsedEmailContext.parsedEmail = {
sender: parsedEmail.from ? `${parsedEmail.from.name} <${parsedEmail.from.address}>` : "",
subject: parsedEmail.subject || "",
text: parsedEmail.text || "",
html: parsedEmail.html || "",
headers: parsedEmail.headers || [],
};
return parsedEmailContext.parsedEmail;
}
catch (e) {
console.error("Failed use PostalMime to parse email", e);
@@ -310,13 +321,13 @@ export const commonGetUserRole = async (
export const getAddressPrefix = async (c: Context<HonoCustomType>): Promise<string | undefined> => {
const user = c.get("userPayload");
if (!user) {
return c.env.PREFIX;
return getStringValue(c.env.PREFIX);
}
const user_role = await commonGetUserRole(c, user.user_id);
if (typeof user_role?.prefix === "string") {
return user_role.prefix;
}
return c.env.PREFIX;
return getStringValue(c.env.PREFIX);
}
export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<string[]> => {
@@ -357,7 +368,7 @@ export async function sendWebhook(settings: WebhookSettings, formatMap: WebhookM
export async function triggerWebhook(
c: Context<HonoCustomType>,
address: string,
raw_mail: string,
parsedEmailContext: ParsedEmailContext,
message_id: string | null
): Promise<void> {
if (!c.env.KV || !getBooleanValue(c.env.ENABLE_WEBHOOK)) {
@@ -390,14 +401,14 @@ export async function triggerWebhook(
`SELECT id FROM raw_mails where address = ? and message_id = ?`
).bind(address, message_id).first<string>("id");
const parsedEmail = await commonParseMail(raw_mail);
const parsedEmail = await commonParseMail(parsedEmailContext);
const webhookMail = {
id: mailId || "",
url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "",
from: parsedEmail?.sender || "",
to: address,
subject: parsedEmail?.subject || "",
raw: raw_mail,
raw: parsedEmailContext.rawEmail || "",
parsedText: parsedEmail?.text || "",
parsedHtml: parsedEmail?.html || ""
}
@@ -408,3 +419,54 @@ export async function triggerWebhook(
}
}
}
export async function triggerAnotherWorker(
c: Context<HonoCustomType>,
rpcEmailMessage: RPCEmailMessage,
parsedText: string | undefined | null
): Promise<void> {
if (!parsedText) {
return;
}
const anotherWorkerList: AnotherWorker[] = getAnotherWorkerList(c);
if (!getBooleanValue(c.env.ENABLE_ANOTHER_WORKER) || anotherWorkerList.length === 0) {
return;
}
const parsedTextLowercase: string = parsedText.toLowerCase();
for (const worker of anotherWorkerList) {
const keywords = worker?.keywords ?? [];
const bindingName = worker?.binding ?? "";
const methodName = worker.method ?? "rpcEmail";
const serviceBinding = (c.env as any)[bindingName] ?? {};
const method = serviceBinding[methodName];
if (!method || typeof method !== "function") {
console.log(`method = ${methodName} not found or not function`);
continue;
}
if (!keywords.some(keyword => keyword && parsedTextLowercase.includes(keyword.toLowerCase()))) {
console.log(`worker.binding = ${bindingName} not match keywords, parsedText = ${parsedText}`);
continue;
}
try {
const bodyObj = { ...rpcEmailMessage } as any;
if (bodyObj.headers && typeof bodyObj.headers.forEach === "function") {
const headerObj: any = {}
bodyObj.headers.forEach((value: string, key: string) => {
headerObj[key] = value;
});
bodyObj.headers = headerObj
}
const requestBody = JSON.stringify(bodyObj);
console.log(`exec worker , binding = ${bindingName} , requestBody = ${requestBody}`);
await method(requestBody);
} catch (e1) {
console.error(`execute method = ${methodName} error`, e1);
}
}
}

View File

@@ -1,5 +1,5 @@
export const CONSTANTS = {
VERSION: 'v0.8.0',
VERSION: 'v0.8.6',
// DB settings
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
@@ -8,6 +8,7 @@ export const CONSTANTS = {
USER_SETTINGS_KEY: 'user_settings',
OAUTH2_SETTINGS_KEY: 'oauth2_settings',
VERIFIED_ADDRESS_LIST_KEY: 'verified_address_list',
NO_LIMIT_SEND_ADDRESS_LIST_KEY: 'no_limit_send_address_list',
// KV
TG_KV_PREFIX: "temp-mail-telegram",

View File

@@ -0,0 +1,52 @@
import { Bindings, ParsedEmailContext } from "../types";
import { getBooleanValue } from "../utils";
import { commonParseMail } from "../common";
import { createMimeMessage } from "mimetext";
export const remove_attachment_if_need = async (
env: Bindings,
parsedEmailContext: ParsedEmailContext,
from_address: string,
to_address: string,
size: number
): Promise<void> => {
// if configured, remove all attachment
const removeAllAttachment = getBooleanValue(env.REMOVE_ALL_ATTACHMENT);
// if attachment size > 2MB, remove attachment
const removeExceedSizeAttachment = getBooleanValue(env.REMOVE_EXCEED_SIZE_ATTACHMENT) && size >= 2 * 1024 * 1024;
const shouldRemoveAttachment = removeAllAttachment || removeExceedSizeAttachment;
if (!shouldRemoveAttachment) return;
const parsedEmail = await commonParseMail(parsedEmailContext);
if (!parsedEmail) return;
const msg = createMimeMessage();
if (parsedEmail?.headers) {
for (const header of parsedEmail.headers) {
try {
msg.setHeader(header["key"], header["value"]);
} catch (error) {
// ignore
}
}
}
msg.setSender({
name: parsedEmail?.sender || from_address,
addr: from_address
});
msg.setRecipient(to_address);
msg.setSubject(parsedEmail?.subject || "Failed to parse email subject");
if (parsedEmail?.html) {
msg.addMessage({
contentType: 'text/html',
data: parsedEmail.html
});
}
if (parsedEmail?.text) {
msg.addMessage({
contentType: 'text/plain',
data: parsedEmail.text
});
}
parsedEmailContext.rawEmail = msg.asRaw();
}

View File

@@ -1,44 +1,64 @@
import { Bindings } from "../types";
import { getBooleanValue } from "../utils";
import { Bindings, ParsedEmailContext } from "../types";
import { getBooleanValue, getStringArray } from "../utils";
import { commonParseMail } from "../common";
export const check_if_junk_mail = async (
env: Bindings, address: string,
raw_mail: string, message_id: string | null
parsedEmailContext: ParsedEmailContext,
message_id: string | null
): Promise<boolean> => {
if (!getBooleanValue(env.ENABLE_CHECK_JUNK_MAIL)) {
return false;
}
const parsedEmail = await commonParseMail(raw_mail);
const parsedEmail = await commonParseMail(parsedEmailContext);
if (!parsedEmail?.headers) return false;
const checkListWhenExist = getStringArray(env.JUNK_MAIL_CHECK_LIST);
const forcePassList = getStringArray(env.JUNK_MAIL_FORCE_PASS_LIST);
const passedList: string[] = [];
const existList: string[] = [];
const headers = parsedEmail.headers;
for (const header of headers) {
if (!header["key"]) continue;
if (!header["value"]) continue;
// check spf
if (header["key"].toLowerCase() == "received-spf"
&&
!header["value"].toLowerCase().includes("pass")
) {
return true;
if (header["key"].toLowerCase() == "received-spf") {
existList.push("spf");
if (header["value"].toLowerCase().includes("pass")) {
passedList.push("spf");
}
}
// check dkim and dmarc
if (header["key"].toLowerCase() == "authentication-results") {
if (header["value"].toLowerCase().includes("dkim=")
&&
!header["value"].toLowerCase().includes("dkim=pass")
) {
return true;
if (header["value"].toLowerCase().includes("dkim=")) {
existList.push("dkim");
if (header["value"].toLowerCase().includes("dkim=pass")) {
passedList.push("dkim");
}
}
if (header["value"].toLowerCase().includes("dmarc=")
&&
!header["value"].toLowerCase().includes("dmarc=pass")
) {
return true;
if (header["value"].toLowerCase().includes("dmarc=")) {
existList.push("dmarc");
if (header["value"].toLowerCase().includes("dmarc=pass")) {
passedList.push("dmarc");
}
}
}
}
return false;
// check if all checkListWhenExist item passed when exist
if (checkListWhenExist?.some(
(checkName) => existList.includes(checkName.toLowerCase())
&& !passedList.includes(checkName.toLowerCase())
)) {
return true;
}
if (forcePassList?.length == 0) return false;
// check force pass list
return forcePassList.some(
(checkName) => !passedList.includes(checkName.toLowerCase())
);
}

View File

@@ -2,11 +2,12 @@ import { Context } from "hono";
import { getEnvStringList } from "../utils";
import { sendMailToTelegram } from "../telegram_api";
import { Bindings, HonoCustomType } from "../types";
import { Bindings, HonoCustomType, RPCEmailMessage, ParsedEmailContext } from "../types";
import { auto_reply } from "./auto_reply";
import { isBlocked } from "./black_list";
import { triggerWebhook } from "../common";
import { triggerWebhook, triggerAnotherWorker, commonParseMail } from "../common";
import { check_if_junk_mail } from "./check_junk";
import { remove_attachment_if_need } from "./check_attachment";
async function email(message: ForwardableEmailMessage, env: Bindings, ctx: ExecutionContext) {
@@ -16,29 +17,44 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
return;
}
const rawEmail = await new Response(message.raw).text();
const parsedEmailContext: ParsedEmailContext = {
rawEmail: rawEmail
};
// check if junk mail
try {
const is_junk = await check_if_junk_mail(env, message.to, rawEmail, message.headers.get("Message-ID"));
const is_junk = await check_if_junk_mail(env, message.to, parsedEmailContext, message.headers.get("Message-ID"));
if (is_junk) {
message.setReject("Junk mail");
console.log(`Junk mail from ${message.from} to ${message.to}`);
return;
}
} catch (error) {
console.log("check junk mail error", error);
console.error("check junk mail error", error);
}
// remove attachment if configured or size > 2MB
try {
await remove_attachment_if_need(env, parsedEmailContext, message.from, message.to, message.rawSize);
} catch (error) {
console.error("remove attachment error", error);
}
const message_id = message.headers.get("Message-ID");
// save email
const { success } = await env.DB.prepare(
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
).bind(
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}`);
try {
const { success } = await env.DB.prepare(
`INSERT INTO raw_mails (source, address, raw, message_id) VALUES (?, ?, ?, ?)`
).bind(
message.from, message.to, parsedEmailContext.rawEmail, message_id
).run();
if (!success) {
message.setReject(`Failed save message to ${message.to}`);
console.error(`Failed save message from ${message.from} to ${message.to}`);
}
}
catch (error) {
console.error("save email error", error);
}
// forward email
@@ -48,26 +64,41 @@ async function email(message: ForwardableEmailMessage, env: Bindings, ctx: Execu
await message.forward(forwardAddress);
}
} catch (error) {
console.log("forward email error", error);
console.error("forward email error", error);
}
// send email to telegram
try {
await sendMailToTelegram(
{ env: env } as Context<HonoCustomType>,
message.to, rawEmail, message_id);
message.to, parsedEmailContext, message_id);
} catch (error) {
console.log("send mail to telegram error", error);
console.error("send mail to telegram error", error);
}
// send webhook
try {
await triggerWebhook(
{ env: env } as Context<HonoCustomType>,
message.to, rawEmail, message_id
message.to, parsedEmailContext, message_id
);
} catch (error) {
console.log("send webhook error", error);
console.error("send webhook error", error);
}
// trigger another worker
try {
const parsedEmail = (await commonParseMail(parsedEmailContext));
const parsedText = parsedEmail?.text ?? ""
const rpcEmail: RPCEmailMessage = {
from: message.from,
to: message.to,
rawEmail: rawEmail,
headers: message.headers
}
await triggerAnotherWorker({ env: env } as Context<HonoCustomType>, rpcEmail, parsedText);
} catch (error) {
console.error("trigger another worker error", error);
}
// auto reply email

View File

@@ -103,6 +103,11 @@ api.get('/api/settings', async (c) => {
})
api.post('/api/new_address', async (c) => {
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
&& !c.get("userPayload")
) {
return c.text("New address for anonymous user is disabled", 403)
}
if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) {
return c.text("New address is disabled", 403)
}

View File

@@ -94,6 +94,9 @@ export const sendMail = async (
reqJson: {
from_name: string, to_mail: string, to_name: string,
subject: string, content: string, is_html: boolean
},
options?: {
isAdmin?: boolean
}
): Promise<void> => {
if (!address) {
@@ -107,7 +110,12 @@ export const sendMail = async (
}
const user_role = c.get("userRolePayload");
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
if (!is_no_limit_send_balance) {
// no need find noLimitSendAddressList if is_no_limit_send_balance
const noLimitSendAddressList = is_no_limit_send_balance ?
[] : await getJsonSetting(c, CONSTANTS.NO_LIMIT_SEND_ADDRESS_LIST_KEY) || [];
const isNoLimitSendAddress = noLimitSendAddressList?.includes(address);
const needCheckBalance = !is_no_limit_send_balance && !options?.isAdmin && !isNoLimitSendAddress;
if (needCheckBalance) {
// check permission
const balance = await c.env.DB.prepare(
`SELECT balance FROM address_sender
@@ -158,7 +166,7 @@ export const sendMail = async (
throw new Error("Please enable resend or verified address list")
}
// update balance
if (!sendByVerifiedAddressList && !is_no_limit_send_balance) {
if (!sendByVerifiedAddressList && needCheckBalance) {
try {
const { success } = await c.env.DB.prepare(
`UPDATE address_sender SET balance = balance - 1 where address = ?`

View File

@@ -1,5 +1,5 @@
import { Context } from "hono";
import { HonoCustomType } from "../types";
import { HonoCustomType, ParsedEmailContext } from "../types";
import { CONSTANTS } from "../constants";
import { AdminWebhookSettings, WebhookSettings } from "../models";
import { getBooleanValue } from "../utils";
@@ -39,8 +39,8 @@ async function testWebhookSettings(c: Context<HonoCustomType>): Promise<Response
const { id: mailId, raw } = await c.env.DB.prepare(
`SELECT id, raw FROM raw_mails WHERE address = ? ORDER BY RANDOM() LIMIT 1`
).bind(address).first<{ id: string, raw: string }>() || {};
const parsedEmail = await commonParseMail(raw);
const parsedEmailContext: ParsedEmailContext = { rawEmail: raw || "" };
const parsedEmail = await commonParseMail(parsedEmailContext);
const res = await sendWebhook(settings, {
id: mailId || "0",
url: c.env.FRONTEND_URL ? `${c.env.FRONTEND_URL}?mail_id=${mailId}` : "",

View File

@@ -42,9 +42,13 @@ export const tgUserNewAddress = async (
export const jwtListToAddressData = async (
c: Context<HonoCustomType>, jwtList: string[]
): Promise<{ addressList: string[], addressIdMap: Record<string, number> }> => {
): Promise<{
addressList: string[], addressIdMap: Record<string, number>,
invalidJwtList: string[]
}> => {
const addressList = [] as string[];
const addressIdMap = {} as Record<string, number>;
const invalidJwtList = [] as string[];
for (const jwt of jwtList) {
try {
const { address, address_id } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
@@ -52,10 +56,11 @@ export const jwtListToAddressData = async (
addressIdMap[address as string] = address_id as number;
} catch (e) {
addressList.push("无效凭证");
invalidJwtList.push(jwt);
console.log(`获取地址列表失败: ${(e as Error).message}`);
}
}
return { addressList, addressIdMap };
return { addressList, addressIdMap, invalidJwtList };
}
export const bindTelegramAddress = async (

View File

@@ -5,7 +5,7 @@ import { callbackQuery } from "telegraf/filters";
import { CONSTANTS } from "../constants";
import { getDomains, getJsonObjectValue, getStringValue } from '../utils';
import { HonoCustomType } from "../types";
import { HonoCustomType, ParsedEmailContext } from "../types";
import { TelegramSettings } from "./settings";
import { bindTelegramAddress, deleteTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress, unbindTelegramByAddress } from "./common";
import { commonParseMail } from "../common";
@@ -41,6 +41,10 @@ const COMMANDS = [
command: "mails",
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
},
{
command: "cleaninvalidadress",
description: "清理无效地址, 请输入 /cleaninvalidadress"
},
]
export function newTelegramBot(c: Context<HonoCustomType>, token: string): Telegraf {
@@ -180,6 +184,26 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
}
});
bot.command("cleaninvalidadress", async (ctx: TgContext) => {
const userId = ctx?.message?.from?.id;
if (!userId) {
return await ctx.reply("无法获取用户信息");
}
try {
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
const { invalidJwtList } = await jwtListToAddressData(c, jwtList);
const newJwtList = jwtList.filter(jwt => !invalidJwtList.includes(jwt));
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify(newJwtList));
const { addressList } = await jwtListToAddressData(c, newJwtList);
return await ctx.reply(`清理无效地址成功:\n\n`
+ `当前地址列表:\n\n`
+ addressList.map(a => `地址: ${a}`).join("\n")
);
} catch (e) {
return await ctx.reply(`清理无效地址失败: ${(e as Error).message}`);
}
});
const queryMail = async (ctx: TgContext, queryAddress: string, mailIndex: number, edit: boolean) => {
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
if (!userId) {
@@ -206,7 +230,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
).bind(
queryAddress, mailIndex
).first<{ raw: string, id: string, created_at: string }>() || {};
const { mail } = raw ? await parseMail(raw, queryAddress, created_at) : { mail: "已经没有邮件了" };
const { mail } = raw ? await parseMail({ rawEmail: raw }, queryAddress, created_at) : { mail: "已经没有邮件了" };
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
const miniAppButtons = []
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
@@ -271,14 +295,14 @@ export async function initTelegramBotCommands(bot: Telegraf) {
}
const parseMail = async (
raw_mail: string | undefined | null,
parsedEmailContext: ParsedEmailContext,
address: string, created_at: string | undefined | null
) => {
if (!raw_mail) {
if (!parsedEmailContext.rawEmail) {
return {};
}
try {
const parsedEmail = await commonParseMail(raw_mail);
const parsedEmail = await commonParseMail(parsedEmailContext);
let parsedText = parsedEmail?.text || "";
if (parsedText.length && parsedText.length > 1000) {
parsedText = parsedEmail?.text.substring(0, 1000) + "\n\n...\n消息过长请到miniapp查看";
@@ -302,19 +326,20 @@ const parseMail = async (
export async function sendMailToTelegram(
c: Context<HonoCustomType>, address: string,
raw_mail: string, message_id: string | null
parsedEmailContext: ParsedEmailContext,
message_id: string | null
) {
if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) {
return;
}
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
const { mail } = await parseMail(raw_mail, address, new Date().toUTCString());
const { mail } = await parseMail(parsedEmailContext, address, new Date().toUTCString());
if (!mail) {
return;
}
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
const golbalPush = settings?.enableGlobalMailPush && settings?.globalMailPushList;
if (!userId && !golbalPush) {
const globalPush = settings?.enableGlobalMailPush && settings?.globalMailPushList;
if (!userId && !globalPush) {
return;
}
const mailId = await c.env.DB.prepare(
@@ -328,7 +353,7 @@ export async function sendMailToTelegram(
url.searchParams.set("mail_id", mailId);
miniAppButtons.push(Markup.button.webApp("查看邮件", url.toString()));
}
if (golbalPush) {
if (globalPush) {
for (const pushId of settings.globalMailPushList) {
await bot.telegram.sendMessage(pushId, mail, {
...Markup.inlineKeyboard([

33
worker/src/types.d.ts vendored
View File

@@ -33,6 +33,7 @@ export type Bindings = {
ENABLE_AUTO_REPLY: string | boolean | undefined
ENABLE_WEBHOOK: string | boolean | undefined
ENABLE_USER_CREATE_EMAIL: string | boolean | undefined
DISABLE_ANONYMOUS_USER_CREATE_EMAIL: string | boolean | undefined
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
ENABLE_INDEX_ABOUT: string | boolean | undefined
DEFAULT_SEND_BALANCE: number | string | undefined
@@ -43,6 +44,14 @@ export type Bindings = {
FORWARD_ADDRESS_LIST: string | string[] | undefined
ENABLE_CHECK_JUNK_MAIL: string | boolean | undefined
JUNK_MAIL_CHECK_LIST: string | string[] | undefined
JUNK_MAIL_FORCE_PASS_LIST: string | string[] | undefined
ENABLE_ANOTHER_WORKER: string | boolean | undefined
ANOTHER_WORKER_LIST: string | AnotherWorker[] | undefined
REMOVE_ALL_ATTACHMENT: string | boolean | undefined
REMOVE_EXCEED_SIZE_ATTACHMENT: string | boolean | undefined
// s3 config
S3_ENDPOINT: string | undefined
@@ -90,3 +99,27 @@ type HonoCustomType = {
"Bindings": Bindings;
"Variables": Variables;
}
type AnotherWorker = {
binding: string | undefined | null,
method: string | undefined | null,
keywords: string[] | undefined | null
}
type RPCEmailMessage = {
from: string | undefined | null,
to: string | undefined | null,
rawEmail: string | undefined | null,
headers: object | undefined | null,
}
type ParsedEmailContext = {
rawEmail: string,
parsedEmail?: {
sender: string,
subject: string,
text: string,
html: string,
headers?: Record<string, string>[]
} | undefined
}

View File

@@ -94,8 +94,8 @@ export default {
const jwt = await Jwt.sign({
user_email: email,
user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
// 90 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET, "HS256")
return c.json({

View File

@@ -193,8 +193,8 @@ export default {
const jwt = await Jwt.sign({
user_email: user_email,
user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
// 90 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET, "HS256")
return c.json({

View File

@@ -165,8 +165,8 @@ export default {
const jwt = await Jwt.sign({
user_email: email,
user_id: user_id,
// 30 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
// 90 days expire in seconds
exp: Math.floor(Date.now() / 1000) + 90 * 24 * 60 * 60,
iat: Math.floor(Date.now() / 1000),
}, c.env.JWT_SECRET, "HS256")
return c.json({

View File

@@ -1,6 +1,6 @@
import { Context } from "hono";
import { createMimeMessage } from "mimetext";
import { HonoCustomType, UserRole } from "./types";
import { HonoCustomType, UserRole,AnotherWorker } from "./types";
export const getJsonObjectValue = <T = any>(
value: string | any
@@ -156,6 +156,22 @@ export const getUserRoles = (c: Context<HonoCustomType>): UserRole[] => {
return c.env.USER_ROLES;
}
export const getAnotherWorkerList = (c: Context<HonoCustomType>): AnotherWorker[] => {
if (!c.env.ANOTHER_WORKER_LIST) {
return [];
}
// check if ANOTHER_WORKER_LIST is an array, if not use json.parse
if (!Array.isArray(c.env.ANOTHER_WORKER_LIST)) {
try {
return JSON.parse(c.env.ANOTHER_WORKER_LIST);
} catch (e) {
console.error("Failed to parse ANOTHER_WORKER_LIST", e);
return [];
}
}
return c.env.ANOTHER_WORKER_LIST;
}
export const getPasswords = (c: Context<HonoCustomType>): string[] => {
if (!c.env.PASSWORDS) {
return [];

View File

@@ -12,7 +12,7 @@ import { api as telegramApi } from './telegram_api'
import { email } from './email';
import { scheduled } from './scheduled';
import { getAdminPasswords, getPasswords, getBooleanValue } from './utils';
import { getAdminPasswords, getPasswords, getBooleanValue, getStringArray } from './utils';
import { HonoCustomType, UserPayload } from './types';
const app = new Hono<HonoCustomType>()
@@ -210,14 +210,21 @@ app.route('/', adminApi)
app.route('/', apiSendMail)
app.route('/', telegramApi)
app.get('/', async c => {
if (!c.env.DB) { return c.text("DB is not available", 400); }
const health_check = async (c: Context<HonoCustomType>) => {
if (!c.env.DB) {
return c.text("DB is not available", 400);
}
if (!c.env.JWT_SECRET) {
return c.text("JWT_SECRET is not set", 400);
}
if (getStringArray(c.env.DOMAINS).length === 0) {
return c.text("DOMAINS is not set", 400);
}
return c.text("OK");
})
app.get('/health_check', async c => {
if (!c.env.DB) { return c.text("DB is not available", 400); }
return c.text("OK");
})
}
app.get('/', health_check)
app.get('/health_check', health_check)
app.all('/*', async c => c.text("Not Found", 404))

View File

@@ -49,6 +49,8 @@ JWT_SECRET = "xxx"
BLACK_LIST = ""
# Allow users to create email addresses
ENABLE_USER_CREATE_EMAIL = true
# Disable anonymous user create email, if set true, users can only create email addresses after logging in
# DISABLE_ANONYMOUS_USER_CREATE_EMAIL = true
# Allow users to delete messages
ENABLE_USER_DELETE_EMAIL = true
# Allow automatic replies to emails
@@ -74,6 +76,28 @@ ENABLE_AUTO_REPLY = false
# FRONTEND_URL = "https://xxxx.xxx"
# Enable check junk mail
# ENABLE_CHECK_JUNK_MAIL = false
# junk mail check list, if status exists and status is not pass, will be marked as junk mail
# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"]
# junk mail force check pass list, if no status or status is not pass, will be marked as junk mail
# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"]
# remove attachment if size exceed 2MB, mail maybe mising some information due to parsing
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
# remove all attachment, mail maybe mising some information due to parsing
# REMOVE_ALL_ATTACHMENT = true
# Calling other woker to process email
# ENABLE_ANOTHER_WORKER = false
# ANOTHER_WORKER_LIST = """
# [
# {
# "binding":"AUTH_INBOX",
# "method":"rpcEmail",
# "keywords":[
# "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
# "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
# ]
# }
# ]
# """
[[d1_databases]]
binding = "DB"
@@ -92,3 +116,8 @@ database_id = "xxx"
# namespace_id = "1001"
# # 10 requests per minute
# simple = { limit = 10, period = 60 }
# binding another worker service (parse the code or link), e.g. auth-inbox
# [[services]]
# binding = "AUTH_INBOX"
# service = "auth-inbox"