mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user