Commits

Yorgos Pagles committed 519422a

* Sending with critsend backend

Comments (0)

Files changed (7)

-Email services for django providing various backends and respective statistics. Non even alpha, stay away from this
+Email services for django providing various backends and respective statistics. Still very alpha.
+
+Current backends
+
+* Critsend
+
+For the tests django_nose is used and coverage reports are generated. The aim is to keep the test coverage really high. Unfortunately some tests perform actual network connections to the respective services.
+
+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	
+	In [1]: from django.conf import settings
+	In [2]: from django.core.mail import send_mail
+	In [3]: settings.EMAIL_SERVICES_CLIENT_ID = 'your user id depending on service'
+	In [4]: settings.EMAIL_SERVICES_CLIENT_KEY = 'your key depending on service'
+	In [5]: send_mail("subj", "body", "sender@example.com", ['recipient@example.com', "recipient@example.com"])
+	Out[5]: True
+

email_services/backends/critsend.py

         Arguments:
         - `messages`: The list of EmailMessage instances to send
         """
-
         try:
             return self.service.send_messages(messages)
         except Exception as e:

email_services/services/critsend.py

 import datetime
 import hmac
 import hashlib
+from email.utils import parseaddr
 
 from suds import WebFault
 from suds.client import Client
              'http://mail14.messaging-master.com',
              'http://mail15.messaging-master.com',
              'http://mail16.messaging-master.com']
+
     wsdl = '/api_2.php?wsdl'
 
     def __init__(self, *args, **kwargs):
         self.client = None
         self.user = settings.EMAIL_SERVICES_CLIENT_ID
         self.key = settings.EMAIL_SERVICES_CLIENT_KEY
+        self.transport = HttpTransport(timeout=2400)
 
     def open(self):
         """
         if self.client:
             return
 
-        t = HttpTransport(timeout=2400)
         which = random.randint(0, len(self.hosts) - 1)
         host = self.hosts[which]
-        self.client = Client(host + self.wsdl, transport=t)
+        self.client = Client(host + self.wsdl, transport=self.transport)
 
     def close(self):
         """
         """
         Sends the email messages using the critsend api
         """
-        pass
+        
+        if not self.client:
+            self.open()
+            
+        for message in messages:
+            subject = message.subject
+            content = message.body
+            html_content = ''
+            
+            alternatives = getattr(message, "alternatives", [])
 
+            for alternative in alternatives:
+                if alternative[1] == "text/html":
+                    html_content = alternative[0]
+            
+            content_obj = self.client.factory.create('Content')
+
+            setattr(content_obj, "subject", subject)
+            setattr(content_obj, "text", content)
+            setattr(content_obj, "html", html_content)
+            
+            campaign_params = self.client.factory.create('CampaignParameters')
+            
+            setattr(campaign_params, "mailfrom_friendly", parseaddr(message.from_email)[0])
+            setattr(campaign_params, "mailfrom", parseaddr(message.from_email)[1])
+            if 'Reply-To' in message.extra_headers :
+                setattr(campaign_params, "replyto", parseaddr(message.extra_headers['Reply-To'])[1])
+            else:
+                setattr(campaign_params, "replyto", parseaddr(message.from_email)[1])
+                
+            emails = self.client.factory.create('ArrayEmail')
+            
+            for subscriber in message.to:
+                email = self._create_email(subscriber)
+                emails.Email.append(email)
+                
+            return self.client.service.sendCampaign(self.credentials, emails,\
+                                                    campaign_params, content_obj)
+
+    def _create_email(self, subscriber):
+        """
+        Generate an email entry respecting the wsdl of
+        the webservice.
+        """
+        
+        email = self.client.factory.create('Email')
+        email.email = parseaddr(subscriber)[1]
+        info_parts = parseaddr(subscriber)[0].split()
+        
+        for part_index in range(15):
+            field_name = ''.join(['field', str(part_index + 1)])
+            field_value = ''
+            if part_index < len(info_parts):
+                field_value = info_parts[part_index]
+                
+            setattr(email, field_name, field_value)
+            
+        return email
+    
     @property
     def credentials(self):
         """

email_services/tests.py

 Tests for the email_services django application
 """
 import urlparse
+import os
+import mock
 
-import mock
 from django.test import TestCase
+from django.core.mail import EmailMessage, EmailMultiAlternatives
 
 from email_services.backends import CritsendEmailBackend
 from email_services.services import CritsendEmailService
 from email_services import settings
-
+from django.conf import settings as djsettings
 
 class CritsendBackendTest(TestCase):
 
         self.backend = CritsendEmailBackend()
         self.backend.service = mock.Mock(spec=CritsendEmailService)
 
-
     def test_connection_delegated_to_service(self):
         """
         When opening a connection the action is delegated to service
         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  CritsendServiceTest(TestCase):
 
         settings.EMAIL_SERVICES_CLIENT_KEY = self.fake_key
         self.service = CritsendEmailService()
         
+        # Get the wsdl locally
+        self.service.wsdl = "%stest.wsdl" % os.path.sep
+        self.service.hosts = ["file://%s" % os.path.join(djsettings.PROJECT_PATH, 
+                                                         "resources")]
+        
     def tearDown(self):
         settings.EMAIL_SERVICES_CLIENT_ID = self.default_user
         settings.EMAIL_SERVICES_CLIENT_KEY = self.default_key
         
-    def test_creadentials(self):
+    def test_credentials(self):
+        """
+        Test that the credentials are created
+        """
         self.assertEquals(self.service.credentials['user'],
                           settings.EMAIL_SERVICES_CLIENT_ID)
         self.assertTrue(self.service.credentials['timestamp'])
         self.assertTrue(self.service.credentials['signature'])
         
     def test_connection_open(self):
+        """
+        Test that the service is opening correctly
+        """
         self.service.open()
-        u = urlparse.urlparse(self.service.client.wsdl.url)
-        self.assertTrue("%s://%s" % (u.scheme, u.netloc) in self.service.hosts)
-        self.assertEquals(u.path, self.service.wsdl.split('?')[0])
-        self.assertEquals(u.query, self.service.wsdl.split('?')[1])
+        url_parsed = urlparse.urlparse(self.service.client.wsdl.url)
+        
+        # We are using a local file so the host should be in path
+        # In online resources it would be different
+        self.assertTrue("%s://%s" % (url_parsed.scheme, 
+                                     os.path.dirname(url_parsed.path)) 
+                        in self.service.hosts)
+        
+        self.assertEquals(os.path.basename(url_parsed.path), 
+                          self.service.wsdl.strip(os.path.sep))
 
         # Check that it is not recreated
         client = self.service.client
         self.assertTrue(client is self.service.client)
         
     def test_connection_close(self):
+        """
+        Test that the service is closing correctly
+        """
         self.service.close()
         self.assertEquals(self.service.client, None)
 
+    def test_email_sending_content(self):
+        """
+        Test that email messages body and content are correctly sent using the
+        critsend api
+        """
+        self.service.client = mock.Mock()
+
+        content_obj = mock.Mock()
+        self.service.client.factory.create.return_value = content_obj
+
+        subject = 'hello'
+        text_content = 'This is an important message.'
+        html_content = '<p>This is an <strong>important</strong> message.</p>'
+
+        msg = EmailMessage(subject, text_content)
+
+        html_msg = EmailMultiAlternatives(subject, text_content)
+        html_msg.attach_alternative(html_content, "text/html")
+
+        messages = [msg,]
+        self.service.send_messages(messages)
+
+        self.assertEquals(content_obj.subject, subject)
+        self.assertEquals(content_obj.text, text_content)
+        self.assertEquals(content_obj.html, "")
+
+        content_obj = mock.Mock()
+        self.service.client.factory.create.return_value = content_obj
+
+        messages = [html_msg,]
+        self.service.send_messages(messages)
+
+        self.assertEquals(content_obj.subject, subject)
+        self.assertEquals(content_obj.text, text_content)
+        self.assertEquals(content_obj.html, html_content)
+        
+    def test_email_sending_parameters(self):
+        """
+        Test that email messages parameters are correctly set using the
+        critsend api
+        """
+
+        self.service.client = mock.Mock()
+
+        parameters_obj = mock.Mock()
+        self.service.client.factory.create.return_value = parameters_obj
+        
+        from_email = 'from@example.com'
+        from_name = 'example dude'
+        reply_to = "replyto@example.com"
+        
+        msg = EmailMessage(from_email = "%s<%s>" % (from_name, from_email),
+                           headers = {'Reply-To': reply_to})
+        
+        messages = [msg,]
+        self.service.send_messages(messages)
+        
+        self.assertEquals(parameters_obj.mailfrom, from_email)
+        self.assertEquals(parameters_obj.mailfrom_friendly, from_name)
+        self.assertEquals(parameters_obj.replyto, reply_to)
+        
+    def test_email_sending_subscribers(self):
+        """
+        Test that email messages recipients are correctly set using the
+        critsend api
+        """
+
+        self.service.client = mock.Mock()
+
+        parameters_obj = mock.Mock()
+        self.service.client.factory.create.return_value = parameters_obj
+        self.service._create_email = mock.Mock()
+        
+        subscribers = [
+            "foo bar<foobar@example.com>",
+            "spam eggs<spameggs@example.com>",
+        ]
+        
+        msg = EmailMessage(to = subscribers)
+        
+        messages = [msg,]
+        self.service.send_messages(messages)
+        
+        self.assertTrue(self.service._create_email.called)
+        self.assertTrue(self.service.client.service.sendCampaign.called)
+
+        
+    def test_email_creation(self):
+
+        self.service.client = mock.Mock()
+
+        email_obj = mock.Mock()
+        self.service.client.factory.create.return_value = email_obj
+        
+        email = 'foobar@example.com'
+        first_name = 'foo'
+        last_name = 'bar'
+        subscriber = "%s %s<%s>" % (first_name, last_name, email)
+        
+        mail = self.service._create_email(subscriber)        
+        
+        self.assertEquals(mail.email, email)
+        self.assertEquals(mail.field1, first_name)
+        self.assertEquals(mail.field2, last_name)
+        
+    def test_email_sending(self):
+        """
+        Test that email messages send is called
+        """
+
+        self.service.client = mock.Mock()
+
+        msg = EmailMessage()
+        
+        messages = [msg,]
+        self.service.send_messages(messages)
+        
+        self.assertTrue(self.service.client.service.sendCampaign.called)
+

test_project/requirements.txt

+Django
+South
+coverage
+distribute
+django-nose
+ipdb
+ipython
+mock
+nose
+suds

test_project/resources/test.wsdl

+<?xml version="1.0" encoding="ISO-8859-1"?>
+<definitions xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/" xmlns:tns="http://mxmaster.net/campaign/0.1" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://mxmaster.net/campaign/0.1">
+<types>
+<xsd:schema targetNamespace="http://mxmaster.net/campaign/0.1"
+>
+ <xsd:import namespace="http://schemas.xmlsoap.org/soap/encoding/" />
+ <xsd:import namespace="http://schemas.xmlsoap.org/wsdl/" />
+ <xsd:complexType name="Authentication">
+  <xsd:all>
+   <xsd:element name="user" type="xsd:string"/>
+   <xsd:element name="timestamp" type="xsd:dateTime"/>
+   <xsd:element name="signature" type="xsd:string"/>
+  </xsd:all>
+ </xsd:complexType>
+ <xsd:complexType name="CampaignParameters">
+  <xsd:all>
+   <xsd:element name="tag" type="tns:ArrayTag"/>
+   <xsd:element name="mailfrom" type="xsd:string"/>
+   <xsd:element name="mailfrom_friendly" type="xsd:string"/>
+   <xsd:element name="replyto" type="xsd:string"/>
+   <xsd:element name="replyto_filtered" type="xsd:boolean"/>
+  </xsd:all>
+ </xsd:complexType>
+ <xsd:complexType name="Content">
+  <xsd:all>
+   <xsd:element name="subject" type="xsd:string"/>
+   <xsd:element name="html" type="xsd:string"/>
+   <xsd:element name="text" type="xsd:string"/>
+  </xsd:all>
+ </xsd:complexType>
+ <xsd:complexType name="Subscribers">
+  <xsd:all>
+   <xsd:element name="descriptor" type="tns:Email"/>
+   <xsd:element name="database" type="tns:ArrayEmail"/>
+  </xsd:all>
+ </xsd:complexType>
+ <xsd:complexType name="Email">
+  <xsd:all>
+   <xsd:element name="email" type="xsd:string"/>
+   <xsd:element name="field1" type="xsd:string"/>
+   <xsd:element name="field2" type="xsd:string"/>
+   <xsd:element name="field3" type="xsd:string"/>
+   <xsd:element name="field4" type="xsd:string"/>
+   <xsd:element name="field5" type="xsd:string"/>
+   <xsd:element name="field6" type="xsd:string"/>
+   <xsd:element name="field7" type="xsd:string"/>
+   <xsd:element name="field8" type="xsd:string"/>
+   <xsd:element name="field9" type="xsd:string"/>
+   <xsd:element name="field10" type="xsd:string"/>
+   <xsd:element name="field11" type="xsd:string"/>
+   <xsd:element name="field12" type="xsd:string"/>
+   <xsd:element name="field13" type="xsd:string"/>
+   <xsd:element name="field14" type="xsd:string"/>
+   <xsd:element name="field15" type="xsd:string"/>
+  </xsd:all>
+ </xsd:complexType>
+ <xsd:complexType name="ArrayEmail">
+  <xsd:sequence>
+   <xsd:element name="Email" type="tns:Email" maxOccurs="unbounded"/>
+  </xsd:sequence>
+ </xsd:complexType>
+ <xsd:complexType name="ArrayTag">
+  <xsd:sequence>
+   <xsd:element name="Tag" type="xsd:string" maxOccurs="unbounded"/>
+  </xsd:sequence>
+ </xsd:complexType>
+</xsd:schema>
+</types>
+<message name="sendCampaignRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="subscribers" type="tns:ArrayEmail" />
+  <part name="parameters" type="tns:CampaignParameters" />
+  <part name="content" type="tns:Content" /></message>
+<message name="sendCampaignResponse">
+  <part name="return" type="xsd:boolean" /></message>
+<message name="sendEmailRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="subscribers" type="tns:ArrayEmail" />
+  <part name="parameters" type="tns:CampaignParameters" />
+  <part name="content" type="tns:Content" /></message>
+<message name="sendEmailResponse">
+  <part name="return" type="xsd:boolean" /></message>
+<message name="createTagRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="tag" type="xsd:string" /></message>
+<message name="createTagResponse">
+  <part name="return" type="xsd:boolean" /></message>
+<message name="resetTagRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="tag" type="xsd:string" /></message>
+<message name="resetTagResponse">
+  <part name="return" type="xsd:boolean" /></message>
+<message name="deleteTagRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="tag" type="xsd:string" /></message>
+<message name="deleteTagResponse">
+  <part name="return" type="xsd:boolean" /></message>
+<message name="listAllTagsRequest">
+  <part name="authentication" type="tns:Authentication" /></message>
+<message name="listAllTagsResponse">
+  <part name="return" type="tns:ArrayTag" /></message>
+<message name="isTagRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="tag" type="xsd:string" /></message>
+<message name="isTagResponse">
+  <part name="return" type="xsd:boolean" /></message>
+<message name="getTagRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="tag" type="xsd:string" /></message>
+<message name="getTagResponse">
+  <part name="return" type="xsd:string" /></message>
+<message name="getTagPaginatedRequest">
+  <part name="authentication" type="tns:Authentication" />
+  <part name="tag" type="xsd:string" />
+  <part name="start" type="xsd:int" />
+  <part name="offset" type="xsd:int" /></message>
+<message name="getTagPaginatedResponse">
+  <part name="return" type="xsd:string" /></message>
+<portType name="WSDL 0.1 for MxMasterPortType">
+  <operation name="sendCampaign">
+    <documentation>To send a campaign of email</documentation>
+    <input message="tns:sendCampaignRequest"/>
+    <output message="tns:sendCampaignResponse"/>
+  </operation>
+  <operation name="sendEmail">
+    <documentation>To send a small quantity of emails (&lt; 10)</documentation>
+    <input message="tns:sendEmailRequest"/>
+    <output message="tns:sendEmailResponse"/>
+  </operation>
+  <operation name="createTag">
+    <documentation>Idempotent operation</documentation>
+    <input message="tns:createTagRequest"/>
+    <output message="tns:createTagResponse"/>
+  </operation>
+  <operation name="resetTag">
+    <input message="tns:resetTagRequest"/>
+    <output message="tns:resetTagResponse"/>
+  </operation>
+  <operation name="deleteTag">
+    <documentation>Deprecated; please use resetTag</documentation>
+    <input message="tns:deleteTagRequest"/>
+    <output message="tns:deleteTagResponse"/>
+  </operation>
+  <operation name="listAllTags">
+    <input message="tns:listAllTagsRequest"/>
+    <output message="tns:listAllTagsResponse"/>
+  </operation>
+  <operation name="isTag">
+    <input message="tns:isTagRequest"/>
+    <output message="tns:isTagResponse"/>
+  </operation>
+  <operation name="getTag">
+    <input message="tns:getTagRequest"/>
+    <output message="tns:getTagResponse"/>
+  </operation>
+  <operation name="getTagPaginated">
+    <input message="tns:getTagPaginatedRequest"/>
+    <output message="tns:getTagPaginatedResponse"/>
+  </operation>
+</portType>
+<binding name="WSDL 0.1 for MxMasterBinding" type="tns:WSDL 0.1 for MxMasterPortType">
+  <soap:binding style="rpc" transport="http://schemas.xmlsoap.org/soap/http"/>
+  <operation name="sendCampaign">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="sendEmail">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="createTag">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="resetTag">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="deleteTag">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="listAllTags">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="isTag">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="getTag">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+  <operation name="getTagPaginated">
+    <soap:operation soapAction="http://mxmaster.net/campaign/0.1#doCampaign" style="rpc"/>
+    <input><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></input>
+    <output><soap:body use="literal" namespace="http://mxmaster.net/campaign/0.1"/></output>
+  </operation>
+</binding>
+<service name="WSDL 0.1 for MxMaster">
+  <port name="WSDL 0.1 for MxMasterPort" binding="tns:WSDL 0.1 for MxMasterBinding">
+    <soap:address location="http://mail1.messaging-master.com/api_2.php"/>
+  </port>
+</service>
+</definitions>

test_project/settings.py

+import os
+
 DEBUG = True
+
+PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
+
 TEMPLATE_DEBUG = DEBUG
 
 DATABASES = {
     'django.contrib.sites',
     'django.contrib.messages',
     'django.contrib.staticfiles',
+    'south',
     'email_services',
+    'django_nose'
 )
 
+EMAIL_BACKEND = 'email_services.backends.CritsendEmailBackend'
+
+TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
+
+NOSE_ARGS = ['--with-coverage', '--cover-package=email_services']
+