We have an online form that gives people the option to upload multiple files. The form is built by a third party, so I don't have any involvement with them. When someone uploads files using the form it dumps the files into a new folder within an s3 bucket. I want to be able to do the following:
Get the files triggered by form filler's uploadAttach the files to an emailSend the email to specific people.
I have done quite a lot of research, but I'm still new to coding and am trying to use Python, which I have decided to focus on as my first proper language.
The code I have got so far is borrowed from other examples I have seen and adjusted. But so far it has only sent me emails with the files when I have uploaded single files into the root of the bucket. Not multiple files into a folder in the bucket. This appears to be because the /
in the path of the files in folders breaks the sendrawemail
functionality.
This lambda is set up to be triggered by a PUT
notification on the creation of new files on the s3 bucket.UPDATE: I have now used different code and have got a solution. Please see the bottom for this.
from email.mime.text import MIMETextfrom email.mime.multipart import MIMEMultipartfrom email.mime.application import MIMEApplicationimport boto3def lambda_handler(event, context): file_obj = event["Records"][0] bucket_name = str(file_obj['s3']['bucket']['name']) key = str(file_obj['s3']['object']['key']) msg = MIMEMultipart() new_body = "test body" text_part = MIMEText(new_body, _subtype="html") msg.attach(text_part) filename = str(event["Records"][0]['s3']['object']['key']) msg["To"] = "test@test.com" msg["From"] = "test@test.com" s3_object = boto3.client('s3') s3_object = s3_object.get_object(Bucket=str(bucket_name), Key=str(key)) body = s3_object['Body'].read() part = MIMEApplication(body, filename) part.add_header("Content-Disposition", 'attachment', filename=filename) msg.attach(part) ses_aws_client = boto3.client('ses', 'eu-west-1') ses_aws_client.send_raw_email(RawMessage={"Data" : msg.as_bytes()})
What I hoped was that when a folder, or multiple files were uploaded, I would be sent an email containing all of the files as attachments (I realise there's a 10mb limit to messages). However, when there are multiple files it appears to be sending multiple emails with one file per email. And if the file is in a folder, so has a key value with a slash in it, the send_raw_email shows the following error:
[ERROR] ClientError: An error occurred (InvalidParameterValue) when calling the SendRawEmail operation: Expected ';', got "/"
I presume I need to encode the path in some way? Is there any way of collating all the files newly uploaded into one email?
Any help would be massively welcome.
Edit 1: As per Jeril's response, I'm sharing the full log entry from the failure. Also I missed a line in my original code block so I will update that, too.
12:14:59[ERROR] ClientError: An error occurred (InvalidParameterValue) when calling the SendRawEmail operation: Expected ';', got "/" Traceback (most recent call last): File "/var/task/lambda_function.py", line 26, in lambda_handler ses_aws_client.send_raw_email(RawMessage={"Data" : msg.as_bytes()}) File "/var/runtime/botocore/client.py", line 320, in _api_call return self._make_api_call(opera[ERROR] ClientError: An error occurred (InvalidParameterValue) when calling the SendRawEmail operation: Expected ';', got "/"Traceback (most recent call last): File "/var/task/lambda_function.py", line 26, in lambda_handler ses_aws_client.send_raw_email(RawMessage={"Data" : msg.as_bytes()}) File "/var/runtime/botocore/client.py", line 320, in _api_call return self._make_api_call(operation_name, kwargs) File "/var/runtime/botocore/client.py", line 623, in _make_api_call raise error_class(parsed_response, operation_name)
Update:I have now managed to get the essential functionality of this working:
import os.pathimport boto3import emailfrom botocore.exceptions import ClientErrorfrom email.mime.multipart import MIMEMultipartfrom email.mime.text import MIMETextfrom email.mime.application import MIMEApplications3 = boto3.client("s3")def lambda_handler(event, context): # Replace sender@example.com with your "From" address. # This address must be verified with Amazon SES. SENDER = "Test Test <test@test.com>" # Replace recipient@example.com with a "To" address. If your account # is still in the sandbox, this address must be verified. RECIPIENT = "Test Test <test@test.com>" # Specify a configuration set. If you do not want to use a configuration # set, comment the following variable, and the # ConfigurationSetName=CONFIGURATION_SET argument below. # CONFIGURATION_SET = "ConfigSet" AWS_REGION = "eu-west-1" SUBJECT = "Test Send Mesage with Attachment" # This is the start of the process to pull the files we need from the S3 bucket into the email. # Get the records for the triggered event FILEOBJ = event["Records"][0] # Extract the bucket name from the records for the triggered event BUCKET_NAME = str(FILEOBJ['s3']['bucket']['name']) # Extract the object key (basicaly the file name/path - note that in S3 there are # no folders, the path is part of the name) from the records for the triggered event KEY = str(FILEOBJ['s3']['object']['key']) # extract just the last portion of the file name from the file. This is what the file # would have been called prior to being uploaded to the S3 bucket FILE_NAME = os.path.basename(KEY) # Using the file name, create a new file location for the lambda. This has to # be in the tmp dir because that's the only place lambdas let you store up to # 500mb of stuff, hence the '/tmp/'+ prefix TMP_FILE_NAME = '/tmp/'+FILE_NAME # Download the file/s from the event (extracted above) to the tmp location s3.download_file(BUCKET_NAME, KEY, TMP_FILE_NAME) # Make explicit that the attachment will have the tmp file path/name. You could just # use the TMP_FILE_NAME in the statments below if you'd like. ATTACHMENT = TMP_FILE_NAME # The email body for recipients with non-HTML email clients. BODY_TEXT = "Hello,\r\nPlease see the attached file related to recent submission." # The HTML body of the email. BODY_HTML = """\<html><head></head><body><h1>Hello!</h1><p>Please see the attached file related to recent submission.</p></body></html>""" # The character encoding for the email. CHARSET = "utf-8" # Create a new SES resource and specify a region. client = boto3.client('ses',region_name=AWS_REGION) # Create a multipart/mixed parent container. msg = MIMEMultipart('mixed') # Add subject, from and to lines. msg['Subject'] = SUBJECT msg['From'] = SENDER msg['To'] = RECIPIENT # Create a multipart/alternative child container. msg_body = MIMEMultipart('alternative') # Encode the text and HTML content and set the character encoding. This step is # necessary if you're sending a message with characters outside the ASCII range. textpart = MIMEText(BODY_TEXT.encode(CHARSET), 'plain', CHARSET) htmlpart = MIMEText(BODY_HTML.encode(CHARSET), 'html', CHARSET) # Add the text and HTML parts to the child container. msg_body.attach(textpart) msg_body.attach(htmlpart) # Define the attachment part and encode it using MIMEApplication. att = MIMEApplication(open(ATTACHMENT, 'rb').read()) # Add a header to tell the email client to treat this part as an attachment, # and to give the attachment a name. att.add_header('Content-Disposition','attachment',filename=os.path.basename(ATTACHMENT)) # Attach the multipart/alternative child container to the multipart/mixed # parent container. msg.attach(msg_body) # Add the attachment to the parent container. msg.attach(att) print(msg) try: #Provide the contents of the email. response = client.send_raw_email( Source=SENDER, Destinations=[ RECIPIENT ], RawMessage={'Data':msg.as_string(), }, # ConfigurationSetName=CONFIGURATION_SET ) # Display an error if something goes wrong. except ClientError as e: print(e.response['Error']['Message']) else: print("Email sent! Message ID:"), print(response['MessageId'])
The only issue now is that if I upload multiple files, I get sent an email per file. Is there any way of collating them all into one email?