DEV Community

Jesus Gonzalez
Jesus Gonzalez

Posted on • Edited on

Enviar correos por GMAIL utilizando el paquete SMTP de GO

El paquete implementa SMTP (Simple Mail Transfer Protocol), el cual es un protocolo de comunicación para transmisión
de correos electrónicos. En su documentación se encuentra un ejemplo, que se adaptará a un formato listo para producción, aplicando testing unitarios, integración que comprobarán los componentes en conjunto y de cobertura.

Lo primero es crearse un directorio, con 2 archivos de extensión .go, uno para el código normal de GO, otro para los testings. Debería quedar como la imagen:

directorio

El nombre del archivo de testing debe cumplir el patrón especifico exigido por el lenguaje, de esta manera se ejecutaran solamente aquellos dedicados a esa función, el cual es el siguiente: tunombrepreferido_test.go .

Se empezará el desarrollo con un primer test a lo mas resaltante del articulo: Enviar Correos Electrónicos .

    package email

    import (
            "fmt"
            "net/smtp"
            "reflect"
            "strings"
            "testing"
    )

    func TestSendEmail(t *testing.T) {
            assertThat := func(assumption, addr string, auth smtp.Auth, from string, to []string, msg []byte, expectedOutput string) {
                    actualOutput := sendEmail(addr, auth, from, to, msg)
                    if actualOutput != expectedOutput {
                            t.Errorf("Error! Outputs are not equal Actual %v Expected %v", actualOutput, expectedOutput)
                    }
            }
            addr := "smtp.gmail.com:587"

            auth := smtp.PlainAuth("", "your-email@gmail.com", "your-password", "smtp.gmail.com")

            from := "your-email@gmail.com"

            to := []string{"to-email-1@gmail.com", "to-email-2@hotmail.com"}
            concatenate := strings.Join(to, ", ")
            toMsg := fmt.Sprintf("To: %v\r\n", concatenate)

            subject := "Test Subject"
            subjectMsg := fmt.Sprintf("Subject: %v\r\n", subject)

            body := "Example body."
            bodyMsg := fmt.Sprintf("%v\r\n", body)

            msg := []byte(toMsg + subjectMsg + "\r\n" + bodyMsg)
            assertThat("Should send email from user's GMAIL account to any email and return a message of successful sent", addr, auth, from, to, msg, "email sent successfully")
    }

Las funciones de igual manera se rigen por un respectivo patrón TestXxx( t *testing.T) donde Xxx es el nombre de cada una.

El testing de arriba se aprobará si el correo fue enviado y retorna el mensaje de exito esperado, si devuelve un mensaje diferente indicara que ocurrio un error y no se aprobó, la funcion a evaluar es sendEmail(), que contendra la misión de enviar los correos electrónicos y se agregara en el archivo email.go::

    func sendEmail(addr string, auth smtp.Auth, from string, to []string, message []byte) string {
            err := smtp.SendMail(addr, auth, from, to, message)
            if err != nil {
                    log.Printf("Error!: %s", err)
                    return "Error! sending mail"
            }
            return "email sent successfully"
    }

Necesita de unos parámetros para realizar el objetivo, como se le esta evaluando a ella solamente se aplicara la tecnica test double, se inicializaran objetos falsos simulando a valores reales para poder correr el testing con el comando:

    $ go test

Existen muchas probabilidades que falle al primer intento, debido a que no se ha configurado la cuenta a utilizar de GMAIL. Se necesitara modificar los siguientes aspectos:

  1. Desactivar el mecanismo de seguridad de autentificacion 2-step.
  2. Activar la opcion de permitir acceso a aplicaciones menos seguras, se esta trabajando localmente, y no se tiene el certificado TLS, GMAIL rechazará la conexion si no se realiza este paso.

Si ya se hicieron los cambios, los posibles errores pueden ser que las credenciales esten erroneas (el correo o la contraseña).

Se ejecuta nuevamente y deberia indicar que la prueba fue aprobada y el correo en la bandeja de entrada a quien fue dirigido.
Recuerda que GMAIL tiene un limite diario para el envio de correos, si se sobrepasa no se podria enviar mas o entrarian en la bandeja de SPAM(Limites).

A continuacion crearan tests a los objectos falsos. Los primeros evaluaran a las funciones que obtendran la address y el email desde donde se enviaran los correos, se creara un archivo .env para almacenar variables importantes o sensibles, se podra acceder a el con la libreria godotenv.

    func TestGetAddressSMTP(t *testing.T) {
            assertThat := func(assumption, expectedAddress string) {
                    actualAddress := getAddressSMTP()
                    if actualAddress != expectedAddress {
                            t.Errorf("Error! Outputs are not equal Actual %v Expected %v", actualAddress, expectedAddress)
                    }
            }
            assertThat("Should get from a environment variable the smtp address", "smtp.gmail.com:587")
    }

    func TestGetEmailFrom(t *testing.T) {
            assertThat := func(assumption, expectedEmail string) {
                    actualEmail := getEmailFrom()
                    if actualEmail != expectedEmail {
                            t.Errorf("Error! Outputs are not equal Actual %v Expected %v", actualEmail, expectedEmail)
                    }
            }
            assertThat("Should return the email from where the messages will be sent.", "your-email@gmail.com")
    }

Las funciones en el email.go

    func getAddressSMTP() string {
            return os.Getenv("address")
    }

    func getEmailFrom() string {
            return os.Getenv("username")
    }

Y variables en .env

    address=smtp.gmail.com:587
    username=your-email@gmail.com

Se ejecutara el comando nuevamente:

    $ go test

Los nuevos test deberian ser aprobados porque los valores esperados son iguales a los obtenidos.

Ahora se evaluara la funcion encargada de la autentificacion de las credenciales de la cuenta a utilizar:

    func TestPlainAuth(t *testing.T) {
            assertThat := func(assumption string, expectedAuth smtp.Auth) {
                    actualAuth := plainAuth()
                    if !reflect.DeepEqual(actualAuth, expectedAuth) {
                            t.Errorf("Error! Outputs are not equal Actual %v Expected %v", actualAuth, expectedAuth)
                    }
            }
            expectedAuth := smtp.PlainAuth("", "your-email@gmail.com", "your-password", "smtp.gmail.com")
            assertThat("Should returns an Auth that implements the PLAIN authentication mechanism", expectedAuth)
    }

El paquete de SMTP contiene 2 metodos de autenficacion el utilizado en este articulo PlainAuth y CRAMMD5Auth, el ultimo es un poco mas seguro porque implementa un mecanismo de seguridad pero lamentablemente no es soportado por GMAIL.

En el archivo email.go

    func plainAuth() smtp.Auth {
            return smtp.PlainAuth(os.Getenv("identity"), os.Getenv("username"), os.Getenv("password"), os.Getenv("host"))
    }

Utiliza mas variables almacenadas en .env, aqui sobresale lo importante que es tener datos tan sensibles como la contraseña del correo en un archivo que no debe subirse al repositorio y como una constante en el código directamente.

    address=smtp.gmail.com:587
    identity=""
    username=your-email@gmail.com
    password=your-password
    host=smtp.gmail.com

Se vuelve a ejecutar el comando para asegurarse que la funcion retorna un objeto Auth como el esperado.

    $ go test

De los ultimos objetos falsos que se crearon fue el de msg, el cual contiene los correos a quienes va dirigido, el tema y el cuerpo del mensaje.

    func TestGenerateMessage(t *testing.T) {
            assertThat := func(assumption string, emailList []string, subject string, body string, expectedMsg []byte) {
                    actualMsg := joinMessageStructure(emailList, subject, body)
                    if !reflect.DeepEqual(actualMsg, expectedMsg) {
                            t.Errorf("Error! Outputs are not equal Actual %v Expected %v", actualMsg, expectedMsg)
                    }
            }
            to := []string{"to-email-1@gmail.com", "to-email-2@hotmail.com"}
            concatenate := strings.Join(to, ", ")
            toMsg := fmt.Sprintf("To: %v\r\n", concatenate)

            subject := "Test Subject"
            subjectMsg := fmt.Sprintf("Subject: %v\r\n", subject)

            body := "Example body."
            bodyMsg := fmt.Sprintf("%v\r\n", body)

            expectedMsg := []byte(toMsg + subjectMsg + "\r\n" + bodyMsg)
            assertThat("Should receive the emails, subject, body and should return an array of bytes being the message", to, subject, body, expectedMsg)
    }

En el archivo email.go la funcion a evaluar unira todos los datos, retornando un array de bytes que representa la estructura.

    func joinMessageStructure(emailList []string, subject string, body string) []byte {
            concatenate := strings.Join(emailList, ", ")
            toMsg := fmt.Sprintf("To: %v\r\n", concatenate)

            subjectMsg := fmt.Sprintf("Subject: %v\r\n", subject)

            bodyMsg := fmt.Sprintf("%v\r\n", body)

            return []byte(toMsg + subjectMsg + "\r\n" + bodyMsg)
    }

Se ejecuta:

    $ go test

Ahora se evaluaran a los componentes en conjunto con un test de integracion:

    func TestIntegrationSMTP(t *testing.T) {
            assertThat := func(assumption string, emailList []string, subject string, body string, expectedOutput string) {
                    address := getAddressSMTP()
                    auth := plainAuth()
                    emailFrom := getEmailFrom()
                    msg := joinMessageStructure(emailList, subject, body)
                    actualOutput := sendEmail(address, auth, emailFrom, emailList, msg)
                    if actualOutput != expectedOutput {
                            t.Errorf("Error! Outputs are not equal Actual %v Expected %v", actualOutput, expectedOutput)
                    }
            }
            to := []string{"to-email-1@gmail.com", "to-email-2@hotmail.com"}
            subject := "Test Subject"
            body := "Example body."
            assertThat("Should get data from different units and send email", to, subject, body, "email sent successfully")
    }

Todavia se necesita de objetos que son falsos, pero ellos en un caso real pueden ser obtenidos por funciones encargadas de capturar esos datos dependiendo de la aplicacion que se esta desarrolando Ejemplo: pueden venir de una peticion que le hicieron a una API o por input de una aplicacion CLI, etc. El proposito es que el correo sea enviado y retorne el mensaje esperado para aprobar el test.

Por ultimo se hara un test coverage (cobertura de pruebas). Nos indicara el porcentaje que se cubren en el código con los tests. Se realiza con el comando:

    $ go test -cover

Arrojara un resultado de:

directorio

No se cubre el 100% del código, se esta cubriendo un 81.2%. El anterior comando no nos indica cuales son las partes que no se cubren, en cambio los siguientes generaran un archivo HTML con el código evaluado y coloreado:

    $ go test -coverprofile=coverage.out
    $ go tool cover -func=coverage.out
    $ go tool cover -html=coverage.out

Cover

Las partes en rojo, son zonas que no estan siendo evaluadas por los testing, existe un debate donde hay un grupo que sostienen que se deben cubrir el 100% del código y el otro donde solo se deben cubrir los casos necesarios, En el código del articulo los casos que faltan por cubrir son los casos se generan errores en las funciones, no son necesarios porque ya tienen su mecanismo de devolver un mensaje de error en caso que haya ocurrido algo diferente a su proposito.

El código final esta mejor preparado para ir a produccion, porque cada funcion tiene una sola responsabilidad, se instauro un archivo especial para las variables sensibles por seguridad y fue testeada cada funcion, asi que ya se conoce como sera su comportamiento.

Si tienen una sugerencia o encuentran un error en el código por favor hagamelo saber.

Espero que les haya gustado.

NOTA : Debes reemplazar la configuracion con su correo, contraseña en los distintos archivos para que funcione correctamente.

Link del repositorio del archivo con Go Modules para el manejo de dependencias 👇

https://github.com/gonzalezlrjesus/email-smtp

Archivo final email.go:

Archivo de testings email_test.go:

Archivo de variables .env

Top comments (2)

Collapse
 
danielgamboar profile image
Daniel Gamboa Rojas

Está buenísimo, Jesús. No desrrollo en Go, pero se que mucha gente lo va a usar de apoyo. Me parece genial que haas compartido el contenido de los archivos que fuiste desarrollando. ¿Lo tienes en un repo de GitHub? Estaría cool que la gente pueda clonarlo.

Collapse
 
gonzalezlrjesus profile image
Jesus Gonzalez • Edited

Gracias Daniel, por el comentario =). No lo tengo en un repo, cuando tenga un chance lo subo a mi repositorio.

[Editado] :
Link del repo
github.com/gonzalezlrjesus/email-smtp