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

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

В Node нет встроенной работы с почтой, поэтому отправка письма — это разговор по SMTP, а на практике — Nodemailer, на котором построены почтовые интеграции всех Node-фреймворков. Ниже — настройка на fmailer, HTML-письма, вложения, рассылка через пул соединений и повторы при сбоях.

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

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

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

Установка

bash
npm install nodemailer

Транспорт

mailer.jsjavascript
// mailer.js
import nodemailer from "nodemailer";

export const transporter = nodemailer.createTransport({
  host: "smtp.fmailer.ru",
  port: 587,
  secure: false,        // на 587 соединение поднимается в TLS через STARTTLS
  requireTLS: true,     // и не отправится вовсе, если поднять не удалось
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,   // параметр называется "pass", не "password"
  },
});

Или с шифрованием с первого байта на порту 465:

javascript
// Порт 465 — шифрование с первого байта.
const transporter = nodemailer.createTransport({
  host: "smtp.fmailer.ru",
  port: 465,
  secure: true,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
});
Nodemailer молча игнорирует незнакомые опции, поэтому password: означает попытку авторизации с пустым паролем, а сервер отвечает 535 Authentication failed. Проверяйте это в первую очередь.

Проверка конфигурации

verify() подключается и авторизуется, ничего не отправляя, — неправильная настройка обнаружится при старте, а не перед пользователем:

javascript
try {
  await transporter.verify();
  console.log("SMTP готов");
} catch (err) {
  console.error("Конфигурация SMTP сломана:", err.message);
  process.exit(1);
}

Отправка письма

Задавайте и text, и html: Nodemailer соберёт multipart/alternative, а HTML без текстовой части почтовые фильтры считают признаком спама:

javascript
const info = await transporter.sendMail({
  from: '"Магазин" <noreply@mail.example.ru>',
  to: "client@example.ru",
  subject: "Подтверждение заказа №4417",
  text: "Заказ принят. Доставка — завтра до 18:00.",
  html: "<h1>Заказ принят</h1><p>Доставка — завтра до 18:00.</p>",
});

console.log("Принято:", info.accepted);
console.log("Идентификатор письма:", info.messageId);

В объекте info возвращается то, что принял сервер: accepted, rejected, response и messageId. Логируйте messageId — по нему потом сопоставляются вебхуки о доставке и отказах.

Вложения и картинки в теле письма

javascript
await transporter.sendMail({
  from: '"Магазин" <noreply@mail.example.ru>',
  to: "client@example.ru",
  subject: "Счёт на оплату",
  text: "Счёт за март во вложении.",
  html: '<p>Счёт во вложении.</p><img src="cid:logo" width="120">',
  attachments: [
    { filename: "invoice.pdf", path: "/srv/invoices/2026-03.pdf" },
    { filename: "report.csv", content: csvString },
    { filename: "logo.png", path: "./assets/logo.png", cid: "logo" },
  ],
});

Массовая рассылка через пул

Переподключение на каждое письмо медленно и рано или поздно упрётся в ограничение скорости. Включите пул и управляйте им по событию idle:

javascript
const bulk = nodemailer.createTransport({
  host: "smtp.fmailer.ru",
  port: 587,
  requireTLS: true,
  auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS },
  pool: true,
  maxConnections: 5,     // параллельных соединений
  maxMessages: 100,      // писем на соединение до переподключения
  rateDelta: 1000,
  rateLimit: 10,         // не больше 10 писем в секунду
});

bulk.on("idle", () => {
  while (bulk.isIdle() && queue.length) {
    const user = queue.shift();
    bulk.sendMail({
      from: '"Магазин" <noreply@mail.example.ru>',
      to: user.email,
      subject: "Еженедельный отчёт",
      text: renderReport(user),
    }).catch((err) => console.error(user.email, err.message));
  }
});
При использовании CC и BCC на каждого получателя внутри системы создаётся отдельное письмо, и лимиты расходуются соответственно.

Повторы при временных сбоях

Ответ 4xx или оборванный сокет — временная проблема; 5xx — окончательный, и повторять его — значит только портить репутацию домена:

javascript
async function sendWithRetry(message, attempts = 3) {
  for (let i = 1; i <= attempts; i++) {
    try {
      return await transporter.sendMail(message);
    } catch (err) {
      // Ответы 4xx и обрывы сокета временные, 5xx — окончательные.
      const permanent = err.responseCode >= 500 && err.responseCode < 600;
      if (permanent || i === attempts) throw err;
      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
  }
}

Разбор ошибок

КодПричина и решение
EAUTHpass написан как password, либо неверные доступы.
ETIMEDOUT, ECONNREFUSED Порт заблокирован. Используйте 8587 или 8465.
ESOCKET — wrong version numbersecure: false на порту 465. Поставьте true.
Зависает без ошибкиsecure: true на порту 587. Нужен requireTLS.
EENVELOPE Домен отправителя не подтверждён либо адрес получателя некорректен.
EMESSAGE — 552 Письмо слишком большое. Выложите вложение и дайте ссылку.

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

Чем отличаются secure, requireTLS и ignoreTLS?

secure: true — сокет шифруется с первого байта, это порт 465. secure: false открывает незашифрованное соединение и вместе с requireTLS: true обязан поднять его через STARTTLS до отправки — это порт 587. ignoreTLS вообще пропускает шифрование и отправляет доступы открытым текстом, использовать его не нужно.

Почему Nodemailer пишет «Invalid login: 535 Authentication failed»?

Чаще всего в конфигурации написано auth.password вместо auth.pass. Незнакомый ключ Nodemailer игнорирует и авторизуется с пустым паролем. Если ключ верный — значит неверны логин или пароль токена.

Нужно ли создавать транспорт на каждое письмо?

Нет. Каждый createTransport с отправкой открывает TCP-соединение и выполняет TLS-рукопожатие. Создайте транспорт один раз на уровне модуля и переиспользуйте. Для больших объёмов включите pool: true — тогда набор соединений держится открытым.

Сколько писем можно отправить через одно соединение?

SMTP-серверы ограничивают число писем на соединение, поэтому длинный цикл рано или поздно получит отказ. С pool: true задайте maxMessages, чтобы Nodemailer переоткрывал соединение сам, и rateLimit, чтобы не выходить за допустимую скорость.

Как прикрепить файл в Nodemailer?

Передайте массив attachments. В каждом элементе укажите filename и одно из: path (файл на диске), content (строка, Buffer или поток) либо href (удалённый адрес). Поле cid позволяет вставить картинку прямо в HTML через <img src="cid:имя">.

Почему отправка зависает без ошибки?

Обычно это secure: true на порту 587 — клиент ждёт TLS-рукопожатия, которого сервер не начинает. На 587 нужен secure: false с requireTLS: true, либо переходите на порт 465 с secure: true.

Что дальше