mailinfra

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 20 Imported by: 0

README

MailInfra SDK для Go

Официальный клиент MailInfra — email API для разработчиков.

go get github.com/g7AzaZLO/mailinfra-go
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)
}

Требования: Go 1.22+. Внешних зависимостей нет — только стандартная библиотека, поэтому SDK ничего не тянет в ваш go.sum.

Пакет называется mailinfra, хотя путь импорта заканчивается на mailinfra-go. Если линтер ругается на неочевидное имя — импортируйте явно: import mailinfra "github.com/g7AzaZLO/mailinfra-go".

Что клиент делает за вас

Возможность Подробности
Идемпотентность На каждую отправку ставится Idempotency-Key, и он переиспользуется при повторах — обрыв сети не превращается в два письма
Повторы 429, 408/425, 5xx и сетевые сбои повторяются с экспоненциальным backoff и half-jitter
Rate limit Retry-After уважается как есть; слишком долгий (>60 с) не блокирует поток, а приходит как Error.RetryAfter
Типизированные ошибки errors.Is по ~20 часовым ошибкам, errors.As до *mailinfra.Error с RequestID и Details
Локальная валидация Лимиты получателей, тегов, темы и вложений проверяются до сетевого вызова; совпадение со спекой проверяет тест
Вебхуки ConstructWebhookEvent — проверка HMAC-подписи и разбор события

Аутентификация

mailinfra.New(mailinfra.WithAPIKey("mi_live_…"))              // явно
mailinfra.New()                                                // из MAILINFRA_API_KEY
mailinfra.New(mailinfra.WithRequireEnvironment(mailinfra.EnvironmentLive)) // упасть на тестовом ключе

mi_live_… отправляет письма по-настоящему. mi_test_… — sandbox: письмо сохраняется со статусом SIMULATED, не доставляется и не расходует месячный объём плана. Окружение доступно как client.Environment().

База API меняется через WithBaseURL или переменную MAILINFRA_BASE_URL.

Client потокобезопасен: держите один на приложение.

Отправка

Имена полей письма совпадают с API по смыслу, а в JSON уходят ровно как в спеке (from, reply_to, content_base64).

По шаблону
result, err := client.Emails.Send(ctx, mailinfra.SendParams{
	From:     "hello@mail.example.ru",
	To:       []string{"user@example.com"},
	Template: "order-confirmed",
	Variables: map[string]any{
		"name":  "Аня",
		"order": "A-1024",
	},
})
С вложением
invoice, err := os.ReadFile("invoice.pdf")
if err != nil {
	return err
}

result, err := client.Emails.Send(ctx, mailinfra.SendParams{
	From:    "billing@mail.example.ru",
	To:      []string{"user@example.com"},
	Subject: "Счёт",
	Text:    "Счёт во вложении.",
	Attachments: []mailinfra.Attachment{{
		Filename:    "invoice.pdf",
		Content:     invoice, // SDK закодирует в base64
		ContentType: "application/pdf",
	}},
})

Если base64 уже готов — положите его в ContentBase64 вместо Content.

Свой ключ идемпотентности
result, err := client.Emails.Send(ctx, mailinfra.SendParams{
	From:           "hello@mail.example.ru",
	To:             []string{"user@example.com"},
	Subject:        "Заказ A-1024 оплачен",
	HTML:           "<p>Спасибо!</p>",
	IdempotencyKey: "order-A-1024-paid", // повтор вернёт то же письмо
})

if result.Replayed {
	log.Println("письмо уже было отправлено раньше, дубля нет")
}

Без IdempotencyKey клиент генерирует UUID сам — на каждый вызов новый. Отправить вообще без ключа можно через WithoutIdempotencyKey: true, но тогда бэкенду нечем распознать повтор, и SDK перестанет ретраить этот запрос после 5xx и таймаутов. Бэкенд хранит ключ 24 часа: повтор с тем же ключом позже создаст новое письмо.

Пакетная отправка
batch, err := client.Emails.SendBatch(ctx, []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 {
	return err
}

// Батч возвращает 202, даже если часть писем не принята.
for _, item := range batch.Results {
	if item.Error != nil {
		log.Printf("письмо #%d отклонено: %s", item.Index, item.Error.Code)
	}
}

До 100 писем за раз. Idempotency-Key этот эндпоинт не принимает — бэкенд коммитит письма батча по одному, поэтому SDK не повторяет его после 5xx, таймаута и обрыва соединения. Повторяется только 429: лимитёр отказывает до приёма писем.

Чтение

email, err := client.Emails.Get(ctx, emailID)

emails, err := client.Emails.List(ctx, mailinfra.ListParams{
	Status: mailinfra.EmailStatusBounced,
	Tag:    "order",
	From:   time.Now().Add(-24 * time.Hour),
	Limit:  50,
})

events, err := client.Emails.Events(ctx, emailID)

From и To в ListParams — это интервал по времени создания письма, а не адреса; адреса фильтруются полями FromEmail и ToEmail. Время уходит в API в UTC.

Ошибки

Ветвиться по причине отказа надо через 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)
	log.Printf("лимит, повторить через %s", apiErr.RetryAfter)

case errors.Is(err, mailinfra.ErrUnprocessable):
	// вся категория 422 разом
	var apiErr *mailinfra.Error
	errors.As(err, &apiErr)
	for _, field := range apiErr.Fields() {
		log.Printf("%s: %s", field.Field, field.Message)
	}

default:
	return err
}

Три уровня ловли:

  • *mailinfra.Error через errors.As — всё, что вернул API, вместе с RequestID;
  • категория статуса: ErrBadRequest, ErrAuthentication, ErrPermissionDenied, ErrNotFound, ErrConflict, ErrUnprocessable, ErrRateLimit, ErrServer, ErrConnection, ErrTimeout;
  • конкретная причина: ErrDomainNotVerified, ErrDomainNotAttached, ErrAllRecipientsSuppressed, ErrTemplateRender, ErrTemplateVariableMissing, ErrReservedEmailHeader, ErrSandboxRecipientNotAllowed, ErrSandboxTrialExhausted, ErrIdempotencyInFlight, ErrLegalDocumentsNotAccepted.

Конкретная причина всегда ловится и своей категорией: errors.Is(err, mailinfra.ErrUnprocessable) истинно и для ErrDomainNotVerified. Неизвестный код с бэкенда деградирует до категории по статусу — новый код не ломает старый SDK.

Отмена контекста приходит как context.Canceled / context.DeadlineExceeded, а не как ошибка SDK: повторять её нечего.

Настройка повторов

client, err := mailinfra.New(
	mailinfra.WithMaxRetries(4),
	mailinfra.WithRetryPolicy(mailinfra.RetryPolicy{
		MaxRetries:     4,
		InitialBackoff: 200 * time.Millisecond,
		MaxBackoff:     5 * time.Second,
		MaxRetryAfter:  30 * time.Second, // дольше — не ждём, отдаём ошибку
	}),
)

Незаполненные поля RetryPolicy заменяются значениями по умолчанию, поэтому менять можно только нужное (незаданный MaxRetries — тоже 2 повтора). WithMaxRetries(0) после WithRetryPolicy отключает повторы.

Таймаут одной попытки — WithTimeout; по умолчанию 30 с. Свой *http.Client (прокси, пул, mTLS) подставляется через WithHTTPClient, и тогда SDK не ставит собственный дедлайн, пока вы не попросили его явно.

Вебхуки

func handler(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 {
		// Причина — в поле Reason: invalid_secret, malformed_header, too_old,
		// signature_mismatch, invalid_json, invalid_payload.
		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())
	case "email.delivered":
		log.Println("доставлено:", event.EmailID())
	}
	w.WriteHeader(http.StatusOK)
}

Тело обязательно брать до разбора JSON: пересериализованный JSON даст другие байты и подпись не сойдётся. Подписи старше 5 минут отклоняются (WithWebhookMaxAge меняет окно, 0 отключает проверку).

Что пока не покрыто

v1 покрывает публичный API писем (/v1/emails*) — всё, что доступно по API-ключу проекта. Управление доменами, шаблонами, списком подавления, вебхуками и ключами — это Management API на сессионном токене пользователя; для машинной интеграции у него пока нет подходящего credential, поэтому в SDK этих ресурсов нет. Следите за CHANGELOG.

Ссылки

Лицензия

MIT

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;
  • проверяет ограничения письма локально до сетевого вызова;
  • проверяет подписи вебхуков (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
}

Потокобезопасность

Client можно держать один на приложение и вызывать из нескольких горутин: состояние запроса в нём не хранится.

Index

Examples

Constants

View Source
const (
	// APIKeyEnvVar — переменная окружения с API-ключом проекта.
	APIKeyEnvVar = "MAILINFRA_API_KEY"
	// BaseURLEnvVar — переменная окружения с базой API.
	BaseURLEnvVar = "MAILINFRA_BASE_URL"
)

Переменные окружения, из которых клиент берёт настройки, если их не передали явно.

View Source
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
	// MaxBatchSize — потолок числа писем в одном батче.
	MaxBatchSize = 100
	// IdempotencyKeyMaxLength — потолок длины Idempotency-Key (бэкенд
	// отклоняет длиннее с 422).
	IdempotencyKeyMaxLength = 255
)

Ограничения запроса, зафиксированные в https://docs.mailinfra.ru/openapi/mailinfra-public.json.

Проверяем их на клиенте, чтобы не тратить запрос: API вернул бы 422, но батч с вложениями уезжает целиком (до ~25 МиБ на письмо) только чтобы вернуться с ошибкой. Совпадение констант со спекой проверяет spec_parity_test.go.

View Source
const (
	// WebhookSignatureHeader — заголовок с подписью доставки.
	WebhookSignatureHeader = "X-MailInfra-Signature"
	// WebhookEventIDHeader — заголовок с идентификатором события; удобен для
	// дедупликации доставок на вашей стороне.
	WebhookEventIDHeader = "X-MailInfra-Event-Id"
	// DefaultWebhookMaxAge — допустимый возраст подписи.
	//
	// Бэкенд отклоняет подписи старше 5 минут (SIGNATURE_MAX_AGE_SEC), держим
	// то же окно, чтобы поведение получателя совпадало с ожиданиями отправителя.
	DefaultWebhookMaxAge = 5 * time.Minute
)
View Source
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).

View Source
const DefaultBaseURL = "https://app.mailinfra.ru/v1"

DefaultBaseURL — продакшен-база Public API.

View Source
const DefaultTimeout = 30 * time.Second

DefaultTimeout — таймаут одного запроса, если вызывающий не задал свой *http.Client.

View Source
const Version = "0.1.0"

Version — версия SDK; уезжает в User-Agent каждого запроса.

Тег релиза в github.com/g7AzaZLO/mailinfra-go берётся отсюда: его ставит sdk/scripts/release-mirror.sh в монорепозитории MailInfra.

Variables

View Source
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 (запрещено чтение).
	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-статусу ответа.

View Source
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: не приняты юридические документы")
)

Часовые ошибки конкретных причин — по `error.code` из ответа.

View Source
var (
	// ErrInvalidRequest — параметры вызова не проходят ограничения спеки.
	ErrInvalidRequest = errors.New("mailinfra: некорректные параметры запроса")
	// ErrMalformedResponse — ответ успешен, но не по контракту.
	ErrMalformedResponse = errors.New("mailinfra: ответ API не по контракту")
)

Часовые ошибки клиентской валидации — запрос до сети не доходит.

View Source
var ErrWebhookSignature = errors.New("mailinfra: подпись вебхука не прошла проверку")

ErrWebhookSignature — часовая ошибка для errors.Is: подпись не прошла проверку. Конкретная причина — в поле Reason у *WebhookSignatureError.

Functions

func SignWebhookPayload

func SignWebhookPayload(body []byte, secret string, timestamp int64) string

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
}

Attachment — вложение письма.

Передайте Content сырыми байтами, и SDK сам закодирует их в base64; если base64 уже готов — положите его в ContentBase64. Указывать оба поля нельзя.

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"
	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, Events.
	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)
}

func New

func New(opts ...Option) (*Client, error)

New создаёт клиент.

Без WithAPIKey ключ берётся из переменной окружения MAILINFRA_API_KEY. Ошибка возвращается сразу, если ключа нет или его префикс не распознан: упасть при создании клиента лучше, чем получить 401 на первой отправке.

func (*Client) AutoIdempotency

func (c *Client) AutoIdempotency() bool

AutoIdempotency сообщает, ставит ли клиент Idempotency-Key сам.

func (*Client) BaseURL

func (c *Client) BaseURL() string

BaseURL возвращает базу API без завершающего слеша.

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"`
	CreatedAt         time.Time          `json:"created_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 — письмо с телом, заголовками и диагностикой доставки.

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 (
	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"
)

Значения 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"`
	CreatedAt         time.Time          `json:"created_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 — письмо в списке.

type EmailSendResponseData

type EmailSendResponseData struct {
	// ID — идентификатор письма в MailInfra.
	ID string `json:"id"`
	// Status — статус сразу после приёма: QUEUED либо 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 (
	// 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"
)

Значения EmailStatus.

type EmailsService

type EmailsService struct {
	// contains filtered or unexported fields
}

EmailsService — письма проекта. Доступен как client.Emails.

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) ([]EmailListItem, error)

List возвращает письма проекта по фильтрам.

Example
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 — интервал по времени создания письма, а не адреса.
	emails, 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(emails))
}

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("письмо уже было отправлено раньше, дубля нет")
	}
}

func (*EmailsService) SendBatch

func (s *EmailsService) SendBatch(ctx context.Context, emails []SendParams) (*BatchSendResponseData, error)

SendBatch отправляет до 100 писем одним запросом.

Батч возвращает 202, даже если часть писем не принята: проверяйте Error у каждого элемента Results, а не только отсутствие ошибки вызова. Idempotency-Key этот эндпоинт не поддерживает, поэтому SDK не повторяет его после 5xx, таймаута и обрыва соединения — повтор мог бы отправить часть писем дважды. Повторяется только 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)
		}
	}
}

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)
	}
}

func (*Error) Error

func (e *Error) Error() string

Error возвращает текст ошибки с машинным кодом, статусом и request_id — всем, что нужно для обращения в поддержку.

func (*Error) Fields

func (e *Error) Fields() []ValidationField

Fields возвращает пер-полевые причины отказа валидации (422 с кодом request_validation_failed). Пустой срез, если детализации нет.

func (*Error) Unwrap

func (e *Error) Unwrap() []error

Unwrap отдаёт часовые ошибки, по которым работает errors.Is: категорию статуса и, если код известен, конкретную причину.

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
}

ListParams — фильтры выборки писем. Нулевое значение поля означает «без фильтра».

type Option

type Option func(*options)

Option — настройка клиента, передаётся в New.

func WithAPIKey

func WithAPIKey(apiKey string) Option

WithAPIKey задаёт API-ключ проекта. По умолчанию ключ берётся из MAILINFRA_API_KEY.

func WithAutoIdempotency

func WithAutoIdempotency(enabled bool) Option

WithAutoIdempotency включает или выключает автоматический Idempotency-Key на отправке письма. По умолчанию включён.

Выключать стоит только если ключ вы генерируете сами: без него бэкенду нечем распознать повтор, и SDK перестаёт ретраить отправку после 5xx и таймаутов.

func WithBaseURL

func WithBaseURL(baseURL string) Option

WithBaseURL задаёт базу API. По умолчанию — MAILINFRA_BASE_URL либо DefaultBaseURL.

func WithHTTPClient

func WithHTTPClient(httpClient *http.Client) Option

WithHTTPClient подставляет свой *http.Client — прокси, пул соединений, mTLS.

Таймаут такого клиента SDK не перетирает: без явного WithTimeout собственный дедлайн не ставится вовсе, и запрос ограничивают только ваши настройки.

func WithMaxRetries

func WithMaxRetries(maxRetries int) Option

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

func WithTimeout(timeout time.Duration) Option

WithTimeout задаёт таймаут одной попытки. 0 — не ставить собственный дедлайн (полезно, если временем управляет ваш http.Client или контекст вызова).

По умолчанию — DefaultTimeout для своего клиента и 0 для переданного через WithHTTPClient.

func WithUserAgentSuffix

func WithUserAgentSuffix(suffix string) Option

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
	// Template — слаг шаблона вместо HTML/Text.
	Template string
	// Variables — переменные шаблона.
	Variables map[string]any
	// Attachments — вложения.
	Attachments []Attachment

	// IdempotencyKey — свой ключ идемпотентности. Пустая строка означает
	// «сгенерировать», если у клиента включён автоматический ключ.
	IdempotencyKey string
	// WithoutIdempotencyKey отправляет письмо вообще без Idempotency-Key.
	//
	// Осознанный отказ от защиты: бэкенду будет нечем распознать повтор, и SDK
	// перестанет ретраить этот запрос после 5xx и таймаутов.
	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

type ValidationField struct {
	Field   string
	Message string
	Code    string
}

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),
	// deliverability.<инцидент> как есть (например deliverability.dns_degraded),
	// test.ping у тестовой отправки из дашборда.
	Type string `json:"type"`
	// CreatedAt — момент события на стороне MailInfra.
	CreatedAt time.Time `json:"created_at"`
	// Data — полезная нагрузка. У событий письма Data["email"] — письмо,
	// 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())
		case "email.delivered":
			log.Println("доставлено:", event.EmailID())
		}
		w.WriteHeader(http.StatusOK)
	})
}

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) 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL