Skip to content

AWS IS CHEAP - Part2. Sending and Receiving emails

If you got a custom domain name, you probably want to send and receive emails from there. There are several open-source or AWS out-of-the-box solutions available out there, but most the time they are quite pricy for only the few mails we would like to exchange. Is there a cheap and simple solution to manage that? Let's discover it in this post.

This is the second part of a series demonstrating how, with the right setup, carefulness, and smart usage, AWS can be incredibly affordable for hosting websites or services. If you want first to host you own static website with custom domain name for less than 2,20$ a month. Please visit AWS Host Static Website

Availabe options

If you want to send and receive emails with your domain name, below are some potential options:

Roundcube: Open source webmail software. However, you will have to host the Roudcube mail server somewhere (most probably on an AWS EC2) and it will costs you.

Amazon Workmail: Fully AWS managed email service. No need to host or setup anything youself. Amazon WorkMail costs $4.00 per user per month and includes 50 GB of mailbox storage for each user. If you are willing to spend $4.00 per month to receive and send few emails without any setup effort, this is your go-to solution.
More info here -> https://aws.amazon.com/workmail/pricing/

Amazon Simple Email Service (SES) AWS email service. (Usually used to send transactional email or mass-marketing messages). Free tier customers receive up to 3,000 message charges free each month for the first 12 months. Then you need to pay depending on your usage.

In combination with Route53, S3, SNS and Lambda we could use that service to send and receive emails for cheap.

More info about pricing here -> https://aws.amazon.com/fr/ses/pricing/

AWS Services Used

AWS Service Description Usage for this project Estimated Price For This Project
Route53 DNS Service Create MX records ~ 0$ / month
SES Amazon email service Send and receive emails ~ 0$ / month if < 3k msgs pr month
S3 Cloud Object Storage Store Emails (< 100mo) ~ 0$ / month
lambda Cloud Object Storage Extract email from S3 and send to SNS ~ 0$ / month
SNS Notification Service Send notification ~ 0$ / month

Total cost of the project: Around 0$

Note

You will also need an email client (Gmail, thunderbird, etc...) to send emails

Pre-requisites

  • An active AWS account
  • Installed and configured Terraform with AWS credentials
  • A Custom Domain Name hosted in AWS Route53
  • An existing email address you own

Manual Steps

Overall goal

  • Email receiving: forward the email that arrives on your custom domain to another email that you already own. The path will be: Route53 (MX Record) -> AWS SES -> S3 -> Lambda -> SNS -> Your existing mailbox
  • Email sending: connect an existing email client (Gmail, thunderbird, etc...) with AWS SES via SMTP credentials (generated by AWS SES)

The general guidance for te manuals steps are the following:

Create a MX record for your Domain

In Route53, click on your hosted zone, then 'Create Record'. Choose MX record time and enter '10 inbound-smtp.eu-west-1.amazonaws.com' as a value. Replace eu-west-1 with your own region. Official doc here -> https://docs.aws.amazon.com/ses/latest/dg/receiving-email-mx-record.html

Create a S3 Bucket to store your emails

It will store all your future incoming emails. Create an S3 bucket policy that authorize SES to store message there. In your newly created S3 bucket, under Permissions -> Bucket Policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSESPuts",
            "Effect": "Allow",
            "Principal": {
                "Service": "ses.amazonaws.com"
            },
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::your-email-bucket-name/*",
            "Condition": {
                "StringEquals": {
                    "aws:Referer": "your-aws-account-id"
                }
            }
        }
    ]
}

Create a SNS topic that will forward incoming domain email to another email you own

Create a SNS topic that will forward your email to your desired email address. (ex: your perso gmail address)Then subscribe to that SNS topic by clicking on "Create subscription", entering the email protocol and entering an email of yours where your incoming email will arrive.

Note

Yes, we will receive the incoming email on our custom domain name on another mailbox we already own.

Create a lambda that will read the S3 and send it to sns

Create a lambda that will read the S3 and send it to sns. The lambda will read the receiving message in S3, extract the information and send it to SNS.

  • Create a lambda with Python runtime. In the configuration tab of the lambda:
  • General configuration: change the timeout to 2 minutes
  • Permissions: click on the Role Name link of the lambda and add 2 inlines permissions:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ReadMessageS3Bucket",
            "Effect": "Allow",
            "Action": [
                "s3:Get*",
                "s3:List*"
            ],
            "Resource": [
                "arn:aws:s3:::your-email-bucket",
                "arn:aws:s3:::your-email-bucket/*"
            ]
        }
    ]
}

and

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "SendMessageToSns",
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "arn:aws:sns:eu-west-1:657242872425:your-sns-topic-name"
        }
    ]
}
  • Add the following code to the lambda:
import boto3
import email
from email import policy
from email.parser import BytesParser


s3 = boto3.client("s3")
sns = boto3.client("sns")

SNS_TOPIC_ARN = "arn:aws:sns:eu-west-1:657242872425:your-sns-topic-name"


def forward_email_to_sns(clean_email):
    response = sns.publish(
        TopicArn=SNS_TOPIC_ARN,
        Message=clean_email,
        Subject="New Message received in S3",
    )
    print(response)


def extract_email_info(email_content: str) -> str:

    # Parse the email content
    msg = email.message_from_string(email_content, policy=default)

    # Extract the sender, date, recipient, subject, and body
    sender = msg.get("From")
    date = msg.get("Date")
    recipient = msg.get("To")
    subject = msg.get("Subject")

    # Initialize variables for plain text and HTML bodies
    plain_body = ""
    html_body = ""

    # Iterate through the email parts
    for part in msg.walk():
        content_type = part.get_content_type()
        payload = part.get_payload(decode=True)

        if content_type == "text/plain":
            plain_body = payload.decode("utf-8")
        elif content_type == "text/html":
            html_body = payload.decode("utf-8")

    # Construct the cleaned email
    cleaned_email = (
        f"From: {sender}\nDate: {date}\nTo: {recipient}\nSubject: {subject}\n"
    )
    cleaned_email += f"Plain Body: {plain_body}\nHTML Body: {html_body}"

    return cleaned_email


def extract_s3_email(record: dict) -> str:

    bucket_name = record["s3"]["bucket"]["name"]
    filename = record["s3"]["object"]["key"]

    print(f"New message {filename} received in S3 bucket {bucket_name}.")

    data = s3.get_object(Bucket=bucket_name, Key=filename)
    email_raw_content = data["Body"].read().decode("utf-8")

    return extract_email_info(email_raw_content)


def lambda_handler(event, context):
    # Each record is an object (message) that arrived in the S3
    for record in event["Records"]:
        clean_email = extract_s3_email(record)
        forward_email_to_sns(clean_email)

Create an S3 event notification

Create an S3 event notification. When a message will arrive here, it will trigger the lambda. In the S3 bucket, tab Event Notification, click on Create Event Notification. Choose "Put" or "Create" for Event Type, and then choose your lambda function as a trigger.

Create an Identity in AWS SES

In AWS console -> Amazon SES (select your region) -> Verified Identities and enter your domain name here.
After clicking on 'Create Identity', DKIM records will be created under your domain in Route53 and verification will happen. Be patient.

Create a Rule in AWS SES that redirect incoming email to S3. Create email S3 Bucket and change bucket policy

In Amazon SES (select your region) -> Email receiving. Create a new rule that will deliver the message to the S3 you created. Set the ruleset as active.

Note

Testing time! You can now send an email to anything@your-custom-domain-name.com and it should arrive in the mailbox you used in the SNS subscribption.

Generate SMTP credentials

Now that we are able to receive emails from ourcustom domain name. We need to be able to respond. In order to do that, we will create SMTP credentials in SES, then add them in our favorite mail client. Steps:

  • In AWS SES, click on 'Create SMTP Credentials'. Fill in the information and save your SMTP username and password.
  • In AWS SES, in 'Verified identities', create an identity and enter the email you would like to create (ex: info@your-custom-domain-name.com)
  • You will then receive a validation email to this address. Click on the link to validate it.
  • In your favorite mail client. (Gmail for me). Go to Settings > Import Account > Send emails as and add your email + SMTP credentials there. You should be able to send emails from that custom domain name now.

Note

Use email-smtp.eu-west-1.amazonaws.com as the SMTP server (change eu-west-1 with your region) Use SMTP Username (AKIAxxx...) and SMTP password (BHxxxxxxxx...) provided whem you create your SMTP credentials.

Use Terraform to deploy everything automatically

The manuals steps may take some time to execute.
Instead you can deploy everything with terraform with a few command lines thanks to this project: Github - AWS Cheap Mailbox

git clone git@github.com:Le-Blanchardeu/aws-is-cheap_mailbox.git
cd aws-is-cheap_mailbox

terraform init
terraform apply -var="domain_name=your-domain-name.com" -var="aws_region=eu-west-1" -var="forward_to_perso_email=homer.simpson@gmail.com"

Other tools to consider using

Some tools found on github. Worth trying it.