From fd04e60171079fab6af91c49fade72976f47516f Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Fri, 20 Jan 2012 16:59:15 -0800 Subject: [PATCH 1/8] Start of the mail backend for djrill. --- djrill/mail/__init__.py | 0 djrill/mail/backends/__init__.py | 0 djrill/mail/backends/djrill.py | 36 ++++++++++++++++++++++++++++++++ settings/base.py | 3 +++ 4 files changed, 39 insertions(+) create mode 100644 djrill/mail/__init__.py create mode 100644 djrill/mail/backends/__init__.py create mode 100644 djrill/mail/backends/djrill.py diff --git a/djrill/mail/__init__.py b/djrill/mail/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djrill/mail/backends/__init__.py b/djrill/mail/backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py new file mode 100644 index 0000000..be4ba9c --- /dev/null +++ b/djrill/mail/backends/djrill.py @@ -0,0 +1,36 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.mail.backends.base import BaseEmailBackend + + +class DjrillBackend(BaseEmailBackend): + """ + Mandrill API Email Backend + """ + + def __init__(self, fail_silently=False, **kwargs): + """ + Set the API key, API url and set the action url. + """ + super(DjrillBackend, self).__init__(**kwargs) + self.api_key = getattr(settings, "MANDRILL_API_KEY", None) + self.api_url = getattr(settings, "MANDRILL_API_URL", None) + + 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" + + def open(self): + """ + Ping the Mandrill API to make sure they are up + and that we have a valid API key and sender. + """ + pass + + def send_messages(self, email_messages): + pass diff --git a/settings/base.py b/settings/base.py index c79b5b7..11f3758 100644 --- a/settings/base.py +++ b/settings/base.py @@ -24,6 +24,9 @@ DATABASES = { MANDRILL_API_KEY = None MANDRILL_API_URL = "http://mandrillapp.com/api/1.0/" +#EMAIL BACKEND +EMAIL_BACKEND = "djrill.mail.backends.djrill.DjrillBackend" + # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name From 3b227b2f700c624b85cccdab8050ad23e25babf9 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Fri, 20 Jan 2012 18:03:33 -0800 Subject: [PATCH 2/8] Basic send_mail functioning. --- djrill/mail/backends/djrill.py | 66 +++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index be4ba9c..087079b 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -1,6 +1,10 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail.message import sanitize_address +from django.utils import simplejson as json + +import requests class DjrillBackend(BaseEmailBackend): @@ -15,6 +19,7 @@ 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.connection = None if not self.api_key: raise ImproperlyConfigured("You have not set your mandrill api key " @@ -24,13 +29,64 @@ class DjrillBackend(BaseEmailBackend): "url to your settings.py") self.api_action = self.api_url + "/messages/send.json" + self.api_verify = self.api_url + "/users/verify-sender.json" - def open(self): + def open(self, sender): """ - Ping the Mandrill API to make sure they are up - and that we have a valid API key and sender. """ - pass + self.connection = None + + valid_sender = requests.post( + self.api_verify, data={"key": self.api_key, "email": sender}) + + if valid_sender.status_code == 200: + data = json.loads(valid_sender.content) + if data["is_enabled"]: + self.connection = True + return True + else: + if not self.fail_silently: + raise def send_messages(self, email_messages): - pass + if not email_messages: + return + + num_sent = 0 + for message in email_messages: + self.open(message.from_email) + if not self.connection: + return + + sent = self._send(message) + + if sent: + num_sent += 1 + + return num_sent + + def _send(self, message): + if not message.recipients(): + return False + + sender = sanitize_address(message.from_email, message.encoding) + recipients = [{"email": sanitize_address(addr, message.encoding)} + for addr in message.recipients()] + + djrill_it = requests.post(self.api_action, data=json.dumps({ + "key": self.api_key, + "message": { + "html": message.body, + "text": message.body, + "subject": message.subject, + "from_email": sender, + "from_name": "Devs", + "to": recipients + } + })) + + if djrill_it.status_code != 200: + if not self.fail_silently: + raise + return False + return True From e4362f8c341b19931fa6a8d9599b21d289bbf0a9 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Fri, 20 Jan 2012 18:24:13 -0800 Subject: [PATCH 3/8] Its broken. Parse recipients. --- djrill/mail/backends/djrill.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index 087079b..b3e70f6 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -70,8 +70,12 @@ class DjrillBackend(BaseEmailBackend): return False sender = sanitize_address(message.from_email, message.encoding) - recipients = [{"email": sanitize_address(addr, message.encoding)} + recipients_list = [sanitize_address(addr, message.encoding) for addr in message.recipients()] + from email.utils import parseaddr + recipients = [{"email": email, "name": name} for email,name in parseaddr(recipients_list[:])] + #recipients = [{"email": sanitize_address(addr, message.encoding)} + #for addr in message.recipients()] djrill_it = requests.post(self.api_action, data=json.dumps({ "key": self.api_key, From d6ff089060f0e338a57a323a5391339b0a3fbd5b Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 28 Feb 2012 11:34:48 -0800 Subject: [PATCH 4/8] Fix recipients list. --- djrill/mail/backends/djrill.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index b3e70f6..dc73231 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -73,9 +73,8 @@ class DjrillBackend(BaseEmailBackend): recipients_list = [sanitize_address(addr, message.encoding) for addr in message.recipients()] from email.utils import parseaddr - recipients = [{"email": email, "name": name} for email,name in parseaddr(recipients_list[:])] - #recipients = [{"email": sanitize_address(addr, message.encoding)} - #for addr in message.recipients()] + recipients = [{"email": e, "name": n} for n,e in [ + parseaddr(r) for r in recipients_list]] djrill_it = requests.post(self.api_action, data=json.dumps({ "key": self.api_key, From 91b846fea31f6d1531bc134a37afa6603d3bd7ad Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 28 Feb 2012 13:03:26 -0800 Subject: [PATCH 5/8] DjrillMessage which wraps Django's EmailMultiAlternatives. Mail backend now works with standard email and multipart through the DjrillMessage object. --- djrill/mail/__init__.py | 37 +++++++++++++++++++++++ djrill/mail/backends/djrill.py | 54 +++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/djrill/mail/__init__.py b/djrill/mail/__init__.py index e69de29..960e87a 100644 --- a/djrill/mail/__init__.py +++ b/djrill/mail/__init__.py @@ -0,0 +1,37 @@ +from django.core.exceptions import ImproperlyConfigured +from django.core.mail import EmailMultiAlternatives + + +class DjrillMessage(EmailMultiAlternatives): + alternative_subtype = "mandrill" + + def __init__(self, subject='', body='', from_email=None, to=None, bcc=None, + connection=None, attachments=None, headers=None, alternatives=None, + cc=None, tags=None, track_opens=True, track_clicks=True): + + super(DjrillMessage, self).__init__(subject, body, from_email, to, bcc, + connection, attachments, headers, alternatives, cc) + + self.tags = self._set_mandrill_tags(tags) + self.track_opens = track_opens + self.track_clicks = track_clicks + + def _set_mandrill_tags(self, tags): + """ + Check that all tags are below 50 chars and that they do not start + with an underscore. + + Raise ImproperlyConfigured if an underscore tag is passed in to + alert the user. Any tag over 50 chars is left out of the list. + """ + tag_list = [] + + for tag in tags: + if len(tag) <= 50 and not tag.startswith("_"): + tag_list.append(tag) + elif tag.startswith("_"): + raise ImproperlyConfigured( + "Tags starting with an underscore are reserved for " + "internal use and will cause errors with Mandill's API") + + return tag_list diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index dc73231..f9acbe5 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -69,23 +69,21 @@ class DjrillBackend(BaseEmailBackend): if not message.recipients(): return False - sender = sanitize_address(message.from_email, message.encoding) + self.sender = sanitize_address(message.from_email, message.encoding) recipients_list = [sanitize_address(addr, message.encoding) for addr in message.recipients()] from email.utils import parseaddr - recipients = [{"email": e, "name": n} for n,e in [ + 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 message.alternative_subtype == "mandrill" and message.alternatives: + self._add_alternatives(message) + djrill_it = requests.post(self.api_action, data=json.dumps({ "key": self.api_key, - "message": { - "html": message.body, - "text": message.body, - "subject": message.subject, - "from_email": sender, - "from_name": "Devs", - "to": recipients - } + "message": self.msg_dict })) if djrill_it.status_code != 200: @@ -93,3 +91,39 @@ class DjrillBackend(BaseEmailBackend): raise return False return True + + def _build_standard_message_dict(self, message): + """ + Build standard message dict. + + 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. + """ + return { + "text": message.body, + "subject": message.subject, + "from_email": self.sender, + "from_name": "Devs - FIX ME", + "to": self.recipients + } + + def _add_alternatives(self, message): + """ + There can be only one! ... alternative attachment. + + Since mandrill does not accept image attachments or anything other + than HTML, the assumption is the only thing you are attaching is + the HTML output for your email. + """ + if len(message.alternatives) > 1: + raise ImproperlyConfigured( + "Mandrill only accepts plain text and html emails. Please " + "check the alternatives you have attached to your message.") + + self.msg_dict.update({ + "html": message.alternatives[0][0], + "tags": message.tags, + "track_opens": message.track_opens, + "track_clicks": message.track_clicks + }) From 64bbd40884ca849c8929ffd1b83f0c628c899628 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 28 Feb 2012 13:08:06 -0800 Subject: [PATCH 6/8] No, really. Now it works with standard emails. --- djrill/mail/backends/djrill.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/djrill/mail/backends/djrill.py b/djrill/mail/backends/djrill.py index f9acbe5..1d05de6 100644 --- a/djrill/mail/backends/djrill.py +++ b/djrill/mail/backends/djrill.py @@ -78,8 +78,10 @@ class DjrillBackend(BaseEmailBackend): self.msg_dict = self._build_standard_message_dict(message) - if message.alternative_subtype == "mandrill" and message.alternatives: - self._add_alternatives(message) + if getattr(message, "alternative_subtype", None): + if message.alternative_subtype == "mandrill": + if message.alternatives: + self._add_alternatives(message) djrill_it = requests.post(self.api_action, data=json.dumps({ "key": self.api_key, From 2d777b5679b9c84de8efd46fb44a55630218ebf3 Mon Sep 17 00:00:00 2001 From: Chris Jones Date: Tue, 28 Feb 2012 14:01:06 -0800 Subject: [PATCH 7/8] Tests for DjrillMessage. --- djrill/tests.py | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/djrill/tests.py b/djrill/tests.py index e69de29..70bd17b 100644 --- a/djrill/tests.py +++ b/djrill/tests.py @@ -0,0 +1,51 @@ +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from djrill.mail import DjrillMessage + + +class DjrillMessageTests(TestCase): + def setUp(self): + self.subject = "Djrill baby djrill." + self.from_email = "test@example" + self.to = ["King Kong ", + "Cheetah Date: Tue, 28 Feb 2012 14:03:50 -0800 Subject: [PATCH 8/8] Add blank models file for tests to run. --- djrill/models.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 djrill/models.py diff --git a/djrill/models.py b/djrill/models.py new file mode 100644 index 0000000..e69de29