DEV Community

loading...
DropConfig

Sending Emails With AWS SES

Clay Murray
I like programming and typically video games too! Working on games and stuff. They/Them
・3 min read

So in my last post I mentioned using your event bus to send emails. I skimmed over the implementation though.
I wanted to write a follow up about how you can send emails with SES quite easily. It's how we send all our emails at DropConfig

SES

AWS SES stands for Simple Email Service. The name is fairly accurate.
I'm also not going to go into specifics about setting up your account. AWS has some good guides

Anyway let's get into it.

We break down our email sending into two parts triggered by events.

  1. Fetching and marshaling the data for an email from a given event.
  2. Sending the actual email.

For example.

We may get an event come in USER_COMMENTED_ON_POST first thing we do is lookup an email in our email triggers

const emailTriggers = {
    "USER_COMMENTED_ON_POST": {
        "to": "user.email",
        templateName: "user-commented"
    }
}

Our trigger has a few parts. We have a JSON path to look up in the event data for who to send the email to.
Then we have an email template which might look something like this.

{
    "Subject": "A user commented on your post",
    "Body": "hello {{user.name}} a user commented on your post {{post.link}}"
}

We use mustache for templates in our emails. Also you could make your email body have HTML tags etc.

So we look up in our email trigger list and put together and email

const trigger = emailTriggers[event.type];
if(!trigger){
   //We don't have a trigger so we don't care
   return true
}
//Here we are fetching the template. Maybe we store these in their own DropConfig?
const template = await loadTemplateFromName(trigger.templateName);

//Using lodash here because it can lookup by a path string
const to = _.get(task.data.data, trigger.to);
if(to && template){
    const body = mustache.render(template.Body, task.data.data);
    const subject = mustache.render(template.Subject, task.data.data);

    const params = {
        Destination: {
            toAddresses: [to]
        },
        Source: "sputnik@dropconfig.com",
        Message: {
            Body: {
                Html: {
                    Data: body
                }
            },
            Subject: {
                Data: subject
            }
        }
    }
}

Next we do create a send-email event and put the params in there.

server.createEvent("send-email", params);

Why do we not just send the email?

The big reason is: Imagine you have many different emails to send based off of a single event. If a user comments on a post you might want to send an email to the owner of the post. But also a different email to other commenters. Now if sending to the owner succeeds but sending to the commenters fails we have a problem. Without splitting the sending of emails into two distinct events. We would re-run the event and notify the people we sent to successfully over and over (or as many times as we retry).

If we split it into two steps only the failed emails ever get retried.

Now that we have created the send-email event. It is a breeze to actually send the email.

  if(task.data.type === "send-email"){
    try {
      const params = task.data.data;
      const res = await ses.sendEmail(params).promise();
      return true;
    } catch(e){
      return false;
    }
  }

We don't need much more logic than that!

Putting it all together.

//This is a task runner as explained in my previous post.
exports.handler = async (task, queue, sqs, server) => {
    const emailTriggers = {
        "USER_COMMENTED_ON_POST": {
        "to": "user.email",
        templateName: "user-commented"
        }
    }

    if(task.data.type === "send-email"){
        try {
          const params = task.data.data;
          const res = await ses.sendEmail(params).promise();
          return true;
        } catch(e){
          return false;
        }
      }

    const trigger = emailTriggers[event.type];
    if(!trigger){
       //We don't have a trigger so we don't care
       return true
    }
    //Here we are fetching the template. Maybe we store these in their own DropConfig?
    const template = await loadTemplateFromName(trigger.templateName);

    //Using lodash here because it can lookup by a path string
    const to = _.get(task.data.data, trigger.to);
    if(to && template){
        const body = mustache.render(template.Body, task.data.data);
        const subject = mustache.render(template.Subject, task.data.data);

        const params = {
        Destination: {
            toAddresses: [to]
        },
        Source: "sputnik@dropconfig.com",
        Message: {
            Body: {
            Html: {
                Data: body
            }
            },
            Subject: {
            Data: subject
            }
        }
        }
    }

}

Thanks for reading this far. Checkout https://dropconfig.com for awesome version control and hosting of your config files.

Let me know if you have any questions. I might be able to help!

Discussion (0)