DEV Community

Jay Parekh
Jay Parekh

Posted on

AWS SES, API Gateway - POST and Serverless

Pre-requisites

  • Require to setup Serverless on workstation and get it configured with AWS as cloud provider and AWS access key - programmatic user

SES-API Gateway-Lambda


Simple Email Service (SES) setup on AWS


Simple Email Service (SES) and Serverless

  • Create a Node.js starter project on serverless workstation
  • Update the handler.js file which has a createContact lambda function
"use strict";
const AWS = require("aws-sdk");
const ses = new AWS.SES();

module.exports.createContact = async (event, context) => {
  console.log("Received:::", event);
  const { to, from, subject, message } = JSON.parse(event.body);

  if (!to || !from || !subject || !message) {
    return {
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Methods": "*",
        "Access-Control-Allow-Origin": "*",
      },
      statusCode: 400,
      body: JSON.stringify({ message: " to or from... are not set properly!" }),
    };
  }
  const params = {
    Destination: {
      ToAddresses: [to],
    },
    Message: {
      Body: {
        Text: { Data: message },
      },
      Subject: { Data: subject },
    },
    Source: from,
  };
  try {
    await ses.sendEmail(params).promise();
    return {
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Methods": "*",
        "Access-Control-Allow-Origin": "*",
      },
      statusCode: 200,
      body: JSON.stringify({
        message: "email sent successfully!",
        success: true,
      }),
    };
  } catch (error) {
    console.error(error);
    return {
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Methods": "*",
        "Access-Control-Allow-Origin": "*",
      },
      statusCode: 400,
      body: JSON.stringify({
        message: "The email failed to send",
        success: true,
      }),
    };
  }
};
Enter fullscreen mode Exit fullscreen mode
  • Update the serverless.yml file.
    • region: ap-south-1 Mumbai
    • functions: createContact
    • events: -http
    • path: /mail
    • method: POST
service: aws-ses-project

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs14.x
  region : ap-south-1

functions:
  createContact:
    handler: handler.createContact
    events:
      - http:
          path: /mail
          method: POST
Enter fullscreen mode Exit fullscreen mode
  • Deploy the changes using serverless deploy which will create
    • Lambda function - createContact
createContact: aws-ses-project-dev-createContact (1.7 kB)

Improve API performance – monitor it with the Serverless Dashboard: run "serverless"
Enter fullscreen mode Exit fullscreen mode
{ "to": "<-sender-email->@gmail.com", 
"from": "<-receiver-email->@gmail.com", 
"subject": "test mail", 
"message": "send successfully" }

Enter fullscreen mode Exit fullscreen mode

Image description

  • NOTE The response of the POST method says that API is successfully set and triggered the event but "the email is failed to send"

  • To diagnose and debug the error go to API POST -> Logs method(Observability Tool - Metrics/Logs/Traces) and choose *Open in CloudWatch * in Logs.
    CloudWatch

  • The error message is as follows :

2022-04-25T18:54:54.759Z    1623581b-c472-4002-bc06-519766d1f0be    ERROR   AccessDenied: User `arn:aws:sts::673040272970:assumed-role/aws-ses-project-dev-ap-south-1-lambdaRole/aws-ses-project-dev-createContact' is not authorized to perform `ses:SendEmail' on resource `arn:aws:ses:ap-south-1:673040272970:identity/<-sender-email->@gmail.com'
    at Request.extractError (/var/runtime/node_modules/aws-sdk/lib/protocol/query.js:50:29)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:78:10)
    at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:686:14)
    at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)
    at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12)
    at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9)
    at Request.<anonymous> (/var/runtime/node_modules/aws-sdk/lib/request.js:688:12)
    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:116:18) {
  code: 'AccessDenied',
  time: 2022-04-25T18:54:54.756Z,
  requestId: 'd2111db9-a75d-4c37-bbee-e4d4738268ea',
  statusCode: 403,
  retryable: false,
  retryDelay: 22.98167156490245
}
Enter fullscreen mode Exit fullscreen mode

What caused the error ? : Simple Email Service (SES) and Lambda are two different AWS managed services which does not have access to each other by default.
Solution: Setup an iam role with policy in a way that lambda can access ses functionalities


Create an IAM Role for Lambda to access SES

  • Go to IAM → Role → AWS Service Role AWS Service
  • Use case : enable Lambda (radio button or select from drop down list).
    Lambda SES

  • Setting up permission - AWSSESFullAccess
    Permission SES Full access

  • Set the name of the role : Lambda-SES-Full-Access

Postman SES

  • NOTE: Message field says : email sent successfully
  • Confirming on gmail Gmail snap

Creating a frontend to send email using Next.js

  • Go to Setup Next.js app in workstation. (To know more Next.js)
  • Create a Next.js application in aws-ses-project folder (same as above)
npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter
Enter fullscreen mode Exit fullscreen mode
  • cd nextjs-blog and run build npm run dev which will deploy a Welcome Next.js page accessible on http://localhost:3000. Webpage
  • Folder structure of nextjs-blog
    Folder structure nextjs-blog

  • Modifying the index.js by creating mail form replacing the welcome index page
    Create Contact Mailform

import Head from "next/head";

export default function Home() {
  const sendMessage = async (event) => {
    event.preventDefault();

    /*
      "to": "youremail@gmail.com",
    "from": "email@gmail.com",
    "subject": "this needs to work!",
    "message": "Hello there,!@"

    */

    const res = await fetch(
 # Replace this by your api method gateway
      "https://3t66ov5uxg.execute-api.us-east-1.amazonaws.com/dev/contact-us",
      {
        body: JSON.stringify({
          to: event.target.to.value,
          from: event.target.from.value,
          subject: event.target.subject.value,
          message: event.target.message.value,
        }),
        headers: {
          "Content-Type": "application/json",
        },
        method: "POST",
      }
    );

    const result = await res.json();
    console.log(result);
  };
  return (
    <div className='container'>
      <Head>
        <title>Contact us</title>
        <link rel='icon' href='/favicon.ico' />
      </Head>

      <main>
        <h1 className='title'>Contact us</h1>

        <div className='grid'>
          <form onSubmit={sendMessage}>
            <label htmlFor='to'>To: </label>
            <input id='to' name='to' type='text' required />

            <label htmlFor='from'>From: </label>
            <input
              id='from'
              name='from'
              type='text'
              autoComplete='from'
              required
            />

            <label htmlFor='subject'>Subject: </label>
            <input id='subject' name='subject' type='text' required />

            <label htmlFor='message'>Message: </label>
            <input id='message' name='message' type='text' required />
            <button type='submit'>Send</button>
          </form>
        </div>
      </main>

      <footer>Powered by Awesomness</footer>

      <style jsx>{`
        /* Style inputs */
        input[type="text"],
        select {
          width: 100%;
          padding: 12px 20px;
          margin: 8px 0;
          display: inline-block;
          border: 1px solid #ccc;
          border-radius: 4px;
          box-sizing: border-box;
        }

        /* Style the submit button */
        input[type="submit"] {
          width: 100%;
          background-color: #04aa6d;
          color: white;
          padding: 14px 20px;
          margin: 8px 0;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }

        /* Add a background color to the submit button on mouse-over */
        input[type="submit"]:hover {
          background-color: #45a049;
        }
        .container {
          min-height: 100vh;
          padding: 0 0.5rem;
          display: flex;
          flex-direction: column;
          justify-content: center;
          align-items: center;
        }

        main {
          padding: 5rem 0;
          flex: 1;
          display: flex;
          flex-direction: column;
          justify-content: center;
          align-items: center;
        }

        footer {
          width: 100%;
          height: 100px;
          border-top: 1px solid #eaeaea;
          display: flex;
          justify-content: center;
          align-items: center;
        }

        footer img {
          margin-left: 0.5rem;
        }

        footer a {
          display: flex;
          justify-content: center;
          align-items: center;
        }

        a {
          color: inherit;
          text-decoration: none;
        }

        .title a {
          color: #0070f3;
          text-decoration: none;
        }

        .title a:hover,
        .title a:focus,
        .title a:active {
          text-decoration: underline;
        }

        .title {
          margin: 0;
          line-height: 1.15;
          font-size: 4rem;
        }

        .title,
        .description {
          text-align: center;
        }

        .description {
          line-height: 1.5;
          font-size: 1.5rem;
        }

        code {
          background: #fafafa;
          border-radius: 5px;
          padding: 0.75rem;
          font-size: 1.1rem;
          font-family: Menlo, Monaco, Lucida Console, Liberation Mono,
            DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
        }

        .grid {
          display: flex;
          align-items: center;
          justify-content: center;
          flex-wrap: wrap;

          max-width: 800px;
          margin-top: 3rem;
        }

        .card {
          margin: 1rem;
          flex-basis: 45%;
          padding: 1.5rem;
          text-align: left;
          color: inherit;
          text-decoration: none;
          border: 1px solid #eaeaea;
          border-radius: 10px;
          transition: color 0.15s ease, border-color 0.15s ease;
        }

        .card:hover,
        .card:focus,
        .card:active {
          color: #0070f3;
          border-color: #0070f3;
        }

        .card h3 {
          margin: 0 0 1rem 0;
          font-size: 1.5rem;
        }

        .card p {
          margin: 0;
          font-size: 1.25rem;
          line-height: 1.5;
        }

        .logo {
          height: 1em;
        }

        @media (max-width: 600px) {
          .grid {
            width: 100%;
            flex-direction: column;
          }
        }
      `}</style>

      <style jsx global>{`
        html,
        body {
          padding: 0;
          margin: 0;
          font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
            Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue,
            sans-serif;
        }

        * {
          box-sizing: border-box;
        }
      `}</style>
    </div>
  );
}   
Enter fullscreen mode Exit fullscreen mode
  • Replace the POST method URL in index.js
    Index.js post method replacement

  • Cross-Origin Resource Sharing (CORS) Error while sending the POST request. Go to inspect elementconsole to know the error.

Access to fetch at 'https://oadnufagsd.execute-api.ap-south-1.amazonaws.com/dev' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Enter fullscreen mode Exit fullscreen mode
  • What caused the error ?: localhost which a different IP-address is trying to fetch https://oadnufagsd.execute-api.ap-south-1.amazonaws.com/dev which is at a different IP address. By default due to security concerns CORS is disabled which prevents information to share with other IPs or resources.
    Solution: CORS policy has to enabled in order to share resource or in other words access a different IP address in our case.

    • Method-1: Through AWS Console
    • Enabling CORS via AWS Console by going to API Gateway → aws-dev-ses-project → /mail/post → Actions → Select Enable CORS from dropdown Image description
    • Method-2: Serverless
      • Through serverless.yml add cors: true under API method serverless.yml
functions:
  createContact:
    handler: handler.createContact
    events:
      - http:
          path: /mail
          method: POST
          cors: true

Enter fullscreen mode Exit fullscreen mode
  • Deploy the changes sls deploy and run cd nextjs-blog && npm run dev

Mail form

  • Confirming through gmail

Gmail CORS


Note: I dont know Node.js was helped by online session

Discussion (0)