From 9e7ab095fab120a745a85495ad4dd1695d27e873 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:14:53 -0500 Subject: [PATCH 01/26] email constants --- .../api/ops/schemas/messaging/messaging.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/fides/api/ops/schemas/messaging/messaging.py b/src/fides/api/ops/schemas/messaging/messaging.py index 2503ee5d87..7ee443abe6 100644 --- a/src/fides/api/ops/schemas/messaging/messaging.py +++ b/src/fides/api/ops/schemas/messaging/messaging.py @@ -23,6 +23,8 @@ class MessagingMethod(Enum): class MessagingServiceType(Enum): """Enum for messaging service type. Upper-cased in the database""" + MAILCHIMP_TRANSACTIONAL = "MAILCHIMP_TRANSACTIONAL" + MAILGUN = "MAILGUN" TWILIO_TEXT = "TWILIO_TEXT" @@ -167,10 +169,13 @@ class EmailForActionType(BaseModel): class MessagingServiceDetails(Enum): """Enum for messaging service details""" + # Generic + DOMAIN = "domain" + EMAIL_FROM = "email_from" + # Mailgun IS_EU_DOMAIN = "is_eu_domain" API_VERSION = "api_version" - DOMAIN = "domain" # Twilio Email TWILIO_EMAIL_FROM = "twilio_email_from" @@ -203,6 +208,9 @@ class Config: class MessagingServiceSecrets(Enum): """Enum for message service secrets""" + # Mailchimp Transactional + MAILCHIMP_TRANSACTIONAL_API_KEY = "mailchimp_transactional_api_key" + # Mailgun MAILGUN_API_KEY = "mailgun_api_key" @@ -216,8 +224,19 @@ class MessagingServiceSecrets(Enum): TWILIO_API_KEY = "twilio_api_key" +class MessagingServiceSecretsMailchimpTransactional(BaseModel): + """The secrets required to connect to Mailchimp Transactional.""" + + mailchimp_transactional_api_key: str + + class Config: + """Restrict adding other fields through this schema.""" + + extra = Extra.forbid + + class MessagingServiceSecretsMailgun(BaseModel): - """The secrets required to connect to mailgun.""" + """The secrets required to connect to Mailgun.""" mailgun_api_key: str @@ -228,7 +247,7 @@ class Config: class MessagingServiceSecretsTwilioSMS(BaseModel): - """The secrets required to connect to twilio SMS.""" + """The secrets required to connect to Twilio SMS.""" twilio_account_sid: str twilio_auth_token: str From 3fb26f370a73dc3fa8b7722be7aaa016d3193ada Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:15:21 -0500 Subject: [PATCH 02/26] add mailchimp transactional last to pecking order --- .../ops/service/connectors/email_connector.py | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/fides/api/ops/service/connectors/email_connector.py b/src/fides/api/ops/service/connectors/email_connector.py index f507ccaf5b..7faf9ec70c 100644 --- a/src/fides/api/ops/service/connectors/email_connector.py +++ b/src/fides/api/ops/service/connectors/email_connector.py @@ -237,6 +237,7 @@ def _get_email_messaging_config_service_type(db: Session) -> Optional[str]: if not messaging_configs: # let messaging dispatch service handle non-existent service return None + twilio_email_config = next( ( config @@ -245,6 +246,10 @@ def _get_email_messaging_config_service_type(db: Session) -> Optional[str]: ), None, ) + if twilio_email_config: + # First choice: use Twilio + return MessagingServiceType.TWILIO_EMAIL.value + mailgun_config = next( ( config @@ -253,9 +258,18 @@ def _get_email_messaging_config_service_type(db: Session) -> Optional[str]: ), None, ) - if twilio_email_config: - # we prefer twilio over mailgun - return MessagingServiceType.TWILIO_EMAIL.value if mailgun_config: + # Second choice: use Mailgun return MessagingServiceType.MAILGUN.value - return None + + mailchimp_transactional_config = next( + ( + config + for config in messaging_configs + if config.service_type == MessagingServiceType.MAILCHIMP_TRANSACTIONAL + ), + None, + ) + if mailchimp_transactional_config: + # Third choice: use Mailchimp Transactional + return MessagingServiceType.MAILCHIMP_TRANSACTIONAL.value From 3bfa50addc71484dc8d02334d98964cb00dfc252 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:15:46 -0500 Subject: [PATCH 03/26] beginnings of dispatcher --- .../messaging/message_dispatch_service.py | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index e6f7673f77..b2e7cf8183 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -344,13 +344,57 @@ def _get_dispatcher_from_config_type( message_service_type: MessagingServiceType, ) -> Optional[Callable[[MessagingConfig, Any, Optional[str]], None]]: """Determines which dispatcher to use based on message service type""" - if message_service_type == MessagingServiceType.MAILGUN: - return _mailgun_dispatcher - if message_service_type == MessagingServiceType.TWILIO_TEXT: - return _twilio_sms_dispatcher - if message_service_type == MessagingServiceType.TWILIO_EMAIL: - return _twilio_email_dispatcher - return None + handler = { + MessagingServiceType.MAILGUN: _mailgun_dispatcher, + MessagingServiceType.MAILCHIMP_TRANSACTIONAL: _mailchimp_transactional_dispatcher, + MessagingServiceType.TWILIO_TEXT: _twilio_sms_dispatcher, + MessagingServiceType.TWILIO_EMAIL: _twilio_email_dispatcher, + } + return handler.get(message_service_type) + + +def _mailchimp_transactional_dispatcher( + messaging_config: MessagingConfig, + message: EmailForActionType, + to: Optional[str], +) -> None: + """Dispatches email using Mailchimp Transactional""" + if not to: + logger.error("Message failed to send. No email identity supplied.") + raise MessageDispatchException("No email identity supplied.") + + if not messaging_config.details or not messaging_config.secrets: + logger.error( + "Message failed to send. No mailgun config details or secrets supplied." + ) + raise MessageDispatchException("No mailgun config details or secrets supplied.") + + email_from = messaging_config.details[ + MessagingServiceDetails.TWILIO_EMAIL_FROM.value + ] + data = { + "key": messaging_config.secrets[ + MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value + ], + "message": { + "from_email": email_from, + "subject": message.subject, + "text": message.body, + "to": [{"email": to.strip(), "type": "to"}], + }, + } + + url = "https://mandrillapp.com/api/1.0/messages/send" + response = requests.post( + url, + headers={"Content-Type": "application/json"}, + json=json.dumps(data), + ) + if not response.ok: + logger.error("Email failed to send with status code: %s", response.status_code) + raise MessageDispatchException( + f"Email failed to send with status code {response.status_code}" + ) def _mailgun_dispatcher( @@ -358,15 +402,17 @@ def _mailgun_dispatcher( message: EmailForActionType, to: Optional[str], ) -> None: - """Dispatches email using mailgun""" + """Dispatches email using Mailgun""" if not to: logger.error("Message failed to send. No email identity supplied.") raise MessageDispatchException("No email identity supplied.") + if not messaging_config.details or not messaging_config.secrets: logger.error( "Message failed to send. No mailgun config details or secrets supplied." ) raise MessageDispatchException("No mailgun config details or secrets supplied.") + base_url = ( "https://api.mailgun.net" if messaging_config.details[MessagingServiceDetails.IS_EU_DOMAIN.value] is False From c4f8aa38f9df2f430393e2f97acd3665cdbb6848 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:35:54 -0500 Subject: [PATCH 04/26] adds messagingservice detail --- src/fides/api/ops/schemas/messaging/messaging.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/fides/api/ops/schemas/messaging/messaging.py b/src/fides/api/ops/schemas/messaging/messaging.py index 7ee443abe6..ddf9673f2f 100644 --- a/src/fides/api/ops/schemas/messaging/messaging.py +++ b/src/fides/api/ops/schemas/messaging/messaging.py @@ -181,6 +181,18 @@ class MessagingServiceDetails(Enum): TWILIO_EMAIL_FROM = "twilio_email_from" +class MessagingServiceDetailsMailchimpTransactional(BaseModel): + """The details required to represent a Mailchimp Transactional email configuration.""" + + domain: str + email_from: str + + class Config: + """Restrict adding other fields through this schema.""" + + extra = Extra.forbid + + class MessagingServiceDetailsMailgun(BaseModel): """The details required to represent a Mailgun email configuration.""" From f1c1bdedfa1f991a628cc6158a6a2abbae9880e1 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:36:08 -0500 Subject: [PATCH 05/26] adds validation option --- src/fides/core/config/notification_settings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fides/core/config/notification_settings.py b/src/fides/core/config/notification_settings.py index 8be55c0e74..c08341ed79 100644 --- a/src/fides/core/config/notification_settings.py +++ b/src/fides/core/config/notification_settings.py @@ -32,7 +32,12 @@ class NotificationSettings(FidesSettings): def validate_notification_service_type(cls, value: Optional[str]) -> Optional[str]: """Ensure the provided type is a valid value.""" if value: - valid_values = ["MAILGUN", "TWILIO_TEXT", "TWILIO_EMAIL"] + valid_values = [ + "MAILCHIMP_TRANSACTIONAL", + "MAILGUN", + "TWILIO_TEXT", + "TWILIO_EMAIL", + ] value = value.upper() # force uppercase for safety if value not in valid_values: From a12c3128ea456c03f0ce26394398a79118c3e334 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:43:53 -0500 Subject: [PATCH 06/26] adds test fixture --- tests/fixtures/application_fixtures.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/application_fixtures.py b/tests/fixtures/application_fixtures.py index 50cf8d2bab..057b936e95 100644 --- a/tests/fixtures/application_fixtures.py +++ b/tests/fixtures/application_fixtures.py @@ -327,6 +327,30 @@ def messaging_config_twilio_sms(db: Session) -> Generator: messaging_config.delete(db) +@pytest.fixture(scope="function") +def messaging_config_mailchimp_transactional(db: Session) -> Generator: + messaging_config = MessagingConfig.create( + db=db, + data={ + "name": str(uuid4()), + "key": "my_mailchimp_transactional_messaging_config", + "service_type": MessagingServiceType.MAILCHIMP_TRANSACTIONAL, + "details": { + MessagingServiceDetails.DOMAIN.value: "some.domain", + MessagingServiceDetails.EMAIL_FROM.value: "test@example.com", + }, + }, + ) + messaging_config.set_secrets( + db=db, + messaging_secrets={ + MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value: "12984r70298r" + }, + ) + yield messaging_config + messaging_config.delete(db) + + @pytest.fixture(scope="function") def https_connection_config(db: Session) -> Generator: name = str(uuid4()) @@ -653,7 +677,6 @@ def erasure_policy_string_rewrite_long( def erasure_policy_two_rules( db: Session, oauth_client: ClientDetail, erasure_policy: Policy ) -> Generator: - second_erasure_rule = Rule.create( db=db, data={ @@ -1618,7 +1641,6 @@ def authenticated_fides_client( @pytest.fixture(scope="function") def system(db: Session) -> System: - system = System.create( db=db, data={ From 7e433058164ba52b52ca534dd7ea07657ba15b72 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 08:49:32 -0500 Subject: [PATCH 07/26] lint --- src/fides/api/ops/service/connectors/email_connector.py | 2 ++ src/fides/api/ops/service/messaging/message_dispatch_service.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fides/api/ops/service/connectors/email_connector.py b/src/fides/api/ops/service/connectors/email_connector.py index 7faf9ec70c..0c6e98dfa4 100644 --- a/src/fides/api/ops/service/connectors/email_connector.py +++ b/src/fides/api/ops/service/connectors/email_connector.py @@ -273,3 +273,5 @@ def _get_email_messaging_config_service_type(db: Session) -> Optional[str]: if mailchimp_transactional_config: # Third choice: use Mailchimp Transactional return MessagingServiceType.MAILCHIMP_TRANSACTIONAL.value + + return None diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index b2e7cf8183..ee582db646 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -342,7 +342,7 @@ def _build_email( # pylint: disable=too-many-return-statements def _get_dispatcher_from_config_type( message_service_type: MessagingServiceType, -) -> Optional[Callable[[MessagingConfig, Any, Optional[str]], None]]: +) -> Optional[function]: """Determines which dispatcher to use based on message service type""" handler = { MessagingServiceType.MAILGUN: _mailgun_dispatcher, From 17b519c2d0b49c232091c374b32221873580faf3 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 10:36:16 -0500 Subject: [PATCH 08/26] adds correct typing --- .../api/ops/service/messaging/message_dispatch_service.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index ee582db646..9b6674104b 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -167,9 +167,9 @@ def dispatch_message( logger.info( "Retrieving appropriate dispatcher for email service: {}", messaging_service ) - dispatcher: Optional[ - Callable[[MessagingConfig, Any, Optional[str]], None] - ] = _get_dispatcher_from_config_type(message_service_type=messaging_service) + dispatcher: Optional[Callable] = _get_dispatcher_from_config_type( + message_service_type=messaging_service + ) if not dispatcher: logger.error( "Dispatcher has not been implemented for message service type: {}", @@ -342,7 +342,7 @@ def _build_email( # pylint: disable=too-many-return-statements def _get_dispatcher_from_config_type( message_service_type: MessagingServiceType, -) -> Optional[function]: +) -> Optional[Callable]: """Determines which dispatcher to use based on message service type""" handler = { MessagingServiceType.MAILGUN: _mailgun_dispatcher, From 709060dfbf1661ebd1ce94f676627972aee45717 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 13:49:11 -0500 Subject: [PATCH 09/26] update tests --- tests/ops/api/v1/endpoints/test_messaging_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 02df838028..97e1b6fd59 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -142,7 +142,7 @@ def test_post_email_config_with_not_supported_service_type( assert 422 == response.status_code assert ( json.loads(response.text)["detail"][0]["msg"] - == "value is not a valid enumeration member; permitted: 'MAILGUN', 'TWILIO_TEXT', 'TWILIO_EMAIL'" + == "value is not a valid enumeration member; permitted: 'MAILCHIMP_TRANSACTIONAL', 'MAILGUN', 'TWILIO_TEXT', 'TWILIO_EMAIL'" ) def test_post_email_config_with_no_key( From b971c0ecb18a267865cfc13313e9808cd4c84836 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 16:15:13 -0500 Subject: [PATCH 10/26] return types back to how they were but manually coerce --- .../service/messaging/message_dispatch_service.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index 9b6674104b..6d409419bd 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -167,9 +167,9 @@ def dispatch_message( logger.info( "Retrieving appropriate dispatcher for email service: {}", messaging_service ) - dispatcher: Optional[Callable] = _get_dispatcher_from_config_type( - message_service_type=messaging_service - ) + dispatcher: Optional[ + Callable[[MessagingConfig, Any, Optional[str]], None] + ] = _get_dispatcher_from_config_type(message_service_type=messaging_service) if not dispatcher: logger.error( "Dispatcher has not been implemented for message service type: {}", @@ -342,7 +342,7 @@ def _build_email( # pylint: disable=too-many-return-statements def _get_dispatcher_from_config_type( message_service_type: MessagingServiceType, -) -> Optional[Callable]: +) -> Optional[Callable[[MessagingConfig, Any, Optional[str]], None]]: """Determines which dispatcher to use based on message service type""" handler = { MessagingServiceType.MAILGUN: _mailgun_dispatcher, @@ -350,7 +350,10 @@ def _get_dispatcher_from_config_type( MessagingServiceType.TWILIO_TEXT: _twilio_sms_dispatcher, MessagingServiceType.TWILIO_EMAIL: _twilio_email_dispatcher, } - return handler.get(message_service_type) + func: Optional[Callable[[MessagingConfig, Any, Optional[str]], None]] = handler.get( + message_service_type + ) + return func def _mailchimp_transactional_dispatcher( From 534026bac7e885c0c1096990cf3b62889f1e1873 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 17:48:05 -0500 Subject: [PATCH 11/26] adds migration to add MAILCHIMP_TRANSACTIONAL to enum --- ...61471_adds_mailchimp_transactional_enum.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py diff --git a/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py b/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py new file mode 100644 index 0000000000..b8daaffdc0 --- /dev/null +++ b/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py @@ -0,0 +1,48 @@ +"""adds MAILCHIMP_TRANSACTIONAL enum + +Revision ID: 39b209861471 +Revises: eb1e6ec39b83 +Create Date: 2023-03-03 21:57:14.368385 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "39b209861471" +down_revision = "eb1e6ec39b83" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("alter type messagingservicetype rename to messagingservicetype_old") + op.execute( + "create type messagingservicetype as enum('MAILCHIMP_TRANSACTIONAL', 'MAILGUN', 'TWILIO_TEXT', 'TWILIO_EMAIL')" + ) + op.execute( + ( + "alter table messagingconfig alter column service_type type messagingservicetype using " + "service_type::text::messagingservicetype" + ) + ) + op.execute("drop type messagingservicetype_old") + + +def downgrade(): + # return + op.execute( + "delete from messagingconfig where service_type in ('MAILCHIMP_TRANSACTIONAL')" + ) + op.execute("alter type messagingservicetype rename to messagingservicetype_old") + op.execute( + "create type messagingservicetype as enum('MAILGUN', 'TWILIO_TEXT', 'TWILIO_EMAIL')" + ) + op.execute( + ( + "alter table messagingconfig alter column service_type type messagingservicetype using " + "service_type::text::messagingservicetype" + ) + ) + op.execute("drop type messagingservicetype_old") From 8d570c1c7cf6370b3c3ed503552e516cac4cd488 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 17:57:25 -0500 Subject: [PATCH 12/26] add secrets validation --- src/fides/api/ops/models/messaging.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/fides/api/ops/models/messaging.py b/src/fides/api/ops/models/messaging.py index 55eaae6336..ad2c8659f7 100644 --- a/src/fides/api/ops/models/messaging.py +++ b/src/fides/api/ops/models/messaging.py @@ -20,6 +20,7 @@ SMS_MESSAGING_SERVICES, SUPPORTED_MESSAGING_SERVICE_SECRETS, MessagingMethod, + MessagingServiceSecretsMailchimpTransactional, MessagingServiceSecretsMailgun, MessagingServiceSecretsTwilioEmail, MessagingServiceSecretsTwilioSMS, @@ -55,6 +56,7 @@ def get_schema_for_secrets( """ try: schema = { + MessagingServiceType.MAILCHIMP_TRANSACTIONAL: MessagingServiceSecretsMailchimpTransactional, MessagingServiceType.MAILGUN: MessagingServiceSecretsMailgun, MessagingServiceType.TWILIO_TEXT: MessagingServiceSecretsTwilioSMS, MessagingServiceType.TWILIO_EMAIL: MessagingServiceSecretsTwilioEmail, From 555f0319d2c7ba1b002c45611d57f3efe696727e Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Fri, 3 Mar 2023 20:20:28 -0500 Subject: [PATCH 13/26] add missing values --- src/fides/api/ops/schemas/messaging/messaging.py | 1 + .../api/ops/service/messaging/message_dispatch_service.py | 4 +--- src/fides/core/config/notification_settings.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/fides/api/ops/schemas/messaging/messaging.py b/src/fides/api/ops/schemas/messaging/messaging.py index ddf9673f2f..3b532d0438 100644 --- a/src/fides/api/ops/schemas/messaging/messaging.py +++ b/src/fides/api/ops/schemas/messaging/messaging.py @@ -42,6 +42,7 @@ def _missing_( EMAIL_MESSAGING_SERVICES: Tuple[str, ...] = ( + MessagingServiceType.MAILCHIMP_TRANSACTIONAL.value, MessagingServiceType.MAILGUN.value, MessagingServiceType.TWILIO_EMAIL.value, ) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index 6d409419bd..5bac0f2ae9 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -372,9 +372,7 @@ def _mailchimp_transactional_dispatcher( ) raise MessageDispatchException("No mailgun config details or secrets supplied.") - email_from = messaging_config.details[ - MessagingServiceDetails.TWILIO_EMAIL_FROM.value - ] + email_from = messaging_config.details[MessagingServiceDetails.EMAIL_FROM.value] data = { "key": messaging_config.secrets[ MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value diff --git a/src/fides/core/config/notification_settings.py b/src/fides/core/config/notification_settings.py index c08341ed79..a1f4bf888b 100644 --- a/src/fides/core/config/notification_settings.py +++ b/src/fides/core/config/notification_settings.py @@ -12,7 +12,7 @@ class NotificationSettings(FidesSettings): notification_service_type: Optional[str] = Field( default=None, - description="Sets the notification service type used to send notifications. Accepts mailgun, twilio_sms, or twilio_email.", + description="Sets the notification service type used to send notifications. Accepts mailchimp_transactional, mailgun, twilio_sms, or twilio_email.", ) send_request_completion_notification: bool = Field( default=False, From 640d07dfe45b8ef27a0586af04333992f6cc7a0f Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 11:30:49 -0500 Subject: [PATCH 14/26] adds acceptable details schema to API input --- src/fides/api/ops/schemas/messaging/messaging.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/fides/api/ops/schemas/messaging/messaging.py b/src/fides/api/ops/schemas/messaging/messaging.py index 3b532d0438..ddac9607e3 100644 --- a/src/fides/api/ops/schemas/messaging/messaging.py +++ b/src/fides/api/ops/schemas/messaging/messaging.py @@ -304,7 +304,11 @@ class MessagingConfigBase(BaseModel): service_type: MessagingServiceType details: Optional[ - Union[MessagingServiceDetailsMailgun, MessagingServiceDetailsTwilioEmail] + Union[ + MessagingServiceDetailsMailchimpTransactional, + MessagingServiceDetailsMailgun, + MessagingServiceDetailsTwilioEmail, + ] ] class Config: From 9c50d9c3932c21d5e0394d93d2dadfd6b0009031 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 17:27:46 -0500 Subject: [PATCH 15/26] refine dispatch logic --- .../messaging/message_dispatch_service.py | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index 5bac0f2ae9..1f78ba834e 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -372,31 +372,48 @@ def _mailchimp_transactional_dispatcher( ) raise MessageDispatchException("No mailgun config details or secrets supplied.") - email_from = messaging_config.details[MessagingServiceDetails.EMAIL_FROM.value] - data = { - "key": messaging_config.secrets[ - MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value - ], - "message": { - "from_email": email_from, - "subject": message.subject, - "text": message.body, - "to": [{"email": to.strip(), "type": "to"}], - }, - } + from_email = messaging_config.details[MessagingServiceDetails.EMAIL_FROM.value] + data = json.dumps( + { + "key": messaging_config.secrets[ + MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value + ], + "message": { + "from_email": from_email, + "subject": message.subject, + "text": message.body, + # On Mailchimp Transactional's free plan `to` must be an email of the same + # domain as `from_email` + "to": [{"email": to.strip(), "type": "to"}], + }, + } + ) - url = "https://mandrillapp.com/api/1.0/messages/send" response = requests.post( - url, + "https://mandrillapp.com/api/1.0/messages/send", headers={"Content-Type": "application/json"}, - json=json.dumps(data), + data=data, ) if not response.ok: - logger.error("Email failed to send with status code: %s", response.status_code) + logger.error("Email failed to send with status code: %s" % response.status_code) raise MessageDispatchException( f"Email failed to send with status code {response.status_code}" ) + send_data = response.json()[0] + email_rejected = send_data.get("status", "rejected") == "rejected" + if email_rejected: + reason = send_data.get("reject_reason", "Fides Error") + explanations = { + "soft-bounce": "A temporary error occured with the target inbox. For example, this inbox could be full. See https://mailchimp.com/developer/transactional/docs/reputation-rejections/#bounces for more info.", + "hard-bounce": "A permanent error occured with the target inbox. See https://mailchimp.com/developer/transactional/docs/reputation-rejections/#bounces for more info.", + "recipient-domain-mismatch": f"You are not authorised to send email to this domain from {from_email}.", + } + explanation = explanations.get(reason, "") + raise MessageDispatchException( + f"Verification email unable to send due to reason: {reason}. {explanation}" + ) + def _mailgun_dispatcher( messaging_config: MessagingConfig, From 9d0338580bdced36ae49b1f09d36aa8bca327824 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 17:41:28 -0500 Subject: [PATCH 16/26] adds secrets to docs --- .../ops/schemas/messaging/messaging_secrets_docs_only.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/fides/api/ops/schemas/messaging/messaging_secrets_docs_only.py b/src/fides/api/ops/schemas/messaging/messaging_secrets_docs_only.py index 18e24a52f4..43eb7b66d5 100644 --- a/src/fides/api/ops/schemas/messaging/messaging_secrets_docs_only.py +++ b/src/fides/api/ops/schemas/messaging/messaging_secrets_docs_only.py @@ -2,12 +2,20 @@ from fides.api.ops.schemas.base_class import NoValidationSchema from fides.api.ops.schemas.messaging.messaging import ( + MessagingServiceSecretsMailchimpTransactional, MessagingServiceSecretsMailgun, MessagingServiceSecretsTwilioEmail, MessagingServiceSecretsTwilioSMS, ) +class MessagingServiceSecretsMailchimpTransactionalDocs( + MessagingServiceSecretsMailchimpTransactional, + NoValidationSchema, +): + """The secrets required to connect Mailchimp Transactional, for documentation""" + + class MessagingSecretsMailgunDocs(MessagingServiceSecretsMailgun, NoValidationSchema): """The secrets required to connect to Mailgun, for documentation""" @@ -25,6 +33,7 @@ class MessagingSecretsTwilioEmailDocs( possible_messaging_secrets = Union[ + MessagingServiceSecretsMailchimpTransactionalDocs, MessagingSecretsMailgunDocs, MessagingSecretsTwilioSMSDocs, MessagingSecretsTwilioEmailDocs, From dbf8052b6e6849dfea658498cd262bcb144ca21b Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 17:49:30 -0500 Subject: [PATCH 17/26] remove unused --- src/fides/api/ops/schemas/messaging/messaging.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/fides/api/ops/schemas/messaging/messaging.py b/src/fides/api/ops/schemas/messaging/messaging.py index ddac9607e3..4b58963da9 100644 --- a/src/fides/api/ops/schemas/messaging/messaging.py +++ b/src/fides/api/ops/schemas/messaging/messaging.py @@ -185,7 +185,6 @@ class MessagingServiceDetails(Enum): class MessagingServiceDetailsMailchimpTransactional(BaseModel): """The details required to represent a Mailchimp Transactional email configuration.""" - domain: str email_from: str class Config: From d4ab92f695cf83c6aba3056826e9f5959cece91b Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 17:49:49 -0500 Subject: [PATCH 18/26] test for creating a mailchimp transactional connector --- .../v1/endpoints/test_messaging_endpoints.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 97e1b6fd59..3152527c83 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -63,6 +63,16 @@ def payload_twilio_email(self): }, } + @pytest.fixture(scope="function") + def payload_mailchimp_transactional(self): + return { + "name": "mailchimp_transactional_email", + "service_type": MessagingServiceType.MAILCHIMP_TRANSACTIONAL.value, + "details": { + MessagingServiceDetails.EMAIL_FROM.value: "user@example.com", + }, + } + @pytest.fixture(scope="function") def payload_twilio_sms(self): return { @@ -255,6 +265,39 @@ def test_post_email_config_service_already_exists( f"Key (service_type)=(MAILGUN) already exists" in response.json()["detail"] ) + def test_post_mailgun_transactional_config( + self, + db: Session, + api_client: TestClient, + payload_mailchimp_transactional, + url, + generate_auth_header, + ): + key = "mailchimp_transactional_messaging_config" + payload_mailchimp_transactional["key"] = key + auth_header = generate_auth_header([MESSAGING_CREATE_OR_UPDATE]) + + response = api_client.post( + url, + headers=auth_header, + json=payload_mailchimp_transactional, + ) + assert 200 == response.status_code + + response_body = json.loads(response.text) + email_config = db.query(MessagingConfig).filter_by(key=key)[0] + + expected_response = { + "key": key, + "name": "twilio_email", + "service_type": MessagingServiceType.TWILIO_EMAIL.value, + "details": { + MessagingServiceDetails.TWILIO_EMAIL_FROM.value: "test@email.com" + }, + } + assert expected_response == response_body + email_config.delete(db) + def test_post_twilio_email_config( self, db: Session, From 2ef07a292182e73a0edf07b82ccfe8fc4a96e1e9 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 17:58:23 -0500 Subject: [PATCH 19/26] test for configuring mailchimp transactional secrets --- .../v1/endpoints/test_messaging_endpoints.py | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 3152527c83..542411b332 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -586,6 +586,48 @@ def test_put_config_secrets( ) +class TestPutMessagingConfigSecretMailchimpTransactional: + @pytest.fixture(scope="function") + def url(self, messaging_config_mailchimp_transactional) -> str: + return (V1_URL_PREFIX + MESSAGING_SECRETS).format( + config_key=messaging_config_mailchimp_transactional.key + ) + + def test_put_config_secrets( + self, + db: Session, + api_client: TestClient, + payload, + url, + generate_auth_header, + messaging_config_mailchimp_transactional, + ): + key = "key-123456789" + auth_header = generate_auth_header([MESSAGING_CREATE_OR_UPDATE]) + response = api_client.put( + url, + headers=auth_header, + json={ + MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value: key, + }, + ) + assert 200 == response.status_code + + db.refresh(messaging_config_mailchimp_transactional) + + assert json.loads(response.text) == { + "msg": f"Secrets updated for MessagingConfig with key: {messaging_config_mailchimp_transactional.key}.", + "test_status": None, + "failure_reason": None, + } + assert ( + messaging_config_mailchimp_transactional.secrets[ + MessagingServiceSecrets.TWILIO_API_KEY.value + ] + == key + ) + + class TestPutMessagingConfigSecretTwilioSms: @pytest.fixture(scope="function") def url(self, messaging_config_twilio_sms) -> str: @@ -744,7 +786,12 @@ def test_get_configs_wrong_scope( assert 403 == response.status_code def test_get_configs( - self, db, api_client: TestClient, url, generate_auth_header, messaging_config + self, + db, + api_client: TestClient, + url, + generate_auth_header, + messaging_config, ): auth_header = generate_auth_header([MESSAGING_READ]) response = api_client.get(url, headers=auth_header) @@ -800,7 +847,11 @@ def test_get_config_invalid( assert 404 == response.status_code def test_get_config( - self, url, api_client: TestClient, generate_auth_header, messaging_config + self, + url, + api_client: TestClient, + generate_auth_header, + messaging_config, ): auth_header = generate_auth_header([MESSAGING_READ]) response = api_client.get(url, headers=auth_header) From 6cdf2fe538b9d6b1826afcb85437438c7f490307 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 21:09:57 -0500 Subject: [PATCH 20/26] adds type-ignore and corrects handler types --- .../service/messaging/message_dispatch_service.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index 1f78ba834e..84d180990b 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -167,9 +167,9 @@ def dispatch_message( logger.info( "Retrieving appropriate dispatcher for email service: {}", messaging_service ) - dispatcher: Optional[ - Callable[[MessagingConfig, Any, Optional[str]], None] - ] = _get_dispatcher_from_config_type(message_service_type=messaging_service) + dispatcher: Optional[function] = _get_dispatcher_from_config_type( + message_service_type=messaging_service + ) if not dispatcher: logger.error( "Dispatcher has not been implemented for message service type: {}", @@ -186,7 +186,7 @@ def dispatch_message( if subject_override and isinstance(message, EmailForActionType): message.subject = subject_override - dispatcher( + dispatcher( # type: ignore messaging_config, message, to_identity.email @@ -342,7 +342,7 @@ def _build_email( # pylint: disable=too-many-return-statements def _get_dispatcher_from_config_type( message_service_type: MessagingServiceType, -) -> Optional[Callable[[MessagingConfig, Any, Optional[str]], None]]: +) -> Optional[function]: """Determines which dispatcher to use based on message service type""" handler = { MessagingServiceType.MAILGUN: _mailgun_dispatcher, @@ -350,9 +350,7 @@ def _get_dispatcher_from_config_type( MessagingServiceType.TWILIO_TEXT: _twilio_sms_dispatcher, MessagingServiceType.TWILIO_EMAIL: _twilio_email_dispatcher, } - func: Optional[Callable[[MessagingConfig, Any, Optional[str]], None]] = handler.get( - message_service_type - ) + func = handler.get(message_service_type) return func From bb735fb0288e865af3d68382a1232c3b2298bae4 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 22:16:54 -0500 Subject: [PATCH 21/26] bump downrev --- .../versions/39b209861471_adds_mailchimp_transactional_enum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py b/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py index b8daaffdc0..042dce8975 100644 --- a/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py +++ b/src/fides/api/ctl/migrations/versions/39b209861471_adds_mailchimp_transactional_enum.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "39b209861471" -down_revision = "eb1e6ec39b83" +down_revision = "9f38dad37628" branch_labels = None depends_on = None From bc364908df796b486878aa99fe90db79d9c856be Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 22:34:43 -0500 Subject: [PATCH 22/26] use Callable not function --- .../ops/service/messaging/message_dispatch_service.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index 118a5ca3eb..914d49fc1e 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -168,7 +168,7 @@ def dispatch_message( logger.info( "Retrieving appropriate dispatcher for email service: {}", messaging_service ) - dispatcher: Optional[function] = _get_dispatcher_from_config_type( + dispatcher: Optional[Callable] = _get_dispatcher_from_config_type( message_service_type=messaging_service ) if not dispatcher: @@ -187,7 +187,7 @@ def dispatch_message( if subject_override and isinstance(message, EmailForActionType): message.subject = subject_override - dispatcher( # type: ignore + dispatcher( messaging_config, message, to_identity.email @@ -343,7 +343,7 @@ def _build_email( # pylint: disable=too-many-return-statements def _get_dispatcher_from_config_type( message_service_type: MessagingServiceType, -) -> Optional[function]: +) -> Optional[Callable]: """Determines which dispatcher to use based on message service type""" handler = { MessagingServiceType.MAILGUN: _mailgun_dispatcher, @@ -351,8 +351,7 @@ def _get_dispatcher_from_config_type( MessagingServiceType.TWILIO_TEXT: _twilio_sms_dispatcher, MessagingServiceType.TWILIO_EMAIL: _twilio_email_dispatcher, } - func = handler.get(message_service_type) - return func + return handler.get(message_service_type) # type: ignore def _mailchimp_transactional_dispatcher( @@ -519,7 +518,6 @@ def _twilio_email_dispatcher( ) try: - sg = sendgrid.SendGridAPIClient( api_key=messaging_config.secrets[ MessagingServiceSecrets.TWILIO_API_KEY.value From 9298a84744a567a4cb54973cadb350557e4267b5 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Mon, 6 Mar 2023 22:49:54 -0500 Subject: [PATCH 23/26] update test --- tests/ops/api/v1/endpoints/test_messaging_endpoints.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 542411b332..8eaca8505b 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -289,11 +289,9 @@ def test_post_mailgun_transactional_config( expected_response = { "key": key, - "name": "twilio_email", - "service_type": MessagingServiceType.TWILIO_EMAIL.value, - "details": { - MessagingServiceDetails.TWILIO_EMAIL_FROM.value: "test@email.com" - }, + "name": payload_mailchimp_transactional["name"], + "service_type": MessagingServiceType.MAILCHIMP_TRANSACTIONAL.value, + "details": {MessagingServiceDetails.EMAIL_FROM.value: "test@email.com"}, } assert expected_response == response_body email_config.delete(db) From 5c11f4447c118a03d20379045cc319504916c481 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Tue, 7 Mar 2023 07:54:30 -0500 Subject: [PATCH 24/26] update test data --- tests/ops/api/v1/endpoints/test_messaging_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 8eaca8505b..2e900b4c37 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -291,7 +291,7 @@ def test_post_mailgun_transactional_config( "key": key, "name": payload_mailchimp_transactional["name"], "service_type": MessagingServiceType.MAILCHIMP_TRANSACTIONAL.value, - "details": {MessagingServiceDetails.EMAIL_FROM.value: "test@email.com"}, + "details": {MessagingServiceDetails.EMAIL_FROM.value: "user@example.com"}, } assert expected_response == response_body email_config.delete(db) From 167828dd858b182ced44d0ab198c7fbb9a2b85c8 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Tue, 7 Mar 2023 08:25:56 -0500 Subject: [PATCH 25/26] remove ref to non existant fixture --- tests/ops/api/v1/endpoints/test_messaging_endpoints.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py index 2e900b4c37..033ffde486 100644 --- a/tests/ops/api/v1/endpoints/test_messaging_endpoints.py +++ b/tests/ops/api/v1/endpoints/test_messaging_endpoints.py @@ -595,7 +595,6 @@ def test_put_config_secrets( self, db: Session, api_client: TestClient, - payload, url, generate_auth_header, messaging_config_mailchimp_transactional, @@ -620,7 +619,7 @@ def test_put_config_secrets( } assert ( messaging_config_mailchimp_transactional.secrets[ - MessagingServiceSecrets.TWILIO_API_KEY.value + MessagingServiceSecrets.MAILCHIMP_TRANSACTIONAL_API_KEY.value ] == key ) From 5c3039bee7c4d5ab9b3f8ba946dcf7c984a2dee4 Mon Sep 17 00:00:00 2001 From: Sean Preston Date: Tue, 7 Mar 2023 10:54:29 -0500 Subject: [PATCH 26/26] fix nits, update changelog --- CHANGELOG.md | 1 + src/fides/api/ops/schemas/messaging/messaging.py | 1 + .../api/ops/service/messaging/message_dispatch_service.py | 6 ++++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea9e730e30..9e63c028fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ The types of changes are: * Custom Field Library Tab [#527](https://github.com/ethyca/fides/pull/2693) * Allow SendGrid template usage [#2728](https://github.com/ethyca/fides/pull/2728) * Added ConnectorRunner to simplify SaaS connector testing [#1795](https://github.com/ethyca/fides/pull/1795) +* Adds support for Mailchimp Transactional as a messaging config [#2742](https://github.com/ethyca/fides/pull/2742) ### Changed diff --git a/src/fides/api/ops/schemas/messaging/messaging.py b/src/fides/api/ops/schemas/messaging/messaging.py index 4b58963da9..3606702580 100644 --- a/src/fides/api/ops/schemas/messaging/messaging.py +++ b/src/fides/api/ops/schemas/messaging/messaging.py @@ -368,6 +368,7 @@ class Config: SUPPORTED_MESSAGING_SERVICE_SECRETS = Union[ + MessagingServiceSecretsMailchimpTransactional, MessagingServiceSecretsMailgun, MessagingServiceSecretsTwilioSMS, MessagingServiceSecretsTwilioEmail, diff --git a/src/fides/api/ops/service/messaging/message_dispatch_service.py b/src/fides/api/ops/service/messaging/message_dispatch_service.py index 914d49fc1e..7db3547105 100644 --- a/src/fides/api/ops/service/messaging/message_dispatch_service.py +++ b/src/fides/api/ops/service/messaging/message_dispatch_service.py @@ -366,9 +366,11 @@ def _mailchimp_transactional_dispatcher( if not messaging_config.details or not messaging_config.secrets: logger.error( - "Message failed to send. No mailgun config details or secrets supplied." + "Message failed to send. No Mailchimp Transactional config details or secrets supplied." + ) + raise MessageDispatchException( + "No Mailchimp Transactional config details or secrets supplied." ) - raise MessageDispatchException("No mailgun config details or secrets supplied.") from_email = messaging_config.details[MessagingServiceDetails.EMAIL_FROM.value] data = json.dumps(