Skip to content
Browse files

Merge branch 'release/v0.4.2'

  • Loading branch information...
2 parents c86bc6e + 41e800d commit 5cd7c1d85f135904dbd4e5ddf55f038cc334ce00 @RyanBalfanz committed Mar 24, 2012
View
1 .gitignore
@@ -2,6 +2,7 @@
*.pyc
*.DS_Store
secret.sh
+docs/_build/*
build/*
dist/*
django_sendgrid.egg-info/*
View
1 README.rst
@@ -45,3 +45,4 @@ Additional Information
- https://docs.djangoproject.com/en/1.3/topics/email/
- http://ryanbalfanz.github.com/django-sendgrid/
- http://pypi.python.org/pypi/django-sendgrid
+ - http://djangopackages.com/packages/p/django-sendgrid/
View
38 example_project/main/templates/main/send_email.html
@@ -3,11 +3,41 @@
<head>
<meta charset="UTF-8">
<title>django-sendgrid example project</title>
+ <style type="text/css">
+ body {
+ padding-top: 60px;
+ padding-bottom: 40px;
+ }
+ </style>
+ <link rel="stylesheet" href="http://twitter.github.com/bootstrap/assets/css/bootstrap.css" type="text/css" media="screen" charset="utf-8">
+ <!-- http://davidwalsh.name/blank-favicon -->
+ <link href="data:image/x-icon;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQEAYAAABPYyMiAAAABmJLR0T///////8JWPfcAAAACXBIWXMAAABIAAAASABGyWs+AAAAF0lEQVRIx2NgGAWjYBSMglEwCkbBSAcACBAAAeaR9cIAAAAASUVORK5CYII=" rel="icon" type="image/x-icon" />
</head>
<body>
- <form action="/sendgrid/" method="post">{% csrf_token %}
- {{ form.as_p }}
- <input type="submit" value="Submit" />
- </form>
+ <div class="navbar navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container">
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" href="#">django-sendgrid</a>
+ <div class="nav-collapse">
+ <ul class="nav">
+ <li><a href="https://github.com/RyanBalfanz/django-sendgrid">Github</a></li>
+ <li><a href="http://pypi.python.org/pypi/django-sendgrid">PyPI</a></li>
+ <li><a href="http://djangopackages.com/packages/p/django-sendgrid/">django-packages</a></li>
+ </ul>
+ </div><!--/.nav-collapse -->
+ </div>
+ </div>
+ </div>
+ <div class="container">
+ <form action="/sendgrid/" method="post">{% csrf_token %}
+ {{ form.as_p }}
+ <input type="submit" value="Send" />
+ </form>
+ </div>
</body>
</html>
View
5 example_project/settings.py
@@ -169,3 +169,8 @@
SENDGRID_EMAIL_PORT = 587
SENDGRID_EMAIL_USERNAME = os.getenv("SENDGRID_EMAIL_USERNAME")
SENDGRID_EMAIL_PASSWORD = os.getenv("SENDGRID_EMAIL_PASSWORD")
+
+try:
+ from settings_local import *
+except ImportError:
+ pass
View
1 example_project/templates/404.html
@@ -0,0 +1 @@
+404 Error
View
1 example_project/templates/500.html
@@ -0,0 +1 @@
+500 Error
View
5 example_project/urls.py
@@ -6,9 +6,8 @@
urlpatterns = patterns('',
# Examples:
- url(r'^$', include('example_project.main.urls')),
- url(r'^sendgrid/$', include('example_project.main.urls')),
-
+ url(r'^$', 'django.views.generic.simple.redirect_to', {'url': '/sendgrid/'}, name='home'),
+ url(r"^sendgrid/", include("example_project.main.urls")),
# Uncomment the admin/doc line below to enable admin documentation:
# url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
View
5 requirements.txt
@@ -0,0 +1,5 @@
+Django==1.3.1
+distribute==0.6.24
+virtualenv==1.6.4
+virtualenvwrapper==2.10.1
+wsgiref==0.1.2
View
6 requirments.txt
@@ -1,6 +0,0 @@
-## The following requirements were added by pip --freeze:
-Django==1.3.1
-distribute==0.6.24
-virtualenv==1.7
-virtualenvwrapper==2.11.1
-wsgiref==0.1.2
View
2 sendgrid/__init__.py
@@ -1,6 +1,6 @@
"""django-sendgrid"""
-VERSION = (0, 4, 1)
+VERSION = (0, 4, 2)
__version__ = ".".join(map(str, VERSION[0:3])) + "".join(VERSION[3:])
__author__ = "Ryan Balfanz"
View
11 sendgrid/backends.py
@@ -1,6 +1,7 @@
import logging
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
from django.core.mail.backends.smtp import EmailBackend
@@ -11,8 +12,7 @@
logger = logging.getLogger(__name__)
-
-def check_settings():
+def check_settings(fail_silently=False):
"""
Checks that the required settings are available.
"""
@@ -29,17 +29,20 @@ def check_settings():
if not value:
logger.warn("{k} is not set".format(k=key))
allOk = False
+ if not fail_silently:
+ raise ImproperlyConfigured("{k} was not found".format(k=key))
return allOk
+
class SendGridEmailBackend(EmailBackend):
"""
A wrapper that manages the SendGrid SMTP network connection.
"""
def __init__(self, host=None, port=None, username=None, password=None, use_tls=None, fail_silently=False, **kwargs):
if not check_settings():
- raise ValueError("A required setting was not found")
-
+ logger.exception("A required setting was not found")
+
super(SendGridEmailBackend, self).__init__(
host=SENDGRID_EMAIL_HOST,
port=SENDGRID_EMAIL_PORT,
View
123 sendgrid/message.py
@@ -1,38 +1,32 @@
+from __future__ import absolute_import
+
+# import datetime
import logging
-import json
+import time
+import uuid
+try:
+ import simplejson as json
+except ImportError:
+ import json
from django.conf import settings
from django.core import mail
from django.core.mail.message import EmailMessage
+from django.core.mail.message import EmailMultiAlternatives
# django-sendgrid imports
-from header import SmtpApiHeader
-from mail import get_sendgrid_connection
-from signals import sendgrid_email_sent
+from .header import SmtpApiHeader
+from .mail import get_sendgrid_connection
+from .signals import sendgrid_email_sent
logger = logging.getLogger(__name__)
-class SendGridEmailMessage(EmailMessage):
+class SendGridEmailMessageMixin:
"""
- Adapts Django's ``EmailMessage`` for use with SendGrid.
-
- >>> from sendgrid.message import SendGridEmailMessage
- >>> myEmail = "rbalfanz@gmail.com"
- >>> mySendGridCategory = "django-sendgrid"
- >>> e = SendGridEmailMessage("Subject", "Message", myEmail, [myEmail], headers={"Reply-To": myEmail})
- >>> e.sendgrid_headers.setCategory(mySendGridCategory)
- >>> response = e.send()
+ Adds support for SendGrid features.
"""
- sendgrid_headers = SmtpApiHeader()
-
- def __init__(self, *args, **kwargs):
- """
- Initialize the object.
- """
- super(SendGridEmailMessage, self).__init__(*args, **kwargs)
-
def _update_headers_with_sendgrid_headers(self):
"""
Updates the existing headers to include SendGrid headers.
@@ -43,28 +37,107 @@ def _update_headers_with_sendgrid_headers(self):
"X-SMTPAPI": self.sendgrid_headers.asJSON()
}
self.extra_headers.update(additionalHeaders)
-
+
logging.debug(str(self.extra_headers))
-
+
return self.extra_headers
+ def _update_unique_args(self, uniqueArgs):
+ """docstring for _update_unique_args"""
+ # assert self.unique_args is None, self.unique_args
+ self.sendgrid_headers.setUniqueArgs(uniqueArgs)
+
+ return self.unique_args
+
def update_headers(self, *args, **kwargs):
"""
Updates the headers.
"""
return self._update_headers_with_sendgrid_headers(*args, **kwargs)
- def send(self, *args, **kwargs):
- """Sends the email message."""
+ def get_category(self):
+ """docstring for get_category"""
+ return self.sendgrid_headers.data["category"]
+ category = property(get_category)
+
+ def get_unique_args(self):
+ """docstring for get_unique_args"""
+ if "unique_args" in self.sendgrid_headers.data:
+ # raise Exception(self.sendgrid_headers.data["unique_args"])
+ uniqueArgs = self.sendgrid_headers.data["unique_args"]
+ else:
+ uniqueArgs = None
+ return uniqueArgs
+ unique_args = property(get_unique_args)
+
+ def setup_connection(self):
+ """docstring for setup_connection"""
# Set up the connection
connection = get_sendgrid_connection()
self.connection = connection
logger.debug("Connection: {c}".format(c=connection))
+
+ def prep_message_for_sending(self):
+ """docstring for prep_message_for_sending"""
+ self.setup_connection()
+
+ # now = tz.localize(datetime.datetime.strptime(timestamp[:26], POSTMARK_DATETIME_STRING)).astimezone(pytz.utc)
+ uniqueArgs = {
+ "message_id": str(self._message_id),
+ # "submition_time": time.time(),
+ }
+ self._update_unique_args(uniqueArgs)
self.update_headers()
+
+
+class SendGridEmailMessage(EmailMessage, SendGridEmailMessageMixin):
+ """
+ Adapts Django's ``EmailMessage`` for use with SendGrid.
+
+ >>> from sendgrid.message import SendGridEmailMessage
+ >>> myEmail = "rbalfanz@gmail.com"
+ >>> mySendGridCategory = "django-sendgrid"
+ >>> e = SendGridEmailMessage("Subject", "Message", myEmail, [myEmail], headers={"Reply-To": myEmail})
+ >>> e.sendgrid_headers.setCategory(mySendGridCategory)
+ >>> response = e.send()
+ """
+ sendgrid_headers = SmtpApiHeader()
+
+ def __init__(self, *args, **kwargs):
+ """
+ Initialize the object.
+ """
+ self._message_id = uuid.uuid4()
+ super(SendGridEmailMessage, self).__init__(*args, **kwargs)
+
+ def send(self, *args, **kwargs):
+ """Sends the email message."""
+ self.prep_message_for_sending()
response = super(SendGridEmailMessage, self).send(*args, **kwargs)
logger.debug("Tried to send an email with SendGrid and got response {r}".format(r=response))
sendgrid_email_sent.send(sender=self, response=response)
return response
+
+
+class SendGridEmailMultiAlternatives(EmailMultiAlternatives, SendGridEmailMessageMixin):
+ """
+ Adapts Django's ``EmailMultiAlternatives`` for use with SendGrid.
+ """
+ sendgrid_headers = SmtpApiHeader()
+
+ def __init__(self, *args, **kwargs):
+ self._message_id = uuid.uuid4()
+ super(SendGridEmailMultiAlternatives, self).__init__(*args, **kwargs)
+
+ def send(self, *args, **kwargs):
+ """Sends the email message."""
+ self.prep_message_for_sending()
+
+ response = super(SendGridEmailMultiAlternatives, self).send(*args, **kwargs)
+ logger.debug("Tried to send an email with SendGrid and got response {r}".format(r=response))
+ sendgrid_email_sent.send(sender=self, response=response)
+
+ return response
View
39 sendgrid/mixins.py
@@ -0,0 +1,39 @@
+from django.conf import settings
+from django.utils import simplejson
+
+from utils import add_unsubscribes
+from utils import delete_unsubscribes
+from utils import get_unsubscribes
+
+
+SENDGRID_EMAIL_USERNAME = getattr(settings, "SENDGRID_EMAIL_USERNAME", None)
+SENDGRID_EMAIL_PASSWORD = getattr(settings, "SENDGRID_EMAIL_PASSWORD", None)
+
+
+class SendGridUserMixin:
+ """
+ Adds SendGrid related convienence functions and properties to ``User`` objects.
+ """
+ def is_unsubscribed(self):
+ """
+ Returns True if the ``User``.``email`` belongs to the unsubscribe list.
+ """
+ response = get_unsubscribes(email=self.email)
+ results = simplejson.loads(response)
+ return len(results) > 0
+
+ def add_to_unsubscribes(self):
+ """
+ Adds the ``User``.``email`` from the unsubscribe list.
+ """
+ response = add_unsubscribes(email=self.email)
+ result = simplejson.loads(response)
+ return result
+
+ def delete_from_unsubscribes(self):
+ """
+ Removes the ``User``.``email`` from the unsubscribe list.
+ """
+ response = delete_unsubscribes(email=self.email)
+ result = simplejson.loads(response)
+ return result
View
106 sendgrid/tests.py
@@ -14,14 +14,55 @@
from mail import get_sendgrid_connection
from mail import send_sendgrid_mail
from message import SendGridEmailMessage
+from message import SendGridEmailMultiAlternatives
from signals import sendgrid_email_sent
from utils import filterutils
+from utils import in_test_environment
validate_filter_setting_value = filterutils.validate_filter_setting_value
validate_filter_specification = filterutils.validate_filter_specification
update_filters = filterutils.update_filters
+
+class SendGridEmailTest(TestCase):
+ """docstring for SendGridEmailTest"""
+ def setUp(self):
+ """docstring for setUp"""
+ pass
+
+ def test_email_has_unique_id(self):
+ """docstring for email_has_unique_id"""
+ email = SendGridEmailMessage()
+ self.assertTrue(email._message_id)
+
+ def test_email_sends_unique_id(self):
+ """docstring for email_sends_unique_id"""
+ email = SendGridEmailMessage()
+ email.send()
+ self.assertTrue(email.sendgrid_headers.data["unique_args"]["message_id"])
+
+ def test_email_sent_signal_has_message(self):
+ """docstring for email_sent_signal_has_message"""
+ @receiver(sendgrid_email_sent)
+ def receive_sendgrid_email_sent(*args, **kwargs):
+ """
+ Receives sendgrid_email_sent signals.
+ """
+ self.assertTrue("response" in kwargs)
+ # self.assertTrue("message" in kwargs)
+
+ email = SendGridEmailMessage()
+ response = email.send()
+
+class SendGridInTestEnvTest(TestCase):
+ def test_in_test_environment(self):
+ """
+ Tests that the test environment is detected.
+ """
+ self.assertEqual(in_test_environment(), True)
+
+
class SendWithSendGridEmailMessageTest(TestCase):
def setUp(self):
"""
@@ -81,6 +122,35 @@ def test_send_with_email_message(self):
email.send()
+class SendWithSendGridEmailMultiAlternativesTest(TestCase):
+ def setUp(self):
+ self.signalsReceived = defaultdict(list)
+
+ def test_send_multipart_email(self):
+ """docstring for send_multipart_email"""
+ subject, from_email, to = 'hello', 'from@example.com', 'to@example.com'
+ text_content = 'This is an important message.'
+ html_content = '<p>This is an <strong>important</strong> message.</p>'
+ msg = SendGridEmailMultiAlternatives(subject, text_content, from_email, [to])
+ msg.attach_alternative(html_content, "text/html")
+ msg.send()
+
+ def test_send_multipart_email_sends_signal(self):
+ @receiver(sendgrid_email_sent)
+ def receive_sendgrid_email_sent(*args, **kwargs):
+ """
+ Receives sendgrid_email_sent signals.
+ """
+ self.signalsReceived["sendgrid_email_sent"].append(1)
+ return True
+
+ email = SendGridEmailMultiAlternatives()
+ email.send()
+
+ numEmailSentSignalsRecieved = sum(self.signalsReceived["sendgrid_email_sent"])
+ self.assertEqual(numEmailSentSignalsRecieved, 1)
+
+
class FilterUtilsTests(TestCase):
"""docstring for FilterUtilsTests"""
def setUp(self):
@@ -101,33 +171,33 @@ def test_validate_filter_spec(self):
"enable": 0,
},
}
- assert validate_filter_specification(filterSpec) == True
+ self.assertEqual(validate_filter_specification(filterSpec), True)
def test_subscriptiontrack_enable_parameter(self):
"""
Tests the ``subscriptiontrack`` filter's ``enable`` paramter.
"""
- assert validate_filter_setting_value("subscriptiontrack", "enable", 0) == True
- assert validate_filter_setting_value("subscriptiontrack", "enable", 1) == True
- assert validate_filter_setting_value("subscriptiontrack", "enable", 0.0) == True
- assert validate_filter_setting_value("subscriptiontrack", "enable", 1.0) == True
- assert validate_filter_setting_value("subscriptiontrack", "enable", "0") == True
- assert validate_filter_setting_value("subscriptiontrack", "enable", "1") == True
- assert validate_filter_setting_value("subscriptiontrack", "enable", "0.0") == False
- assert validate_filter_setting_value("subscriptiontrack", "enable", "1.0") == False
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", 0), True)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", 1), True)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", 0.0), True)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", 1.0), True)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", "0"), True)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", "1"), True)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", "0.0"), False)
+ self.assertEqual(validate_filter_setting_value("subscriptiontrack", "enable", "1.0"), False)
def test_opentrack_enable_parameter(self):
"""
Tests the ``opentrack`` filter's ``enable`` paramter.
"""
- assert validate_filter_setting_value("opentrack", "enable", 0) == True
- assert validate_filter_setting_value("opentrack", "enable", 1) == True
- assert validate_filter_setting_value("opentrack", "enable", 0.0) == True
- assert validate_filter_setting_value("opentrack", "enable", 1.0) == True
- assert validate_filter_setting_value("opentrack", "enable", "0") == True
- assert validate_filter_setting_value("opentrack", "enable", "1") == True
- assert validate_filter_setting_value("opentrack", "enable", "0.0") == False
- assert validate_filter_setting_value("opentrack", "enable", "1.0") == False
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", 0), True)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", 1), True)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", 0.0), True)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", 1.0), True)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", "0"), True)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", "1"), True)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", "0.0"), False)
+ self.assertEqual(validate_filter_setting_value("opentrack", "enable", "1.0"), False)
class UpdateFiltersTests(TestCase):
@@ -147,4 +217,4 @@ def test_update_filters(self):
},
}
update_filters(self.email, filterSpec)
- self.email.send()
+ self.email.send()
View
120 sendgrid/utils/__init__.py
@@ -1,7 +1,123 @@
+import datetime
+import httplib
+import logging
+import time
+import urllib
+import urllib2
+
+from django.conf import settings
+from django.core import mail
+
+
+SENDGRID_EMAIL_USERNAME = getattr(settings, "SENDGRID_EMAIL_USERNAME", None)
+SENDGRID_EMAIL_PASSWORD = getattr(settings, "SENDGRID_EMAIL_PASSWORD", None)
+
+logger = logging.getLogger(__name__)
+
def in_test_environment():
"""
Returns True if in a test environment, False otherwise.
"""
- from django.core import mail
-
return hasattr(mail, 'outbox')
+
+def remove_keys_without_value(d):
+ """
+ Removes all key-value pairs with empty values.
+ """
+ dCopy = d.copy()
+
+ delKeys = [k for k, v in dCopy.iteritems() if not v]
+ for k in delKeys:
+ del dCopy[k]
+
+ return dCopy
+
+def normalize_parameters(d):
+ """
+ Normalizes the parameters, adds authorization details and removes empty entries.
+ """
+ dCopy = d.copy()
+
+ authorization = {
+ "api_user": SENDGRID_EMAIL_USERNAME,
+ "api_key": SENDGRID_EMAIL_PASSWORD,
+ }
+ dCopy.update(authorization)
+ dCopy = remove_keys_without_value(dCopy)
+
+ return dCopy
+
+def get_unsubscribes(date=None, days=None, start_date=None, end_date=None, limit=None, offset=None, email=None):
+ """
+ Returns a list of unsubscribes with addresses and optionally with dates.
+ """
+ ENDPOINT = "https://sendgrid.com/api/unsubscribes.get.json"
+
+ if days and (start_date or end_date):
+ raise AttributeError
+
+ if days:
+ if start_date or end_date:
+ raise AttributeError
+ elif start_date and end_date:
+ if days:
+ raise AttributeError
+
+ parameters = {
+ "date": date,
+ "days": days,
+ "start_date": start_date,
+ "end_date": end_date,
+ "limit": limit,
+ "offset": offset,
+ "email": email,
+ }
+ parameters = normalize_parameters(parameters)
+
+ data = urllib.urlencode(parameters)
+ request = urllib2.Request(ENDPOINT, data)
+ response = urllib2.urlopen(request)
+ content = response.read()
+
+ return content
+
+def add_unsubscribes(email):
+ """
+ Add email addresses to the Unsubscribe list.
+ """
+ ENDPOINT = "https://sendgrid.com/api/unsubscribes.add.json"
+
+ parameters = {
+ "email": email,
+ }
+ parameters = normalize_parameters(parameters)
+
+ data = urllib.urlencode(parameters)
+ request = urllib2.Request(ENDPOINT, data)
+ response = urllib2.urlopen(request)
+ content = response.read()
+
+ return content
+
+def delete_unsubscribes(email, start_date=None, end_date=None):
+ """
+ Delete an address from the Unsubscribe list. Please note that if no parameters are provided the ENTIRE list will be removed.
+ """
+ ENDPOINT = "https://sendgrid.com/api/unsubscribes.delete.json"
+
+ if not ((start_date and end_date) or email):
+ raise Exception("You're about to delete the entire list!")
+
+ parameters = {
+ "start_date": start_date,
+ "end_date": end_date,
+ "email": email,
+ }
+ parameters = normalize_parameters(parameters)
+
+ data = urllib.urlencode(parameters)
+ request = urllib2.Request(ENDPOINT, data)
+ response = urllib2.urlopen(request)
+ content = response.read()
+
+ return content

0 comments on commit 5cd7c1d

Please sign in to comment.
Something went wrong with that request. Please try again.