-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: email treasury report zip file (#399)
* chore: email treasury report * chore: reorganizes email-lambda and provides ses permission (#405) * chore: reorganizes email-lambda and provides ses permission * chore: remove the extra configuration related to emails * Minor changes * Update poetry.lock * Get email html to work * fix lint * chore: Remove duplicate import * Update typing, naming and error handling --------- Co-authored-by: Victor Shia <[email protected]> Co-authored-by: aditya <[email protected]>
- Loading branch information
1 parent
a59f6e7
commit ed30f13
Showing
11 changed files
with
1,612 additions
and
670 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
142 changes: 142 additions & 0 deletions
142
python/src/functions/generate_presigned_url_and_send_email.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
import os | ||
|
||
import boto3 | ||
import chevron | ||
import structlog | ||
from aws_lambda_typing.context import Context | ||
from pydantic import BaseModel | ||
from typing import Optional, Tuple | ||
|
||
from src.lib.logging import get_logger, reset_contextvars | ||
from src.lib.s3_helper import get_presigned_url | ||
from src.lib.treasury_generation_common import ( | ||
OrganizationObj, | ||
UserObj, | ||
) | ||
from src.lib.email import send_email | ||
|
||
|
||
treasury_email_html = """ | ||
Your treasury report can be downloaded <a href={url}>here</a>. | ||
""" | ||
|
||
treasury_email_text = """ | ||
Hello, | ||
Your treasury report can be downloaded here: {url}. | ||
""" | ||
|
||
class SendTreasuryEmailLambdaPayload(BaseModel): | ||
organization: OrganizationObj | ||
user: UserObj | ||
|
||
|
||
@reset_contextvars | ||
def handle(event: SendTreasuryEmailLambdaPayload, context: Context): | ||
"""Lambda handler for emailing Treasury reports | ||
Given a user and organization object- send an email to the user that | ||
contains a pre-signed URL to the following S3 object if it exists: | ||
treasuryreports/{organization.id}/{organization.preferences.current_reporting_period_id}/report.zip | ||
If the object does not exist then raise an exception. | ||
Args: | ||
event: S3 Lambda event of type `s3:ObjectCreated:*` | ||
context: Lambda context | ||
""" | ||
structlog.contextvars.bind_contextvars(lambda_event={"step_function": event}) | ||
logger = get_logger() | ||
logger.info("received new invocation event from step function") | ||
|
||
try: | ||
payload = SendTreasuryEmailLambdaPayload.model_validate(event) | ||
except Exception: | ||
logger.exception("Exception parsing Send Treasury Email event payload") | ||
return {"statusCode": 400, "body": "Bad Request"} | ||
|
||
try: | ||
process_event(payload, logger) | ||
except Exception: | ||
logger.exception("Exception processing sending treasury report email") | ||
return {"statusCode": 500, "body": "Internal Server Error"} | ||
|
||
return {"statusCode": 200, "body": "Success"} | ||
|
||
|
||
def generate_email( | ||
user: UserObj, | ||
logger: structlog.stdlib.BoundLogger, | ||
presigned_url: str = "", | ||
) -> Tuple[Optional[str], Optional[str], Optional[str]]: | ||
try: | ||
with open("src/static/email_templates/formatted_body.html") as g: | ||
email_body = chevron.render(g, { | ||
"body_title": 'Hello,', | ||
"body_detail": treasury_email_html.format( | ||
url = presigned_url | ||
), | ||
}) | ||
with open("src/static/email_templates/base.html") as f: | ||
email_html = chevron.render(f, { | ||
"tool_name": "CPF", | ||
"title": "CPF Treasury Report", | ||
"preheader": False, | ||
"webview_available": False, | ||
"base_url_safe": "", | ||
"usdr_logo_url": 'https://grants.usdigitalresponse.org/usdr_logo_transparent.png', | ||
"presigned_url": presigned_url, | ||
"notifications_url_safe": False, | ||
"email_body": email_body, | ||
}, | ||
partials_dict = { | ||
"email_body": email_body, | ||
}) | ||
email_text = treasury_email_text.format(url=presigned_url) | ||
subject = "USDR CPF Treasury Report" | ||
return email_html, email_text, subject | ||
except Exception as e: | ||
logger.error(f"Failed to generate treasury email: {e}") | ||
return None, None, None | ||
|
||
|
||
def process_event( | ||
payload: SendTreasuryEmailLambdaPayload, | ||
logger: structlog.stdlib.BoundLogger, | ||
): | ||
""" | ||
This function is structured as followed: | ||
1) Check to see if the s3 object exists: | ||
treasuryreports/{organization.id}/{organization.preferences.current_reporting_period_id}/report.zip | ||
2) If it does not, raise an exception and quit | ||
3) Generate a pre-signed URL with an expiration date of 1 hour | ||
4) Generate an email | ||
5) Send email to the user | ||
""" | ||
s3_client = boto3.client("s3") | ||
user = payload.user | ||
organization = payload.organization | ||
|
||
presigned_url = get_presigned_url( | ||
s3_client = s3_client, | ||
bucket = os.getenv("REPORTING_DATA_BUCKET_NAME"), | ||
key = f"treasuryreports/{organization.id}/{organization.preferences.current_reporting_period_id}/report.zip", | ||
expiration_time = 60 * 60, # 1 hour | ||
) | ||
if presigned_url is None: | ||
raise Exception('Failed to generate signed-URL or file not found') | ||
|
||
email_html, email_text, subject = generate_email( | ||
user = user, | ||
presigned_url = presigned_url, | ||
logger = logger, | ||
) | ||
if not email_html: | ||
return False | ||
|
||
send_email( | ||
dest_email = user.email, | ||
email_html = email_html, | ||
email_text = email_text, | ||
subject = subject, | ||
logger = logger, | ||
) | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import os | ||
import boto3 | ||
from botocore.exceptions import ClientError | ||
|
||
CHARSET = "UTF-8" | ||
|
||
|
||
def send_email( | ||
dest_email: str, | ||
email_html: str, | ||
email_text: str, | ||
subject: str, | ||
logger | ||
) -> bool: | ||
# Email user | ||
email_client = boto3.client("ses") | ||
|
||
# Try to send the email. | ||
try: | ||
#Provide the contents of the email. | ||
response = email_client.send_email( | ||
Destination={ | ||
"ToAddresses": [ | ||
dest_email, | ||
], | ||
}, | ||
Message={ | ||
"Body": { | ||
"Html": { | ||
"Charset": CHARSET, | ||
"Data": email_html, | ||
}, | ||
"Text": { | ||
"Charset": CHARSET, | ||
"Data": email_text, | ||
}, | ||
}, | ||
"Subject": { | ||
"Charset": CHARSET, | ||
"Data": subject, | ||
}, | ||
}, | ||
Source=os.getenv("NOTIFICATIONS_EMAIL"), | ||
) | ||
# Display an error if something goes wrong. | ||
except ClientError as e: | ||
logger.info(e.response["Error"]["Message"]) | ||
return False | ||
else: | ||
logger.info("Email sent! Message ID:"), | ||
logger.info(response["MessageId"]) | ||
return True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.