• For devs

Delivering HTML Emails With Mailgun-Go

Derrick Wippler
5 min read

In this tutorial, I will demonstrate how you can send HTML emails with embedded images with mailgun-go. Before we dive into the code, lets first define the problem space and how we can use Mailgun to enhance the user experience of our application.

Introducing Channel-stats

Channel-stats is a Slack bot for collecting statistics on messages sent to a Slack channel. In addition to collecting counts of emoji’s and links shared in the channel, it also performs sentiment analysis of the messages and provides a positive or negative score for the messages which can later be reviewed by users and graphed as a percentage of total messages.

We want to expand on this capability with a weekly email report on the statistics of a channel to our users. Since our email will include graphed data, plain text emails would be pretty boring, instead, we want to send rich HTML email with graphs to our user’s inbox. To accomplish this we need to craft some HTML, with inline CSS and images to make it visually appealing.

HTML Email

A lot has been written on the subject of sending HTML in emails but here are a few good rules to follow:

  • DO use inline CSS

  • DO use HTML TABLES for layout

  • DO use images (prefer .png)

  • DO inline images

  • DON’T use HTML5

  • DON’T use animation CSS

  • DON’T  link to an external stylesheet

  • DON’T use CSS styles in the HEAD

  • DON’T use javascript

  • DON’T use flash

It’s often not enough to follow the above rules as there are no established standards on how HTML in emails is rendered. If you are committed to having your emails rendered correctly on as many clients as possible you might consider using a service like Litmus to build, preview, and test your email across a variety of clients. However, for our purpose and since channel-stats is an open source project, I’m keeping production costs low and using some free templates provided by Mailgun (I did get some help from one our UX/UI designers). The result looks like the following:

Now that we have our HTML and CSS, we need to inline the CSS so the majority of email clients will render our email properly. There are a variety of online tools to accomplish this, however we recommend Dialect Premailer for this purpose.

The Code

Since we want the email to be sent on a weekly basis we use a cron library to create a function that will run every Sunday night at midnight. Next, we need to generate the images that will go into our email. Channel-stats already uses go-chart to render .png chart images for the UI, so we can just adapt that for our purposes.

Additionally, when shipping our final project we don’t want to distribute HTML and CSS files separately from our final compiled golang binary, so channel-stats uses the go-bindata project to bundle HTML and CSS into a single channel-stats binary.

Now let’s take a look at the render code.

1func NewReporter(conf Config, list ChanLister, notify Mailer, store Storer) (Reporter, error) {
2 r := Report{
3 log: GetLogger().WithField("prefix", "reporter"),
4 cron: cron.New(),
5 mail: notify,
6 store: store,
7 conf: conf,
8 list: list,
9}
10 return &r, r.start()
11}
12
13func (r *Report) start() error {
14 err := r.cron.AddFunc(r.conf.Report.Schedule, func() {
15 timeRange := toTimeRange(r.conf.Report.ReportDuration.Duration)
16 r.log.Debugf("Creating report for %s to %s", timeRange.Start, timeRange.End)
17
18 for _, channel := range r.list.Channels() {
19 // Skip channels the bot is not in
20 if !channel.IsMember {
21 continue
22 }
23
24 html, err := r.genHtml("html/templates/email.tmpl", channel.Name)
25 if err != nil {
26 r.log.Errorf("during email generate: %s", err)
27 return
28 }
29
30 data := ReportData{
31 Images: make(map[string][]byte),
32 Html: html,
33 }
34
35 // Generate the images for the report
36 data.Images["most-active.png"] = r.genImage(RenderSum, timeRange, channel.Id, "messages")
37 data.Images["top-links.png"] = r.genImage(RenderSum, timeRange, channel.Id, "link")
38 data.Images["top-emoji.png"] = r.genImage(RenderSum, timeRange, channel.Id, "emoji")
39 data.Images["most-negative.png"] = r.genImage(RenderPercentage, timeRange, channel.Id, "negative")
40 data.Images["most-positive.png"] = r.genImage(RenderPercentage, timeRange, channel.Id, "positive")
41
42 // Email the report
43 if err := r.mail.Report(channel.Name, data); err != nil {
44 r.log.Errorf("while sending report: %s", err)
45 }
46 }
47 })
48 if err != nil {
49 return err
50 }
51
52 r.cron.Start()
53 return nil
54}

In the Start() method we iterate through all the channels the bot is a member of and generate a report for each channel, We then make a call to genHtml() which retrieves our HTML email as a template called templates/email.tmpl from our compiled asset store in the HTML package. We then run the template through golang standard HTML/template engine to produce the final HTML. Next genImage() calls the render function with the range of hours and the type of counter we want to retrieve from the data store. Once ReportData is complete, we pass the data to mail.Report() for delivery.

Now that we have our images and HTML, let’s pause and talk a little about HTML MIME and image encoding. MIME is the format which email bodies are encoded to when sent via the SMTP protocol. It is the format that allows email clients to encode HTML, attach and retrieve files and images in an email.

In order for our images to display properly in HTML, we have to encode the images into the MIME. For this we have 2 options: we could add the images as an attachment, or we could inline the images. The RFC on Content disposition says that inline indicates the entity should be immediately displayed to the user, whereas attachment means that the user should take additional action to view the entity. Since our images are to be displayed immediately to the user via HTML — we choose inline.

At this point, we could use any number of MIME libraries for golang to inline our images and generate the body of the email in MIME format, but with Mailgun, we don’t have to. Mailgun will generate the MIME for us and provides options to inline files and images via the public API.

Now that we know how to inline images into the MIME, we have to reference them from our HTML. To do this, we use the cid: prefix in our <img> tags. Such that if our inlined image is called most-active.png our image tag would be <img src="cid:most-active.png">

With our HTML ready, let’s look at how we send the email and images with mailgun-go.

1func NewMailgunNotifier(conf Config) (Mailer, error) {
2 return &Mailgun{
3 mg: mailgun.NewMailgun(conf.Mailgun.Domain, conf.Mailgun.APIKey),
4 log: GetLogger().WithField("prefix", "mailer"),
5 conf: conf,
6 }, nil
7}
8
9// Send a report to the designated email address (could be mailing list)
10func (m *Mailgun) Report(channelName string, data ReportData) error {
11 if m.conf.Mailgun.ReportAddr == "" {
12 m.log.Errorf("mailgun.enabled = true; however mailgun.report-address is empty; skipping..")
13 return nil
14 }
15
16 // Create a subject for the report
17 subject := fmt.Sprintf("[channel-stats] Report for %s", channelName)
18 // Create a message with no text body
19 message := m.mg.NewMessage(m.conf.Mailgun.From, subject, "", m.conf.Mailgun.ReportAddr)
20 // Send the HTML to mailgun for MIME encoding
21 message.SetHtml(string(data.Html))
22
23 for file, contents := range data.Images {
24 message.AddReaderInline(file, ioutil.NopCloser(bytes.NewBuffer(contents)))
25 }
26
27 ctx, cancel := context.WithTimeout(context.Background(), m.conf.Mailgun.Timeout.Duration)
28 defer cancel()
29
30 _, id, err := m.mg.Send(ctx, message)
31 if err != nil {
32 return err
33 }
34 m.log.Infof("Sent report via mailgun (%s)", id)
35 return nil
36}

First, we create a new instance of Mailgun using our domain name and API key in NewMailgunNotifier(). Next in the Report() method we call NewMessage() to craft an object to which we will add our HTML and images. Notice the text argument to NewMessage() is empty string. While it is possible to encode both plain text and HTML into the MIME message, we only provide HTML here because inline chart images would be useless to a text-only client. Next, we call SetHtml() and append our inline images via a read closer object which we create on the fly from our []byte buffer. Finally, we ship our crafted request to the mailgun API for MIME construction and delivery using the Send() method.

Conclusion

Hopefully, this tutorial has given some insight on how to deliver high-quality HTML based emails using mailgun-go and the Mailgun API. If you have feedback or find bugs in either project the complete code and library can be found below.

Interested in working at Mailgun? We’re hiring! And there are several dev positions available.Check out our current openings here.

Featured Webinar – Predictions & Resolutions: Sending in 2019

Miss out on this webinar at the beginning of 2019? Don't worry, we recorded it! Rewatch Nick and Natalie talk about some things that happened in email in 2018, and what they thought was ahead for 2019. Technical and marketing met in the middle on this one, and you can rewatch it here.

sign up

It's easy to get started. And it's free.

What will you accomplish with 10,000 free emails and a 100 free validations every month?

Sign Up For Free

Last updated on September 24, 2019

  • Related posts
  • Recent posts
  • Top posts
View all

Always be in the know and grab free email resources!

No spam, ever. Only musings and writings from the Mailgun team.

Mailgun is committed to protecting your privacy. Please read ourPrivacy Policybefore providing us with your details.

sign up

It's easy to get started. And it's free.

What will you accomplish with 10,000 free emails and a 100 free validations every month?
Sign up for Free