Send email using SMTP in Go with and without Transport Layer Security

Nikhil Shrestha
readytowork, Inc.
Published in
5 min readJul 24, 2023

Writing email is an integral part of our professional life. It is a standard and professional channel to establish communication between two people or parties. While we may use different domains for email, as a developer, we might face a situation where we might have to integrate it in our system as a major feature.

In this article, we will be going through a similar scenario. We will implement email using SMTP in Go.

Before jumping into the coding part, let’s look at the project structure:

First, lets have a look at the .env file. Here, we will set up the environment variables for our program.

SMTP_HOST=YOUR SMTP_HOST
SMTP_PORT=YOUR SMTP_PORT
EMAIL_FROM=YOUR EMAIL_FROM
EMAIL_PASSWORD=YOUR EMAIL_PASSWORD

Moving to the email.go file

Here we will be looking at two different approaches first using simply SMTP and then, adding Transport Layer Security (TLS).

First, let's look at the common section for both cases:

type Body struct {
To string `json:"email_to"`
}

Here, we have defined a struct, named Body with a field To, which we will use later on to bind json from context.

Next, we will create a function named SendEmail

func SendEmail(c *gin.Context) {
var data Body
if err := c.BindJSON(&data); err != nil {
//Handle error
}

if err := godotenv.Load(".env"); err != nil {
//Handle error
}
emailFrom := os.Getenv("EMAIL_FROM")
emailPassword := os.Getenv("EMAIL_PASSWORD")
emailTo := data.To
smtpHost := os.Getenv("SMTP_HOST")
smtpPort := os.Getenv("SMTP_PORT")

Inside the SendEmail function, first we will use the struct defined earlier to bind json from context. Then, we will load environment variables from .env file and initialize the values to different variables accordingly.

// Generate a random Message-ID
r := rand.New(rand.NewSource(time.Now().UnixNano()))
messageID := strconv.FormatInt(r.Int63(), 10) + "@" + smtpHost

messageBody := "Thank you for reading this article."

message := "From: " + emailFrom + "\n" +
"To: " + emailTo + "\n" +
"Subject: " + "This is a subject" + "\n" +
"MIME-version: 1.0;\n" +
"Content-Type: text/html; charset=\"UTF-8\";\n" +
"Message-ID: <" + messageID + ">\n\n" +
messageBody

Next, we will generate a random message id for our email. This step might be avoidable depending on your email host. Then, we will create an overall message of our email where we will initialize the headers for email,i.e., email host, email recipient, subject of the email, MIME-version, Content-Type, and Message-ID, and finally the message body.

The Content-Type for the email may vary depending upon the content of the body.

// Set up authentication
auth := smtp.PlainAuth("", emailFrom, emailPassword, smtpHost)

The last piece of code we will be writing before jumping into our cases is for setting up authentication. We will be using PlainAuth the function, which returns authentication, of the smtp package. As mentioned in the name of the function, the returned authentication implements a plain authentication mechanism using the provided credentials.

Case 1: Plain SMTP

We can send the email by using a function of SMTP.

err := smtp.SendMail(smtpHost+":"+smtpPort, auth, emailFrom, []string{emailTo}, []byte(message))
if err != nil {
//Handle error
}

Case 2: Adding Transport Layer Security Connection

// TLS configuration
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: smtpHost,
}

conn, err := tls.Dial("tcp", smtpHost+":"+smtpPort, tlsConfig)
if err != nil {
//Handle error
}

client, err := smtp.NewClient(conn, smtpHost)
if err != nil {
//Handle error
}

// Authenticate
if err := client.Auth(auth); err != nil {
//Handle error
}

// Set the sender
if err := client.Mail(emailFrom); err != nil {
//Handle error
}

// Set the recipient
if err := client.Rcpt(emailTo); err != nil {
return
}

wc, err := client.Data()
if err != nil {
//Handle error
}

_, err = wc.Write([]byte(message))
if err != nil {
//Handle error
}
err = wc.Close()
if err != nil {
//Handle error
}

c.IndentedJSON(http.StatusOK, gin.H{
"response": "success",
"message": "email sent",
})

Breaking down the code:

tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: smtpHost,
}

Firstly, we have created an object tlsConfig to configure TLS connection settings. The setting includes the InsecureSkipVerify field set to false and ServerName the field set to the SMTP host.

The setting for InsecureSkipVerify can vary depending on the server as it is used to verify the server’s certificate.

conn, err := tls.Dial("tcp", smtpHost+":"+smtpPort, tlsConfig)
if err != nil {
//Handle error
}

Then, we established a TLS connection using the Dial function, which returns a connection object initialized as conn

client, err := smtp.NewClient(conn, smtpHost)
if err != nil {
//Handle error
}

We have established a new SMTP client by passing the TLS connection and SMTP host.

// Authenticate
if err := client.Auth(auth); err != nil {
//Handle error
}

// Set the sender
if err := client.Mail(emailFrom); err != nil {
//Handle error
}

// Set the recipient
if err := client.Rcpt(emailTo); err != nil {
return
}

In the code above, we have authenticated the client with the SMTP server using the authentication created from PlainAuth the function. Then, we have set the sender and recipient to the client.

wc, err := client.Data()
if err != nil {
//Handle error
}

_, err = wc.Write([]byte(message))
if err != nil {
//Handle error
}
err = wc.Close()
if err != nil {
//Handle error
}

c.IndentedJSON(http.StatusOK, gin.H{
"response": "success",
"message": "email sent",
})

Now, we have used the Data method to obtain a writer and initialized it as wc . We have used the Write method of the writer to write the message for the email and after that close the writer. With these steps, the email is sent to the concerned recipient with Transport Layer Security.

Moving to the main.go file

package main

import (
"github.com/gin-gonic/gin"
"github.com/stha-nikhil/go-email/api"
)

func main() {
router := gin.Default()
router.POST("/send-email", api.SendEmail)
router.Run()
}

Here, we have handled the routing for our program. Firstly, we imported the required packages and then, set up a route using the Gin router to call the SendEmailmethod.

Conclusion

Since we have gone through the code for both of the cases, let's dig deeper into their comparison.

Case 1 is a simpler approach to sending email using SMTP. In this case, no kind of encryption is set up. Case 2 is a more complex approach with multiple steps but includes TLS encryption and is more secure in nature. Now, moving on to the question that we all might be having:

Which of these approaches should we adopt?

Well, it depends!

It all depends on the SMTP server you are using to set up your email service. Some SMTP servers may only support plain TCP, while others may require some kind of TLS configuration. Hence, the choice may vary as per the requirement and the use case of the SMTP server that you are using.

I appreciate your reading. Hope your email finds its way!

--

--