From 8aab5e31b737394adf8b4ac5dec34486674f0ced Mon Sep 17 00:00:00 2001 From: medmunds Date: Tue, 4 Dec 2012 17:28:15 -0800 Subject: [PATCH 1/2] Expose most Mandrill send features on EmailMessage objects. * Supports additional Mandrill send-API attributes on any ``EmailMessage``-derived object -- see details in readme * Removes need for MANDRILL_API_URL in settings (since this is tightly tied to the code) * Removes ``DjrillMessage`` from the readme (but not the code or tests) -- its functionality is now duplicated or exceeded by standard EmailMessage with additional attributes * Ensures send(fail_silently=True) works as expected --- README.rst | 128 +++++++++++++++++++++++---------- djrill/mail/backends/djrill.py | 117 ++++++++++++++++++------------ djrill/tests.py | 128 ++++++++++++++++++++++++++++++++- djrill/views.py | 9 ++- 4 files changed, 291 insertions(+), 91 deletions(-) diff --git a/README.rst b/README.rst index 6925ecb..364d2dc 100644 --- a/README.rst +++ b/README.rst @@ -17,38 +17,45 @@ Djrill is made available under the BSD license. Installation ------------ -:: +Install from PyPI:: pip install djrill -The only dependency other than Django is the requests_ library from Kenneth Reitz. If you do not install through PyPI you will -need to do :: +The only dependency other than Django is the requests_ library from Kenneth +Reitz. (If you do not install Djrill using pip or setuptools, you will also +need to ``pip install requests``.) - pip install requests Configuration ------------- In ``settings.py``: -1. Add ``djrill`` to your ``INSTALLED_APPS``. :: +1. Add ``djrill`` to your ``INSTALLED_APPS``: + +.. code:: python INSTALLED_APPS = ( ... "djrill" ) -2. Add the following two lines, substituting your own ``MANDRILL_API_KEY``:: +2. Add the following line, substituting your own ``MANDRILL_API_KEY``: + +.. code:: python MANDRILL_API_KEY = "brack3t-is-awesome" - MANDRILL_API_URL = "http://mandrillapp.com/api/1.0" -3. Override your existing email backend with the following line:: +3. Override your existing email backend with the following line: + +.. code:: python EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend" -4. (optional) If you want to be able to add senders through Django's admin or view stats about your - messages, do the following in your base ``urls.py`` :: +4. (optional) If you want to be able to add senders through Django's admin or + view stats about your messages, do the following in your base ``urls.py``: + +.. code:: python ... from django.contrib import admin @@ -69,46 +76,86 @@ Usage Since you are replacing the global ``EMAIL_BACKEND``, **all** emails are sent through Mandrill's service. -If you just want to use Mandrill for sending emails through Django's built-in ``send_mail`` and ``send_mass_mail`` methods, all -you need to do is follow steps 1 through 3 of the above Configuration. +In general, Djrill "just works" with Django's built-in `django.core.mail`_ +package, including ``send_mail``, ``send_mass_mail``, ``EmailMessage`` and +``EmailMultiAlternatives``. -If, however, you want more control over the messages, to include an HTML version, or to attach tags or tracked URLs to an email, -usage of our ``DjrillMessage`` class, which is a thin wrapper around Django's ``EmailMultiAlternatives`` is required. +You can also take advantage of Mandrill-specific features like tags, metadata, +and tracking by creating a ``django.mail.EmailMessage`` (or for HTML, +``django.mail.EmailMultiAlternatives``) object and setting Mandrill-specific +properties on it before calling its ``send`` method. -Example, in a view: :: +Example: - from django.views.generic import View +.. code:: python - from djrill.mail import DjrillMessage + from django.core.mail import EmailMultiAlternatives # or just EmailMessage if you don't need HTML - class SendEmailView(View): + subject = "Djrill Message" + from_email = "Djrill Sender " # this has to be in your Mandrill account's sending domains + to = ["Djrill Receiver ", "djrill.two@example.com"] + reply_email = "Customer Service " # optional + text_content = "This is the text version of your email" + html_content = "

This is the HTML version of your email

" # optional, use with ``attach_alternative`` below - def get(self, request): - subject = "Djrill Message" - from_email = "djrill@example.com" # this has to be one of your approved senders - from_name = "Djrill" # optional - to = ["Djrill Receiver ", "djrill.two@example.com"] - text_content = "This is the text version of your email" - html_content = "

This is the HTML version of your email

" # optional, requires the ``attach_alternative`` line below - tags = ["one tag", "two tag", "red tag", "blue tag"] # optional, can't be over 50 chars or start with an underscore + msg = EmailMultiAlternatives(subject, text_content, from_email, to, headers={'Reply-To': reply_email}) + msg.tags = ["one tag", "two tag", "red tag", "blue tag"] # optional, Mandrill-specific message extension + msg.metadata = {'user_id': "8675309"} # optional, Mandrill-specific message extension + msg.attach_alternative(html_content, "text/html") + msg.send() - msg = DjrillMessage(subject, text_content, from_email, to, tags=tags, from_name=from_name) - msg.attach_alternative(html_content, "text/html") - msg.send() - ... # you'll want to return some sort of HttpResponse +If the Mandrill API returns an error response for any reason, the send call will +raise a ``djrill.mail.backends.djrill.DjrillBackendHTTPError`` exception +(unless called with fail_silently=True). -Any tags over 50 characters in length are silently ignored since Mandrill doesn't support them. Any tags starting with an underscore will raise an ``ImproperlyConfigured`` -exception. Tags with an underscore are reserved by Mandrill. +Djrill supports most of the functionality of Django's ``EmailMessage`` and +``EmailMultiAlternatives``. Some limitations: -If you attach more than one alternative type, an ``ImproperlyConfigured`` exception will be raised. Mandrill does not support attaching -files to an email, so attachments will be silently ignored. +* Djrill accepts additional headers, but only ``Reply-To`` and ``X-*`` (since + that is all that Mandrill accepts). Any other extra headers will raise a + ``ValueError`` exception when you attempt to send the message. +* Djrill requires that if you ``attach_alternative`` to a message, there must be + only one alternative type, and it must be text/html. Otherwise, Djrill will + raise a ``ValueError`` exception when you attempt to send the message. + (Mandrill doesn't support sending multiple html alternative parts, or any + non-html alternatives.) +* Djrill (currently) silently ignores all attachments on a message. +* Djrill treats all cc and bcc recipients as if they were additional "to" + addresses. (Mandrill does not distinguish cc, and only allows a single bcc -- + which Djrill doesn't use. *Caution:* depending on the ``preserve_recipients`` + setting, this could result in exposing bcc addresses to all recipients. It's + probably best to just avoid bcc.) -Not shown above, but settable, are the two options, ``track_clicks`` and ``track_opens``. They are both set to ``True`` by default, but can be set to ``False`` and passed in when you instantiate your ``DjrillMessage`` -object. +Many of the options from the Mandrill `messages/send.json API`_ ``message`` +struct can be set directly on an ``EmailMessage`` (or subclass) object: + +* ``track_opens`` - Boolean +* ``track_clicks`` - Boolean (If you want to track clicks in HTML only, not + plaintext mail, you must *not* set this property, and instead just set the + default in your Mandrill account sending options.) +* ``auto_text`` - Boolean +* ``url_strip_qs`` - Boolean +* ``preserve_recipients`` - Boolean -- see the caution about bcc addresses above +* ``global_merge_vars`` - a dict -- e.g., + ``{ 'company': "ACME", 'offer': "10% off" }`` +* ``recipient_merge_vars`` - a dict whose keys are the recipient email addresses + and whose values are dicts of merge vars for each recipient -- e.g., + ``{ 'wiley@example.com': { 'offer': "15% off anvils" } }`` +* ``tags`` - a list of strings +* ``google_analytics_domains`` - a list of string domain names +* ``google_analytics_campaign`` - a string or list of strings +* ``metadata`` - a dict +* ``recipient_metadata`` - a dict whose keys are the recipient email addresses, + and whose values are dicts of metadata for each recipient (similar to + ``recipient_merge_vars``) + +These Mandrill-specific properties work with *any* ``EmailMessage``-derived +object, so you can use them with many other apps that add Django mail +functionality (such as Django template-based messages). + +If you have any questions about the python syntax for any of these properties, +see ``DjrillMandrillFeatureTests`` in tests.py for examples. -Just like Django's ``EmailMessage`` and ``EmailMultiAlternatives``, ``DjrillMessage`` accepts extra headers through the -``headers`` argument. Currently it only accepts ``Reply-To`` and ``X-*`` headers since that is all that Mandrill accepts. Any -extra headers are silently discarded. Testing ------- @@ -145,3 +192,6 @@ the awesome ``requests`` library. .. _requests: http://docs.python-requests.org .. _django-adminplus: https://github.com/jsocol/django-adminplus .. _mock: http://www.voidspace.org.uk/python/mock/index.html +.. _django.core.mail: https://docs.djangoproject.com/en/dev/topics/email/ +.. _messages/send.json API: https://mandrillapp.com/api/docs/messages.html#method=send + diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index 47a8136..fb63822 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -7,11 +7,16 @@ from django.utils import simplejson as json from email.utils import parseaddr import requests +# This backend was developed against this API endpoint. +# 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, log_message=None): + 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): @@ -33,14 +38,11 @@ class DjrillBackend(BaseEmailBackend): """ super(DjrillBackend, self).__init__(**kwargs) self.api_key = getattr(settings, "MANDRILL_API_KEY", None) - self.api_url = getattr(settings, "MANDRILL_API_URL", None) + self.api_url = getattr(settings, "MANDRILL_API_URL", MANDRILL_API_URL) if not self.api_key: raise ImproperlyConfigured("You have not set your mandrill api key " "in the settings.py file.") - if not self.api_url: - raise ImproperlyConfigured("You have not added the Mandrill api " - "url to your settings.py") self.api_action = self.api_url + "/messages/send.json" @@ -51,6 +53,7 @@ class DjrillBackend(BaseEmailBackend): num_sent = 0 for message in email_messages: sent = self._send(message) + if sent: num_sent += 1 @@ -60,20 +63,11 @@ class DjrillBackend(BaseEmailBackend): if not message.recipients(): return False - self.sender = sanitize_address(message.from_email, message.encoding) - recipients_list = [sanitize_address(addr, message.encoding) - for addr in message.recipients()] - self.recipients = [{"email": e, "name": n} for n,e in [ - parseaddr(r) for r in recipients_list]] - - self.msg_dict = self._build_standard_message_dict(message) - - if getattr(message, "alternative_subtype", None): - if message.alternative_subtype == "mandrill": - self._build_advanced_message_dict(message) try: + msg_dict = self._build_standard_message_dict(message) + self._add_mandrill_options(message, msg_dict) if getattr(message, 'alternatives', None): - self._add_alternatives(message) + self._add_alternatives(message, msg_dict) except ValueError: if not self.fail_silently: raise @@ -81,61 +75,98 @@ class DjrillBackend(BaseEmailBackend): djrill_it = requests.post(self.api_action, data=json.dumps({ "key": self.api_key, - "message": self.msg_dict + "message": msg_dict })) if djrill_it.status_code != 200: if not self.fail_silently: raise DjrillBackendHTTPError( status_code=djrill_it.status_code, + response = djrill_it, log_message="Failed to send a message to %s, from %s" % - (self.recipients, self.sender)) + (msg_dict['to'], msg_dict['from_email'])) return False return True def _build_standard_message_dict(self, message): - """ - Build standard message dict. + """Create a Mandrill send message struct from a Django EmailMessage. Builds the standard dict that Django's send_mail and send_mass_mail use by default. Standard text email messages sent through Django will still work through Mandrill. + + Raises ValueError for any standard EmailMessage features that cannot be + accurately communicated to Mandrill (e.g., prohibited headers). """ - from_name, from_email = parseaddr(self.sender) + sender = sanitize_address(message.from_email, message.encoding) + from_name, from_email = parseaddr(sender) + + recipients = [parseaddr(sanitize_address(addr, message.encoding)) + for addr in message.recipients()] + to_list = [{"email": to_email, "name": to_name} + for (to_name, to_email) in recipients] + msg_dict = { "text": message.body, "subject": message.subject, "from_email": from_email, - "to": self.recipients + "to": to_list } if from_name: msg_dict["from_name"] = from_name if message.extra_headers: - accepted_headers = {} for k in message.extra_headers.keys(): - if k.startswith("X-") or k == "Reply-To": - accepted_headers.update( - {"%s" % k: message.extra_headers[k]}) - msg_dict.update({"headers": accepted_headers}) + if k != "Reply-To" and not k.startswith("X-"): + raise ValueError("Invalid message header '%s' - Mandrill " + "only allows Reply-To and X-* headers" % k) + msg_dict["headers"] = message.extra_headers return msg_dict - def _build_advanced_message_dict(self, message): - """ - Builds advanced message dict - """ - self.msg_dict.update({ - "tags": message.tags, - "track_opens": message.track_opens, - "track_clicks": message.track_clicks, - "preserve_recipients": message.preserve_recipients, - }) - if message.from_name: - self.msg_dict["from_name"] = message.from_name + def _add_mandrill_options(self, message, msg_dict): + """Extend msg_dict to include Mandrill options set on message""" + # Mandrill attributes that can be copied directly: + mandrill_attrs = [ + 'from_name', # overrides display name parsed from from_email above + 'track_opens', 'track_clicks', 'auto_text', 'url_strip_qs', + 'tags', 'preserve_recipients', + 'google_analytics_domains', 'google_analytics_campaign', + 'metadata'] + for attr in mandrill_attrs: + if hasattr(message, attr): + msg_dict[attr] = getattr(message, attr) + + # Allow simple python dicts in place of Mandrill + # [{name:name, value:value},...] arrays... + if hasattr(message, 'global_merge_vars'): + msg_dict['global_merge_vars'] = \ + self._expand_merge_vars(message.global_merge_vars) + if hasattr(message, 'merge_vars'): + # For testing reproducibility, we sort the recipients + msg_dict['merge_vars'] = [ + { 'rcpt': rcpt, + 'vars': self._expand_merge_vars(message.merge_vars[rcpt]) } + for rcpt in sorted(message.merge_vars.keys()) + ] + if hasattr(message, 'recipient_metadata'): + # For testing reproducibility, we sort the recipients + msg_dict['recipient_metadata'] = [ + { 'rcpt': rcpt, 'values': message.recipient_metadata[rcpt] } + for rcpt in sorted(message.recipient_metadata.keys()) + ] - def _add_alternatives(self, message): + def _expand_merge_vars(self, vars): + """Convert a Python dict to an array of name-value used by Mandrill. + + { name: value, ... } --> [ {'name': name, 'value': value }, ... ] + """ + # For testing reproducibility, we sort the keys + return [ { 'name': name, 'value': vars[name] } + for name in sorted(vars.keys()) ] + + def _add_alternatives(self, message, msg_dict): """ There can be only one! ... alternative attachment, and it must be text/html. @@ -154,6 +185,4 @@ class DjrillBackend(BaseEmailBackend): "Mandrill only accepts plain text and html emails." % mimetype) - self.msg_dict.update({ - "html": content - }) + msg_dict['html'] = content diff --git a/djrill/tests.py b/djrill/tests.py index e5c006f..6aa261b 100644 --- a/djrill/tests.py +++ b/djrill/tests.py @@ -10,6 +10,8 @@ from django.test import TestCase from django.utils import simplejson as json from djrill.mail import DjrillMessage +from djrill.mail.backends.djrill import DjrillBackendHTTPError + class DjrillBackendMockAPITestCase(TestCase): """TestCase that uses Djrill EmailBackend with a mocked Mandrill API""" @@ -26,7 +28,6 @@ class DjrillBackendMockAPITestCase(TestCase): self.mock_post.return_value = self.MockResponse() settings.MANDRILL_API_KEY = "FAKE_API_KEY_FOR_TESTING" - settings.MANDRILL_API_URL = "http://mandrillapp.com/api/1.0" # Django TestCase sets up locmem EmailBackend; override it here self.original_email_backend = settings.EMAIL_BACKEND @@ -93,8 +94,7 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase): bcc=['bcc1@example.com', 'Also BCC '], cc=['cc1@example.com', 'Also CC '], headers={'Reply-To': 'another@example.com', - 'X-MyHeader': 'my value', - 'Errors-To': 'silently stripped'}) + 'X-MyHeader': 'my value'}) email.send() data = self.get_api_call_data() self.assertEqual(data['message']['subject'], "Subject") @@ -124,6 +124,22 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase): self.assertEqual(data['message']['text'], text_content) self.assertEqual(data['message']['html'], html_content) + def test_extra_header_errors(self): + email = mail.EmailMessage('Subject', 'Body', 'from@example.com', + ['to@example.com'], + headers={'Non-X-Non-Reply-To-Header': 'not permitted'}) + with self.assertRaises(ValueError): + email.send() + + # Make sure fail_silently is respected + email = mail.EmailMessage('Subject', 'Body', 'from@example.com', + ['to@example.com'], + headers={'Non-X-Non-Reply-To-Header': 'not permitted'}) + sent = email.send(fail_silently=True) + self.assertFalse(self.mock_post.called, + msg="Mandrill API should not be called when send fails silently") + self.assertEqual(sent, 0) + def test_alternative_errors(self): # Multiple alternatives not allowed email = mail.EmailMultiAlternatives('Subject', 'Body', @@ -149,6 +165,112 @@ class DjrillBackendTests(DjrillBackendMockAPITestCase): msg="Mandrill API should not be called when send fails silently") self.assertEqual(sent, 0) + def test_mandrill_api_failure(self): + self.mock_post.return_value = self.MockResponse(status_code=400) + with self.assertRaises(DjrillBackendHTTPError): + sent = mail.send_mail('Subject', 'Body', 'from@example.com', + ['to@example.com']) + self.assertEqual(sent, 0) + + # Make sure fail_silently is respected + self.mock_post.return_value = self.MockResponse(status_code=400) + sent = mail.send_mail('Subject', 'Body', 'from@example.com', + ['to@example.com'], fail_silently=True) + self.assertEqual(sent, 0) + + +class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase): + """Test Djrill backend support for Mandrill-specific features""" + + def setUp(self): + super(DjrillMandrillFeatureTests, self).setUp() + self.message = mail.EmailMessage('Subject', 'Text Body', + 'from@example.com', ['to@example.com']) + + def test_tracking(self): + # First make sure we're not setting the API param if the track_click + # attr isn't there. (The Mandrill account option of True for html, + # False for plaintext can't be communicated through the API, other than + # by omitting the track_clicks API param to use your account default.) + self.message.send() + data = self.get_api_call_data() + self.assertFalse('track_clicks' in data['message']) + # Now re-send with the params set + self.message.track_opens = True + self.message.track_clicks = True + self.message.url_strip_qs = True + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['track_opens'], True) + self.assertEqual(data['message']['track_clicks'], True) + self.assertEqual(data['message']['url_strip_qs'], True) + + def test_message_options(self): + self.message.auto_text = True + self.message.preserve_recipients = True + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['auto_text'], True) + self.assertEqual(data['message']['preserve_recipients'], True) + + def test_merge(self): + # Djrill expands simple python dicts into the more-verbose name/value + # structures the Mandrill API uses + self.message.global_merge_vars = { 'GREETING': "Hello", + 'ACCOUNT_TYPE': "Basic" } + self.message.merge_vars = { + "customer@example.com": { 'GREETING': "Dear Customer", + 'ACCOUNT_TYPE': "Premium" }, + "guest@example.com": { 'GREETING': "Dear Guest" }, + } + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['global_merge_vars'], + [ {'name': 'ACCOUNT_TYPE', 'value': "Basic"}, + {'name': "GREETING", 'value': "Hello"} ]) + self.assertEqual(data['message']['merge_vars'], + [ { 'rcpt': "customer@example.com", + 'vars': [{ 'name': 'ACCOUNT_TYPE', 'value': "Premium" }, + { 'name': "GREETING", 'value': "Dear Customer"}] }, + { 'rcpt': "guest@example.com", + 'vars': [{ 'name': "GREETING", 'value': "Dear Guest"}] } + ]) + + def test_tags(self): + self.message.tags = ["receipt", "repeat-user"] + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['tags'], ["receipt", "repeat-user"]) + + def test_google_analytics(self): + self.message.google_analytics_domains = ["example.com"] + self.message.google_analytics_campaign = "Email Receipts" + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['google_analytics_domains'], + ["example.com"]) + self.assertEqual(data['message']['google_analytics_campaign'], + "Email Receipts") + + def test_metadata(self): + self.message.metadata = { 'batch_num': "12345", 'type': "Receipts" } + self.message.recipient_metadata = { + # Djrill expands simple python dicts into the more-verbose + # name/value structures the Mandrill API uses + "customer@example.com": { 'cust_id': "67890", 'order_id': "54321" }, + "guest@example.com": { 'cust_id': "94107", 'order_id': "43215" } + } + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['message']['metadata'], { 'batch_num': "12345", + 'type': "Receipts" }) + self.assertEqual(data['message']['recipient_metadata'], + [ { 'rcpt': "customer@example.com", + 'values': { 'cust_id': "67890", 'order_id': "54321" } }, + { 'rcpt': "guest@example.com", + 'values': { 'cust_id': "94107", 'order_id': "43215" } } + ]) + def reset_admin_site(): """Return the Django admin globals to their original state""" diff --git a/djrill/views.py b/djrill/views.py index f2cf2fd..4f4c5da 100644 --- a/djrill/views.py +++ b/djrill/views.py @@ -5,6 +5,8 @@ from django.core.exceptions import ImproperlyConfigured from django.utils import simplejson as json from django.views.generic import TemplateView +from djrill.mail.backends.djrill import MANDRILL_API_URL + import requests @@ -23,14 +25,11 @@ class DjrillApiMixin(object): """ def __init__(self): self.api_key = getattr(settings, "MANDRILL_API_KEY", None) - self.api_url = getattr(settings, "MANDRILL_API_URL", None) + self.api_url = getattr(settings, "MANDRILL_API_URL", MANDRILL_API_URL) if not self.api_key: raise ImproperlyConfigured("You have not set your mandrill api key " "in the settings file.") - if not self.api_url: - raise ImproperlyConfigured("You have not added the Mandrill api " - "url to your settings.py") def get_context_data(self, **kwargs): kwargs = super(DjrillApiMixin, self).get_context_data(**kwargs) @@ -53,7 +52,7 @@ class DjrillApiJsonObjectsMixin(object): def get_api_uri(self): if self.api_uri is None: - raise ImproperlyConfigured(u"%(cls)s is missing an api_uri. Define " + raise NotImplementedError(u"%(cls)s is missing an api_uri. Define " u"%(cls)s.api_uri or override %(cls)s.get_api_uri()." % { "cls": self.__class__.__name__ }) From 0a2595d7318acc5a64a7ea13ba8186ac0956c8b1 Mon Sep 17 00:00:00 2001 From: medmunds Date: Wed, 5 Dec 2012 08:56:35 -0800 Subject: [PATCH 2/2] Add test to ensure Mandrill account settings not overridden by default. Default behavior should be to not pass any options to the Mandrill send API that haven't been specifically requested by the caller. --- djrill/tests.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/djrill/tests.py b/djrill/tests.py index 6aa261b..7c9e175 100644 --- a/djrill/tests.py +++ b/djrill/tests.py @@ -271,6 +271,29 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase): 'values': { 'cust_id': "94107", 'order_id': "43215" } } ]) + def test_default_omits_options(self): + """Make sure by default we don't send any Mandrill-specific options. + + Options not specified by the caller should be omitted entirely from + the Mandrill API call (*not* sent as False or empty). This ensures + that your Mandrill account settings apply by default. + """ + self.message.send() + data = self.get_api_call_data() + self.assertFalse('from_name' in data['message']) + self.assertFalse('track_opens' in data['message']) + self.assertFalse('track_clicks' in data['message']) + self.assertFalse('auto_text' in data['message']) + self.assertFalse('url_strip_qs' in data['message']) + self.assertFalse('tags' in data['message']) + self.assertFalse('preserve_recipients' in data['message']) + self.assertFalse('google_analytics_domains' in data['message']) + self.assertFalse('google_analytics_campaign' in data['message']) + self.assertFalse('metadata' in data['message']) + self.assertFalse('global_merge_vars' in data['message']) + self.assertFalse('merge_vars' in data['message']) + self.assertFalse('recipient_metadata' in data['message']) + def reset_admin_site(): """Return the Django admin globals to their original state"""