Documentation
¶
Overview ¶
Package postmarkwebhook authenticates Postmark webhook requests using HTTP Basic Auth and provides credential-free typed notification definitions. New receives outbound notifications; NewInbound receives parsed inbound email on a separate route. Neither constructor performs network I/O or needs an API token. The application owns bindings, body limits, durable acceptance and deduplication through core/http/webhook.
HTTPS is required at the public endpoint. Basic Auth does not sign payloads or provide freshness/replay protection; protect credentials and allowlist Postmark IP ranges at a trusted edge where possible. Forward Authorization unchanged through trusted TLS-terminating proxies. Sender claims in received email remain untrusted, regardless of endpoint authentication. New and NewInbound reject empty credentials, control characters, and a colon in the username with webhook.ErrInvalid. Valid password bytes are not trimmed.
Example:
source, err := postmarkwebhook.New("webhooks", "endpoint-secret")
if err != nil {
return err
}
handler, err := webhook.Handler(source, func(receiver *webhook.Builder[postmarkwebhook.Envelope]) {
receiver.Handle(postmarkwebhook.Delivered, func(ctx context.Context, delivery webhook.Delivery[postmarkwebhook.Delivery]) error {
return nil // Persist or durably enqueue before returning nil.
})
})
if err != nil {
return err
}
_ = handler
Index ¶
Constants ¶
This section is empty.
Variables ¶
var Bounced = webhook.NewEvent("Bounce", decode[Bounce])
Bounced decodes delivery failures, including the provider's bounce category. Not every bounce makes a recipient inactive; inspect Inactive explicitly. Example: receiver.Handle(postmarkwebhook.Bounced, acceptBounce)
var Clicked = webhook.NewEvent("Click", decode[Tracking])
Clicked decodes link tracking, which can also be generated by automated scanners. Repeated clicks for one message are distinct notifications. Example: receiver.Handle(postmarkwebhook.Clicked, acceptClick)
var Delivered = webhook.NewEvent("Delivery", decode[Delivery])
Delivered decodes successful delivery notifications without credentials. Delivery means acceptance by the receiving mail server, not by a human. Example: receiver.Handle(postmarkwebhook.Delivered, acceptDelivery)
var Inbound = webhook.NewEvent("Inbound", decode[Email])
Inbound decodes received email on a source created by NewInbound. Sender addresses and content are untrusted even after endpoint authentication. Example: receiver.Handle(postmarkwebhook.Inbound, acceptEmail)
var Opened = webhook.NewEvent("Open", decode[Tracking])
Opened decodes tracking observations, not proof that a human read the email. Repeated opens for one message are distinct notifications. Example: receiver.Handle(postmarkwebhook.Opened, acceptOpen)
var SpamComplaint = webhook.NewEvent("SpamComplaint", decode[Bounce])
SpamComplaint decodes abuse reports using the same fields as Bounce. These reports can include sensitive recipient and diagnostic information. Example: receiver.Handle(postmarkwebhook.SpamComplaint, acceptComplaint)
var SubscriptionChanged = webhook.NewEvent("SubscriptionChange", decode[Subscription])
SubscriptionChanged decodes recipient suppression changes, including removals. SuppressSending determines the resulting state, not the event name alone. Example: receiver.Handle(postmarkwebhook.SubscriptionChanged, acceptChange)
Functions ¶
func New ¶
New authenticates outbound notifications using endpoint-specific Basic Auth. It performs no I/O. Configure these credentials on the Postmark webhook URL, not with a Postmark API token. HTTPS is mandatory at the public endpoint; TLS may terminate at a trusted proxy. Basic Auth neither signs the body nor prevents credential-holder replay. Apply IP allowlisting at the trusted edge. Missing or incorrect credentials wrap webhook.ErrRejected; malformed authenticated JSON produces a retryable processing failure. Empty credentials, control characters, or a colon in username return webhook.ErrInvalid at construction. Credentials are not trimmed or logged.
Delivery IDs use X-PM-Webhook-Trace-Id, documented as stable across retries. Without that header the ID is empty, disabling deduplication rather than incorrectly collapsing separate opens/clicks sharing a MessageID.
Example:
source, err := postmarkwebhook.New("webhooks", "endpoint-secret")
if err != nil {
return err
}
_ = source.Err()
func NewInbound ¶
NewInbound authenticates parsed inbound email, whose JSON has no RecordType. Use a separate route from New; every accepted envelope binds to Inbound. The same HTTPS and Basic Auth limitations as New apply. Delivery IDs use the inbound MessageID, which identifies the received email across retries. Construction applies the same credential format checks as New.
Example:
source, err := postmarkwebhook.NewInbound("inbound", "endpoint-secret")
if err != nil {
return err
}
_ = source.Err()
Types ¶
type Bounce ¶
type Bounce struct {
// Message supplies shared context; Email is the affected address here.
// Example: _ = notification.MessageID
Message
// ID is the provider's numeric bounce or complaint identifier.
// Example: _ = notification.ID
ID int64 `json:"ID"`
// Type is the provider category, such as HardBounce or SpamComplaint.
// Example: _ = notification.Type
Type string `json:"Type"`
// Email is the affected recipient address.
// Example: _ = notification.Email
Email string `json:"Email"`
// Description is the provider's explanation, not a client-safe error.
// Example: _ = notification.Description
Description string `json:"Description"`
// BouncedAt is the provider's event time for bounces and complaints.
// Example: _ = notification.BouncedAt.UTC()
BouncedAt time.Time `json:"BouncedAt"`
// Inactive reports whether Postmark deactivated the address.
// Example: _ = notification.Inactive
Inactive bool `json:"Inactive"`
}
Bounce describes a bounce or spam complaint. Extra provider fields are ignored; use Envelope with a custom event if full raw diagnostics are required. Example: _ = postmarkwebhook.Bounce{Inactive: true}
type Delivery ¶
type Delivery struct {
// Message supplies shared message context.
// Example: _ = notification.MessageID
Message
// DeliveredAt is the provider's acceptance time, decoded from ISO 8601.
// Example: _ = notification.DeliveredAt.UTC()
DeliveredAt time.Time `json:"DeliveredAt"`
// Details contains the remote mail server's response and may be sensitive.
// Example: _ = notification.Details
Details string `json:"Details"`
}
Delivery reports mail-server acceptance with its timestamp and SMTP response. It does not promise inbox placement or reading. Example: _ = postmarkwebhook.Delivery{}.DeliveredAt
type Email ¶
type Email struct {
// MessageID identifies the received email across processing retries.
// Example: _ = notification.MessageID
MessageID string `json:"MessageID"`
// From is the sender's address claim, not an authenticated application user.
// Example: _ = notification.From
From string `json:"From"`
// To contains the formatted recipient list.
// Example: _ = notification.To
To string `json:"To"`
// Subject is untrusted message subject text.
// Example: _ = notification.Subject
Subject string `json:"Subject"`
// Date retains the sender-selected timestamp format unchanged.
// Example: _ = notification.Date
Date string `json:"Date"`
// TextBody is the plain text content, possibly empty.
// Example: _ = notification.TextBody
TextBody string `json:"TextBody"`
// HTMLBody is unsanitized HTML; never render it without sanitization.
// Example: _ = notification.HTMLBody
HTMLBody string `json:"HtmlBody"`
// StrippedTextReply is Postmark's extracted reply text when available.
// Example: _ = notification.StrippedTextReply
StrippedTextReply string `json:"StrippedTextReply"`
}
Email contains parsed inbound email. Date remains a string because the sending mail server chooses its format. HTML, sender claims and attachments are untrusted. Attachments and full address structures remain accessible through Envelope. Example: _ = postmarkwebhook.Email{Subject: "Support request"}
type Envelope ¶
type Envelope struct {
// RecordType selects the native event binding; inbound email uses "Inbound".
// Example: _ = postmarkwebhook.Envelope{RecordType: "Open"}
RecordType string
// Body owns the original JSON, decoded only when the event is bound.
// Example: _ = json.Unmarshal(envelope.Body, ¬ification)
Body jsontext.Value
}
Envelope retains an authenticated Postmark notification for a bound decoder. Body contains personal data and must not be logged. Unknown fields are retained.
Example: _ = postmarkwebhook.Envelope{RecordType: "Delivery"}
type Message ¶
type Message struct {
// MessageID identifies the email, not an individual open or click.
// Example: _ = notification.MessageID
MessageID string `json:"MessageID"`
// MessageStream is the stream configured in Postmark.
// Example: _ = notification.MessageStream
MessageStream string `json:"MessageStream"`
// Recipient is the destination address for delivery and tracking events.
// Example: _ = notification.Recipient
Recipient string `json:"Recipient"`
// Tag is the application's optional message label.
// Example: _ = notification.Tag
Tag string `json:"Tag"`
// Metadata carries application-supplied string attributes.
// Example: _ = notification.Metadata["order"]
Metadata map[string]string `json:"Metadata"`
// ServerID identifies the server when supplied by the event type.
// Example: _ = notification.ServerID
ServerID int64 `json:"ServerID"`
}
Message contains shared outbound message context. Fields omitted by Postmark retain zero values; MessageID is not a unique tracking-event identity. Example: _ = postmarkwebhook.Message{MessageID: "message-id"}
type Subscription ¶
type Subscription struct {
// Message supplies stream and recipient context.
// Example: _ = notification.Recipient
Message
// ChangedAt is when the suppression changed at Postmark.
// Example: _ = notification.ChangedAt.UTC()
ChangedAt time.Time `json:"ChangedAt"`
// SuppressSending is the resulting suppression state.
// Example: _ = notification.SuppressSending
SuppressSending bool `json:"SuppressSending"`
// Origin identifies what initiated the change, such as Recipient.
// Example: _ = notification.Origin
Origin string `json:"Origin"`
// SuppressionReason contains the provider reason, when supplied.
// Example: _ = notification.SuppressionReason
SuppressionReason string `json:"SuppressionReason"`
}
Subscription reports a suppression-state change. MessageID can be absent for changes not associated with a message; do not require it for routing. Example: _ = postmarkwebhook.Subscription{SuppressSending: true}
type Tracking ¶
type Tracking struct {
// Message supplies shared message context.
// Example: _ = notification.MessageID
Message
// ReceivedAt is the provider's observation time, not local receipt time.
// Example: _ = notification.ReceivedAt.UTC()
ReceivedAt time.Time `json:"ReceivedAt"`
// FirstOpen distinguishes the first recorded open for the message.
// Example: _ = notification.FirstOpen
FirstOpen bool `json:"FirstOpen"`
// OriginalLink is the original clicked URL and may contain private tokens.
// Example: _ = notification.OriginalLink
OriginalLink string `json:"OriginalLink"`
// ClickLocation is the provider's body location, such as HTML.
// Example: _ = notification.ClickLocation
ClickLocation string `json:"ClickLocation"`
// UserAgent describes the opener or clicker and is untrusted input.
// Example: _ = notification.UserAgent
UserAgent string `json:"UserAgent"`
}
Tracking contains useful open and click fields. Click-only fields remain empty for opens; FirstOpen is only meaningful for open events. Example: _ = postmarkwebhook.Tracking{OriginalLink: "https://example.com"}