Sending E-mails with Go.

Sending E-mails with Go.

Practical solution to building email services with Go

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 a 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!