定制 ttbooking/mailspoon 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

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 (повтор на следующем запуске)
  1. Команда забирает непрочитанные письма из папки ящика (по умолчанию INBOX) и на каждое диспатчит событие MessageReceived из ImapEngine.
  2. Слушатель StoreIncomingMessage сохраняет сырой MIME в хранилище, создаёт запись о письме со статусом pending и сразу помечает письмо прочитанным — приём надёжно зафиксирован локально.
  3. Команда 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 — расписание штатной команды Laravel model: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.phpschedule (все задачи — с 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

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固