def deliver_emails_for_batch() -> Tuple[str, HTTPStatus]: """Deliver a batch of generated emails. Validates email addresses provided in the query params. Query parameters: batch_id: (required) Identifier for this batch redirect_address: (optional) An email address to which all emails will be sent. This can be used for redirecting all of the reports to a supervisor. cc_address: (optional) An email address to which all emails will be CC'd. This can be used for sending a batch of reports to multiple recipients. Multiple cc_address params can be given. Example: ?batch_id=123&cc_address=cc-one%40test.org&cc_address=cc_two%40test.org&cc_address=cc_three%40test.org subject_override: (optional) Override for subject being sent. Returns: Text indicating the results of the run and an HTTP status Raises: Nothing. Catch everything so that we can always return a response to the request """ try: batch_id = get_only_str_param_value("batch_id", request.args) redirect_address = get_only_str_param_value("redirect_address", request.args) cc_addresses = get_str_param_values("cc_address", request.args) subject_override = get_only_str_param_value("subject_override", request.args, preserve_case=True) validate_email_address(redirect_address) for cc_address in cc_addresses: validate_email_address(cc_address) except ValueError as error: logging.error(error) return str(error), HTTPStatus.BAD_REQUEST if not batch_id: msg = "Query parameter 'batch_id' not received" logging.error(msg) return msg, HTTPStatus.BAD_REQUEST success_count, failure_count = email_delivery.deliver( batch_id, redirect_address=redirect_address, cc_addresses=cc_addresses, subject_override=subject_override, ) redirect_text = (f"to the redirect email address {redirect_address}" if redirect_address else "") cc_addresses_text = (f"CC'd {','.join(email for email in cc_addresses)}." if cc_addresses else "") return ( f"Sent {success_count} emails {redirect_text}. {cc_addresses_text} " f"{failure_count} emails failed to send", HTTPStatus.OK, )
def send_message(self, to_email: str, from_email: str, from_email_name: str, subject: str, html_content: str, redirect_address: Optional[str] = None) -> bool: """Sends the email to the provided address by making a Twilio SendGrid API request. If there is a redirect_address, it creates the message for the redirect address instead and updates the subject line to include to original recipient's email address. Args: to_email: The recipient's email address from_email: The sender's email address from_email_name: A personalized name for the sender to display in the email client subject: The email subject line html_content: An string with HTML content for the email body redirect_address: (Optional) An email address to which all emails will be sent instead of the to_email address. Returns: True if the message is sent successfully False if the response is not OK or an exception is thrown """ if redirect_address: validate_email_address(redirect_address) subject = f"[{to_email}] {subject}" to_email = redirect_address message = self._create_message(to_email=to_email, from_email=from_email, from_email_name=from_email_name, subject=subject, html_content=html_content) try: response = self.client.send(message) except Exception: logging.exception("Error sending the file created for %s", to_email) return False logging.info("Sent email to %s. Status code = %s", to_email, response.status_code) return True
def test_validate_email_address_empty_string(self) -> None: """Given an empty string, it does raise a ValueError.""" with self.assertRaises(ValueError): utils.validate_email_address("")
def test_validate_email_address_none_provided(self) -> None: """Given an empty argument, it does not raise a ValueError.""" utils.validate_email_address(None)
def test_validate_email_address_invalid(self) -> None: """Given an invalid email address, it does raise a ValueError.""" with self.assertRaises(ValueError): utils.validate_email_address("some random string @ fake domain")
def test_validate_email_address_valid(self) -> None: """Given a valid email address, it does not raise a ValueError.""" utils.validate_email_address("*****@*****.**") utils.validate_email_address("*****@*****.**")
def _send_emails(state_code_str: str) -> Tuple[str, HTTPStatus]: try: data = request.json state_code = StateCode(state_code_str) if state_code not in EMAIL_STATE_CODES: raise ValueError("State code is invalid for the monthly reports") batch_id = data.get("batchId") redirect_address = data.get("redirectAddress") cc_addresses = data.get("ccAddresses") subject_override = data.get("subjectOverride") email_allowlist = data.get("emailAllowlist") validate_email_address(redirect_address) if cc_addresses is not None: for cc_address in cc_addresses: validate_email_address(cc_address) if email_allowlist is not None: for recipient_email in email_allowlist: validate_email_address(recipient_email) except ValueError as error: logging.error(error) return str(error), HTTPStatus.BAD_REQUEST if not state_code: msg = "Query parameter 'state_code' not received" logging.error(msg) return msg, HTTPStatus.BAD_REQUEST if not batch_id: msg = "Query parameter 'batch_id' not received" logging.error(msg) return msg, HTTPStatus.BAD_REQUEST # TODO(#7790): Support more email types. try: report_type = get_report_type(batch_id, state_code) if report_type != ReportType.POMonthlyReport: raise InvalidReportTypeError( f"Invalid report type: Sending emails with {report_type} is not implemented yet." ) except InvalidReportTypeError as error: logging.error(error) return str(error), HTTPStatus.NOT_IMPLEMENTED try: report_date = generate_report_date(batch_id, state_code) except EmailMetadataReportDateError as error: logging.error(error) return str(error), HTTPStatus.BAD_REQUEST result = email_delivery.deliver( batch_id=batch_id, state_code=state_code, redirect_address=redirect_address, cc_addresses=cc_addresses, subject_override=subject_override, email_allowlist=email_allowlist, report_date=report_date, ) redirect_text = ( f"to the redirect email address {redirect_address}" if redirect_address else "" ) cc_addresses_text = ( f"CC'd {','.join(email for email in cc_addresses)}." if cc_addresses else "" ) success_text = ( f"Sent {len(result.successes)} emails {redirect_text}. {cc_addresses_text} " ) if result.failures and not result.successes: return ( f"{success_text} " f"All emails failed to send", HTTPStatus.INTERNAL_SERVER_ERROR, ) if result.failures: return ( f"{success_text} " f"{len(result.failures)} emails failed to send: {','.join(result.failures)}", HTTPStatus.MULTI_STATUS, ) return (f"{success_text}"), HTTPStatus.OK
def _generate_emails(state_code_str: str) -> Tuple[str, HTTPStatus]: try: data = request.json state_code = StateCode(state_code_str) if state_code not in EMAIL_STATE_CODES: raise ValueError("State code is invalid for PO monthly reports") # TODO(#7790): Support more email types. report_type = ReportType(data.get("reportType")) if report_type != ReportType.POMonthlyReport: raise ValueError(f"{report_type.value} is not a valid ReportType") test_address = data.get("testAddress") region_code = data.get("regionCode") message_body_override = data.get("messageBodyOverride") email_allowlist = data.get("emailAllowlist") validate_email_address(test_address) if email_allowlist is not None: for recipient_email in email_allowlist: validate_email_address(recipient_email) except (ValueError, JSONDecodeError) as error: logging.error(error) return str(error), HTTPStatus.BAD_REQUEST if test_address == "": test_address = None if region_code not in REGION_CODES: region_code = None try: batch_id = generate_batch_id() if in_development(): with local_project_id_override(GCP_PROJECT_STAGING): result: MultiRequestResult[str, str] = data_retrieval.start( state_code=state_code, report_type=report_type, batch_id=batch_id, test_address=test_address, region_code=region_code, email_allowlist=email_allowlist, message_body_override=message_body_override, ) else: result = data_retrieval.start( state_code=state_code, report_type=report_type, batch_id=batch_id, test_address=test_address, region_code=region_code, email_allowlist=email_allowlist, message_body_override=message_body_override, ) except InvalidRegionCodeException: return "Invalid region code provided", HTTPStatus.BAD_REQUEST new_batch_text = f"New batch started for {state_code} and {report_type}. Batch id = {batch_id}." test_address_text = ( f"Emails generated for test address: {test_address}" if test_address else "" ) counts_text = f"Successfully generated {len(result.successes)} email(s)" success_text = f"{new_batch_text} {test_address_text} {counts_text}." if result.failures and not result.successes: return ( f"{success_text}" f" Failed to generate all emails. Retry the request again." ), HTTPStatus.INTERNAL_SERVER_ERROR if result.failures: return ( f"{success_text}" f" Failed to generate {len(result.failures)} email(s): {', '.join(result.failures)}" ), HTTPStatus.MULTI_STATUS return ( jsonify( { "batchId": batch_id, "statusText": f"{success_text}", } ), HTTPStatus.OK, )
def dashboard_user_restrictions_by_email( ) -> Tuple[Union[Auth0AppMetadata, str], HTTPStatus]: """This endpoint is accessed by a service account used by an Auth0 hook that is called at the pre-registration when a user first signs up for an account. Given a user email address in the request, it responds with the app_metadata that the hook will save on the user so that the UP dashboards can apply the appropriate restrictions. Query parameters: email_address: (required) The email address that requires a user restrictions lookup region_code: (required) The region code to use to lookup the user restrictions Returns: JSON response of the app_metadata associated with the given email address and an HTTP status Raises: Nothing. Catch everything so that we can always return a response to the request """ email_address = get_only_str_param_value("email_address", request.args) region_code = get_only_str_param_value("region_code", request.args, preserve_case=True) try: if not email_address: return "Missing email_address param", HTTPStatus.BAD_REQUEST if not region_code: return "Missing region_code param", HTTPStatus.BAD_REQUEST _validate_region_code(region_code) validate_email_address(email_address) except ValueError as error: logging.error(error) return str(error), HTTPStatus.BAD_REQUEST database_key = SQLAlchemyDatabaseKey.for_schema( schema_type=SchemaType.CASE_TRIAGE) # TODO(#8046): Don't use the deprecated session fetcher session = SessionFactory.deprecated__for_database( database_key=database_key) try: user_restrictions = (session.query( DashboardUserRestrictions.allowed_supervision_location_ids, DashboardUserRestrictions.allowed_supervision_location_level, DashboardUserRestrictions.can_access_leadership_dashboard, DashboardUserRestrictions.can_access_case_triage, ).filter( DashboardUserRestrictions.state_code == region_code.upper(), func.lower(DashboardUserRestrictions.restricted_user_email) == email_address.lower(), ).one()) restrictions = _format_db_results(user_restrictions) return (restrictions, HTTPStatus.OK) except sqlalchemy.orm.exc.NoResultFound: return ( f"User not found for email address {email_address} and region code {region_code}.", HTTPStatus.NOT_FOUND, ) except Exception as error: logging.error(error) return ( f"An error occurred while fetching dashboard user restrictions with the email {email_address} for " f"region_code {region_code}: {error}", HTTPStatus.INTERNAL_SERVER_ERROR, ) finally: session.close()
def start_new_batch() -> Tuple[str, HTTPStatus]: """Start a new batch of email generation for the indicated state. Validates the test address provided in the params. Query parameters: state_code: (required) A valid state code for which reporting is enabled (ex: "US_ID") report_type: (required) A valid report type identifier (ex: "po_monthly_report) test_address: (optional) Should only be used for testing. When provided, the test_address is used to generate the email filenames, ensuring that all emails in the batch can only be delivered to the test_address and not to the usual recipients of the report. The email filenames will include the original recipient's email username, for example: [email protected]. region_code: (optional) Indicates the sub-region of the state to generate emails for. If omitted, we generate emails for all sub-regions of the state. email_allowlist: (optional) A json list of emails we should generate emails for. Emails that do not exist in the report will be silently skipped. message_body: (optional) If included, overrides the default message body. Returns: Text indicating the results of the run and an HTTP status Raises: Nothing. Catch everything so that we can always return a response to the request """ try: state_code = get_only_str_param_value("state_code", request.args) report_type = get_only_str_param_value("report_type", request.args) test_address = get_only_str_param_value("test_address", request.args) region_code = get_only_str_param_value("region_code", request.args) raw_email_allowlist = get_only_str_param_value("email_allowlist", request.args) message_body = get_only_str_param_value("message_body", request.args, preserve_case=True) validate_email_address(test_address) email_allowlist: Optional[List[str]] = ( json.loads(raw_email_allowlist) if raw_email_allowlist else None) if email_allowlist is not None: for recipient_email in email_allowlist: validate_email_address(recipient_email) except (ValueError, JSONDecodeError) as error: logging.error(error) return str(error), HTTPStatus.BAD_REQUEST if not state_code or not report_type: msg = "Request does not include 'state_code' and/or 'report_type' parameters" logging.error(msg) return msg, HTTPStatus.BAD_REQUEST # Normalize query param inputs state_code = state_code.upper() if test_address == "": test_address = None region_code = None if not region_code else region_code.upper() try: batch_id = email_reporting_utils.generate_batch_id() failure_count, success_count = data_retrieval.start( state_code=state_code, report_type=report_type, batch_id=batch_id, test_address=test_address, region_code=region_code, email_allowlist=email_allowlist, message_body=message_body, ) except InvalidRegionCodeException: return "Invalid region code provided", HTTPStatus.BAD_REQUEST else: test_address_text = ( f"Emails generated for test address: {test_address}" if test_address else "") counts_text = f"Successfully generated {success_count} email(s)" if failure_count: counts_text += f" Failed to generate {failure_count} email(s)" return ( f"New batch started for {state_code} and {report_type}. Batch " f"id = {batch_id}. {test_address_text} {counts_text}" ), HTTPStatus.OK