From 3b9cb963efce875ac9232ba13022e39bfb1a45e9 Mon Sep 17 00:00:00 2001 From: medmunds Date: Thu, 14 Sep 2017 11:45:17 -0700 Subject: [PATCH] SendGrid: convert long to str in headers, metadata SendGrid requires extra headers and metadata values be strings. Anymail has always coerced int and float; this treats Python 2's `long` integer type the same. Fixes #74 --- anymail/backends/sendgrid.py | 6 +++--- anymail/backends/sendgrid_v2.py | 4 ++-- anymail/utils.py | 4 ++++ tests/test_sendgrid_backend.py | 13 ++++++++++--- tests/test_sendgrid_v2_backend.py | 7 ++++++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/anymail/backends/sendgrid.py b/anymail/backends/sendgrid.py index d345e81..1e702d5 100644 --- a/anymail/backends/sendgrid.py +++ b/anymail/backends/sendgrid.py @@ -7,7 +7,7 @@ from requests.structures import CaseInsensitiveDict from .base_requests import AnymailRequestsBackend, RequestsPayload from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning from ..message import AnymailRecipientStatus -from ..utils import get_anymail_setting, timestamp, update_deep, parse_address_list +from ..utils import BASIC_NUMERIC_TYPES, get_anymail_setting, timestamp, update_deep, parse_address_list class EmailBackend(AnymailRequestsBackend): @@ -240,7 +240,7 @@ class SendGridPayload(RequestsPayload): # SendGrid requires header values to be strings -- not integers. # We'll stringify ints and floats; anything else is the caller's responsibility. self.data["headers"].update({ - k: str(v) if isinstance(v, (int, float)) else v + k: str(v) if isinstance(v, BASIC_NUMERIC_TYPES) else v for k, v in headers.items() }) @@ -290,7 +290,7 @@ class SendGridPayload(RequestsPayload): # if they're not.) # We'll stringify ints and floats; anything else is the caller's responsibility. self.data["custom_args"] = { - k: str(v) if isinstance(v, (int, float)) else v + k: str(v) if isinstance(v, BASIC_NUMERIC_TYPES) else v for k, v in metadata.items() } diff --git a/anymail/backends/sendgrid_v2.py b/anymail/backends/sendgrid_v2.py index c045a6f..889b763 100644 --- a/anymail/backends/sendgrid_v2.py +++ b/anymail/backends/sendgrid_v2.py @@ -5,7 +5,7 @@ from requests.structures import CaseInsensitiveDict from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning from ..message import AnymailRecipientStatus -from ..utils import get_anymail_setting, timestamp +from ..utils import BASIC_NUMERIC_TYPES, get_anymail_setting, timestamp from .base_requests import AnymailRequestsBackend, RequestsPayload @@ -238,7 +238,7 @@ class SendGridPayload(RequestsPayload): # We'll stringify ints and floats; anything else is the caller's responsibility. # (This field gets converted to json in self.serialize_data) self.data["headers"].update({ - k: str(v) if isinstance(v, (int, float)) else v + k: str(v) if isinstance(v, BASIC_NUMERIC_TYPES) else v for k, v in headers.items() }) diff --git a/anymail/utils.py b/anymail/utils.py index d245f66..7241d08 100644 --- a/anymail/utils.py +++ b/anymail/utils.py @@ -18,6 +18,10 @@ from six.moves.urllib.parse import urlsplit, urlunsplit from .exceptions import AnymailConfigurationError, AnymailInvalidAddress + +BASIC_NUMERIC_TYPES = six.integer_types + (float,) # int, float, and (on Python 2) long + + UNSET = object() # Used as non-None default value diff --git a/tests/test_sendgrid_backend.py b/tests/test_sendgrid_backend.py index 731380d..498087d 100644 --- a/tests/test_sendgrid_backend.py +++ b/tests/test_sendgrid_backend.py @@ -7,6 +7,7 @@ from decimal import Decimal from email.mime.base import MIMEBase from email.mime.image import MIMEImage +import six from django.core import mail from django.test import SimpleTestCase from django.test.utils import override_settings @@ -19,6 +20,9 @@ from anymail.message import attach_inline_image_file from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin +# noinspection PyUnresolvedReferences +longtype = int if six.PY3 else long + @override_settings(EMAIL_BACKEND='anymail.backends.sendgrid.EmailBackend', ANYMAIL={'SENDGRID_API_KEY': 'test_api_key'}) @@ -144,12 +148,13 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase): self.assertEqual(data['content'][0], {'type': "text/html", 'value': html_content}) def test_extra_headers(self): - self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123, + self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123, 'X-Long': longtype(123), 'Reply-To': '"Do Not Reply" '} self.message.send() data = self.get_api_call_json() self.assertEqual(data['headers']['X-Custom'], 'string') self.assertEqual(data['headers']['X-Num'], '123') # converted to string (undoc'd SendGrid requirement) + self.assertEqual(data['headers']['X-Long'], '123') # converted to string (undoc'd SendGrid requirement) # Reply-To must be moved to separate param self.assertNotIn('Reply-To', data['headers']) self.assertEqual(data['reply_to'], {'name': "Do Not Reply", 'email': "noreply@example.com"}) @@ -331,12 +336,14 @@ class SendGridBackendAnymailFeatureTests(SendGridBackendMockAPITestCase): """Test backend support for Anymail added features""" def test_metadata(self): - self.message.metadata = {'user_id': "12345", 'items': 6} + self.message.metadata = {'user_id': "12345", 'items': 6, 'float': 98.6, 'long': longtype(123)} self.message.send() data = self.get_api_call_json() data['custom_args'].pop('smtp-id', None) # remove Message-ID we added as tracking workaround self.assertEqual(data['custom_args'], {'user_id': "12345", - 'items': "6"}) # number converted to string + 'items': "6", # int converted to a string, + 'float': "98.6", # float converted to a string (watch binary rounding!) + 'long': "123"}) # long converted to string def test_send_at(self): utc_plus_6 = get_fixed_timezone(6 * 60) diff --git a/tests/test_sendgrid_v2_backend.py b/tests/test_sendgrid_v2_backend.py index 22ec41b..e2d6c7a 100644 --- a/tests/test_sendgrid_v2_backend.py +++ b/tests/test_sendgrid_v2_backend.py @@ -7,6 +7,7 @@ from decimal import Decimal from email.mime.base import MIMEBase from email.mime.image import MIMEImage +import six from django.core import mail from django.core.exceptions import ImproperlyConfigured from django.test import SimpleTestCase @@ -19,6 +20,9 @@ from anymail.message import attach_inline_image_file from .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin +# noinspection PyUnresolvedReferences +longtype = int if six.PY3 else long + @override_settings(EMAIL_BACKEND='anymail.backends.sendgrid_v2.EmailBackend', ANYMAIL={'SENDGRID_API_KEY': 'test_api_key'}) @@ -154,12 +158,13 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase): self.assertEqual(data['html'], html_content) def test_extra_headers(self): - self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123} + self.message.extra_headers = {'X-Custom': 'string', 'X-Num': 123, 'X-Long': longtype(123)} self.message.send() data = self.get_api_call_data() headers = json.loads(data['headers']) self.assertEqual(headers['X-Custom'], 'string') self.assertEqual(headers['X-Num'], '123') # number converted to string (per SendGrid requirement) + self.assertEqual(headers['X-Long'], '123') # number converted to string (per SendGrid requirement) def test_extra_headers_serialization_error(self): self.message.extra_headers = {'X-Custom': Decimal(12.5)}