Документация / Next.js

Отправка email в Next.js через SMTP

В Next.js нет своего почтового слоя, поэтому отправка письма — это вызов SMTP-клиента из серверного кода: Server Action, Route Handler или API-роут. Ниже — подключение Nodemailer к fmailer для App Router, шаблоны React Email и то ограничение runtime, на котором спотыкаются чаще всего.

Логин и пароль можно получить на странице управления токенами домена.

Параметры подключения

ПараметрЗначение
SMTP-хостsmtp.fmailer.ru
Порт587
ШифрованиеSTARTTLS (соединение переводится в TLS до авторизации)
ЛогинЛогин SMTP-токена вашего домена
ПарольПароль этого токена — показывается один раз, при создании
Каждый режим доступен и на высоком порту: 8465 (SSL/TLS), 8587 (STARTTLS) и 8025 (без шифрования). Многие хостинги и облака блокируют исходящие 25, 465 и 587 — если соединение отваливается по таймауту, используйте соответствующий высокий порт

Установка

bash
npm install nodemailer
npm install -D @types/nodemailer

Транспорт

Вынесите транспорт в отдельный серверный модуль, чтобы он создавался один раз, а не пересобирался на каждый запрос:

lib/mailer.tsjavascript
// 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"
  },
});
.env.localproperties
SMTP_USER=<логин токена>
SMTP_PASS=<пароль токена>
MAIL_FROM="Магазин <noreply@mail.example.ru>"
Этот префикс встраивает значение в JavaScript-бандл, который получает каждый посетитель. Утёкший так SMTP-токен позволяет кому угодно отправлять письма от вашего домена.

Отправка из Server Action

Для формы внутри вашего же приложения это самый короткий путь — без API-роута и без запроса из браузера:

app/contact/actions.tsjavascript
// 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.

Отправка из Route Handler

Когда вызов приходит извне — вебхук или мобильный клиент:

app/api/send/route.tsjavascript
// 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 });
  }
}
Nodemailer открывает обычный TCP-сокет, а Edge runtime этого не позволяет — там доступен только fetch. Указывайте export const runtime = "nodejs" в каждом обработчике с отправкой почты либо используйте REST API по HTTPS, который работает в обоих окружениях.

HTML-письма через React Email

Верстать табличный HTML для писем вручную мучительно. React Email позволяет собрать письмо компонентами и отрисовать в строку:

javascript
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: 535password: вместо pass: либо неверные доступы.
ETIMEDOUT после деплоя Платформа блокирует SMTP. Попробуйте 8587 или перейдите на REST API.
process.env.SMTP_USER undefined на проде Переменная есть только в .env.local — добавьте её в окружение деплоя.
Функция падает по таймауту на холодном старте Холодный старт плюс SMTP-рукопожатие не укладываются в лимит. Отправляйте из очереди.

Частые вопросы

Можно ли отправлять письма из Edge runtime в Next.js?

Нет. Nodemailer открывает обычный TCP-сокет, а на Edge доступен только fetch. Добавьте export const runtime = 'nodejs' в любой Route Handler, который отправляет почту. Server Actions и так выполняются на Node, если весь маршрут не переведён на Edge.

Откуда отправлять — из Server Action или Route Handler?

Server Action удобнее, когда отправку запускает форма вашего же приложения: работает без JavaScript и не создаёт публичного эндпоинта, который надо защищать. Route Handler нужен, когда вызывает кто-то извне: вебхук, мобильный клиент или сторонний сервис.

Почему сборка падает на импорте Nodemailer?

Модуль попал в клиентский компонент. Всё, что импортирует lib/mailer, должно исполняться только на сервере: держите его подальше от файлов с 'use client' и добавьте в модуль import 'server-only', чтобы ошибочный импорт падал на сборке, а не утаскивал доступы в браузерный бандл.

Почему переменные окружения с доступами undefined в продакшене?

Переменные без префикса NEXT_PUBLIC_ доступны только на сервере — и это правильно, почтовые доступы этим префиксом помечать нельзя никогда. Если они undefined и на сервере, значит их добавили только в локальный .env.local, но не в окружение деплоя.

Как верстать HTML-письма в Next.js?

React Email позволяет описывать письмо React-компонентами и отрисовывать его в HTML-строку через @react-email/render. Эту строку передавайте в html, а её текстовую версию — в text. Шаблоны писем остаются на том же языке и в той же модели компонентов, что и остальное приложение.

Почему локально письма уходят, а после деплоя таймаут?

Многие serverless-платформы блокируют исходящие SMTP-порты, а некоторые — полностью. Сначала попробуйте высокие порты 8587 или 8465. Если платформа режет SMTP целиком, отправляйте через REST API по HTTPS: это обычный fetch, он работает в любом runtime, включая Edge.

Что дальше