go-notificationgo-notification
Channels/Email

Email Overview

Seven email drivers from self-hosted SMTP to modern APIs. Pricing, deliverability, and the cloud port-blocking caveat.

SMTP port blocking on cloud

Most cloud providers (GCP, AWS, DigitalOcean, Azure) block SMTP port 25 outbound. Port 587 is sometimes restricted too.

If you're deploying on cloud, use an API-based driver (Mailgun, SendGrid, SES, Resend, Postmark, Mailtrap) instead of SMTP. API drivers use HTTPS (port 443) which is never blocked.

See the full guide: SMTP Port Blocking on Cloud.

Driver Comparison

DriverFree TierPricingTransportBest For
SMTPSelf-hosted—SMTPOn-premise, corporate mail
Mailgun100/day$0.80/1000APIStartups, dev
SendGrid100/dayFrom $19.95APIEnterprise, compliance
AWS SES3,000/mo$0.10/1000APIHigh volume, AWS-resident stacks
Resend3,000/moFrom $20APIModern developer experience
Postmark100/moFrom $15APIBest inbox deliverability
Mailtrap1,000/moFrom $15APITest + production in one vendor

Free tiers change. Check each provider's pricing page before committing.

Quick Start

main.go
import (
    "github.com/gopackx/go-notification/channel/mail"
    "github.com/gopackx/go-notification/channel/mail/mailgun"
)

notifier.RegisterChannel(mailgun.New(mailgun.Config{
    Domain: "mg.example.com",
    APIKey: os.Getenv("MAILGUN_API_KEY"),
    From:   mail.Address{Name: "Acme", Address: "noreply@example.com"},
}))

The driver registers itself as "mail". Return that from your notification's Via() and implement ToMail():

main.go
func (n OrderShipped) Via(u notification.Notifiable) []string {
    return []string{"mail"}
}

func (n OrderShipped) ToMail(u notification.Notifiable) *mail.Message {
    return mail.NewMessage().
        SetSubject("Your order shipped").
        SetGreeting("Hi there!").
        Line("Your order is on its way.").
        Action("Track package", "https://example.com/track")
}

Running two mail drivers

Every mail driver defaults to the name "mail", and RegisterChannel keys on that name — registering a second mail driver without a custom name silently replaces the first. To run transactional and marketing mail side by side, give each a distinct Name and route on it:

main.go
notifier.RegisterChannel(postmark.New(postmark.Config{Name: "mail_tx", /* ... */}))
notifier.RegisterChannel(mailgun.New(mailgun.Config{Name: "mail_mk", /* ... */}))

func (n Receipt) Via(notification.Notifiable) []string { return []string{"mail_tx"} }
func (n Promo) Via(notification.Notifiable) []string   { return []string{"mail_mk"} }

func (u User) RouteNotificationFor(channel string) string {
    switch channel {
    case "mail_tx", "mail_mk":
        return u.Email
    }
    return ""
}

Custom names reach RouteNotificationFor since v1.0.1

A mail driver with a custom Name now calls RouteNotificationFor with that name — "mail_tx", not "mail". A notifiable that only answers to "mail" returns "" for the custom channel, and the channel is skipped silently for that recipient. Handle the custom name explicitly, as above.

See Multiple instances of one driver for the same pattern across families.

Choosing a driver

The short version:

  • Cloud deployment? Use an API driver (Mailgun, SES, Resend, Postmark).
  • Best deliverability? Postmark.
  • Cheapest at scale? AWS SES ($0.10 per 1,000).
  • Just starting? Mailgun or Resend — both have generous free tiers.
  • Testing? Mailtrap sandbox, then flip the same account to production.
  • On-premise / no cloud restrictions? SMTP is fine.

Long version with flowchart: Choosing an Email Driver.