Mail

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.Message type.
  • 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.Default used by mail.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.