Thursday, February 01, 2007

Creating a RSS feed of an Exchange 2007 Mailbox Folder using Exchange Web Services C# and Powershell

I’m a real fan of RSS it tends to by my preferred method of reading and aggregating information so I though I’d see how easy it would be to generate a RSS feed using the new Exchange Web Services in Exchange 2007. Fortunately the XmlTextWriter class in .NET makes creating XML very easy when compared with the old XMLDOM Com object. To create a feed of items from a folder using EWS you need to first use a FindItem request which will return a list of items in a folder I used a restriction so it would only return items that where less then 7 days old. One of the restictions with the FindItem operation as stated in the SDK “FindItem returns only the first 512 bytes of any streamable property. For Unicode, it returns the first 255 characters by using a null-terminated Unicode string. It does not return any of the message body formats or the recipient lists. FindItem will return a recipient summary. You can use the GetItem Operation to get the details of an item.” ref This means you would only be able to return a small percentage of body of a message which if you’re creating a summary feed is okay because this will be enough characters to give you a decent summary. I wanted a full feed so I went with using a separate GetItem Operation on each of the EntryIDs that are returned from the finditem request.

One of the other new things I’ve used in this Code is one of the cooler new features of Exchange Web Services which is Impersonation. Previously if you wanted to write code that was going to run against a mailbox that was being run under the security context of an account that wasn’t the owner of that mailbox that the account in question would need to be given rights to this mailbox eg via delegation or AD Users and Computers etc. With Impersonation you still have to grant rights for another account to access a mailbox other then its own but these rights are just specific to EWS impersonation so granting rights in this way means that the account can use EWS to access a mailbox but it cant use OWA or Outlook (or any other API). So from a security standpoint this is pretty desirable and gives you much more leverage in your applications (and keeps those auditors happy). The Exchange SDK has details on how to give an account Impersonation rights it involves granting two rights the first is on the Server which allows a user to submit impersonation calls and the second is either on the user account you wish to access itself or the Mailbox database if you wanted to access all mailboxes within a particular mail store see. Using Impersonation is relatively easy you can use the UPN, SID or SMTP address of the account you want to access I went for the SMTP address. The impersonation information is added to the SOAP header eg in the powershell script the following represents adding the SOAP header for impersation

+ "<soap:Header>"`
+ "<t:ExchangeImpersonation>" `
+ "<t:ConnectingSID>" `
+ "<t:PrimarySmtpAddress>" + $mbMailboxToAccess + "</t:PrimarySmtpAddress>" `
+ "</t:ConnectingSID>" `
+ "</t:ExchangeImpersonation>" `
+ "</soap:Header>" `
+ "<soap:Body>" `

Another thing to note in this script is in the Link node of each RSS item I put a link into so if you double clicked on the item it would open the original email in OWA. Now with Exchange 2003/2000 you could just use the Href value of the email in question and OWA would render the item okay. With OWA on 2007 this doesn’t work instead you need to use the EntryID of the Item which you can get from the FindItem request. The one thing I did find is that this itemID cant be used as is from the FindItem request and needed to be transposed a bit to make it work. I’m not sure if this will vary between servers or not and there’s no documentation on this type of thing so its really a best guess. What I used did work for me on multiple mailboxes on the same server.

I stared out writing this as a WebService in C# using the EWS proxy objects but I decided I’d much rather have it as a powershell script as it would be easy to schedule and run so I ended up creating a C# version and a Powershell version. Both versions require 5 variables to be set

$snServername = "Servername"

Self Explanatory
$unUserName = "Username"
$psPassword = "password"
$dnDomainName = "domain"


This is the Username and Password of the account you will be using to do the Impersonation. (You don’t have to hardcode them if you can use NTLM)


$mbMailboxToAccess = user@smtpdomain.com

This is the smtp address of the user you want to aggregate

The code aggregates the last 7 days worth or items in the inbox this could be changed to another folder other then the inbox (if you wanted) or the number of days to aggregate can be changed by modifying the following line.

+ "<t:Constant Value=`"" +
$datetimetoquery.ToUniversalTime().AddDays(-7).ToString("yyyy-MM-ddThh:mm:ssZ")
+ "`"/>"`

I’ve put a download of the C# code and powershell script here the script itself looks like.

function
GetItem($smSoapMessage){
$bdBodytext = ""
$WDRequest1 = [System.Net.WebRequest]::Create($strRootURI)
$WDRequest1.ContentType = "text/xml"
$WDRequest1.Headers.Add("Translate", "F")
$WDRequest1.Method = "Post"
$WDRequest1.Credentials = $cdUsrCredentials
$bytes1 = [System.Text.Encoding]::UTF8.GetBytes($smSoapMessage)
$WDRequest1.ContentLength = $bytes1.Length
$RequestStream1 = $WDRequest1.GetRequestStream()
[void]$RequestStream1.Write($bytes1, 0, $bytes1.Length)
[void]$RequestStream1.Close()
$WDResponse1 = $WDRequest1.GetResponse()
$ResponseStream1 = $WDResponse1.GetResponseStream()
$ResponseXmlDoc1 = new-object System.Xml.XmlDocument
$ResponseXmlDoc1.Load($ResponseStream1)
$tbBodyNodes = @($ResponseXmlDoc1.getElementsByTagName("t:Body"))
for($itemNums=0;$itemNums -lt $tbBodyNodes.Count;$itemNums++){
$bdBodytext = $tbBodyNodes[$itemNums].'#text'.ToString()
}
return $bdBodytext
}


$snServername = "servername"
$unUserName = "user"
$psPassword = "password"
$dnDomainName = "domain"
$mbMailboxToAccess = "user@smtpdomain.com"
$cdUsrCredentials = new-object System.Net.NetworkCredential($unUserName , $psPassword
, $dnDomainName)
$xsXmlFileName = "c:\feedname.xml"
[System.Reflection.Assembly]::LoadWithPartialName("System.Web") > $null
$xrXmlWritter = new-object
System.Xml.XmlTextWriter($xsXmlFileName,[System.Text.Encoding]::UTF8)
$xrXmlWritter.WriteStartDocument()
$xrXmlWritter.WriteStartElement("rss")
$xrXmlWritter.WriteAttributeString("version", "2.0")
$xrXmlWritter.WriteStartElement("channel")
$xrXmlWritter.WriteElementString("title", "Inbox Feed For " + $mbMailboxToAccess)
$xrXmlWritter.WriteElementString("link", "https://" + $snServerName + "/owa/")
$xrXmlWritter.WriteElementString("description", "Exchange Inbox Feed For" +
$mbMailboxToAccess)
$datetimetoquery = get-date
$smSoapMessage = "<?xml version='1.0' encoding='utf-8'?>" `
+ "<soap:Envelope xmlns:soap=`"http://schemas.xmlsoap.org/soap/envelope/`" " `
+ " xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`"
xmlns:xsd=`"http://www.w3.org/2001/XMLSchema`"" `
+ " xmlns:t=`"http://schemas.microsoft.com/exchange/services/2006/types`" >" `
+ "<soap:Header>" `
+ "<t:ExchangeImpersonation>" `
+ "<t:ConnectingSID>" `
+ "<t:PrimarySmtpAddress>" + $mbMailboxToAccess + "</t:PrimarySmtpAddress>" `
+ "</t:ConnectingSID>" `
+ "</t:ExchangeImpersonation>" `
+ "</soap:Header>" `
+ "<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>" `
+ "<AdditionalProperties
xmlns=""http://schemas.microsoft.com/exchange/services/2006/types"">" `
+ "<ExtendedFieldURI PropertyTag=""0x3FD9"" PropertyType=""String"" />" `
+ "<ExtendedFieldURI PropertyTag=""0x10F3"" PropertyType=""String"" />" `
+ "<ExtendedFieldURI PropertyTag=""0x0C1A"" PropertyType=""String"" />" `
+ "</AdditionalProperties>" `
+ "</ItemShape>" `
+ "<Restriction>" `
+ "<t:IsGreaterThanOrEqualTo>" `
+ "<t:FieldURI FieldURI=`"item:DateTimeSent`"/>"`
+ "<t:FieldURIOrConstant>" `
+ "<t:Constant Value=`"" +
$datetimetoquery.ToUniversalTime().AddDays(-7).ToString("yyyy-MM-ddThh:mm:ssZ")
+ "`"/>"`
+ "</t:FieldURIOrConstant>"`
+ "</t:IsGreaterThanOrEqualTo>"`
+ "</Restriction>"`
+ "<ParentFolderIds>" `
+ "<t:DistinguishedFolderId Id=`"inbox`"/>" `
+ "</ParentFolderIds>" `
+ "</FindItem>" `
+ "</soap:Body></soap:Envelope>"

$strRootURI = "https://" + $snServername + "/ews/Exchange.asmx"
$WDRequest = [System.Net.WebRequest]::Create($strRootURI)
$WDRequest.ContentType = "text/xml"
$WDRequest.Headers.Add("Translate", "F")
$WDRequest.Method = "Post"
$WDRequest.Credentials = $cdUsrCredentials
$bytes = [System.Text.Encoding]::UTF8.GetBytes($smSoapMessage)
$WDRequest.ContentLength = $bytes.Length
$RequestStream = $WDRequest.GetRequestStream()
$RequestStream.Write($bytes, 0, $bytes.Length)
$RequestStream.Close()
$WDResponse = $WDRequest.GetResponse()
$ResponseStream = $WDResponse.GetResponseStream()
$ResponseXmlDoc = new-object System.Xml.XmlDocument
$ResponseXmlDoc.Load($ResponseStream)
$subjectnodes = @($ResponseXmlDoc.getElementsByTagName("t:Subject"))
$FromNodes = @($ResponseXmlDoc.getElementsByTagName("t:Name"))
$SentNodes = @($ResponseXmlDoc.getElementsByTagName("t:DateTimeSent"))
$SizeNodes = @($ResponseXmlDoc.getElementsByTagName("t:Size"))
$IDNodes = @($ResponseXmlDoc.getElementsByTagName("t:ItemId"))
$dsDescription = @($ResponseXmlDoc.getElementsByTagName("t:Value"))
for($i=0;$i -lt $subjectnodes.Count;$i++){
$Senttime = [System.Convert]::ToDateTime($SentNodes[$i].'#text'.ToString())
$Senttime.ToString() + " " + $FromNodes[$i].'#text' + " " +
$subjectnodes[$i].'#text' + " " + $SizeNodes[$i].'#text'
$IdNodeID = $IDNodes[$i].GetAttributeNode("Id")
$ckChangeKey = $IDNodes[$i].GetAttributeNode("ChangeKey")
$smSoapMessage = "<?xml version='1.0' encoding='utf-8'?>" `
+ "<soap:Envelope xmlns:soap=`"http://schemas.xmlsoap.org/soap/envelope/`" "
`
+ " xmlns:xsi=`"http://www.w3.org/2001/XMLSchema-instance`"
xmlns:xsd=`"http://www.w3.org/2001/XMLSchema`"" `
+ " xmlns:t=`"http://schemas.microsoft.com/exchange/services/2006/types`" >"
`
+ "<soap:Header>" `
+ "<t:ExchangeImpersonation>" `
+ "<t:ConnectingSID>" `
+ "<t:PrimarySmtpAddress>" + $mbMailboxToAccess + "</t:PrimarySmtpAddress>"
`
+ "</t:ConnectingSID>" `
+ "</t:ExchangeImpersonation>" `
+ "</soap:Header>" `
+ "<soap:Body><GetItem
xmlns=`"http://schemas.microsoft.com/exchange/services/2006/messages`"><ItemShape>"
`
+ "<BaseShape
xmlns=`"http://schemas.microsoft.com/exchange/services/2006/types`">Default</BaseShape></ItemShape>"
`
+ "<ItemIds><ItemId Id=`"" + $IdNodeID.'#text' + "`"" `
+ " xmlns=`"http://schemas.microsoft.com/exchange/services/2006/types`"
/></ItemIds></GetItem></soap:Body>" `
+ "</soap:Envelope>"
$xrXmlWritter.WriteStartElement("item")
$xrXmlWritter.WriteElementString("title", $subjectnodes[$i].'#text')
$xrXmlWritter.WriteElementString("link", "https://" + $snServername +
"/owa/?ae=Item&t=IPM.Note&id=Rg" +
[System.Web.HttpUtility]::UrlEncode($IdNodeID.'#text').Substring(58).Replace("%3d","J"))
$xrXmlWritter.WriteElementString("author", $FromNodes[$i].'#text')
$xrXmlWritter.WriteStartElement("description")
$xrXmlWritter.WriteRaw("<![CDATA[")
$bdBodytext = GetItem($smSoapMessage)
$xrXmlWritter.WriteRaw($bdBodytext)
$xrXmlWritter.WriteRaw("]]>")
$xrXmlWritter.WriteEndElement()
$xrXmlWritter.WriteElementString("pubDate", $Senttime.ToString("r"))
$xrXmlWritter.WriteElementString("guid", $IdNodeID.'#text')
$xrXmlWritter.WriteEndElement()

}
$xrXmlWritter.WriteEndElement()
$xrXmlWritter.WriteEndElement()
$xrXmlWritter.WriteEndDocument()
$xrXmlWritter.Close()
"Done"