Hello everyone, welcome to the backend master class! Today let's learn
how to send emails in Golang. We can do that with the help of the
standard net/smtp
package. It
implements the Simple Mail Transfer Protocol defined in RFC 5321. If you're
curious about this RFC, you can follow this link to
read it. The most important function in the smtp
package is the
SendMail
function. There is an
example here that shows how we can use it to send emails.
package main
import (
"log"
"net/smtp"
)
func main() {
// Set up authentication information.
auth := smtp.PlainAuth("", "[email protected]", "password", "mail.example.com")
// Connect to the server, authenticate, set the sender and recipient,
// and send the email all in one step.
to := []string{"[email protected]"}
msg := []byte("To: [email protected]\r\n" +
"Subject: discount Gophers!\r\n" +
"\r\n" +
"This is the email body.\r\n")
err := smtp.SendMail("mail.example.com:25", auth, "[email protected]", to, msg)
if err != nil {
log.Fatal(err)
}
}
Basically, we will first create an SMTP plain auth object by providing an email address, its password, and an authentication server. Then we define the recipient's email address and the content of the message. Everything in the email, including the attachments must be concatenated together into 1 single binary message, which should follow the RFC 822 standard. You can read more about this standard by following this link.
Once we have the message correctly formatted, we can use the
smtp.SendMail
function to send the email to the recipients. Now, although
it's possible to write the code on our own to format the message exactly
as specified in the standard, it's a bit time-consuming and tedious. So
Let's save us some time by using an existing community package that
already implemented this feature.
One package that has nearly 4000
stars is gomail
. Its API is pretty easy to use and supports many features.
However, its last update was back in 2016, 7 years ago. So I think it's a
bit old and no longer maintained. Another package is
email
by Jordan Wright, which has more than 2000 stars, and the last
updated time is 2021, only 2 years ago. I've used both of these packages,
and they're all good. But I think email
package's API is cleaner and
easier to use than gomail
. So I'm gonna use it in this video. Let's
copy this go get
command and run it in the terminal to install the
package.
go get github.com/jordan-wright/email
go: downloading github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
go: added github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
Alright, the package has been downloaded and also added to the indirect
dependency list of go.mod
file.
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/json-iterator/go v1.1.12 // indirect
Next, I'm gonna create a new folder called mail
. And inside this folder,
let's add a new file: sender.go
. We will add some code to send emails
in this file. As always, to make the code more abstract and easier to
test, I'm gonna define an EmailSender
interface.
type EmailSender interface {
}
It will have only 1 function called SendEmail
, which takes several input
arguments, such as: the email's subject
of type string
, the content
of
the email, also of type string
, then a list of email addresses to send
the email to. Sometimes we also want to send the email with cc
and
bcc
recipients, so the next 2 arguments are used for this purpose.
Finally, we might want to attach some files to the email, so the last
argument is a list of attached files' names. This function will return an
error if it fails to send the email.
type EmailSender interface {
SendEmail(
subject string,
content string,
to []string,
cc []string,
bcc []string,
attachFiles []string,
) error
}
Alright, now let's write a struct that implements this interface. For this demo, we're gonna send emails from Gmail account which I have created for testing purposes.
So I'm gonna define a new type GmailSender
struct. In this struct, let's
add a name
field of type string
. The recipients will see this name as
the sender of the email. The next field (fromEmailAddress
) is the address
of the account from which we will send emails. And the last field
(fromEmailPassword
) is gonna be the password to access that account. Of
course, we won't use the real password here, but I will show you how to
generate an app password later.
type GmailSender struct {
name string
fromEmailAddress string
fromEmailPassword string
}
For now, let's add a function to create a NewGmailSender
. It will take
3 input arguments for the name
, the fromEmailAddress
, and
fromEmailPassword
, all of type string
. And it will return the abstract
EmailSender
interface we defined above.
func NewGmailSender(name string, fromEmailAddress string, fromEmailPassword string) EmailSender {
}
In this function, we'll return a pointer to a new GmailSender
object,
and we store the 3 input arguments to the corresponding fields of this
object.
func NewGmailSender(name string, fromEmailAddress string, fromEmailPassword string) EmailSender {
return &GmailSender{
name: name,
fromEmailAddress: fromEmailAddress,
fromEmailPassword: fromEmailPassword,
}
}
Now, we're seeing some red lines here, because the GmailSender
doesn't
implement the SendEmail method required by EmailSender
interface yet.
So I'm gonna copy that method signature, and paste it to the end of this
file. Then, let's add a sender
receiver of type GmailSender
to the
front of this function. That's how we make this struct to satisfy the
requirement of the interface.
func (sender *GmailSender) SendEmail(
subject string,
content string,
to []string,
cc []string,
bcc []string,
attachFiles []string,
) error {
}
Alright, now let's implement the method.
First, let's create a new email object by calling email.NewEmail()
. This
function comes from the email
package that we installed before.
func (sender *GmailSender) SendEmail(
subject string,
content string,
to []string,
cc []string,
bcc []string,
attachFiles []string,
) error {
e := email.NewEmail()
}
So now, as we have really used the package, we can run
go mod tidy
in the terminal to move it to the direct dependency list.
github.com/hibiken/asynq v0.23.0
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/lib/pq v1.10.5
OK, let's go back to the code and set the correct values to the NewEmail
object. First, the e.From
field is gonna be a combination of the email
sender's name and email address. So here I use the fmt.Sprintf()
function
to join them together. Next, the Subject
field will be set to the input
subject
. For the content, we normally want to write it in HTML, so here
I'll set e.HTML
to the input content
variable. But we have to do a
type conversion here because content is a string, while the e.HTML
field
is a []byte
slice. The next 3 fields: e.To
, e.Cc
and e.Bcc
can be
set to their corresponding input arguments. OK, now we will iterate through
this attachment list attachFiles
, and for each file, call e.AttachFile
and pass in its name. This function will return an attachment object and
an error. We don't need to do anything with the attachment, so here I just
put a blank identifier for it. Then let's check if error is nil
or not.
If it is not nil
, we'll wrap and return it with this message: "failed
to attach file".
func (sender *GmailSender) SendEmail(
subject string,
content string,
to []string,
cc []string,
bcc []string,
attachFiles []string,
) error {
e := email.NewEmail()
e.From = fmt.Sprintf("%s <%s>", sender.name, sender.fromEmailAddress)
e.Subject = subject
e.HTML = []byte(content)
e.To = to
e.Cc = cc
e.Bcc = bcc
for _, f := range attachFiles {
_, err := e.AttachFile(f)
if err != nil {
return fmt.Errorf("failed to attach file %s: %w", f, err)
}
}
}
Finally, if no errors occur, we'll move to the next step, which is,
authenticating with the SMTP server. I'm gonna call the smtp.PlainAuth()
function. Usually, the first argument - identity
can be left empty,
then the username should be set to the sender's fromEmailAddress
. The
password should be the sender's fromEmailPassword
. And the last argument
is a host, which should be set to the address of the SMTP authentication
server. In our case, we're using Gmail, so I'll define a constant
smtpAuthAddress
at the top of the file,
import (
"fmt"
"net/smtp"
"github.com/jordan-wright/email"
)
const (
smtpAuthAddress = "smtp.gmail.com"
)
and set its value to "smtp.gmail.com".
Then we can pass this constant into the smtp.PlainAuth()
function. The
output of it will be an smtp.Auth
object. So I'm gonna save it to this
variable.
func (sender *GmailSender) SendEmail(
subject string,
content string,
to []string,
cc []string,
bcc []string,
attachFiles []string,
) error {
...
for _, f := range attachFiles {
_, err := e.AttachFile(f)
if err != nil {
return fmt.Errorf("failed to attach file %s: %w", f, err)
}
}
smtpAuth := smtp.PlainAuth("", sender.fromEmailAddress, sender.fromEmailPassword, smtpAuthAddress)
}
With this variable, we're finally able to send the email by calling
e.Send()
. But we also need to give it the address of the SMTP server.
For Gmail, this address will be a bit different from the auth server
address. So I'm gonna declare a new constant here,
const (
smtpAuthAddress = "smtp.gmail.com"
smtpServerAddress = "smtp.gmail.com:587"
)
and set its value to "smtp.gmail.com", port "587".
Alright, now we can pass in this function the SMTP server address and the SMTP auth object. Finally, we return the output error of it to the caller of this function.
func (sender *GmailSender) SendEmail(
subject string,
content string,
to []string,
cc []string,
bcc []string,
attachFiles []string,
) error {
...
smtpAuth := smtp.PlainAuth("", sender.fromEmailAddress, sender.fromEmailPassword, smtpAuthAddress)
return e.Send(smtpServerAddress, smtpAuth)
}
And that's it! The SendEmail()
function is completed.
Now let's write some tests to see if it's working properly or not. I'm
gonna create a new file called sender_test.go
in the same mail
package. Then let's add a new function TestSendEmailWithGmail()
, which
takes a testing.T
object as input argument.
func TestSendEmailWithGmail(t *testing.T) {
}
In order to send emails, we'll need a sender's name, an email address
and its password. So I'm gonna define and get them from environment
variables. In the app.env
file let's add a new variable called
EMAIL_SENDER_NAME
and set its value to Simple Bank
. Then another
variable for the EMAIL_SENDER_ADDRESS
. I've created a Gmail account
just for testing, its address is [email protected]
. So I'm
gonna copy and paste it to our app.env
file. Now comes the most
important value: EMAIL_SENDER_PASSWORD
. Should we use the real password
of the simplebanktest
account here? No. Even if you try to use the real
password here, Gmail won't allow you to authenticate with it. For
programmatic access, we will have to generate an app password, which I
think, is better because we can easily manage it, and revoke access
if necessary. First, we have to open the "Manage your Google Account"
page. And open the "Security" tab on the left.
In order to create an app password, we have to enable 2-step verification. So I'm gonna click on these "2-Step Verification" and "GET STARTED" buttons.
Google might ask you to enter your password, then it will ask for a
phone number that you want to use. You can choose how to get the
verification code (via "Text message" or "Phone call"). I'm gonna choose
text message, and click Next
.
On the next page, we'll have to enter the code that Google sent us.
If the code is correct, we can click this button
to turn on the 2-step verification.
Now, if we go back to the Security
page, a new App passwords
button will
show up in the Signing in to Google
section.
Let's click on it to open the App passwords
screen.
We don't have any app passwords yet, so let's go ahead to create a new
one. I'm gonna select the Mail
app, and in the Select device
list
let's choose "Other".
We can give it a name to easily remember. Let's call it Simple Bank Service
. Then click Generate
.
Google will give you this app password with 16 characters.
Let's copy and paste it to our app.env
file.
EMAIL_SENDER_NAME=Simple Bank
EMAIL_SENDER_ADDRESS=[email protected]
EMAIL_SENDER_PASSWORD=jekfcygyenvzekke
Once we click Done
and go back to the Security
page, we'll see that
there's 1 app password available here.
If we click on this button,
it will bring us to a page to manage the app passwords. This is our Simple Bank Service's password, and next to it is a delete icon, which we can use to delete the password if we want to.
Alright, let's go back to the code. Now, as we've added these 3 new
variables, we'll need to update the Config
struct to include them. I'm
gonna duplicate this TokenSymmetricKey
field, and change its name to
EmailSenderName
. And copy its environment variable's name to the
mapstructure
tag.
type Config struct {
...
EmailSenderName string `mapstructure:"EMAIL_SENDER_NAME"`
}
Let's do the same for the EmailSenderAddress
and the EmailSenderPassword
field.
type Config struct {
...
EmailSenderAddress string `mapstructure:"EMAIL_SENDER_ADDRESS"`
EmailSenderPassword string `mapstructure:"EMAIL_SENDER_PASSWORD"`
}
OK, now we're ready to write the test.
In this TestSendEmailWithGmail()
function, I'm gonna load the config
by calling util.LoadConfig()
, and pass in the location of the folder
containing the app.env
file.
func TestSendEmailWithGmail(t *testing.T) {
config, err := util.LoadConfig("..")
require.NoError(t, err)
}
These 2 dots mean the parent folder of the current mail
folder. We
require that no error should be returned. Then we can create a
NewGmailSender
with the config.EmailSenderName
,
config.EmailSenderAddress
and config.EmailSenderPassword
.
func TestSendEmailWithGmail(t *testing.T) {
config, err := util.LoadConfig("..")
require.NoError(t, err)
sender := NewGmailSender(config.EmailSenderName, config.EmailSenderAddress, config.EmailSenderPassword)
}
Now I'm gonna define the subject of the email, let's say "A test email".
The content of the email will be a simple HTML script. In Go, we can use
this backtick character to define a multiline string. Let's start the
email with a <h1>
tag saying "Hello world" followed by a <p>
tag
with content: "This is a test message from" and a link to the
techschool.guru
website. Now, the recipient email will be
[email protected]
. Of course, you can add more emails here if
you want since it's a list of strings.
func TestSendEmailWithGmail(t *testing.T) {
...
sender := NewGmailSender(config.EmailSenderName, config.EmailSenderAddress, config.EmailSenderPassword)
subject := "A test email"
content := `
<h1>Hello world</h1>
<p>This is a test message from <a href="http://techschool.guru">Tech school</a></p>
`
to := []string{"[email protected]"}
}
For this test, let's ignore the cc
or bcc
list. So next, the attachment
files. I'm gonna add the README.md
file to the list. OK, now we can
call sender.SendEmail()
function, and pass in the subject, the content,
and the receiver's email address. The cc
and bcc
parameters will be
nil
. And finally the list of attached files. We require this function
to return no errors. And that's it. We're done!
func TestSendEmailWithGmail(t *testing.T) {
...
to := []string{"[email protected]"}
attachFiles := []string{"../README.md"}
err = sender.SendEmail(subject, content, to, nil, nil, attachFiles)
require.NoError(t, err)
}
Let's click this button "Run test" to run the test.
=== RUN TestSendEmailWithGmail
--- PASS: TestSendEmailWithGmail (1.11s)
PASS
ok github.com/techschool/simplebank/mail 1.417s
It passed. Awesome!
I'm gonna open Tech School's email to check if the email is actually delivered. And yes, it is successfully delivered.
We can see the test email here.
The name of the sender is Simple Bank
, and its address is
[email protected]
. The receiver's address is indeed
[email protected]
. Its body contains a big "Hello world"
message and a link to the Tech School's YouTube page. And finally,
the README.md
file is presented in the attachment. If we open the
"Sent" box of the Simple Bank Test account,
we will also see the same email.
So it works!
We've successfully written the code to send emails using a Gmail account. Of course, there are other ways to send emails, like using a third-party service, which often involves a domain verification step.
It will be very useful if you want to be able to send emails from any address of the same domain that you own.
If you're using AWS, you can take a look at SES, or Amazon Simple Email Service. It's pretty easy to use, and most of our code written so far can be used together with the AWS Golang SDK, with just a few lines of configuration.
And that's all I wanted to share with you in this video. I hope it was interesting and useful for you.
Thanks a lot for watching! Happy learning, and see you in the next lecture.