Configuring AWS SES email S3 bucket and Forwarding

SES rules for receiving emails

SES Rules for Receiving Emails

If we are using SES to receive emails, (rather than a 3rd party) we use the SES console (Oregon region for Australia) and configure “Email receiving” rules.

Go to SES > Configuration > Email receiving and click on default-rule-set. Click on “Create rule”.

You can click on the default-rule-set at any time later to edit or disable rules.

The diagram below shows how we make a rule to capture admin@mydomain.com. We can add other addresses as well. Or, we can use the domain name to capture any addresses. The rule is given any name. We use the pop-down menu as shown to choose an existing S3 bucket to put the emails into, or better still, we request the menu to create a new bucket for us, which will choose Oregon, and set the bucket permissions correctly for our account. If you previously created a bucket, as per our prior notes, you would have to have added the permissions manually. As a note, later when we add Lambda functions to the same rule, (to check for spam and do forwarding) it will ask us to grant permissions to those functions, so we say yes.

Here is an example where I chose an existing bucket, but it would have been better to let SES create a bucket.

The diagram shows “Object key prefix – optional”. This is where you can request emails go into a sub-folder. For instance, you could say “.ses/” with the forward slash. I use “.ses/” as a hidden directory as I also run crontab shell scripts that process the emails for archiving in a more complex manner where I found it helpful to have hidden directories. I’ll add this scripting elsewhere as an optional tool. My Lambda scripts also make use of my nominated .ses sub-folder.

Add/Verify and test an email

Add and test an email

We now add an email address and test it. It is helpful to add admin@mydomain.com. Our previous example uses this address.

As we are in sandbox mode, create the email address identity, then go to the S3 bucket and see if the email is there (or in the sub-directory nominated).

The domain identity must be verified and not in a pending state. I can’t go into problem solving here, but if an issue, check your spelling and completeness of records in your DNS, and confirm they are seen in the dns checker website. Amazon must also have sent yu an email saying the domain and dmarc is verified. If things get really our of whack (whatever that is) you can delete the domain and re-add it.

Assuming all is ok:

Go to SES (Oregon) > Configuration > Identities > “Create identity” to create an identity.

Click on the email button rather than the domain button.

Do not add a new “Mail From domain” or any other records on the summary screen. Simply add admin@mydomain.com

The SES identity page will then show the address is pending. This is where we now check the S3 bucket. You can download the email and extract the verification URL or add .eml to the filename and open it in your PC browser.

If it is not in the bucket, check all your configurations. There is no error logging at this point. We can check for errors in the Lambda functions that we add later, but not as to whether an email goes into the bucket or not.

If the email is present, click on download up the top of the page. This admin@ address is particularly important when you need to get an email received for a paid SSL.

We still cannot send other email addresses to SES from your PC email client as we are in sandbox mode. You could add other addresses if you wish to have those in your testing.

Next we add Lambda functions (Oregon) to forward emails. The address(es) you forward to must be verified as an email identity as well, due to sandbox mode, so please add those now. For instance me@gmail.com

Add Lambda IAM Role (for SES forwarding rules)

Add IAM Role – Lambda & S3 Bucket

We need to an a Lambda IAM Role so that we can create Lambda functions to access and process emails in the S3 Bucket. These functions will be called from the “SES (Oregon for Australia) > Configuration > Email receiving” rules.

Go to the IAM console > Roles > “Create Role” button. The Trusted entity type is Amazon service. From the Use case menu, select Lambda, then Next.

As in the screenshots below, I add quite a number of permissions. I have not tested reducing these, but they are fine. (You should have previously learnt how to add these sorts of items when adding an IAM User.) While creating this role, give it a name of your choosing.

Add these to the role: AdministratorAccess, AmazonS3FullAccess, AWSLambda_FullAccess, CloudwWatchFullAccess, and V2.

Click “Add Role”. Go back to the main Role panel, find the new role, then click on it so we can add a policy.

On the right you will see a small pop-down menu, “Add permissions”. Click on the sub-menu item called “Create inline policy”.

On the next panel, from the Select a service menu, choose S3. Then you will see more options. Click the checkbox “Manual actions > All S3 actions”. Don’t be concerned about warnings or errors as it is the JSON tab that we really want right.

On the Policy editor at the top, click on the JSON tab.

Using your own bucket name, add the following lines with the bucket name you intend to use (even if the bucket is not created as yet) then click Next.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": "ses:SendRawEmail",
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_EMAIL_BUCKET_NAME/*"
        }
    ]
}

On the next panel you give this a name. You cannot edit names later.

Click on the “Create policy” button. This takes yo back to the policy screen where you see your inline policy has been added. If you made a coding mistake, the editor should tell you, but you can edit the JSON at any time.

The Role will now be used to create a Lambda function in your email region, e.g. Oregon.

Here is a diagram showing the above steps:

Email Lambda functions - A Basic Notification

Add Lambda functions Node.js 22.x (SDK3)

Amazon continually removes older code. At time of writing SDK3 Node.js.22.x works.
When SDK2 was removed, there were no SDK3 replacements for a couple of years. no new clients could use the older code.

An awkward method was to install/configure postfix to use “mutt” to send email as a zip file attachment, or simply to notify an email had gone into the bucket. I still like to send a notification this has happened. Editing Lambda is not in my scope, but I have been able to work through some changes here and there.

The diagram below shows Node.js.20.x (use current instead). Go to the Lambda console (you can search on it), select your email region (e.g. Oregon), and click on Functions, Create function.

Copy and paste the following code into the Lambda function – firstly change the values for your own bucket name, domain, and personal email address. Notice the use of us-west-2 for Oregon.

Let's assume MY_BUCKET is your existing or intended email bucket name. e.g. laurenceshaw.au.inbox. I have shown me@gmail.com to illustrate.
When the new function opens remove the few lines of code to have a blank page, then add these lines (modify for your address - in sandbox mode make sure the email is verified)


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
const ses = new SESClient({ region: "us-west-2" });

export const handler = async(event,context,callback) => {
  const sesNotification = event.Records[0].ses;
  const messageId = sesNotification.mail.messageId;
  const receipt = sesNotification.receipt;
  const mail = sesNotification.mail;
  

  console.log('Notification of Email in S3 Bucket', sesNotification.mail.commonHeaders.subject);
  const notify = ('New SES MY_BUCKET e-mail: ' + sesNotification.mail.commonHeaders.subject);


  const command = new SendEmailCommand({
    Destination: {
      ToAddresses: ["me@gmail.com"],
    },
    Message: {
      Body: {
        Text: { Data: "An e-mail for MY_DOMAIN has arrived in MY_BUCKET"},
      },

      Subject: {Data: notify },
    },
    Source: "me@gmail.com",
  });

  try {
    let response = await ses.send(command);
    // process data.
    return response;
  }
  catch (error) {
    // error handling.
  }
  finally {
    // finally.
  }
};

Next, we slightly change the configuration.

Next we will add this as an SES Email receiving rule.

Add our Basic Lambda funtion to SES as an Email receiving rule

Add a Lambda function to SES Email receiving rules

The SES “Receiving rule” below will catch emails for admin@mydomain.com. Have this address identity verified in your sandbox mode. It places the email into your S3 bucket, called mydomain.com.inbox in this example. Below the rule that adds it to the bucket, we choose the Lambda function(s) we want to run. I am showing the basic notification function we previously created. This assumes you have previously created the SES and S3 bucket “bare-bones” configurations.

From your PC send an email to admin@mydomain.com. You should receive a notification in your PC email client. To get used to all this, check the email is in the bucket. If you created a bucket sub-directory, such as .ses/, you could fill out the field “Object key prefix – optional) with the string .ses/ and that would direct the email there, or whatever name you choose.

Then go to the S3 Bucket and verify it arrived. As an exercise, click on it, download it, add a .eml file extension, and view it.

Whether or not things work, we need to review the CloudWatch logs (Oregon for our examples). They will show if the Lambda function passed or failed.

Go to: CloudWatch (Oregon) > Logs > Log groups

You should see a Log group with your Lambda function name, e.g. mydomain_com_notify.

Click on the left checkbox, then under the Action button, Edit retention setting(s). We don’t want to keep log entries forever, so perhaps change to a a few days. After doing this, refresh to page to see the result.

Click on the Log group’s URL. You will see a Log stream. Click on it.

You will see a page with the Timestamp of all the processes that happened when receiving the email under that Lambda rule.

This is where you check for errors. If an error is obvious, you will know. If not, check your function for typos, or did you forget to add permissions to the S3 bucket.

The configurations I have given do work, but I have often found I missed a step or typed incorrectly.

Next we will add more Lambda functions.

Move out of Sandbox Mode

Remove sandbox mode so all emails are delivered

(Remember, if using postfix sendmail commands for any reason, you still have to verify the email address in SES)

Amazon changes how it does things from time to time. Removing sandbox mode can be searched in Google – “AWS remove sandbox mode”.

AWS will request some information as you proceed, or clarification requested in emails after you start the process.

There are some things to remember which the following can help you with:

You will apply for the 50,000 emails a day at 14 per second for your configuration. There is no other smaller setting.

Basically, in the forms say you will comply with SES, that it is not for marketing, it will be used for the WordPress website contact form, only using the WordPress emails to reply to clients (e.g. not creating email lists – you could put an option to add to a subscription in your contact form or use of products like MailChimp but do not need to advise of this) and forwarding a small number of emails to your personal address. Say you have configured the required postmaster@, abuse@, admin@ addresses, and you will monitor the statistics for any bounces and correct. That’s it. I also add administrator@, webmaster@, noreply@ and dmarc@. I add dmarc@ as a separate SES rule and don’t need notifications on those. I set a life cycle rule of a few days on the bucket that holds dmarc@ emails.

This should get you approved.

Once you are out of sandbox mode, you can go to town on adding addresses to any Lambda functions and the SES receiving rules.

You can review if you are still in public mode by your region’s SES account dashboard. Other regions will show you are in sandbox mode. Have some care to ensure bounced emails are never excessive, which can be reviewed in your SES statistics.

If you wish to open ports in your local region (e.g. Sydney or Melbourne) for using your own email service on Amazon, you request to open it out of sandbox mode as well, and say why. However, configuring your own email server is no simple matter and has high risks of failures. I found literally after three+ years of testing it was not worth using certain email services.

I also found serious problems for some known 3rd party email services – slowness, emails not downloading which stop other emails being viewed or created, emails with no content in the summary line.

SDK3 Node.js.22x Lambda Functions

These are the functions we can use to filter bad emails and forward to our PC/Mobile client.

  1. Drop spam
  2. Drop bad domains or addresses
  3. Filter for unwanted words in subject line
  4. Filter spam using bad replyTo addresses
  5. Forward to your email client

Add these to your SES Email receiving rule as required. First rule is to place into the S3 Bucket. Edit the rule until you get to the page to Add a New Action. From the drop-down select Invoke AWS Lambda function and choose the one you want. I’d suggest the basic function we did earlier if you always want a notification as any subsequent failures will stop more processing. Remember the CloudWatch logs help.

Always check “RequestResponse invocation” for each function, and use “Event invocation” for the last rule.

When you save your rule, AWS will prompt to add permissions – yes.

You can create another rule to the same bucket, using something like .dmarc/ as a sub-directory for dmarc emails, and use a life cyle rule of a few days.

Your lifecycle for other emails could be 365 days.

Drop spam

I show the first basic cut of the code, and then more extended. You have to modify strings shown for yourself.
If email fails, the follow on Lambda functions are not processed. This is why it is best to add these after the basic Lambda notification function we previously made.

We should have a dmarc test or too many spams go through to us, so I added dmarcVerdict.status below.

LAMBDA FUNCTION to Drop Spam - e.g. call it SDK3_mydomain_com_spam

export const handler = async (event, context, callback) => {
  console.log('Spam filter');
  
  const sesNotification = event.Records[0].ses;
  console.log("SES Notification:\n", JSON.stringify(sesNotification, null, 2));
  
  // Check if any spam check failed
  if (sesNotification.receipt.spfVerdict.status === 'FAIL'
          || sesNotification.receipt.dkimVerdict.status === 'FAIL'
          || sesNotification.receipt.spamVerdict.status === 'FAIL'
          || sesNotification.receipt.dmarcVerdict.status === 'FAIL'
          || sesNotification.receipt.virusVerdict.status === 'FAIL') {
              
      console.log('Dropping spam');

      // Stop processing rule set, dropping message
      callback(null, {'disposition':'STOP_RULE_SET'});
  } else {
      callback(null, {'disposition':'CONTINUE'});   
  }
};

[save the code]
Under configuration change the time from 3s to 5s.

I have now modified to include more tests:

export const handler = async (event, context, callback) => {
  console.log('Spam filter');
  
  const sesNotification = event.Records[0].ses;
  console.log("SES Notification:\n", JSON.stringify(sesNotification, null, 2));
  
  // Check if any spam check failed
        if (sesNotification.receipt.spfVerdict.status === 'FAIL'
          || sesNotification.receipt.spfVerdict.status === 'GRAY'
          || sesNotification.receipt.spfVerdict.status === 'PROCESSING_FAILED'
          || sesNotification.receipt.spamVerdict.status === 'FAIL'
          || sesNotification.receipt.spamVerdict.status === 'GRAY'
          || sesNotification.receipt.spamVerdict.status === 'PROCESSING_FAILED'
          || sesNotification.receipt.dkimVerdict.status === 'FAIL'
        //  || sesNotification.receipt.dkimVerdict.status === 'GRAY'
          || sesNotification.receipt.dkimVerdict.status === 'PROCESSING_FAILED'
          || sesNotification.receipt.dmarcVerdict.status === 'FAIL'
          || sesNotification.receipt.dmarcVerdict.status === 'GRAY'
          || sesNotification.receipt.dmarcVerdict.status === 'PROCESSING_FAILED'
          || sesNotification.receipt.virusVerdict.status === 'GRAY'
          || sesNotification.receipt.virusVerdict.status === 'PROCESSING_FAILED'
          || sesNotification.receipt.virusVerdict.status === 'FAIL') {
              
      console.log('Dropping spam');

      // Stop processing rule set, dropping message
      callback(null, {'disposition':'STOP_RULE_SET'});
  } else {
      callback(null, {'disposition':'CONTINUE'});   
  }
};

I had to comment out the GRAY test for dkim as there were too many valid emails being dropped. If you find emails dropped that should not, modify the above.

Drop bad domains or addresses

LAMBDA SDK3 Function to Drop bad domains or email addresses - e.g. call it SDK3_mydomain_com_domains

'use strict';

export const handler = async (event) => {
    console.log('Blocking email filter starting');
    const sesNotification = event.Records[0].ses;
    const messageId = sesNotification.mail.messageId;
    const receipt = sesNotification.receipt;
    const mail = sesNotification.mail;

    // Convert the environment variable into array. Clean spaces from it.
    const blockingListString = process.env.blockingList;
    const blockingListArray = blockingListString.replace(/\s/g, '').split(",");

    // Check if the mail source matches with any of the email addresses or domains defined in the environment variable
    const isListed = () => {
        return blockingListArray.some(item => mail.source.endsWith(item));
    };

    console.log('Processing message:', messageId);

    // Processing the message
    if (isListed()) {
        console.log('Rejecting messageId: ', messageId, ' - Source: ', mail.source, ' - Recipients: ', receipt.recipients, ' - Subject: ', mail.commonHeaders['subject']);
        return { disposition: 'STOP_RULE_SET' };
    } else {
        console.log('Accepting messageId:', messageId, ' - Source: ', mail.source, ' - Recipients: ', receipt.recipients, ' - Subject: ', mail.commonHeaders['subject']);
        return { disposition: 'CONTINUE' };
    }
};


[save the code]

Change the Configuration tab to show 5s instead of 3s.
Create the blocking rules via the Configuration > Environment variables tab:
Call the key: blockingList. (with a capital L)
In the values field use things like this: .io,.info,.biz,.cn,.ru,.tech,.mx, baddomain.com, jo@youareapest.com

Filter unwanted words in Subject line

This function will filter out words included in the subject line

e.g. SEO

Notes:

– this can be changed to equal a word by removing ‘include’ and using ===

– this is case dependent

– if you filtered out a Subject: word like ‘Business’ it would also filter out things like ‘Business ergonomics are our our thing’ so a little care is needed. You will get to know which spam subject words you receive over time.

'use strict';

export const handler = async (event,context,callback) => {
    console.log('Blocking email filter starting');
    const sesNotification = event.Records[0].ses;
    const messageId = sesNotification.mail.messageId;
    const receipt = sesNotification.receipt;
    const mail = sesNotification.mail;
    
    const searchValuea = "SEO";
    const searchValueb = "Potential";
    const searchValuec = "Failure";
    const searchValued = "Boost";
    const searchValuee = "Brand";
    const searchValuef = "Business";
    const searchValueg = "Proven";
    const searchValueh = "Can I";
    const searchValuei = "Fix";
    const searchValuej = "Social Media";
    const searchValuek = "business";

  if (sesNotification.mail.commonHeaders.subject.includes(searchValuea)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValueb)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValuec)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValued)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValuee)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValuef)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValueg)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValueh)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValuei)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValuej)
  ||  sesNotification.mail.commonHeaders.subject.includes(searchValuek)) {
              
      console.log('Dropping Subject', sesNotification.mail.commonHeaders.subject);

      // Stop processing rule set, dropping message
      callback(null, {'disposition':'STOP_RULE_SET'});
  } else {
      callback(null, {'disposition':'CONTINUE'});   
  }

};

Filter bad replyTo addresses

Suppose the email passed all tests, seemingly valid, but it sneakily inserted a replyTo field to some scam site in Japan.

If the replyTo field is absent, we can’t test for a value, so we first test if there is a replyTo field and then block what we want.

For example:

'use strict';
export const handler = async (event,context,callback) => {
console.log('Blocking email replyTo filtering');
const sesNotification = event.Records[0].ses;
const messageId = sesNotification.mail.messageId;
const receipt = sesNotification.receipt;
const mail = sesNotification.mail;

const searchReplya = ".jp";

if (sesNotification.mail.commonHeaders.replyTo)

{

if (sesNotification.mail.commonHeaders.replyTo.includes(searchReplya)) {

console.log('Dropping replyTo', sesNotification.mail.commonHeaders.replyTo);
// Stop processing rule set, dropping message
callback(null, {'disposition':'STOP_RULE_SET'});
} else {
callback(null, {'disposition':'CONTINUE'});   
}

}

};

This searches for Japan .jp email domains. Alter the standard configuration of 3s to 5s (seconds)

We then add the rule to SES Email rules, perhaps below the spam and filter tests. I will show an example in a moment.

Forward to your email client

This is the ultimate function we want to have – forward the email. You can configure to forward to multiple people, and catch various addresses, so long as those addresses are in the SES Email receiving configuration, and you are out of sandbox mode.

You need to edit the fields near the top for your own Oregon (assumed) bucket name where it says MY_BUCKET. My example uses the sub-directory .ses/

The example code lists one email primary domain with some aliases and multiple forwarding – so change to suite your addresses.

"use strict";

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { SESClient, SendRawEmailCommand } from "@aws-sdk/client-ses";

console.log("AWS Lambda SES Forwarder // @arithmetric // Version 5.0.0");

// Configure the S3 bucket and key prefix for stored raw emails, and the
// mapping of email addresses to forward from and to.
//
// Expected keys/values:
//
// - fromEmail: Forwarded emails will come from this verified address
//
// - subjectPrefix: Forwarded emails subject will contain this prefix
//
// - emailBucket: S3 bucket name where SES stores emails.
//
// - emailKeyPrefix: S3 key name prefix where SES stores email. Include the
//   trailing slash.
//
// - allowPlusSign: Enables support for plus sign suffixes on email addresses.
//   If set to `true`, the username/mailbox part of an email address is parsed
//   to remove anything after a plus sign. For example, an email sent to
//   `example+test@example.com` would be treated as if it was sent to
//   `example@example.com`.
//
// - forwardMapping: Object where the key is the lowercase email address from
//   which to forward and the value is an array of email addresses to which to
//   send the message.
//
//   To match all email addresses on a domain, use a key without the name part
//   of an email address before the "at" symbol (i.e. `@example.com`).
//
//   To match a mailbox name on all domains, use a key without the "at" symbol
//   and domain part of an email address (i.e. `info`).
//
//   To match all email addresses matching no other mapping, use "@" as a key.
var defaultConfig = {
  fromEmail: "contact@mydomain.com",
  subjectPrefix: "",
  emailBucket: "MY_BUCKET",
  emailKeyPrefix: ".ses/",
  allowPlusSign: true,
  forwardMapping: {
    "admin@mydomain.com": [
      "me@pm.me"
     ],
    "john@mydomain.com": [
      "john@gmail.com"
    ],
    "contact@mydomain.com": [
      "me@pm.me",
      "you@gmail.com"
    ],
    "noreply@mydomain.com": [
      "me@gmail.com"
    ],
    "postmaster@mydomain.com": [
      "me@gmail.com"
    ],
    "webmaster@mydomain.com": [
      "me@gmail.com"
    ],
    "administrator@mydomain.com": [
      "bob@outlook.com"
    ],
    "abuse@mydomain.com": [
      "me@gmail.com",
      "you@gmail.com"
    ]
  }
};

/**
 * Parses the SES event record provided for the `mail` and `receipients` data.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
async function parseEvent(data) {
  // Validate characteristics of a SES event record.
  if (!data.event ||
      !data.event.hasOwnProperty('Records') ||
      data.event.Records.length !== 1 ||
      !data.event.Records[0].hasOwnProperty('eventSource') ||
      data.event.Records[0].eventSource !== 'aws:ses' ||
      data.event.Records[0].eventVersion !== '1.0') {
    return Promise.reject(new Error('Error: Received invalid SES message.'));
  }

  data.email = data.event.Records[0].ses.mail;
  data.recipients = data.event.Records[0].ses.receipt.recipients;
  return Promise.resolve(data);
}

/**
 * Transforms the original recipients to the desired forwarded destinations.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */

async function transformRecipients(data) {
  const newRecipients = new Set(); // Use a Set for efficient uniqueness checks
  data.originalRecipients = data.recipients;

  for (const origEmail of data.originalRecipients) {
    const origEmailKey = data.config.allowPlusSign
      ? origEmail.toLowerCase().split('+')[0]
      : origEmail.toLowerCase();

    // Check for direct email mappings
    if (data.config.forwardMapping[origEmailKey]) {
      data.config.forwardMapping[origEmailKey].forEach(email => newRecipients.add(email));
      continue; // Skip further checks if a direct mapping is found
    }

    // Check for domain and username mappings
    const [origEmailUser, origEmailDomain] = origEmailKey.split('@');
    if (origEmailDomain && data.config.forwardMapping[`@${origEmailDomain}`]) {
      data.config.forwardMapping[`@${origEmailDomain}`].forEach(email => newRecipients.add(email));
    } else if (origEmailUser && data.config.forwardMapping[origEmailUser]) {
      data.config.forwardMapping[origEmailUser].forEach(email => newRecipients.add(email));
    } else if (data.config.forwardMapping['@']) {
      data.config.forwardMapping['@'].forEach(email => newRecipients.add(email));
    }
  }

  data.recipients = Array.from(newRecipients); // Convert Set back to Array
  return data;
}


/**
 * Fetches the message data from S3.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
async function fetchMessage(data) {
    // Copying email object to ensure read permission
    const s3Params = {
        Bucket: data.config.emailBucket,
        Key: data.config.emailKeyPrefix + data.email.messageId,
    };

    try {
        const s3Response = await data.s3.send(new GetObjectCommand(s3Params));
        // Stream the S3 object contents
        const stream = s3Response.Body;
        const chunks = [];
        for await (const chunk of stream) {
            chunks.push(chunk);
        }
        data.emailData = Buffer.concat(chunks).toString('utf-8');
        return data;
    } catch (error) {
        console.error("Error fetching message from S3:", error);
        throw new Error('Error: Failed to load message body from S3.');
    }
}

/**
 * Processes the message data, making updates to recipients and other headers
 * before forwarding message.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
async function processMessage(data) {
  var match = data.emailData.match(/^((?:.+\r?\n)*)(\r?\n(?:.*\s+)*)/m);
  var header = match && match[1] ? match[1] : data.emailData;
  var body = match && match[2] ? match[2] : '';

  // Add "Reply-To:" with the "From" address if it doesn't already exists
  if (!/^reply-to:[\t ]?/mi.test(header)) {
    match = header.match(/^from:[\t ]?(.*(?:\r?\n\s+.*)*\r?\n)/mi);
    var from = match && match[1] ? match[1] : '';
    if (from) {
      header = header + 'Reply-To: ' + from;
    }
  }

  // SES does not allow sending messages from an unverified address,
  // so replace the message's "From:" header with the original
  // recipient (which is a verified domain)
  header = header.replace(
    /^from:[\t ]?(.*(?:\r?\n\s+.*)*)/mgi,
    function(match, from) {
      var fromText;
      if (data.config.fromEmail) {
        fromText = 'From: ' + from.replace(/<(.*)>/, '').trim() +
        ' <' + data.config.fromEmail + '>';
      } else {
        fromText = 'From: ' + from.replace('<', 'at ').replace('>', '') +
        ' <' + data.originalRecipient + '>';
      }
      return fromText;
    });

  // Add a prefix to the Subject
  if (data.config.subjectPrefix) {
    header = header.replace(
      /^subject:[\t ]?(.*)/mgi,
      function(match, subject) {
        return 'Subject: ' + data.config.subjectPrefix + subject;
      });
  }

  // Replace original 'To' header with a manually defined one
  if (data.config.toEmail) {
    header = header.replace(/^to:[\t ]?(.*)/mgi, () => 'To: ' + data.config.toEmail);
  }

  // Remove the Return-Path header.
  header = header.replace(/^return-path:[\t ]?(.*)\r?\n/mgi, '');

  // Remove Sender header.
  header = header.replace(/^sender:[\t ]?(.*)\r?\n/mgi, '');

  // Remove Message-ID header.
  header = header.replace(/^message-id:[\t ]?(.*)\r?\n/mgi, '');

  // Remove all DKIM-Signature headers to prevent triggering an
  // "InvalidParameterValue: Duplicate header 'DKIM-Signature'" error.
  // These signatures will likely be invalid anyways, since the From
  // header was modified.
  header = header.replace(/^dkim-signature:[\t ]?.*\r?\n(\s+.*\r?\n)*/mgi, '');

  data.emailData = header + body;
  return Promise.resolve(data);
}

/**
 * Send email using the SES sendRawEmail command.
 *
 * @param {object} data - Data bundle with context, email, etc.
 *
 * @return {object} - Promise resolved with data.
 */
async function sendMessage(data) {
  var params = {
    Destinations: data.recipients,
    Source: data.originalRecipient,
    RawMessage: {
      Data: Buffer.from(data.emailData)
    }
  };
  return new Promise(function(resolve, reject) {
    data.ses.send(new SendRawEmailCommand(params), function(err, result) {
      if (err) {
        return reject(new Error('Error: Email sending failed.'));
      }
      resolve(data);
    });
  });
}

/**
 * Handler function to be invoked by AWS Lambda with an inbound SES email as
 * the event.
 *
 * @param {object} event - Lambda event from inbound email received by AWS SES.
 * @param {object} context - Lambda context object.
 * @param {object} callback - Lambda callback object.
 * @param {object} overrides - Overrides for the default data, including the
 * configuration, SES object, and S3 object.
 */
export const handler = async (event, context, callback, overrides) => {
  try {
    let steps = overrides?.steps || [
      parseEvent,
      transformRecipients,
      fetchMessage,
      processMessage,
      sendMessage
    ];

    let data = {
      event,
      context,
      config: overrides?.config || defaultConfig,
      ses: overrides?.ses || new SESClient(),
      s3: overrides?.s3 || new S3Client({ signatureVersion: 'v4' })
    };

    for (const step of steps) {
      data = await step(data);
    }

    console.log("Process finished successfully.");
    callback(null, 'Success');
  } catch (error) {
    console.error("Error in processing:", error);
    callback(error);
  }
};

 

Familiarity with all the previous content is a major milestone that most web services will not cover or therefore be able to offer. It is quite a learning curve and an achievement.

Start typing and press Enter to search