mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-06-28 02:42:44 +08:00
feat: telegram mini app open mail from bot (#256)
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<!-- markdownlint-disable-file MD004 MD024 MD034 MD036 -->
|
||||
# CHANGE LOG
|
||||
|
||||
## main branch
|
||||
|
||||
- telegram mini app
|
||||
- telegram mini 增加 `ubind`, `delete` 指令
|
||||
- telegram bot 增加 `ubind`, `delete` 指令
|
||||
|
||||
## v0.4.3
|
||||
|
||||
@@ -20,7 +21,6 @@
|
||||
- UI: 发件箱也采用左右分栏显示(类似收件箱)
|
||||
- `SMTP IMAP Proxy` 添加发件箱查看
|
||||
|
||||
|
||||
* feat: telegram bot TelegramSettings && webhook by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/244
|
||||
* fix build by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/245
|
||||
* feat: UI changes by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/247
|
||||
@@ -201,7 +201,6 @@ set
|
||||
- 添加 RATE_LIMITER 限流 发送邮件 和 新建地址
|
||||
- 一些 bug 修复
|
||||
|
||||
---
|
||||
- feat: allow user delete mail && notify when send access changed by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/132
|
||||
- feat: requset_send_mail_access default 1 balance by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/143
|
||||
- fix: RATE_LIMITER not call jwt by @dreamhunter2333 in https://github.com/dreamhunter2333/cloudflare_temp_email/pull/146
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Index from '../views/Index.vue'
|
||||
import UserLogin from '../views/user/UserLogin.vue'
|
||||
import User from '../views/User.vue'
|
||||
import SendMail from '../views/index/SendMail.vue'
|
||||
import Admin from '../views/Admin.vue'
|
||||
import TelegramMail from '../views/telegram/Mail.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -19,7 +18,11 @@ const router = createRouter({
|
||||
{
|
||||
path: '/admin',
|
||||
component: Admin
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/telegram_mail',
|
||||
component: TelegramMail
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const { t } = useI18n({
|
||||
enable: 'Enable',
|
||||
telegramAllowList: 'Telegram Allow List',
|
||||
save: 'Save',
|
||||
miniAppUrl: 'Telegram Mini App URL',
|
||||
},
|
||||
zh: {
|
||||
init: '初始化',
|
||||
@@ -31,6 +32,7 @@ const { t } = useI18n({
|
||||
enable: '启用',
|
||||
telegramAllowList: 'Telegram 白名单',
|
||||
save: '保存',
|
||||
miniAppUrl: '电报小程序 URL(请输入你部署的电报小程序网页地址)',
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -63,14 +65,16 @@ const init = async () => {
|
||||
class TelegramSettings {
|
||||
enableAllowList: boolean;
|
||||
allowList: string[];
|
||||
miniAppUrl: string;
|
||||
|
||||
constructor(enableAllowList: boolean, allowList: string[]) {
|
||||
constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) {
|
||||
this.enableAllowList = enableAllowList;
|
||||
this.allowList = allowList;
|
||||
this.miniAppUrl = miniAppUrl;
|
||||
}
|
||||
}
|
||||
|
||||
const settings = ref(new TelegramSettings(false, []))
|
||||
const settings = ref(new TelegramSettings(false, [], ''))
|
||||
|
||||
const getSettings = async () => {
|
||||
try {
|
||||
@@ -111,6 +115,9 @@ onMounted(async () => {
|
||||
:placeholder="t('telegramAllowList')" />
|
||||
</n-input-group>
|
||||
</n-form-item-row>
|
||||
<n-form-item-row :label="t('miniAppUrl')">
|
||||
<n-input v-model:value="settings.miniAppUrl"></n-input>
|
||||
</n-form-item-row>
|
||||
<n-button @click="saveSettings" type="primary" block>
|
||||
{{ t('save') }}
|
||||
</n-button>
|
||||
|
||||
@@ -9,6 +9,29 @@ import Turnstile from '../../components/Turnstile.vue'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
|
||||
const props = defineProps({
|
||||
bindUserAddress: {
|
||||
type: Function,
|
||||
default: async () => { await api.bindUserAddress(); },
|
||||
requried: true
|
||||
},
|
||||
newAddressPath: {
|
||||
type: Function,
|
||||
default: async (address_name, domain, cf_token) => {
|
||||
return await api.fetch("/api/new_address", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: address_name,
|
||||
domain: domain,
|
||||
cf_token: cf_token,
|
||||
}),
|
||||
});
|
||||
},
|
||||
requried: true
|
||||
},
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -32,7 +55,7 @@ const login = async () => {
|
||||
jwt.value = credential.value;
|
||||
await api.getSettings();
|
||||
try {
|
||||
await api.bindUserAddress();
|
||||
await props.bindUserAddress();
|
||||
} catch (error) {
|
||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||
}
|
||||
@@ -98,20 +121,17 @@ const generateName = async () => {
|
||||
|
||||
const newEmail = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/api/new_address`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
name: emailName.value,
|
||||
domain: emailDomain.value,
|
||||
cf_token: cfToken.value,
|
||||
}),
|
||||
});
|
||||
const res = await props.newAddressPath(
|
||||
emailName.value,
|
||||
emailDomain.value,
|
||||
cfToken.value
|
||||
);
|
||||
jwt.value = res["jwt"];
|
||||
await api.getSettings();
|
||||
await router.push("/");
|
||||
showAddressCredential.value = true;
|
||||
try {
|
||||
await api.bindUserAddress();
|
||||
await props.bindUserAddress();
|
||||
} catch (error) {
|
||||
message.error(`${t('bindUserAddressError')}: ${error.message}`);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ const { t } = useI18n({
|
||||
locale: localeCache.value || 'zh',
|
||||
messages: {
|
||||
en: {
|
||||
addressManage: 'Address Manage',
|
||||
changeAddress: 'Change Address',
|
||||
ok: 'OK',
|
||||
copy: 'Copy',
|
||||
@@ -34,6 +35,7 @@ const { t } = useI18n({
|
||||
userLogin: 'User Login',
|
||||
},
|
||||
zh: {
|
||||
addressManage: '地址管理',
|
||||
changeAddress: '更换地址',
|
||||
ok: '确定',
|
||||
copy: '复制',
|
||||
@@ -74,7 +76,7 @@ onMounted(async () => {
|
||||
<b>{{ settings.address }}</b>
|
||||
<n-button v-if="isTelegram" style="margin-left: 10px" @click="showTelegramChangeAddress = true"
|
||||
size="small" tertiary type="primary">
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('changeAddress') }}
|
||||
<n-icon :component="ExchangeAlt" /> {{ t('addressManage') }}
|
||||
</n-button>
|
||||
<n-button v-else-if="userJwt" style="margin-left: 10px" @click="showChangeAddress = true"
|
||||
size="small" tertiary type="primary">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, h, onMounted } from 'vue';
|
||||
import { useSessionStorage } from '@vueuse/core';
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { NPopconfirm, NButton } from 'naive-ui'
|
||||
|
||||
@@ -8,6 +7,8 @@ import { NPopconfirm, NButton } from 'naive-ui'
|
||||
import { useGlobalState } from '../../store'
|
||||
// @ts-ignore
|
||||
import { api } from '../../api'
|
||||
// @ts-ignore
|
||||
import Login from '../common/Login.vue';
|
||||
|
||||
const { localeCache, jwt, telegramApp } = useGlobalState()
|
||||
// @ts-ignore
|
||||
@@ -21,21 +22,27 @@ const { t } = useI18n({
|
||||
address: 'Address',
|
||||
actions: 'Actions',
|
||||
changeMailAddress: 'Change Mail Address',
|
||||
unbindMailAddress: 'Unbind Mail Address',
|
||||
bind: 'Bind',
|
||||
bindAddressSuccess: 'Bind Address Success',
|
||||
},
|
||||
zh: {
|
||||
success: '成功',
|
||||
address: '地址',
|
||||
actions: '操作',
|
||||
changeMailAddress: '切换邮箱地址',
|
||||
unbindMailAddress: '解绑邮箱地址',
|
||||
bind: '绑定',
|
||||
bindAddressSuccess: '绑定地址成功',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const data = useSessionStorage("telegram-bind-address", [])
|
||||
const data = ref([]);
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
data.value = await api.fetch(`/telegram/bind_address`, {
|
||||
data.value = await api.fetch(`/telegram/get_bind_address`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
initData: telegramApp.value.initData
|
||||
@@ -46,6 +53,32 @@ const fetchData = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const newAddressPath = async (address_name: string, domain: string, cf_token: string) => {
|
||||
return await api.fetch("/telegram/new_address", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
initData: telegramApp.value.initData,
|
||||
address: `${address_name}@${domain}`,
|
||||
cf_token: cf_token,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const bindAddress = async () => {
|
||||
try {
|
||||
await api.fetch(`/telegram/bind_address`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
initData: telegramApp.value.initData,
|
||||
jwt: jwt.value
|
||||
})
|
||||
});
|
||||
message.success(t('bindAddressSuccess'));
|
||||
} catch (error) {
|
||||
message.error((error as Error).message || "error");
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: t('address'),
|
||||
@@ -73,6 +106,31 @@ const columns = [
|
||||
),
|
||||
default: () => `${t('changeMailAddress')}?`
|
||||
}
|
||||
),
|
||||
h(NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => {
|
||||
api.fetch(`/telegram/unbind_address`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
initData: telegramApp.value.initData,
|
||||
address: row.address
|
||||
})
|
||||
});
|
||||
jwt.value = ""
|
||||
location.reload()
|
||||
}
|
||||
},
|
||||
{
|
||||
trigger: () => h(NButton,
|
||||
{
|
||||
tertiary: true,
|
||||
type: "warning",
|
||||
},
|
||||
{ default: () => t('unbindMailAddress') }
|
||||
),
|
||||
default: () => `${t('unbindMailAddress')}?`
|
||||
}
|
||||
)
|
||||
])
|
||||
}
|
||||
@@ -89,6 +147,13 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
<n-tabs type="segment">
|
||||
<n-tab-pane name="address" :tab="t('address')">
|
||||
<n-data-table :columns="columns" :data="data" :bordered="false" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="bind" :tab="t('bind')">
|
||||
<Login :newAddressPath="newAddressPath" :bindUserAddress="bindAddress" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
70
frontend/src/views/telegram/Mail.vue
Normal file
70
frontend/src/views/telegram/Mail.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { useGlobalState } from '../../store'
|
||||
import { api } from '../../api'
|
||||
import { onMounted, watch } from 'vue';
|
||||
import { processItem } from '../../utils/email-parser'
|
||||
|
||||
const { telegramApp } = useGlobalState()
|
||||
const route = useRoute()
|
||||
|
||||
const curMail = ref({});
|
||||
|
||||
watch(telegramApp, async () => {
|
||||
if (telegramApp.value.initData) {
|
||||
curMail.value = await fetchMailData();
|
||||
}
|
||||
});
|
||||
|
||||
const fetchMailData = async () => {
|
||||
try {
|
||||
const res = await api.fetch(`/telegram/get_mail`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
initData: telegramApp.value.initData,
|
||||
mailId: route.query.mail_id
|
||||
})
|
||||
});
|
||||
return await processItem(res);
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
curMail.value = await fetchMailData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="center">
|
||||
<n-card v-if="curMail.message" style="max-width: 800px; overflow: auto;">
|
||||
<n-tag type="info">
|
||||
ID: {{ curMail.id }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
Date: {{ curMail.created_at }}
|
||||
</n-tag>
|
||||
<n-tag type="info">
|
||||
FROM: {{ curMail.source }}
|
||||
</n-tag>
|
||||
<n-tag v-if="showEMailTo" type="info">
|
||||
TO: {{ curMail.address }}
|
||||
</n-tag>
|
||||
<div v-html="curMail.message" style="margin-top: 10px;"></div>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.center {
|
||||
display: flex;
|
||||
text-align: left;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Context } from 'hono';
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
import { getDomains, getStringValue } from './utils';
|
||||
import { getBooleanValue, getDomains, getStringValue } from './utils';
|
||||
import { HonoCustomType } from './types';
|
||||
import { CONSTANTS } from './constants';
|
||||
import { unbindTelegramByAddress } from './telegram_api/common';
|
||||
@@ -14,7 +14,7 @@ export const newAddress = async (
|
||||
// remove special characters
|
||||
name = name.replace(/[^a-zA-Z0-9.]/g, '')
|
||||
// check name length
|
||||
if (name.length < 0) {
|
||||
if (name.length <= 0) {
|
||||
throw new Error("Name too short")
|
||||
}
|
||||
// create address
|
||||
@@ -39,7 +39,8 @@ export const newAddress = async (
|
||||
throw new Error("Failed to create address")
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
throw new Error("Address already exists")
|
||||
}
|
||||
throw new Error("Failed to create address")
|
||||
@@ -98,6 +99,9 @@ export const deleteAddressWithData = async (
|
||||
address: string | undefined | null,
|
||||
address_id: number | undefined | null
|
||||
): Promise<boolean> => {
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
throw new Error("Delete email is disabled")
|
||||
}
|
||||
if (!address && !address_id) {
|
||||
throw new Error("Address or address_id required")
|
||||
}
|
||||
@@ -139,13 +143,19 @@ export const deleteAddressWithData = async (
|
||||
export const handleListQuery = async (
|
||||
c: Context<HonoCustomType>,
|
||||
query: string, countQuery: string, params: string[],
|
||||
limit: number | undefined | null,
|
||||
offset: number | undefined | null
|
||||
limit: string | number | undefined | null,
|
||||
offset: string | number | undefined | null
|
||||
): Promise<Response> => {
|
||||
if (typeof limit === "string") {
|
||||
limit = parseInt(limit);
|
||||
}
|
||||
if (typeof offset === "string") {
|
||||
offset = parseInt(offset);
|
||||
}
|
||||
if (!limit || limit < 0 || limit > 100) {
|
||||
return c.text("Invalid limit", 400)
|
||||
}
|
||||
if (!offset || offset < 0) {
|
||||
if (offset == null || offset == undefined || offset < 0) {
|
||||
return c.text("Invalid offset", 400)
|
||||
}
|
||||
const resultsQuery = `${query} order by id desc limit ? offset ?`;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Context } from "hono";
|
||||
import { getBooleanValue } from "../utils";
|
||||
import { HonoCustomType } from "../types";
|
||||
|
||||
|
||||
export default {
|
||||
getAutoReply: async (c) => {
|
||||
getAutoReply: async (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
}
|
||||
@@ -21,7 +23,7 @@ export default {
|
||||
name: results.name,
|
||||
})
|
||||
},
|
||||
saveAutoReply: async (c) => {
|
||||
saveAutoReply: async (c: Context<HonoCustomType>) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_AUTO_REPLY)) {
|
||||
return c.text("Auto reply is disabled", 403)
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Hono } from 'hono'
|
||||
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getBooleanValue, getJsonSetting, checkCfTurnstile } from '../utils';
|
||||
import { newAddress, handleListQuery } from '../common'
|
||||
import { newAddress, handleListQuery, deleteAddressWithData } from '../common'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import auto_reply from './auto_reply'
|
||||
import webhook_settings from './webhook_settings';
|
||||
|
||||
const api = new Hono()
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
api.get('/api/auto_reply', auto_reply.getAutoReply)
|
||||
api.post('/api/auto_reply', auto_reply.saveAutoReply)
|
||||
@@ -103,7 +104,7 @@ api.post('/api/new_address', async (c) => {
|
||||
// check name block list
|
||||
try {
|
||||
const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
const blockList = value || [];
|
||||
const blockList = (value || []) as string[];
|
||||
if (blockList.some((item) => name.includes(item))) {
|
||||
return c.text(`Name[${name}]is blocked`, 400)
|
||||
}
|
||||
@@ -114,37 +115,14 @@ api.post('/api/new_address', async (c) => {
|
||||
const res = await newAddress(c, name, domain, true);
|
||||
return c.json(res);
|
||||
} catch (e) {
|
||||
return c.text(`Failed create address: ${e.message}`, 400)
|
||||
return c.text(`Failed create address: ${(e as Error).message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
api.delete('/api/delete_address', async (c) => {
|
||||
if (!getBooleanValue(c.env.ENABLE_USER_DELETE_EMAIL)) {
|
||||
return c.text("User delete email is disabled", 403)
|
||||
}
|
||||
const { address, address_id } = c.get("jwtPayload")
|
||||
let name = address;
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`DELETE FROM address WHERE name = ? `
|
||||
).bind(name).run();
|
||||
if (!success) {
|
||||
return c.text("Failed to delete address", 500)
|
||||
}
|
||||
const { success: mailSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM raw_mails WHERE address = ? `
|
||||
).bind(address).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 = ? `
|
||||
).bind(address).run();
|
||||
const { success: addressSuccess } = await c.env.DB.prepare(
|
||||
`DELETE FROM users_address WHERE address_id = ? `
|
||||
).bind(address_id).run();
|
||||
const success = await deleteAddressWithData(c, address, address_id);
|
||||
return c.json({
|
||||
success: success && mailSuccess && sendAccess && addressSuccess
|
||||
success: success
|
||||
})
|
||||
})
|
||||
|
||||
export { api }
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Hono } from 'hono'
|
||||
import { Context, Hono } from 'hono'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { CONSTANTS } from '../constants'
|
||||
import { getJsonSetting, getDomains } from '../utils';
|
||||
import { GeoData } from '../models'
|
||||
import { getJsonSetting, getDomains, getIntValue } from '../utils';
|
||||
import { GeoData } from '../models/models'
|
||||
import { handleListQuery } from '../common'
|
||||
import { HonoCustomType } from '../types';
|
||||
|
||||
|
||||
const api = new Hono()
|
||||
export const api = new Hono<HonoCustomType>()
|
||||
|
||||
api.post('/api/requset_send_mail_access', async (c) => {
|
||||
const { address } = c.get("jwtPayload")
|
||||
@@ -14,7 +15,7 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
return c.text("No address", 400)
|
||||
}
|
||||
try {
|
||||
const default_balance = c.env.DEFAULT_SEND_BALANCE || 0;
|
||||
const default_balance = getIntValue(c.env.DEFAULT_SEND_BALANCE, 0);
|
||||
const { success } = await c.env.DB.prepare(
|
||||
`INSERT INTO address_sender (address, balance, enabled) VALUES (?, ?, ?)`
|
||||
).bind(
|
||||
@@ -24,15 +25,22 @@ api.post('/api/requset_send_mail_access', async (c) => {
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message && e.message.includes("UNIQUE")) {
|
||||
return c.text("Already requested", 400)
|
||||
const message = (e as Error).message;
|
||||
if (message && message.includes("UNIQUE")) {
|
||||
throw new Error("Address already requested")
|
||||
}
|
||||
return c.text("Failed to request send mail access", 500)
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
export const sendMail = async (c, address, reqJson) => {
|
||||
export const sendMail = async (
|
||||
c: Context<HonoCustomType>, address: string,
|
||||
reqJson: {
|
||||
from_name: string, to_mail: string, to_name: string,
|
||||
subject: string, content: string, is_html: boolean
|
||||
}
|
||||
) => {
|
||||
if (!address) {
|
||||
throw new Error("No address")
|
||||
}
|
||||
@@ -46,7 +54,7 @@ export const sendMail = async (c, address, reqJson) => {
|
||||
const balance = await c.env.DB.prepare(
|
||||
`SELECT balance FROM address_sender
|
||||
where address = ? and enabled = 1`
|
||||
).bind(address).first("balance");
|
||||
).bind(address).first<number>("balance");
|
||||
if (!balance || balance <= 0) {
|
||||
throw new Error("No balance")
|
||||
}
|
||||
@@ -58,7 +66,7 @@ export const sendMail = async (c, address, reqJson) => {
|
||||
throw new Error("Invalid to mail")
|
||||
}
|
||||
// check SEND_BLOCK_LIST_KEY
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY);
|
||||
const sendBlockList = await getJsonSetting(c, CONSTANTS.SEND_BLOCK_LIST_KEY) as string[];
|
||||
if (sendBlockList && sendBlockList.some((item) => to_mail.includes(item))) {
|
||||
throw new Error("to_mail address is blocked")
|
||||
}
|
||||
@@ -124,12 +132,12 @@ export const sendMail = async (c, address, reqJson) => {
|
||||
}
|
||||
// save to sendbox
|
||||
try {
|
||||
if (body?.personalizations?.[0]?.dkim_private_key) {
|
||||
delete body.personalizations[0].dkim_private_key;
|
||||
if ((body as any)?.personalizations?.[0]?.dkim_private_key) {
|
||||
delete (body as any).personalizations[0].dkim_private_key;
|
||||
}
|
||||
const reqIp = c.req.raw.headers.get("cf-connecting-ip")
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf);
|
||||
body.geoData = geoData;
|
||||
const geoData = new GeoData(reqIp, c.req.raw.cf as any);
|
||||
(body as any).geoData = geoData;
|
||||
const { success: success2 } = await c.env.DB.prepare(
|
||||
`INSERT INTO sendbox (address, raw) VALUES (?, ?)`
|
||||
).bind(address, JSON.stringify(body)).run();
|
||||
@@ -148,7 +156,7 @@ api.post('/api/send_mail', async (c) => {
|
||||
await sendMail(c, address, reqJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to send mail", e);
|
||||
return c.text(`Failed to send mail ${e.message}`, 400)
|
||||
return c.text(`Failed to send mail ${(e as Error).message}`, 400)
|
||||
}
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
@@ -165,11 +173,14 @@ api.post('/external/api/send_mail', async (c) => {
|
||||
return c.json({ status: "ok" })
|
||||
} catch (e) {
|
||||
console.error("Failed to send mail", e);
|
||||
return c.text(`Failed to send mail ${e.message}`, 400)
|
||||
return c.text(`Failed to send mail ${(e as Error).message}`, 400)
|
||||
}
|
||||
})
|
||||
|
||||
const getSendbox = async (c, address, limit, offset) => {
|
||||
export const getSendbox = async (
|
||||
c: Context<HonoCustomType>,
|
||||
address: string, limit: string, offset: string
|
||||
): Promise<Response> => {
|
||||
if (!address) {
|
||||
return c.json({ "error": "No address" }, 400)
|
||||
}
|
||||
@@ -185,5 +196,3 @@ api.get('/api/sendbox', async (c) => {
|
||||
const { limit, offset } = c.req.query();
|
||||
return getSendbox(c, address, limit, offset);
|
||||
})
|
||||
|
||||
export { api, getSendbox }
|
||||
@@ -96,9 +96,9 @@ export async function trigerWebhook(
|
||||
from: parsedEmail.from.address || "",
|
||||
to: address,
|
||||
headers: JSON.stringify(parsedEmail.headers, null, 2),
|
||||
subject: parsedEmail.subject || "",
|
||||
raw: raw_mail,
|
||||
parsedText: parsedEmail.text || parsedEmail.html || ""
|
||||
subject: JSON.stringify(parsedEmail.subject) || "",
|
||||
raw: JSON.stringify(raw_mail),
|
||||
parsedText: JSON.stringify(parsedEmail.text) || JSON.stringify(parsedEmail.html) || ""
|
||||
});
|
||||
if (!res.success) {
|
||||
console.log(res.message);
|
||||
|
||||
@@ -38,3 +38,35 @@ export class CleanupSettings {
|
||||
this.cleanSendBoxDays = cleanSendBoxDays || 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class GeoData {
|
||||
|
||||
ip: string;
|
||||
country: string | undefined;
|
||||
city: string | undefined;
|
||||
timezone: string | undefined;
|
||||
postalCode: string | undefined;
|
||||
region: string | undefined;
|
||||
latitude: number | undefined;
|
||||
longitude: number | undefined;
|
||||
regionCode: string | undefined;
|
||||
asOrganization: string | undefined;
|
||||
|
||||
constructor(ip: string | null, data: GeoData | undefined | null) {
|
||||
const {
|
||||
country, city, timezone, postalCode, region,
|
||||
latitude, longitude, regionCode, asOrganization
|
||||
} = data || {};
|
||||
this.ip = ip || "unknown";
|
||||
this.country = country;
|
||||
this.city = city;
|
||||
this.timezone = timezone;
|
||||
this.postalCode = postalCode;
|
||||
this.region = region;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.regionCode = regionCode;
|
||||
this.asOrganization = asOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,60 @@ import { Context } from "hono";
|
||||
import { Jwt } from "hono/utils/jwt";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { HonoCustomType } from "../types";
|
||||
import { getIntValue, getJsonSetting } from "../utils";
|
||||
import { newAddress } from "../common";
|
||||
|
||||
export const tgUserNewAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
): Promise<{ address: string, jwt: string }> => {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
|
||||
)
|
||||
if (!success) {
|
||||
throw Error("Rate limit exceeded")
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
address = address || Math.random().toString(36).substring(2, 15);
|
||||
console.log(`new address: ${address}`);
|
||||
const [name, domain] = address.includes("@") ? address.split("@") : [address, null];
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限");
|
||||
}
|
||||
// check name block list
|
||||
const value = await getJsonSetting(c, CONSTANTS.ADDRESS_BLOCK_LIST_KEY);
|
||||
const blockList = (value || []) as string[];
|
||||
if (blockList.some((item) => name.includes(item))) {
|
||||
throw Error(`Name[${name}]is blocked`);
|
||||
}
|
||||
const res = await newAddress(c,
|
||||
name || Math.random().toString(36).substring(2, 15),
|
||||
domain, true
|
||||
);
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${res.address}`, userId.toString());
|
||||
return res;
|
||||
}
|
||||
|
||||
export const bindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, jwt: string
|
||||
): Promise<string> => {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
throw Error("无效凭证");
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
throw Error("绑定地址数量已达上限");
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${address}`, userId.toString());
|
||||
return address;
|
||||
}
|
||||
|
||||
export const unbindTelegramAddress = async (
|
||||
c: Context<HonoCustomType>, userId: string, address: string
|
||||
|
||||
@@ -68,4 +68,8 @@ api.get("/admin/telegram/status", async (c) => {
|
||||
|
||||
api.get("/admin/telegram/settings", settings.getTelegramSettings);
|
||||
api.post("/admin/telegram/settings", settings.saveTelegramSettings);
|
||||
api.post("/telegram/bind_address", miniapp.getTelegramBindAddress);
|
||||
api.post("/telegram/get_bind_address", miniapp.getTelegramBindAddress);
|
||||
api.post("/telegram/new_address", miniapp.newTelegramAddress);
|
||||
api.post("/telegram/bind_address", miniapp.bindAddress);
|
||||
api.post("/telegram/unbind_address", miniapp.unbindAddress);
|
||||
api.post("/telegram/get_mail", miniapp.getMail);
|
||||
|
||||
@@ -2,26 +2,28 @@ import { Context } from "hono";
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
import { HonoCustomType } from "../types";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { bindTelegramAddress, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||
import { checkCfTurnstile } from "../utils";
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
const TG_AUTH_TIMEOUT = 300;
|
||||
|
||||
|
||||
async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const checkTelegramAuth = async (
|
||||
c: Context<HonoCustomType>, initData: string
|
||||
): Promise<string> => {
|
||||
// check if the request is from telegram
|
||||
const { initData } = await c.req.json();
|
||||
const initDataObj = new URLSearchParams(initData);
|
||||
initDataObj.sort()
|
||||
const hash = initDataObj.get('hash');
|
||||
initDataObj.delete("hash");
|
||||
const dataToCheck = [...initDataObj.entries()].map(([key, value]) => key + "=" + value).join("\n");
|
||||
const auth_date = Number(initDataObj.get('auth_date'));
|
||||
// valid for 300 seconds
|
||||
if (auth_date + 300 < (new Date().getTime() / 1000)) {
|
||||
return c.text("OutDate initData", 400);
|
||||
if (auth_date + TG_AUTH_TIMEOUT < (new Date().getTime() / 1000)) {
|
||||
throw Error("Auth date expired");
|
||||
}
|
||||
const user = initDataObj.get('user');
|
||||
if (!hash || !user) {
|
||||
return c.text("Invalid initData", 400);
|
||||
throw Error("Invalid initData");
|
||||
}
|
||||
const { id: userId } = JSON.parse(user);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
@@ -48,23 +50,109 @@ async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Respo
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
if (calcHash != hash) {
|
||||
return c.text("Invalid initData", 400);
|
||||
throw Error("Invalid initData");
|
||||
}
|
||||
// get the address list from the KV
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const res = [];
|
||||
for (const jwt of jwtList) {
|
||||
try {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
res.push({ address, jwt });
|
||||
} catch (e) {
|
||||
console.error(`failed to verify jwt with error: ${e}`)
|
||||
continue;
|
||||
return userId;
|
||||
}
|
||||
|
||||
async function getTelegramBindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// get the address list from the KV
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const res = [];
|
||||
for (const jwt of jwtList) {
|
||||
try {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
res.push({ address, jwt });
|
||||
} catch (e) {
|
||||
console.error(`failed to verify jwt with error: ${e}`)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return c.json(res);
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function newTelegramAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address, 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)
|
||||
}
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
// get the address list from the KV
|
||||
const res = await tgUserNewAddress(c, userId, address)
|
||||
return c.json(res);
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function bindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, jwt } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await bindTelegramAddress(c, userId, jwt);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function unbindAddress(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, address } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
await unbindTelegramAddress(c, userId, address);
|
||||
return c.json({ success: true });
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMail(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const { initData, mailId } = await c.req.json();
|
||||
try {
|
||||
const userId = await checkTelegramAuth(c, initData);
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
const addressList = [];
|
||||
for (const jwt of jwtList) {
|
||||
try {
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
addressList.push(address);
|
||||
} catch (e) {
|
||||
addressList.push("此凭证无效");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where id = ?`
|
||||
).bind(mailId).first();
|
||||
if (result?.address && !addressList.includes(result.address)) {
|
||||
return c.text("无权查看此邮件", 403);
|
||||
}
|
||||
return c.json(result);
|
||||
}
|
||||
catch (e) {
|
||||
return c.text((e as Error).message, 400);
|
||||
}
|
||||
return c.json(res);
|
||||
}
|
||||
|
||||
export default {
|
||||
getTelegramBindAddress
|
||||
getTelegramBindAddress,
|
||||
newTelegramAddress,
|
||||
bindAddress,
|
||||
unbindAddress,
|
||||
getMail,
|
||||
}
|
||||
|
||||
@@ -5,16 +5,18 @@ import { CONSTANTS } from "../constants";
|
||||
export class TelegramSettings {
|
||||
enableAllowList: boolean;
|
||||
allowList: string[];
|
||||
miniAppUrl: string;
|
||||
|
||||
constructor(enableAllowList: boolean, allowList: string[]) {
|
||||
constructor(enableAllowList: boolean, allowList: string[], miniAppUrl: string) {
|
||||
this.enableAllowList = enableAllowList;
|
||||
this.allowList = allowList;
|
||||
this.miniAppUrl = miniAppUrl;
|
||||
}
|
||||
}
|
||||
|
||||
async function getTelegramSettings(c: Context<HonoCustomType>): Promise<Response> {
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
return c.json(settings || new TelegramSettings(false, []));
|
||||
return c.json(settings || new TelegramSettings(false, [], ""));
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,11 +7,10 @@ import PostalMime from 'postal-mime';
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getIntValue, getDomains, getStringValue } from '../utils';
|
||||
// @ts-ignore
|
||||
import { deleteAddressWithData, newAddress } from '../common'
|
||||
import { deleteAddressWithData } from '../common'
|
||||
import { HonoCustomType } from "../types";
|
||||
import { TelegramSettings } from "./settings";
|
||||
import { unbindTelegramAddress } from "./common";
|
||||
import { bindTelegramAddress, tgUserNewAddress, unbindTelegramAddress } from "./common";
|
||||
|
||||
const COMMANDS = [
|
||||
{
|
||||
@@ -48,6 +47,9 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const bot = new Telegraf(token);
|
||||
|
||||
bot.use(async (ctx, next) => {
|
||||
// skip non-message
|
||||
if (ctx.updateType != "message") return await next();
|
||||
// check if in private chat
|
||||
if (ctx.chat?.type !== "private") {
|
||||
return await ctx.reply("请在私聊中使用");
|
||||
}
|
||||
@@ -75,38 +77,23 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const prefix = getStringValue(c.env.PREFIX)
|
||||
const domains = getDomains(c);
|
||||
return await ctx.reply(
|
||||
"欢迎使用本机器人, 您可以点击左下角打开 mini app \n\n"
|
||||
"欢迎使用本机器人, 您可以打开 mini app \n\n"
|
||||
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
|
||||
+ `当前可用域名: ${JSON.stringify(domains)}\n`
|
||||
+ "请使用以下命令:\n"
|
||||
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
bot.command("new", async (ctx: TgContext) => {
|
||||
const userId = ctx?.message?.from?.id;
|
||||
if (!userId) {
|
||||
return await ctx.reply("无法获取用户信息");
|
||||
}
|
||||
try {
|
||||
if (c.env.RATE_LIMITER) {
|
||||
const { success } = await c.env.RATE_LIMITER.limit(
|
||||
{ key: `${CONSTANTS.TG_KV_PREFIX}:${userId}` }
|
||||
)
|
||||
if (!success) {
|
||||
return await ctx.reply("操作过于频繁");
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
const address = ctx?.message?.text.slice("/new".length).trim() || Math.random().toString(36).substring(2, 15);
|
||||
const [name, domain] = address.includes("@") ? address.split("@") : [address, null];
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
return await ctx.reply("绑定地址数量已达上限");
|
||||
}
|
||||
const res = await newAddress(c, name, domain, true);
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, res.jwt]));
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${res.address}`, userId.toString());
|
||||
const address = ctx?.message?.text.slice("/new".length).trim();
|
||||
const res = await tgUserNewAddress(c, userId.toString(), address);
|
||||
return await ctx.reply(`创建地址成功:\n`
|
||||
+ `地址: ${res.address}\n`
|
||||
+ `凭证: ${res.jwt}\n`
|
||||
@@ -127,17 +114,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
if (!jwt) {
|
||||
return await ctx.reply("请输入凭证");
|
||||
}
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
if (!address) {
|
||||
return await ctx.reply("凭证无效");
|
||||
}
|
||||
const jwtList = await c.env.KV.get<string[]>(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, 'json') || [];
|
||||
if (jwtList.length >= getIntValue(c.env.TG_MAX_ADDRESS, 5)) {
|
||||
return await ctx.reply("绑定地址数量已达上限");
|
||||
}
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, JSON.stringify([...jwtList, jwt]));
|
||||
// for mail push to telegram
|
||||
await c.env.KV.put(`${CONSTANTS.TG_KV_PREFIX}:${address}`, userId.toString());
|
||||
const address = await bindTelegramAddress(c, userId.toString(), jwt);
|
||||
return await ctx.reply(`绑定成功:\n`
|
||||
+ `地址: ${address}`
|
||||
);
|
||||
@@ -218,7 +195,6 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
const { address } = await Jwt.verify(jwt, c.env.JWT_SECRET, "HS256");
|
||||
addressList.push(address);
|
||||
} catch (e) {
|
||||
addressList.push("此凭证无效");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -228,18 +204,27 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
if (!addressList.includes(queryAddress)) {
|
||||
return await ctx.reply(`未绑定此地址 ${queryAddress}`);
|
||||
}
|
||||
const raw = await c.env.DB.prepare(
|
||||
const { raw, id: mailId } = await c.env.DB.prepare(
|
||||
`SELECT * FROM raw_mails where address = ? `
|
||||
+ ` order by id desc limit 1 offset ?`
|
||||
).bind(
|
||||
queryAddress, mailIndex
|
||||
).first<string>("raw");
|
||||
const { mail } = await parseMail(raw);
|
||||
).first<{ raw: string, id: string }>() || {};
|
||||
const { mail } = raw ? await parseMail(raw) : { mail: "已经没有邮件了" };
|
||||
const settings = await c.env.KV.get<TelegramSettings>(CONSTANTS.TG_KV_SETTINGS_KEY, "json");
|
||||
const miniAppButtons = []
|
||||
if (settings?.miniAppUrl && settings?.miniAppUrl?.length > 0 && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
url.pathname = "/telegram_mail"
|
||||
url.searchParams.set("mail_id", mailId);
|
||||
miniAppButtons.push(Markup.button.webApp("OpenApp", url.toString()));
|
||||
}
|
||||
if (edit) {
|
||||
return await ctx.editMessageText(mail || "无邮件",
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
@@ -249,6 +234,7 @@ export function newTelegramBot(c: Context<HonoCustomType>, token: string): Teleg
|
||||
{
|
||||
...Markup.inlineKeyboard([
|
||||
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
|
||||
...miniAppButtons,
|
||||
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
|
||||
])
|
||||
},
|
||||
@@ -303,7 +289,7 @@ const parseMail = async (raw_mail: string | undefined | null) => {
|
||||
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
|
||||
+ `Subject: ${parsedEmail.subject}\n`
|
||||
+ `Date: ${parsedEmail.date}\n`
|
||||
+ `Content:\n${parsedEmail.text || "解析失败,请点击左下角打开 mini app 查看"}`
|
||||
+ `Content:\n${parsedEmail.text || "解析失败,请打开 mini app 查看"}`
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
|
||||
5
worker/src/types.d.ts
vendored
5
worker/src/types.d.ts
vendored
@@ -16,9 +16,14 @@ export type Bindings = {
|
||||
ENABLE_USER_CREATE_EMAIL: string | boolean | undefined
|
||||
ENABLE_USER_DELETE_EMAIL: string | boolean | undefined
|
||||
ENABLE_INDEX_ABOUT: string | boolean | undefined
|
||||
DEFAULT_SEND_BALANCE: number | string | undefined
|
||||
ADMIN_CONTACT: string | undefined
|
||||
COPYRIGHT: string | undefined
|
||||
|
||||
// dkim
|
||||
DKIM_SELECTOR: string | undefined
|
||||
DKIM_PRIVATE_KEY: string | undefined
|
||||
|
||||
// cf turnstile
|
||||
CF_TURNSTILE_SITE_KEY: string | undefined
|
||||
CF_TURNSTILE_SECRET_KEY: string | undefined
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { Hono } from 'hono';
|
||||
|
||||
import { HonoCustomType } from '../types';
|
||||
// @ts-ignore
|
||||
import settings from './settings';
|
||||
// @ts-ignore
|
||||
import user from './user';
|
||||
// @ts-ignore
|
||||
import bind_address from './bind_address';
|
||||
|
||||
const api = new Hono();
|
||||
export const api = new Hono<HonoCustomType>();
|
||||
|
||||
api.get('/user_api/open_settings', settings.openSettings);
|
||||
api.get('/user_api/settings', settings.settings);
|
||||
@@ -15,5 +19,3 @@ api.get('/user_api/bind_address', bind_address.getBindedAddresses);
|
||||
api.post('/user_api/bind_address', bind_address.bind);
|
||||
api.get('/user_api/bind_address_jwt/:address_id', bind_address.getBindedAddressJwt);
|
||||
api.post('/user_api/unbind_address', bind_address.unbind);
|
||||
|
||||
export { api }
|
||||
@@ -3,16 +3,14 @@ import { cors } from 'hono/cors';
|
||||
import { jwt } from 'hono/jwt'
|
||||
import { Jwt } from 'hono/utils/jwt'
|
||||
|
||||
// @ts-ignore
|
||||
import { api as apiV1 } from './deprecated';
|
||||
|
||||
import { api as commonApi } from './commom_api';
|
||||
// @ts-ignore
|
||||
import { api as mailsApi } from './mails_api'
|
||||
// @ts-ignore
|
||||
import { api as userApi } from './user_api';
|
||||
// @ts-ignore
|
||||
import { api as adminApi } from './admin_api';
|
||||
// @ts-ignore
|
||||
import { api as apiV1 } from './deprecated';
|
||||
// @ts-ignore
|
||||
import { api as apiSendMail } from './mails_api/send_mail_api'
|
||||
import { api as telegramApi } from './telegram_api'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user