feat: telegram bot (#238)

This commit is contained in:
Dream Hunter
2024-05-16 12:57:23 +08:00
committed by GitHub
parent 6bb6fa8298
commit 78badf2eaa
23 changed files with 844 additions and 57 deletions

View File

@@ -17,6 +17,7 @@ import MailsUnknow from './admin/MailsUnknow.vue';
import About from './common/About.vue';
import Maintenance from './admin/Maintenance.vue';
import Appearance from './common/Appearance.vue';
import Telegram from './admin/Telegram.vue';
const {
localeCache, adminAuth, showAdminAuth, adminTab, loading, globalTabplacement
@@ -46,6 +47,7 @@ const { t } = useI18n({
unknow: 'Mails with unknow receiver',
senderAccess: 'Sender Access Control',
sendBox: 'Send Box',
telegram: 'Telegram Bot',
statistics: 'Statistics',
maintenance: 'Maintenance',
appearance: 'Appearance',
@@ -64,6 +66,7 @@ const { t } = useI18n({
unknow: '无收件人邮件',
senderAccess: '发件权限控制',
sendBox: '发件箱',
telegram: '电报机器人',
statistics: '统计',
maintenance: '维护',
appearance: '外观',
@@ -121,6 +124,9 @@ onMounted(async () => {
<n-tab-pane name="sendBox" :tab="t('sendBox')">
<SendBox />
</n-tab-pane>
<n-tab-pane name="telegram" :tab="t('telegram')">
<Telegram />
</n-tab-pane>
<n-tab-pane name="statistics" :tab="t('statistics')">
<Statistics />
</n-tab-pane>

View File

@@ -0,0 +1,77 @@
<script setup>
import { useI18n } from 'vue-i18n'
import { useGlobalState } from '../../store'
import { api } from '../../api'
const { localeCache } = useGlobalState()
const message = useMessage()
const { t } = useI18n({
locale: localeCache.value || 'zh',
messages: {
en: {
init: 'Init',
successTip: 'Success',
status: 'Check Status',
},
zh: {
init: '初始化',
successTip: '成功',
status: '查看状态',
}
}
});
const status = ref({
fetched: false,
})
const fetchData = async () => {
try {
const res = await api.fetch(`/admin/telegram/status`)
Object.assign(status.value, res)
status.value.fetched = true
} catch (error) {
message.error(error.message || "error");
}
}
const save = async () => {
try {
await api.fetch(`/admin/telegram/init`, {
method: 'POST',
})
message.success(t('successTip'))
} catch (error) {
message.error(error.message || "error");
}
}
</script>
<template>
<div class="center">
<n-card style="max-width: 800px; overflow: auto;">
<n-button @click="save" type="primary" block>
{{ t('init') }}
</n-button>
<n-button @click="fetchData" secondary block>
{{ t('status') }}
</n-button>
<pre v-if="status.fetched">{{ JSON.stringify(status, null, 2) }}</pre>
</n-card>
</div>
</template>
<style scoped>
.center {
display: flex;
text-align: left;
place-items: center;
justify-content: center;
}
.n-button {
margin-top: 10px;
}
</style>

View File

@@ -5,22 +5,19 @@ import { GithubAlt, Discord, Telegram } from '@vicons/fa'
<template>
<div class="center">
<n-card>
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email" secondary
block strong>
<n-button tag="a" target="_blank" href="https://github.com/dreamhunter2333/cloudflare_temp_email">
<template #icon>
<n-icon :component="GithubAlt" />
</template>
Github
</n-button>
<n-button tag="a" target="_blank" href="https://discord.gg/dQEwTWhA6Q" secondary
block strong>
<n-button tag="a" target="_blank" href="https://discord.gg/dQEwTWhA6Q">
<template #icon>
<n-icon :component="Discord" />
</template>
Discord
</n-button>
<n-button tag="a" target="_blank" href="https://t.me/cloudflare_temp_email" secondary
block strong>
<n-button tag="a" target="_blank" href="https://t.me/cloudflare_temp_email">
<template #icon>
<n-icon :component="Telegram" />
</template>
@@ -38,10 +35,10 @@ import { GithubAlt, Discord, Telegram } from '@vicons/fa'
.n-card {
max-width: 800px;
text-align: left;
}
.n-button {
margin-top: 10px;
margin-left: 10px;
}
</style>

View File

@@ -95,6 +95,8 @@ ENABLE_AUTO_REPLY = false
# 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
# telegram bot
# TG_MAX_ACCOUNTS = 5
[[d1_databases]]
binding = "DB"

View File

@@ -12,6 +12,7 @@ cp wrangler.toml.template wrangler.toml
> [!NOTE]
> 如果你要启用注册用户功能,并需要发送邮件验证,则需要创建 `KV` 缓存, 不需要可跳过此步骤
> 如果需要 Telegram Bot需要创建 `KV` 缓存,不需要可跳过此步骤
通过命令行创建 KV 缓存,或者在 Cloudflare 控制台创建,然后复制对应配置到 `wrangler.toml` 文件中
@@ -62,6 +63,8 @@ ENABLE_AUTO_REPLY = false
# dkim config
# DKIM_SELECTOR = "mailchannels" # 参考 DKIM 部分 mailchannels._domainkey 的 mailchannels
# DKIM_PRIVATE_KEY = "" # 参考 DKIM 部分 priv_key.txt 的内容
# telegram bot 最多绑定邮箱数量
# TG_MAX_ACCOUNTS = 5
# D1 数据库的名称和 ID 可以在 cloudflare 控制台查看
[[d1_databases]]
@@ -83,6 +86,17 @@ database_id = "xxx" # D1 数据库 ID
# simple = { limit = 10, period = 60 }
```
## Telegram Bot 配置
> [!NOTE]
> 如果不需要 Telegram Bot, 可跳过此步骤
请先创建一个 Telegram Bot然后获取 `token`,然后执行下面的命令,将 `token` 添加到 secrets 中
```bash
pnpm wrangler secret put TELEGRAM_BOT_TOKEN
```
## 部署
第一次部署会提示创建项目, `production` 分支请填写 `production`

View File

@@ -28,6 +28,8 @@ docker-compose up -d
修改 docker-compose.yaml 中的环境变量, 注意选择合适的 `tag`
`proxy_url``worker` 的 URL 地址
```yaml
services:
smtp_proxy_server:

View File

@@ -7,9 +7,9 @@
```python
limit = 10
offset = 0
res = requests.post(
res = requests.get(
f"http://localhost:8787/api/mails?limit={limit}&offset={offset}`;",
json=send_body, headers={
headers={
"Authorization": f"Bearer {你的JWT密码}",
# "x-custom-auth": "<你的网站密码>", # 如果启用了自定义密码
"Content-Type": "application/json"

View File

@@ -45,3 +45,10 @@
![worker-kv](/ui_install/worker-kv.png)
![worker-kv-bind](/ui_install/worker-kv-bind.png)
9. Telegram Bot 配置
> [!NOTE]
> 如果不需要 Telegram Bot, 可跳过此步骤
请先创建一个 Telegram Bot然后获取 `token`,然后执行下面的命令,将 `token` 添加到 `Variables` 中, Name: `TELEGRAM_BOT_TOKEN`

1
worker/.gitignore vendored
View File

@@ -131,3 +131,4 @@ dist
.wrangler
wrangler.toml
.dev.vars

View File

@@ -5,15 +5,23 @@
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"deploy": "wrangler deploy --minify",
"start": "wrangler dev",
"build": "wrangler deploy src/worker.js --dry-run --outdir dist --minify"
},
"devDependencies": {
"wrangler": "^3.53.1"
"@cloudflare/workers-types": "^4.20240512.0",
"wrangler": "^3.55.0"
},
"dependencies": {
"hono": "^4.3.0",
"mimetext": "^3.0.24"
"hono": "^4.3.6",
"mimetext": "^3.0.24",
"postal-mime": "^2.2.5",
"telegraf": "4.16.3"
},
"pnpm": {
"patchedDependencies": {
"telegraf@4.16.3": "patches/telegraf@4.16.3.patch"
}
}
}

View File

@@ -0,0 +1,163 @@
diff --git a/lib/core/network/client.js b/lib/core/network/client.js
index 25fbbbb47c7f88e83ae26f629e5ae1a0c141725c..209d4a6bf05352f44eeb082eb327581d698de5ce 100644
--- a/lib/core/network/client.js
+++ b/lib/core/network/client.js
@@ -1,18 +1,18 @@
"use strict";
-var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
- desc = { enumerable: true, get: function() { return m[k]; } };
+ desc = { enumerable: true, get: function () { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
-}) : (function(o, m, k, k2) {
+}) : (function (o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
-var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function (o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
-}) : function(o, v) {
+}) : function (o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
@@ -29,8 +29,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
/* eslint @typescript-eslint/restrict-template-expressions: [ "error", { "allowNumber": true, "allowBoolean": true } ] */
const crypto = __importStar(require("crypto"));
const fs = __importStar(require("fs"));
-const promises_1 = require("fs/promises");
-const https = __importStar(require("https"));
+// const promises_1 = require("fs/promises");
+// const https = __importStar(require("https"));
const path = __importStar(require("path"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const check_1 = require("../helpers/check");
@@ -61,10 +61,10 @@ const DEFAULT_OPTIONS = {
apiRoot: 'https://api.telegram.org',
apiMode: 'bot',
webhookReply: true,
- agent: new https.Agent({
- keepAlive: true,
- keepAliveMsecs: 10000,
- }),
+ // agent: new https.Agent({
+ // keepAlive: true,
+ // keepAliveMsecs: 10000,
+ // }),
attachmentAgent: undefined,
testEnv: false,
};
@@ -112,9 +112,9 @@ async function buildFormDataConfig(payload, agent) {
}
const boundary = crypto.randomBytes(32).toString('hex');
const formData = new multipart_stream_1.default(boundary);
- await Promise.all(Object.keys(payload).map((key) =>
- // @ts-expect-error payload[key] can obviously index payload, but TS doesn't trust us
- attachFormValue(formData, key, payload[key], agent)));
+ await Promise.all(Object.keys(payload).map((key) =>
+ // @ts-expect-error payload[key] can obviously index payload, but TS doesn't trust us
+ attachFormValue(formData, key, payload[key], agent)));
return {
method: 'POST',
compress: true,
@@ -205,14 +205,15 @@ async function attachFormMedia(form, media, id, agent) {
if ('source' in media && media.source) {
let mediaSource = media.source;
if (typeof media.source === 'string') {
- const source = await (0, promises_1.realpath)(media.source);
- if ((await (0, promises_1.stat)(source)).isFile()) {
- fileName = (_c = media.filename) !== null && _c !== void 0 ? _c : path.basename(media.source);
- mediaSource = await fs.createReadStream(media.source);
- }
- else {
- throw new TypeError(`Unable to upload '${media.source}', not a file`);
- }
+ throw new TypeError(`Unable to upload '${media.source}', not a file`);
+ // const source = await (0, promises_1.realpath)(media.source);
+ // if ((await (0, promises_1.stat)(source)).isFile()) {
+ // fileName = (_c = media.filename) !== null && _c !== void 0 ? _c : path.basename(media.source);
+ // mediaSource = await fs.createReadStream(media.source);
+ // }
+ // else {
+ // throw new TypeError(`Unable to upload '${media.source}', not a file`);
+ // }
}
if (isStream(mediaSource) || Buffer.isBuffer(mediaSource)) {
form.addPart({
diff --git a/lib/core/network/polling.js b/lib/core/network/polling.js
index 42f20a5090304c56d0970da56eeaaacaa518ca92..0ae889c32d46e33440c62ad6d27a290c0fe3dda2 100644
--- a/lib/core/network/polling.js
+++ b/lib/core/network/polling.js
@@ -6,10 +6,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.Polling = void 0;
const abort_controller_1 = __importDefault(require("abort-controller"));
const debug_1 = __importDefault(require("debug"));
-const util_1 = require("util");
+// const util_1 = require("util");
const error_1 = require("./error");
const debug = (0, debug_1.default)('telegraf:polling');
-const wait = (0, util_1.promisify)(setTimeout);
+// const wait = (0, util_1.promisify)(setTimeout);
function always(x) {
return () => x;
}
@@ -47,7 +47,8 @@ class Polling {
(err instanceof error_1.TelegramError && err.code >= 500)) {
const retryAfter = (_b = (_a = err.parameters) === null || _a === void 0 ? void 0 : _a.retry_after) !== null && _b !== void 0 ? _b : 5;
debug('Failed to fetch updates, retrying after %ds.', retryAfter, err);
- await wait(retryAfter * 1000);
+ // await wait(retryAfter * 1000);
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
continue;
}
if (err instanceof error_1.TelegramError &&
diff --git a/lib/telegraf.js b/lib/telegraf.js
index 23d021c3d5f98493bd714a2114ec8fa853560e5c..90094d18316138b7e12eab42f722e69ccc9b6c1f 100644
--- a/lib/telegraf.js
+++ b/lib/telegraf.js
@@ -28,8 +28,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true });
exports.Telegraf = void 0;
const crypto = __importStar(require("crypto"));
-const http = __importStar(require("http"));
-const https = __importStar(require("https"));
+// const http = __importStar(require("http"));
+// const https = __importStar(require("https"));
const composer_1 = require("./composer");
const compact_1 = require("./core/helpers/compact");
const context_1 = __importDefault(require("./context"));
@@ -157,13 +157,13 @@ class Telegraf extends composer_1.Composer {
const callback = typeof cb === 'function'
? (req, res) => webhookCb(req, res, () => cb(req, res))
: webhookCb;
- this.webhookServer =
- tlsOptions != null
- ? https.createServer(tlsOptions, callback)
- : http.createServer(callback);
- this.webhookServer.listen(port, host, () => {
- debug('Webhook listening on port: %s', port);
- });
+ // this.webhookServer =
+ // tlsOptions != null
+ // ? https.createServer(tlsOptions, callback)
+ // : http.createServer(callback);
+ // this.webhookServer.listen(port, host, () => {
+ // debug('Webhook listening on port: %s', port);
+ // });
return this;
}
secretPathComponent() {
@@ -176,7 +176,7 @@ class Telegraf extends composer_1.Composer {
/**
* @see https://github.com/telegraf/telegraf/discussions/1344#discussioncomment-335700
*/
- async launch(config = {},
+ async launch(config = {},
/** @experimental */
onLaunch) {
var _a, _b;

189
worker/pnpm-lock.yaml generated
View File

@@ -4,20 +4,34 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
patchedDependencies:
telegraf@4.16.3:
hash: pnqp5pf7vetkijrc5uah4r5w6m
path: patches/telegraf@4.16.3.patch
importers:
.:
dependencies:
hono:
specifier: ^4.3.0
version: 4.3.0
specifier: ^4.3.6
version: 4.3.6
mimetext:
specifier: ^3.0.24
version: 3.0.24
postal-mime:
specifier: ^2.2.5
version: 2.2.5
telegraf:
specifier: 4.16.3
version: 4.16.3(patch_hash=pnqp5pf7vetkijrc5uah4r5w6m)
devDependencies:
'@cloudflare/workers-types':
specifier: ^4.20240512.0
version: 4.20240512.0
wrangler:
specifier: ^3.53.1
version: 3.53.1
specifier: ^3.55.0
version: 3.55.0(@cloudflare/workers-types@4.20240512.0)
packages:
@@ -63,6 +77,9 @@ packages:
cpu: [x64]
os: [win32]
'@cloudflare/workers-types@4.20240512.0':
resolution: {integrity: sha512-o2yTEWg+YK/I1t/Me+dA0oarO0aCbjibp6wSeaw52DSE9tDyKJ7S+Qdyw/XsMrKn4t8kF6f/YOba+9O4MJfW9w==}
'@cspotcode/source-map-support@0.8.1':
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
engines: {node: '>=12'}
@@ -223,11 +240,18 @@ packages:
'@jridgewell/trace-mapping@0.3.9':
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
'@telegraf/types@7.1.0':
resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==}
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
'@types/node@20.12.8':
resolution: {integrity: sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==}
'@types/node@20.12.12':
resolution: {integrity: sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==}
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
acorn-walk@8.3.2:
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
@@ -256,6 +280,15 @@ packages:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
engines: {node: '>=8'}
buffer-alloc-unsafe@1.1.0:
resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==}
buffer-alloc@1.2.0:
resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==}
buffer-fill@1.0.0:
resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==}
capnp-ts@0.7.0:
resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==}
@@ -267,8 +300,8 @@ packages:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
core-js-pure@3.37.0:
resolution: {integrity: sha512-d3BrpyFr5eD4KcbRvQ3FTUx/KWmaDesr7+a3+1+P46IUnNoEt+oiLijPINZMEon7w9oGkIINWxrBAU9DEciwFQ==}
core-js-pure@3.37.1:
resolution: {integrity: sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==}
data-uri-to-buffer@2.0.2:
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
@@ -294,6 +327,10 @@ packages:
estree-walker@0.6.1:
resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==}
event-target-shim@5.0.1:
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
engines: {node: '>=6'}
exit-hook@2.2.1:
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
engines: {node: '>=6'}
@@ -324,8 +361,8 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hono@4.3.0:
resolution: {integrity: sha512-rf9142VLQNMVBj+BjVLISgDWDxnJGUIuX39dvqcdySwr2gTsPfsqW1twWDUjfwQNWm9hEn40MpDu9RFGUN+e8A==}
hono@4.3.6:
resolution: {integrity: sha512-2IqXwrxWF4tG2AR7b5tMYn+KEnWK8UvdC/NUSbOKWj/Kj11OJqel58FxyiXLK5CcKLiL8aGtTe4lkBKXyaHMBQ==}
engines: {node: '>=16.0.0'}
is-binary-path@2.1.0:
@@ -369,11 +406,15 @@ packages:
mimetext@3.0.24:
resolution: {integrity: sha512-UdG1KVfcxeEfo6el91lzFG2WLLTm8DxSK/rosxx5H2Pjla50+DSsjTgr9BRAfAkbQWaxvzcaTO+bHK5ZrdKdfA==}
miniflare@3.20240419.0:
resolution: {integrity: sha512-fIev1PP4H+fQp5FtvzHqRY2v5s+jxh/a0xAhvM5fBNXvxWX7Zod1OatXfXwYbse3hqO3KeVMhb0osVtrW0NwJg==}
miniflare@3.20240419.1:
resolution: {integrity: sha512-Q9n0W07uUD/u0c/b03E4iogeXOAMjZnE3P7B5Yi8sPaZAx6TYWwjurGBja+Pg2yILN2iMaliEobfVyAKss33cA==}
engines: {node: '>=16.13'}
hasBin: true
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
ms@2.1.2:
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
@@ -386,6 +427,15 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-forge@1.3.1:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
engines: {node: '>= 6.13.0'}
@@ -394,6 +444,10 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
p-timeout@4.1.0:
resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==}
engines: {node: '>=10'}
path-parse@1.0.7:
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
@@ -404,6 +458,9 @@ packages:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
postal-mime@2.2.5:
resolution: {integrity: sha512-6eTJf+B47JMdDuLF/4MBiGpTinxl0W8bA9CzrSoiQrNVRqK8Vhe59VrS6sXh2lG/lgo0bxpZFcWOF4Dv1FaSfg==}
printable-characters@1.0.42:
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
@@ -432,6 +489,13 @@ packages:
rollup-pluginutils@2.8.2:
resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
safe-compare@1.1.4:
resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==}
sandwich-stream@2.0.2:
resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==}
engines: {node: '>= 0.10'}
selfsigned@2.4.1:
resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==}
engines: {node: '>=10'}
@@ -455,10 +519,18 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
telegraf@4.16.3:
resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==}
engines: {node: ^12.20.0 || >=14.13.1}
hasBin: true
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
@@ -469,13 +541,19 @@ packages:
resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==}
engines: {node: '>=14.0'}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
workerd@1.20240419.0:
resolution: {integrity: sha512-9yV98KpkQgG+bdEsKEW8i1AYZgxns6NVSfdOVEB2Ue1pTMtIEYfUyqUE+O2amisRrfaC3Pw4EvjtTmVaoetfeg==}
engines: {node: '>=16'}
hasBin: true
wrangler@3.53.1:
resolution: {integrity: sha512-bdMRQdHYdvowIwOhEMFkARIZUh56aDw7HLUZ/2JreBjj760osXE4Fc4L1TCkfRRBWgB6/LKF5LA4OcvORMYmHg==}
wrangler@3.55.0:
resolution: {integrity: sha512-VhtCioKxOdVqkHa8jQ6C6bX3by2Ko0uM0DKzrA+6lBZvfDUlGDWSOPiG+1fOHBHj2JTVBntxWCztXP6L+Udr8w==}
engines: {node: '>=16.17.0'}
hasBin: true
peerDependencies:
@@ -502,14 +580,14 @@ packages:
youch@3.3.3:
resolution: {integrity: sha512-qSFXUk3UZBLfggAW3dJKg0BMblG5biqSF8M34E06o5CSsZtH92u9Hqmj2RzGiHDi64fhe83+4tENFP2DB6t6ZA==}
zod@3.23.6:
resolution: {integrity: sha512-RTHJlZhsRbuA8Hmp/iNL7jnfc4nZishjsanDAfEY1QpDQZCahUp3xDzl+zfweE9BklxMUcgBgS1b7Lvie/ZVwA==}
zod@3.23.8:
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
snapshots:
'@babel/runtime-corejs3@7.24.5':
dependencies:
core-js-pure: 3.37.0
core-js-pure: 3.37.1
regenerator-runtime: 0.14.1
'@babel/runtime@7.24.5':
@@ -535,6 +613,8 @@ snapshots:
'@cloudflare/workerd-windows-64@1.20240419.0':
optional: true
'@cloudflare/workers-types@4.20240512.0': {}
'@cspotcode/source-map-support@0.8.1':
dependencies:
'@jridgewell/trace-mapping': 0.3.9
@@ -626,14 +706,20 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.4.15
'@telegraf/types@7.1.0': {}
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 20.12.8
'@types/node': 20.12.12
'@types/node@20.12.8':
'@types/node@20.12.12':
dependencies:
undici-types: 5.26.5
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
acorn-walk@8.3.2: {}
acorn@8.11.3: {}
@@ -655,6 +741,15 @@ snapshots:
dependencies:
fill-range: 7.0.1
buffer-alloc-unsafe@1.1.0: {}
buffer-alloc@1.2.0:
dependencies:
buffer-alloc-unsafe: 1.1.0
buffer-fill: 1.0.0
buffer-fill@1.0.0: {}
capnp-ts@0.7.0:
dependencies:
debug: 4.3.4
@@ -676,7 +771,7 @@ snapshots:
cookie@0.5.0: {}
core-js-pure@3.37.0: {}
core-js-pure@3.37.1: {}
data-uri-to-buffer@2.0.2: {}
@@ -713,6 +808,8 @@ snapshots:
estree-walker@0.6.1: {}
event-target-shim@5.0.1: {}
exit-hook@2.2.1: {}
fill-range@7.0.1:
@@ -739,7 +836,7 @@ snapshots:
dependencies:
function-bind: 1.1.2
hono@4.3.0: {}
hono@4.3.6: {}
is-binary-path@2.1.0:
dependencies:
@@ -778,7 +875,7 @@ snapshots:
js-base64: 3.7.7
mime-types: 2.1.35
miniflare@3.20240419.0:
miniflare@3.20240419.1:
dependencies:
'@cspotcode/source-map-support': 0.8.1
acorn: 8.11.3
@@ -791,28 +888,38 @@ snapshots:
workerd: 1.20240419.0
ws: 8.17.0
youch: 3.3.3
zod: 3.23.6
zod: 3.23.8
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
mri@1.2.0: {}
ms@2.1.2: {}
mustache@4.2.0: {}
nanoid@3.3.7: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-forge@1.3.1: {}
normalize-path@3.0.0: {}
p-timeout@4.1.0: {}
path-parse@1.0.7: {}
path-to-regexp@6.2.2: {}
picomatch@2.3.1: {}
postal-mime@2.2.5: {}
printable-characters@1.0.42: {}
readdirp@3.6.0:
@@ -843,6 +950,12 @@ snapshots:
dependencies:
estree-walker: 0.6.1
safe-compare@1.1.4:
dependencies:
buffer-alloc: 1.2.0
sandwich-stream@2.0.2: {}
selfsigned@2.4.1:
dependencies:
'@types/node-forge': 1.3.11
@@ -861,10 +974,26 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
telegraf@4.16.3(patch_hash=pnqp5pf7vetkijrc5uah4r5w6m):
dependencies:
'@telegraf/types': 7.1.0
abort-controller: 3.0.0
debug: 4.3.4
mri: 1.2.0
node-fetch: 2.7.0
p-timeout: 4.1.0
safe-compare: 1.1.4
sandwich-stream: 2.0.2
transitivePeerDependencies:
- encoding
- supports-color
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
tr46@0.0.3: {}
tslib@2.6.2: {}
undici-types@5.26.5: {}
@@ -873,6 +1002,13 @@ snapshots:
dependencies:
'@fastify/busboy': 2.1.1
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
workerd@1.20240419.0:
optionalDependencies:
'@cloudflare/workerd-darwin-64': 1.20240419.0
@@ -881,7 +1017,7 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20240419.0
'@cloudflare/workerd-windows-64': 1.20240419.0
wrangler@3.53.1:
wrangler@3.55.0(@cloudflare/workers-types@4.20240512.0):
dependencies:
'@cloudflare/kv-asset-handler': 0.3.2
'@esbuild-plugins/node-globals-polyfill': 0.2.3(esbuild@0.17.19)
@@ -889,7 +1025,7 @@ snapshots:
blake3-wasm: 2.1.5
chokidar: 3.6.0
esbuild: 0.17.19
miniflare: 3.20240419.0
miniflare: 3.20240419.1
nanoid: 3.3.7
path-to-regexp: 6.2.2
resolve: 1.22.8
@@ -898,6 +1034,7 @@ snapshots:
source-map: 0.6.1
xxhash-wasm: 1.0.2
optionalDependencies:
'@cloudflare/workers-types': 4.20240512.0
fsevents: 2.3.3
transitivePeerDependencies:
- bufferutil
@@ -914,4 +1051,4 @@ snapshots:
mustache: 4.2.0
stacktracey: 2.1.8
zod@3.23.6: {}
zod@3.23.8: {}

View File

@@ -36,7 +36,12 @@ api.post('/admin/new_address', async (c) => {
if (!name) {
return c.text("Please provide a name", 400)
}
return newAddress(c, name, domain, enablePrefix);
try {
const res = await newAddress(c, name, domain, enablePrefix);
return c.json(res);
} catch (e) {
return c.text(`Failed create address: ${e.message}`, 400)
}
})
api.delete('/admin/delete_address/:id', async (c) => {

View File

@@ -7,14 +7,14 @@ export const newAddress = async (c, name, domain, enablePrefix) => {
name = name.replace(/[^a-zA-Z0-9.]/g, '')
// check name length
if (name.length < 0) {
return c.text("Name too short", 400)
throw new Error("Name too short")
}
// create address
if (enablePrefix) {
name = getStringValue(c.env.PREFIX) + name;
}
if (name.length >= 30) {
return c.text("Name too long (max 30)", 400)
throw new Error("Name too long (max 30)")
}
// check domain, generate random domain
const domains = getDomains(c);
@@ -28,30 +28,27 @@ export const newAddress = async (c, name, domain, enablePrefix) => {
`INSERT INTO address(name) VALUES(?)`
).bind(name).run();
if (!success) {
return c.text("Failed to create address", 500)
throw new Error("Failed to create address")
}
} catch (e) {
if (e.message && e.message.includes("UNIQUE")) {
return c.text("Address already exists, please retry a new address", 400)
throw new Error("Address already exists")
}
return c.text("Failed to create address", 500)
throw new Error("Failed to create address")
}
let address_id = 0;
try {
address_id = await c.env.DB.prepare(
`SELECT id FROM address where name = ?`
).bind(name).first("id");
} catch (error) {
console.log(error);
}
address_id = await c.env.DB.prepare(
`SELECT id FROM address where name = ?`
).bind(name).first("id");
// create jwt
const jwt = await Jwt.sign({
address: name,
address_id: address_id
}, c.env.JWT_SECRET, "HS256")
return c.json({
jwt: jwt
})
return {
jwt: jwt,
address: name,
}
}
export const cleanup = async (c, cleanType, cleanDays) => {

View File

@@ -1,7 +1,12 @@
export const CONSTANTS = {
VERSION: 'v0.4.2',
// DB settings
ADDRESS_BLOCK_LIST_KEY: 'address_block_list',
SEND_BLOCK_LIST_KEY: 'send_block_list',
AUTO_CLEANUP_KEY: 'auto_cleanup',
USER_SETTINGS_KEY: 'user_settings',
// KV
TG_KV_PREFIX: "temp-mail-telegram"
}

View File

@@ -1,5 +1,6 @@
import { createMimeMessage } from "mimetext";
import { getBooleanValue } from "./utils";
import { sendMailToTelegram } from "./telegram_api";
async function email(message, env, ctx) {
if (env.BLACK_LIST && env.BLACK_LIST.split(",").some(word => message.from.includes(word))) {
@@ -20,6 +21,15 @@ async function email(message, env, ctx) {
console.log(`Failed save message from ${message.from} to ${message.to}`);
}
// send email to telegram
try {
await sendMailToTelegram({
env: env,
}, message.to, rawEmail);
} catch (error) {
console.log("send mail to telegram error", error);
}
// auto reply email
if (getBooleanValue(env.ENABLE_AUTO_REPLY)) {
try {

View File

@@ -106,7 +106,12 @@ api.post('/api/new_address', async (c) => {
} catch (error) {
console.error(error);
}
return newAddress(c, name, domain, true);
try {
const res = await newAddress(c, name, domain, true);
return c.json(res);
} catch (e) {
return c.text(`Failed create address: ${e.message}`, 400)
}
})
api.delete('/api/delete_address', async (c) => {

View File

@@ -0,0 +1,49 @@
import { Hono, Context } from 'hono'
import { ServerResponse } from 'node:http'
import { Writable } from 'node:stream'
import { newTelegramBot, initTelegramBotCommands, sendMailToTelegram } from './telegram'
export const api = new Hono()
export { sendMailToTelegram }
api.post("/telegram/webhook", async (c: Context) => {
const token = c.env.TELEGRAM_BOT_TOKEN;
const bot = newTelegramBot(c, token);
let body = null;
const res = new Writable();
Object.assign(res, {
headersSent: false,
setHeader: (name: string, value: string) => c.header(name, value),
end: (data: any) => body = data,
});
const reqJson = await c.req.json();
await bot.handleUpdate(reqJson, res as ServerResponse);
return c.body(body);
});
api.post("/admin/telegram/init", async (c: Context) => {
if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) {
return c.text("TELEGRAM_BOT_TOKEN and KV are required", 400);
}
const domain = new URL(c.req.url).host;
const token = c.env.TELEGRAM_BOT_TOKEN;
const webhookUrl = `https://${domain}/telegram/webhook`;
console.log(`setting webhook to ${webhookUrl}`);
const bot = newTelegramBot(c, token);
await bot.telegram.setWebhook(webhookUrl)
await initTelegramBotCommands(bot);
return c.json({
message: "webhook set successfully",
});
});
api.get("/admin/telegram/status", async (c: Context) => {
if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) {
return c.text("TELEGRAM_BOT_TOKEN and KV are required", 400);
}
const token = c.env.TELEGRAM_BOT_TOKEN;
const bot = newTelegramBot(c, token);
const info = await bot.telegram.getWebhookInfo()
const commands = await bot.telegram.getMyCommands()
return c.json({ info, commands });
});

View File

@@ -0,0 +1,269 @@
import { Context } from "hono";
import { Jwt } from 'hono/utils/jwt'
import { Telegraf, Context as TgContext, Markup } from "telegraf";
import { callbackQuery } from "telegraf/filters";
import PostalMime from 'postal-mime';
import { CONSTANTS } from "../constants";
// @ts-ignore
import { getIntValue, getDomains, getStringValue } from '../utils';
// @ts-ignore
import { newAddress } from '../common'
const COMMANDS = [
{
command: "start",
description: "开始使用"
},
{
command: "new",
description: "新建邮箱地址, 如果要自定义邮箱地址, 请输入 /new <name>@<domain>, name [a-zA-Z0-9.] 有效"
},
{
command: "address",
description: "查看邮箱地址列表"
},
{
command: "bind",
description: "绑定邮箱地址, 请输入 /bind <邮箱地址凭证>"
},
{
command: "mails",
description: "查看邮件, 请输入 /mails <邮箱地址>, 不输入地址默认查看第一个地址"
},
]
export function newTelegramBot(c: Context, token: string): Telegraf {
const bot = new Telegraf(token);
bot.command("start", async (ctx: TgContext) => {
if (ctx.chat?.type !== "private") {
return await ctx.reply("请在私聊中使用");
}
const prefix = getStringValue(c.env.PREFIX)
const domains = getDomains(c);
return await ctx.reply(
"欢迎使用本机器人\n\n"
+ (prefix ? `当前已启用前缀: ${prefix}\n` : '')
+ "新建邮箱地址, 如果要自定义邮箱地址, "
+ "请输入 /new <name>@<domain>, name [a-zA-Z0-9.] 有效\n"
+ `当前可用域名: ${JSON.stringify(domains)}\n`
+ "请使用以下命令:\n"
+ COMMANDS.map(c => `/${c.command}: ${c.description}`).join("\n")
);
});
bot.command("new", async (ctx: TgContext) => {
if (ctx.chat?.type !== "private") {
return await ctx.reply("请在私聊中使用");
}
const userId = ctx?.message?.from?.id;
if (!userId) {
return await ctx.reply("无法获取用户信息");
}
try {
// @ts-ignore
const address = ctx?.message?.text.slice("/new".length).trim();
if (!address) {
return await ctx.reply("请输入邮箱地址");
}
const [name, domain] = address.includes("@") ? address.split("@") : [address, null];
const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, { type: '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);
return await ctx.reply(`创建地址成功:\n`
+ `地址: ${res.address}\n`
+ `凭证: ${res.jwt}\n`
);
} catch (e) {
return await ctx.reply(`创建地址失败: ${(e as Error).message}`);
}
});
bot.command("bind", async (ctx: TgContext) => {
if (ctx.chat?.type !== "private") {
return await ctx.reply("请在私聊中使用");
}
const userId = ctx?.message?.from?.id;
if (!userId) {
return await ctx.reply("无法获取用户信息");
}
try {
// @ts-ignore
const jwt = ctx?.message?.text.slice("/bind".length).trim();
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(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, { type: '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);
return await ctx.reply(`绑定成功:\n`
+ `地址: ${address}`
);
}
catch (e) {
return await ctx.reply(`绑定失败: ${(e as Error).message}`);
}
});
bot.command("address", async (ctx: TgContext) => {
if (ctx.chat?.type !== "private") {
return await ctx.reply("请在私聊中使用");
}
const userId = ctx?.message?.from?.id;
if (!userId) {
return await ctx.reply("无法获取用户信息");
}
try {
const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, { type: '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;
}
}
return await ctx.reply(`地址列表:\n\n`
+ addressList.map(a => `地址: ${a}`).join("\n")
);
} catch (e) {
return await ctx.reply(`获取地址列表失败: ${(e as Error).message}`);
}
});
const queryMail = async (ctx: TgContext, queryAddress: string, mailIndex: number, edit: boolean) => {
const userId = ctx?.message?.from?.id || ctx.callbackQuery?.message?.chat?.id;
if (!userId) {
return await ctx.reply("无法获取用户信息");
}
const jwtList = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${userId}`, { type: '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;
}
}
if (!queryAddress && addressList.length > 0) {
queryAddress = addressList[0];
}
if (!addressList.includes(queryAddress)) {
return await ctx.reply(`未绑定此地址 ${queryAddress}`);
}
const raw = await c.env.DB.prepare(
`SELECT * FROM raw_mails where address = ? `
+ ` order by id desc limit 1 offset ?`
).bind(
queryAddress, mailIndex
).first("raw");
const { mail } = await parseMail(raw);
if (edit) {
return await ctx.editMessageText(mail || "无邮件",
{
...Markup.inlineKeyboard([
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
])
},
);
}
return await ctx.reply(mail || "无邮件",
{
...Markup.inlineKeyboard([
Markup.button.callback("上一条", `mail_${queryAddress}_${mailIndex - 1}`, mailIndex <= 0),
Markup.button.callback("下一条", `mail_${queryAddress}_${mailIndex + 1}`, !raw),
])
},
);
}
bot.command("mails", async ctx => {
try {
const queryAddress = ctx?.message?.text.slice("/mails".length).trim();
return await queryMail(ctx, queryAddress, 0, false);
} catch (e) {
return await ctx.reply(`获取邮件失败: ${(e as Error).message}`);
}
});
bot.on(callbackQuery("data"), async ctx => {
// Use ctx.callbackQuery.data
try {
const data = ctx.callbackQuery.data;
if (data && data.startsWith("mail_") && data.split("_").length === 3) {
const [_, queryAddress, mailIndex] = data.split("_");
await queryMail(ctx, queryAddress, parseInt(mailIndex), true);
}
}
catch (e) {
console.log(`获取邮件失败: ${(e as Error).message}`, e);
return await ctx.answerCbQuery(`获取邮件失败: ${(e as Error).message}`);
}
await ctx.answerCbQuery();
});
return bot;
}
export async function initTelegramBotCommands(bot: Telegraf) {
bot.telegram.sendMessage
await bot.telegram.setMyCommands(COMMANDS);
}
const parseMail = async (raw_mail: string) => {
if (!raw_mail) {
return {};
}
try {
const parsedEmail = await PostalMime.parse(raw_mail);
return {
isHtml: false,
mail: `From: ${parsedEmail.from ? `${parsedEmail.from.name}[${parsedEmail.from.address}]` : "无发件人"}\n`
+ `To: ${parsedEmail.to?.map(t => `${t.name}[${t.address}]`).join(" ")}\n`
+ `Subject: ${parsedEmail.subject}\n`
+ `Date: ${parsedEmail.date}\n`
+ `Content:\n${parsedEmail.text?.substring(0, 100) || "解析失败"}`
};
} catch (e) {
return {
isHtml: false,
mail: `解析邮件失败: ${(e as Error).message}`
};
}
}
export async function sendMailToTelegram(c: Context, address: string, raw_mail: string) {
if (!c.env.TELEGRAM_BOT_TOKEN || !c.env.KV) {
return;
}
const userId = await c.env.KV.get(`${CONSTANTS.TG_KV_PREFIX}:${address}`);
if (!userId) {
return;
}
const { mail } = await parseMail(raw_mail);
if (!mail) {
return;
}
const bot = newTelegramBot(c, c.env.TELEGRAM_BOT_TOKEN);
await bot.telegram.sendMessage(userId, mail);
}

View File

@@ -47,10 +47,24 @@ export const getBooleanValue = (value) => {
if (typeof value === "string") {
return value === "true";
}
console.error("Invalid boolean value", value);
console.error(`Failed to parse boolean value: ${value}`);
return false;
}
export const getIntValue = (value, defaultValue = 0) => {
if (typeof value === "number") {
return value;
}
if (typeof value === "string") {
try {
return parseInt(value);
} catch (e) {
console.error(`Failed to parse int value: ${value}`);
}
}
return defaultValue;
}
export const getDomains = (c) => {
if (!c.env.DOMAINS) {
return [];

View File

@@ -9,6 +9,7 @@ import { api as userApi } from './user_api';
import { api as adminApi } from './admin_api';
import { api as apiV1 } from './deprecated';
import { api as apiSendMail } from './mails_api/send_mail_api'
import { api as telegramApi } from './telegram_api'
import { email } from './email';
import { scheduled } from './scheduled';
@@ -107,6 +108,7 @@ app.route('/', userApi)
app.route('/', adminApi)
app.route('/', apiV1)
app.route('/', apiSendMail)
app.route('/', telegramApi)
app.get('/', async c => c.text("OK"))
app.get('/health_check', async c => c.text("OK"))

15
worker/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"lib": [
"ESNext"
],
"types": [
"@cloudflare/workers-types"
]
},
}

View File

@@ -38,6 +38,8 @@ ENABLE_AUTO_REPLY = false
# dkim config
# DKIM_SELECTOR = ""
# DKIM_PRIVATE_KEY = ""
# telegram bot
# TG_MAX_ACCOUNTS = 5
[[d1_databases]]
binding = "DB"