Skip to content

Commit

Permalink
Unisender Go: Add fields to backend, fix typos, test changes
Browse files Browse the repository at this point in the history
  • Loading branch information
Arondit committed Jan 22, 2024
1 parent 9d8974d commit 80f382f
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 45 deletions.
130 changes: 112 additions & 18 deletions anymail/backends/unisender_go.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
class EmailBackend(AnymailRequestsBackend):
"""Unsidender GO v1 API Email Backend"""

esp_name = "UnisenderGo"
esp_name = "Unisender Go"

def __init__(self, **kwargs: typing.Any):
"""Init options from Django settings"""
Expand All @@ -34,13 +34,9 @@ def __init__(self, **kwargs: typing.Any):
"merge_field_format", esp_name=esp_name, kwargs=kwargs, default=None
)

api_url = get_anymail_setting(
"api_url", esp_name=esp_name, kwargs=kwargs, default=None
) # Don't set default, because url depends on location
# url template is https://go<number>.unisender.<lang>/<lang>/transactional/api/v1
api_url = get_anymail_setting("api_url", esp_name=esp_name, kwargs=kwargs)
# Don't set default, because url depends on location

if api_url is None:
raise AnymailConfigurationError("api_url required")
super().__init__(api_url, **kwargs)

def build_message_payload(
Expand Down Expand Up @@ -169,19 +165,107 @@ def __init__(
def get_api_endpoint(self) -> str:
return "email/send.json"

def init_payload(self) -> None:
self.data = {"headers": CaseInsensitiveDict()} # becomes json
def set_skip_unsubscribe(self, extra: dict) -> None:
"""
By default, Unisender Go adds unsubscribe link.
# by default, Unisender Go adds unsubscribe link
# to fix this, you have to request tech support
if (
get_anymail_setting(
"skip_unsubscribe", esp_name=self.esp_name, default=False
)
is True
It's needed due to guarantee not abusing users with spam.
Before to use this setting, you have to request tech support.
Expects skip_unsubscribe to be True or False, then transfers it to 0 or 1.
Anyway, works if skip_unsubscribe converts to True or False (for flexibility).
"""
if "skip_unsubscribe" in extra and extra["skip_unsubscribe"]:
self.data["skip_unsubscribe"] = 1

def set_global_language(self, extra):
"""
Language for link language and unsubscribe page.
Options: 'be', 'de', 'en', 'es', 'fr', 'it', 'pl', 'pt', 'ru', 'ua', 'kz'.
"""
if "global_language" in extra and extra["global_language"]:
self.data["global_language"] = extra["global_language"]

def set_amp(self, extra):
"""AMP-part of email"""
if "amp" in extra and extra["amp"]:
self.data["amp"] = extra["amp"]

def set_bypass_settings(self, extra):
"""
Set extra settings with bypass prefix.
bypass_global: optional 0/1 (0 by default)
If 1: To ignore list of global unavailability.
Can be forbidden for some system records.
bypass_unavailable: optional 0/1 (0 by default)
If 1: To ignore current project unavailable addresses.
Works only with bypass_global = 1.
bypass_unsubscribed: optional 0/1 (0 by default)
If 1: To ignore list of unsubscribed people.
Works only with bypass_global=1 and requires tech support's approve.
bypass_complained: optional 0/1 (0 by default)
If 1: To ignore complainers on project.
Works only with bypass_global=1 and requires tech support's approve.
"""
bypass_fields = (
"bypass_global",
"bypass_unavailable",
"bypass_unsubscribed",
"bypass_complained",
)
for field in bypass_fields:
if field in extra:
self.data[field] = extra[field]

def set_template_engine(self, extra):
"""
Templating choosing parameter. Can be either 'simple' or 'velocity' or 'none'.
'simple' by default.
'none' available only for emails with
‘track_links’ and ‘track_read’ equal 0 and with turned off unsubscribe block.
"Simple" templating is for simple substitutions.
"Velocity" templating allows loops, arrays, etc.
"""
if "template_engine" in extra and extra["template_engine"]:
self.data["template_engine"] = extra["template_engine"]

def set_esp_extra(self, extra: dict) -> None:
"""Set every esp extra parameter with its docstring"""
self.set_skip_unsubscribe(extra)
self.set_global_language(extra)
self.set_template_engine(extra)
self.set_amp(extra)
self.set_bypass_settings(extra)

def set_global_settings_from_config(self):
"""
Here we set variables from global config.
If there is no esp_extra in backend's kwargs, set_esp_extra won't be called.
So we have to set default values in init.
You can change them with backend's kwarg esp_extra.
"""
if get_anymail_setting(
"skip_unsubscribe", esp_name=self.esp_name, default=False
):
self.data["skip_unsubscribe"] = 1

global_language = get_anymail_setting(
"global_language", esp_name=self.esp_name, default=None
)
if global_language:
self.data["global_language"] = global_language

def init_payload(self) -> None:
self.data = {"headers": CaseInsensitiveDict()} # becomes json
self.set_global_settings_from_config()

def serialize_data(self) -> str:
"""Performs any necessary serialization on self.data, and returns the result."""
if self.generate_message_id:
Expand Down Expand Up @@ -211,7 +295,7 @@ def set_merge_global_data(self, merge_global_data: dict[str, str]) -> None:
}

def set_anymail_id(self) -> None:
"""Ensure each personalization has a known anymail_id for later event tracking"""
"""Ensure each personalization has a known anymail_id for event tracking"""
for recipient in self.data["recipients"]:
anymail_id = str(uuid.uuid4())

Expand Down Expand Up @@ -246,6 +330,13 @@ def set_reply_to(self, emails: list[EmailAddress]) -> None:
self.data["reply_to"] = emails[0].addr_spec

def set_extra_headers(self, headers: dict[str, str]) -> None:
"""
Available service extra headers are:
- X-UNISENDER-GO-Global-Language
- X-UNISENDER-GO-Template-Engine
Value in header has higher priority than in config.
"""
self.data["headers"].update(headers)

def set_text_body(self, body: str) -> None:
Expand All @@ -263,10 +354,13 @@ def set_html_body(self, body: str) -> None:
self.data["body"]["html"] = body

def add_attachment(self, attachment: Attachment) -> None:
"""Seek! Name must not have / in it, esp fails in this case."""
if "/" in attachment.name:
raise AnymailConfigurationError("found '/' in attachment name")
att = {
"content": attachment.b64content,
"type": attachment.mimetype,
"name": attachment.name or "", # required -- submit empty string if unknown
"name": attachment.name or "", # required - submit empty string if unknown
}
if attachment.inline:
self.data.setdefault("inline_attachments", []).append(att)
Expand Down
9 changes: 5 additions & 4 deletions anymail/webhooks/unisender_go.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@
class UnisenderGoTrackingWebhookView(AnymailCoreWebhookView):
"""Handler for UniSender delivery and engagement tracking webhooks"""

esp_name = "UnisenderGo"
esp_name = "Unisender Go"
signal = tracking
warn_if_no_basic_auth = False # because we validate against signature

event_types = {
"sent": EventType.SENT,
Expand Down Expand Up @@ -120,7 +121,7 @@ def validate_request(self, request: HttpRequest) -> None:
"""
request_json = json.loads(request.body.decode("utf-8"))
request_auth = request_json.get("auth", "")
request_json["auth"] = settings.ANYMAIL_UNISENDERGO_API_KEY
request_json["auth"] = settings.ANYMAIL_UNISENDER_GO_API_KEY
json_with_key = json.dumps(request_json, separators=(",", ":"))

expected_auth = md5(json_with_key.encode("utf-8")).hexdigest()
Expand Down Expand Up @@ -166,7 +167,7 @@ def esp_to_anymail_event(self, esp_event: dict) -> AnymailTrackingEvent | None:
mta_response=delivery_info.get("destination_response", ""),
tags=None,
metadata=metadata,
click_url=None,
user_agent=delivery_info.get("use_ragent", ""),
click_url=event_data.get("url"),
user_agent=delivery_info.get("user_agent", ""),
esp_event=event_data,
)
130 changes: 112 additions & 18 deletions tests/test_unisender_go_payload.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from django.test import SimpleTestCase, override_settings
from django.test import SimpleTestCase, override_settings, tag

from anymail.backends.unisender_go import EmailBackend, UnisenderGoPayload
from anymail.message import AnymailMessageMixin
Expand All @@ -18,12 +18,10 @@
SUBSTITUTION_TWO = {"arg2": "arg2"}


@tag("unisender_go")
@override_settings(ANYMAIL_UNISENDER_GO_API_KEY=None, ANYMAIL_UNISENDER_GO_API_URL="")
class TestUnisenderGoPayload(SimpleTestCase):
@override_settings(
ANYMAIL_UNISENDERGO_SKIP_UNSUBSCRIBE=False,
ANYMAIL_UNISENDERGO_API_KEY=None,
ANYMAIL_UNISENDERGO_API_URL="",
)
@override_settings(ANYMAIL_UNISENDER_GO_SKIP_UNSUBSCRIBE=False)
def test_unisender_go_payload__full(self):
substitutions = {TO_EMAIL: SUBSTITUTION_ONE, OTHER_TO_EMAIL: SUBSTITUTION_TWO}
email = AnymailMessageMixin(
Expand Down Expand Up @@ -58,11 +56,7 @@ def test_unisender_go_payload__full(self):

self.assertEqual(payload.data, expected_payload)

@override_settings(
ANYMAIL_UNISENDERGO_SKIP_UNSUBSCRIBE=False,
ANYMAIL_UNISENDERGO_API_KEY=None,
ANYMAIL_UNISENDERGO_API_URL="",
)
@override_settings(ANYMAIL_UNISENDER_GO_SKIP_UNSUBSCRIBE=False)
def test_unisender_go_payload__parse_from__with_name(self):
email = AnymailMessageMixin(
subject=SUBJECT,
Expand All @@ -85,9 +79,7 @@ def test_unisender_go_payload__parse_from__with_name(self):
self.assertEqual(payload.data, expected_payload)

@override_settings(
ANYMAIL_UNISENDERGO_SKIP_UNSUBSCRIBE=False,
ANYMAIL_UNISENDERGO_API_KEY=None,
ANYMAIL_UNISENDERGO_API_URL="",
ANYMAIL_UNISENDER_GO_SKIP_UNSUBSCRIBE=False,
)
def test_unisender_go_payload__parse_from__without_name(self):
email = AnymailMessageMixin(
Expand All @@ -111,16 +103,38 @@ def test_unisender_go_payload__parse_from__without_name(self):
self.assertEqual(payload.data, expected_payload)

@override_settings(
ANYMAIL_UNISENDERGO_SKIP_UNSUBSCRIBE=True,
ANYMAIL_UNISENDERGO_API_KEY=None,
ANYMAIL_UNISENDERGO_API_URL="",
ANYMAIL_UNISENDER_GO_SKIP_UNSUBSCRIBE=True,
)
def test_unisender_go_payload__parse_from__with_unsub(self):
def test_unisender_go_payload__parse_from__with_unsub__in_settings(self):
email = AnymailMessageMixin(
subject=SUBJECT,
merge_global_data=GLOBAL_DATA,
from_email=f"{FROM_NAME} <{FROM_EMAIL}>",
to=[TO_EMAIL],
)
backend = EmailBackend()

payload = UnisenderGoPayload(message=email, backend=backend, defaults={})
expected_payload = {
"from_email": FROM_EMAIL,
"from_name": FROM_NAME,
"global_substitutions": GLOBAL_DATA,
"headers": {},
"recipients": [{"email": TO_EMAIL, "substitutions": {"to_name": ""}}],
"subject": SUBJECT,
"skip_unsubscribe": 1,
}

self.assertEqual(payload.data, expected_payload)

@override_settings(ANYMAIL_UNISENDER_GO_SKIP_UNSUBSCRIBE=False)
def test_unisender_go_payload__parse_from__with_unsub__in_args(self):
email = AnymailMessageMixin(
subject=SUBJECT,
merge_global_data=GLOBAL_DATA,
from_email=f"{FROM_NAME} <{FROM_EMAIL}>",
to=[TO_EMAIL],
esp_extra={"skip_unsubscribe": 1},
)
backend = EmailBackend()

Expand All @@ -136,3 +150,83 @@ def test_unisender_go_payload__parse_from__with_unsub(self):
}

self.assertEqual(payload.data, expected_payload)

@override_settings(
ANYMAIL_UNISENDER_GO_GLOBAL_LANGUAGE="en",
)
def test_unisender_go_payload__parse_from__global_language__in_settings(self):
email = AnymailMessageMixin(
subject=SUBJECT,
merge_global_data=GLOBAL_DATA,
from_email=f"{FROM_NAME} <{FROM_EMAIL}>",
to=[TO_EMAIL],
)
backend = EmailBackend()

payload = UnisenderGoPayload(message=email, backend=backend, defaults={})
expected_payload = {
"from_email": FROM_EMAIL,
"from_name": FROM_NAME,
"global_substitutions": GLOBAL_DATA,
"headers": {},
"recipients": [{"email": TO_EMAIL, "substitutions": {"to_name": ""}}],
"subject": SUBJECT,
"global_language": "en",
}

self.assertEqual(payload.data, expected_payload)

@override_settings(ANYMAIL_UNISENDER_GO_GLOBAL_LANGUAGE="fr")
def test_unisender_go_payload__parse_from__global_language__in_args(self):
email = AnymailMessageMixin(
subject=SUBJECT,
merge_global_data=GLOBAL_DATA,
from_email=f"{FROM_NAME} <{FROM_EMAIL}>",
to=[TO_EMAIL],
esp_extra={"global_language": "en"},
)
backend = EmailBackend()

payload = UnisenderGoPayload(message=email, backend=backend, defaults={})
expected_payload = {
"from_email": FROM_EMAIL,
"from_name": FROM_NAME,
"global_substitutions": GLOBAL_DATA,
"headers": {},
"recipients": [{"email": TO_EMAIL, "substitutions": {"to_name": ""}}],
"subject": SUBJECT,
"global_language": "en",
}

self.assertEqual(payload.data, expected_payload)

def test_unisender_go_payload__parse_from__bypass_esp_extra(self):
email = AnymailMessageMixin(
subject=SUBJECT,
merge_global_data=GLOBAL_DATA,
from_email=f"{FROM_NAME} <{FROM_EMAIL}>",
to=[TO_EMAIL],
esp_extra={
"bypass_global": 1,
"bypass_unavailable": 1,
"bypass_unsubscribed": 1,
"bypass_complained": 1,
},
)
backend = EmailBackend()

payload = UnisenderGoPayload(message=email, backend=backend, defaults={})
expected_payload = {
"from_email": FROM_EMAIL,
"from_name": FROM_NAME,
"global_substitutions": GLOBAL_DATA,
"headers": {},
"recipients": [{"email": TO_EMAIL, "substitutions": {"to_name": ""}}],
"subject": SUBJECT,
"bypass_global": 1,
"bypass_unavailable": 1,
"bypass_unsubscribed": 1,
"bypass_complained": 1,
}

self.assertEqual(payload.data, expected_payload)
Loading

0 comments on commit 80f382f

Please sign in to comment.