AWS SES, API Gateway - POST and Serverless
Tags: aws, serverless, node
Published: 2022-05-01 22:12:09

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yn4dh540lzpri50xst76.png)

---

## Simple Email Service (SES) setup on AWS 

* Go to SES →  Create identity sender email : blah.aws@gmail.com and receiver email : blah-blah@gmail.com
![SES email registration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zv65j0t221l3kqflbbs5.png)
 
* Verify the email

---

## 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 
```javascript
"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,
      }),
    };
  }
};
```
* Update the serverless.yml file.
    * region: ap-south-1 Mumbai
    * functions: createContact 
    * events: -http
    * path: /mail
    * method: POST 
```yaml
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
```
* Deploy the changes using `serverless deploy` which will create
   * Lambda function - createContact 

```bash
createContact: aws-ses-project-dev-createContact (1.7 kB)

Improve API performance – monitor it with the Serverless Dashboard: run "serverless"
```
   * API Gateway - /mail POST method - Trigger to invoke createContact - API Gateway POST endpoint: **https://oadnufagsd.execute-api.ap-south-1.amazonaws.com/dev/mail
**   
![AWS-SES-Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yetgq3ob9kkq38v395eq.png)

 * Testing the API and Lambda function using POSTMAN - POST method
    * Checking the POST method url: https://oadnufagsd.execute-api.ap-south-1.amazonaws.com/dev/mail 
    * body : 

```json
{ "to": "<-sender-email->@gmail.com", 
"from": "<-receiver-email->@gmail.com", 
"subject": "test mail", 
"message": "send successfully" }

```
 
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ctoj36ycwcx168ke17ml.png)

* **<u>NOTE</u>** 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xoohfupln0c5xyxtuwr1.png)

* The error message is as follows : 
```Bash
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
}
```
**_<u>What caused the error ? </u>_**: **Simple Email Service (SES)** and **Lambda** are two different AWS managed services which does not have access to each other by default. 
**<u>Solution:</u>** 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wygwkk1l1tqy3jerjycp.png)
* Use case : enable **Lambda** (radio button or select from drop down list).
![Lambda SES](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4y4bhxo60iun11hkl24v.png)
 
* Setting up **permission** - `AWSSESFullAccess`
![Permission SES Full access](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9wmg68t3ksd3c8vxn5k.png)
* Set the name of the role : `Lambda-SES-Full-Access`


![Postman SES](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sf2gxt518l12ajekypo2.png)

* **<u>_NOTE:_</u>** Message field says : **_email sent successfully_**
* Confirming on gmail 
![Gmail snap](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqcczx9ko3029b99n5nq.png)
 

---

## Creating a frontend to send email using Next.js

* Go to [Setup Next.js app](https://nextjs.org/learn/basics/create-nextjs-app/setup) in workstation. (To know more [Next.js](https://nextjs.org/))
* Create a Next.js application in **aws-ses-project** folder (same as above) 

```javascript
npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn/tree/master/basics/learn-starter
```
* `cd nextjs-blog` and `run build npm run dev` which will deploy a _**Welcome Next.js**_ page accessible on `http://localhost:3000`.
![Webpage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qwy0l1z2n9ugvi6sfg7o.png)
* Folder structure of **nextjs-blog**
![Folder structure nextjs-blog](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l9b6wxrw6nqkgk2op67c.png)
 
* Modifying the **index.js** by creating **mail form** replacing the welcome index page
![Create Contact Mailform](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1nmpxscirse7lhlm92rq.png)
 ```javascript
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>
  );
}   
```
 * Replace the POST method URL in **index.js**
![Index.js post method replacement](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j86q84p007lgg297gesx.png)
 
 * **Cross-Origin Resource Sharing (CORS) Error** while sending the POST request. Go to **inspect element** → **console** to know the error.

```bash
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.
```

 * **_<u>What caused the error ?</u>_**: **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.
  **<u>Solution:</u>** **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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6brtm3va9d56kwe6wcdv.png)
   * Method-2: Serverless 
     * Through **serverless.yml** add `cors: true` under API method
![serverless.yml](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aa497dzmsnjcqn5z9w6k.png)

```yaml
functions:
  createContact:
    handler: handler.createContact
    events:
      - http:
          path: /mail
          method: POST
          cors: true

```
 * Deploy the changes `sls deploy` and run `cd nextjs-blog && npm run dev`

![Mail form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcusc4dg9xvht15m2f5f.jpg)

 * Confirming through gmail

![Gmail CORS](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dnpk5vgrgvxw1bw3wvbe.png)

---

**<sub><u>_Note_</u></sub>**<sub>: I dont know **Node.js** was helped by online session</sub>