Documentation
¶
Overview ¶
Package mailinfra — официальный Go SDK MailInfra.
Быстрый старт:
package main
import (
"context"
"fmt"
"log"
"github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New() // ключ из MAILINFRA_API_KEY
if err != nil {
log.Fatal(err)
}
result, err := client.Emails.Send(context.Background(), mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Добро пожаловать!",
HTML: "<p>Привет!</p>",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.ID, result.Status)
}
Что клиент делает за вас:
- ставит Idempotency-Key на каждую отправку — и письма, и батча — и переиспользует его при повторах: сбой сети не превращается в два письма;
- повторяет 429/5xx/таймауты с экспоненциальным backoff и уважает Retry-After;
- отдаёт ошибки API часовыми значениями для errors.Is (ErrRateLimit, ErrDomainNotVerified, …) и структурой *Error для errors.As;
- проверяет ограничения письма (в том числе Metadata и TTL) локально до сетевого вызова;
- обходит все письма выборки по курсору (ListAll);
- проверяет подписи вебхуков (ConstructWebhookEvent), в том числе во время ротации секрета.
Покрытие v1 — публичный API писем (/v1/emails*). Управление доменами, шаблонами, вебхуками и ключами — Management API на сессионном токене, оно в SDK пока не входит.
Ошибки ¶
Ветвиться по причине отказа надо через errors.Is, а метаданные для логов брать через errors.As:
result, err := client.Emails.Send(ctx, params)
switch {
case err == nil:
// принято
case errors.Is(err, mailinfra.ErrDomainNotVerified):
// домен отправителя ещё не подтверждён
case errors.Is(err, mailinfra.ErrRateLimit):
var apiErr *mailinfra.Error
errors.As(err, &apiErr)
// apiErr.RetryAfter — сколько просил подождать сервер
default:
return err
}
Отложенная отправка ¶
Письмо с SendParams.SendAt получает статус SCHEDULED и уходит в заданный момент (не дальше 7 дней от приёма; время в прошлом — сразу). До отправки его можно отменить или перенести; TTL ограничивает срок жизни письма, а Metadata возвращается в Get, List и каждом вебхуке письма:
result, err := client.Emails.Send(ctx, mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Напоминание об оплате",
Text: "Заказ A-1024 ждёт оплаты.",
Metadata: map[string]string{"order_id": "A-1024"},
SendAt: time.Now().Add(24 * time.Hour),
TTL: 2 * time.Hour,
})
…
// Напоминание больше не нужно…
_, err = client.Emails.Cancel(ctx, result.ID)
// …или нужно, но позже.
_, err = client.Emails.Reschedule(ctx, result.ID, time.Now().Add(48*time.Hour))
На письмо, которое уже не запланировано, обе операции отвечают ErrEmailNotScheduled.
Inline-картинка — вложение с Attachment.ContentID, на которое HTML ссылается как cid:<ContentID>.
Пагинация ¶
List отдаёт одну страницу писем (EmailPage) с курсором следующей. Обойти выборку целиком проще через ListAll — он сам запрашивает страницы по курсору:
pager := client.Emails.ListAll(mailinfra.ListParams{Status: mailinfra.EmailStatusBounced})
for pager.Next(ctx) {
email := pager.Email()
…
}
if err := pager.Err(); err != nil {
return err
}
Пакетная отправка ¶
SendBatch ставит один Idempotency-Key на батч, и бэкенд выводит из него ключ каждого письма: повтор после сбоя не дублирует уже принятые письма. Свой ключ — WithBatchIdempotencyKey, отказ от ключа — WithoutBatchIdempotencyKey; признак идемпотентного повтора — BatchResult.Replayed.
Вебхуки ¶
Во время ротации секрета в заголовке X-MailInfra-Signature две подписи v1 — новым секретом и прежним; ConstructWebhookEvent и VerifyWebhookSignature принимают доставку, если совпала любая из них.
Потокобезопасность ¶
Client можно держать один на приложение и вызывать из нескольких горутин: состояние запроса в нём не хранится.
Index ¶
- Constants
- Variables
- func SignWebhookPayload(body []byte, secret string, timestamp int64) string
- func VerifyWebhookSignature(body []byte, secret, signatureHeader string, opts ...WebhookOption) bool
- type Attachment
- type BatchOption
- type BatchResult
- type BatchSendError
- type BatchSendResponseData
- type BatchSendResult
- type BounceCategory
- type BounceType
- type Client
- type DkimSigningStatus
- type EmailDetail
- type EmailEvent
- type EmailEventType
- type EmailListItem
- type EmailPage
- type EmailPager
- type EmailScheduleData
- type EmailSendResponseData
- type EmailSource
- type EmailStatus
- type EmailsService
- func (s *EmailsService) Cancel(ctx context.Context, emailID string) (*EmailScheduleData, error)
- func (s *EmailsService) Events(ctx context.Context, emailID string) ([]EmailEvent, error)
- func (s *EmailsService) Get(ctx context.Context, emailID string) (*EmailDetail, error)
- func (s *EmailsService) List(ctx context.Context, params ListParams) (*EmailPage, error)
- func (s *EmailsService) ListAll(params ListParams) *EmailPager
- func (s *EmailsService) Reschedule(ctx context.Context, emailID string, sendAt time.Time) (*EmailScheduleData, error)
- func (s *EmailsService) Send(ctx context.Context, params SendParams) (*SendResult, error)
- func (s *EmailsService) SendBatch(ctx context.Context, emails []SendParams, opts ...BatchOption) (*BatchResult, error)
- type Environment
- type Error
- type ListParams
- type Option
- func WithAPIKey(apiKey string) Option
- func WithAutoIdempotency(enabled bool) Option
- func WithBaseURL(baseURL string) Option
- func WithHTTPClient(httpClient *http.Client) Option
- func WithMaxRetries(maxRetries int) Option
- func WithRequireEnvironment(environment Environment) Option
- func WithRetryPolicy(policy RetryPolicy) Option
- func WithTimeout(timeout time.Duration) Option
- func WithUserAgentSuffix(suffix string) Option
- type RetryPolicy
- type SendParams
- type SendResult
- type SuppressionReason
- type ValidationField
- type WebhookEvent
- type WebhookOption
- type WebhookSignatureError
Examples ¶
Constants ¶
const ( // APIKeyEnvVar — переменная окружения с API-ключом проекта. APIKeyEnvVar = "MAILINFRA_API_KEY" // BaseURLEnvVar — переменная окружения с базой API. BaseURLEnvVar = "MAILINFRA_BASE_URL" )
Переменные окружения, из которых клиент берёт настройки, если их не передали явно.
const ( // MaxRecipients — потолок адресов в каждом из полей To, CC и BCC. MaxRecipients = 50 // MaxReplyTo — потолок адресов в Reply-To. MaxReplyTo = 10 // MaxTags — потолок тегов письма. MaxTags = 10 // MaxSubjectLength — потолок длины темы. MaxSubjectLength = 998 // MaxFromNameLength — потолок длины отображаемого имени отправителя. MaxFromNameLength = 200 // MaxTemplateLength — потолок длины слага шаблона. MaxTemplateLength = 100 // MaxAttachments — потолок числа вложений в письме. MaxAttachments = 10 // MaxAttachmentBase64Length — потолок длины base64-содержимого вложения. MaxAttachmentBase64Length = 14348938 // MaxFilenameLength — потолок длины имени файла вложения. MaxFilenameLength = 255 // MaxContentTypeLength — потолок длины MIME-типа вложения. MaxContentTypeLength = 255 // MaxContentIDLength — потолок длины Content-ID inline-вложения: 255 // символов и пара внешних угловых скобок. MaxContentIDLength = 257 // MaxBatchSize — потолок числа писем в одном батче. MaxBatchSize = 100 // IdempotencyKeyMaxLength — потолок длины Idempotency-Key (бэкенд // отклоняет длиннее с 422). IdempotencyKeyMaxLength = 255 // BatchIdempotencyKeyMaxLength — потолок длины Idempotency-Key батча: из // 255 символов ключа письма бэкенд берёт до трёх под номер письма в батче. BatchIdempotencyKeyMaxLength = 252 )
Ограничения запроса, зафиксированные в https://docs.mailinfra.ru/openapi/mailinfra-public.json.
Проверяем их на клиенте, чтобы не тратить запрос: API вернул бы 422, но батч с вложениями уезжает целиком (до ~25 МиБ на письмо) только чтобы вернуться с ошибкой. Совпадение констант со спекой проверяет spec_parity_test.go.
const ( // MinTTL — самый короткий срок жизни письма. MinTTL = time.Minute // MaxTTL — самый длинный срок жизни письма. MaxTTL = 24 * time.Hour )
Срок жизни письма (SendParams.TTL) — тоже из спеки.
const ( // MaxMetadataKeys — потолок числа ключей в Metadata. MaxMetadataKeys = 20 // MaxMetadataKeyLength — потолок длины ключа Metadata. MaxMetadataKeyLength = 40 // MaxMetadataValueLength — потолок длины значения Metadata в символах. MaxMetadataValueLength = 500 // MaxMetadataBytes — потолок размера Metadata целиком: байты UTF-8 его // JSON-представления. MaxMetadataBytes = 4096 )
Ограничения metadata письма.
const ( // WebhookSignatureHeader — заголовок с подписью доставки. WebhookSignatureHeader = "X-MailInfra-Signature" // WebhookEventIDHeader — заголовок с идентификатором события; удобен для // дедупликации доставок на вашей стороне. WebhookEventIDHeader = "X-MailInfra-Event-Id" // DefaultWebhookMaxAge — допустимый возраст подписи. // // Бэкенд отклоняет подписи старше 5 минут (SIGNATURE_MAX_AGE_SEC), держим // то же окно, чтобы поведение получателя совпадало с ожиданиями отправителя. DefaultWebhookMaxAge = 5 * time.Minute )
const ( // WebhookReasonInvalidSecret — secret пуст. WebhookReasonInvalidSecret = "invalid_secret" // WebhookReasonMalformedHeader — заголовок подписи не разобран. WebhookReasonMalformedHeader = "malformed_header" // WebhookReasonTooOld — подпись старше допустимого возраста (возможен replay). WebhookReasonTooOld = "too_old" // WebhookReasonSignatureMismatch — подпись не совпала с телом. WebhookReasonSignatureMismatch = "signature_mismatch" // WebhookReasonInvalidJSON — тело не является JSON-объектом. WebhookReasonInvalidJSON = "invalid_json" // WebhookReasonInvalidPayload — событие пришло не по контракту. WebhookReasonInvalidPayload = "invalid_payload" )
Причины отказа (поле Reason у WebhookSignatureError).
const DefaultBaseURL = "https://app.mailinfra.ru/v1"
DefaultBaseURL — продакшен-база Public API.
const DefaultTimeout = 30 * time.Second
DefaultTimeout — таймаут одного запроса, если вызывающий не задал свой *http.Client.
const Version = "0.2.0"
Version — версия SDK; уезжает в User-Agent каждого запроса.
Тег релиза в github.com/g7AzaZLO/mailinfra-go берётся отсюда: его ставит sdk/scripts/release-mirror.sh в монорепозитории MailInfra.
Variables ¶
var ( // ErrConnection — не удалось получить ответ: DNS, TCP, TLS, обрыв соединения. ErrConnection = errors.New("mailinfra: не удалось соединиться с API") // ErrTimeout — запрос не успел уложиться в таймаут. Вкладывается в // ErrConnection: ответа не было в обоих случаях. ErrTimeout = errors.New("mailinfra: истёк таймаут запроса") // ErrBadRequest — 400, запрос некорректен. ErrBadRequest = errors.New("mailinfra: некорректный запрос") // ErrAuthentication — 401, API-ключ отсутствует, невалиден или отозван. ErrAuthentication = errors.New("mailinfra: ошибка аутентификации") // ErrPermissionDenied — 403, у ключа нет прав на операцию: ключ выпущен с // правами READ_ONLY (запрещены отправка, отмена и перенос) или // SENDING_ONLY (запрещено чтение), либо запрос пришёл с IP-адреса вне // белого списка ключа (ErrIPNotAllowed). ErrPermissionDenied = errors.New("mailinfra: недостаточно прав") // ErrNotFound — 404, ресурс не найден или принадлежит другому проекту. ErrNotFound = errors.New("mailinfra: ресурс не найден") // ErrConflict — 409, конфликт состояния. ErrConflict = errors.New("mailinfra: конфликт состояния") // ErrUnprocessable — 422, не прошла валидация полей. ErrUnprocessable = errors.New("mailinfra: запрос не прошёл валидацию") // ErrRateLimit — 429, исчерпан лимит отправки: месячный объём или // дневной потолок организации. ErrRateLimit = errors.New("mailinfra: превышен лимит запросов") // ErrServer — 5xx, ошибка на стороне MailInfra. Такие запросы SDK ретраит сам. ErrServer = errors.New("mailinfra: ошибка на стороне API") // ErrAPI — ответ по статусу, для которого отдельной категории нет. ErrAPI = errors.New("mailinfra: ошибка API") )
Часовые ошибки категорий — по HTTP-статусу ответа.
var ( // ErrDomainNotVerified — домен отправителя ещё не подтверждён (DNS-записи не сошлись). ErrDomainNotVerified = errors.New("mailinfra: домен не подтверждён") // ErrDomainNotAttached — домен не привязан к проекту, которому принадлежит ключ. ErrDomainNotAttached = errors.New("mailinfra: домен не привязан к проекту") // ErrAllRecipientsSuppressed — все получатели в списке подавления, отправлять некому. ErrAllRecipientsSuppressed = errors.New("mailinfra: все получатели в списке подавления") // ErrSandboxRecipientNotAllowed — пробное письмо с sandbox-домена (LIVE-ключ) уходит только владельцу ключа. ErrSandboxRecipientNotAllowed = errors.New("mailinfra: пробное письмо с sandbox-домена — только владельцу ключа") // ErrSandboxTrialExhausted — пробная sandbox-отправка уже израсходована; нужен свой домен. ErrSandboxTrialExhausted = errors.New("mailinfra: пробная sandbox-отправка израсходована") // ErrTemplateRender — шаблон не отрендерился. ErrTemplateRender = errors.New("mailinfra: шаблон не отрендерился") // ErrTemplateVariableMissing — в variables не хватает переменной, которую требует шаблон. ErrTemplateVariableMissing = errors.New("mailinfra: не хватает переменной шаблона") // ErrReservedEmailHeader — попытка переопределить системный заголовок письма. ErrReservedEmailHeader = errors.New("mailinfra: системный заголовок письма нельзя переопределить") // ErrIdempotencyInFlight — запрос с таким Idempotency-Key ещё выполняется, повторите позже. ErrIdempotencyInFlight = errors.New("mailinfra: запрос с этим Idempotency-Key ещё выполняется") // ErrLegalDocumentsNotAccepted — не приняты обязательные юридические документы; // список ждущих согласия — в Details["pending"]. ErrLegalDocumentsNotAccepted = errors.New("mailinfra: не приняты юридические документы") // ErrEmailNotScheduled — письмо уже не запланировано (отправлено, // отправляется или отменено), поэтому отменить или перенести его нельзя; // текущий статус — в Details["status"]. ErrEmailNotScheduled = errors.New("mailinfra: письмо уже не запланировано") // ErrSendAtTooFar — время отправки дальше 7 дней от приёма письма; горизонт // в днях — в Details["max_days"]. ErrSendAtTooFar = errors.New("mailinfra: время отправки дальше допустимого горизонта") // ErrSendAtInvalid — время отправки без часового пояса. ErrSendAtInvalid = errors.New("mailinfra: время отправки без часового пояса") // ErrIPNotAllowed — API-ключ использован с IP-адреса вне его белого списка. ErrIPNotAllowed = errors.New("mailinfra: API-ключ нельзя использовать с этого IP-адреса") )
Часовые ошибки конкретных причин — по `error.code` из ответа.
var ( // ErrInvalidRequest — параметры вызова не проходят ограничения спеки. ErrInvalidRequest = errors.New("mailinfra: некорректные параметры запроса") // ErrMalformedResponse — ответ успешен, но не по контракту. ErrMalformedResponse = errors.New("mailinfra: ответ API не по контракту") )
Часовые ошибки клиентской валидации — запрос до сети не доходит.
var ErrWebhookSignature = errors.New("mailinfra: подпись вебхука не прошла проверку")
ErrWebhookSignature — часовая ошибка для errors.Is: подпись не прошла проверку. Конкретная причина — в поле Reason у *WebhookSignatureError.
Functions ¶
func SignWebhookPayload ¶
SignWebhookPayload считает hex-подпись тела — та же функция, что на бэкенде.
Нужна в первую очередь тестам и локальной отладке обработчика: позволяет собрать корректный X-MailInfra-Signature без реального вебхука.
func VerifyWebhookSignature ¶
func VerifyWebhookSignature(body []byte, secret, signatureHeader string, opts ...WebhookOption) bool
VerifyWebhookSignature проверяет подпись, не возвращая ошибки.
Удобно, когда причина отказа неважна и нужен только ответ «пускать или нет». Если причина нужна для логов — берите ConstructWebhookEvent.
Types ¶
type Attachment ¶
type Attachment struct {
// Filename — имя файла в письме.
Filename string
// Content — сырые байты файла.
Content []byte
// ContentBase64 — готовая base64-строка (альтернатива Content).
ContentBase64 string
// ContentType — MIME-тип; пустая строка означает application/octet-stream.
ContentType string
// ContentID — Content-ID inline-картинки (до 255 символов, внешние угловые
// скобки допустимы); пустая строка — обычное вложение.
ContentID string
}
Attachment — вложение письма.
Передайте Content сырыми байтами, и SDK сам закодирует их в base64; если base64 уже готов — положите его в ContentBase64. Указывать оба поля нельзя.
С ContentID вложение становится inline-картинкой: оно встраивается в письмо, а не прикладывается файлом, и HTML ссылается на него как <img src="cid:<ContentID>">.
type BatchOption ¶ added in v0.2.0
type BatchOption func(*batchOptions)
BatchOption — настройка одного вызова SendBatch.
func WithBatchIdempotencyKey ¶ added in v0.2.0
func WithBatchIdempotencyKey(key string) BatchOption
WithBatchIdempotencyKey задаёт свой Idempotency-Key батча вместо сгенерированного SDK — например, привязанный к бизнес-событию, чтобы и повторный вызов SendBatch не дублировал уже принятые письма. Не длиннее BatchIdempotencyKeyMaxLength.
func WithoutBatchIdempotencyKey ¶ added in v0.2.0
func WithoutBatchIdempotencyKey() BatchOption
WithoutBatchIdempotencyKey отправляет батч вообще без Idempotency-Key.
Осознанный отказ от защиты: повтор после 5xx, таймаута или обрыва соединения мог бы отправить часть писем дважды, поэтому SDK повторяет такой батч только на 429.
type BatchResult ¶ added in v0.2.0
type BatchResult struct {
BatchSendResponseData
// Replayed — true, если это идемпотентный повтор: все принятые письма
// батча уже были приняты предыдущим запросом с тем же Idempotency-Key,
// повторной отправки не произошло (ответ 200 + заголовок
// Idempotent-Replayed).
Replayed bool
// IdempotencyKey — ключ, с которым ушёл батч: сгенерированный SDK или
// переданный через WithBatchIdempotencyKey; пустая строка — батч ушёл без
// ключа. Повтор батча с этим ключом не дублирует уже принятые письма.
IdempotencyKey string
// RequestID — идентификатор запроса для обращений в поддержку.
RequestID string
}
BatchResult — результат SendBatch: ответ API вместе с метаданными запроса.
type BatchSendError ¶
type BatchSendError struct {
StatusCode int `json:"status_code"`
Code string `json:"code"`
Message string `json:"message"`
Details map[string]any `json:"details"`
}
BatchSendError — ошибка одного письма внутри батча.
type BatchSendResponseData ¶
type BatchSendResponseData struct {
Results []BatchSendResult `json:"results"`
AcceptedCount int `json:"accepted_count"`
FailedCount int `json:"failed_count"`
}
BatchSendResponseData — результат пакетной отправки.
type BatchSendResult ¶
type BatchSendResult struct {
// Index — позиция письма в исходном списке.
Index int `json:"index"`
// ID — идентификатор принятого письма; nil, если письмо отклонено.
ID *string `json:"id"`
// Status — статус принятого письма; nil, если письмо отклонено.
Status *EmailStatus `json:"status"`
// SkippedRecipients — отброшенные получатели с причиной в значении.
SkippedRecipients []map[string]string `json:"skipped_recipients"`
// Error — причина отказа; nil, если письмо принято.
Error *BatchSendError `json:"error"`
}
BatchSendResult — результат одного письма внутри батча.
type BounceCategory ¶
type BounceCategory string
BounceCategory — причина недоставки.
const ( BounceInvalidRecipient BounceCategory = "invalid-recipient" BounceMailboxFull BounceCategory = "mailbox-full" BounceMessageTooLarge BounceCategory = "message-too-large" BounceContentSpamBlock BounceCategory = "content-spam-block" BouncePolicyRejection BounceCategory = "policy-rejection" BounceReputationBlocklist BounceCategory = "reputation-blocklist" BounceGreylisting BounceCategory = "greylisting" BounceDNSMXFailure BounceCategory = "dns-mx-failure" BounceConnectionError BounceCategory = "connection-error" BounceRateLimited BounceCategory = "rate-limited" BounceAuthFailure BounceCategory = "auth-failure" // BounceDeliveryExpired — письмо не доставлено за SendParams.TTL и больше // не повторяется. BounceDeliveryExpired BounceCategory = "delivery-expired" BounceUnknown BounceCategory = "unknown" )
Значения BounceCategory.
type BounceType ¶
type BounceType string
BounceType — характер недоставки: постоянная или временная.
const ( // BounceHard — постоянный отказ: адрес в suppression после первого же случая. BounceHard BounceType = "hard" // BounceSoft — временный отказ: имеет смысл повторить позже. BounceSoft BounceType = "soft" )
Значения BounceType.
type Client ¶
type Client struct {
// Emails — письма: Send, SendBatch, Get, List, ListAll, Events, Cancel,
// Reschedule.
Emails *EmailsService
// contains filtered or unexported fields
}
Client — клиент Public API MailInfra.
Создаётся через New и безопасен для параллельного использования: состояние запроса в нём не хранится.
client, err := mailinfra.New() // ключ из MAILINFRA_API_KEY
if err != nil {
return err
}
result, err := client.Emails.Send(ctx, mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Привет",
HTML: "<p>Привет!</p>",
})
Example (Send) ¶
package main
import (
"context"
"fmt"
"log"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New() // ключ из MAILINFRA_API_KEY
if err != nil {
log.Fatal(err)
}
result, err := client.Emails.Send(context.Background(), mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Добро пожаловать!",
HTML: "<p>Привет!</p>",
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.ID, result.Status)
}
Output:
func New ¶
New создаёт клиент.
Без WithAPIKey ключ берётся из переменной окружения MAILINFRA_API_KEY. Ошибка возвращается сразу, если ключа нет или его префикс не распознан: упасть при создании клиента лучше, чем получить 401 на первой отправке.
func (*Client) AutoIdempotency ¶
AutoIdempotency сообщает, ставит ли клиент Idempotency-Key сам.
func (*Client) Environment ¶
func (c *Client) Environment() Environment
Environment возвращает окружение ключа, распознанное по его префиксу.
func (*Client) RetryPolicy ¶
func (c *Client) RetryPolicy() RetryPolicy
RetryPolicy возвращает действующую политику повторов.
type DkimSigningStatus ¶
type DkimSigningStatus string
DkimSigningStatus — что известно про DKIM-подпись письма.
const ( DkimExpected DkimSigningStatus = "expected" DkimConfirmed DkimSigningStatus = "confirmed" DkimFailed DkimSigningStatus = "failed" DkimNotSigned DkimSigningStatus = "not_signed" )
Значения DkimSigningStatus.
type EmailDetail ¶
type EmailDetail struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
FromEmail string `json:"from_email"`
FromName *string `json:"from_name"`
ToEmails []string `json:"to_emails"`
CCEmails []string `json:"cc_emails"`
BCCEmails []string `json:"bcc_emails"`
ReplyTo []string `json:"reply_to"`
Subject string `json:"subject"`
HTMLBody *string `json:"html_body"`
TextBody *string `json:"text_body"`
Headers map[string]string `json:"headers"`
Status EmailStatus `json:"status"`
Source EmailSource `json:"source"`
Tags []string `json:"tags"`
Metadata map[string]string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
ScheduledAt *time.Time `json:"scheduled_at"`
ExpiresAt *time.Time `json:"expires_at"`
SentAt *time.Time `json:"sent_at"`
DeliveredAt *time.Time `json:"delivered_at"`
FailedAt *time.Time `json:"failed_at"`
MessageID *string `json:"message_id"`
BounceType *BounceType `json:"bounce_type"`
BounceCategory *BounceCategory `json:"bounce_category"`
SuppressionReason *SuppressionReason `json:"suppression_reason"`
FinalSMTPCode *int `json:"final_smtp_code"`
FinalSMTPEnhanced *string `json:"final_smtp_enhanced"`
FinalSMTPText *string `json:"final_smtp_text"`
SPFDomain *string `json:"spf_domain"`
SPFAligned *bool `json:"spf_aligned"`
SPFAuthorizedIP *bool `json:"spf_authorized_ip"`
DkimD *string `json:"dkim_d"`
DkimSelector *string `json:"dkim_selector"`
DkimAligned *bool `json:"dkim_aligned"`
DkimSigningStatus *DkimSigningStatus `json:"dkim_signing_status"`
DmarcAligned *bool `json:"dmarc_aligned"`
}
EmailDetail — письмо с телом, заголовками и диагностикой доставки.
Metadata, ScheduledAt и ExpiresAt — как у EmailListItem.
type EmailEvent ¶
type EmailEvent struct {
ID string `json:"id"`
Type EmailEventType `json:"type"`
Data map[string]any `json:"data"`
Timestamp time.Time `json:"timestamp"`
IPAddress *string `json:"ip_address"`
UserAgent *string `json:"user_agent"`
}
EmailEvent — событие жизненного цикла письма.
type EmailEventType ¶
type EmailEventType string
EmailEventType — тип события жизненного цикла письма.
const ( // EmailEventScheduled — письмо запланировано на SendAt; повторяется при // переносе. EmailEventScheduled EmailEventType = "scheduled" EmailEventQueued EmailEventType = "queued" EmailEventSending EmailEventType = "sending" EmailEventSent EmailEventType = "sent" EmailEventDelivered EmailEventType = "delivered" EmailEventSMTPConnected EmailEventType = "smtp_connected" EmailEventOpened EmailEventType = "opened" EmailEventClicked EmailEventType = "clicked" EmailEventBounced EmailEventType = "bounced" EmailEventComplained EmailEventType = "complained" EmailEventFailed EmailEventType = "failed" EmailEventSimulated EmailEventType = "simulated" // EmailEventDeferred — временный отказ сервера получателя, MTA повторит попытку. EmailEventDeferred EmailEventType = "deferred" // EmailEventCanceled — запланированное письмо отменено до отправки. EmailEventCanceled EmailEventType = "canceled" )
Значения EmailEventType.
type EmailListItem ¶
type EmailListItem struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
FromEmail string `json:"from_email"`
ToEmails []string `json:"to_emails"`
Subject string `json:"subject"`
Status EmailStatus `json:"status"`
Source EmailSource `json:"source"`
Tags []string `json:"tags"`
Metadata map[string]string `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
ScheduledAt *time.Time `json:"scheduled_at"`
ExpiresAt *time.Time `json:"expires_at"`
SentAt *time.Time `json:"sent_at"`
DeliveredAt *time.Time `json:"delivered_at"`
MessageID *string `json:"message_id"`
BounceType *BounceType `json:"bounce_type"`
BounceCategory *BounceCategory `json:"bounce_category"`
SuppressionReason *SuppressionReason `json:"suppression_reason"`
FinalSMTPCode *int `json:"final_smtp_code"`
FinalSMTPEnhanced *string `json:"final_smtp_enhanced"`
FinalSMTPText *string `json:"final_smtp_text"`
SPFDomain *string `json:"spf_domain"`
SPFAligned *bool `json:"spf_aligned"`
SPFAuthorizedIP *bool `json:"spf_authorized_ip"`
DkimD *string `json:"dkim_d"`
DkimSelector *string `json:"dkim_selector"`
DkimAligned *bool `json:"dkim_aligned"`
DkimSigningStatus *DkimSigningStatus `json:"dkim_signing_status"`
DmarcAligned *bool `json:"dmarc_aligned"`
}
EmailListItem — письмо в списке.
Metadata — пары из SendParams.Metadata (пустой объект, если их не передавали). ScheduledAt — на когда запланирована отправка, nil у письма без SendAt. ExpiresAt — когда истекает SendParams.TTL, nil у письма без срока жизни.
type EmailPage ¶ added in v0.2.0
type EmailPage struct {
// Data — письма страницы, новые сначала.
Data []EmailListItem `json:"data"`
// HasMore — есть ли письма после этой страницы.
HasMore bool `json:"has_more"`
// NextCursor — курсор следующей страницы для ListParams.Cursor; пустая
// строка, если следующей страницы нет.
NextCursor string `json:"next_cursor"`
}
EmailPage — страница писем: ответ GET /v1/emails.
type EmailPager ¶ added in v0.2.0
type EmailPager struct {
// contains filtered or unexported fields
}
EmailPager обходит все письма выборки, сам запрашивая следующие страницы по курсору. Создаётся через EmailsService.ListAll:
pager := client.Emails.ListAll(mailinfra.ListParams{Status: mailinfra.EmailStatusBounced})
for pager.Next(ctx) {
email := pager.Email()
log.Println(email.ID, email.Subject)
}
if err := pager.Err(); err != nil {
return err
}
Каждая страница — отдельный запрос со своими повторами; ошибка любой из них останавливает обход и возвращается из Err. Pager не безопасен для параллельного использования: одна горутина — один обход.
func (*EmailPager) Email ¶ added in v0.2.0
func (p *EmailPager) Email() EmailListItem
Email возвращает письмо, на которое перешёл последний успешный Next.
func (*EmailPager) Err ¶ added in v0.2.0
func (p *EmailPager) Err() error
Err возвращает ошибку, остановившую обход, или nil, если письма просто кончились.
func (*EmailPager) Next ¶ added in v0.2.0
func (p *EmailPager) Next(ctx context.Context) bool
Next переходит к следующему письму и сообщает, есть ли оно. Когда письма текущей страницы кончаются, Next запрашивает следующую с контекстом ctx.
false означает конец обхода: письма кончились или запрос страницы не удался — что именно, скажет Err.
type EmailScheduleData ¶ added in v0.2.0
type EmailScheduleData struct {
// ID — идентификатор письма.
ID string `json:"id"`
// Status — статус после операции: CANCELED после отмены, SCHEDULED после
// переноса (и тогда, когда новое время уже наступило: письмо уйдёт с
// ближайшим выпуском запланированных).
Status EmailStatus `json:"status"`
// ScheduledAt — время отправки: после переноса — новое (момент переноса,
// если переданное уже наступило), после отмены — то, на которое письмо
// было запланировано.
ScheduledAt *time.Time `json:"scheduled_at"`
}
EmailScheduleData — состояние планирования письма: ответ Cancel и Reschedule. Тела письма в нём нет.
type EmailSendResponseData ¶
type EmailSendResponseData struct {
// ID — идентификатор письма в MailInfra.
ID string `json:"id"`
// Status — статус сразу после приёма: QUEUED, SCHEDULED для письма с
// SendAt в будущем либо SIMULATED для sandbox-ключа.
Status EmailStatus `json:"status"`
// SkippedRecipients — получатели, которых API отбросил (например, адрес в
// списке подавления), с причиной в значении.
SkippedRecipients []map[string]string `json:"skipped_recipients"`
}
EmailSendResponseData — данные успешной отправки.
type EmailSource ¶
type EmailSource string
EmailSource — транспорт, через который письмо попало в отправку.
const ( EmailSourceAPI EmailSource = "API" EmailSourceSMTP EmailSource = "SMTP" EmailSourceCampaign EmailSource = "CAMPAIGN" )
Значения EmailSource.
type EmailStatus ¶
type EmailStatus string
EmailStatus — статус письма.
const ( // EmailStatusScheduled — письмо принято с SendParams.SendAt в будущем и // ждёт своего часа; до отправки его можно отменить (Cancel) или перенести // (Reschedule). EmailStatusScheduled EmailStatus = "SCHEDULED" // EmailStatusQueued — письмо принято и ждёт отправки. EmailStatusQueued EmailStatus = "QUEUED" // EmailStatusSending — письмо передаётся MTA. EmailStatusSending EmailStatus = "SENDING" // EmailStatusSent — письмо принято сервером получателя. EmailStatusSent EmailStatus = "SENT" // EmailStatusDelivered — доставка подтверждена. EmailStatusDelivered EmailStatus = "DELIVERED" // EmailStatusBounced — письмо отбито (см. BounceType и BounceCategory). EmailStatusBounced EmailStatus = "BOUNCED" // EmailStatusComplained — получатель пожаловался на спам. EmailStatusComplained EmailStatus = "COMPLAINED" // EmailStatusFailed — отправка не удалась. EmailStatusFailed EmailStatus = "FAILED" // EmailStatusSimulated — sandbox-отправка по ключу mi_test_…: письмо // сохранено, но не доставлено и не расходует месячный объём. EmailStatusSimulated EmailStatus = "SIMULATED" // EmailStatusCanceled — запланированное письмо отменено до отправки. EmailStatusCanceled EmailStatus = "CANCELED" )
Значения EmailStatus.
type EmailsService ¶
type EmailsService struct {
// contains filtered or unexported fields
}
EmailsService — письма проекта. Доступен как client.Emails.
func (*EmailsService) Cancel ¶ added in v0.2.0
func (s *EmailsService) Cancel(ctx context.Context, emailID string) (*EmailScheduleData, error)
Cancel отменяет запланированное письмо (SendParams.SendAt) до отправки: письмо получает статус CANCELED, резерв месячного объёма возвращается.
Отменить можно только письмо в статусе SCHEDULED; на остальные API отвечает 409 с ErrEmailNotScheduled, текущий статус — в Details["status"]. Нужен ключ с правом отправки. SDK повторяет отмену после 5xx и таймаутов: если первая попытка успела отменить письмо, повтор вернёт ErrEmailNotScheduled со статусом CANCELED.
Example ¶
Отменить можно только письмо, которое ещё не ушло: на остальные API отвечает ErrEmailNotScheduled.
package main
import (
"context"
"errors"
"fmt"
"log"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
emailID := "018e1b7a-1111-7000-aaaa-111111111111" // result.ID отложенной отправки
canceled, err := client.Emails.Cancel(context.Background(), emailID)
switch {
case err == nil:
fmt.Println("отменено:", canceled.Status) // CANCELED
case errors.Is(err, mailinfra.ErrEmailNotScheduled):
var apiErr *mailinfra.Error
errors.As(err, &apiErr)
fmt.Println("письмо уже не запланировано, статус:", apiErr.Details["status"])
default:
log.Fatal(err)
}
}
Output:
func (*EmailsService) Events ¶
func (s *EmailsService) Events(ctx context.Context, emailID string) ([]EmailEvent, error)
Events возвращает события письма в хронологическом порядке.
func (*EmailsService) Get ¶
func (s *EmailsService) Get(ctx context.Context, emailID string) (*EmailDetail, error)
Get возвращает письмо с телом, заголовками и диагностикой доставки.
func (*EmailsService) List ¶
func (s *EmailsService) List(ctx context.Context, params ListParams) (*EmailPage, error)
List возвращает одну страницу писем проекта по фильтрам, новые сначала.
Следующая страница — тот же вызов с Cursor = page.NextCursor, пока page.HasMore. Обойти все письма выборки проще через ListAll: за курсором он следит сам.
Example ¶
List отдаёт одну страницу; следующая — по NextCursor, пока HasMore.
package main
import (
"context"
"fmt"
"log"
"time"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
// From и To — интервал по времени создания письма, а не адреса.
page, err := client.Emails.List(context.Background(), mailinfra.ListParams{
Status: mailinfra.EmailStatusBounced,
Tag: "order",
From: time.Now().Add(-24 * time.Hour),
Limit: 50,
})
if err != nil {
log.Fatal(err)
}
fmt.Println("на первой странице:", len(page.Data), "ещё есть:", page.HasMore)
}
Output:
func (*EmailsService) ListAll ¶ added in v0.2.0
func (s *EmailsService) ListAll(params ListParams) *EmailPager
ListAll возвращает итератор по всем письмам, подходящим под фильтры params.
Сам по себе вызов в сеть не ходит: первая страница запрашивается при первом Next. params.Limit задаёт размер страницы, а не общее число писем; params.Cursor позволяет продолжить обход с сохранённого курсора.
Example ¶
ListAll обходит все письма выборки и сам запрашивает следующие страницы.
package main
import (
"context"
"fmt"
"log"
"time"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
pager := client.Emails.ListAll(mailinfra.ListParams{
Status: mailinfra.EmailStatusBounced,
From: time.Now().Add(-7 * 24 * time.Hour),
Limit: 100, // размер страницы, а не потолок обхода
})
bounced := 0
for pager.Next(ctx) {
email := pager.Email()
if email.Metadata["order_id"] != "" {
bounced++
}
}
if err := pager.Err(); err != nil {
log.Fatal(err)
}
fmt.Println("отбитых писем с заказом за неделю:", bounced)
}
Output:
func (*EmailsService) Reschedule ¶ added in v0.2.0
func (s *EmailsService) Reschedule(ctx context.Context, emailID string, sendAt time.Time) (*EmailScheduleData, error)
Reschedule переносит запланированное письмо на sendAt.
Горизонт — 7 дней от приёма письма, а не от переноса: дальше API ответит ErrSendAtTooFar. Время в прошлом — отправить сейчас, с ближайшим выпуском запланированных писем. Срок жизни письма (TTL) переезжает вместе с ним. Как и у Cancel, письмо должно быть в статусе SCHEDULED (иначе ErrEmailNotScheduled), а ключ — с правом отправки.
Example ¶
Перенос считает горизонт в 7 дней от приёма письма; время в прошлом — отправить сейчас.
package main
import (
"context"
"errors"
"fmt"
"log"
"time"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
emailID := "018e1b7a-1111-7000-aaaa-111111111111"
moved, err := client.Emails.Reschedule(context.Background(), emailID, time.Now().Add(48*time.Hour))
if errors.Is(err, mailinfra.ErrSendAtTooFar) {
fmt.Println("так далеко запланировать нельзя")
return
}
if err != nil {
log.Fatal(err)
}
fmt.Println("уйдёт в", moved.ScheduledAt)
}
Output:
func (*EmailsService) Send ¶
func (s *EmailsService) Send(ctx context.Context, params SendParams) (*SendResult, error)
Send отправляет письмо.
Возвращает идентификатор, статус, отброшенных получателей, флаг идемпотентного повтора и использованный ключ идемпотентности.
Example (IdempotencyKey) ¶
Ключ идемпотентности, привязанный к бизнес-событию, делает повторный вызов безопасным: второй раз письмо не уйдёт.
package main
import (
"context"
"fmt"
"log"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
result, err := client.Emails.Send(context.Background(), mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Заказ A-1024 оплачен",
HTML: "<p>Спасибо!</p>",
IdempotencyKey: "order-A-1024-paid",
})
if err != nil {
log.Fatal(err)
}
if result.Replayed {
fmt.Println("письмо уже было отправлено раньше, дубля нет")
}
}
Output:
Example (Scheduled) ¶
Отложенная отправка: письмо получит статус SCHEDULED и уйдёт в SendAt, а TTL не даст ему уйти, если до получателя оно не дошло вовремя. Metadata вернётся в каждом вебхуке письма.
package main
import (
"context"
"fmt"
"log"
"time"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
result, err := client.Emails.Send(context.Background(), mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Напоминание об оплате",
Text: "Заказ A-1024 ждёт оплаты.",
Metadata: map[string]string{"order_id": "A-1024"},
SendAt: time.Now().Add(24 * time.Hour),
TTL: 2 * time.Hour,
})
if err != nil {
log.Fatal(err)
}
fmt.Println(result.ID, result.Status) // … SCHEDULED
}
Output:
func (*EmailsService) SendBatch ¶
func (s *EmailsService) SendBatch(ctx context.Context, emails []SendParams, opts ...BatchOption) (*BatchResult, error)
SendBatch отправляет до 100 писем одним запросом.
Батч возвращает 202, даже если часть писем не принята: проверяйте Error у каждого элемента Results, а не только отсутствие ошибки вызова.
Idempotency-Key SDK ставит сам, один на вызов (свой — WithBatchIdempotencyKey): бэкенд выводит из него ключ каждого письма, поэтому повтор после 5xx, таймаута и обрыва соединения не дублирует уже принятые письма, а отклонённые пробует принять заново. Без ключа (WithoutBatchIdempotencyKey, WithAutoIdempotency(false)) батч повторяется только на 429 — повтор мог бы отправить часть писем дважды.
Example ¶
Батч возвращает 202, даже если часть писем не принята: проверять надо каждый элемент, а не только отсутствие ошибки вызова.
package main
import (
"context"
"fmt"
"log"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
batch, err := client.Emails.SendBatch(context.Background(), []mailinfra.SendParams{
{From: "hello@mail.example.ru", To: []string{"a@example.com"}, Subject: "Раз", Text: "1"},
{From: "hello@mail.example.ru", To: []string{"b@example.com"}, Subject: "Два", Text: "2"},
})
if err != nil {
log.Fatal(err)
}
for _, item := range batch.Results {
if item.Error != nil {
fmt.Printf("письмо #%d отклонено: %s\n", item.Index, item.Error.Code)
}
}
}
Output:
Example (IdempotencyKey) ¶
Ключ батча, привязанный к бизнес-событию, делает безопасным и повторный вызов: уже принятые письма второй раз не уйдут, а отклонённые батч попробует принять заново.
package main
import (
"context"
"fmt"
"log"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
batch, err := client.Emails.SendBatch(context.Background(), []mailinfra.SendParams{
{From: "hello@mail.example.ru", To: []string{"a@example.com"}, Subject: "Раз", Text: "1"},
{From: "hello@mail.example.ru", To: []string{"b@example.com"}, Subject: "Два", Text: "2"},
}, mailinfra.WithBatchIdempotencyKey("digest-2026-10-28"))
if err != nil {
log.Fatal(err)
}
if batch.Replayed {
fmt.Println("батч уже был принят раньше, дублей нет")
}
}
Output:
type Environment ¶
type Environment string
Environment — окружение API-ключа.
const ( // EnvironmentLive — рабочий ключ mi_live_…: письма уходят получателям. EnvironmentLive Environment = "live" // EnvironmentTest — sandbox-ключ mi_test_…: письмо сохраняется со статусом // SIMULATED, не доставляется и не расходует месячный объём. EnvironmentTest Environment = "test" )
Значения Environment.
type Error ¶
type Error struct {
// Message — человеко-читаемое сообщение от API (на русском).
Message string
// StatusCode — HTTP-статус ответа; 0 для сетевых ошибок и клиентской валидации.
StatusCode int
// Code — машинный код ошибки (error.code).
Code string
// Details — безопасные структурированные детали (error.details).
Details map[string]any
// RequestID — идентификатор запроса для поддержки (он же заголовок X-Request-ID).
RequestID string
// RetryAfter — сколько просил подождать сервер; значимо только вместе с
// HasRetryAfter.
RetryAfter time.Duration
// HasRetryAfter — был ли в ответе разобранный заголовок Retry-After.
HasRetryAfter bool
// contains filtered or unexported fields
}
Error — ошибка вызова MailInfra: и ответ API, и транспортный сбой, и отказ клиентской валидации.
Ловить её целиком удобно через errors.As, а ветвиться — через errors.Is по часовым ошибкам (см. описание пакета).
Example ¶
Ветвиться по причине отказа надо через errors.Is, а метаданные для логов брать через errors.As.
package main
import (
"context"
"errors"
"fmt"
"log"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
client, err := mailinfra.New()
if err != nil {
log.Fatal(err)
}
_, err = client.Emails.Send(context.Background(), mailinfra.SendParams{
From: "hello@mail.example.ru",
To: []string{"user@example.com"},
Subject: "Привет",
Text: "Привет!",
})
switch {
case err == nil:
fmt.Println("принято")
case errors.Is(err, mailinfra.ErrDomainNotVerified):
fmt.Println("домен отправителя ещё не подтверждён")
case errors.Is(err, mailinfra.ErrRateLimit):
var apiErr *mailinfra.Error
errors.As(err, &apiErr)
fmt.Println("лимит, повторить через", apiErr.RetryAfter)
case errors.Is(err, mailinfra.ErrUnprocessable):
var apiErr *mailinfra.Error
errors.As(err, &apiErr)
for _, field := range apiErr.Fields() {
fmt.Printf("%s: %s\n", field.Field, field.Message)
}
default:
log.Fatal(err)
}
}
Output:
func (*Error) Error ¶
Error возвращает текст ошибки с машинным кодом, статусом и request_id — всем, что нужно для обращения в поддержку.
func (*Error) Fields ¶
func (e *Error) Fields() []ValidationField
Fields возвращает пер-полевые причины отказа валидации (422 с кодом request_validation_failed). Пустой срез, если детализации нет.
type ListParams ¶
type ListParams struct {
// Status — фильтр по статусу.
Status EmailStatus
// FromEmail — фильтр по отправителю.
FromEmail string
// ToEmail — фильтр по получателю.
ToEmail string
// Tag — фильтр по тегу.
Tag string
// From — начало интервала по времени создания.
From time.Time
// To — конец интервала по времени создания.
To time.Time
// Limit — размер страницы, 1..100 (по умолчанию 25 на стороне API).
Limit int
// Cursor — EmailPage.NextCursor предыдущей страницы; пустая строка —
// первая страница. Курсор непрозрачен: собирать или менять его не нужно.
Cursor string
}
ListParams — фильтры выборки писем. Нулевое значение поля означает «без фильтра».
type Option ¶
type Option func(*options)
Option — настройка клиента, передаётся в New.
func WithAPIKey ¶
WithAPIKey задаёт API-ключ проекта. По умолчанию ключ берётся из MAILINFRA_API_KEY.
func WithAutoIdempotency ¶
WithAutoIdempotency включает или выключает автоматический Idempotency-Key на отправке письма и батча. По умолчанию включён.
Выключать стоит только если ключ вы генерируете сами: без него бэкенду нечем распознать повтор, и SDK перестаёт ретраить отправку после 5xx и таймаутов.
func WithBaseURL ¶
WithBaseURL задаёт базу API. По умолчанию — MAILINFRA_BASE_URL либо DefaultBaseURL.
func WithHTTPClient ¶
WithHTTPClient подставляет свой *http.Client — прокси, пул соединений, mTLS.
Таймаут такого клиента SDK не перетирает: без явного WithTimeout собственный дедлайн не ставится вовсе, и запрос ограничивают только ваши настройки.
func WithMaxRetries ¶
WithMaxRetries задаёт число повторов сверх первой попытки. 0 — без повторов.
func WithRequireEnvironment ¶
func WithRequireEnvironment(environment Environment) Option
WithRequireEnvironment требует ключ нужного окружения: New вернёт ошибку, если префикс ключа не совпал.
Страхует от самой дорогой ошибки конфигурации — тестовый ключ в проде не отправит ни одного письма, а рабочий в тестах отправит настоящие.
func WithRetryPolicy ¶
func WithRetryPolicy(policy RetryPolicy) Option
WithRetryPolicy задаёт политику повторов. Незаполненные поля заменяются значениями по умолчанию — и MaxRetries тоже: нулевой MaxRetries в политике значит «не задан» (по умолчанию 2 повтора), а не «без повторов». Отключить повторы — WithMaxRetries(0) после WithRetryPolicy или отрицательный MaxRetries в самой политике.
func WithTimeout ¶
WithTimeout задаёт таймаут одной попытки. 0 — не ставить собственный дедлайн (полезно, если временем управляет ваш http.Client или контекст вызова).
По умолчанию — DefaultTimeout для своего клиента и 0 для переданного через WithHTTPClient.
func WithUserAgentSuffix ¶
WithUserAgentSuffix добавляет к User-Agent имя вашей интеграции.
type RetryPolicy ¶
type RetryPolicy struct {
// MaxRetries — сколько раз повторить сверх первой попытки. Как и у
// остальных полей, 0 в WithRetryPolicy — «не задан» (по умолчанию 2);
// отрицательное значение — повторов нет. Проще отключить повторы через
// WithMaxRetries(0).
MaxRetries int
// InitialBackoff — базовая задержка первой паузы.
InitialBackoff time.Duration
// BackoffFactor — множитель роста задержки между попытками.
BackoffFactor float64
// MaxBackoff — потолок собственной задержки.
MaxBackoff time.Duration
// MaxRetryAfter — порог доверия к `Retry-After`. Если сервер просит
// подождать дольше — SDK не спит, а отдаёт ошибку с ErrRateLimit, чтобы
// вызывающий сам решил, ждать или деградировать.
MaxRetryAfter time.Duration
// RetryableStatuses — статусы, повторяемые для идемпотентных запросов.
// nil — набор по умолчанию.
RetryableStatuses []int
// RetryableStatusesNonIdempotent — то же для неидемпотентных. nil —
// набор по умолчанию.
RetryableStatusesNonIdempotent []int
}
RetryPolicy — настройки повторов.
Нулевое значение полей означает «взять дефолт», поэтому частичная настройка через WithRetryPolicy не обнуляет остальное.
func DefaultRetryPolicy ¶
func DefaultRetryPolicy() RetryPolicy
DefaultRetryPolicy возвращает политику повторов по умолчанию: две попытки сверх первой, backoff 0.5s → 1s → … с потолком 8s, доверие к `Retry-After` до минуты.
type SendParams ¶
type SendParams struct {
// From — адрес отправителя на подтверждённом домене.
From string
// FromName — отображаемое имя отправителя.
FromName string
// To — получатели, минимум один.
To []string
// CC — копия.
CC []string
// BCC — скрытая копия.
BCC []string
// ReplyTo — адреса для ответа.
ReplyTo []string
// Subject — тема; обязательна, если письмо шлётся без шаблона.
Subject string
// HTML — HTML-часть письма.
HTML string
// Text — текстовая часть письма.
Text string
// Headers — дополнительные заголовки письма (системные запрещены).
Headers map[string]string
// Tags — теги для фильтрации в дашборде и вебхуках.
Tags []string
// Metadata — ваши пары «ключ — значение» (номер заказа, id пользователя).
// На доставку не влияют, а возвращаются в Get и List (поле Metadata
// письма) и в каждом вебхуке письма (WebhookEvent.EmailMetadata).
//
// До MaxMetadataKeys ключей длиной до MaxMetadataKeyLength из латиницы,
// цифр, '_', '-' и '.'; значения — до MaxMetadataValueLength символов без
// NUL; весь объект — до MaxMetadataBytes байт JSON.
Metadata map[string]string
// Template — слаг шаблона вместо HTML/Text.
Template string
// Variables — переменные шаблона.
Variables map[string]any
// Attachments — вложения; inline-картинки — вложения с ContentID.
Attachments []Attachment
// SendAt — отложенная отправка: письмо получит статус SCHEDULED и уйдёт в
// этот момент, а до того его можно отменить (Cancel) или перенести
// (Reschedule). Не дальше 7 дней от приёма письма — иначе API ответит
// ErrSendAtTooFar; время в прошлом — отправить сразу. Нулевое значение —
// отправить сразу.
SendAt time.Time
// TTL — срок жизни письма от MinTTL до MaxTTL в целых секундах, отсчёт от
// SendAt или от приёма. Не дошедшее за это время письмо не отправляется и
// не повторяется: статус FAILED с причиной expired или отказ с категорией
// BounceDeliveryExpired. Для кодов входа и других писем, которые поздно уже
// не нужны. 0 — без срока жизни.
TTL time.Duration
// IdempotencyKey — свой ключ идемпотентности. Пустая строка означает
// «сгенерировать», если у клиента включён автоматический ключ.
//
// В SendBatch не задаётся: ключ там один на весь батч
// (WithBatchIdempotencyKey).
IdempotencyKey string
// WithoutIdempotencyKey отправляет письмо вообще без Idempotency-Key.
//
// Осознанный отказ от защиты: бэкенду будет нечем распознать повтор, и SDK
// перестанет ретраить этот запрос после 5xx и таймаутов. В SendBatch не
// действует — там это WithoutBatchIdempotencyKey.
WithoutIdempotencyKey bool
}
SendParams — параметры отправки письма.
Обязательны только From и To; остальное зависит от того, шлёте вы готовое тело (Subject + HTML/Text) или шаблон (Template + Variables).
type SendResult ¶
type SendResult struct {
EmailSendResponseData
// Replayed — true, если это идемпотентный повтор: письмо было принято
// предыдущим запросом с тем же Idempotency-Key, повторной отправки не
// произошло (ответ 200 + заголовок Idempotent-Replayed).
Replayed bool
// IdempotencyKey — ключ, с которым ушёл запрос: сгенерированный SDK или
// переданный вызывающим. Стоит залогировать: с ним повтор гарантированно
// не создаст второе письмо.
IdempotencyKey string
// RequestID — идентификатор запроса для обращений в поддержку.
RequestID string
}
SendResult — результат отправки одного письма.
type SuppressionReason ¶
type SuppressionReason string
SuppressionReason — причина попадания адреса в список подавления.
const ( SuppressionHardBounce SuppressionReason = "HARD_BOUNCE" SuppressionComplaint SuppressionReason = "COMPLAINT" SuppressionUnsubscribe SuppressionReason = "UNSUBSCRIBE" SuppressionManual SuppressionReason = "MANUAL" )
Значения SuppressionReason.
type ValidationField ¶
ValidationField — пер-полевая причина отказа валидации из Details["fields"].
type WebhookEvent ¶
type WebhookEvent struct {
// ID — идентификатор события: evt_<uuid> у событий письма,
// evt_deliverability_<hex> у deliverability.*, evt_test_ping у теста.
ID string `json:"id"`
// Type — тип: email.<событие> у событий письма (например email.bounced,
// email.scheduled, email.canceled — значения EmailEventType с префиксом
// email.), deliverability.<инцидент> как есть (например
// deliverability.dns_degraded), test.ping у тестовой отправки из дашборда.
Type string `json:"type"`
// CreatedAt — момент события на стороне MailInfra.
CreatedAt time.Time `json:"created_at"`
// Data — полезная нагрузка. У событий письма Data["email"] — письмо (в том
// числе его metadata), Data["event"] — специфика события (MX, IP,
// bounce-детали, URL клика).
// У deliverability.* письма нет: domain_id, domain, severity, title,
// details инцидента.
Data map[string]any `json:"data"`
}
WebhookEvent — разобранное событие вебхука.
func ConstructWebhookEvent ¶
func ConstructWebhookEvent(body []byte, secret, signatureHeader string, opts ...WebhookOption) (*WebhookEvent, error)
ConstructWebhookEvent проверяет подпись и возвращает типизированное событие.
Это рекомендуемый способ: тело нельзя разобрать раньше, чем проверена подпись, иначе обработчик доверяет неаутентифицированным данным.
Ошибка всегда *WebhookSignatureError; причина — в поле Reason.
Example ¶
Тело обязательно брать до разбора JSON: пересериализованный JSON даст другие байты и подпись не сойдётся.
package main
import (
"errors"
"io"
"log"
"net/http"
"os"
mailinfra "github.com/g7AzaZLO/mailinfra-go"
)
func main() {
http.HandleFunc("/mailinfra/events", func(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
event, err := mailinfra.ConstructWebhookEvent(
body,
os.Getenv("MAILINFRA_WEBHOOK_SECRET"),
r.Header.Get(mailinfra.WebhookSignatureHeader),
)
if err != nil {
var signatureErr *mailinfra.WebhookSignatureError
if errors.As(err, &signatureErr) {
log.Printf("подпись отклонена: %s", signatureErr.Reason)
}
http.Error(w, "", http.StatusBadRequest)
return
}
switch event.Type {
case "email.bounced":
log.Println("отбито:", event.EmailID(), "заказ:", event.EmailMetadata()["order_id"])
case "email.delivered":
log.Println("доставлено:", event.EmailID())
}
w.WriteHeader(http.StatusOK)
})
}
Output:
func (*WebhookEvent) Email ¶
func (e *WebhookEvent) Email() map[string]any
Email возвращает письмо, к которому относится событие, или nil для событий без письма (deliverability.*, test.ping).
func (*WebhookEvent) EmailID ¶
func (e *WebhookEvent) EmailID() string
EmailID возвращает идентификатор письма или пустую строку, если события письма нет.
func (*WebhookEvent) EmailMetadata ¶ added in v0.2.0
func (e *WebhookEvent) EmailMetadata() map[string]string
EmailMetadata возвращает metadata письма — пары из SendParams.Metadata, по которым событие связывается с вашей системой. nil, если у события нет письма; пустой map, если metadata письму не задавали.
func (*WebhookEvent) IsTest ¶
func (e *WebhookEvent) IsTest() bool
IsTest сообщает, тестовое ли это событие («Отправить тест» в дашборде).
type WebhookOption ¶
type WebhookOption func(*webhookOptions)
WebhookOption — настройка проверки подписи.
func WithWebhookMaxAge ¶
func WithWebhookMaxAge(maxAge time.Duration) WebhookOption
WithWebhookMaxAge задаёт допустимый возраст подписи; защита от replay. Нулевое значение отключает проверку времени.
type WebhookSignatureError ¶
type WebhookSignatureError struct {
// Message — человеко-читаемое описание.
Message string
// Reason — машинная причина: одна из констант WebhookReason*.
Reason string
}
WebhookSignatureError — подпись не прошла проверку.
func (*WebhookSignatureError) Error ¶
func (e *WebhookSignatureError) Error() string
Error возвращает описание вместе с машинной причиной.
func (*WebhookSignatureError) Unwrap ¶
func (e *WebhookSignatureError) Unwrap() error
Unwrap отдаёт часовую ошибку, по которой работает errors.Is.