[Python] Python Lambda to send files uploaded to s3 as email attachments

Discussão em 'Python' iniciado por Stack, Abril 18, 2025.

  1. Stack

    Stack Membro Participativo

    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 upload Attach the files to an email Send 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 MIMEText
    from email.mime.multipart import MIMEMultipart
    from email.mime.application import MIMEApplication
    import boto3

    def 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.path
    import boto3
    import email
    from botocore.exceptions import ClientError
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText
    from email.mime.application import MIMEApplication


    s3 = 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?

    Continue reading...

Compartilhe esta Página