A vendor-free Go core for transactional email: verification links, security notices, receipts and other mail an application sends as itself.
OmniMail defines one Sender interface, a validated message model, MIME
assembly, templates and classified errors. Applications depend only on the
core; the deployment chooses the delivery provider. Vendor SDKs live in
separate adapter modules, so the core has no dependencies outside the Go
standard library.
OmniMail is not a mailbox or chat client. For conversational messaging authenticated as a user, see OmniChat.
- One interface -
Sender.Send(ctx, *Message) (*SendResult, error)for every provider - Safe by default - address validation via
net/mail, internationalized addresses, header-injection protection (CR/LF rejected in every header value) - MIME assembly - RFC 5322
multipart/alternative, quoted-printable/base64 bodies, RFC 2047 headers,DateandMessage-ID; Bcc never written - Templates - subject/text/HTML rendering with
html/templateautoescaping, from strings or anfs.FS(embed.FS) - Classified errors -
InvalidMessage,InvalidAddress,Rejected,Throttled,Transient,AuthwithRetryable() - Built-in senders - SMTP (STARTTLS / implicit TLS, PLAIN/LOGIN), log (bodies redacted), memory (tests)
- Conformance suite -
providertestkeeps every adapter behaving the same
| Provider | Module / Package | Status |
|---|---|---|
| SMTP | github.com/plexusone/omnimail/smtp |
Available |
| Log (development) | github.com/plexusone/omnimail/logsender |
Available |
| Memory (tests) | github.com/plexusone/omnimail/memsender |
Available |
| AWS SES v2 | github.com/plexusone/omni-aws/omnimail |
Planned |
| SendGrid | github.com/plexusone/omni-twilio/omnimail |
Planned |
| Gmail API | github.com/plexusone/omni-google/omnimail |
Planned |
go get github.com/plexusone/omnimailpackage main
import (
"context"
"log"
"os"
"github.com/plexusone/omnimail"
"github.com/plexusone/omnimail/smtp"
)
func main() {
sender, err := smtp.New(smtp.Config{
Host: os.Getenv("SMTP_HOST"),
Username: os.Getenv("SMTP_USERNAME"),
Password: os.Getenv("SMTP_PASSWORD"),
})
if err != nil {
log.Fatal(err)
}
msg := &omnimail.Message{
From: omnimail.Address{Name: "Example", Email: "no-reply@example.com"},
To: []omnimail.Address{{Email: "user@example.org"}},
Subject: "Verify your email",
Text: "Open this link to verify your address: https://example.com/verify?t=...",
HTML: `<p><a href="https://example.com/verify?t=...">Verify your address</a></p>`,
Tags: map[string]string{"purpose": "verify_email"},
}
res, err := sender.Send(context.Background(), msg)
if err != nil {
if omnimail.IsRetryable(err) {
log.Printf("temporary failure, retry later: %v", err)
return
}
log.Fatal(err)
}
log.Printf("sent %s via %s", res.MessageID, res.Provider)
}//go:embed mail/*.tmpl
var mailFS embed.FS
tmpl, err := omnimail.ParseTemplateFS(mailFS, "mail/verify") // verify.subject.tmpl, verify.text.tmpl, verify.html.tmpl
if err != nil {
return err
}
if err := tmpl.Apply(msg, map[string]any{"Name": user.Name, "Link": link}); err != nil {
return err
}_, err := sender.Send(ctx, msg)
switch {
case errors.Is(err, omnimail.ErrInvalidMessage): // fix the message; never retry
case errors.Is(err, omnimail.ErrThrottled): // back off: omnimail.RetryAfter(err)
case omnimail.IsRetryable(err): // transient: retry with backoff
case errors.Is(err, omnimail.ErrAuth): // credentials or permissions
}s := memsender.New()
app := NewApp(s)
// ... exercise the app ...
if s.Len() != 1 || !strings.Contains(s.Last().Text, "/verify?") {
t.Fatal("verification email not sent")
}Adapters implement omnimail.Sender in their own module and run the
conformance suite against a fake endpoint:
func TestConformance(t *testing.T) {
providertest.RunAll(t, providertest.Config{
Harness: newFakeEndpointHarness(t),
Provider: "ses",
SupportsTags: true,
})
}See the adapter guide and conformance suite.
MIT License - see LICENSE for details.