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'), )
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
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']) })
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 )
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
def _send_email_side_effect(email_address, *args, **kwargs): if email_address == "*****@*****.**": raise EmailError("foo") return {}