[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