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("*****@*****.**")
Beispiel #7
0
    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
Beispiel #8
0
    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,
        )
Beispiel #9
0
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