def send_email(self,
                   to_email_address,
                   template_name_or_id,
                   personalisation=None,
                   allow_resend=True,
                   reference=None,
                   reply_to_address_id=None):
        """
        Method to send an email using the Notify api.

        :param to_email_address: String email address for recipient
        :param template_name_or_id: Template accessible on the Notify account,
                                    can either be a key to the `templates` dictionary or a Notify template ID.
        :param personalisation: The template variables, dict
        :param allow_resend: if False instantiate the delivered reference cache and ensure we are not sending duplicates
        :param reply_to_address_id: String id of reply-to email address. Must be set up in Notify config before use
        :return: response from the api. For more information see https://github.com/alphagov/notifications-python-client
        """
        template_id = self.templates.get(template_name_or_id,
                                         template_name_or_id)
        reference = reference or self.get_reference(
            to_email_address, template_id, personalisation)

        if not allow_resend and self.has_been_sent(reference):
            self.logger.info(
                "Email with reference '{reference}' has already been sent",
                extra=dict(client=self.client.__class__,
                           to_email_address=hash_string(to_email_address),
                           template_name_or_id=template_name_or_id,
                           reference=reference,
                           reply_to_address_id=reply_to_address_id),
            )
            return

        # NOTE how the potential replacement of the email address happens *after* the has_been_sent check and
        # reference generation
        final_email_address = (
            self._redirect_domains_to_address
            and self._redirect_domains_to_address.get(
                # splitting at rightmost @ should reliably give us the domain
                to_email_address.rsplit("@", 1)
                [-1].lower())) or to_email_address

        try:
            with log_external_request(service='Notify'):
                response = self.client.send_email_notification(
                    final_email_address,
                    template_id,
                    personalisation=personalisation,
                    reference=reference,
                    email_reply_to_id=reply_to_address_id)

        except HTTPError as e:
            self._log_email_error_message(to_email_address,
                                          template_name_or_id, reference, e)
            raise EmailError(str(e))

        self._update_cache(reference)

        return response
    def test_should_be_an_error_if_send_email_fails_for_inactive_user(
            self, send_email, current_app):
        send_email.side_effect = EmailError(Exception('Notify API is down'))
        self.data_api_client.get_user.return_value = self.user(
            123,
            "*****@*****.**",
            1234,
            'email',
            'Name',
            active=False,
        )
        res = self.client.post('/user/reset-password',
                               data={'email_address': '*****@*****.**'})

        assert res.status_code == 503
        assert PASSWORD_RESET_EMAIL_ERROR in res.get_data(as_text=True)

        assert current_app.logger.error.call_args_list == [
            mock.call(
                '{code}: {email_type} email for email_hash {email_hash} failed to send. Error: {error}',
                extra={
                    'email_hash': self.expected_email_hash,
                    'error': 'Notify API is down',
                    'code': 'login.reset-email-inactive.notify-error',
                    'email_type': "Password reset (inactive user)"
                })
        ]
def test_notify_users_returns_false_on_error(send_email):
    send_email.side_effect = EmailError('Error')
    assert not notify_users(
        NOTIFY_API_KEY, 'preview', {
            'id':
            100,
            'title':
            'My brief title',
            'lotSlug':
            'lot-slug',
            'frameworkSlug':
            'framework-slug',
            'users': [
                {
                    'emailAddress': '*****@*****.**',
                    'active': True
                },
                {
                    'emailAddress': '*****@*****.**',
                    'active': False
                },
                {
                    'emailAddress': '*****@*****.**',
                    'active': True
                },
            ],
        })
    def test_should_log_an_error_and_redirect_if_change_password_email_sending_fails(
            self, send_email, current_app):
        self.login_as_supplier()
        send_email.side_effect = EmailError(Exception('Notify API is down'))

        response = self.client.post('/user/change-password',
                                    data={
                                        'old_password': '******',
                                        'password': '******',
                                        'confirm_password': '******'
                                    })

        assert response.status_code == 302
        assert response.location == 'http://localhost/suppliers'
        self.assert_flashes(reset_password.PASSWORD_UPDATED_MESSAGE, "success")

        assert current_app.logger.error.call_args_list == [
            mock.call(
                '{code}: {email_type} email for email_hash {email_hash} failed to send. Error: {error}',
                extra={
                    'email_hash':
                    '8yc90Y2VvBnVHT5jVuSmeebxOCRJcnKicOe7VAsKu50=',
                    'error': 'Notify API is down',
                    'code': 'login.password-change-alert-email.notify-error',
                    'email_type': "Password change alert"
                })
        ]

        # the email failure shouldn't have prevented the password from being changed though
        self.data_api_client.update_user_password.assert_called_once_with(
            123,
            'o987654321',
            updater=self._user.get('email'),
        )
Пример #5
0
    def test_notify_suppliers_whether_application_made_email_error_logs_supplier_id(
            self):
        expected_error_count = 1
        self.notify_client.send_email.side_effect = [
            EmailError("Arghhh!"),  # The first user email fails
            None,
            None
        ]

        assert notify_suppliers_whether_application_made(
            self.data_api_client, self.notify_client, 'g-cloud-12',
            self.logger) == expected_error_count

        assert self.logger.info.call_args_list == [
            mock.call("Supplier '712345'"),
            mock.call(
                "Sending 'application_not_made' email to supplier '712345' "
                "user 's2qDcB8cMZHhlyLW-QJ0vBtVAf5p6_MzE-RA_ksP4hA='"),
            mock.call("Supplier '712346'"),
            mock.call("Sending 'application_made' email to supplier '712346' "
                      "user 'KzsrnOhCq4tqbGFMsflgS7ig1QLRr0nFJrcrEIlOlbU='"),
            mock.call("Sending 'application_made' email to supplier '712346' "
                      "user 'iYYo4oiQ-Te98Ak5He9Ch5xAGkvPG1_STnONn12oy7s='")
        ]
        assert self.logger.error.call_args_list == [
            mock.call(
                "Error sending email to supplier '712345' user 's2qDcB8cMZHhlyLW-QJ0vBtVAf5p6_MzE-RA_ksP4hA=': Arghhh!"
            )
        ]
    def send_email(self,
                   email_address,
                   template_id,
                   personalisation=None,
                   allow_resend=True,
                   reference=None):
        """
        Method to send an email using the Notify api.

        :param email_address: String email address for recipient
        :param template_id: Template accessible on the Notify account whose  api_key you instantiated the class with
        :param personalisation: The template variables, dict
        :param allow_resend: if False instantiate the delivered reference cache and ensure we are not sending duplicates
        :return: response from the api. For more information see https://github.com/alphagov/notifications-python-client
        """
        reference = reference or self.get_reference(email_address, template_id,
                                                    personalisation)
        if not allow_resend and self.has_been_sent(reference):
            self.logger.info(
                "Email {reference} (template {template_id}) has already been sent to {email_address} through Notify",
                extra=dict(
                    email_address=hash_string(email_address),
                    template_id=template_id,
                    reference=reference,
                ),
            )
            return
        try:
            response = self.client.send_email_notification(
                email_address,
                template_id,
                personalisation=personalisation,
                reference=reference,
            )
        except HTTPError as e:
            self.logger.error(
                self.get_error_message(hash_string(email_address), e))
            raise EmailError(str(e))
        self._update_cache(reference)
        self.logger.info(
            "Sent email {reference} to {email_address} (id: {notify_id}, template: {template_id}) through Notify",
            extra=dict(
                email_address=hash_string(email_address),
                notify_id=response['id'],
                template_id=template_id,
                reference=reference,
            ),
        )
        return response
    def test_should_be_an_error_if_send_invitation_email_fails(
            self, DMNotifyClient):
        notify_client_mock = mock.Mock()
        notify_client_mock.send_email.side_effect = EmailError()
        DMNotifyClient.return_value = notify_client_mock

        self.login()

        res = self.client.post('/suppliers/invite-user',
                               data={
                                   'email_address': '*****@*****.**',
                                   'name': 'valid'
                               })

        assert res.status_code == 503
Пример #8
0
def send_email(to_email_addresses,
               email_body,
               api_key,
               subject,
               from_email,
               from_name,
               tags,
               reply_to=None,
               metadata=None,
               logger=None):
    logger = logger or current_app.logger

    if isinstance(to_email_addresses, string_types):
        to_email_addresses = [to_email_addresses]

    try:
        mandrill_client = Mandrill(api_key)

        message = {
            'html':
            email_body,
            'subject':
            subject,
            'from_email':
            from_email,
            'from_name':
            from_name,
            'to': [{
                'email': email_address,
                'type': 'to'
            } for email_address in to_email_addresses],
            'important':
            False,
            'track_opens':
            False,
            'track_clicks':
            False,
            'auto_text':
            True,
            'tags':
            tags,
            'metadata':
            metadata,
            'headers': {
                'Reply-To': reply_to or from_email
            },
            'preserve_recipients':
            False,
            'recipient_metadata': [{
                'rcpt': email_address
            } for email_address in to_email_addresses]
        }

        result = mandrill_client.messages.send(message=message, async=True)
    except Error as e:
        # Mandrill errors are thrown as exceptions
        logger.error("Failed to send an email: {error}", extra={'error': e})
        raise EmailError(e)
    logger.info("Sent {tags} response: id={id}, email={email_hash}",
                extra={
                    'tags': tags,
                    'id': result[0]['_id'],
                    'email_hash': hash_string(result[0]['email'])
                })
Пример #9
0
def test_upload_counterpart_file_sends_correct_emails(
    notify_raise_email_error,
    notify_fail_early,
    find_users_iterable,
    expected_send_email_emails,
):
    bucket = mock.Mock()
    data_api_client = mock.Mock()
    data_api_client.get_supplier_framework_info.return_value = {
        "frameworkInterest": {
            "agreementId": 23,
            "declaration": {
                "supplierRegisteredName": "The supplier who signed",
                "primaryContactEmail": "*****@*****.**",
            },
        },
    }
    data_api_client.find_users_iter.side_effect = lambda *args, **kwargs: iter(find_users_iterable)
    dm_notify_client = mock.Mock()
    if notify_raise_email_error:
        dm_notify_client.send_email.side_effect = EmailError("Forgot the stamp")

    with mock.patch.object(builtins, 'open', mock.mock_open(read_data='foo')):
        with (pytest.raises(EmailError) if notify_raise_email_error else _empty_context_manager()):
            upload_counterpart_file(
                bucket,
                {
                    "frameworkLiveAtUTC": '2019-07-02T11:00:00.603808Z',
                    "name": "Dos Two",
                    "slug": "digital-outcomes-and-specialists-2",
                },
                'pdfs/123456-file.pdf',
                False,
                data_api_client,
                "Framework Agreement",
                dm_notify_client=dm_notify_client,
                notify_template_id="dead-beef-baad-f00d",
                notify_fail_early=notify_fail_early,
            )

        assert bucket.save.called is True
        assert data_api_client.update_framework_agreement.called is True
        data_api_client.find_users_iter.assert_called_with(supplier_id=123456)

        expected_personalisation = {
            "framework_slug": "digital-outcomes-and-specialists-2",
            "framework_name": "Dos Two",
            "supplier_name": "The supplier who signed",
            "contract_title": "Framework Agreement",
            "frameworkLiveAt_dateformat": "2 July 2019"
        }

        if notify_raise_email_error and notify_fail_early:
            # we don't want to dictate anything about the order emails are tried in so we can't know which one it will
            # have tried first - this is probably the only useful thing we can assert
            assert len(dm_notify_client.send_email.call_args_list) == 1
        else:
            assert sorted(dm_notify_client.send_email.call_args_list) == sorted(
                (
                    (email, "dead-beef-baad-f00d", expected_personalisation),
                    {"allow_resend": True},
                ) for email in expected_send_email_emails
            )
Пример #10
0
def upload_counterpart_file(
    bucket,
    framework,
    file_path,
    dry_run,
    data_api_client,
    dm_notify_client=None,
    notify_template_id=None,
    notify_fail_early=True,
    logger=None,
):
    if bool(dm_notify_client) != bool(notify_template_id):
        raise TypeError(
            "Either specify both dm_notify_client and notify_template_id or neither"
        )

    logger = logger or logging_helpers.getLogger()

    supplier_id = get_supplier_id_from_framework_file_path(file_path)
    supplier_framework = data_api_client.get_supplier_framework_info(
        supplier_id, framework["slug"])
    supplier_framework = supplier_framework['frameworkInterest']
    supplier_name = (
        supplier_framework['declaration']['supplierRegisteredName']
        if 'supplierRegisteredName' in supplier_framework['declaration'] else
        supplier_framework['declaration']['nameOfOrganisation'])
    download_filename = generate_download_filename(supplier_id,
                                                   COUNTERPART_FILENAME,
                                                   supplier_name)

    email_addresses_to_notify = dm_notify_client and frozenset(
        chain(
            (supplier_framework["declaration"]["primaryContactEmail"], ),
            (user["emailAddress"] for user in data_api_client.find_users_iter(
                supplier_id=int(supplier_id)) if user["active"]),
        ))

    upload_path = generate_timestamped_document_upload_path(
        framework["slug"], supplier_id, "agreements", COUNTERPART_FILENAME)
    try:
        if not dry_run:
            # Upload file - need to open in binary mode as it's not plain text
            with open(file_path, 'rb') as source_file:
                bucket.save(upload_path,
                            source_file,
                            acl='bucket-owner-full-control',
                            download_filename=download_filename)
                logger.info("UPLOADED: '{}' to '{}'".format(
                    file_path, upload_path))

            # Save filepath to framework agreement
            data_api_client.update_framework_agreement(
                supplier_framework['agreementId'],
                {"countersignedAgreementPath": upload_path},
                'upload-counterpart-agreements script run by {}'.format(
                    getpass.getuser()))
            logger.info(
                "countersignedAgreementPath='{}' for agreement ID {}".format(
                    upload_path, supplier_framework['agreementId']))
        else:
            logger.info("[Dry-run] UPLOAD: '{}' to '{}'".format(
                file_path, upload_path))
            logger.info(
                "[Dry-run] countersignedAgreementPath='{}' for agreement ID {}"
                .format(upload_path, supplier_framework['agreementId']))

        failed_send_email_calls = 0
        for notify_email in (email_addresses_to_notify or ()):
            try:
                if not dry_run:
                    dm_notify_client.send_email(
                        notify_email,
                        notify_template_id, {
                            "framework_slug": framework["slug"],
                            "framework_name": framework["name"],
                            "supplier_name": supplier_name,
                        },
                        allow_resend=True)
                    logger.debug(
                        f"NOTIFY: sent email to supplier '{supplier_id}' user {hash_string(notify_email)}"
                    )
                else:
                    logger.info(
                        f"[Dry-run] Send notify email to supplier '{supplier_id}' user {hash_string(notify_email)}"
                    )
            except EmailError as e:
                logger.error(
                    f"NOTIFY: Error sending email to supplier '{supplier_id}' user {hash_string(notify_email)}"
                )

                if isinstance(e, EmailTemplateError):
                    raise  # do not try to continue

                if notify_fail_early:
                    raise

                else:
                    failed_send_email_calls += 1

    # just catching these exceptions for logging then reraising
    except (OSError, IOError) as e:
        logger.error("Error reading file '{}': {}".format(
            file_path, e.message))
        raise
    except S3ResponseError as e:
        logger.error("Error uploading '{}' to '{}': {}".format(
            file_path, upload_path, e.message))
        raise
    except APIError as e:
        logger.error(
            "API error setting upload path '{}' on agreement ID {}: {}".format(
                upload_path, supplier_framework['agreementId'], e.message))
        raise

    if failed_send_email_calls:
        raise EmailError("{} notify send_emails calls failed".format(
            failed_send_email_calls))
    def send_email(self,
                   to_email_address,
                   template_name_or_id,
                   personalisation=None,
                   allow_resend=True,
                   reference=None,
                   reply_to_address_id=None,
                   use_recent_cache=True):
        """
        Method to send an email using the Notify api.

        :param to_email_address: String email address for recipient
        :param template_name_or_id: Template accessible on the Notify account,
                                    can either be a key to the `templates` dictionary or a Notify template ID.
        :param personalisation: The template variables, dict
        :param allow_resend: if False instantiate the delivered reference cache and ensure we are not sending duplicates
        :param reply_to_address_id: String id of reply-to email address. Must be set up in Notify config before use
        :param use_recent_cache: Use the client's cache of recently sent references. If set to False, any
                                 has_been_sent() calls will check the reference in the Notify API directly
        :return: response from the api. For more information see https://github.com/alphagov/notifications-python-client
        """
        template_id = self.templates.get(template_name_or_id,
                                         template_name_or_id)
        reference = reference or self.get_reference(
            to_email_address, template_id, personalisation)
        email_obj = DMNotifyEmail(to_email_address, template_name_or_id,
                                  reference, personalisation)

        if not allow_resend and self.has_been_sent(
                reference, use_recent_cache=use_recent_cache):
            self._log(
                logging.WARNING,
                "Email with reference '{reference}' has already been sent",
                email_obj,
            )
            return

        # NOTE how the potential replacement of the email address happens *after* the has_been_sent check and
        # reference generation
        final_email_address = (
            self._redirect_domains_to_address
            and self._redirect_domains_to_address.get(
                # splitting at rightmost @ should reliably give us the domain
                to_email_address.rsplit("@", 1)
                [-1].lower())) or to_email_address

        try:
            with log_external_request(service='Notify'):
                response = self.client.send_email_notification(
                    final_email_address,
                    template_id,
                    personalisation=personalisation,
                    reference=reference,
                    email_reply_to_id=reply_to_address_id)

        except HTTPError as e:
            self._log_email_error_message(email_obj, e)
            if isinstance(e.message, list) and \
                    any(msg["message"].startswith("Missing personalisation") for msg in e.message):
                raise EmailTemplateError(str(e))
            raise EmailError(str(e))

        self._log(
            logging.INFO,
            f"Email with reference '{reference}' sent to Notify successfully",
            email_obj)

        self._update_cache(reference)

        return response
Пример #12
0
 def _send_email_side_effect(email_address, *args, **kwargs):
     if email_address == "*****@*****.**":
         raise EmailError("foo")
     return {}