ttbooking/mailspoon
最新稳定版本:v3.0.0
Composer 安装命令:
composer require ttbooking/mailspoon
包简介
Simple Mailgun compatible IMAP to HTTP webhook relay for Laravel.
README 文档
README
Простое реле IMAP → HTTP-вебхук, совместимое с Mailgun. Пакет для Laravel.
Mailspoon подключается к обычному IMAP-ящику, следит за появлением новых писем и
пересылает каждое входящее письмо на HTTP-эндпоинт, используя тот же формат
данных и схему подписи, что и входящие вебхуки Mailgun. Это позволяет
продолжать обрабатывать почту привычным Mailgun-эндпоинтом (например,
laravel-mailbox), даже когда
письма приходят по обычному IMAP, а не через Mailgun.
Устанавливается composer-пакетом в любое приложение
Laravel 13; чтение почты — на базе
ImapEngine
(directorytree/imapengine-laravel).
Как это работает
Mailspoon работает по схеме store-and-forward: чтение ящика отделено от доставки вебхука, поэтому медленный или недоступный эндпоинт не блокирует однопоточное чтение почты.
IMAP-ящик ──(mailspoon:pull / mailspoon:sentry)──▶ событие MessageReceived
│
└─▶ StoreIncomingMessage: архивирует сырой MIME + создаёт запись (pending)
│
└─▶ письмо сразу помечается прочитанным (\Seen)
mailspoon:deliver (отдельно, по планировщику)
│
└─▶ берёт pending из хранилища ──POST (body-mime + подпись Mailgun)──▶ ваш эндпоинт
│
└─▶ статус delivered, либо failed (повтор на следующем запуске)
- Команда забирает непрочитанные письма из папки ящика (по умолчанию 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).
Требования
- PHP 8.3+
- Приложение Laravel 13 (хост)
- IMAP-ящик
- HTTP-эндпоинт для приёма пересылаемых писем
- База данных — хранит записи о письмах и статус доставки
- Диск хранилища (
config/filesystems.php) с'throw' => true— для архива сырого MIME
Установка
composer require ttbooking/mailspoon # конфиг Mailspoon → config/mailspoon.php php artisan vendor:publish --tag=mailspoon-config # конфиг IMAP-подключений → config/imap.php php artisan vendor:publish --provider="DirectoryTree\ImapEngine\Laravel\ImapServiceProvider" php artisan migrate
Миграции пакета применяются автоматически; при желании их можно скопировать в
приложение: php artisan vendor:publish --tag=mailspoon-migrations.
Диск архива: обязателен 'throw' => true
Архив .eml — единственная копия письма после пометки прочитанным, поэтому
ошибки записи/чтения/удаления не должны подавляться Flysystem. Mailspoon
отказывается работать с диском, у которого 'throw' => false (значение по
умолчанию в свежем Laravel). Включите его для выбранного диска в
config/filesystems.php:
'local' => [ 'driver' => 'local', 'root' => storage_path('app/private'), 'serve' => true, 'throw' => true, ],
Конфигурация
IMAP-подключение (config/imap.php)
IMAP_HOST=imap.example.com IMAP_PORT=993 IMAP_USERNAME=your-username IMAP_PASSWORD=your-password IMAP_ENCRYPTION=ssl # ssl | tls | starttls | false
Дополнительные необязательные переменные: 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=https://example.com/laravel-mailbox/mailgun/mime MAILSPOON_KEY=key-55c5c5c5c55f55ca5cd5f55d5c555c55
MAILSPOON_ENDPOINT— URL, который принимает пересылаемые письма.MAILSPOON_KEY— общий секрет для подписи каждого запроса.
Хранилище и доставка (config/mailspoon.php)
MAILSPOON_ARCHIVE_DISK=local # диск из config/filesystems.php для сырого MIME MAILSPOON_ARCHIVE_PATH=mailspoon # префикс пути внутри диска MAILSPOON_RETENTION_DAYS=3 # срок хранения записей и MIME; 0 отключает очистку MAILSPOON_PRUNE_CRON="0 3 * * *" # расписание очистки при включённом retention MAILSPOON_TIMEOUT=15 # общий таймаут запроса доставки, сек MAILSPOON_CONNECT_TIMEOUT=3 # таймаут на TCP-handshake, сек MAILSPOON_TRIES=3 # быстрых in-process повторов на одну попытку MAILSPOON_BACKOFF=60,300,900,3600 # пауза между запусками, сек, по номеру попытки MAILSPOON_MAX_ATTEMPTS=10 # сколько попыток доставки, прежде чем сдаться
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:
'schedule' => [ // ... 'pull' => [ 'default' => '*/5 * * * *', 'secondary' => '0 * * * *', ], ],
Опубликованный конфиг должен сохранять полную структуру секций: merge с дефолтами пакета выполняется только по верхнему уровню.
Использование
Mailspoon предоставляет команды чтения (mailspoon:pull, mailspoon:sentry)
и команду доставки (mailspoon:deliver). Аргумент mailbox — это имя ящика из
config/imap.php (для встроенного используйте default). Необязательный
аргумент folder выбирает папку, отличную от INBOX.
mailspoon:pull — разовая проверка
Забирает все текущие непрочитанные письма, сохраняет их и завершается.
php artisan mailspoon:pull default
php artisan mailspoon:pull default "INBOX/Archive"
Опции:
--with=— список через запятую частей письма для подгрузки. Если опция не задана или пуста, используютсяflags,headers,body, необходимые для сохранения полного сырого MIME.
Подходит для запуска по расписанию (cron), когда долгоживущий процесс не нужен.
mailspoon:sentry — забрать накопившееся и следить дальше
Сначала один раз выполняет mailspoon:pull, чтобы сохранить накопившиеся
письма, затем начинает следить за ящиком в реальном времени (через IMAP IDLE) и
сохраняет письма по мере поступления. Это рекомендуемый способ запускать
Mailspoon как постоянный воркер.
php artisan mailspoon:sentry default
Опции:
--method=idle— метод слежения (по умолчаниюidle).--with=— части письма для подгрузки (по умолчаниюflags,headers,body).--timeout=30— таймаут IDLE в секундах.--attempts=5— число попыток переподключения.--debug=false— включить отладочный вывод.
Запускайте под супервизором процессов (systemd, Supervisor и т. п.), чтобы он перезапускался автоматически:
[program:mailspoon] command=php /path/to/app/artisan mailspoon:sentry default autostart=true autorestart=true
Команда
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, без блокирующих пауз в воркере.
Так зависший или медленный эндпоинт никогда не тормозит чтение ящика.
php artisan mailspoon:deliver php artisan mailspoon:deliver --limit=100 --max-attempts=5
Опции:
--limit=50— максимум писем за один запуск.--max-attempts=— переопределитьMAILSPOON_MAX_ATTEMPTS.
Команда — разовая (one-shot); запускать её периодически проще всего
планировщиком (см. ниже), который уже вызывает mailspoon:deliver с
withoutOverlapping().
Запуск и расписание
Mailspoon регистрирует свои задачи в планировщике хост-приложения. Если
системный cron для schedule:run ещё не настроен, добавьте одну строку:
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1
Что именно планируется, задаётся в 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:
MAILBOX_DRIVER=mailgun MAILBOX_MAILGUN_KEY=key-55c5c5c5c55f55ca5cd5f55d5c555c55
а в Mailspoon направьте реле на его эндпоинт и используйте тот же ключ, чтобы подписи совпадали:
MAILSPOON_ENDPOINT=https://your-app.com/laravel-mailbox/mailgun/mime # MAILSPOON_KEY должен совпадать с MAILBOX_MAILGUN_KEY MAILSPOON_KEY=key-55c5c5c5c55f55ca5cd5f55d5c555c55
Дальше обрабатывайте письма как обычно через маршруты Laravel Mailbox:
use BeyondCode\Mailbox\Facades\Mailbox; use BeyondCode\Mailbox\InboundEmail; Mailbox::from('sender@example.com', function (InboundEmail $email) { $subject = $email->subject(); // ... });
Итоговый поток: IMAP-ящик → Mailspoon → вебхук Mailgun → Laravel Mailbox → ваши обработчики.
Лицензия
Mailspoon распространяется по лицензии MIT.
统计信息
- 总下载量: 0
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 2
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-06-10