|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionHotmail is probably the most popular e-mailing facility found on the web today. Millions of people around the world use it for everyday communication with friends and relatives. The main reason for it’s popularity is the fact that you can use the service absolutely free of charge. Users are able to read their e-mails daily using a web interface and there is a client program available, called Outlook Express, which most people will only use at home. Only these two methods are officially available to read mail from your Hotmail account. This is strange because Outlook Express is not the safest client to use for reading e-mails, especially the spam-sensitive Hotmail accounts. Secondly, web-mail is generally considered to be an annoyance, that is only there to offer access to your account from locations such as schools and public Internet access points. No longer! This article will enable you to build your own client, using a sure and solid way to communicate with Hotmail in the same way as Outlook does. It will be shown how the protocol can be used to your own advantage, it isn’t at all hard to understand either. To build a working client we will need to know about the way Outlook communicates with Hotmail. The protocol that is being used is called Now that the manner of communication is clear, there are some other things that need clarification. For instance; how do we authenticate ourselves, and how the heck are we going to implement such authentication? Usually this will mean calculating hashes and keeping passwords in a safe place. At first sight, this could seem like an enormous task to implement. It turned out to be not so difficult at all. After a review of the SourceForge article, a few things are noticeably interesting. The authentication is done via HTTP headers and is described in an RFC (2617). It’s even called HTTP authentication, there have got to be some classes in the .NET framework that are able to do that! To parse the response in a safe manner, we will make use of XPath, it should be up to the job and there are classes available for that too! Building the clientTo build the client, two components need to be created:
Instead of building a custom proxy, another method could be used. Microsoft ships a component called
Before reading the rest of this article, I assume that you have basic knowledge of the following subjects:
Please note that the code is fully documented in the source files. Hotmail proxyThe proxy will be responsible for making HTTP requests to the Hotmail servers. This will require various things to be implemented:
To send the requests and receive the responses, the class will make use of the Let’s start with building the class framework. public class HotmailProxy
{
private CookieContainer ccContainer;
public HotmailProxy ()
{
ccContainer = new CookieContainer();
}
}
Ok, not too interesting. Now let’s get on with building the really important part, building the method that actually sends the client’s request to a remote host. It will implement all the proxy’s requirements that are stated above! The method will need to accommodate the request as a byte array, the destination, and the necessary credentials for authentication (which can be private string SendRequestTo(byte[] requestBytes,
Uri destination, NetworkCredential credential)
The first thing that needs to be done is build the Implementation of the authentication process is done by adding the {
HttpWebRequest webRequest =
(HttpWebRequest)WebRequest.Create(destination);
webRequest.Method = "PROPFIND";
webRequest.Accept = "*/*";
webRequest.AllowAutoRedirect = false;
webRequest.UserAgent = "Mozilla/4.0 (compatible;" +
" MSIE 6.0; Windows NT 5.0; .NET CLR 1.0.3705)";
webRequest.CookieContainer = new CookieContainer();
webRequest.ContentLength = requestBytes.Length;
webRequest.ContentType = "text/xml";
webRequest.Credentials = credential;
webRequest.CookieContainer.Add(ccContainer.GetCookies(destination));
Now the request has been build, we will write the request to the requested host and try to get a response: try
{
Stream reqStream = webRequest.GetRequestStream();
reqStream.Write(requestBytes,0,requestBytes.Length);
reqStream.Close();
HttpWebResponse webResponse =
(HttpWebResponse)webRequest.GetResponse();
After verification that a response was received, cookies and redirection can be handled. Otherwise an exception will be thrown (build your own if (webRequest.HaveResponse)
{
// First handle cookies
foreach(Cookie retCookie in webResponse.Cookies)
{
bool cookieFound = false;
foreach(Cookie oldCookie in
ccContainer.GetCookies(destination))
{
if (retCookie.Name.Equals(oldCookie.Name))
{
oldCookie.Value = retCookie.Value;
cookieFound = true;
}
}
if (!cookieFound)
ccContainer.Add(retCookie);
}
// Next is redirection
if ((webResponse.StatusCode == HttpStatusCode.Found) ||
(webResponse.StatusCode == HttpStatusCode.Redirect) ||
(webResponse.StatusCode == HttpStatusCode.Moved) ||
(webResponse.StatusCode == HttpStatusCode.MovedPerm..y))
{
// Get new location and call recursively
WebHeaderCollection headers = webResponse.Headers;
return SendRequestTo(requestBytes,
new Uri(headers["location"]),credential);
}
Now that redirection has been handled and all cookies are set, the response stream can be read to receive the final server’s response. This finishes the method. // Read response
StreamReader stream = new
StreamReader(webResponse.GetResponseStream());
string responseString = stream.ReadToEnd();
stream.Close();
return responseString;
}
throw new Exception("No response received from host.");
}
catch(WebException e)
{
throw new Exception("Exception occured while sending request.",e);
}
}
To complete the class, a public interface will need to be provided, that calls the public string SendRequest(string request,
Uri destination, NetworkCredential credential)
{
if(request == null || request.Trim().Length == 0)
throw new ArgumentNullException("request");
else if (destination == null)
throw new ArgumentNullException("destination");
else
{
byte[] xmlBytes = Encoding.ASCII.GetBytes(request);
return SendRequestTo(xmlBytes,destination, credential);
}
}
Because authentication isn’t done in all the requests, the following method has been made available to make further requests from a selected host after logging in. public string SendRequest(string request, Uri destination)
{
return SendRequest(request,destination,null);
}
Hoera! This completes our proxy, it is now able to do all the things we started out to do! It is able to send requests to Hotmail servers across the world. Hotmail clientNow for implementing a class that provides access to Hotmail. The following example will show how to connect to Hotmail using the proxy, get the First let’s begin again with the class framework: public class HotmailClient
{
private HotmailProxy hHttp = null;
public HotmailClient()
{}
}
Connect()Now for the only public method, First of all, the proxy class en credentials needed for authentication are built: public void Connect(string username, string password)
{
hHttp = new HotmailHttp();
NetworkCredential credentials =
new NetworkCredential(username,password,null);
Pffew, that was hard, one complete line of code. Easy enough as you can see! The next job will be to build the XML query for the string query = "<?xml version='1.0'?>" +
"<D:propfind xmlns:D='DAV:' " +
"xmlns:h='http://schemas.microsoft.com/hotmail/' " +
"xmlns:hm='urn:schemas:httpmail:'>" +
"<D:prop>" +
"<hm:msgfolderroot/>" +
"</D:prop>" +
"</D:propfind>";
The query and the credentials can be used to get a response from the Hotmail gateway located at http://services.msn.com/svcs/hotmail/httpmail.asp. This will cause several redirections and cause the HTTP authentication to take place. All can be done with just a single line of code. try
{
string hotmailEntryPoint =
"http://services.msn.com/svcs/hotmail/httpmail.asp";
string response = hHttp.SendRequest(query,
new Uri(hotmailEntryPoint),credentials);
// Verify response
if (response == null || response.Trim().Length == 0)
throw new Exception();
The Hotmail server will respond with the URL of the // Parse the response, further verifying it.
Uri folderRootUri = ParseConnectResponse(response);
With the now obtained URL, information about all the mailboxes on the server can be retrieved. // Obtain available folders using folderRootUrl
RetrieveMailboxes(folderRootUri);
}
catch(Exception e)
{
// Something went wrong.
throw new MailException
("Exception occured while connecting to remote host.",e);
}
}
This completes our first method. As you can see, it calls two other methods which we will now construct, these two contain the interesting parts, parsing the response! But first an important helper method needs to be build, without it all XPath queries fail. CreateNamespaceManager (XmlNameTable table)To obtain the URL of the folder root, we will use XPath. The problem lies in the fact that XPath will not return results for nodes that are declared in a namespace. It is able to do so, but information about namespaces need to be stated with the XPath query. This can be done by constructing an private XmlNamespaceManager CreateNamespaceManager(XmlNameTable table)
{
XmlNamespaceManager m = new XmlNamespaceManager(table);
m.AddNamespace("hm","urn:schemas:httpmail:");
m.AddNamespace("D","DAV:");
m.AddNamespace("m","urn:schemas:mailheader:");
m.AddNamespace("c","urn:schemas:contacts:");
m.AddNamespace("h","http://schemas.microsoft.com/hotmail/");
return m;
}
ParseConnectResponse(string response)To parse the response, there are a two things that need to be done; place the returned response in an private Uri ParseConnectResponse(string response)
{
try
{
// Load response into XmlDocument
XmlDocument dom = new XmlDocument();
dom.LoadXml(response);
// Query XmlDocument for msgfolderroot node.
string xpath = "//hm:msgfolderroot";
XmlNamespaceManager context = CreateNamespaceManager(dom.NameTable);
XmlNode folderRoot = dom.SelectSingleNode(xpath,context);
// Verify node
if (folderRoot == null)
throw new Exception("Node '" + xpath + "' not found.");
// Get node text and verify,
string folderRootUrl = folderRoot.InnerText;
if ((folderRootUrl == null) || (folderRootUrl.Trim().Length == 0))
throw new Exception("No url found in node '" + xpath + "'.");
try
{
// Return the uri, this may result in a
// UriFormatException
return new Uri(folderRootUrl);
}
catch
{
throw new MailException("Url found in node '"
+ xpath + "' is invalid:" + folderRootUrl);
}
}
catch(Exception e)
{
// Something went wrong.
throw new Exception
("Error occured while parsing connect response.",e);
}
}
The way this method works is pretty basic now. The correct query to make can be determined using the webpage stated in the introduction, it’s also possible to use a network analyzer for this purpose. Probably you will need to use both. To parse the response, the RetrieveMailboxes(Uri folderRootUrl)The last method in this example will retrieve information about all mailboxes on the server. Basically it does the same thing as the first two methods combined, so you should already be able to build it yourself. The method is shown here for completion sake. It will build a query and send it to the URL of the private void RetrieveMailboxes(Uri folderRootUrl)
{
try
{
// Build the needed query
string query = "<?xml version='1.0'?>" +
"<D:propfind xmlns:D='DAV:' " +
"xmlns:hm='urn:schemas:httpmail:'>" +
"<D:prop>" +
"<D:displayname/>" +
"<hm:special/>" +
"<hm:unreadcount/>" +
"<D:visiblecount/>" +
"</D:prop>" +
"</D:propfind>";
// Get a response from server No Credentials are used!
string response = hHttp.SendRequest(query,folderRootUrl);
// Verify response
if (response == null || response.Trim().Length == 0)
throw new ApplicationException
("No response received from host.");
// Load response into XmlDocument
XmlDocument dom = new XmlDocument();
dom.LoadXml(response);
// Query XmlDocument for all mailboxes using XPath
string xpath = "//D:response";
XmlNamespaceManager context =
CreateNamespaceManager(dom.NameTable);
XmlNodeList mailBoxNodes = dom.SelectNodes(xpath,context);
// Parse each node found
foreach(XmlNode mailBoxNode in mailBoxNodes)
{
try
{
// Direct mapping using XPath, should not
// result in any errors
// as long as Hotmail keeps it protocol
// the same.
string type = mailBoxNode.SelectSingleNode
("descendant::hm:special",context).InnerText;
string nameUrl = mailBoxNode.SelectSingleNode
("descendant::D:href",context).InnerText;
int visibleCount = Int32.Parse
(mailBoxNode.SelectSingleNode
("descendant::D:visiblecount",
context).InnerText);
int unreadCount = Int32.Parse
(mailBoxNode.SelectSingleNode
("descendant::hm:unreadcount",
context).InnerText);
Console.WriteLine("MailBox found: " +
type + "\r\n" + "\turl: " +
nameUrl + "\r\n" + "\tVisible: " +
visibleCount + "\r\n" + "\tUnread: " +
unreadCount + "\r\n");
}
catch(Exception e)
{
Console.WriteLine
("Exception occured while " +
"obtaining mailbox info: " + e.Message);
}
}
}
catch(Exception e)
{
// Something went wrong.
throw new ApplicationException("Error occured" +
" while retrieving available mailboxes.",e);
}
}
ConclusionAs you can see, accessing Hotmail from C# can be as easy as any other type of mail server. The code works without any strange loops or use of classes that function only partly. With this code it should be easy to build your own e-mail client that is able to access Hotmail with the same type of interface as you would build for IMAP or POP. Happy coding! | ||||||||||||||||||||