Quick and Dirty UNIX Shell Scripting with EWS


One of the great advantages of Exchange Web Services (EWS) is that you can write applications on just about any platform that can make HTTP requests.  This characteristic of web services means that you can integrate EWS in to just about every application or environment you can imagine.  This aspect of web services was brought home to me the other day while using my Mac OS X laptop, which I have to test Microsoft Entourage Exchange Web Services Edition Beta against Exchange.  I wanted to quickly find the phone number for a colleague.  I knew I could find the information in Entourage, but I wanted to be able to get the information without leaving my terminal window, so I dug deep in my memory to my first programming project as a 10 year old on a Unix workstation writing shell scripts (yes, that is how my parents kept me occupied).


First, I needed a suitable utility to make an HTTP request, and preferably one that could perform Windows Integrated authentication, which is the default for EWS.  Curl is a great little tool for quickly issuing requests; it is supported cross-platform and has an NTLM authentication stack built in.  This cool post shows how to use it for building a Mac OSX Widget that works against EWS.  I extracted the necessary line and modified them for our use:






curl -s -u username@contoso.com:password -L https://servername/ews/exchange.asmx -d “$DATA” -H “Content-Type:text/xml”    ntlm


 


Here is a breakdown of the parameters and their function:


–s parameter tells curl to run silently


 -u allows me to pass UPN and password on the command line


-L allows us to specify the URL


-d allows me to specify the body of the request should come from a variable called $DATA


 -H allows me to set the content header which is required


 –ntlm allows me to specify that Curl should try ntlm auth (–anyauth doesn’t work)


The request is then customized for the alias I want by piping the XML for the resolve names request through sed so that I can replace the alias to resolve with the alias I want to resolve as an argument to the utility that I’m going to call resolvename.






DATA=$(sed ‘s/alias/’$1’/’ resolvenames.txt)


 


resolvenames.txt






<?xml version=”1.0″ encoding=”utf-8″?>


<soap:Envelope xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”


               xmlns:xsd=”http://www.w3.org/2001/XMLSchema”


               xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”


         xmlns:t=”http://schemas.microsoft.com/exchange/services/2006/types”>


  <soap:Body>


    <ResolveNames xmlns=”http://schemas.microsoft.com/exchange/services/2006/messages”               xmlns:t=”http://schemas.microsoft.com/exchange/services/2006/types”


                  ReturnFullContactData=”true”>


      <UnresolvedEntry>alias</UnresolvedEntry>


    </ResolveNames>


  </soap:Body>


</soap:Envelope>


 


Getting this far is pretty easy and you are left with a simple utility which takes in an alias as a parameter, formats the request with the alias, and makes the request.  You can now take this output and pipe it to some utility which can consume it, ideally one that can natively handle the output xml. 






<?xml version=1.0 encoding=utf-8?>


<s:Envelope xmlns:s=http://schemas.xmlsoap.org/soap/envelope/>


  <s:Header>


    <h:ServerVersionInfo MajorVersion=8 MinorVersion=1 MajorBuildNumber=200 MinorBuildNumber=1 xmlns:h=http://schemas.microsoft.com/exchange/services/2006/types xmlns=http://schemas.microsoft.com/exchange/services/2006/types xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd=http://www.w3.org/2001/XMLSchema/></s:Header><s:Body xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xmlns:xsd=http://www.w3.org/2001/XMLSchema>


      <m:ResolveNamesResponse xmlns:m=http://schemas.microsoft.com/exchange/services/2006/messages xmlns:t=http://schemas.microsoft.com/exchange/services/2006/types>


        <m:ResponseMessages>


          <m:ResolveNamesResponseMessage ResponseClass=Success>


            <m:ResponseCode>NoError</m:ResponseCode>


            <m:ResolutionSet TotalItemsInView=1 IncludesLastItemInRange=true><t:Resolution>


              <t:Mailbox>


                <t:Name>Jason Henderson</t:Name>


                <t:EmailAddress>jason@contoso.com</t:EmailAddress>


                <t:RoutingType>SMTP</t:RoutingType>


                <t:MailboxType>Mailbox</t:MailboxType>


              </t:Mailbox>


              <t:Contact>


                <t:Culture>en-US</t:Culture>


                <t:DisplayName>Jason Henderson</t:DisplayName>


                <t:GivenName>Jason</t:GivenName


                ><t:CompanyName>Contoso</t:CompanyName>


                <t:EmailAddresses>


                  <t:Entry Key=EmailAddress1>smtp:jason@contoso.com</t:Entry>


                    </t:EmailAddresses>


                <t:PhoneNumbers>


                  <t:Entry Key=BusinessPhone>555-5555</t:Entry></t:PhoneNumbers>


                <t:ContactSource>ActiveDirectory</t:ContactSource>


                <t:Department>Web Services</t:Department>


                <t:JobTitle>SENIOR PROGRAM MANAGER LEAD</t:JobTitle>


                <t:Manager>Bob Manager</t:Manager>


                <t:OfficeLocation>55/5555</t:OfficeLocation>


                <t:Surname>Henderson</t:Surname>


              </t:Contact></t:Resolution>


            </m:ResolutionSet>


          </m:ResolveNamesResponseMessage>


        </m:ResponseMessages>


      </m:ResolveNamesResponse>


    </s:Body>


</s:Envelope>


 


Since I did shell scripting before XML parsing was in high demand I don’t have any good XML parsing tools up my sleeve, just good old awk and sed.   After a couple of false starts I dropped a mail to my good friend John Muster, author of Unix Made Easy (always at my side when I do any kind of shell scripting) and asked for his help.  He quickly obliged with a quick and dirty XML parser using expr that we both admit is ugly and incomplete, but works well enough for parsing out office location, phone numbers, job titles, and any other standard field.  The complete listing of the utility I call resolvename is below.


resolvename






DATA=$(sed ‘s/alias/’$1’/’ resolvenames.txt)


curl -s -u jasonhen@contoso.com:password -L https://servername/ews/exchange.asmx -d “$DATA”  -H “Content-Type:text/xml” –ntlm | tr -d ‘<:>’ \


| tr ‘ ‘ ‘+’ >  /tmp/output$$


expr `cat /tmp/output$$` :  ‘.*’$2’\(.*\)/\t’$2 > /tmp/selection$$


tr + ‘ ‘  < /tmp/selection$$


rm /tmp/output$$ /tmp/selection$$


 


The usage for the utility is: resolvename <alias> <xmlnode>, so for example “resolvename jason JobTitle” returns  “Senior Program Manager Lead” or “resolvename jasonhen OfficeLocation” returns my building and office number.  Pretty slick if I do say so myself.


Warnings – This code is not even prototype quality.  Among numerous other shortcomings, no error codes are handled, and XML parsing only works properly with elements that occur once and have no attributes.  If I were going to write a production quality application, I’d probably choose a language like php, python, or perl which have nice xml parsing stacks—but for something quick and dirty that works anywhere that you have curl and a bash environment this fits the bill.


Comments (6)

  1. Microsoft Exchange Server 2007 performance tutorial Securing your Exchange Server 2007 journaling archives

  2. Andy says:

    Tiny typo – you’ve got an extra ‘t’ on the end of the link to the Mac OS Widget – it should be http://www.codeplex.com/EWSMacWidget

  3. JasonHen says:

    You are right, thanks for catching that.  I just updated the post to point to the correct URL.

  4. Phil says:

    I love your code example, it’s the only I found on the entire Net that show how to use EWS without the auto-generated proxy and without coding in .net (for a service app it’s a must imo).

    (Im a old school guy that like to do it the hard way, but with a result of a speedier code and to learn how thing work in behind at the same time)

  5. mjc says:

    This is great info, but it's not working with an Exchange 2010 sp1 server. Is there something that needs to be changed on either end?

    Thanks!