mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-12 11:39:56 +08:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41bed8b1db | ||
|
|
869bf99340 | ||
|
|
f63c4ebd9c | ||
|
|
26969bebb8 | ||
|
|
1d191a091a | ||
|
|
4d6c4e2d10 | ||
|
|
7f456078ea | ||
|
|
68c18a6153 | ||
|
|
2d01639ecd | ||
|
|
53b7cfccde |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,8 +1,24 @@
|
||||
# CHANGE LOG
|
||||
|
||||
## main branch to be released
|
||||
## main branch
|
||||
|
||||
- 修复 Admin 删除邮件报错
|
||||
- UI: 回复邮件按钮, 引用原始邮件文本
|
||||
- 添加发送邮件地址黑名单
|
||||
- 添加 `CF Turnstile` 人机验证配置
|
||||
- 添加 `/external/api/send_mail` 发送邮件 api, 使用 body 验证
|
||||
|
||||
## v0.3.2
|
||||
|
||||
## What's Changed
|
||||
|
||||
- UI: 添加回复邮件按钮
|
||||
- 添加定时清理功能,可在 admin 页面配置(需要在配置文件启用定时任务)
|
||||
- 修复删除账户无反应的问题
|
||||
|
||||
* feat: UI: MailBox add reply button by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/187
|
||||
* feat: add cron auto clean up by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/189
|
||||
* fix: delete account by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/190
|
||||
|
||||
## v0.3.1
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 使用 cloudflare 免费服务,搭建临时邮箱
|
||||
|
||||
> 本项目仅供学习和个人用途,请勿将其用于任何违法行为,否则后果自负。
|
||||
|
||||
## [查看部署文档](https://temp-mail-docs.awsl.uk)
|
||||
|
||||
## [English Docs](https://temp-mail-docs.awsl.uk/en/)
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<link rel="icon" href="/logo.png" sizes="any">
|
||||
<link rel="apple-touch-icon" href="/logo.png">
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
@@ -63,6 +63,7 @@ const getOpenSettings = async (message) => {
|
||||
enableUserDeleteEmail: res["enableUserDeleteEmail"] || false,
|
||||
enableAutoReply: res["enableAutoReply"] || false,
|
||||
copyright: res["copyright"] || openSettings.value.copyright,
|
||||
cfTurnstileSiteKey: res["cfTurnstileSiteKey"] || "",
|
||||
});
|
||||
if (openSettings.value.needAuth) {
|
||||
showAuth.value = true;
|
||||
|
||||
@@ -169,9 +169,9 @@ const replyMail = async () => {
|
||||
Object.assign(sendMailModel.value, {
|
||||
toName: toName,
|
||||
toMail: toMail,
|
||||
subject: localeCache.value == 'zh' ? `回复: ${curMail.value.subject}` : `Re: ${curMail.value.subject}`,
|
||||
contentType: 'text',
|
||||
content: "",
|
||||
subject: `${t('reply')}: ${curMail.value.subject}`,
|
||||
contentType: 'rich',
|
||||
content: curMail.value.text ? `<p><br></p><blockquote>${curMail.value.text}</blockquote><p><br></p>` : '',
|
||||
});
|
||||
await router.push('/send');
|
||||
};
|
||||
|
||||
88
frontend/src/components/Turnstile.vue
Normal file
88
frontend/src/components/Turnstile.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<script setup>
|
||||
import { ref, watch, defineModel, onMounted } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../store'
|
||||
const { localeCache, openSettings, isDark } = useGlobalState()
|
||||
|
||||
const cfToken = defineModel('value')
|
||||
|
||||
const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
refresh: 'Refresh'
|
||||
},
|
||||
zh: {
|
||||
refresh: '刷新'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const cfTurnstileId = ref("")
|
||||
const turnstileLoading = ref(false)
|
||||
|
||||
const checkCfTurnstile = async (remove) => {
|
||||
if (!openSettings.value.cfTurnstileSiteKey) return;
|
||||
turnstileLoading.value = true;
|
||||
try {
|
||||
let container = document.getElementById("cf-turnstile");
|
||||
let count = 100;
|
||||
while (!container && count-- > 0) {
|
||||
container = document.getElementById("cf-turnstile");
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
}
|
||||
count = 100;
|
||||
while (!window.turnstile && count-- > 0) {
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
}
|
||||
if (remove && cfTurnstileId.value) {
|
||||
window.turnstile.remove(cfTurnstileId.value);
|
||||
}
|
||||
cfTurnstileId.value = window.turnstile.render(
|
||||
"#cf-turnstile",
|
||||
{
|
||||
sitekey: openSettings.value.cfTurnstileSiteKey,
|
||||
language: localeCache.value == 'zh' ? 'zh-CN' : 'en-US',
|
||||
theme: isDark.value ? 'dark' : 'light',
|
||||
callback: function (token) {
|
||||
cfToken.value = token;
|
||||
},
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
turnstileLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(isDark, async (isDark) => {
|
||||
checkCfTurnstile(true)
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
cfToken.value = "";
|
||||
checkCfTurnstile(true);
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="openSettings.cfTurnstileSiteKey" class="center">
|
||||
<n-spin description="loading..." :show="turnstileLoading">
|
||||
<n-form-item-row>
|
||||
<div id="cf-turnstile"></div>
|
||||
<n-button text @click="checkCfTurnstile(true)">
|
||||
{{ t('refresh') }}
|
||||
</n-button>
|
||||
</n-form-item-row>
|
||||
</n-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.n-button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ref } from "vue";
|
||||
import { createGlobalState, useStorage } from '@vueuse/core'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { createGlobalState, useStorage, useDark, useToggle } from '@vueuse/core'
|
||||
|
||||
export const useGlobalState = createGlobalState(
|
||||
() => {
|
||||
@@ -16,6 +15,7 @@ export const useGlobalState = createGlobalState(
|
||||
enableAutoReply: false,
|
||||
domains: [],
|
||||
copyright: 'Dream Hunter',
|
||||
cfTurnstileSiteKey: '',
|
||||
})
|
||||
const settings = ref({
|
||||
fetched: false,
|
||||
|
||||
@@ -14,6 +14,7 @@ export async function processItem(item) {
|
||||
item.source = parsedEmail.sender || item.source;
|
||||
item.subject = parsedEmail.subject || '';
|
||||
item.message = parsedEmail.body_html || parsedEmail.text || '';
|
||||
item.text = parsedEmail.text || '';
|
||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||
const blob_url = URL.createObjectURL(
|
||||
new Blob(
|
||||
@@ -46,6 +47,7 @@ export async function processItem(item) {
|
||||
}
|
||||
item.subject = parsedEmail.subject || 'No Subject';
|
||||
item.message = parsedEmail.html || parsedEmail.text || item.raw;
|
||||
item.text = parsedEmail.text || '';
|
||||
item.attachments = parsedEmail.attachments?.map((a_item) => {
|
||||
const blob_url = URL.createObjectURL(
|
||||
new Blob(
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminContact from './admin/AdminContact.vue'
|
||||
import Turnstile from '../components/Turnstile.vue'
|
||||
|
||||
import { useGlobalState } from '../store'
|
||||
import { api } from '../api'
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
jwt, localeCache, loading, openSettings, showPassword
|
||||
@@ -15,6 +18,7 @@ const tabValue = ref('signin')
|
||||
const password = ref('')
|
||||
const emailName = ref("")
|
||||
const emailDomain = ref("")
|
||||
const cfToken = ref("")
|
||||
|
||||
const login = async () => {
|
||||
if (!password.value) {
|
||||
@@ -81,11 +85,14 @@ const generateName = async () => {
|
||||
|
||||
const newEmail = async () => {
|
||||
try {
|
||||
const res = await api.fetch(
|
||||
`/api/new_address`
|
||||
+ `?name=${emailName.value || ''}`
|
||||
+ `&domain=${emailDomain.value || ''}`
|
||||
);
|
||||
const res = await api.fetch(`/api/new_address`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: emailName.value,
|
||||
domain: emailDomain.value,
|
||||
cf_token: cfToken.value,
|
||||
}),
|
||||
});
|
||||
jwt.value = res["jwt"];
|
||||
await api.getSettings();
|
||||
showPassword.value = true;
|
||||
@@ -95,7 +102,7 @@ const newEmail = async () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0].value : "";
|
||||
emailDomain.value = openSettings.value.domains ? openSettings.value.domains[0]?.value : "";
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -136,6 +143,7 @@ onMounted(async () => {
|
||||
<n-select v-model:value="emailDomain" :consistent-menu-width="false"
|
||||
:options="openSettings.domains" />
|
||||
</n-input-group>
|
||||
<Turnstile v-model:value="cfToken" />
|
||||
<n-button type="primary" block secondary strong @click="newEmail" :loading="loading">
|
||||
{{ t('ok') }}
|
||||
</n-button>
|
||||
|
||||
@@ -88,6 +88,7 @@ const deleteEmail = async () => {
|
||||
await fetchData()
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
showDelteAccount.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,22 +16,26 @@ const { t } = useI18n({
|
||||
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',
|
||||
},
|
||||
zh: {
|
||||
save: '保存',
|
||||
successTip: '保存成功',
|
||||
address_block_list: '用户地址屏蔽关键词(管理员可跳过检查)',
|
||||
address_block_list: '邮件地址屏蔽关键词(管理员可跳过检查)',
|
||||
address_block_list_placeholder: '请输入您想要屏蔽的关键词',
|
||||
send_address_block_list: '发送邮件地址屏蔽关键词',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const addressBlockList = ref([])
|
||||
const sendAddressBlockList = ref([])
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/admin/account_settings`)
|
||||
addressBlockList.value = res.blockList || []
|
||||
sendAddressBlockList.value = res.sendBlockList || []
|
||||
} catch (error) {
|
||||
message.error(error.message || "error");
|
||||
}
|
||||
@@ -42,7 +46,8 @@ const save = async () => {
|
||||
await api.fetch(`/admin/account_settings`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
blockList: addressBlockList.value || []
|
||||
blockList: addressBlockList.value || [],
|
||||
sendBlockList: sendAddressBlockList.value || []
|
||||
})
|
||||
})
|
||||
message.success(t('successTip'))
|
||||
@@ -64,6 +69,10 @@ onMounted(async () => {
|
||||
<n-select v-model:value="addressBlockList" filterable multiple tag
|
||||
:placeholder="t('address_block_list_placeholder')" />
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('send_address_block_list')">
|
||||
<n-select v-model:value="sendAddressBlockList" filterable multiple tag
|
||||
:placeholder="t('address_block_list_placeholder')" />
|
||||
</n-form-item-row>
|
||||
<n-button @click="save" type="primary" block :loading="loading">
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
|
||||
@@ -37,7 +37,7 @@ const { t } = useI18n({
|
||||
tip: '请输入清理天数',
|
||||
mailBoxLabel: '收件箱清理天数',
|
||||
mailUnknowLabel: "无收件人邮件清理天数",
|
||||
addressUnActiveLabel: "未激活地址清理天数",
|
||||
addressUnActiveLabel: "未活跃地址清理天数",
|
||||
sendBoxLabel: "发件箱清理天数",
|
||||
autoCleanup: "自动清理",
|
||||
cleanupSuccess: "清理成功",
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const { localeCache, settings, adminAuth, adminSendBoxTabAddress } = useGlobalState()
|
||||
const { localeCache, adminAuth, adminSendBoxTabAddress } = useGlobalState()
|
||||
const message = useMessage()
|
||||
|
||||
const { t } = useI18n({
|
||||
@@ -129,9 +129,9 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="settings.address">
|
||||
<n-modal v-model:show="showModal" preset="dialog">
|
||||
<pre>{{ curRow.raw }}</pre>
|
||||
<div>
|
||||
<n-modal v-model:show="showModal" preset="dialog" style="width: 100%;">
|
||||
<pre style="overflow: auto;">{{ curRow.raw }}</pre>
|
||||
</n-modal>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="adminSendBoxTabAddress" />
|
||||
|
||||
@@ -14,14 +14,14 @@ const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
userCount: 'User Count',
|
||||
activeUser: '7 days Active User',
|
||||
userCount: 'Account Count',
|
||||
activeUser: '7 days Active Mail Account',
|
||||
mailCount: 'Mail Count',
|
||||
sendMailCount: 'Send Mail Count'
|
||||
},
|
||||
zh: {
|
||||
userCount: '用户总数',
|
||||
activeUser: '周活跃用户',
|
||||
userCount: '地址总数',
|
||||
activeUser: '周活跃邮箱地址',
|
||||
mailCount: '邮件总数',
|
||||
sendMailCount: '发送邮件总数'
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div v-if="settings.address">
|
||||
<n-modal v-model:show="showModal" preset="dialog">
|
||||
<pre>{{ curRow.raw }}</pre>
|
||||
<n-modal v-model:show="showModal" preset="dialog" style="width: 100%;">
|
||||
<pre style="overflow: auto;">{{ curRow.raw }}</pre>
|
||||
</n-modal>
|
||||
<div style="display: inline-block;">
|
||||
<n-pagination v-model:page="page" v-model:page-size="pageSize" :item-count="count"
|
||||
|
||||
@@ -91,7 +91,7 @@ const send = async () => {
|
||||
message.error(error.message || "error");
|
||||
} finally {
|
||||
message.success(t("successSend"));
|
||||
router.push('/sendbox');
|
||||
router.push('/user');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const {
|
||||
jwt, localeCache, settings, showPassword, mailboxSplitSize, useIframeShowMail
|
||||
jwt, localeCache, settings, showPassword, loading,
|
||||
mailboxSplitSize, useIframeShowMail
|
||||
} = useGlobalState()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
@@ -87,6 +87,7 @@ class CustomSMTPHandler:
|
||||
_logger.info(f"Parsed mail from {from_name} to {to_mail_map}")
|
||||
# Send mail
|
||||
send_body = {
|
||||
"token": session.auth_data.password.decode(),
|
||||
"from_name": from_name,
|
||||
"to_name": to_mail_map.get(to_mail),
|
||||
"to_mail": to_mail,
|
||||
@@ -99,9 +100,8 @@ class CustomSMTPHandler:
|
||||
_logger.info(f"Send mail {send_body}")
|
||||
try:
|
||||
res = requests.post(
|
||||
f"{settings.proxy_url}/api/send_mail",
|
||||
f"{settings.proxy_url}/external/api/send_mail",
|
||||
json=send_body, headers={
|
||||
"Authorization": f"Bearer {session.auth_data.password.decode()}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -87,6 +87,9 @@ ENABLE_AUTO_REPLY = false
|
||||
# COPYRIGHT = "Dream Hunter"
|
||||
# default send balance, if not set, it will be 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# Turnstile verification configuration
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = "mailchannels" # Refer to the DKIM section mailchannels._domainkey for mailchannels
|
||||
# DKIM_PRIVATE_KEY = "" # Refer to the contents of priv_key.txt in the DKIM section
|
||||
|
||||
@@ -45,6 +45,9 @@ ENABLE_AUTO_REPLY = false
|
||||
# COPYRIGHT = "Dream Hunter"
|
||||
# 默认发送邮件余额,如果不设置,将为 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# Turnstile 人机验证配置
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
|
||||
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
|
||||
|
||||
@@ -17,7 +17,25 @@ send_body = {
|
||||
res = requests.post(
|
||||
"http://localhost:8787/api/send_mail",
|
||||
json=send_body, headers={
|
||||
"Authorization": f"Bearer {session.auth_data.password.decode()}",
|
||||
"Authorization": f"Bearer {你的JWT密码}",
|
||||
"x-custom-auth": "<你的网站密码>",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
# 使用 body 验证
|
||||
send_body = {
|
||||
"token": "<你的JWT密码>
|
||||
"from_name": "发件人名字",
|
||||
"to_name": "收件人名字",
|
||||
"to_mail": "收件人地址",
|
||||
"subject": "邮件主题",
|
||||
"is_html": False, # 根据内容设置是否为 HTML
|
||||
"content": "<邮件内容:html 或者 文本>",
|
||||
}
|
||||
res = requests.post(
|
||||
"http://localhost:8787/external/api/send_mail",
|
||||
json=send_body, headers={
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
|
||||
@@ -73,14 +73,15 @@ api.delete('/admin/delete_address/:id', async (c) => {
|
||||
return c.text("Failed to delete address", 500)
|
||||
}
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM mails WHERE address IN
|
||||
(select name from address where id = ?) `
|
||||
`DELETE FROM mails WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
if (!mailSuccess) {
|
||||
return c.text("Failed to delete mails", 500)
|
||||
}
|
||||
const { success: sendAccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM address_sender WHERE address_id = ? `
|
||||
`DELETE FROM address_sender WHERE address IN`
|
||||
+ ` (select name from address where id = ?) `
|
||||
).bind(id).run();
|
||||
return c.json({
|
||||
success: success && mailSuccess && sendAccess
|
||||
@@ -302,9 +303,11 @@ api.post('/admin/auto_cleanup', cleanup_api.saveCleanup)
|
||||
|
||||
api.get('/admin/account_settings', async (c) => {
|
||||
try {
|
||||
const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
const blockList = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
return c.json({
|
||||
blockList: value || []
|
||||
blockList: blockList || [],
|
||||
sendBlockList: sendBlockList || []
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -313,14 +316,18 @@ api.get('/admin/account_settings', async (c) => {
|
||||
})
|
||||
|
||||
api.post('/admin/account_settings', async (c) => {
|
||||
const { blockList } = await c.req.json();
|
||||
if (!blockList) {
|
||||
return c.text("Invalid blockList", 400)
|
||||
const { blockList, sendBlockList } = await c.req.json();
|
||||
if (!blockList || !sendBlockList) {
|
||||
return c.text("Invalid blockList or sendBlockList", 400)
|
||||
}
|
||||
await saveSetting(
|
||||
c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY,
|
||||
JSON.stringify(blockList)
|
||||
);
|
||||
await saveSetting(
|
||||
c, CONSTANTS.SEND_BLOCK_LIST_KEY,
|
||||
JSON.stringify(sendBlockList)
|
||||
);
|
||||
return c.json({
|
||||
success: true
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const CONSTANTS = {
|
||||
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
|
||||
SEND_BLOCK_LIST_KEY: 'send_block_list',
|
||||
AUTO_CLEANUP_KEY: 'auto_cleanup',
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import {
|
||||
getDomains, getPasswords, getBooleanValue, getJsonSetting
|
||||
getDomains, getPasswords, getBooleanValue, getJsonSetting,
|
||||
checkCfTurnstile
|
||||
} from './utils';
|
||||
import { newAddress } from './common'
|
||||
import { CONSTANTS } from './constants'
|
||||
@@ -116,14 +117,21 @@ api.get('/open_api/settings', async (c) => {
|
||||
"enableUserDeleteEmail": getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL),
|
||||
"enableAutoReply": getBooleanValue(c.env.ENABLE_AUTO_REPLY),
|
||||
"copyright": c.env.COPYRIGHT,
|
||||
"cfTurnstileSiteKey": c.env.CF_TURNSTILE_SITE_KEY,
|
||||
});
|
||||
})
|
||||
|
||||
api.get('/api/new_address', async (c) => {
|
||||
api.post('/api/new_address', async (c) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_CREATE_EMAIL)) {
|
||||
return c.text("New address is disabled", 403)
|
||||
}
|
||||
let { name, domain } = c.req.query();
|
||||
let { name, domain, cf_token } = await c.req.json();
|
||||
// check cf turnstile
|
||||
try {
|
||||
await checkCfTurnstile(c, cf_token);
|
||||
} catch (error) {
|
||||
return c.text("Failed to check cf turnstile", 500)
|
||||
}
|
||||
// if no name, generate random name
|
||||
if (!name) {
|
||||
name = Math.random().toString(36).substring(2, 15);
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { CONSTANTS } from './constants'
|
||||
import { getJsonSetting } from './utils';
|
||||
|
||||
const api = new Hono()
|
||||
|
||||
@@ -26,9 +29,7 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
|
||||
api.post('/api/send_mail', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
const sendMail = async (c, address) => {
|
||||
// check permission
|
||||
const balance = await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender
|
||||
@@ -47,6 +48,11 @@ api.post('/api/send_mail', async (c) => {
|
||||
if (!to_mail) {
|
||||
return c.text("Invalid to mail", 400)
|
||||
}
|
||||
// check SEND_BLOCK_LIST_KEY
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
|
||||
return c.text("to_mail address is blocked", 400);
|
||||
}
|
||||
from_name = from_name || address;
|
||||
to_name = to_name || to_mail;
|
||||
if (!subject) {
|
||||
@@ -113,6 +119,8 @@ api.post('/api/send_mail', async (c) => {
|
||||
if (body?.personalizations?.[0]?.dkim_private_key) {
|
||||
delete body.personalizations[0].dkim_private_key;
|
||||
}
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
body.reqIp = reqIp;
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
`INSERT INTO sendbox (address, raw) VALUES (?, ?)`
|
||||
).bind(address, JSON.stringify(body)).run();
|
||||
@@ -123,6 +131,25 @@ api.post('/api/send_mail', async (c) => {
|
||||
console.warn(`Failed to save to sendbox for ${address}`);
|
||||
}
|
||||
return c.json({ status: "ok" });
|
||||
}
|
||||
|
||||
api.post('/api/send_mail', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
return await sendMail(c, address);
|
||||
})
|
||||
|
||||
api.post('/external/api/send_mail', async (c) => {
|
||||
const { token } = await c.req.json();
|
||||
try {
|
||||
const { address } = await Jwt.verify(token, c.env.JWT_SECRET);
|
||||
if (!address) {
|
||||
return c.text("No address", 400)
|
||||
}
|
||||
return await sendMail(c, address);
|
||||
} catch (e) {
|
||||
console.error("Failed to verify token", e);
|
||||
return c.text("Unauthorized", 401)
|
||||
}
|
||||
})
|
||||
|
||||
const getSendbox = async (c, address, limit, offset) => {
|
||||
|
||||
@@ -129,3 +129,27 @@ export const sendAdminInternalMail = async (c, toMail, subject, text) => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkCfTurnstile = async (c, token) => {
|
||||
if (!c.env.CF_TURNSTILE_SITE_KEY) {
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
throw new Error("Captcha token is required");
|
||||
}
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
let formData = new FormData();
|
||||
formData.append('secret', c.env.CF_TURNSTILE_SECRET_KEY);
|
||||
formData.append('response', token);
|
||||
formData.append('remoteip', reqIp);
|
||||
const url = 'https://challenges.cloudflare.com/turnstile/v0/siteverify';
|
||||
const result = await fetch(url, {
|
||||
body: formData,
|
||||
method: 'POST',
|
||||
});
|
||||
const captchaRes = await result.json();
|
||||
if (!captchaRes.success) {
|
||||
console.log("Captcha failed", captchaRes);
|
||||
throw new Error("Captcha failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ ENABLE_AUTO_REPLY = false
|
||||
# COPYRIGHT = "Dream Hunter"
|
||||
# default send balance, if not set, it will be 0
|
||||
# DEFAULT_SEND_BALANCE = 1
|
||||
# Turnstile verification
|
||||
# CF_TURNSTILE_SITE_KEY = ""
|
||||
# CF_TURNSTILE_SECRET_KEY = ""
|
||||
# dkim config
|
||||
# DKIM_SELECTOR = ""
|
||||
# DKIM_PRIVATE_KEY = ""
|
||||
|
||||
Reference in New Issue
Block a user