[Python-modules-commits] [django-anymail] 01/05: Imported Upstream version 0.11.1
Scott Kitterman
kitterman at moszumanska.debian.org
Sun Aug 20 19:16:49 UTC 2017
This is an automated email from the git hooks/post-receive script.
kitterman pushed a commit to branch debian/master
in repository django-anymail.
commit ad3420de144ad891f824d18c1c596c5803dc5fb8
Author: Scott Kitterman <scott at kitterman.com>
Date: Sun Aug 20 14:53:38 2017 -0400
Imported Upstream version 0.11.1
---
AUTHORS.txt | 2 +
PKG-INFO | 28 ++--
README.rst | 8 +-
anymail/_version.py | 2 +-
anymail/backends/mailjet.py | 258 +++++++++++++++++++++++++++++++++++
anymail/exceptions.py | 12 +-
anymail/signals.py | 4 +-
anymail/urls.py | 2 +
anymail/webhooks/mailgun.py | 6 +-
anymail/webhooks/mailjet.py | 97 +++++++++++++
anymail/webhooks/mandrill.py | 4 +-
anymail/webhooks/postmark.py | 2 +-
anymail/webhooks/sendgrid.py | 4 +-
anymail/webhooks/sparkpost.py | 4 +-
django_anymail.egg-info/PKG-INFO | 28 ++--
django_anymail.egg-info/SOURCES.txt | 2 +
django_anymail.egg-info/requires.txt | 2 +
setup.py | 6 +-
18 files changed, 423 insertions(+), 48 deletions(-)
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 64dd87d..28ca498 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -2,6 +2,8 @@ Anymail
=======
Mike Edmunds
+Calvin Jeong
+Peter Wu
Anymail was forked from Djrill, which included contributions from:
diff --git a/PKG-INFO b/PKG-INFO
index 5c8c01a..dcf8bd6 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,13 +1,13 @@
Metadata-Version: 1.1
Name: django-anymail
-Version: 0.10
-Summary: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and other transactional ESPs
+Version: 0.11.1
+Summary: Django email backends for Mailgun, Mailjet, Postmark, SendGrid, SparkPost and other transactional ESPs
Home-page: https://github.com/anymail/django-anymail
Author: Mike Edmunds <medmunds at gmail.com>
Author-email: medmunds at gmail.com
License: BSD License
-Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and more
- ==================================================================================
+Description: Anymail: Django email backends for Mailgun, Mailjet, Postmark, SendGrid, SparkPost and more
+ ===========================================================================================
**PRE-1.0**
@@ -40,14 +40,14 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
with a consistent API that lets you use ESP-added features without locking your code
to a particular ESP.
- It currently fully supports Mailgun, Postmark, SendGrid, and SparkPost,
+ It currently fully supports Mailgun, Mailjet, Postmark, SendGrid, and SparkPost,
and has limited support for Mandrill.
Anymail normalizes ESP functionality so it "just works" with Django's
built-in `django.core.mail` package. It includes:
* Support for HTML, attachments, extra headers, and other features of
- `Django's built-in email <https://docs.djangoproject.com/en/v0.10/topics/email/>`_
+ `Django's built-in email <https://docs.djangoproject.com/en/v0.11.1/topics/email/>`_
* Extensions that make it easy to use extra ESP functionality, like tags, metadata,
and tracking, with code that's portable between ESPs
* Simplified inline images for HTML email
@@ -61,17 +61,17 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. END shared-intro
- .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.10
+ .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.11.1
:target: https://travis-ci.org/anymail/django-anymail
:alt: build status on Travis-CI
- .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.10
- :target: https://anymail.readthedocs.io/en/v0.10/
+ .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.11.1
+ :target: https://anymail.readthedocs.io/en/v0.11.1/
:alt: documentation on ReadTheDocs
**Resources**
- * Full documentation: https://anymail.readthedocs.io/en/v0.10/
+ * Full documentation: https://anymail.readthedocs.io/en/v0.11.1/
* Package on PyPI: https://pypi.python.org/pypi/django-anymail
* Project on Github: https://github.com/anymail/django-anymail
@@ -83,7 +83,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. This quickstart section is also included in docs/quickstart.rst
- This example uses Mailgun, but you can substitute Postmark or SendGrid
+ This example uses Mailgun, but you can substitute Mailjet or Postmark or SendGrid
or SparkPost or any other supported ESP where you see "mailgun":
1. Install Anymail from PyPI:
@@ -115,7 +115,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
DEFAULT_FROM_EMAIL = "you at example.com" # if you don't already have this in settings
- 3. Now the regular `Django email functions <https://docs.djangoproject.com/en/v0.10/topics/email/>`_
+ 3. Now the regular `Django email functions <https://docs.djangoproject.com/en/v0.11.1/topics/email/>`_
will send through your chosen ESP:
.. code-block:: python
@@ -159,10 +159,10 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. END quickstart
- See the `full documentation <https://anymail.readthedocs.io/en/v0.10/>`_
+ See the `full documentation <https://anymail.readthedocs.io/en/v0.11.1/>`_
for more features and options.
-Keywords: django,email,email backend,ESP,transactional mail,mailgun,mandrill,postmark,sendgrid
+Keywords: django,email,email backend,ESP,transactional mail,mailgun,mailjet,mandrill,postmark,sendgrid
Platform: UNKNOWN
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Programming Language :: Python
diff --git a/README.rst b/README.rst
index 3cc0245..59f0808 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,5 @@
-Anymail: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and more
-==================================================================================
+Anymail: Django email backends for Mailgun, Mailjet, Postmark, SendGrid, SparkPost and more
+===========================================================================================
**PRE-1.0**
@@ -32,7 +32,7 @@ Anymail integrates several transactional email service providers (ESPs) into Dja
with a consistent API that lets you use ESP-added features without locking your code
to a particular ESP.
-It currently fully supports Mailgun, Postmark, SendGrid, and SparkPost,
+It currently fully supports Mailgun, Mailjet, Postmark, SendGrid, and SparkPost,
and has limited support for Mandrill.
Anymail normalizes ESP functionality so it "just works" with Django's
@@ -75,7 +75,7 @@ Anymail 1-2-3
.. This quickstart section is also included in docs/quickstart.rst
-This example uses Mailgun, but you can substitute Postmark or SendGrid
+This example uses Mailgun, but you can substitute Mailjet or Postmark or SendGrid
or SparkPost or any other supported ESP where you see "mailgun":
1. Install Anymail from PyPI:
diff --git a/anymail/_version.py b/anymail/_version.py
index 68eeb94..d8b8c35 100644
--- a/anymail/_version.py
+++ b/anymail/_version.py
@@ -1,3 +1,3 @@
-VERSION = (0, 10)
+VERSION = (0, 11, 1)
__version__ = '.'.join([str(x) for x in VERSION]) # major.minor.patch or major.minor.devN
__minor_version__ = '.'.join([str(x) for x in VERSION[:2]]) # Sphinx's X.Y "version"
diff --git a/anymail/backends/mailjet.py b/anymail/backends/mailjet.py
new file mode 100644
index 0000000..68ef529
--- /dev/null
+++ b/anymail/backends/mailjet.py
@@ -0,0 +1,258 @@
+from ..exceptions import AnymailRequestsAPIError
+from ..message import AnymailRecipientStatus, ANYMAIL_STATUSES
+from ..utils import get_anymail_setting, ParsedEmail, parse_address_list
+
+from .base_requests import AnymailRequestsBackend, RequestsPayload
+
+
+class EmailBackend(AnymailRequestsBackend):
+ """
+ Mailjet API Email Backend
+ """
+
+ esp_name = "Mailjet"
+
+ def __init__(self, **kwargs):
+ """Init options from Django settings"""
+ esp_name = self.esp_name
+ self.api_key = get_anymail_setting('api_key', esp_name=esp_name, kwargs=kwargs, allow_bare=True)
+ self.secret_key = get_anymail_setting('secret_key', esp_name=esp_name, kwargs=kwargs, allow_bare=True)
+ api_url = get_anymail_setting('api_url', esp_name=esp_name, kwargs=kwargs,
+ default="https://api.mailjet.com/v3")
+ if not api_url.endswith("/"):
+ api_url += "/"
+ super(EmailBackend, self).__init__(api_url, **kwargs)
+
+ def build_message_payload(self, message, defaults):
+ return MailjetPayload(message, defaults, self)
+
+ def raise_for_status(self, response, payload, message):
+ # Improve Mailjet's (lack of) error message for bad API key
+ if response.status_code == 401 and not response.content:
+ raise AnymailRequestsAPIError(
+ "Invalid Mailjet API key or secret",
+ email_message=message, payload=payload, response=response, backend=self)
+ super(EmailBackend, self).raise_for_status(response, payload, message)
+
+ def parse_recipient_status(self, response, payload, message):
+ # Mailjet's (v3.0) transactional send API is not covered in their reference docs.
+ # The response appears to be either:
+ # {"Sent": [{"Email": ..., "MessageID": ...}, ...]}
+ # where only successful recipients are included
+ # or if the entire call has failed:
+ # {"ErrorCode": nnn, "Message": ...}
+ parsed_response = self.deserialize_json_response(response, payload, message)
+ if "ErrorCode" in parsed_response:
+ raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response,
+ backend=self)
+
+ recipient_status = {}
+ try:
+ for key in parsed_response:
+ status = key.lower()
+ if status not in ANYMAIL_STATUSES:
+ status = 'unknown'
+
+ for item in parsed_response[key]:
+ message_id = str(item['MessageID'])
+ email = item['Email']
+ recipient_status[email] = AnymailRecipientStatus(message_id=message_id, status=status)
+ except (KeyError, TypeError):
+ raise AnymailRequestsAPIError("Invalid Mailjet API response format",
+ email_message=message, payload=payload, response=response,
+ backend=self)
+ # Make sure we ended up with a status for every original recipient
+ # (Mailjet only communicates "Sent")
+ for recipients in payload.recipients.values():
+ for email in recipients:
+ if email.email not in recipient_status:
+ recipient_status[email.email] = AnymailRecipientStatus(message_id=None, status='unknown')
+
+ return recipient_status
+
+
+class MailjetPayload(RequestsPayload):
+
+ def __init__(self, message, defaults, backend, *args, **kwargs):
+ self.esp_extra = {} # late-bound in serialize_data
+ auth = (backend.api_key, backend.secret_key)
+ http_headers = {
+ 'Content-Type': 'application/json',
+ }
+ # Late binding of recipients and their variables
+ self.recipients = {}
+ self.merge_data = None
+ super(MailjetPayload, self).__init__(message, defaults, backend,
+ auth=auth, headers=http_headers, *args, **kwargs)
+
+ def get_api_endpoint(self):
+ return "send"
+
+ def serialize_data(self):
+ self._finish_recipients()
+ self._populate_sender_from_template()
+ return self.serialize_json(self.data)
+
+ #
+ # Payload construction
+ #
+
+ def _finish_recipients(self):
+ # NOTE do not set both To and Recipients, it behaves specially: each
+ # recipient receives a separate mail but the To address receives one
+ # listing all recipients.
+ if "cc" in self.recipients or "bcc" in self.recipients:
+ self._finish_recipients_single()
+ else:
+ self._finish_recipients_with_vars()
+
+ def _populate_sender_from_template(self):
+ # If no From address was given, use the address from the template.
+ # Unfortunately, API 3.0 requires the From address to be given, so let's
+ # query it when needed. This will supposedly be fixed in 3.1 with a
+ # public beta in May 2017.
+ template_id = self.data.get("Mj-TemplateID")
+ if template_id and not self.data.get("FromEmail"):
+ response = self.backend.session.get(
+ "%sREST/template/%s/detailcontent" % (self.backend.api_url, template_id),
+ auth=self.auth
+ )
+ self.backend.raise_for_status(response, None, self.message)
+ json_response = self.backend.deserialize_json_response(response, None, self.message)
+ # Populate email address header from template.
+ try:
+ headers = json_response["Data"][0]["Headers"]
+ if "From" in headers:
+ # Workaround Mailjet returning malformed From header
+ # if there's a comma in the template's From display-name:
+ from_email = headers["From"].replace(",", "||COMMA||")
+ parsed = parse_address_list([from_email])[0]
+ if parsed.name:
+ parsed.name = parsed.name.replace("||COMMA||", ",")
+ else:
+ name_addr = (headers["SenderName"], headers["SenderEmail"])
+ parsed = ParsedEmail(name_addr)
+ except KeyError:
+ raise AnymailRequestsAPIError("Invalid Mailjet template API response",
+ email_message=self.message, response=response, backend=self.backend)
+ self.set_from_email(parsed)
+
+ def _finish_recipients_with_vars(self):
+ """Send bulk mail with different variables for each mail."""
+ assert "Cc" not in self.data and "Bcc" not in self.data
+ recipients = []
+ merge_data = self.merge_data or {}
+ for email in self.recipients["to"]:
+ recipient = {
+ "Email": email.email,
+ "Name": email.name,
+ "Vars": merge_data.get(email.email)
+ }
+ # Strip out empty Name and Vars
+ recipient = {k: v for k, v in recipient.items() if v}
+ recipients.append(recipient)
+ self.data["Recipients"] = recipients
+
+ def _finish_recipients_single(self):
+ """Send a single mail with some To, Cc and Bcc headers."""
+ assert "Recipients" not in self.data
+ if self.merge_data:
+ # When Cc and Bcc headers are given, then merge data cannot be set.
+ raise NotImplementedError("Cannot set merge data with bcc/cc")
+ for recipient_type, emails in self.recipients.items():
+ # Workaround Mailjet 3.0 bug parsing display-name with commas
+ # (see test_comma_in_display_name in test_mailjet_backend for details)
+ formatted_emails = [
+ email.address if "," not in email.name
+ # else name has a comma, so force it into MIME encoded-word utf-8 syntax:
+ else ParsedEmail((email.name.encode('utf-8'), email.email)).formataddr('utf-8')
+ for email in emails
+ ]
+ self.data[recipient_type.capitalize()] = ", ".join(formatted_emails)
+
+ def init_payload(self):
+ self.data = {
+ }
+
+ def set_from_email(self, email):
+ self.data["FromEmail"] = email.email
+ if email.name:
+ self.data["FromName"] = email.name
+
+ def set_recipients(self, recipient_type, emails):
+ assert recipient_type in ["to", "cc", "bcc"]
+ # Will be handled later in serialize_data
+ if emails:
+ self.recipients[recipient_type] = emails
+
+ def set_subject(self, subject):
+ self.data["Subject"] = subject
+
+ def set_reply_to(self, emails):
+ headers = self.data.setdefault("Headers", {})
+ if emails:
+ headers["Reply-To"] = ", ".join([str(email) for email in emails])
+ elif "Reply-To" in headers:
+ del headers["Reply-To"]
+
+ def set_extra_headers(self, headers):
+ self.data.setdefault("Headers", {}).update(headers)
+
+ def set_text_body(self, body):
+ self.data["Text-part"] = body
+
+ def set_html_body(self, body):
+ if "Html-part" in self.data:
+ # second html body could show up through multiple alternatives, or html body + alternative
+ self.unsupported_feature("multiple html parts")
+
+ self.data["Html-part"] = body
+
+ def add_attachment(self, attachment):
+ if attachment.inline:
+ field = "Inline_attachments"
+ name = attachment.cid
+ else:
+ field = "Attachments"
+ name = attachment.name or ""
+ self.data.setdefault(field, []).append({
+ "Content-type": attachment.mimetype,
+ "Filename": name,
+ "content": attachment.b64content
+ })
+
+ def set_metadata(self, metadata):
+ # Mailjet expects a single string payload
+ self.data["Mj-EventPayLoad"] = self.serialize_json(metadata)
+
+ def set_tags(self, tags):
+ # The choices here are CustomID or Campaign, and Campaign seems closer
+ # to how "tags" are handled by other ESPs -- e.g., you can view dashboard
+ # statistics across all messages with the same Campaign.
+ if len(tags) > 0:
+ self.data["Tag"] = tags[0]
+ self.data["Mj-campaign"] = tags[0]
+ if len(tags) > 1:
+ self.unsupported_feature('multiple tags (%r)' % tags)
+
+ def set_track_clicks(self, track_clicks):
+ # 1 disables tracking, 2 enables tracking
+ self.data["Mj-trackclick"] = 2 if track_clicks else 1
+
+ def set_track_opens(self, track_opens):
+ # 1 disables tracking, 2 enables tracking
+ self.data["Mj-trackopen"] = 2 if track_opens else 1
+
+ def set_template_id(self, template_id):
+ self.data["Mj-TemplateID"] = template_id
+ self.data["Mj-TemplateLanguage"] = True
+
+ def set_merge_data(self, merge_data):
+ # Will be handled later in serialize_data
+ self.merge_data = merge_data
+
+ def set_merge_global_data(self, merge_global_data):
+ self.data["Vars"] = merge_global_data
+
+ def set_esp_extra(self, extra):
+ self.data.update(extra)
diff --git a/anymail/exceptions.py b/anymail/exceptions.py
index d448fa8..ceab253 100644
--- a/anymail/exceptions.py
+++ b/anymail/exceptions.py
@@ -1,6 +1,7 @@
import json
from traceback import format_exception_only
+import six
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
from requests import HTTPError
@@ -65,7 +66,16 @@ class AnymailError(Exception):
"""Return a formatted string of self.status_code and response, or None"""
if self.status_code is None:
return None
- description = "%s API response %d:" % (self.esp_name or "ESP", self.status_code)
+
+ # Decode response.reason to text -- borrowed from requests.Response.raise_for_status:
+ reason = self.response.reason
+ if isinstance(reason, six.binary_type):
+ try:
+ reason = reason.decode('utf-8')
+ except UnicodeDecodeError:
+ reason = reason.decode('iso-8859-1')
+
+ description = "%s API response %d: %s" % (self.esp_name or "ESP", self.status_code, reason)
try:
json_response = self.response.json()
description += "\n" + json.dumps(json_response, indent=2)
diff --git a/anymail/signals.py b/anymail/signals.py
index 273b939..c5231a8 100644
--- a/anymail/signals.py
+++ b/anymail/signals.py
@@ -32,11 +32,11 @@ class AnymailTrackingEvent(AnymailEvent):
self.click_url = kwargs.pop('click_url', None) # str
self.description = kwargs.pop('description', None) # str, usually human-readable, not normalized
self.message_id = kwargs.pop('message_id', None) # str, format may vary
- self.metadata = kwargs.pop('metadata', None) # dict
+ self.metadata = kwargs.pop('metadata', {}) # dict
self.mta_response = kwargs.pop('mta_response', None) # str, may include SMTP codes, not normalized
self.recipient = kwargs.pop('recipient', None) # str email address (just the email portion; no name)
self.reject_reason = kwargs.pop('reject_reason', None) # normalized to a RejectReason str
- self.tags = kwargs.pop('tags', None) # list of str
+ self.tags = kwargs.pop('tags', []) # list of str
self.user_agent = kwargs.pop('user_agent', None) # str
diff --git a/anymail/urls.py b/anymail/urls.py
index 5a22094..e3e6cb5 100644
--- a/anymail/urls.py
+++ b/anymail/urls.py
@@ -1,6 +1,7 @@
from django.conf.urls import url
from .webhooks.mailgun import MailgunTrackingWebhookView
+from .webhooks.mailjet import MailjetTrackingWebhookView
from .webhooks.mandrill import MandrillTrackingWebhookView
from .webhooks.postmark import PostmarkTrackingWebhookView
from .webhooks.sendgrid import SendGridTrackingWebhookView
@@ -10,6 +11,7 @@ from .webhooks.sparkpost import SparkPostTrackingWebhookView
app_name = 'anymail'
urlpatterns = [
url(r'^mailgun/tracking/$', MailgunTrackingWebhookView.as_view(), name='mailgun_tracking_webhook'),
+ url(r'^mailjet/tracking/$', MailjetTrackingWebhookView.as_view(), name='mailjet_tracking_webhook'),
url(r'^mandrill/tracking/$', MandrillTrackingWebhookView.as_view(), name='mandrill_tracking_webhook'),
url(r'^postmark/tracking/$', PostmarkTrackingWebhookView.as_view(), name='postmark_tracking_webhook'),
url(r'^sendgrid/tracking/$', SendGridTrackingWebhookView.as_view(), name='sendgrid_tracking_webhook'),
diff --git a/anymail/webhooks/mailgun.py b/anymail/webhooks/mailgun.py
index 94026ca..cd3de0f 100644
--- a/anymail/webhooks/mailgun.py
+++ b/anymail/webhooks/mailgun.py
@@ -113,7 +113,7 @@ class MailgunTrackingWebhookView(MailgunBaseWebhookView):
try:
headers = json.loads(esp_event['message-headers'])
except (KeyError, ):
- metadata = None
+ metadata = {}
else:
variables = [value for [field, value] in headers
if field == 'X-Mailgun-Variables']
@@ -121,10 +121,10 @@ class MailgunTrackingWebhookView(MailgunBaseWebhookView):
# Each X-Mailgun-Variables value is JSON. Parse and merge them all into single dict:
metadata = combine(*[json.loads(value) for value in variables])
else:
- metadata = None
+ metadata = {}
# tags are sometimes delivered as X-Mailgun-Tag fields, sometimes as tag
- tags = esp_event.getlist('tag', esp_event.getlist('X-Mailgun-Tag', None))
+ tags = esp_event.getlist('tag', esp_event.getlist('X-Mailgun-Tag', []))
return AnymailTrackingEvent(
event_type=event_type,
diff --git a/anymail/webhooks/mailjet.py b/anymail/webhooks/mailjet.py
new file mode 100644
index 0000000..bb8c5da
--- /dev/null
+++ b/anymail/webhooks/mailjet.py
@@ -0,0 +1,97 @@
+import json
+from datetime import datetime
+
+from django.utils.timezone import utc
+
+from .base import AnymailBaseWebhookView
+from ..signals import tracking, AnymailTrackingEvent, EventType, RejectReason
+
+
+class MailjetTrackingWebhookView(AnymailBaseWebhookView):
+ """Handler for Mailjet delivery and engagement tracking webhooks"""
+
+ signal = tracking
+
+ def parse_events(self, request):
+ esp_events = json.loads(request.body.decode('utf-8'))
+ return [self.esp_to_anymail_event(esp_event) for esp_event in esp_events]
+
+ # https://dev.mailjet.com/guides/#events
+ event_types = {
+ # Map Mailjet event: Anymail normalized type
+ 'sent': EventType.DELIVERED, # accepted by receiving MTA
+ 'open': EventType.OPENED,
+ 'click': EventType.CLICKED,
+ 'bounce': EventType.BOUNCED,
+ 'blocked': EventType.REJECTED,
+ 'spam': EventType.COMPLAINED,
+ 'unsub': EventType.UNSUBSCRIBED,
+ }
+
+ reject_reasons = {
+ # Map Mailjet error strings to Anymail normalized reject_reason
+ # error_related_to: recipient
+ 'user unknown': RejectReason.BOUNCED,
+ 'mailbox inactive': RejectReason.BOUNCED,
+ 'quota exceeded': RejectReason.BOUNCED,
+ 'blacklisted': RejectReason.BLOCKED, # might also be previous unsubscribe
+ 'spam reporter': RejectReason.SPAM,
+ # error_related_to: domain
+ 'invalid domain': RejectReason.BOUNCED,
+ 'no mail host': RejectReason.BOUNCED,
+ 'relay/access denied': RejectReason.BOUNCED,
+ 'greylisted': RejectReason.OTHER, # see special handling below
+ 'typofix': RejectReason.INVALID,
+ # error_related_to: spam (all Mailjet policy/filtering; see above for spam complaints)
+ 'sender blocked': RejectReason.BLOCKED,
+ 'content blocked': RejectReason.BLOCKED,
+ 'policy issue': RejectReason.BLOCKED,
+ # error_related_to: mailjet
+ 'preblocked': RejectReason.BLOCKED,
+ 'duplicate in campaign': RejectReason.OTHER,
+ }
+
+ def esp_to_anymail_event(self, esp_event):
+ event_type = self.event_types.get(esp_event['event'], EventType.UNKNOWN)
+ if esp_event.get('error', None) == 'greylisted' and not esp_event.get('hard_bounce', False):
+ # "This is a temporary error due to possible unrecognised senders. Delivery will be re-attempted."
+ event_type = EventType.DEFERRED
+
+ try:
+ timestamp = datetime.fromtimestamp(esp_event['time'], tz=utc)
+ except (KeyError, ValueError):
+ timestamp = None
+
+ try:
+ # convert bigint MessageID to str to match backend AnymailRecipientStatus
+ message_id = str(esp_event['MessageID'])
+ except (KeyError, TypeError):
+ message_id = None
+
+ if 'error' in esp_event:
+ reject_reason = self.reject_reasons.get(esp_event['error'], RejectReason.OTHER)
+ else:
+ reject_reason = None
+
+ tag = esp_event.get('customcampaign', None)
+ tags = [tag] if tag else []
+
+ try:
+ metadata = json.loads(esp_event['Payload'])
+ except (KeyError, ValueError):
+ metadata = {}
+
+ return AnymailTrackingEvent(
+ event_type=event_type,
+ timestamp=timestamp,
+ message_id=message_id,
+ event_id=None,
+ recipient=esp_event.get('email', None),
+ reject_reason=reject_reason,
+ mta_response=esp_event.get('smtp_reply', None),
+ tags=tags,
+ metadata=metadata,
+ click_url=esp_event.get('url', None),
+ user_agent=esp_event.get('agent', None),
+ esp_event=esp_event,
+ )
diff --git a/anymail/webhooks/mandrill.py b/anymail/webhooks/mandrill.py
index 0cd3685..3d5257a 100644
--- a/anymail/webhooks/mandrill.py
+++ b/anymail/webhooks/mandrill.py
@@ -128,12 +128,12 @@ class MandrillTrackingWebhookView(MandrillBaseWebhookView):
try:
metadata = esp_event['msg']['metadata']
except KeyError:
- metadata = None
+ metadata = {}
try:
tags = esp_event['msg']['tags']
except KeyError:
- tags = None
+ tags = []
return AnymailTrackingEvent(
click_url=esp_event.get('url', None),
diff --git a/anymail/webhooks/postmark.py b/anymail/webhooks/postmark.py
index a63c181..672029c 100644
--- a/anymail/webhooks/postmark.py
+++ b/anymail/webhooks/postmark.py
@@ -89,7 +89,7 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView):
try:
tags = [esp_event['Tag']]
except KeyError:
- tags = None
+ tags = []
return AnymailTrackingEvent(
description=esp_event.get('Description', None),
diff --git a/anymail/webhooks/sendgrid.py b/anymail/webhooks/sendgrid.py
index 9f52c97..0bd22d2 100644
--- a/anymail/webhooks/sendgrid.py
+++ b/anymail/webhooks/sendgrid.py
@@ -72,7 +72,7 @@ class SendGridTrackingWebhookView(SendGridBaseWebhookView):
if len(metadata_keys) > 0:
metadata = {key: esp_event[key] for key in metadata_keys}
else:
- metadata = None
+ metadata = {}
return AnymailTrackingEvent(
event_type=event_type,
@@ -82,7 +82,7 @@ class SendGridTrackingWebhookView(SendGridBaseWebhookView):
recipient=esp_event.get('email', None),
reject_reason=reject_reason,
mta_response=mta_response,
- tags=esp_event.get('category', None),
+ tags=esp_event.get('category', []),
metadata=metadata,
click_url=esp_event.get('url', None),
user_agent=esp_event.get('useragent', None),
diff --git a/anymail/webhooks/sparkpost.py b/anymail/webhooks/sparkpost.py
index aeb3b37..b958646 100644
--- a/anymail/webhooks/sparkpost.py
+++ b/anymail/webhooks/sparkpost.py
@@ -108,7 +108,7 @@ class SparkPostTrackingWebhookView(SparkPostBaseWebhookView):
tag = event['campaign_id'] # not 'rcpt_tags' -- those don't come from sending a message
tags = [tag] if tag else None
except KeyError:
- tags = None
+ tags = []
try:
reject_reason = self.reject_reasons.get(event['bounce_class'], RejectReason.OTHER)
@@ -129,7 +129,7 @@ class SparkPostTrackingWebhookView(SparkPostBaseWebhookView):
mta_response=event.get('raw_reason', None),
# description=???,
tags=tags,
- metadata=event.get('rcpt_meta', None) or None, # message + recipient metadata
+ metadata=event.get('rcpt_meta', None) or {}, # message + recipient metadata
click_url=event.get('target_link_url', None),
user_agent=event.get('user_agent', None),
esp_event=raw_event,
diff --git a/django_anymail.egg-info/PKG-INFO b/django_anymail.egg-info/PKG-INFO
index 5c8c01a..dcf8bd6 100644
--- a/django_anymail.egg-info/PKG-INFO
+++ b/django_anymail.egg-info/PKG-INFO
@@ -1,13 +1,13 @@
Metadata-Version: 1.1
Name: django-anymail
-Version: 0.10
-Summary: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and other transactional ESPs
+Version: 0.11.1
+Summary: Django email backends for Mailgun, Mailjet, Postmark, SendGrid, SparkPost and other transactional ESPs
Home-page: https://github.com/anymail/django-anymail
Author: Mike Edmunds <medmunds at gmail.com>
Author-email: medmunds at gmail.com
License: BSD License
-Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and more
- ==================================================================================
+Description: Anymail: Django email backends for Mailgun, Mailjet, Postmark, SendGrid, SparkPost and more
+ ===========================================================================================
**PRE-1.0**
@@ -40,14 +40,14 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
with a consistent API that lets you use ESP-added features without locking your code
to a particular ESP.
- It currently fully supports Mailgun, Postmark, SendGrid, and SparkPost,
+ It currently fully supports Mailgun, Mailjet, Postmark, SendGrid, and SparkPost,
and has limited support for Mandrill.
Anymail normalizes ESP functionality so it "just works" with Django's
built-in `django.core.mail` package. It includes:
* Support for HTML, attachments, extra headers, and other features of
- `Django's built-in email <https://docs.djangoproject.com/en/v0.10/topics/email/>`_
+ `Django's built-in email <https://docs.djangoproject.com/en/v0.11.1/topics/email/>`_
* Extensions that make it easy to use extra ESP functionality, like tags, metadata,
and tracking, with code that's portable between ESPs
* Simplified inline images for HTML email
@@ -61,17 +61,17 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. END shared-intro
- .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.10
+ .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.11.1
:target: https://travis-ci.org/anymail/django-anymail
:alt: build status on Travis-CI
- .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.10
- :target: https://anymail.readthedocs.io/en/v0.10/
+ .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.11.1
+ :target: https://anymail.readthedocs.io/en/v0.11.1/
:alt: documentation on ReadTheDocs
**Resources**
- * Full documentation: https://anymail.readthedocs.io/en/v0.10/
+ * Full documentation: https://anymail.readthedocs.io/en/v0.11.1/
* Package on PyPI: https://pypi.python.org/pypi/django-anymail
* Project on Github: https://github.com/anymail/django-anymail
@@ -83,7 +83,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. This quickstart section is also included in docs/quickstart.rst
- This example uses Mailgun, but you can substitute Postmark or SendGrid
+ This example uses Mailgun, but you can substitute Mailjet or Postmark or SendGrid
or SparkPost or any other supported ESP where you see "mailgun":
1. Install Anymail from PyPI:
@@ -115,7 +115,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
DEFAULT_FROM_EMAIL = "you at example.com" # if you don't already have this in settings
- 3. Now the regular `Django email functions <https://docs.djangoproject.com/en/v0.10/topics/email/>`_
+ 3. Now the regular `Django email functions <https://docs.djangoproject.com/en/v0.11.1/topics/email/>`_
will send through your chosen ESP:
.. code-block:: python
@@ -159,10 +159,10 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. END quickstart
- See the `full documentation <https://anymail.readthedocs.io/en/v0.10/>`_
+ See the `full documentation <https://anymail.readthedocs.io/en/v0.11.1/>`_
for more features and options.
-Keywords: django,email,email backend,ESP,transactional mail,mailgun,mandrill,postmark,sendgrid
+Keywords: django,email,email backend,ESP,transactional mail,mailgun,mailjet,mandrill,postmark,sendgrid
Platform: UNKNOWN
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Programming Language :: Python
diff --git a/django_anymail.egg-info/SOURCES.txt b/django_anymail.egg-info/SOURCES.txt
index e2bbe00..b2f5912 100644
--- a/django_anymail.egg-info/SOURCES.txt
+++ b/django_anymail.egg-info/SOURCES.txt
@@ -14,6 +14,7 @@ anymail/backends/__init__.py
anymail/backends/base.py
anymail/backends/base_requests.py
anymail/backends/mailgun.py
+anymail/backends/mailjet.py
anymail/backends/mandrill.py
anymail/backends/postmark.py
anymail/backends/sendgrid.py
@@ -23,6 +24,7 @@ anymail/backends/test.py
anymail/webhooks/__init__.py
anymail/webhooks/base.py
anymail/webhooks/mailgun.py
+anymail/webhooks/mailjet.py
anymail/webhooks/mandrill.py
anymail/webhooks/postmark.py
anymail/webhooks/sendgrid.py
diff --git a/django_anymail.egg-info/requires.txt b/django_anymail.egg-info/requires.txt
index 2dad747..58caf6c 100644
--- a/django_anymail.egg-info/requires.txt
+++ b/django_anymail.egg-info/requires.txt
@@ -4,6 +4,8 @@ six
[mailgun]
+[mailjet]
+
[mandrill]
[postmark]
diff --git a/setup.py b/setup.py
index cac1281..f90bc34 100644
--- a/setup.py
+++ b/setup.py
@@ -28,8 +28,9 @@ with open('README.rst') as f:
setup(
name="django-anymail",
version=__version__,
- description='Django email backends for Mailgun, Postmark, SendGrid, SparkPost and other transactional ESPs',
- keywords="django, email, email backend, ESP, transactional mail, mailgun, mandrill, postmark, sendgrid",
+ description='Django email backends for Mailgun, Mailjet, Postmark, SendGrid, SparkPost '
+ 'and other transactional ESPs',
+ keywords="django, email, email backend, ESP, transactional mail, mailgun, mailjet, mandrill, postmark, sendgrid",
author="Mike Edmunds <medmunds at gmail.com>",
author_email="medmunds at gmail.com",
url="https://github.com/anymail/django-anymail",
@@ -42,6 +43,7 @@ setup(
# (e.g., AWS-SES would want boto).
# For simplicity, requests is included in the base requirements.
"mailgun": [],
+ "mailjet": [],
"mandrill": [],
"postmark": [],
"sendgrid": [],
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-anymail.git
More information about the Python-modules-commits
mailing list