DEV Community

Cover image for Schedule and Cancel Sending an Email in Node.js with SendGrid
Sharfuddin Ahammed
Sharfuddin Ahammed

Posted on

Schedule and Cancel Sending an Email in Node.js with SendGrid

Unlocking Precision Email Delivery in Your Node.js Apps

In the realm of Node.js development, handling email communications effectively is crucial. However, timing is everything when it comes to reaching your audience with the right message at the right moment. That's where SendGrid comes in, empowering you to schedule and cancel emails with ease, directly within your Node.js applications.

Stepping Up Your Email with SendGrid:

SendGrid's versatile API seamlessly integrates with Node.js, providing you with full control over email scheduling and cancellation. Here's a breakdown of how it works:

  1. Setting Up Your Node.js Environment:

First, make sure you have Node.js and npm (Node Package Manager) installed on your machine.

Create a new directory for your project and navigate into it:

mkdir send-schedule-email
cd send-schedule-email
Enter fullscreen mode Exit fullscreen mode

Initialize a new Node.js project and install all dependencies packages:

npm init -y

npm install express express-async-handler dotenv cors body-parser moment-timezone @sendgrid/client @sendgrid/mail
Enter fullscreen mode Exit fullscreen mode

Create a file named index.js and open it in a code editor. Add the following code:

import bodyParser from "body-parser";
import cors from "cors";
import dotEnv from "dotenv";
import express from "express";
dotEnv.config(); // allow .env file to load

const app = express(); // initializing express app

const corsOptions = {
  // cors configuration options
  origin: "*",
  optionsSuccessStatus: 200,
};

app.use(cors(corsOptions)); // enable cors

app.use(bodyParser.urlencoded({ extended: true })); // enable body parsing

app.use(express.json({ limit: "3.5mb" })); // enable JSON serialization

// Demo Route for testing
app.get("/", (req, res) => {
  res.send("Our server is running...");
});

const Port = process.env.PORT || 8080;
// listening to the port
app.listen(Port, console.log("Listening to port ", Port));


Enter fullscreen mode Exit fullscreen mode

You can also install nodemon as a development dependency:

npm install --save-dev nodemon
Enter fullscreen mode Exit fullscreen mode

In your package.json file, add "start", and "dev" scripts to easily run your application in both the production & development environments:

{
  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  }
}
Enter fullscreen mode Exit fullscreen mode

Add a ".env" file, and update the environment variables with appropriate values. An example of environment variables is shown below.

PORT=8080
SENDGRID_API_KEY=<YOUR_SENDGRID_API_KEY>
Enter fullscreen mode Exit fullscreen mode

Save your changes and run the application:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Our node app should be running at "http://localhost:8080"

Get your SendGrid API key from your SendGrid account and verify the sender's email.

Now create a controller file named 'sendEmailController.js' and add the following code:

import sendGridClient from "@sendgrid/client";
import sendGridMail from "@sendgrid/mail";
import asyncHandler from "express-async-handler";
import moment from "moment-timezone";
import dotEnv from "dotenv";


dotEnv.config(); // allow .env file to load
sendGridClient.setApiKey(process.env.SENDGRID_API_KEY);
sendGridMail.setApiKey(process.env.SENDGRID_API_KEY);

// get batch Id for a scheduled email
const generateBatchId = async () => {
  const createBatchRequest = {
    url: `/v3/mail/batch`,
    method: "POST",
  };

  try {
    const [response, body] = await 
    sendGridClient.request(createBatchRequest);
    return body.batch_id;
  } catch (e) {
    console.log("Error from generateBatchId: ", e);
    return null;
  }
};

export const sendEmailIndividual = asyncHandler(async (req, res) => {

  const { to, from, subject, content, attachments, send_at } = req.body;

  try {
   // convert time into GMT-0, send_at should contain a value like "2024-01-03T16:00:00+06:00"
    let sendAt = send_at ? moment.tz(send_at, "Etc/UTC") : moment().tz("Etc/UTC");

  // Convert the Time to Unix time
  sendAt = sendAt.unix();



    // Check if scheduling is more than 72 hours in advance
    const now = moment();
    const sendAtMoment = moment(send_at);
    const diffInHours = sendAtMoment.diff(now, "hours");
    if (diffInHours > 72) {
      return res.status(400).json({
        message: "Scheduling more than 72 hours in advance is forbidden.",
      });
    }


    let batchId;

    const msg = {
      to,
      from,
      subject,
      content,
      attachments,
    };

    if (send_at) {
      const send_at_date = moment(send_at);
      if (!send_at_date.isBefore(now)) {
        // if send_at_date is not in the past
        // Create a batch id;
        batchId = await generateBatchId();
        current_status = "scheduled";
      }

      msg.send_at = sendAt;
      // include batch ID
      if (batchId) msg.batch_id = batchId;
    }

    // Send the email
    const sentResult = await sendGridMail.send(msg);
    // console.log({ sentResult });  only statusCode, headers
    // pick the send-grid message id from the headers
    const messageId =
      sentResult && sentResult.length > 0
        ? sentResult[0]?.headers["x-message-id"]
        : undefined;
    const sendAtDate = new Date(sendAt * 1000);

    res
      .status(200)
      .json({ message: "Email sent successfully.", data:{
messageId, sendAtDate, batchId: msg.batch_id
} });
  } catch (err) {
    console.log(
      "Error from sendEmailIndividual:",
      err,
      err?.response?.body?.errors
    );
    if (err.code === 400) {
      return res.status(400).json({ message: "Email payload is invalid." });
    } else {
      return res.status(500).json({ message: "Somthing went wrong." });
    }
  }
});



// Cancel Scheduled Send
export const cancelScheduledSend = asyncHandler(async (req, res) => {
  // Getting batch Id from body
  const { batch_id } = req.body;
  const cancelBatchRequest = {
    url: `/v3/user/scheduled_sends`,
    method: "POST",
    body: JSON.stringify({
      batch_id,
      status: "cancel",
    }),
  };

  try {
    // Cancel schedule from send grid
    await sendGridClient.request(cancelBatchRequest);

    return res.status(200).json({ message: "Schedule cancelled" });
  } catch (error) {
    console.log(
      "Error from cancelScheduledSend",
      error,
      error?.response?.body?.errors
    );

    if (error.response.body.errors) {
      return res
        .status(400)
        .json({ message: error.response.body.errors[0].message });
    }
    return res.status(500).json({ message: "Internal Server Error" });
  }
});

Enter fullscreen mode Exit fullscreen mode

Let's break it down, first, we have a couple of import statements,
then we initialize the sendGridClient and sendGridMail packages with the send grid API key.

The "generateBatchId" function generates a batch ID for a scheduled send. This function returns the generated batch ID or null ( if any error appears ).

In the "sendEmailIndividual" first we've destructuring assignment
of the request body, then we convert the send_at time to GMT-0 Unix time.

Next, we validate that the send_at is not more than 72 hours in advance. Send grid does not allow more than 72 hours.

Then we prepared the sendGridMail "send" method's payload called msg, we also added the "send_at" and "batch_id" fields to that payload.

Next, we request to send a scheduled mail via SendGrid like this,

const sentResult = await sendGridMail.send(msg);
Enter fullscreen mode Exit fullscreen mode

We can get the message ID from the response headers, and send it back as our API response along with the batchId.

Cancel a scheduled send is much simpler, we just need the batch ID of the scheduled sent-email, here we are getting that with the request payload, we are preparing a request payload as documented in SendGrid.

Next, we made a request, and it was done.

// Getting batch Id from body
  const { batch_id } = req.body;
  const cancelBatchRequest = {
    url: `/v3/user/scheduled_sends`,
    method: "POST",
    body: JSON.stringify({
      batch_id,
      status: "cancel",
    }),
  };

  try {
    // Cancel schedule from send grid
    await sendGridClient.request(cancelBatchRequest);

    return res.status(200).json({ message: "Schedule cancelled" });
  } catch (error) {
    console.log(
      "Error from cancelScheduledSend",
      error,
      error?.response?.body?.errors
    );

    if (error.response.body.errors) {
      return res
        .status(400)
        .json({ message: error.response.body.errors[0].message });
    }
    return res.status(500).json({ message: "Internal Server Error" });
Enter fullscreen mode Exit fullscreen mode

We are done with our controller file, let's create a "routes.js" and add the following code.

import express from "express";
import {
  sendEmailIndividual,
  cancelScheduledSend
} from "./sendEmailController.js";


const router = express.Router();
// Send Individual Email
router
  .route("/send-individual-email")
  .post(sendEmailIndividual);

// Cancel Scheduled Email
router
  .route("/cancel-schedule-email")
  .put(cancelScheduledSend);

export default router;

Enter fullscreen mode Exit fullscreen mode

Next, let's introduce these routes to our index.js file before the demo route

import emailRoutes from "./routes.js"; // (new addition)

//... old code 
app.use(express.json({ limit: "3.5mb" })); // enable JSON serialization ( old code )

// new addition
// The email routes
app.use("/api", emailRoutes);

// Demo Route for testing ( old code )
app.get("/", (req, res) => {
  res.send("Our server is running...");
});
Enter fullscreen mode Exit fullscreen mode

Check that our dev server is running or not, if needed run

npm run dev
Enter fullscreen mode Exit fullscreen mode

Finally, let's test it on Postman.

(https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8qeyozhshvv65uw74rlt.png)

(https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mrvuyda9fqdm1m13fe9x.png)

Thank you.

Top comments (0)