[Python-modules-commits] [django-anymail] 01/03: import django-anymail_0.6.1.orig.tar.gz
Scott Kitterman
kitterman at moszumanska.debian.org
Fri Nov 18 00:04:22 UTC 2016
This is an automated email from the git hooks/post-receive script.
kitterman pushed a commit to branch master
in repository django-anymail.
commit 34f55f0466a2fdd8f83f224460955f116ae13be4
Author: Scott Kitterman <scott at kitterman.com>
Date: Thu Nov 17 18:12:21 2016 -0500
import django-anymail_0.6.1.orig.tar.gz
---
AUTHORS.txt | 25 ++
LICENSE | 29 ++
MANIFEST.in | 2 +
PKG-INFO | 176 ++++++++++++
README.rst | 153 +++++++++++
anymail/__init__.py | 1 +
anymail/_version.py | 3 +
anymail/backends/__init__.py | 0
anymail/backends/base.py | 392 +++++++++++++++++++++++++++
anymail/backends/base_requests.py | 158 +++++++++++
anymail/backends/mailgun.py | 201 ++++++++++++++
anymail/backends/mandrill.py | 302 +++++++++++++++++++++
anymail/backends/postmark.py | 208 ++++++++++++++
anymail/backends/sendgrid.py | 310 +++++++++++++++++++++
anymail/backends/sparkpost.py | 204 ++++++++++++++
anymail/backends/test.py | 121 +++++++++
anymail/exceptions.py | 173 ++++++++++++
anymail/message.py | 102 +++++++
anymail/signals.py | 88 ++++++
anymail/urls.py | 17 ++
anymail/utils.py | 282 +++++++++++++++++++
anymail/webhooks/__init__.py | 0
anymail/webhooks/base.py | 142 ++++++++++
anymail/webhooks/mailgun.py | 134 +++++++++
anymail/webhooks/mandrill.py | 141 ++++++++++
anymail/webhooks/postmark.py | 104 +++++++
anymail/webhooks/sendgrid.py | 123 +++++++++
anymail/webhooks/sparkpost.py | 136 ++++++++++
django_anymail.egg-info/PKG-INFO | 176 ++++++++++++
django_anymail.egg-info/SOURCES.txt | 34 +++
django_anymail.egg-info/dependency_links.txt | 1 +
django_anymail.egg-info/not-zip-safe | 1 +
django_anymail.egg-info/requires.txt | 14 +
django_anymail.egg-info/top_level.txt | 1 +
setup.cfg | 5 +
setup.py | 66 +++++
36 files changed, 4025 insertions(+)
diff --git a/AUTHORS.txt b/AUTHORS.txt
new file mode 100644
index 0000000..64dd87d
--- /dev/null
+++ b/AUTHORS.txt
@@ -0,0 +1,25 @@
+Anymail
+=======
+
+Mike Edmunds
+
+
+Anymail was forked from Djrill, which included contributions from:
+
+Kenneth Love
+Chris Jones
+Mike Edmunds
+ArnaudF
+Théo Crevon
+Rafael E. Belliard
+Jared Morse
+peillis
+José Padilla
+Jens Alm
+Eric Hennings
+Michael Hobbs
+Sameer Al-Sakran
+Kyle Gibson
+Wes Winham
+nikolay-saskovets
+William Hector
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5657690
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,29 @@
+[The BSD 3-Clause License]
+
+Copyright (c) Anymail Contributors.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of the copyright holder nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..cbcb0ed
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1,2 @@
+include README.rst AUTHORS.txt LICENSE
+recursive-include anymail *.py
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..4014186
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,176 @@
+Metadata-Version: 1.1
+Name: django-anymail
+Version: 0.6.1
+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>
+Author-email: medmunds at gmail.com
+License: BSD License
+Description: Anymail: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and more
+ ==================================================================================
+
+ **EARLY DEVELOPMENT**
+
+ This project is undergoing rapid development to get to a 1.0 release.
+ Before 1.0, minor version bumps might include breaking changes.
+ Please check the
+ `release notes <https://github.com/anymail/django-anymail/releases>`_
+
+ .. This README is reused in multiple places:
+ * Github: project page, exactly as it appears here
+ * Docs: shared-intro section gets included in docs/index.rst
+ quickstart section gets included in docs/quickstart.rst
+ * PyPI: project page (via setup.py long_description),
+ with several edits to freeze it to the specific PyPI release
+ (see long_description_from_readme in setup.py)
+ You can use docutils 1.0 markup, but *not* any Sphinx additions.
+ GitHub rst supports code-block, but *no other* block directives.
+
+
+ .. default-role:: literal
+
+
+ .. _shared-intro:
+
+ .. This shared-intro section is also included in docs/index.rst
+
+ Anymail integrates several transactional email service providers (ESPs) into Django,
+ 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,
+ 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/stable/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
+ * Normalized sent-message status and tracking notification, by connecting
+ your ESP's webhooks to Django signals
+ * "Batch transactional" sends using your ESP's merge and template features
+
+ Anymail is released under the BSD license. It is extensively tested against Django 1.8--1.10
+ (including Python 2.7, Python 3 and PyPy).
+ Anymail releases follow `semantic versioning <http://semver.org/>`_.
+
+ .. END shared-intro
+
+ .. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=v0.6
+ :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/
+ :alt: documentation on ReadTheDocs
+
+ **Resources**
+
+ * Full documentation: https://anymail.readthedocs.io/en/v0.6/
+ * Package on PyPI: https://pypi.python.org/pypi/django-anymail
+ * Project on Github: https://github.com/anymail/django-anymail
+
+
+ Anymail 1-2-3
+ -------------
+
+ .. _quickstart:
+
+ .. This quickstart section is also included in docs/quickstart.rst
+
+ This example uses Mailgun, but you can substitute Postmark or SendGrid
+ or SparkPost or any other supported ESP where you see "mailgun":
+
+ 1. Install Anymail from PyPI:
+
+ .. code-block:: console
+
+ $ pip install django-anymail[mailgun]
+
+ (The `[mailgun]` part installs any additional packages needed for that ESP.
+ Mailgun doesn't have any, but some other ESPs do.)
+
+
+ 2. Edit your project's ``settings.py``:
+
+ .. code-block:: python
+
+ INSTALLED_APPS = [
+ # ...
+ "anymail",
+ # ...
+ ]
+
+ ANYMAIL = {
+ # (exact settings here depend on your ESP...)
+ "MAILGUN_API_KEY": "<your Mailgun key>",
+ "MAILGUN_SENDER_DOMAIN": 'mg.example.com', # your Mailgun domain, if needed
+ }
+ EMAIL_BACKEND = "anymail.backends.mailgun.MailgunBackend" # or sendgrid.SendGridBackend, or...
+ 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/stable/topics/email/>`_
+ will send through your chosen ESP:
+
+ .. code-block:: python
+
+ from django.core.mail import send_mail
+
+ send_mail("It works!", "This will get sent through Mailgun",
+ "Anymail Sender <from at example.com>", ["to at example.com"])
+
+
+ You could send an HTML message, complete with an inline image,
+ custom tags and metadata:
+
+ .. code-block:: python
+
+ from django.core.mail import EmailMultiAlternatives
+ from anymail.message import attach_inline_image_file
+
+ msg = EmailMultiAlternatives(
+ subject="Please activate your account",
+ body="Click to activate your account: http://example.com/activate",
+ from_email="Example <admin at example.com>",
+ to=["New User <user1 at example.com>", "account.manager at example.com"],
+ reply_to=["Helpdesk <support at example.com>"])
+
+ # Include an inline image in the html:
+ logo_cid = attach_inline_image_file(msg, "/path/to/logo.jpg")
+ html = """<img alt="Logo" src="cid:{logo_cid}">
+ <p>Please <a href="http://example.com/activate">activate</a>
+ your account</p>""".format(logo_cid=logo_cid)
+ msg.attach_alternative(html, "text/html")
+
+ # Optional Anymail extensions:
+ msg.metadata = {"user_id": "8675309", "experiment_variation": 1}
+ msg.tags = ["activation", "onboarding"]
+ msg.track_clicks = True
+
+ # Send it:
+ msg.send()
+
+ .. END quickstart
+
+
+ See the `full documentation <https://anymail.readthedocs.io/en/v0.6/>`_
+ for more features and options.
+
+Keywords: django,email,email backend,ESP,transactional mail,mailgun,mandrill,postmark,sendgrid
+Platform: UNKNOWN
+Classifier: Development Status :: 2 - Pre-Alpha
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: Implementation :: PyPy
+Classifier: Programming Language :: Python :: Implementation :: CPython
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.4
+Classifier: Programming Language :: Python :: 3.5
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Framework :: Django
+Classifier: Environment :: Web Environment
diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..40d8b15
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,153 @@
+Anymail: Django email backends for Mailgun, Postmark, SendGrid, SparkPost and more
+==================================================================================
+
+ **EARLY DEVELOPMENT**
+
+ This project is undergoing rapid development to get to a 1.0 release.
+ Before 1.0, minor version bumps might include breaking changes.
+ Please check the
+ `release notes <https://github.com/anymail/django-anymail/releases>`_
+
+.. This README is reused in multiple places:
+ * Github: project page, exactly as it appears here
+ * Docs: shared-intro section gets included in docs/index.rst
+ quickstart section gets included in docs/quickstart.rst
+ * PyPI: project page (via setup.py long_description),
+ with several edits to freeze it to the specific PyPI release
+ (see long_description_from_readme in setup.py)
+ You can use docutils 1.0 markup, but *not* any Sphinx additions.
+ GitHub rst supports code-block, but *no other* block directives.
+
+
+.. default-role:: literal
+
+
+.. _shared-intro:
+
+.. This shared-intro section is also included in docs/index.rst
+
+Anymail integrates several transactional email service providers (ESPs) into Django,
+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,
+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/stable/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
+* Normalized sent-message status and tracking notification, by connecting
+ your ESP's webhooks to Django signals
+* "Batch transactional" sends using your ESP's merge and template features
+
+Anymail is released under the BSD license. It is extensively tested against Django 1.8--1.10
+(including Python 2.7, Python 3 and PyPy).
+Anymail releases follow `semantic versioning <http://semver.org/>`_.
+
+.. END shared-intro
+
+.. image:: https://travis-ci.org/anymail/django-anymail.svg?branch=master
+ :target: https://travis-ci.org/anymail/django-anymail
+ :alt: build status on Travis-CI
+
+.. image:: https://readthedocs.org/projects/anymail/badge/?version=latest
+ :target: https://anymail.readthedocs.io/en/latest/
+ :alt: documentation on ReadTheDocs
+
+**Resources**
+
+* Full documentation: https://anymail.readthedocs.io/en/latest/
+* Package on PyPI: https://pypi.python.org/pypi/django-anymail
+* Project on Github: https://github.com/anymail/django-anymail
+
+
+Anymail 1-2-3
+-------------
+
+.. _quickstart:
+
+.. This quickstart section is also included in docs/quickstart.rst
+
+This example uses Mailgun, but you can substitute Postmark or SendGrid
+or SparkPost or any other supported ESP where you see "mailgun":
+
+1. Install Anymail from PyPI:
+
+ .. code-block:: console
+
+ $ pip install django-anymail[mailgun]
+
+ (The `[mailgun]` part installs any additional packages needed for that ESP.
+ Mailgun doesn't have any, but some other ESPs do.)
+
+
+2. Edit your project's ``settings.py``:
+
+ .. code-block:: python
+
+ INSTALLED_APPS = [
+ # ...
+ "anymail",
+ # ...
+ ]
+
+ ANYMAIL = {
+ # (exact settings here depend on your ESP...)
+ "MAILGUN_API_KEY": "<your Mailgun key>",
+ "MAILGUN_SENDER_DOMAIN": 'mg.example.com', # your Mailgun domain, if needed
+ }
+ EMAIL_BACKEND = "anymail.backends.mailgun.MailgunBackend" # or sendgrid.SendGridBackend, or...
+ 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/stable/topics/email/>`_
+ will send through your chosen ESP:
+
+ .. code-block:: python
+
+ from django.core.mail import send_mail
+
+ send_mail("It works!", "This will get sent through Mailgun",
+ "Anymail Sender <from at example.com>", ["to at example.com"])
+
+
+ You could send an HTML message, complete with an inline image,
+ custom tags and metadata:
+
+ .. code-block:: python
+
+ from django.core.mail import EmailMultiAlternatives
+ from anymail.message import attach_inline_image_file
+
+ msg = EmailMultiAlternatives(
+ subject="Please activate your account",
+ body="Click to activate your account: http://example.com/activate",
+ from_email="Example <admin at example.com>",
+ to=["New User <user1 at example.com>", "account.manager at example.com"],
+ reply_to=["Helpdesk <support at example.com>"])
+
+ # Include an inline image in the html:
+ logo_cid = attach_inline_image_file(msg, "/path/to/logo.jpg")
+ html = """<img alt="Logo" src="cid:{logo_cid}">
+ <p>Please <a href="http://example.com/activate">activate</a>
+ your account</p>""".format(logo_cid=logo_cid)
+ msg.attach_alternative(html, "text/html")
+
+ # Optional Anymail extensions:
+ msg.metadata = {"user_id": "8675309", "experiment_variation": 1}
+ msg.tags = ["activation", "onboarding"]
+ msg.track_clicks = True
+
+ # Send it:
+ msg.send()
+
+.. END quickstart
+
+
+See the `full documentation <https://anymail.readthedocs.io/en/latest/>`_
+for more features and options.
diff --git a/anymail/__init__.py b/anymail/__init__.py
new file mode 100644
index 0000000..1107fd2
--- /dev/null
+++ b/anymail/__init__.py
@@ -0,0 +1 @@
+from ._version import __version__, VERSION
diff --git a/anymail/_version.py b/anymail/_version.py
new file mode 100644
index 0000000..1abae49
--- /dev/null
+++ b/anymail/_version.py
@@ -0,0 +1,3 @@
+VERSION = (0, 6, 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/__init__.py b/anymail/backends/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/anymail/backends/base.py b/anymail/backends/base.py
new file mode 100644
index 0000000..877f6db
--- /dev/null
+++ b/anymail/backends/base.py
@@ -0,0 +1,392 @@
+from datetime import date, datetime
+
+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
+
+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
+
+
+class AnymailBaseBackend(BaseEmailBackend):
+ """
+ Base Anymail email backend
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(AnymailBaseBackend, self).__init__(*args, **kwargs)
+
+ self.ignore_unsupported_features = get_anymail_setting('ignore_unsupported_features',
+ kwargs=kwargs, default=False)
+ self.ignore_recipient_status = get_anymail_setting('ignore_recipient_status',
+ kwargs=kwargs, default=False)
+
+ # Merge SEND_DEFAULTS and <esp_name>_SEND_DEFAULTS settings
+ send_defaults = get_anymail_setting('send_defaults', default={}) # but not from kwargs
+ esp_send_defaults = get_anymail_setting('send_defaults', esp_name=self.esp_name,
+ kwargs=kwargs, default=None)
+ if esp_send_defaults is not None:
+ send_defaults = send_defaults.copy()
+ send_defaults.update(esp_send_defaults)
+ self.send_defaults = send_defaults
+
+ def open(self):
+ """
+ Open and persist a connection to the ESP's API, and whether
+ a new connection was created.
+
+ Callers must ensure they later call close, if (and only if) open
+ returns True.
+ """
+ # Subclasses should use an instance property to maintain a cached
+ # connection, and return True iff they initialize that instance
+ # property in _this_ open call. (If the cached connection already
+ # exists, just do nothing and return False.)
+ #
+ # Subclasses should swallow operational errors if self.fail_silently
+ # (e.g., network errors), but otherwise can raise any errors.
+ #
+ # (Returning a bool to indicate whether connection was created is
+ # borrowed from django.core.email.backends.SMTPBackend)
+ return False
+
+ def close(self):
+ """
+ Close the cached connection created by open.
+
+ You must only call close if your code called open and it returned True.
+ """
+ # Subclasses should tear down the cached connection and clear
+ # the instance property.
+ #
+ # Subclasses should swallow operational errors if self.fail_silently
+ # (e.g., network errors), but otherwise can raise any errors.
+ pass
+
+ def send_messages(self, email_messages):
+ """
+ Sends one or more EmailMessage objects and returns the number of email
+ messages sent.
+ """
+ # This API is specified by Django's core BaseEmailBackend
+ # (so you can't change it to, e.g., return detailed status).
+ # Subclasses shouldn't need to override.
+
+ num_sent = 0
+ if not email_messages:
+ return num_sent
+
+ created_session = self.open()
+
+ try:
+ for message in email_messages:
+ try:
+ sent = self._send(message)
+ except AnymailError:
+ if self.fail_silently:
+ sent = False
+ else:
+ raise
+ if sent:
+ num_sent += 1
+ finally:
+ if created_session:
+ self.close()
+
+ return num_sent
+
+ def _send(self, message):
+ """Sends the EmailMessage message, and returns True if the message was sent.
+
+ This should only be called by the base send_messages loop.
+
+ Implementations must raise exceptions derived from AnymailError for
+ anticipated failures that should be suppressed in fail_silently mode.
+ """
+ message.anymail_status = AnymailStatus()
+ if not self.run_pre_send(message): # (might modify message)
+ return False # cancel send without error
+
+ if not message.recipients():
+ return False
+
+ payload = self.build_message_payload(message, self.send_defaults)
+ response = self.post_to_esp(payload, message)
+ message.anymail_status.esp_response = response
+
+ recipient_status = self.parse_recipient_status(response, payload, message)
+ message.anymail_status.set_recipient_status(recipient_status)
+
+ self.run_post_send(message) # send signal before raising status errors
+ self.raise_for_recipient_status(message.anymail_status, response, payload, message)
+
+ return True
+
+ def run_pre_send(self, message):
+ """Send pre_send signal, and return True if message should still be sent"""
+ try:
+ pre_send.send(self.__class__, message=message, esp_name=self.esp_name)
+ return True
+ except AnymailCancelSend:
+ return False # abort without causing error
+
+ def run_post_send(self, message):
+ """Send post_send signal to all receivers"""
+ results = post_send.send_robust(
+ self.__class__, message=message, status=message.anymail_status, esp_name=self.esp_name)
+ for (receiver, response) in results:
+ if isinstance(response, Exception):
+ raise response
+
+ def build_message_payload(self, message, defaults):
+ """Returns a payload that will allow message to be sent via the ESP.
+
+ Derived classes must implement, and should subclass :class:BasePayload
+ to get standard Anymail options.
+
+ Raises :exc:AnymailUnsupportedFeature for message options that
+ cannot be communicated to the ESP.
+
+ :param message: :class:EmailMessage
+ :param defaults: dict
+ :return: :class:BasePayload
+ """
+ raise NotImplementedError("%s.%s must implement build_message_payload" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def post_to_esp(self, payload, message):
+ """Post payload to ESP send API endpoint, and return the raw response.
+
+ payload is the result of build_message_payload
+ message is the original EmailMessage
+ return should be a raw response
+
+ Can raise AnymailAPIError (or derived exception) for problems posting to the ESP
+ """
+ raise NotImplementedError("%s.%s must implement post_to_esp" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def parse_recipient_status(self, response, payload, message):
+ """Return a dict mapping email to AnymailRecipientStatus for each recipient.
+
+ Can raise AnymailAPIError (or derived exception) if response is unparsable
+ """
+ raise NotImplementedError("%s.%s must implement parse_recipient_status" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def raise_for_recipient_status(self, anymail_status, response, payload, message):
+ """If *all* recipients are refused or invalid, raises AnymailRecipientsRefused"""
+ if not self.ignore_recipient_status:
+ # Error if *all* recipients are invalid or refused
+ # (This behavior parallels smtplib.SMTPRecipientsRefused from Django's SMTP EmailBackend)
+ if anymail_status.status.issubset({"invalid", "rejected"}):
+ raise AnymailRecipientsRefused(email_message=message, payload=payload, response=response)
+
+ @property
+ def esp_name(self):
+ """
+ Read-only name of the ESP for this backend.
+
+ (E.g., MailgunBackend will return "Mailgun")
+ """
+ return self.__class__.__name__.replace("Backend", "")
+
+
+class BasePayload(object):
+ # attr, combiner, converter
+ 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),
+ ('reply_to', combine, 'parsed_emails'),
+ ('extra_headers', combine, None),
+ ('body', last, None), # special handling below checks message.content_subtype
+ ('alternatives', combine, None),
+ ('attachments', combine, 'prepped_attachments'),
+ )
+ anymail_message_attrs = (
+ # Anymail expando-props
+ ('metadata', combine, None),
+ ('send_at', last, 'aware_datetime'),
+ ('tags', combine, None),
+ ('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),
+ )
+ esp_message_attrs = () # subclasses can override
+
+ def __init__(self, message, defaults, backend):
+ self.message = message
+ self.defaults = defaults
+ self.backend = backend
+ self.esp_name = backend.esp_name
+
+ self.init_payload()
+
+ # we should consider hoisting the first text/html out of alternatives into set_html_body
+ 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 combiner is not None:
+ default_value = self.defaults.get(attr, UNSET)
+ value = combiner(default_value, value)
+ if value is not UNSET:
+ if converter is not None:
+ if not callable(converter):
+ converter = getattr(self, converter)
+ value = converter(value)
+ if value is not UNSET:
+ if attr == 'body':
+ setter = self.set_html_body if message.content_subtype == 'html' else self.set_text_body
+ else:
+ # AttributeError here? Your Payload subclass is missing a set_<attr> implementation
+ setter = getattr(self, 'set_%s' % attr)
+ setter(value)
+
+ def unsupported_feature(self, feature):
+ if not self.backend.ignore_unsupported_features:
+ raise AnymailUnsupportedFeature("%s does not support %s" % (self.esp_name, feature),
+ email_message=self.message, payload=self, backend=self.backend)
+
+ #
+ # Attribute converters
+ #
+
+ def parsed_email(self, address):
+ return ParsedEmail(address, self.message.encoding)
+
+ def parsed_emails(self, addresses):
+ encoding = self.message.encoding
+ return [ParsedEmail(address, encoding) for address in addresses]
+
+ def prepped_attachments(self, attachments):
+ str_encoding = self.message.encoding or settings.DEFAULT_CHARSET
+ return [Attachment(attachment, str_encoding) for attachment in attachments]
+
+ def aware_datetime(self, value):
+ """Converts a date or datetime or timestamp to an aware datetime.
+
+ Naive datetimes are assumed to be in Django's current_timezone.
+ Dates are interpreted as midnight that date, in Django's current_timezone.
+ Integers are interpreted as POSIX timestamps (which are inherently UTC).
+
+ Anything else (e.g., str) is returned unchanged, which won't be portable.
+ """
+ if isinstance(value, datetime):
+ dt = value
+ else:
+ if isinstance(value, date):
+ dt = datetime(value.year, value.month, value.day) # naive, midnight
+ else:
+ try:
+ dt = datetime.utcfromtimestamp(value).replace(tzinfo=utc)
+ except (TypeError, ValueError):
+ return value
+ if is_naive(dt):
+ dt = make_aware(dt, get_current_timezone())
+ return dt
+
+ #
+ # Abstract implementation
+ #
+
+ def init_payload(self):
+ raise NotImplementedError("%s.%s must implement init_payload" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def set_from_email(self, email):
+ raise NotImplementedError("%s.%s must implement set_from_email" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def set_to(self, emails):
+ return self.set_recipients('to', emails)
+
+ def set_cc(self, emails):
+ return self.set_recipients('cc', emails)
+
+ def set_bcc(self, emails):
+ return self.set_recipients('bcc', emails)
+
+ def set_recipients(self, recipient_type, emails):
+ for email in emails:
+ self.add_recipient(recipient_type, email)
+
+ def add_recipient(self, recipient_type, email):
+ raise NotImplementedError("%s.%s must implement add_recipient, set_recipients, or set_{to,cc,bcc}" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def set_subject(self, subject):
+ raise NotImplementedError("%s.%s must implement set_subject" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def set_reply_to(self, emails):
+ self.unsupported_feature('reply_to')
+
+ def set_extra_headers(self, headers):
+ self.unsupported_feature('extra_headers')
+
+ def set_text_body(self, body):
+ raise NotImplementedError("%s.%s must implement set_text_body" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def set_html_body(self, body):
+ raise NotImplementedError("%s.%s must implement set_html_body" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ def set_alternatives(self, alternatives):
+ for content, mimetype in alternatives:
+ if mimetype == "text/html":
+ # This assumes that there's at most one html alternative,
+ # and so it should be the html body. (Most ESPs don't
+ # support multiple html alternative parts anyway.)
+ self.set_html_body(content)
+ else:
+ self.add_alternative(content, mimetype)
+
+ def add_alternative(self, content, mimetype):
+ self.unsupported_feature("alternative part with type '%s'" % mimetype)
+
+ def set_attachments(self, attachments):
+ for attachment in attachments:
+ self.add_attachment(attachment)
+
+ def add_attachment(self, attachment):
+ raise NotImplementedError("%s.%s must implement add_attachment or set_attachments" %
+ (self.__class__.__module__, self.__class__.__name__))
+
+ # Anymail-specific payload construction
+ def set_metadata(self, metadata):
+ self.unsupported_feature("metadata")
+
+ def set_send_at(self, send_at):
+ self.unsupported_feature("send_at")
+
+ def set_tags(self, tags):
+ self.unsupported_feature("tags")
+
+ def set_track_clicks(self, track_clicks):
+ self.unsupported_feature("track_clicks")
+
+ def set_track_opens(self, track_opens):
+ self.unsupported_feature("track_opens")
+
+ def set_template_id(self, template_id):
+ self.unsupported_feature("template_id")
+
+ def set_merge_data(self, merge_data):
+ self.unsupported_feature("merge_data")
+
+ def set_merge_global_data(self, merge_global_data):
+ self.unsupported_feature("merge_global_data")
+
+ # ESP-specific payload construction
+ def set_esp_extra(self, extra):
+ self.unsupported_feature("esp_extra")
diff --git a/anymail/backends/base_requests.py b/anymail/backends/base_requests.py
new file mode 100644
index 0000000..294ad5f
--- /dev/null
+++ b/anymail/backends/base_requests.py
@@ -0,0 +1,158 @@
+import json
+
+import requests
+# noinspection PyUnresolvedReferences
+from six.moves.urllib.parse import urljoin
+
+from .base import AnymailBaseBackend, BasePayload
+from ..exceptions import AnymailRequestsAPIError, AnymailSerializationError
+from .._version import __version__
+
+
+class AnymailRequestsBackend(AnymailBaseBackend):
+ """
+ Base Anymail email backend for ESPs that use an HTTP API via requests
+ """
+
+ def __init__(self, api_url, **kwargs):
+ """Init options from Django settings"""
+ self.api_url = api_url
+ super(AnymailRequestsBackend, self).__init__(**kwargs)
+ self.session = None
+
+ def open(self):
+ if self.session:
+ return False # already exists
+
+ try:
+ self.session = requests.Session()
+ except requests.RequestException:
+ if not self.fail_silently:
+ raise
+ else:
+ self.session.headers["User-Agent"] = "django-anymail/{version}-{esp} {orig}".format(
+ esp=self.esp_name.lower(), version=__version__,
+ orig=self.session.headers.get("User-Agent", ""))
+ return True
+
+ def close(self):
+ if self.session is None:
+ return
+ try:
+ self.session.close()
+ except requests.RequestException:
+ if not self.fail_silently:
+ raise
+ finally:
+ self.session = None
+
+ def _send(self, message):
+ if self.session is None:
+ class_name = self.__class__.__name__
+ raise RuntimeError(
+ "Session has not been opened in {class_name}._send. "
+ "(This is either an implementation error in {class_name}, "
+ "or you are incorrectly calling _send directly.)".format(class_name=class_name))
+ return super(AnymailRequestsBackend, self)._send(message)
+
+ def post_to_esp(self, payload, message):
+ """Post payload to ESP send API endpoint, and return the raw response.
+
+ payload is the result of build_message_payload
+ message is the original EmailMessage
+ return should be a requests.Response
+
+ Can raise AnymailRequestsAPIError for HTTP errors in the post
+ """
+ params = payload.get_request_params(self.api_url)
+ try:
+ response = self.session.request(**params)
+ except requests.RequestException as err:
+ # raise an exception that is both AnymailRequestsAPIError
+ # and the original requests exception type
+ exc_class = type('AnymailRequestsAPIError', (AnymailRequestsAPIError, type(err)), {})
+ raise exc_class(
+ "Error posting to %s:" % params.get('url', '<missing url>'),
+ raised_from=err, email_message=message, payload=payload)
+ self.raise_for_status(response, payload, message)
+ return response
+
+ def raise_for_status(self, response, payload, message):
+ """Raise AnymailRequestsAPIError if response is an HTTP error
+
+ Subclasses can override for custom error checking
+ (though should defer parsing/deserialization of the body to
+ parse_recipient_status)
+ """
+ if response.status_code != 200:
+ raise AnymailRequestsAPIError(email_message=message, payload=payload, response=response)
+
+ def deserialize_json_response(self, response, payload, message):
+ """Deserialize an ESP API response that's in json.
+
+ Useful for implementing deserialize_response
+ """
+ try:
+ return response.json()
+ except ValueError:
+ raise AnymailRequestsAPIError("Invalid JSON in %s API response" % self.esp_name,
+ email_message=message, payload=payload, response=response)
+
+
+class RequestsPayload(BasePayload):
+ """Abstract Payload for AnymailRequestsBackend"""
+
+ def __init__(self, message, defaults, backend,
+ method="POST", params=None, data=None,
+ headers=None, files=None, auth=None):
+ self.method = method
+ self.params = params
+ self.data = data
+ self.headers = headers
+ self.files = files
+ self.auth = auth
+ super(RequestsPayload, self).__init__(message, defaults, backend)
+
+ def get_request_params(self, api_url):
+ """Returns a dict of requests.request params that will send payload to the ESP.
+
... 3280 lines suppressed ...
--
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