diff --git a/app/clients/__init__.py b/app/clients/__init__.py index 1f7743786..89a97dab9 100644 --- a/app/clients/__init__.py +++ b/app/clients/__init__.py @@ -13,6 +13,11 @@ class Client(object): pass +STATISTICS_REQUESTED = 'requested' +STATISTICS_DELIVERED = 'delivered' +STATISTICS_FAILURE = 'failure' + + class ClientResponse: def __init__(self): self.__response_model__ = None @@ -24,7 +29,6 @@ class ClientResponse: return self.response_code_to_object(response_code)['message'] def response_code_to_notification_status(self, response_code): - print(response_code) return self.response_code_to_object(response_code)['notification_status'] def response_code_to_notification_statistics_status(self, response_code): diff --git a/app/clients/email/aws_ses.py b/app/clients/email/aws_ses.py index 6eb4e7b26..fa38da961 100644 --- a/app/clients/email/aws_ses.py +++ b/app/clients/email/aws_ses.py @@ -1,7 +1,7 @@ import boto3 from flask import current_app from monotonic import monotonic -from app.clients import ClientResponse +from app.clients import ClientResponse, STATISTICS_DELIVERED, STATISTICS_FAILURE from app.clients.email import (EmailClientException, EmailClient) @@ -13,19 +13,19 @@ class AwsSesResponses(ClientResponse): "message": 'Bounced', "success": False, "notification_status": 'bounce', - "notification_statistics_status": 'failed' + "notification_statistics_status": STATISTICS_FAILURE }, 'Delivery': { "message": 'Delivered', "success": True, "notification_status": 'delivered', - "notification_statistics_status": 'delivered' + "notification_statistics_status": STATISTICS_DELIVERED }, 'Complaint': { "message": 'Complaint', "success": False, "notification_status": 'complaint', - "notification_statistics_status": 'failed' + "notification_statistics_status": STATISTICS_FAILURE } } diff --git a/app/clients/sms/firetext.py b/app/clients/sms/firetext.py index c50e4b079..87e0119a6 100644 --- a/app/clients/sms/firetext.py +++ b/app/clients/sms/firetext.py @@ -6,7 +6,7 @@ from app.clients.sms import ( ) from flask import current_app from requests import request, RequestException, HTTPError -from app.clients import ClientResponse +from app.clients import ClientResponse, STATISTICS_DELIVERED, STATISTICS_FAILURE logger = logging.getLogger(__name__) @@ -17,14 +17,14 @@ class FiretextResponses(ClientResponse): self.__response_model__ = { '0': { "message": 'Delivered', - "notification_statistics_status": 'delivered', + "notification_statistics_status": STATISTICS_DELIVERED, "success": True, "notification_status": 'delivered' }, '1': { "message": 'Declined', "success": False, - "notification_statistics_status": 'failed', + "notification_statistics_status": STATISTICS_FAILURE, "notification_status": 'failed' }, '2': { diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index f2ef227ac..f19d05dd0 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -3,7 +3,8 @@ from app import db from app.models import Notification, Job, NotificationStatistics, TEMPLATE_TYPE_SMS, TEMPLATE_TYPE_EMAIL from sqlalchemy import desc from datetime import datetime, timedelta -from app.clients.sms.firetext import FiretextResponses +from app.clients import (STATISTICS_FAILURE, STATISTICS_DELIVERED, STATISTICS_REQUESTED) + def dao_get_notification_statistics_for_service(service_id): return NotificationStatistics.query.filter_by( @@ -49,18 +50,16 @@ def dao_create_notification(notification, notification_type): def update_query(notification_type, status): - print(notification_type) - print(status) mapping = { - 'sms': { - 'requested': NotificationStatistics.sms_requested, - 'success': NotificationStatistics.sms_delivered, - 'failure': NotificationStatistics.sms_error + TEMPLATE_TYPE_SMS: { + STATISTICS_REQUESTED: NotificationStatistics.sms_requested, + STATISTICS_DELIVERED: NotificationStatistics.sms_delivered, + STATISTICS_FAILURE: NotificationStatistics.sms_error }, - 'email': { - 'requested': NotificationStatistics.emails_requested, - 'success': NotificationStatistics.emails_delivered, - 'failure': NotificationStatistics.emails_error + TEMPLATE_TYPE_EMAIL: { + STATISTICS_REQUESTED: NotificationStatistics.emails_requested, + STATISTICS_DELIVERED: NotificationStatistics.emails_delivered, + STATISTICS_FAILURE: NotificationStatistics.emails_error } } return { @@ -74,29 +73,46 @@ def dao_update_notification(notification): db.session.commit() -def update_notification_status_by_id(notification_id, status): +def update_notification_status_by_id(notification_id, status, notification_statistics_status): count = db.session.query(Notification).filter_by( id=notification_id ).update({ Notification.status: status }) - if count == 1: + + if count == 1 and notification_statistics_status: notification = Notification.query.get(notification_id) + db.session.query(NotificationStatistics).filter_by( day=notification.created_at.strftime('%Y-%m-%d'), service_id=notification.service_id - ).update(update_query(notification.template.template_type, FiretextResponses.response_code_to_notify_stats(status))) + ).update( + update_query(notification.template.template_type, notification_statistics_status) + ) db.session.commit() return count -def update_notification_status_by_reference(reference, status): +def update_notification_status_by_reference(reference, status, notification_statistics_status): count = db.session.query(Notification).filter_by( reference=reference ).update({ Notification.status: status }) + + if count == 1: + notification = Notification.query.filter_by( + reference=reference + ).first() + + db.session.query(NotificationStatistics).filter_by( + day=notification.created_at.strftime('%Y-%m-%d'), + service_id=notification.service_id + ).update( + update_query(notification.template.template_type, notification_statistics_status) + ) + db.session.commit() return count diff --git a/app/notifications/rest.py b/app/notifications/rest.py index 60ea93eff..100209cc7 100644 --- a/app/notifications/rest.py +++ b/app/notifications/rest.py @@ -11,8 +11,8 @@ from flask import ( ) from utils.template import Template -from app.clients.sms.firetext import firetext_response_status -from app.clients.email.aws_ses import ses_response_status +from app.clients.sms.firetext import FiretextResponses +from app.clients.email.aws_ses import AwsSesResponses from app import api_user, encryption, create_uuid, DATETIME_FORMAT, DATE_FORMAT from app.authentication.auth import require_admin from app.dao import ( @@ -34,6 +34,9 @@ from app.errors import register_errors register_errors(notifications) +aws_response = AwsSesResponses() +firetext_response = FiretextResponses() + @notifications.route('/notifications/email/ses', methods=['POST']) def process_ses_response(): @@ -57,35 +60,54 @@ def process_ses_response(): result="error", message="SES callback failed: notificationType missing" ), 400 - status = ses_response_status.get(ses_message['notificationType'], None) - if not status: + try: + aws_response.response_code_to_object(ses_message['notificationType']) + except KeyError: current_app.logger.info( - "SES callback failed: status {} not found.".format(status) + "SES callback failed: status {} not found.".format(ses_message['notificationType']) ) return jsonify( result="error", message="SES callback failed: status {} not found".format(ses_message['notificationType']) ), 400 + notification_status = aws_response.response_code_to_notification_status(ses_message['notificationType']) + notification_statistics_status = aws_response.response_code_to_notification_statistics_status( + ses_message['notificationType'] + ) + try: source = ses_message['mail']['source'] if is_not_a_notification(source): current_app.logger.info( - "SES callback for notify success:. source {} status {}".format(source, status['notify_status']) + "SES callback for notify success:. source {} status {}".format(source, notification_status) ) return jsonify( result="success", message="SES callback succeeded" ), 200 reference = ses_message['mail']['messageId'] - if notifications_dao.update_notification_status_by_reference(reference, status['notify_status']) == 0: + if notifications_dao.update_notification_status_by_reference( + reference, + notification_status, + notification_statistics_status + ) == 0: current_app.logger.info( - "SES callback failed: notification not found. Status {}".format(status['notify_status']) + "SES callback failed: notification not found. Status {}".format(notification_status) ) return jsonify( result="error", - message="SES callback failed: notification not found. Status {}".format(status['notify_status']) + message="SES callback failed: notification not found. Status {}".format(notification_status) ), 404 + + if not aws_response.response_code_to_notification_success(ses_message['notificationType']): + current_app.logger.info( + "SES delivery failed: notification {} has error found. Status {}".format( + reference, + aws_response.response_code_to_message(ses_message['notificationType']) + ) + ) + return jsonify( result="success", message="SES callback succeeded" ), 200 @@ -149,14 +171,22 @@ def process_firetext_response(): result="error", message="Firetext callback with invalid reference {}".format(reference) ), 400 - notification_status = firetext_response_status.get(status, None) - if not notification_status: + try: + firetext_response.response_code_to_object(status) + except KeyError: current_app.logger.info( "Firetext callback failed: status {} not found.".format(status) ) return jsonify(result="error", message="Firetext callback failed: status {} not found.".format(status)), 400 - if notifications_dao.update_notification_status_by_id(reference, notification_status['notify_status']) == 0: + notification_status = firetext_response.response_code_to_notification_status(status) + notification_statistics_status = firetext_response.response_code_to_notification_statistics_status(status) + + if notifications_dao.update_notification_status_by_id( + reference, + notification_status, + notification_statistics_status + ) == 0: current_app.logger.info( "Firetext callback failed: notification {} not found. Status {}".format(reference, status) ) @@ -164,15 +194,15 @@ def process_firetext_response(): result="error", message="Firetext callback failed: notification {} not found. Status {}".format( reference, - notification_status['firetext_message'] + firetext_response.response_code_to_message(status) ) ), 404 - if not notification_status['success']: + if not firetext_response.response_code_to_notification_success(status): current_app.logger.info( "Firetext delivery failed: notification {} has error found. Status {}".format( reference, - firetext_response_status[status]['firetext_message'] + FiretextResponses().response_code_to_message(status) ) ) return jsonify( diff --git a/tests/app/clients/test_aws_ses.py b/tests/app/clients/test_aws_ses.py index da527dd3e..452dd4960 100644 --- a/tests/app/clients/test_aws_ses.py +++ b/tests/app/clients/test_aws_ses.py @@ -15,14 +15,14 @@ def test_should_return_correct_details_for_delivery(): def test_should_return_correct_details_for_bounced(): assert aws_responses.response_code_to_message('Bounce') == 'Bounced' assert aws_responses.response_code_to_notification_status('Bounce') == 'bounce' - assert aws_responses.response_code_to_notification_statistics_status('Bounce') == 'failed' + assert aws_responses.response_code_to_notification_statistics_status('Bounce') == 'failure' assert not aws_responses.response_code_to_notification_success('Bounce') def test_should_return_correct_details_for_complaint(): assert aws_responses.response_code_to_message('Complaint') == 'Complaint' assert aws_responses.response_code_to_notification_status('Complaint') == 'complaint' - assert aws_responses.response_code_to_notification_statistics_status('Complaint') == 'failed' + assert aws_responses.response_code_to_notification_statistics_status('Complaint') == 'failure' assert not aws_responses.response_code_to_notification_success('Complaint') diff --git a/tests/app/clients/test_firetext.py b/tests/app/clients/test_firetext.py index f38e4ec4e..b68837a44 100644 --- a/tests/app/clients/test_firetext.py +++ b/tests/app/clients/test_firetext.py @@ -15,7 +15,7 @@ def test_should_return_correct_details_for_delivery(): def test_should_return_correct_details_for_bounced(): assert responses.response_code_to_message('1') == 'Declined' assert responses.response_code_to_notification_status('1') == 'failed' - assert responses.response_code_to_notification_statistics_status('1') == 'failed' + assert responses.response_code_to_notification_statistics_status('1') == 'failure' assert not responses.response_code_to_notification_success('1') diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 63bad1b8f..b4eddbb78 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -221,7 +221,6 @@ def sample_job(notify_db, } job = Job(**data) dao_create_job(job) - print(job.created_at) return job diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index e859fdb76..3fe16c3b9 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -32,26 +32,98 @@ def test_should_by_able_to_update_reference_by_id(sample_notification): assert Notification.query.get(sample_notification.id).reference == 'reference' -def test_should_by_able_to_update_status_by_reference(sample_notification): - assert Notification.query.get(sample_notification.id).status == "sent" - update_notification_reference_by_id(sample_notification.id, 'reference') - update_notification_status_by_reference('reference', 'delivered') - assert Notification.query.get(sample_notification.id).status == 'delivered' +def test_should_by_able_to_update_status_by_reference(sample_email_template): + data = { + 'to': '+44709123456', + 'service': sample_email_template.service, + 'service_id': sample_email_template.service.id, + 'template': sample_email_template, + 'created_at': datetime.utcnow() + } + + notification = Notification(**data) + dao_create_notification(notification, sample_email_template.template_type) + + assert Notification.query.get(notification.id).status == "sent" + update_notification_reference_by_id(notification.id, 'reference') + update_notification_status_by_reference('reference', 'delivered', 'delivered') + assert Notification.query.get(notification.id).status == 'delivered' + assert NotificationStatistics.query.filter_by( + service_id=sample_email_template.service.id + ).one().emails_delivered == 1 + assert NotificationStatistics.query.filter_by( + service_id=sample_email_template.service.id + ).one().emails_requested == 1 + assert NotificationStatistics.query.filter_by( + service_id=sample_email_template.service.id + ).one().emails_error == 0 def test_should_by_able_to_update_status_by_id(sample_notification): assert Notification.query.get(sample_notification.id).status == 'sent' - count = update_notification_status_by_id(sample_notification.id, 'delivered') + count = update_notification_status_by_id(sample_notification.id, 'delivered', 'delivered') assert count == 1 assert Notification.query.get(sample_notification.id).status == 'delivered' + assert NotificationStatistics.query.filter_by( + service_id=sample_notification.service.id + ).one().sms_delivered == 1 + assert NotificationStatistics.query.filter_by( + service_id=sample_notification.service.id + ).one().sms_requested == 1 + assert NotificationStatistics.query.filter_by( + service_id=sample_notification.service.id + ).one().sms_error == 0 + + +def test_should_be_able_to_record_statistics_failure_for_sms(sample_notification): + assert Notification.query.get(sample_notification.id).status == 'sent' + count = update_notification_status_by_id(sample_notification.id, 'delivered', 'failure') + assert count == 1 + assert Notification.query.get(sample_notification.id).status == 'delivered' + assert NotificationStatistics.query.filter_by( + service_id=sample_notification.service.id + ).one().sms_delivered == 0 + assert NotificationStatistics.query.filter_by( + service_id=sample_notification.service.id + ).one().sms_requested == 1 + assert NotificationStatistics.query.filter_by( + service_id=sample_notification.service.id + ).one().sms_error == 1 + + +def test_should_be_able_to_record_statistics_failure_for_email(sample_email_template): + data = { + 'to': '+44709123456', + 'service': sample_email_template.service, + 'service_id': sample_email_template.service.id, + 'template': sample_email_template, + 'created_at': datetime.utcnow() + } + + notification = Notification(**data) + dao_create_notification(notification, sample_email_template.template_type) + + update_notification_reference_by_id(notification.id, 'reference') + count = update_notification_status_by_reference('reference', 'bounce', 'failure') + assert count == 1 + assert Notification.query.get(notification.id).status == 'bounce' + assert NotificationStatistics.query.filter_by( + service_id=notification.service.id + ).one().emails_delivered == 0 + assert NotificationStatistics.query.filter_by( + service_id=notification.service.id + ).one().emails_requested == 1 + assert NotificationStatistics.query.filter_by( + service_id=notification.service.id + ).one().emails_error == 1 def test_should_return_zero_count_if_no_notification_with_id(): - assert update_notification_status_by_id(str(uuid.uuid4()), 'delivered') == 0 + assert update_notification_status_by_id(str(uuid.uuid4()), 'delivered', 'delivered') == 0 def test_should_return_zero_count_if_no_notification_with_reference(): - assert update_notification_status_by_reference('something', 'delivered') == 0 + assert update_notification_status_by_reference('something', 'delivered', 'delivered') == 0 def test_should_be_able_to_get_statistics_for_a_service(sample_template): diff --git a/tests/app/notifications/test_rest.py b/tests/app/notifications/test_rest.py index 9bc9ada62..536820129 100644 --- a/tests/app/notifications/test_rest.py +++ b/tests/app/notifications/test_rest.py @@ -7,7 +7,7 @@ from flask import json from app.models import Service from app.dao.templates_dao import dao_get_all_templates_for_service from app.dao.services_dao import dao_update_service -from app.dao.notifications_dao import get_notification_by_id +from app.dao.notifications_dao import get_notification_by_id, dao_get_notification_statistics_for_service from freezegun import freeze_time @@ -982,7 +982,7 @@ def test_firetext_callback_should_return_404_if_cannot_find_notification_id(noti assert json_resp['result'] == 'error' assert json_resp['message'] == 'Firetext callback failed: notification {} not found. Status {}'.format( missing_notification_id, - 'delivered' + 'Delivered' ) @@ -1007,6 +1007,10 @@ def test_firetext_callback_should_update_notification_status(notify_api, sample_ ) updated = get_notification_by_id(sample_notification.id) assert updated.status == 'delivered' + assert get_notification_by_id(sample_notification.id).status == 'delivered' + assert dao_get_notification_statistics_for_service(sample_notification.service_id)[0].sms_delivered == 1 + assert dao_get_notification_statistics_for_service(sample_notification.service_id)[0].sms_requested == 1 + assert dao_get_notification_statistics_for_service(sample_notification.service_id)[0].sms_error == 0 def test_firetext_callback_should_update_notification_status_failed(notify_api, sample_notification): @@ -1030,6 +1034,9 @@ def test_firetext_callback_should_update_notification_status_failed(notify_api, ) updated = get_notification_by_id(sample_notification.id) assert updated.status == 'failed' + assert dao_get_notification_statistics_for_service(sample_notification.service_id)[0].sms_delivered == 0 + assert dao_get_notification_statistics_for_service(sample_notification.service_id)[0].sms_requested == 1 + assert dao_get_notification_statistics_for_service(sample_notification.service_id)[0].sms_error == 1 def test_firetext_callback_should_update_notification_status_sent(notify_api, notify_db, notify_db_session): @@ -1054,6 +1061,42 @@ def test_firetext_callback_should_update_notification_status_sent(notify_api, no ) updated = get_notification_by_id(notification.id) assert updated.status == 'sent' + assert dao_get_notification_statistics_for_service(notification.service_id)[0].sms_delivered == 0 + assert dao_get_notification_statistics_for_service(notification.service_id)[0].sms_requested == 1 + assert dao_get_notification_statistics_for_service(notification.service_id)[0].sms_error == 0 + + +def test_firetext_callback_should_update_multiple_notification_status_sent(notify_api, notify_db, notify_db_session): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + notification1 = sample_notification(notify_db, notify_db_session, status='delivered') + notification2 = sample_notification(notify_db, notify_db_session, status='delivered') + notification3 = sample_notification(notify_db, notify_db_session, status='delivered') + + client.post( + path='/notifications/sms/firetext', + data='mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference={}'.format( + notification1.id + ), + headers=[('Content-Type', 'application/x-www-form-urlencoded')]) + + client.post( + path='/notifications/sms/firetext', + data='mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference={}'.format( + notification2.id + ), + headers=[('Content-Type', 'application/x-www-form-urlencoded')]) + + client.post( + path='/notifications/sms/firetext', + data='mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference={}'.format( + notification3.id + ), + headers=[('Content-Type', 'application/x-www-form-urlencoded')]) + + assert dao_get_notification_statistics_for_service(notification1.service_id)[0].sms_delivered == 3 + assert dao_get_notification_statistics_for_service(notification1.service_id)[0].sms_requested == 3 + assert dao_get_notification_statistics_for_service(notification1.service_id)[0].sms_error == 0 def test_ses_callback_should_not_need_auth(notify_api): @@ -1126,11 +1169,20 @@ def test_ses_callback_should_fail_if_notification_cannot_be_found(notify_db, not assert json_resp['message'] == 'SES callback failed: notification not found. Status delivered' -def test_ses_callback_should_update_notification_status(notify_api, notify_db, notify_db_session): +def test_ses_callback_should_update_notification_status( + notify_api, + notify_db, + notify_db_session, + sample_email_template): with notify_api.test_request_context(): with notify_api.test_client() as client: - notification = sample_notification(notify_db, notify_db_session, reference='ref') + notification = sample_notification( + notify_db, + notify_db_session, + template=sample_email_template, + reference='ref' + ) assert get_notification_by_id(notification.id).status == 'sent' @@ -1144,6 +1196,64 @@ def test_ses_callback_should_update_notification_status(notify_api, notify_db, n assert json_resp['result'] == 'success' assert json_resp['message'] == 'SES callback succeeded' assert get_notification_by_id(notification.id).status == 'delivered' + assert dao_get_notification_statistics_for_service(notification.service_id)[0].emails_delivered == 1 + assert dao_get_notification_statistics_for_service(notification.service_id)[0].emails_requested == 1 + assert dao_get_notification_statistics_for_service(notification.service_id)[0].emails_error == 0 + + +def test_ses_callback_should_update_multiple_notification_status_sent( + notify_api, + notify_db, + notify_db_session, + sample_email_template): + with notify_api.test_request_context(): + with notify_api.test_client() as client: + + notification1 = sample_notification( + notify_db, + notify_db_session, + template=sample_email_template, + reference='ref1', + status='delivered') + + notification2 = sample_notification( + notify_db, + notify_db_session, + template=sample_email_template, + reference='ref2', + status='delivered') + + notification3 = sample_notification( + notify_db, + notify_db_session, + template=sample_email_template, + reference='ref2', + status='delivered') + + client.post( + path='/notifications/sms/firetext', + data='mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference={}'.format( + notification1.id + ), + headers=[('Content-Type', 'application/x-www-form-urlencoded')]) + + client.post( + path='/notifications/sms/firetext', + data='mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference={}'.format( + notification2.id + ), + headers=[('Content-Type', 'application/x-www-form-urlencoded')]) + + client.post( + path='/notifications/sms/firetext', + data='mobile=441234123123&status=0&time=2016-03-10 14:17:00&reference={}'.format( + notification3.id + ), + headers=[('Content-Type', 'application/x-www-form-urlencoded')]) + + assert dao_get_notification_statistics_for_service(notification1.service_id)[0].emails_delivered == 3 + assert dao_get_notification_statistics_for_service(notification1.service_id)[0].emails_requested == 3 + assert dao_get_notification_statistics_for_service(notification1.service_id)[0].emails_error == 0 def test_should_handle_invite_email_callbacks(notify_api, notify_db, notify_db_session): @@ -1203,3 +1313,7 @@ def ses_missing_notification_id_callback(): def ses_invalid_notification_type_callback(): return b'{\n "Type" : "Notification",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{\\"notificationType\\":\\"Unknown\\",\\"mail\\":{\\"timestamp\\":\\"2016-03-14T12:35:25.909Z\\",\\"source\\":\\"test@test-domain.com\\",\\"sourceArn\\":\\"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify\\",\\"sendingAccountId\\":\\"123456789012\\",\\"destination\\":[\\"testing@digital.cabinet-office.gov.uk\\"]},\\"delivery\\":{\\"timestamp\\":\\"2016-03-14T12:35:26.567Z\\",\\"processingTimeMillis\\":658,\\"recipients\\":[\\"testing@digital.cabinet-office.gov.uk\\"],\\"smtpResponse\\":\\"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp\\",\\"reportingMTA\\":\\"a6-238.smtp-out.eu-west-1.amazonses.com\\"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}' # noqa + + + +{\n "Type" : "Notification",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{"notificationType{\n "Type" : "Notification",\n "TopicArn" : "arn:aws:sns:eu-west-1:123456789012:testing",\n "Message" : "{"notificationType":"Unknown","mail":{"timestamp":"2016-03-14T12:35:25.909Z","source":"test@test-domain.com","sourceArn":"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify","sendingAccountId":"123456789012","destination":["testing@digital.cabinet-office.gov.uk"]},"delivery":{"timestamp":"2016-03-14T12:35:26.567Z","processingTimeMillis":658,"recipients":["testing@digital.cabinet-office.gov.uk"],"smtpResponse":"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp","reportingMTA":"a6-238.smtp-out.eu-west-1.amazonses.com"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n}":"Unknown","mail":{"timestamp":"2016-03-14T12:35:25.909Z","source":"test@test-domain.com","sourceArn":"arn:aws:ses:eu-west-1:123456789012:identity/testing-notify","sendingAccountId":"123456789012","destination":["testing@digital.cabinet-office.gov.uk"]},"delivery":{"timestamp":"2016-03-14T12:35:26.567Z","processingTimeMillis":658,"recipients":["testing@digital.cabinet-office.gov.uk"],"smtpResponse":"250 2.0.0 OK 1457958926 uo5si26480932wjc.221 - gsmtp","reportingMTA":"a6-238.smtp-out.eu-west-1.amazonses.com"}}",\n "Timestamp" : "2016-03-14T12:35:26.665Z",\n "SignatureVersion" : "1",\n "Signature" : "X8d7eTAOZ6wlnrdVVPYanrAlsX0SMPfOzhoTEBnQqYkrNWTqQY91C0f3bxtPdUhUtOowyPAOkTQ4KnZuzphfhVb2p1MyVYMxNKcBFB05/qaCX99+92fjw4x9LeUOwyGwMv5F0Vkfi5qZCcEw69uVrhYLVSTFTrzi/yCtru+yFULMQ6UhbY09GwiP6hjxZMVr8aROQy5lLHglqQzOuSZ4KeD85JjifHdKzlx8jjQ+uj+FLzHXPMAPmPU1JK9kpoHZ1oPshAFgPDpphJe+HwcJ8ezmk+3AEUr3wWli3xF+49y8Z2anASSVp6YI2YP95UT8Rlh3qT3T+V9V8rbSVislxA==",\n "SigningCertURL" : "https://sns.eu-west-1.amazonaws.com/SimpleNotificationService-bb750dd426d95ee9390147a5624348ee.pem",\n "UnsubscribeURL" : "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:302763885840:preview-emails:d6aad3ef-83d6-4cf3-a470-54e2e75916da"\n} \ No newline at end of file