feat: add email forwarding configs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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]));
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
@@ -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
@@ -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
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user