mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-23 05:11:04 -05:00
@@ -2,7 +2,7 @@ language: python
|
|||||||
python:
|
python:
|
||||||
- "2.6"
|
- "2.6"
|
||||||
- "2.7"
|
- "2.7"
|
||||||
# - "3.2" # Requests setup currently broken under python 3
|
- "3.2"
|
||||||
env:
|
env:
|
||||||
- DJANGO=django==1.3
|
- DJANGO=django==1.3
|
||||||
- DJANGO=django==1.4
|
- DJANGO=django==1.4
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ ArnaudF
|
|||||||
Théo Crevon
|
Théo Crevon
|
||||||
Rafael E. Belliard
|
Rafael E. Belliard
|
||||||
Jared Morse
|
Jared Morse
|
||||||
|
peillis
|
||||||
|
|||||||
170
README.rst
170
README.rst
@@ -7,6 +7,10 @@ Djrill, for Mandrill
|
|||||||
Djrill is an email backend for Django users who want to take advantage of the
|
Djrill is an email backend for Django users who want to take advantage of the
|
||||||
Mandrill_ transactional email service from MailChimp_.
|
Mandrill_ transactional email service from MailChimp_.
|
||||||
|
|
||||||
|
In general, Djrill "just works" with Django's built-in `django.core.mail`_
|
||||||
|
package. You can also take advantage of Mandrill-specific features like tags,
|
||||||
|
metadata, and tracking.
|
||||||
|
|
||||||
An optional Django admin interface is included. The admin interface allows you to:
|
An optional Django admin interface is included. The admin interface allows you to:
|
||||||
|
|
||||||
* Check the status of your Mandrill API connection.
|
* Check the status of your Mandrill API connection.
|
||||||
@@ -74,7 +78,9 @@ In ``settings.py``:
|
|||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
Since you are replacing the global ``EMAIL_BACKEND``, **all** emails are sent through Mandrill's service.
|
Since you are replacing the global ``EMAIL_BACKEND``, **all** emails are sent
|
||||||
|
through Mandrill's service. (To selectively use Mandrill for some messages, see
|
||||||
|
`Using Multiple Email Backends`_ below.)
|
||||||
|
|
||||||
In general, Djrill "just works" with Django's built-in `django.core.mail`_
|
In general, Djrill "just works" with Django's built-in `django.core.mail`_
|
||||||
package, including ``send_mail``, ``send_mass_mail``, ``EmailMessage`` and
|
package, including ``send_mail``, ``send_mass_mail``, ``EmailMessage`` and
|
||||||
@@ -83,7 +89,8 @@ package, including ``send_mail``, ``send_mass_mail``, ``EmailMessage`` and
|
|||||||
You can also take advantage of Mandrill-specific features like tags, metadata,
|
You can also take advantage of Mandrill-specific features like tags, metadata,
|
||||||
and tracking by creating a Django EmailMessage_ (or for HTML,
|
and tracking by creating a Django EmailMessage_ (or for HTML,
|
||||||
EmailMultiAlternatives_) object and setting Mandrill-specific
|
EmailMultiAlternatives_) object and setting Mandrill-specific
|
||||||
properties on it before calling its ``send`` method.
|
properties on it before calling its ``send`` method. (See
|
||||||
|
`Mandrill Message Options`_ below.)
|
||||||
|
|
||||||
Example, sending HTML email with Mandrill tags and metadata:
|
Example, sending HTML email with Mandrill tags and metadata:
|
||||||
|
|
||||||
@@ -107,33 +114,54 @@ Example, sending HTML email with Mandrill tags and metadata:
|
|||||||
# Send it:
|
# Send it:
|
||||||
msg.send()
|
msg.send()
|
||||||
|
|
||||||
If the Mandrill API returns an error response for any reason, the send call will
|
If the email tries to use features that aren't supported by Mandrill, the send
|
||||||
raise a ``djrill.mail.backends.djrill.DjrillBackendHTTPError`` exception
|
call will raise a ``djrill.NotSupportedByMandrillError`` exception (a subclass
|
||||||
(unless called with fail_silently=True).
|
of ValueError).
|
||||||
|
|
||||||
|
If the Mandrill API fails or returns an error response, the send call will
|
||||||
|
raise a ``djrill.MandrillAPIError`` exception (a subclass of
|
||||||
|
requests.HTTPError).
|
||||||
|
|
||||||
|
|
||||||
|
Django EmailMessage Support
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Djrill supports most of the functionality of Django's `EmailMessage`_ and
|
Djrill supports most of the functionality of Django's `EmailMessage`_ and
|
||||||
`EmailMultiAlternatives`_ classes. Some limitations:
|
`EmailMultiAlternatives`_ classes. Some notes and limitations:
|
||||||
|
|
||||||
* Djrill accepts additional headers, but only ``Reply-To`` and ``X-*`` (since
|
* **Display Names:** All email addresses (from, to, cc) can be simple
|
||||||
that is all that Mandrill accepts). Any other extra headers will raise a
|
("email@example.com") or can include a display name
|
||||||
``ValueError`` exception when you attempt to send the message.
|
("Real Name <email@example.com>").
|
||||||
* Djrill requires that if you ``attach_alternative`` to a message, there must be
|
* **From Address:** The ``from_email`` must be in one of the approved sending
|
||||||
only one alternative type, and it must be text/html. Otherwise, Djrill will
|
domains in your Mandrill account.
|
||||||
raise a ``ValueError`` exception when you attempt to send the message.
|
* **CC Recipients:** Djrill treats all "cc" recipients as if they were
|
||||||
(Mandrill doesn't support sending multiple html alternative parts, or any
|
additional "to" addresses. (Mandrill does not distinguish "cc" from "to".)
|
||||||
non-html alternatives.)
|
Note that you will also need to set ``preserve_recipients`` True if you want
|
||||||
* Djrill (currently) silently ignores all attachments on a message.
|
each recipient to see the other recipients listed in the email headers.
|
||||||
* Djrill treats all cc and bcc recipients as if they were additional "to"
|
* **BCC Recipients:** Mandrill does not permit more than one "bcc" address.
|
||||||
addresses. (Mandrill does not distinguish cc, and only allows a single bcc --
|
Djrill raises ``djrill.NotSupportedByMandrillError`` if you attempt to send a
|
||||||
which Djrill doesn't use. *Caution:* depending on the ``preserve_recipients``
|
message with multiple bcc's. (Mandrill's bcc option seems intended primarily
|
||||||
setting, this could result in exposing bcc addresses to all recipients. It's
|
for logging. To send a single message to multiple recipients without exposing
|
||||||
probably best to just avoid bcc.)
|
their email addresses to each other, simply include them all in the "to" list
|
||||||
* All email addresses (from, to, cc) can be simple ("email@example.com") or
|
and leave ``preserve_recipients`` set to False.)
|
||||||
can include a display name ("Real Name <email@example.com>").
|
* **Attachments:** Djrill includes a message's attachments, but only with the
|
||||||
* The ``from_email`` must be in one of the approved sending domains in your
|
mimetypes "text/\*", "image/\*", or "application/pdf" (since that is all
|
||||||
Mandrill account.
|
Mandrill allows). Any other attachment types will raise
|
||||||
|
``djrill.NotSupportedByMandrillError`` when you attempt to send the message.
|
||||||
|
* **Headers:** Djrill accepts additional headers, but only ``Reply-To`` and
|
||||||
|
``X-*`` (since that is all that Mandrill accepts). Any other extra headers
|
||||||
|
will raise ``djrill.NotSupportedByMandrillError`` when you attempt to send the
|
||||||
|
message.
|
||||||
|
* **Alternative Parts:** Djrill requires that if you ``attach_alternative`` to a
|
||||||
|
message, there must be only one alternative part, and it must be text/html.
|
||||||
|
Otherwise, Djrill will raise ``djrill.NotSupportedByMandrillError`` when you
|
||||||
|
attempt to send the message. (Mandrill doesn't support sending multiple html
|
||||||
|
alternative parts, or any non-html alternatives.)
|
||||||
|
|
||||||
Many of the options from the Mandrill `messages/send.json API`_ ``message``
|
Mandrill Message Options
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Many of the options from the Mandrill `messages/send API`_ ``message``
|
||||||
struct can be set directly on an ``EmailMessage`` (or subclass) object:
|
struct can be set directly on an ``EmailMessage`` (or subclass) object:
|
||||||
|
|
||||||
* ``track_opens`` - Boolean
|
* ``track_opens`` - Boolean
|
||||||
@@ -142,7 +170,7 @@ struct can be set directly on an ``EmailMessage`` (or subclass) object:
|
|||||||
default in your Mandrill account sending options.)
|
default in your Mandrill account sending options.)
|
||||||
* ``auto_text`` - Boolean
|
* ``auto_text`` - Boolean
|
||||||
* ``url_strip_qs`` - Boolean
|
* ``url_strip_qs`` - Boolean
|
||||||
* ``preserve_recipients`` - Boolean -- see the caution about bcc addresses above
|
* ``preserve_recipients`` - Boolean
|
||||||
* ``global_merge_vars`` - a dict -- e.g.,
|
* ``global_merge_vars`` - a dict -- e.g.,
|
||||||
``{ 'company': "ACME", 'offer': "10% off" }``
|
``{ 'company': "ACME", 'offer': "10% off" }``
|
||||||
* ``recipient_merge_vars`` - a dict whose keys are the recipient email addresses
|
* ``recipient_merge_vars`` - a dict whose keys are the recipient email addresses
|
||||||
@@ -161,14 +189,67 @@ object, so you can use them with many other apps that add Django mail
|
|||||||
functionality (such as Django template-based messages).
|
functionality (such as Django template-based messages).
|
||||||
|
|
||||||
If you have any questions about the python syntax for any of these properties,
|
If you have any questions about the python syntax for any of these properties,
|
||||||
see ``DjrillMandrillFeatureTests`` in tests.py for examples.
|
see ``DjrillMandrillFeatureTests`` in tests/test_mandrill_send.py for examples.
|
||||||
|
|
||||||
|
Mandrill Templates
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To use a Mandrill (MailChimp) template, set a ``template_name`` and (optionally)
|
||||||
|
``template_content`` on your ``EmailMessage`` object:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
msg = EmailMessage(subject="Shipped!", from_email="store@example.com",
|
||||||
|
to=["customer@example.com", "accounting@example.com"])
|
||||||
|
msg.template_name = "SHIPPING_NOTICE" # A Mandrill template name
|
||||||
|
msg.template_content = { # Content blocks to fill in
|
||||||
|
'TRACKING_BLOCK': "<a href='.../\*\|TRACKINGNO\|\*'>track it</a>" }
|
||||||
|
msg.global_merge_vars = { # Merge tags in your template
|
||||||
|
'ORDERNO': "12345", 'TRACKINGNO': "1Z987" }
|
||||||
|
msg.merge_vars = { # Per-recipient merge tags
|
||||||
|
'accounting@example.com': { 'NAME': "Pat" },
|
||||||
|
'customer@example.com': { 'NAME': "Kim" } }
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
If template_name is set, Djrill will use Mandrill's `messages/send-template API`_,
|
||||||
|
rather than messages/send. All of the other options listed above can be used.
|
||||||
|
|
||||||
|
(This is for *MailChimp* templates stored in your Mandrill account. If you
|
||||||
|
want to use a *Django* template, you can use Django's render_to_string_ template
|
||||||
|
shortcut to build the body and html, and send using EmailMultiAlternatives as
|
||||||
|
in the earlier examples.)
|
||||||
|
|
||||||
|
Using Multiple Email Backends
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
You can use Django mail's optional ``connection`` argument to send some mail
|
||||||
|
through Mandrill and others through a different system. This can be useful to
|
||||||
|
send customer emails with Mandrill, but admin emails directly through an SMTP
|
||||||
|
server. Example:
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
from django.core.mail import send_mail, get_connection
|
||||||
|
|
||||||
|
# send_mail connection defaults to the settings EMAIL_BACKEND, which
|
||||||
|
# we've set to DjrillBackend. This will be sent using Mandrill:
|
||||||
|
send_mail("Subject", "Body", "support@example.com", ["user@example.com"])
|
||||||
|
|
||||||
|
# Get a connection to an SMTP backend, and send using that instead:
|
||||||
|
smtp_backend = get_connection('django.core.mail.backends.smtp.EmailBackend')
|
||||||
|
send_mail("Subject", "Body", "admin@example.com", ["alert@example.com"],
|
||||||
|
connection=smtp_backend)
|
||||||
|
|
||||||
|
You can supply a different connection to Django's `django.core.mail`_
|
||||||
|
``send_mail`` and ``send_mass_mail`` helpers, and in the constructor for an
|
||||||
|
EmailMessage_ or EmailMultiAlternatives_.
|
||||||
|
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
Djrill is tested against Django 1.3 and 1.4 on Python 2.6 and 2.7, and
|
Djrill is tested against Django 1.3 and 1.4 on Python 2.6 and 2.7, and
|
||||||
Django 1.5beta on Python 2.7.
|
Django 1.5RC on Python 2.7 and 3.2.
|
||||||
(It may also work with Django 1.2 and Python 2.5, if you use an older
|
(It may also work with Django 1.2 and Python 2.5, if you use an older
|
||||||
version of requests compatible with that code.)
|
version of requests compatible with that code.)
|
||||||
|
|
||||||
@@ -180,16 +261,41 @@ calls, without actually calling Mandrill or sending any email. So the tests
|
|||||||
don't require a Mandrill API key, but they *do* require mock_
|
don't require a Mandrill API key, but they *do* require mock_
|
||||||
(``pip install mock``). To run the tests, either::
|
(``pip install mock``). To run the tests, either::
|
||||||
|
|
||||||
python setup.py test
|
python -Wall setup.py test
|
||||||
|
|
||||||
or::
|
or::
|
||||||
|
|
||||||
python runtests.py
|
python -Wall runtests.py
|
||||||
|
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
Djrill is maintained by its users -- it's not managed by the folks at MailChimp.
|
||||||
|
Pull requests are always welcome to improve support for Mandrill and Django
|
||||||
|
features.
|
||||||
|
|
||||||
|
Please include test cases with pull requests. (And by submitting a pull request,
|
||||||
|
you're agreeing to release your changes under under the same BSD license as the
|
||||||
|
rest of this project.)
|
||||||
|
|
||||||
|
|
||||||
Release Notes
|
Release Notes
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
Version 0.3.0:
|
||||||
|
|
||||||
|
* Attachments are now supported
|
||||||
|
* Mandrill templates are now supported
|
||||||
|
* A bcc address is now passed to Mandrill as bcc, rather than being lumped in
|
||||||
|
with the "to" recipients. Multiple bcc recipients will now raise an exception,
|
||||||
|
as Mandrill only allows one.
|
||||||
|
* Python 3 support (with Django 1.5)
|
||||||
|
* Exceptions should be more useful: ``djrill.NotSupportedByMandrillError``
|
||||||
|
replaces generic ValueError; ``djrill.MandrillAPIError`` replaces
|
||||||
|
DjrillBackendHTTPError, and is now derived from requests.HTTPError. (New
|
||||||
|
exceptions are backwards compatible with old ones for existing code.)
|
||||||
|
|
||||||
Version 0.2.0:
|
Version 0.2.0:
|
||||||
|
|
||||||
* ``MANDRILL_API_URL`` is no longer required in settings.py
|
* ``MANDRILL_API_URL`` is no longer required in settings.py
|
||||||
@@ -215,5 +321,7 @@ the awesome ``requests`` library.
|
|||||||
.. _django.core.mail: https://docs.djangoproject.com/en/dev/topics/email/
|
.. _django.core.mail: https://docs.djangoproject.com/en/dev/topics/email/
|
||||||
.. _EmailMessage: https://docs.djangoproject.com/en/dev/topics/email/#django.core.mail.EmailMessage
|
.. _EmailMessage: https://docs.djangoproject.com/en/dev/topics/email/#django.core.mail.EmailMessage
|
||||||
.. _EmailMultiAlternatives: https://docs.djangoproject.com/en/dev/topics/email/#sending-alternative-content-types
|
.. _EmailMultiAlternatives: https://docs.djangoproject.com/en/dev/topics/email/#sending-alternative-content-types
|
||||||
.. _messages/send.json API: https://mandrillapp.com/api/docs/messages.html#method=send
|
.. _render_to_string: https://docs.djangoproject.com/en/dev/ref/templates/api/#the-render-to-string-shortcut
|
||||||
|
.. _messages/send API: https://mandrillapp.com/api/docs/messages.html#method=send
|
||||||
|
.. _messages/send-template API: https://mandrillapp.com/api/docs/messages.html#method=send-template
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
|
from django.conf import settings
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
|
|
||||||
VERSION = (0, 2, 0)
|
from djrill.exceptions import MandrillAPIError, NotSupportedByMandrillError
|
||||||
|
|
||||||
|
VERSION = (0, 3, 0)
|
||||||
__version__ = '.'.join([str(x) for x in VERSION])
|
__version__ = '.'.join([str(x) for x in VERSION])
|
||||||
|
|
||||||
|
# This backend was developed against this API endpoint.
|
||||||
|
# You can override in settings.py, if desired.
|
||||||
|
MANDRILL_API_URL = getattr(settings, "MANDRILL_API_URL",
|
||||||
|
"http://mandrillapp.com/api/1.0")
|
||||||
|
|
||||||
class DjrillAdminSite(AdminSite):
|
class DjrillAdminSite(AdminSite):
|
||||||
index_template = "djrill/index.html"
|
index_template = "djrill/index.html"
|
||||||
@@ -31,6 +38,7 @@ class DjrillAdminSite(AdminSite):
|
|||||||
from django.conf.urls import include, patterns, url
|
from django.conf.urls import include, patterns, url
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Django 1.3
|
# Django 1.3
|
||||||
|
#noinspection PyDeprecation
|
||||||
from django.conf.urls.defaults import include, patterns, url
|
from django.conf.urls.defaults import include, patterns, url
|
||||||
for path, view, name, display_name in self.custom_views:
|
for path, view, name, display_name in self.custom_views:
|
||||||
urls += patterns('',
|
urls += patterns('',
|
||||||
|
|||||||
33
djrill/exceptions.py
Normal file
33
djrill/exceptions.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from requests import HTTPError
|
||||||
|
|
||||||
|
class MandrillAPIError(HTTPError):
|
||||||
|
"""Exception for unsuccessful response from Mandrill API."""
|
||||||
|
def __init__(self, status_code, response=None, log_message=None):
|
||||||
|
super(MandrillAPIError, self).__init__()
|
||||||
|
self.status_code = status_code
|
||||||
|
self.response = response # often contains helpful Mandrill info
|
||||||
|
self.log_message = log_message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
message = "Mandrill API response %d" % self.status_code
|
||||||
|
if self.log_message:
|
||||||
|
message += "\n" + self.log_message
|
||||||
|
if self.response:
|
||||||
|
message += "\nResponse: " + getattr(self.response, 'content', "")
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
|
class NotSupportedByMandrillError(ValueError):
|
||||||
|
"""Exception for email features that Mandrill doesn't support.
|
||||||
|
|
||||||
|
This is typically raised when attempting to send a Django EmailMessage that
|
||||||
|
uses options or values you might expect to work, but that are silently
|
||||||
|
ignored by or can't be communicated to Mandrill's API. (E.g., unsupported
|
||||||
|
attachment types, multiple bcc recipients.)
|
||||||
|
|
||||||
|
It's generally *not* raised for Mandrill-specific features, like limitations
|
||||||
|
on Mandrill tag names or restrictions on from emails. (Djrill expects
|
||||||
|
Mandrill to return an API error for these where appropriate, and tries to
|
||||||
|
avoid duplicating Mandrill's validation logic locally.)
|
||||||
|
|
||||||
|
"""
|
||||||
@@ -1,31 +1,20 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.core.mail.backends.base import BaseEmailBackend
|
from django.core.mail.backends.base import BaseEmailBackend
|
||||||
from django.core.mail.message import sanitize_address
|
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
|
# Oops: this file has the same name as our app, and cannot be renamed.
|
||||||
|
#from djrill import MANDRILL_API_URL, MandrillAPIError, NotSupportedByMandrillError
|
||||||
|
from ... import MANDRILL_API_URL, MandrillAPIError, NotSupportedByMandrillError
|
||||||
|
|
||||||
|
from base64 import b64encode
|
||||||
|
from email.mime.base import MIMEBase
|
||||||
from email.utils import parseaddr
|
from email.utils import parseaddr
|
||||||
|
import mimetypes
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
# This backend was developed against this API endpoint.
|
DjrillBackendHTTPError = MandrillAPIError # Backwards-compat Djrill<=0.2.0
|
||||||
# You can override in settings.py, if desired.
|
|
||||||
MANDRILL_API_URL = "http://mandrillapp.com/api/1.0"
|
|
||||||
|
|
||||||
class DjrillBackendHTTPError(Exception):
|
|
||||||
"""An exception that will turn into an HTTP error response."""
|
|
||||||
def __init__(self, status_code, response=None, log_message=None):
|
|
||||||
super(DjrillBackendHTTPError, self).__init__()
|
|
||||||
self.status_code = status_code
|
|
||||||
self.response = response # often contains helpful Mandrill info
|
|
||||||
self.log_message = log_message
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
message = "DjrillBackendHTTP %d" % self.status_code
|
|
||||||
if self.log_message:
|
|
||||||
return message + " " + self.log_message
|
|
||||||
else:
|
|
||||||
return message
|
|
||||||
|
|
||||||
|
|
||||||
class DjrillBackend(BaseEmailBackend):
|
class DjrillBackend(BaseEmailBackend):
|
||||||
"""
|
"""
|
||||||
@@ -38,13 +27,14 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
"""
|
"""
|
||||||
super(DjrillBackend, self).__init__(**kwargs)
|
super(DjrillBackend, self).__init__(**kwargs)
|
||||||
self.api_key = getattr(settings, "MANDRILL_API_KEY", None)
|
self.api_key = getattr(settings, "MANDRILL_API_KEY", None)
|
||||||
self.api_url = getattr(settings, "MANDRILL_API_URL", MANDRILL_API_URL)
|
self.api_url = MANDRILL_API_URL
|
||||||
|
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ImproperlyConfigured("You have not set your mandrill api key "
|
raise ImproperlyConfigured("You have not set your mandrill api key "
|
||||||
"in the settings.py file.")
|
"in the settings.py file.")
|
||||||
|
|
||||||
self.api_action = self.api_url + "/messages/send.json"
|
self.api_send = self.api_url + "/messages/send.json"
|
||||||
|
self.api_send_template = self.api_url + "/messages/send-template.json"
|
||||||
|
|
||||||
def send_messages(self, email_messages):
|
def send_messages(self, email_messages):
|
||||||
if not email_messages:
|
if not email_messages:
|
||||||
@@ -68,21 +58,34 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
self._add_mandrill_options(message, msg_dict)
|
self._add_mandrill_options(message, msg_dict)
|
||||||
if getattr(message, 'alternatives', None):
|
if getattr(message, 'alternatives', None):
|
||||||
self._add_alternatives(message, msg_dict)
|
self._add_alternatives(message, msg_dict)
|
||||||
except ValueError:
|
self._add_attachments(message, msg_dict)
|
||||||
|
except NotSupportedByMandrillError:
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise
|
raise
|
||||||
return False
|
return False
|
||||||
|
|
||||||
djrill_it = requests.post(self.api_action, data=json.dumps({
|
api_url = self.api_send
|
||||||
|
api_params = {
|
||||||
"key": self.api_key,
|
"key": self.api_key,
|
||||||
"message": msg_dict
|
"message": msg_dict
|
||||||
}))
|
}
|
||||||
|
|
||||||
if djrill_it.status_code != 200:
|
# check if template is set in message to send it via
|
||||||
|
# api url: /messages/send-template.json
|
||||||
|
if hasattr(message, 'template_name'):
|
||||||
|
api_url = self.api_send_template
|
||||||
|
api_params['template_name'] = message.template_name
|
||||||
|
if hasattr(message, 'template_content'):
|
||||||
|
api_params['template_content'] = \
|
||||||
|
self._expand_merge_vars(message.template_content)
|
||||||
|
|
||||||
|
response = requests.post(api_url, data=json.dumps(api_params))
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
if not self.fail_silently:
|
if not self.fail_silently:
|
||||||
raise DjrillBackendHTTPError(
|
raise MandrillAPIError(
|
||||||
status_code=djrill_it.status_code,
|
status_code=response.status_code,
|
||||||
response = djrill_it,
|
response=response,
|
||||||
log_message="Failed to send a message to %s, from %s" %
|
log_message="Failed to send a message to %s, from %s" %
|
||||||
(msg_dict['to'], msg_dict['from_email']))
|
(msg_dict['to'], msg_dict['from_email']))
|
||||||
return False
|
return False
|
||||||
@@ -95,16 +98,18 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
use by default. Standard text email messages sent through Django will
|
use by default. Standard text email messages sent through Django will
|
||||||
still work through Mandrill.
|
still work through Mandrill.
|
||||||
|
|
||||||
Raises ValueError for any standard EmailMessage features that cannot be
|
Raises NotSupportedByMandrillError for any standard EmailMessage
|
||||||
accurately communicated to Mandrill (e.g., prohibited headers).
|
features that cannot be accurately communicated to Mandrill
|
||||||
|
(e.g., prohibited headers).
|
||||||
"""
|
"""
|
||||||
sender = sanitize_address(message.from_email, message.encoding)
|
sender = sanitize_address(message.from_email, message.encoding)
|
||||||
from_name, from_email = parseaddr(sender)
|
from_name, from_email = parseaddr(sender)
|
||||||
|
|
||||||
recipients = [parseaddr(sanitize_address(addr, message.encoding))
|
recipients = message.to + message.cc # message.recipients() w/o bcc
|
||||||
for addr in message.recipients()]
|
parsed_rcpts = [parseaddr(sanitize_address(addr, message.encoding))
|
||||||
|
for addr in recipients]
|
||||||
to_list = [{"email": to_email, "name": to_name}
|
to_list = [{"email": to_email, "name": to_name}
|
||||||
for (to_name, to_email) in recipients]
|
for (to_name, to_email) in parsed_rcpts]
|
||||||
|
|
||||||
msg_dict = {
|
msg_dict = {
|
||||||
"text": message.body,
|
"text": message.body,
|
||||||
@@ -115,10 +120,20 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
if from_name:
|
if from_name:
|
||||||
msg_dict["from_name"] = from_name
|
msg_dict["from_name"] = from_name
|
||||||
|
|
||||||
|
if len(message.bcc) == 1:
|
||||||
|
bcc = message.bcc[0]
|
||||||
|
_, bcc_addr = parseaddr(sanitize_address(bcc, message.encoding))
|
||||||
|
msg_dict['bcc_address'] = bcc_addr
|
||||||
|
elif len(message.bcc) > 1:
|
||||||
|
raise NotSupportedByMandrillError(
|
||||||
|
"Too many bcc addresses (%d) - Mandrill only allows one"
|
||||||
|
% len(message.bcc))
|
||||||
|
|
||||||
if message.extra_headers:
|
if message.extra_headers:
|
||||||
for k in message.extra_headers.keys():
|
for k in message.extra_headers.keys():
|
||||||
if k != "Reply-To" and not k.startswith("X-"):
|
if k != "Reply-To" and not k.startswith("X-"):
|
||||||
raise ValueError("Invalid message header '%s' - Mandrill "
|
raise NotSupportedByMandrillError(
|
||||||
|
"Invalid message header '%s' - Mandrill "
|
||||||
"only allows Reply-To and X-* headers" % k)
|
"only allows Reply-To and X-* headers" % k)
|
||||||
msg_dict["headers"] = message.extra_headers
|
msg_dict["headers"] = message.extra_headers
|
||||||
|
|
||||||
@@ -175,14 +190,74 @@ class DjrillBackend(BaseEmailBackend):
|
|||||||
the HTML output for your email.
|
the HTML output for your email.
|
||||||
"""
|
"""
|
||||||
if len(message.alternatives) > 1:
|
if len(message.alternatives) > 1:
|
||||||
raise ValueError(
|
raise NotSupportedByMandrillError(
|
||||||
"Too many alternatives attached to the message. "
|
"Too many alternatives attached to the message. "
|
||||||
"Mandrill only accepts plain text and html emails.")
|
"Mandrill only accepts plain text and html emails.")
|
||||||
|
|
||||||
(content, mimetype) = message.alternatives[0]
|
(content, mimetype) = message.alternatives[0]
|
||||||
if mimetype != 'text/html':
|
if mimetype != 'text/html':
|
||||||
raise ValueError("Invalid alternative mimetype '%s'. "
|
raise NotSupportedByMandrillError(
|
||||||
|
"Invalid alternative mimetype '%s'. "
|
||||||
"Mandrill only accepts plain text and html emails."
|
"Mandrill only accepts plain text and html emails."
|
||||||
% mimetype)
|
% mimetype)
|
||||||
|
|
||||||
msg_dict['html'] = content
|
msg_dict['html'] = content
|
||||||
|
|
||||||
|
def _add_attachments(self, message, msg_dict):
|
||||||
|
"""Extend msg_dict to include any attachments in message"""
|
||||||
|
if message.attachments:
|
||||||
|
str_encoding = message.encoding or settings.DEFAULT_CHARSET
|
||||||
|
attachments = [
|
||||||
|
self._make_mandrill_attachment(attachment, str_encoding)
|
||||||
|
for attachment in message.attachments
|
||||||
|
]
|
||||||
|
if len(attachments) > 0:
|
||||||
|
msg_dict['attachments'] = attachments
|
||||||
|
|
||||||
|
def _make_mandrill_attachment(self, attachment, str_encoding=None):
|
||||||
|
"""Return a Mandrill dict for an EmailMessage.attachments item"""
|
||||||
|
# Note that an attachment can be either a tuple of (filename, content,
|
||||||
|
# mimetype) or a MIMEBase object. (Also, both filename and mimetype may
|
||||||
|
# be missing.)
|
||||||
|
if isinstance(attachment, MIMEBase):
|
||||||
|
filename = attachment.get_filename()
|
||||||
|
content = attachment.get_payload(decode=True)
|
||||||
|
mimetype = attachment.get_content_type()
|
||||||
|
else:
|
||||||
|
(filename, content, mimetype) = attachment
|
||||||
|
|
||||||
|
# Guess missing mimetype, borrowed from
|
||||||
|
# django.core.mail.EmailMessage._create_attachment()
|
||||||
|
if mimetype is None and filename is not None:
|
||||||
|
mimetype, _ = mimetypes.guess_type(filename)
|
||||||
|
if mimetype is None:
|
||||||
|
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
||||||
|
|
||||||
|
# Mandrill silently filters attachments with unsupported mimetypes.
|
||||||
|
# This can be confusing, so we raise an exception instead.
|
||||||
|
(main, sub) = mimetype.lower().split('/')
|
||||||
|
attachment_allowed = (
|
||||||
|
main == 'text' or main == 'image'
|
||||||
|
or (main == 'application' and sub == 'pdf'))
|
||||||
|
if not attachment_allowed:
|
||||||
|
raise NotSupportedByMandrillError(
|
||||||
|
"Invalid attachment mimetype '%s'. Mandrill only supports "
|
||||||
|
"text/*, image/*, and application/pdf attachments."
|
||||||
|
% mimetype)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_b64 = b64encode(content)
|
||||||
|
except TypeError:
|
||||||
|
# Python 3 b64encode requires bytes. Convert str attachment:
|
||||||
|
if isinstance(content, str):
|
||||||
|
content_bytes = content.encode(str_encoding)
|
||||||
|
content_b64 = b64encode(content_bytes)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': mimetype,
|
||||||
|
'name': filename or "",
|
||||||
|
'content': content_b64.decode('ascii'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
4
djrill/tests/__init__.py
Normal file
4
djrill/tests/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from djrill.tests.test_admin import *
|
||||||
|
from djrill.tests.test_legacy import *
|
||||||
|
from djrill.tests.test_mandrill_send import *
|
||||||
|
from djrill.tests.test_mandrill_send_template import *
|
||||||
64
djrill/tests/mock_backend.py
Normal file
64
djrill/tests/mock_backend.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from mock import patch
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.utils import simplejson as json
|
||||||
|
|
||||||
|
class DjrillBackendMockAPITestCase(TestCase):
|
||||||
|
"""TestCase that uses Djrill EmailBackend with a mocked Mandrill API"""
|
||||||
|
|
||||||
|
class MockResponse:
|
||||||
|
"""requests.post return value mock sufficient for DjrillBackend"""
|
||||||
|
def __init__(self, status_code=200, content="{}"):
|
||||||
|
self.status_code = status_code
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.patch = patch('requests.post', autospec=True)
|
||||||
|
self.mock_post = self.patch.start()
|
||||||
|
self.mock_post.return_value = self.MockResponse()
|
||||||
|
|
||||||
|
settings.MANDRILL_API_KEY = "FAKE_API_KEY_FOR_TESTING"
|
||||||
|
|
||||||
|
# Django TestCase sets up locmem EmailBackend; override it here
|
||||||
|
self.original_email_backend = settings.EMAIL_BACKEND
|
||||||
|
settings.EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend"
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.patch.stop()
|
||||||
|
settings.EMAIL_BACKEND = self.original_email_backend
|
||||||
|
|
||||||
|
def assert_mandrill_called(self, endpoint):
|
||||||
|
"""Verifies the (mock) Mandrill API was called on endpoint.
|
||||||
|
|
||||||
|
endpoint is a Mandrill API, e.g., "/messages/send.json"
|
||||||
|
"""
|
||||||
|
# This assumes the last (or only) call to requests.post is the
|
||||||
|
# Mandrill API call of interest.
|
||||||
|
if self.mock_post.call_args is None:
|
||||||
|
raise AssertionError("Mandrill API was not called")
|
||||||
|
(args, kwargs) = self.mock_post.call_args
|
||||||
|
try:
|
||||||
|
post_url = kwargs.get('url', None) or args[0]
|
||||||
|
except IndexError:
|
||||||
|
raise AssertionError("requests.post was called without an url (?!)")
|
||||||
|
if not post_url.endswith(endpoint):
|
||||||
|
raise AssertionError(
|
||||||
|
"requests.post was not called on %s\n(It was called on %s)"
|
||||||
|
% (endpoint, post_url))
|
||||||
|
|
||||||
|
def get_api_call_data(self):
|
||||||
|
"""Returns the data posted to the Mandrill API.
|
||||||
|
|
||||||
|
Fails test if API wasn't called.
|
||||||
|
"""
|
||||||
|
if self.mock_post.call_args is None:
|
||||||
|
raise AssertionError("Mandrill API was not called")
|
||||||
|
(args, kwargs) = self.mock_post.call_args
|
||||||
|
try:
|
||||||
|
post_data = kwargs.get('data', None) or args[1]
|
||||||
|
except IndexError:
|
||||||
|
raise AssertionError("requests.post was called without data")
|
||||||
|
return json.loads(post_data)
|
||||||
|
|
||||||
|
|
||||||
72
djrill/tests/test_admin.py
Normal file
72
djrill/tests/test_admin.py
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
|
||||||
|
|
||||||
|
|
||||||
|
def reset_admin_site():
|
||||||
|
"""Return the Django admin globals to their original state"""
|
||||||
|
admin.site = admin.AdminSite() # restore default
|
||||||
|
if 'djrill.admin' in sys.modules:
|
||||||
|
del sys.modules['djrill.admin'] # force autodiscover to re-import
|
||||||
|
|
||||||
|
|
||||||
|
class DjrillAdminTests(DjrillBackendMockAPITestCase):
|
||||||
|
"""Test the Djrill admin site"""
|
||||||
|
|
||||||
|
# These tests currently just verify that the admin site pages load
|
||||||
|
# without error -- they don't test any Mandrill-supplied content.
|
||||||
|
# (Future improvements could mock the Mandrill responses.)
|
||||||
|
|
||||||
|
# These urls set up the DjrillAdminSite as suggested in the readme
|
||||||
|
urls = 'djrill.tests.admin_urls'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
# Other test cases may muck with the Django admin site globals,
|
||||||
|
# so return it to the default state before loading test_admin_urls
|
||||||
|
reset_admin_site()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(DjrillAdminTests, self).setUp()
|
||||||
|
# Must be authenticated staff to access admin site...
|
||||||
|
admin = User.objects.create_user('admin', 'admin@example.com', 'secret')
|
||||||
|
admin.is_staff = True
|
||||||
|
admin.save()
|
||||||
|
self.client.login(username='admin', password='secret')
|
||||||
|
|
||||||
|
def test_admin_senders(self):
|
||||||
|
response = self.client.get('/admin/djrill/senders/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Senders")
|
||||||
|
|
||||||
|
def test_admin_status(self):
|
||||||
|
response = self.client.get('/admin/djrill/status/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Status")
|
||||||
|
|
||||||
|
def test_admin_tags(self):
|
||||||
|
response = self.client.get('/admin/djrill/tags/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Tags")
|
||||||
|
|
||||||
|
def test_admin_urls(self):
|
||||||
|
response = self.client.get('/admin/djrill/urls/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "URLs")
|
||||||
|
|
||||||
|
def test_admin_index(self):
|
||||||
|
"""Make sure Djrill section is included in the admin index page"""
|
||||||
|
response = self.client.get('/admin/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, "Djrill")
|
||||||
|
|
||||||
|
|
||||||
|
class DjrillNoAdminTests(TestCase):
|
||||||
|
def test_admin_autodiscover_without_djrill(self):
|
||||||
|
"""Make sure autodiscover doesn't die without DjrillAdminSite"""
|
||||||
|
reset_admin_site()
|
||||||
|
admin.autodiscover() # test: this shouldn't error
|
||||||
89
djrill/tests/test_legacy.py
Normal file
89
djrill/tests/test_legacy.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# Tests deprecated Djrill features
|
||||||
|
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from djrill.mail import DjrillMessage
|
||||||
|
from djrill import MandrillAPIError, NotSupportedByMandrillError
|
||||||
|
|
||||||
|
|
||||||
|
class DjrillMessageTests(TestCase):
|
||||||
|
"""Test the DjrillMessage class (deprecated as of Djrill v0.2.0)
|
||||||
|
|
||||||
|
Maintained for compatibility with older code.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.subject = "Djrill baby djrill."
|
||||||
|
self.from_name = "Tarzan"
|
||||||
|
self.from_email = "test@example"
|
||||||
|
self.to = ["King Kong <kingkong@example.com>",
|
||||||
|
"Cheetah <cheetah@example.com", "bubbles@example.com"]
|
||||||
|
self.text_content = "Wonderful fallback text content."
|
||||||
|
self.html_content = "<h1>That's a nice HTML email right there.</h1>"
|
||||||
|
self.headers = {"Reply-To": "tarzan@example.com"}
|
||||||
|
self.tags = ["track", "this"]
|
||||||
|
|
||||||
|
def test_djrill_message_success(self):
|
||||||
|
msg = DjrillMessage(self.subject, self.text_content, self.from_email,
|
||||||
|
self.to, tags=self.tags, headers=self.headers,
|
||||||
|
from_name=self.from_name)
|
||||||
|
|
||||||
|
self.assertIsInstance(msg, DjrillMessage)
|
||||||
|
self.assertEqual(msg.body, self.text_content)
|
||||||
|
self.assertEqual(msg.recipients(), self.to)
|
||||||
|
self.assertEqual(msg.tags, self.tags)
|
||||||
|
self.assertEqual(msg.extra_headers, self.headers)
|
||||||
|
self.assertEqual(msg.from_name, self.from_name)
|
||||||
|
|
||||||
|
def test_djrill_message_html_success(self):
|
||||||
|
msg = DjrillMessage(self.subject, self.text_content, self.from_email,
|
||||||
|
self.to, tags=self.tags)
|
||||||
|
msg.attach_alternative(self.html_content, "text/html")
|
||||||
|
|
||||||
|
self.assertEqual(msg.alternatives[0][0], self.html_content)
|
||||||
|
|
||||||
|
def test_djrill_message_tag_failure(self):
|
||||||
|
with self.assertRaises(ValueError):
|
||||||
|
DjrillMessage(self.subject, self.text_content, self.from_email,
|
||||||
|
self.to, tags=["_fail"])
|
||||||
|
|
||||||
|
def test_djrill_message_tag_skip(self):
|
||||||
|
"""
|
||||||
|
Test that tags over 50 chars are not included in the tags list.
|
||||||
|
"""
|
||||||
|
tags = ["works", "awesomesauce",
|
||||||
|
"iwilltestmycodeiwilltestmycodeiwilltestmycodeiwilltestmycode"]
|
||||||
|
msg = DjrillMessage(self.subject, self.text_content, self.from_email,
|
||||||
|
self.to, tags=tags)
|
||||||
|
|
||||||
|
self.assertIsInstance(msg, DjrillMessage)
|
||||||
|
self.assertIn(tags[0], msg.tags)
|
||||||
|
self.assertIn(tags[1], msg.tags)
|
||||||
|
self.assertNotIn(tags[2], msg.tags)
|
||||||
|
|
||||||
|
def test_djrill_message_no_options(self):
|
||||||
|
"""DjrillMessage with only basic EmailMessage options should work"""
|
||||||
|
msg = DjrillMessage(self.subject, self.text_content,
|
||||||
|
self.from_email, self.to) # no Mandrill-specific options
|
||||||
|
|
||||||
|
self.assertIsInstance(msg, DjrillMessage)
|
||||||
|
self.assertEqual(msg.body, self.text_content)
|
||||||
|
self.assertEqual(msg.recipients(), self.to)
|
||||||
|
self.assertFalse(hasattr(msg, 'tags'))
|
||||||
|
self.assertFalse(hasattr(msg, 'from_name'))
|
||||||
|
self.assertFalse(hasattr(msg, 'preserve_recipients'))
|
||||||
|
|
||||||
|
|
||||||
|
class DjrillLegacyExceptionTests(TestCase):
|
||||||
|
def test_DjrillBackendHTTPError(self):
|
||||||
|
"""MandrillApiError was DjrillBackendHTTPError in 0.2.0"""
|
||||||
|
# ... and had to be imported from deep in the package:
|
||||||
|
from djrill.mail.backends.djrill import DjrillBackendHTTPError
|
||||||
|
ex = MandrillAPIError("testing")
|
||||||
|
self.assertIsInstance(ex, DjrillBackendHTTPError)
|
||||||
|
|
||||||
|
def test_NotSupportedByMandrillError(self):
|
||||||
|
"""Unsupported features used to just raise ValueError in 0.2.0"""
|
||||||
|
ex = NotSupportedByMandrillError("testing")
|
||||||
|
self.assertIsInstance(ex, ValueError)
|
||||||
@@ -1,62 +1,21 @@
|
|||||||
from mock import patch
|
from base64 import b64decode
|
||||||
import sys
|
from email.mime.base import MIMEBase
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core import mail
|
from django.core import mail
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.test import TestCase
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
from djrill.mail import DjrillMessage
|
from djrill import MandrillAPIError, NotSupportedByMandrillError
|
||||||
from djrill.mail.backends.djrill import DjrillBackendHTTPError
|
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
|
||||||
|
|
||||||
|
|
||||||
class DjrillBackendMockAPITestCase(TestCase):
|
|
||||||
"""TestCase that uses Djrill EmailBackend with a mocked Mandrill API"""
|
|
||||||
|
|
||||||
class MockResponse:
|
|
||||||
"""requests.post return value mock sufficient for DjrillBackend"""
|
|
||||||
def __init__(self, status_code=200, content="{}"):
|
|
||||||
self.status_code = status_code
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.patch = patch('requests.post')
|
|
||||||
self.mock_post = self.patch.start()
|
|
||||||
self.mock_post.return_value = self.MockResponse()
|
|
||||||
|
|
||||||
settings.MANDRILL_API_KEY = "FAKE_API_KEY_FOR_TESTING"
|
|
||||||
|
|
||||||
# Django TestCase sets up locmem EmailBackend; override it here
|
|
||||||
self.original_email_backend = settings.EMAIL_BACKEND
|
|
||||||
settings.EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend"
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
self.patch.stop()
|
|
||||||
settings.EMAIL_BACKEND = self.original_email_backend
|
|
||||||
|
|
||||||
def get_api_call_data(self):
|
|
||||||
"""Returns the data posted to the Mandrill API.
|
|
||||||
|
|
||||||
Fails test if API wasn't called.
|
|
||||||
"""
|
|
||||||
if self.mock_post.call_args is None:
|
|
||||||
raise AssertionError("Mandrill API was not called")
|
|
||||||
(args, kwargs) = self.mock_post.call_args
|
|
||||||
if 'data' not in kwargs:
|
|
||||||
raise AssertionError("requests.post was called without data kwarg "
|
|
||||||
"-- Maybe tests need to be updated for backend changes?")
|
|
||||||
return json.loads(kwargs['data'])
|
|
||||||
|
|
||||||
|
|
||||||
class DjrillBackendTests(DjrillBackendMockAPITestCase):
|
class DjrillBackendTests(DjrillBackendMockAPITestCase):
|
||||||
"""Test Djrill's support for Django mail wrappers"""
|
"""Test Djrill backend support for Django mail wrappers"""
|
||||||
|
|
||||||
def test_send_mail(self):
|
def test_send_mail(self):
|
||||||
mail.send_mail('Subject here', 'Here is the message.',
|
mail.send_mail('Subject here', 'Here is the message.',
|
||||||
'from@example.com', ['to@example.com'], fail_silently=False)
|
'from@example.com', ['to@example.com'], fail_silently=False)
|
||||||
|
self.assert_mandrill_called("/messages/send.json")
|
||||||
data = self.get_api_call_data()
|
data = self.get_api_call_data()
|
||||||
self.assertEqual(data['message']['subject'], "Subject here")
|
self.assertEqual(data['message']['subject'], "Subject here")
|
||||||
self.assertEqual(data['message']['text'], "Here is the message.")
|
self.assertEqual(data['message']['text'], "Here is the message.")
|
||||||
@@ -76,42 +35,52 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
|
|||||||
|
|
||||||
(Test both sender and recipient addresses)
|
(Test both sender and recipient addresses)
|
||||||
"""
|
"""
|
||||||
mail.send_mail('Subject', 'Message', 'From Name <from@example.com>',
|
msg = mail.EmailMessage('Subject', 'Message',
|
||||||
['Recipient #1 <to1@example.com>', 'to2@example.com'])
|
'From Name <from@example.com>',
|
||||||
|
['Recipient #1 <to1@example.com>', 'to2@example.com'],
|
||||||
|
cc=['Carbon Copy <cc1@example.com>', 'cc2@example.com'],
|
||||||
|
bcc=['Blind Copy <bcc@example.com>'])
|
||||||
|
msg.send()
|
||||||
data = self.get_api_call_data()
|
data = self.get_api_call_data()
|
||||||
self.assertEqual(data['message']['from_name'], "From Name")
|
self.assertEqual(data['message']['from_name'], "From Name")
|
||||||
self.assertEqual(data['message']['from_email'], "from@example.com")
|
self.assertEqual(data['message']['from_email'], "from@example.com")
|
||||||
self.assertEqual(len(data['message']['to']), 2)
|
self.assertEqual(len(data['message']['to']), 4)
|
||||||
self.assertEqual(data['message']['to'][0]['name'], "Recipient #1")
|
self.assertEqual(data['message']['to'][0]['name'], "Recipient #1")
|
||||||
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
|
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
|
||||||
self.assertEqual(data['message']['to'][1]['name'], "")
|
self.assertEqual(data['message']['to'][1]['name'], "")
|
||||||
self.assertEqual(data['message']['to'][1]['email'], "to2@example.com")
|
self.assertEqual(data['message']['to'][1]['email'], "to2@example.com")
|
||||||
|
self.assertEqual(data['message']['to'][2]['name'], "Carbon Copy")
|
||||||
|
self.assertEqual(data['message']['to'][2]['email'], "cc1@example.com")
|
||||||
|
self.assertEqual(data['message']['to'][3]['name'], "")
|
||||||
|
self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com")
|
||||||
|
# Mandrill only supports email, not name, for bcc:
|
||||||
|
self.assertEqual(data['message']['bcc_address'], "bcc@example.com")
|
||||||
|
|
||||||
def test_email_message(self):
|
def test_email_message(self):
|
||||||
email = mail.EmailMessage('Subject', 'Body goes here',
|
email = mail.EmailMessage('Subject', 'Body goes here',
|
||||||
'from@example.com',
|
'from@example.com',
|
||||||
['to1@example.com', 'Also To <to2@example.com>'],
|
['to1@example.com', 'Also To <to2@example.com>'],
|
||||||
bcc=['bcc1@example.com', 'Also BCC <bcc2@example.com>'],
|
bcc=['bcc@example.com'],
|
||||||
cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
|
cc=['cc1@example.com', 'Also CC <cc2@example.com>'],
|
||||||
headers={'Reply-To': 'another@example.com',
|
headers={'Reply-To': 'another@example.com',
|
||||||
'X-MyHeader': 'my value'})
|
'X-MyHeader': 'my value'})
|
||||||
email.send()
|
email.send()
|
||||||
|
self.assert_mandrill_called("/messages/send.json")
|
||||||
data = self.get_api_call_data()
|
data = self.get_api_call_data()
|
||||||
self.assertEqual(data['message']['subject'], "Subject")
|
self.assertEqual(data['message']['subject'], "Subject")
|
||||||
self.assertEqual(data['message']['text'], "Body goes here")
|
self.assertEqual(data['message']['text'], "Body goes here")
|
||||||
self.assertEqual(data['message']['from_email'], "from@example.com")
|
self.assertEqual(data['message']['from_email'], "from@example.com")
|
||||||
self.assertEqual(data['message']['headers'],
|
self.assertEqual(data['message']['headers'],
|
||||||
{ 'Reply-To': 'another@example.com', 'X-MyHeader': 'my value' })
|
{ 'Reply-To': 'another@example.com', 'X-MyHeader': 'my value' })
|
||||||
# Mandrill doesn't have a notion of cc, and only allows a single bcc.
|
# Mandrill doesn't have a notion of cc.
|
||||||
# Djrill just treats cc and bcc as though they were "to" addresses,
|
# Djrill just treats cc as additional "to" addresses,
|
||||||
# which may or may not be what you want.
|
# which may or may not be what you want.
|
||||||
self.assertEqual(len(data['message']['to']), 6)
|
self.assertEqual(len(data['message']['to']), 4)
|
||||||
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
|
self.assertEqual(data['message']['to'][0]['email'], "to1@example.com")
|
||||||
self.assertEqual(data['message']['to'][1]['email'], "to2@example.com")
|
self.assertEqual(data['message']['to'][1]['email'], "to2@example.com")
|
||||||
self.assertEqual(data['message']['to'][2]['email'], "cc1@example.com")
|
self.assertEqual(data['message']['to'][2]['email'], "cc1@example.com")
|
||||||
self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com")
|
self.assertEqual(data['message']['to'][3]['email'], "cc2@example.com")
|
||||||
self.assertEqual(data['message']['to'][4]['email'], "bcc1@example.com")
|
self.assertEqual(data['message']['bcc_address'], "bcc@example.com")
|
||||||
self.assertEqual(data['message']['to'][5]['email'], "bcc2@example.com")
|
|
||||||
|
|
||||||
def test_html_message(self):
|
def test_html_message(self):
|
||||||
text_content = 'This is an important message.'
|
text_content = 'This is an important message.'
|
||||||
@@ -120,15 +89,55 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
|
|||||||
'from@example.com', ['to@example.com'])
|
'from@example.com', ['to@example.com'])
|
||||||
email.attach_alternative(html_content, "text/html")
|
email.attach_alternative(html_content, "text/html")
|
||||||
email.send()
|
email.send()
|
||||||
|
self.assert_mandrill_called("/messages/send.json")
|
||||||
data = self.get_api_call_data()
|
data = self.get_api_call_data()
|
||||||
self.assertEqual(data['message']['text'], text_content)
|
self.assertEqual(data['message']['text'], text_content)
|
||||||
self.assertEqual(data['message']['html'], html_content)
|
self.assertEqual(data['message']['html'], html_content)
|
||||||
|
# Don't accidentally send the html part as an attachment:
|
||||||
|
self.assertFalse('attachments' in data['message'])
|
||||||
|
|
||||||
|
def test_attachments(self):
|
||||||
|
email = mail.EmailMessage('Subject', 'Body goes here',
|
||||||
|
'from@example.com', ['to1@example.com'])
|
||||||
|
|
||||||
|
text_content = "* Item one\n* Item two\n* Item three"
|
||||||
|
email.attach(filename="test.txt", content=text_content,
|
||||||
|
mimetype="text/plain")
|
||||||
|
|
||||||
|
# Should guess mimetype if not provided...
|
||||||
|
png_content = b"PNG\xb4 pretend this is the contents of a png file"
|
||||||
|
email.attach(filename="test.png", content=png_content)
|
||||||
|
|
||||||
|
# Should work with a MIMEBase object (also tests no filename)...
|
||||||
|
pdf_content = b"PDF\xb4 pretend this is valid pdf data"
|
||||||
|
mimeattachment = MIMEBase('application', 'pdf')
|
||||||
|
mimeattachment.set_payload(pdf_content)
|
||||||
|
email.attach(mimeattachment)
|
||||||
|
|
||||||
|
email.send()
|
||||||
|
|
||||||
|
def decode_att(att):
|
||||||
|
return b64decode(att.encode('ascii'))
|
||||||
|
|
||||||
|
data = self.get_api_call_data()
|
||||||
|
attachments = data['message']['attachments']
|
||||||
|
self.assertEqual(len(attachments), 3)
|
||||||
|
self.assertEqual(attachments[0]["type"], "text/plain")
|
||||||
|
self.assertEqual(attachments[0]["name"], "test.txt")
|
||||||
|
self.assertEqual(decode_att(attachments[0]["content"]).decode('ascii'),
|
||||||
|
text_content)
|
||||||
|
self.assertEqual(attachments[1]["type"], "image/png") # inferred
|
||||||
|
self.assertEqual(attachments[1]["name"], "test.png")
|
||||||
|
self.assertEqual(decode_att(attachments[1]["content"]), png_content)
|
||||||
|
self.assertEqual(attachments[2]["type"], "application/pdf")
|
||||||
|
self.assertEqual(attachments[2]["name"], "") # none
|
||||||
|
self.assertEqual(decode_att(attachments[2]["content"]), pdf_content)
|
||||||
|
|
||||||
def test_extra_header_errors(self):
|
def test_extra_header_errors(self):
|
||||||
email = mail.EmailMessage('Subject', 'Body', 'from@example.com',
|
email = mail.EmailMessage('Subject', 'Body', 'from@example.com',
|
||||||
['to@example.com'],
|
['to@example.com'],
|
||||||
headers={'Non-X-Non-Reply-To-Header': 'not permitted'})
|
headers={'Non-X-Non-Reply-To-Header': 'not permitted'})
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(NotSupportedByMandrillError):
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Make sure fail_silently is respected
|
# Make sure fail_silently is respected
|
||||||
@@ -146,14 +155,14 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
|
|||||||
'from@example.com', ['to@example.com'])
|
'from@example.com', ['to@example.com'])
|
||||||
email.attach_alternative("<p>First html is OK</p>", "text/html")
|
email.attach_alternative("<p>First html is OK</p>", "text/html")
|
||||||
email.attach_alternative("<p>But not second html</p>", "text/html")
|
email.attach_alternative("<p>But not second html</p>", "text/html")
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(NotSupportedByMandrillError):
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Only html alternatives allowed
|
# Only html alternatives allowed
|
||||||
email = mail.EmailMultiAlternatives('Subject', 'Body',
|
email = mail.EmailMultiAlternatives('Subject', 'Body',
|
||||||
'from@example.com', ['to@example.com'])
|
'from@example.com', ['to@example.com'])
|
||||||
email.attach_alternative("{'not': 'allowed'}", "application/json")
|
email.attach_alternative("{'not': 'allowed'}", "application/json")
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(NotSupportedByMandrillError):
|
||||||
email.send()
|
email.send()
|
||||||
|
|
||||||
# Make sure fail_silently is respected
|
# Make sure fail_silently is respected
|
||||||
@@ -165,9 +174,35 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase):
|
|||||||
msg="Mandrill API should not be called when send fails silently")
|
msg="Mandrill API should not be called when send fails silently")
|
||||||
self.assertEqual(sent, 0)
|
self.assertEqual(sent, 0)
|
||||||
|
|
||||||
|
def test_attachment_errors(self):
|
||||||
|
# Mandrill silently strips attachments that aren't text/*, image/*,
|
||||||
|
# or application/pdf. We want to alert the Djrill user:
|
||||||
|
with self.assertRaises(NotSupportedByMandrillError):
|
||||||
|
msg = mail.EmailMessage('Subject', 'Body',
|
||||||
|
'from@example.com', ['to@example.com'])
|
||||||
|
# This is the default mimetype, but won't work with Mandrill:
|
||||||
|
msg.attach(content="test", mimetype="application/octet-stream")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
with self.assertRaises(NotSupportedByMandrillError):
|
||||||
|
msg = mail.EmailMessage('Subject', 'Body',
|
||||||
|
'from@example.com', ['to@example.com'])
|
||||||
|
# Can't send Office docs, either:
|
||||||
|
msg.attach(filename="presentation.ppt", content="test",
|
||||||
|
mimetype="application/vnd.ms-powerpoint")
|
||||||
|
msg.send()
|
||||||
|
|
||||||
|
def test_bcc_errors(self):
|
||||||
|
# Mandrill only allows a single bcc address
|
||||||
|
with self.assertRaises(NotSupportedByMandrillError):
|
||||||
|
msg = mail.EmailMessage('Subject', 'Body',
|
||||||
|
'from@example.com', ['to@example.com'],
|
||||||
|
bcc=['bcc1@example.com>', 'bcc2@example.com'])
|
||||||
|
msg.send()
|
||||||
|
|
||||||
def test_mandrill_api_failure(self):
|
def test_mandrill_api_failure(self):
|
||||||
self.mock_post.return_value = self.MockResponse(status_code=400)
|
self.mock_post.return_value = self.MockResponse(status_code=400)
|
||||||
with self.assertRaises(DjrillBackendHTTPError):
|
with self.assertRaises(MandrillAPIError):
|
||||||
sent = mail.send_mail('Subject', 'Body', 'from@example.com',
|
sent = mail.send_mail('Subject', 'Body', 'from@example.com',
|
||||||
['to@example.com'])
|
['to@example.com'])
|
||||||
self.assertEqual(sent, 0)
|
self.assertEqual(sent, 0)
|
||||||
@@ -279,8 +314,10 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
|||||||
that your Mandrill account settings apply by default.
|
that your Mandrill account settings apply by default.
|
||||||
"""
|
"""
|
||||||
self.message.send()
|
self.message.send()
|
||||||
|
self.assert_mandrill_called("/messages/send.json")
|
||||||
data = self.get_api_call_data()
|
data = self.get_api_call_data()
|
||||||
self.assertFalse('from_name' in data['message'])
|
self.assertFalse('from_name' in data['message'])
|
||||||
|
self.assertFalse('bcc_address' in data['message'])
|
||||||
self.assertFalse('track_opens' in data['message'])
|
self.assertFalse('track_opens' in data['message'])
|
||||||
self.assertFalse('track_clicks' in data['message'])
|
self.assertFalse('track_clicks' in data['message'])
|
||||||
self.assertFalse('auto_text' in data['message'])
|
self.assertFalse('auto_text' in data['message'])
|
||||||
@@ -295,129 +332,3 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
|
|||||||
self.assertFalse('recipient_metadata' in data['message'])
|
self.assertFalse('recipient_metadata' in data['message'])
|
||||||
|
|
||||||
|
|
||||||
def reset_admin_site():
|
|
||||||
"""Return the Django admin globals to their original state"""
|
|
||||||
admin.site = admin.AdminSite() # restore default
|
|
||||||
if 'djrill.admin' in sys.modules:
|
|
||||||
del sys.modules['djrill.admin'] # force autodiscover to re-import
|
|
||||||
|
|
||||||
|
|
||||||
class DjrillAdminTests(DjrillBackendMockAPITestCase):
|
|
||||||
"""Test the Djrill admin site"""
|
|
||||||
|
|
||||||
# These tests currently just verify that the admin site pages load
|
|
||||||
# without error -- they don't test any Mandrill-supplied content.
|
|
||||||
# (Future improvements could mock the Mandrill responses.)
|
|
||||||
|
|
||||||
# These urls set up the DjrillAdminSite as suggested in the readme
|
|
||||||
urls = 'djrill.test_admin_urls'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
# Other test cases may muck with the Django admin site globals,
|
|
||||||
# so return it to the default state before loading test_admin_urls
|
|
||||||
reset_admin_site()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(DjrillAdminTests, self).setUp()
|
|
||||||
# Must be authenticated staff to access admin site...
|
|
||||||
admin = User.objects.create_user('admin', 'admin@example.com', 'secret')
|
|
||||||
admin.is_staff = True
|
|
||||||
admin.save()
|
|
||||||
self.client.login(username='admin', password='secret')
|
|
||||||
|
|
||||||
def test_admin_senders(self):
|
|
||||||
response = self.client.get('/admin/djrill/senders/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertContains(response, "Senders")
|
|
||||||
|
|
||||||
def test_admin_status(self):
|
|
||||||
response = self.client.get('/admin/djrill/status/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertContains(response, "Status")
|
|
||||||
|
|
||||||
def test_admin_tags(self):
|
|
||||||
response = self.client.get('/admin/djrill/tags/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertContains(response, "Tags")
|
|
||||||
|
|
||||||
def test_admin_urls(self):
|
|
||||||
response = self.client.get('/admin/djrill/urls/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertContains(response, "URLs")
|
|
||||||
|
|
||||||
def test_admin_index(self):
|
|
||||||
"""Make sure Djrill section is included in the admin index page"""
|
|
||||||
response = self.client.get('/admin/')
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
self.assertContains(response, "Djrill")
|
|
||||||
|
|
||||||
|
|
||||||
class DjrillNoAdminTests(TestCase):
|
|
||||||
def test_admin_autodiscover_without_djrill(self):
|
|
||||||
"""Make sure autodiscover doesn't die without DjrillAdminSite"""
|
|
||||||
reset_admin_site()
|
|
||||||
admin.autodiscover() # test: this shouldn't error
|
|
||||||
|
|
||||||
|
|
||||||
class DjrillMessageTests(TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.subject = "Djrill baby djrill."
|
|
||||||
self.from_name = "Tarzan"
|
|
||||||
self.from_email = "test@example"
|
|
||||||
self.to = ["King Kong <kingkong@example.com>",
|
|
||||||
"Cheetah <cheetah@example.com", "bubbles@example.com"]
|
|
||||||
self.text_content = "Wonderful fallback text content."
|
|
||||||
self.html_content = "<h1>That's a nice HTML email right there.</h1>"
|
|
||||||
self.headers = {"Reply-To": "tarzan@example.com"}
|
|
||||||
self.tags = ["track", "this"]
|
|
||||||
|
|
||||||
def test_djrill_message_success(self):
|
|
||||||
msg = DjrillMessage(self.subject, self.text_content, self.from_email,
|
|
||||||
self.to, tags=self.tags, headers=self.headers,
|
|
||||||
from_name=self.from_name)
|
|
||||||
|
|
||||||
self.assertIsInstance(msg, DjrillMessage)
|
|
||||||
self.assertEqual(msg.body, self.text_content)
|
|
||||||
self.assertEqual(msg.recipients(), self.to)
|
|
||||||
self.assertEqual(msg.tags, self.tags)
|
|
||||||
self.assertEqual(msg.extra_headers, self.headers)
|
|
||||||
self.assertEqual(msg.from_name, self.from_name)
|
|
||||||
|
|
||||||
def test_djrill_message_html_success(self):
|
|
||||||
msg = DjrillMessage(self.subject, self.text_content, self.from_email,
|
|
||||||
self.to, tags=self.tags)
|
|
||||||
msg.attach_alternative(self.html_content, "text/html")
|
|
||||||
|
|
||||||
self.assertEqual(msg.alternatives[0][0], self.html_content)
|
|
||||||
|
|
||||||
def test_djrill_message_tag_failure(self):
|
|
||||||
with self.assertRaises(ValueError):
|
|
||||||
DjrillMessage(self.subject, self.text_content, self.from_email,
|
|
||||||
self.to, tags=["_fail"])
|
|
||||||
|
|
||||||
def test_djrill_message_tag_skip(self):
|
|
||||||
"""
|
|
||||||
Test that tags over 50 chars are not included in the tags list.
|
|
||||||
"""
|
|
||||||
tags = ["works", "awesomesauce",
|
|
||||||
"iwilltestmycodeiwilltestmycodeiwilltestmycodeiwilltestmycode"]
|
|
||||||
msg = DjrillMessage(self.subject, self.text_content, self.from_email,
|
|
||||||
self.to, tags=tags)
|
|
||||||
|
|
||||||
self.assertIsInstance(msg, DjrillMessage)
|
|
||||||
self.assertIn(tags[0], msg.tags)
|
|
||||||
self.assertIn(tags[1], msg.tags)
|
|
||||||
self.assertNotIn(tags[2], msg.tags)
|
|
||||||
|
|
||||||
def test_djrill_message_no_options(self):
|
|
||||||
"""DjrillMessage with only basic EmailMessage options should work"""
|
|
||||||
msg = DjrillMessage(self.subject, self.text_content,
|
|
||||||
self.from_email, self.to) # no Mandrill-specific options
|
|
||||||
|
|
||||||
self.assertIsInstance(msg, DjrillMessage)
|
|
||||||
self.assertEqual(msg.body, self.text_content)
|
|
||||||
self.assertEqual(msg.recipients(), self.to)
|
|
||||||
self.assertFalse(hasattr(msg, 'tags'))
|
|
||||||
self.assertFalse(hasattr(msg, 'from_name'))
|
|
||||||
self.assertFalse(hasattr(msg, 'preserve_recipients'))
|
|
||||||
50
djrill/tests/test_mandrill_send_template.py
Normal file
50
djrill/tests/test_mandrill_send_template.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from django.core import mail
|
||||||
|
|
||||||
|
from djrill.tests.mock_backend import DjrillBackendMockAPITestCase
|
||||||
|
|
||||||
|
|
||||||
|
class DjrillMandrillSendTemplateTests(DjrillBackendMockAPITestCase):
|
||||||
|
"""Test Djrill backend support for Mandrill send-template features"""
|
||||||
|
|
||||||
|
def test_send_template(self):
|
||||||
|
msg = mail.EmailMessage('Subject', 'Text Body',
|
||||||
|
'from@example.com', ['to@example.com'])
|
||||||
|
msg.template_name = "PERSONALIZED_SPECIALS"
|
||||||
|
msg.template_content = {
|
||||||
|
'HEADLINE': "<h1>Specials Just For *|FNAME|*</h1>",
|
||||||
|
'OFFER_BLOCK': "<p><em>Half off</em> all fruit</p>"
|
||||||
|
}
|
||||||
|
msg.send()
|
||||||
|
self.assert_mandrill_called("/messages/send-template.json")
|
||||||
|
data = self.get_api_call_data()
|
||||||
|
self.assertEqual(data['template_name'], "PERSONALIZED_SPECIALS")
|
||||||
|
# Djrill expands simple python dicts into the more-verbose name/value
|
||||||
|
# structures the Mandrill API uses
|
||||||
|
self.assertEqual(data['template_content'],
|
||||||
|
[ {'name': "HEADLINE",
|
||||||
|
'value': "<h1>Specials Just For *|FNAME|*</h1>"},
|
||||||
|
{'name': "OFFER_BLOCK",
|
||||||
|
'value': "<p><em>Half off</em> all fruit</p>"} ]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_no_template_content(self):
|
||||||
|
# Just a template, without any template_content to be merged
|
||||||
|
msg = mail.EmailMessage('Subject', 'Text Body',
|
||||||
|
'from@example.com', ['to@example.com'])
|
||||||
|
msg.template_name = "WELCOME_MESSAGE"
|
||||||
|
msg.send()
|
||||||
|
self.assert_mandrill_called("/messages/send-template.json")
|
||||||
|
data = self.get_api_call_data()
|
||||||
|
self.assertEqual(data['template_name'], "WELCOME_MESSAGE")
|
||||||
|
self.assertFalse('template_content' in data)
|
||||||
|
|
||||||
|
def test_non_template_send(self):
|
||||||
|
# Make sure the non-template case still uses /messages/send.json
|
||||||
|
msg = mail.EmailMessage('Subject', 'Text Body',
|
||||||
|
'from@example.com', ['to@example.com'])
|
||||||
|
msg.send()
|
||||||
|
self.assert_mandrill_called("/messages/send.json")
|
||||||
|
data = self.get_api_call_data()
|
||||||
|
self.assertFalse('template_name' in data)
|
||||||
|
self.assertFalse('template_content' in data)
|
||||||
|
self.assertFalse('async' in data)
|
||||||
@@ -5,7 +5,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
from django.utils import simplejson as json
|
from django.utils import simplejson as json
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
|
|
||||||
from djrill.mail.backends.djrill import MANDRILL_API_URL
|
from djrill import MANDRILL_API_URL
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ class DjrillApiMixin(object):
|
|||||||
"""
|
"""
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.api_key = getattr(settings, "MANDRILL_API_KEY", None)
|
self.api_key = getattr(settings, "MANDRILL_API_KEY", None)
|
||||||
self.api_url = getattr(settings, "MANDRILL_API_URL", MANDRILL_API_URL)
|
self.api_url = MANDRILL_API_URL
|
||||||
|
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ImproperlyConfigured("You have not set your mandrill api key "
|
raise ImproperlyConfigured("You have not set your mandrill api key "
|
||||||
@@ -52,8 +52,8 @@ class DjrillApiJsonObjectsMixin(object):
|
|||||||
|
|
||||||
def get_api_uri(self):
|
def get_api_uri(self):
|
||||||
if self.api_uri is None:
|
if self.api_uri is None:
|
||||||
raise NotImplementedError(u"%(cls)s is missing an api_uri. Define "
|
raise NotImplementedError("%(cls)s is missing an api_uri. Define "
|
||||||
u"%(cls)s.api_uri or override %(cls)s.get_api_uri()." % {
|
"%(cls)s.api_uri or override %(cls)s.get_api_uri()." % {
|
||||||
"cls": self.__class__.__name__
|
"cls": self.__class__.__name__
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
20
setup.py
20
setup.py
@@ -1,14 +1,19 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
with open('LICENSE') as file:
|
||||||
|
license_text = file.read()
|
||||||
|
with open('README.rst') as file:
|
||||||
|
long_description = file.read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="djrill",
|
name="djrill",
|
||||||
version="0.2.0",
|
version="0.3.0",
|
||||||
description='Django email backend for Mandrill.',
|
description='Django email backend for Mandrill.',
|
||||||
keywords="django, mailchimp, mandrill, email, email backend",
|
keywords="django, mailchimp, mandrill, email, email backend",
|
||||||
author="Kenneth Love <kenneth@brack3t.com>, Chris Jones <chris@brack3t.com>",
|
author="Kenneth Love <kenneth@brack3t.com>, Chris Jones <chris@brack3t.com>",
|
||||||
author_email="kenneth@brack3t.com",
|
author_email="kenneth@brack3t.com",
|
||||||
url="https://github.com/brack3t/Djrill/",
|
url="https://github.com/brack3t/Djrill/",
|
||||||
license="BSD",
|
license=license_text,
|
||||||
packages=["djrill"],
|
packages=["djrill"],
|
||||||
zip_safe=False,
|
zip_safe=False,
|
||||||
install_requires=["requests", "django"],
|
install_requires=["requests", "django"],
|
||||||
@@ -22,14 +27,5 @@ setup(
|
|||||||
"Framework :: Django",
|
"Framework :: Django",
|
||||||
"Environment :: Web Environment",
|
"Environment :: Web Environment",
|
||||||
],
|
],
|
||||||
long_description="""\
|
long_description=long_description,
|
||||||
Djrill is an email backend for Django users who want to take advantage of the
|
|
||||||
`Mandrill <http://mandrill.com>`_ transactional email service from MailChimp.
|
|
||||||
|
|
||||||
In general, Djrill "just works" with Django's built-in ``django.core.mail``
|
|
||||||
package. You can also take advantage of Mandrill-specific features like tags,
|
|
||||||
metadata, and tracking. An optional Django admin interface is included.
|
|
||||||
|
|
||||||
Full details are on the `project page <https://github.com/brack3t/Djrill>`_.
|
|
||||||
""",
|
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user