- Best Practices
Golang’s Superior Cache Solution to Memcached and Redis
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.
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.
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.
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}1213func (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)1718 for _, channel := range r.list.Channels() {19 // Skip channels the bot is not in20 if !channel.IsMember {21 continue22 }2324 html, err := r.genHtml("html/templates/email.tmpl", channel.Name)25 if err != nil {26 r.log.Errorf("during email generate: %s", err)27 return28 }2930 data := ReportData{31 Images: make(map[string][]byte),32 Html: html,33 }3435 // Generate the images for the report36 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")4142 // Email the report43 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 err50 }5152 r.cron.Start()53 return nil54}
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 }, nil7}89// 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 nil14 }1516 // Create a subject for the report17 subject := fmt.Sprintf("[channel-stats] Report for %s", channelName)18 // Create a message with no text body19 message := m.mg.NewMessage(m.conf.Mailgun.From, subject, "", m.conf.Mailgun.ReportAddr)20 // Send the HTML to mailgun for MIME encoding21 message.SetHtml(string(data.Html))2223 for file, contents := range data.Images {24 message.AddReaderInline(file, ioutil.NopCloser(bytes.NewBuffer(contents)))25 }2627 ctx, cancel := context.WithTimeout(context.Background(), m.conf.Mailgun.Timeout.Duration)28 defer cancel()2930 _, id, err := m.mg.Send(ctx, message)31 if err != nil {32 return err33 }34 m.log.Infof("Sent report via mailgun (%s)", id)35 return nil36}
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.
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.
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.
Learn about our Deliverability Services
Looking to send a high volume of emails? Our email experts can supercharge your email performance. See how we've helped companies like Lyft, Shopify, Github increase their email delivery rates to an average of 97%.
Last updated on May 17, 2021
Golang’s Superior Cache Solution to Memcached and Redis
HTTP/2 Cleartext (H2C) Client Example in Go
How we built a Lucene-inspired parser in Go
What Toasters And Distributed Systems Might Have In Common
Introducing A Cross-Platform Debugger For Go
The Official Go SDK, Available Now
Designing HTML Email Templates For Transactional Emails
5 Ideas For Better Developer-Designer Collaboration
What Is a RESTful API, How It Works, Advantages, and Examples
How to Improve the Way WordPress Websites Send Email
InboxReady x Salesforce: The Key to a Stronger Email Deliverability
Become an Email Pro With Our Templates API
Google Postmaster Tools: Understanding Sender Reputation
Navigating Your Career as a Woman in Tech
Implementing Dmarc – A Step-by-Step Guide
Email Bounces: What To Do About Them
Announcing InboxReady: The deliverability suite you need to hit the inbox
Black History Month in Tech: 7 Visionaries Who Shaped The Future
How To Create a Successful Triggered Email Program
Designing HTML Email Templates For Transactional Emails
InboxReady x Salesforce: The Key to a Stronger Email Deliverability
Implementing Dmarc – A Step-by-Step Guide
Announcing InboxReady: The deliverability suite you need to hit the inbox
Designing HTML Email Templates For Transactional Emails
Email Security Best Practices: How To Keep Your Email Program Safe
Mailgun’s Active Defense Against Log4j
Email Blasts: The Dos And Many Don’ts Of Mass Email Sending
Email's Best of 2021
5 Ideas For Better Developer-Designer Collaboration
Mailgun Joins Sinch: The Future of Customer Communications Is Here
Always be in the know and grab free email resources!
By sending this form, I agree that Mailgun may contact me and process my data in accordance with its Privacy Policy.