Implement an Email Masking Proof-of-concept Using AWS Simple Email Service (SES) and AWS Rust SDK

Stem the email marketing tide using masking

Julien de Charentenay
Better Programming

--

Image by Anne-Onyme from Pixabay

This story presents a proof-of-concept that allow me to give a unique email address anything@mydomain.com when subscribing to mailing lists, and registering unimportant accounts. anything can be any word that I decide on the spot when I give the address. The emails are forwarded to my normal email address — and if I find that I am receiving too many, I can unsubscribe and blacklist the address…

As an added benefit, I am now using a different password and email address when registering on websites… That should keep me safer.

The views/opinions expressed in this story are my own. This story relates to my personal experience and choices and is provided in the hope that it will be useful but without any warranty.

I like to subscribe to news, forums, etc. But, I have been pwned… and I do not read most emails — just casually scan through subjects to see if anything spike my interest… I can unsubscribe, but I have the impression that these emails just keep on coming. I recently started to use a brand new email address — that I would like to keep clean and hence have been looking for a way to control who I give this email address to.

A hacker news post [1] got me onto email masking. Google led me to the AWS blog post (Forward Incoming Email to an External Destination [2]) and this implementation.

AWS Infrastructure Diagram

The diagram below shows the AWS infrastructure used in the implementation of the email masking service — namely, receiving and storing the email, checking the address and forwarding it if appropriate.

AWS Infrastructure diagram

The AWS infrastructure consists of:

  • A route53 record to direct incoming mail to the SES service;
  • SES rules that handle incoming emails to anything@mydomain.com, save them to S3 and invoke a lambda function;
  • A lambda function that processes the incoming email, and forwards it to my normal email myemail@anotherdomain.com if the incoming email address anything@mydomain.com is not blacklisted.

I personally use terraform to manage the AWS infrastructure, but one can create it via the console easily — it is the same infrastructure as described in [2] — or using any other Infrastructure-as-Code tool.

The main elements of the terraform script read as follows:

  • SES rule: The following script sets a SES rule that handles any incoming email with an address @mydomain.com. The rule has 2 actions. The first action save the email to an S3 bucket aws_s3_bucket.x.id — this bucket is created using an aws_s3_bucket resource and, importantly, a policy is applied to that bucket that allows write, aka PutObject action, from SES. The second action triggers the lambda function aws_lambda_function.x.arn. The rule is assigned to the rule set aws_ses_receipt_rule_set.mail.id that is defined using an aws_ses_receipt_rule_set resource.
  • Lambda function: The lambda function is defined with a provided runtime — ie the function is a self contained executable with no dependencies as described in [3]. The maximum memory usage is set 128MB and a runtime of 5s that has proved sufficient for my use. The function is initialized with an empty zip file as I deploy the executable using the AWS command line interface.

To allow the SES rule and lambda to work, permissions need to be setup:

  • A resource is defined that allows the SES service to call the lambda function — I attempted to nominate the SES caller using the source arn entries, but was unable to this approach led to a circular reference.
  • Define the role used by the lambda function with a policy that allows it to read from the S3 bucket and send emails usingthe AWS SES service. The role also allows the lambda function to write logs — for CloudWatch.

Lambda function

I chose to write the lambda function in Rust using the Rust Runtime for AWS [3] and the AWS SDK for Rust [4] — currently in developer preview. The implementation is fairly similar to the python implementation in [2] to the exception that I wanted to (a) check the email address against a blacklist and (b) forward the email content rather than a link to access the email from the S3 bucket.

After installing rust, a new binary application is created using cargo new ses_handler. The following dependencies are added to the Cargo.toml:

  • lambda_runtime — The glue required to write an AWS lambda function in Rust;
  • aws-types, aws-config, aws-sdk-ses and aws-sdk-s3 — Components of the Rust SDK to access to SES and S3 services;
  • mailparse to parse the email received by SES and saved to S3. There are a number of libraries available — I started using this one and as it works satisfactorily I did not investigate any other one.

The implementation is structured around the following 3 Rust structs:

  • The SesEvent struct maps the SES event into an object that identifies the email ID, sender, subject and list of destinations. The struct and its construction method is shown below. The construction method is intimately coupled with the SES incoming email event format [5] that is provided as the from_json method argument payload.
  • The Address struct stores the fields used by the lambda function when forwarding the email — the from field is used to store the email address to which the email was originally sent to, i.e anything@mydomain.com. This is used as the originator of the forwarded email. The key field stores the email address key, i.e anything.
  • Instances of the Address struct are constructed from the email address anything@mydomain.com. The key is used to decide whether the email is forwarded or not as shown below. For key that are not forwarded, the method returns a None option.
  • The above method is used in the following method that translates the SES event destinations into a list of addresses:
  • The final struct Mail does the heavy lifting of reading the email from the S3 bucket, parsing it, creating the email to be forwarded — only the text and html portions of the message are forwarded, no attachments— and sending it. The Mail struct is defined from the message_id — the identifier of the email given by the SES event. The other fields, config, client, and message, allow for caching of information in the event that a message is to be sent multiple times — an edge case if the email was part of a mailing list with different @mydomain.com addresses.
  • The email parsing is handled by the following function that extracts the subject, plain text, and HTML components of the email:
  • The implementation of the send functionality of the Mail struct is by far the heaviest part of the implementation and is shown below. The implementation is reasonably clear and straightforward: (a) initialise the AWS config, (b) read the incoming message from the S3 bucket, (c) create the SES message from the plain text and HTML content of the incoming message, (d) initialize the SES client and (e) send the message on…

Using the above structs, the main function takes on the following form where the incoming event is processed into an SesEvent object.

The SesEvent message id is used to create a Mail object and the SesEvent list of destinations is converted to Address that are used to call the send method of the Mail object.

Conclusion

I have been using my anything@mydomain.com proof-of-concept for a few weeks with quite a bit of success. But as the blacklist is hardcoded, I am not updating it very often. Whilst there is room for improvement, the implementation of the proof-of-concept went quite smoothly despite my lack of familiarity with the SES service.

The AWS Rust SDK works effectively with the AWS SES and S3 services — despite being in developer preview. I am currently using it with Amazon DynamoDB which is proving a little less stable currently.

[1] https://news.ycombinator.com/item?id=31116861
[2] https://aws.amazon.com/blogs/messaging-and-targeting/forward-incoming-email-to-an-external-destination/
[3] https://aws.amazon.com/blogs/opensource/rust-runtime-for-aws-lambda/
[4] https://aws.amazon.com/sdk-for-rust/
[5] https://docs.aws.amazon.com/ses/latest/dg/receiving-email-action-lambda-event.html

--

--