Перейти к документации
PHP

Конфигурация

Как создать клиент, все опции и то, что он отклоняет до отправки запроса.

Опции

clients.php
use OpenEmail\OpenEmail; $client = new OpenEmail(); OpenEmail::init(timeout: 10);OpenEmail::getClient()->me->ping(); $billing = new OpenEmail(apiKey: (string) getenv('OPENEMAIL_BILLING_API_KEY')); echo $client->mode, ' ', $billing->mode, PHP_EOL;
Точка входаЧто вы получаете
new OpenEmail(...)Клиент, собранный из переданных вами именованных аргументов. Всё, что вы не указали, читается из окружения: ключ из OPENEMAIL_API_KEY или токен из OPENEMAIL_ACCESS_TOKEN, если вы не передали учётные данные, и базовый URL из OPENEMAIL_BASE_URL, если вы его не передали.
OpenEmail::createClient(...)Тот же клиент, что и new OpenEmail(...), для кода, которому удобнее вызывать фабрику.
OpenEmail::init(...)Создаёт клиент, сохраняет его как общий и возвращает. Принимает те же именованные аргументы.
OpenEmail::getClient()Общий клиент, доступный из любого места процесса. Если вызвать его до init, при первом вызове он соберёт клиент из окружения.
OpenEmail::resetClient()Сбрасывает общий клиент, так что следующий getClient() соберёт новый, а именно это и нужно тесту между случаями.

Приведение getenv() к строке сделано намеренно. Незаданная переменная превращается в пустой ключ, который клиент отклоняет с сообщением, называющим нужную ему переменную, тогда как null молча откатился бы к OPENEMAIL_API_KEY.

options.php
use OpenEmail\Http\CurlHttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(    apiKey: (string) getenv('OPENEMAIL_API_KEY'),    baseUrl: 'https://api.openemail.uk',    httpClient: new CurlHttpClient(),    maxRetries: 2,    timeout: 30,    userAgent: 'billing-service/1.4',    headers: ['X-Team' => 'billing'],    disableUpdateNotice: true,);
ОпцияПо умолчаниюПримечания
apiKey:OPENEMAIL_API_KEYДолжен начинаться с oe_live_ или oe_test_. Читается из окружения, только если вы не передали ни apiKey:, ни accessToken:.
accessToken:OPENEMAIL_ACCESS_TOKENТокен доступа OAuth или вызываемый объект, который возвращает токен. См. раздел «Токены доступа OAuth» ниже. Передавайте ключ или токен, но никогда оба сразу.
baseUrl:https://api.openemail.ukИли OPENEMAIL_BASE_URL. Завершающие слэши обрезаются, перед голым хостом добавляется https://, а перед хостом на этой машине (localhost, адрес 127.x.x.x или ::1) добавляется http://. Учётные данные никогда не отправляются по обычному http ни на какой другой хост, а 0.0.0.0 или [::] отклоняются при сборке клиента, потому что это адреса, на которых слушает сервер, а не адреса для отправки запросов.
timeout:30Секунды на попытку, а не на вызов; это время охватывает подключение и чтение всего ответа. 0 отключает таймаут. files->upload ждёт не меньше 600 секунд, если вы не передадите timeout: в этом вызове.
maxRetries:2Дополнительные попытки после первой для вызовов, которые безопасно повторять. Задаётся на клиенте, а не для отдельного вызова. 0 отключает повторы.
httpClient:CurlHttpClientHTTP-слой: всё, что реализует OpenEmail\Http\HttpClient, например Psr18HttpClient поверх Guzzle или Symfony HttpClient, или подделка в тесте. Каждый вариант описан на странице «HTTP-клиенты».
headers:[]Отправляются с каждым запросом.
userAgent:openemail-php/<version>Отправляются с каждым запросом.
disableUpdateNotice:falseПропускает проверку новой версии на Packagist, которая выполняется один раз за процесс. Проверка запускается только из командной строки, когда стандартный вывод является терминалом, и OPENEMAIL_DISABLE_UPDATE_NOTICE тоже её отключает.

Переменные окружения

ПеременнаяЧто делает
OPENEMAIL_API_KEYКлюч, который клиент использует, если вы не передали ни apiKey:, ни accessToken:.
OPENEMAIL_ACCESS_TOKENТокен доступа OAuth. Читается, только если вы не передали ни ключ, ни токен и OPENEMAIL_API_KEY не задан, так что ключ в окружении имеет приоритет.
OPENEMAIL_BASE_URLБазовый URL, если вы его не передали. К голому хосту вроде localhost:2222 добавляется схема.
OPENEMAIL_DISABLE_UPDATE_NOTICEЛюбое непустое значение отключает уведомление об обновлении для всех клиентов в процессе.
HTTPS_PROXY и NO_PROXY или https_proxy и no_proxyПрокси, через который подключается cURL, и хосты, к которым он подключается напрямую. См. раздел «Прокси и TLS» ниже.

Каждая переменная сначала читается через getenv(), затем из $_SERVER и $_ENV, поэтому значение, которое ваш фреймворк загрузил из файла .env, тоже учитывается. Переменная, которая задана, но пуста, считается незаданной.

Что он отклоняет до отправки

Эти случаи выбрасывают OpenEmail\Exception\InvalidArgumentException из той строки, где было неверное значение, а не всплывают непонятным сбоем при первой отправке. Сообщение говорит, что было не так и что передать вместо этого, и никогда не повторяет учётные данные.

ОтклоненоПочему
Нет никаких учётных данныхНе передан ни apiKey:, ни accessToken:, и не задана ни одна из переменных, так что аутентифицироваться нечем. Выбрасывается при сборке клиента.
Ключ и токен одновременноКаждый запрос несёт одни учётные данные, поэтому клиент не может понять, что вы имели в виду.
Cookie сессии, токен сессии или ключ от другого сервисаЗдесь аутентифицируют только oe_live_ и oe_test_, и API говорит то же самое. Проверяется только префикс и ничего больше, поэтому отозванный ключ всё равно отклонит уже сам API, с ошибкой AuthenticationException.
baseUrl:, который не является URL с http или https или содержит имя пользователя или парольНи до чего другого достучаться нельзя, а учётным данным место в apiKey: или accessToken:, а не в URL. Выбрасывается при сборке клиента.
Учётные данные по обычному http на хост, который находится не на этой машинеВыбрасывается вызовом ещё до того, как что-либо отправлено. Используйте базовый URL с https.
Отрицательный timeout:Передайте секунды или 0, чтобы обойтись без таймаута. Выбрасывается при сборке клиента или самим вызовом, если таймаут передан в отдельный вызов.
Имя заголовка, которое не является токеном, или перевод строки либо другой управляющий символ в значении заголовкаПроверяется в headers:, userAgent: и idempotencyKey:, потому что перевод строки начал бы второй заголовок. Пробелы, табуляции и переводы строк по краям значения сначала удаляются, как это делает fetch, поэтому ключ, прочитанный из файла с переводом строки в конце, всё равно работает.
Пустой идентификатор или идентификатор из одних точек в любом методеВыбрасывается при вызове метода. Сегмент пути из точек удаляется любым парсером URL, так что запрос попал бы на другой эндпоинт. Идентификатор, который не является корректным UTF-8, тоже отклоняется.
Содержимое вложения не в base64Строка всегда читается как base64, поэтому сырые байты в ней ушли бы как мусор. Закодируйте их через OpenEmail::toBase64() или передайте SplFileInfo, поток или поток PSR-7, и клиент закодирует его сам.

Класс наследует собственный InvalidArgumentException из PHP, поэтому код, который уже его перехватывает, продолжает работать, и реализует OpenEmail\Exception\OpenEmailException, как и любое другое исключение, которое выбрасывает пакет. Значение неверного типа, например число там, где ожидается строковый id, приводит к TypeError от самого PHP, потому что каждый метод объявляет свои типы.

Параметра testMode: нет и не будет. Схема ключа является частью учётных данных, а не подсказкой, поэтому режим является свойством ключа. $client->mode читает префикс, live или test, и ничего не решает.

Один клиент, несколько ключей

Соберите клиент один раз и разделяйте его. Новый клиент на каждый запрос впустую выбрасывает своё открытое соединение, а никакая часть его состояния не привязана к вызывающему.

Для случая, который иначе потребовал бы по клиенту на ключ, например задания, отправляющего от имени нескольких рабочих пространств, передайте apiKey: в вызов. Он заменяет заголовок Authorization для этого запроса и ничего не оставляет на клиенте.

per_call_key.php
$message = [    'from' => '[email protected]',    'to' => '[email protected]',    'subject' => 'Your invoice',    'text' => 'Attached.',];$workspaceKey = (string) getenv('OPENEMAIL_API_KEY'); $client->emails->send($message); $client->emails->send($message, apiKey: $workspaceKey); $client->threads->list(folder: 'inbox', apiKey: $workspaceKey);$client->webhooks->list(apiKey: $workspaceKey);

Каждый метод вне tempMail принимает его последним именованным аргументом, после фильтров списка, а методы tempMail вместо него принимают inboxToken:. Он проверяется до отправки запроса по тому же правилу, что использует клиент, поэтому опечатка выбрасывает InvalidArgumentException про apiKey, переданный в этот вызов, а не 401 про учётные данные, которые потом ещё придётся искать. Повторный вызов сохраняет ключ, который ему дали.

$client->mode описывает ключ, с которым клиент был СОБРАН, и не следует за переопределением. Когда один клиент обслуживает несколько ключей, единого режима, о котором можно сообщить, нет, поэтому определяйте его по ключу, который вы передали. var_dump($client) показывает режим и базовый URL, но никогда не ключ, а каждый параметр, принимающий учётные данные, помечен #[\SensitiveParameter], поэтому трассировка стека печатает на его месте заглушку.

Эндпоинты, которые не оборачивает ни один метод

$client->raw является транспортом, через который проходит каждый метод. $client->raw->request() вызывает путь, который пока не оборачивает ни один метод, применяя учётные данные клиента, базовый URL, таймаут и политику повторов, и возвращает декодированное тело так же, как это делает метод.

raw_request.php
$ping = $client->raw->request('/ping'); $label = $client->raw->request('/labels', method: 'POST', body: ['name' => 'Invoices']); var_dump($ping, $label);
Именованный аргументЧто делает
method:GET, если не указано иное: POST, PUT, PATCH или DELETE.
query:Массив параметров запроса. null и пустые значения опускаются, список склеивается через запятую, а DateTimeInterface отправляется как момент времени в формате ISO 8601 в UTC.
body:Массив, который отправляется как JSON.
raw: и contentType:Байты, которые отправляются как есть, в виде строки, ресурса потока, SplFileInfo или потока PSR-7, с типом application/octet-stream, если вы не укажете другой.
accept: и binary:При accept:, отличном от JSON, тело возвращается как текст, а binary: true возвращает его как строку байтов.
idempotent: и idempotencyKey:idempotent: true добавляет Idempotency-Key, который генерируется, если вы не передали свой.
repeatable:Повторяется ли вызов после сбоя. Повторяется только GET, если вы не передадите repeatable: true.
anonymous:true не отправляет никаких учётных данных.
apiKey:, inboxToken: и timeout:Те же учётные данные для отдельного вызова и таймаут в секундах только для этого вызова.

Путь должен начинаться с одного /, а путь, итоговый URL которого вышел бы за пределы источника (origin) базового URL, выбрасывает InvalidArgumentException ещё до отправки, поэтому учётные данные никогда не попадают на другой хост.

Одноразовые ящики

OpenEmail::createTempMail() собирает клиент для одноразовых ящиков, который не несёт API-ключа и не читает его из окружения. Он создаёт ящики анонимно, а каждое чтение отправляет токен ящика, который вернул create, или более новый, который вернул extend: либо в каждом вызове как inboxToken:, либо один раз как OpenEmail::createTempMail(inboxToken: ...).

temp_mail.php
use OpenEmail\OpenEmail; $tempMail = OpenEmail::createTempMail(); $inbox = $tempMail->create();$page = $tempMail->listMessages($inbox['id'], inboxToken: $inbox['token']); echo count($page->items), ' ', $page->expiresAt, PHP_EOL;

OpenEmail::createTempMail() принимает baseUrl:, httpClient:, maxRetries:, timeout:, userAgent:, headers: и disableUpdateNotice:, как любой клиент, и читает OPENEMAIL_BASE_URL, если вы не передали базовый URL.

Токены доступа OAuth

Приложение, которое человек подключил через OAuth, например инструмент командной строки или агент, держит токен доступа вместо API-ключа. Передайте его как accessToken:: либо сам токен, либо вызываемый объект, который его возвращает, например замыкание или first-class callable. Он выполняется один раз на каждый вызов, а повторы этого вызова переиспользуют то, что он вернул, поэтому обновляйте токен внутри него, когда срок действия подходит к концу, и клиент никогда не придётся пересобирать.

access_token.php
use OpenEmail\OpenEmail; $tokens = ['current' => 'token-from-your-oauth-flow']; $oauthClient = new OpenEmail(accessToken: fn(): string => $tokens['current']); $me = $oauthClient->me->get(); if ($me['object'] === 'oauth_token') {    echo $me['clientId'], ' ', $me['expiresAt'], PHP_EOL;}
СлучайЧто происходит
apiKey: и accessToken: вместе или ни одного из нихКлиент выбрасывает InvalidArgumentException при сборке. Если нет ни того, ни другого, сообщение называет OPENEMAIL_API_KEY и OPENEMAIL_ACCESS_TOKEN.
Значение, которое не является токеномТокен содержит от 1 до 512 символов и не начинается с oe_: именно это проверяет OpenEmail::isAccessToken(). Строка, не прошедшая проверку, отклоняется при сборке клиента, а вызываемый объект, который вернул такую строку, заставляет вызов выбросить InvalidArgumentException ещё до отправки.
OPENEMAIL_ACCESS_TOKENЧитается, если вы не передали ни ключ, ни токен и OPENEMAIL_API_KEY не задан, так что ключ в окружении имеет приоритет.
Вызываемый объект, который выбрасывает исключениеВызов выбрасывает это исключение без изменений, и ничего не отправляется.
apiKey: для отдельного вызоваЗаменяет токен для этого одного запроса, а вызываемый объект не вызывается.
$client->modeС токеном всегда live.
OpenEmail::createTempMail()Не отправляет никаких учётных данных, что бы ни было в окружении.
me->get() и me->ping()Для токена get отвечает с object, равным oauth_token, с id и roleId, равными null, с clientId подключённого приложения и с expiresAt, моментом, когда истекает согласие человека на это приложение. ping отвечает с kind, равным oauth, с keyId, равным null, и с clientId. Проверяйте object или kind, прежде чем читать id или keyId.

Токен действует от имени человека и читает его почту так, как может он сам, поэтому держите его на сервере, как и ключ.

Коды подтверждения

Перед чувствительным изменением, например удалением домена или изменением вебхука, API запрашивает у токена доступа код подтверждения, который веб-приложение запросило бы у человека. Вызов выбрасывает PermissionException, ошибку 403, у которой isStepUpRequired() равно true, и ничего не изменилось. Запросите код, проверьте тот, который даст вам человек, затем повторите вызов. У API-ключа код никогда не запрашивается.

step_up.php
use OpenEmail\Exception\ApiException; $domainId = 'b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f'; try {    $client->domains->delete($domainId);} catch (ApiException $error) {    if (!$error->isStepUpRequired()) {        throw $error;    }     $challenge = $client->security->beginStepUp();     if ($challenge['method'] === 'email') {        echo 'Enter the code we emailed to ', $challenge['sentTo'], PHP_EOL;    } else {        echo 'Enter the code from your authenticator app, or a backup code', PHP_EOL;    }     $client->security->verifyStepUp(['code' => trim((string) fgets(STDIN))]);    $client->domains->delete($domainId);}
МетодЧто делает
security->stepUpStatus()Подтверждено ли приложение прямо сейчас (elevated, elevatedUntil), как будет проверяться следующий код (method, email или totp) и minutes, длина окна. Ничего не отправляет и не сообщает о паузе.
security->beginStepUp()Открывает проверку. С email шестизначный код уходит на адрес, с которым человек входит в систему, а sentTo показывает этот адрес в замаскированном виде. С totp человек берёт код из приложения-аутентификатора или использует резервный код. Проверка, которая ещё открыта и у которой остались попытки, переиспользуется, если вы не передадите ['resend' => true], а заблокированная или истёкшая заменяется обычным вызовом. Каждое приложение может открыть 5 проверок в час и 20 за 24 часа для каждого человека, а следующая выбрасывает 429 step_up_throttled.
security->verifyStepUp(['code' => ...])Проверяет код и разблокирует чувствительные изменения для этого приложения на 60 минут, до elevatedUntil, через REST и через инструменты MCP, которые вносят те же изменения. После 10 неверных кодов за 24 часа от этого приложения или 20 от всех приложений человека вместе этот вызов и beginStepUp выбрасывают 429 step_up_locked с сообщением, в котором сказано, когда подтверждение снова станет доступно.

Клиент никогда сам не запрашивает код и не повторяет вызов, и ни один из трёх методов не повторяется автоматически, потому что повтор после потерянного ответа мог бы отправить второе письмо или потратить вторую попытку. Им не нужна область, а API-ключ, вызвавший один из них, получает 400 step_up_not_applicable. OpenEmail\Constants\StepUpErrorCodes перечисляет все причины, по которым подтверждение может не пройти, а страница ошибок API говорит, что делать в каждом случае.

Уведомление об обновлении

Когда на Packagist есть более новая версия пакета, клиент сообщает об этом один раз за процесс в стандартный поток ошибок строкой вроде ℹ openemail/sdk 0.0.2 is available, you are on 0.0.1., за которой следует страница пакета. Проверка запускается только из командной строки, когда стандартный вывод является терминалом, и никогда под веб-сервером. Она начинается при сборке первого клиента и идёт параллельно с вашими запросами, а в конце скрипта ждёт столько, сколько осталось от бюджета в две секунды. Неудачная попытка связаться с Packagist игнорируется.

Проверка делает собственный запрос через cURL, в обход httpClient: клиента, поэтому поддельный HTTP-клиент в тесте его никогда не видит. Чтобы отключить её, передайте disableUpdateNotice: true или задайте OPENEMAIL_DISABLE_UPDATE_NOTICE.

Прокси и TLS

CurlHttpClient по умолчанию оставляет прокси на cURL, который читает https_proxy или HTTPS_PROXY для прокси и no_proxy или NO_PROXY для хостов, к которым подключение идёт напрямую. Чтобы задать прокси в коде, передайте proxy:. Имя пользователя и пароль из URL прокси отправляются самому прокси.

curl_options.php
use OpenEmail\Http\CurlHttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new CurlHttpClient(    caBundle: '/etc/ssl/certs/corporate-ca.pem',    proxy: 'http://proxy.internal:3128',    curlOptions: [CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4],));

Соединения используют TLS 1.2 или новее и проверяют сертификат и имя хоста сервера, а перенаправления никогда не выполняются. caBundle: задаёт центры сертификации, которым следует доверять, для прокси, который инспектирует TLS. curlOptions: устанавливает любую другую опцию cURL, но настройки, которые нужны запросу, всегда имеют приоритет: URL и его порт, метод, заголовки, тело и отключённые перенаправления. CURLOPT_REQUEST_TARGET отклоняется, а опция, которую cURL не принимает, выбрасывает InvalidArgumentException с её названием.