From b26ba42e774994f23e7915dbffcffa18cf3318e3 Mon Sep 17 00:00:00 2001 From: medmunds Date: Sat, 19 Oct 2013 13:09:05 -0700 Subject: [PATCH] Support async, ip_pool, and send_at. Closes #40. Closes #48. --- djrill/mail/backends/djrill.py | 65 ++++++++++++++++++++++-------- djrill/tests/test_mandrill_send.py | 38 +++++++++++++++++ docs/history.rst | 2 + docs/usage/sending_mail.rst | 14 +++++++ 4 files changed, 103 insertions(+), 16 deletions(-) diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index 7998ac4..099055d 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -8,6 +8,7 @@ from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_T from ... import MANDRILL_API_URL, MandrillAPIError, NotSupportedByMandrillError from base64 import b64encode +from datetime import date, datetime from email.mime.base import MIMEBase from email.utils import parseaddr import json @@ -17,6 +18,25 @@ import requests DjrillBackendHTTPError = MandrillAPIError # Backwards-compat Djrill<=0.2.0 +class JSONDateUTCEncoder(json.JSONEncoder): + """JSONEncoder that encodes dates in string format used by Mandrill. + + datetime becomes "YYYY-MM-DD HH:MM:SS" + converted to UTC, if timezone-aware + microseconds removed + date becomes "YYYY-MM-DD 00:00:00" + """ + def default(self, obj): + if isinstance(obj, datetime): + dt = obj.replace(microsecond=0) + if dt.utcoffset() is not None: + dt = (dt - dt.utcoffset()).replace(tzinfo=None) + return dt.isoformat(' ') + elif isinstance(obj, date): + return obj.isoformat() + ' 00:00:00' + return super(JSONDateUTCEncoder, self).default(obj) + + class DjrillBackend(BaseEmailBackend): """ Mandrill API Email Backend @@ -54,32 +74,35 @@ class DjrillBackend(BaseEmailBackend): if not message.recipients(): return False + api_url = self.api_send + api_params = { + "key": self.api_key, + } + 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, msg_dict) self._add_attachments(message, msg_dict) + api_params['message'] = msg_dict + + # 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 + api_params['template_content'] = \ + self._expand_merge_vars(getattr(message, 'template_content', {})) + + self._add_mandrill_toplevel_options(message, api_params) + except NotSupportedByMandrillError: if not self.fail_silently: raise return False - api_url = self.api_send - api_params = { - "key": self.api_key, - "message": msg_dict - } - - # 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 - api_params['template_content'] = \ - self._expand_merge_vars(getattr(message, 'template_content', {})) - - response = requests.post(api_url, data=json.dumps(api_params)) + response = requests.post(api_url, data=json.dumps(api_params, cls=JSONDateUTCEncoder)) if response.status_code != 200: if not self.fail_silently: @@ -140,8 +163,18 @@ class DjrillBackend(BaseEmailBackend): return msg_dict + def _add_mandrill_toplevel_options(self, message, api_params): + """Extend api_params to include Mandrill global-send options set on message""" + # Mandrill attributes that can be copied directly: + mandrill_attrs = [ + 'async', 'ip_pool', 'send_at' + ] + for attr in mandrill_attrs: + if hasattr(message, attr): + api_params[attr] = getattr(message, attr) + def _add_mandrill_options(self, message, msg_dict): - """Extend msg_dict to include Mandrill options set on message""" + """Extend msg_dict to include Mandrill per-message options set on message""" # Mandrill attributes that can be copied directly: mandrill_attrs = [ 'from_name', # overrides display name parsed from from_email above diff --git a/djrill/tests/test_mandrill_send.py b/djrill/tests/test_mandrill_send.py index d0c52e0..9e5690a 100644 --- a/djrill/tests/test_mandrill_send.py +++ b/djrill/tests/test_mandrill_send.py @@ -1,4 +1,5 @@ from base64 import b64decode +from datetime import date, datetime, timedelta, tzinfo from email.mime.base import MIMEBase from email.mime.image import MIMEImage import os @@ -312,6 +313,8 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase): self.message.preserve_recipients = True self.message.tracking_domain = "click.example.com" self.message.signing_domain = "example.com" + self.message.async = True + self.message.ip_pool = "Bulk Pool" self.message.send() data = self.get_api_call_data() self.assertEqual(data['message']['auto_text'], True) @@ -320,6 +323,8 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase): self.assertEqual(data['message']['preserve_recipients'], True) self.assertEqual(data['message']['tracking_domain'], "click.example.com") self.assertEqual(data['message']['signing_domain'], "example.com") + self.assertEqual(data['async'], True) + self.assertEqual(data['ip_pool'], "Bulk Pool") def test_merge(self): # Djrill expands simple python dicts into the more-verbose name/content @@ -379,6 +384,35 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase): 'values': { 'cust_id': "94107", 'order_id': "43215" } } ]) + def test_send_at(self): + # String passed unchanged + self.message.send_at = "2013-11-12 01:02:03" + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['send_at'], "2013-11-12 01:02:03") + + # Timezone-naive datetime assumed to be UTC + self.message.send_at = datetime(2022, 10, 11, 12, 13, 14, 567) + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['send_at'], "2022-10-11 12:13:14") + + # Timezone-aware datetime converted to UTC: + class GMTminus8(tzinfo): + def utcoffset(self, dt): return timedelta(hours=-8) + def dst(self, dt): return timedelta(0) + + self.message.send_at = datetime(2016, 3, 4, 5, 6, 7, tzinfo=GMTminus8()) + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['send_at'], "2016-03-04 13:06:07") + + # Date-only treated as midnight UTC + self.message.send_at = date(2022, 10, 22) + self.message.send() + data = self.get_api_call_data() + self.assertEqual(data['send_at'], "2022-10-22 00:00:00") + def test_default_omits_options(self): """Make sure by default we don't send any Mandrill-specific options. @@ -408,5 +442,9 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase): self.assertFalse('merge_vars' in data['message']) self.assertFalse('recipient_metadata' in data['message']) self.assertFalse('images' in data['message']) + # Options at top level of api params (not in message dict): + self.assertFalse('send_at' in data) + self.assertFalse('async' in data) + self.assertFalse('ip_pool' in data) diff --git a/docs/history.rst b/docs/history.rst index f77a0ed..71be03f 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -3,6 +3,8 @@ Release Notes Version 0.7 (development): +* Support for Mandrill send options :attr:`async`, :attr:`ip_pool`, and :attr:`send_at` + Version 0.6: diff --git a/docs/usage/sending_mail.rst b/docs/usage/sending_mail.rst index cb3c3b2..235404c 100644 --- a/docs/usage/sending_mail.rst +++ b/docs/usage/sending_mail.rst @@ -219,6 +219,20 @@ Most of the options from the Mandrill and values are dicts of metadata for each recipient (similar to :attr:`merge_vars`) +.. attribute:: async + + ``Boolean``: whether Mandrill should use an async mode optimized for bulk sending. + +.. attribute:: ip_pool + + ``str``: name of one of your Mandrill dedicated IP pools to use for sending this message. + +.. attribute:: send_at + + ``datetime`` or ``date`` or ``str``: instructs Mandrill to delay sending this message + until the specified time. (Djrill allows timezone-aware Python datetimes, and converts them + to UTC for Mandrill. Timezone-naive datetimes are assumed to be UTC.) + These Mandrill-specific properties work with *any* :class:`~django.core.mail.EmailMessage`-derived object, so you can use them with