В Next.js нет своего почтового слоя, поэтому отправка письма — это вызов SMTP-клиента из серверного кода: Server Action, Route Handler или API-роут. Ниже — подключение Nodemailer к fmailer для App Router, шаблоны React Email и то ограничение runtime, на котором спотыкаются чаще всего.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
npm install nodemailer
npm install -D @types/nodemailerВынесите транспорт в отдельный серверный модуль, чтобы он создавался один раз, а не пересобирался на каждый запрос:
// lib/mailer.ts
import nodemailer from "nodemailer";
// Уровень модуля: один транспорт на все вызовы, а не по одному на запрос.
export const transporter = nodemailer.createTransport({
host: "smtp.fmailer.ru",
port: 587,
secure: false, // на 587 поднимается STARTTLS
requireTLS: true,
auth: {
user: process.env.SMTP_USER!,
pass: process.env.SMTP_PASS!, // "pass", а не "password"
},
});SMTP_USER=<логин токена>
SMTP_PASS=<пароль токена>
MAIL_FROM="Магазин <noreply@mail.example.ru>"Для формы внутри вашего же приложения это самый короткий путь — без API-роута и без запроса из браузера:
// app/contact/actions.ts
"use server";
import { transporter } from "@/lib/mailer";
export async function sendContactEmail(formData: FormData) {
const email = String(formData.get("email"));
const message = String(formData.get("message"));
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: "sales@mail.example.ru",
replyTo: email, // From остаётся вашим, ответ уйдёт посетителю
subject: "Заявка с сайта",
text: message,
});
return { ok: true };
} Обратите внимание на replyTo: адрес в from остаётся на подтверждённом домене — именно для него опубликованы DKIM и SPF, — а ответ уйдёт посетителю. Подстановка его адреса в from и есть причина, по которой письма с форм не проходят DMARC.
Когда вызов приходит извне — вебхук или мобильный клиент:
// app/api/send/route.ts
import { NextResponse } from "next/server";
import { transporter } from "@/lib/mailer";
// Nodemailer открывает TCP-сокет — на Edge это невозможно.
export const runtime = "nodejs";
export async function POST(request: Request) {
const { to, subject, text } = await request.json();
try {
const info = await transporter.sendMail({
from: process.env.MAIL_FROM,
to,
subject,
text,
});
return NextResponse.json({ ok: true, messageId: info.messageId });
} catch (error) {
console.error("Письмо не отправлено", error);
return NextResponse.json({ ok: false }, { status: 502 });
}
}fetch. Указывайте export const runtime = "nodejs" в каждом обработчике с отправкой почты либо используйте REST API по HTTPS, который работает в обоих окружениях. Верстать табличный HTML для писем вручную мучительно. React Email позволяет собрать письмо компонентами и отрисовать в строку:
import { render, toPlainText } from "@react-email/render";
import OrderConfirmed from "@/emails/order-confirmed";
const html = await render(<OrderConfirmed orderId={order.id} />);
await transporter.sendMail({
from: process.env.MAIL_FROM,
to: order.email,
subject: `Подтверждение заказа №${order.id}`,
html,
text: toPlainText(html), // текстовая часть нужна всегда
});| Ошибка | Причина и решение |
|---|---|
The edge runtime does not support Node.js 'net' module | Добавьте export const runtime = "nodejs". |
Module not found: Can't resolve 'dns' при сборке | Мейлер импортирован в клиентский компонент. Добавьте import "server-only". |
EAUTH — Invalid login: 535 | password: вместо pass: либо неверные доступы. |
ETIMEDOUT после деплоя | Платформа блокирует SMTP. Попробуйте 8587 или перейдите на REST API. |
process.env.SMTP_USER undefined на проде | Переменная есть только в .env.local — добавьте её в окружение деплоя. |
| Функция падает по таймауту на холодном старте | Холодный старт плюс SMTP-рукопожатие не укладываются в лимит. Отправляйте из очереди. |
Нет. Nodemailer открывает обычный TCP-сокет, а на Edge доступен только fetch. Добавьте export const runtime = 'nodejs' в любой Route Handler, который отправляет почту. Server Actions и так выполняются на Node, если весь маршрут не переведён на Edge.
Server Action удобнее, когда отправку запускает форма вашего же приложения: работает без JavaScript и не создаёт публичного эндпоинта, который надо защищать. Route Handler нужен, когда вызывает кто-то извне: вебхук, мобильный клиент или сторонний сервис.
Модуль попал в клиентский компонент. Всё, что импортирует lib/mailer, должно исполняться только на сервере: держите его подальше от файлов с 'use client' и добавьте в модуль import 'server-only', чтобы ошибочный импорт падал на сборке, а не утаскивал доступы в браузерный бандл.
Переменные без префикса NEXT_PUBLIC_ доступны только на сервере — и это правильно, почтовые доступы этим префиксом помечать нельзя никогда. Если они undefined и на сервере, значит их добавили только в локальный .env.local, но не в окружение деплоя.
React Email позволяет описывать письмо React-компонентами и отрисовывать его в HTML-строку через @react-email/render. Эту строку передавайте в html, а её текстовую версию — в text. Шаблоны писем остаются на том же языке и в той же модели компонентов, что и остальное приложение.
Многие serverless-платформы блокируют исходящие SMTP-порты, а некоторые — полностью. Сначала попробуйте высокие порты 8587 или 8465. Если платформа режет SMTP целиком, отправляйте через REST API по HTTPS: это обычный fetch, он работает в любом runtime, включая Edge.