This guide covers sending emails from your Nimbus application. You will learn how to:
- Configure SMTP-based mail drivers (SMTP, SES, Mailgun, SendGrid, Postmark).
- Use native API drivers (SendGrid v3, Mailgun API, SES API, Resend) โ no SMTP needed.
- Send emails using the
mail.Messagetype. - Organize mail configuration in one place (e.g.
bin/server.go). - Queue emails for background delivery using the queue system.
- Test email functionality by plugging in a fake driver.
Overview
The mail package provides a minimal, transport-agnostic API for sending emails. It is intentionally simple and easy to swap out in production.
- Message โ represents an email:
From string,To []string,Subject string,Body string,HTML bool. - Driver โ sends messages. Any type that implements:
Send(m *mail.Message) error. - Default driver โ a global variable
mail.Defaultused bymail.Send.
Built-in SMTP drivers:
mail.SMTPDriverโ generic SMTP driver.mail.SESDriverโ Amazon SES (SMTP-backed).mail.MailgunSMTPDriverโ Mailgun (SMTP-backed).mail.SendGridSMTPDriverโ SendGrid (SMTP-backed).mail.PostmarkDriverโ Postmark (SMTP-backed).
Native API drivers (no SMTP required โ recommended for production):
mail.SendGridDriverโ SendGrid v3 HTTP API.mail.MailgunAPIDriverโ Mailgun HTTP API.mail.SESAPIDriverโ Amazon SES HTTP API.mail.ResendDriverโ Resend HTTP API.
Configuration
Nimbus starter apps keep mail settings in config/mail.go. The config.Mail struct is populated from environment variables, and bin/server.go wires those settings into the mail driver.
Mail config (config/mail.go)
// File: config/mail.go
package config
var Mail MailConfig
type MailConfig struct {
Driver string
SMTP SMTPConfig
}
type SMTPConfig struct {
Host string
Port int
Username string
Password string
From string
FromName string
}
func loadMail() {
Mail = MailConfig{
Driver: env("MAIL_DRIVER", "smtp"),
SMTP: SMTPConfig{
Host: env("SMTP_HOST", "localhost"),
Port: envInt("SMTP_PORT", 1025),
Username: env("SMTP_USERNAME", ""),
Password: env("SMTP_PASSWORD", ""),
From: env("MAIL_FROM", "noreply@example.com"),
FromName: env("MAIL_FROM_NAME", "Nimbus App"),
},
}
}
Example .env:
MAIL_DRIVER=smtp
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=user@example.com
SMTP_PASSWORD=secret
MAIL_FROM=noreply@example.com
MAIL_FROM_NAME="Nimbus App"
Simple SMTP wiring in bin/server.go
// File: bin/server.go
import (
"fmt"
"net/smtp"
"github.com/CodeSyncr/nimbus"
"github.com/CodeSyncr/nimbus/mail"
"nimbus-starter/config"
)
func Boot() *nimbus.App {
config.Load()
if config.Mail.Driver == "smtp" {
host := config.Mail.SMTP.Host
port := config.Mail.SMTP.Port
if host != "" && port != 0 {
addr := fmt.Sprintf("%s:%d", host, port)
var auth smtp.Auth
if config.Mail.SMTP.Username != "" {
auth = smtp.PlainAuth("", config.Mail.SMTP.Username, config.Mail.SMTP.Password, host)
}
mail.Default = mail.NewSMTPDriver(addr, auth, config.Mail.SMTP.From)
}
}
app := nimbus.New()
// ...
return app
}
If the SMTP addr has no port, :25 is used. auth can be nil for unauthenticated SMTP.
Native API drivers (recommended for production)
Native API drivers communicate directly with the provider's HTTP API โ no SMTP server needed. They are faster and more reliable for production deployments.
// SendGrid (v3 API)
mail.Default = mail.NewSendGridDriver(
os.Getenv("SENDGRID_API_KEY"),
"noreply@yourdomain.com",
)
// Mailgun (API)
mail.Default = mail.NewMailgunAPIDriver(
"your-domain.com",
os.Getenv("MAILGUN_API_KEY"),
"noreply@your-domain.com",
)
// Amazon SES (API)
mail.Default = mail.NewSESAPIDriver(
"us-east-1",
os.Getenv("AWS_ACCESS_KEY_ID"),
os.Getenv("AWS_SECRET_ACCESS_KEY"),
"noreply@yourdomain.com",
)
// Resend (API)
mail.Default = mail.NewResendDriver(
os.Getenv("RESEND_API_KEY"),
"noreply@yourdomain.com",
)
Provider-specific SMTP drivers (SES, Mailgun, SendGrid, Postmark)
Provider drivers are thin wrappers around SMTPDriver with the right host/port. You still use the same mail.Message type.
// Amazon SES
import "net/smtp"
host := "email-smtp.us-east-1.amazonaws.com"
port := "587"
user := os.Getenv("SES_USER")
pass := os.Getenv("SES_PASS")
auth := smtp.PlainAuth("", user, pass, host)
mail.Default = mail.NewSESDriver(host+":"+port, auth, "noreply@example.com")
// Mailgun (SMTP)
mail.Default = mail.NewMailgunSMTPDriver(
"smtp.mailgun.org:587",
smtp.PlainAuth("", os.Getenv("MAILGUN_USER"), os.Getenv("MAILGUN_PASS"), "smtp.mailgun.org"),
"noreply@example.com",
)
// SendGrid (SMTP)
mail.Default = mail.NewSendGridSMTPDriver(
"smtp.sendgrid.net:587",
smtp.PlainAuth("", "apikey", os.Getenv("SENDGRID_API_KEY"), "smtp.sendgrid.net"),
"noreply@example.com",
)
// Postmark
mail.Default = mail.NewPostmarkDriver(
"smtp.postmarkapp.com:587",
smtp.PlainAuth("", os.Getenv("POSTMARK_USER"), os.Getenv("POSTMARK_API_KEY"), "smtp.postmarkapp.com"),
"noreply@example.com",
)
Sending your first email
Once mail.Default is configured, sending an email is a single function call:
import (
"github.com/CodeSyncr/nimbus/mail"
"github.com/CodeSyncr/nimbus/context"
)
func Welcome(c *http.Context) error {
err := mail.Send(&mail.Message{
From: "noreply@example.com",
To: []string{"user@example.com"},
Subject: "Welcome to Nimbus",
Body: "Hello from Nimbus!",
HTML: false,
})
if err != nil {
return err
}
return c.JSON(200, map[string]string{"status": "sent"})
}
Configuring the message
The mail.Message struct is intentionally small:
type Message struct {
From string
To []string
Subject string
Body string
HTML bool
}
Recipients
Set one or more recipients via To:
msg := &mail.Message{
From: "noreply@example.com",
To: []string{"a@example.com", "b@example.com"},
Subject: "Team update",
Body: "Hello team!",
}
Plain text vs HTML
Set HTML to true for HTML emails:
msg := &mail.Message{
From: "noreply@example.com",
To: []string{"user@example.com"},
Subject: "Reset your password",
Body: `Reset your password
<p>Click <a href="` + resetURL + `">here</a> to reset your password.</p>`,
HTML: true,
}
For more complex layouts, render HTML using your template engine (e.g. Go templates or a JS frontend) and assign the rendered string to Body.
Queueing emails
Sending emails synchronously will block the request until your SMTP provider responds. For production, prefer dispatching a queue job that sends the email in the background.
Example: queue job for welcome email
// File: app/jobs/send_welcome_email.go
package jobs
import (
"context"
"github.com/CodeSyncr/nimbus/mail"
)
type SendWelcomeEmail struct {
Email string
}
func (j *SendWelcomeEmail) Handle(ctx context.Context) error {
return mail.Send(&mail.Message{
From: "noreply@example.com",
To: []string{j.Email},
Subject: "Welcome!",
Body: "Thanks for signing up.",
HTML: false,
})
}
// File: start/jobs.go
package start
import "github.com/CodeSyncr/nimbus/queue"
import "nimbus-starter/app/jobs"
func RegisterQueueJobs() {
m := queue.GetGlobal()
if m == nil {
return
}
m.Register(&jobs.SendWelcomeEmail{})
}
Dispatch the job from your controller:
import "github.com/CodeSyncr/nimbus/queue"
import "nimbus-starter/app/jobs"
func Register(c *http.Context) error {
// ... create user ...
_ = queue.Dispatch(&jobs.SendWelcomeEmail{Email: user.Email}).Dispatch(c.Request.Context())
return c.JSON(201, user)
}
Run the queue worker:
nimbus queue:work
Testing mail
Because mail drivers implement a simple interface, you can plug in a fake driver in tests to capture outbound messages.
// File: mail_fake_test.go
type FakeDriver struct {
Messages []*mail.Message
}
func (f *FakeDriver) Send(m *mail.Message) error {
f.Messages = append(f.Messages, m)
return nil
}
func TestSendsWelcomeEmail(t *testing.T) {
fake := &FakeDriver{}
mail.Default = fake
// Call handler or job that sends email...
if len(fake.Messages) != 1 {
t.Fatalf("expected 1 email, got %d", len(fake.Messages))
}
msg := fake.Messages[0]
if msg.Subject != "Welcome!" {
t.Fatalf("unexpected subject: %s", msg.Subject)
}
}
For more advanced scenarios, you can wrap the fake driver in your own test helper and assert on recipients, subjects, and bodies across your test suite.