mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-11 18:10:01 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3987d364c | ||
|
|
3a542a8391 | ||
|
|
241e0b7b28 | ||
|
|
b43353ea47 | ||
|
|
6c334d32f6 | ||
|
|
7889d2edea | ||
|
|
2426e0b51a | ||
|
|
61434ab6f7 | ||
|
|
7f6a02ca38 | ||
|
|
6ae3b0d85e | ||
|
|
01e6cb1075 | ||
|
|
814f6fada2 | ||
|
|
31901aacc5 | ||
|
|
fb9b9f6ae4 | ||
|
|
095951ab45 | ||
|
|
37614ce6fa | ||
|
|
3f81fbee6d |
16
.github/workflows/tag_build.yml
vendored
16
.github/workflows/tag_build.yml
vendored
@@ -44,10 +44,24 @@ jobs:
|
||||
- name: Build Backend
|
||||
run: cd worker && pnpm install --no-frozen-lockfile && pnpm build
|
||||
|
||||
- name: Move worker.js
|
||||
run: cd worker/dist && mv worker.js ../
|
||||
|
||||
- name: Build Worker with wasm mail parser
|
||||
run: |
|
||||
cd worker
|
||||
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"
|
||||
pnpm build
|
||||
zip -r worker-with-wasm-mail-parser.zip dist/worker.js dist/*.wasm
|
||||
|
||||
- name: Upload to Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
frontend/frontend.zip
|
||||
frontend/telegram-frontend.zip
|
||||
worker/dist/worker.js
|
||||
worker/worker.js
|
||||
worker/worker-with-wasm-mail-parser.zip
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,7 +1,24 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main(v0.8.5)
|
||||
## main(v0.9.0)
|
||||
|
||||
- feat: | Worker | 支持多语言
|
||||
- feat: | Worker | `NO_LIMIT_SEND_ROLE` 配置支持多角色, 逗号分割
|
||||
- feat: | Actions | build 里增加 `worker-with-wasm-mail-parser.zip` 支持 UI 部署带 `wasm` 的 worker
|
||||
|
||||
## v0.8.7
|
||||
|
||||
- fix: |UI| 修复移动设备日期显示问题
|
||||
- feat: |Worker| 支持通过 `SMTP` 发送邮件, 使用 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
|
||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||
|
||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
|
||||
[](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/)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.8.5",
|
||||
"version": "0.9.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -20,33 +20,33 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "10.0.0",
|
||||
"@unhead/vue": "^1.11.15",
|
||||
"@vueuse/core": "^12.4.0",
|
||||
"@unhead/vue": "^1.11.19",
|
||||
"@vueuse/core": "^12.7.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.7.9",
|
||||
"jszip": "^3.10.1",
|
||||
"mail-parser-wasm": "^0.2.1",
|
||||
"naive-ui": "^2.41.0",
|
||||
"postal-mime": "^2.4.1",
|
||||
"postal-mime": "^2.4.3",
|
||||
"vooks": "^0.2.12",
|
||||
"vue": "^3.5.13",
|
||||
"vue-clipboard3": "^2.0.0",
|
||||
"vue-i18n": "^11.0.1",
|
||||
"vue-i18n": "^11.1.1",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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.7",
|
||||
"unplugin-auto-import": "^19.1.0",
|
||||
"unplugin-vue-components": "^28.4.0",
|
||||
"vite": "^6.1.1",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-top-level-await": "^1.4.4",
|
||||
"vite-plugin-top-level-await": "^1.5.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"workbox-build": "^7.3.0",
|
||||
"workbox-window": "^7.3.0",
|
||||
"wrangler": "^3.101.0"
|
||||
"wrangler": "^3.109.2"
|
||||
}
|
||||
}
|
||||
|
||||
3215
frontend/pnpm-lock.yaml
generated
3215
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useGlobalState } from '../store'
|
||||
import { h } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
import i18n from '../i18n'
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_BASE || "";
|
||||
const {
|
||||
loading, auth, jwt, settings, openSettings,
|
||||
@@ -21,6 +24,7 @@ const apiFetch = async (path, options = {}) => {
|
||||
method: options.method || 'GET',
|
||||
data: options.body || null,
|
||||
headers: {
|
||||
'x-lang': i18n.global.locale.value,
|
||||
'x-user-token': userJwt.value,
|
||||
'x-user-access-token': userSettings.value.access_token,
|
||||
'x-custom-auth': auth.value,
|
||||
@@ -31,14 +35,12 @@ const apiFetch = async (path, options = {}) => {
|
||||
});
|
||||
if (response.status === 401 && path.startsWith("/admin")) {
|
||||
showAdminAuth.value = true;
|
||||
throw new Error("Unauthorized, your admin password is wrong")
|
||||
}
|
||||
if (response.status === 401 && openSettings.value.auth) {
|
||||
showAuth.value = true;
|
||||
throw new Error("Unauthorized, you access password is wrong")
|
||||
}
|
||||
if (response.status >= 300) {
|
||||
throw new Error(`${response.status} ${response.data}` || "error");
|
||||
throw new Error(`[${response.status}]: ${response.data}` || "error");
|
||||
}
|
||||
const data = response.data;
|
||||
return data;
|
||||
@@ -52,7 +54,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"] || [];
|
||||
@@ -89,10 +91,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) {
|
||||
|
||||
15
frontend/src/i18n.ts
Normal file
15
frontend/src/i18n.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: 'zh', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
'en': {
|
||||
messages: {}
|
||||
},
|
||||
'zh': {
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n;
|
||||
@@ -1,20 +1,9 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import router from './router'
|
||||
import { createHead } from '@unhead/vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false, // you must set `false`, to use Composition API
|
||||
locale: 'zh', // set locale
|
||||
fallbackLocale: 'en', // set fallback locale
|
||||
'en': {
|
||||
messages: {}
|
||||
},
|
||||
'zh': {
|
||||
messages: {}
|
||||
}
|
||||
})
|
||||
import i18n from './i18n'
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.params.lang && ['en', 'zh'].includes(to.params.lang)) {
|
||||
|
||||
@@ -19,6 +19,9 @@ export const utcToLocalDate = (utcDate: string, useUTCDate: boolean) => {
|
||||
}
|
||||
try {
|
||||
const date = new Date(utcDateString);
|
||||
// if invalid date string
|
||||
if (isNaN(date.getTime())) return utcDateString;
|
||||
|
||||
return date.toLocaleString();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -34,6 +34,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
const notification = useNotification()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
@@ -195,7 +196,7 @@ const showNewAddressTab = computed(() => {
|
||||
|
||||
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 : "";
|
||||
});
|
||||
|
||||
@@ -24,6 +24,7 @@ export default defineConfig({
|
||||
{
|
||||
'naive-ui': [
|
||||
'useMessage',
|
||||
'useNotification',
|
||||
'NButton',
|
||||
'NPopconfirm',
|
||||
'NIcon',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "temp-email-pages",
|
||||
"version": "0.8.5",
|
||||
"version": "0.9.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -11,6 +11,6 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"wrangler": "^3.101.0"
|
||||
"wrangler": "^3.109.2"
|
||||
}
|
||||
}
|
||||
|
||||
15
scripts/update-dependencies.sh
Executable file
15
scripts/update-dependencies.sh
Executable 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 ..
|
||||
@@ -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' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -74,16 +74,17 @@ compatibility_flags = [ "nodejs_compat" ]
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# DEFAULT_LANG = "zh"
|
||||
# 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
|
||||
@@ -120,7 +121,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# DISABLE_SHOW_GITHUB = true # Disable Show GitHub link
|
||||
# default send balance, if not set, it will be 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# NO_LIMIT_SEND_ROLE = "vip" # the role which can send emails without limit
|
||||
# the role which can send emails without limit, multiple roles can be separated by ,
|
||||
# NO_LIMIT_SEND_ROLE = "vip"
|
||||
# Turnstile verification configuration
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
|
||||
@@ -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 发送邮件
|
||||
---
|
||||
|
||||
10
vitepress-docs/docs/zh/guide/actions/auto-update.md
Normal file
10
vitepress-docs/docs/zh/guide/actions/auto-update.md
Normal 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/)
|
||||
3
vitepress-docs/docs/zh/guide/actions/d1.md
Normal file
3
vitepress-docs/docs/zh/guide/actions/d1.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 初始化/更新 D1 数据库
|
||||
|
||||
参考 [命令行更新 d1](/zh/guide/cli/d1) 或者 [用户界面更新 d1](/zh/guide/ui/d1)
|
||||
@@ -1,16 +1,13 @@
|
||||
# 通过 Github Actions 部署
|
||||
|
||||
::: warning 注意
|
||||
目前只支持 worker 和 pages 的部署,D1 数据库以及 Email 部分请参考 [UI/CLI 部署](/)。
|
||||
目前只支持 worker 和 pages 的部署。
|
||||
有问题请通过 `Github Issues` 反馈,感谢。
|
||||
自动更新不会执行 sql 文件,需要手动执行。
|
||||
:::
|
||||
|
||||
[](https://deploy.workers.cloudflare.com/?url=https://github.com/dreamhunter2333/cloudflare_temp_email)
|
||||
|
||||
## 部署步骤
|
||||
|
||||
1. 点击按钮 fork 本仓库 或者直接 fork 本仓库
|
||||
1. 在 GitHub fork 本仓库
|
||||
|
||||
2. 打开仓库的 `Actions` 页面,找到 `Deploy Backend Production` 和 `Deploy Frontend`,点击 `enable workflow` 启用 `workflow`
|
||||
|
||||
@@ -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"
|
||||
@@ -43,94 +46,20 @@ compatibility_flags = [ "nodejs_compat" ]
|
||||
# ]
|
||||
|
||||
[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
|
||||
# 禁用匿名用户创建邮箱,如果设置为 true,则用户只能在登录后创建邮箱地址
|
||||
# DISABLE_ANONYMOUS_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
|
||||
# 垃圾邮件检查配置, 任何一项 存在 且 不通过 则被判定为垃圾邮件
|
||||
# JUNK_MAIL_CHECK_LIST = = ["spf", "dkim", "dmarc"]
|
||||
# 垃圾邮件检查配置, 任何一项 不存在 或者 不通过 则被判定为垃圾邮件
|
||||
# JUNK_MAIL_FORCE_PASS_LIST = ["spf", "dkim", "dmarc"]
|
||||
# 如果附件大小超过 2MB,则删除附件,邮件可能由于解析而丢失一些信息
|
||||
# REMOVE_EXCEED_SIZE_ATTACHMENT = true
|
||||
# 移除所有附件,邮件可能由于解析而丢失一些信息
|
||||
# REMOVE_ALL_ATTACHMENT = true
|
||||
# 是否开启其他 worker 处理邮件
|
||||
# ENABLE_ANOTHER_WORKER = false
|
||||
# 其他 worker 处理邮件的配置,可以配置多个其他 worker。
|
||||
# 通过关键词筛选,调用对应绑定的 worker 的方法(默认方法名为 rpcEmail)
|
||||
# keywords必填,否则 worker 将不会被触发
|
||||
#ANOTHER_WORKER_LIST ="""
|
||||
#[
|
||||
# {
|
||||
# "binding":"AUTH_INBOX",
|
||||
# "method":"rpcEmail",
|
||||
# "keywords":[
|
||||
# "验证码","激活码","激活链接","确认链接","验证邮箱","确认邮件","账号激活","邮件验证","账户确认","安全码","认证码","安全验证","登陆码","确认码","启用账户","激活账户","账号验证","注册确认",
|
||||
# "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
# ]
|
||||
# }
|
||||
#]
|
||||
#"""
|
||||
|
||||
# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看
|
||||
[[d1_databases]]
|
||||
|
||||
41
vitepress-docs/docs/zh/guide/common-issues.md
Normal file
41
vitepress-docs/docs/zh/guide/common-issues.md
Normal 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获取邮件失败:400:Bad 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 的 前端部署分支 是否相同 |
|
||||
@@ -1,9 +1,11 @@
|
||||
|
||||
# 配置发送邮件
|
||||
|
||||
## 使用 Cloudflare Workers 给已认证的邮箱发送邮件
|
||||
::: warning 注意
|
||||
三种方式可以同时配置,发送邮件时会优先使用 `resend`,如果没有配置 `resend`,则会使用 `smtp`.
|
||||
|
||||
admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)`
|
||||
如果配置了 Cloudflare 已认证的转发邮箱地址,会优先使用 cf 内部 API 发送邮件
|
||||
:::
|
||||
|
||||
## 使用 resend 发送邮件
|
||||
|
||||
@@ -30,3 +32,53 @@ wrangler secret put RESEND_TOKEN
|
||||
wrangler secret put RESEND_TOKEN_XXX_COM
|
||||
wrangler secret put RESEND_TOKEN_DREAMHUNTER2333_XYZ
|
||||
```
|
||||
|
||||
## 使用 SMTP 发送邮件
|
||||
|
||||
`SMTP_CONFIG` 的格式如下,key 为域名,value 为 SMTP 配置,SMTP 配置格式详情可以参考 [zou-yu/worker-mailer](https://github.com/zou-yu/worker-mailer/blob/main/README_zh-CN.md)
|
||||
|
||||
```json
|
||||
{
|
||||
"awsl.uk": {
|
||||
"host": "smtp.xxx.com",
|
||||
"port": 465,
|
||||
"secure": true,
|
||||
"authType": [
|
||||
"plain",
|
||||
"login"
|
||||
],
|
||||
"credentials": {
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后执行下面的命令,将 `SMTP_CONFIG` 添加到 secrets 中
|
||||
|
||||
> [!NOTE]
|
||||
> 如果你觉得麻烦,也可以直接明文放在 `wrangler.toml` 中 `[vars]` 下面,但是不推荐这样做
|
||||
|
||||
如果你是通过 UI 部署的,可以在 Cloudflare 的 UI 界面中添加到 `Variables and Secrets` 下面
|
||||
|
||||
```bash
|
||||
# 切换到 worker 目录
|
||||
cd worker
|
||||
wrangler secret put SMTP_CONFIG
|
||||
```
|
||||
|
||||
## 给 Cloudflare 上已认证的转发邮箱发送邮件
|
||||
|
||||
仅支持 CLI 部署时使用,在 `wrangler.toml` 中添加 `send_email` 配置
|
||||
|
||||
发送的目的邮箱地址必须是 Cloudflare 上已认证的邮箱地址,局限性较大,如果需要发送邮件给其他邮箱,可以使用 `resend` 或者 `smtp` 发送邮件
|
||||
|
||||
```toml
|
||||
# 通过 Cloudflare 发送邮件
|
||||
send_email = [
|
||||
{ name = "SEND_MAIL" },
|
||||
]
|
||||
```
|
||||
|
||||
admin 后台 账号配置 `已验证地址列表(可通过 cf 内部 api 发送邮件)`
|
||||
|
||||
@@ -3,7 +3,25 @@
|
||||
> [!NOTE]
|
||||
> 如果你使用了 webhook 转发,或者 telegram bot 接受邮件,但是邮件内容是乱码,或者无法解析,你对解析的需要更高的要求,可以使用这个功能。
|
||||
|
||||
## 修改代码
|
||||
## UI 部署
|
||||
|
||||
1. 下载 [worker-with-wasm-mail-parser.zip](https://github.com/dreamhunter2333/cloudflare_temp_email/releases/latest/download/worker-with-wasm-mail-parser.zip)
|
||||
|
||||
2. 回到 `Overview`,找到刚刚创建的 worker,点击 `Edit Code`, 删除原来的文件,上传 `worker.js` 和 `wasm` 后缀的文件, 点击 `Deploy`
|
||||
|
||||
> [!NOTE]
|
||||
> 上传需要先点击左侧菜单的 Explorer,
|
||||
> 在文件列表的窗口里点击鼠标右键,在右键菜单里找到 `Upload`,
|
||||
> 请参考下面的截图
|
||||
>
|
||||
> 参考: [issues156](https://github.com/dreamhunter2333/cloudflare_temp_email/issues/156#issuecomment-2079453822)
|
||||
|
||||

|
||||

|
||||
|
||||
## CLI 部署
|
||||
|
||||
### 修改代码
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
@@ -56,7 +74,7 @@ export const commonParseMail = async (raw_mail: string | undefined | null): Prom
|
||||
}
|
||||
```
|
||||
|
||||
## 部署
|
||||
### 部署
|
||||
|
||||
```bash
|
||||
cd worker
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
# 快速开始
|
||||
|
||||
- 良好的网络环境
|
||||
- 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)
|
||||
|
||||
@@ -22,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)
|
||||
|
||||
@@ -26,34 +26,36 @@
|
||||

|
||||

|
||||
|
||||
6. 点击 `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":""}]`
|
||||
6. 点击 `Settings` -> `Variables`, 如图所示添加变量
|
||||
|
||||

|
||||
|
||||
7. 以下是 `Settings` -> `Variables` 中必须配置的变量列表
|
||||
> [!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` | 你的域名, 支持多个域名 | `["awsl.uk", "dreamhunter2333.xyz"]` |
|
||||
| `ADMIN_PASSWORDS` | admin 控制台密码, 不配置则不允许访问控制台 | `["123", "456"]` |
|
||||
| `JWT_SECRET` | 用于生成 jwt 的密钥, jwt 用于登录以及鉴权 | `xxx` |
|
||||
| `ENABLE_USER_CREATE_EMAIL` | 是否允许用户创建邮箱, 不配置则不允许 | `true` |
|
||||
| `ENABLE_USER_DELETE_EMAIL` | 是否允许用户删除邮箱, 不配置则不允许 | `true` |
|
||||
建议配置的变量列表
|
||||
|
||||
8. 点击 `Settings` -> `Variables`, 下拉找到 `D1 Database`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 D1 数据库,点击 `Deploy`
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| -------------------------- | ----------- | ------------------------------------------ | ------------------------------------ |
|
||||
| `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`
|
||||
|
||||

|
||||
|
||||
9. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。
|
||||
8. 点击 `Settings` -> `Trggers`, 这里可以添加自己的域名,你也可以使用自动生成的 `*.workers.dev` 的域名。记录下这个域名,后面部署前端会用到。
|
||||
|
||||
> [!NOTE]
|
||||
> 打开 `worker` 的 `url`,如果显示 `OK` 说明部署成功
|
||||
@@ -62,7 +64,7 @@
|
||||
|
||||

|
||||
|
||||
10. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤,点击 `Workers & Pages` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace`,然后在 `Settings` -> `Variables`, 下拉找到 `KV`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 `KV` 缓存,点击 `Deploy`
|
||||
9. 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤,点击 `Workers & Pages` -> `KV` -> `Create Namespace`, 如图,点击 `Create Namespace`,然后在 `Settings` -> `Variables`, 下拉找到 `KV`, 点击 `Add Binding`, 名称如图,选择刚刚创建的 `KV` 缓存,点击 `Deploy`
|
||||
|
||||
> [!NOTE] 重要
|
||||
> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
|
||||
@@ -72,9 +74,14 @@
|
||||

|
||||

|
||||
|
||||
11. Telegram Bot 配置
|
||||
10. Telegram Bot 配置
|
||||
|
||||
> [!NOTE]
|
||||
> 如果不需要 Telegram Bot, 可跳过此步骤
|
||||
|
||||
请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 `Variables` 中, Name: `TELEGRAM_BOT_TOKEN`
|
||||
|
||||
11. 如果你想要使用 admin 页面中的定时任务清理邮件,需要到 `Settings` -> `Triggers` -> `Cron Triggers` 中添加定时任务.
|
||||
|
||||
> [!NOTE]
|
||||
> 选择 `cron` 表达式,输入 `0 0 * * *`(此表达式表示每天午夜运行),点击 `Add` 增加。请根据您的需求调整此表达式。
|
||||
|
||||
139
vitepress-docs/docs/zh/guide/worker-vars.md
Normal file
139
vitepress-docs/docs/zh/guide/worker-vars.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 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,admin` | `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 = "" }]`
|
||||
|
||||
## 网页相关变量
|
||||
|
||||
| 变量名 | 类型 | 说明 | 示例 |
|
||||
| ------------------------- | ----------- | ------------------------------------------------ | --------------------- |
|
||||
| `DEFAULT_LANG` | 文本 | Worker 错误信息默认语言, zh/en | `zh` |
|
||||
| `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"
|
||||
> # ]
|
||||
> # }
|
||||
> #]
|
||||
> #
|
||||
> ```
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "temp-mail-docs",
|
||||
"private": true,
|
||||
"version": "0.8.5",
|
||||
"version": "0.9.0",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"vitepress": "^1.5.0",
|
||||
"wrangler": "^3.101.0"
|
||||
"@types/node": "^22.13.4",
|
||||
"vitepress": "^1.6.3",
|
||||
"wrangler": "^3.109.2"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vitepress dev docs",
|
||||
|
||||
2506
vitepress-docs/pnpm-lock.yaml
generated
2506
vitepress-docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cloudflare_temp_email",
|
||||
"version": "0.8.5",
|
||||
"version": "0.9.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -11,23 +11,24 @@
|
||||
"build": "wrangler deploy --dry-run --outdir dist --minify"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250109.0",
|
||||
"@cloudflare/workers-types": "^4.20250214.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@simplewebauthn/types": "10.0.0",
|
||||
"eslint": "9.18.0",
|
||||
"globals": "^15.14.0",
|
||||
"typescript-eslint": "^8.19.1",
|
||||
"wrangler": "^3.101.0"
|
||||
"globals": "^15.15.0",
|
||||
"typescript-eslint": "^8.24.1",
|
||||
"wrangler": "^3.109.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.726.1",
|
||||
"@aws-sdk/s3-request-presigner": "^3.726.1",
|
||||
"@aws-sdk/client-s3": "^3.750.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.750.0",
|
||||
"@simplewebauthn/server": "10.0.1",
|
||||
"hono": "^4.6.16",
|
||||
"hono": "^4.7.2",
|
||||
"mimetext": "^3.0.27",
|
||||
"postal-mime": "^2.4.1",
|
||||
"resend": "^4.0.1",
|
||||
"telegraf": "4.16.3"
|
||||
"postal-mime": "^2.4.3",
|
||||
"resend": "^4.1.2",
|
||||
"telegraf": "4.16.3",
|
||||
"worker-mailer": "^1.0.1"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
|
||||
1510
worker/pnpm-lock.yaml
generated
1510
worker/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import i18n from '../i18n'
|
||||
import { HonoCustomType } from '../types'
|
||||
import { sendAdminInternalMail, getJsonSetting, saveSetting, getUserRoles } from '../utils'
|
||||
import { newAddress, handleListQuery } from '../common'
|
||||
@@ -40,6 +41,8 @@ api.get('/admin/address', async (c) => {
|
||||
|
||||
api.post('/admin/new_address', async (c) => {
|
||||
const { name, domain, enablePrefix } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!name) {
|
||||
return c.text("Please provide a name", 400)
|
||||
}
|
||||
@@ -53,7 +56,7 @@ api.post('/admin/new_address', async (c) => {
|
||||
});
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { Context } from 'hono';
|
||||
|
||||
import { HonoCustomType } from '../types';
|
||||
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList } from '../utils';
|
||||
import { getAdminPasswords, getBooleanValue, getDefaultDomains, getDomains, getIntValue, getPasswords, getStringArray, getStringValue, getUserRoles, getAnotherWorkerList, getSplitStringListValue } from '../utils';
|
||||
import { CONSTANTS } from '../constants';
|
||||
import { isS3Enabled } from '../mails_api/s3_attachment';
|
||||
|
||||
export default {
|
||||
getConfig: async (c: Context<HonoCustomType>) => {
|
||||
return c.json({
|
||||
"DEFAULT_LANG": c.env.DEFAULT_LANG,
|
||||
"TITLE": c.env.TITLE,
|
||||
"HAS_PASSWORD": getPasswords(c).length,
|
||||
"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),
|
||||
@@ -29,7 +30,7 @@ export default {
|
||||
"ADMIN_USER_ROLE": getStringValue(c.env.ADMIN_USER_ROLE),
|
||||
"USER_DEFAULT_ROLE": getStringValue(c.env.USER_DEFAULT_ROLE),
|
||||
"USER_ROLES": getUserRoles(c),
|
||||
"NO_LIMIT_SEND_ROLE": getStringValue(c.env.NO_LIMIT_SEND_ROLE),
|
||||
"NO_LIMIT_SEND_ROLE": getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE),
|
||||
|
||||
"ADMIN_CONTACT": c.env.ADMIN_CONTACT,
|
||||
"ENABLE_USER_CREATE_EMAIL": getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -321,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[]> => {
|
||||
@@ -339,26 +339,26 @@ export const getAllowDomains = async (c: Context<HonoCustomType>): Promise<strin
|
||||
return user_role?.domains || getDefaultDomains(c);;
|
||||
}
|
||||
|
||||
export async function sendWebhook(settings: WebhookSettings, formatMap: WebhookMail): Promise<{ success: boolean, message?: string }> {
|
||||
export async function sendWebhook(
|
||||
settings: WebhookSettings, formatMap: WebhookMail
|
||||
): Promise<{ success: boolean, message?: string }> {
|
||||
// send webhook
|
||||
let body = settings.body;
|
||||
for (const key of Object.keys(formatMap)) {
|
||||
/* eslint-disable no-useless-escape */
|
||||
body = body.replace(
|
||||
new RegExp(`\\$\\{${key}\\}`, "g"),
|
||||
JSON.stringify(
|
||||
formatMap[key as keyof WebhookMail]
|
||||
).replace(/^"(.*)"$/, '\$1')
|
||||
).replace(/^"(.*)"$/, '$1')
|
||||
);
|
||||
/* eslint-enable no-useless-escape */
|
||||
}
|
||||
console.log("send webhook", settings.url, settings.method, settings.headers, body);
|
||||
const response = await fetch(settings.url, {
|
||||
method: settings.method,
|
||||
headers: JSON.parse(settings.headers),
|
||||
body: body
|
||||
});
|
||||
if (!response.ok) {
|
||||
console.log("send webhook error", settings.url, settings.method, settings.headers, body);
|
||||
console.log("send webhook error", response.status, response.statusText);
|
||||
return { success: false, message: `send webhook error: ${response.status} ${response.statusText}` };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: 'v0.8.5',
|
||||
VERSION: 'v' + '0.9.0',
|
||||
|
||||
// DB settings
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
|
||||
43
worker/src/i18n/en.ts
Normal file
43
worker/src/i18n/en.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LocaleMessages } from "./type";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
CustomAuthPasswordMsg: "You have enabled the private site password, please provide the password",
|
||||
UserTokenExpiredMsg: "Your token has expired, please login again",
|
||||
UserAcceesTokenExpiredMsg: "Your access token has expired, please refresh the page",
|
||||
UserRoleIsNotAdminMsg: "Your user role is not admin, no access to visit this page",
|
||||
NeedAdminPasswordMsg: "You need to provide the admin password to access this page",
|
||||
|
||||
KVNotAvailableMsg: "KV is not available, please contact the administrator",
|
||||
DBNotAvailableMsg: "DB is not available, please contact the administrator",
|
||||
JWTSecretNotSetMsg: "JWT_SECRET is not set, please contact the administrator",
|
||||
WebhookNotEnabledMsg: "Webhook is not enabled, please contact the administrator",
|
||||
DomainsNotSetMsg: "Domains are not set, please contact the administrator",
|
||||
|
||||
TurnstileCheckFailedMsg: "Human verification check failed",
|
||||
NewAddressDisabledMsg: "New address is disabled, please contact the administrator",
|
||||
NewAddressAnonymousDisabledMsg: "New address for anonymous user is disabled, please contact the administrator",
|
||||
FailedCreateAddressMsg: "Failed to create address",
|
||||
InvalidAddressMsg: "Invalid address",
|
||||
InvalidAddressCredentialMsg: "Invalid address credential",
|
||||
UserDeleteEmailDisabledMsg: "User delete address/email is disabled, please contact the administrator",
|
||||
|
||||
UserNotFoundMsg: "User not found",
|
||||
UserAlreadyExistsMsg: "User already exists, please login",
|
||||
FailedToRegisterMsg: "Failed to register",
|
||||
UserRegistrationDisabledMsg: "User registration is disabled, please contact the administrator",
|
||||
UserMailDomainMustInMsg: "User mail domain must be in this list",
|
||||
InvalidVerifyCodeMsg: "Invalid verify code",
|
||||
InvalidEmailOrPasswordMsg: "Invalid email or password",
|
||||
VerifyMailSenderNotSetMsg: "Verify mail sender address is not set, please contact the administrator",
|
||||
CodeAlreadySentMsg: "Code already sent, please wait",
|
||||
InvalidUserDefaultRoleMsg: "Invalid user default role, please contact the administrator",
|
||||
FailedUpdateUserDefaultRoleMsg: "Failed to update user default role, please contact the administrator",
|
||||
|
||||
Oauth2ClientIDNotFoundMsg: "Oauth2 client ID is not set, please contact the administrator",
|
||||
Oauth2CliendIDOrCodeMissingMsg: "Oauth2 client ID or code is missing",
|
||||
Oauth2FailedGetUserInfoMsg: "Failed to get user info from Oauth2 provider",
|
||||
Oauth2FailedGetAccessTokenMsg: "Failed to get access token from Oauth2 provider",
|
||||
Oauth2FailedGetUserEmailMsg: "Failed to get user email from Oauth2 provider",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
16
worker/src/i18n/index.ts
Normal file
16
worker/src/i18n/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { LocaleMessages } from "./type";
|
||||
import zh from "./zh";
|
||||
import en from "./en";
|
||||
|
||||
export default {
|
||||
getMessages: (
|
||||
locale: string | null | undefined
|
||||
): LocaleMessages => {
|
||||
// multi-language support
|
||||
if (locale === "en") return en;
|
||||
if (locale === "zh") return zh;
|
||||
|
||||
// fallback language
|
||||
return en;
|
||||
}
|
||||
}
|
||||
39
worker/src/i18n/type.ts
Normal file
39
worker/src/i18n/type.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export type LocaleMessages = {
|
||||
CustomAuthPasswordMsg: string
|
||||
UserTokenExpiredMsg: string
|
||||
UserAcceesTokenExpiredMsg: string
|
||||
UserRoleIsNotAdminMsg: string
|
||||
NeedAdminPasswordMsg: string
|
||||
|
||||
KVNotAvailableMsg: string
|
||||
DBNotAvailableMsg: string
|
||||
JWTSecretNotSetMsg: string
|
||||
WebhookNotEnabledMsg: string
|
||||
DomainsNotSetMsg: string
|
||||
|
||||
TurnstileCheckFailedMsg: string
|
||||
NewAddressDisabledMsg: string
|
||||
NewAddressAnonymousDisabledMsg: string
|
||||
FailedCreateAddressMsg: string
|
||||
InvalidAddressMsg: string
|
||||
InvalidAddressCredentialMsg: string
|
||||
UserDeleteEmailDisabledMsg: string
|
||||
|
||||
UserNotFoundMsg: string
|
||||
UserAlreadyExistsMsg: string
|
||||
FailedToRegisterMsg: string
|
||||
UserRegistrationDisabledMsg: string
|
||||
UserMailDomainMustInMsg: string
|
||||
InvalidVerifyCodeMsg: string
|
||||
InvalidEmailOrPasswordMsg: string
|
||||
VerifyMailSenderNotSetMsg: string
|
||||
CodeAlreadySentMsg: string
|
||||
InvalidUserDefaultRoleMsg: string
|
||||
FailedUpdateUserDefaultRoleMsg: string
|
||||
|
||||
Oauth2ClientIDNotFoundMsg: string
|
||||
Oauth2CliendIDOrCodeMissingMsg: string
|
||||
Oauth2FailedGetUserInfoMsg: string
|
||||
Oauth2FailedGetAccessTokenMsg: string
|
||||
Oauth2FailedGetUserEmailMsg: string
|
||||
}
|
||||
43
worker/src/i18n/zh.ts
Normal file
43
worker/src/i18n/zh.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { LocaleMessages } from "./type";
|
||||
|
||||
const messages: LocaleMessages = {
|
||||
CustomAuthPasswordMsg: "你已启用私有站点密码,请提供密码",
|
||||
UserTokenExpiredMsg: "您的令牌已过期, 请重新登录",
|
||||
UserAcceesTokenExpiredMsg: "您的访问令牌已过期, 请刷新页面",
|
||||
UserRoleIsNotAdminMsg: "您的用户角色不是管理员, 无权访问",
|
||||
NeedAdminPasswordMsg: "您需要提供管理员密码才能访问此页面",
|
||||
|
||||
KVNotAvailableMsg: "KV 不可用, 请联系管理员",
|
||||
DBNotAvailableMsg: "DB 不可用, 请联系管理员",
|
||||
JWTSecretNotSetMsg: "JWT_SECRET 未设置, 请联系管理员",
|
||||
WebhookNotEnabledMsg: "Webhook 未启用, 请联系管理员",
|
||||
DomainsNotSetMsg: "域名列表未设置, 请联系管理员",
|
||||
|
||||
TurnstileCheckFailedMsg: "人机验证检查失败",
|
||||
NewAddressDisabledMsg: "新建邮箱地址已禁用, 请联系管理员",
|
||||
NewAddressAnonymousDisabledMsg: "匿名用户新建邮箱地址已禁用, 请联系管理员",
|
||||
FailedCreateAddressMsg: "创建邮箱地址失败",
|
||||
InvalidAddressMsg: "无效的邮箱地址",
|
||||
InvalidAddressCredentialMsg: "无效的邮箱地址凭据",
|
||||
UserDeleteEmailDisabledMsg: "用户删除邮箱/邮件已禁用, 请联系管理员",
|
||||
|
||||
UserNotFoundMsg: "用户不存在",
|
||||
UserAlreadyExistsMsg: "用户已存在, 请登录",
|
||||
FailedToRegisterMsg: "注册失败",
|
||||
UserRegistrationDisabledMsg: "用户注册已禁用, 请联系管理员",
|
||||
UserMailDomainMustInMsg: "用户邮箱域必须在此列表中",
|
||||
InvalidVerifyCodeMsg: "无效的验证码",
|
||||
InvalidEmailOrPasswordMsg: "无效的邮箱或密码",
|
||||
VerifyMailSenderNotSetMsg: "验证邮件发送邮箱未设置, 请联系管理员",
|
||||
CodeAlreadySentMsg: "验证码已发送, 请稍等",
|
||||
InvalidUserDefaultRoleMsg: "无效的用户默认角色, 请联系管理员",
|
||||
FailedUpdateUserDefaultRoleMsg: "更新用户默认角色失败, 请联系管理员",
|
||||
|
||||
Oauth2ClientIDNotFoundMsg: "Oauth2 客户端 ID 未设置, 请联系管理员",
|
||||
Oauth2CliendIDOrCodeMissingMsg: "Oauth2 客户端 ID 或 code 缺失",
|
||||
Oauth2FailedGetUserInfoMsg: "从 Oauth2 提供商获取用户信息失败",
|
||||
Oauth2FailedGetAccessTokenMsg: "从 Oauth2 提供商获取访问令牌失败",
|
||||
Oauth2FailedGetUserEmailMsg: "从 Oauth2 提供商获取用户邮箱失败",
|
||||
}
|
||||
|
||||
export default messages;
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue } from '../utils';
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile, getStringValue, getSplitStringListValue } from '../utils';
|
||||
import { newAddress, handleListQuery, deleteAddressWithData, getAddressPrefix, getAllowDomains } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import auto_reply from './auto_reply'
|
||||
@@ -42,8 +43,10 @@ api.get('/api/mail/:mail_id', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/mails/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
@@ -59,16 +62,18 @@ api.delete('/api/mails/:id', async (c) => {
|
||||
api.get('/api/settings', async (c) => {
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
const user_role = c.get("userRolePayload")
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (address_id && address_id > 0) {
|
||||
try {
|
||||
const db_address_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM address where id = ? `
|
||||
).bind(address_id).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
} catch (error) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
}
|
||||
// check address id
|
||||
@@ -78,11 +83,11 @@ api.get('/api/settings', async (c) => {
|
||||
`SELECT id FROM address where name = ? `
|
||||
).bind(address).first("id");
|
||||
if (!db_address_id) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return c.text("Invalid address", 400)
|
||||
return c.text(msgs.InvalidAddressMsg, 400)
|
||||
}
|
||||
// update address updated_at
|
||||
try {
|
||||
@@ -92,7 +97,8 @@ api.get('/api/settings', async (c) => {
|
||||
} catch (e) {
|
||||
console.warn("Failed to update address")
|
||||
}
|
||||
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||
const balance = is_no_limit_send_balance ? 99999 : await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender where address = ? and enabled = 1`
|
||||
).bind(address).first("balance");
|
||||
@@ -103,13 +109,15 @@ api.get('/api/settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/api/new_address', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (getBooleanValue(c.env.DISABLE_ANONYMOUS_USER_CREATE_EMAIL)
|
||||
&& !c.get("userPayload")
|
||||
) {
|
||||
return c.text("New address for anonymous user is disabled", 403)
|
||||
return c.text(msgs.NewAddressAnonymousDisabledMsg, 403)
|
||||
}
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) {
|
||||
return c.text("New address is disabled", 403)
|
||||
return c.text(msgs.NewAddressDisabledMsg, 403)
|
||||
}
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { name, domain, cf_token } = await c.req.json();
|
||||
@@ -117,7 +125,7 @@ api.post('/api/new_address', async (c) => {
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
return c.text(msgs.TurnstileCheckFailedMsg, 500)
|
||||
}
|
||||
// if no name, generate random name
|
||||
if (!name) {
|
||||
@@ -143,7 +151,7 @@ api.post('/api/new_address', async (c) => {
|
||||
});
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
return c.text(`${msgs.FailedCreateAddressMsg}: ${(e as Error).message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@ import { Context, Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { createMimeMessage } from 'mimetext';
|
||||
import { Resend } from 'resend';
|
||||
import { WorkerMailer, WorkerMailerOptions } from 'worker-mailer';
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue } from '../utils';
|
||||
import { getJsonSetting, getDomains, getIntValue, getBooleanValue, getStringValue, getJsonObjectValue, getSplitStringListValue } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
@@ -89,6 +91,32 @@ const sendMailByResend = async (
|
||||
console.log(`Resend success: ${JSON.stringify(data)}`);
|
||||
}
|
||||
|
||||
const sendMailBySmtp = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
from_name: string, to_mail: string, to_name: string,
|
||||
subject: string, content: string, is_html: boolean
|
||||
},
|
||||
smtpOptions: WorkerMailerOptions
|
||||
): Promise<void> => {
|
||||
await WorkerMailer.send(
|
||||
smtpOptions,
|
||||
{
|
||||
from: {
|
||||
name: reqJson.from_name,
|
||||
email: address
|
||||
},
|
||||
to: {
|
||||
name: reqJson.to_name,
|
||||
email: reqJson.to_mail
|
||||
},
|
||||
subject: reqJson.subject,
|
||||
text: reqJson.is_html ? undefined : reqJson.content,
|
||||
html: reqJson.is_html ? reqJson.content : undefined
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export const sendMail = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
@@ -109,7 +137,8 @@ export const sendMail = async (
|
||||
throw new Error("Invalid domain")
|
||||
}
|
||||
const user_role = c.get("userRolePayload");
|
||||
const is_no_limit_send_balance = user_role && user_role === getStringValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const no_limit_roles = getSplitStringListValue(c.env.NO_LIMIT_SEND_ROLE);
|
||||
const is_no_limit_send_balance = user_role && no_limit_roles.includes(user_role);
|
||||
// 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) || [];
|
||||
@@ -138,15 +167,20 @@ export const sendMail = async (
|
||||
throw new Error("to_mail address is blocked")
|
||||
}
|
||||
if (!subject) {
|
||||
throw new Error("Invalid subject")
|
||||
throw new Error("Subject is empty")
|
||||
}
|
||||
if (!content) {
|
||||
throw new Error("Invalid content")
|
||||
throw new Error("Content is empty")
|
||||
}
|
||||
|
||||
// send to verified address list, do not update balance
|
||||
const resendEnabled = c.env.RESEND_TOKEN || c.env[
|
||||
`RESEND_TOKEN_${mailDomain.replace(/\./g, "_").toUpperCase()}`
|
||||
];
|
||||
// send by smtp
|
||||
const smtpConfigMap = getJsonObjectValue<Record<string, WorkerMailerOptions>>(c.env.SMTP_CONFIG);
|
||||
const smtpConfig = smtpConfigMap ? smtpConfigMap[mailDomain] : null;
|
||||
// send by verified address list
|
||||
let sendByVerifiedAddressList = false;
|
||||
if (c.env.SEND_MAIL) {
|
||||
const verifiedAddressList = await getJsonSetting(c, CONSTANTS.VERIFIED_ADDRESS_LIST_KEY) || [];
|
||||
@@ -155,6 +189,8 @@ export const sendMail = async (
|
||||
sendByVerifiedAddressList = true;
|
||||
}
|
||||
}
|
||||
|
||||
// send mail workflow
|
||||
if (sendByVerifiedAddressList) {
|
||||
// do not update balance
|
||||
}
|
||||
@@ -162,9 +198,16 @@ export const sendMail = async (
|
||||
else if (resendEnabled) {
|
||||
await sendMailByResend(c, address, reqJson);
|
||||
}
|
||||
else {
|
||||
throw new Error("Please enable resend or verified address list")
|
||||
else if (smtpConfig) {
|
||||
await sendMailBySmtp(c, address, reqJson, smtpConfig);
|
||||
}
|
||||
else {
|
||||
if (c.env.SEND_MAIL) {
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}. Or add ${to_mail} to verified address list`);
|
||||
}
|
||||
throw new Error(`Please enable resend or smtp for domain ${mailDomain}`);
|
||||
}
|
||||
|
||||
// update balance
|
||||
if (!sendByVerifiedAddressList && needCheckBalance) {
|
||||
try {
|
||||
@@ -247,8 +290,10 @@ api.get('/api/sendbox', async (c) => {
|
||||
})
|
||||
|
||||
api.delete('/api/sendbox/:id', async (c) => {
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
return c.text(msgs.UserDeleteEmailDisabledMsg, 403)
|
||||
}
|
||||
const { address } = c.get("jwtPayload")
|
||||
const { id } = c.req.param();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { CONSTANTS } from "../constants";
|
||||
import { bindTelegramAddress, jwtListToAddressData, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||
import { checkCfTurnstile } from "../utils";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import i18n from "../i18n";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const TG_AUTH_TIMEOUT = 300;
|
||||
@@ -84,11 +85,13 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
return c.text(msgs.TurnstileCheckFailedMsg, 500)
|
||||
}
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
|
||||
7
worker/src/types.d.ts
vendored
7
worker/src/types.d.ts
vendored
@@ -12,6 +12,7 @@ export type Bindings = {
|
||||
SEND_MAIL: any
|
||||
|
||||
// config
|
||||
DEFAULT_LANG: string | undefined
|
||||
TITLE: string | undefined
|
||||
ANNOUNCEMENT: string | undefined | null
|
||||
PREFIX: string | undefined
|
||||
@@ -66,7 +67,10 @@ export type Bindings = {
|
||||
|
||||
// resend
|
||||
RESEND_TOKEN: string | undefined
|
||||
[key: `RESEND_TOKEN_${string}`]: string | undefined;
|
||||
[key: `RESEND_TOKEN_${string}`]: string | undefined
|
||||
|
||||
// SMTP config
|
||||
SMTP_CONFIG: string | object | undefined
|
||||
|
||||
// telegram config
|
||||
TELEGRAM_BOT_TOKEN: string
|
||||
@@ -93,6 +97,7 @@ type Variables = {
|
||||
userPayload: UserPayload,
|
||||
userRolePayload: string | undefined | null,
|
||||
jwtPayload: JwtPayload,
|
||||
lang: string | undefined | null
|
||||
}
|
||||
|
||||
type HonoCustomType = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { HonoCustomType } from '../types';
|
||||
import { getJsonSetting } from '../utils';
|
||||
import { UserOauth2Settings } from '../models';
|
||||
@@ -11,22 +12,26 @@ export default {
|
||||
getOauth2LoginUrl: async (c: Context<HonoCustomType>) => {
|
||||
const settings = await getJsonSetting<UserOauth2Settings[]>(c, CONSTANTS.OAUTH2_SETTINGS_KEY);
|
||||
const { clientID, state } = c.req.query();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
const setting = settings?.find(s => s.clientID === clientID);
|
||||
if (!setting) {
|
||||
return c.text("Client not found", 400);
|
||||
return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400);
|
||||
}
|
||||
const url = `${setting.authorizationURL}?client_id=${setting.clientID}&response_type=code&redirect_uri=${setting.redirectURL}&scope=${setting.scope}&state=${state}`
|
||||
return c.json({ url });
|
||||
},
|
||||
oauth2Login: async (c: Context<HonoCustomType>) => {
|
||||
const { clientID, code } = await c.req.json<{ clientID?: string, code?: string }>();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!clientID || !code) {
|
||||
return c.text("clientID or code is missing", 400);
|
||||
return c.text(msgs.Oauth2CliendIDOrCodeMissingMsg, 400);
|
||||
}
|
||||
const settings = await getJsonSetting<UserOauth2Settings[]>(c, CONSTANTS.OAUTH2_SETTINGS_KEY);
|
||||
const setting = settings?.find(s => s.clientID === clientID);
|
||||
if (!setting) {
|
||||
return c.text("Client not found", 400);
|
||||
return c.text(msgs.Oauth2ClientIDNotFoundMsg, 400);
|
||||
}
|
||||
const params = {
|
||||
code,
|
||||
@@ -48,7 +53,7 @@ export default {
|
||||
})
|
||||
if (!res.ok) {
|
||||
console.error(`Failed to get access token: ${res.status} ${res.statusText} ${await res.text()}`)
|
||||
return c.text("Failed to get access token", 400);
|
||||
return c.text(msgs.Oauth2FailedGetAccessTokenMsg, 400);
|
||||
}
|
||||
const resJson = await res.json();
|
||||
const { access_token, token_type } = resJson as { access_token: string, token_type?: string };
|
||||
@@ -61,17 +66,17 @@ export default {
|
||||
})
|
||||
if (!user.ok) {
|
||||
console.error(`Failed to get user info: ${res.status} ${res.statusText} ${await res.text()}`)
|
||||
return c.text("Failed to get user info", 400);
|
||||
return c.text(msgs.Oauth2FailedGetUserInfoMsg, 400);
|
||||
}
|
||||
const userInfo = await user.json()
|
||||
const { [setting.userEmailKey]: email } = userInfo as { [key: string]: string };
|
||||
if (!email) {
|
||||
return c.text("Failed to get user email", 400);
|
||||
return c.text(msgs.Oauth2FailedGetUserEmailMsg, 400);
|
||||
}
|
||||
// check email in mail allow list
|
||||
const mailDomain = email.split("@")[1];
|
||||
if (setting.enableMailAllowList && !setting.mailAllowList?.includes(mailDomain)) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(setting.mailAllowList, null, 2)}`, 400)
|
||||
return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(setting.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
// insert or update user
|
||||
const { success } = await c.env.DB.prepare(
|
||||
@@ -82,13 +87,13 @@ export default {
|
||||
email, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
return c.text(msgs.FailedToRegisterMsg, 500)
|
||||
}
|
||||
const { id: user_id } = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where user_email = ?`
|
||||
).bind(email).first() || {};
|
||||
if (!user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
import i18n from "../i18n";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { UserOauth2Settings, UserSettings } from "../models";
|
||||
import { getJsonSetting, getUserRoles } from "../utils"
|
||||
@@ -31,12 +32,14 @@ export default {
|
||||
},
|
||||
settings: async (c: Context<HonoCustomType>) => {
|
||||
const user = c.get("userPayload");
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
// check if user exists
|
||||
const db_user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where id = ?`
|
||||
).bind(user.user_id).first<number | undefined | null>("id");
|
||||
if (!db_user_id) {
|
||||
return c.text("User not found", 400);
|
||||
return c.text(msgs.UserNotFoundMsg, 400);
|
||||
}
|
||||
const user_role = await commonGetUserRole(c, db_user_id);
|
||||
const is_admin = (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import i18n from '../i18n';
|
||||
import { HonoCustomType } from '../types';
|
||||
import { checkCfTurnstile, getJsonSetting, checkUserPassword, getUserRoles, getStringValue } from "../utils"
|
||||
import { CONSTANTS } from "../constants";
|
||||
@@ -10,11 +11,13 @@ import { sendMail } from "../mails_api/send_mail_api";
|
||||
export default {
|
||||
verifyCode: async (c: Context<HonoCustomType>) => {
|
||||
const { email, cf_token } = await c.req.json();
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
return c.text(msgs.TurnstileCheckFailedMsg, 500)
|
||||
}
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
@@ -24,15 +27,15 @@ export default {
|
||||
&& settings.mailAllowList
|
||||
&& !settings.mailAllowList.includes(mailDomain)
|
||||
) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
if (!settings.verifyMailSender) {
|
||||
return c.text("Verify mail sender not set", 400)
|
||||
return c.text(msgs.VerifyMailSenderNotSetMsg, 400)
|
||||
}
|
||||
// check if code exists in KV
|
||||
const tmpcode = await c.env.KV.get(`temp-mail:${email}`)
|
||||
if (tmpcode) {
|
||||
return c.text("Code already sent, please wait", 400)
|
||||
return c.text(msgs.CodeAlreadySentMsg, 400)
|
||||
}
|
||||
// generate code 6 digits and convert to string
|
||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
||||
@@ -59,18 +62,20 @@ export default {
|
||||
register: async (c: Context<HonoCustomType>) => {
|
||||
const value = await getJsonSetting(c, CONSTANTS.USER_SETTINGS_KEY);
|
||||
const settings = new UserSettings(value)
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
// check enable
|
||||
if (!settings.enable) {
|
||||
return c.text("User registration is disabled");
|
||||
return c.text(msgs.UserRegistrationDisabledMsg, 403);
|
||||
}
|
||||
// check request
|
||||
const { email, password, code } = await c.req.json();
|
||||
if (!email || !password) {
|
||||
return c.text("Invalid email or password", 400)
|
||||
return c.text(msgs.InvalidEmailOrPasswordMsg, 400)
|
||||
}
|
||||
checkUserPassword(password);
|
||||
if (settings.enableMailVerify && !code) {
|
||||
return c.text("Need verify code", 400)
|
||||
return c.text(msgs.InvalidVerifyCodeMsg, 400)
|
||||
}
|
||||
// check mail domain allow list
|
||||
const mailDomain = email.split("@")[1];
|
||||
@@ -78,13 +83,13 @@ export default {
|
||||
&& settings.mailAllowList
|
||||
&& !settings.mailAllowList.includes(mailDomain)
|
||||
) {
|
||||
return c.text(`Mail domain must in ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
return c.text(`${msgs.UserMailDomainMustInMsg} ${JSON.stringify(settings.mailAllowList, null, 2)}`, 400)
|
||||
}
|
||||
// check code
|
||||
if (settings.enableMailVerify) {
|
||||
const verifyCode = await c.env.KV.get(`temp-mail:${email}`)
|
||||
if (verifyCode != code) {
|
||||
return c.text("Invalid verify code", 400)
|
||||
return c.text(msgs.InvalidVerifyCodeMsg, 400)
|
||||
}
|
||||
}
|
||||
// geo data
|
||||
@@ -101,14 +106,14 @@ export default {
|
||||
email, password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
return c.text(msgs.FailedToRegisterMsg, 500)
|
||||
}
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (error.message && error.message.includes("UNIQUE")) {
|
||||
return c.text("User already exists, please login", 400)
|
||||
return c.text(msgs.UserAlreadyExistsMsg, 400)
|
||||
}
|
||||
return c.text(`Failed to register: ${error.message}`, 500)
|
||||
return c.text(`${msgs.FailedToRegisterMsg}: ${error.message}`, 500)
|
||||
}
|
||||
return c.json({ success: true })
|
||||
}
|
||||
@@ -122,20 +127,20 @@ export default {
|
||||
password, JSON.stringify(userInfo)
|
||||
).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to register", 500)
|
||||
return c.text(msgs.FailedToRegisterMsg, 400);
|
||||
}
|
||||
const defaultRole = getStringValue(c.env.USER_DEFAULT_ROLE);
|
||||
if (!defaultRole) return c.json({ success: true })
|
||||
const user_roles = getUserRoles(c);
|
||||
if (!user_roles.find((r) => r.role === defaultRole)) {
|
||||
return c.text("Invalid role_text", 400)
|
||||
return c.text(msgs.InvalidUserDefaultRoleMsg, 500);
|
||||
}
|
||||
// find user_id
|
||||
const user_id = await c.env.DB.prepare(
|
||||
`SELECT id FROM users where user_email = ?`
|
||||
).bind(email).first<number | undefined | null>("id");
|
||||
if (!user_id) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 500);
|
||||
}
|
||||
// update user roles
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
@@ -144,22 +149,24 @@ export default {
|
||||
+ ` ON CONFLICT(user_id) DO NOTHING`
|
||||
).bind(user_id, defaultRole).run();
|
||||
if (!success2) {
|
||||
return c.text("Failed to update user roles", 500)
|
||||
return c.text(msgs.FailedUpdateUserDefaultRoleMsg, 500);
|
||||
}
|
||||
return c.json({ success: true })
|
||||
},
|
||||
login: async (c: Context<HonoCustomType>) => {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) return c.text("Invalid email or password", 400);
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!email || !password) return c.text(msgs.InvalidEmailOrPasswordMsg, 400);
|
||||
const { id: user_id, password: dbPassword } = await c.env.DB.prepare(
|
||||
`SELECT id, password FROM users where user_email = ?`
|
||||
).bind(email).first() || {};
|
||||
if (!dbPassword) {
|
||||
return c.text("User not found", 400)
|
||||
return c.text(msgs.UserNotFoundMsg, 400)
|
||||
}
|
||||
// TODO: need check password use random salt
|
||||
if (dbPassword != password) {
|
||||
return c.text("Invalid password", 400)
|
||||
return c.text(msgs.InvalidEmailOrPasswordMsg, 400)
|
||||
}
|
||||
// create jwt
|
||||
const jwt = await Jwt.sign({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Context } from "hono";
|
||||
import { createMimeMessage } from "mimetext";
|
||||
import { HonoCustomType, UserRole,AnotherWorker } from "./types";
|
||||
import { HonoCustomType, UserRole, AnotherWorker } from "./types";
|
||||
|
||||
export const getJsonObjectValue = <T = any>(
|
||||
value: string | any
|
||||
@@ -69,6 +69,15 @@ export const getStringValue = (value: any): string => {
|
||||
return "";
|
||||
}
|
||||
|
||||
export const getSplitStringListValue = (
|
||||
value: any, demiliter: string = ","
|
||||
): string[] => {
|
||||
const valueToSplit = getStringValue(value);
|
||||
return valueToSplit.split(demiliter)
|
||||
.map((item: string) => item.trim())
|
||||
.filter((item: string) => item.length > 0);
|
||||
}
|
||||
|
||||
export const getBooleanValue = (
|
||||
value: boolean | string | any
|
||||
): boolean => {
|
||||
|
||||
@@ -10,9 +10,10 @@ import { api as adminApi } from './admin_api';
|
||||
import { api as apiSendMail } from './mails_api/send_mail_api'
|
||||
import { api as telegramApi } from './telegram_api'
|
||||
|
||||
import i18n from './i18n';
|
||||
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>()
|
||||
@@ -25,6 +26,10 @@ app.onError((err, c) => {
|
||||
})
|
||||
// rate limit
|
||||
app.use('/*', async (c, next) => {
|
||||
// save language in context
|
||||
const lang = c.req.raw.headers.get("x-lang");
|
||||
if (lang) { c.set("lang", lang); }
|
||||
const msgs = i18n.getMessages(lang || c.env.DEFAULT_LANG);
|
||||
if (
|
||||
c.req.path.startsWith("/api/new_address")
|
||||
|| c.req.path.startsWith("/api/send_mail")
|
||||
@@ -49,17 +54,17 @@ app.use('/*', async (c, next) => {
|
||||
|| c.req.path.startsWith("/admin/mail_webhook")
|
||||
) {
|
||||
if (!c.env.KV) {
|
||||
return c.text("KV is not available", 400);
|
||||
return c.text(msgs.KVNotAvailableMsg, 400);
|
||||
}
|
||||
if (!getBooleanValue(c.env.ENABLE_WEBHOOK)) {
|
||||
return c.text("Webhook is disabled", 403);
|
||||
return c.text(msgs.WebhookNotEnabledMsg, 403);
|
||||
}
|
||||
}
|
||||
if (!c.env.DB) {
|
||||
return c.text("DB is not available", 400);
|
||||
return c.text(msgs.DBNotAvailableMsg, 400);
|
||||
}
|
||||
if (!c.env.JWT_SECRET) {
|
||||
return c.text("JWT_SECRET is not set", 400);
|
||||
return c.text(msgs.JWTSecretNotSetMsg, 400);
|
||||
}
|
||||
await next()
|
||||
});
|
||||
@@ -110,7 +115,9 @@ app.use('/api/*', async (c, next) => {
|
||||
if (passwords && passwords.length > 0) {
|
||||
const auth = c.req.raw.headers.get("x-custom-auth");
|
||||
if (!auth || !passwords.includes(auth)) {
|
||||
return c.text("Need Password", 401)
|
||||
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
|
||||
const messages = i18n.getMessages(lang);
|
||||
return c.text(messages.CustomAuthPasswordMsg, 401)
|
||||
}
|
||||
}
|
||||
if (c.req.path.startsWith("/api/new_address")) {
|
||||
@@ -123,7 +130,15 @@ app.use('/api/*', async (c, next) => {
|
||||
) {
|
||||
await checkoutUserRolePayload(c);
|
||||
}
|
||||
return jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
||||
|
||||
try {
|
||||
return await jwt({ secret: c.env.JWT_SECRET, alg: "HS256" })(c, next);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
const lang = c.get("lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
return c.text(msgs.InvalidAddressCredentialMsg, 401)
|
||||
}
|
||||
});
|
||||
// user_api auth
|
||||
app.use('/user_api/*', async (c, next) => {
|
||||
@@ -138,20 +153,24 @@ app.use('/user_api/*', async (c, next) => {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
|
||||
try {
|
||||
const token = c.req.raw.headers.get("x-user-token");
|
||||
if (!token) return c.text("Need User Token", 401)
|
||||
if (!token) return c.text(msgs.UserTokenExpiredMsg, 401)
|
||||
const payload = await Jwt.verify(token, c.env.JWT_SECRET, "HS256");
|
||||
// check expired
|
||||
if (!payload.exp) return c.text("Invalid Token", 401);
|
||||
if (!payload.exp) return c.text(msgs.UserTokenExpiredMsg, 401);
|
||||
// exp is in seconds
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return c.text("Token Expired", 401)
|
||||
return c.text(msgs.UserTokenExpiredMsg, 401)
|
||||
}
|
||||
c.set("userPayload", payload as UserPayload);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.text("Need User Token", 401)
|
||||
return c.text(msgs.UserTokenExpiredMsg, 401)
|
||||
}
|
||||
if (c.req.path.startsWith('/user_api/bind_address')
|
||||
&& c.req.method === 'POST'
|
||||
@@ -172,19 +191,21 @@ app.use('/admin/*', async (c, next) => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
// check if user is admin
|
||||
const access_token = c.req.raw.headers.get("x-user-access-token");
|
||||
if (c.env.ADMIN_USER_ROLE && access_token) {
|
||||
try {
|
||||
const payload = await Jwt.verify(access_token, c.env.JWT_SECRET, "HS256");
|
||||
// check expired
|
||||
if (!payload.exp) return c.text("Invalid Token", 401);
|
||||
if (!payload.exp) return c.text(msgs.UserAcceesTokenExpiredMsg, 401);
|
||||
// exp is in seconds
|
||||
if (payload.exp < Math.floor(Date.now() / 1000)) {
|
||||
return c.text("Token Expired", 401)
|
||||
return c.text(msgs.UserAcceesTokenExpiredMsg, 401)
|
||||
}
|
||||
if (payload.user_role !== c.env.ADMIN_USER_ROLE) {
|
||||
return c.text("Need Admin Role", 401)
|
||||
return c.text(msgs.UserRoleIsNotAdminMsg, 401)
|
||||
}
|
||||
await next();
|
||||
return;
|
||||
@@ -199,7 +220,7 @@ app.use('/admin/*', async (c, next) => {
|
||||
return;
|
||||
}
|
||||
|
||||
return c.text("Need Admin Password", 401)
|
||||
return c.text(msgs.NeedAdminPasswordMsg, 401)
|
||||
});
|
||||
|
||||
|
||||
@@ -210,14 +231,23 @@ 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>) => {
|
||||
const lang = c.req.raw.headers.get("x-lang") || c.env.DEFAULT_LANG;
|
||||
const msgs = i18n.getMessages(lang);
|
||||
if (!c.env.DB) {
|
||||
return c.text(msgs.DBNotAvailableMsg, 400);
|
||||
}
|
||||
if (!c.env.JWT_SECRET) {
|
||||
return c.text(msgs.JWTSecretNotSetMsg, 400);
|
||||
}
|
||||
if (getStringArray(c.env.DOMAINS).length === 0) {
|
||||
return c.text(msgs.DomainsNotSetMsg, 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))
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ compatibility_flags = [ "nodejs_compat" ]
|
||||
# ]
|
||||
|
||||
[vars]
|
||||
# DEFAULT_LANG = "zh"
|
||||
# TITLE = "Custom Title" # custom title
|
||||
# ANNOUNCEMENT = "Custom Announcement"
|
||||
PREFIX = "tmp"
|
||||
@@ -62,7 +63,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# DISABLE_SHOW_GITHUB = true
|
||||
# default send balance, if not set, it will be 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# NO_LIMIT_SEND_ROLE = "vip" # the role which can send emails without limit
|
||||
# the role which can send emails without limit, multiple roles can be separated by ,
|
||||
# NO_LIMIT_SEND_ROLE = "vip"
|
||||
# Turnstile verification
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
@@ -85,9 +87,9 @@ ENABLE_AUTO_REPLY = false
|
||||
# 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 ="""
|
||||
#[
|
||||
# ENABLE_ANOTHER_WORKER = false
|
||||
# ANOTHER_WORKER_LIST = """
|
||||
# [
|
||||
# {
|
||||
# "binding":"AUTH_INBOX",
|
||||
# "method":"rpcEmail",
|
||||
@@ -96,8 +98,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# "account","activation","verify","verification","activate","confirmation","email","code","validate","registration","login","code","expire","confirm"
|
||||
# ]
|
||||
# }
|
||||
#]
|
||||
#"""
|
||||
# ]
|
||||
# """
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
|
||||
Reference in New Issue
Block a user