[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