Download the PHP package ttbooking/mailspoon without Composer
On this page you can find all versions of the php package ttbooking/mailspoon. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Informations about the package mailspoon
Mailspoon
Простое реле IMAP → HTTP-вебхук, совместимое с Mailgun. Пакет для Laravel.
Mailspoon подключается к обычному IMAP-ящику, следит за появлением новых писем и
пересылает каждое входящее письмо на HTTP-эндпоинт, используя тот же формат
данных и схему подписи, что и входящие вебхуки Mailgun. Это позволяет
продолжать обрабатывать почту привычным Mailgun-эндпоинтом (например,
laravel-mailbox), даже когда
письма приходят по обычному IMAP, а не через Mailgun.
Устанавливается composer-пакетом в любое приложение
Laravel 13; чтение почты — на базе
ImapEngine
(directorytree/imapengine-laravel).
Как это работает
Mailspoon работает по схеме store-and-forward: чтение ящика отделено от доставки вебхука, поэтому медленный или недоступный эндпоинт не блокирует однопоточное чтение почты.
- Команда забирает непрочитанные письма из папки ящика (по умолчанию INBOX)
и на каждое диспатчит событие
MessageReceivedизImapEngine. - Слушатель
StoreIncomingMessageсохраняет сырой MIME в хранилище, создаёт запись о письме со статусомpendingи сразу помечает письмо прочитанным — приём надёжно зафиксирован локально. - Команда
mailspoon:deliverнезависимо разбираетpending-записи и шлёт POST на эндпоинт. Успех →delivered; ошибка →attempts++иfailed, письмо переотправится на следующем запуске (доMAILSPOON_MAX_ATTEMPTS).
Дедупликация по Message-Id (или хешу письма, если заголовка нет) исключает
повторную обработку одного и того же сообщения.
Содержимое вебхука
Запрос отправляется как application/x-www-form-urlencoded и содержит
следующие поля, повторяющие входящий MIME-вебхук Mailgun:
| Поле | Описание |
|---|---|
body-mime |
Полный исходный MIME-текст письма. |
timestamp |
Unix-метка момента отправки вебхука. |
token |
Случайный hex-токен длиной 50 символов, уникальный для каждого запроса. |
signature |
HMAC-SHA256(timestamp + token, MAILSPOON_KEY) — проверяется на стороне получателя. |
Проверяйте подпись на своей стороне так же, как для Mailgun:
hash_hmac('sha256', $timestamp . $token, $signingKey).
Помимо полей формы запрос несёт служебные HTTP-заголовки:
| Заголовок | Описание |
|---|---|
X-Mailspoon-Message-Id |
Message-Id письма (отсутствует, если у письма нет этого заголовка). |
X-Mailspoon-Attempt |
Номер попытки доставки этой записи, начиная с 1. |
Доставка — at-least-once. Успехом считается полученный ответ 2xx: если
эндпоинт обработал запрос, но ответ потерялся (таймаут, обрыв), письмо будет
отправлено повторно. Обработчики на принимающей стороне должны быть
идемпотентными — заголовок X-Mailspoon-Message-Id позволяет отбросить
дубликат ещё до разбора MIME.
Требования
- PHP 8.3+
- Приложение Laravel 13 (хост)
- IMAP-ящик
- HTTP-эндпоинт для приёма пересылаемых писем
- База данных — хранит записи о письмах и статус доставки
- Диск хранилища (
config/filesystems.php) с'throw' => true— для архива сырого MIME
Установка
Миграции пакета применяются автоматически; при желании их можно скопировать в
приложение: php artisan vendor:publish --tag=mailspoon-migrations.
Диск архива: обязателен 'throw' => true
Архив .eml — единственная копия письма после пометки прочитанным, поэтому
ошибки записи/чтения/удаления не должны подавляться Flysystem. Mailspoon
отказывается работать с диском, у которого 'throw' => false (значение по
умолчанию в свежем Laravel). Включите его для выбранного диска в
config/filesystems.php:
Конфигурация
IMAP-подключение (config/imap.php)
Дополнительные необязательные переменные: IMAP_TIMEOUT, IMAP_DEBUG,
IMAP_VALIDATE_CERT, IMAP_AUTHENTICATION, а также настройки прокси
(IMAP_PROXY_SOCKET, IMAP_PROXY_USERNAME, IMAP_PROXY_PASSWORD,
IMAP_PROXY_REQUEST_FULLURI).
В config/imap.php под ключом mailboxes можно описать несколько ящиков;
встроенный называется default.
Адрес пересылки (config/mailspoon.php)
MAILSPOON_ENDPOINT— URL, который принимает пересылаемые письма.MAILSPOON_KEY— общий секрет для подписи каждого запроса.
Маршрутизация ящиков (config/mailspoon.php)
Каждому ящику можно назначить собственный эндпоинт и ключ подписи — карта
routes в опубликованном конфиге, ключ — имя ящика из config/imap.php:
Опции маршрута: endpoint и key (откат на глобальные), mark (маркер
просмотра, см. «Ящики-люди»), filters (заменяют глобальные целиком, см.
«Фильтрация писем») и enabled (см. «Приостановка ящика»).
Ящик без маршрута (или с частичным маршрутом) использует глобальные
MAILSPOON_ENDPOINT/MAILSPOON_KEY; если маршруты заданы для всех ящиков,
глобальные значения можно не задавать вовсе — mailspoon:doctor проверяет,
что каждый ящик резолвится хоть во что-то. Эндпоинт фиксируется в записи в момент
захвата письма, а ключ подписи выбирается в момент доставки — поэтому ротация
ключа действует и на ещё не доставленные письма, а смена эндпоинта — только на
новые.
Приостановка ящика (enabled)
Чтобы временно остановить или отложить чтение конкретного ящика, не удаляя его
настройки, задайте в маршруте 'enabled' => false:
Это касается только захвата: mailspoon:pull сразу завершается, не подключаясь
к IMAP и не двигая UID-курсор; mailspoon:sentry не запускает ни pull, ни
IDLE-наблюдение; а cron-poll из schedule.pull для этого ящика не регистрируется
в планировщике. Письма, уже сохранённые в архиве, продолжают доставляться
mailspoon:deliver как обычно. По умолчанию ящик включён (флаг можно опустить).
Прямой запуск imap:watch пакета imapengine-laravel флаг не проверяет.
Фильтрация писем (config/mailspoon.php)
Правила include/exclude применяются до захвата: отфильтрованное письмо
помечается просмотренным, но не попадает ни в журнал, ни в архив, ни на
эндпоинт. Карта filters — глобально или на маршруте (маршрутная заменяет
глобальную целиком):
deny приоритетнее allow; пустой allow пропускает всё. Поля: from,
subject, header, has_attachment. Кривое правило (битый регэксп,
неизвестное поле) ловится на старте и в mailspoon:doctor, а не молча
пропускает письма. Проверить правила на конкретном письме до запуска чтения —
mailspoon:filter-test.
Каждое отфильтрованное письмо оставляет след: запись в логе Laravel и событие
MessageFiltered — слишком строгое allow-правило видно по логу,
а не по тишине в журнале.
Пример: пропускать только подтверждения о прочтении (MDN) — формат
multipart/report с report-type=disposition-notification:
Ящики-люди: маркер просмотренного (mark)
По умолчанию mailspoon помечает обработанные письма прочитанными (\Seen —
его курсор), что годится для ящика-робота. Если ящик читают люди (общий ящик
операторов), прочитанность трогать нельзя — настройка mark глобально или на
маршруте:
seen(дефолт) — текущее поведение;keyword:<имя>— кастомный IMAP-кейворд: невидим в почтовых клиентах, курсор живёт на сервере; сервер должен разрешать кастомные кейворды (PERMANENTFLAGS \*— Dovecot, Gmail, Exchange умеют);none— письмо не трогается вовсе; позиция отслеживается UID-курсором в БД (таблицаrelay_cursors, сбрасывается при сменеUIDVALIDITY). Курсор продвигает толькоmailspoon:pull; IDLE-режим (mailspoon:sentry) видит лишь новые поступления и захватывает их в журнал, но курсор не двигает — после рестарта pre-fetch перечитает диапазон с последнего pull, дедуп отбросит уже захваченное (повторной доставки не будет, только повторное скачивание). Для cron-poll эта оговорка не действует: там каждый запуск — pull.
Тонкость стыка none × retention: дедуп-записи журнала живут
MAILSPOON_RETENTION_DAYS дней. Если сервер сбросит UIDVALIDITY (переезд,
пересоздание папки) после того, как записи о старых письмах уже вычищены,
UID-курсор обнулится и эти письма будут захвачены и доставлены повторно —
дедупу не с чем их сравнить. Ситуация редкая (нужны оба события сразу), но на
ящике с mark: none и короткой retention стоит про неё помнить; защита на
принимающей стороне — те же идемпотентные обработчики.
Маркер ставится всем просмотренным письмам, включая отфильтрованные —
иначе они перечитывались бы каждым запуском; на эндпоинт уходят только
прошедшие фильтр. Первый запуск на ящике с keyword:/none просмотрит весь
ящик (а не только непрочитанное) — это сознательно: обрабатывается вся
история, совпавшая с фильтром.
Хранилище и доставка (config/mailspoon.php)
MAILSPOON_ARCHIVE_DISK/MAILSPOON_ARCHIVE_PATH— куда складывается архив.eml; диск обязан иметь'throw' => true(см. выше).MAILSPOON_RETENTION_DAYS— сколько дней хранить завершённые записи вместе с.eml; по умолчанию3, значение0отключает автоматическую очистку.MAILSPOON_PRUNE_CRON— расписание штатной команды Laravelmodel:prune.MAILSPOON_TIMEOUT/MAILSPOON_CONNECT_TIMEOUT— общий таймаут запроса и отдельный лимит на установление TCP-соединения, чтобы зависший handshake не подвешивал воркер.MAILSPOON_TRIES— короткие повторы внутри одной попытки для мгновенных блипов (сеть, 5xx, 429); постоянные 4xx не повторяются.MAILSPOON_BACKOFF— растущая пауза между запускамиmailspoon:deliver: упавшее письмо берётся повторно только после задержки, соответствующей номеру попытки (последнее значение применяется для всех дальнейших).MAILSPOON_MAX_ATTEMPTS— после стольких неудачных попыток письмо перестаёт переотправляться и остаётся в статусеfailedдля ручного разбора.
Карты — в опубликованном конфиге
Структурные настройки (например, расписание cron-poll по ящикам) задаются
обычным PHP в config/mailspoon.php — без сериализации в env:
Опубликованный конфиг должен сохранять полную структуру секций: merge с дефолтами пакета выполняется только по верхнему уровню.
Маршруты из хранилища (Mailspoon::register())
Если набор ящиков не статичен — например, приложение использует Mailspoon как
читца почты и каждый клиент регистрирует свой ящик и вебхук через UI, — маршруты
можно задавать в рантайме, не редактируя config/mailspoon.php. Фасад
Mailspoon повторяет Imap из imapengine: метод register() добавляет или
переопределяет маршрут для ящика.
Зарегистрированный маршрут имеет приоритет над одноимённым в конфиге и заменяет
его целиком (не сливается). Опции — те же, что в карте routes, плюс
необязательный schedule: ящик с расписанием попадает в cron-poll наравне с
записями schedule.pull, так что новый ящик начинает опрашиваться без правки
конфига. Семантика времени прежняя: endpoint фиксируется при захвате, key
выбирается при доставке, mark/filters — при захвате.
Динамические IMAP-подключения (Imap::register())
Маршрут описывает доставку. Чтобы pull-режим тоже работал из хранилища, ящику
нужно само IMAP-подключение: mailspoon:pull/:sentry соединяются через
Imap::mailbox($name), который резолвит config/imap.php. Своей абстракции для
этого не нужно — у imapengine есть публичный Imap::register($name, $config),
кладущий подключение в его менеджер на время процесса. Регистрируйте подключение
и маршрут рядом, в boot() своего сервис-провайдера:
Так покрыты все точки входа: mailspoon:pull (в т.ч. фоновый), mailspoon:sentry
(регистрация в одном процессе переживает вложенные pull и vendor imap:watch)
и mailspoon:deliver (к IMAP не обращается).
Что учесть:
boot()выполняется на каждый процесс — кэшируйте выборку из БД либо регистрируйте только подrunningInConsole()/нужные команды, чтобы не делать запрос на каждый веб-реквест.- IMAP-пароли в БД храните через encrypted-cast.
- Прямой запуск vendor
imap:watchмимо команд Mailspoon подключение из БД не получит — регистрируйте его сами или держите ящик вconfig/imap.php.
Использование
Mailspoon предоставляет команды чтения (mailspoon:pull, mailspoon:sentry)
и команду доставки (mailspoon:deliver). Аргумент mailbox — это имя ящика из
config/imap.php (для встроенного используйте default). Необязательный
аргумент folder выбирает папку, отличную от INBOX.
mailspoon:pull — разовая проверка
Забирает все текущие непрочитанные письма, сохраняет их и завершается.
Опции:
--with=— список через запятую частей письма для подгрузки. Если опция не задана или пуста, используютсяflags,headers,body, необходимые для сохранения полного сырого MIME.--chunk=— сколько писем забирать одной IMAP-командой (по умолчаниюMAILSPOON_PULL_CHUNK, 100). Письма выбираются пачками от старых к новым: одинFETCHс тысячами UID (большой бэклог, первый прогон с маркеромkeyword:/none) превышает лимит длины команды сервера — Dovecot отвечаетBAD ... Too long argument. Дляnone-маркера UID-курсор сохраняется после каждой пачки, так что прерванный прогон бэклога продолжится с места остановки.
Подходит для запуска по расписанию (cron), когда долгоживущий процесс не нужен.
mailspoon:sentry — забрать накопившееся и следить дальше
Сначала один раз выполняет mailspoon:pull, чтобы сохранить накопившиеся
письма, затем начинает следить за ящиком в реальном времени (через IMAP IDLE) и
сохраняет письма по мере поступления. Это рекомендуемый способ запускать
Mailspoon как постоянный воркер.
Опции:
--method=idle— метод слежения (по умолчаниюidle).--with=— части письма для подгрузки (по умолчаниюflags,headers,body).--timeout=30— таймаут IDLE в секундах.--attempts=5— число попыток переподключения.--debug=false— включить отладочный вывод.
Запускайте под супервизором процессов (systemd, Supervisor и т. п.), чтобы он перезапускался автоматически:
Команда
imap:watch(только слежение, без предварительного разбора) предоставляется самим ImapEngine;mailspoon:sentry— это обёртка надmailspoon:pull+imap:watch.Команды чтения только сохраняют письма (архив + запись
pending) и помечают их прочитанными. Сама доставка на эндпоинт выполняется отдельно — командойmailspoon:deliver.
mailspoon:deliver — доставка сохранённых писем
Разбирает pending-записи (и ранее проваленные, у которых прошёл backoff и не
исчерпан лимит попыток), читает сырой MIME из архива и шлёт подписанный POST на
эндпоинт. Ретрай двухуровневый:
- внутри попытки — короткие повторы (
MAILSPOON_TRIES) для мгновенных сетевых блипов и ответов 5xx/429, с ограничением таймаутов (MAILSPOON_TIMEOUT,MAILSPOON_CONNECT_TIMEOUT); - между запусками — упавшее письмо переносится на потом через
next_attempt_atпо расписаниюMAILSPOON_BACKOFF, без блокирующих пауз в воркере.
Так зависший или медленный эндпоинт никогда не тормозит чтение ящика.
Опции:
--limit=50— максимум писем за один запуск.--max-attempts=— переопределитьMAILSPOON_MAX_ATTEMPTS.--dry-run— показать таблицей, что и куда ушло бы (эндпоинт, источник ключа, состояние архива), не отправляя запросов и не меняя записи.
Команда — разовая (one-shot); запускать её периодически проще всего
планировщиком (см. ниже), который уже вызывает mailspoon:deliver с
withoutOverlapping().
mailspoon:replay — переотправка писем
Сбрасывает записи журнала обратно в pending — фактическую отправку выполнит
ближайший запуск mailspoon:deliver (сырой MIME читается из архива, лезть в
ящик заново не нужно). Счётчик попыток обнуляется, так что переотправляются и
письма с исчерпанным лимитом.
Replay — явное действие оператора: дедупликация сознательно обходится, можно переотправить и уже доставленное письмо (например, после потери данных на стороне получателя).
mailspoon:doctor — диагностика конфигурации
Проверяет всю цепочку до запуска воркера: наличие таблицы журнала, запись и
чтение на диске архива (включая обязательный 'throw' => true), эндпоинт и
ключ каждого ящика (маршрут или глобальные), реальный IMAP-логин и доступность
эндпоинта. Печатает образец подписи для сверки ключа с получателем
(MAILBOX_MAILGUN_KEY у laravel-mailbox). Завершается ненулевым кодом при
любой провальной проверке — удобно как preflight в деплое.
Отдельная проверка schedule сообщает cron-расписание ящика. Её отсутствие —
не ошибка (ящик может читаться mailspoon:sentry или ручным mailspoon:pull),
поэтому это предупреждение (строка !), а не провал: код возврата остаётся
нулевым, в JSON-отчёте статус проверки — warn. Расписание проверяется и для
паузнутого маршрута (enabled => false): doctor — preflight, его часто гоняют
во время настройки, пока ящик ещё выключен, поэтому пауза без расписания тоже
даёт предупреждение, а не прячет недонастройку.
По умолчанию эндпоинт только пробуется OPTIONS-запросом (без тестовой почты в
принимающее приложение); --send отправляет полноценное подписанное письмо с
заголовком X-Mailspoon-Doctor: true и требует ответа 2xx.
mailspoon:filter-test — сухой прогон фильтров
Прогоняет письмо через filters ящика без захвата: не ходит в журнал, не
архивирует, не помечает письмо и ничего не доставляет. Печатает вердикт и
правило, которое его решило — какой deny отбросил письмо или какой allow
его пропустил. Удобно при настройке правил, до запуска чтения.
Работает и на выключенном ящике (enabled => false): вопрос «отфильтруется
ли это письмо» одинаково валиден для маршрута, который ещё только настраивают.
Команда — тонкая обёртка над сервисом FilterTester (см. ниже), так что тот же
вердикт можно получить из дашборда. Матчер берётся живой — из конфига или
рантайм-Mailspoon::register(), — поэтому результат точно совпадает с тем, что
решит захват. --file ничего не требует от IMAP; --uid тянет письмо из ящика.
Вызов операций из приложения (сервисы)
За командами mailspoon:doctor/:replay/:deliver стоят сервисы, которые
можно вызвать из своего кода (контроллера, Job'а) и получить структурный
результат вместо текста консоли — удобно, когда ящики и маршруты заводятся
динамически (см. «Маршруты из хранилища») и обслуживаются из веб-интерфейса.
Mailspoon HTTP-слой не шипит: контроллеры и UI строит хост, ниже — примеры.
Каждый результат реализует Arrayable/JsonSerializable, поэтому годится прямо
для response()->json().
На что обратить внимание:
DoctorиDelivererделают сетевые операции (IMAP-логин, HTTP-проба или доставка) синхронно и с таймаутами — в вебе запускайте их через очередь/Job, а не в реквест-цикле.ReplayиDelivererменяют данные (сбрасывают/доставляют записи) — авторизацию таких действий обеспечивает хост.FilterTesterничего не делает — чистая оценка без IMAP, БД и сети, без побочных эффектов, — поэтому его безопасно звать прямо в реквест-цикле.
События
Реле остаётся «тупой трубой»: оно не шлёт уведомлений и не строит метрик, но объявляет хост-приложению о двух ситуациях, которые иначе остались бы незамеченными. Оба события дублируются записью в лог Laravel, так что минимум наблюдаемости есть и без слушателей.
TTBooking\Mailspoon\Events\MessageFiltered— письмо отклонено правиламиfilters(свойства:message,mailbox). Отфильтрованное письмо помечается просмотренным, но не попадает ни в журнал, ни в архив — событие — его единственный след. По умолчанию пакет сам логирует его (info) дефолтным слушателемLogFilteredMessage;MAILSPOON_LOG_FILTERED=falseотключает лог, оставляя логирование на усмотрение подписчика события.TTBooking\Mailspoon\Events\DeliveryPermanentlyFailed— письмо исчерпалоMAILSPOON_MAX_ATTEMPTSи больше не будет переотправляться (свойство:message— модельRelayedMessage). Запись остаётся в журнале со статусомfailedдо ручногоmailspoon:replay; лог-запись —error.
Подписка — штатными средствами Laravel, например уведомление о застрявшем письме:
Запуск и расписание
Mailspoon регистрирует свои задачи в планировщике хост-приложения. Если
системный cron для schedule:run ещё не настроен, добавьте одну строку:
Что именно планируется, задаётся в config/mailspoon.php → schedule
(все задачи — с withoutOverlapping()):
mailspoon:deliver— включён по умолчанию (MAILSPOON_DELIVER_CRON, по умолчанию каждую минуту). Нужен в любом режиме, поскольку чтение только сохраняет письма. Чтобы отключить — задайтеMAILSPOON_DELIVER_CRONпустым.mailspoon:pullпо ящикам — картаимя ящика => cronв опубликованном конфиге (ключschedule.pull), по умолчанию пуста.- Очистка журнала и архива — по умолчанию включена с retention 3 дня.
При
MAILSPOON_RETENTION_DAYS > 0запускаетсяmodel:pruneпо расписаниюMAILSPOON_PRUNE_CRON(по умолчанию ежедневно в 03:00). Записьrelayed_messagesудаляется только вместе со связанным.eml. Очищаются только успешно доставленные письма; записиpendingиfailedсохраняются для повторной доставки и ручного разбора.
Отсюда два режима эксплуатации:
| Режим | Чтение | Демон / supervisor | Латентность |
|---|---|---|---|
| Cron-poll | mailspoon:pull по карте schedule.pull |
не нужен | = интервал cron |
| Realtime | mailspoon:sentry (IMAP IDLE) под supervisor |
нужен для watcher | секунды |
В обоих режимах доставку выполняет запланированный mailspoon:deliver —
отдельный демон или очередь для неё не требуются.
Связка с Laravel Mailbox
Mailspoon отлично сочетается с
beyondcode/laravel-mailbox.
Поскольку Mailspoon шлёт запрос в точности так же, как входящий MIME-вебхук
Mailgun, приложение может принимать пересылаемые письма штатным
mailgun-драйвером Laravel Mailbox — никакого кастомного кода для приёма не
требуется. Mailspoon можно установить как в отдельное приложение-реле, так и
прямо в приложение с Laravel Mailbox — тогда оно само читает свой ящик и
шлёт вебхук на собственный эндпоинт.
В приложении-получателе с установленным Laravel Mailbox:
а в Mailspoon направьте реле на его эндпоинт и используйте тот же ключ, чтобы подписи совпадали:
Дальше обрабатывайте письма как обычно через маршруты Laravel Mailbox:
Итоговый поток: IMAP-ящик → Mailspoon → вебхук Mailgun → Laravel Mailbox → ваши обработчики.
Лицензия
Mailspoon распространяется по лицензии MIT.
All versions of mailspoon with dependencies
directorytree/imapengine-laravel Version ^1.3
illuminate/console Version ^13.0
illuminate/container Version ^13.0
illuminate/contracts Version ^13.0
illuminate/database Version ^13.0
illuminate/events Version ^13.0
illuminate/filesystem Version ^13.0
illuminate/http Version ^13.0
illuminate/support Version ^13.0