Skip to content

goforj/mail

Repository files navigation

mail

Fluent email composition and pluggable delivery for GoForj packages and apps.

Go Reference CI Go version Latest tag Go Report Card Codecov Unit tests (executed count)
mail coverage mailfake coverage maillog coverage mailmailgun coverage mailpostmark coverage mailresend coverage mailsendgrid coverage mailses coverage mailsmtp coverage

Installation

go get github.com/goforj/mail

Quick Start

package main

import (
	"context"
	"log"

	"github.com/goforj/mail"
	"github.com/goforj/mail/mailsmtp"
)

func main() {
	driver, err := mailsmtp.New(mailsmtp.Config{
		Host:     "smtp.example.com",
		Port:     587,
		Username: "smtp-user",
		Password: "smtp-password",
	})
	if err != nil {
		log.Fatal(err)
	}

	mailer := mail.New(
		driver,
		mail.WithDefaultFrom("no-reply@example.com", "Example"),
	)

	err = mailer.Message().
		To("alice@example.com", "Alice").
		Subject("Welcome").
		Text("hello world").
		Send(context.Background())
	if err != nil {
		log.Fatal(err)
	}
}

Gmail via SMTP

Gmail does not need its own driver. Use mailsmtp with Gmail's SMTP host and an app password:

driver, err := mailsmtp.New(mailsmtp.Config{
	Host:     "smtp.gmail.com",
	Port:     587,
	Username: "you@gmail.com",
	Password: "gmail-app-password",
})

Notes:

  • Use a Google app password, not your normal account password.
  • 587 is the usual STARTTLS port. Use 465 with ForceTLS: true if you explicitly want implicit TLS.
  • Gmail is fine for personal or low-volume transactional sending, but a dedicated provider like Resend, Postmark, Mailgun, or SendGrid is usually a better production default.

Driver Capabilities

Driver HTML/Text Headers Tags Metadata Attachments Notes
mailsmtp x x Covers Gmail and other SMTP providers.
mailresend API-backed transactional delivery.
mailpostmark First tag is native; additional tags are mapped into metadata.
mailmailgun Uses Mailgun multipart message uploads.
mailsendgrid Maps tags to categories and metadata to custom args.
mailses Uses SES raw email with the same MIME rendering as SMTP.
maillog x x Local/dev inspection only; logs the composed message.
mailfake Test helper; captures the full portable message.

API

API Index

Group Functions
Composition Mailer.Message MessageBuilder.Bcc MessageBuilder.Cc MessageBuilder.From MessageBuilder.Message MessageBuilder.ReplyTo MessageBuilder.To
Construction New
Content MessageBuilder.Attach MessageBuilder.AttachFile MessageBuilder.HTML MessageBuilder.Header MessageBuilder.Metadata MessageBuilder.Subject MessageBuilder.Tag MessageBuilder.Text
Defaults WithDefaultFrom WithDefaultHeader WithDefaultMetadata WithDefaultReplyTo WithDefaultTag
Delivery Mailer.Send MessageBuilder.Build MessageBuilder.Send
Logging maillog.Driver.Send maillog.New maillog.WithBodies maillog.WithNow
Mailgun mailmailgun.Driver.Send mailmailgun.New
Message Model AttachmentFromBytes AttachmentFromPath Message.Clone Message.Validate
Postmark mailpostmark.Driver.Send mailpostmark.New
Resend mailresend.Driver.Send mailresend.New
SES mailses.Driver.Send mailses.New
SMTP mailsmtp.Driver.Send mailsmtp.New mailsmtp.Render
SendGrid mailsendgrid.Driver.Send mailsendgrid.New
Testing mailfake.Driver.Last mailfake.Driver.Messages mailfake.Driver.Reset mailfake.Driver.Send mailfake.Driver.SentCount mailfake.Driver.SetError mailfake.New

API Reference

Generated from public API comments and examples.

Composition

Mailer.Message

Message starts a new fluent message builder bound to this mailer.

fake := mailfake.New()
mailer := mail.New(fake, mail.WithDefaultFrom("no-reply@example.com", "Example"))
_ = mailer.Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Send(context.Background())
fmt.Println(fake.SentCount())
// 1

MessageBuilder.Bcc

Bcc appends one blind-carbon-copy recipient.

msg, _ := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Bcc("audit@example.com", "Audit").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.Bcc[0].Email)
// audit@example.com

MessageBuilder.Cc

Cc appends one carbon-copy recipient.

msg, _ := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Cc("manager@example.com", "Manager").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.Cc[0].Email)
// manager@example.com

MessageBuilder.From

From sets the from recipient.

msg, _ := mail.New(mailfake.New()).Message().
	From("team@example.com", "Example Team").
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.From.Email)
// team@example.com

MessageBuilder.Message

Message returns the currently composed message without applying mailer defaults.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Message()
fmt.Println(msg.Subject)
// Welcome

MessageBuilder.ReplyTo

ReplyTo appends one reply-to recipient.

msg, _ := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	ReplyTo("support@example.com", "Support").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.ReplyTo[0].Email)
// support@example.com

MessageBuilder.To

To appends one primary recipient.

msg, _ := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(len(msg.To))
// 1

Construction

New

New creates a Mailer backed by the provided driver.

fake := mailfake.New()
mailer := mail.New(fake, mail.WithDefaultFrom("no-reply@example.com", "Example"))
fmt.Println(mailer != nil)
// true

Content

MessageBuilder.Attach

Attach appends one in-memory attachment.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Attach("report.txt", "text/plain", []byte("hello world")).
	Message()
fmt.Println(msg.Attachments[0].Filename)
// report.txt

MessageBuilder.AttachFile

AttachFile loads one attachment from disk and appends it to the message.

_ = os.WriteFile("report.txt", []byte("hello world"), 0o644)
defer os.Remove("report.txt")
msg, _ := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	AttachFile("report.txt").
	Build()
fmt.Println(msg.Attachments[0].Filename)
// report.txt

MessageBuilder.HTML

HTML sets the HTML body.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	HTML("<p>hello world</p>").
	Message()
fmt.Println(msg.HTML)
// <p>hello world</p>

MessageBuilder.Header

Header sets or replaces one message header.

message, _ := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Header("X-Request-ID", "req_123").
	Tag("welcome").
	Metadata("tenant_id", "tenant_123").
	Build()
fmt.Println(message.Headers["X-Request-ID"])
// req_123

MessageBuilder.Metadata

Metadata sets one provider-facing metadata key/value pair.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Metadata("tenant_id", "tenant_123").
	Message()
fmt.Println(msg.Metadata["tenant_id"])
// tenant_123

MessageBuilder.Subject

Subject sets the message subject.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Message()
fmt.Println(msg.Subject)
// Welcome

MessageBuilder.Tag

Tag appends one provider-facing message tag.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Tag("welcome").
	Message()
fmt.Println(msg.Tags[0])
// welcome

MessageBuilder.Text

Text sets the plain text body.

msg := mail.New(mailfake.New()).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Message()
fmt.Println(msg.Text)
// hello world

Defaults

WithDefaultFrom

WithDefaultFrom configures the default from recipient applied when a message omits one.

mailer := mail.New(
	mailfake.New(),
	mail.WithDefaultFrom("no-reply@example.com", "Example"),
)
fmt.Println(mailer != nil)
// true

WithDefaultHeader

WithDefaultHeader configures a header applied when a message omits that header key.

msg, _ := mail.New(
	mailfake.New(),
	mail.WithDefaultHeader("X-App", "goforj"),
).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.Headers["X-App"])
// goforj

WithDefaultMetadata

WithDefaultMetadata configures metadata applied when a message omits that metadata key.

msg, _ := mail.New(
	mailfake.New(),
	mail.WithDefaultMetadata("tenant_id", "tenant_123"),
).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.Metadata["tenant_id"])
// tenant_123

WithDefaultReplyTo

WithDefaultReplyTo configures the default reply-to recipients applied when a message omits them.

mailer := mail.New(
	mailfake.New(),
	mail.WithDefaultReplyTo(mail.Recipient{Email: "support@example.com", Name: "Support"}),
)
msg, _ := mailer.Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.ReplyTo[0].Email)
// support@example.com

WithDefaultTag

WithDefaultTag configures a tag prepended to every message sent by the mailer.

msg, _ := mail.New(
	mailfake.New(),
	mail.WithDefaultTag("transactional"),
).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.Tags[0])
// transactional

Delivery

Mailer.Send

Send validates the message, applies defaults, and delegates delivery to the driver.

mailer := mail.New(mailfake.New(), mail.WithDefaultFrom("no-reply@example.com", "Example"))
err := mailer.Send(context.Background(), mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com", Name: "Alice"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// true

MessageBuilder.Build

Build applies defaults, validates, and returns the composed message without sending it.

msg, _ := mail.New(
	mailfake.New(),
	mail.WithDefaultFrom("no-reply@example.com", "Example"),
).Message().
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Build()
fmt.Println(msg.From.Email)
// no-reply@example.com

MessageBuilder.Send

Send delegates the composed message to the bound mailer.

fake := mailfake.New()
_ = mail.New(fake).Message().
	From("no-reply@example.com", "Example").
	To("alice@example.com", "Alice").
	Subject("Welcome").
	Text("hello world").
	Send(context.Background())
fmt.Println(fake.SentCount())
// 1

Logging

maillog.Driver.Send

Send writes one JSON log record for the message.

var out bytes.Buffer
_ = maillog.New(&out).Send(context.Background(), mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(strings.Contains(out.String(), "\"subject\":\"Welcome\""))
// true

maillog.New

New creates a log mail driver that writes one JSON record per sent message.

var out bytes.Buffer
mailer := maillog.New(&out)
_ = mail.New(mailer).Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(strings.Contains(out.String(), "\"subject\":\"Welcome\""))
// true

maillog.WithBodies

WithBodies controls whether HTML and text bodies are included in log output.

var out bytes.Buffer
mailer := maillog.New(&out, maillog.WithBodies(true))
_ = mail.New(mailer).Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(strings.Contains(out.String(), "\"text\":\"hello world\""))
// true

maillog.WithNow

WithNow overrides the timestamp source used by log entries.

var out bytes.Buffer
mailer := maillog.New(&out, maillog.WithNow(func() time.Time {
	return time.Date(2026, time.April, 19, 0, 0, 0, 0, time.UTC)
}))
_ = mail.New(mailer).Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(strings.Contains(out.String(), "2026-04-19T00:00:00Z"))
// true

Mailgun

mailmailgun.Driver.Send

Send validates and transmits one message through Mailgun.

driver, _ := mailmailgun.New(mailmailgun.Config{
	Domain:   "mg.example.com",
	APIKey:   "key-test",
	Endpoint: "http://127.0.0.1:1",
})
err := driver.Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// false

mailmailgun.New

New creates a Mailgun mail driver from the given config.

driver, _ := mailmailgun.New(mailmailgun.Config{
	Domain: "mg.example.com",
	APIKey: "key-test",
})
fmt.Println(driver != nil)
// true

Message Model

AttachmentFromBytes

AttachmentFromBytes creates one attachment from in-memory content.

attachment := mail.AttachmentFromBytes("report.txt", "text/plain", []byte("hello world"))
fmt.Println(attachment.Filename)
// report.txt

AttachmentFromPath

AttachmentFromPath loads one attachment from a local file path.

_ = os.WriteFile("report.txt", []byte("hello world"), 0o644)
defer os.Remove("report.txt")
attachment, _ := mail.AttachmentFromPath("report.txt")
fmt.Println(attachment.Filename)
// report.txt

Message.Clone

Clone returns a copy of the message safe for reuse in tests and builders.

original := mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com", Name: "Alice"}},
	Subject: "Welcome",
	Text:    "hello world",
}
cloned := original.Clone()
cloned.Subject = "Changed"
fmt.Println(original.Subject)
// Welcome

Message.Validate

Validate checks that the message has valid recipients, subject, body, and headers.

err := (mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com", Name: "Example"},
	To:      []mail.Recipient{{Email: "alice@example.com", Name: "Alice"}},
	Subject: "Welcome",
	Text:    "hello world",
}).Validate()
fmt.Println(err == nil)
// true

Postmark

mailpostmark.Driver.Send

Send validates and transmits one message through Postmark.

driver, _ := mailpostmark.New(mailpostmark.Config{
	ServerToken: "pm_test_token",
	Endpoint:    "http://127.0.0.1:1",
})
err := driver.Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// false

mailpostmark.New

New creates a Postmark mail driver from the given config.

driver, _ := mailpostmark.New(mailpostmark.Config{
	ServerToken: "pm_test_token",
})
fmt.Println(driver != nil)
// true

Resend

mailresend.Driver.Send

Send validates and transmits one message through Resend.

driver, _ := mailresend.New(mailresend.Config{
	APIKey:   "re_test_key",
	Endpoint: "http://127.0.0.1:1",
})
err := driver.Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// false

mailresend.New

New creates a Resend mail driver from the given config.

driver, _ := mailresend.New(mailresend.Config{
	APIKey: "re_test_key",
})
fmt.Println(driver != nil)
// true

SES

mailses.Driver.Send

Send validates and transmits one message through Amazon SES.

driver, _ := mailses.New(mailses.Config{
	Region:          "us-east-1",
	AccessKeyID:     "test",
	SecretAccessKey: "test",
	Endpoint:        "http://127.0.0.1:1",
})
err := driver.Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// false

mailses.New

New creates an Amazon SES mail driver from the given config.

driver, _ := mailses.New(mailses.Config{
	Region:          "us-east-1",
	AccessKeyID:     "test",
	SecretAccessKey: "test",
})
fmt.Println(driver != nil)
// true

SMTP

mailsmtp.Driver.Send

Send validates and transmits one message over SMTP.

driver, _ := mailsmtp.New(mailsmtp.Config{
	Host: "smtp.example.com",
	Port: 587,
})
err := driver.Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// false

mailsmtp.New

New creates an SMTP mail driver from the given config.

driver, _ := mailsmtp.New(mailsmtp.Config{
	Host: "smtp.example.com",
	Port: 587,
})
fmt.Println(driver != nil)
// true

gmail:

driver, _ := mailsmtp.New(mailsmtp.Config{
	Host:     "smtp.gmail.com",
	Port:     587,
	Username: "you@gmail.com",
	Password: "gmail-app-password",
})
fmt.Println(driver != nil)
// true

mailsmtp.Render

Render turns one message into an RFC 822 style SMTP payload.

raw, _ := mailsmtp.Render(mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com", Name: "Example"},
	To:      []mail.Recipient{{Email: "alice@example.com", Name: "Alice"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(strings.Contains(string(raw), "Subject: Welcome"))
// true

SendGrid

mailsendgrid.Driver.Send

Send validates and transmits one message through SendGrid.

driver, _ := mailsendgrid.New(mailsendgrid.Config{
	APIKey:   "SG.test_key",
	Endpoint: "http://127.0.0.1:1",
})
err := driver.Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err == nil)
// false

mailsendgrid.New

New creates a SendGrid mail driver from the given config.

driver, _ := mailsendgrid.New(mailsendgrid.Config{
	APIKey: "SG.test_key",
})
fmt.Println(driver != nil)
// true

Testing

mailfake.Driver.Last

Last returns the last recorded message when one exists.

fake := mailfake.New()
_ = mail.New(fake).Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
last, _ := fake.Last()
fmt.Println(last.Subject)
// Welcome

mailfake.Driver.Messages

Messages returns a copy of every recorded message.

fake := mailfake.New()
_ = mail.New(fake).Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(len(fake.Messages()))
// 1

mailfake.Driver.Reset

Reset clears recorded messages and any configured send error.

fake := mailfake.New()
_ = fake.Send(context.Background(), mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fake.Reset()
fmt.Println(fake.SentCount())
// 0

mailfake.Driver.Send

Send records the message and returns the configured error when set.

fake := mailfake.New()
_ = fake.Send(context.Background(), mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(fake.SentCount())
// 1

mailfake.Driver.SentCount

SentCount reports the number of recorded messages.

fake := mailfake.New()
_ = fake.Send(context.Background(), mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(fake.SentCount())
// 1

mailfake.Driver.SetError

SetError configures the error returned by future sends.

fake := mailfake.New()
fake.SetError(errors.New("boom"))
err := fake.Send(context.Background(), mail.Message{
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(err != nil)
// true

mailfake.New

New creates an in-memory fake mail driver for tests.

fake := mailfake.New()
_ = mail.New(fake).Send(context.Background(), mail.Message{
	From:    &mail.Recipient{Email: "no-reply@example.com"},
	To:      []mail.Recipient{{Email: "alice@example.com"}},
	Subject: "Welcome",
	Text:    "hello world",
})
fmt.Println(fake.SentCount())
// 1

Docs Tooling

  • go run ./docs/examplegen/main.go
  • go run ./docs/readme/main.go
  • go run ./docs/readme/testcounts/main.go
  • ./docs/watcher.sh

About

Fluent email composition and pluggable delivery for GoForj packages and apps

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors