Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save dkavraal/356dc60f8f6beb8b5070e891adadab96 to your computer and use it in GitHub Desktop.

Select an option

Save dkavraal/356dc60f8f6beb8b5070e891adadab96 to your computer and use it in GitHub Desktop.
AWS Lambda Receive Email Forward Function
from __future__ import print_function
import boto3
import json
import os
import re
from email.parser import Parser
from datetime import datetime
import logging
## Thanks to:
# https://gist.github.com/stenius/c6983f990bbbb1e49e4f
# https://bravokeyl.com/how-to-set-up-email-forwarding-with-amazon-ses/#Create-a-Lambda-function-to-forward-recieved-email
# https://github.com/arithmetric/aws-lambda-ses-forwarder
###
# Dincer Kavraal -- dincer(AT)mctdata.com
###
Log = logging.getLogger()
Log.setLevel(logging.DEBUG)
FORWARD_ADDRESSES = ["to-me@example.com",
"to-him-1@example.com",
"to-her-2@example.com"]
FROM_ADDRESS = "Sender <example@example.com>"
EMAIL_DOMAIN = "example.com" # my aws receiver email domain
SPAMMER_DOMAINS = map(re.compile, ["example.com"])
SPAMMER_EMAILS = map(re.compile, ["spammer-jack@example.com"])
RE_DOMAIN = re.compile("\@(.*)$")
def decode_email(msg_str):
p = Parser()
message = p.parsestr(msg_str)
decoded_message = ''
for part in message.walk():
charset = part.get_content_charset()
if part.get_content_type() == 'text/plain':
part_str = part.get_payload(decode=1)
decoded_message += part_str.decode(charset)
return decoded_message
def print_with_timestamp(*args):
print(datetime.utcnow().isoformat(), *args)
def lambda_handler(event, context):
print_with_timestamp('Starting - inbound-sns-spam-filter')
Log.debug(json.dumps(event, indent=4))
ses_notification = event['Records'][0]['Sns']
message_id = ses_notification['MessageId']
message = json.loads(ses_notification["Message"])
receipt = message['receipt']
sender = message['mail']['source']
subject = message['mail']['commonHeaders']['subject']
sender_domain = (RE_DOMAIN.findall(sender) or [""])[0]
print_with_timestamp('Processing message:', message_id)
# Check if any spam check failed
if (receipt['spfVerdict']['status'] == 'FAIL' or
receipt['dkimVerdict']['status'] == 'FAIL' or
receipt['spamVerdict']['status'] == 'FAIL' or
receipt['virusVerdict']['status'] == 'FAIL' or
all(map(lambda x: x.search(sender_domain), SPAMMER_DOMAINS)) or
all(map(lambda x: x.search(sender), SPAMMER_EMAILS))):
send_bounce_params = {
'OriginalMessageId': message_id,
'BounceSender': 'mailer-daemon@{}'.format(EMAIL_DOMAIN),
'MessageDsn': {
'ReportingMta': 'dns; {}'.format(EMAIL_DOMAIN),
'ArrivalDate': datetime.now().isoformat()
},
'BouncedRecipientInfoList': []
}
for recipient in receipt['recipients']:
send_bounce_params['BouncedRecipientInfoList'].append({
'Recipient': recipient,
'BounceType': 'ContentRejected'
})
print_with_timestamp('Bouncing message with parameters:')
print_with_timestamp(json.dumps(send_bounce_params))
try:
ses_client = boto3.client('ses')
bounceResponse = ses_client.send_bounce(**send_bounce_params)
print_with_timestamp('Bounce for message ', message_id, ' sent, bounce message ID: ', bounceResponse['MessageId'])
return {'disposition': 'stop_rule_set'}
except Exception as e:
print_with_timestamp(e)
print_with_timestamp('An error occurred while sending bounce for message: ', message_id)
raise e
else:
print_with_timestamp('Accepting message:', message_id)
# now distribute to list:
action = receipt['action']
if (action['type'] != "S3"):
Log.exception("Mail body is not saved to S3. Or I have done sth wrong.")
return None
try:
ses_client = boto3.client('ses')
s3_client = boto3.resource('s3')
mail_obj = s3_client.Object(action['bucketName'], action['objectKey'])
body = decode_email(mail_obj.get()["Body"].read())
try:
response = ses_client.send_email(
Source=FROM_ADDRESS,
Destination={
'ToAddresses': FORWARD_ADDRESSES,
},
Message={
'Subject': {
'Data': subject,
},
'Body': {
'Text': {
'Data': body,
},
'Html': {
'Data': body.replace("\n", "<br />"),
}
}
},
)
except Exception as e1:
print_with_timestamp(e1)
print_with_timestamp('An error occurred while sending bounce for message: ', message_id)
raise e1
except Exception as e2:
print_with_timestamp(e2)
print_with_timestamp('An error occurred while sending bounce for message: ', message_id)
raise e2
return None

Set up an IAM Role called (say) SNSEmailForwarder:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "sns:GetTopicAttributes",
                "sns:List*",
                "sns:Publish"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ses:SendRawEmail",
                "ses:SendEmail",
                "ses:SendBounce"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:PutObject"
            ],
            "Resource": "arn:aws:s3:::YOUR_S3_BUCKET/*"
        }
    ]
}

Verify your email domain on AWS SES (you can't use sandboxed mail domain, so says AWS) :

  1. http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-domains.html

Setup an SNS topic:

  1. Go to https://console.aws.amazon.com/sns
  2. Create topic
  3. Topic Name: SNSForwardEmails

Setup a Lambda function: 0) Go to https://console.aws.amazon.com/lambda

  1. Create new lambda function
  2. Run time: Python 2.7
  3. Select blueprint > Blank Function
  4. Configure Triggers > Select "SNS" in the gray empty box on left of lambda logo-sign
  5. SNS topic: select the topic you have created above. (SNSForwardEmails)
  6. Enable Trigger: check the box
  7. Create the function
  8. Name: LambdaForwardEmails Runtime: Python 2.7 Code entry type: Edit code inline in the text area, copy-paste the whole file I shared here. Role: Choose an existing role Existing Role: the one you have created above. (SNSEmailForwarder)
  9. Next > Create Function

Setup SES:

  1. Go to https://console.aws.amazon.com/ses
  2. Rule Sets (on the left menu)
  3. Create a Receipt Rule
  4. Rule Set Name: EmailForwardingRules
  5. OPTIONAL: enter your domain name without at sign such as: example.com (Add Receipt)
  6. Next Step
  7. Add Action: S3
  8. S3 Bucket: (create sth) Emails Encrypt Message: (uncheck, I am not sure about the consequences of custom encryption) SNS Topic: SNSEmailForwarder (this is important)
  9. Create Rule

It seems ok. Test with a real email. (In the Lambda editor) The test scenario AWS provides cannot simulate an email message totally.

@pwnptl

pwnptl commented Dec 3, 2019

Copy link
Copy Markdown

Hi I am stuck with an issue in sending a bounce. Just want to ask if my understanding is correct!
You have extracted SNS messageId here and used it in bounce parameters here.

But as I was reading here that message Id of bounce message is required. Which is provided by SES and obtained by using ses_notification['Message']['mail']['commonHeaders']['messageId']

Btw I am getting below error by using either of the messageId:
Client ErrorFailed to generate a bounce for <3d8f617b-bded-559c-b4bb-371bb2ffdbd5>: <3d8f617b-bded-559c-b4bb-371bb2ffdbd5> does not appear to be a valid original message ID.

@UnquietCode

Copy link
Copy Markdown

@pwnptl It is definitely the case that with the SNS notification version you need to get the id from a different field.

data = event['Records'][0]['Sns']
data = json.loads(data['Message'])
message_id = data['mail']['messageId']

@pwnptl

pwnptl commented Dec 3, 2019

Copy link
Copy Markdown

@UnquietCode It could be the issue but data['mail']['messageId'] and ['mail']['commonHeaders']['messageId'] will always have same value.

@JustinMarotta13

Copy link
Copy Markdown

Hey, this works for me. Thank you so much! However, is there a way to change FROM_ADDRESS to whoever originally sent the email? I tried setting 'FROM_ADDRESS' to 'sender' and that was a fail.

@pwnptl

pwnptl commented Jul 5, 2020

Copy link
Copy Markdown

@JustinMarotta13 Are you sending a Bounce or new email ?
Have you followed https://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-email-addresses.html
Your BounceSender/Source parameter also referred here as 'sender@example.com' must be verified by Amazon SES first.

@JustinMarotta13

JustinMarotta13 commented Jul 5, 2020 via email

Copy link
Copy Markdown

@pwnptl

pwnptl commented Jul 5, 2020

Copy link
Copy Markdown

Where and how are you storing the eml file? what is the content you are getting there ?
Are you trying to send a new email with someone else's behalf? send_email() api sends a new email. You cannot send a email using from_address which is not verified in your aws account.

@JustinMarotta13

JustinMarotta13 commented Jul 5, 2020 via email

Copy link
Copy Markdown

@JustinMarotta13

JustinMarotta13 commented Jul 5, 2020 via email

Copy link
Copy Markdown

@pwnptl

pwnptl commented Jul 5, 2020

Copy link
Copy Markdown

Not sure about SES or Lambda but SNS can forward emails to different email address.

@JustinMarotta13

JustinMarotta13 commented Jul 5, 2020 via email

Copy link
Copy Markdown

@pwnptl

pwnptl commented Jul 5, 2020

Copy link
Copy Markdown

currently you must be doing SES -> lambda.
you can do SES-> SNS ->lambda. This way you can forward emails in SNS level and do other stuff in lambda. I cannot find a way to forward using lambda and original sender.

One reason it should not exist in lambda level is that lambda can hamper the original email content/fields and sending such content using original sender email address is a security risk and impersonation. Correct me if my hypothesis wrong here.

@JustinMarotta13

Copy link
Copy Markdown

Makes total sense. Thanks so much for your help!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment