import { htmlToText as coreHtmlToText } from "../../core/html-to-text.ts";
import type { DeliveryResult, MailAdapter, OwnerRef, ParsedMail } from "../../core/types.ts";
import { sendDocument, sendMessage, TelegramApiError, type TelegramFile } from "./telegram-rest.ts";
// Telegram's hard cap per sendMessage call. Anything longer gets sent as a
// message.txt attachment instead, same shape as Discord's inline-body cap.
const TELEGRAM_MESSAGE_CAP = 4096;
const INLINE_BODY_CAP = 3500;
// Telegram's own limit for bot-uploaded documents.
const TELEGRAM_FILE_CAP = 50 * 1024 * 1024;
const MAX_HTML_LENGTH = 256 * 1024;
const SPARSE_TEXT_THRESHOLD = 200;
const SUBSTANTIAL_HTML_THRESHOLD = 1500;
function htmlToText(html: string): string {
return coreHtmlToText(html, {
// Sent as plain text (no parse_mode), so nothing needs escaping -- unlike
// Discord, there's no markdown for attacker-controlled mail to abuse.
escape: (text) => text,
// Link previews are disabled per-message via the API instead of by
// obscuring the URL's syntax, so it can just be printed plainly.
formatLink: (url) => `(${url})`,
});
}
function truncateAtLineBoundary(text: string, maxLength: number): string {
const lines = text.split("\n");
let result = "";
for (const line of lines) {
const candidate = result ? `${result}\n${line}` : line;
if (candidate.length > maxLength) {
break;
}
result = candidate;
}
return result || text.slice(0, maxLength);
}
export function createTelegramAdapter(botToken: string): MailAdapter {
return {
name: "telegram",
async deliver(owner: OwnerRef, mail: ParsedMail): Promise {
try {
const header = `From: ${mail.from}\nTo: ${mail.to}\nSubject: ${mail.subject}\n`;
const files: TelegramFile[] = [];
const notes: string[] = [];
const html = mail.html && mail.html.length > MAX_HTML_LENGTH ? mail.html.slice(0, MAX_HTML_LENGTH) : mail.html;
const readableText = html ? htmlToText(html) : mail.text;
let bodyText = readableText;
if (readableText.length > INLINE_BODY_CAP) {
bodyText = `${truncateAtLineBoundary(readableText, INLINE_BODY_CAP - 1)}…`;
notes.push("(full message attached)");
files.push({
filename: "message.txt",
contentType: "text/plain; charset=utf-8",
content: new TextEncoder().encode(readableText).buffer as ArrayBuffer,
});
} else if (readableText.length === 0) {
bodyText = "(no readable content)";
}
if (html && readableText.trim().length < SPARSE_TEXT_THRESHOLD && html.length > SUBSTANTIAL_HTML_THRESHOLD) {
notes.push("(mostly images, original HTML attached. It is the sender's markup, so only open it if you trust them)");
files.push({
filename: "message.html",
contentType: "text/html; charset=utf-8",
content: new TextEncoder().encode(html).buffer as ArrayBuffer,
});
}
// Telegram documents are sent one at a time (no multi-attachment
// payload the way Discord bundles files with a message), so budget
// and skip per file against its own cap rather than a combined one.
let skipped = 0;
const toSend: TelegramFile[] = [];
for (const a of mail.attachments) {
if (a.size <= TELEGRAM_FILE_CAP) {
toSend.push({ filename: a.filename, contentType: a.contentType, content: a.content });
} else {
skipped++;
}
}
if (skipped > 0) {
notes.push(`(${skipped} attachment${skipped === 1 ? "" : "s"} too large, discarded)`);
}
let content = `${header}\n${bodyText}`;
if (notes.length > 0) {
content += `\n\n${notes.join("\n")}`;
}
if (content.length > TELEGRAM_MESSAGE_CAP) {
content = truncateAtLineBoundary(content, TELEGRAM_MESSAGE_CAP - 1) + "…";
}
await sendMessage(botToken, owner.id, content);
for (const file of [...files, ...toSend]) {
await sendDocument(botToken, owner.id, file);
}
return { success: true };
} catch (err) {
if (err instanceof TelegramApiError) {
return { success: false, error: err.message };
}
return { success: false, error: err instanceof Error ? err.message : String(err) };
}
},
async notify(owner: OwnerRef, message: string): Promise {
try {
const content =
message.length > TELEGRAM_MESSAGE_CAP
? truncateAtLineBoundary(message, TELEGRAM_MESSAGE_CAP - 1) + "…"
: message;
await sendMessage(botToken, owner.id, content);
return { success: true };
} catch (err) {
if (err instanceof TelegramApiError) {
return { success: false, error: err.message };
}
return { success: false, error: err instanceof Error ? err.message : String(err) };
}
},
};
}