SendGrid: change message_id from Message-ID/smtp-id to UUID anymail_id

SendGrid does not always correctly provide the sent Message-ID header value 
to a tracking webhook's smtp-id field, making it unreliable to use for Anymail's 
`message_id`.

Instead, generate a UUID `message_id` for Anymail tracking, and pass it from 
send to webhooks in SendGrid custom args as anymail_id.

Webhooks will fall back to smtp-id for compatibility with previously-sent 
messages that didn't have an anymail_id custom arg.

Fixes #108
This commit is contained in:
Josh Kersey
2018-05-30 13:52:36 -05:00
committed by Mike Edmunds
parent 51d2a404c0
commit d8d1407c61
7 changed files with 52 additions and 86 deletions

View File

@@ -1,7 +1,7 @@
import uuid
from email.utils import quote as rfc822_quote
import warnings
from django.core.mail import make_msgid
from requests.structures import CaseInsensitiveDict
from .base_requests import AnymailRequestsBackend, RequestsPayload
@@ -99,7 +99,7 @@ class SendGridPayload(RequestsPayload):
"""Performs any necessary serialization on self.data, and returns the result."""
if self.generate_message_id:
self.ensure_message_id()
self.set_anymail_id()
self.build_merge_data()
if not self.data["headers"]:
@@ -107,28 +107,11 @@ class SendGridPayload(RequestsPayload):
return self.serialize_json(self.data)
def ensure_message_id(self):
"""Ensure message has a known Message-ID for later event tracking"""
if "Message-ID" not in self.data["headers"]:
# Only make our own if caller hasn't already provided one
self.data["headers"]["Message-ID"] = self.make_message_id()
self.message_id = self.data["headers"]["Message-ID"]
def set_anymail_id(self):
"""Ensure message has a known anymail_id for later event tracking"""
# Workaround for missing message ID (smtp-id) in SendGrid engagement events
# (click and open tracking): because unique_args get merged into the raw event
# record, we can supply the 'smtp-id' field for any events missing it.
self.data.setdefault("custom_args", {})["smtp-id"] = self.message_id
def make_message_id(self):
"""Returns a Message-ID that could be used for this payload
Tries to use the from_email's domain as the Message-ID's domain
"""
try:
_, domain = self.data["from"]["email"].split("@")
except (AttributeError, KeyError, TypeError, ValueError):
domain = None
return make_msgid(domain=domain)
self.message_id = str(uuid.uuid4())
self.data.setdefault("custom_args", {})["anymail_id"] = self.message_id
def build_merge_data(self):
"""Set personalizations[...]['substitutions'] and data['sections']"""

View File

@@ -1,6 +1,6 @@
import uuid
import warnings
from django.core.mail import make_msgid
from requests.structures import CaseInsensitiveDict
from ..exceptions import AnymailConfigurationError, AnymailRequestsAPIError, AnymailWarning
@@ -99,7 +99,7 @@ class SendGridPayload(RequestsPayload):
"""Performs any necessary serialization on self.data, and returns the result."""
if self.generate_message_id:
self.ensure_message_id()
self.set_anymail_id()
self.build_merge_data()
if self.merge_data is not None:
@@ -136,29 +136,11 @@ class SendGridPayload(RequestsPayload):
return self.data
def ensure_message_id(self):
"""Ensure message has a known Message-ID for later event tracking"""
headers = self.data["headers"]
if "Message-ID" not in headers:
# Only make our own if caller hasn't already provided one
headers["Message-ID"] = self.make_message_id()
self.message_id = headers["Message-ID"]
def set_anymail_id(self):
"""Ensure message has a known anymail_id for later event tracking"""
# Workaround for missing message ID (smtp-id) in SendGrid engagement events
# (click and open tracking): because unique_args get merged into the raw event
# record, we can supply the 'smtp-id' field for any events missing it.
self.smtpapi.setdefault('unique_args', {})['smtp-id'] = self.message_id
def make_message_id(self):
"""Returns a Message-ID that could be used for this payload
Tries to use the from_email's domain as the Message-ID's domain
"""
try:
_, domain = self.data["from"].split("@")
except (AttributeError, KeyError, TypeError, ValueError):
domain = None
return make_msgid(domain=domain)
self.message_id = str(uuid.uuid4())
self.smtpapi.setdefault('unique_args', {})["anymail_id"] = self.message_id
def build_merge_data(self):
"""Set smtpapi['sub'] and ['section']"""