[Python-modules-commits] [django-anymail] 01/01: Imported Upstream version 0.10
Scott Kitterman
kitterman at moszumanska.debian.org
Tue Jun 20 04:38:25 UTC 2017
This is an automated email from the git hooks/post-receive script.
kitterman pushed a commit to branch upstream
in repository django-anymail.
commit 5abe9c540cd002786357df507111580175e0c919
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