Documentation
¶
Overview ¶
Small formatting helpers matched to the API's own parsing rules (docs/api.md in the duva repository).
Attachment helpers. The server remains the authority on the limits below (they can change): these are a courtesy, so a mistake fails locally instead of after an upload.
Package duva is the official Duva client library. Duva is a transactional email API hosted in Canada. See https://duva.ca and https://duva.ca/en/docs for the full HTTP reference this package wraps.
client, err := duva.New("dv_...", "example.com") // or DUVA_API_KEY / DUVA_DOMAIN
message, err := client.Messages.Send(ctx, duva.SendMessageParams{
From: "Example <notifications@example.com>",
To: []string{"client@example.org"},
Subject: "Your order",
Text: "Thank you for your order.",
})
TestTransport: a fake Doer and a response builder, for testing YOUR OWN application without a real network call. Pass it via WithHTTPClient when constructing a Client in your own tests.
Transport: builds requests, applies the retry policy of docs/bibliotheques-clientes.md section 3.5 (in the duva repository) exactly, and turns a Duva error response into the right typed error.
Webhook signature verification, "Standard Webhooks" format (see docs/api.md "Webhooks" in the duva repository). Duva SENDS webhooks; this file is for VERIFYING them on your side.
Index ¶
- Constants
- func ErrorCode(err error) (string, bool)
- func FormatAddress(address, name string) string
- func JSONResponse(status int, body any) *http.Response
- func SignWebhookRequest(secret, eventID string, body []byte, timestamp time.Time) http.Header
- func UnsubscribeHeaders(opts UnsubscribeOptions) (http.Header, error)
- func VerifyWebhookSignature(secrets []string, headers http.Header, rawBody []byte, opts *VerifyOptions) bool
- type APIError
- type Attachment
- type AttachmentOption
- type AuthenticationError
- type Client
- type ConflictError
- type ConnectionError
- type CreateWebhookParams
- type Doer
- type Event
- type EventPage
- type EventType
- type EventsService
- type GetStatsParams
- type HealthStatus
- type ListEventsParams
- type ListSuppressionsParams
- type Message
- type MessageAccepted
- type MessageAcceptedStatus
- type MessageStatus
- type MessagesService
- type NotFoundError
- type Option
- type PayloadTooLargeError
- type PermissionError
- type QuotaExceededError
- type RateLimitError
- type Recipient
- type RecipientStatus
- type RecordedRequest
- type SendMessageParams
- type SendMessageResult
- type ServerError
- type Stats
- type StatsGranularity
- type StatsPeriod
- type StatsService
- type Suppression
- type SuppressionPage
- type SuppressionReason
- type SuppressionsService
- func (s *SuppressionsService) Add(ctx context.Context, email string) (suppression *Suppression, created bool, err error)
- func (s *SuppressionsService) List(ctx context.Context, params ListSuppressionsParams) (*SuppressionPage, error)
- func (s *SuppressionsService) ListAll(ctx context.Context, params ListSuppressionsParams, maxItems int) iter.Seq2[*Suppression, error]
- func (s *SuppressionsService) Remove(ctx context.Context, email string) error
- type TestTransport
- type TimeoutError
- type Tracking
- type UnsubscribeOptions
- type ValidationError
- type ValidationField
- type VerifyOptions
- type Webhook
- type WebhookDelivery
- type WebhookDeliveryList
- type WebhookDeliveryStatus
- type WebhookEvent
- type WebhookList
- type WebhookSignatureError
- type WebhookStatus
- type WebhooksService
- func (s *WebhooksService) Create(ctx context.Context, params CreateWebhookParams) (*Webhook, error)
- func (s *WebhooksService) Delete(ctx context.Context, id string) error
- func (s *WebhooksService) Deliveries(ctx context.Context, id string, limit int) ([]WebhookDelivery, error)
- func (s *WebhooksService) Get(ctx context.Context, id string) (*Webhook, error)
- func (s *WebhooksService) List(ctx context.Context) ([]Webhook, error)
Constants ¶
const ( MaxAttachments = 10 MaxAttachmentTotalBytes = 5 * 1024 * 1024 )
Mirrors services/messages.py in the duva repository at the time of writing: 10 attachments, 5 MB decoded in total, these extensions refused. Re-check against docs/api.md if this drifts.
Variables ¶
This section is empty.
Functions ¶
func ErrorCode ¶
ErrorCode returns err's error.code when err is one of this package's typed API errors (see APIError), and whether it was found. Useful to branch on the code without an errors.As for every possible type.
func FormatAddress ¶
FormatAddress returns "Name <address>" (Name quoted if it contains a `"` or `,`), or just address when name is empty.
func JSONResponse ¶
JSONResponse builds an *http.Response for a TestTransport handler: status, with body encoded as JSON.
func SignWebhookRequest ¶
SignWebhookRequest builds a validly signed request FOR YOUR OWN TESTS: the headers a real Duva webhook delivery would carry for body, signed with secret as of timestamp (the zero value means now). Never used by this package to send anything: Duva is the only real sender.
func UnsubscribeHeaders ¶
func UnsubscribeHeaders(opts UnsubscribeOptions) (http.Header, error)
UnsubscribeHeaders builds List-Unsubscribe (and List-Unsubscribe-Post for one-click) exactly as the API validates them: at most 3 links, https:// or mailto: only.
func VerifyWebhookSignature ¶
func VerifyWebhookSignature(secrets []string, headers http.Header, rawBody []byte, opts *VerifyOptions) bool
VerifyWebhookSignature verifies a webhook request. secrets accepts one or several (for key rotation: while both the old and the new secret are active, a webhook signed with either must verify).
headers is looked up case-insensitively (http.Header always is). rawBody must be the EXACT bytes Duva sent: re-encoding a parsed-then-re-serialized JSON body changes its bytes and invalidates every signature (see your framework's raw-body option, documented per-framework in docs/api.md).
Types ¶
type APIError ¶
type APIError struct {
StatusCode int
Code string
Message string
Fields []ValidationField
RawBody string // bounded to 4 KB; never your API key
}
APIError is the common shape of every error Duva actually answered (as opposed to a network failure: see *ConnectionError and *TimeoutError). Every typed error below embeds one: use errors.As with the specific type (e.g. *NotFoundError) to match it, or compare .Code directly for a code this package doesn't have a dedicated type for yet.
type Attachment ¶
type Attachment = generated.Attachment
Attachment is both the wire format Messages.Send sends and what NewAttachmentFromBytes / NewAttachmentFromFile build; see attachment.go.
func NewAttachmentFromBytes ¶
func NewAttachmentFromBytes(filename string, content []byte, opts ...AttachmentOption) (Attachment, error)
NewAttachmentFromBytes builds an Attachment from bytes already in memory.
func NewAttachmentFromFile ¶
func NewAttachmentFromFile(path string, opts ...AttachmentOption) (Attachment, error)
NewAttachmentFromFile reads a file from disk.
type AttachmentOption ¶
type AttachmentOption func(*Attachment)
AttachmentOption sets an optional field on an Attachment built by NewAttachmentFromBytes or NewAttachmentFromFile.
func WithContentID ¶
func WithContentID(contentID string) AttachmentOption
WithContentID turns the attachment into an inline image the HTML body references with cid:.
func WithContentType ¶
func WithContentType(contentType string) AttachmentOption
WithContentType sets the attachment's MIME type (guessed by the server when omitted).
type AuthenticationError ¶
type AuthenticationError struct{ *APIError }
AuthenticationError: 401 unauthorized.
type Client ¶
type Client struct {
Messages *MessagesService
Events *EventsService
Suppressions *SuppressionsService
Webhooks *WebhooksService
Stats *StatsService
// contains filtered or unexported fields
}
Client is bound to one domain and its API key (an API key only ever opens one domain).
client, err := duva.New("dv_...", "example.com") // or DUVA_API_KEY / DUVA_DOMAIN
type ConflictError ¶
type ConflictError struct{ *APIError }
ConflictError: 409 idempotency_conflict or limit_reached.
type ConnectionError ¶
type ConnectionError struct{ Err error }
ConnectionError: no response was received at all (DNS, TLS, connection refused, reset...).
func (*ConnectionError) Error ¶
func (e *ConnectionError) Error() string
func (*ConnectionError) Unwrap ¶
func (e *ConnectionError) Unwrap() error
type CreateWebhookParams ¶
CreateWebhookParams: URL is required.
type Doer ¶
Doer is the minimal HTTP client interface this package needs: *http.Client satisfies it, and so does any test double or wrapper (proxying, logging, mTLS). See TestTransport for testing your own application without a real network call.
type Event ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type EventPage ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type EventType ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type EventsService ¶
type EventsService struct {
// contains filtered or unexported fields
}
EventsService is Client.Events.
func (*EventsService) List ¶
func (s *EventsService) List(ctx context.Context, params ListEventsParams) (*EventPage, error)
List reads one page.
func (*EventsService) ListAll ¶
func (s *EventsService) ListAll(ctx context.Context, params ListEventsParams, maxItems int) iter.Seq2[*Event, error]
ListAll follows next_cursor across every page, never loading them all into memory at once. maxItems <= 0 means unlimited. The cursor stays opaque: never built or interpreted here.
for event, err := range client.Events.ListAll(ctx, duva.ListEventsParams{}, 0) {
if err != nil { ... }
}
type GetStatsParams ¶
GetStatsParams: Granularity is "day" (default) or "hour"; periods are in UTC.
type HealthStatus ¶
type HealthStatus struct {
Status string `json:"status"`
}
HealthStatus is what Health returns.
type ListEventsParams ¶
type ListEventsParams struct {
MessageID string
Type string
Recipient string
Since *time.Time
Limit int
Cursor string
}
ListEventsParams filters one page of List / ListAll.
type ListSuppressionsParams ¶
ListSuppressionsParams filters one page of List / ListAll.
type Message ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type MessageAccepted ¶
type MessageAccepted = generated.MessageAccepted
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type MessageAcceptedStatus ¶
type MessageAcceptedStatus = generated.MessageAcceptedStatus
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type MessageStatus ¶
type MessageStatus = generated.MessageStatus
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type MessagesService ¶
type MessagesService struct {
// contains filtered or unexported fields
}
MessagesService is Client.Messages.
func (*MessagesService) Send ¶
func (s *MessagesService) Send(ctx context.Context, params SendMessageParams) (*SendMessageResult, error)
Send sends a message: always asynchronous ("queued"), never returns a delivery outcome.
type Option ¶
type Option func(*config)
Option configures a Client created by New.
func WithBaseURL ¶
WithBaseURL overrides the default https://api.duva.ca (http://localhost is allowed, for tests).
func WithHTTPClient ¶
WithHTTPClient injects your own Doer (proxy, mTLS, logging, or TestTransport for tests) instead of the default *http.Client.
func WithLanguage ¶
WithLanguage sets Accept-Language ("en" or "fr"): the language of error.message.
func WithMaxRetries ¶
WithMaxRetries sets how many times a safe-to-retry call is retried after a transient failure (2 by default; 0 disables retries).
func WithMaxRetryWait ¶
WithMaxRetryWait sets the longest Retry-After a 429 rate_limited is retried for (30s by default); a longer wait raises *RateLimitError instead.
func WithTimeout ¶
WithTimeout sets the per-attempt request timeout (10s by default). Only takes effect when no WithHTTPClient is given: your own *http.Client keeps its own timeout.
func WithUserAgent ¶
WithUserAgent appends text to the User-Agent header (never replaces it), for diagnostics.
type PayloadTooLargeError ¶
type PayloadTooLargeError struct{ *APIError }
PayloadTooLargeError: 413 payload_too_large.
type PermissionError ¶
type PermissionError struct{ *APIError }
PermissionError: 403 domain_not_verified or sending_not_allowed (.Code distinguishes the two).
type QuotaExceededError ¶
QuotaExceededError: a 429 on YOUR ACCOUNT quota (daily or monthly). RetryAfter can be hours: never retried automatically, by design (see docs/bibliotheques-clientes.md section 3.5 in the duva repository).
type RateLimitError ¶
RateLimitError: a 429 from the per-key rate limit (unrelated to your sending quota). Retried automatically when RetryAfter fits within the client's WithMaxRetryWait.
type Recipient ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type RecipientStatus ¶
type RecipientStatus = generated.RecipientStatus
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type RecordedRequest ¶
type RecordedRequest struct {
Method string
Path string
Query url.Values
Header http.Header
Body []byte
}
RecordedRequest is one request TestTransport received, kept for assertions in your tests.
type SendMessageParams ¶
type SendMessageParams struct {
From string
To []string
Subject string
HTML string
Text string
Tags []string
Tracking *Tracking
ReplyTo string
Headers map[string]string
Metadata map[string]string
Attachments []Attachment
// IdempotencyKey: generated (UUID v4) when empty. 1 to 255 printable ASCII characters, no
// spaces, unique per domain, when you supply your own. A key generated per call does not
// de-duplicate BETWEEN two calls: to de-duplicate a logical send (an order), pass a stable key
// ("order-4821").
IdempotencyKey string
}
SendMessageParams: From, To and Subject are required; at least one of HTML and Text too. Nothing more is checked locally: recopying the server's own rules would drift, and it answers 422 with Fields regardless.
type SendMessageResult ¶
type SendMessageResult struct {
ID string
Status string
// Replayed is true when this answer replays an earlier identical request (the
// Idempotent-Replayed response header): a retried call with the same idempotency key never
// creates a duplicate.
Replayed bool
// Location is GET /v1/{domain}/messages/{id}'s address (the Location response header), or
// empty if absent.
Location string
}
SendMessageResult is what Messages.Send returns: the accepted message id and status, plus what the response headers say about idempotency replay and the new message's location.
type ServerError ¶
type ServerError struct{ *APIError }
ServerError: a 5xx, or a response whose body was not the documented error envelope.
type Stats ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type StatsGranularity ¶
type StatsGranularity = generated.StatsGranularity
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type StatsPeriod ¶
type StatsPeriod = generated.StatsPeriod
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type StatsService ¶
type StatsService struct {
// contains filtered or unexported fields
}
StatsService is Client.Stats.
func (*StatsService) Get ¶
func (s *StatsService) Get(ctx context.Context, params GetStatsParams) (*Stats, error)
Get reads aggregated send statistics for this domain.
type Suppression ¶
type Suppression = generated.Suppression
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type SuppressionPage ¶
type SuppressionPage = generated.SuppressionPage
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type SuppressionReason ¶
type SuppressionReason = generated.SuppressionReason
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type SuppressionsService ¶
type SuppressionsService struct {
// contains filtered or unexported fields
}
SuppressionsService is Client.Suppressions.
func (*SuppressionsService) Add ¶
func (s *SuppressionsService) Add(ctx context.Context, email string) (suppression *Suppression, created bool, err error)
Add suppresses an address. created is true on a 201 (newly added), false on a 200 (already suppressed, for this reason or another).
func (*SuppressionsService) List ¶
func (s *SuppressionsService) List(ctx context.Context, params ListSuppressionsParams) (*SuppressionPage, error)
List reads one page.
func (*SuppressionsService) ListAll ¶
func (s *SuppressionsService) ListAll(ctx context.Context, params ListSuppressionsParams, maxItems int) iter.Seq2[*Suppression, error]
ListAll follows next_cursor across every page, never loading them all into memory at once. maxItems <= 0 means unlimited.
type TestTransport ¶
type TestTransport struct {
Requests []RecordedRequest
// contains filtered or unexported fields
}
TestTransport is a fake Doer: no network call. handler decides the response; every request is recorded in Requests, in order.
func NewTestTransport ¶
NewTestTransport builds a TestTransport that answers every request with handler.
type TimeoutError ¶
type TimeoutError struct{ Err error }
TimeoutError: the request exceeded the configured timeout before any response arrived.
func (*TimeoutError) Error ¶
func (e *TimeoutError) Error() string
func (*TimeoutError) Unwrap ¶
func (e *TimeoutError) Unwrap() error
type Tracking ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type UnsubscribeOptions ¶
type UnsubscribeOptions struct {
HTTPSURL string
Mailto string
// OneClick adds List-Unsubscribe-Post (RFC 8058). Defaults to true when HTTPSURL is set,
// false otherwise.
OneClick *bool
}
UnsubscribeOptions configures UnsubscribeHeaders.
type ValidationError ¶
type ValidationError struct{ *APIError }
ValidationError: 422 invalid_request. .Fields lists the offending fields.
type ValidationField ¶
ValidationField is one entry of a *ValidationError's Fields (e.g. {Field: "to[0]", Message: "..."}).
type VerifyOptions ¶
type VerifyOptions struct {
// ToleranceSeconds: 0 means the default (300s).
ToleranceSeconds int
// Now: the current instant, for YOUR OWN tests only (see SignWebhookRequest and the fixtures
// of duva-mail/duva-conformance, which document the exact instant each vector was signed at).
// The zero value means time.Now().
Now time.Time
}
VerifyOptions configures VerifyWebhookSignature and ConstructEvent. The zero value uses the defaults (5 minute tolerance, the real current time).
type Webhook ¶
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type WebhookDelivery ¶
type WebhookDelivery = generated.WebhookDelivery
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type WebhookDeliveryList ¶
type WebhookDeliveryList = generated.WebhookDeliveryList
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type WebhookDeliveryStatus ¶
type WebhookDeliveryStatus = generated.WebhookDeliveryStatus
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type WebhookEvent ¶
type WebhookEvent struct {
ID string `json:"id"`
Type string `json:"type"`
Domain string `json:"domain"`
Data map[string]any `json:"data"`
}
WebhookEvent is the JSON body Duva sends to your webhook URL, already parsed.
func ConstructEvent ¶
func ConstructEvent(secrets []string, headers http.Header, rawBody []byte, opts *VerifyOptions) (*WebhookEvent, error)
ConstructEvent: VerifyWebhookSignature, then parses the body. Returns a *WebhookSignatureError on a bad signature rather than a boolean, for call sites that want to fail closed. Never includes the secret or the raw body in the error.
type WebhookList ¶
type WebhookList = generated.WebhookList
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type WebhookSignatureError ¶
type WebhookSignatureError struct {
// contains filtered or unexported fields
}
WebhookSignatureError: a webhook signature failed to verify. Never carries the secret or body.
func (*WebhookSignatureError) Error ¶
func (e *WebhookSignatureError) Error() string
type WebhookStatus ¶
type WebhookStatus = generated.WebhookStatus
Response types, generated from docs/openapi.json (internal/generated, never edited by hand; see scripts/generate.sh). Re-exported here so callers only ever import this package.
type WebhooksService ¶
type WebhooksService struct {
// contains filtered or unexported fields
}
WebhooksService is Client.Webhooks.
func (*WebhooksService) Create ¶
func (s *WebhooksService) Create(ctx context.Context, params CreateWebhookParams) (*Webhook, error)
Create registers a webhook endpoint. The returned Webhook's Secret is present ONLY in this response: it never comes back from List or Get. Never logged, never retried (the outcome of a timed-out first attempt is unknown).
func (*WebhooksService) Delete ¶
func (s *WebhooksService) Delete(ctx context.Context, id string) error
Delete removes a webhook endpoint. Not safe to retry.
func (*WebhooksService) Deliveries ¶
func (s *WebhooksService) Deliveries(ctx context.Context, id string, limit int) ([]WebhookDelivery, error)
Deliveries lists this webhook's delivery attempts (most recent first). limit <= 0 uses the server's default page size.