Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


We will try to send emails (with attachments) via SMTP with Golang. We will also explore some possibilities of using queuing/messaging service like RabbitMQ for splitting email-sending service into another service/application.

Overview

There are times when businesses/products need to send emails to users (customers or business partners), whether it is transactional or marketing email or both. Email nowaday is still a legit choice for communicating with users. Email is not only about sending messages between users/businesses, email is also the primary identity for a user on the internet other than phone number.

We usually have our user accounts in every place on internet being tied to our email account. Log in? Place email address. Forgot your password? Confirm via the email address. Our bills and our salaries may also be found in our email inboxes. Email is still important, it’s part of our modern culture nowadays (and won’t likely go away in near decades), that’s why every product/business on the internet need to have mechanism for sending email to users, whether for communication or promotional/marketing purpose.

But we’re not here to talk about the importance of email in business/social perspective. Here, we’re going to write the technical aspects of it, using Golang.

Email in its raw form is just bytes transmitted over internet. Those bytes collectively form a text that adheres to a standarized email protocol. There are some protocols designed that are adopted widely: SMTP, IMAP and POP3. In this article, we will write Golang code that sends emails with SMTP protocol.

Our Golang Code

We are going to write some code for sending email via SMTP, along with it are some attachment files. We can just write a code to open a connection for sending the email and compose such text. Here’s how the raw text might look like (notes the provided headers and base64-encoded text for the attachment).

Fortunately for us, there are several open-source packages that we can use so we don’t need to do those things manually. We are going to use: https://github.com/go-gomail/gomail

Before we begin, for the purpose of demonstration, we need to set up 2 email addresses, one for sending and one is the destination.

The sender email address is ours (we can also use a service, there are plenty options, notable ones are like Mailgun, Sendgrid or Amazon SES), we provide the hostname, username and password for authentication.

While the destination email address is the one we want to send our email to. In test environment we mustn’t use users’ email, instead we create a new email address used only in test environment. But fortunately for us again, we don’t need to have those in place in order to make sure that our code runs correctly. We can just use a service like https://mailtrap.io/.

With these tools in place, let’s set up our Golang app. (Use your own github account for initializing the new module via go mod init below).

$ mkdir golang-smtp
$ cd golang-smtp
$ go mod init github.com/dwahyudi/golang-smtp

And do not forget to install the gomail library as well.

$ go get gopkg.in/gomail.v2

The next step is to setup a demo inbox in https://mailtrap.io/. After creating an account, they will provide us with an inbox ready for us to use. This inbox has certain settings that we need to follow. This inbox show us how our email will look like.

Now, by using the gomail library we have installed, we can write a demonstration code for sending an email. Here we will send an HTML code, along with some images as attachments.

The code will be fairly simple, we need a struct type to represent an email request. Here the struct specifies To and Attachment as array of strings, because in a single email request, we may want to send it to multiple recipients with multiple file attachments.

This struct will be placed in util package.

/*
Mail is a generic struct type for representing a mail send request.
*/
type Mail struct {
	From       string
	To         []string
	Subject    string
	BodyType   string
	Body       string
	Attachment []string
}

The Mail struct data will be passed to MailSend() function below. Please supply the envs with values provided by mailtrap.

This function will handle sending to multiple recipients and multiple file attachments. gomail package provide us with convenient API for doing so.

This function will be placed in util package.

/*
MailSend sends email with settings configured by envs.
*/
func MailSend(mail Mail) {
	m := gomail.NewMessage()
	m.SetHeader("From", mail.From)
	m.SetHeader("To", mail.To...)
	m.SetHeader("Subject", mail.Subject)
	m.SetBody(mail.BodyType, mail.Body)
	if len(mail.Attachment) > 0 {
		for _, attachment := range mail.Attachment {
			m.Attach(attachment)
		}
	}

	port, err := strconv.Atoi(os.Getenv("SMTP_PORT"))
	CheckErr(err)
	d := gomail.NewDialer(os.Getenv("SMTP_HOSTNAME"),
		port,
		os.Getenv("SMTP_USERNAME"),
		os.Getenv("SMTP_PASSWORD"))

	err = d.DialAndSend(m)
	CheckErr(err)
}

/*
CheckErr checks for error, and log fatal if it is not nil.
*/
func CheckErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

And here is our demonstration code, first we build the Mail struct data then we send it to the MailSend function. Here we send along some images as attachments (there are some images prepared in temp/ directory).

package demo

import "github.com/dwahyudi/golang-smtp/util"

func SimpleMailDemo() {
	mail := util.Mail{
		From:       "no-reply@example00.com",
		To:         []string{"user01@example99.com", "user02@example.com"},
		Subject:    "Sample Subject",
		BodyType:   "text/html",
		Body:       "<html><body><p>Sample body</p></body></html>",
		Attachment: []string{"temp/cat.jpg", "temp/orange.jpg"},
	}

	util.MailSend(mail)
}

Call that demo function from main function, and run the application, here’s how our email will look like in mailtrap inbox:

This is a full html email, if we have a fancy email template we can use it. Golang provides html templating package that we can utilize, so we can have reusable html templates for email, but that’s out of scope of this article.

Extracting the Code

Now, we’re done writing code for sending html email with attachments. How can we explore more?

The code that’s calling SimpleMailDemo() and sending the email is in the same codebase, we can just call that function with goroutine like: go SimpleMailDemo() (so it won’t block main thread/web server), but it still runs in the same Golang instance.

What if we want to separate the code for sending email into another instance/app? So each instance/app have dedicated resources and easier to scale and maintain (there might be a case where the number of emails need to be sent is big that it might disrupt the resources of main application).

To do this we need to have a dedicated storage for saving the message. But this storage will need to notify and give the message to the email service as well, something like a pubsub service.

  • An app we call it publisher, will order the email sending request, send data to a pubsub service.
  • The email service will subscribe to the pubsub service, waits for messages.
  • Once data is available at a pubsub service, either the pubsub service push the data to email service, or the email service pull the data from the pubsub service.
  • Once data is by email service, send the email based on the received data.

There are plenty options available for us to choose, but in this article, we’re going to use RabbitMQ. Let’s install RabbitMQ: https://www.rabbitmq.com/download.html. Once installed, we can run RabbitMQ server with rabbitmq-server.

We’re going to make the existing golang-smtp app as the email service which will wait messages from RabbitMQ. For demonstration purpose, let’s create a new function (and a new package) for sending email, but this time, we have a destination email address emailAddress as the param. This param value will be filled with value received from RabbitMQ.

package emailing

import (
	"github.com/dwahyudi/golang-smtp/util"
)

/*
RegistrationWelcomeSend send registration email to designated email address.
*/
func RegistrationWelcomeSend(emailAddress string) {
	mail := util.Mail{
		From:       "no-reply@hogwartz.com",
		To:         []string{emailAddress},
		Subject:    "Welcome to Hogwartz",
		BodyType:   "text/html",
		Body:       registrationMailBody(),
		Attachment: []string{"temp/hogwartz.jpg", "temp/owl.jpg", "temp/apprentice-equip-list"},
	}

	util.MailSend(mail)
}

func registrationMailBody() string {
	return "<html><body><p>We pleased to inform you that you have a place at Hogwartz School of Witchcraft and Wizardry. <br/>Please find enclosed a list of necessary books and equipments.</p></body></html>"
}

Receiving

And here’s how our code will look like in order to wait for messages from RabbitMQ.

Make sure that we supply RABBITMQ_URL env, it should look like this:

amqp://username:password@localhost:5672/

Be eager to look at queueName value below, in our case, the sender/publisher and the receiver must agree on the same queue name.

func emailSendWaiter() {
	queueName := "registration-email-welcome"

	conn, err := amqp.Dial(os.Getenv("RABBITMQ_URL"))
	util.CheckErr(err)
	defer conn.Close()

	ch, err := conn.Channel()
	util.CheckErr(err)
	defer ch.Close()

	q, err := ch.QueueDeclare(
		queueName,   // name
		false,       // durable
		false,       // delete when unused
		false,       // exclusive
		false,       // no-wait
		nil,         // arguments
	)
	util.CheckErr(err)

	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	util.CheckErr(err)

	forever := make(chan bool)

	go func() {
		for d := range msgs {
			emailAddress := string(d.Body)
			emailing.RegistrationWelcomeSend(emailAddress)
		}
	}()

	log.Printf(" [*] Waiting for messages. To exit press CTRL+C")
	<-forever
}

Above code will run forever, because it waits messages from RabbitMQ, but it shouldn’t block the main thread or web server if we run it in a dedicated Goroutine.

When we run the code, it will just waits for messages (destination email address that we will send our email to) from RabbitMQ. Don’t stop/exit this app, keep it running.

Sending

We can then send data/message to that RabbitMQ, we can use another programming language if we want, but here, let’s write another Golang app.

$ mkdir golang-rabbitmq-sender
$ cd golang-rabbitmq-sender
$ go mod init github.com/dwahyudi/golang-rabbitmq-sender

And here’s the code for publishing/sending data to RabbitMQ. Please take a look at registration-email-welcome below, it is the queue name that we want to publish our data to. This code will publish messages to that queue, and previous receiver code will receive the data from it.

package main

import (
	"log"
	"os"

	"github.com/streadway/amqp"
)

func main() {
	sendDemo()
}

func sendDemo() {
	conn, err := amqp.Dial(os.Getenv("RABBITMQ_URL"))
	checkErr(err)
	defer conn.Close()

	ch, err := conn.Channel()
	checkErr(err)
	defer ch.Close()

	q, err := ch.QueueDeclare(
		"registration-email-welcome", // name
		false,                        // durable
		false,                        // delete when unused
		false,                        // exclusive
		false,                        // no-wait
		nil,                          // arguments
	)
	checkErr(err)

	err = ch.Publish(
		"",     // exchange
		q.Name, // routing key
		false,  // mandatory
		false,  // immediate
		amqp.Publishing{
			ContentType: "text/plain",
			Body:        []byte("harrypotter@hogwartz.com"),
		})
	checkErr(err)
}

/*
CheckErr checks for error, and log fatal if it is not nil.
*/
func checkErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

Run this code, while receiving code is waiting. So we have 2 Golang apps running at the same time. If everything goes well, this sending code will send harrypotter@hogwartz.com to registration-email-welcome queue in RabbitMQ. RabbitMQ will then push this value to receiver code (because the receiver code subscribes to that queue), and finally the receiver code will send the email to that address via emailing.RegistrationWelcomeSend(emailAddress).

Here’s the code for receiving: https://github.com/dwahyudi/golang-smtp

And here’s the code for sending: https://github.com/dwahyudi/golang-rabbitmq-sender