-
+
Github
-
+
Discord
-
+
@@ -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;
}
diff --git a/vitepress-docs/docs/en/cli.md b/vitepress-docs/docs/en/cli.md
index c1077a5c..42e4b94e 100644
--- a/vitepress-docs/docs/en/cli.md
+++ b/vitepress-docs/docs/en/cli.md
@@ -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"
diff --git a/vitepress-docs/docs/zh/guide/cli/worker.md b/vitepress-docs/docs/zh/guide/cli/worker.md
index e2c4e322..33227d8e 100644
--- a/vitepress-docs/docs/zh/guide/cli/worker.md
+++ b/vitepress-docs/docs/zh/guide/cli/worker.md
@@ -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`
diff --git a/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md b/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md
index e7800e7a..456ad3d2 100644
--- a/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md
+++ b/vitepress-docs/docs/zh/guide/feature/config-smtp-proxy.md
@@ -28,6 +28,8 @@ docker-compose up -d
修改 docker-compose.yaml 中的环境变量, 注意选择合适的 `tag`
+`proxy_url` 为 `worker` 的 URL 地址
+
```yaml
services:
smtp_proxy_server:
diff --git a/vitepress-docs/docs/zh/guide/feature/mail-api.md b/vitepress-docs/docs/zh/guide/feature/mail-api.md
index 96dbdfaa..3335a3c9 100644
--- a/vitepress-docs/docs/zh/guide/feature/mail-api.md
+++ b/vitepress-docs/docs/zh/guide/feature/mail-api.md
@@ -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"
diff --git a/vitepress-docs/docs/zh/guide/ui/worker.md b/vitepress-docs/docs/zh/guide/ui/worker.md
index 7a987b39..8c710e94 100644
--- a/vitepress-docs/docs/zh/guide/ui/worker.md
+++ b/vitepress-docs/docs/zh/guide/ui/worker.md
@@ -45,3 +45,10 @@


+
+9. Telegram Bot 配置
+
+ > [!NOTE]
+ > 如果不需要 Telegram Bot, 可跳过此步骤
+
+ 请先创建一个 Telegram Bot,然后获取 `token`,然后执行下面的命令,将 `token` 添加到 `Variables` 中, Name: `TELEGRAM_BOT_TOKEN`
diff --git a/worker/.gitignore b/worker/.gitignore
index 299aafcc..ae1cca8d 100644
--- a/worker/.gitignore
+++ b/worker/.gitignore
@@ -131,3 +131,4 @@ dist
.wrangler
wrangler.toml
+.dev.vars
diff --git a/worker/package.json b/worker/package.json
index 4312f6b1..f2208754 100644
--- a/worker/package.json
+++ b/worker/package.json
@@ -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"
+ }
}
}
diff --git a/worker/patches/telegraf@4.16.3.patch b/worker/patches/telegraf@4.16.3.patch
new file mode 100644
index 00000000..7e68d59d
--- /dev/null
+++ b/worker/patches/telegraf@4.16.3.patch
@@ -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;
diff --git a/worker/pnpm-lock.yaml b/worker/pnpm-lock.yaml
index 52820311..700b8de5 100644
--- a/worker/pnpm-lock.yaml
+++ b/worker/pnpm-lock.yaml
@@ -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: {}
diff --git a/worker/src/admin_api/index.js b/worker/src/admin_api/index.js
index ee102fec..90e25c30 100644
--- a/worker/src/admin_api/index.js
+++ b/worker/src/admin_api/index.js
@@ -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) => {
diff --git a/worker/src/common.js b/worker/src/common.js
index c992eb60..ae82e77f 100644
--- a/worker/src/common.js
+++ b/worker/src/common.js
@@ -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) => {
diff --git a/worker/src/constants.js b/worker/src/constants.ts
similarity index 76%
rename from worker/src/constants.js
rename to worker/src/constants.ts
index 7f942c0e..20895243 100644
--- a/worker/src/constants.js
+++ b/worker/src/constants.ts
@@ -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"
}
diff --git a/worker/src/email.js b/worker/src/email.js
index c09b4a6b..59f3eaa0 100644
--- a/worker/src/email.js
+++ b/worker/src/email.js
@@ -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 {
diff --git a/worker/src/mails_api/index.js b/worker/src/mails_api/index.js
index 2c33d894..93199fbe 100644
--- a/worker/src/mails_api/index.js
+++ b/worker/src/mails_api/index.js
@@ -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) => {
diff --git a/worker/src/telegram_api/index.ts b/worker/src/telegram_api/index.ts
new file mode 100644
index 00000000..a6485a17
--- /dev/null
+++ b/worker/src/telegram_api/index.ts
@@ -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 });
+});
diff --git a/worker/src/telegram_api/telegram.ts b/worker/src/telegram_api/telegram.ts
new file mode 100644
index 00000000..43b5e408
--- /dev/null
+++ b/worker/src/telegram_api/telegram.ts
@@ -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 [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 [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);
+}
diff --git a/worker/src/utils.js b/worker/src/utils.js
index 40d3c124..e0e445c7 100644
--- a/worker/src/utils.js
+++ b/worker/src/utils.js
@@ -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 [];
diff --git a/worker/src/worker.js b/worker/src/worker.js
index c04d9c0b..a585b14f 100644
--- a/worker/src/worker.js
+++ b/worker/src/worker.js
@@ -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"))
diff --git a/worker/tsconfig.json b/worker/tsconfig.json
new file mode 100644
index 00000000..ffafc905
--- /dev/null
+++ b/worker/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "strict": true,
+ "skipLibCheck": true,
+ "lib": [
+ "ESNext"
+ ],
+ "types": [
+ "@cloudflare/workers-types"
+ ]
+ },
+}
diff --git a/worker/wrangler.toml.template b/worker/wrangler.toml.template
index 790f5ded..ef3b1718 100644
--- a/worker/wrangler.toml.template
+++ b/worker/wrangler.toml.template
@@ -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"