Help us shape the future of Go! Take the 2026 Go Developer Survey

postmarkmail

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 11 Imported by: 0

README

Postmark Mail Driver

arkoren.dev/driver/postmark/postmarkmail implements mail.Driver with Postmark's HTTP API; Postmark has no maintained official Go SDK. New(token, configure ...func(*Builder)) requires a server token and can override the endpoint for tests. The driver maps text and HTML bodies, To/CC/BCC, Reply-To, regular base64 attachments, and inline Content-ID attachments. A successful submission returns Postmark's MessageID as mail.Receipt.ID.

For inline images, set the portable attachment's ContentID to a bare ID such as logo and reference it as <img src="cid:logo"> in HTML. The driver adds cid: to Postmark's JSON ContentID without changing your message, matching the Postmark inline-image format.

Borrow an HTTP client with an explicit timeout:

import (
    "net/http"
    "os"
    "time"

    "arkoren.dev/driver/postmark/postmarkmail"
)

driver, err := postmarkmail.New(os.Getenv("POSTMARK_TOKEN"), func(transport *postmarkmail.Builder) {
	transport.HTTPClient(&http.Client{Timeout: 10 * time.Second})
})

Pass driver to mail.New. Callbacks run once, synchronously in order on one fresh draft before validation. Nil entries panic; expanded nil or empty slices omit edits. Publication copies settings and borrows the HTTP client. Each send uses a shallow client copy to disable redirects without modifying or closing your client. Its transport and cookie jar remain shared and under your ownership; configure them before concurrent use. Empty Endpoint restores the production URL; nil HTTPClient restores a fresh client with a 30-second timeout. No NewFrom or SDK accessor exists.

The driver never follows redirects, including same-origin redirects, even when your HTTP client's CheckRedirect permits them. That callback is not invoked. A redirect returns the original status and up to 4096 response-body bytes as a submission error, with an empty receipt. Set Builder.Endpoint explicitly to a trusted final URL rather than relying on a redirect to move credentials or replay the message. Custom transports are trusted code: they receive the token and mail contents and must not follow redirects internally.

Value formatting (%v, %+v, %#v) prints only postmarkmail.Driver; it does not expose the server token, endpoint, or HTTP client internals. This does not make provider errors or message contents safe to log.

Install with go get arkoren.dev/driver/postmark and check the constructor error before creating the mailer. Keep custom endpoints trusted because the server token is sent there. Provider errors can contain personal data; redact them before logging. Success is acceptance, not delivery. See Postmark setup and tasks for outbound notifications and parsed inbound email using separate Basic Auth credentials with postmarkwebhook.

Documentation

Overview

Package postmarkmail provides a Postmark mail.Driver.

New requires a server token and accepts Builder callbacks for the send-email endpoint and borrowed HTTP client. Callbacks run directly once in order before validation; nil entries panic. Publication detaches the draft's settings, not the live HTTP client or transport. No request is made until Send, and there are no lifecycle hooks or native SDK accessors.

Send never follows redirects, including same-origin redirects, and returns a submission error with an empty receipt for responses with status 300 or higher. A per-send shallow client copy enforces that policy without modifying or closing a supplied client. Its transport and cookie jar remain shared. Trust custom endpoints and transports with both the server token and message contents. Inline attachments keep bare ContentID values in the portable message; Send adds Postmark's required cid: prefix on the wire. Driver value formatting suppresses credentials, but does not sanitize provider errors or message data.

Example:

driver, err := postmarkmail.New("server-token", func(transport *postmarkmail.Builder) {
	transport.HTTPClient(&http.Client{Timeout: 10 * time.Second})
})
if err != nil {
	panic(err)
}
_ = driver // Pass to mail.New to submit portable messages.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Builder

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

Builder edits a Postmark construction draft. The zero value selects the production endpoint and a client with a 30-second timeout. New copies settings after all callbacks; supplied HTTP clients remain borrowed live dependencies.

Example:

driver, err := postmarkmail.New("server-token", func(transport *postmarkmail.Builder) {
	transport.HTTPClient(&http.Client{Timeout: 10 * time.Second})
})
if err != nil {
	panic(err)
}
_ = driver

func (*Builder) Endpoint

func (transport *Builder) Endpoint(endpoint string) *Builder

Endpoint replaces the send-email URL and returns the same draft. Empty uses https://api.postmarkapp.com/email. Like the original constructor, New does not validate a nonempty URL; malformed URLs fail when Send constructs its request.

Example:

driver, err := postmarkmail.New("server-token", func(transport *postmarkmail.Builder) {
	transport.Endpoint("https://api.postmarkapp.com/email")
})
if err != nil {
	panic(err)
}
_ = driver

func (*Builder) HTTPClient

func (transport *Builder) HTTPClient(client *http.Client) *Builder

HTTPClient replaces the borrowed request client and returns the same draft. Nil restores a fresh client with a 30-second timeout. Send reads the client's current settings into a shallow copy and replaces only CheckRedirect: no redirects are followed and the supplied callback is not invoked. The caller's client is not modified or closed. Its transport and cookie jar remain shared, not cloned or closed; their ownership remains with the caller. Configure the client before concurrent use and trust its transport with credentials and mail.

Example:

client := &http.Client{Timeout: 10 * time.Second}
driver, err := postmarkmail.New("server-token", func(transport *postmarkmail.Builder) {
	transport.HTTPClient(client)
})
if err != nil {
	panic(err)
}
mailer := mail.New(driver)
_ = mailer // The client remains yours to manage.
Example

ExampleBuilder_HTTPClient constructs a mailer with a borrowed HTTP client. Construction makes no request and does not transfer ownership of the client or its transport to the driver.

client := &http.Client{Timeout: 10 * time.Second}
driver, err := New("server-token", func(transport *Builder) {
	transport.HTTPClient(client)
})

if err != nil {
	fmt.Println(err)

	return
}

mailer := mail.New(driver)
fmt.Println(mailer.Driver() == driver)
Output:
true

type BuilderFunc

type BuilderFunc = func(*Builder)

BuilderFunc edits one Postmark construction draft. New runs callbacks once in order before validation and copies completed settings; nil entries panic. The alias accepts ordinary functions and function slices without conversion.

Example:

var timeout postmarkmail.BuilderFunc = func(transport *postmarkmail.Builder) {
	transport.HTTPClient(&http.Client{Timeout: 10 * time.Second})
}
driver, err := postmarkmail.New("server-token", timeout)
if err != nil {
	panic(err)
}
_ = mail.New(driver)

type Driver

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

Driver submits mail.Message values through the Postmark API. Each Send applies the no-redirect policy without modifying a borrowed client. Format hides credentials and connection settings; a zero Driver is not usable.

Example:

driver, err := postmarkmail.New("server-token")
if err != nil {
	panic(err)
}
_ = mail.New(driver)

func New

func New(token string, configure ...BuilderFunc) (*Driver, error)

New creates a Postmark Driver authenticating with token.

token is the Postmark server token, sent as X-Postmark-Server-Token. Callbacks apply Builder.Endpoint and Builder.HTTPClient. No request is made until Driver.Send.

Callbacks run directly once, synchronously in order on one fresh draft before validation. Nil entries panic; expanded nil or empty slices omit edits. Publication copies settings, not the supplied live HTTP client or its transport.

Send never follows redirects, including same-origin redirects, so a redirect cannot forward the server token or replay the message. Responses with status 300 or higher return a submission error and an empty receipt. Each send uses a shallow client copy with its own redirect policy, ignoring any supplied CheckRedirect callback without modifying or closing the caller's client. The transport and cookie jar remain shared; configure them before use and trust custom transports with the token and message. A custom endpoint must be the trusted final URL, not a redirecting URL.

New returns an error when token is empty.

Example:

driver, err := postmarkmail.New("server-token")
if err != nil {
	panic(err)
}
mailer := mail.New(driver)
_ = mailer // Use Send with a request context and an immutable message.
Example

ExampleNew constructs the default driver and mail facade without network I/O. A synthetic token is sufficient until Send; production callers supply their own server token and handle construction errors before using the mailer.

package main

import (
	"fmt"

	"arkoren.dev/core/service/mail"
	"arkoren.dev/driver/postmark/postmarkmail"
)

func main() {
	driver, err := postmarkmail.New("server-token")

	if err != nil {
		fmt.Println(err)

		return
	}

	mailer := mail.New(driver)
	fmt.Println(mailer.Driver() == driver)
}
Output:
true

func (Driver) Format

func (driver Driver) Format(state fmt.State, _ rune)

Format replaces fmt's value output with a fixed type label, ignoring the supplied verb and flags, so reflection cannot disclose the server token. Its value receiver protects pointers and copied values without formatting endpoints or client internals. The formatter has no error return, so fmt-owned writer errors cannot propagate. The fmt package still handles type (%T) and pointer (%p) verbs itself.

Example:

driver, err := postmarkmail.New("server-token")
if err != nil {
	panic(err)
}
text := fmt.Sprintf("%+v", driver) // "postmarkmail.Driver"
_ = text
Example

ExampleDriver_Format demonstrates the public formatting protocol's credential boundary without constructing a request or revealing the synthetic token.

package main

import (
	"fmt"

	"arkoren.dev/driver/postmark/postmarkmail"
)

func main() {
	driver, err := postmarkmail.New("server-token")

	if err != nil {
		fmt.Println(err)

		return
	}

	fmt.Printf("%#v\n", driver)
}
Output:
postmarkmail.Driver

func (*Driver) Send

func (driver *Driver) Send(ctx context.Context, message mail.Message) (mail.Receipt, error)

Send validates an immutable mail.Message and submits its snapshot through Postmark. The message must include a sender. Validation, encoding, HTTP, and provider failures return an empty receipt without changing the message.

Example using a configured driver and request context:

message := mail.NewMessage(mail.Recipients{
	To: []mail.Address{{Email: "person@example.com"}},
}, mail.Body{Text: "Hello."}, func(letter *mail.MessageBuilder) {
	letter.From(mail.Address{Email: "sender@example.com"})
})
receipt, err := driver.Send(ctx, message)
if err != nil {
	return err
}
_ = receipt.ID

Jump to

Keyboard shortcuts

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