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 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