In this article, you’ll learn how to send HTML emails with Golang, Gomail, MongoDB-Go-Driver, Redis, and Docker-compose. Also, you’ll learn how to generate HTML templates with the standard Golang html/template package.

CRUD RESTful API with Golang + MongoDB Series:

  1. API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
  2. Golang & MongoDB: JWT Authentication and Authorization
  3. API with Golang + MongoDB: Send HTML Emails with Gomail
  4. API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
  5. Build Golang gRPC Server and Client: SignUp User & Verify Email
  6. Build Golang gRPC Server and Client: Access & Refresh Tokens
  7. Build CRUD RESTful API Server with Golang, Gin, and MongoDB

Related article:

API with Golang + MongoDB Send HTML Emails with Gomail

Send Emails with Golang, MongoDB, and Gomail Overview

A user provides the required credentials to signup for an account

registration form with no validation errors react hook form and zod

The server validates the request body, adds the user to the database, and sends a verification code to the user’s email address.

API-with-Node.js-PostgreSQL-TypeORM-email-verification-page

The user then opens the verification email and clicks on the “Verify Your Account” button.

email sent by gomail golang

The user is redirected to the email verification page and the verification code gets pre-filled in the text field.

API with Node.js + PostgreSQL + TypeORM send verification code

When the user clicks on the ‘Verify Email’ button, a GET request will be made with the verification code as a URL parameter to the server.

The server then verifies the code and updates the user’s information in the database assuming the verification code is valid.

The frontend application receives the success message and redirects the user to the login page.

API with Node.js + PostgreSQL + TypeORM email verified

Creating the HTML Email Templates with Golang

There are other template engines in Golang, like jigopongo2, or mustache, but we’ll use the official html/template package to dynamically generate the email templates.

This email template is based on a free HTML email template I copied from GitHub.

Create a templates folder in the root directory. Within the templates folder, create a file named styles.html .

templates/styles.html

 {{define "styles"}} <style> /* ------------------------------------- GLOBAL RESETS ------------------------------------- */ /*All the styling goes here*/ img { border: none; -ms-interpolation-mode: bicubic; max-width: 100%; } body { background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; } table { border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; } table td { font-family: sans-serif; font-size: 14px; vertical-align: top; } /* ------------------------------------- BODY & CONTAINER ------------------------------------- */ .body { background-color: #f6f6f6; width: 100%; } /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ .container { display: block; margin: 0 auto !important; /* makes it centered */ max-width: 580px; padding: 10px; width: 580px; } /* This should also be a block element, so that it will fill 100% of the .container */ .content { box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px; } /* ------------------------------------- HEADER, FOOTER, MAIN ------------------------------------- */ .main { background: #ffffff; border-radius: 3px; width: 100%; } .wrapper { box-sizing: border-box; padding: 20px; } .content-block { padding-bottom: 10px; padding-top: 10px; } .footer { clear: both; margin-top: 10px; text-align: center; width: 100%; } .footer td, .footer p, .footer span, .footer a { color: #999999; font-size: 12px; text-align: center; } /* ------------------------------------- TYPOGRAPHY ------------------------------------- */ h1, h2, h3, h4 { color: #000000; font-family: sans-serif; font-weight: 400; line-height: 1.4; margin: 0; margin-bottom: 30px; } h1 { font-size: 35px; font-weight: 300; text-align: center; text-transform: capitalize; } p, ul, ol { font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px; } p li, ul li, ol li { list-style-position: inside; margin-left: 5px; } a { color: #3498db; text-decoration: underline; } /* ------------------------------------- BUTTONS ------------------------------------- */ .btn { box-sizing: border-box; width: 100%; } .btn > tbody > tr > td { padding-bottom: 15px; } .btn table { width: auto; } .btn table td { background-color: #ffffff; border-radius: 5px; text-align: center; } .btn a { background-color: #ffffff; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; color: #3498db; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; } .btn-primary table td { background-color: #3498db; } .btn-primary a { background-color: #3498db; border-color: #3498db; color: #ffffff; } /* ------------------------------------- OTHER STYLES THAT MIGHT BE USEFUL ------------------------------------- */ .last { margin-bottom: 0; } .first { margin-top: 0; } .align-center { text-align: center; } .align-right { text-align: right; } .align-left { text-align: left; } .clear { clear: both; } .mt0 { margin-top: 0; } .mb0 { margin-bottom: 0; } .preheader { color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0; } .powered-by a { text-decoration: none; } hr { border: 0; border-bottom: 1px solid #f6f6f6; margin: 20px 0; } /* ------------------------------------- RESPONSIVE AND MOBILE FRIENDLY STYLES ------------------------------------- */ @media only screen and (max-width: 620px) { table.body h1 { font-size: 28px !important; margin-bottom: 10px !important; } table.body p, table.body ul, table.body ol, table.body td, table.body span, table.body a { font-size: 16px !important; } table.body .wrapper, table.body .article { padding: 10px !important; } table.body .content { padding: 0 !important; } table.body .container { padding: 0 !important; width: 100% !important; } table.body .main { border-left-width: 0 !important; border-radius: 0 !important; border-right-width: 0 !important; } table.body .btn table { width: 100% !important; } table.body .btn a { width: 100% !important; } table.body .img-responsive { height: auto !important; max-width: 100% !important; width: auto !important; } } /* ------------------------------------- PRESERVE THESE STYLES IN THE HEAD ------------------------------------- */ @media all { .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } .apple-link a { color: inherit !important; font-family: inherit !important; font-size: inherit !important; font-weight: inherit !important; line-height: inherit !important; text-decoration: none !important; } #MessageViewBody a { color: inherit; text-decoration: none; font-size: inherit; font-family: inherit; font-weight: inherit; line-height: inherit; } .btn-primary table td:hover { background-color: #34495e !important; } .btn-primary a:hover { background-color: #34495e !important; border-color: #34495e !important; } } </style> {{end}}  

Next, create a base.html file in the templates folder. You can use this base template to generate all kinds of email templates (verification email, welcome email, password reset email, etc)

I included the styles.html in the base.html via template actions, which is similar to the Jinja2 include tag.

Also, I defined a block named content which will be overridden by a child template.

templates/base.html

 {{define "base"}} <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> {{template "styles" .}} <title>{{ .Subject}}</title> </head> <body> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" > <tr> <td>&nbsp;</td> <td class="container"> <div class="content"> <!-- START CENTERED WHITE CONTAINER --> {{block "content" .}}{{end}} <!-- END CENTERED WHITE CONTAINER --> </div> </td> <td>&nbsp;</td> </tr> </table> </body> </html> {{end}}  

Finally, the verificationCode.html child template extends the base.html template and overrides the content block in the base template.

templates/verificationCode.html

 {{template "base" .}} {{define "content"}} <table role="presentation" class="main"> <!-- START MAIN CONTENT AREA --> <tr> <td class="wrapper"> <table role="presentation" border="0" cellpadding="0" cellspacing="0"> <tr> <td> <p>Hi {{ .FirstName}},</p> <p>Please verify your account to be able to login</p> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" > <tbody> <tr> <td align="left"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" > <tbody> <tr> <td> <a href="{{.URL}}" target="_blank" >Verify your account</a > </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> <p>Good luck! Codevo CEO.</p> </td> </tr> </table> </td> </tr> <!-- END MAIN CONTENT AREA --> </table> {{end}}  

Create an SMTP Provider Account

There are several SMTP providers (SendGrid, Mailgun, etc) you can use but I decided to use Mailtrap to capture the development emails instead of sending them to real email addresses.

Navigate to Mailtrap to create a new account.

create mailtrap account for golang email

Log into your Mailtrap account then click on the “Add Inbox” button to add a new inbox. Next, click on the settings icon to view the credentials page.

mailtrap account settings

On the settings page, click on the “Show Credentials” dropdown to display the SMTP credentials.

mailtrap account copy smtp configurations

Update the Environment Variables

.env

 EMAIL_FROM=admin@admin.com SMTP_HOST=smtp.mailtrap.io SMTP_USER=474944ff9f466c SMTP_PASS=37328d894f83f5 SMTP_PORT=587  

Update the Viper Configurations

Update the default.go file for Viper to load and make SMTP credentials available in the application.

config/default.go

 type Config struct {	DBUri string `mapstructure:"MONGODB_LOCAL_URI"`	RedisUri string `mapstructure:"REDIS_URL"`	Port string `mapstructure:"PORT"`	AccessTokenPrivateKey string `mapstructure:"ACCESS_TOKEN_PRIVATE_KEY"`	AccessTokenPublicKey string `mapstructure:"ACCESS_TOKEN_PUBLIC_KEY"`	RefreshTokenPrivateKey string `mapstructure:"REFRESH_TOKEN_PRIVATE_KEY"`	RefreshTokenPublicKey string `mapstructure:"REFRESH_TOKEN_PUBLIC_KEY"`	AccessTokenExpiresIn time.Duration `mapstructure:"ACCESS_TOKEN_EXPIRED_IN"`	RefreshTokenExpiresIn time.Duration `mapstructure:"REFRESH_TOKEN_EXPIRED_IN"`	AccessTokenMaxAge int `mapstructure:"ACCESS_TOKEN_MAXAGE"`	RefreshTokenMaxAge int `mapstructure:"REFRESH_TOKEN_MAXAGE"`	Origin string `mapstructure:"CLIENT_ORIGIN"`	EmailFrom string `mapstructure:"EMAIL_FROM"`	SMTPHost string `mapstructure:"SMTP_HOST"`	SMTPPass string `mapstructure:"SMTP_PASS"`	SMTPPort int `mapstructure:"SMTP_PORT"`	SMTPUser string `mapstructure:"SMTP_USER"` } func LoadConfig(path string) (config Config, err error) {	viper.AddConfigPath(path)	viper.SetConfigType("env")	viper.SetConfigName("app")	viper.AutomaticEnv()	err = viper.ReadInConfig()	if err != nil {	return	}	err = viper.Unmarshal(&config)	return }  

Create a Utility Function to Send the Emails

Install the Gomail package to send the SMTP emails

 go get gopkg.in/gomail.v2  

Install the HTML2Text package to convert the HTML to text.

 go get github.com/k3a/html2text  

utils/email.go

 package utils import (	"bytes"	"crypto/tls"	"html/template"	"log"	"github.com/k3a/html2text"	"github.com/wpcodevo/golang-mongodb/config"	"github.com/wpcodevo/golang-mongodb/models"	"gopkg.in/gomail.v2" ) type EmailData struct {	URL string	FirstName string	Subject string } // ? Email template parser func SendEmail(user *models.DBResponse, data *EmailData, temp *template.Template, templateName string) error {	config, err := config.LoadConfig(".")	if err != nil {	log.Fatal("could not load config", err)	}	// Sender data.	from := config.EmailFrom	smtpPass := config.SMTPPass	smtpUser := config.SMTPUser	to := user.Email	smtpHost := config.SMTPHost	smtpPort := config.SMTPPort	var body bytes.Buffer	if err := temp.ExecuteTemplate(&body, templateName, &data); err != nil {	log.Fatal("Could not execute template", err)	}	m := gomail.NewMessage()	m.SetHeader("From", from)	m.SetHeader("To", to)	m.SetHeader("Subject", data.Subject)	m.SetBody("text/html", body.String())	m.AddAlternative("text/plain", html2text.HTML2Text(body.String()))	d := gomail.NewDialer(smtpHost, smtpPort, smtpUser, smtpPass)	d.TLSConfig = &tls.Config{InsecureSkipVerify: true}	// Send Email	if err := d.DialAndSend(m); err != nil {	return err	}	return nil }  

Update the SignUp Controller

Next, update the SignUpUser method to send the verification email once the user has been added to the database.

 package controllers import (	"context"	"fmt"	"html/template"	"log"	"net/http"	"strings"	"time"	"github.com/gin-gonic/gin"	"github.com/thanhpk/randstr"	"github.com/wpcodevo/golang-mongodb/config"	"github.com/wpcodevo/golang-mongodb/models"	"github.com/wpcodevo/golang-mongodb/services"	"github.com/wpcodevo/golang-mongodb/utils"	"go.mongodb.org/mongo-driver/bson"	"go.mongodb.org/mongo-driver/mongo" ) type AuthController struct {	authService services.AuthService	userService services.UserService	ctx context.Context	collection *mongo.Collection	temp *template.Template } func NewAuthController(authService services.AuthService, userService services.UserService, ctx context.Context, collection *mongo.Collection, temp *template.Template) AuthController {	return AuthController{authService, userService, ctx, collection, temp} } func (ac *AuthController) SignUpUser(ctx *gin.Context) {	var user *models.SignUpInput	if err := ctx.ShouldBindJSON(&user); err != nil {	ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})	return	}	if user.Password != user.PasswordConfirm {	ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Passwords do not match"})	return	}	newUser, err := ac.authService.SignUpUser(user)	if err != nil {	if strings.Contains(err.Error(), "email already exist") {	ctx.JSON(http.StatusConflict, gin.H{"status": "error", "message": err.Error()})	return	}	ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": err.Error()})	return	}	config, err := config.LoadConfig(".")	if err != nil {	log.Fatal("Could not load config", err)	}	// Generate Verification Code	code := randstr.String(20)	verificationCode := utils.Encode(code)	// Update User in Database	ac.userService.UpdateUserById(newUser.ID.Hex(), "verificationCode", verificationCode)	var firstName = newUser.Name	if strings.Contains(firstName, " ") {	firstName = strings.Split(firstName, " ")[1]	}	// ? Send Email	emailData := utils.EmailData{	URL: config.Origin + "/verifyemail/" + code,	FirstName: firstName,	Subject: "Your account verification code",	}	err = utils.SendEmail(newUser, &emailData, ac.temp, "verificationCode.html")	if err != nil {	ctx.JSON(http.StatusBadGateway, gin.H{"status": "success", "message": "There was an error sending email"})	return	}	message := "We sent an email with a verification code to " + user.Email	ctx.JSON(http.StatusCreated, gin.H{"status": "success", "message": message}) }  

Update the Main File

 package main import (	"context"	"fmt"	"html/template"	"log"	"net/http"	"github.com/gin-contrib/cors"	"github.com/gin-gonic/gin"	"github.com/go-redis/redis/v8"	"github.com/wpcodevo/golang-mongodb/config"	"github.com/wpcodevo/golang-mongodb/controllers"	"github.com/wpcodevo/golang-mongodb/routes"	"github.com/wpcodevo/golang-mongodb/services"	"go.mongodb.org/mongo-driver/mongo"	"go.mongodb.org/mongo-driver/mongo/options"	"go.mongodb.org/mongo-driver/mongo/readpref" ) var (	server *gin.Engine	ctx context.Context	mongoclient *mongo.Client	redisclient *redis.Client	userService services.UserService	UserController controllers.UserController	UserRouteController routes.UserRouteController	authCollection *mongo.Collection	authService services.AuthService	AuthController controllers.AuthController	AuthRouteController routes.AuthRouteController	temp *template.Template ) func init() {	temp = template.Must(template.ParseGlob("templates/*.html"))	config, err := config.LoadConfig(".")	if err != nil {	log.Fatal("Could not load environment variables", err)	}	ctx = context.TODO()	// Connect to MongoDB	mongoconn := options.Client().ApplyURI(config.DBUri)	mongoclient, err := mongo.Connect(ctx, mongoconn)	if err != nil {	panic(err)	}	if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil {	panic(err)	}	fmt.Println("MongoDB successfully connected...")	// Connect to Redis	redisclient = redis.NewClient(&redis.Options{	Addr: config.RedisUri,	})	if _, err := redisclient.Ping(ctx).Result(); err != nil {	panic(err)	}	err = redisclient.Set(ctx, "test", "Welcome to Golang with Redis and MongoDB", 0).Err()	if err != nil {	panic(err)	}	fmt.Println("Redis client connected successfully...")	// Collections	authCollection = mongoclient.Database("golang_mongodb").Collection("users")	userService = services.NewUserServiceImpl(authCollection, ctx)	authService = services.NewAuthService(authCollection, ctx)	AuthController = controllers.NewAuthController(authService, userService, ctx, authCollection, temp)	AuthRouteController = routes.NewAuthRouteController(AuthController)	UserController = controllers.NewUserController(userService)	UserRouteController = routes.NewRouteUserController(UserController)	server = gin.Default() } func main() {	config, err := config.LoadConfig(".")	if err != nil {	log.Fatal("Could not load config", err)	}	defer mongoclient.Disconnect(ctx)	value, err := redisclient.Get(ctx, "test").Result()	if err == redis.Nil {	fmt.Println("key: test does not exist")	} else if err != nil {	panic(err)	}	corsConfig := cors.DefaultConfig()	corsConfig.AllowOrigins = []string{config.Origin}	corsConfig.AllowCredentials = true	server.Use(cors.New(corsConfig))	router := server.Group("/api")	router.GET("/healthchecker", func(ctx *gin.Context) {	ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": value})	})	AuthRouteController.AuthRoute(router, userService)	UserRouteController.UserRoute(router, userService)	log.Fatal(server.Run(":" + config.Port)) }  

Conclusion

Congrats on reaching the end. In this article, you learned how to send emails in Golang with Gomail and the standard Golang html/template package.

Check out the source codes: