[Python-modules-commits] [django-anymail] 03/05: Imported Upstream version 0.10

Scott Kitterman kitterman at moszumanska.debian.org
Tue Jun 20 04:38:00 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 7f8487fb08562694f6331024ed4ff2c35bce87b9
Author: Scott Kitterman <scott at kitterman.com>
Date:   Tue Jun 20 00:26:37 2017 -0400

    Imported Upstream version 0.10
---
 PKG-INFO                         |  19 +++---
 anymail/__init__.py              |   3 +-
 anymail/_version.py              |   2 +-
 anymail/backends/base.py         |  56 +++++++++++-----
 anymail/backends/mailgun.py      |  15 ++---
 anymail/backends/mandrill.py     |   3 +-
 anymail/backends/postmark.py     |   6 +-
 anymail/backends/sendgrid.py     |   5 +-
 anymail/backends/sparkpost.py    |   6 +-
 anymail/utils.py                 | 135 ++++++++++++++++++++++++++++++++-------
 anymail/webhooks/mailgun.py      |   9 +++
 anymail/webhooks/mandrill.py     |   2 +-
 anymail/webhooks/postmark.py     |   6 +-
 anymail/webhooks/sendgrid.py     |   1 -
 django_anymail.egg-info/PKG-INFO |  19 +++---
 setup.cfg                        |   1 -
 setup.py                         |   5 +-
 17 files changed, 210 insertions(+), 83 deletions(-)

diff --git a/PKG-INFO b/PKG-INFO
index 6815805..5c8c01a 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,7 +1,7 @@
 Metadata-Version: 1.1
 Name: django-anymail
-Version: 0.8
-Summary: Django email backends for Mailgun, Postmark, SendGrid and other transactional ESPs
+Version: 0.10
+Summary: Django email backends for Mailgun, 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
@@ -47,7 +47,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
         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.8/topics/email/>`_
+          `Django's built-in email <https://docs.djangoproject.com/en/v0.10/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.8
+        .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.10
                :target: https://travis-ci.org/anymail/django-anymail
                :alt:    build status on Travis-CI
         
-        .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.8
-               :target: https://anymail.readthedocs.io/en/v0.8/
+        .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.10
+               :target: https://anymail.readthedocs.io/en/v0.10/
                :alt:    documentation on ReadTheDocs
         
         **Resources**
         
-        * Full documentation: https://anymail.readthedocs.io/en/v0.8/
+        * Full documentation: https://anymail.readthedocs.io/en/v0.10/
         * Package on PyPI: https://pypi.python.org/pypi/django-anymail
         * Project on Github: https://github.com/anymail/django-anymail
         
@@ -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.8/topics/email/>`_
+        3. Now the regular `Django email functions <https://docs.djangoproject.com/en/v0.10/topics/email/>`_
            will send through your chosen ESP:
         
            .. code-block:: python
@@ -159,7 +159,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
         .. END quickstart
         
         
-        See the `full documentation <https://anymail.readthedocs.io/en/v0.8/>`_
+        See the `full documentation <https://anymail.readthedocs.io/en/v0.10/>`_
         for more features and options.
         
 Keywords: django,email,email backend,ESP,transactional mail,mailgun,mandrill,postmark,sendgrid
@@ -172,6 +172,7 @@ Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Framework :: Django
diff --git a/anymail/__init__.py b/anymail/__init__.py
index 1107fd2..29089a0 100644
--- a/anymail/__init__.py
+++ b/anymail/__init__.py
@@ -1 +1,2 @@
-from ._version import __version__, VERSION
+# Expose package version at root of package
+from ._version import __version__, VERSION  # NOQA: F401
diff --git a/anymail/_version.py b/anymail/_version.py
index 9d7d8ee..68eeb94 100644
--- a/anymail/_version.py
+++ b/anymail/_version.py
@@ -1,3 +1,3 @@
-VERSION = (0, 8)
+VERSION = (0, 10)
 __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/base.py b/anymail/backends/base.py
index e05bb0c..c5f658a 100644
--- a/anymail/backends/base.py
+++ b/anymail/backends/base.py
@@ -1,5 +1,6 @@
 from datetime import date, datetime
 
+import six
 from django.conf import settings
 from django.core.mail.backends.base import BaseEmailBackend
 from django.utils.timezone import is_naive, get_current_timezone, make_aware, utc
@@ -7,8 +8,8 @@ from django.utils.timezone import is_naive, get_current_timezone, make_aware, ut
 from ..exceptions import AnymailCancelSend, AnymailError, AnymailUnsupportedFeature, AnymailRecipientsRefused
 from ..message import AnymailStatus
 from ..signals import pre_send, post_send
-from ..utils import (Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting,
-                     force_non_lazy, force_non_lazy_list, force_non_lazy_dict)
+from ..utils import (Attachment, UNSET, combine, last, get_anymail_setting, parse_address_list,
+                     force_non_lazy, force_non_lazy_list, force_non_lazy_dict, is_lazy)
 
 
 class AnymailBaseBackend(BaseEmailBackend):
@@ -215,12 +216,12 @@ class BasePayload(object):
     # the combined/converted results for each attr.
     base_message_attrs = (
         # Standard EmailMessage/EmailMultiAlternatives props
-        ('from_email', last, 'parsed_email'),
-        ('to', combine, 'parsed_emails'),
-        ('cc', combine, 'parsed_emails'),
-        ('bcc', combine, 'parsed_emails'),
+        ('from_email', last, parse_address_list),  # multiple from_emails are allowed
+        ('to', combine, parse_address_list),
+        ('cc', combine, parse_address_list),
+        ('bcc', combine, parse_address_list),
         ('subject', last, force_non_lazy),
-        ('reply_to', combine, 'parsed_emails'),
+        ('reply_to', combine, parse_address_list),
         ('extra_headers', combine, force_non_lazy_dict),
         ('body', last, force_non_lazy),  # special handling below checks message.content_subtype
         ('alternatives', combine, 'prepped_alternatives'),
@@ -252,6 +253,8 @@ class BasePayload(object):
         message_attrs = self.base_message_attrs + self.anymail_message_attrs + self.esp_message_attrs
         for attr, combiner, converter in message_attrs:
             value = getattr(message, attr, UNSET)
+            if attr in ('to', 'cc', 'bcc', 'reply_to') and value is not UNSET:
+                self.validate_not_bare_string(attr, value)
             if combiner is not None:
                 default_value = self.defaults.get(attr, UNSET)
                 value = combiner(default_value, value)
@@ -263,6 +266,8 @@ class BasePayload(object):
             if value is not UNSET:
                 if attr == 'body':
                     setter = self.set_html_body if message.content_subtype == 'html' else self.set_text_body
+                elif attr == 'from_email':
+                    setter = self.set_from_email_list
                 else:
                     # AttributeError here? Your Payload subclass is missing a set_<attr> implementation
                     setter = getattr(self, 'set_%s' % attr)
@@ -274,16 +279,28 @@ class BasePayload(object):
                                             email_message=self.message, payload=self, backend=self.backend)
 
     #
-    # Attribute converters
+    # Attribute validators
     #
 
-    def parsed_email(self, address):
-        return ParsedEmail(address, self.message.encoding)  # (handles lazy address)
+    def validate_not_bare_string(self, attr, value):
+        """EmailMessage to, cc, bcc, and reply_to are specced to be lists of strings.
 
-    def parsed_emails(self, addresses):
-        encoding = self.message.encoding
-        return [ParsedEmail(address, encoding)  # (handles lazy address)
-                for address in addresses]
+        This catches the common error where a single string is used instead.
+        (See also checks in EmailMessage.__init__.)
+        """
+        # Note: this actually only runs for reply_to. If to, cc, or bcc are
+        # set to single strings, you'll end up with an earlier cryptic TypeError
+        # from EmailMesssage.recipients (called from EmailMessage.send) before
+        # the Anymail backend even gets involved:
+        #   TypeError: must be str, not list
+        #   TypeError: can only concatenate list (not "str") to list
+        #   TypeError: Can't convert 'list' object to str implicitly
+        if isinstance(value, six.string_types) or is_lazy(value):
+            raise TypeError('"{attr}" attribute must be a list or other iterable'.format(attr=attr))
+
+    #
+    # Attribute converters
+    #
 
     def prepped_alternatives(self, alternatives):
         return [(force_non_lazy(content), mimetype)
@@ -325,8 +342,17 @@ class BasePayload(object):
         raise NotImplementedError("%s.%s must implement init_payload" %
                                   (self.__class__.__module__, self.__class__.__name__))
 
+    def set_from_email_list(self, emails):
+        # If your backend supports multiple from emails, override this to handle the whole list;
+        # otherwise just implement set_from_email
+        if len(emails) > 1:
+            self.unsupported_feature("multiple from emails")
+            # fall through if ignoring unsupported features
+        if len(emails) > 0:
+            self.set_from_email(emails[0])
+
     def set_from_email(self, email):
-        raise NotImplementedError("%s.%s must implement set_from_email" %
+        raise NotImplementedError("%s.%s must implement set_from_email or set_from_email_list" %
                                   (self.__class__.__module__, self.__class__.__name__))
 
     def set_to(self, emails):
diff --git a/anymail/backends/mailgun.py b/anymail/backends/mailgun.py
index a5492f3..5f8d933 100644
--- a/anymail/backends/mailgun.py
+++ b/anymail/backends/mailgun.py
@@ -124,15 +124,12 @@ class MailgunPayload(RequestsPayload):
         self.data = {}   # {field: [multiple, values]}
         self.files = []  # [(field, multiple), (field, values)]
 
-    def set_from_email(self, email):
-        self.data["from"] = str(email)
-        if self.sender_domain is None:
-            # try to intuit sender_domain from from_email
-            try:
-                _, domain = email.email.split('@')
-                self.sender_domain = domain
-            except ValueError:
-                pass
+    def set_from_email_list(self, emails):
+        # Mailgun supports multiple From email addresses
+        self.data["from"] = [email.address for email in emails]
+        if self.sender_domain is None and len(emails) > 0:
+            # try to intuit sender_domain from first from_email
+            self.sender_domain = emails[0].domain or None
 
     def set_recipients(self, recipient_type, emails):
         assert recipient_type in ["to", "cc", "bcc"]
diff --git a/anymail/backends/mandrill.py b/anymail/backends/mandrill.py
index 14449f4..d45bc7d 100644
--- a/anymail/backends/mandrill.py
+++ b/anymail/backends/mandrill.py
@@ -214,7 +214,7 @@ class MandrillPayload(RequestsPayload):
                         {'rcpt': rcpt, 'values': recipient_metadata[rcpt]}
                         for rcpt in sorted(recipient_metadata.keys())]
             # Merge esp_extra with payload data: shallow merge within ['message'] and top-level keys
-            self.data.update({k:v for k,v in esp_extra.items() if k != 'message'})
+            self.data.update({k: v for k, v in esp_extra.items() if k != 'message'})
             try:
                 self.data['message'].update(esp_extra['message'])
             except KeyError:
@@ -311,4 +311,5 @@ class MandrillPayload(RequestsPayload):
         setter.__name__ = setter_name
         return setter
 
+
 MandrillPayload.define_message_attr_setters()
diff --git a/anymail/backends/postmark.py b/anymail/backends/postmark.py
index 86f6d22..22b9795 100644
--- a/anymail/backends/postmark.py
+++ b/anymail/backends/postmark.py
@@ -138,8 +138,10 @@ class PostmarkPayload(RequestsPayload):
     def init_payload(self):
         self.data = {}   # becomes json
 
-    def set_from_email(self, email):
-        self.data["From"] = email.address
+    def set_from_email_list(self, emails):
+        # Postmark accepts multiple From email addresses
+        # (though truncates to just the first, on their end, as of 4/2017)
+        self.data["From"] = ", ".join([email.address for email in emails])
 
     def set_recipients(self, recipient_type, emails):
         assert recipient_type in ["to", "cc", "bcc"]
diff --git a/anymail/backends/sendgrid.py b/anymail/backends/sendgrid.py
index 3d2c5ea..afe6a16 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, AnymailDeprecationWarning
 from ..message import AnymailRecipientStatus
-from ..utils import get_anymail_setting, timestamp, update_deep
+from ..utils import get_anymail_setting, timestamp, update_deep, parse_address_list
 
 
 class EmailBackend(AnymailRequestsBackend):
@@ -115,7 +115,7 @@ class SendGridPayload(RequestsPayload):
         if "Reply-To" in headers:
             # Reply-To must be in its own param
             reply_to = headers.pop('Reply-To')
-            self.set_reply_to([self.parsed_email(reply_to)])
+            self.set_reply_to(parse_address_list([reply_to]))
         if len(headers) > 0:
             self.data["headers"] = dict(headers)  # flatten to normal dict for json serialization
         else:
@@ -344,4 +344,3 @@ class SendGridPayload(RequestsPayload):
                 "or use 'anymail.backends.sendgrid_v2.EmailBackend' for the old API."
             )
         update_deep(self.data, extra)
-
diff --git a/anymail/backends/sparkpost.py b/anymail/backends/sparkpost.py
index 6d1d8f6..b3976b7 100644
--- a/anymail/backends/sparkpost.py
+++ b/anymail/backends/sparkpost.py
@@ -129,8 +129,10 @@ class SparkPostPayload(BasePayload):
 
         return self.params
 
-    def set_from_email(self, email):
-        self.params['from_email'] = email.address
+    def set_from_email_list(self, emails):
+        # SparkPost supports multiple From email addresses,
+        # as a single comma-separated string
+        self.params['from_email'] = ", ".join([email.address for email in emails])
 
     def set_to(self, emails):
         if emails:
diff --git a/anymail/utils.py b/anymail/utils.py
index 66c307a..d245f66 100644
--- a/anymail/utils.py
+++ b/anymail/utils.py
@@ -113,35 +113,116 @@ def update_deep(dct, other):
     # (like dict.update(), no return value)
 
 
-def parse_one_addr(address):
-    # This is email.utils.parseaddr, but without silently returning
-    # partial content if there are commas or parens in the string:
-    addresses = getaddresses([address])
-    if len(addresses) > 1:
-        raise ValueError("Multiple email addresses (parses as %r)" % addresses)
-    elif len(addresses) == 0:
-        return ('', '')
-    return addresses[0]
+def parse_address_list(address_list):
+    """Returns a list of ParsedEmail objects from strings in address_list.
+
+    Essentially wraps :func:`email.utils.getaddresses` with better error
+    messaging and more-useful output objects
+
+    Note that the returned list might be longer than the address_list param,
+    if any individual string contains multiple comma-separated addresses.
+
+    :param list[str]|str|None|list[None] address_list:
+        the address or addresses to parse
+    :return list[:class:`ParsedEmail`]:
+    :raises :exc:`AnymailInvalidAddress`:
+    """
+    if isinstance(address_list, six.string_types) or is_lazy(address_list):
+        address_list = [address_list]
+
+    if address_list is None or address_list == [None]:
+        return []
+
+    # For consistency with Django's SMTP backend behavior, extract all addresses
+    # from the list -- which may split comma-seperated strings into multiple addresses.
+    # (See django.core.mail.message: EmailMessage.message to/cc/bcc/reply_to handling;
+    # also logic for ADDRESS_HEADERS in forbid_multi_line_headers.)
+    address_list_strings = [force_text(address) for address in address_list]  # resolve lazy strings
+    name_email_pairs = getaddresses(address_list_strings)
+    if name_email_pairs == [] and address_list_strings == [""]:
+        name_email_pairs = [('', '')]  # getaddresses ignores a single empty string
+    parsed = [ParsedEmail(name_email_pair) for name_email_pair in name_email_pairs]
+
+    # Sanity-check, and raise useful errors
+    for address in parsed:
+        if address.localpart == '' or address.domain == '':
+            # Django SMTP allows localpart-only emails, but they're not meaningful with an ESP
+            errmsg = "Invalid email address '%s' parsed from '%s'." % (
+                address.email, ", ".join(address_list_strings))
+            if len(parsed) > len(address_list):
+                errmsg += " (Maybe missing quotes around a display-name?)"
+            raise AnymailInvalidAddress(errmsg)
+
+    return parsed
 
 
 class ParsedEmail(object):
-    """A sanitized, full email address with separate name and email properties."""
+    """A sanitized, complete email address with separate name and email properties.
+
+    (Intended for Anymail internal use.)
+
+    Instance properties, all read-only:
+    :ivar str name:
+        the address's display-name portion (unqouted, unescaped),
+        e.g., 'Display Name, Inc.'
+    :ivar str email:
+        the address's addr-spec portion (unquoted, unescaped),
+        e.g., 'user at example.com'
+    :ivar str address:
+        the fully-formatted address, with any necessary quoting and escaping,
+        e.g., '"Display Name, Inc." <user at example.com>'
+    :ivar str localpart:
+        the local part (before the '@') of email,
+        e.g., 'user'
+    :ivar str domain:
+        the domain part (after the '@') of email,
+        e.g., 'example.com'
+    """
 
-    def __init__(self, address, encoding):
-        if address is None:
-            self.name = self.email = self.address = None
-            return
+    def __init__(self, name_email_pair):
+        """Construct a ParsedEmail.
+
+        You generally should use :func:`parse_address_list` rather than creating
+        ParsedEmail objects directly.
+
+        :param tuple(str, str) name_email_pair:
+            the display-name and addr-spec (both unquoted) for the address,
+            as returned by :func:`email.utils.parseaddr` and
+            :func:`email.utils.getaddresses`
+        """
+        self._address = None  # lazy formatted address
+        self.name, self.email = name_email_pair
         try:
-            self.name, self.email = parse_one_addr(force_text(address))
-            if self.email == '':
-                # normalize sanitize_address py2/3 behavior:
-                raise ValueError('No email found')
-            # Django's sanitize_address is like email.utils.formataddr, but also
-            # escapes as needed for use in email message headers:
-            self.address = sanitize_address((self.name, self.email), encoding)
-        except (IndexError, TypeError, ValueError) as err:
-            raise AnymailInvalidAddress("Invalid email address format %r: %s"
-                                        % (address, str(err)))
+            self.localpart, self.domain = self.email.split("@", 1)
+        except ValueError:
+            self.localpart = self.email
+            self.domain = ''
+
+    @property
+    def address(self):
+        if self._address is None:
+            # (you might be tempted to use `encoding=settings.DEFAULT_CHARSET` here,
+            # but that always forces the display-name to quoted-printable/base64,
+            # even when simple ascii would work fine--and be more readable)
+            self._address = self.formataddr()
+        return self._address
+
+    def formataddr(self, encoding=None):
+        """Return a fully-formatted email address, using encoding.
+
+        This is essentially the same as :func:`email.utils.formataddr`
+        on the ParsedEmail's name and email properties, but uses
+        Django's :func:`~django.core.mail.message.sanitize_address`
+        for improved PY2/3 compatibility, consistent handling of
+        encoding (a.k.a. charset), and proper handling of IDN
+        domain portions.
+
+        :param str|None encoding:
+            the charset to use for the display-name portion;
+            default None uses ascii if possible, else 'utf-8'
+            (quoted-printable utf-8/base64)
+        """
+        return sanitize_address((self.name, self.email), encoding)
 
     def __str__(self):
         return self.address
@@ -171,6 +252,12 @@ class Attachment(object):
         if isinstance(attachment, MIMEBase):
             self.name = attachment.get_filename()
             self.content = attachment.get_payload(decode=True)
+            if self.content is None:
+                if hasattr(attachment, 'as_bytes'):
+                    self.content = attachment.as_bytes()
+                else:
+                    # Python 2.7 fallback
+                    self.content = attachment.as_string().encode(self.encoding)
             self.mimetype = attachment.get_content_type()
 
             if get_content_disposition(attachment) == 'inline':
diff --git a/anymail/webhooks/mailgun.py b/anymail/webhooks/mailgun.py
index bb0a785..94026ca 100644
--- a/anymail/webhooks/mailgun.py
+++ b/anymail/webhooks/mailgun.py
@@ -92,6 +92,15 @@ class MailgunTrackingWebhookView(MailgunBaseWebhookView):
             mta_status = int(esp_event['code'])
         except (KeyError, TypeError):
             pass
+        except ValueError:
+            # RFC-3463 extended SMTP status code (class.subject.detail, where class is "2", "4" or "5")
+            try:
+                status_class = esp_event['code'].split('.')[0]
+            except (TypeError, IndexError):
+                # illegal SMTP status code format
+                pass
+            else:
+                reject_reason = RejectReason.BOUNCED if status_class in ("4", "5") else RejectReason.OTHER
         else:
             reject_reason = self.reject_reasons.get(
                 mta_status,
diff --git a/anymail/webhooks/mandrill.py b/anymail/webhooks/mandrill.py
index 27cf485..0cd3685 100644
--- a/anymail/webhooks/mandrill.py
+++ b/anymail/webhooks/mandrill.py
@@ -81,7 +81,7 @@ class MandrillTrackingWebhookView(MandrillBaseWebhookView):
         'send': EventType.SENT,
         'deferral': EventType.DEFERRED,
         'hard_bounce': EventType.BOUNCED,
-        'soft_bounce': EventType.DEFERRED,
+        'soft_bounce': EventType.BOUNCED,
         'open': EventType.OPENED,
         'click': EventType.CLICKED,
         'spam': EventType.COMPLAINED,
diff --git a/anymail/webhooks/postmark.py b/anymail/webhooks/postmark.py
index 466bdfc..a63c181 100644
--- a/anymail/webhooks/postmark.py
+++ b/anymail/webhooks/postmark.py
@@ -36,7 +36,7 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView):
         'SpamNotification': (EventType.COMPLAINED, RejectReason.SPAM),
         'OpenRelayTest': (EventType.DEFERRED, None),  # Receiving MTA is testing Postmark
         'Unknown': (EventType.UNKNOWN, None),
-        'SoftBounce': (EventType.DEFERRED, RejectReason.BOUNCED),  # until HardBounce later
+        'SoftBounce': (EventType.BOUNCED, RejectReason.BOUNCED),  # might also receive HardBounce later
         'VirusNotification': (EventType.BOUNCED, RejectReason.OTHER),
         'ChallengeVerification': (EventType.AUTORESPONDED, None),
         'BadEmailAddress': (EventType.REJECTED, RejectReason.INVALID),
@@ -48,8 +48,8 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView):
         'InboundError': (EventType.INBOUND_FAILED, None),
         'DMARCPolicy': (EventType.REJECTED, RejectReason.BLOCKED),
         'TemplateRenderingFailed': (EventType.FAILED, None),
-        # Postmark does not report DELIVERED
-        # Postmark does not report CLICKED (because it doesn't implement click-tracking)
+        # DELIVERED doesn't have a Type field; detected separately below
+        # CLICKED doesn't have a Postmark webhook (yet?)
         # OPENED doesn't have a Type field; detected separately below
         # INBOUND doesn't have a Type field; should come in through different webhook
     }
diff --git a/anymail/webhooks/sendgrid.py b/anymail/webhooks/sendgrid.py
index 24d5ad9..9f52c97 100644
--- a/anymail/webhooks/sendgrid.py
+++ b/anymail/webhooks/sendgrid.py
@@ -120,4 +120,3 @@ class SendGridTrackingWebhookView(SendGridBaseWebhookView):
         'url_offset',  # click tracking
         'useragent',  # click/open tracking
     }
-
diff --git a/django_anymail.egg-info/PKG-INFO b/django_anymail.egg-info/PKG-INFO
index 6815805..5c8c01a 100644
--- a/django_anymail.egg-info/PKG-INFO
+++ b/django_anymail.egg-info/PKG-INFO
@@ -1,7 +1,7 @@
 Metadata-Version: 1.1
 Name: django-anymail
-Version: 0.8
-Summary: Django email backends for Mailgun, Postmark, SendGrid and other transactional ESPs
+Version: 0.10
+Summary: Django email backends for Mailgun, 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
@@ -47,7 +47,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
         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.8/topics/email/>`_
+          `Django's built-in email <https://docs.djangoproject.com/en/v0.10/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.8
+        .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.10
                :target: https://travis-ci.org/anymail/django-anymail
                :alt:    build status on Travis-CI
         
-        .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.8
-               :target: https://anymail.readthedocs.io/en/v0.8/
+        .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.10
+               :target: https://anymail.readthedocs.io/en/v0.10/
                :alt:    documentation on ReadTheDocs
         
         **Resources**
         
-        * Full documentation: https://anymail.readthedocs.io/en/v0.8/
+        * Full documentation: https://anymail.readthedocs.io/en/v0.10/
         * Package on PyPI: https://pypi.python.org/pypi/django-anymail
         * Project on Github: https://github.com/anymail/django-anymail
         
@@ -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.8/topics/email/>`_
+        3. Now the regular `Django email functions <https://docs.djangoproject.com/en/v0.10/topics/email/>`_
            will send through your chosen ESP:
         
            .. code-block:: python
@@ -159,7 +159,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
         .. END quickstart
         
         
-        See the `full documentation <https://anymail.readthedocs.io/en/v0.8/>`_
+        See the `full documentation <https://anymail.readthedocs.io/en/v0.10/>`_
         for more features and options.
         
 Keywords: django,email,email backend,ESP,transactional mail,mailgun,mandrill,postmark,sendgrid
@@ -172,6 +172,7 @@ Classifier: Programming Language :: Python :: 2.7
 Classifier: Programming Language :: Python :: 3
 Classifier: Programming Language :: Python :: 3.4
 Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Framework :: Django
diff --git a/setup.cfg b/setup.cfg
index 861a9f5..8bfd5a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,4 @@
 [egg_info]
 tag_build = 
 tag_date = 0
-tag_svn_revision = 0
 
diff --git a/setup.py b/setup.py
index 0094dd7..cac1281 100644
--- a/setup.py
+++ b/setup.py
@@ -20,13 +20,15 @@ def long_description_from_readme(rst):
                  release, rst)  # (?<=...) is "positive lookbehind": must be there, but won't get replaced
     return rst
 
+
 with open('README.rst') as f:
     long_description = long_description_from_readme(f.read())
 
+
 setup(
     name="django-anymail",
     version=__version__,
-    description='Django email backends for Mailgun, Postmark, SendGrid and other transactional ESPs',
+    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",
     author="Mike Edmunds <medmunds at gmail.com>",
     author_email="medmunds at gmail.com",
@@ -57,6 +59,7 @@ setup(
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.4",
         "Programming Language :: Python :: 3.5",
+        "Programming Language :: Python :: 3.6",
         "License :: OSI Approved :: BSD License",
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Framework :: Django",

-- 
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