Enviando correos electronicos con AWS Lambda y SES desde un formulario HTML


lambda

Serie sobre Serverless

Parte I: Sirviendo sitios web estáticos con S3 y CloudFront, es recomendable revisar esa parte antes de empezar con esta para entender cómo llegamos hasta aquí.


Parte II: Enviando emails con AWS Lambda y SES desde un formulario HTML, Estás aquí.


Introducción

Este artículo forma parte de la serie sobre tecnologías serverless. Aquí veremos cómo crear una función serverless en AWS Lambda para enviar un email desde un formulario HTML en el sitio. El código fuente lo podés encontrar acá para la versión en Go, pero si preferís Node.js, podés usar esta versión.


Framework Serverless

Como de costumbre, voy a usar el framework Serverless para gestionar nuestras funciones. Creamos el proyecto:

mkdir techsquad-functions && cd techsquad-functions && serverless create -t aws-go
# OUTPUT:
# Serverless: Generating boilerplate...
#  _______                             __
# |   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
# |   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
# |____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
# |   |   |             The Serverless Application Framework
# |       |                           serverless.com, v1.36.1
#  -------'
#
# Serverless: Successfully generated boilerplate for template: "aws-go"
# Serverless: NOTE: Please update the "service" property in serverless.yml with your service name

Después de crear el proyecto, actualizamos el manifiesto de serverless de la siguiente manera:

service: sendMail

frameworkVersion: ">=1.28.0 <2.0.0"

provider:
  name: aws
  runtime: go1.x
  region: us-east-1
  memorySize: 128
  versionFunctions: false
  stage: 'prod'

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "ses:*"
        - "lambda:*"
      Resource:
        - "*"

package:
 exclude:
   - ./**
 include:
   - ./send_mail/send_mail

functions:
  send_mail:
    handler: send_mail/send_mail
    events:
      - http:
          path: sendMail
          method: post

Las partes interesantes aquí son los permisos de IAM y la función send_mail. El resto es bastante estándar: definimos una función y el evento HTTP POST para el API Gateway, donde se encuentra nuestro ejecutable, y también solicitamos permisos para enviar emails a través de SES.


Desplegamos la función

make deploy
# OUTPUT:
# rm -rf ./send_mail/send_mail
# env GOOS=linux go build -ldflags="-s -w" -o send_mail/send_mail send_mail/main.go
# sls deploy --verbose
# ...
# Serverless: Stack update finished...
# Service Information
# service: sendMail
# stage: prod
# region: us-east-1
# stack: sendMail-prod
# api keys:
#   None
# endpoints:
#   POST - https://m8ebtlirjg.execute-api.us-east-1.amazonaws.com/prod/sendMail
# functions:
#   send_mail: sendMail-prod-send_mail
# layers:
#   None
#
# Stack Outputs
# ServiceEndpoint: https://m8ebtlirjg.execute-api.us-east-1.amazonaws.com/prod
# ServerlessDeploymentBucketName: sendmail-prod-serverlessdeploymentbucket-1vbmb6gwt8559

Todo se ve bien, ¿qué sigue? El código fuente.


Lambda

Este es básicamente el código fuente completo de esta función. Como verás, es bastante simple:

package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/awserr"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/ses"
)

type Response events.APIGatewayProxyResponse

type RequestData struct {
    Email   string
    Message string
}

// Este valor podría ser una variable de entorno
const (
    Sender    = "[email protected]"
    Recipient = "[email protected]"
    CharSet   = "UTF-8"
)

func Handler(ctx context.Context, request events.APIGatewayProxyRequest) (Response, error) {
    fmt.Printf("Request: %+v\n", request)

    fmt.Printf("Processing request data for request %s.\n", request.RequestContext.RequestID)
    fmt.Printf("Body size = %d.\n", len(request.Body))

    var requestData RequestData
    json.Unmarshal([]byte(request.Body), &requestData)

    fmt.Printf("RequestData: %+v", requestData)
    var result string
    if len(requestData.Email) > 0 && len(requestData.Message) > 0 {
        result, _ = send(requestData.Email, requestData.Message)
    }

    resp := Response{
        StatusCode:      200,
        IsBase64Encoded: false,
        Body:            result,
        Headers: map[string]string{
            "Content-Type":           "application/json",
            "X-MyCompany-Func-Reply": "send-mail-handler",
        },
    }

    return resp, nil
}

func send(Email string, Message string) (string, error) {
    // Esto podría ser una variable de entorno
    sess, err := session.NewSession(&aws.Config{
        Region: aws.String("us-east-1")},
    )

    // Creamos una sesión en SES.
    svc := ses.New(sess)

    // Armamos el correo electrónico.
    input := &ses.SendEmailInput{
        Destination: &ses.Destination{
            CcAddresses: []*string{},
            ToAddresses: []*string{
                aws.String(Recipient),
            },
        },
        Message: &ses.Message{
            Body: &ses.Body{
                Html: &ses.Content{
                    Charset: aws.String(CharSet),
                    Data:    aws.String(Message),
                },
                Text: &ses.Content{
                    Charset: aws.String(CharSet),
                    Data:    aws.String(Message),
                },
            },
            Subject: &ses.Content{
                Charset: aws.String(CharSet),
                Data:    aws.String(Email),
            },
        },
        // Usamos el mismo remitente porque necesita estar validado en SES.
        Source: aws.String(Sender),

        // Descomentar para usar un conjunto de configuración
        //ConfigurationSetName: aws.String(ConfigurationSet),
    }

    // Intentamos enviar el correo electrónico.
    result, err := svc.SendEmail(input)

    // Mostramos los mensajes de error si los hay.
    if err != nil {
        if aerr, ok := err.(awserr.Error); ok {
            switch aerr.Code() {
            case ses.ErrCodeMessageRejected:
                fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
            case ses.ErrCodeMailFromDomainNotVerifiedException:
                fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
            case ses.ErrCodeConfigurationSetDoesNotExistException:
                fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
            default:
                fmt.Println(aerr.Error())
            }
        } else {
            fmt.Println(err.Error())
        }

        return "hubo un error inesperado", err
    }

    fmt.Println("Email enviado a la dirección: " + Recipient)
    fmt.Println(result)
    return "¡Enviado!", err
}

func main() {
    lambda.Start(Handler)
}

El código es bastante directo: solo espera 2 parámetros, enviará un email y devolverá “sent!” si todo salió bien. Podés depurar y compilar tu función antes de subirla ejecutando el comando make (muy útil), y si usás make deploy, ahorrarás tiempo al desplegar solo los archivos que funcionan.


SES

Para que esto funcione, vas a necesitar verificar/validar tu dominio en SES.


Andá a SES->Domains->Verify a New Domain. image


Después de ingresar tu dominio, verás algo como esto: image


Como no tengo este dominio en Route53, no tengo el botón para agregar los registros (lo que lo haría más simple y rápido), pero es fácil, solo tenés que crear unos registros DNS y esperar unos minutos hasta que obtengas algo como esto: image


Después de eso, probalo

serverless invoke -f send_mail -d '{ "Email": "[email protected]", "Message

": "test" }'
# OUTPUT:
{
    "statusCode": 200,
    "headers": {
        "Content-Type": "application/json",
        "X-MyCompany-Func-Reply": "send-mail-handler"
    },
    "body": ""
}

Después de presionar Enter, el mensaje apareció de inmediato en mi bandeja de entrada :).


Otra opción es usar httpie

echo '{ "email": "[email protected]", "message": "test2" }' | http https://m8ebtlirjg.execute-api.us-east-1.amazonaws.com/prod/sendMail
# OUTPUT:
# HTTP/1.1 200 OK
# ...
# sent!

O curl

curl -i -X POST https://m8ebtlirjg.execute-api.us-east-1.amazonaws.com/prod/sendMail -d '{ "email": "[email protected]", "message": "test3" }'
# OUTPUT:
# HTTP/2 200
# ...
# sent!

Y eso es todo por ahora, nos vemos en el próximo artículo.


Errata

Si encontrás algún error o tenés alguna sugerencia, por favor enviame un mensaje para que lo corrija.



No tienes cuenta? Regístrate aqui

Ya registrado? Iniciar sesión a tu cuenta ahora.

Iniciar session con GitHub
Iniciar sesion con Google
  • Comentarios

    Online: 0

Por favor inicie sesión para poder escribir comentarios.

by Gabriel Garrido