Simple Go Lang web service to send emails πŸ§Έβœ‰οΈ

Simple Go Lang web service to send emails πŸ§Έβœ‰οΈ
Photo by Chinmay B / Unsplash

In this article, we'll be creating a simple Go application that acts as a REST Service.

The application will have one end-point /api/contact will run on port 8080.

Let's start.

What is Go 🧸 ?

Go is a simple and efficient programming language created by Google. It is known for its clean syntax, strong concurrency support, and excellent performance.

Learn more about Go: https://go.dev

Preparing the Go environment

I'm using Ubuntu 20 for this tutorial, If you need to install go on another platform you can refer to the official documentation: https://go.dev/dl/

On Ubuntu installing Go is as simple as running

apt update && apt install golang

and then adding the go binaries to the system path by updating my ~/.bashrc with these two lines.

export GOPATH=$HOME/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin

To explain, the lines added above are shell commands commonly used in Go development environments to set up the necessary environment variables.

The first line, export GOPATH=$HOME/go, sets the value of the GOPATH environment variable to the path $HOME/go. The GOPATH is an important variable in Go that specifies the root directory for Go projects and their dependencies.

In this case, $HOME/go is the path to the go directory within the user's home directory ($HOME). This is where Go packages and binaries will be stored.

The second line, export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin, modifies the PATH environment variable to include the Go binary directories. It appends :/usr/local/go/bin:$GOPATH/bin to the existing PATH.

  • /usr/local/go/bin is the path where the Go compiler (go) and other Go tools are typically installed.
  • $GOPATH/bin is the path where Go binaries (executables) are installed when using the go install command.

By adding these directories to the PATH, the system will be able to locate and execute Go-related commands and binaries from anywhere in the shell. This allows for convenient usage of Go tools and running Go programs without specifying their full paths.

Project structure

Our project structure will be like this:

  • email-handler.go: Contains the request handler logic for the email service.
  • email-sender.go: Implements the business logic for sending emails.
  • models.go: Defines the data models used within the email service.
  • main.go: Serves as the entry point of the application.
  • go.mod: Specifies the project's module and its dependencies.
  • email-template.html: A template for the mail that we'll send.
go-email-service
β”œβ”€β”€ email-handler.go    
β”œβ”€β”€ email-sender.go     
β”œβ”€β”€ models.go          
β”œβ”€β”€ main.go             
β”œβ”€β”€ email-template.html     
β”œβ”€β”€ config.yaml         
└── go.mod

In go.mod we'll define the module name and the required Go version for the project.

We will need the yaml package to read SMTP server configuration from the config file

module main

go 1.20

require gopkg.in/yaml.v2 v2.4.0 // indirect

In the file main.go we will load SMTP server configuration from yaml file and then set up an HTTP server with an endpoint for sending emails and start the server on port 8080.

config.yaml

smtpHost: example-smtpserver.emailprovider.com
smtpPort: 465
smtpEmail: [email protected]
smtpPassword: example-sender-password

main.go

package main

import (
	"log"
	"net/http"
	"os"

	"gopkg.in/yaml.v2"
)

// Global SMTP server configuration variable
var smtpConfig SMTPConfig

// Load SMTP server configuration from the YAML file
func loadSMTPConfig(filename string) error {
	data, err := os.ReadFile(filename)
	if err != nil {
		return err
	}

	err = yaml.Unmarshal(data, &smtpConfig)
	if err != nil {
		return err
	}

	return nil
}

func main() {

	err := loadSMTPConfig("config.yaml")
	if err != nil {
		log.Fatalf("Failed to load SMTP configuration: %v", err)
	}

	// Define the endpoint for sending the email
	http.HandleFunc("/api/book", SendEmailHandler)

	// Start the HTTP server
	log.Println("Server listening on http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

For email-hander.go we can create our handler function SendEmailHandler receives HTTP requests, parses a JSON request body, composes email messages using HTML template, and sends it concurrently using goroutines. Finally, it sends a response back to the client indicating successful email delivery. (more about goroutines here )

package main

import (
	"encoding/json"
	"fmt"
	"bytes"
	"html/template"
	"log"
	"net/http"
	"sync"
)

// Handler function for sending email
func SendEmailHandler(w http.ResponseWriter, r *http.Request) {
	log.Println("Received request /api/contact")

	// Parse the JSON request body
	var requestBody RequestBody
	err := json.NewDecoder(r.Body).Decode(&requestBody)
	if err != nil {
		http.Error(w, "Failed to parse request body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()

	// Send the email concurrently
	var wg sync.WaitGroup
	wg.Add(1)

	// Compose the email message
	subject := fmt.Sprintf("Contact received for %s", requestBody.Name)
	body, err := getEmailBody("email-template.html", requestBody)
	if err != nil {
		log.Println("Failed to read email template:", err)
		http.Error(w, "Failed to read email template", http.StatusInternalServerError)
		return
	}

	go SendEmail(subject, body, requestBody.RecipientEmail, &wg)

	// Send a response back to the client
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Email sent successfully"))
}


func getEmailBody(templateFile string, data interface{}) (string, error) {
	tmpl, err := template.ParseFiles(templateFile)
	if err != nil {
		return "", err
	}

	var result bytes.Buffer
	err = tmpl.Execute(&result, data)
	if err != nil {
		return "", err
	}

	return result.String(), nil
}

The email-sender.go defines a Go function SendEmail sends an email using an SMTP server. It establishes a secure connection with the SMTP server, authenticates using the provided credentials, and sends the email message.

package main

import (
	"bytes"
	"crypto/tls"
	"fmt"
	"log"
	"net/smtp"
	"sync"
)

func SendEmail(subject string, body string, to string, wg *sync.WaitGroup) bool {
	defer wg.Done()

	message := []byte("From: Contact <" + smtpConfig.Sender + ">\r\n" +
		"To: " + to + "\r\n" +
		"Subject: " + subject + "\r\n" +
		"MIME-Version: 1.0\r\n" +
		"Content-Type: text/html; charset=utf-8\r\n" +
		"\r\n" +
		body + "\r\n")

	// Create authentication credentials
	auth := smtp.PlainAuth("", smtpConfig.Sender, smtpConfig.Password, smtpConfig.SMTPHost)

	// Create the TLS configuration
	tlsConfig := &tls.Config{
		InsecureSkipVerify: true,
		ServerName:         smtpConfig.SMTPHost,
	}

	// Connect to the SMTP server
	conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", smtpConfig.SMTPHost, smtpConfig.SMTPPort), tlsConfig)
	if err != nil {
		log.Printf("Failed to connect to the SMTP server: %v", err)
		return false
	}

	// Create the SMTP client
	client, err := smtp.NewClient(conn, smtpConfig.SMTPHost)
	if err != nil {
		log.Printf("Failed to create SMTP client: %v", err)
		return false
	}

	// Authenticate with the SMTP server
	if err := client.Auth(auth); err != nil {
		log.Printf("SMTP authentication failed: %v", err)
		return false
	}

	// Set the sender and recipient
	if err := client.Mail(smtpConfig.Sender); err != nil {
		log.Printf("Failed to set sender: %v", err)
		return false
	}
	if err := client.Rcpt(to); err != nil {
		log.Printf("Failed to set recipient: %v", err)
		return false
	}

	// Send the email message
	w, err := client.Data()
	if err != nil {
		log.Printf("Failed to open data writer: %v", err)
		return false
	}

	buf := bytes.NewBuffer(make([]byte, 0, 1024))
	buf.Write(message)

	_, err = buf.WriteTo(w)
	if err != nil {
		log.Printf("Failed to write email message: %v", err)
		return false
	}

	err = w.Close()
	if err != nil {
		log.Printf("Failed to close data writer: %v", err)
		return false
	}

	// Close the connection to the SMTP server
	client.Quit()

	log.Println("Email sent successfully!")
	return true
}

models.go will contain data structures used in the project like smtp config and http request body

package main

type RequestBody struct {
	RecipientEmail string `json:"email"`
	PhoneNumber    string `json:"phone"`
	Name           string `json:"name"`
}

type SMTPConfig struct {
	SMTPHost string `yaml:"smtpHost"`
	SMTPPort int    `yaml:"smtpPort"`
	Sender   string `yaml:"smtpEmail"`
	Password string `yaml:"smtpPassword"`
}

Build and run

To build the project, first, we fetch the dependencies (make sure you are in the root folder of the project)

go get .

Then we run

go run .

If you only want to build the project without running it

go build .

An executable file will be generated. On Windows, it will be a main.exe file (you can double-click it to run) and on Linux or MacOS it will be an executable file main

After running the server and sending a request using curl or Postman

you will get an output like this in the console

2023/05/29 12:30:57 Server listening on http://localhost:8080
2023/05/29 12:31:08 Received request /api/contact
2023/05/29 12:31:10 Email sent successfully!

And you will receive an email with the content from the template