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
This commit is contained in:
medmunds
2017-09-14 11:45:17 -07:00
parent 168d46a254
commit 3b9cb963ef
5 changed files with 25 additions and 9 deletions

View File

@@ -7,7 +7,7 @@ from requests.structures import CaseInsensitiveDict
from .base_requests import AnymailRequestsBackend, RequestsPayload from .base_requests import AnymailRequestsBackend, RequestsPayload
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning
from ..message import AnymailRecipientStatus 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): class EmailBackend(AnymailRequestsBackend):
@@ -240,7 +240,7 @@ class SendGridPayload(RequestsPayload):
# SendGrid requires header values to be strings -- not integers. # SendGrid requires header values to be strings -- not integers.
# We'll stringify ints and floats; anything else is the caller's responsibility. # We'll stringify ints and floats; anything else is the caller's responsibility.
self.data["headers"].update({ 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() for k, v in headers.items()
}) })
@@ -290,7 +290,7 @@ class SendGridPayload(RequestsPayload):
# if they're not.) # if they're not.)
# We'll stringify ints and floats; anything else is the caller's responsibility. # We'll stringify ints and floats; anything else is the caller's responsibility.
self.data["custom_args"] = { 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() for k, v in metadata.items()
} }

View File

@@ -5,7 +5,7 @@ from requests.structures import CaseInsensitiveDict
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning
from ..message import AnymailRecipientStatus 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 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. # We'll stringify ints and floats; anything else is the caller's responsibility.
# (This field gets converted to json in self.serialize_data) # (This field gets converted to json in self.serialize_data)
self.data["headers"].update({ 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() for k, v in headers.items()
}) })

View File

@@ -18,6 +18,10 @@ from six.moves.urllib.parse import urlsplit, urlunsplit
from .exceptions import AnymailConfigurationError, AnymailInvalidAddress 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 UNSET = object() # Used as non-None default value

View File

@@ -7,6 +7,7 @@ from decimal import Decimal
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
import six
from django.core import mail from django.core import mail
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.utils import override_settings 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 .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin 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', @override_settings(EMAIL_BACKEND='anymail.backends.sendgrid.EmailBackend',
ANYMAIL={'SENDGRID_API_KEY': 'test_api_key'}) 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}) self.assertEqual(data['content'][0], {'type': "text/html", 'value': html_content})
def test_extra_headers(self): 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" <noreply@example.com>'} 'Reply-To': '"Do Not Reply" <noreply@example.com>'}
self.message.send() self.message.send()
data = self.get_api_call_json() data = self.get_api_call_json()
self.assertEqual(data['headers']['X-Custom'], 'string') 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-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 # Reply-To must be moved to separate param
self.assertNotIn('Reply-To', data['headers']) self.assertNotIn('Reply-To', data['headers'])
self.assertEqual(data['reply_to'], {'name': "Do Not Reply", 'email': "noreply@example.com"}) 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""" """Test backend support for Anymail added features"""
def test_metadata(self): 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() self.message.send()
data = self.get_api_call_json() data = self.get_api_call_json()
data['custom_args'].pop('smtp-id', None) # remove Message-ID we added as tracking workaround data['custom_args'].pop('smtp-id', None) # remove Message-ID we added as tracking workaround
self.assertEqual(data['custom_args'], {'user_id': "12345", 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): def test_send_at(self):
utc_plus_6 = get_fixed_timezone(6 * 60) utc_plus_6 = get_fixed_timezone(6 * 60)

View File

@@ -7,6 +7,7 @@ from decimal import Decimal
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
import six
from django.core import mail from django.core import mail
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.test import SimpleTestCase 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 .mock_requests_backend import RequestsBackendMockAPITestCase, SessionSharingTestCasesMixin
from .utils import sample_image_content, sample_image_path, SAMPLE_IMAGE_FILENAME, AnymailTestMixin 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', @override_settings(EMAIL_BACKEND='anymail.backends.sendgrid_v2.EmailBackend',
ANYMAIL={'SENDGRID_API_KEY': 'test_api_key'}) ANYMAIL={'SENDGRID_API_KEY': 'test_api_key'})
@@ -154,12 +158,13 @@ class SendGridBackendStandardEmailTests(SendGridBackendMockAPITestCase):
self.assertEqual(data['html'], html_content) self.assertEqual(data['html'], html_content)
def test_extra_headers(self): 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() self.message.send()
data = self.get_api_call_data() data = self.get_api_call_data()
headers = json.loads(data['headers']) headers = json.loads(data['headers'])
self.assertEqual(headers['X-Custom'], 'string') self.assertEqual(headers['X-Custom'], 'string')
self.assertEqual(headers['X-Num'], '123') # number converted to string (per SendGrid requirement) 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): def test_extra_headers_serialization_error(self):
self.message.extra_headers = {'X-Custom': Decimal(12.5)} self.message.extra_headers = {'X-Custom': Decimal(12.5)}