feat: add email forwarding configs

This commit is contained in:
oiov
2025-07-28 09:49:36 +08:00
parent 3f5d2a8364
commit cfcbc3fe26
10 changed files with 222 additions and 28 deletions
+80 -2
View File
@@ -35,6 +35,7 @@ export default function AppConfigs({}: {}) {
} = useSWR<Record<string, any>>("/api/admin/configs", fetcher);
const [notification, setNotification] = useState("");
const [catchAllEmails, setCatchAllEmails] = useState("");
const [forwardEmailTargets, setForwardEmailTargets] = useState("");
const [emailSuffix, setEmailSuffix] = useState("");
const [tgBotToken, setTgBotToken] = useState("");
const [tgChatId, setTgChatId] = useState("");
@@ -52,8 +53,9 @@ export default function AppConfigs({}: {}) {
setTgChatId(configs?.tg_email_chat_id);
setTgTemplate(configs?.tg_email_template);
setTgWhiteList(configs?.tg_email_target_white_list);
setForwardEmailTargets(configs?.email_forward_targets);
}
// 计算登录方式数量
if (!isLoading) {
let count = 0;
if (configs?.enable_google_oauth) count++;
@@ -365,7 +367,7 @@ export default function AppConfigs({}: {}) {
</CollapsibleTrigger>
<CollapsibleContent className="space-y-3 bg-neutral-100 p-4 dark:bg-neutral-800">
<div className="space-y-6">
{/* Catch-All */}
{/* Catch-All*/}
<Collapsible>
<CollapsibleTrigger className="flex w-full items-center justify-between space-x-2">
<div className="space-y-1 leading-none">
@@ -445,6 +447,82 @@ export default function AppConfigs({}: {}) {
</div>
</CollapsibleContent>
</Collapsible>
{/* Forward Email to other email address */}
<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("Email Forwarding")}
</p>
<p className="text-start text-xs text-muted-foreground">
{t(
"If enabled, forward all received emails to other platform email addresses (Send with Resend)",
)}
</p>
</div>
{configs && (
<div
className="ml-auto flex items-center gap-3"
onClick={(e) => e.stopPropagation()}
>
{configs.enable_email_forward &&
!configs.email_forward_targets && (
<Badge variant="yellow">
<Icons.warning className="mr-1 size-3" />{" "}
{t("Need to configure")}
</Badge>
)}
<Switch
defaultChecked={configs.enable_email_forward}
onCheckedChange={(v) =>
handleChange(v, "enable_email_forward", "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("Forward Email Targets")}</p>
<p className="text-xs text-muted-foreground">
{t(
"Set forward email address targets, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email forwarding 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="example1@wr.do,example2@wr.do"
rows={5}
value={forwardEmailTargets}
disabled={!configs.enable_email_forward}
onChange={(e) => setForwardEmailTargets(e.target.value)}
/>
<Button
className="h-9 text-nowrap"
disabled={
isPending ||
forwardEmailTargets === configs.email_forward_targets
}
onClick={() =>
handleChange(
forwardEmailTargets,
"email_forward_targets",
"STRING",
)
}
>
{t("Save")}
</Button>
</div>
)}
</div>
</CollapsibleContent>
</Collapsible>
{/* Telegram */}
<Collapsible>
+4 -1
View File
@@ -157,10 +157,13 @@ export default function S3Configs({}: {}) {
buckets: [
{
bucket: "",
custom_domain: "",
prefix: "",
file_types: "",
region: "auto",
custom_domain: "",
file_size: "26214400",
max_storage: "1073741824",
max_files: "1000",
public: true,
},
],
+2
View File
@@ -34,6 +34,8 @@ export async function GET(req: NextRequest) {
"enable_email_registration_suffix_limit",
"email_registration_suffix_limit_white_list",
"enable_subdomain_status_email_pusher",
"enable_email_forward",
"email_forward_targets",
]);
return Response.json(configs, { status: 200 });
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ error: "Ids are required" }, { status: 400 });
}
const data = await getUserShortLinksByIds(ids, user.id);
const data = await getUserShortLinksByIds(ids);
const dataMap = new Map(data.map((item) => [item.id, item]));
+92 -19
View File
@@ -1,5 +1,6 @@
import { OriginalEmail, saveForwardEmail } from "@/lib/dto/email";
import { getMultipleConfigs } from "@/lib/dto/system-config";
import { resend } from "@/lib/email";
export async function POST(req: Request) {
try {
@@ -16,27 +17,12 @@ export async function POST(req: Request) {
"tg_email_chat_id",
"tg_email_template",
"tg_email_target_white_list",
"enable_email_forward",
"email_forward_targets",
]);
// Catch-all
if (configs.enable_email_catch_all) {
const validEmails = parseAndValidateEmails(configs.catch_all_emails);
if (validEmails.length === 0) {
return Response.json(
{ error: "No valid catch-all emails configured" },
{ status: 400 },
);
}
const forwardPromises = validEmails.map((email) =>
saveForwardEmail({ ...data, to: email }),
);
await Promise.all(forwardPromises);
} else {
await saveForwardEmail(data);
}
// 处理邮件转发和保存
await handleEmailForwarding(data, configs);
// Telegram
if (configs.enable_tg_email_push) {
@@ -56,6 +42,93 @@ export async function POST(req: Request) {
}
}
async function handleEmailForwarding(data: OriginalEmail, configs: any) {
const actions = determineEmailActions(configs);
const promises: Promise<void>[] = [];
if (actions.includes("CATCH_ALL")) {
promises.push(handleCatchAllEmail(data, configs));
}
if (actions.includes("EXTERNAL_FORWARD")) {
promises.push(handleExternalForward(data, configs));
}
if (actions.includes("NORMAL_SAVE")) {
promises.push(handleNormalEmail(data));
}
// 并行执行所有操作
const results = await Promise.allSettled(promises);
// 检查是否有失败的操作
const failures = results.filter((result) => result.status === "rejected");
if (failures.length > 0) {
console.error("Some email operations failed:", failures);
const firstFailure = failures[0] as PromiseRejectedResult;
throw new Error(`Email operation failed: ${firstFailure.reason}`);
}
}
function determineEmailActions(configs: any): string[] {
const actions: string[] = [];
// 检查是否配置了任何转发功能
const hasAnyForward =
configs.enable_email_catch_all || configs.enable_email_forward;
if (configs.enable_email_catch_all) {
actions.push("CATCH_ALL");
}
if (configs.enable_email_forward) {
actions.push("EXTERNAL_FORWARD");
}
// 只有在没有配置任何转发时,才进行正常保存原始邮件
if (!hasAnyForward) {
actions.push("NORMAL_SAVE");
}
return actions;
}
async function handleCatchAllEmail(data: OriginalEmail, configs: any) {
const validEmails = parseAndValidateEmails(configs.catch_all_emails);
if (validEmails.length === 0) {
throw new Error("No valid catch-all emails configured");
}
// 转发到内部邮箱(保存转发后的邮件)
const forwardPromises = validEmails.map((email) =>
saveForwardEmail({ ...data, to: email }),
);
await Promise.all(forwardPromises);
}
async function handleExternalForward(data: OriginalEmail, configs: any) {
const validEmails = parseAndValidateEmails(configs.email_forward_targets);
if (validEmails.length === 0) {
throw new Error("No valid forward emails configured");
}
await resend.emails.send({
from: data.from,
to: validEmails,
subject: data.subject ?? "No subject",
html: data.html ?? "-",
text: data.text,
});
}
async function handleNormalEmail(data: OriginalEmail) {
await saveForwardEmail(data);
}
function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email.trim());
+2 -2
View File
@@ -124,12 +124,12 @@ export async function getUserShortUrlCount(
}
}
export async function getUserShortLinksByIds(ids: string[], userId: string) {
export async function getUserShortLinksByIds(ids: string[], userId?: string) {
try {
return await prisma.userUrl.findMany({
where: {
id: { in: ids },
userId,
...(userId && { userId }),
},
});
} catch (error) {
+5 -1
View File
@@ -645,6 +645,10 @@
"{length} Buckets": "{length} Buckets",
"Save Modifications": "Save Modifications",
"Max File Count": "Max File Count",
"Password": "Password"
"Password": "Password",
"Email Forwarding": "Email Forwarding",
"If enabled, forward all received emails to other platform email addresses (Send with Resend)": "If enabled, forward all received emails to other platform email addresses (Send with Resend)",
"Forward Email Targets": "Forward Email Targets",
"Set forward email address targets, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email forwarding is enabled": "Set forward email address targets, split by comma if more than one, such as: 1@a.com,2@b.com, Only works when email forwarding is enabled"
}
}
+5 -1
View File
@@ -645,6 +645,10 @@
"Add Provider": "添加渠道",
"{length} Buckets": "{length}个存储桶",
"Save Modifications": "保存修改",
"Password": "账户密码"
"Password": "账户密码",
"Email Forwarding": "邮件转发",
"If enabled, forward all received emails to other platform email addresses (Send with Resend)": "如果启用,则将所有收到的电子邮件转发到其他平台邮箱 (使用 Resend 发送)",
"Forward Email Targets": "目标收件箱",
"Set forward email address targets, split by comma if more than one, such as: 1@a-com,2@b-com, Only works when email forwarding is enabled": "设置转发目标邮箱,多个邮件地址请用逗号分隔,例如:1@a.com,2@b.com,仅在启用邮件转发时生效"
}
}
@@ -0,0 +1,30 @@
INSERT INTO "system_configs"
(
"key",
"value",
"type",
"description"
)
VALUES
(
'enable_email_forward',
'false',
'BOOLEAN',
'是否开启邮件转发'
);
INSERT INTO "system_configs"
(
"key",
"value",
"type",
"description"
)
VALUES
(
'email_forward_targets',
'',
'STRING',
'邮件转发目标,以逗号分隔'
);
+1 -1
View File
File diff suppressed because one or more lines are too long