Introduction

Equinox is an email client library targeting .NET 4.0 and Mono 2.8. It is written in C# and consists of 100% managed code. The project is currently hosted on Codeplex.

With this article I want to peek your interest and give you a sneak peek of the basic features supported by this library.

Background and Motivation

The reason for my creating yet another email client library is simple. For another open source project I required an open source library to add email support, leveraging Mono and the .NET Framework.

You might ask, why not just take one of the bazillion open source libraries floating around the web? Well, it is a valid question and I was in fact planning to do so, unfortunately reality struck hard. I tried several and most of them performed fine for 70% to 80% of all mails, but in the end all of them failed in certain aspects; It was the little things.

Some had problems with encodings, some performed poorly in parsing more complex MIME constructs, many only supported a small subset of the protocols capabilities, some just seemed wrong ;) and the feature that was neglected most of the time was an adequate implementation of the fetch command.

The fetch command is the most important and complex part of the IMAP protocol because it is the only command which produces more or less unexpected responses depending on the query and the server, which is probably the reason why most implementations don't go further than ... get the headers or get it all ... no compromises.

On first glance this seems to be enough, but if you take a closer look you will see that there's more going on.

It is for instance pretty useful to know if a mail has been read or not, for this we need the message flags. Sometimes it is nice to know the size and layout of the message before actually downloading the message or to know whether it has attachments or not.

Some might say it would be great to be able to read the 3 lines of plain text without having to download the 20MB attachment or check for a specific header without having to fetch all 34.

Some of those queries are not hard to create and/or parse by hand unless you combine them into a single, since different servers will return your items in different orders depending on the query and the server implementation.

In addition the IMAP protocol has some nasty surprises since the server can include status updates into most responses including fetch arbitrarily. I'm not saying it is impossible, but I already had the pleasure ending up inside a forest full of regular expressions and string comparisons.

To have more control over the sequence we could fetch every item separately, which would result in easier parsing but would be inefficient in terms of net traffic. This is not a problem if we are talking about a single message, but imagine 50, 100, 1000 or even more messages, I can assure you there will be a noticeable difference in speed if we have to perform 5 queries for each message instead of 1.

By itself I would not consider these issues critical to the war effort, but they accumulate and at some point you'll just notice that it won't work as intended.

All in all, I just wasn't satisfied with the solutions out there; therefore I took it upon myself to develop yet another email client library trying to address some of those issues.

Assemblies

The library is separated into four main assemblies.

  • Crystalbyte.Equinox.Core
  • Crystalbyte.Equinox.Mime
  • Crystalbyte.Equinox.Imap
  • Crystalbyte.Equinox.Smtp (not yet included in current release)

The MIME assembly holds the standalone MIME parser, obviously. The parser is capable of creating an entity object model from a MIME string and vice versa. This assembly has no dependencies apart from the .NET Framework and can therefore be used without any of the other assemblies.

The IMAP and SMTP assemblies should be self explanatory, they hold the respective client classes. Both assemblies do not depend on each other. The core assembly merely exists to reduce redundancy since both clients share a lot of code.

Using the code

Since the Codeplex documentation is not yet up to date I will introduce the structure and basic usage of the IMAP client in this article. Once the SMTP part has been released I will update this article, but for now I will stick to the IMAP client.

Using the IMAP client

Lets create an instance and log us in.

using Crystalbyte.Equinox.Security;
using Crystalbyte.Equinox.Imap;

using(var client = new Client())
{
    var host = "foo.bar.com";
    var port = Ports.Default; // 143 (993 for SSL)
    client.Connect(host, port);
    client.Login("name","pass");
} // Dispose() => client.Logout(); client.Disconnect();

Like with most clients the strongest supported SASL mechanic will be used to authenticate the client to the server, so there is no need to fiddle with those.

Encryption is off by default and can be enabled by changing the

client.Security = SecurityPolicies.Implicit; // STARTTLS
client.Security = SecurityPolicies.Explicit; // SSL

property before connecting.

When leaving the scope of the using block the client will automatically log us out and close the connection gracefully. If you don't have the luxury of a using block u can of course call the appropriate methods manually.

Once we are logged in we can call other methods on the client. I'm not going to present the basic IMAP methods here since they all carry the name of the corresponding IMAP commands and can be looked up all over the net including here on CodeProject or here.

In fact there is only a single aspect to this class that differs from other clients which I will talk about next.

LINQ to IMAP

Although the client has a regular Search() and Fetch() method I would not recommend those for anything but the most simple requests. I previously talked about addressing some of the issues I criticized earlier. One of those were inflexible or incomplete implementations of the fetch command.

To address this I implemented a LINQ provider that enables us to fetch messages or parts of messages directly from the server. There are two advantages to this solution. First, no matter how complex the query is or how many items we request it will all be done in a single stroke using one fetch command.

In addition we don't have to parse or map any responses manually since this is taken care of by the LINQ provider. If you are familiar with LINQ you should have no problem using this part of the library.

So lets fetch something ... for instance the Envelope, the Uid, the Flags and the Size from all unread messages that arrived last week. Before we do that we need a container to store all this arbitrary data.

class MyContainer
{
    Envelope envelope { get; set; }
    int Uid { get; set; }
    MessageFlags Flags { get; set; }
    Size Size { get; set; }
}

Next we create the query.

var query = client.Messages.Where(x => x.Date > DateTime.Today.AddDays(-7) 
&& !x.Flags.HasFlag(MessageFlags.Read)).Select(x =>new MyContainer
{
    Envelope = x.Envelope,
    Uid = x.Uid,
    Flags = x.Flags,
    Size = x.Size
});

Finally we execute the query by enumerating through the results.

foreach(var container in query)
{
    Debug.WriteLine(container.Envelope.Subject);
}

As with many LINQ providers there are limitations and restrictions because I have to work within the boundaries of the IMAP protocol. For instance multiple or nested Where/Select statements are not permitted.

To be more precise there must always be one Where and one Select clause. With a few exceptions none of the other extension methods like Any(), Single() or SelectMany() are supported. A more detailed list of supported and unsupported methods as well as instructions for common fetch scenarios will be posted on Codeplex soon, but getting into the do's and don't in handling custom LINQ providers is out of the scope of this article.

Let's fetch some more ...

Although everything can, not everything should be fetched using the LINQ engine. When you have decided to download the complete message without any omissions the client has two methods which will accomplish the task.

var message = client.FetchMessageByUid(187); 
var message = client.FetchMessageBySequenceNumber(10);

Earlier I talked about downloading separate parts of a message, remember? Well... for this to work we need the body structure, since it contains a compact version of the message's entity object model.

There are four types of interest: views, attachments, nested messages and related objects (embedded stuff and such). So lets fetch the body structure for the first 20 mails in the mailbox.

var query = from m in client.Messages
            where m.SequenceNumber >= 1 && m.SequenceNumber <= 20
            select m.BodyStructure;

Lets assume the fifth message has a nested message which contains a single attachment, let's get it.

var structures = query.ToList();
var handle = structures[4].Children[0].Attachments[0];

var attachment = client.FetchAttachment(handle);

Similar methods are available for Views, Messages and RelatedObjects. If you take a look at the code you will see that these methods also utilize the LINQ engine to download it's message parts. 

At this point I feel obligated to tell you that this is the first release of the software and considered Alpha. The body struct parser still has some issues, if you have ever tried to parse the body structure by hand you must have noticed understanding it is a little bit like trying to understand code written in Brainf*ck. The parser is implemented using Coco\R which, by the way, is a great piece of software, but creating a parser based on an EBNF grammar is not trivial.

Threading 

You might have noticed that there is not a single ...Async method or Begin ... End ... pattern implementation. I decided against implementing asynchronous behavior since I found it be a trivial detail only obstructing the code. In essence all we need to do is surrounding our code with

new Thread(
    () => {// Do synchronous stuff }
).Start();

Perhaps some will say, but wait... there is a lot more to consider when using asynchronous calls - and I have to agree, but those concerns won't disappear just by implementing asynchronous methods into the client.

Epilogue

That's pretty much it, first of all I thank you for your interest, apparently you managed to arrive here without falling asleep. The source and binaries can be found on Codeplex. Here's the link again.

There is obviously more, but going into every detail of this library would make this a really long and boring article, more like a manual, if you catch my drift. I promise to update the Documentation on Codeplex to reflect capabilities and limitations soon.

Meanwhile I would welcome comments, suggestions and even constructive criticism. ;) If you found bugs or having problems using the code I would appreciate it if you could post issues and problems on the Codeplex site.

In addition to the library I implemented a demo app demonstrating the most important features and capabilities of the IMAP client which is included in the binary release package on Codeplex.

It is important to note that the app does not run on a Mac OSX although the library does. The demo app uses the WebBrowser control to display HTML views, which is not (yet) supported on Mac OSX by Mono.

I have to admit, I have not tested it on LINUX yet, I would also welcome any feedback on this topic.

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架