Sending email notifications are essential for effective communication between users and service providers. Beneath this notable piece of technology lies several layers of abstractions which might require more than a blog post to have a clear or complete idea of; which in retrospect isn't the aim of this post.
In this short article i will quickly try to uncover some issues i faced whilst trying to implement the email notifications for a service i was building at work.
By default using the standard SMTP package provided by golang i.e net/smtp
suffices for most use cases, but depending on the Email service provider you're using, you might experience some bottle necks using the smtp
package.
We use Webmail where i work and the first time i tried using net/smtp
with it, i experienced some irregularities, for instance my client was successfully able to authenticate my credentials, and send the mail message using the smtp.SendMail
method, with the sample code below:
package mails
import (
"log"
"net/smtp"
)
var (
From_mail = os.Getenv("FROM_MAIL")
Mail_password = os.Getenv("MAIL_PASSWORD")
SMTP_Host = os.Getenv("HOST")
)
func SendMail(msg string, recipient []string) {
// Message.
message := []byte("This is a test email message.")
// Authentication.
auth := smtp.PlainAuth("", From_mail, Mail_password, SMTP_Host)
fmt.Println(auth)
// Sending email.
if err := smtp.SendMail(fmt.Sprintf("%s:%d", SMTP_Host, 587), auth, From_mail, recipient, message); err != nil {
log.Printf("Error sending mail %v", err)
return
}
log.Println("Email Sent Successfully!")
}
but on the recieving end no mails were delivered.
After series of relentless searching for a reasonable explanations to why i was experiencing such, i got to learn some email service providers like the one i used preferred sending mails over port 465
requiring an ssl connection from the very beginning (without starttls), as compared to the standard 587
which uses plain TCP to send the mail traffic to the server with subsequent calls using Starttls.
There's been argument over these ports for a long time, but generally sending mails over port 587
is more recommended as it is provides a much secured transmission layer compared to port 465
. How these protocols are implemented depends mainly on the service providers you're using and the network protocol they choose for each port, for example Gmail uses SSL for the SMTP server on port 465 and TLS for port 587, how SSL and TLS are implemented and used for secure data tranmission on the internet is beyond the scope of this article, but you can read more about these protocols here.
Luckily i found this github "gist", which solved the problem we've been trying to figure out. I refactored the code to fit my use case, but you can get the actual solution from the "gist" link above, now unto the actual code:
package main
import (
"crypto/tls"
"fmt"
"log"
"net/mail"
"net/smtp"
"os"
"sync"
)
var (
From_mail = os.Getenv("FROM_MAIL")
Mail_password = os.Getenv("MAIL_PASSWORD")
SMTP_Host = os.Getenv("HOST")
Mail_subject string
Mail_body string
from *mail.Address
auth smtp.Auth
tlsconfig *tls.Config
mailwg sync.WaitGroup
)
type Container struct {
m sync.Mutex
Headers map[string]string
}
func NewContainer() *Container {
return &Container{
Headers: make(map[string]string),
}
}
func main() {
SendMails("subject", "article message", []string{"testuser1@gmail.com", "testuser2@gmail.com"})
}
func init() {
from = &mail.Address{Name: "Test-mail", Address: From_mail}
auth = smtp.PlainAuth("", From_mail, Mail_password, SMTP_Host)
tlsconfig = &tls.Config{
InsecureSkipVerify: true,
ServerName: SMTP_Host,
}
}
func SendSSLMail(subject, msg string, recipient string) {
to := mail.Address{Name: "", Address: recipient}
Mail_subject = subject
Mail_body = msg
// initialize new container object
container := NewContainer()
// call mutex.lock to avoid multiple writes to
// one header instance from running goroutines
container.m.Lock()
container.Headers["From"] = from.String()
container.Headers["To"] = to.String()
container.Headers["Subject"] = Mail_subject
// unlock mutex after function returns
defer container.m.Unlock()
// Setup message
message := ""
for k, v := range container.Headers {
message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + Mail_body
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTP_Host, 465), tlsconfig)
if err != nil {
log.Printf("Error sending mail %v", err)
return
}
c, err := smtp.NewClient(conn, SMTP_Host)
if err != nil {
log.Printf("Error sending mail %v", err)
return
}
// Auth
if err = c.Auth(auth); err != nil {
log.Printf("Error sending mail %v", err)
return
}
// To && From
if err = c.Mail(from.Address); err != nil {
log.Printf("Error sending mail %v", err)
return
}
if err = c.Rcpt(to.Address); err != nil {
log.Printf("Error sending mail %v", err)
return
}
// Data
w, err := c.Data()
if err != nil {
log.Printf("Error sending mail %v", err)
return
}
_, err = w.Write([]byte(message))
if err != nil {
log.Printf("Error sending mail %v", err)
return
}
err = w.Close()
if err != nil {
log.Printf("Error sending mail %v", err)
return
}
if err = c.Quit(); err != nil {
return
}
}
// Concurrently sending mails to multiple recipients
func SendMails(subject, msg string, recipients []string) {
mailwg.Add(len(recipients))
for _, v := range recipients {
go func(recipient string) {
defer mailwg.Done()
SendSSLMail(subject, msg, recipient)
}(v)
}
mailwg.Wait()
}
In the code above we declared variables to hold our smtp credentials, then we declared a struct named Container
which basically contains a mutex m
field, that essentially allows us to avoid Race Conditions
i.e when different threads/goroutines try to access and mutate the state of a resource at the same time, this is to lock the Headers
field which is the actual email header containing meta-data of each email to be sent.
The init()
function allows us to initialize some of the resource to be used before the function is called, you can see we initialize the auth
variable with our mail credentials, which returns an smtp.Auth
type; we then move further to set up tlsconfig
which basically determines how the client and server should handle data transfer.
The SendMails
function lets us send mails concurrently to avoid calling the SendSSLMail
function for each number of recipients we want to distribute the mail to, I believe the SendSSLMail
function body is quite straight-forward and doesn't require much explanation, as at the time of publishing this article, this service is being used so i'm pretty sure it is stable enough to be used by anyone.
Conclusion
Sending mails are very essential to the growth of any business, as they allow direct communication with the customers. Knowing how to effectively build stable email services is quitessential to every software developer out there. I hope this article helps solve any challenge you might encounter while writing mail services with Go.
Let me know what you think about this article and possibly any amendments that could be made to improve it's user experience, thanks for reading and have a great time!
Top comments (2)
There are a lot of caveats with sending email. While your solution should work in the most basic why, there are lots of things to consider when sending mails (encoding, line lengths, mail headers and formating of those, etc.). I suggest you have a look at github.com/wneessen/go-mail which will take care of lots of those things for you already.
well said Neessen, basically this article aims at highlighting protocol issues when using some email providers. It is by no means a fullstack solution to sending emails as you can see there are no support for features like
attachments
,authentication
etcwill def take a look at your package; thank you :))