Fluent email composition and pluggable delivery for GoForj packages and apps.
go get github.com/goforj/mailpackage 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 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.
587is the usual STARTTLS port. Use465withForceTLS: trueif 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 | 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. |
Generated from public API comments and examples.
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())
// 1Bcc 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.comCc 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.comFrom 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.comMessage 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)
// WelcomeReplyTo 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.comTo 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))
// 1New 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)
// trueAttach 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.txtAttachFile 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.txtHTML 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>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_123Metadata 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_123Subject 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)
// WelcomeTag 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])
// welcomeText 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 worldWithDefaultFrom 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)
// trueWithDefaultHeader 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"])
// goforjWithDefaultMetadata 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_123WithDefaultReplyTo 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.comWithDefaultTag 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])
// transactionalSend 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)
// trueBuild 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.comSend 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())
// 1Send 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\""))
// trueNew 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\""))
// trueWithBodies 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\""))
// trueWithNow 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"))
// trueSend 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)
// falseNew 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)
// trueAttachmentFromBytes creates one attachment from in-memory content.
attachment := mail.AttachmentFromBytes("report.txt", "text/plain", []byte("hello world"))
fmt.Println(attachment.Filename)
// report.txtAttachmentFromPath 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.txtClone 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)
// WelcomeValidate 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)
// trueSend 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)
// falseNew creates a Postmark mail driver from the given config.
driver, _ := mailpostmark.New(mailpostmark.Config{
ServerToken: "pm_test_token",
})
fmt.Println(driver != nil)
// trueSend 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)
// falseNew creates a Resend mail driver from the given config.
driver, _ := mailresend.New(mailresend.Config{
APIKey: "re_test_key",
})
fmt.Println(driver != nil)
// trueSend 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)
// falseNew 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)
// trueSend 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)
// falseNew creates an SMTP mail driver from the given config.
driver, _ := mailsmtp.New(mailsmtp.Config{
Host: "smtp.example.com",
Port: 587,
})
fmt.Println(driver != nil)
// truegmail:
driver, _ := mailsmtp.New(mailsmtp.Config{
Host: "smtp.gmail.com",
Port: 587,
Username: "you@gmail.com",
Password: "gmail-app-password",
})
fmt.Println(driver != nil)
// trueRender 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"))
// trueSend 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)
// falseNew creates a SendGrid mail driver from the given config.
driver, _ := mailsendgrid.New(mailsendgrid.Config{
APIKey: "SG.test_key",
})
fmt.Println(driver != nil)
// trueLast 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)
// WelcomeMessages 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()))
// 1Reset 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())
// 0Send 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())
// 1SentCount 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())
// 1SetError 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)
// trueNew 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())
// 1go run ./docs/examplegen/main.gogo run ./docs/readme/main.gogo run ./docs/readme/testcounts/main.go./docs/watcher.sh