Outlook Webmail Add-in for Windows Home Server

Published 10 August 07 04:33 AM | Coding4Fun 
Office_MAIL In this article, Brian Peek will demonstrate how to create an add-in for Windows Home Server that will allow users to view their Outlook mail from a web browser.
ASPSOFT, Inc.

Difficulty: Intermediate
Time Required: 2-3 hours
Cost: Free
Software: Windows Home ServerMicrosoft Outlook 2000/XP/2003/2007 (not Outlook Express/Windows Mail), Visual Basic or Visual C# Express Editions, Visual Web Developer Express Edition, Microsoft .NET Framework 3.0 runtime, Outlook/Office Primary Interop Assemblies (more on this below), Windows SDK (Vista or later, which includes the .NET 3.0 bits.  Note that this installs and works just fine under Windows XP.  The SDK can build applications for Windows XP and greater.)
Hardware: None
Download: CodePlex Project

Introduction

Windows Home Server is a new product from Microsoft which allows home users to manage and share data, including photos, documents, videos, music, etc.  It also provides a very easy way to backup all computers on your home network to a central storage server.

Windows Home Server can also be extended via add-ins to enhance the experience and provide new and interesting functionality other than what comes in the box.

One feature not present in WHS that I would find useful is the ability to view my Outlook mail box from the web at any time.  I have 6 or 7 email accounts that are all setup to retrieve via POP3 to Outlook.  Most of these accounts do not support IMAP or have a web-based interface.  Therefore, Outlook is generally open all day and checking messages.  When I'm away from home for work or pleasure, it's very often inconvenient to have to remote desktop into the machine with Outlook running to read my email, so it would be nice to have a web-based version of my current Outlook folders so I can view all email (old and new) at any time simply by browsing to a web server at home.  Windows Home Server comes with Internet Information Services 6 (IIS6) and one can easily add a new web application to IIS on the server.

So, this article will attempt to show how to build a new web site using ASP.NET that can be added to your Windows Home Server installation that will allow one to view the Outlook folders running on whatever computer contains your current Outlook installation and message store.

If you wish to just use the application, download the sample from above and skip down to the deployment section for installation instructions.

NOTE: This application will only work with Microsoft Outlook.  It will not work with Outlook Express, Windows Mail, or any other mail client.

Setup

Setup can be a bit tricky.  Office/Outlook will need to be installed on your development machine.  It does not need to be the same machine which contains your store at this point, but that too would help.  Once Office/Outlook is installed, the Primary Interop Assemblies for the version of Office you are using need to be installed.  For Office 2003/2007, this can be done choosing .NET Programming Support from the list of sub-items in the Microsoft Office Outlook section of the setup program.

2003

pia

Unfortunately I do not have an earlier copy of Office with which to check, but the procedure should be the same.  If anyone happens to try this with Office XP/2000, please let me know if/how it works.

If you will be developing on an OS earlier than Vista, install the .NET Framework 3.0 runtime.

Finally, install the Windows SDK linked above accepting all defaults.

Architecture

The architecture we will be using is very similar that to an N-tier application.  The machine running Outlook with the message store to be viewed is, in essence, the server machine.  That machine will run a host process that we will develop which will expose several methods via Windows Communication Foundation.  These methods will be consumed by an ASP.NET application running on the Windows Home Server.

The Host

Let's start by building the host application.  This will run on the computer where Outlook is installed and the messages are stored.  The application will be written to run in the notification area next to the Windows system clock.

To start, create a new Windows Application project named WHSMailHost.  Rename the default Form1.cs/.vb file to frmMain.cs/.vb.  Double-click on the file in the Solution Explorer to bring up the design surface.

In order to get the application to run in the notification area, drag and drop a NotifyIcon control from the Toolbox to the design surface, name it niIcon.  Also, drag over a ContextMenuStrip to the design surface and name it cmsMenu.  This will be used to pop up a context menu when the icon in the notification area is clicked.  Finally, set the following properties on the niIcon control:

Text WHS Mail Host
ContextMenuStrip cmsMenu
Visible True

Select the cmsMenu control and add a single menu item named Exit to the list.  Double-click on that menu item to create a default Click event.  In the code for the Click event, simply close the form as follows:

C#

private void mnuExit_Click(object sender, EventArgs e)
{
    // exit the application
    this.Close();
}

VB

Private Sub mnuExit_Click(ByVal sender As Object, ByVal e As EventArgs) Handles mnuExit.Click
    ' exit the application
    Me.Close()
End Sub

Finally, add the following events to frmMain by selecting them from the Event Property window and implementing them with the following code:

C#

private void frmMain_Resize(object sender, EventArgs e)
{
    // hide the form
    this.Hide();
}

private void frmMain_Load(object sender, EventArgs e)
{
    // start the WCF service
    MyServiceHost.StartService();
}

private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
{
    // stop the WCF service
    MyServiceHost.StopService();
}

VB

Private Sub frmMain_Resize(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Resize
    ' hide the form
    Me.Hide()
End Sub

Private Sub frmMain_Load(ByVal sender As Object, ByVal e As EventArgs) Handles MyBase.Load
    ' start the WCF service
    MyServiceHost.StartService()
End Sub

Private Sub frmMain_FormClosing(ByVal sender As Object, ByVal e As FormClosingEventArgs) Handles MyBase.FormClosing
    ' stop the WCF service
    MyServiceHost.StopService()
End Sub

The code above references a class named MyServiceHost.  This is what starts the WCF host and will be discussed next.

 

Windows Communication Foundation (WCF)

Windows Communication Foundation (formerly known as Indigo) is a feature of the .NET Framework 3.0 that allows one to build and run connected systems.  The simplest definition that fits with what we will be doing here is that it allows an application running on one machine (the client, in this case, the ASP.NET application) to execute a method on another machine (the host, in this case, the application we're currently building).

First, add a reference to the System.ServiceModel assembly.  Then, add a class to the project named WHSMailService.  Open the class and add the following code beneath the generated WHSMailService class implementation:

C#

internal class MyServiceHost
{
    internal static ServiceHost myServiceHost = null;

    internal static void StartService()
    {
        // Instantiate new ServiceHost 
        myServiceHost = new ServiceHost(typeof(WHSMailService));
        myServiceHost.Open();
    }

    internal static void StopService()
    {
        // Call StopService from your shutdown logic (i.e. dispose method)
        if (myServiceHost.State != CommunicationState.Closed)
            myServiceHost.Close();
    }
}

VB

Friend Class MyServiceHost
    Friend Shared myServiceHost As ServiceHost = Nothing

    Friend Shared Sub StartService()
        ' Instantiate new ServiceHost 
        myServiceHost = New ServiceHost(GetType(WHSMailService))
        myServiceHost.Open()
    End Sub

    Friend Shared Sub StopService()
        ' Call StopService from your shutdown logic (i.e. dispose method)
        If myServiceHost.State <> CommunicationState.Closed Then
            myServiceHost.Close()
        End If
    End Sub
End Class

This code simply creates a new WCF ServiceHost of the type WHSMailService (which we will implement next) and then opens that host so that it may receive incoming connections.  We will look at how this connections are configured later in the article.

Remember that the code in the form above above called the StartService and StopService methods located here when the main form loads and closes.  This very easily allows to immediately start the service host when the application starts and closes the service host when the application exits.

Contracts and Entities

A contract is an interface that defines which methods are exposed by the service host that can be consumed by the client application.  Both the client application and the server application will need to know what is in this interface, so we will need to create a second project that will contain the interface definition.  Additionally, we will need a way to pass folder and email information to and from each application, so we will define some custom classes to encapsulate those objects.

Create a new Class Library project in the current solution named WHSMailCommon.  In this new project, create a directory named Contracts and a directory named Entities.

Inside the Entities directory, create a new class named Folder.  This will represent an Outlook message folder.  The class should look like the following:

C#

using System;
using System.Collections.Generic;

namespace WHSMailCommon.Entities
{
    // entity object representing an Folder
    [Serializable]
    public class Folder : IComparable
    {
        private string _entryID;
        private string _name;
        private List<Folder> _folders;
        private int _unreadMessages;
        private int _totalMessages;

        public Folder(string entryID, string name, int unreadMessages, int totalMessages)
        {
            _entryID = entryID;
            _name = name;
            _unreadMessages = unreadMessages;
            _totalMessages = totalMessages;
        }

        // MAPI unique identifier
        public string EntryID
        {
            get { return _entryID; }
            set { _entryID = value; }
        }

        // subfolders of this folder
        public List<Folder> Folders
        {
            get { return _folders; }
            set { _folders = value; }
        }

        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        public int UnreadMessages
        {
            get { return this._unreadMessages; }
            set { this._unreadMessages = value; }
        }

        public int TotalMessages
        {
            get { return this._totalMessages; }
            set { this._totalMessages = value; }
        }

        // used so we can sort the folders alphabetically later on
        public int CompareTo(object obj)
        {
            return string.Compare(this.Name, ((Folder)obj).Name);
        }
    }
}

VB

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Generic

Namespace WHSMailCommon.Entities
    ' entity object representing an Folder
    <Serializable> _
    Public Class Folder
        Implements IComparable
        Private _entryID As String
        Private _name As String
        Private _folders As List(Of Folder)
        Private _unreadMessages As Integer
        Private _totalMessages As Integer

        Public Sub New(ByVal entryID As String, ByVal name As String, ByVal unreadMessages As Integer, ByVal totalMessages As Integer)
            _entryID = entryID
            _name = name
            _unreadMessages = unreadMessages
            _totalMessages = totalMessages
        End Sub

        ' MAPI unique identifier
        Public Property EntryID() As String
            Get
                Return _entryID
            End Get
            Set(ByVal value As String)
                _entryID = value
            End Set
        End Property

        ' subfolders of this folder
        Public Property Folders() As List(Of Folder)
            Get
                Return _folders
            End Get
            Set(ByVal value As List(Of Folder))
                _folders = value
            End Set
        End Property

        Public Property Name() As String
            Get
                Return _name
            End Get
            Set(ByVal value As String)
                _name = value
            End Set
        End Property

        Public Property UnreadMessages() As Integer
            Get
                Return Me._unreadMessages
            End Get
            Set(ByVal value As Integer)
                Me._unreadMessages = value
            End Set
        End Property

        Public Property TotalMessages() As Integer
            Get
                Return Me._totalMessages
            End Get
            Set(ByVal value As Integer)
                Me._totalMessages = value
            End Set
        End Property

        ' used so we can sort the folders alphabetically later on
        Public Function CompareTo(ByVal obj As Object) As Integer Implements IComparable.CompareTo
            Return String.Compare(Me.Name, (CType(obj, Folder)).Name)
        End Function
    End Class
End Namespace

This class defines several properties to describe the folder (EntryID, Name, etc.) and additionally implements the IComparable interface's CompareTo method so that we can easily sort the folders alphabetically later on.

Next, create a class named Email.  The code for this class looks like the following:

C#

using System;
using System.Collections.Generic;
using System.Text;

namespace WHSMailCommon.Entities
{
    // entity object representing an Email
    [Serializable]
    public class Email
    {
        private string _entryID;
        private string _from;
        private string _fromName;
        private string _subject;
        private DateTime _received;
        private int _size;
        private string _body;

        public Email(string entryID, string from, string fromName, string subject, DateTime received, int size)
        {
            _entryID = entryID;
            _from = from;
            _fromName = fromName;
            _subject = string.IsNullOrEmpty(subject) ? "(no subject)" : subject;
            _received = received;
            _size = size;
        }

        public Email(string entryID, string from, string fromName, string subject, DateTime received, int size, string body) :
            this(entryID, from, fromName, subject, received, size)
        {
            _body = body;
        }

        // MAPI unique ID
        public string EntryID
        {
            get { return _entryID; }
            set { _entryID = value; }
        }

        // email address of sender
        public string From
        {
            get { return _from; }
            set { _from = value; }
        }

        // name of sender
        public string FromName
        {
            get { return _fromName; }
            set { _fromName = value; }
        }

        public string Subject
        {
            get { return _subject; }
            set { _subject = value; }
        }

        public string Body
        {
            get { return _body; }
            set { _body = value; }
        }

        public DateTime Received
        {
            get { return _received; }
            set { _received = value; }
        }

        public int Size
        {
            get { return _size; }
            set { _size = value; }
        }
    }
}

VB

Imports Microsoft.VisualBasic
Imports System
Imports System.Collections.Generic
Imports System.Text

Namespace WHSMailCommon.Entities
    ' entity object representing an Email
    <Serializable> _
    Public Class Email
        Private _entryID As String
        Private _from As String
        Private _fromName As String
        Private _subject As String
        Private _received As DateTime
        Private _size As Integer
        Private _body As String

        Public Sub New(ByVal entryID As String, ByVal From As String, ByVal fromName As String, ByVal subject As String, ByVal received As DateTime, ByVal size As Integer)
            _entryID = entryID
            _from = From
            _fromName = fromName
            If String.IsNullOrEmpty(subject) Then
                _subject = "(no subject)"
            Else
                _subject = subject
            End If
            _received = received
            _size = size
        End Sub

        Public Sub New(ByVal entryID As String, ByVal From As String, ByVal fromName As String, ByVal subject As String, ByVal received As DateTime, ByVal size As Integer, ByVal body As String)
            Me.New(entryID, From, fromName, subject, received, size)
            _body = body
        End Sub

        ' MAPI unique ID
        Public Property EntryID() As String
            Get
                Return _entryID
            End Get
            Set(ByVal value As String)
                _entryID = value
            End Set
        End Property

        ' email address of sender
        Public Property From() As String
            Get
                Return _from
            End Get
            Set(ByVal value As String)
                _from = value
            End Set
        End Property

        ' name of sender
        Public Property FromName() As String
            Get
                Return _fromName
            End Get
            Set(ByVal value As String)
                _fromName = value
            End Set
        End Property

        Public Property Subject() As String
            Get
                Return _subject
            End Get
            Set(ByVal value As String)
                _subject = value
            End Set
        End Property

        Public Property Body() As String
            Get
                Return _body
            End Get
            Set(ByVal value As String)
                _body = value
            End Set
        End Property

        Public Property Received() As DateTime
            Get
                Return _received
            End Get
            Set(ByVal value As DateTime)
                _received = value
            End Set
        End Property

        Public Property Size() As Integer
            Get
                Return _size
            End Get
            Set(ByVal value As Integer)
                _size = value
            End Set
        End Property
    End Class
End Namespace

This class simply contains properties to describe an email message.

You will note that both of these classes have the [Serializable] attribute attached to them.  When objects are passed through WCF, they are serialized at the source and deserialized at the destination.  By marking the objects with the [Serializable] attribute, the .NET CLR can do this for us automatically  since we are not using any complex data types in our entities.

With our entities out of the way, we can define our contract.  Inside the Contracts directory, create a new Interface file named IWHSMailService.

This interface/contract will define three methods:  one method to return a tree of objects which represent the folder tree in Outlook (GetFolders), one method to return a list of messages inside that folder (GetMessages), and one method to return the contents of a specific message (GetMessages).  The interface will look like the following:

C#

using System.Collections.Generic;
using System.ServiceModel;
using WHSMailCommon.Entities;

namespace WHSMailCommon.Contracts
{
    // list of methods of the WHSMailService service
    [ServiceContract()]
    public interface IWHSMailService
    {
        [OperationContract]
        List<Folder> GetFolders();

        [OperationContract]
        List<Email> GetMessages(string entryID, int numPerPage, int pageNum);

        [OperationContract]
        Email GetMessage(string entryID);
    }
}

VB

Imports Microsoft.VisualBasic
Imports System.Collections.Generic
Imports System.ServiceModel
Imports WHSMailCommon.Entities

Namespace WHSMailCommon.Contracts
    ' list of methods of the WHSMailService service
    <ServiceContract()> _
    Public Interface IWHSMailService
        <OperationContract> _
        Function GetFolders() As List(Of Folder)

        <OperationContract> _
        Function GetMessages(ByVal entryID As String, ByVal numPerPage As Integer, ByVal pageNum As Integer) As List(Of Email)

        <OperationContract> _
        Function GetMessage(ByVal entryID As String) As Email
    End Interface
End Namespace

As with the entities, this interface is also decorated with several attributes.  First, any WCF contract interface must be tagged with the [ServiceContract()] attribute to define it as a contract to WCF.  Additionally, all methods which will be exposed for consumption by a client must be marked with the [OperationContract] attribute.

The implementation and description of these methods will come later when we write the contract implementation.

Configuration

The final thing to setup on the WCF server is the configuration file.  The service can very easily be configured using an application configuration file.  Add an Application Configuration file to the project named App.config.  Set the contents of the file to the following:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <netTcpBinding>
                <binding name="NewBinding0">
                    <security mode="None" />
                </binding>
            </netTcpBinding>
        </bindings>
        <services>
            <service name="WHSMailHost.WHSMailService">
                <endpoint address="net.tcp://localhost:12345/IWHSMailService"
                    binding="netTcpBinding" bindingConfiguration="NewBinding0"
                    contract="WHSMailCommon.Contracts.IWHSMailService" />
            </service>
        </services>
    </system.serviceModel>
</configuration>

The <services> section defines the services that the WCF host will enable.  This creates a single <service> named WHSMailHost.WHSMailService, which is the full name of the service class that we will implement shortly.  Inside the <service> tag an endpoint is defined.  And endpoint is essentially the "port" on which the client connects.  The endpoint defined here is using the net.tcp protocol and is set to listen on the localhost on port 12345 at the name IWHSMailService.  The next thing defined is a binding.  This sets protocol by which the service will communicate, its configuration, and the contract it implements.  The binding configuration named NewBinding0 (a default name) can be found above inside the <bindings> tag.  The only configuration item that is specified is that security will be turned off for communication between the client and server.  Given that these two machines will be connecting to each other on your own local network, security is not a great concern.  If you were to use this over a real internet connection where the client and server were open to the outside world, you would definitely not want to do this.

There are around several billion other options, protocols, bindings, etc. etc. etc. that can be configured here.  At the end of the article you will find several links to WCF documentation that you can explore to learn more about the WCF internals.  Also note that you can configure WCF with a windows UI by selecting the Service Configuration Editor application installed with the Windows SDK.  You'll find this in the Microsoft Windows SDK program group off your Start menu.

Outlook and MAPI

MAPI (Messaging Application Programming Interface) is what allows one to develop applications and plugins for Microsoft Outlook and other messaging applications which support it (Exchange, Windows Messaging, etc.).  We will be using MAPI to get the data we require from Outlook for our application.

Earlier we created a contract named IWHSMailService.  This contract will be implemented by the WHSMailService class that was also created earlier.  To do this, set a reference to the project's WHSMailCommon assembly.  Then, bring the Contracts and Entities namespaces into the WHSMailService class with the following:

C#

using WHSMailCommon.Contracts;
using WHSMailCommon.Entities;

VB

Imports WHSMailCommon.Contracts
Imports WHSMailCommon.Entities

Then, setup the class to implement the interface as follows:

C#

public class WHSMailService : IWHSMailService

VB

Public Class WHSMailService
    Implements IWHSMailService

Visual Studio will then create the 3 methods that must be implemented according to the interface (GetFolders, GetMessages, GetMessage).  We will fill those in shortly.  But first, we must implement our constructor.  When WCF calls the service, a new instance of the object will be created on each call.  Therefore, it is easy to setup any initialization code for the service in the default constructor method.  In this case, we will initialize the MAPI layer and logon to the default instance.

First, set a reference to the Microsoft Outlook XX.0 Object Library which you will find under the COM tab assuming Outlook is installed as per the instructions above.  Note that the version will depend on what version of Outlook you have installed on your local machine.  I'm using Outlook 2007, so version 12 is what the sample code above is referencing.  If you are using a different version, reference the appropriate version before continuing.

Once the reference is set, bring the namespace into the WHSMailService class with the following line which will import the namespace and setup an alias named Outlook to save some typing:

C#

using Outlook = Microsoft.Office.Interop.Outlook;

VB

Imports Outlook = Microsoft.Office.Interop.Outlook

Now the constructor can be implemented.  We need to get an instance of the Outlook ApplicationClass.  From there we can get an instance of the MAPI namespace.  All methods that we will be using hang off that namespace object.  The code for the constructor follows:

C#

private readonly Outlook.NameSpace _nameSpace = null;

public WHSMailService()
{
    // get an instance of the MAPI namespace and login
    Outlook.Application app = new Outlook.ApplicationClass();
    _nameSpace = app.GetNamespace("MAPI");
    _nameSpace.Logon(null, null, false, false);
}

VB

Private ReadOnly _nameSpace As Outlook.NameSpace = Nothing

Public Sub New()
    ' get an instance of the MAPI namespace and login
    Dim app As Outlook.Application = New Outlook.ApplicationClass()
    _nameSpace = app.GetNamespace("MAPI")
    _nameSpace.Logon(Nothing, Nothing, False, False)
End Sub

With a handle to the namespace, we can write our GetFolders method.  This method will get the default Inbox folder in the default Outlook message store, look at the parent node, recursively enumerate from there, pulling out only folders that contain mail items, and finally sort them alphabetically (recall the CompareTo method of the IComparable interface we implemented earlier).

C#

public List<Folder> GetFolders()
{
    List<Folder> list = new List<Folder>();

    // get the inbox and then go up one level...that *should* be the root of the default store
    Outlook.MAPIFolder root = (Outlook.MAPIFolder)_nameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox).Parent;

    // add root folder
    Folder folder = new Folder(root.EntryID, root.Name, root.UnReadItemCount, root.Items.Count);
    list.Add(folder);

    // Enumerate the sub-folders
    EnumerateFolders(root.Folders, folder);

    return list;
}

private void EnumerateFolders(Outlook.Folders folders, Folder rootFolder)
{
    foreach(Outlook.MAPIFolder f in folders)
    {
        // ensure it's a folder that contains mail messages (i.e. no contacts, appointments, etc.)
        if(f.DefaultItemType == Outlook.OlItemType.olMailItem)
        {
            if(rootFolder.Folders == null)
                rootFolder.Folders = new List<Folder>();

            // add the current folder and enumerate all sub-folders
            Folder subFolder = new Folder(f.EntryID, f.Name, f.UnReadItemCount, f.Items.Count);
            rootFolder.Folders.Add(subFolder);
            if(f.Folders.Count > 0)
                this.EnumerateFolders(f.Folders, subFolder);
        }
    }

    // alphabetize the list (Folder implements IComparable)
    rootFolder.Folders.Sort();
}

VB

Public Function GetFolders() As List(Of Folder) Implements IWHSMailService.GetFolders
    Dim list As List(Of Folder) = New List(Of Folder)()

    ' get the inbox and then go up one level...that *should* be the root of the default store
    Dim root As Outlook.MAPIFolder = CType(_nameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox).Parent, Outlook.MAPIFolder)

    ' add root folder
    Dim folder As Folder = New Folder(root.EntryID, root.Name, root.UnReadItemCount, root.Items.Count)
    list.Add(folder)

    ' Enumerate the sub-folders
    EnumerateFolders(root.Folders, folder)

    Return list
End Function

Private Sub EnumerateFolders(ByVal folders As Outlook.Folders, ByVal rootFolder As Folder)
    For Each f As Outlook.MAPIFolder In folders
        ' ensure it's a folder that contains mail messages (i.e. no contacts, appointments, etc.)
        If f.DefaultItemType = Outlook.OlItemType.olMailItem Then
            If rootFolder.Folders Is Nothing Then
                rootFolder.Folders = New List(Of Folder)()
            End If

            ' add the current folder and enumerate all sub-folders
            Dim subFolder As Folder = New Folder(f.EntryID, f.Name, f.UnReadItemCount, f.Items.Count)
            rootFolder.Folders.Add(subFolder)
            If f.Folders.Count > 0 Then
                Me.EnumerateFolders(f.Folders, subFolder)
            End If
        End If
    Next f

    ' alphabetize the list (Folder implements IComparable)
    rootFolder.Folders.Sort()
End Sub

This code will return a generic hierarchal list of our Folder entity objects which will be displayed in the web application.  Note that one of the items assigned to the Folder entity is the EntryID property from the MAPIFolder object.  All MAPI items, be they email messages, folders, appointments, etc. have a unique identifier which is stored in the EntryID field.  We will need this unique value later on to retrieve messages from that folder.

Next, let's implement the GetMessages method.  This method will return a list of messages (minus the bodies) from the folder specified using the GetFolderFromID method, sorted by the received date with the most current first.  It will also handle paging so that the entire folder is not returned at once.

C#

public List<Email> GetMessages(string entryID, int numPerPage, int pageNum)
{
    List<Email> list = new List<Email>();

    Outlook.MAPIFolder f;

    // if no ID specified, open the inbox
    if(string.IsNullOrEmpty(entryID))
        f = _nameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
    else
        f = _nameSpace.GetFolderFromID(entryID, "");

    // to handle the sorting, one needs to cache their own instance of the items object
    Outlook.Items items = f.Items;

    // sort descending by received time
    items.Sort("[ReceivedTime]", true);

    // pull in the correct number of items based on number of items per page and current page number
    for(int i = (numPerPage*pageNum)+1; i <= (numPerPage*pageNum)+numPerPage && i <= items.Count; i++)
    {
        // ensure it's a mail message
        Outlook.MailItem mi = (items[i] as Outlook.MailItem);
        if(mi != null)
            list.Add(new Email(mi.EntryID, mi.SenderEmailAddress, mi.SenderName, mi.Subject, mi.ReceivedTime, mi.Size));
    }

    return list;
}

VB

Public Function GetMessages(ByVal entryID As String, ByVal numPerPage As Integer, ByVal pageNum As Integer) As List(Of Email) Implements IWHSMailService.GetMessages
    Dim list As List(Of Email) = New List(Of Email)()

    Dim f As Outlook.MAPIFolder

    ' if no ID specified, open the inbox
    If String.IsNullOrEmpty(entryID) Then
        f = _nameSpace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox)
    Else
        f = _nameSpace.GetFolderFromID(entryID, "")
    End If

    ' to handle the sorting, one needs to cache their own instance of the items object
    Dim items As Outlook.Items = f.Items

    ' sort descending by received time
    items.Sort("[ReceivedTime]", True)

    ' pull in the correct number of items based on number of items per page and current page number
    Dim i As Integer = (numPerPage*pageNum)+1
    Do While i <= (numPerPage*pageNum)+numPerPage AndAlso i <= items.Count
        ' ensure it's a mail message
        Dim mi As Outlook.MailItem = (TryCast(items(i), Outlook.MailItem))
        If Not mi Is Nothing Then
            list.Add(New Email(mi.EntryID, mi.SenderEmailAddress, mi.SenderName, mi.Subject, mi.ReceivedTime, mi.Size))
        End If
        i += 1
    Loop

    Return list
End Function

The method takes a string parameter named entryID which is the unique identifier of the folder (from the code above) from which to return the messages.  If no ID is specified, messages are returned from the Inbox.  This code also handles paging by indexing into the array of Items of the MAPIFolder object at the specified position and counting out numPerPage records to be returned.  A list of Email entity objects is returned from this method to be displayed on the web interface.

Finally, let's implement the GetMessage method.  This will return a specific message based on the MAPI EntryID using the GetItemFromID method and format the body for plain text or HTML display.

C#

public Email GetMessage(string entryID)
{
    // pull the message
    Outlook.MailItem mi = (_nameSpace.GetItemFromID(entryID, "") as Outlook.MailItem);

    if (mi != null)
    {
        string body;

        // if it's a plain format message, wrap it in <pre> tags for nice output
        if(mi.BodyFormat == Outlook.OlBodyFormat.olFormatPlain)
            body = "<pre>" + mi.Body + "</pre>";
        else
            body = mi.HTMLBody;

        return new Email(mi.EntryID, mi.SenderEmailAddress, mi.SenderName, mi.Subject, mi.ReceivedTime, mi.Size, body);
    }
    else
        return null;
}

VB

Public Function GetMessage(ByVal entryID As String) As Email Implements IWHSMailService.GetMessage
    ' pull the message
    Dim mi As Outlook.MailItem = (TryCast(_nameSpace.GetItemFromID(entryID, ""), Outlook.MailItem))

    If Not mi Is Nothing Then
        Dim body As String

        ' if it's a plain format message, wrap it in <pre> tags for nice output
        If mi.BodyFormat = Outlook.OlBodyFormat.olFormatPlain Then
            body = "<pre>" & mi.Body & "</pre>"
        Else
            body = mi.HTMLBody
        End If

        Return New Email(mi.EntryID, mi.SenderEmailAddress, mi.SenderName, mi.Subject, mi.ReceivedTime, mi.Size, body)
    Else
        Return Nothing
    End If
End Function

With these three methods in place, we can get our folders, get messages in those folders, and get a specific message from a folder.  Now we can switch our attention to the web-based "client" application which will plug into WHS.

 

ASP.NET "Client"

The client we are going to produce is going to be a very simple 3-paned screen much like Outlook with folders on the left, messages on the top right, and message text on the bottom right.

Create a new ASP.NET Web Application named WHSMailWeb (if you are using the Express editions of Visual Studio, you will need to do this in Visual Web Developer Express).  Set a reference to the WHSMailCommon assembly along with the System.ServiceModel assembly.  Again, if you are in Express, you will have to set the reference to WHSMailCommon in the other project's bin directory.

First, let's configure the ASP.NET application so it can properly call our WCF service running on the host machine.  Open the web.config file and add the following after the end </system.web> tag and before the end </configuration> tab:

<system.serviceModel>
    <bindings>
        <netTcpBinding>
            <binding name="NewBinding0" maxReceivedMessageSize="1048576">
                <readerQuotas maxStringContentLength="1048576" />
                <security mode="None" />
            </binding>
        </netTcpBinding>
    </bindings>
    <client>
        <endpoint address="net.tcp://SERVERNAME:12345/IWHSMailService"
            binding="netTcpBinding" bindingConfiguration="NewBinding0"
            contract="WHSMailCommon.Contracts.IWHSMailService" name="WHSMailService" />
    </client>
</system.serviceModel>

This looks very similar to the configuration information from our host service.  An endpoint is defined which points to the host server.  YOU WILL NEED TO CHANGE THE "SERVERNAME" HOST TO THE NAME OR IP ADDRESS OF THE MACHINE THAT WILL RUN THE HOST SERVICE WE CREATED ABOVE!

You will see the same setup for the binding and contract.  The name parameter will be used by the code a bit later so WCF knows how to contact the host to instantiate the remote object.

The one thing that is different here is the netTcpBinding configuration.  The security mode is set to none as it was before, but we have the addition of the maxReceivedMessageSize and maxStringContentLength.  Both of these are set at 1 megabyte to allow that much data to be transferred from the host to the client.  The default is only 64K which is not enough to return most of the data we need to serialize and transmit between the two machines.

Next, add a page to the project named BasePage.  This page will be inherited by all pages in the project to easily startup and shutdown the WCF channel required to retrieve the message data.

Open the code-behind file for BasePage and add the following code:

C#

using System;
using System.ServiceModel;
using WHSMailCommon.Contracts;

namespace WHSMailWeb
{
    public partial class BasePage : System.Web.UI.Page
    {
        ChannelFactory<IWHSMailService> _factory = null;
        private IWHSMailService _channel = null;

        protected void Page_Init(object sender, EventArgs e)
        {
            // create a channel factory and then instantiate a proxy channel
            _factory = new ChannelFactory<IWHSMailService>("WHSMailService");
            _channel = _factory.CreateChannel();
        }

        protected void Page_Unload(object sender, EventArgs e)
        {
            try
            {
                _factory.Close();
            }
            catch
            {
                // for the moment, we don't really care what happens here...if it fails, so be it
            }
        }

        public IWHSMailService WHSMailService
        {
            get { return _channel; }
        }
    
    }
}

VB

Imports Microsoft.VisualBasic
Imports System
Imports System.ServiceModel
Imports WHSMailCommon.Contracts

Namespace WHSMailWeb
    Public Partial Class BasePage
        Inherits System.Web.UI.Page
        Private _factory As ChannelFactory(Of IWHSMailService) = Nothing
        Private _channel As IWHSMailService = Nothing

        Protected Sub Page_Init(ByVal sender As Object, ByVal e As EventArgs)
            ' create a channel factory and then instantiate a proxy channel
            _factory = New ChannelFactory(Of IWHSMailService)("WHSMailService")
            _channel = _factory.CreateChannel()
        End Sub

        Protected Sub Page_Unload(ByVal sender As Object, ByVal e As EventArgs)
            Try
                _factory.Close()
            Catch
                ' for the moment, we don't really care what happens here...if it fails, so be it
            End Try
        End Sub

        Public ReadOnly Property WHSMailService() As IWHSMailService
            Get
                Return _channel
            End Get
        End Property

    End Class
End Namespace

This code overrides the page's Init and Unload methods.  Page_Init is called at the beginning of a page request and Page_Unload is called just before the request completes.

The Page_Init method here uses WCF to create a ChannelFactory object factory that will return proxy objects of type IWHSMailService.  Recall that this is the interface which defines the contract of our service.  The name passed as a parameter to the ChannelFactory constructor must match the name of the service in the configuration file, as discussed earlier.

Next, a channel object is created by calling CreateChannel from the ChannelFactory.  This is what returns the fake, proxy object defined by our contract.  A property named WHSMailService at the bottom of the class exposes this so that the inherited pages can easily use the object to call the host service.  We'll see this a bit later.

The Page_Unload method simply closes the factory object and very poorly handles any exceptions that may occur when doing so.  An error on close isn't critical to this application, but that is certainly not always the case.

Now we will implement the default page.  I'm not going to repeat the entire HTML of the page itself here, and I'm certainly not a HTML/CSS designer, so I wouldn't recommend learning from that anyhow.  It does, however, get the job done.

What you do need to know is that the page contains 4 <DIV> tags:  one contains an ASP.NET TreeView object (the left-most pane), one contains an ASP.NET GridView object (the top-right pane), one contains 2 ASP.NET LinkButton's which implement a Next/Back paging scheme (the middle-right pane),  and the last contains a table to display some header information, and a <DIV> to display the message contents.

After that poor description, here's what the application looks like in an instance of Internet Explorer with my instance of Outlook (with some blurring over the (not really) sensitive data):

webmail

Now let's implement the actual logic for the page.  Open the Default.aspx page's code behind file.  Bring in the WHSMailCommon.Entities namespace as follows:

C#

using WHSMailCommon.Entities;

VB

Imports WHSMailCommon.Entities

Next, change the _Default class to inherit from BasePage instead of System.Web.UI.Page:

C#

public partial class _Default : BasePage

VB

Public Partial Class _Default
    Inherits BasePage

Then, implement the Page_Load method.  This method will be called after the Page_Init method which is implemented in our parent object and will be called automatically.

C#

private string _folderEntryID = string.Empty;
private int _pageNum = 0;

protected void Page_Load(object sender, EventArgs e)
{
    // first time in (tvFolders has viewstate enabled
    if(tvFolders.Nodes.Count == 0)
    {
        // get the folder tree
        List<Folder> folderList = this.WHSMailService.GetFolders();

        // add the root node and expand it
        TreeNode node = new TreeNode(folderList[0].Name, folderList[0].EntryID);
        node.Expanded = true;
        tvFolders.Nodes.Add(node);

        // load up the sub-folders
        LoadNode(folderList[0].Folders, node);

        // get the inbox list and bind it to the grid
        List<Email> list = GetMessages();
        BindMessages(list);

        // save off the default page number and folder ID
        ViewState["PageNum"] = _pageNum;
        ViewState["FolderID"] = _folderEntryID;
    }

    _pageNum = int.Parse(ViewState["PageNum"].ToString());
    _folderEntryID = ViewState["FolderID"].ToString();
}

private void LoadNode(List<Folder> folders, TreeNode node)
{
    foreach(Folder f in folders)
    {
        // add the node with the format of Folder (Unread/Total)
        TreeNode subNode = new TreeNode(f.Name + " (" + f.UnreadMessages + "/" + f.TotalMessages + ")", f.EntryID);

        // expand and select the inbox
        subNode.Expanded = subNode.Selected = (f.Name == "Inbox");
        node.ChildNodes.Add(subNode);

        // load the subfolders
        if(f.Folders != null)
            LoadNode(f.Folders, subNode);
    }
}

private List<Email> GetMessages()
{
    // get a group of messages based on the current page number and size
    return this.WHSMailService.GetMessages(_folderEntryID, GridView1.PageSize, _pageNum);
}

private void BindMessages(List<Email> list)
{
    // load the grid
    GridView1.DataSource = list;
    GridView1.DataBind();

    // save off the new page number
    ViewState["PageNum"] = _pageNum;
}

VB

Private _folderEntryID As String = String.Empty
Private _pageNum As Integer = 0

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
    ' first time in (tvFolders has viewstate enabled
    If tvFolders.Nodes.Count = 0 Then
        ' get the folder tree
        Dim folderList As List(Of Folder) = Me.WHSMailService.GetFolders()

        ' add the root node and expand it
        Dim node As TreeNode = New TreeNode(folderList(0).Name, folderList(0).EntryID)
        node.Expanded = True
        tvFolders.Nodes.Add(node)

        ' load up the sub-folders
        LoadNode(folderList(0).Folders, node)

        ' get the inbox list and bind it to the grid
        Dim list As List(Of Email) = GetMessages()
        BindMessages(list)

        ' save off the default page number and folder ID
        ViewState("PageNum") = _pageNum
        ViewState("FolderID") = _folderEntryID
    End If

    _pageNum = Integer.Parse(ViewState("PageNum").ToString())
    _folderEntryID = ViewState("FolderID").ToString()
End Sub

Private Sub LoadNode(ByVal folders As List(Of Folder), ByVal node As TreeNode)
    For Each f As Folder In folders
        ' add the node with the format of Folder (Unread/Total)
        Dim subNode As TreeNode = New TreeNode(f.Name & " (" & f.UnreadMessages & "/" & f.TotalMessages & ")", f.EntryID)

        ' expand and select the inbox
        subNode.Selected = (f.Name = "Inbox")
        subNode.Expanded = subNode.Selected
        node.ChildNodes.Add(subNode)

        ' load the subfolders
        If Not f.Folders Is Nothing Then
            LoadNode(f.Folders, subNode)
        End If
    Next f
End Sub

Private Function GetMessages() As List(Of Email)
    ' get a group of messages based on the current page number and size
    Return Me.WHSMailService.GetMessages(_folderEntryID, GridView1.PageSize, _pageNum)
End Function

Private Sub BindMessages(ByVal list As List(Of Email))
    ' load the grid
    GridView1.DataSource = list
    GridView1.DataBind()

    ' save off the new page number
    ViewState("PageNum") = _pageNum
End Sub

If the tvFolders tree-view has not been filled in, we call our host service's GetFolders method from the WHSMailService property as defined above.  The method then enumerates through the hierarchical list of folders returned, building the same tree structure that exists in Outlook.  The Text property of each TreeNode is set to the folder name with a count of messages unread and total count of messages.  The Value property of the node is set to the unique MAPI-defined EntryID so that we can later grab that value to return the list of messages from that specific folder.

Next, we call our service's GetMessages method to return messages from the Inbox.  We pass in the value from the grid view's PageSize property and a member variable named _pageNum.  This will handle our paging scheme as we discussed earlier.  Once that list of messages is returned, is it bound to the data grid for display.

Finally, the default page number and folder entry ID (0 and "" respectively) are stored in the ViewState so they can be assigned back to our member variables on each page request.

If you were to run the application right now, you would see the same page that you do above, minus the message text at the bottom.

Next, we will implement what occurs when a user clicks on a folder in the tree view.  We simply pull out the MAPI unique EntryID which is stored in the Value field of the selected node, assign it to the local member variable and view state, and then call the GetMessages method from our service to return a list of messages from that folder, just as we did with the inbox above.  The code below shows the process:

C#

protected void tvFolders_SelectedNodeChanged(object sender, EventArgs e)
{
    // new folder, so reset the view
    _pageNum = 0;
    _folderEntryID = this.tvFolders.SelectedNode.Value;
    ViewState["FolderID"] = _folderEntryID;

    List<Email> list = GetMessages();
    BindMessages(list);
}

VB

Protected Sub tvFolders_SelectedNodeChanged(ByVal sender As Object, ByVal e As EventArgs)
    ' new folder, so reset the view
    _pageNum = 0
    _folderEntryID = Me.tvFolders.Selecte