Commits

Yorgos Pagles committed d63b26f

SES backend

  • Participants
  • Parent commits 3b55ecb

Comments (0)

Files changed (17)

 Version 1.0
 
-- Support for critsend api (both sending and statistics)
+- Support for critsend API (sending)
+- Support for postmark API (sending)
+- Support for amazon SES API (sending)
 Django Email Services
 =====================
 
-Email services for django providing various backends and respective statistics. Still alpha.
+Email services for django providing various backends (future plans include respective statistics).
 
 ------------
 Installation
 ------------
 
-Since this is still in alpha I wouldn't recommend using it in production environments (although the backends listed in this readme are working they have not been extensively used in a production environment).
-Currently it can be installed from the repository::
+From PyPI using pip::
+
+    pip install -e hg+http://bitbucket.org/pagles/django-email-services#egg=django-email-services
+
+The email services package can also be installed from the repository::
 
     pip install -e hg+http://bitbucket.org/pagles/django-email-services#egg=django-email-services
 
 Add the 'email_services' application to your installed apps and set the following in your settings file::
 
-	EMAIL_BACKEND = 'email_services.backends.BackendName' # Replace BackendName with CritsendEmailBackend or PostmarkEmailBackend
+    # Replace BackendName with CritsendEmailBackend or PostmarkEmailBackend or AmazonSESBackend
+	EMAIL_BACKEND = 'email_services.backends.BackendName'
 	EMAIL_SERVICES_CLIENT_ID = 'your id'
 	EMAIL_SERVICES_CLIENT_KEY = 'your key'
 
------------
-Development
------------
-
-For the tests django_nose is used and coverage reports are generated. The aim is to keep the test coverage really high.
-
-To run the tests clone the repository, navigate to the test_project folder and (preferably after creating a virtual environment) run::
-	
-	pip install -r requirements.txt
-	python manage.py test email_services
-	
-	
-With the requirements installed from inside the test_project folder you can test the functionality::
-
-	python manage.py shell
-		
-	from django.conf import settings
-	from django.core.mail import send_mail
-	settings.EMAIL_BACKEND = 'the backend you'd like to use'
-	settings.EMAIL_SERVICES_CLIENT_ID = 'your user id depending on service'
-	settings.EMAIL_SERVICES_CLIENT_KEY = 'your key depending on service'
-	send_mail("subj", "body", "sender@example.com", ['recipient@example.com', "recipient@example.com"])
-	
-	
---------
-Backends
---------
-
-Critsend
---------
-
-Critsend exposes a SOAP API to interact with their service. The API works heavily with tags where we can store arbitrary information and then lookup statistics per tag.
-The Critsend service of django-email-services uses this feature each time an email is sent setting a tag to be the current site if the sites framework is installed.
-
-The settings should be your username / password as EMAIL_SERVICES_CLIENT_ID / EMAIL_SERVICES_CLIENT_KEY respectively and 'email_services.backends.CritsendEmailBackend' as the EMAIL_BACKEND.
-
-Postmark
---------
-
-Postmark exposes a REST API to interact with their service.
-
-The settings should be your api token as EMAIL_SERVICES_CLIENT_KEY and 'email_services.backends.PostmarkEmailBackend' as the EMAIL_BACKEND.
-
+Use as a typical email backend

File docs/index.rst

 Django Email Services
 =====================
 
-Email services for django providing various backends and respective statistics. Still alpha.
+Email services for django providing various backends (future plans include respective statistics).
 
 ------------
 Installation
 ------------
 
-Since this is still in alpha is isn't recommended using it in production environments (although the backends listed in this readme are working they have not been extensively used in a production environment).
-Currently it can be installed from the repository:
+From PyPI using pip:
+
+.. code-block:: bash
+
+    pip install -e hg+http://bitbucket.org/pagles/django-email-services#egg=django-email-services
+
+The email services package can also be installed from the repository:
 
 .. code-block:: bash
 
 
 .. code-block:: python
 
-	EMAIL_BACKEND = 'email_services.backends.BackendName' # Replace BackendName with CritsendEmailBackend or PostmarkEmailBackend
+    # Replace BackendName with CritsendEmailBackend or PostmarkEmailBackend or AmazonSESBackend
+	EMAIL_BACKEND = 'email_services.backends.BackendName'
 	EMAIL_SERVICES_CLIENT_ID = 'your id'
 	EMAIL_SERVICES_CLIENT_KEY = 'your key'
 
 		
 	EMAIL_BACKEND = 'email_services.backends.PostmarkEmailBackend'
 	EMAIL_SERVICES_CLIENT_KEY = 'your Postmark API key'
+
+
+Amazon SES
+----------
+
+For Amazon SES backend the boto library is used.
+
+The settings should be your access id key / secret access key for AWS as EMAIL_SERVICES_CLIENT_ID / EMAIL_SERVICES_CLIENT_KEY respectively and 'email_services.backends.AmazonSESBackend' as the EMAIL_BACKEND.
+
+.. code-block:: python
+
+	EMAIL_BACKEND = 'email_services.backends.PostmarkEmailBackend'
+	EMAIL_SERVICES_CLIENT_KEY = 'your Postmark API key'

File email_services/backends/__init__.py

 from email_services.backends.critsend import CritsendEmailBackend
 from email_services.backends.postmark import PostmarkEmailBackend
+from email_services.backends.amazon_ses import AmazonSESBackend

File email_services/backends/amazon_ses.py

+"""
+The email backend for Amazon's SES API. This is a thin wrapper
+around an email service that exposes more information using the
+API than the typical email backends that are only used to send
+emails. This is only used as a compatibility layer between django
+and what it expects as an email backend and the respective service
+"""
+
+import logging
+from email_services.backends.base import BaseServiceEmailBackend
+from email_services.services import AmazonSEService
+
+logger = logging.getLogger(__name__)
+
+class AmazonSESBackend(BaseServiceEmailBackend):
+    """
+    Amazon SES email backend. Can be used as a drop in replacement for
+    any of the django email backends. Uses the Amazon API through
+    the respective service calss for sending emails.
+    """
+
+    def __init__(self, fail_silently=False, *args, **kwargs):
+        """
+        Initializes the backend. Set fail_silently to True to make the
+        backend not raise on errors.
+        """
+        self.service = AmazonSEService()
+        self.fail_silently = fail_silently

File email_services/backends/base.py

+"""
+The email backend for Critsend's API. This is a thin wrapper
+around an email service that exposes more information using the
+API than the typical email backends that are only used to send
+emails. This is only used as a compatibility layer between django
+and what it expects as an email backend and the respective service
+"""
+
+import logging
+from django.core.mail.backends.base import BaseEmailBackend
+
+logger = logging.getLogger(__name__)
+
+class BaseServiceEmailBackend(BaseEmailBackend):
+    """
+    Critsend email backend. Can be used as a drop in replacement for
+    any of the django email backends. Uses the Critsend SOAP API through
+    the respective service calss for sending emails.
+    """
+    
+    def __init__(self, fail_silently=False, *args, **kwargs):
+        """
+        Initializes the backend. Set fail_silently to True to make the
+        backend not raise on errors.
+        """
+        self.service = None
+        self.fail_silently = fail_silently
+
+    def open(self):
+        """
+        Delegates opening the connection to the email provider
+        to the respective service
+        """
+        try:
+            self.service.open()
+        except Exception as e:
+            logger.error("Opening connection failed: %s", e)
+            if not self.fail_silently:
+                raise
+
+    def close(self):
+        """
+        Delegates closing the connection to the email provider
+        to the respective service
+        """
+        try:
+            self.service.close()
+        except Exception as e:
+            logger.error("Closing connection failed: %s", e)
+            if not self.fail_silently:
+                raise
+
+    def send_messages(self,messages):
+        """
+        Sends the messages using the critsend email service
+        
+        Arguments:
+        - `messages`: The list of EmailMessage instances to send
+        """
+        try:
+            return self.service.send_messages(messages)
+        except Exception as e:
+            logger.error("Sending email messages failed: %s" % e)
+            if not self.fail_silently:
+                raise
+        
+

File email_services/backends/critsend.py

 """
 
 import logging
-from django.core.mail.backends.base import BaseEmailBackend
+from email_services.backends.base import BaseServiceEmailBackend
 from email_services.services import CritsendEmailService
 
 logger = logging.getLogger(__name__)
 
-class CritsendEmailBackend(BaseEmailBackend):
+class CritsendEmailBackend(BaseServiceEmailBackend):
     """
     Critsend email backend. Can be used as a drop in replacement for
     any of the django email backends. Uses the Critsend SOAP API through
         """
         self.service = CritsendEmailService()
         self.fail_silently = fail_silently
-
-    def open(self):
-        """
-        Delegates opening the connection to the email provider
-        to the respective service
-        """
-        try:
-            self.service.open()
-        except Exception as e:
-            logger.error("Opening connection failed: %s", e)
-            if not self.fail_silently:
-                raise
-
-    def close(self):
-        """
-        Delegates closing the connection to the email provider
-        to the respective service
-        """
-        try:
-            self.service.close()
-        except Exception as e:
-            logger.error("Closing connection failed: %s", e)
-            if not self.fail_silently:
-                raise
-
-    def send_messages(self,messages):
-        """
-        Sends the messages using the critsend email service
-        
-        Arguments:
-        - `messages`: The list of EmailMessage instances to send
-        """
-        try:
-            return self.service.send_messages(messages)
-        except Exception as e:
-            logger.error("Sending email messages failed: %s" % e)
-            if not self.fail_silently:
-                raise
-        
-

File email_services/backends/postmark.py

 """
 
 import logging
-from django.core.mail.backends.base import BaseEmailBackend
+from email_services.backends.base import BaseServiceEmailBackend
 from email_services.services import PostmarkEmailService
 
 logger = logging.getLogger(__name__)
 
-class PostmarkEmailBackend(BaseEmailBackend):
+class PostmarkEmailBackend(BaseServiceEmailBackend):
     """
-    Critsend email backend. Can be used as a drop in replacement for
-    any of the django email backends. Uses the Critsend SOAP API through
+    Postmark email backend. Can be used as a drop in replacement for
+    any of the django email backends. Uses the Postmark REST API through
     the respective service calss for sending emails.
     """
     
         """
         self.service = PostmarkEmailService()
         self.fail_silently = fail_silently
-
-    def open(self):
-        """
-        Delegates opening the connection to the email provider
-        to the respective service
-        """
-        try:
-            self.service.open()
-        except Exception as e:
-            logger.error("Opening connection failed: %s", e)
-            if not self.fail_silently:
-                raise
-
-    def close(self):
-        """
-        Delegates closing the connection to the email provider
-        to the respective service
-        """
-        try:
-            self.service.close()
-        except Exception as e:
-            logger.error("Closing connection failed: %s", e)
-            if not self.fail_silently:
-                raise
-
-    def send_messages(self,messages):
-        """
-        Sends the messages using the critsend email service
-        
-        Arguments:
-        - `messages`: The list of EmailMessage instances to send
-        """
-        try:
-            return self.service.send_messages(messages)
-        except Exception as e:
-            logger.error("Sending email messages failed: %s" % e)
-            if not self.fail_silently:
-                raise
-        
-

File email_services/services/__init__.py

 from email_services.services.critsend import CritsendEmailService
 from email_services.services.postmark import PostmarkEmailService
+from email_services.services.amazon_ses import AmazonSEService

File email_services/services/amazon_ses.py

+from boto.ses.connection import SESConnection
+from email_services import settings
+from email_services.services.base import EmailService
+
+class AmazonSEService(EmailService):
+
+    def __init__(self, *args, **kwargs):
+        """
+        Initializes the Amazon SES email service.
+        """
+        self.connection = None
+        self.id = settings.EMAIL_SERVICES_CLIENT_ID
+        self.key = settings.EMAIL_SERVICES_CLIENT_KEY
+
+    def open(self):
+        """
+        Creates the connection that will interact with the Amazon API
+        using Boto.
+        """
+        if self.connection:
+            return
+
+        self.connection = SESConnection(aws_access_key_id=self.id,
+                                        aws_secret_access_key=self.key)
+
+    def close(self):
+        """
+        Creates the connection that will interact with the Amazon API
+        using Boto.
+        """
+        if not self.connection:
+            return
+
+        self.connection.close()
+        self.connection = None
+
+    def send_messages(self, email_messages):
+        """
+        Sends one or more email messages using throught amazon SES
+        using boto.
+        """
+        if not self.connection:
+            self.open()
+
+        for message in email_messages:
+            self.connection.send_raw_email(
+                source=message.from_email,
+                destinations=message.recipients(),
+                raw_message=message.message().as_string())

File email_services/services/base.py

+"""
+Base classes for the services implementation
+"""
+
 class EmailServiceError(Exception):
+    """
+    Base exception that the services should throw on error
+    """
     pass
 
 class EmailService(object):
+    """
+    Base service. Actual services subclass this one
+    in order to keep a consistent API. This could be
+    the actual backend but to keep the backend api close
+    to the one provided by django the choice was to separate
+    the differences and name the whole thing a service.
+
+    In the base class all the methods throw a NotImplementedError
+    """
 
     def open(self):
+        """
+        If the service is not initialized this is where the
+        service initialization should be implemented
+        """
         raise NotImplementedError
 
     def close(self):
+        """
+        If the service is initialized this should close it
+        making it unavailable.
+        """
         raise NotImplementedError
 
     def send_messages(self, messages):
+        """
+        Sends a list of EmailMessage (or EmailMultiAlternatives)
+        instances that represent email messages using the service
+        API
+        """
         raise NotImplementedError
             
             

File email_services/services/critsend.py

+"""
+Email service using Critsend's SOAP API
+"""
+
 import logging
 import random
 import datetime

File email_services/services/postmark.py

+"""
+Email service using Postmarks's REST API
+"""
+
 from restkit import Resource
 
 try:
         
         
 class PostmarkEmailService(EmailService):
+    """
+    Interaction with the Postmark REST API.
+    """
     
     def __init__(self, *args, **kwargs):
         self.resource = None
 
     def open(self):
+        """
+        Sets up the connection to Postmark
+        """
         auth = PostmarkAuth()
         self.resource = PostmarkResource(filters = [auth]) 
 
     def close(self):
+        """
+        Removes the connection to postmark
+        """
         self.resource = None
 
     def send_messages(self, messages):
+        """
+        Sends a  list of messages using the postmark api
+        """
         if not self.resource:
             self.open()
             

File email_services/tests/__init__.py

-from email_services.tests.base import EmailServiceTest
-from email_services.tests.critsend import ( CritsendBackendTest,
-                                            CritsendServiceTest )
-from email_services.tests.postmark import ( PostmarkBackendTest,
-                                            PostmarkServiceTest,
-                                            PostmarkAuthTest,
-                                            PostmarkResourceTest, )
+from email_services.tests.base import (EmailServiceTest,
+                                       BaseBackendTest,)
+from email_services.tests.critsend import (CritsendBackendTest,
+                                           CritsendServiceTest,)
+from email_services.tests.postmark import (PostmarkBackendTest,
+                                           PostmarkServiceTest,
+                                           PostmarkAuthTest,
+                                           PostmarkResourceTest,)
+from email_services.tests.amazon_ses import (AmazonSESBackendTest,
+                                             AmazonSEServiceTest,)

File email_services/tests/amazon_ses.py

+"""
+Tests for the email_services django application
+"""
+import os
+
+import mock
+
+from django.test import TestCase
+from django.core.mail.message import EmailMessage
+
+from boto.ses.connection import SESConnection
+
+from email_services import settings
+from email_services.backends import AmazonSESBackend
+from email_services.services import AmazonSEService
+
+class AmazonSESBackendTest(TestCase):
+
+    def setUp(self):
+        from email_services.services import AmazonSEService
+        self.backend = AmazonSESBackend()
+        self.backend.service = mock.Mock(spec=AmazonSEService)
+
+    def test_connection_delegated_to_service(self):
+        """
+        When opening a connection the action is delegated to service
+        """
+        self.backend.open()
+        self.assertTrue(self.backend.service.open.called)
+
+    def test_connection_closing_delegated_to_service(self):
+        """
+        When closing a connection the action is delegated to service
+        """
+        self.backend.close()
+        self.assertTrue(self.backend.service.close.called)
+
+    def test_connection_exception_handling_on_open(self):
+        """
+        When an exception is raised while connecting it is reraised be the backend
+        in the default non silent mode
+        """
+        self.backend.service.open.side_effect = Exception("Raised something arbitrary")
+        with self.assertRaises(Exception) as e:
+            self.backend.open()
+        
+    def test_called_service_send(self):
+        """
+        When sending email from the backend the service call method is called
+        """
+        messages = []
+        self.backend.send_messages(messages)
+        self.assertTrue(self.backend.service.send_messages.called)
+
+    def test_connection_exception_handling_on_close(self):
+        """
+        When an exception is raised while closing it is reraised be the backend
+        in the default non silent mode
+        """
+        self.backend.service.close.side_effect = Exception("Raised something arbitrary")
+        with self.assertRaises(Exception) as e:
+            self.backend.close()
+
+    def test_connection_exception_handling_on_send(self):
+        """
+        When an exception is raised while sending it is reraised be the backend
+        in the default non silent mode
+        """
+        self.backend.service.send_messages.side_effect = Exception("Raised something arbitrary")
+        with self.assertRaises(Exception) as e:
+            self.backend.send_messages([])
+
+
+class  AmazonSEServiceTest(TestCase):
+
+    def setUp(self):
+        self.fake_user = "QWERTY"
+        self.fake_key = "QWERTY12345"
+        self.default_user = settings.EMAIL_SERVICES_CLIENT_ID
+        self.default_key = settings.EMAIL_SERVICES_CLIENT_KEY
+        settings.EMAIL_SERVICES_CLIENT_ID = self.fake_user
+        settings.EMAIL_SERVICES_CLIENT_KEY = self.fake_key
+
+        self.service = AmazonSEService()
+
+    def tearDown(self):
+        settings.EMAIL_SERVICES_CLIENT_ID = self.default_user
+        settings.EMAIL_SERVICES_CLIENT_KEY = self.default_key
+
+    def test_connection_open(self):
+        self.service.open()
+        self.assertTrue(self.service.connection)
+
+        # Check that it is not recreated
+        client = self.service.connection
+        self.service.open()
+        self.assertTrue(client is self.service.connection)
+
+    def test_connection_close(self):
+        self.service.close()
+        self.assertFalse(self.service.connection)
+
+        self.service.connection = mock.Mock(spec=SESConnection)
+        self.service.close()
+        self.assertFalse(self.service.connection)
+
+
+    def test_message_sending(self):
+        self.service.open = mock.Mock()
+
+        self.service.send_messages([])
+        self.assertTrue(self.service.open.called)
+
+        messages = [EmailMessage()]
+        self.service.connection = mock.Mock(spec=SESConnection)
+        self.service.send_messages(messages)
+        self.assertTrue(self.service.connection.send_raw_email.called)

File email_services/tests/base.py

+import mock
+
 from django.test import TestCase
+
+from email_services.backends.base import BaseServiceEmailBackend
 from email_services.services.base import EmailService
 
+class BaseBackendTest(TestCase):
+
+    def setUp(self):
+        self.backend = BaseServiceEmailBackend()
+        self.backend.service = mock.Mock()
+
+    def test_connection_delegated_to_service(self):
+        """
+        When opening a connection the action is delegated to service
+        """
+        self.backend.open()
+        self.assertTrue(self.backend.service.open.called)
+
+    def test_connection_closing_delegated_to_service(self):
+        """
+        When closing a connection the action is delegated to service
+        """
+        self.backend.close()
+        self.assertTrue(self.backend.service.close.called)
+
+    def test_connection_exception_handling_on_open(self):
+        """
+        When an exception is raised while connecting it is reraised be the backend
+        in the default non silent mode
+        """
+        self.backend.service.open.side_effect = Exception("Raised something arbitrary")
+        with self.assertRaises(Exception) as e:
+            self.backend.open()
+
+    def test_called_service_send(self):
+        """
+        When sending email from the backend the service call method is called
+        """
+        messages = []
+        self.backend.send_messages(messages)
+        self.assertTrue(self.backend.service.send_messages.called)
+
+    def test_connection_exception_handling_on_close(self):
+        """
+        When an exception is raised while closing it is reraised be the backend
+        in the default non silent mode
+        """
+        self.backend.service.close.side_effect = Exception("Raised something arbitrary")
+        with self.assertRaises(Exception) as e:
+            self.backend.close()
+
+    def test_connection_exception_handling_on_send(self):
+        """
+        When an exception is raised while sending it is reraised be the backend
+        in the default non silent mode
+        """
+        self.backend.service.send_messages.side_effect = Exception("Raised something arbitrary")
+        with self.assertRaises(Exception) as e:
+            self.backend.send_messages([])
+
 class EmailServiceTest(TestCase):
     
     def setUp(self):
 
       author='Yorgos Pagles',
       author_email='yorgos@pagles.org',
-      url='http://pagles.org',
+      url='http://django-email-services.readthedocs.org/',
 
       license='BSD License',
       packages=find_packages(exclude=['test_project']),
       include_package_data=True,
       zip_safe=False,
       install_requires=['suds',
-                        'restkit'])
+                        'restkit',
+                        'boto'])