Dwi Wahyudi

Senior Software Engineer (Ruby, Golang, Java)


We’re going to continue from the previous article where we’ve already made some repo codes for our registration module. Now it’s time to create the service and HTTP restful handler for it. But before that, let’s dive in to how we will handle email service code.

Preparing the Emailing Service

Before we move further, we’re going to setup our emailing service code, which will email user’s email (with a link to verify the email) when successfully registered to our marketplace.

We can just use mailtrap.io for testing and development purposes, so email won’t really get sent, but instead it will go into isolated testing inboxes provided by mailtrap. We need 4 things in order for our e-mail service to work (this also applies in production).

Lots of e-mail service providers also provide exclusive API, but don’t use this, always use general SMTP connection setting, don’t use any of their exclusive proprietary API.

Email Service Config

Those SMTP settings will be filled in our config/app.toml, we also need to add some additional settings for our email verification feature, first is registration_confirmation_from and base url of email registration confirmation URL:

[smtpconfig]
smtp_host_name="smtp.mailtrap.io"
smtp_port=2525
smtp_username="example-username"
smtp_password="example-password"

[registrationconfig]
registration_confirmation_from="no-reply@testmarketplace.com"
registration_confirmation_base_url="localhost:4545/accounts/registration/email_confirmation"

We’re going to update external/external.go to capture those newly made configurations:

config struct {
    SMTPConfig         SMTPConfig         `toml:"smtpconfig"`
    RegistrationConfig RegistrationConfig `toml:"registrationconfig"`
}

SMTPConfig struct {
    SMTPHostName string `toml:"smtp_host_name"`
    SMTPPort     int    `toml:"smtp_port"`
    SMTPUsername string `toml:"smtp_username"`
    SMPTPassword string `toml:"smtp_password"`
}

RegistrationConfig struct {
    RegistrationConfirmationFrom    string `toml:"registration_confirmation_from"`
    RegistrationConfirmationBaseURL string `toml:"registration_confirmation_base_url"`
}

We might want to place SMTP instance in external package, but email service usually grows larger as we develop the application. So we put this email service in external/emailservice/.

Email Service Package

We’re going to need a dedicated package for our email service, external/emailservice/. We want HTML email to be delivered, so we need html/template package in order to parse the HTML templates for our emails. Since parsing HTML in large numbers can be quite expensive if done many times, we’re going to do it once only and save the parsed tempates to a variable.

Here is external/emailservice/templates.go:

package emailservice

import (
	"errors"
	"html/template"
)

var (
	parsedTemplates = make(map[string]*template.Template)

	registrationSubject = "Welcome to our marketplace"

	registrationConfirmation = "registrationConfirmation"

	rawTemplates = map[string]string{
		registrationConfirmation: `
			<div>
				<p>Hello {{.UserName}}, welcome to our marketplace.</p>
				<p><a href="{{.ConfirmationURL}}">Please click this link to verify your email.</a></p>

				<p>Link above will be valid for 6 hours.</p>
			</div>
		`,
	}
)

func parseTemplates() (map[string]*template.Template, error) {
	var err error

	for templateKey, rawTemplate := range rawTemplates {
		var parsedTemplate *template.Template

		tmpl := template.New(templateKey)
		parsedTemplate, err = tmpl.Parse(rawTemplate)
		if err != nil {
			return nil, err
		}

		parsedTemplates[templateKey] = parsedTemplate
	}
	if err != nil {
		return nil, err
	}

	return parsedTemplates, nil
}

func getTemplate(key string) (*template.Template, error) {
	parsedTemplate, ok := parsedTemplates[key]
	if !ok {
		return nil, errors.New("template not found")
	}

	return parsedTemplate, nil
}
  • For now, we only have 1 email template: registrationConfirmation.
  • parseTemplates() parses each of rawTemplates and save all of them into a variable parsedTemplates, which is a package variable. We should run parseTemplates() once and only once.
  • getTemplate(key string) will return the parsed template based on the rawTemplates’s key we want. Such parsed template can be processed by inserting the needed text to its placeholder (if any) like {{.UserName}} and {{.ConfirmationURL}} above.

Now we move to external/emailservice/email_service.go:

package emailservice

import (
	"bytes"
	"context"
	"fmt"
	"go-mini-commerce/entity"
	"html/template"

	"gopkg.in/gomail.v2"
)

const (
	HTML = "text/html"
)

type (
	EmailService interface {
		UserRegistrationConfirmation(ctx context.Context, username, userEmail, confirmationHash string) error
		Send(ctx context.Context, email entity.Email) error
	}

	SMTPConfig struct {
		SMTPHostName string
		SMTPPort     int
		SMTPUsername string
		SMTPPassword string
	}

	RegistrationConfirmation struct {
		From    string
		BaseURL string
	}

	emailService struct {
		SMTPConfig
		RegistrationConfirmation
	}

	registrationConfirmationMailBody struct {
		UserName        string
		ConfirmationURL template.URL
	}
)

func (e *emailService) UserRegistrationConfirmation(
	ctx context.Context, userName, userEmail, confirmationHash string,
) error {
	parsedTemplate, err := getTemplate(registrationConfirmation)
	if err != nil {
		return err
	}

	var bodyBytes bytes.Buffer
	confirmationLink := fmt.Sprintf(
		"%s?confirmation_id=%s", e.RegistrationConfirmation.BaseURL, confirmationHash,
	)
	confirmationURL := template.URL(confirmationLink)
	err = parsedTemplate.Execute(&bodyBytes, registrationConfirmationMailBody{
		UserName:        userName,
		ConfirmationURL: confirmationURL,
	})
	if err != nil {
		return err
	}

	e.Send(
		ctx,
		entity.Email{
			From:     e.From,
			To:       []string{userEmail},
			Subject:  registrationSubject,
			BodyType: HTML,
			Body:     string(bodyBytes.Bytes()),
		},
	)

	return nil
}

func (e *emailService) Send(ctx context.Context, email entity.Email) error {
	m := gomail.NewMessage()
	m.SetHeader("From", email.From)
	m.SetHeader("To", email.To...)
	m.SetHeader("Subject", email.Subject)
	m.SetBody(email.BodyType, email.Body)
	if len(email.Attachment) > 0 {
		for _, attachment := range email.Attachment {
			m.Attach(attachment)
		}
	}

	d := gomail.NewDialer(
		e.SMTPHostName,
		e.SMTPPort,
		e.SMTPUsername,
		e.SMTPPassword,
	)

	err := d.DialAndSend(m)
	if err != nil {
		return err
	}

	return nil
}
  • We’re utilizing gomail package for sending emails.
  • The Send method will be our hallmark and one and only method that interacts with gomail package for sending email. It requires email entity.Email struct. The content of entity.Email will be like this in entity package:
package entity

type (
	Email struct {
		From       string
		To         []string
		Subject    string
		BodyType   string
		Body       string
		Attachment []string
	}
)
  • UserRegistrationConfirmation method is a specific method for sending email regarding user’s email confirmation. We get the parsed template from getTemplate method in external/emailservice/templates.go. We then build the email body from the parsed template and injecting some values into its placeholders, the user’s name and the confirmation hash.
  • We then send the email with specified subject and built email body with Send method.
  • Do note ConfirmationURL template.URL in registrationConfirmationMailBody struct, this is done in order to safely put the url inside the email.

Now it’s time to provide injection method for this EmailService interface. Here’s our external/emailservice/inject.go:

package emailservice

import (
	"sync"
)

var (
	emailServiceOnce sync.Once
	emailSvc         EmailService
)

func InjectNewEmailService(smtpCfg SMTPConfig, registrationCfg RegistrationConfirmation) EmailService {
	// parse html templates `rawTemplates` once and saved into `parsedTemplates` variable/
	parseTemplates()

	emailServiceOnce.Do(func() {
		emailSvc = &emailService{
			SMTPConfig: SMTPConfig{
				SMTPHostName: smtpCfg.SMTPHostName,
				SMTPPort:     smtpCfg.SMTPPort,
				SMTPUsername: smtpCfg.SMTPUsername,
				SMTPPassword: smtpCfg.SMTPPassword,
			},
			RegistrationConfirmation: RegistrationConfirmation{
				From:    registrationCfg.From,
				BaseURL: registrationCfg.BaseURL,
			},
		}
	})

	return emailSvc
}

As we can see here, parseTemplates() are called, this is intended because we want to parse those HTML templates and save them to parsedTemplates variable once and only once. This InjectNewEmailService will be called by our external package above.

Connecting Email Service Package and External Package

Now we already have our email service in place, it’s time to connect it with our external package for our infra package to use.

// .. skipped for brevity

type (
	External interface {
		EmailService() emailservice.EmailService
		CommonSvc() extcommon.CommonService
	}

	config struct {
		SMTPConfig         SMTPConfig         `toml:"smtpconfig"`
		RegistrationConfig RegistrationConfig `toml:"registrationconfig"`
	}

	SMTPConfig struct {
		SMTPHostName string `toml:"smtp_host_name"`
		SMTPPort     int    `toml:"smtp_port"`
		SMTPUsername string `toml:"smtp_username"`
		SMPTPassword string `toml:"smtp_password"`
	}

	RegistrationConfig struct {
		RegistrationConfirmationFrom    string `toml:"registration_confirmation_from"`
		RegistrationConfirmationBaseURL string `toml:"registration_confirmation_base_url"`
	}

	external struct {
		config

		emailSvc  emailservice.EmailService
		commonSvc extcommon.CommonService
	}
)

// .. skipped for brevity

var (
	emailServiceOnce sync.Once
	emailService     emailservice.EmailService
)

func (e *external) EmailService() emailservice.EmailService {
	emailServiceOnce.Do(func() {
		emailService = e.emailSvc
	})

	return emailService
}

// .. skipped for brevity

Again as we can see, we don’t inject email service here. We inject email service in external/inject.go. Here’s how it will be done:

package external

import (
	"go-mini-commerce/external/emailservice"
	extcommon "go-mini-commerce/external/extcommon"
	"log"
	"os"
	"sync"

	"github.com/BurntSushi/toml"
)

var (
	externalOnce sync.Once
	externalSvc  External
)

func InjectNewExternalService(
	commonSvc *extcommon.CommonService,
	emailSvc *emailservice.EmailService,
) External {

	var cfg config
	var decidedCommonSvc extcommon.CommonService
	var decidedEmailSvc emailservice.EmailService

    // .. skipped for brevity

	if emailSvc == nil {
		decidedEmailSvc = emailservice.InjectNewEmailService(
			emailservice.SMTPConfig{
				SMTPHostName: cfg.SMTPConfig.SMTPHostName,
				SMTPPort:     cfg.SMTPConfig.SMTPPort,
				SMTPUsername: cfg.SMTPConfig.SMTPUsername,
				SMTPPassword: cfg.SMTPConfig.SMPTPassword,
			},
			emailservice.RegistrationConfirmation{
				From:    cfg.RegistrationConfig.RegistrationConfirmationFrom,
				BaseURL: cfg.RegistrationConfig.RegistrationConfirmationBaseURL,
			},
		)
	}

	externalOnce.Do(func() {
		externalSvc = &external{
			commonSvc: decidedCommonSvc,
			emailSvc:  decidedEmailSvc,
		}
	})

	return externalSvc
}

As we can see here, nothing is changing in infra/infra.go. Injecting from above can be done as simple as this:

infra.NewInfra(external.InjectNewExternalService(nil, nil))

In the future, when there are numerous args to InjectNewExternalService, it’s better to use a struct arg for better clarity, but for now, we’re done with external and infra packages, let’s move to service code in the next article.