Guide: sending email with mail
github.com/angvp/tango/mail sends outgoing email: one plain-text message to one recipient, optionally with attachments. It's what accounts uses for password reset and email verification, and any app can use it directly.
sender, err := mail.SMTPSenderFromEnv()
if err != nil {
return err // mail.ErrNotConfigured when TANGO_SMTP_URL is unset
}
err = sender.Send(ctx, mail.Message{
From: "Shop <[email protected]>",
To: "[email protected]",
Subject: "Your invoice",
Text: "Your invoice is attached.\n",
Attachments: []mail.Attachment{
{Filename: "invoice.pdf", ContentType: "application/pdf", Data: pdf},
},
})
Senders
A mail.Sender has one method, Send(ctx, Message) error. It's synchronous and truthful: it returns nil only once the message has been handed on. It must also return promptly once ctx is done, because accounts relies on that to finish within its shutdown deadline. Three come with tanGO:
mail.NewSMTPSender(url), ormail.SMTPSenderFromEnv()readingTANGO_SMTP_URL: an SMTP server.mail.WriterSender(w): writes each message towin the exact wire format SMTP would send. For development.mailtest.Senderfrommail/mailtest: records messages for your tests, and rejects invalid ones exactly as a real sender does.
A provider with an HTTP API is a Sender you write yourself.
SMTP
The URL's scheme decides how the connection is secured:
| URL | Security | Default port |
|---|---|---|
smtp://user:[email protected] |
STARTTLS, required: a server that doesn't offer it is refused before any credentials are sent | 587 |
smtps://user:[email protected] |
TLS from the first byte | 465 |
smtp+insecure://localhost:1025 |
Plaintext. Only for localhost or a loopback IP, never with credentials, and only with an explicit port |
none: required |
smtp+insecure is for local tools such as Mailpit; give their port, since they differ (1025, 2525, …). There's no plaintext mode for a real server. Each message opens one connection, bounded by the context's deadline, or 30 seconds when it has none. Errors never contain the URL, the credentials, the recipient or the message: when the server refuses a command, the error names the stage and reply code but not the reply's text, which can echo what tanGO sent, and any other failure names only the server and the stage. errors.Is still matches a timeout or cancellation.
Put From in your code or config, not in TANGO_SMTP_URL.
Development
Nothing falls back to printing mail when SMTP isn't configured; choose a sender explicitly:
var sender mail.Sender
if smtp, err := mail.SMTPSenderFromEnv(); err == nil {
sender = smtp
} else if errors.Is(err, mail.ErrNotConfigured) {
sender = mail.WriterSender(os.Stdout) // development only
} else {
return err
}
Messages carry live links, such as password-reset links, so don't use WriterSender in production.
Validation
Every sender validates a message the same way before sending anything:
FromandTomust each be one address thatnet/mailcan parse, optionally with a display name.- A line break in
From,ToorSubjectis rejected, which closes header injection. - Both fail with
mail.ErrInvalidMessage.
Attachments
- A message with attachments is sent as
multipart/mixed: the text first, then each attachment base64-encoded. Filenameis required, and must not contain control characters.- An empty
ContentTypeis sent asapplication/octet-stream, and emptyDatais fine. - A bad attachment fails with an error matching both
mail.ErrInvalidAttachmentandmail.ErrInvalidMessage.
By default a message's attachments may total 10 MiB of raw bytes; mail.WithMaxAttachmentSize(n) changes that, and over the limit is mail.ErrMessageTooLarge. Base64 makes attachments about a third larger on the wire. The limit bounds what is sent, not the memory your Data already uses.
Not included
HTML mail, inline images, streamed or file-path attachments, more than one recipient, DKIM signing, queueing and retries, delivery receipts and bounce handling. accounts queues its own emails; a Send that fails returns the error, and nothing retries it.