You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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. 步骤1:在Resend控制台配置自定义域名

    • 登录Resend控制台,添加并验证你的业务域名(如eeslcareers.com)
    • 创建一个专用发件邮箱(如noreply@eeslcareers.com)
  2. 步骤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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 08:44:50