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
| Driver | Free Tier | Pricing | Transport | Best For |
|---|---|---|---|---|
| SMTP | Self-hosted | — | SMTP | On-premise, corporate mail |
| Mailgun | 100/day | $0.80/1000 | API | Startups, dev |
| SendGrid | 100/day | From $19.95 | API | Enterprise, compliance |
| AWS SES | 3,000/mo | $0.10/1000 | API | High volume, AWS-resident stacks |
| Resend | 3,000/mo | From $20 | API | Modern developer experience |
| Postmark | 100/mo | From $15 | API | Best inbox deliverability |
| Mailtrap | 1,000/mo | From $15 | API | Test + production in one vendor |
Free tiers change. Check each provider's pricing page before committing.
Quick Start
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():
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:
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.