Перейти к содержимому
Поиск
Разработка

Mailpit: проверка писем PHP и Node.js — мастерская и 8 практических задач

Проверяем уведомления до отправки клиенту: HTML и текст, вложения, адреса, теги, API и SMTP-ошибки. Восемь задач, 16 вариантов и готовый проект Windows/Linux.

Александр Некрасов
Mailpit: проверка писем PHP и Node.js — мастерская и 8 практических задач — обложка

Mailpit на практике: тестовый SMTP-сервер, письма из PHP и Node.js, HTML, вложения, API и ошибки. Восемь задач, шестнадцать вариантов, мастерская и исходники Windows/Linux.

Mailpit: увидеть письмо до того, как его получит клиент

Регистрация, заявка с сайта, подтверждение заказа, восстановление доступа, отчёт менеджеру — почти у любого сервиса есть почтовые уведомления. Во время разработки нужно проверить адрес, тему, оформление и вложения. Если отправлять каждую попытку через настоящий почтовый ящик, тесты быстро смешиваются с рабочей перепиской, а случайный адрес клиента превращается в нежелательное уведомление.

Mailpit принимает SMTP-сообщения в отдельный тестовый ящик. Приложение действительно соединяется с почтовым сервером и передаёт письмо, а разработчик открывает результат через веб-интерфейс или HTTP API. Это позволяет проверить весь участок от PHP или Node.js до принятого сообщения: заголовки, текстовую и HTML-часть, MIME и вложенные файлы. При этом внешний маршрут доставки в нашем стенде не настроен.

Ниже разберём восемь практических задач, ошибки и запуск на Windows и Linux. Мини-мастерская показывает шестнадцать результатов настоящего выполнения, позволяет переключать представления и скачивать письма. Исходники содержат отправителей на двух языках и повторяемую проверку, поэтому пример можно перенести в свой проект и включить в разработку уведомлений.

Где применять: формы, интернет-магазины, CRM и фоновые задачи

В форме обратной связи Mailpit помогает проверить, что пользовательский текст попал в нужное поле, адрес ответа указывает на отправителя заявки, а внутренний адрес получателя не зависит от произвольного ввода. В интернет-магазине удобно проверять номер заказа, валюту, состав вложения и различия уведомлений для клиента и менеджера. В CRM можно увидеть, не перепутались ли адреса сотрудников и скрытые копии.

Отдельная полезная ситуация — изменение шаблона. Дизайнер или разработчик обновляет заголовок, кнопку и текст, запускает тестовый сценарий и открывает конкретное письмо. Вместо обсуждения предположительного вида есть полученный HTML, текстовая альтернатива и исходный MIME. Для шаблонов на PHP пригодится материал про Twig и подготовку писем.

В фоновых очередях важно проверять ещё и реакцию на отказ SMTP. Сообщение может быть сформировано правильно, но сервер временно его не принимает. Mailpit позволяет воспроизвести такое состояние. После отдельной проверки отправителя можно подключить его к очереди из материала про BullMQ, повторы и прогресс задач.

Что именно доказывает тестовый почтовый сервер

Успешный SMTP-ответ означает, что выбранный сервер принял сообщение на данном этапе обмена. В нашей лаборатории этим сервером является Mailpit. Это проверяемый и полезный результат: приложение подключилось, отправило разрешённого получателя, передало данные, а в API появилось письмо. Он ещё не означает, что реальный пользователь прочитал уведомление или что внешний провайдер положит его во входящие.

Доставляемость, репутация домена, SPF, DKIM, DMARC и антиспам реального провайдера относятся к другому этапу. Проверка HTML в браузере также не заменяет просмотр в целевых почтовых клиентах: их поддержка CSS и поведение с изображениями различаются. Mailpit полезен как быстрый воспроизводимый слой разработки, после которого проводят необходимые внешние проверки своего продукта.

Полезно разделять четыре результата: отправитель не выбросил исключение; тестовый сервер сохранил письмо; содержание соответствует ожидаемому контракту; рабочая система доставляет уведомления адресатам. В шестой задаче мы специально получим письмо с успешной передачей, но ошибочным ожиданием темы. Так становится видно, почему одного вызова send недостаточно для полноценного теста.

Состав проекта и закреплённые версии

В архиве закреплены Mailpit 1.33.1, Nodemailer 10.5.0 и PHPMailer 7.2.0. Версии сверены по официальным источникам 13 ноября 2026 года. JavaScript-зависимости устанавливаются через package-lock.json, PHP-зависимости — через composer.lock. Не удаляйте lock-файлы перед повтором: они фиксируют именно тот набор, на котором проверены примеры.

Сервисы mailpit и chaos используют один образ, но разные настройки и порты. Первый принимает письма, второй предсказуемо возвращает ошибку 451. Linux-образ запускает Node.js и PHP из того же проекта. Готовая папка public содержит статическую мастерскую и результаты проверок двух сред. Авторский код распространяется по MIT; лицензии зависимостей перечислены отдельно, PHPMailer сохраняет свою LGPL-лицензию.

Для первого запуска нужны Docker Compose v2, Node.js 24 или новее, PHP 8.2 или новее и Composer 2. Если менеджер зависимостей PHP ещё незнаком, начните с практического руководства по Composer. В этой статье команды выполняются из корня распакованного mailpit-project.

npm ci --ignore-scripts
composer install --no-dev --no-interaction --prefer-dist
docker compose up -d mailpit chaos
npm test
npm run capture
npm run serve

Мини-мастерская: пройти письмо от отправителя до проверки

Выберите одну из восьми задач и вариант результата. Переключатель Windows/Linux открывает запись соответствующего реального запуска. В представлении «Письмо» виден текст или безопасное представление HTML, в «Проверках» — утверждения теста, в «Исходнике» — код отправителя, в «Протоколе» — ответ SMTP и связанные данные API. Файл .eml можно скачать и изучить в подходящем редакторе или почтовом клиенте.

Мастерская воспроизводит записанное исполнение. Нажатие на вариант не соединяется с SMTP и не запускает PHP на сервере сайта. Чтобы отправить новое учебное письмо, скачайте проект и выполните команды локально. Результаты намеренно содержат вымышленные адреса example.invalid. Даты, идентификаторы и число писем в ящике относятся к конкретному запуску и могут отличаться при повторении.

Ниже находится отдельный мини-конструктор: измените имя и номер заявки, посмотрите получившийся шаблон и скачайте HTML. Это уже работа с вашим вводом прямо в браузере, но без отправки. Спецсимволы экранируются, а некорректный номер блокирует экспорт с понятным сообщением. Файлы реализации доступны в последнем блоке мастерской.

Два порта: SMTP принимает письмо, HTTP показывает результат

У Mailpit по умолчанию SMTP слушает порт 1025, а веб-интерфейс вместе с API — 8025. В нашем Compose они опубликованы на хосте как 11025 и 18025, чтобы отделить лабораторию от других локальных сервисов. Поэтому Node.js, запущенный в Windows, отправляет письмо на 127.0.0.1:11025, а браузер открывает http://127.0.0.1:18025.

Внутри Docker действует другой адрес: контейнер отправителя обращается к сервису mailpit по имени и внутреннему порту 1025. localhost внутри контейнера обозначает сам контейнер. Это частая причина Connection refused при переносе работающего локального примера в Compose. В переменных окружения сервиса linux уже заданы правильные значения, а клиент на хосте использует локальные значения по умолчанию.

Привязка опубликованных портов к 127.0.0.1 означает доступ с самого хоста. Такой вариант удобен для разработчика и закрытого VPS-стенда. Базовый принцип сетей и сервисов разобран в статье про Docker Compose для PHP и Node.js.

services:
  mailpit:
    image: axllent/mailpit:v1.33.1
    ports:
      - "127.0.0.1:18025:8025"
      - "127.0.0.1:11025:1025"
# Полный compose.yaml в архиве также ограничивает тестовых получателей.

Windows: первый запуск и первое полученное письмо

В Windows сначала запустите Docker Desktop в режиме Linux containers. Откройте PowerShell в папке проекта, установите зависимости и поднимите два сервиса. После завершения docker compose up проверьте их статус. Если браузер ещё не открывает 18025, посмотрите docker compose logs mailpit: команда создания контейнера и готовность приложения могут наступать не одновременно.

Команда npm run capture выполняет все шестнадцать вариантов. Она использует настоящие Nodemailer, PHPMailer и Mailpit API, сохраняет запись Windows и завершится ошибкой, если обязательное утверждение не выполнено. Передайте в PHP_BIN полный путь к php.exe, если команда php отсутствует в PATH. В PowerShell переменная задаётся через $env:, а не через префикс VAR=value, привычный для Bash.

После запуска откройте настоящий интерфейс Mailpit и найдите письма текущего опыта. Затем запустите npm run serve и откройте мастерскую на 8533. Веб-интерфейс Mailpit и авторская мастерская решают разные задачи: первый показывает живое содержимое лабораторного ящика, вторая помогает разобрать сохранённые случаи и код.

docker compose ps
docker compose logs --tail=50 mailpit
$env:PHP_BIN = "C:\php\php.exe" # Только если нужен собственный путь
npm run capture
# Настоящий Mailpit: http://127.0.0.1:18025
# Мастерская после npm run serve: http://127.0.0.1:8533

Linux: локальный запуск и полностью контейнерный повтор

На Linux можно использовать установленные PHP, Composer и Node.js, оставив только Mailpit в Docker. Команды установки зависимостей те же. Отличается запись переменной окружения: CAPTURE_NAME=linux перед npm run capture создаёт отдельный файл linux.json. Не переносите выражение с $env: из PowerShell в Bash без изменения синтаксиса.

Второй вариант — собрать сервис linux из Dockerfile и выполнить его через docker compose run –rm linux. В образ копируются lock-файлы, отправители и тесты; зависимости устанавливаются внутри. В общий каталог записывается только результат выполнения. Это позволяет проверить проект без настройки PHP и Node на самом сервере. Папку проекта всё равно нужно сохранить рядом с compose.yaml: относительные пути томов считаются от расположения конфигурации.

При сравнении запусков ориентируйтесь на ожидаемые проверки и исходники, а не на полное совпадение JSON. Message-ID, время и случайный идентификатор прогона создаются заново. В нашем повторе хеши исполняемых исходников совпали, а каждая среда выполнила 45 проверок реальных операций и пять отдельных тестов.

docker compose up -d mailpit chaos
docker compose build linux
docker compose run --rm linux
# Или при установленных PHP/Node:
CAPTURE_NAME=linux npm run capture

Задача 1. Письмо из Node.js через Nodemailer

Создайте SMTP transport с адресом своего Mailpit и отправьте сообщение с From, To, Subject и Text. Для простого уведомления текстового тела достаточно: это удобный первый тест соединения, кодировки и адресации. В мастерской можно переключить русский и английский текст и посмотреть исходное сообщение.

Важные параметры нужно задавать осознанно. Значение secure:false относится к способу установления соединения; само по себе оно не запрещает STARTTLS. В нашем закрытом тестовом Mailpit сертификаты не настроены, поэтому обмен остаётся локальным обычным SMTP. Настройки настоящего провайдера могут требовать STARTTLS, TLS и аутентификацию — переносить лабораторную конфигурацию как готовую боевую нельзя.

Ограниченные таймауты дают приложению возможность закончить неудачную попытку, а не висеть бесконечно. В исходниках дополнительно разрешены только собственные имена локального стенда и отключено чтение вложений по произвольным файловым или сетевым путям. Это ограничения учебного примера, которые помогают избежать случайного использования другого сервера во время эксперимента.

import { transport } from './src/mail.mjs';
const info = await transport().sendMail({
  from: 'workshop@example.invalid',
  to: 'reader@example.invalid',
  subject: 'Заявка №104',
  text: 'Заявка принята. Это тестовое уведомление.'
});
console.log(info.messageId, info.response);

Задача 2. PHPMailer: HTML и текстовая альтернатива

В PHP удобно использовать SMTP-клиент, который сам формирует заголовки и MIME-части. PHPMailer подключается через Composer autoload, получает адрес Mailpit и отправляет сообщение независимо от локального sendmail. Это особенно удобно, когда один проект запускают и в Windows, и на Linux.

HTML-письму нужна понятная текстовая альтернатива. Body содержит оформление, AltBody — смысл сообщения без тегов. Она полезна для клиентов и режимов отображения, которые не используют HTML, а также для проверок содержания. Не стоит автоматически превращать сложный шаблон в текст простым удалением тегов: можно потерять смысл ссылок, списков и таблиц. В примере обе версии подготовлены явно.

Второй вариант задачи вставляет имя со знаками <, > и &. htmlspecialchars превращает их в обычный текст внутри HTML. Отправитель должен экранировать данные в месте вставки, а не надеяться, что почтовый клиент исправит ошибку. В мастерской можно убедиться, что имя отображается полностью и не создаёт дополнительные элементы разметки.

$mail = new PHPMailer\PHPMailer\PHPMailer(true);
$mail->isSMTP();
$mail->Host = '127.0.0.1';
$mail->Port = 11025;
$mail->CharSet = 'UTF-8';
$mail->setFrom('workshop@example.invalid');
$mail->addAddress('reader@example.invalid');
$mail->Subject = 'Заявка №104';
$mail->isHTML(true);
$mail->Body = '<h1>Заявка №104</h1><p>Принята.</p>';
$mail->AltBody = 'Заявка №104 принята.';
$mail->send();

Задача 3. Вложения: проверить содержимое, а не значок скрепки

Наличие вложения в списке писем ещё не доказывает, что прикреплён правильный отчёт. Файл может быть пустым, относиться к предыдущему заказу или иметь неверную кодировку. В нашей задаче отправляются небольшой CSV и JSON. После получения тест скачивает MIME-часть через API и побайтово сравнивает её с ожидаемым содержимым.

Для небольших генерируемых файлов удобно передать библиотеке готовую строку или Buffer. Тогда не требуется создавать временный файл только ради отправки. Для больших документов подход будет другим: поток, ограничение размера и управление временными ресурсами. В любом случае путь к вложению не должен произвольно поступать из пользовательского запроса.

В мастерской вы увидите имя, размер, SHA-256 и текст извлечённого файла. Проверка работает с реальным результатом разборки MIME в Mailpit. Если вы измените генератор отчёта, обновите и ожидание теста: иначе успешная SMTP-передача продолжит скрывать ошибку содержания. Имя файла и Content-Type также стоит проверять отдельно в своём проекте.

await transport().sendMail({
  from: 'workshop@example.invalid', to: 'reader@example.invalid',
  subject: 'Учебный отчёт', text: 'Отчёт во вложении.',
  attachments: [{
    filename: 'report.csv', contentType: 'text/csv',
    content: 'id,total\n104,12500\n'
  }]
});

Задача 4. From, Reply-To, Cc и Bcc решают разные задачи

From описывает отправителя сообщения, Reply-To задаёт адрес для ответа, Cc добавляет видимую копию, Bcc — скрытого получателя. В форме обратной связи эти роли особенно легко смешать. Для своего домена обычно используют стабильный адрес отправителя, а проверенный адрес автора заявки помещают в Reply-To, если такой сценарий предусмотрен продуктом.

Варианты мастерской показывают видимую и скрытую копию. Здесь важна особенность тестового приёмника: Mailpit знает получателей SMTP envelope и может показывать Bcc в своём интерфейсе или API. Это не означает, что обычный адресат должен видеть скрытый адрес в полученном письме. Проверяя конфиденциальность копий, различайте данные тестового сервера и MIME, который увидит конечный пользователь.

Адреса должны проходить проверку бизнес-правил. Синтаксически корректный email ещё не означает, что пользователь имеет право добавить его получателем системного отчёта. В учебных данных все адреса принадлежат example.invalid, а нормальный SMTP-сервис дополнительно ограничен этим суффиксом. Для реальной формы определите допустимые поля и получателей до вызова библиотеки.

const options = {
  from: 'workshop@example.invalid',
  to: 'reader@example.invalid',
  replyTo: 'support@example.invalid',
  bcc: 'audit@example.invalid',
  subject: 'Проверка адресации', text: 'Учебный текст'
};
await transport().sendMail(options);

Задача 5. Поиск и теги: отделить нужное письмо от общего потока

Когда несколько разработчиков или тестов пользуются одним ящиком, выбор «последнего сообщения» становится ненадёжным. Оно может относиться к другой задаче. Mailpit поддерживает фильтры по адресам, теме, тегам, наличию вложений и другим признакам. В нашей задаче конкретному письму назначается тег текущего запуска, затем выполняется поиск по этому тегу.

API возвращает несколько счётчиков. Поле total описывает весь ящик; messages_count — число сообщений, соответствующих запросу. Вариант без результата специально показывает различие: общий ящик не пуст, но поиск отсутствующего тега ничего не находит. Проверять total === 0 в таком тесте было бы ошибкой.

Операция установки тегов заменяет набор тегов выбранных сообщений. Если в вашем процессе есть другие полезные метки, сначала учтите их, иначе новая запись затрёт прежнюю классификацию. Подробный синтаксис фильтров приведён в официальном руководстве по поиску, а семантика меток — в разделе tagging.

const query = 'tag:aen-test-run';
const response = await fetch(
  'http://127.0.0.1:18025/api/v1/search?query=' + encodeURIComponent(query)
);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const result = await response.json();
console.log('Совпадений:', result.messages_count);
console.log('Во всём ящике:', result.total);

Задача 6. Проверить бизнес-содержание письма через API

Хорошая проверка уведомления отвечает на конкретные вопросы: правильный ли номер заказа, указан ли нужный получатель, есть ли обязательная ссылка, не пропало ли вложение. Mailpit API позволяет получить разобранное сообщение и выполнить такие утверждения обычным кодом теста. Начните с двух-трёх важных условий вместо снимка всего MIME, который меняется из-за времени и идентификаторов.

В шестой задаче первый вариант сравнивает тему с правильным ожиданием. Во втором мы намеренно задаём другую тему. SMTP в обоих случаях принимает сообщение, но проверка контракта во втором варианте показывает ошибку. Мастерская отдельно отмечает, что это ожидаемая демонстрация: техническая проверка опыта прошла, а бизнес-утверждение специально не выполнено.

В реальном CI такое несоответствие должно завершать тест с ошибкой. Не превращайте catch в безусловный успешный ответ только ради зелёного отчёта. Если ожидаемый отказ действительно является сценарием, проверяйте точный тип и причину ошибки. Так набор тестов отличает предусмотренное негативное поведение от случайно сломанного подключения или неправильно сформированного JSON.

import assert from 'node:assert/strict';
import { checkContract } from './src/mail.mjs';
const checks = checkContract(receivedMail, expectedSubject);
assert.ok(checks.every(item => item.ok),
  checks.filter(item => !item.ok).map(item => item.label).join('; '));
// receivedMail — сообщение, прочитанное через Mailpit API.
// Полный получатель и воспроизводимый запуск — в tools/capture.mjs.

Задача 7. Chaos: получить настоящий отказ SMTP 451

Неудачное подключение и отказ команды SMTP — разные ситуации. Чтобы проверить именно ответ сервера, в Compose есть отдельный сервис chaos. Он запускается с Sender:451:100: на этапе отправителя возвращается 451 с вероятностью сто процентов. Такой режим детерминирован и подходит для воспроизводимого теста; случайная вероятность была бы неудобна для обязательной проверки CI.

Nodemailer получает ошибку с кодом ответа и командой. В мастерской показано, что письмо не принято, а скачивание .eml отключено: у этого опыта нет сохранённого успешного сообщения. Второй вариант обращается к исправному SMTP-порту и показывает успешную отправку. Это явное переключение стенда для проверки восстановления, а не скрытая реализация автоматического повтора.

В рабочем приложении временный отказ можно обрабатывать очередью с ограничением числа попыток и задержкой. Нужно учитывать и неопределённый результат, когда сервер успел принять письмо, но клиент не получил подтверждение: повтор может создать дубль. Обработка таких случаев зависит от уведомления и требований бизнеса. Официальные параметры находятся в документации Chaos.

environment:
  MP_CHAOS_TRIGGERS: Sender:451:100
# Только отдельный тестовый сервис.
# Вариант клиента:
# await transport(true).sendMail(fixture);

Задача 8. Message-ID и изоляция параллельных тестов

Каждый запуск получает уникальную метку, а каждое письмо — собственный Message-ID. Клиент ждёт появление именно этого сообщения, а не первого элемента общего списка. Это полезно и локально, и в CI, где несколько заданий могут обращаться к одному стенду. Восьмая задача сравнивает поиск правильного идентификатора и поиск заведомо чужого.

Ожидание построено как ограниченный опрос API: запрос, проверка результата, небольшая пауза и следующий запрос до заданного срока. Отдельный HTTP-запрос также имеет таймаут. Общая длительность может включать время завершающего запроса, поэтому формулировка «ровно пять секунд» для такого цикла была бы неточной. Важен конечный выход с понятной ошибкой, если письмо не найдено.

Message-ID помогает сопоставить результат, но сам по себе не обеспечивает идемпотентность бизнес-операции. Для уведомления о заказе решение о повторной отправке обычно связывают с ID события или записью состояния в приложении. Наша лаборатория не очищает весь общий ящик перед каждым тестом: изоляция достигается адресным поиском, поэтому чужие тестовые сообщения сохраняются.

const query = 'message-id:' + messageId.replace(/[<>]/g, '');
const response = await fetch(apiUrl + '/api/v1/search?query=' +
  encodeURIComponent(query), { signal: AbortSignal.timeout(5000) });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const found = await response.json();
if (found.messages_count !== 1) throw new Error('Нет однозначного результата');

Мини-конструктор: безопасно подготовить шаблон уведомления

Конструктор на странице решает небольшую самостоятельную задачу: позволяет изменить имя и номер заявки, сразу увидеть результат и скачать HTML. Вы можете ввести имя со спецсимволами и проверить, что они останутся текстом. Поле номера допускает от одной до двенадцати цифр; при ошибке интерфейс сообщает причину и отключает выгрузку.

Готовый HTML ещё не является MIME-письмом. Его нужно передать выбранной почтовой библиотеке как тело, подготовить текстовую альтернативу, добавить тему и получателя. Именно библиотека сформирует заголовки и границы частей. Не собирайте SMTP-протокол вручную ради подстановки пары полей: это усложнит поддержку кодировок, вложений и адресов.

В своём проекте отделяйте шаблон от транспортного слоя. Тогда один тест проверяет подстановку и экранирование, второй — правильную передачу SMTP, третий — обязательное содержание полученного сообщения. Шаблон проще менять без вмешательства в настройки сервера. Для сложных писем используйте шаблонизатор, а исходные данные формируйте из проверенной модели заказа или заявки.

function escapeHtml(value) {
  return String(value).replace(/[&<>"']/g, char => ({
    '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;'
  }[char]));
}
const body = `<p>Здравствуйте, ${escapeHtml(name)}!</p>`;
// Экранирование показано для текстового содержимого HTML.
// URL и другие контексты требуют собственных правил проверки.

Как подключить к существующему приложению

Начните с конфигурации транспорта в окружении разработки. Меняйте адрес SMTP и порт через настройки, а не через редактирование каждого места отправки. Для контейнерного приложения используйте имя сервиса, для процесса на хосте — опубликованный loopback-порт. Разделите конфигурации разработки, тестов и рабочей среды, чтобы переход между ними был явным.

Создайте в приложении один небольшой сервис отправки уведомлений. Пусть он получает готовые данные, выбирает шаблон и возвращает понятный результат или исключение. Контроллеру формы не нужно знать MIME-детали, но он должен понимать, что делать при ошибке: сохранить заявку, поставить задачу в очередь, показать корректное сообщение пользователю. Не связывайте успешное сохранение бизнес-данных с обещанием мгновенной доставки письма, если архитектура этого не гарантирует.

Учебный транспорт ограничивает допустимые SMTP-хосты специально. При переносе в реальную систему замените эту проверку собственной конфигурацией и предусмотрите секреты, TLS и права доступа. Удаление ограничения должно быть осознанным изменением проекта, а не случайной переменной, переданной из HTTP-запроса пользователя.

# Приложение на самом хосте
SMTP_HOST=127.0.0.1
SMTP_PORT=11025
MAILPIT_URL=http://127.0.0.1:18025

# Приложение в сети Compose
SMTP_HOST=mailpit
SMTP_PORT=1025
MAILPIT_URL=http://mailpit:8025

Linux/VPS: закрыть стенд и открыть его через SSH-туннель

Тестовый ящик может содержать тексты заявок, ссылки восстановления и внутренние отчёты, если приложение подключено без фильтрации данных. Поэтому на выделенном сервере полезно начать с закрытой схемы. Наш Compose публикует интерфейс только на 127.0.0.1. Для просмотра с рабочего компьютера создайте SSH-туннель, направляющий локальный порт на loopback-порт VPS.

После подключения адрес в браузере останется http://127.0.0.1:18025, но запросы пойдут по SSH к серверу. Замените user@your-server своей учётной записью и адресом. Если локальный порт уже занят, выберите другой левый порт в -L. Для нескольких разработчиков можно дать каждому собственный туннель и обычный доступ SSH, не открывая SMTP или API всему интернету.

Командный доступ к Docker и файлам проекта должен соответствовать вашим правилам администрирования. Внешние DNS, firewall и сертификаты в этом учебном повторе не настраивались. Если сервису нужен общий веб-адрес, настройте HTTPS, аутентификацию и сетевые ограничения отдельно; параметры приведены в официальном разделе HTTP/UI.

# Выполнить на рабочем компьютере, работает и в OpenSSH для Windows:
ssh -N -L 18025:127.0.0.1:18025 user@your-server
# Затем открыть http://127.0.0.1:18025

# На VPS из каталога проекта:
docker compose up -d mailpit chaos
docker compose logs --tail=100 mailpit

Хранение писем, сроки жизни и остановка лаборатории

В текущей конфигурации Mailpit использует временную базу. После удаления контейнера накопленные письма исчезнут, а сохранённые JSON-записи проекта останутся на диске. Для самостоятельного учебного стенда это удобно: не требуется поддерживать архив почты. Для команды, которой нужно сохранять результаты между перезапусками, можно задать MP_DATABASE и отдельный том.

Постоянное хранение меняет обязанности: нужно определить срок жизни тестовых сообщений, резервирование и доступ. Ограничение количества писем не является полноценной политикой хранения чувствительных данных. Если тесты используют настоящие ссылки или реквизиты, сначала пересмотрите состав фикстур. В нашей публичной мастерской используются только авторские учебные сообщения.

Завершайте работу командой docker compose down из каталога конкретного проекта. Она относится к этому стенду; глобальная очистка Docker для такой задачи не нужна. Локальный сервер мастерской останавливается Ctrl+C. При добавлении постоянного тома отдельно решайте, когда удалять данные: автоматическое применение down -v может уничтожить нужную историю тестов.

docker compose stop       # Остановить сервисы, оставив контейнеры
docker compose start      # Запустить остановленные контейнеры
docker compose down       # Удалить контейнеры и сеть этого проекта
# После down временная база внутри контейнера не сохраняется.

Проверки в CI: воспроизводимость и понятная диагностика

В CI полезно поднимать свежий Mailpit рядом с тестируемым приложением и ждать его готовности перед отправкой. Закрепляйте версии образов и зависимости, чтобы изменение внешнего пакета не превращало один и тот же коммит в разные результаты. Сохраняйте достаточно диагностики для ошибки: идентификатор опыта, ожидаемый получатель, статус API и причина несовпадения. Полный SMTP debug может содержать тело сообщения, поэтому состав логов выбирайте осознанно.

Наш capture выполняет шестнадцать вариантов последовательно и завершает процесс при нарушении обязательного утверждения. Ожидаемые ошибки проверяются отдельно: 451 должен быть именно 451, а неверный контракт должен провалиться по предусмотренной причине. Отсутствие PHP, недоступный API или другая сетевая ошибка не считаются успешным негативным тестом.

Для ускорения обычно достаточно нескольких важных сценариев на каждый тип уведомления. Не нужно проверять библиотеку SMTP во всех подробностях, если ваша ответственность — правильные данные заказа и обработка результата. После изменения шаблона повторите контрактные проверки и визуально посмотрите письмо в требуемых клиентских средах.

npm ci --ignore-scripts
composer install --no-dev --no-interaction --prefer-dist
docker compose up -d mailpit chaos
npm test
npm run capture
# Ненулевой exit code останавливает CI-этап.
# Очистку docker compose down выполняйте в завершающем этапе.

Частые ошибки и быстрый маршрут диагностики

Если соединение отклонено, начните с адреса и порта: процесс на хосте использует 11025, контейнер в общей сети — mailpit:1025. Затем проверьте docker compose ps и логи. Если SMTP принял сообщение, но тест его не находит, сравните Message-ID и строку поиска, убедитесь, что API обращается к тому же экземпляру Mailpit.

Если тест поиска показывает странное количество, проверьте messages_count вместо total. Если HTML выглядит как обычный текст, посмотрите, какой MIME-тип сформировал отправитель и заполнено ли HTML-поле API. Если имя с символами меняет разметку, исправьте экранирование в шаблоне. Если вложение пустое, сравните реальные скачанные байты с результатом генератора до передачи.

Ошибка 451 на обычном сценарии может означать, что клиент по ошибке использует Chaos-порт 11026. Это полезная причина держать стенды отдельно и ясно подписывать их. Для неожиданного Bcc проверьте как envelope, так и назначение поля в интерфейсе тестового приёмника. Для каждой исправленной проблемы добавьте одно содержательное утверждение, которое поймает её повторение.

Что проверено и где заканчивается этот пример

В Windows выполнены пять тестов и 45 проверок SMTP/HTTP для шестнадцати вариантов. Тот же набор прошёл в Linux-контейнере. Windows-клиенты работали на Node.js 24.19.0 и PHP 8.3.33, Linux-клиенты — на Node.js 24.22.2 и PHP 8.3.36. Сам Mailpit в обоих случаях работал отдельным Linux-контейнером версии 1.33.1. Хеши исполняемых исходников между средами совпали.

Браузерная мастерская проверяется отдельно от серверных тестов: переключение случаев, ожидаемые ошибки, скачивание .eml, генерация HTML и узкая компоновка. Это не запуск SMTP внутри страницы и не доказательство работы на любом физическом телефоне. Записи позволяют разобраться с результатами, а исходный проект нужен для нового исполнения и изменения логики.

Проверки намеренно не выполняют реальную внешнюю рассылку, не включают relay/forwarding и не оценивают репутацию почтового домена. Для production-приложения добавьте собственную конфигурацию транспорта, очереди, наблюдение за ошибками, обработку повторов и отдельный процесс проверки доставляемости. Смысл лаборатории — сделать этот путь понятным и воспроизводимым.

Скачать проект и внедрить одну полезную проверку

В архиве находятся SMTP-стенд, отправители PHP и Node.js, unit-тесты, реальный capture, готовая мастерская и инструкции Windows/Linux/VPS. Откройте README, повторите стандартный запуск и выберите одно уведомление из своего приложения. Подмените транспорт в тестовой среде, проверьте получателя и два обязательных поля. Затем добавьте вложение, если оно есть, и предусмотренный отказ отправки.

Дальше проект можно расширять постепенно: дополнительные типы писем, данные разных локалей, отсутствующие поля, ограничения размера вложений и проверка нескольких получателей. Каждый сценарий должен объяснять, какую ошибку он ловит и какой результат считается правильным. Так почтовая интеграция превращается из ручного «вроде пришло» в понятную часть тестирования продукта.

Полезные первичные источники: возможности Mailpit, HTTP API, параметры запуска, SMTP transport Nodemailer и PHPMailer. Актуальные версии и изменения проверяйте перед обновлением своего стенда.