def test_deliver_no_attachments(
        self, mock_load_files_from_storage: MagicMock
    ) -> None:
        """When emails exist, but no attachments exist, an attachment is not delivered to the recipients"""

        def fake_load_files(_bucket: str, prefix: str) -> Dict[str, str]:
            if "attachments" not in prefix:
                return self.mock_files

            return {}

        mock_load_files_from_storage.side_effect = fake_load_files

        with self.assertLogs(level="INFO"):
            email_delivery.deliver(
                self.batch_id,
                self.state_code,
                self.report_date,
            )

        self.mock_sendgrid_client.send_message.assert_called_with(
            to_email=self.to_address,
            from_email=self.mock_env_vars["FROM_EMAIL_ADDRESS"],
            from_email_name=self.mock_env_vars["FROM_EMAIL_NAME"],
            subject="Your monthly Recidiviz report",
            html_content=self.mock_files[self.to_address],
            attachment_title=self.attachment_title,
            redirect_address=None,
            cc_addresses=None,
            text_attachment_content=None,
        )
    def test_deliver_with_no_files_fails(self) -> None:
        """Test that load_files_from_storage raises an IndexError when there are no files to retrieve"""
        bucket_name = "bucket-name"
        self.mock_utils.get_email_content_bucket_name.return_value = bucket_name

        with self.assertRaises(IndexError):
            email_delivery.deliver(self.batch_id)
    def test_deliver_calls_send_message(
        self, mock_load_files_from_storage: MagicMock
    ) -> None:
        """Given a batch_id, test that the SendGridClientWrapper send_message is called with
        the data it needs to send an email.
        """
        mock_load_files_from_storage.return_value = self.mock_files
        with self.assertLogs(level="INFO"):
            email_delivery.deliver(
                self.batch_id,
                self.state_code,
                self.report_date,
            )

        self.mock_sendgrid_client.send_message.assert_called_with(
            to_email=self.to_address,
            from_email=self.mock_env_vars["FROM_EMAIL_ADDRESS"],
            from_email_name=self.mock_env_vars["FROM_EMAIL_NAME"],
            subject="Your monthly Recidiviz report",
            html_content=self.mock_files[self.to_address],
            attachment_title=self.attachment_title,
            redirect_address=None,
            cc_addresses=None,
            text_attachment_content=self.mock_files[self.to_address],
        )
    def test_deliver_with_redirect_address(
        self, mock_load_files_from_storage: MagicMock
    ) -> None:
        """Given a batch_id and a redirect_address, test that the SendGridClientWrapper send_message is called with
        the redirect_address."""
        mock_load_files_from_storage.return_value = self.mock_files

        with self.assertLogs(level="INFO"):
            email_delivery.deliver(
                batch_id=self.batch_id,
                state_code=self.state_code,
                redirect_address=self.redirect_address,
                report_date=self.report_date,
            )

        mock_load_files_from_storage.assert_has_calls(
            [
                call(self.bucket_name, "my-html-folder"),
                call(self.bucket_name, "my-attachments-folder"),
            ]
        )

        self.mock_sendgrid_client.send_message.assert_called_with(
            to_email=self.to_address,
            from_email=self.mock_env_vars["FROM_EMAIL_ADDRESS"],
            from_email_name=self.mock_env_vars["FROM_EMAIL_NAME"],
            subject="Your monthly Recidiviz report",
            html_content=self.mock_files[self.to_address],
            attachment_title=self.attachment_title,
            redirect_address=self.redirect_address,
            cc_addresses=None,
            text_attachment_content=self.mock_files[self.to_address],
        )
def deliver_emails_for_batch() -> Tuple[str, HTTPStatus]:
    """Deliver a batch of generated emails.

    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.

    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
    """
    batch_id = get_str_param_value('batch_id', request.args)
    redirect_address = get_str_param_value('redirect_address', request.args)

    if not batch_id:
        msg = "Query parameter 'batch_id' not received"
        logging.error(msg)
        return msg, HTTPStatus.BAD_REQUEST

    if redirect_address:
        success_count, failure_count = email_delivery.deliver(batch_id, redirect_address=redirect_address)
        return (f"Sent {success_count} emails to the test address {redirect_address}. "
                f"{failure_count} emails failed to send"), HTTPStatus.OK

    success_count, failure_count = email_delivery.deliver(batch_id)
    return f"Sent {success_count} emails. {failure_count} emails failed to send", HTTPStatus.OK
Exemple #6
0
    def test_deliver_fails_missing_env_vars(self, mock_retrieve_html_files: MagicMock) -> None:
        """Given a batch_id and a redirect_address, test that the SendGridClientWrapper send_message is called with
        the redirect_address, and the to address is included in the subject."""
        self.mock_utils.get_env_var.side_effect = KeyError
        with self.assertRaises(KeyError), self.assertLogs(level='ERROR'):
            email_delivery.deliver(self.batch_id)

        mock_retrieve_html_files.assert_not_called()
        self.mock_sendgrid_client.send_message.assert_not_called()
    def test_deliver_fails_missing_env_vars(
        self, mock_load_files_from_storage: MagicMock
    ) -> None:
        """Given a batch_id and a redirect_address, test that the SendGridClientWrapper send_message is called with
        the redirect_address, and the to address is included in the subject."""
        self.mock_utils.get_env_var.side_effect = KeyError
        with self.assertRaises(KeyError), self.assertLogs(level="ERROR"):
            email_delivery.deliver(
                self.batch_id,
                self.state_code,
                self.report_date,
            )

        mock_load_files_from_storage.assert_not_called()
        self.mock_sendgrid_client.send_message.assert_not_called()
 def test_deliver_correctly_with_email_allowlist(
     self, mock_load_files_from_storage: MagicMock
 ) -> None:
     """Given a batch_id and an email allowlist, test that the SendGridClientWrapper send_message is only called with
     allowlisted to address."""
     self.mock_files.update({"*****@*****.**": "<html><body></html>"})
     mock_load_files_from_storage.return_value = self.mock_files
     self.mock_sendgrid_client.send_message.return_value = True
     with self.assertLogs(level="INFO"):
         result = email_delivery.deliver(
             batch_id=self.batch_id,
             state_code=self.state_code,
             report_date=self.report_date,
             email_allowlist=[self.to_address],
         )
     self.mock_sendgrid_client.send_message.assert_called_with(
         to_email=self.to_address,
         from_email=self.mock_env_vars["FROM_EMAIL_ADDRESS"],
         from_email_name=self.mock_env_vars["FROM_EMAIL_NAME"],
         subject="Your monthly Recidiviz report",
         html_content=self.mock_files[self.to_address],
         attachment_title=self.attachment_title,
         redirect_address=None,
         cc_addresses=None,
         text_attachment_content=self.mock_files[self.to_address],
     )
     self.assertEqual(len(result.successes), 1)
     self.assertEqual(len(result.failures), 0)
     self.assertNotIn("*****@*****.**", result.successes)
Exemple #9
0
    def test_deliver_with_redirect_address(self, mock_retrieve_html_files: MagicMock) -> None:
        """Given a batch_id and a redirect_address, test that the SendGridClientWrapper send_message is called with
        the redirect_address."""
        mock_retrieve_html_files.return_value = self.mock_html_files
        with self.assertLogs(level='INFO'):
            email_delivery.deliver(self.batch_id, self.redirect_address)

        mock_retrieve_html_files.assert_called_with(self.batch_id)
        self.mock_sendgrid_client.send_message.assert_called_with(
            to_email=self.to_address,
            from_email=self.mock_env_vars['FROM_EMAIL_ADDRESS'],
            from_email_name=self.mock_env_vars['FROM_EMAIL_NAME'],
            subject='Your monthly Recidiviz report',
            html_content=self.mock_html_files[self.to_address],
            redirect_address=self.redirect_address
        )
Exemple #10
0
 def test_deliver_with_redirect_address_fails(self, mock_retrieve_html_files: MagicMock) -> None:
     """Given a batch_id and a redirect_address, test the fail count increases if redirect_address fails to send."""
     mock_retrieve_html_files.return_value = self.mock_html_files
     self.mock_sendgrid_client.send_message.return_value = False
     [success_count, fail_count] = email_delivery.deliver(self.batch_id, self.redirect_address)
     self.assertEqual(fail_count, 1)
     self.assertEqual(success_count, 0)
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,
    )
Exemple #12
0
    def test_deliver_returns_fail_count(self, mock_retrieve_html_files: MagicMock) -> None:
        """Given a batch_id, test that the deliver returns the fail_count value when it fails"""
        mock_retrieve_html_files.return_value = self.mock_html_files
        self.mock_sendgrid_client.send_message.return_value = False
        [success_count, fail_count] = email_delivery.deliver(self.batch_id)

        self.assertEqual(success_count, 0)
        self.assertEqual(fail_count, 1)
Exemple #13
0
    def test_deliver_returns_success_count(self, mock_retrieve_html_files: MagicMock) -> None:
        """Given a batch_id, test that the deliver returns the success_count and fail_count values"""
        mock_retrieve_html_files.return_value = self.mock_html_files
        self.mock_sendgrid_client.send_message.return_value = True
        with self.assertLogs(level='INFO'):
            [success_count, fail_count] = email_delivery.deliver(self.batch_id)

        self.assertEqual(success_count, 1)
        self.assertEqual(fail_count, 0)
    def test_deliver_returns_fail_count(
        self, mock_load_files_from_storage: MagicMock
    ) -> None:
        """Given a batch_id, test that the deliver returns the fail_count value when it fails"""
        mock_load_files_from_storage.return_value = self.mock_files
        self.mock_sendgrid_client.send_message.return_value = False
        result = email_delivery.deliver(
            self.batch_id,
            self.state_code,
            self.report_date,
        )

        self.assertEqual(len(result.successes), 0)
        self.assertEqual(len(result.failures), 1)
 def test_deliver_with_redirect_address_fails(
     self, mock_load_files_from_storage: MagicMock
 ) -> None:
     """Given a batch_id and a redirect_address, test the fail count increases if redirect_address fails to send."""
     mock_load_files_from_storage.return_value = self.mock_files
     self.mock_sendgrid_client.send_message.return_value = False
     result = email_delivery.deliver(
         self.batch_id,
         self.state_code,
         self.report_date,
         self.redirect_address,
     )
     self.assertEqual(len(result.failures), 1)
     self.assertEqual(len(result.successes), 0)
    def test_deliver_returns_success_count(
        self, mock_load_files_from_storage: MagicMock
    ) -> None:
        """Given a batch_id, test that the deliver returns the success_count and fail_count values"""
        mock_load_files_from_storage.return_value = self.mock_files
        self.mock_sendgrid_client.send_message.return_value = True
        with self.assertLogs(level="INFO"):
            result = email_delivery.deliver(
                self.batch_id,
                self.state_code,
                self.report_date,
            )

        self.assertEqual(len(result.successes), 1)
        self.assertEqual(len(result.failures), 0)
Exemple #17
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