mirror of
https://github.com/dreamhunter2333/cloudflare_temp_email.git
synced 2026-05-11 18:10:01 +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:
@@ -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