What is WebDAV?
WebDAV stands for "Web-based Distributed Authoring and Versioning".
It's a set of extensions to the HTTP protocol which allows users to collaboratively edit and manage files on remote web servers.
It can also be used to access mail on exchange server. I used it for this.
It great for accessing Exchange because all you need is an XML body sent over HTTP. No installing of anything.
How it works?
WebDAV works with XML. It sends xml requests to the server and gets the responses in return.
It uses MSXML or
System.Net.HttpWebRequest in .NET.
WebDAV has some extended keywords besides the standard HTTP ones that do it's bidding :)
Which they are and how are they formed and used can be found on
this MSDN site
so i won't go into that.
The type of Request we're making (
PROPPATCH,
MOVE,
PROPFIND, ...) is specified in the HttpWebRequest's Method Property XML
PROPPATCH request is created like this:
<?xml version=\"1.0\"?>
<a:propertyupdate xmlns:a=\"DAV:\" xmlns:n0=\"http://schemas.microsoft.com/exchange/\" xmlns:n1=\"http://schemas.microsoft.com/mapi/proptag/\" xmlns:n2=\"urn:schemas:httpmail:\" xmlns:n3=\"urn:schemas:mailheader:\">
<a:set>
<a:prop>
<n0:outlookmessageclass>IPM.Note</n0:outlookmessageclass>
<n1:0x1000001E>Test Text</n1:0x1000001E>
<a:contentclass>urn:content-classes:message</a:contentclass>
<n2:subject>Test Subject</n2:subject>
<n3:to>valid@emailAddress.com</n3:to>
<n3:x-mailer>some mailer</n3:x-mailer>
</a:prop>
</a:set>
</a:propertyupdate>
Server response is like this:
<?xml version=\"1.0\"?>
<a:multistatus xmlns:a=\"DAV:\" xmlns:d=\"urn:schemas:httpmail:\" xmlns:e=\"urn:schemas:mailheader:\" xmlns:c=\"http://schemas.microsoft.com/mapi/proptag/\" xmlns:b=\"http://schemas.microsoft.com/exchange/\">
<a:response>
<a:href>http://servername/Exchange/username/Drafts/Test%20Subject.eml</a:href>
<a:status>HTTP/1.1 201 Created</a:status>
<a:propstat>
<a:status>HTTP/1.1 200 OK</a:status>
<a:prop>
<b:outlookmessageclass></b:outlookmessageclass>
<c:0x1000001E></c:0x1000001E>
<a:contentclass></a:contentclass>
<d:subject></d:subject>
<e:to></e:to>
<e:x-mailer></e:x-mailer>
</a:prop>
</a:propstat>
</a:response>
</a:multistatus>
Each Property can be saved or removed this way. Custom properties can also be added.
Exchange also has a special folder called
##DavMailSubmissionURI## to which the item must be moved to get sent.
Life of an item in WebDAV generation process and problems that arise
Here i'll explain steps that must be done to each item to get it to work properly. Request methods to use are in CAPS letters.
1. Email message
-
PROPPATCH the xml request to drafts folder.
- Attach any Attachment with
PUT Request
-
MOVE the item from Drafts to ##DavMailSubmissionURI##
2. Appointment
-
PROPPATCH the apponitment in the calendar folder
3. Meeting Requests
- Create an appointment in your calendar folder (point 2)
-
COPY the Appointment in your calendar folder with a differnet URI
- set the
http://schemas.microsoft.com/exchange/outlookmessageclass property to
"IPM.Schedule.Meeting.Request"
- set the
DAV:contentclass property to
"urn:content-classes:calendarmessage"
- set the urn:schemas:calendar:method property to "REQUEST"
-
PROPPATCH (save) the meeting requests properties
- Attach any Attachment with
PUT Request
-
MOVE the item to ##DavMailSubmissionURI##
4. Contacts, tasks and others
-
PROPPATCH the request to proper folder
- Attach any Attachment with
PUT Request
-
MOVE the item from Drafts to ##DavMailSubmissionURI## if needed to send
Development problems and bugs
1. Property naming
A lot of
http://schemas.microsoft.com/mapi/proptag/ and
http://schemas.microsoft.com/mapi/id/ properties start with
'0x'.
If you're using XmlDocument to store xml data like I do then you know that it doesn't allow tags to start with
'0'.
Luckily replacing
0x... with
x... when loading the document fixes this.
I load the XmlDocument with the xml returned from the server.
I bulid the XML request that gets sent to the server by hand. That's because http://schemas.microsoft.com/mapi/id/
properties need to start with 0x to get set properly while
http://schemas.microsoft.com/mapi/proptag/ don't.
2. Case sensitivity
All properties
ARE CASE SENSITIVE. all properties can be found on
this MSDN site
3. Getting associated appointment from a meeting request reply
I spent a lot of time trying to figue out how to get associated appointment from a meeting request reply.
Since a meeting request is acctually based on an appointment we can deny, accept it or set it to tentative.
Now when this reply is returned to us it is basicaly a message and not an appintment.
It's
DAV:contentclass is
"urn:content-classes:calendarmessage" and its
http://schemas.microsoft.com/exchange/outlookmessageclass
is one of the following:
"IPM.Schedule.Meeting.Resp.Pos", "
IPM.Schedule.Meeting.Resp.Tent" or "
IPM.Schedule.Meeting.Resp.Neg"
The only property that gets preserved is:
http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x23
which i've named AppointmentAndMeetingRequestResponseConnector :))
This is a base64 encoded property.
4. Appointment Reccurence
I used this document:
http://www.ietf.org/rfc/rfc2445.txt
paragraf "4.3.10 Recurrence Rule" and "4.8.5.4 Recurrence Rule"
This is a multivalued property so it's property must be set in the following way:
<c:rrule><r:v xmlns:r=\"xml:\">Recurrencerule Here</r:v></c:rrule>
where
"c:" is a prefix for
"urn:schemas:calendar:" namespace.
5. Mail body
We have
urn:schemas:httpmail:htmldescription and
urn:schemas:httpmail:textdescription properties
but there's no property named "Message Body" or something similar.
This property is it:
http://schemas.microsoft.com/mapi/proptag/0x1000001E
Search
Search with WebDAV is implemented with a SQL dialect. More info can be found on
this MSDN site.
Never use Select * because it returns only properties defaulted to the collection and not all of the collections proerties. It's also slower since it has to go look which properties are deafult for the collection. When using search we must cast some properties to proper types.
For example when we search the calendar to get associated appointment from a meeting request reply the
http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x23 must be cased to
"bin.base64"
XML request for this looks like this:
<?xml version=\"1.0\"?>
<a:searchrequest xmlns:a=\"DAV:\">
<a:sql>
SELECT \"DAV:contentclass\"
FROM SCOPE('SHALLOW TRAVERSAL OF \"http://servername/Exchange/username/Calendar/\")
WHERE \"http://schemas.microsoft.com/mapi/id/{6ED8DA90-450B-101B-98DA-00AA003F1305}/0x23\"
= CAST(\"BAAAAIIA4AB0xbcQGoLgCAAAAAAAAAAAAAAAAAAAAAAAAAAATQAAAHZDYWwtVWlkAQAAAENEMDAwMDAwOEI5NTExRDE4MkQ4MDBDMDRGQjE2MjVEOUI1MjY5M0U0MjVBRUU0OTkwMzcxMjE0Njk2NEZERjMA\" as \"bin.base64\")
</a:sql>
</a:searchrequest>
This requests response looks like this:
<?xml version=\"1.0\"?>
<a:multistatus xmlns:a=\"DAV:\" xmlns:d=\"urn:schemas-microsoft-com:office:office\" xmlns:c=\"xml:\" xmlns:b=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\">
<a:response>
<a:href>http://servername/Exchange/username/Calendar/Some%20Appointment.eml</a:href>
<a:propstat>
<a:status>HTTP/1.1 200 OK</a:status>
<a:prop>
<a:contentclass>urn:content-classes:appointment</a:contentclass>
</a:prop>
</a:propstat>
</a:response>
</a:multistatus>
Href property returns the associated appointment URI.
Well that's about it... :)
WebDAV is a cool protocol if you ask me and i like it.
It's very simple and powerfull to use but it's hard to find all of the little details.
You can also manipulate MS Office files with it.
Oracle and Adobe also support it and a few others like Apache, MS IIS, Subversion (open source control program)...
Code
This a class I use for XML request:
public class Request
{
#region Variables
private HttpWebRequest _HttpRequest;
private byte[] _HttpRequestBody;
#endregion
#region Constructors
/// <SUMMARY>
/// Init class
/// </SUMMARY>
/// <PARAM name="uri">Request URI</PARAM>
/// <PARAM name="body">Request body (byte array)</PARAM>
/// <PARAM name="methodName">Request method name</PARAM>
/// <PARAM name="contentType">Request Content type</PARAM>
/// <PARAM name="httpRequestHeaders">Request HTTP Headers</PARAM>
public Request(string uri, byte[] body, string methodName, string contentType, WebHeaderCollection httpRequestHeaders)
{
try
{
_HttpRequest = (System.Net.HttpWebRequest)HttpWebRequest.Create(uri);
_HttpRequest.Credentials = new NetworkCredential("userName", "password", "domain");
_HttpRequestBody = body;
_HttpRequest.Method = methodName;
_HttpRequest.Headers = (httpRequestHeaders ?? new WebHeaderCollection);
_HttpRequest.ContentType = contentType;
}
catch (Exception ex)
{
throw ex;
}
}
/// <SUMMARY>
/// Init class
/// </SUMMARY>
/// <PARAM name="uri">Request URI</PARAM>
/// <PARAM name="body">Request body (string)</PARAM>
/// <PARAM name="methodName">Request method name</PARAM>
/// <PARAM name="contentType">Request Content type</PARAM>
/// <PARAM name="httpRequestHeaders">Request HTTP Headers</PARAM>
public Request(string uri, string body, string methodName, string contentType, WebHeaderCollection httpRequestHeaders)
: this(uri, Encoding.UTF8.GetBytes(body), methodName, contentType, httpRequestHeaders)
{ }
#endregion
#region Methods
/// <SUMMARY>
/// Run request
/// </SUMMARY>
/// <RETURNS>XML document containting response XML</RETURNS>
public XmlDocument RunRequest()
{
XmlDocument xml = new XmlDocument();
try
{
_HttpRequest.ContentLength = _HttpRequestBody.Length;
Stream requestStream = _HttpRequest.GetRequestStream();
requestStream.Write(_HttpRequestBody, 0, _HttpRequestBody.Length);
requestStream.Close();
WebResponse httpResponse = (System.Net.HttpWebResponse)_HttpRequest.GetResponse();
// handles methods like move, etc that don't return anything
if (httpResponse.ContentLength == 0)
xml.LoadXml("<RESPONSE><ZEROCONTENTLENGTH><method>" + _HttpRequest.Method + "</method></ZEROCONTENTLENGTH></RESPONSE>");
else
{
Stream receiveStream = httpResponse.GetResponseStream();
StreamReader sr = new StreamReader(receiveStream, System.Text.Encoding.GetEncoding("utf-8"));
string xmlResult = sr.ReadToEnd();
// this is a very stupid exchange xml "bug"
// XmlDocument.LoadXml returns an error if the tag starts with 0 (i.e.: <dav:0x008a></>)
// so we must change the xml's :0x to :x Exchange doesn't care about it :)
xmlResult = xmlResult.Replace(":0x", ":x");
xml.LoadXml(xmlResult);
httpResponse.Close();
}
}
catch (Exception ex)
{
// A few of the most important and common error.
//
// 403 - Forbidden - This means there is not enough access to create this folder.
// 405 - Method not allowed - This can mean the user is overwriting a folder (among
// other things).
// 404 - Not found - This is often used to find out if something exists.
// 409 - Conflict - Happens when we want to to do something with an item that hasn't been
// proppatched yet. Usually happens when we create a message, we have to
// proppacth it and then add attachments.
// 505 - Server unavailable
xml.LoadXml("<RESPONSE><ERROR><method>" + _HttpRequest.Method + "</method><MESSAGE>" + ex.Message + "</MESSAGE></ERROR></RESPONSE>");
}
return xml;
}
/// <SUMMARY>
/// Adds range of rows to return
/// </SUMMARY>
/// <PARAM name="startRow">Start at row</PARAM>
/// <PARAM name="endRow">End at row</PARAM>
public void AddRange(int startRow, int endRow)
{
_HttpRequest.AddRange("rows", startRow, endRow);
}
#endregion
}
This is a class i use for creating a recurrence rule:
using System;
using System.Collections.Generic;
using System.Text;
namespace WebDAV
{
public class Recurrence
{
/*
Standard definition:
recur = "FREQ"=freq *(
; either UNTIL or COUNT may appear in a 'recur',
; but UNTIL and COUNT MUST NOT occur in the same 'recur'
( ";" "UNTIL" "=" enddate ) /
( ";" "COUNT" "=" 1*DIGIT ) /
; the rest of these keywords are optional,
; but MUST NOT occur more than once
( ";" "INTERVAL" "=" 1*DIGIT ) /
( ";" "BYSECOND" "=" byseclist ) /
( ";" "BYMINUTE" "=" byminlist ) /
( ";" "BYHOUR" "=" byhrlist ) /
( ";" "BYDAY" "=" bywdaylist ) /
( ";" "BYMONTHDAY" "=" bymodaylist ) /
( ";" "BYYEARDAY" "=" byyrdaylist ) /
( ";" "BYWEEKNO" "=" bywknolist ) /
( ";" "BYMONTH" "=" bymolist ) /
( ";" "BYSETPOS" "=" bysplist ) /
( ";" "WKST" "=" weekday ) /
*/
#region Range of Recurrence
private DateTime _RangeStartDate;
private DateTime _RangeEndDate;
private int _Occurrences;
private bool _NoEndDate;
#endregion
#region AppointmentTime
private DateTime _AppointmentStartDate;
private DateTime _AppointmentEndDate;
private DateTime _AppointmentDuration;
#endregion
#region Recurrence Pattern
private string _RecurrenceType;
private int _Interval;
private string _WeekStart;
private string _ByDay;
private string _ByMonthDay;
private string _ByYearDay;
private string _ByWeekNumber;
private string _ByMonth;
private int _BySetPos;
private string _RRule;
#endregion
public Recurrence()
{
_RangeStartDate = DateTime.MinValue;
_RangeEndDate = DateTime.MinValue;
_Occurrences = -1;
_NoEndDate = false;
_AppointmentStartDate = DateTime.MinValue;
_AppointmentEndDate = DateTime.MinValue;
_AppointmentDuration = DateTime.MinValue;
_RecurrenceType = Enums.RecurrenceType.Daily;
_Interval = -1;
_WeekStart = Enums.DayOfWeek.Sunday;
_ByDay = string.Empty;
_ByMonthDay = string.Empty;
_ByYearDay = string.Empty;
_ByWeekNumber = string.Empty;
_ByMonth = string.Empty;
_BySetPos = 0;
_RRule = string.Empty;
}
#region Properties
public DateTime RangeStartDate
{
get { return _RangeStartDate; }
set { _RangeStartDate = value; }
}
public DateTime RangeEndDate
{
get { return _RangeEndDate; }
set { _RangeEndDate = value; }
}
public int Occurrences
{
get { return _Occurrences; }
set { _Occurrences = value; }
}
public bool NoEndDate
{
get { return _NoEndDate; }
set { _NoEndDate = value; }
}
public DateTime AppointmentStartDate
{
get { return _AppointmentStartDate; }
set { _AppointmentStartDate = value; }
}
public DateTime AppointmentEndDate
{
get { return _AppointmentEndDate; }
set { _AppointmentEndDate = value; }
}
public DateTime AppointmentDuration
{
get { return _AppointmentDuration; }
set { _AppointmentDuration = value; }
}
public string RecurrenceType
{
get { return _RecurrenceType; }
set { _RecurrenceType = value; }
}
public int Interval
{
get { return _Interval; }
set { _Interval = value; }
}
public string WeekStart
{
get { return _WeekStart; }
set { _WeekStart = value; }
}
public int Position
{
get { return _BySetPos; }
set { _BySetPos = value; }
}
#endregion
#region Methods
public string GetRecurrenceRule()
{
_RRule = "RRULE:";
_RRule += "FREQ=" + _RecurrenceType;
_RRule += ";INTERVAL=" + _Interval;
if (_ByDay != string.Empty)
_RRule += ";BYDAY=" + _ByDay.TrimEnd(',');
if (_ByMonth != string.Empty)
_RRule += ";BYMONTH=" + _ByMonth.TrimEnd(',');
if (_ByMonthDay != string.Empty)
_RRule += ";BYMONTHDAY=" + _ByMonthDay.TrimEnd(',');
if (_ByYearDay != string.Empty)
_RRule += ";BYYEARDAY=" + _ByYearDay.TrimEnd(',');
if (_ByWeekNumber != string.Empty)
_RRule += ";BYWEEKNO=" + _ByWeekNumber.TrimEnd(',');
if (_BySetPos != 0)
_RRule += ";BYSETPOS=" + _BySetPos;
_RRule += ";WKST=" + _WeekStart;
if (!_NoEndDate)
{
if (_AppointmentEndDate > DateTime.MinValue)
_RRule += ";UNTIL=" + _AppointmentEndDate.ToUniversalTime().ToString("yyyyMMddTHHmmssZ");
else if (_Occurrences > 0)
_RRule += ";COUNT=" + _Occurrences;
}
return "<r:v xmlns:r=\"xml:\">" + _RRule.ToUpper() + "</r:v>";
}
public void AddDayOfMonth(int[] positionInMonth)
{
for (int i = 0; i < positionInMonth.Length; i++)
{
_ByMonthDay += positionInMonth[i].ToString() + ",";
}
}
public void AddDayOfYear(int[] positionInYear)
{
for (int i = 0; i < positionInYear.Length; i++)
{
_ByYearDay += positionInYear[i].ToString() + ",";
}
}
public void AddDayOfWeek(Enums.DayOfWeek dayOfWeek, int positionInWeek)
{
_ByDay += positionInWeek.ToString() + dayOfWeek + ",";
}
public void AddDayOfWeek(Enums.DayOfWeek dayOfWeek)
{
_ByDay += dayOfWeek + ",";
}
public void AddWeekOfYear(int[] positionInYear)
{
for (int i = 0; i < positionInYear.Length; i++)
{
_ByWeekNumber += positionInYear[i].ToString() + ",";
}
}
public void AddMonthOfYear(int[] positionInYear)
{
for (int i = 0; i < positionInYear.Length; i++)
{
_ByMonth += positionInYear[i].ToString() + ",";
}
}
#endregion
}
}
This is a request and response to get well known Exchange folder names since they are localized:
<?xml version="1.0" ?>
<a:propfind xmlns:a="DAV:" xmlns:n0="urn:schemas:httpmail:">
<a:prop>
<n0:calendar></n0:calendar>
<n0:contacts></n0:contacts>
<n0:deleteditems></n0:deleteditems>
<n0:drafts></n0:drafts>
<n0:inbox></n0:inbox>
<n0:journal></n0:journal>
<n0:notes></n0:notes>
<n0:outbox></n0:outbox>
<n0:sentitems></n0:sentitems>
<n0:tasks></n0:tasks>
</a:prop>
</a:propfind>
<?xml version=\"1.0\"?>
<a:multistatus xmlns:a=\"DAV:\" xmlns:d=\"urn:schemas:httpmail:\" xmlns:e=\"urn:schemas-microsoft-com:office:office\" xmlns:c=\"xml:\" xmlns:b=\"urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/\">
<a:response>
<a:href>http://servername/Exchange/username/</a:href>
<a:propstat>
<a:status>HTTP/1.1 200 OK</a:status>
<a:prop>
<d:calendar>http://servername/Exchange/username/Calendar</d:calendar>
<d:contacts>http://servername/Exchange/username/Contacts</d:contacts>
<d:deleteditems>http://servername/Exchange/username/Deleted%20Items</d:deleteditems>
<d:drafts>http://servername/Exchange/username/Drafts</d:drafts>
<d:inbox>http://servername/Exchange/username/Inbox</d:inbox>
<d:journal>http://servername/Exchange/username/Journal</d:journal>
<d:notes>http://servername/Exchange/username/Notes</d:notes>
<d:outbox>http://servername/Exchange/username/Outbox</d:outbox>
<d:sentitems>http://servername/Exchange/username/Sent%20Items</d:sentitems>
<d:tasks>http://servername/Exchange/username/Tasks</d:tasks>
</a:prop>
</a:propstat>
</a:response>
</a:multistatus>
|
|