Send emails and attachments with Amazon SES and NextJS

October 16, 2021 ☼ AWSNextJSNode.js

In this blog post I will show you how can you leverage Amazon SES and its Typescript SDK (@aws-sdk/client-ses) to send emails and attachments in your NextJS Serverless functions.

Use case

In one of my recent projects I had to send a daily CSV report of the sales orders via email. I’m using a simple CRON Job for this (via GitHub Actions) that hits my endpoint on a daily basis.

The endpoint is a relatively simple Serverless function that queries a DB and sends the email using @aws-sdk/client-ses client.

Diagram

Code

Sending attachments in your emails, unfortunately, is a bit more complicated than standard emails. You will have to use SendRawEmail (see the docs).

I will not cover how you set up an Amazon account, IAMs roles and deployment. There are plenty of resources on how to do so. AWS docs are quite good as well (for once 😉)

First things first, let’s install the packages:

npm i @aws-sdk/client-ses mailcomposer

mailcomposer is a Node.JS module for generating e-mail messages that can be streamed to SMTP or file.

Now, let’s create a new file (send-email) under pages/api of your NextJS project.

import { NextApiRequest, NextApiResponse } from 'next';
import { tmpdir } from 'os';
import Path from 'path';
import fs from 'fs/promises';

import mailcomposer from 'mailcomposer';
import { SendRawEmailCommand, SESClient } from '@aws-sdk/client-ses';

// IMPORTANT: it needs to be placed outside the Function Handler
const sesClient = new SESClient({
  region: 'eu-west-2', // Change it to match your region
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

// The following two utility functions are needed later on in the handler function

// The generated file will be streamed and added to the email
async function writeCSVtoFile(csv: string) {
  const filename = `orders-${new Date().getTime()}.csv`;
  const path = Path.join(tmpdir(), filename);
  
  await fs.writeFile(path, csv);
  console.info('✅ File written to disk', path);

  return { path, filename };
}

// Using mailcomposer simplifies a bit the object creation. It's not a hard requirement though.

const buildEmail = (emailTo: string, attachment: { filename: string; path: string }) => {

  return mailcomposer({
    to: [emailTo], // success@simulator.amazonses.com (can be used for testing)
    from: 'hey@domain.com',
    html: `
          <h1>Orders from Website</h1>
          <p>Please download the attached CSV</p>
          `,
    subject: `Ecommerce - CSV Export`,
    attachments: [attachment],
  });
};

export default async (req: NextApiRequest, res: NextApiResponse<CheckoutAPIRes>) => {
  if (req.method !== 'POST') {
    return res.status(405).send({
      error: 'Method Not Allowed',
    });
  }
  
    try {
    // Let's pretend the following method returns a string containing our data (CSV)
    const csvData = await orderService.getDailyOrders({format: 'csv'});
    console.log('✅ CSV data is ready!');

    const attachment = writeCSVtoFile(csvData);

    // mailcomposer doesn't have type definitions. I could have created them but I decided to be a bit lazy here :)
    buildEmail('hello@domain.com', attachment).build(async (err: any, message: any) => {
      if (err) {
        throw `Error sending raw email: ${err}`;
      }
      const data = await sesClient.send(new SendRawEmailCommand({ RawMessage: { Data: message } }));
      console.log('Email Message Id: ', data.MessageId);
    });

    res.status(200).json({ sent: 'ok' });
  } catch (error) {
    console.error(`❌ Failed to send email: `, error);

    return res.status(400).send({
      error: 'Could not send the email',
    });
  }
  
}

That’s all folks!


If you have any suggestions, questions, corrections or if you want to add anything please DM or tweet me: @zanonnicola