mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-06 20:32:55 +08:00
feat: support attachment push for Telegram and Webhook (#895)
* feat: support attachment push for Telegram and Webhook (#894) - Parse email attachments via postal-mime in commonParseMail - Send attachments via Telegram Bot API sendDocument after text message - Include base64-encoded attachments in webhook payload - Add e2e tests for webhook attachment push - Add i18n messages for attachment-related notifications Closes #894 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove user-facing error message for failed attachment send Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove unused i18n attachment messages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use sendMediaGroup for batch attachment sending Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove redundant commonParseMail call, use cached result Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: remove webhook attachment support, raw already contains attachments Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use sendDocument for single attachment, sendMediaGroup for 2+ Telegram sendMediaGroup requires 2-10 items minimum. Use sendDocument for single attachment case. Update CHANGELOG with 50MB limit info. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: batch sendMediaGroup in groups of 9, add attachments to wasm parser Telegram sendMediaGroup supports 2-10 items. Batch large attachment lists into groups of 9. Also add attachments field to commented-out wasm parser for future compatibility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add caption to attachment messages, update wasm patch Add email sender and subject as caption on Telegram attachment messages. Caption is shown on the first attachment only for sendMediaGroup. Update wasm parser patch to include attachments field mapping, and fix wasm comment to use correct field names (content_type, content as Uint8Array directly). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: unify attachment sending with sendMediaGroup for all cases sendMediaGroup works with 1+ files (tested). Remove sendDocument special case and always use sendMediaGroup with batching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: reduce sendMediaGroup batch size to 6 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: change WASM parse email comment from TODO to NOTE Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: regenerate wasm parser patch with attachments support Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add ENABLE_TG_PUSH_ATTACHMENT env var to control attachment push Add environment variable to enable/disable Telegram attachment push (default disabled). Update type definitions, wrangler template, worker-vars docs (zh/en), telegram feature docs (zh/en), and changelogs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
25
.github/config/mail-parser-wasm-worker.patch
vendored
25
.github/config/mail-parser-wasm-worker.patch
vendored
@@ -1,16 +1,14 @@
|
||||
diff --git a/worker/src/common.ts b/worker/src/common.ts
|
||||
index bd9bcc9..e7e2748 100644
|
||||
index 9b758f0..e2150b5 100644
|
||||
--- a/worker/src/common.ts
|
||||
+++ b/worker/src/common.ts
|
||||
@@ -273,23 +273,23 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
@@ -469,29 +469,29 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
}
|
||||
const raw_mail = parsedEmailContext.rawEmail;
|
||||
// TODO: WASM parse email
|
||||
// NOTE: WASM parse email
|
||||
- // try {
|
||||
- // const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
+ try {
|
||||
+ const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
-
|
||||
- // const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
- // parsedEmailContext.parsedEmail = {
|
||||
- // sender: parsedEmail.sender || "",
|
||||
@@ -20,11 +18,20 @@ index bd9bcc9..e7e2748 100644
|
||||
- // (header) => ({ key: header.key, value: header.value })
|
||||
- // ) || [],
|
||||
- // html: parsedEmail.body_html || "",
|
||||
- // attachments: (parsedEmail.attachments || []).map(att => ({
|
||||
- // filename: att.filename || "attachment",
|
||||
- // mimeType: att.content_type || "application/octet-stream",
|
||||
- // content: att.content,
|
||||
- // disposition: "attachment",
|
||||
- // })),
|
||||
- // };
|
||||
- // return parsedEmailContext.parsedEmail;
|
||||
- // } catch (e) {
|
||||
- // console.error("Failed use mail-parser-wasm-worker to parse email", e);
|
||||
- // }
|
||||
+ try {
|
||||
+ const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
+
|
||||
+ const parsedEmail = parse_message_wrapper(raw_mail);
|
||||
+ parsedEmailContext.parsedEmail = {
|
||||
+ sender: parsedEmail.sender || "",
|
||||
@@ -34,6 +41,12 @@ index bd9bcc9..e7e2748 100644
|
||||
+ (header) => ({ key: header.key, value: header.value })
|
||||
+ ) || [],
|
||||
+ html: parsedEmail.body_html || "",
|
||||
+ attachments: (parsedEmail.attachments || []).map(att => ({
|
||||
+ filename: att.filename || "attachment",
|
||||
+ mimeType: att.content_type || "application/octet-stream",
|
||||
+ content: att.content,
|
||||
+ disposition: "attachment",
|
||||
+ })),
|
||||
+ };
|
||||
+ return parsedEmailContext.parsedEmail;
|
||||
+ } catch (e) {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
- feat: |自动回复| 发件人过滤支持正则表达式匹配,使用 `/pattern/` 语法(如 `/@example\.com$/`),同时保持前缀匹配的向后兼容
|
||||
- feat: |Turnstile| 新增全局登录表单 Turnstile 人机验证,通过 `ENABLE_GLOBAL_TURNSTILE_CHECK` 环境变量控制(#767)
|
||||
- feat: |Telegram| Telegram 推送支持发送邮件附件(单文件限制 50MB),多附件通过 `sendMediaGroup` 批量发送,通过 `ENABLE_TG_PUSH_ATTACHMENT` 环境变量开启(#894)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
- feat: |Auto Reply| Add regex matching support for sender filter using `/pattern/` syntax (e.g. `/@example\.com$/`), backward compatible with prefix matching
|
||||
- feat: |Turnstile| Add global Turnstile CAPTCHA for all login forms via `ENABLE_GLOBAL_TURNSTILE_CHECK` env var (#767)
|
||||
- feat: |Telegram| Support sending email attachments in Telegram push (50MB per file limit), multiple attachments sent via `sendMediaGroup`, controlled by `ENABLE_TG_PUSH_ATTACHMENT` env var (#894)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
|
||||
@@ -80,6 +80,17 @@ Admins can enable **global mail push** in the admin panel under `Settings` -> `T
|
||||
> [!NOTE]
|
||||
> Global push and per-user push can work simultaneously. If an address is bound to a user who is also in the global push list, they will receive two notifications.
|
||||
|
||||
### Attachment Push
|
||||
|
||||
> [!NOTE]
|
||||
> This feature is available since v1.5.0
|
||||
|
||||
Set `ENABLE_TG_PUSH_ATTACHMENT = true` to enable sending email attachments via Telegram push.
|
||||
|
||||
- Single file size limit is 50MB (Telegram Bot API limit), oversized attachments are skipped
|
||||
- Multiple attachments are sent in batches via `sendMediaGroup`, up to 6 per batch
|
||||
- The first attachment includes the sender and subject as caption
|
||||
|
||||
## Mini App
|
||||
|
||||
Can be deployed via command line or UI interface
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
| `TG_MAX_ADDRESS` | Number | Maximum number of mailboxes that can be bound to telegram bot | `5` |
|
||||
| `TG_BOT_INFO` | Text | Optional, telegram BOT_INFO, predefined BOT_INFO can reduce webhook latency | `{}` |
|
||||
| `TG_ALLOW_USER_LANG` | Text/JSON | Allow users to switch language via `/lang` command, default `false` | `true` |
|
||||
| `ENABLE_TG_PUSH_ATTACHMENT` | Boolean | Enable sending email attachments via Telegram push, default `false`, 50MB per file limit | `true` |
|
||||
|
||||
> [!NOTE]
|
||||
> Telegram functionality requires email parsing, free tier CPU is limited, may cause large email parsing timeout
|
||||
|
||||
@@ -80,6 +80,17 @@ Telegram Bot 支持 **每用户独立推送**,用户绑定地址后,该地
|
||||
> [!NOTE]
|
||||
> 全局推送和每用户推送可以同时生效。如果某地址已绑定用户,同时该用户也在全局推送列表中,则会收到两条通知。
|
||||
|
||||
### 附件推送
|
||||
|
||||
> [!NOTE]
|
||||
> 此功能从 v1.5.0 版本开始支持
|
||||
|
||||
配置 `ENABLE_TG_PUSH_ATTACHMENT = true` 后,邮件附件会随推送一起发送到 Telegram。
|
||||
|
||||
- 单个附件大小限制 50MB(Telegram Bot API 限制),超过的附件会被跳过
|
||||
- 多附件通过 `sendMediaGroup` 批量发送,每批最多 6 个
|
||||
- 第一个附件会附带邮件发件人和主题信息作为 caption
|
||||
|
||||
## Mini App
|
||||
|
||||
可以通过命令行部署,或者 UI 界面部署
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
| `TG_MAX_ADDRESS` | 数字 | telegram bot 最多绑定邮箱数量 | `5` |
|
||||
| `TG_BOT_INFO` | 文本 | 可不配置,telegram BOT_INFO,预定义的 BOT_INFO 可以降低 webhook 的延迟 | `{}` |
|
||||
| `TG_ALLOW_USER_LANG`| 文本/JSON | 是否允许用户通过 `/lang` 命令切换语言,默认 `false` | `true`|
|
||||
| `ENABLE_TG_PUSH_ATTACHMENT`| 布尔值 | 是否启用 Telegram 推送邮件附件,默认 `false`,单文件限制 50MB | `true`|
|
||||
|
||||
> [!NOTE]
|
||||
> Telegram 功能需要解析邮件,免费版 CPU 有限,可能会导致大邮件解析超时
|
||||
|
||||
@@ -456,7 +456,8 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
headers?: Record<string, string>[]
|
||||
headers?: Record<string, string>[],
|
||||
attachments?: ParsedEmailAttachment[],
|
||||
} | undefined> => {
|
||||
// check parsed email context is valid
|
||||
if (!parsedEmailContext || !parsedEmailContext.rawEmail) {
|
||||
@@ -467,7 +468,7 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
return parsedEmailContext.parsedEmail;
|
||||
}
|
||||
const raw_mail = parsedEmailContext.rawEmail;
|
||||
// TODO: WASM parse email
|
||||
// NOTE: WASM parse email
|
||||
// try {
|
||||
// const { parse_message_wrapper } = await import('mail-parser-wasm-worker');
|
||||
|
||||
@@ -480,6 +481,12 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
// (header) => ({ key: header.key, value: header.value })
|
||||
// ) || [],
|
||||
// html: parsedEmail.body_html || "",
|
||||
// attachments: (parsedEmail.attachments || []).map(att => ({
|
||||
// filename: att.filename || "attachment",
|
||||
// mimeType: att.content_type || "application/octet-stream",
|
||||
// content: att.content,
|
||||
// disposition: "attachment",
|
||||
// })),
|
||||
// };
|
||||
// return parsedEmailContext.parsedEmail;
|
||||
// } catch (e) {
|
||||
@@ -494,6 +501,12 @@ export const commonParseMail = async (parsedEmailContext: ParsedEmailContext): P
|
||||
text: parsedEmail.text || "",
|
||||
html: parsedEmail.html || "",
|
||||
headers: parsedEmail.headers || [],
|
||||
attachments: (parsedEmail.attachments || []).map(att => ({
|
||||
filename: att.filename || "attachment",
|
||||
mimeType: att.mimeType || "application/octet-stream",
|
||||
content: new Uint8Array(att.content),
|
||||
disposition: att.disposition || "attachment",
|
||||
})),
|
||||
};
|
||||
return parsedEmailContext.parsedEmail;
|
||||
}
|
||||
@@ -605,7 +618,7 @@ export async function triggerWebhook(
|
||||
subject: parsedEmail?.subject || "",
|
||||
raw: parsedEmailContext.rawEmail || "",
|
||||
parsedText: parsedEmail?.text || "",
|
||||
parsedHtml: parsedEmail?.html || ""
|
||||
parsedHtml: parsedEmail?.html || "",
|
||||
}
|
||||
for (const settings of webhookList) {
|
||||
const res = await sendWebhook(settings, webhookMail);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { Context } from "hono";
|
||||
import { Telegraf, Context as TgContext, Markup } from "telegraf";
|
||||
import { callbackQuery } from "telegraf/filters";
|
||||
import { InputMediaDocument } from "telegraf/types";
|
||||
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { getBooleanValue, getDomains, getJsonObjectValue, getStringValue } from '../utils';
|
||||
@@ -12,6 +13,8 @@ import { UserFromGetMe } from "telegraf/types";
|
||||
import i18n from "../i18n";
|
||||
import { LocaleMessages } from "../i18n/type";
|
||||
|
||||
const TG_MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB Telegram Bot API limit
|
||||
|
||||
// Helper to get messages by userId
|
||||
const getTgMessages = async (
|
||||
c: Context<HonoCustomType>,
|
||||
@@ -424,6 +427,7 @@ export async function sendMailToTelegram(
|
||||
const buildAndSend = async (targetUserId: string, msgs: LocaleMessages) => {
|
||||
const { mail } = await parseMail(msgs, parsedEmailContext, address, new Date().toUTCString());
|
||||
if (!mail) return;
|
||||
const attachments = parsedEmailContext.parsedEmail?.attachments || [];
|
||||
const buttons = [];
|
||||
if (settings?.miniAppUrl && mailId) {
|
||||
const url = new URL(settings.miniAppUrl);
|
||||
@@ -434,6 +438,32 @@ export async function sendMailToTelegram(
|
||||
await bot.telegram.sendMessage(targetUserId, mail, {
|
||||
...Markup.inlineKeyboard([...buttons])
|
||||
});
|
||||
// send attachments
|
||||
if (!getBooleanValue(c.env.ENABLE_TG_PUSH_ATTACHMENT)) return;
|
||||
const validAttachments = attachments.filter(att => {
|
||||
if (att.content.byteLength > TG_MAX_FILE_SIZE) {
|
||||
console.log(`Skipping attachment ${att.filename}: ${(att.content.byteLength / 1024 / 1024).toFixed(1)}MB exceeds 50MB limit`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (validAttachments.length > 0) {
|
||||
const caption = `From: ${parsedEmailContext.parsedEmail?.sender || ""}\nSubject: ${parsedEmailContext.parsedEmail?.subject || ""}`;
|
||||
const batchSize = 6;
|
||||
for (let i = 0; i < validAttachments.length; i += batchSize) {
|
||||
const batch = validAttachments.slice(i, i + batchSize);
|
||||
try {
|
||||
const mediaGroup: InputMediaDocument[] = batch.map((att, idx) => ({
|
||||
type: 'document',
|
||||
media: { source: Buffer.from(att.content), filename: att.filename },
|
||||
...(i === 0 && idx === 0 ? { caption } : {}),
|
||||
}));
|
||||
await bot.telegram.sendMediaGroup(targetUserId, mediaGroup);
|
||||
} catch (e) {
|
||||
console.error(`Failed to send attachment batch ${i / batchSize + 1}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (globalPush) {
|
||||
|
||||
11
worker/src/types.d.ts
vendored
11
worker/src/types.d.ts
vendored
@@ -86,6 +86,7 @@ type Bindings = {
|
||||
TG_MAX_ADDRESS: number | undefined
|
||||
TG_BOT_INFO: string | object | undefined
|
||||
TG_ALLOW_USER_LANG: string | boolean | undefined
|
||||
ENABLE_TG_PUSH_ATTACHMENT: string | boolean | undefined
|
||||
|
||||
// webhook config
|
||||
FRONTEND_URL: string | undefined
|
||||
@@ -135,6 +136,13 @@ type RPCEmailMessage = {
|
||||
headers: object | undefined | null,
|
||||
}
|
||||
|
||||
type ParsedEmailAttachment = {
|
||||
filename: string,
|
||||
mimeType: string,
|
||||
content: Uint8Array,
|
||||
disposition: string,
|
||||
}
|
||||
|
||||
type ParsedEmailContext = {
|
||||
rawEmail: string,
|
||||
parsedEmail?: {
|
||||
@@ -142,7 +150,8 @@ type ParsedEmailContext = {
|
||||
subject: string,
|
||||
text: string,
|
||||
html: string,
|
||||
headers?: Record<string, string>[]
|
||||
headers?: Record<string, string>[],
|
||||
attachments?: ParsedEmailAttachment[],
|
||||
} | undefined
|
||||
}
|
||||
|
||||
|
||||
@@ -93,6 +93,8 @@ ENABLE_AUTO_REPLY = false
|
||||
# TG_BOT_INFO = "{}"
|
||||
# allow user to switch language via /lang command
|
||||
# TG_ALLOW_USER_LANG = true
|
||||
# enable sending email attachments via Telegram push (50MB per file limit)
|
||||
# ENABLE_TG_PUSH_ATTACHMENT = true
|
||||
# global forward address list, if set, all emails will be forwarded to these addresses
|
||||
# FORWARD_ADDRESS_LIST = ["xxx@xxx.com"]
|
||||
# subdomain forward address list, if set, subdomain emails will be forwarded to these addresses
|
||||
|
||||
Reference in New Issue
Block a user