postal

package module
v1.1.1 Latest Latest
Warning

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

Go to latest
Published: Sep 16, 2026 License: MIT Imports: 17 Imported by: 0

README

Built with GoLang Version License Go Report Card

Postal

Postal is a simple package which allows you to send email messages, either via an SMTP server or using MailGun's api. Postal implements a simple worker pool to make sending messages both easy and efficient. Postal uses standard Go templates (html/template).

Installation

Install it the usual way:

go get -u github.com/tsawler/postal

Usage

To create a worker pool to send email, first define a postal.Service object with settings appropriate for your environment. This is the postal.Service type:

// Service is the type used to create a MailDispatcher.
type Service struct {
    Method        int                           // How to send the message: postal.SMTP or postal.MailGun.
    ServerURL     string                        // The URL of the server mail is sent from.
    SMTPServer    string                        // The SMTP server.
    SMTPPort      int                           // The SMTP server's port.
    SMTPUser      string                        // The username for the SMTP server.
    SMTPPassword  string                        // The password for the SMTP server.
    ErrorChan     chan error // Optional channel for results (an error, or nil on success).
    MaxWorkers    int        // Maximum number of workers in the pool.
    MaxMessages   int        // How big the buffer should be for the JobQueue.
    Domain        string     // The domain used to send mail.
    APIKey        string     // The API key for mailgun.
    SendingFromEU bool       // If using mailgun and sending from EU, set to true.
    TemplateDir   string     // Where templates are stored.

    ConnectTimeout time.Duration // Bounds the SMTP connection. Zero means 10s.
    SendTimeout    time.Duration // Bounds transmission / the Mailgun call. Zero means 10s.
    ErrorChanGrace time.Duration // How long a worker waits on an unread ErrorChan. Zero means 5s.
}

Then, create a postal.MailDispatcher by calling postal.New with your postal.Service as the parameter:

dispatcher, _ := postal.New(myService)

Create a mail message by defining a postal.MailData object. This is the type for postal.MailData:

// MailData holds all information for a given message.
type MailData struct {
	ToName       string         // The name of the recipient.
	ToAddress    string         // The email address of the recipient.
	FromName     string         // The name of the sender.
	FromAddress  string         // The email address of the sender.
	ReplyTo      string         // The reply-to email address.
	AdditionalTo []string       // Additional TO recipients.
	Subject      string         // The subject of the email message.
	Content      template.HTML  // The content of the message, as HTML.
	ContentType  string         // Optional: "text/html" (default), "text/plain" (Content sent as-is, no template), "application/xml", etc.
	Template     string         // The template to use. If not specified, will use a simple default template.
	CC           []string       // A slice of CC recipient emails.
	BCC          []string       // A slice of BCC recipient emails.
	Attachments  []string       // A slice of attachments, which must exist on disk (i.e. []string{"./files/myfile.pdf"}).
	Data         map[string]any // Data which is to be passed to the Go template.
	InlineImages []string       // A slice of images to be inlined in the email. PNG is preferred.
	ServerURL    string         // The URL of the server (for backlinks in message).
}

Finally, send the message. There are two ways to do this.

SendAndWait (recommended) delivers one message and returns that message's result:

ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()

if err := dispatcher.SendAndWait(ctx, msg); err != nil {
    log.Println("could not send:", err)
}

Each call carries a private result channel, so concurrent callers never receive one another's results, and a caller whose context expires cannot strand the worker still delivering its message. Allow the context at least Service.MaxSendDuration() (ConnectTimeout + SendTimeout, 20s by default), or slow but successful sends are abandoned and misreported as failures.

Send is fire and forget. It returns as soon as the message is queued, and the result is delivered to Service.ErrorChan if one is configured:

dispatcher.Send(msg)

// Wait for a response.
err := <-dispatcher.ErrorChan

Because ErrorChan is shared by every in-flight message, a value read from it cannot be attributed to any particular Send. Use SendAndWait when the caller needs to know whether its message was delivered.

When you are finished with a dispatcher, call Close() to stop the worker pool and wait for in-flight deliveries to finish.

Concurrency and lifetime
  • Multiple dispatchers may be created and used concurrently; each keeps its own configuration and template cache.
  • SendAndWait is safe to call from any number of goroutines at once.
  • A worker is never blocked indefinitely. If no one is reading ErrorChan, the result is dropped after ErrorChanGrace rather than parking the worker.
  • Run and Close are both idempotent. A dispatcher cannot be restarted after Close.

An example of sending via SMTP:

package main

import (
	"fmt"
	"github.com/tsawler/postal"
)

func main() {
	// Define a postal.Service variable with values appropriate for your environment.
	service := postal.Service{
		Method:         postal.SMTP,       // Method to send. Options are postal.SMTP or postal.MailGun.
		ServerURL:     "http://localhost", // The URL of the server (for backlinks in mail).
		SMTPServer:    "localhost",        // The name of the SMTP server you are sending through.
		SMTPPort:      1025,               // The port the SMTP server listens on.
		SMTPUser:      "",                 // SMTP username.
		SMTPPassword:  "",                 // SMTP password.
		ErrorChan:     make(chan error),   // A channel to receive errors (or nil for success).
		MaxWorkers:    2,                  // Number of workers in our pool.
		MaxMessages:   10,                 // Buffer size for send channel.
		TemplateDir:   "./templates",      // The directory where mail templates live.
	}
	
	// Get a dispatcher by calling postal.New(service).
	dispatcher, _ := postal.New(service)

	// Run the worker pool.
	dispatcher.Run()

	// Create a mail message.
	msg := postal.MailData{
		ToName:      "Me",
		ToAddress:   "me@here.com",
		FromName:    "Jack",
		FromAddress: "jack@there.com",
		Subject:     "Test subject",
		Content:     "Hello, world!",
		CC:          []string{"you@here.com", "him@here.com"},
		//Template:    "my-template.gohtml", // You can specify your own template, or leave this out and use the default.
	}

	// Send the message by calling dispatcher.Send.
	fmt.Println("Sending mail")
	dispatcher.Send(msg)

	// Wait for something back from ErrorChan.
	fmt.Println("Waiting for response")
	err := <-dispatcher.ErrorChan
	fmt.Println("Error", err)
}

An example sending through MailGun's api:

package main

import (
	"fmt"
	"github.com/tsawler/postal"
)

func main() {
	
	// Define a postal.Service variable with values appropriate for your environment.
	service := postal.Service{
		Method:        postal.MailGun,     // Method to send. Options are postal.SMTP or postal.MailGun.
		ServerURL:    "http://localhost",  // The URL of your server, in case you want backlinks in your message.
		ErrorChan:     make(chan error),   // A channel to receive error messages (or nil for successful sends).
		MaxWorkers:    2,                  // The number of workers you want in the worker pool.
		MaxMessages:   10,                 // The size of the job queue (a buffered channel).
		SendingFromEU: false,              // Set to true if sending from European Union (mailgun only).
		TemplateDir:   "./templates",      // The directory where mail templates live.
		Domain:        "yourdomain.com",   // The domain you are sending from (mailgun only).
		APIKey:        "your-mailgun-api", // The mailgun api key (mailgun only).
	}
	
	// Call postal.New with your service to get a message dispatcher.
	dispatcher, _ := postal.New(service)

	// Run the worker pool associated with the dispatcher.
	dispatcher.Run()

	// Create a mail message, of postal.MailData type.
	msg := postal.MailData{
		ToName:        "Me",
		ToAddress:     "me@here.com",
		FromName:      "Jack",
		FromAddress:   "jack@there.com",
		Subject:       "Test subject",
		Content:       "Hello, world!",
		CC:            []string{"you@here.com", "him@here.com"},
	}

	// Send the message.
	fmt.Println("Sending mail")
	dispatcher.Send(msg)

	
	// Wait for a response.
	fmt.Println("Waiting for response")
	err := <-dispatcher.ErrorChan
	
	// If err is nil, then the message was sent.
	fmt.Println("Error", err)
}

Documentation

Index

Constants

View Source
const (
	SMTP    = 1
	MailGun = 2
)
View Source
const (
	// DefaultConnectTimeout is how long to wait for an SMTP connection.
	DefaultConnectTimeout = 10 * time.Second

	// DefaultSendTimeout is how long to wait for a message to be transmitted
	// once connected. It also bounds the Mailgun API call.
	DefaultSendTimeout = 10 * time.Second

	// DefaultErrorChanGrace is how long a worker will wait to hand a result to
	// a Service.ErrorChan that nobody is currently reading, before abandoning
	// that result and returning to the pool.
	//
	// A worker is never blocked for longer than this. Previously the hand-off
	// was an unconditional channel send, so a result arriving after its caller
	// had stopped waiting parked that worker permanently; once every worker was
	// parked the dispatcher accepted mail forever without sending any of it.
	// Use SendAndWait to avoid the situation entirely.
	DefaultErrorChanGrace = 5 * time.Second
)

Default timeouts. Each may be overridden per Service.

Variables

View Source
var (
	// ErrDispatcherClosed is returned by SendAndWait once Close has been called.
	ErrDispatcherClosed = errors.New("postal: dispatcher is closed")

	// ErrNotRunning is returned by SendAndWait when Run has not been called.
	// Without workers, a queued message would never be consumed.
	ErrNotRunning = errors.New("postal: dispatcher is not running; call Run first")
)

Errors returned by the dispatcher.

Functions

This section is empty.

Types

type MailData

type MailData struct {
	ToName       string         // The name of the recipient.
	ToAddress    string         // The email address of the recipient.
	FromName     string         // The name of the sender.
	FromAddress  string         // The email address of the sender.
	ReplyTo      string         // The reply-to email address.
	AdditionalTo []string       // Additional TO recipients.
	Subject      string         // The subject of the email message.
	Content      template.HTML  // The content of the message, as HTML; sent verbatim when ContentType is "text/plain".
	ContentType  string         // Optional: "text/html" (default), "text/plain" (Content sent as-is, no template), "application/xml", etc.
	Template     string         // The template to use. If not specified, will use a simple default template.
	CC           []string       // A slice of CC recipient emails.
	BCC          []string       // A slice of BCC recipient emails.
	Attachments  []string       // A slice of attachments, which must exist on disk (i.e. []string{"./files/myfile.pdf"}).
	Data         map[string]any // Data which is to be passed to the Go template.
	InlineImages []string       // A slice of images to be inlined in the email. PNG is preferred.
	ServerURL    string         // The URL of the server (for backlinks in message).
}

MailData holds all information for a given message.

type MailDispatcher

type MailDispatcher struct {
	JobQueue  chan MailProcessingJob // The channel we send work to.
	ErrorChan chan error             // Optional channel results are sent to.
	// contains filtered or unexported fields
}

MailDispatcher is the main interface to this package. Calling New returns an instance of this type. Send mail with SendAndWait or Send.

All configuration and template state is held per dispatcher, so several dispatchers may be created and used concurrently in one process.

func New

func New(s Service) (*MailDispatcher, error)

New returns a new mail dispatcher. Call Run to start its worker pool, then send mail with SendAndWait (synchronous, result returned to the caller) or Send (fire and forget, result delivered to Service.ErrorChan if set). Call Close when finished.

func (*MailDispatcher) Close added in v1.1.0

func (md *MailDispatcher) Close()

Close stops the dispatch loop and the worker pool, and waits for in-flight deliveries to finish. It is safe to call more than once. A dispatcher cannot be restarted after Close.

func (*MailDispatcher) Run

func (md *MailDispatcher) Run()

Run runs the workers in our worker pool. It is safe to call more than once; subsequent calls are no-ops.

func (*MailDispatcher) Send

func (md *MailDispatcher) Send(msg MailData)

Send takes a msg in postal.MailData format, wraps it in a postal.MailProcessingJob and sends it to the job queue for delivery.

Send is fire and forget: it returns as soon as the message is queued, and the result is delivered to Service.ErrorChan if one was configured. Because that channel is shared by every in-flight message, a result read from it cannot be attributed to any particular Send. Prefer SendAndWait when the caller needs to know whether this message was delivered.

func (*MailDispatcher) SendAndWait added in v1.1.0

func (md *MailDispatcher) SendAndWait(ctx context.Context, msg MailData) error

SendAndWait delivers one message and returns its result.

The result belongs to this message alone: the job carries a private buffered channel, so a worker never blocks handing the result back and no other caller can consume it. If ctx expires first, the worker still completes and reports without blocking, and ctx.Err() is returned here.

The context should allow at least Service.MaxSendDuration, or slow but successful sends will be abandoned and misreported as failures.

type MailMessage

type MailMessage struct {
	MailData MailData
}

MailMessage is a simple wrapper for MailData.

type MailProcessingJob

type MailProcessingJob struct {
	MailMessage MailData
	// contains filtered or unexported fields
}

MailProcessingJob is the unit of work performed by a worker: one message to deliver, plus where to report the outcome.

type Service

type Service struct {
	Method        int        // How to send the message: postal.SMTP or postal.MailGun.
	ServerURL     string     // The URL of the server mail is sent from.
	SMTPServer    string     // The SMTP server.
	SMTPPort      int        // The SMTP server's port.
	SMTPUser      string     // The username for the SMTP server.
	SMTPPassword  string     // The password for the SMTP server.
	ErrorChan     chan error // Optional channel for results (an error, or nil on success).
	MaxWorkers    int        // Maximum number of workers in the pool.
	MaxMessages   int        // How big the buffer should be for the JobQueue.
	Domain        string     // The domain used to send mail.
	APIKey        string     // The API key for mailgun.
	SendingFromEU bool       // If using mailgun and sending from EU, set to true.
	TemplateDir   string     // Where templates are stored.

	// ConnectTimeout bounds establishing the SMTP connection.
	// Zero means DefaultConnectTimeout.
	ConnectTimeout time.Duration

	// SendTimeout bounds transmitting the message once connected, and bounds
	// the Mailgun API call. Zero means DefaultSendTimeout.
	SendTimeout time.Duration

	// ErrorChanGrace bounds how long a worker waits to deliver a result to
	// ErrorChan when no reader is ready. Zero means DefaultErrorChanGrace.
	//
	// Callers needing results delivered reliably should use SendAndWait, which
	// carries a private result channel and never drops anything.
	ErrorChanGrace time.Duration
}

Service is the type used to create a MailDispatcher.

A Service is copied into the MailDispatcher that New returns, and each dispatcher keeps its own configuration. Multiple dispatchers may be created and used concurrently within one process.

func (Service) MaxSendDuration added in v1.1.0

func (s Service) MaxSendDuration() time.Duration

MaxSendDuration reports the longest a single delivery attempt can take, excluding template rendering. Callers imposing their own deadline on SendAndWait should keep it comfortably above this value, or slow but successful sends will be abandoned and misreported as failures.

Jump to

Keyboard shortcuts

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