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"




9 comments:

Darren Elmslie said...

VERY cool Glen!

Could the RSS feed method be expanded to notify of Rules or DACLs set by a user on their mailbox as well?

Glen said...

Unfortunately there isn't yet any direct support from Rules or ACL's in EWS (which is a disappointment to me) hopefully they will include this in the future i consider both to be critical functionality. You can get the ACL in XML using a specific property but its is hard to decode. Rules because you can't access hidden items you have no chance of with EWS. The best API to do that type of notification with would be use CDO 1.2 and making use of the ACL.dll for getting access to ACL and Rule.dll to get access to rules. I wrote a snapshot application for doing the Exchange mailbox DACL's last month I might look at adapting that when i get time to try and see if it will do what you have suggested it sounds like an interesting thing to keep track off.

Cheers
Glen

Håkon said...

Glenn,

Do you know if this has changed in Exchange 2007 SP1?

I'm doing some reasearch for a project I'm about to start where I want to create AppointmentItems in the users private Calendar, but the user should not be allowed to delete the appointment.

Any tips as to how set the persmisson on a individual Item?

Glen said...

Permissions and delegation have been added in SP1 with EWS. In regards to what you want to do i don't believe its possible a user will always have full rights to their calendar. But you can create appointment on other users calendar easily generally its not such a big deal if a user deletes these and usually this level of control is desirable if they have mobile sync etc.

Anonymous said...

Hi Glen, I tried your script but I get 403 forbidden. the user/pass i assign it is for the account with access to the smtp address correct? I tried accessing the exchange.asmx through browser with the user/pass I gave it and it redirects me to the wsdl page so I believe access is fine. Is there a problem with the soap message? this schema doesn't seem to exist since i can't browse to it, is that a problem?
http://schemas.microsoft.com/exchange/services/2006/types

Here's the error:
Exception calling "GetResponse" with "0" argument(s): "The remote server returned an error: (403) Forbidden."
At D:\STUFF\Connections\Exchange\exchange 2007\rss\exrssfeedv2full.ps1:89 char:37
+ $WDResponse = $WDRequest.GetResponse( <<<< )
You cannot call a method on a null-valued expression.
At D:\STUFF\Connections\Exchange\exchange 2007\rss\exrssfeedv2full.ps1:90 char:48
+ $ResponseStream = $WDResponse.GetResponseStream( <<<< )
Exception calling "Load" with "1" argument(s): "Object reference not set to an instance of an object."
At D:\STUFF\Connections\Exchange\exchange 2007\rss\exrssfeedv2full.ps1:92 char:21
+ $ResponseXmlDoc.Load( <<<< $ResponseStream)

Glen said...

Try this instead http://gsexdev.blogspot.com/2008/07/creating-rss-feed-from-calendar-or.html This is a much better method that address a lot of issue that can occur if you just building the SOAP manually.

Cheers
Glen

Anonymous said...

u are THE man!!! the new dll works great, i love the powershell way with .net

you should work for microsoft and build this stuff into exchange.

Anonymous said...

Hi Glen, colleagues expressed disbelief, when I mentioned your article and suggested that you had come up with a process to generate RSS feed from within Exchange 2007. All have said 'surely you need a separate RSS Server? I passed on the link, but the disbelief hasn't abated. Please could you confirm I've understood your article correctly: Your feed method really doesn't need an RSS Server, just EWS and Powershell ! Thanks. B.

Glen said...

Yes you dont need a RSS server generating RSS feeds is pretty simple. If you combine this script with EWS notifcations you can have realtime generation whenever new items arrive. Its pretty simple an RSS server is overkill unless you have a large number of mailboxes.

Cheers
Glen