コード例 #1
0
def deliver(batch_id: str, redirect_address: Optional[str] = None) -> Tuple[int, int]:
    """Delivers emails for the given batch.

    Delivers emails to the email address specified in the generated email filename.

    If a redirect_address is provided, emails are sent to the redirect_address and the original recipient's email
    address's name (without the domain) is appended to the name (i.e. [email protected]).

    Args:
        batch_id: The identifier for the batch
        redirect_address: (optional) An email address to which all emails will be sent

    Returns:
        A tuple with counts of successful deliveries and failures (successes, failures)

    Raises:
        Raises errors related to external services like Google Storage but continues attempting to send subsequent
        emails if it receives an exception while attempting to send.  In that case the error is logged.
    """
    if redirect_address:
        logging.info("Redirecting all emails for batch %s to be sent to %s", batch_id, redirect_address)
    else:
        logging.info("Delivering emails for batch %s", batch_id)

    try:
        from_email_address = utils.get_env_var('FROM_EMAIL_ADDRESS')
        from_email_name = utils.get_env_var('FROM_EMAIL_NAME')
    except KeyError:
        logging.error("Unable to get a required environment variables `FROM_EMAIL_ADDRESS` or `FROM_EMAIL_NAME`. "
                      "Exiting.")
        raise

    files = retrieve_html_files(batch_id)
    success_count = 0
    fail_count = 0
    sendgrid = SendGridClientWrapper()

    for recipient_email_address in files:
        sent_successfully = sendgrid.send_message(to_email=recipient_email_address,
                                                  from_email=from_email_address,
                                                  from_email_name=from_email_name,
                                                  subject=EMAIL_SUBJECT,
                                                  html_content=files[recipient_email_address],
                                                  redirect_address=redirect_address)

        if sent_successfully:
            success_count = success_count + 1
        else:
            fail_count = fail_count + 1

    logging.info("Sent %s emails. %s emails failed to send", success_count, fail_count)
    return success_count, fail_count
コード例 #2
0
 def setUp(self) -> None:
     HttpResponse = collections.namedtuple('Response', ['status_code'])
     self.error_response = HttpResponse(404)
     self.success_response = HttpResponse(202)
     self.client_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.SendGridAPIClient')
     self.secrets_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.secrets').start()
     self.mail_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.Mail')
     self.email_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.Email')
     self.mock_client = self.client_patcher.start().return_value
     self.mock_mail = self.mail_patcher.start()
     self.mock_email = self.email_patcher.start()
     self.secrets_patcher.get_secret.return_value = 'secret'
     self.wrapper = SendGridClientWrapper()
コード例 #3
0
 def setUp(self) -> None:
     HttpResponse = collections.namedtuple("HttpResponse", ["status_code"])
     self.error_response = HttpResponse(404)
     self.success_response = HttpResponse(202)
     self.client_patcher = patch(
         "recidiviz.reporting.sendgrid_client_wrapper.SendGridAPIClient")
     self.secrets_patcher = patch(
         "recidiviz.reporting.sendgrid_client_wrapper.secrets").start()
     self.mail_helpers_patcher = patch(
         "recidiviz.reporting.sendgrid_client_wrapper.mail_helpers")
     self.mock_mail_helpers = self.mail_helpers_patcher.start()
     self.mock_client = self.client_patcher.start().return_value
     self.secrets_patcher.get_secret.return_value = "secret"
     self.wrapper = SendGridClientWrapper()
コード例 #4
0
class SendGridClientWrapperTest(TestCase):
    """Tests for the wrapper class SendGridClientWrapper"""

    def setUp(self) -> None:
        HttpResponse = collections.namedtuple("HttpResponse", ["status_code"])
        self.error_response = HttpResponse(404)
        self.success_response = HttpResponse(202)
        self.client_patcher = patch(
            "recidiviz.reporting.sendgrid_client_wrapper.SendGridAPIClient"
        )
        self.secrets_patcher = patch(
            "recidiviz.reporting.sendgrid_client_wrapper.secrets"
        ).start()
        self.mail_helpers_patcher = patch(
            "recidiviz.reporting.sendgrid_client_wrapper.mail_helpers"
        )
        self.mock_mail_helpers = self.mail_helpers_patcher.start()
        self.mock_client = self.client_patcher.start().return_value
        self.secrets_patcher.get_secret.return_value = "secret"
        self.wrapper = SendGridClientWrapper()
        self.attachment_title = "2021-05 Recidiviz Monthly Report - Client Details.txt"

    def tearDown(self) -> None:
        self.client_patcher.stop()
        self.secrets_patcher.stop()
        self.mail_helpers_patcher.stop()

    def test_send_message(self) -> None:
        """Test that send_message sends the return from create_message client's send method and that the Mail helper
        is called with the right arguments. Test that it returns True on success."""
        to_email = "*****@*****.**"
        from_email = "<Recidiviz> [email protected]"
        mail_message = "message"
        subject = "Your monthly Recidiviz Report"
        html_content = "<html></html>"
        self.mock_mail_helpers.Email.return_value = from_email
        self.mock_mail_helpers.Mail.return_value = mail_message
        self.mock_client.send.return_value = self.success_response
        with self.assertLogs(level="INFO"):
            response = self.wrapper.send_message(
                to_email=to_email,
                from_email="*****@*****.**",
                subject=subject,
                html_content=html_content,
                attachment_title=self.attachment_title,
                from_email_name="Recidiviz",
            )
            self.assertTrue(response)

        self.mock_mail_helpers.CC.assert_not_called()
        self.mock_mail_helpers.Email.assert_called_with(
            "*****@*****.**", "Recidiviz"
        )
        self.mock_mail_helpers.Mail.assert_called_with(
            to_emails=to_email,
            from_email=from_email,
            subject=subject,
            html_content=html_content,
        )
        self.mock_client.send.assert_called_with(mail_message)

    def test_send_message_with_cc(self) -> None:
        """Given cc_addresses, test that _create_message creates a list of Cc objects for the Mail message."""
        self.mock_client.send.return_value = self.success_response

        Mail = collections.namedtuple("Mail", [])
        mock_message = MagicMock(return_value=Mail())
        self.mock_mail_helpers.Mail.return_value = mock_message

        mock_parent = Mock()
        mock_parent.attach_mock(self.mock_mail_helpers.Cc, "Cc")
        cc_addresses = ["*****@*****.**", "*****@*****.**"]
        expected_calls = [call.Cc(email=cc_address) for cc_address in cc_addresses]

        self.wrapper.send_message(
            to_email="*****@*****.**",
            from_email="*****@*****.**",
            subject="Your monthly Recidiviz Report",
            html_content="<html></html>",
            attachment_title=self.attachment_title,
            from_email_name="Recidiviz",
            cc_addresses=cc_addresses,
        )
        self.assertTrue(hasattr(mock_message, "cc"))
        self.assertEqual(len(mock_message.cc), 2)
        mock_parent.assert_has_calls(expected_calls)

    def test_send_message_exception(self) -> None:
        """Test that an error is logged when an exception is raised and it returns False"""
        self.mock_client.send.side_effect = Exception
        with self.assertLogs(level="ERROR"):
            response = self.wrapper.send_message(
                to_email="*****@*****.**",
                from_email="*****@*****.**",
                subject="Your monthly Recidiviz Report",
                html_content="<html></html>",
                attachment_title=self.attachment_title,
                from_email_name="Recidiviz",
            )
            self.assertFalse(response)

    def test_send_message_with_redirect_address(self) -> None:
        """Given a redirect_address, test that _create_message is called with the correct to_email and subject line."""
        self.mock_mail_helpers.Email.return_value = "<Recidiviz> [email protected]"
        self.mock_client.send.return_value = self.success_response
        redirect_address = "*****@*****.**"
        to_email = "*****@*****.**"
        subject = "Your monthly Recidiviz Report"
        with self.assertLogs(level="INFO"):
            self.wrapper.send_message(
                to_email=to_email,
                from_email="*****@*****.**",
                subject=subject,
                html_content="<html></html>",
                attachment_title=self.attachment_title,
                from_email_name="Recidiviz",
                redirect_address=redirect_address,
            )

        self.mock_mail_helpers.Mail.assert_called_with(
            to_emails=redirect_address,
            from_email="<Recidiviz> [email protected]",
            subject=f"[{to_email}] {subject}",
            html_content="<html></html>",
        )

    def test_send_message_with_text_attachment_content(self) -> None:
        """Given text_attachment_content, test that an attachment is created and attached to the outgoing message."""
        file_content = "Fake email attachment content"
        self.wrapper.send_message(
            to_email="*****@*****.**",
            from_email="*****@*****.**",
            subject="Your monthly Recidiviz Report",
            html_content="<html></html>",
            attachment_title=self.attachment_title,
            from_email_name="Recidiviz",
            text_attachment_content=file_content,
        )
        self.mock_mail_helpers.Attachment.assert_called_with(
            file_content=self.mock_mail_helpers.FileContent(
                "Fake email attachment content"
            ),
            file_name=self.mock_mail_helpers.FileName(
                "2021-05 Recidiviz Monthly Report - Client Details.txt"
            ),
            file_type=self.mock_mail_helpers.FileType("text/plain"),
            disposition=self.mock_mail_helpers.Disposition("attachment"),
        )

    def test_send_message_with_text_attachment_content_none(self) -> None:
        """Given no text_attachment_content, assert that an attachment is not created."""
        self.wrapper.send_message(
            to_email="*****@*****.**",
            from_email="*****@*****.**",
            subject="Your monthly Recidiviz Report",
            html_content="<html></html>",
            attachment_title=self.attachment_title,
            from_email_name="Recidiviz",
            text_attachment_content=None,
        )
        self.mock_mail_helpers.Attachment.assert_not_called()
コード例 #5
0
def deliver(
    batch_id: str,
    state_code: StateCode,
    report_date: date,
    redirect_address: Optional[str] = None,
    cc_addresses: Optional[List[str]] = None,
    subject_override: Optional[str] = None,
    email_allowlist: Optional[List[str]] = None,
) -> MultiRequestResult[str, str]:
    """Delivers emails for the given batch.

    Delivers emails to the email address specified in the generated email filename.

    If a redirect_address is provided, emails are sent to the redirect_address and the original recipient's email
    address's name (without the domain) is appended to the name (i.e. [email protected]).

    Args:
        batch_id: The identifier for the batch
        state_code: (required) A valid state code for which reporting is enabled (ex. "US_ID")
        report_date: The date of the report (ex. 2021, 5, 31)
        redirect_address: (optional) An email address to which all emails will be sent
        cc_addresses: (optional) A list of email addressed to include on the cc line
        subject_override: (optional) The subject line to override to.
        email_allowlist: (optional) A subset list of email addresses that are to receive the email. If not provided,
        all recipients will be sent emails to.

    Returns:
        A MultiRequestResult containing successes and failures for the emails that were sent

    Raises:
        Raises errors related to external services like Google Storage but continues attempting to send subsequent
        emails if it receives an exception while attempting to send.  In that case the error is logged.
    """
    logging.info("Delivering emails for batch %s", batch_id)

    if redirect_address:
        logging.info(
            "Redirecting all emails for batch %s to be sent to %s",
            batch_id,
            redirect_address,
        )

    if cc_addresses:
        logging.info(
            "CCing the following addresses: [%s]",
            ",".join(address for address in cc_addresses),
        )

    try:
        from_email_address = utils.get_env_var("FROM_EMAIL_ADDRESS")
        from_email_name = utils.get_env_var("FROM_EMAIL_NAME")
    except KeyError:
        logging.error(
            "Unable to get a required environment variables `FROM_EMAIL_ADDRESS` or `FROM_EMAIL_NAME`. "
            "Exiting.")
        raise

    content_bucket = utils.get_email_content_bucket_name()
    html_files = load_files_from_storage(
        content_bucket, utils.get_html_folder(batch_id, state_code))
    attachment_files = load_files_from_storage(
        content_bucket, utils.get_attachments_folder(batch_id, state_code))

    if len(html_files.items()) == 0:
        msg = f"No files found for batch {batch_id} for state {state_code} in the bucket {content_bucket}"
        logging.error(msg)
        raise IndexError(msg)

    if email_allowlist is not None:
        html_files = {
            email: content
            for email, content in html_files.items()
            if email in email_allowlist
        }

    succeeded_email_sends: List[str] = []
    failed_email_sends: List[str] = []
    sendgrid = SendGridClientWrapper()
    subject = DEFAULT_EMAIL_SUBJECT if subject_override is None else subject_override
    report_date_str = report_date.strftime("%Y-%m")
    attachment_title = (
        f"{report_date_str} Recidiviz Monthly Report - Client Details.txt")

    for recipient_email_address in html_files:
        sent_successfully = sendgrid.send_message(
            to_email=recipient_email_address,
            from_email=from_email_address,
            from_email_name=from_email_name,
            subject=subject,
            html_content=html_files[recipient_email_address],
            attachment_title=attachment_title,
            redirect_address=redirect_address,
            cc_addresses=cc_addresses,
            text_attachment_content=attachment_files.get(
                recipient_email_address),
        )

        if sent_successfully:
            succeeded_email_sends.append(recipient_email_address)
        else:
            failed_email_sends.append(recipient_email_address)

    logging.info(
        "Sent %s emails. %s emails failed to send",
        len(succeeded_email_sends),
        len(failed_email_sends),
    )
    return MultiRequestResult(successes=succeeded_email_sends,
                              failures=failed_email_sends)
コード例 #6
0
def deliver(
    batch_id: str,
    redirect_address: Optional[str] = None,
    cc_addresses: Optional[List[str]] = None,
    subject_override: Optional[str] = None,
) -> Tuple[int, int]:
    """Delivers emails for the given batch.

    Delivers emails to the email address specified in the generated email filename.

    If a redirect_address is provided, emails are sent to the redirect_address and the original recipient's email
    address's name (without the domain) is appended to the name (i.e. [email protected]).

    Args:
        batch_id: The identifier for the batch
        redirect_address: (optional) An email address to which all emails will be sent
        cc_addresses: (optional) A list of email addressed to include on the cc line
        subject_override: (optional) The subject line to override to.

    Returns:
        A tuple with counts of successful deliveries and failures (successes, failures)

    Raises:
        Raises errors related to external services like Google Storage but continues attempting to send subsequent
        emails if it receives an exception while attempting to send.  In that case the error is logged.
    """
    logging.info("Delivering emails for batch %s", batch_id)

    if redirect_address:
        logging.info(
            "Redirecting all emails for batch %s to be sent to %s",
            batch_id,
            redirect_address,
        )

    if cc_addresses:
        logging.info(
            "CCing the following addresses: [%s]",
            ",".join(address for address in cc_addresses),
        )

    try:
        from_email_address = utils.get_env_var("FROM_EMAIL_ADDRESS")
        from_email_name = utils.get_env_var("FROM_EMAIL_NAME")
    except KeyError:
        logging.error(
            "Unable to get a required environment variables `FROM_EMAIL_ADDRESS` or `FROM_EMAIL_NAME`. "
            "Exiting.")
        raise

    content_bucket = utils.get_email_content_bucket_name()
    html_files = load_files_from_storage(content_bucket,
                                         utils.get_html_folder(batch_id))
    attachment_files = load_files_from_storage(
        content_bucket, utils.get_attachments_folder(batch_id))

    if len(html_files.items()) == 0:
        msg = f"No files found for batch {batch_id} in the bucket {content_bucket}"
        logging.error(msg)
        raise IndexError(msg)

    success_count = 0
    fail_count = 0
    sendgrid = SendGridClientWrapper()
    subject = DEFAULT_EMAIL_SUBJECT if subject_override is None else subject_override

    for recipient_email_address in html_files:
        sent_successfully = sendgrid.send_message(
            to_email=recipient_email_address,
            from_email=from_email_address,
            from_email_name=from_email_name,
            subject=subject,
            html_content=html_files[recipient_email_address],
            redirect_address=redirect_address,
            cc_addresses=cc_addresses,
            text_attachment_content=attachment_files.get(
                recipient_email_address),
        )

        if sent_successfully:
            success_count = success_count + 1
        else:
            fail_count = fail_count + 1

    logging.info("Sent %s emails. %s emails failed to send", success_count,
                 fail_count)
    return success_count, fail_count
コード例 #7
0
class SendGridClientWrapperTest(TestCase):
    """Tests for the wrapper class SendGridClientWrapper"""

    def setUp(self) -> None:
        HttpResponse = collections.namedtuple('Response', ['status_code'])
        self.error_response = HttpResponse(404)
        self.success_response = HttpResponse(202)
        self.client_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.SendGridAPIClient')
        self.secrets_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.secrets').start()
        self.mail_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.Mail')
        self.email_patcher = patch('recidiviz.reporting.sendgrid_client_wrapper.Email')
        self.mock_client = self.client_patcher.start().return_value
        self.mock_mail = self.mail_patcher.start()
        self.mock_email = self.email_patcher.start()
        self.secrets_patcher.get_secret.return_value = 'secret'
        self.wrapper = SendGridClientWrapper()

    def tearDown(self) -> None:
        self.client_patcher.stop()
        self.secrets_patcher.stop()
        self.mail_patcher.stop()
        self.email_patcher.stop()


    def test_send_message(self) -> None:
        """Test that send_message sends the return from create_message client's send method and that the Mail helper
        is called with the right arguments. Test that it returns True on success."""
        to_email = '*****@*****.**'
        from_email = '<Recidiviz> [email protected]'
        mail_message = 'message'
        subject = 'Your monthly Recidiviz Report'
        html_content = '<html></html>'
        self.mock_email.return_value = from_email
        self.mock_mail.return_value = mail_message
        self.mock_client.send.return_value = self.success_response
        args = {
            'to_email': to_email,
            'from_email': '*****@*****.**',
            'subject': subject,
            'from_email_name': 'Recidiviz',
            'html_content': html_content
        }

        with self.assertLogs(level='INFO'):
            response = self.wrapper.send_message(**args)
            self.assertTrue(response)

        self.mock_email.assert_called_with('*****@*****.**', 'Recidiviz')
        self.mock_mail.assert_called_with(to_emails=to_email,
                                          from_email=from_email,
                                          subject=subject,
                                          html_content=html_content)
        self.mock_client.send.assert_called_with(mail_message)

    def test_send_message_exception(self) -> None:
        """Test that an error is logged when an exception is raised and it returns False"""
        self.mock_client.send.side_effect = Exception
        args = {
            'to_email': '*****@*****.**',
            'from_email': '*****@*****.**',
            'subject': 'Your monthly Recidiviz Report',
            'from_email_name': 'Recidiviz',
            'html_content': '<html></html>'
        }
        with self.assertLogs(level='ERROR'):
            response = self.wrapper.send_message(**args)
            self.assertFalse(response)

    def test_send_message_with_redirect_address(self) -> None:
        """Given a redirect_address, test that _create_message is called with the correct to_email and subject line."""
        self.mock_email.return_value = '<Recidiviz> [email protected]'
        self.mock_client.send.return_value = self.success_response
        redirect_address = '*****@*****.**'
        to_email = '*****@*****.**'
        subject = 'Your monthly Recidiviz Report'
        args = {
            'to_email': to_email,
            'from_email': '*****@*****.**',
            'subject': subject,
            'from_email_name': 'Recidiviz',
            'html_content': '<html></html>',
            'redirect_address': redirect_address
        }
        with self.assertLogs(level='INFO'):
            self.wrapper.send_message(**args)

        self.mock_mail.assert_called_with(to_emails=redirect_address,
                                          from_email='<Recidiviz> [email protected]',
                                          subject=f'[{to_email}] {subject}',
                                          html_content='<html></html>')