1. Introduction
Electronic mail (email) are one of the most commonly used means to exchange and keep private information. Although a user's emails can be read online using
web interface or offline using many free email clients, there are still times when one would like to have an email reader that can be integrated into his/her
own systems because of various reasons. For example, one may need an email display plug-in for a WPF project that is free and provide
downloading, sorting, searching, safekeeping, and reading a large amount of email messages, many of which are still not provided by any of the online and offline solutions
available to public. In addition, it has to parse and display most messages that a user can encounter. After some investigation and searching on the
internet, it can be concluded that nothing that suit the need can be found. It is time to develop one on our own.
The current package contains the source code and a simple demo program for a plug-in in Microsoft Managed Extensibility Framework (MEF)
that provides visual display of email messages. It is ready to be incorporated into any WPF based application. Since this is an initial version, emphasis was on the completeness of
information representation. The visuals and ease of use aspects will be improved in later versions of it, therefore it is not about WPF visual effects.
2. Background
Most of the email messages are composed using the Multipurpose Internet Mail Extensions (MIME) format.
It breaks the original ASCII text based messaging limitations to transmit, via SMTP,
non-ASCII characters, binary data and alternative views of a message in a single ASCII text based data block. These messages also most likely contains a
HTML formatted view part for a better presentation quality, media embedding and possibility to link to other sources.
The text based MIME message represents a tree, each of its node contains a particular part of the message . The node type and ID is used to declare
relationship between those parts of the message. It needs to be parsed to generate a MIME entity before presenting. Modern email clients can generate rather complex multimedia MIME messages
with different encodings. A good parser that fully support MIME format is needed. After a few modifications and adaptations, the corresponding MIMIE parser provide by Ivar Lumi
(click here)
was extracted from his much larger library. The resulting dll is included inside the download package.
HTML formatted messages can be displayed natively using WPF controls (namely, inside a
FlowDocument
object). But because of the complexity, to fully recreate the same
effect as they are displayed in a browser does require lengthy work, too much to be justified for our purposes. They are therefore to be displayed in a web browser hosted inside a WPF
control. Web browsers do not accept in memory data as input, the data must either be send via a network socket or using a local file. Using local file has a few risks:
- It fills disk space by temporary data and potentially too much fragment in it.
- It exposes more security weaknesses and causes privacy leak concerns.
- It does not taste that good.
- etc.
It was decided to use a embedded HTTP server to send the in-memory data over to the browser to display.
A full fledged HTTP server can be very complicated to develop because it has to implement full HTTP protocol, handle scalability, extensibility,
concurrency, cache management, etc.. But for our single user and embedded usage context, what is needed is just a partial implementation of HTTP
protocol, turn the cache off, and ignore all other concerns. It is in fact rather simple to develop, especially when there is even a
HttpListener
class in .NET 4.0.
3. The Entry Point, Event, Delegate
Let's introduce the interface using which a host that can interact with the library. A UI library exposes not only
the API, but
also events and callbacks (delegates). It
is to this end that the current section is supposed to be read.
The entry point for the current library is a message body display panel supported by a type called
EMailPresenter
. It implements IEMailReader
.
public interface IMessageReader
{
Uri SourceUri { get; set; }
}
public delegate bool QueryMessageThreadHanlder(string MsgID, out ThreadedMessage StartMsg);
public interface IEMailReader : IMessageReader
{
QueryMessageThreadHanlder QueryThread
{
get;
set;
}
}
which is defined in a separated lib called IFileSystemVShell
, which should be shared by the host and the
current library. Shown below is the top part of the EMailPresenter
class definition that implement the interface:
namespace CryptoGateway.Documents.Mime
{
[Export(typeof(IEMailReader))]
public partial class EMailPresenter : UserControl, IEMailReader
{
public Uri SourceUri
{
get { return (Uri)GetValue(SourceUriProperty); }
set
{
if (SourceUri == null && value != null || SourceUri != null && value == null
|| SourceUri.ToString() != value.ToString())
SetValue(SourceUriProperty, value);
if (prev_uri == value.ToString())
return;
prev_uri = value.ToString();
if (IsAlreadyExpanded || !IsExpandedViewOpened)
{
if (IsAlreadyExpanded)
{
if (!ThreadSelection && RootModel.ThreadViewer != null
&& !RootModel.ThreadViewer.IsInThread(value.LocalPath))
RefreshThreadPending = true;
}
DisplayMsg();
}
else
RootModel.ReaderWindow.SourceUri = value;
}
}
public static readonly DependencyProperty SourceUriProperty =
DependencyProperty.Register("SourceUri", typeof(Uri), typeof(EMailPresenter),
new UIPropertyMetadata(null, (o, e) =>
{
(o as EMailPresenter).SourceUri = e.NewValue as Uri;
}));
...
...
...
...
}
}
- The attribute "
[Export(typeof(IEMailReader))]
" exports the control as a plug-in inside the MEF framework.
- Property "
SourceUri
" implements the corresponding interface declared inside
IEMailReader
.
The host can simply set a valid Uri for it to have the embedded HTTP server to send intended data to the browser to be displayed. Only when
SourceUri
refers to a (MIME message) file on a
user's local file system is handled in the current version of the library. A user can easily
extend it so it can load from other sources, like from a HTTP or FTP server. The extension can be done by modifying a single method, namely:
private void DisplayMsg()
{
if (SourceUri == null)
{
ClearView();
return;
}
if (File.Exists(SourceUri.LocalPath))
{
StreamReader sr = new StreamReader(SourceUri.LocalPath);
Data.RawText = sr.ReadToEnd();
sr.Close();
byte[] bf = Encoding.ASCII.GetBytes(Data.RawText);
DisplayMsg(bf);
}
else
ClearView();
}
of EMailPresenter
class. This is the single point inside the library where an MIME message is loaded before been parsed to generate an MIME entity (tree).
The Event TransCompSelectFolderEvent
is raised inside the current library when it needs to ask a user to select
a folder. For example, inside the EMailAttachments
control which handles MIME attachments. Instead of providing a folder selection dialog itself,
the library raises an event to let the host to decide. This is because WPF does not has a folder selection dialog built in, so an custom folder selection dialog has to be
used, like the one provided by WinForms. Making the current library depending on an extra custom library, like WinForms, will cause it to depend on something it need not to.
So it is delegated to the hosting environment in which a user could have other preferred folder selector than the WinForms one, like in our FileSystem SQLizer System.
A user who like to handle the event should capture it somewhere inside the visual tree containing the plug-in.
QueryMessageThreadHanlder
delegate. The library is supposed to display a single message provided by the host. It has no idea
about the set of messages the current displaying message belongs to. This delegate is used to query the host, which hold a complete set of MIME messages, for message threading information.
4. WPF Controls
The user control EMailPresenter
has multiple appearances, depending on the particular message under viewing and on whether the display is
in brief or detailed mode. Besides some control buttons, it only contains a
ContentPresenter
control, which is late-binded to a DataTemplate
selected from the resource
library defined inside it Resources property:
<UserControl x:Class="CryptoGateway.Documents.Mime.EMailPresenter"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:loc="clr-namespace:CryptoGateway.Documents.Mime"
xmlns:res="clr-namespace:CryptoGateway.Documents.Mime.Properties"
xmlns:avalonedit="http://icsharpcode.net/sharpdevelop/avalonedit"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300" Loaded="OnLoaded"
Initialized="OnInitialized">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/ControlStyles.xaml" />
</ResourceDictionary.MergedDictionaries>
<DataTemplate x:Key="A">
<TabControl>
<TabItem Header="{Binding TextHeaderStr}">
<TextBox Text="{Binding PlainText}" IsReadOnly="True" Background="White"
TextWrapping="Wrap"
VerticalScrollBarVisibility="Auto" />
</TabItem>
<TabItem Header="{Binding RawHeaderStr}">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
VerticalAlignment="Top" >
<loc:LargeTextBox Text="{Binding RawText}" />
</ScrollViewer>
</TabItem>
<TabItem Header="{Binding OtherAspectsHeaderStr}">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"
VerticalAlignment="Top" >
<loc:EMailMetaDetails DataContext="{Binding EntityRoot}"/>
</ScrollViewer>
</TabItem>
</TabControl>
</DataTemplate>
<DataTemplate x:Key="AD">
<TabControl>
<TabItem Header="{Binding TextHeaderStr}">
....
</TabItem>
...
</TabControl>
</DataTemplate>
</UserControl.Resources>
...
</UserControl>
where each appearance is identified by the resource Key ("A", "AD", ...) above. The library select the right one for each message that is going to be displayed.
The display contains tab pages, in the most complete display, the following views about the MIME message are displayed:
Figure: Correlated message threading
Figure: Attachment preview
- Plain text view, if available.
- HTML view if available.
- Raw data view. It contains the raw message data for a knowledgeable or inquisitive user to inspect.
- Others, which contains a trace information about sending agent, attachments, etc., contained inside the message. This is one of the
extension point where future release of the library will add more information into it.
EMailPresenter
, which is an embedded user control for brief viewing, opens the full reader
EMailReader
.
It contains:
EMailThreadView
is a control that correlates MIME messages in an conversation process between a user and one of his/her peer.
Given an arbitrary message in a related set of messages, it displays the threading of MIME messages in a tree view
that represents which message a particular message is replying to and the set of messages that replied that message. Such a functionality is quite
often seen in a public forum, but is lacking in any email clients known to the author. Gmail can display such correlation, but in a flat fashion that confuses
at least the author. In even a two person conversation, such a tree can be very complex but is hiding from a user so far.
QueryMessageThreadHanlder
is used to query the host for the initial message and the entire correlation tree information (see the
figure on the right) carried by an object of type ThreadedMessage
, given the message id of the current message.
The implementation of the handler involves recursive traversal of the set of message that is related to the current message. It will not be discussed further here.
- Message header information area where the basic MIME message header information is displayed.
EMailPresenter
which displays the body of the message.
EMailPresenter
contains the EMailMetaDetails
,
EMailAttachments
, and LargeTextBox
user controls.
EMailAttachments
contains a list view on top and a multimedia preview panel at the bottom.
Current types of multimedia data that can be previewed are only those for image data varieties. It emits a TransCompSelectFolderEvent
event when a user tries to save one of the attachments, which has to be handled by the host to answer the question: "where to save the data?".
private void OnSaveAttachment(object sender, RoutedEventArgs e)
{
Button btn = e.OriginalSource as Button;
if (!(btn.DataContext is MimeWrapper))
return;
MimeEntity de = (btn.DataContext as MimeWrapper).Entity;
if (de != null)
{
TransCompSelectFolderEventArgs ex = new TransCompSelectFolderEventArgs(
ComponentEvents.TransCompSelectFolderEvent);
ex.EventTitle = Properties.Resources.AttachmentSaveDirSelWord;
ex.InitialFolderPath = LastDepositFolder;
RaiseEvent(ex);
if (ex.Handled && ex.IsSelectionMade &&
!string.IsNullOrEmpty(ex.SelectedFolderPath))
{
if (Directory.Exists(ex.SelectedFolderPath))
{
LastDepositFolder = ex.SelectedFolderPath;
SaveFile(ex.SelectedFolderPath,
de.ContentDisposition_FileName, de);
}
else if (ex.SelectedFolderPath.LastIndexOf('\\') != -1)
{
string dir = ex.SelectedFolderPath.Substring(0,
ex.SelectedFolderPath.LastIndexOf('\\') + 1);
if (Directory.Exists(dir))
SaveFile(dir,
ex.SelectedFolderPath.Substring(dir.Length), de);
}
}
}
}
LargeTextBox
for display raw text data of an MIME message. Because some MIME messages contain multimedia data, their size could be significant.
The WPF TextBlock
control is not suitable to display large text data. Here the WPF FlowDocument
control is used as it was found to perform quit well even
when the text block loaded into it is very large.
EMailMetaDetails
contains EMailAttachments
to handle attachments, routing information and Other information about a message.
- Replying message information.
- Message routing information.
- An extension slot for more information extractable from the message MIME entity.
5. Serving HTTP Requests
5.1 The Server
The embedded HTTP server, which is implemented in one class called HttpContentServer
, is controlled by its host (the present library).
The "Start" method generates random port from 10000 to 60000, starts the "server" of type
HttpListener
and starts a thread, the "ServerThread
"
thread, which waits for the incoming requests.
The method "ServerThread
" contains an infinite loop that passes the HTTP context object generated by the "server" to a GET handler.
When in error, the server returns 500 status code.
private static void ServerThread()
{
IsServing = true;
serverstarting = false;
HttpListenerContext ctx = null;
while (!stopserving)
{
try
{
ctx = server.GetContext();
if (ctx.Request.HttpMethod != "GET")
{
Return500(ctx);
continue;
}
HandlerRequest(ctx);
}
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
Return500(ctx);
}
}
IsServing = false;
}
private static void Return500(HttpListenerContext ctx)
{
HttpListenerResponse resp = ctx.Response;
resp.StatusCode = 500;
}
5.2 Protocol Implementation
HTTP protocol requires a full server to handle GET, POST, HEAD, PUT,
DELETE, TRACE, and CONNECT methods. In our application scenario, only
GET will allowed to be used by the client. Other kind of requests will be ignored or treated as an error.
There are a rich set of status to reflect different state of the server in response to an request. The simple
server shown here has only three states:
- Success (code 200),
- Not found (code 404), and
- Error (code 500).
It certainly can be expanded to have more states to be handled by the client in the future.
The server has two modes "Message" and "Data", it is to be set by the host before serving data.
- Message mode: When serving top level MIME message part (MIME node), the server is set to Message mode.
- Data mode: When serving multimedia data that is not contained in a MIME entity for a message, the server should be set to
Data mode and with
MediaBuffer
being initialized and loaded by the host.
private static void HandlerRequest(HttpListenerContext ctx)
{
HttpListenerRequest req = ctx.Request;
HttpListenerResponse resp = ctx.Response;
if (State == HttpContentServerState.Data)
{
string ct = ContentTypeString;
if (ct.IndexOf(';') != -1)
ct = ct.Substring(0, ct.IndexOf(';'));
resp.StatusCode = 200;
resp.Headers.Add("Cache-Control: no-cache");
resp.ContentType = ct.ToLower();
resp.ContentLength64 = MediaBuffer.Length;
SendData(resp, MediaBuffer);
}
else
ServeMessage(ctx);
}
After specifying the correct HTTP Content-Type, the data is sent back to the browser to have it displayed.
For a simple server, a user can let the HttpContentServer
to add other HTTP response headers accordingly.
5.3 Server Data Source
Normal the initial data for a http server in preparing a response (to a request) is most likely a pre-existing file on the local file system of the server. The present
embedded http server obtains the data from a set of common buffers/objects that are updated for each different request. The host is responsible for loading the correct data
into the them, depending on the mode of the server:
- Message mode: the field "
CurrentEntity
" MIME entity (tree) should be updated to correspond to the current message.
- Data mode: the field "
MediaBuffer
" buffer should be updated to correspond to the current media item.
5.4 Browser Cache Off
HTTP server can dictates how a client is going to save the returned data in its cache by using Cache-Control
header. In out present application scenario, the client (browser) should not be allowed to cache any data, so the following response header is added:
resp.Headers.Add("Cache-Control: no-cache");
for all responses.
This is because our initial request uri is always '/' for all initial requests (each subsequent request can has an identifiable
URI, please browse the code for such
details), in which case the browser will never know when to use the cached content and when not to.
5.5 MIME Related
MIME format specification is a too large topic to be discussed here. However, a few points that are relevant to the current topic is given below:
- Message decoding problems. There are email clients that make mistakes in specifying the character encoding, like misspelling of the encoding names.
A user can fix these problems by specifying transformation rules inside "LumiSoft.Net.Mime.dll.config" configuration file under the section
"
userSettings/LumiSoft.Net.Properties.Settings
" where the setting
's name is "EncodingMapps
".
The format of the transformation rule is
- [rule] := [wrong names] '->' {correct name}(';' [wrong names] '->' {correct name})*
- [wrong names] := {wrong name}(',' {wrong name})*
Here those items inside [] are none-terminal nodes, those items inside {} are place holders for string values. {wrong
name} is a place holder for the encoding name in error and {correct name} is the place holder for the correct encoding name, and those items inside
single quote '' are concrete string values. A regular expression like syntax is used for pattern repetition, namely ()*
stands for the pattern in brackets can be repeated zero to any time.
To make it less formal, the included sample configuration file contains an example for one rule, which is currently used in our related products. More rules can be added by
adding string ";" followed by similar rule to the existing one. The wrong names are a list of wrong name separated by ",".
- Problems in displaying none-ASCII characters. There are homebrew email clients that does not follow the MIME specifications by embedding non-ASCII
texts directly into the message body and/or message header. Since the user is most like to download a message using a client that talks to the server in
the so called Post Office Protocol (POP), only ASCII character can be transmitted reliably, the downloaded message
will most likely to be irreversibly corrupted, which can not be recovered. The reason they can be displayed correctly in a web based interface is
because modern web servers do not have to retrieve the message using the POP3 protocol.
For a user having a question to the effect that "how to traverse the MIME entity to display the message?", he/she could find part of the answer inside the HTTP server class.
6. Using the code
Included is a simple demo WPF programs using which a user can load a pre-existing MIME message on his/her hard disk for an visual display. There is no message header
information on top since these information are supposed to be supplied by the message list host (of it) in our application scenarios. For a fuller display, a
user can click the expansion button on the top right corner to open the reader.
Figure: Reading subscribed email messages from the CodeProject
To use the present library in a user's own code, one must compile the library first and then place the out put
DLLs in a plug-in folder that the user find
suitable. The host of the library need to have a way to find or define the plug-in directory and load it, like what is done in the following:
public partial class AppWindow : Window
{
[Import(typeof(IEMailReader))]
private IEMailReader reader
{
get;
set;
}
public AppWindow()
{
InitializeComponent();
AddHandler(ComponentEvents.TransCompSelectFolderEvent,
new TransCompSelectFolderEventHandler(OnDepositionSelectFolder));
string pluginDir = ...;
if (System.IO.Directory.Exists(pluginDir))
LoadReader(pluginDir);
}
....
private void LoadReader(string pluginDir)
{
DirectoryCatalog categ = new DirectoryCatalog(pluginDir);
string cname =
AttributedModelServices.GetContractName(typeof(IEMailReader));
System.Linq.Expressions.Expression<Func<ExportDefinition, bool>>
exp = a => a.ContractName == cname;
ImportDefinition id = new ImportDefinition(exp,
cname, ImportCardinality.ExactlyOne, true, true);
List<Tuple<ComposablePartDefinition, ExportDefinition>>
_plugins = categ.GetExports(id).ToList();
if (_plugins.Count == 1)
{
var cc = new CompositionContainer(categ);
cc.ComposeParts(this);
reader.QueryThread = OnQueryThread;
}
else if (_plugins.Count == 0)
{
creader.Visibility = System.Windows.Visibility.Collapsed;
System.Windows.MessageBox.Show("Failed to find any plug-in!");
}
}
...
...
private bool OnQueryThread(string MsgID, out ThreadedMessage StartMsg)
{
...
StartMsg = ...;
return true;
}
...
...
}
where the event TransCompSelectFolderEvent
handler is attached and message thread query delegate
QueryThread
is attached.
7. Point of Interest
Writing a MIME reader is not hard in the way it is presented here. What's harder is how to deal with the large amount emails coming from various sources today.
The idea of using a small single purpose embedded HTTP server can be applied to many other HTML base offline dynamic or non-file based content display system as well.
8. Final Note
The demo program is only for a demonstration on how to interact with the plug-in library. For that reason it was kept
as simple as possible to show the points. To have an email reader application that can be put to real use, one need to write his/her own host and hopefully
use this plug-in as the message display means.
It is welcomed to modify the library for your own needs, but it is also appreciated if your modifications and/or improvements can be feedback to the author.
For a user who don't have something handy to download emails to test it, although he/she absolutely does not have to since it was developed by us, he/she still has an option to install this freeware to accomplish it. The said program actually use this plugin to display emails.