Friday 30 September 2011

Sending Email through Exchange 2010 EWS with VBScript

At work we need to monitor the backups for all of our clients.  To make life easier a colleague of mine put some scripts together to read all the backup emails from the various servers and put them into a report, to easily show which have failed.

This worked great until a couple of weeks ago when we migrated from Exchange 2003 to 2010 and it all stopped working.

Of course, the guy who put this all together has since left the company so it was down to me to work out what was going on and put it right.

The mailbox monitoring script was written in well commented VBScript so it was fairly easy to follow.  I turned on some 'debugging' (wscript.echo !) and found that the script was crashing when trying to parse long email subject lines (it breaks the subject line into fields to find the client name, job name, success or failure, etc.) because there were not as many fields as it was expecting.

Now there is no native way to read a mailbox in VBScript so this script uses a 3rd party ActiveX control called FreePop to connect to the mailbox and download the messages.

A little experimentation found that the Exchange server was line-wrapping the subject at 72 characters which effectively truncated it as far as FreePop was concerned, causing the problem.  At first I looked for some way to change this line-wrapping, but it seems to be hard-coded and immutable, so I then looked about for an alternative way to access a mailbox from VBScript.

Of course, I could have used a different language (Powershell sprung to mind) but then I would have had to re-write the whole thing.  Far preferable would be to simply replace the section of code that pulls the emails from the mailbox.

After a bit of Googling I discovered that Exchange 2010 exposes a web service (imaginatively called Exchange Web Services or EWS) that can be used to do this and much more.  The only problem is that all the examples I could find were written in other languages than VBScript.

Still, not one to be deterred, I fiddled around until I got some test code successfully reading a mailbox inbox.
This code manually creates a SOAP xml message, submits it to the web service and parses the response - don't worry if you have no idea of what these things are, copy and paste works a treat :)

Watch out for those pesky line-breaks...

' set some variables
servername = "myExchangeServer"
username = "DOMAIN\User"
password = "UserPassword"

' enumerate items in inbox
smsoapmessage  = "<?xml version='1.0' encoding='utf-8'?>" _
& "<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:t='http://schemas.microsoft.com/exchange/services/2006/types'>" _
& "  <soap:Body>"_
& "    <FindItem xmlns='http://schemas.microsoft.com/exchange/services/2006/messages' xmlns:t='http://schemas.microsoft.com/exchange/services/2006/types' Traversal='Shallow'>" _
& "      <ItemShape>" _
& "        <t:BaseShape>AllProperties</t:BaseShape>" _
& "      </ItemShape>" _
& "      <ParentFolderIds>" _
& "        <t:DistinguishedFolderId Id='inbox'/>" _
& "      </ParentFolderIds>" _
& "    </FindItem>" _
& "  </soap:Body>" _
& "</soap:Envelope>"

set req = createobject("microsoft.xmlhttp")
req.Open "post", "https://" & servername & "/ews/Exchange.asmx", False,username,password 
req.setRequestHeader "Content-Type", "text/xml"
req.setRequestHeader "translate", "F"
req.send smSoapMessage

if req.status = 200 then
  set oXMLDoc = req.responseXML
  set ResponseCode = oXMLDoc.getElementsByTagName("m:ResponseCode")
  if ResponseCode(0).childNodes(0).text = "NoError" then
    Set oXMLSubject = oXMLDoc.getElementsByTagName("t:Subject")
      For i = 0 To (oXMLSubject.length -1)
        set oSubject = oXMLSubject.nextNode
        WScript.echo i & vbTab & oSubject.text
      next
  else
    WScript.echo "Exchange server returned error: " & ResponseCode(0).childNodes(0).text
  end if
else
  WScript.echo "Failed to connect to Exchange server, error code: " & req.status
end if



When I first ran this I was getting a 200 for the request object which is a success but the Exchange server was returning "ErrorServerBusy".  This threw me for a while, but I then discovered another new feature in Exchange 2010 called Throttling Policies.

Throttling policies are designed to prevent malicious or unintentional large requests from affecting the performance of the server and are enabled by default in Exchange 2010

MSDN blog on Understanding Throttling Policies


Using the Exchange shell to run the get-ThrottlingPolicy shows you all the default settings.  In my case the problem was caused by the default EWSFindCountLimit being 1000 - the inbox I was querying had over 2000 items in it.

I could have changed the EWSFindCountLimit setting in either the default throttling policy or created a custom one just for this user, but as the only reason there was so many emails in the inbox was because the script had not been running for a while and this normally clears them out when it finishes, I just deleted all the old emails to get below a thousand.
Everything then worked beautifully.

Deleting the emails from the inbox after they have been processed is achieved by calling the EmptyFolder method with another SOAP request.  Here is the xml for the request, I'll leave the actual VBScript to do this as an exercise for the reader :)

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Header>
    <t:RequestServerVersion Version="Exchange2010_SP1" />
  </soap:Header>
  <soap:Body>
    <m:EmptyFolder DeleteType="HardDelete" DeleteSubFolders="false">
      <m:FolderIds>
        <t:DistinguishedFolderId Id='inbox'/>
      </m:FolderIds>
    </m:EmptyFolder>
  </soap:Body>
</soap:Envelope>

5 comments:

  1. Glad my original code was commented well enough for you to update it without too much difficulty ;)

    Did I mention I wrote that that script under GNU, do you care to submit your changes?

    Thanks

    ReplyDelete
  2. Under a GNU or under the GPL ?
    Of course I'm happy to contribute to the community
    :)

    ReplyDelete
  3. Great code snippet here. I am trying use this to do something similar to your task other than I need to retrieve the Message Body text as well. Any other tidbits you could share that might help in this endeavor?

    Bob

    ReplyDelete
  4. Hi Bob!

    Unfortunately that is not straight forward (which is why I didn't do it before...) but basically what you do is iterate over the ItemId elements that have been returned (instead of the Subject elements) and then send a GetItem SOAP message for each ItemId and get the Body element that is returned.

    An example of a GetItem SOAP message is in the EWS documentation on MSDN here: http://msdn.microsoft.com/en-us/library/exchange/aa566013(v=exchg.140).aspx

    Hopefully this will help - I know it might seem a bit confusing but it is difficult to explain well in a comment.

    I will try to write a follow up article detailing fully how to do this once I have had a chance to properly test.

    ReplyDelete
  5. Hi Jon,

    Thanks for the reply. This was the same direction I was looking. I have gotten the ItemID and the ChangeKey and formed the XML to retrieve the message. The Message Text however appears to be Base64 encoded (Guess here as I am actually a Network Geek that dabbles in a little scripting) so I am a bit lost once I have the message object as to how to pull the body text into a script readable reply.

    Bob

    ReplyDelete