Merge pull request #23 from brack3t/v030

Version 0.3.0
This commit is contained in:
Mike Edmunds
2013-01-12 14:41:28 -08:00
15 changed files with 687 additions and 276 deletions

View File

@@ -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

View File

@@ -8,3 +8,4 @@ ArnaudF
Théo Crevon Théo Crevon
Rafael E. Belliard Rafael E. Belliard
Jared Morse Jared Morse
peillis

View File

@@ -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

View File

@@ -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
View 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.)
"""

View File

@@ -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
View 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 *

View 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)

View 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

View 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)

View File

@@ -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'))

View 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)

View File

@@ -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__
}) })

View File

@@ -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>`_.
""",
) )