如何在NextJS中通过Resend动态设置发件人邮箱地址?
问题
在NextJS项目中使用Resend发送邮件时,目前仅能显示发件人名称,但发件邮箱固定为onboarding@resend.dev域名。通过求职申请表单收集用户信息后,希望当用户使用arka@gmail.com提交申请时,Gmail收件箱中显示from: arka@gmail.com,询问是否有办法实现动态发件人邮箱地址。
相关代码如下:
route.ts 代码
import { NextResponse } from "next/server"; import { Resend } from "resend"; import ApplicationEmail from "../../../../emails/application"; const resend = new Resend(process.env.RESEND_API_KEY); export async function POST(req: Request) { const formData = await req.formData(); const name = formData.get("name"); const email = formData.get("email"); const phone = formData.get("phone"); const education = formData.get("education"); const role = formData.get("role"); const resume = formData.get("resume") as File; if (!name || !email || !phone || !education || !role || !resume) { return NextResponse.json( { message: "All fields are required" }, { status: 400 } ); } try { const data = await resend.emails.send({ from: `${formData.get("name")} <onboarding@resend.dev>`, replyTo: formData.get("email") as string, to: [`eeslcareers@gmail.com`], subject: `New Application for ${formData.get("role")} Position`, react: ApplicationEmail({ name: formData.get("name") as string, role: formData.get("role") as string, email: formData.get("email") as string, phone: formData.get("phone") as string, }), attachments: [ { filename: (formData.get("resume") as File).name, content: Buffer.from( await (formData.get("resume") as File).arrayBuffer() ), }, ], }); console.log("Email sent successfully:", data); return NextResponse.json({ data }); } catch (error) { console.error("Error sending email:", error); return NextResponse.json({ error }); } }
application-form.tsx(用户数据来源)代码
"use client"; import { useState, useEffect } from "react"; import { Dialog } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { X } from "lucide-react"; import PhoneInput from "react-phone-input-2"; import "react-phone-input-2/lib/style.css"; import toast from "react-hot-toast"; import { useDropzone } from "react-dropzone"; import { motion } from "framer-motion"; interface ApplicationFormProps { role: string; isOpen: boolean; onClose: () => void; } export function ApplicationForm({ role, isOpen, onClose, }: ApplicationFormProps) { const [formData, setFormData] = useState({ name: "", email: "", phone: "+91", education: "", resume: null as File | null, }); const [isSubmitting, setIsSubmitting] = useState(false); const { getRootProps, getInputProps, isDragActive } = useDropzone({ accept: { "application/pdf": [".pdf"] }, maxSize: 10 * 1024 * 1024, onDrop: (acceptedFiles) => { if (acceptedFiles[0]) { setFormData({ ...formData, resume: acceptedFiles[0] }); } }, }); useEffect(() => { const handleEscape = (e: KeyboardEvent) => { if (e.key === "Escape") { onClose(); } }; window.addEventListener("keydown", handleEscape); return () => window.removeEventListener("keydown", handleEscape); }, [onClose]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); try { const submissionData = new FormData(); submissionData.append("name", formData.name); submissionData.append("email", formData.email); submissionData.append("phone", formData.phone); submissionData.append("education", formData.education); submissionData.append("role", role); if (formData.resume) { submissionData.append("resume", formData.resume); } const response = await fetch("/api/submit-application", { method: "POST", body: submissionData, }); if (response.ok) { toast.success("Application submitted successfully!"); setFormData({ name: "", email: "", phone: "", education: "", resume: null, }); onClose(); } else { throw new Error("Failed to submit application"); } } catch (error) { toast.error("Failed to submit application. Please try again."); console.error("Error submitting application:", error); } finally { setIsSubmitting(false); } }; return ( <> <Dialog open={isOpen} onOpenChange={onClose}> <div className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center"> <motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -20, opacity: 0 }} className="bg-white/95 rounded-2xl w-full max-w-xl p-8 m-4 relative shadow-2xl" > <button onClick={onClose} className="absolute top-4 right-4 rounded-full p-2 hover:bg-gray-100 transition-colors" > <X className="w-5 h-5 text-gray-500" /> </button> <h2 className="text-3xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-gray-900 to-gray-600 mb-6"> {role === "PhD" ? "Apply for PhD" : `Apply for ${role}`} </h2> <form onSubmit={handleSubmit} className="space-y-6"> <div> <label className="text-sm font-medium text-gray-700"> Full Name </label> <Input value={formData.name} onChange={(e) => setFormData({ ...formData, name: e.target.value }) } className="mt-1 w-full rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" placeholder="Enter your full name" required autoFocus /> </div> <div> <label className="text-sm font-medium text-gray-700"> Email </label> <Input type="email" value={formData.email} onChange={(e) => setFormData({ ...formData, email: e.target.value }) } className="mt-1 w-full rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" placeholder="Enter your email address" required /> </div> <div> <label className="text-sm font-medium text-gray-700"> Phone Number </label> <PhoneInput country="in" value={formData.phone} onChange={(phone) => setFormData({ ...formData, phone })} inputClass="w-full p-2 rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" containerClass="w-full" /> </div> <div> <label className="text-sm font-medium text-gray-700"> Educational Qualification </label> <Input value={formData.education} onChange={(e) => setFormData({ ...formData, education: e.target.value }) } className="mt-1 w-full rounded-lg border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all" placeholder="Enter your highest qualification" required /> </div> <div> <label className="text-sm font-medium text-gray-700"> Resume (PDF, max 10MB) </label> <div {...getRootProps()} className={` mt-1 rounded-lg border-2 border-dashed p-8 text-center cursor-pointer transition-all ${isDragActive ? "border-blue-500 bg-blue-50" : "border-gray-300 hover:border-blue-400"} `} > <input {...getInputProps()} required={!formData.resume} /> {formData.resume ? ( <div className="flex items-center justify-center gap-2 text-gray-600"> <svg className="w-6 h-6 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg> <span>{formData.resume.name}</span> </div> ) : ( <p className="text-gray-500"> {isDragActive ? "Drop your resume here" : "Drag & drop your resume, or click to select"} </p> )} </div> </div> <div className="flex justify-end gap-4 mt-8"> <Button variant="outline" onClick={onClose} className="px-6 hover:bg-gray-50" > Cancel </Button> <Button type="submit" disabled={isSubmitting} className="bg-blue-600 hover:bg-blue-700 text-white px-8 py-2.5 rounded-lg shadow-lg hover:shadow-xl transform transition-all duration-200 hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed" > {isSubmitting ? ( <div className="flex items-center gap-2"> <motion.div animate={{ rotate: 360 }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} className="w-5 h-5 border-2 border-white border-t-transparent rounded-full" /> <span>Submitting...</span> </div> ) : ( "Submit Application" )} </Button> </div> </form> </motion.div> </div> </Dialog> </> ); }
解决方案
核心限制说明
不能直接将用户邮箱设为from地址。Gmail等邮件服务商通过SPF/DKIM/DMARC验证发件域名的合法性,你无法验证用户的第三方域名(如gmail.com),直接使用用户邮箱作为发件人会导致验证失败,邮件会被标记为垃圾邮件甚至直接拒收。
推荐实现方案(兼顾显示效果与邮件送达率)
使用Resend验证的自定义域名作为发件邮箱,同时通过发件人名称+回复地址模拟用户发件的效果:
步骤1:在Resend控制台配置自定义域名
- 登录Resend控制台,添加并验证你的业务域名(如
eeslcareers.com) - 创建一个专用发件邮箱(如
noreply@eeslcareers.com)
- 登录Resend控制台,添加并验证你的业务域名(如
步骤2:修改route.ts中的发件配置
将from字段改为包含用户名称的自定义域名邮箱,保留replyTo为用户邮箱:from: `${name} (EESL Careers) <noreply@eeslcareers.com>`, replyTo: email as string,此时Gmail收件箱会显示发件人为
用户名称 (EESL Careers),点击回复时默认发送到用户的真实邮箱,既保证了邮件正常送达,又满足了业务需求。
修改后的完整route.ts关键片段
try { const data = await resend.emails.send({ // 自定义发件人格式:用户名称 + 平台标识 + 验证后的域名邮箱 from: `${name} (EESL Careers) <noreply@eeslcareers.com>`, // 回复地址设为用户提交的邮箱 replyTo: email as string, to: [`eeslcareers@gmail.com`], subject: `New Application for ${role} Position`, react: ApplicationEmail({ name: name as string, role: role as string, email: email as string, phone: phone as string, }), attachments: [ { filename: resume.name, content: Buffer.from(await resume.arrayBuffer()), }, ], }); console.log("Email sent successfully:", data); return NextResponse.json({ data }); } catch (error) { console.error("Error sending email:", error); return NextResponse.json({ error }); }
内容的提问来源于stack exchange,提问作者Arkadipta Mojumder
相关产品推荐
相关产品推荐

