Cross-thread operations with PostSharp

When you try to inform User Interface (UI) about the background operation progress or completion, you can not do it from the background thread.

UI doesn’t like to be informed about anything from a different thread: you’ll get “System.InvalidOperationException: Cross-thread operation not valid: Control ‘xxx’ accessed from a thread other than the thread it was created on.” exception from WinForms control, if you try:

public void ShowStatus(ApplicationStatus status)
{
    this._lblServiceAddress.Text = "Connected to: "
        + status.WebServiceAddress;
    this._lblUserId.Text = "Working as: "
        + status.UserId;
}

The easiest sollution is to use BeginInvoke method on the control Control or Form:

public void ShowStatus(ApplicationStatus status)
{
    this.BeginInvoke((MethodInvoker)(() =>
        {
            this._lblServiceAddress.Text = "Connected to: "
                + status.WebServiceAddress;
            this._lblUserId.Text = "Working as: "
                + status.UserId;
        }));
}

Well, it’s fun to write this once, but if you have many operations done in background sooner or later you’d like to have something nicer. Like an attribute for example:

[ThreadAccessibleUI]
public void ShowStatus(ApplicationStatus status)
{
    this._lblServiceAddress.Text = "Connected to: " + status.WebServiceAddress;
    this._lblUserId.Text = "Working as: " + status.UserId;
}

Here’s the attribute implementation of such attribute using PostSharp 1.5:

/// <summary>
/// PostSharp attribute.
/// Use it to mark Control's methods that
/// are invoked from background thread.
/// </summary>
/// <remarks>
/// Be careful as BeginInvoke uses the message queue.
/// This means that the interface will be refreshed
/// when application has a chance to process its messages.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
[Serializable] // required by PostSharp
public class ThreadAccessibleUIAttribute : OnMethodInvocationAspect
{
    public override void OnInvocation(
        MethodInvocationEventArgs eventArgs)
    {
        Control control = eventArgs.Instance as Control;
        if (control == null)
            throw new ApplicationException(
                "ThreadAccessibleUIAttribute" +
                "can be applied only to methods on Control class");

        // The form may be closed before
        // this method is called from another thread.
        if (control.Created == false)
            return;

        control.BeginInvoke((MethodInvoker)eventArgs.Proceed);
    }
};

Download email attachments in .NET

.NET framework does not contain classes that allow access to email servers (SmtpClient can only send messages). Having this in mind, the first thing you’ll need is an .NET IMAP component or .NET POP3 component to download emails from the server.

IMAP and POP3 are protocols that allow communication with email servers, like Exchange or Gmail, and download email messages. IMAP is more robust, as it allows searching and grouping emails into folders. You can see IMAP vs POP3 comparision here.

The email attachments are downloaded along with the email message. Attachments are stored within the email as part of a mime tree. Usually Quoted-Printable or Base64 encoding is used. This is why apart of an IMAP/POP3 client, MIME parser is needed. Mail.dll is going to parse such MIME tree for you and expose all attachments as well-known .NET collections. Of course all other email properties, like subject, date, recipients and body, are also available.

IMail (class that represents an email after it was downloaded and parsed) uses 4 collections for storing attachments:

  • IMail.Attachments – all attachments (includes Visuals, NonVisuals and Alternatives).
  • IMail.Visuals – visual elements, files that should be displayed to the user, such as images embedded in an HTML email.
  • IMail.NonVisuals – non visual elements, “real” attachments.
  • IMail.Alternatives – alternative content representations, for example ical appointment.

Below you’ll find samples of how you can save all attachments to disk using C# and VB.NET via POP3 and IMAP protocols.

Download attachments from IMAP server

// C#

using(Imap imap = new Imap())
{
	imap.Connect("imap.example.com");   // or ConnectSSL for SSL
	imap.UseBestLogin("user", "password");

	imap.SelectInbox();
	List<long> uids = imap.Search(Flag.All);

	foreach (long uid in uids)
	{
		IMail email = new MailBuilder()
			.CreateFromEml(imap.GetMessageByUID(uid));

		Console.WriteLine(email.Subject);

		// save all attachments to disk
		foreach(MimeData mime in email.Attachments)
		{
			mime.Save(mime.SafeFileName);
		}
	}
	imap.Close();
}
' VB.NET

Using imap As New Imap()
	imap.Connect("imap.example.com")   ' or ConnectSSL for SSL
	imap.UseBestLogin("user", "password")

	imap.SelectInbox()
	Dim uids As List(Of Long) = imap.Search(Flag.All)

	For Each uid As Long In uids
		Dim email As IMail = New MailBuilder()_
			.CreateFromEml(imap.GetMessageByUID(uid))

		Console.WriteLine(email.Subject)

		' save all attachments to disk
		For Each mime As MimeData In email.Attachments
			mime.Save(mime.SafeFileName)
		Next
	Next
	imap.Close()
End Using

Download attachments from POP3 server

// C# 

using(Pop3 pop3 = new Pop3())
{
	pop3.Connect("pop3.example.com");   // or ConnectSSL
	pop3.UseBestLogin("user", "password");

	foreach (string uid in pop3.GetAll())
	{
		IMail email = new MailBuilder()
			.CreateFromEml(pop3.GetMessageByUID(uid));

		Console.WriteLine(email.Subject);

		// save all attachments to disk
		foreach(MimeData mime in email.Attachments)
		{
			mime.Save(mime.SafeFileName);
		}
	}
	pop3.Close();
}
' VB.NET

Using pop3 As New Pop3()
	pop3.Connect("pop3.example.com") ' or ConnectSSL
	pop3.UseBestLogin("user", "password")

	For Each uid As String In pop3.GetAll()
		Dim email As IMail = New MailBuilder() _
      			.CreateFromEml(pop3.GetMessageByUID(uid))

		Console.WriteLine(email.Subject)

		' save all attachments to disk
		For Each mime As MimeData In email.Attachments
			mime.Save(mime.SafeFileName)
		Next
	Next
	pop3.Close()
End Using

Accessing attachment’s data

You can also save attachment to stream (using MimeData.Save(Stream stream) method), get direct access to attachments data as stream (using MemoryStream MimeData.GetMemoryStream() method) or even as a byte array (using byte[] MimeData.Data).

Process emails embedded as attachments

In many situations you’ll receive a message that has another message attached to it. You can use Mail.dll to extract all attachments from all inner messages no matter on how deep the embedding level is.

FindAll, ConvertAll are your friends

Let’s take a look at following code:

public List<string> GetDeleteWarnings_ForEach()
{
    List<string> messages = new List<string>();
    foreach (ItemReference reference in _itemReferences)
    {
        if (!reference.CanBeDeleted)
        {
            messages.Add(reference.DeleteMessage);
        }
    }
    return messages;
}

Using FindAll and ConvertAll you can do it in one, very obvious, line:

public List<string> GetDeleteWarnings_Fluently()
{
    return _itemReferences
        .FindAll(x => !x.CanBeDeleted)
        .ConvertAll(x => x.DeleteMessage);
}

Uploading emails using IMAP

Uploading emails in .NET to the IMAP server is fairly easy with Mail.dll IMAP library for .NET

Uploading new email to IMAP

First sample shows how to create new email message and upload it to specified IMAP folder. As usual we’ll be using MailBuilder class to create new email. It’s going to be a simple text message from Alice to Bob:

MailBuilder builder = new MailBuilder();
builder.Subject = "subject";
builder.From.Add(new MailBox("alice@email.com", "Alice"));
builder.To.Add(new MailBox("bob@email.com", "Bob"));
builder.Text = "This is plain text email";
IMail email = builder.Create();

Next we’ll connect to IMAP server and upload this email. Of course you can send this email before uploading.

using (Imap imap = new Imap())
{
    imap.Connect("imap.example.com");    // or ConnectSSL for SSL
    imap.UseBestLogin("user", "password");

    // The name of the folder depends on your IMAP server
    imap.UploadMessage("[Gmail]/Sent Mail", email);

    imap.Close();
}

You may choose any folder for your upload. However in the most common scenario, you’ll want to upload message to the Sent folder. If your server supports XLIST, SPECIAL-USE extension or at least follow common naming conventions CommonFolders may help you:

CommonFolders folders = new CommonFolders(imap.GetFolders());
imap.UploadMessage(folders.Sent, email);

Here you can find more details on obtaining common IMAP folders

Here’s the VB .NET version of the above code:

' VB.NET code

' Create new mail message
Dim builder As New MailBuilder()
builder.Subject = "subject"
builder.From.Add(New MailBox("alice@email.com", "Alice"))
builder.[To].Add(New MailBox("bob@email.com", "Bob"))
builder.Text = "This is plain text email"

Dim email As IMail = builder.Create()

Using imap As New Imap()
	imap.Connect(""imap.example.com")    ' or ConnectSSL for SSL
	imap.UseBestLogin("user", "password")

	' The name of the folder depends on your IMAP server
	imap.UploadMessage("[Gmail]/Sent Mail", email)

	imap.Close()
End Using

Uploading existing email to IMAP

In the second example we’ll upload an existing email in eml format from disk to the IMAP server in .NET.

*.eml extension is a standard extension used for storing emails. Eml file contains raw data received from IMAP or POP3 server. You can use GetMessageByUID on Pop3 or Imap class to obtain those data. It can be also created using IMail.Render method.

Eml file includes, email message body in plain text, HTML (if defined) formats, all email headers (such as: from, to, subject, date and so on), and all visual elements and all attachments.

// C# code

using (Imap imap = new Imap())
{
    imap.Connect("server");  // or ConnectSSL for SSL
    imap.Login("user", "password");

    byte[] eml = File.ReadAllBytes("email.eml");

    // The name of the folder depends on your IMAP server
    imap.UploadMessage("[Gmail]/Sent Mail", eml);

    imap.Close();
}
' VB.NET code

Using imap As New Imap()
	imap.Connect("server")  ' or ConnectSSL for SSL
	imap.Login("user", "password")

	Dim eml As Byte() = File.ReadAllBytes("email.eml")

	' The name of the folder depends on your IMAP server
	imap.UploadMessage("[Gmail]/Sent Mail", eml)

	imap.Close()
End Using

Please note that only few IMAP servers are going to send the message to the actual recipients when it is uploaded. Most servers will only store the message without sending it. You should use SMTP protocol to send email before uploading.

Dictionary: SerializationException

When you to inherit your custom dictionary from Dictionary<k,V> class you’ll get nasty exception during deserialization:

System.Runtime.Serialization.SerializationException : The constructor to deserialize an object of type ‘SafeDictionary`2[System.String,System.String]’ was not found.

[Serializable]
internal class SafeDictionary<k, V> : Dictionary<k, V>
{
    public new V this[K key]
    {
            get
            {
                V value;
                if (this.TryGetValue(key, out value) == true)
                    return value;
                return default(V);
            }
            set
            {
                base[key] = value;
            }
    }

     public SafeDictionary()
    {
    }
};

Here’s the test:

[Test]
public void IsSerializable()
{
    SafeDictionary<string,string> dictionary =
        new SafeDictionary<string, string>();
    dictionary["key"] = "value";
    dictionary =
        SerializationHelper.SerializeAndDeserialize(dictionary);
    Assert.AreEqual("value", dictionary["key"]);
}

And serialization utility class:

class SerializationHelper
{
    public static T SerializeAndDeserialize<t>(T sm)
    {
        BinaryFormatter formatter = new BinaryFormatter();
        using (MemoryStream stream = new MemoryStream())
        {
            formatter.Serialize(stream, sm);
            stream.Position = 0;
            return (T)formatter.Deserialize(stream);
        }
    }
}

Dictionary class implements ISerializable interface.

The ISerializable interface implies a constructor with the signature constructor (SerializationInfo information, StreamingContext context).

At deserialization time, the current constructor is called only after the data in the SerializationInfo has been deserialized by the formatter. In general, this constructor should be protected if the class is not sealed.

So what you need to do is to add the following constructor:

    //Needed for deserialization.
    protected SafeDictionary(SerializationInfo information, StreamingContext context)
            : base(information,context)
    {
    }

Now the test will pass.