Перейти к основному содержимому

PHP SDK

composer require mailinfra/mailinfra

Требуется PHP 8.1+ и расширения curl, json, mbstring. Внешних зависимостей у пакета нет — он не конфликтует с версией Guzzle, которая уже стоит в проекте, что важно для сборок Bitrix, WordPress и Laravel постарше.

Первое письмо​

use MailInfra\MailInfra;

$client = new MailInfra(); // ключ из MAILINFRA_API_KEY

$result = $client->emails->send(
from: 'hello@ваш-домен.ru',
to: 'user@example.com',
subject: 'Добро пожаловать!',
html: '<p>Привет!</p>',
text: 'Привет!',
);

echo $result->id, ' ', $result->status->value;

Ключ можно передать явно: new MailInfra(apiKey: 'mi_live_…'). Тестовый ключ mi_test_… работает через тот же клиент: письмо сохраняется со статусом SIMULATED, не доставляется и не расходует месячный объём плана.

Все методы принимают именованные аргументы — порядок запоминать не нужно.

Защита от перепутанных ключей

new MailInfra(requireEnvironment: Environment::Live) не даст клиенту создаться, если в конфигурации оказался тестовый ключ — и наоборот.

Отправка по шаблону​

$client->emails->send(
from: 'no-reply@ваш-домен.ru',
to: ['anna@example.com', 'boris@example.com'],
template: 'welcome', // тема берётся из шаблона
variables: ['user_name' => 'Анна', 'app_name' => 'MyApp'],
tags: ['onboarding'],
);

Вложения​

use MailInfra\Model\Attachment;

$client->emails->send(
from: 'billing@ваш-домен.ru',
to: 'user@example.com',
subject: 'Счёт №A-1024',
text: 'Счёт во вложении.',
attachments: [
Attachment::fromFile('/var/invoices/A-1024.pdf', contentType: 'application/pdf'),
],
);

Attachment::fromContent($filename, $content) возьмёт содержимое из строки, Attachment::fromBase64($filename, $base64) — уже закодированное. До 10 вложений, суммарно до 25 МиБ вместе с письмом.

Идемпотентность​

$result = $client->emails->send(
from: 'hello@ваш-домен.ru',
to: 'user@example.com',
subject: 'Заказ A-1024 оплачен',
html: '<p>Спасибо!</p>',
idempotencyKey: 'order-A-1024-paid',
);

if ($result->replayed) {
// письмо уже было отправлено раньше — дубля нет
}

Без idempotencyKey клиент генерирует uuid4 сам, на каждый вызов новый. Привязывайте ключ к бизнес-событию, если хотите, чтобы повтор вашей же операции не создавал второе письмо. Подробнее — идемпотентность отправки.

Пакетная отправка​

$batch = $client->emails->sendBatch([
['from' => 'hello@ваш-домен.ru', 'to' => 'anna@example.com', 'subject' => 'Раз', 'text' => '1'],
['from' => 'hello@ваш-домен.ru', 'to' => 'boris@example.com', 'subject' => 'Два', 'text' => '2'],
]);

echo $batch->acceptedCount, ' принято, ', $batch->failedCount, ' отклонено';

foreach ($batch->failed() as $item) {
error_log("письмо #{$item->index}: {$item->error->code}");
}

Ключи элемента — те же, что у send(). До 100 писем за раз. Ответ приходит с кодом 202, даже если часть писем отклонена, поэтому проверяйте каждый элемент, а не только отсутствие исключения. Подробнее — пакетная отправка.

Чтение статусов​

use MailInfra\Enum\EmailStatus;

$email = $client->emails->get($emailId);
echo $email->status->value, ' ', $email->deliveredAt?->format('c');

$bounced = $client->emails->list(
status: EmailStatus::Bounced,
tag: 'onboarding',
from: new DateTimeImmutable('-24 hours'),
limit: 50,
);

foreach ($client->emails->events($emailId) as $event) {
echo $event->type?->value, ' ', $event->timestamp->format('c'), PHP_EOL;
}

from и to в list() — интервал по времени создания письма, а не адреса: адреса фильтруются через fromEmail и toEmail. Время уходит в API в UTC.

Ошибки​

use MailInfra\Exception\DomainNotVerifiedException;
use MailInfra\Exception\MailInfraException;
use MailInfra\Exception\RateLimitException;
use MailInfra\Exception\UnprocessableException;

try {
$client->emails->send(/* … */);
} catch (DomainNotVerifiedException $e) {
// домен отправителя ещё не подтверждён
} catch (RateLimitException $e) {
sleep($e->getRetryAfter() ?? 60);
} catch (UnprocessableException $e) {
foreach ($e->getFields() as $field) {
error_log("{$field['field']}: {$field['message']}");
}
} catch (MailInfraException $e) {
error_log("{$e->getMessage()} (request_id={$e->getRequestId()})");
}

Ловить можно на трёх уровнях: всё (MailInfraException), категорию HTTP-статуса (UnprocessableException, RateLimitException, NotFoundException, …) или конкретную причину (DomainNotVerifiedException, AllRecipientsSuppressedException, …). Конкретная причина всегда наследует свою категорию, поэтому порядок catch имеет значение: сначала частные классы, потом базовые.

Незнакомый error.code с бэкенда приходит как исключение по HTTP-статусу — новый код в API не ломает уже выпущенный SDK. Полный список кодов — ошибки и лимиты.

Повторы и таймауты​

use MailInfra\RetryPolicy;

$client = new MailInfra(
timeout: 10.0, // секунды на запрос; null — управляет ваш транспорт
retryPolicy: new RetryPolicy(
maxRetries: 4,
initialBackoff: 0.2,
maxBackoff: 5.0,
maxRetryAfter: 30.0, // дольше — не ждём, отдаём RateLimitException
),
);

maxRetries: 0 отключает повторы. Пакетная отправка и отправка без ключа идемпотентности повторяются только на 429: после 5xx или обрыва соединения повтор мог бы отправить часть писем второй раз.

Свой HTTP-транспорт​

По умолчанию используется cURL. Прокси и собственный CA-бандл задаются без адаптера:

use MailInfra\Http\CurlHttpClient;

$client = new MailInfra(httpClient: new CurlHttpClient([
CURLOPT_PROXY => 'http://proxy.internal:3128',
]));

Если в проекте уже есть Guzzle, Symfony HttpClient или любой PSR-18 клиент — реализуйте MailInfra\Http\HttpClientInterface (один метод) и передайте свой объект в httpClient. Пул соединений, ретраи вашего стека и логирование останутся вашими.

Вебхуки​

use MailInfra\Exception\WebhookSignatureException;
use MailInfra\Webhooks;

try {
$event = Webhooks::constructEvent(
body: file_get_contents('php://input'),
secret: getenv('MAILINFRA_WEBHOOK_SECRET'),
signatureHeader: $_SERVER['HTTP_X_MAILINFRA_SIGNATURE'] ?? '',
);
} catch (WebhookSignatureException $e) {
error_log('подпись отклонена: ' . $e->getReason());
http_response_code(400);
exit;
}

match ($event->type) {
'email.bounced' => handleBounce($event->emailId()),
'email.delivered' => handleDelivered($event->emailId()),
default => null,
};

http_response_code(200);

Тело обязательно брать сырым (php://input), а не из $_POST: пересериализованный JSON даст другие байты и подпись не сойдётся. Подписи старше 5 минут отклоняются; причина отказа — в getReason(): invalid_secret, malformed_header, too_old, signature_mismatch, invalid_json, invalid_payload. Подробнее — проверка подписи.

Ссылки​