Introduction
RSS is the standard format for feeds. There are hundreds of readers and aggregators for every platform. WebDAV, on the other side, is an obscure and poorly documented/supported protocol.
The purpose of this article is to create an adapter that allows users to access a Microsoft Exchange folder exposed through WebDAV using an RSS reader.
We're going to use IHttpHandler
s, XSLT, WebDAV, and HTTP Authentication, all of which are described in the Background section.
Using the Code
Setting up the WebDAVAdapter is easy:
- Create an empty application in IIS
- Configure it to use ASP.NET 2.0 and anonymous access
- Copy the source files to the virtual directory
- Point your RSS reader to http://WEBSERVER/VDIR/GetFeed.ashx?URL=http(s)://EXCHANGESERVER/Exchange/USER/FOLDER/
Background
About IHttpHandler
We've had HTTP Handlers for a lot of years. Those .asp "pages" were simply that: classes that take an HTTP request, process it without any inherited structure, and output text and headers.
ASP.NET .aspx pages are more complex classes that extend IHttpHandler
and add abstractions to the processing model in order to make it easier to develop event-driven web pages.
IHttpHandler
provides just two methods:
public interface IHttpHandler
{
bool IsReusable { get; }
void ProcessRequest(HttpContext context);
}
IsReusable
should return true
if another request can use the IHttpHandler
instance. ProcessRequest
contains all of our code.
About WebDAV
WebDAV (Web-based Distributed Authoring and Versioning) is a set of extensions to the HTTP protocol that provide additional "verbs" besides the standard GET and POST. It is used by Outlook Web Access to provide a rich web interface.
The verb we'll be using in this module is SEARCH, which provides a SQL-like syntax to query folders.
About XSLT
XSLT (eXtensible Stylesheet Language Transformation) is a language for transforming XML documents into other XML documents. It's based on templates that match nodes of the source document and generate markup on the target.
About HTTP Authentication
HTTP Authentication is described in RFC 2617. It describes a method for the server to require a set of credentials from the client.
To authenticate using the Basic scheme, the client must send an Authorization header containing:
Authorization: Basic user:pass
with user:pass encoded in Base64.
If the header is not present, or the credentials are invalid, a 401 (Unauthorized) status code is returned, and an Authenticate header like the following is added:
WWW-Authenticate: Basic realm="The Site"
When the browser finds this header, it usually presents the user with a dialog box to enter his username and password, and retries the request.
The Code
The first thing we need to do is create a request for the WebDAV folder URL:
string url = context.Request.QueryString["URL"];
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = "SEARCH";
request.ContentType = "text/xml";
Notice that the Method
(verb) is changed from the default GET to SEARCH. We also set ContentType
to let the server know we are sending XML in the request body.
Next, we parse the Authentication header and create a credential for the request:
string authorizationHeader = context.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authorizationHeader))
{
string userPassBase64 = authorizationHeader.Split(' ')[1];
byte[] userPassBytes = Convert.FromBase64String(userPassBase64);
string userPassString = Encoding.Default.GetString(userPassBytes);
string[] userPassArray = userPassString.Split(':');
request.Credentials = new NetworkCredential(userPassArray[0], userPassArray[1]);
}
We need a reference to the request stream:
using (Stream requestStream = request.GetRequestStream())
{
using (XmlTextWriter writer = new XmlTextWriter(requestStream, Encoding.UTF8))
And, we use it to write the SEARCH query:
writer.WriteStartDocument();
writer.WriteStartElement("searchrequest", "DAV:");
writer.WriteStartElement("sql", "DAV:");
writer.WriteString(string.Format(@"
SELECT
""urn:schemas:httpmail:subject"",
""urn:schemas:httpmail:fromname"",
""urn:schemas:httpmail:date"",
""urn:schemas:httpmail:htmldescription""
FROM SCOPE('Deep traversal of ""{0}""')
WHERE ""DAV:ishidden"" = False
", url));
writer.WriteEndDocument();
SCOPE('Deep traversal of "url"')
tells the server to also look for messages in subfolders. You can find a reference of the Exchange Store properties here and a description of the SEARCH method here.
And now, the most important part.
We get the response stream from the server and set a XmlReader
over it:
using (WebResponse response = request.GetResponse())
{
using (Stream responseStream = response.GetResponseStream())
{
using (XmlTextReader reader = new XmlTextReader(responseStream))
We load the transformation and create a parameter representing a link to the folder:
XslCompiledTransform transform = new XslCompiledTransform();
transform.Load(context.Request.MapPath("RSS.xsl"));
XsltArgumentList arguments = new XsltArgumentList();
arguments.AddParam("link", string.Empty, url);
And finally, we send the transformed XML to the client:
context.Response.ContentType = "text/xml";
transform.Transform(reader, arguments, context.Response.OutputStream);
Authorization might fail if the supplied credentials are not correct. We'll handle that case by wrapping GetResponse()
in a try-catch
block and requesting authentication from the client again:
catch (WebException ex)
{
if ((ex.Response as HttpWebResponse).StatusCode == HttpStatusCode.Unauthorized)
{
context.Response.StatusCode = 401;
context.Response.AppendHeader("WWW-Authenticate",
string.Format(@"Basic realm=""{0}""", url));
}
else
{
throw;
}
}
If there were no problems to this point, the request completes and your RSS reader will be displaying an item for each message in the folder.
The Transformation
The XSLT we're using is quite simple.
First, declare the stylesheet and the namespaces we're using:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:d="urn:schemas:httpmail:" xmlns:a="DAV:">
Add a parameter for the link URL. Parameters are useful, among other things, to include data that is not part of the input document.
<xsl:param name="link"/>
Next is the "root" template that will create the rss
element. The apply-templates
element will select each item (message) and pass it to the corresponding template.
<xsl:template match="/">
<rss version="2.0">
<channel>
<title>
<xsl:value-of select="$link" />
</title>
<link>
<xsl:value-of select="$link" />
</link>
<xsl:apply-templates select="a:multistatus/a:response" />
</channel>
</rss>
</xsl:template>
The matching template creates the RSS item
elements and their link
, and uses apply-templates
again for the rest of the properties:
<xsl:template match="a:response">
<item>
<link>
<xsl:value-of select="a:href/text()" />
</link>
<xsl:apply-templates select="a:propstat/a:prop" />
</item>
</xsl:template>
The last template completes the item
properties:
<xsl:template match="a:prop">
<title>
<xsl:value-of select="d:subject/text()" />
</title>
<author>
<xsl:value-of select="d:fromname/text()" />
</author>
<date>
<xsl:value-of select="d:date/text()" />
</date>
<description>
<xsl:value-of select="d:htmldescription/text()" />
</description>
</xsl:template>
Further Development (Homework!)
While this sample supports only RSS, it would be trivial to add, for example, Atom support. You would have to:
- Create a new Atom.xsl file
- Parametrize the loading of
XslCompiledTransform
(for example, sending the desired format in the QueryString
)
Once the transformation loading is parameterized, you'd only need to add new XSL files to support other formats.