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 ofrawTemplates
and save all of them into a variableparsedTemplates
, which is a package variable. We should runparseTemplates()
once and only once.getTemplate(key string)
will return the parsed template based on therawTemplates
’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 withgomail
package for sending email. It requiresemail entity.Email
struct. The content ofentity.Email
will be like this inentity
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 fromgetTemplate
method inexternal/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
inregistrationConfirmationMailBody
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.