[Python-modules-commits] [django-anymail] 01/04: Import django-anymail_0.7.orig.tar.gz
Scott Kitterman
kitterman at moszumanska.debian.org
Mon Jan 2 13:24:15 UTC 2017
This is an automated email from the git hooks/post-receive script.
kitterman pushed a commit to branch master
in repository django-anymail.
commit 107063a19afd93a49dd2f8de54f47b72261201b6
Author: Scott Kitterman <scott at kitterman.com>
Date: Mon Jan 2 08:14:47 2017 -0500
Import django-anymail_0.7.orig.tar.gz
---
PKG-INFO | 12 +++---
anymail/_version.py | 2 +-
anymail/backends/base.py | 49 +++++++++++++++-------
anymail/exceptions.py | 4 ++
anymail/utils.py | 87 +++++++++++++++++++++++++++++++---------
anymail/webhooks/mandrill.py | 12 +++++-
anymail/webhooks/postmark.py | 4 +-
django_anymail.egg-info/PKG-INFO | 12 +++---
8 files changed, 131 insertions(+), 51 deletions(-)
diff --git a/PKG-INFO b/PKG-INFO
index 4014186..8397941 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: django-anymail
-Version: 0.6.1
+Version: 0.7
Summary: Django email backends for Mailgun, Postmark, SendGrid and other transactional ESPs
Home-page: https://github.com/anymail/django-anymail
Author: Mike Edmunds <medmunds at gmail.com>
@@ -59,17 +59,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.6
+ .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.7
:target: https://travis-ci.org/anymail/django-anymail
:alt: build status on Travis-CI
- .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.6
- :target: https://anymail.readthedocs.io/en/v0.6/
+ .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.7
+ :target: https://anymail.readthedocs.io/en/v0.7/
:alt: documentation on ReadTheDocs
**Resources**
- * Full documentation: https://anymail.readthedocs.io/en/v0.6/
+ * Full documentation: https://anymail.readthedocs.io/en/v0.7/
* Package on PyPI: https://pypi.python.org/pypi/django-anymail
* Project on Github: https://github.com/anymail/django-anymail
@@ -157,7 +157,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. END quickstart
- See the `full documentation <https://anymail.readthedocs.io/en/v0.6/>`_
+ See the `full documentation <https://anymail.readthedocs.io/en/v0.7/>`_
for more features and options.
Keywords: django,email,email backend,ESP,transactional mail,mailgun,mandrill,postmark,sendgrid
diff --git a/anymail/_version.py b/anymail/_version.py
index 1abae49..783f1a5 100644
--- a/anymail/_version.py
+++ b/anymail/_version.py
@@ -1,3 +1,3 @@
-VERSION = (0, 6, 1)
+VERSION = (0, 7)
__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 877f6db..bf13c80 100644
--- a/anymail/backends/base.py
+++ b/anymail/backends/base.py
@@ -7,7 +7,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
+from ..utils import (Attachment, ParsedEmail, UNSET, combine, last, get_anymail_setting,
+ force_non_lazy, force_non_lazy_list, force_non_lazy_dict)
class AnymailBaseBackend(BaseEmailBackend):
@@ -195,31 +196,43 @@ class AnymailBaseBackend(BaseEmailBackend):
class BasePayload(object):
- # attr, combiner, converter
+ # Listing of EmailMessage/EmailMultiAlternatives attributes
+ # to process into Payload. Each item is in the form:
+ # (attr, combiner, converter)
+ # attr: the property name
+ # combiner: optional function(default_value, value) -> value
+ # to combine settings defaults with the EmailMessage property value
+ # (usually `combine` to merge, or `last` for message value to override default;
+ # use `None` if settings defaults aren't supported)
+ # converter: optional function(value) -> value transformation
+ # (can be a callable or the string name of a Payload method, or `None`)
+ # The converter must force any Django lazy translation strings to text.
+ # The Payload's `set_<attr>` method will be called with
+ # 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'),
- ('subject', last, None),
+ ('subject', last, force_non_lazy),
('reply_to', combine, 'parsed_emails'),
- ('extra_headers', combine, None),
- ('body', last, None), # special handling below checks message.content_subtype
- ('alternatives', combine, None),
+ ('extra_headers', combine, force_non_lazy_dict),
+ ('body', last, force_non_lazy), # special handling below checks message.content_subtype
+ ('alternatives', combine, 'prepped_alternatives'),
('attachments', combine, 'prepped_attachments'),
)
anymail_message_attrs = (
# Anymail expando-props
- ('metadata', combine, None),
+ ('metadata', combine, force_non_lazy_dict),
('send_at', last, 'aware_datetime'),
- ('tags', combine, None),
+ ('tags', combine, force_non_lazy_list),
('track_clicks', last, None),
('track_opens', last, None),
- ('template_id', last, None),
- ('merge_data', combine, None),
- ('merge_global_data', combine, None),
- ('esp_extra', combine, None),
+ ('template_id', last, force_non_lazy),
+ ('merge_data', combine, force_non_lazy_dict),
+ ('merge_global_data', combine, force_non_lazy_dict),
+ ('esp_extra', combine, force_non_lazy_dict),
)
esp_message_attrs = () # subclasses can override
@@ -261,15 +274,21 @@ class BasePayload(object):
#
def parsed_email(self, address):
- return ParsedEmail(address, self.message.encoding)
+ return ParsedEmail(address, self.message.encoding) # (handles lazy address)
def parsed_emails(self, addresses):
encoding = self.message.encoding
- return [ParsedEmail(address, encoding) for address in addresses]
+ return [ParsedEmail(address, encoding) # (handles lazy address)
+ for address in addresses]
+
+ def prepped_alternatives(self, alternatives):
+ return [(force_non_lazy(content), mimetype)
+ for (content, mimetype) in alternatives]
def prepped_attachments(self, attachments):
str_encoding = self.message.encoding or settings.DEFAULT_CHARSET
- return [Attachment(attachment, str_encoding) for attachment in attachments]
+ return [Attachment(attachment, str_encoding) # (handles lazy content, filename)
+ for attachment in attachments]
def aware_datetime(self, value):
"""Converts a date or datetime or timestamp to an aware datetime.
diff --git a/anymail/exceptions.py b/anymail/exceptions.py
index 8e55532..c0af427 100644
--- a/anymail/exceptions.py
+++ b/anymail/exceptions.py
@@ -101,6 +101,10 @@ class AnymailRecipientsRefused(AnymailError):
super(AnymailRecipientsRefused, self).__init__(message, *args, **kwargs)
+class AnymailInvalidAddress(AnymailError, ValueError):
+ """Exception when using an invalidly-formatted email address"""
+
+
class AnymailUnsupportedFeature(AnymailError, ValueError):
"""Exception for Anymail features that the ESP doesn't support.
diff --git a/anymail/utils.py b/anymail/utils.py
index 5e84ba2..a4a4483 100644
--- a/anymail/utils.py
+++ b/anymail/utils.py
@@ -2,15 +2,17 @@ import mimetypes
from base64 import b64encode
from datetime import datetime
from email.mime.base import MIMEBase
-from email.utils import formatdate, parseaddr, unquote
+from email.utils import formatdate, getaddresses, unquote
from time import mktime
import six
from django.conf import settings
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
+from django.utils.encoding import force_text
+from django.utils.functional import Promise
from django.utils.timezone import utc
-from .exceptions import AnymailConfigurationError
+from .exceptions import AnymailConfigurationError, AnymailInvalidAddress
UNSET = object() # Used as non-None default value
@@ -93,31 +95,39 @@ def getfirst(dct, keys, default=UNSET):
return default
+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]
+
+
class ParsedEmail(object):
- """A sanitized, full email address with separate name and email properties"""
+ """A sanitized, full email address with separate name and email properties."""
def __init__(self, address, encoding):
- self.address = sanitize_address(address, encoding)
- self._name = None
- self._email = None
-
- def _parse(self):
- if self._email is None:
- self._name, self._email = parseaddr(self.address)
+ if address is None:
+ self.name = self.email = self.address = None
+ return
+ 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)))
def __str__(self):
return self.address
- @property
- def name(self):
- self._parse()
- return self._name
-
- @property
- def email(self):
- self._parse()
- return self._email
-
class Attachment(object):
"""A normalized EmailMessage.attachments item with additional functionality
@@ -153,6 +163,9 @@ class Attachment(object):
else:
(self.name, self.content, self.mimetype) = attachment
+ self.name = force_non_lazy(self.name)
+ self.content = force_non_lazy(self.content)
+
# Guess missing mimetype from filename, borrowed from
# django.core.mail.EmailMessage._create_attachment()
if self.mimetype is None and self.name is not None:
@@ -280,3 +293,37 @@ def rfc2822date(dt):
# but treats naive datetimes as local rather than "UTC with no information ..."
timeval = timestamp(dt)
return formatdate(timeval, usegmt=True)
+
+
+def is_lazy(obj):
+ """Return True if obj is a Django lazy object."""
+ # See django.utils.functional.lazy. (This appears to be preferred
+ # to checking for `not isinstance(obj, six.text_type)`.)
+ return isinstance(obj, Promise)
+
+
+def force_non_lazy(obj):
+ """If obj is a Django lazy object, return it coerced to text; otherwise return it unchanged.
+
+ (Similar to django.utils.encoding.force_text, but doesn't alter non-text objects.)
+ """
+ if is_lazy(obj):
+ return six.text_type(obj)
+
+ return obj
+
+
+def force_non_lazy_list(obj):
+ """Return a (shallow) copy of sequence obj, with all values forced non-lazy."""
+ try:
+ return [force_non_lazy(item) for item in obj]
+ except (AttributeError, TypeError):
+ return force_non_lazy(obj)
+
+
+def force_non_lazy_dict(obj):
+ """Return a (deep) copy of dict obj, with all values forced non-lazy."""
+ try:
+ return {key: force_non_lazy_dict(value) for key, value in obj.items()}
+ except (AttributeError, TypeError):
+ return force_non_lazy(obj)
diff --git a/anymail/webhooks/mandrill.py b/anymail/webhooks/mandrill.py
index b1d0c63..f3d13ce 100644
--- a/anymail/webhooks/mandrill.py
+++ b/anymail/webhooks/mandrill.py
@@ -23,15 +23,23 @@ class MandrillSignatureMixin(object):
def __init__(self, **kwargs):
# noinspection PyUnresolvedReferences
esp_name = self.esp_name
- webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name,
+ # webhook_key is required for POST, but not for HEAD when Mandrill validates webhook url.
+ # Defer "missing setting" error until we actually try to use it in the POST...
+ webhook_key = get_anymail_setting('webhook_key', esp_name=esp_name, default=None,
kwargs=kwargs, allow_bare=True)
- self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3
+ if webhook_key is not None:
+ self.webhook_key = webhook_key.encode('ascii') # hmac.new requires bytes key in python 3
self.webhook_url = get_anymail_setting('webhook_url', esp_name=esp_name, default=None,
kwargs=kwargs, allow_bare=True)
# noinspection PyArgumentList
super(MandrillSignatureMixin, self).__init__(**kwargs)
def validate_request(self, request):
+ if self.webhook_key is None:
+ # issue deferred "missing setting" error (re-call get-setting without a default)
+ # noinspection PyUnresolvedReferences
+ get_anymail_setting('webhook_key', esp_name=self.esp_name, allow_bare=True)
+
try:
signature = request.META["HTTP_X_MANDRILL_SIGNATURE"]
except KeyError:
diff --git a/anymail/webhooks/postmark.py b/anymail/webhooks/postmark.py
index be34b4a..466bdfc 100644
--- a/anymail/webhooks/postmark.py
+++ b/anymail/webhooks/postmark.py
@@ -62,6 +62,8 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView):
except KeyError:
if 'FirstOpen' in esp_event:
event_type = EventType.OPENED
+ elif 'DeliveredAt' in esp_event:
+ event_type = EventType.DELIVERED
elif 'From' in esp_event:
# This is an inbound event
raise AnymailConfigurationError(
@@ -73,7 +75,7 @@ class PostmarkTrackingWebhookView(PostmarkBaseWebhookView):
recipient = getfirst(esp_event, ['Email', 'Recipient'], None) # Email for bounce; Recipient for open
try:
- timestr = getfirst(esp_event, ['BouncedAt', 'ReceivedAt'])
+ timestr = getfirst(esp_event, ['DeliveredAt', 'BouncedAt', 'ReceivedAt'])
except KeyError:
timestamp = None
else:
diff --git a/django_anymail.egg-info/PKG-INFO b/django_anymail.egg-info/PKG-INFO
index 4014186..8397941 100644
--- a/django_anymail.egg-info/PKG-INFO
+++ b/django_anymail.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 1.1
Name: django-anymail
-Version: 0.6.1
+Version: 0.7
Summary: Django email backends for Mailgun, Postmark, SendGrid and other transactional ESPs
Home-page: https://github.com/anymail/django-anymail
Author: Mike Edmunds <medmunds at gmail.com>
@@ -59,17 +59,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.6
+ .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.7
:target: https://travis-ci.org/anymail/django-anymail
:alt: build status on Travis-CI
- .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.6
- :target: https://anymail.readthedocs.io/en/v0.6/
+ .. image:: https://readthedocs.org/projects/anymail/badge/?version=v0.7
+ :target: https://anymail.readthedocs.io/en/v0.7/
:alt: documentation on ReadTheDocs
**Resources**
- * Full documentation: https://anymail.readthedocs.io/en/v0.6/
+ * Full documentation: https://anymail.readthedocs.io/en/v0.7/
* Package on PyPI: https://pypi.python.org/pypi/django-anymail
* Project on Github: https://github.com/anymail/django-anymail
@@ -157,7 +157,7 @@ Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, Spa
.. END quickstart
- See the `full documentation <https://anymail.readthedocs.io/en/v0.6/>`_
+ See the `full documentation <https://anymail.readthedocs.io/en/v0.7/>`_
for more features and options.
Keywords: django,email,email backend,ESP,transactional mail,mailgun,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