Exchange Web Services, suds, and Python

This post is out of date. Please see this new post about Exchange 2010.

I’d quite like to be able to interact with the University’s Exchange 2007 instance (Nexus) programmatically using suds (a Python SOAP client) and Exchange Web Services (EWS), but it’s not straightforward to get it set up. After a couple of hours of debugging, I’ve worked out what was going wrong.

This walk-through uses Alex Koshelev’s EWS-specific fork of suds, which you can grab from BitBucket. You will need to apply the patch attached to this ticket, which we’ll explain later. For the final code, jump straight to the bottom.

Authentication

Exchange Web Services is a SOAP-based API for interacting with an Exchange instance. Its WSDL definition is generally available at http://exchange.example.org/EWS/Services.wsdl. In Oxford’s case, it is here.

Exchange requires you to authenticate before you can access the WSDL, which you can do with the following code:

from suds.transport.https import HttpAuthenticated
from suds.client import Client

transport = HttpAuthenticated(username='abcd0123',
                              password='secret')
client = Client("https://nexus.ox.ac.uk/EWS/Services.wsdl",
                transport=transport)

This works fine until you try to call a method:

print client.service.GetUserOofSettings()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    print client.service.GetUserOofSettings()
  File "/usr/lib/python2.7/site-packages/suds/client.py", line 537, in __call__
    return client.invoke(args, kwargs)
  File "/usr/lib/python2.7/site-packages/suds/client.py", line 597, in invoke
    result = self.send(msg)
  File "/usr/lib/python2.7/site-packages/suds/client.py", line 635, in send
    result = self.failed(binding, e)
  File "/usr/lib/python2.7/site-packages/suds/client.py", line 700, in failed
    raise Exception((status, reason))
Exception: (401, u'Unauthorized ( The server requires authorization to fulfill the request. Access to the Web server is denied. Contact the server administrator.  )')

But we’ve already given it credentials! By turning on debugging in urllib2 (hacking suds.transport.https.HttpAuthenticated to add a handler as explained by Jamie Grove) we realise that each request is being sent without authentication, receiving a 401, and then (for GET requests, as for the WSDL) being resent with authentication. However, when we POST our getUserOofSettings() request it doesn’t resend it, probably because it’s forbidden by the HTTP spec.

To get round this, we need to make sure the client sends an authenticated request the first time round by specifying the URL and realm explicitly. Let’s define our own transport, modelled on suds.transport.https.HttpAuthenticated:

import urllib2
from suds.transport.http import HttpTransport
from suds.client import Client

class Transport(HttpTransport):
    def __init__(self, **kwargs):
        realm, uri = kwargs.pop('realm'), kwargs.pop('uri')
        HttpTransport.__init__(self, **kwargs)
        self.handler = urllib2.HTTPBasicAuthHandler()
        self.handler.add_password(realm=realm,
                                  user=self.options.username,
                                  passwd=self.options.password,
                                  uri=uri)
        self.urlopener = urllib2.build_opener(self.handler)

transport = Transport(realm='nexus.ox.ac.uk',
                      uri='https://nexus.ox.ac.uk/',
                      username='abcd0123',
                      password='secret')
client = Client('https://nexus.ox.ac.uk/EWS/Services.wsdl',
                transport=transport)

Using this, we get the following much more promising response, complaining about a missing argument, not missing authentication:

suds.WebFault: Server raised fault: ‘The request failed schema validation: The element ‘GetUserOofSettingsRequest’ in namespace ‘http://schemas.microsoft.com/exchange/services/2006/messages’ has incomplete content. List of possible elements expected: ‘Mailbox’ in namespace ‘http://schemas.microsoft.com/exchange/services/2006/types’.’

Namespace issues

Without the patch mentioned earlier in the article, we might attempt the following:

address = client.factory.create('ns1:EmailAddress')
address.Address = 'firstname.lastname@unit.ox.ac.uk'

client.service.GetUserOofSettings(address)

However, this fails:

suds.WebFault: Server raised fault: ‘The request failed schema validation: The element ‘GetUserOofSettingsRequest’ in namespace ‘http://schemas.microsoft.com/exchange/services/2006/messages’ has invalid child element ‘Mailbox’ in namespace ‘http://schemas.microsoft.com/exchange/services/2006/messages’. List of possible elements expected: ‘Mailbox’ in namespace ‘http://schemas.microsoft.com/exchange/services/2006/types’.’

Configuring logging (import logging; logging.basicConfig(level=logging.INFO)) reveals that suds is sending the following:

<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://schemas.xmlsoap.org/soap/envelope/">
   <Header/>
   <Body>
      <GetUserOofSettingsRequest xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">
         <Mailbox>
            <Address xmlns="http://schemas.microsoft.com/exchange/services/2006/types">firstname.lastname@unit.ox.ac.uk</Address>
         </Mailbox>
      </GetUserOofSettingsRequest>
   </Body>
</Envelope>

It should look like the example in the EWS 2007 documentation, but it’s stuck Mailbox in the messages namespace, not the types one. Applying the patch mentioned above resolves this issue.

Summary

So, grab suds-ews, apply the patch, and use the following code:

import urllib2

from suds.client import Client
from suds.transport.http import HttpTransport

class Transport(HttpTransport):
    def __init__(self, **kwargs):
        realm, uri = kwargs.pop('realm'), kwargs.pop('uri')
        HttpTransport.__init__(self, **kwargs)
        self.handler = urllib2.HTTPBasicAuthHandler()
        self.handler.add_password(realm=realm,
                                  user=self.options.username,
                                  passwd=self.options.password,
                                  uri=uri)
        self.urlopener = urllib2.build_opener(self.handler)


transport = Transport(realm='nexus.ox.ac.uk',
                      uri='https://nexus.ox.ac.uk/',
                      username='abcd0123',
                      password='secret')
client = Client('https://nexus.ox.ac.uk/EWS/Services.wsdl',
                transport=transport)

address = client.factory.create('ns1:EmailAddress')
address.Address = 'firstname.lastname@unit.ox.ac.uk'

response = client.service.GetUserOofSettings(address)
print response

In my case this prints the following:

(reply){
   GetUserOofSettingsResponse = 
      (GetUserOofSettingsResponse){
         ResponseMessage = 
            (ResponseMessageType){
               _ResponseClass = "Success"
               ResponseCode = "NoError"
            }
         OofSettings = 
            (UserOofSettings){
               OofState = "Enabled"
               ExternalAudience = "None"
               Duration = 
                  (Duration){
                     StartTime = 2011-05-14 14:00:00
                     EndTime = 2011-05-15 14:00:00
                  }
               InternalReply = 
                  (ReplyBody){
                     Message = "[snip]"
                  }
               ExternalReply = 
                  (ReplyBody){
                     Message = None
                  }
            }
         AllowExternalOof = "All"
      }
 }

The internal reply fragment can be pulled out using response.GetUserOofSettingsResponse.OofSettings.InternalReply.Message.

And we’re done! You may find the API documentation useful when working out what methods are available to play with.

Posted in Exchange Web Services, Python, SharePoint and Exchange | Tagged , , , , , | 8 Comments

8 Responses to “Exchange Web Services, suds, and Python”

  1. […] previously mentioned hacking suds to use an urllib2 handler with debugging enabled. Modifying libraries systemwide, […]

  2. Tim Fernando says:

    Thanks for this, this is sure to be very useful for implementing exchange services inside of the Molly Project.

  3. Interesting article. I started to play with this a little bit, and noticed a couple of typos in your final code listing that I had to fix to get it to work that you may want to edit.

    “import urllib2” should be replaced with “import urllib2 as u2” since later on you are calling it that way.

    “client = suds.client.Client(‘https://nexus.ox.ac.uk/EWS/Services.wsdl’,” should have the “suds.client.” part removed since you are already calling “from suds.client import Client”.

    Once I changed those it would run.

  4. Matt says:

    It’s great ! Hope you write more instruction to work with EWS. Tks 🙂

  5. Toby says:

    What’s u2?

  6. Tim says:

    To get it to authenticate properly, I had to _also_ enable NLTM. Took me ages to figure out. If you can’t connect over SSL, that might be why. See here — https://code.google.com/p/python-ntlm/

  7. mike says:

    Hi, happen to know how I can set the timezone for requests with suds as described here?
    http://msdn.microsoft.com/en-us/library/dd633689%28v=exchg.80%29.aspx

    I’m not sure how to go about it and few seem knowledgeable about it.

  8. Alexander Dutton says:

    Hi Tames and Toby. Thanks for pointing that out; I’ve now edited the code samples. Sorry for taking so long to approve your posts; we get a lot of spam here and I’ve not sorted the wheat from the chaff in a little while.

Leave a Reply