Skip to main content
  1. Posts/

Sending mails in Go the easy way

·2252 words·11 mins

Note: I initially wrote this article on dev.to but also imported it over here since I redesigned my blog.

go-mail Logo

I recently read this article on dev.to about “How to Send Emails in Go”. It seems to be a copy from a blog post from mailtrap.io.

While the article is certainly not wrong on what is written there, it seems like its main intention is to make the point that it’s easier to use a 3rd party mail service - which is perfectly fine… yet, I found the conclusion a bit baffling. In the article it says:

This wraps up our rather quick guide to sending emails in Golang. As you can see, the choice comes down to either utilizing basic in-built functionalities of Go or connecting to 3rd parties to send emails on your behalf.

I have to disagree with this since there are lots of good Go mail libraries that really make your job of sending mail so much easier.

go-mail makes it easy for you #

As the backbone for my form mailing microservice js-mailer, I’ve created go-mail as a state-of-the-art Go mail library that is comprehensive, yet easy to use.

go-mail consists of two main components. The Msg represents the mail message and everything related to it and the Client, which handles the mail server communication and the delivery of the Msg.

Since the original article first mentions smtp.SendMail, let us have a look at how we would archive the same with go-mail.

The original example creates a simple mail from bill@gates.com delivered to bill@gates.com with a simple subject why are you not using Mailtrap yet? and a mail body saying Here’s the space for our great sales pitch. The whole thing is delivered via SMTP server smtp.mailtrap.io:25 and using SMTP Plain Authentication.

With go-mail it would look like this:

package main

import (
	"github.com/wneessen/go-mail"
	"log"
)

func main() {
	// First we create a mail message
	m := mail.NewMsg()
	if err := m.From("bill@gates.com"); err != nil {
		log.Fatalf("failed to set From address: %s", err)
	}
	if err := m.To("bill@gates.com"); err != nil {
		log.Fatalf("failed to set To address: %s", err)
	}
	m.Subject("Why are you not using go-mail yet?")
	m.SetBodyString(mail.TypeTextPlain, "You won't need a sales pitch. It's FOSS.")

	// Secondly the mail client
	c, err := mail.NewClient("smtp.mailtrap.io",
		mail.WithPort(25), mail.WithSMTPAuth(mail.SMTPAuthPlain),
		mail.WithUsername("piotr@mailtrap.io"), mail.WithPassword("extremely_secret_pass"))
	if err != nil {
		log.Fatalf("failed to create mail client: %s", err)
	}

	// Finally let's send out the mail
	if err := c.DialAndSend(m); err != nil {
		log.Fatalf("failed to send mail: %s", err)
	}
}

At first glance, this might look like much more code, but with net/smtp and smtp.SendMail you need to make sure that all parameters (like mail addresses) are syntactically right and line breaks are put in correctly, go-mail will take care of all of this for you. Additionally, the syntax is self-explanatory and easy to understand.

Authentication #

The original article talks about the authentication methods that can be used. We make use of the smtp.Auth interface, which allows the user to implement their own authentication methods and use them with go-mail without issues.

This means, analogous to net/smtp, go-mail supports SMTP PLAIN and CRAM-MD5 authentication. Additionally, I’ve also added support for SMTP LOGIN (which is similar to SMTP PLAIN but not the same).

TLS #

Nowadays, you want to make sure that your authentication is secure. SMTP PLAIN and LOGIN basically send your username and password in plaintext over the wire. Therefore we need to make use of transport encryption using TLS.

I am a firm believer in strong encryption by default and that nowadays, with all the easy tools like Let’s Encrypt and co., there is basically no excuse to not use TLS for any connection anymore. For that reason, go-mail defaults to mandatory TLS in the client - which means the client will fail if the server does not support STARTTLS for the connection. Still, you have the option to set the client to opportunistic TLS (use TLS if available, otherwise don’t) or disable TLS completely.

To do so, all you need to do is give the NewClient() method an additional option.

Re-using the code from our initial example, it would look like this:

c, err := mail.NewClient("smtp.mailtrap.io",
	mail.WithPort(25), mail.WithSMTPAuth(mail.SMTPAuthPlain), 
	mail.WithUsername("piotr@mailtrap.io"), 
	mail.WithPassword("extremely_secret_pass"), 
	mail.WithTLSPolicy(mail.TLSOpportunistic)))

Multiple recipients and (B)CC #

Next, the article mentions that things get a bit more sophisticated when it comes to multiple recipients. Not for go-mail though. The Msg.To() method allows setting multiple recipients. So for our initial example, all we would need to do is the following:

if err := m.To("bill@gates.com", "stevie@microsoft.com"); err != nil {
	log.Fatalf("failed to set To address: %s", err)
}

Easy, isn’t it?

The original article now wants to separate the 2nd address from the To-Address list. No problem! We have Msg.Cc() in go-mail. Which makes sure that the envelope address header and mail address header are set up correctly for this operation. Here is what our code would look like now:

if err := m.To("bill@gates.com"); err != nil {
	log.Fatalf("failed to set To address: %s", err)
}
if err := m.Cc("stevie@microsoft.com"); err != nil {
	log.Fatalf("failed to set CC address: %s", err)
}

We still get the benefit from go-mails syntax checks and we don’t need to care about any newlines or formatting.

Finally, the article doesn’t want Bill to know that Steve knows. So they separate envelope recipients from the mail header recipients - which makes the code look really confusing in my opinion. go-mail has a much more comfortable solution: Msg.Bcc(). Same as you would set a blind carbon copy recipient in your mail client, go-mail takes care of all the bells and whistles.

Here is our new code snippet:

if err := m.To("bill@gates.com"); err != nil {
	log.Fatalf("failed to set To address: %s", err)
}
if err := m.Bcc("stevie@microsoft.com"); err != nil {
	log.Fatalf("failed to set BCC address: %s", err)
}

Yes, it’s really as easy as that.

Quality of Life #

When I initially created go-mail one of my main goals was to make go-mail act similar to a normal mail user agent (MUA). Hence go-mail has a lot of - what I would call - “Quality of Life” methods. Things that you would likely be able to do yourself without go-mail providing these methods but it’s so much easier to not have to think about it. Let’s for example look at mail importance.

Mails can have Importance-flags. Unfortunately not every mail client handles those in the same way. It’s all depending on the correct mail headers and the values you provide them. To make the user’s life easier, go-mail provides a Msg.SetImportance() method. All you need to do is provide the importance you like to set for your message and go-mail will take care of the rest.

Example:

m.SetImportance(mail.ImportanceHigh)

We can do much more #

In the final chapter of the article, it switches from Go’s own tools to using 3rd party APIs for sending emails, giving you access to i. e. “beautiful HTML mails” or “message previews”. Let’s address those two points in terms of go-mail.

HTML mails #

go-mail is built with full MIME and multipart support. This means it takes care of proper MIME encoding as well as being able to handle multiple mail parts like mail body + attachment. Not only this, we have built-in support for Go’s text/template and html/template systems.

To send an HTML mail, all we would need to do is change one line in our initial example code:

Replace:

m.SetBodyString(mail.TypeTextPlain, "You won't need a sales pitch. It's FOSS.")

with:

m.SetBodyString(mail.TypeTextHTML, "<h1>You won't need a sales pitch. It's FOSS.</h1>")

Now let’s assume we live in a modern world, where mail not only consists of plain text or HTML, but we let the client decide what to show instead. For this, we can make use of alternative mail body parts. Let’s have a look at how this would look in code:

m.SetBodyString(mail.TypeTextHTML, "<h1>You won't need a sales pitch. It's FOSS and HTML!</h1>")
m.AddAlternativeString(mail.TypeTextPlain, "You won't need a sales pitch. It's FOSS and plain text!")

A more advanced example would be the use of HTML templates. Let’s have a quick look on how this would work.

import "html/template"
// [...]
type MyStruct struct {
	Placeholder string
}
data := MyStruct{Placeholder: "Teststring"}
tpl, err := template.New("test").Parse("This is a {{.Placeholder}}")
if err != nil {
	log.Fatalf("failed to parse HTML template: %s", err)
}
if err := m.SetBodyHTMLTemplate(tpl, data); err != nil {
	log.Fatalf("failed to set HTML template mail body: %s", err)
}

Here we create a new example struct that would represent our template data. We create a new html/template template and add some example text with placeholders that would fit our template data. All that’s left is to use Msg.SetBodyHTMLTemplate() with our template and data and go-mail will take care of the rest.

In fact, go-mail has support for many different kinds of attachments. Be it simple files from the local file system, from an io.Reader interface, from an embed.FS or the already mentioned template packages.

Message preview #

I already mentioned that go-mail consists of two main components. The Client and the Msg. The beauty of this is, that we have implemented an io.WriteTo interface into the Msg. This means we get access to our mail message without having to pump it through our Client. Not only that, but we also have methods on the Msg which allows us to store the mail directly into a file or push it to an io.Writer interface. Let’s have a closer look…

If you work on a UNIX-like OS (Linux, *BSD, macOS) you probably know that there are special devices for standard input and output. One of them is /dev/stdout which is your standard output on a terminal console. In Go, this interface is represented by the os.Stdout file pointer. Since it satisfies the io.Writer interface, it makes it super easy for us to preview a mail.

Check this out:

if _, err := m.WriteTo(os.Stdout); err != nil {
	log.Fatalf("failed to write mail to STDOUT: %s", err)
}

We tell go-mail to write our mail message to STDOUT and it will perform all it’s magic first and just pump the whole output into the STDOUT file. The output would look similar to this in my Linux console:

$ go run main.go

Date: Sat, 08 Oct 2022 12:47:50 +0200
MIME-Version: 1.0
Message-ID: <116004.6075668864260473870.1665226070@arch-vm.local.host>
Subject: Why are you not using go-mail yet?
User-Agent: go-mail v0.2.9 // https://github.com/wneessen/go-mail
X-Mailer: go-mail v0.2.9 // https://github.com/wneessen/go-mail
From: <bill@gates.com>
To: <bill@gates.com>
Content-Type: multipart/alternative;
 boundary=9c5a427bc18e45ff56e0b8f7053cddceaca54a5c985a425213544ab7977f

--9c5a427bc18e45ff56e0b8f7053cddceaca54a5c985a425213544ab7977f
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8

<h1>You won't need a sales pitch. It's FOSS and HTML!</h1>
--9c5a427bc18e45ff56e0b8f7053cddceaca54a5c985a425213544ab7977f
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8

You won't need a sales pitch. It's FOSS and plain text!
--9c5a427bc18e45ff56e0b8f7053cddceaca54a5c985a425213544ab7977f--

See how go-mail internally took care of the encoding, the formatting, and the different mail parts? That’s the beauty of this library. You, the user, only need to take care of what is important to you and go-mail makes sure it complies with the corresponding mail RFCs.

Local mail server #

Sometimes you don’t have an external mail service to send you emails from and all you want to do is use a local sendmail client to deliver the mail. go-mail has you covered here as well. We provide access to local sendmail programs using its own Msg.WriteToSendmail() method (or Msg.WriteToSendmailWithCommand() if your sendmail binary is not found in /usr/sbin/sendmail).

We can basically use the same code syntax as in our previous example, just with the new method:

if _, err := m.WriteToSendmail(); err != nil {
	log.Fatalf("failed to write mail to local sendmail: %s", err)
}

Middleware #

One of the principles that I introduced when I wrote go-mail was that I want to only rely on the Go standard library and not introduce any 3rd party code. This way go-mail can concentrate on its main functionality: sending emails.

With the v0.2.8 release, we received a PR for an awesome feature though: Msg.Middleware. The middleware interface allows the user to implement their own methods on the Msg without having the restriction of only the Go standard library. The interface is super easy:

type Middleware interface {
	Handle(*Msg) *Msg
}

All your code needs to do is provide a Handle method that takes a Msg pointer and in the end, returns the Msg pointer again. What happens with the mail message in between is totally up to you.

Inspired by this cool feature, I started an additional GH repository called go-mail-middleware, which is supposed to be a collection of useful middleware that interact with go-mail. For now, I’ve only added a simple subject capitalization middleware, as well as a middleware to add DKIM signatures to your emails, but I am hoping that other users will provide cool stuff there as well in the future. If you have a cool idea for mail middleware, a PR is more than welcome.

Summary #

As you can hopefully see, go-mail is a powerful tool in your Go toolbox. It not only makes sure that your emails comply with the mail RFCs it also has many small “Quality of Life” features which will make your work with emails in Go so much easier. Check out the full documentation and find out how go-mail can make your life easier.

The project might be relatively new but we already have a healthy community and according to the GitHub stats, a couple of projects already rely on go-mail for their code.

We also have a discord channel on the official Gopher Discord server. Feel free to stop by and say hello!