Documentation previewThese docs are actively being built. Some pages may change as the framework and examples are finalized.
Skip to content
Support this libraryStar on GitHub

mail

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

Go ReferenceCIGo versionLatest tagGo Report CardCodecovUnit tests (executed count)
mail coveragemailfake coveragemaillog coveragemailmailgun coveragemailpostmark coveragemailresend coveragemailsendgrid coveragemailses coveragemailsmtp coverage

Using With GoForj Apps

Generated Apps resolve mail through the generated mail manager, provider wiring, auth delivery integration, metrics, and inspects. Start with Auth, Configuration, and Environment Variables when mail is part of a full App.

Use this page when you need standalone message composition, driver constructors, delivery behavior, or fakes for tests.

Installation

bash
go get github.com/goforj/mail

Quick Start

go
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:

go
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

DriverHTML/TextHeadersTagsMetadataAttachmentsNotes
mailsmtpxxCovers Gmail and other SMTP providers.
mailresendAPI-backed transactional delivery.
mailpostmarkFirst tag is native; additional tags are mapped into metadata.
mailmailgunUses Mailgun multipart message uploads.
mailsendgridMaps tags to categories and metadata to custom args.
mailsesUses SES raw email with the same MIME rendering as SMTP.
maillogxxLocal/dev inspection only; logs the composed message.
mailfakeTest helper; captures the full portable message.

API

API Index

GroupFunctions
CompositionMailer.Message MessageBuilder.Bcc MessageBuilder.Cc MessageBuilder.From MessageBuilder.Message MessageBuilder.ReplyTo MessageBuilder.To
ConstructionNew
ContentMessageBuilder.Attach MessageBuilder.AttachFile MessageBuilder.HTML MessageBuilder.Header MessageBuilder.Metadata MessageBuilder.Subject MessageBuilder.Tag MessageBuilder.Text
DefaultsWithDefaultFrom WithDefaultHeader WithDefaultMetadata WithDefaultReplyTo WithDefaultTag
DeliveryMailer.Send MessageBuilder.Build MessageBuilder.Send
Loggingmaillog.Driver.Send maillog.New maillog.WithBodies maillog.WithNow
Mailgunmailmailgun.Driver.Send mailmailgun.New
Message ModelAttachmentFromBytes AttachmentFromPath Message.Clone Message.Validate
Postmarkmailpostmark.Driver.Send mailpostmark.New
Resendmailresend.Driver.Send mailresend.New
SESmailses.Driver.Send mailses.New
SMTPmailsmtp.Driver.Send mailsmtp.New mailsmtp.Render
SendGridmailsendgrid.Driver.Send mailsendgrid.New
Testingmailfake.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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
_ = 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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
_ = 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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

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

gmail:

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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.

go
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