feats(email): add telegram pusher
This commit is contained in:
@@ -28,6 +28,8 @@ WR.DO 是一个一站式网络工具平台,集成短链服务、临时邮箱
|
||||
- 可创建无限数量邮箱
|
||||
- 支持接收无限制邮件 (依赖 Cloudflare Email Worker)
|
||||
- 支持发送邮件(依赖 Resend)
|
||||
- 支持 Catch-All 配置
|
||||
- 支持 Telegram 推送(多频道/群组)
|
||||
- 支持调用 API 创建邮箱
|
||||
- 支持调用 API 获取收件箱邮件
|
||||
-
|
||||
|
||||
@@ -27,6 +27,8 @@ WR.DO is a all-in-one web utility platform featuring short links with analytics,
|
||||
- Unlimited mailbox creation
|
||||
- Receive unlimited emails (powered by Cloudflare Email Worker)
|
||||
- Send emails (powered by Resend)
|
||||
- Support catch-all emails
|
||||
- Support push to telegram groups
|
||||
- API endpoints for mailbox creation
|
||||
- API endpoints for inbox retrieval
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { toast } from "sonner";
|
||||
import useSWR from "swr";
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/components/ui/collapsible";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -31,15 +33,21 @@ export default function AppConfigs({}: {}) {
|
||||
} = useSWR<Record<string, any>>("/api/admin/configs", fetcher);
|
||||
const [notification, setNotification] = useState("");
|
||||
const [catchAllEmails, setCatchAllEmails] = useState("");
|
||||
const [tgBotToken, setTgBotToken] = useState("");
|
||||
const [tgChatId, setTgChatId] = useState("");
|
||||
const [tgTemplate, setTgTemplate] = useState("");
|
||||
const [tgWhiteList, setTgWhiteList] = useState("");
|
||||
|
||||
const t = useTranslations("Setting");
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && configs?.system_notification) {
|
||||
setNotification(configs.system_notification);
|
||||
}
|
||||
if (!isLoading && configs?.catch_all_emails) {
|
||||
setCatchAllEmails(configs.catch_all_emails);
|
||||
if (!isLoading && configs) {
|
||||
setNotification(configs?.system_notification);
|
||||
setCatchAllEmails(configs?.catch_all_emails);
|
||||
setTgBotToken(configs?.tg_email_bot_token);
|
||||
setTgChatId(configs?.tg_email_chat_id);
|
||||
setTgTemplate(configs?.tg_email_template);
|
||||
setTgWhiteList(configs?.tg_email_target_white_list);
|
||||
}
|
||||
// 计算登录方式数量
|
||||
if (!isLoading) {
|
||||
@@ -60,10 +68,10 @@ export default function AppConfigs({}: {}) {
|
||||
body: JSON.stringify({ key, value, type }),
|
||||
});
|
||||
if (res.ok) {
|
||||
toast.success("Updated!");
|
||||
toast.success("Saved");
|
||||
mutate();
|
||||
} else {
|
||||
toast.error("Failed!", {
|
||||
toast.error("Failed to save", {
|
||||
description: await res.text(),
|
||||
});
|
||||
}
|
||||
@@ -71,11 +79,7 @@ export default function AppConfigs({}: {}) {
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<Skeleton className="h-48 w-full rounded-lg" />
|
||||
</>
|
||||
);
|
||||
return <Skeleton className="h-48 w-full rounded-lg" />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -86,7 +90,7 @@ export default function AppConfigs({}: {}) {
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
<Icons.settings className="ml-3 size-4 transition-all group-hover:scale-110" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<CollapsibleContent className="space-y-3 bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
@@ -229,9 +233,6 @@ export default function AppConfigs({}: {}) {
|
||||
)
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -249,90 +250,279 @@ export default function AppConfigs({}: {}) {
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
<Icons.mail className="ml-3 size-4 transition-all group-hover:scale-110" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<CollapsibleContent className="space-y-3 bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<div className="space-y-6">
|
||||
{/* Catch-All */}
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="flex items-center gap-1 font-medium">
|
||||
Catch-All <Badge>Beta</Badge>
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Enable email catch-all, all user's email address which created on this platform will be redirected to the catch-all email address",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={configs.enable_email_catch_all}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_email_catch_all", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Catch-All Email Address")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder="1@a.com,2@b.com"
|
||||
rows={5}
|
||||
// defaultValue={configs.catch_all_emails}
|
||||
value={catchAllEmails}
|
||||
disabled={!configs.enable_email_catch_all}
|
||||
onChange={(e) => setCatchAllEmails(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || catchAllEmails === configs.catch_all_emails
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(catchAllEmails, "catch_all_emails", "STRING")
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
Catch-All <Badge>Beta</Badge>
|
||||
</p>
|
||||
<p className="text-start text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Enable email catch-all, all user's email address which created on this platform will be redirected to the catch-all email address",
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{configs && (
|
||||
<div
|
||||
className="ml-auto flex items-center gap-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{configs.enable_email_catch_all &&
|
||||
!configs.catch_all_emails && (
|
||||
<Badge className="" variant={"yellow"}>
|
||||
Need Configs
|
||||
</Badge>
|
||||
)}
|
||||
<Switch
|
||||
defaultChecked={configs.enable_email_catch_all}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_email_catch_all", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Message Pusher */}
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="flex items-center gap-1 font-medium">
|
||||
{t("Message Pusher")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Push message to third-party services, such as Telegram, 飞书 etc",
|
||||
<Icons.chevronDown className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-4 space-y-4 rounded-md border p-4 shadow-md">
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">
|
||||
{t("Catch-All Email Address")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder="1@a.com,2@b.com"
|
||||
rows={5}
|
||||
// defaultValue={configs.catch_all_emails}
|
||||
value={catchAllEmails}
|
||||
disabled={!configs.enable_email_catch_all}
|
||||
onChange={(e) => setCatchAllEmails(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending ||
|
||||
catchAllEmails === configs.catch_all_emails
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(
|
||||
catchAllEmails,
|
||||
"catch_all_emails",
|
||||
"STRING",
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<Switch
|
||||
defaultChecked={false}
|
||||
disabled
|
||||
// onCheckedChange={(v) =>
|
||||
// handleChange(v, "enable_email_catch_all", "BOOLEAN")
|
||||
// }
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Telegram */}
|
||||
<Collapsible>
|
||||
<CollapsibleTrigger className="flex w-full items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
{t("Telegram Pusher")} <Badge>Beta</Badge>
|
||||
</p>
|
||||
<p className="text-start text-xs text-muted-foreground">
|
||||
{t("Push message to Telegram groups")}.{" "}
|
||||
<Link
|
||||
href="/docs/developer/telegram-bot"
|
||||
className="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
{t("How to configure Telegram bot")} ?
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div
|
||||
className="ml-auto flex items-center gap-3"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{configs.enable_tg_email_push &&
|
||||
(!configs.tg_email_bot_token ||
|
||||
!configs.tg_email_chat_id) && (
|
||||
<Badge className="" variant={"yellow"}>
|
||||
Need Configs
|
||||
</Badge>
|
||||
)}
|
||||
<Switch
|
||||
defaultChecked={configs.enable_tg_email_push}
|
||||
onCheckedChange={(v) =>
|
||||
handleChange(v, "enable_tg_email_push", "BOOLEAN")
|
||||
}
|
||||
/>
|
||||
<Icons.chevronDown className="size-4" />
|
||||
</div>
|
||||
)}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="mt-4 space-y-4 rounded-md border p-4 shadow-md">
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Telegram Bot Token")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set Telegram bot token, Only works when Telegram pusher is enabled",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Input
|
||||
className="bg-white dark:bg-neutral-700"
|
||||
placeholder="Enter your Telegram bot token"
|
||||
type="password"
|
||||
value={tgBotToken}
|
||||
disabled={!configs.enable_tg_email_push}
|
||||
onChange={(e) => setTgBotToken(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || tgBotToken === configs.tg_email_bot_token
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(
|
||||
tgBotToken,
|
||||
"tg_email_bot_token",
|
||||
"STRING",
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">{t("Telegram Group ID")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set Telegram group ID, split by comma if more than one, such as: -10054275724,-10045343642",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder=""
|
||||
rows={5}
|
||||
value={tgChatId}
|
||||
disabled={!configs.enable_tg_email_push}
|
||||
onChange={(e) => setTgChatId(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || tgChatId === configs.tg_email_chat_id
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(tgChatId, "tg_email_chat_id", "STRING")
|
||||
}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">
|
||||
{t("Telegram Message Template")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("Set Telegram email message template")}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder=""
|
||||
rows={5}
|
||||
value={tgTemplate}
|
||||
disabled={!configs.enable_tg_email_push}
|
||||
onChange={(e) => setTgTemplate(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending || tgTemplate === configs.tg_email_template
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(
|
||||
tgTemplate,
|
||||
"tg_email_template",
|
||||
"STRING",
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
<p className="font-medium">
|
||||
{t("Telegram Push Email White List")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(
|
||||
"Set Telegram push email white list, split by comma, if not set, will push all emails",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
{configs && (
|
||||
<div className="flex w-full items-start gap-2">
|
||||
<Textarea
|
||||
className="h-16 max-h-32 min-h-9 resize-y bg-white dark:bg-neutral-700"
|
||||
placeholder=""
|
||||
rows={5}
|
||||
value={tgWhiteList}
|
||||
disabled={!configs.enable_tg_email_push}
|
||||
onChange={(e) => setTgWhiteList(e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
className="h-9 text-nowrap"
|
||||
disabled={
|
||||
isPending ||
|
||||
tgWhiteList === configs.tg_email_target_white_list
|
||||
}
|
||||
onClick={() =>
|
||||
handleChange(
|
||||
tgWhiteList,
|
||||
"tg_email_target_white_list",
|
||||
"STRING",
|
||||
)
|
||||
}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* Webhook */}
|
||||
<div className="flex flex-col items-start justify-start gap-3">
|
||||
<div className="space-y-1 leading-none">
|
||||
@@ -357,9 +547,6 @@ export default function AppConfigs({}: {}) {
|
||||
handleChange(catchAllEmails, "catch_all_emails", "STRING")
|
||||
}
|
||||
>
|
||||
{isPending && (
|
||||
<Icons.spinner className="mr-1 size-4 animate-spin" />
|
||||
)}
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -375,7 +562,7 @@ export default function AppConfigs({}: {}) {
|
||||
<Icons.chevronDown className="ml-auto size-4" />
|
||||
<Icons.globeLock className="ml-3 size-4 transition-all group-hover:scale-110" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="space-y-3 border-t bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<CollapsibleContent className="space-y-3 bg-neutral-100 p-4 dark:bg-neutral-800">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="space-y-1 leading-none">
|
||||
|
||||
@@ -26,6 +26,11 @@ export async function GET(req: NextRequest) {
|
||||
"enable_email_password_login",
|
||||
"enable_email_catch_all",
|
||||
"catch_all_emails",
|
||||
"enable_tg_email_push",
|
||||
"tg_email_bot_token",
|
||||
"tg_email_chat_id",
|
||||
"tg_email_template",
|
||||
"tg_email_target_white_list",
|
||||
]);
|
||||
|
||||
return Response.json(configs, { status: 200 });
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getMultipleConfigs } from "@/lib/dto/system-config";
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const data = (await req.json()) as OriginalEmail;
|
||||
// console.log("Received email:", data);
|
||||
if (!data) {
|
||||
return Response.json("No email data received", { status: 400 });
|
||||
}
|
||||
@@ -12,8 +11,14 @@ export async function POST(req: Request) {
|
||||
const configs = await getMultipleConfigs([
|
||||
"enable_email_catch_all",
|
||||
"catch_all_emails",
|
||||
"enable_tg_email_push",
|
||||
"tg_email_bot_token",
|
||||
"tg_email_chat_id",
|
||||
"tg_email_template",
|
||||
"tg_email_target_white_list",
|
||||
]);
|
||||
|
||||
// Catch-all
|
||||
if (configs.enable_email_catch_all) {
|
||||
const validEmails = parseAndValidateEmails(configs.catch_all_emails);
|
||||
|
||||
@@ -24,19 +29,26 @@ export async function POST(req: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
// 方案1: 转发给所有配置的邮箱
|
||||
const forwardPromises = validEmails.map((email) =>
|
||||
saveForwardEmail({ ...data, to: email }),
|
||||
);
|
||||
|
||||
await Promise.all(forwardPromises);
|
||||
|
||||
// 方案2: 只想转发给第一个邮箱
|
||||
// await saveForwardEmail({ ...data, to: validEmails[0] });
|
||||
} else {
|
||||
await saveForwardEmail(data);
|
||||
}
|
||||
|
||||
// Telegram
|
||||
if (configs.enable_tg_email_push) {
|
||||
const shouldPush = shouldPushToTelegram(
|
||||
data,
|
||||
configs.tg_email_target_white_list,
|
||||
);
|
||||
if (shouldPush) {
|
||||
await sendToTelegram(data, configs);
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ status: 200 });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
@@ -70,3 +82,157 @@ function parseAndValidateEmails(emailsString: string): string[] {
|
||||
|
||||
return validEmails;
|
||||
}
|
||||
|
||||
/* Pusher */
|
||||
function shouldPushToTelegram(
|
||||
email: OriginalEmail,
|
||||
whiteList: string,
|
||||
): boolean {
|
||||
if (!whiteList || whiteList.trim() === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解析白名单
|
||||
const whiteListArray = whiteList
|
||||
.split(",")
|
||||
.map((email) => email.trim())
|
||||
.filter((email) => email.length > 0);
|
||||
|
||||
return whiteListArray.includes(email.to);
|
||||
}
|
||||
|
||||
async function sendToTelegram(email: OriginalEmail, configs: any) {
|
||||
const { tg_email_bot_token, tg_email_chat_id, tg_email_template } = configs;
|
||||
|
||||
if (!tg_email_bot_token || !tg_email_chat_id) {
|
||||
console.error("Telegram bot token or chat ID not configured");
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析多个 chat ID(支持逗号分隔)
|
||||
const chatIds = tg_email_chat_id
|
||||
.split(",")
|
||||
.map((id: string) => id.trim())
|
||||
.filter((id: string) => id.length > 0);
|
||||
|
||||
if (chatIds.length === 0) {
|
||||
console.error("No valid chat IDs found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const message = formatEmailForTelegram(email, tg_email_template);
|
||||
|
||||
const sendPromises = chatIds.map(async (chatId: string) => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.telegram.org/bot${tg_email_bot_token}/sendMessage`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_id: chatId,
|
||||
text: message,
|
||||
parse_mode: "Markdown",
|
||||
disable_web_page_preview: true,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
console.error(
|
||||
`Failed to send message to Telegram chat ${chatId}:`,
|
||||
error,
|
||||
);
|
||||
return { chatId, success: false, error };
|
||||
} else {
|
||||
console.log(`Email successfully sent to Telegram chat ${chatId}`);
|
||||
return { chatId, success: true };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error sending to Telegram chat ${chatId}:`, error);
|
||||
return { chatId, success: false, error };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(sendPromises);
|
||||
|
||||
const successCount = results.filter((r) => r.success).length;
|
||||
const totalCount = results.length;
|
||||
|
||||
console.log(
|
||||
`Telegram push completed: ${successCount}/${totalCount} successful`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error in sendToTelegram:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化邮件内容为 Telegram 消息(Markdown 格式)
|
||||
function formatEmailForTelegram(
|
||||
email: OriginalEmail,
|
||||
template?: string,
|
||||
): string {
|
||||
if (template) {
|
||||
return template
|
||||
.replace("{{from}}", escapeMarkdown(email.from))
|
||||
.replace("{{fromName}}", escapeMarkdown(email.fromName || email.from))
|
||||
.replace("{{to}}", escapeMarkdown(email.to))
|
||||
.replace("{{subject}}", escapeMarkdown(email.subject || "No Subject"))
|
||||
.replace("{{text}}", escapeMarkdown(email.text || "No content"))
|
||||
.replace("{{date}}", email.date || "--");
|
||||
}
|
||||
|
||||
const fromInfo = email.fromName
|
||||
? `${escapeMarkdown(email.fromName)} <${escapeMarkdown(email.from)}>`
|
||||
: escapeMarkdown(email.from);
|
||||
const subject = escapeMarkdown(email.subject || "No Subject");
|
||||
const content = escapeMarkdown(
|
||||
email.text || email.html?.replace(/<[^>]*>/g, "") || "No content",
|
||||
);
|
||||
const date = email.date || "Unknown date";
|
||||
|
||||
// 限制内容长度
|
||||
const maxContentLength = 2000;
|
||||
const truncatedContent =
|
||||
content.length > maxContentLength
|
||||
? content.substring(0, maxContentLength) + "..."
|
||||
: content;
|
||||
|
||||
let message = `📧 *New Email*\n\n`;
|
||||
message += `*From:* ${fromInfo}\n`;
|
||||
message += `*To:* ${escapeMarkdown(email.to)}\n`;
|
||||
message += `*Subject:* ${subject}\n`;
|
||||
message += `*Date:* ${date}\n\n`;
|
||||
message += `*Content:*\n\`\`\`\n${truncatedContent}\n\`\`\``;
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// Markdown 转义函数
|
||||
function escapeMarkdown(text: string): string {
|
||||
// Telegram Markdown V2 需要转义的特殊字符
|
||||
return text
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/\*/g, "\\*")
|
||||
.replace(/_/g, "\\_")
|
||||
.replace(/\[/g, "\\[")
|
||||
.replace(/\]/g, "\\]")
|
||||
.replace(/\(/g, "\\(")
|
||||
.replace(/\)/g, "\\)")
|
||||
.replace(/~/g, "\\~")
|
||||
.replace(/`/g, "\\`")
|
||||
.replace(/>/g, "\\>")
|
||||
.replace(/#/g, "\\#")
|
||||
.replace(/\+/g, "\\+")
|
||||
.replace(/-/g, "\\-")
|
||||
.replace(/=/g, "\\=")
|
||||
.replace(/\|/g, "\\|")
|
||||
.replace(/\{/g, "\\{")
|
||||
.replace(/\}/g, "\\}")
|
||||
.replace(/\./g, "\\.")
|
||||
.replace(/!/g, "\\!");
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "1.0.5",
|
||||
"versionName": "1.0.6",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
title: Telegram Bot
|
||||
description: How to configure Telegram bot
|
||||
---
|
||||
+12
-2
@@ -493,10 +493,20 @@
|
||||
"Catch-All Email Address": "Catch-All Email Address",
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled": "Set catch-all email address, split by comma if more than one, such as: 1@a.com,2@b.com, Only works when email catch all is enabled",
|
||||
"Message Pusher": "Message Pusher",
|
||||
"Push message to third-party services, such as Telegram, 飞书 etc.": "Push message to third-party services, such as Telegram, 飞书 etc",
|
||||
"Push message to Telegram groups": "Push message to Telegram groups",
|
||||
"Update now": "Update now",
|
||||
"Dismiss": "Dismiss",
|
||||
"New version available": "New version available",
|
||||
"Check for updates": "Check for updates"
|
||||
"Check for updates": "Check for updates",
|
||||
"Telegram Pusher": "Telegram Pusher",
|
||||
"Telegram Bot Token": "Telegram Bot Token",
|
||||
"Telegram Group ID": "Telegram Group ID",
|
||||
"Telegram Message Template": "Telegram Message Template",
|
||||
"Telegram Push Email White List": "Telegram Push Email White List",
|
||||
"Set Telegram bot token, Only works when Telegram pusher is enabled": "Set Telegram bot token, Only works when Telegram pusher is enabled",
|
||||
"Set Telegram group ID, split by comma if more than one, such as: -10054275724,-10045343642": "Set Telegram group ID, split by comma if more than one, such as: -10054275724,-10045343642",
|
||||
"Set Telegram email message template": "设置 Telegram 邮件消息模板",
|
||||
"Set Telegram push email white list, split by comma, if not set, will push all emails": "Set Telegram push email white list, split by comma, if not set, will push all emails",
|
||||
"How to configure Telegram bot": "How to configure Telegram bot"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-3
@@ -491,12 +491,22 @@
|
||||
"Email Configs": "电子邮件配置",
|
||||
"Enable email catch-all, all user's email address which created on this platform will be redirected to the catch-all email address": "启用 Catch-All,所有用户在此平台创建的邮箱所接收的邮件都会被转发到 Catch-All 设置的邮箱地址",
|
||||
"Catch-All Email Address": "Catch-All 邮箱地址",
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled": "设置 Catch-All 邮箱地址白名单 (仅支持在此平台创建的邮箱),多个邮箱地址请用逗号分隔,例如:1@a.com,2@b.com。仅在启用 Catch-All 时生效",
|
||||
"Set catch-all email address, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email catch all is enabled": "设置 Catch-All 目标邮箱地址 (仅支持在此平台创建的邮箱),多个邮箱地址请用逗号分隔,例如:1@a.com,2@b.com。仅在启用 Catch-All 时生效",
|
||||
"Message Pusher": "消息推送",
|
||||
"Push message to third-party services, such as Telegram, 飞书 etc": "推送消息到第三方服务,例如 Telegram,飞书等",
|
||||
"Push message to Telegram groups": "推送邮件到 Telegram 频道、群组",
|
||||
"Update now": "立即更新",
|
||||
"Dismiss": "忽略",
|
||||
"New version available": "有新版本可用",
|
||||
"Check for updates": "检查更新"
|
||||
"Check for updates": "检查版本更新",
|
||||
"Telegram Pusher": "Telegram 推送",
|
||||
"Telegram Bot Token": "Telegram Bot Token",
|
||||
"Telegram Group ID": "Telegram 群组/频道 ID",
|
||||
"Telegram Message Template": "Telegram 消息模板",
|
||||
"Telegram Push Email White List": "Telegram 推送邮件白名单",
|
||||
"Set Telegram bot token, Only works when Telegram pusher is enabled": "设置 Telegram Bot Token,仅在启用 Telegram 推送时生效",
|
||||
"Set Telegram group ID, split by comma if more than one, such as: -10054275724,-10045343642": "设置 Telegram 群组/频道 ID,多个群组/频道 ID请用逗号分隔,例如:-10054275724,-10045343642",
|
||||
"Set Telegram email message template": "设置 Telegram 邮件消息模板",
|
||||
"Set Telegram push email white list, split by comma, if not set, will push all emails": "设置 Telegram 推送邮件白名单,多个邮箱地址请用逗号分隔,若不设置,将推送所有邮件",
|
||||
"How to configure Telegram bot": "如何配置 Telegram Bot"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wr.do",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.6",
|
||||
"author": {
|
||||
"name": "oiov",
|
||||
"url": "https://github.com/oiov"
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'enable_tg_email_push',
|
||||
'false',
|
||||
'BOOLEAN',
|
||||
'是否启用 Telegram 邮件推送'
|
||||
);
|
||||
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'tg_email_bot_token',
|
||||
'',
|
||||
'STRING',
|
||||
'Telegram 邮件推送 Bot Token'
|
||||
);
|
||||
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'tg_email_chat_id',
|
||||
'',
|
||||
'STRING',
|
||||
'Telegram 邮件推送 Chat ID'
|
||||
);
|
||||
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'tg_email_template',
|
||||
'📧 *New Email*\n\n*From:* {{fromName}}\n*Subject:* {{subject}}\n\n```\n{{text}}\n```',
|
||||
'STRING',
|
||||
'Telegram 邮件推送模板'
|
||||
);
|
||||
|
||||
INSERT INTO "system_configs"
|
||||
(
|
||||
"key",
|
||||
"value",
|
||||
"type",
|
||||
"description"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'tg_email_target_white_list',
|
||||
'',
|
||||
'STRING',
|
||||
'Telegram 邮件推送目标白名单'
|
||||
);
|
||||
|
||||
-- {
|
||||
-- "enable_tg_email_push": true,
|
||||
-- "tg_email_bot_token": "1234567890:ABCdefGHIjklMNOpqrsTUVwxyz",
|
||||
-- "tg_email_chat_id": "-1001234567890,-1001234567891,-1001234567892",
|
||||
-- "tg_email_template": "📧 *New Email*\n\n*From:* {fromName}\n*Subject:* {subject}\n\n```\n{text}\n```",
|
||||
-- "tg_email_target_white_list": "admin@example.com,support@example.com,notifications@example.com"
|
||||
-- }
|
||||
|
||||
-- 多群组推送说明:
|
||||
-- - tg_email_chat_id 支持多个 Chat ID,用逗号分隔
|
||||
-- - 系统会并发推送到所有配置的群组/频道
|
||||
-- - 单个群组推送失败不会影响其他群组
|
||||
-- - 控制台会显示推送成功/失败统计
|
||||
|
||||
-- 白名单说明:
|
||||
-- - 如果 tg_email_target_white_list 为空或未设置,则转发所有邮件到 Telegram
|
||||
-- - 如果 tg_email_target_white_list 有值,则只转发发送到白名单中邮箱地址的邮件
|
||||
-- - 白名单支持多个邮箱地址,用逗号分隔
|
||||
-- - 邮箱地址匹配不区分大小写
|
||||
-- */
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "1.0.5",
|
||||
"versionName": "1.0.6",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"short_name": "WR.DO",
|
||||
"description": "Shorten links with analytics, manage emails and control subdomains—all on one platform.",
|
||||
"appid": "com.wr.do",
|
||||
"versionName": "1.0.5",
|
||||
"versionName": "1.0.6",
|
||||
"versionCode": "1",
|
||||
"start_url": "/",
|
||||
"orientation": "portrait",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user