From 74f203448771cfbda77afce2279bd9f9b704a195 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 29 Feb 2024 22:17:48 -0800 Subject: [PATCH] Email business logic --- .../runbooks/update_python_dependencies.md | 6 +- .../generate_current_metadata_report.py | 20 ++-- src/registrar/utility/email.py | 110 ++++++++---------- 3 files changed, 64 insertions(+), 72 deletions(-) diff --git a/docs/operations/runbooks/update_python_dependencies.md b/docs/operations/runbooks/update_python_dependencies.md index 04fb936c6..468270d09 100644 --- a/docs/operations/runbooks/update_python_dependencies.md +++ b/docs/operations/runbooks/update_python_dependencies.md @@ -3,7 +3,7 @@ 1. Check the [Pipfile](../../../src/Pipfile) for pinned dependencies and manually adjust the version numbers 2. Run `docker-compose stop` to spin down the current containers and images so we can start afresh -2. Run +3. Run cd src docker-compose run app bash -c "pipenv lock && pipenv requirements > requirements.txt" @@ -13,9 +13,9 @@ It is necessary to use `bash -c` because `run pipenv requirements` will not recognize that it is running non-interactively and will include garbage formatting characters. The requirements.txt is used by Cloud.gov. It is needed to work around a bug in the CloudFoundry buildpack version of Pipenv that breaks on installing from a git repository. -3. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt. +4. Change geventconnpool back to what it was originally within the Pipfile.lock and requirements.txt. This is done by either saving what it was originally or opening a PR and using that as a reference to undo changes to any mention of geventconnpool. Geventconnpool, when set as a requirement without the reference portion, is defaulting to get a commit from 2014 which then breaks the code, as we want the newest version from them. -4. Run `docker-compose build` to build a new image for local development with the updated dependencies. +5. Run `docker-compose build` to build a new image for local development with the updated dependencies. The reason for de-coupling the `build` and `lock` steps is to increase consistency between builds--a run of `build` will always get exactly the dependencies listed in `Pipfile.lock`, nothing more, nothing less. \ No newline at end of file diff --git a/src/registrar/management/commands/generate_current_metadata_report.py b/src/registrar/management/commands/generate_current_metadata_report.py index a27199cdb..1a33c2791 100644 --- a/src/registrar/management/commands/generate_current_metadata_report.py +++ b/src/registrar/management/commands/generate_current_metadata_report.py @@ -71,17 +71,23 @@ class Command(BaseCommand): # Secret is encrypted into getgov-credentials # TODO: Update secret in getgov-credentials via cloud.gov and my own .env when ready - # encrypted_metadata is the encrypted output + # Encrypt the metadata + # TODO: UPDATE SECRET_ENCRYPT_METADATA pw getgov-credentials on stable encrypted_metadata = self._encrypt_metadata(s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)) print("encrypted_metadata is:", encrypted_metadata) - + print("the type is: ", type(encrypted_metadata)) # Send the metadata file that is zipped - # Q: Would we set the vars I set in email.py here to pass in to the helper function or best way to invoke - # send_templated_email(encrypted_metadata, attachment=True) - + # TODO: Make new .txt files + send_templated_email( + "emails/metadata_body.txt", + "emails/metadata_subject.txt", + to_address="rebecca.hsieh@truss.works", # TODO: Update to settings.DEFAULT_FROM_EMAIL once tested + file=encrypted_metadata, + ) + def _encrypt_metadata(self, input_file, output_file, password): - # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities - # Could also use compression=pyzipper.ZIP_LZMA? + # Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster + # We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size with pyzipper.AESZipFile(output_file, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as f_out: f_out.setpassword(password) f_out.writestr('encrypted_metadata.txt', input_file) diff --git a/src/registrar/utility/email.py b/src/registrar/utility/email.py index 199a6c304..5f3e42eb5 100644 --- a/src/registrar/utility/email.py +++ b/src/registrar/utility/email.py @@ -4,6 +4,10 @@ import boto3 import logging from django.conf import settings from django.template.loader import get_template +from email.mime.base import MIMEBase +from email.mime.application import MIMEApplication +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText logger = logging.getLogger(__name__) @@ -15,7 +19,7 @@ class EmailSendingError(RuntimeError): pass -def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}): +def send_templated_email(template_name: str, subject_template_name: str, to_address: str, context={}, file: str=None): """Send an email built from a template to one email address. template_name and subject_template_name are relative to the same template @@ -40,74 +44,56 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr except Exception as exc: raise EmailSendingError("Could not access the SES client.") from exc - # Are we okay with passing in "attachment" var in as boolean parameter - # If so, TODO: add attachment boolean to other functions try: - #if not attachment: - ses_client.send_email( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [to_address]}, - Content={ - "Simple": { - "Subject": {"Data": subject}, - "Body": {"Text": {"Data": email_body}}, + if file is None: + ses_client.send_email( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [to_address]}, + Content={ + "Simple": { + "Subject": {"Data": subject}, + "Body": {"Text": {"Data": email_body}}, + }, }, - }, - ) - # else: # has attachment - # same as above but figure out how to attach a file - # via boto3 "boto3 SES file attachment" - # we also want this to only send to the help email - - # from email.mime.multipart import MIMEMultipart - # from email.mime.text import MIMEText - # from email.mime.application import MIMEApplication + ) + if file is not None: + # TODO: Update sender email when we figure out + ses_client = boto3.client( + "ses", + region_name=settings.AWS_REGION, + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + config=settings.BOTO_CONFIG, + ) - # sender_email = 'sender@example.com' - # recipient_email = 'help@get.gov' - # subject = 'DOTGOV-Full Domain Metadata' - # body = 'Domain metadata email, should have an attachment included change here later.' - # attachment_path = 'path/to/attachment/file.pdf' - # aws_region = 'sesv2' - - # response = send_email_with_attachment(sender_email, recipient_email, subject, body, attachment_path, aws_region) - # print(response) + #TODO: Update sender to settings.DEFAULT_FROM_EMAIL + response = send_email_with_attachment(settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, file, ses_client) + print("Response from send_email_with_attachment_is:", response) except Exception as exc: raise EmailSendingError("Could not send SES email.") from exc +def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client): + # Create a multipart/mixed parent container + msg = MIMEMultipart('mixed') + msg['Subject'] = subject + msg['From'] = sender + msg['To'] = recipient -# def send_email_with_attachment(sender, recipient, subject, body, attachment_path, aws_region): - # # Create a multipart/mixed parent container - # msg = MIMEMultipart('mixed') - # msg['Subject'] = subject - # msg['From'] = sender_email - # msg['To'] = recipient_email + # Add the text part + text_part = MIMEText(body, 'plain') + msg.attach(text_part) - # # Add the text part - # text_part = MIMEText(body, 'plain') - # msg.attach(text_part) + # Add the attachment part - # # Add the attachment part - # with open(attachment_path, 'rb') as attachment_file: - # attachment_data = attachment_file.read() - # attachment_part = MIMEApplication(attachment_data) - # attachment_part.add_header('Content-Disposition', f'attachment; filename="{attachment_path}"') - # msg.attach(attachment_part) + # set it into this "type" + attachment_part = MIMEApplication(attachment_file) + # Adding attachment header + filename that the attachment will be called + attachment_part.add_header('Content-Disposition', f'attachment; filename="encrypted_metadata.zip"') + msg.attach(attachment_part) - # # Send the email - # response = ses_client.send_raw_email( - # Source=sender, - # Destinations=[recipient], - # RawMessage={'Data': msg.as_string()} - # ) - - # ses_client.send_email( - # FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - # Destination={"ToAddresses": [to_address]}, - # Content={ - # "Simple": { - # "Subject": {"Data": subject}, - # "Body": {"Text": {"Data": email_body}}, - # }, - # }, - # ) \ No newline at end of file + response = ses_client.send_raw_email( + Source=sender, + Destinations=[recipient], + RawMessage={"Data": msg.as_string()} + ) + return response