Support async, ip_pool, and send_at.

Closes #40.
Closes #48.
This commit is contained in:
medmunds
2013-10-19 13:09:05 -07:00
parent f565a4c294
commit b26ba42e77
4 changed files with 103 additions and 16 deletions

View File

@@ -8,6 +8,7 @@ from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_T
from ... import MANDRILL_API_URL, MandrillAPIError, NotSupportedByMandrillError from ... import MANDRILL_API_URL, MandrillAPIError, NotSupportedByMandrillError
from base64 import b64encode from base64 import b64encode
from datetime import date, datetime
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.utils import parseaddr from email.utils import parseaddr
import json import json
@@ -17,6 +18,25 @@ import requests
DjrillBackendHTTPError = MandrillAPIError # Backwards-compat Djrill<=0.2.0 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): class DjrillBackend(BaseEmailBackend):
""" """
Mandrill API Email Backend Mandrill API Email Backend
@@ -54,32 +74,35 @@ class DjrillBackend(BaseEmailBackend):
if not message.recipients(): if not message.recipients():
return False return False
api_url = self.api_send
api_params = {
"key": self.api_key,
}
try: try:
msg_dict = self._build_standard_message_dict(message) msg_dict = self._build_standard_message_dict(message)
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)
self._add_attachments(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: except NotSupportedByMandrillError:
if not self.fail_silently: if not self.fail_silently:
raise raise
return False return False
api_url = self.api_send response = requests.post(api_url, data=json.dumps(api_params, cls=JSONDateUTCEncoder))
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))
if response.status_code != 200: if response.status_code != 200:
if not self.fail_silently: if not self.fail_silently:
@@ -140,8 +163,18 @@ class DjrillBackend(BaseEmailBackend):
return msg_dict 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): 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 attributes that can be copied directly:
mandrill_attrs = [ mandrill_attrs = [
'from_name', # overrides display name parsed from from_email above 'from_name', # overrides display name parsed from from_email above

View File

@@ -1,4 +1,5 @@
from base64 import b64decode from base64 import b64decode
from datetime import date, datetime, timedelta, tzinfo
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.image import MIMEImage from email.mime.image import MIMEImage
import os import os
@@ -312,6 +313,8 @@ class DjrillMandrillFeatureTests(DjrillBackendMockAPITestCase):
self.message.preserve_recipients = True self.message.preserve_recipients = True
self.message.tracking_domain = "click.example.com" self.message.tracking_domain = "click.example.com"
self.message.signing_domain = "example.com" self.message.signing_domain = "example.com"
self.message.async = True
self.message.ip_pool = "Bulk Pool"
self.message.send() self.message.send()
data = self.get_api_call_data() data = self.get_api_call_data()
self.assertEqual(data['message']['auto_text'], True) 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']['preserve_recipients'], True)
self.assertEqual(data['message']['tracking_domain'], "click.example.com") self.assertEqual(data['message']['tracking_domain'], "click.example.com")
self.assertEqual(data['message']['signing_domain'], "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): def test_merge(self):
# Djrill expands simple python dicts into the more-verbose name/content # 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" } } '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): def test_default_omits_options(self):
"""Make sure by default we don't send any Mandrill-specific options. """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('merge_vars' in data['message'])
self.assertFalse('recipient_metadata' in data['message']) self.assertFalse('recipient_metadata' in data['message'])
self.assertFalse('images' 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)

View File

@@ -3,6 +3,8 @@ Release Notes
Version 0.7 (development): Version 0.7 (development):
* Support for Mandrill send options :attr:`async`, :attr:`ip_pool`, and :attr:`send_at`
Version 0.6: Version 0.6:

View File

@@ -219,6 +219,20 @@ Most of the options from the Mandrill
and values are dicts of metadata for each recipient (similar to and values are dicts of metadata for each recipient (similar to
:attr:`merge_vars`) :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* These Mandrill-specific properties work with *any*
:class:`~django.core.mail.EmailMessage`-derived object, so you can use them with :class:`~django.core.mail.EmailMessage`-derived object, so you can use them with