import logging from datetime import datetime from typing import Any, Literal from fansifter_common.adapters.sendgrid.constants import ( BRAND_NAME, ORCHARD_INTERNAL_VENDORS_IDS, ) from fansifter_common.adapters.sendgrid.exceptions import SendGridClientError from fansifter_common.adapters.sendgrid.models import Email, FanData from fansifter_common.core.enums import Brand from fansifter_common.email import ( EmailType, encode_email_to_local_part, encode_reply_to_local_part, ) from fansifter_common.httpclient.base import HTTPClient from fansifter_common.legal_info.types import LegalEntity logger = logging.getLogger(__name__) class SendGridClient(HTTPClient): exception_class = SendGridClientError base_url = "https://api.sendgrid.com" def __init__( self, sme_api_key: str, orchard_api_key: str, awal_api_key: str, unsubscribe_host: str, preference_center_url: str, reply_to_header_enabled: bool = True, reply_to_sme_domain: str | None = None, reply_to_orchard_domain: str | None = None, reply_to_awal_domain: str | None = None, reply_to_secret_key: str | None = None, ) -> None: super().__init__( client_options={ "base_url": self.base_url, "headers": { "Content-Type": "application/json", }, } ) self._api_keys: dict[Brand, str] = { Brand.SME: sme_api_key, Brand.THEORCHARD: orchard_api_key, Brand.AWAL: awal_api_key, } self._unsubscribe_host = unsubscribe_host self._preference_center_url = preference_center_url self._reply_to_header_enabled = reply_to_header_enabled self._reply_to_domains: dict[Brand, tuple[str | None, str]] = { Brand.SME: (reply_to_sme_domain, "Sony Music"), Brand.THEORCHARD: (reply_to_orchard_domain, "The Orchard"), Brand.AWAL: (reply_to_awal_domain, "AWAL"), } self._reply_to_secret_key: str | None = reply_to_secret_key def _get_brand_api_key(self, brand: Brand) -> str: try: return self._api_keys[brand] except KeyError as err: raise SendGridClientError( "Not supported brand. Can't find proper API key." ) from err def _get_auth_headers(self, brand: Brand) -> dict[str, str]: return {"Authorization": f"Bearer {self._get_brand_api_key(brand)}"} def _build_personalization_headers( self, email_id: str, email_type: EmailType, automated_email_trigger_id: str | None, version: Literal["v1", "v2", "v3"], fan_profile_token: str | None, is_seedlist: bool, ) -> dict[str, str]: headers = {} try: encoded_local_part = encode_email_to_local_part( email_id=email_id, email_type=email_type, automated_email_trigger_id=automated_email_trigger_id, version=version, ) except ValueError as e: raise SendGridClientError(str(e)) from e if is_seedlist: headers["X-InboxMonster-CID"] = email_id if fan_profile_token: headers["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click" list_unsubscribe_links = ", <{preference_center_url}/en/profile/{profile_token}/unsubscribe>" headers["List-Unsubscribe"] = list_unsubscribe_links.format( encoded_local_part=encoded_local_part, unsubscribe_host=self._unsubscribe_host, preference_center_url=self._preference_center_url, profile_token=fan_profile_token, ) return headers @staticmethod def get_user_privacy_footer_block( email: str, privacy_links_block: str, opt_in_info: str, opt_in_info_external: str, unsubscribe_text: str, unsubscribe_url: str | None, vendor_id: int, vendor_name: str, legal_entity: LegalEntity, brand: Brand, preview: bool = False, profile_text: str | None = None, profile_url: str | None = None, ) -> str: current_year = datetime.now().year if brand == Brand.AWAL or ( brand == Brand.THEORCHARD and vendor_id not in ORCHARD_INTERNAL_VENDORS_IDS ): brand_name = BRAND_NAME[brand] opt_in_text = opt_in_info_external.format( accountName=vendor_name, brandName=brand_name, ) copyright_block = "" else: email_link = f'{email}' opt_in_text = opt_in_info.format(email=email_link) copyright_block = ( f'© {current_year} {legal_entity.name}
' f"{legal_entity.address}" ) unsubscribe_link_target = 'target="_blank"' if preview: unsubscribe_link_target = "" if profile_text and profile_url: profile_link = f'{profile_text} | ' else: profile_link = "" if unsubscribe_url is not None: unsubscribe_line = ( f'{profile_link}{unsubscribe_text}
' ) else: unsubscribe_line = "" footer = ( f'{opt_in_text}
' f"{privacy_links_block}
" f"{unsubscribe_line}" f"{copyright_block}" ) return footer def send_mail( self, *, email_id: str, email_type: EmailType, automated_email_trigger_id: str | None = None, version: Literal["v1", "v2", "v3"], brand: Brand, vendor_name: str, vendor_id: int, subject: str, to_fans: list[FanData], from_email: Email, html_content: str, preview_text: str | None = None, is_test: bool = False, category: str | None = None, is_seedlist: bool = False, ) -> None: headers = self._get_auth_headers(brand) personalizations: list[dict[str, Any]] = [] reply_to_domain_value, reply_to_name = self._reply_to_domains[brand] if ( self._reply_to_header_enabled and self._reply_to_secret_key and reply_to_domain_value ): encoded_reply_to = encode_reply_to_local_part( email_id=email_id, email_type=email_type, key=self._reply_to_secret_key, automated_email_trigger_id=automated_email_trigger_id, ) reply_to = Email( email=f"{encoded_reply_to}@{reply_to_domain_value}", name=reply_to_name, ) else: reply_to = from_email for fan in to_fans: if fan.profile_token: unsubscribe_url: str | None = ( f"{self._preference_center_url}/profile/{fan.profile_token}/subscriptions" ) profile_url = ( f"{self._preference_center_url}/profile/{fan.profile_token}" if fan.profile_text else None ) elif is_test or is_seedlist: unsubscribe_url = "#" profile_url = None else: unsubscribe_url = None profile_url = None personalizations.append( { "to": [{"email": fan.email}], "substitutions": { "-previewText-": preview_text if preview_text else "", "-doubleOptInUrl-": fan.double_opt_in_url if fan.double_opt_in_url else "#", "-privacyFooterBlock-": self.get_user_privacy_footer_block( email=fan.email, privacy_links_block=fan.privacy_links_block, opt_in_info=fan.opt_in_info, opt_in_info_external=fan.opt_in_info_external, unsubscribe_text=fan.unsubscribe_text, unsubscribe_url=unsubscribe_url, vendor_id=vendor_id, vendor_name=vendor_name, legal_entity=LegalEntity( name=fan.legal_entity_name, address=fan.legal_entity_address, ), brand=brand, profile_text=fan.profile_text, profile_url=profile_url, ), **fan.merge_tags.to_substitutions(), }, "headers": self._build_personalization_headers( email_id=email_id, email_type=email_type, automated_email_trigger_id=automated_email_trigger_id, version=version, fan_profile_token=fan.profile_token, is_seedlist=is_seedlist, ), } ) payload: dict[str, Any] = { "personalizations": personalizations, "from": from_email.get(), "subject": subject, "content": [{"type": "text/html", "value": html_content}], "custom_args": { "campaign_id": email_id, # DEPRECATED "email_id": email_id, "email_type": email_type, "automated_email_trigger_id": automated_email_trigger_id, "is_test": is_test or is_seedlist, }, "reply_to": reply_to.get(), } if category: payload["categories"] = [category] self.request( "POST", "/v3/mail/send", json=payload, headers=headers, )