Download emails with SQLServer

In this blog post I’ll describe how to use Mail.dll .NET email component with SQL Server to download emails.

First, you need to create a regular .NET assembly with definition of SQL stored procedure. Remember to add reference to Mail.dll.

In this example the assembly name is SqlEmailDownloader.dll:

// C#

using System.Data;
using Microsoft.SqlServer.Server;
using Limilabs.Client.IMAP;
using Limilabs.Mail;

public class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void GetEmails()
    {
        SqlDataRecord record = new SqlDataRecord(new[]
             {
                 new SqlMetaData("UID", SqlDbType.BigInt),
                 new SqlMetaData("Subject", SqlDbType.NVarChar, 128)
             });

        using (Imap client = new Imap())
        {
            client.Connect("server"); // or ConnectSSL
            client.UseBestLogin("user", "password");
            client.SelectInbox();

            SqlContext.Pipe.SendResultsStart(record);
            foreach (long uid in client.Search(Flag.Unseen))
            {
                var eml = client.PeekHeadersByUID(uid);
                IMail email = new MailBuilder().CreateFromEml(eml);

                record.SetSqlInt64(0, uid);
                record.SetSqlString(1, email.Subject);
                SqlContext.Pipe.SendResultsRow(record);
            }
            client.Close();
            SqlContext.Pipe.SendResultsEnd();
        }
    }
} ;

The code above:

  • connects to the IMAP server,
  • finds all unseen emails,
  • downloads headers and
  • passes the results to SQL server.

Here’s the VB.NET version:

' VB.NET

Imports System.Data
Imports Microsoft.SqlServer.Server
Imports Limilabs.Client.IMAP
Imports Limilabs.Mail

Public Class StoredProcedures
	<microsoft.SqlServer.Server.SqlProcedure> _
	Public Shared Sub GetEmails()
		Dim record As New SqlDataRecord(New () { _
			New SqlMetaData("UID", SqlDbType.BigInt), _
			New SqlMetaData("Subject", SqlDbType.NVarChar, 128)})

		Using client As New Imap()
			client.Connect("server") ' or ConnectSSL
			client.UseBestLogin("user", "password")
			client.SelectInbox()

			SqlContext.Pipe.SendResultsStart(record)
			For Each uid As Long In client.Search(Flag.Unseen)
				Dim eml = client.PeekHeadersByUID(uid)
				Dim email As IMail = New MailBuilder() _
					.CreateFromEml(eml)

				record.SetSqlInt64(0, uid)
				record.SetSqlString(1, email.Subject)
				SqlContext.Pipe.SendResultsRow(record)
			Next
			client.Close()
			SqlContext.Pipe.SendResultsEnd()
		End Using
	End Sub
End Class

In SQL Server Management Studio you need to execute following script (HelloWorld is a sample database):

USE HelloWorld
GO

EXEC sp_configure 'clr enabled' , '1'
GO
RECONFIGURE
GO

Alter Database HelloWorld Set TrustWorthy On
GO

CREATE ASSEMBLY [System.Windows.Forms] FROM 'C:\Windows\Microsoft.NET\Framework\v2.0.50727\System.Windows.Forms.dll'
With Permission_Set=Unsafe
GO
CREATE ASSEMBLY SqlEmailDownloaderAssembly FROM 'D:\1\SqlEmailDownloader\bin\Debug\SqlEmailDownloader.dll'
With Permission_Set=Unsafe
GO

CREATE PROCEDURE GetEmails
AS EXTERNAL NAME SqlEmailDownloaderAssembly.StoredProcedures.GetEmails
GO

Finally you can execute the procedure:

EXEC GetEmails

…and see the results:

Click here to download Mail.dll IMAP component

Better public API: GetAccountStat

Here’s some code I recently found in Mail.dll and decided to refactor.

// Before:

using(Pop3 client = new Pop3())
{
    client.GetAccountStat();
    Console.WriteLine(
        "Inbox has {0} emails.",
        client.MessageCount);
}

There are several things wrong with this code.

  • The method is called Get… but it does not get anything, it changes the internal state of the object.
  • Message count is stored in Pop3 object:
    • If the user of your API connects later to a different server you need to remember to reset this variable.
    • If the user of your API forgets to call GetAccountStat, message count is undefined (Should it be null?)

It’s really hard to say that this is a friendly API, as it requires the user to perform actions in specific order (call GetAccountStat before accessing message count).

Another problem is that GetAccountStat method is responsible for parsing the server response. It’s not necessary a bad thing, but if you have hundreds such methods then Pop3 class gets bloated with hard-to-test parsing logic.

Now lets take a look at the After code:

// After:

using(Pop3 client = new Pop3())
{
    AccountStats stats = client.GetAccountStat();
    Console.WriteLine(
        "Inbox has {0} emails.",
        stats.MessageCount);

}

Here we can see a good API:

  • Method is called Get…. and it actually gets something.
  • No specific call order is required, you simply call one method and act on the result.
  • Parsing logic was moved to the AccountStats class.

This is not seen here but AccountStats method has a static Parse method…and look how easy is to write unit test for it’s behavior:

[Test]
public void Parse_MessageCountAndMailboxSize_AreFilled()
{
    AccountStats stats = AccountStats.Parse("2 373766");
    Assert.AreEqual(2, stats.MessageCount);
    Assert.AreEqual(373766, stats.MailboxSize);
}

Note also that actually we have NOT introduced a breaking change to our public API. Following code still works:

using(Pop3 client = new Pop3())
{
    client.GetAccountStat();
    Console.WriteLine(
        "Inbox has {0} emails.",
        client.MessageCount);
}

You’ll get 2 obsolete warnings:

warning CS0618: 'Limilabs.Client.POP3.Pop3.MessageCount' is obsolete: 'Please use the return value of GetAccountStat method instead.'

warning CS0618: 'Limilabs.Client.POP3.Pop3.MailboxSize' is obsolete: 'Please use the return value of GetAccountStat method instead.'

As we marked MessageCount and MailboxSize with [obsolete] attribute, but that’s it!

POP3 vs IMAP for .NET developers

POP3 and IMAP are application-layer Internet standard protocols used by email clients to retrieve e-mails from a remote server over a TCP/IP connection. Virtually all modern e-mail clients and mail servers support both protocols as a means of transferring e-mail messages from a server.

They are both text-based and both transmit more or less the same amount of data over the network.

POP3 (Post Office Protocol 3) and IMAP (Internet Message Access Protocol) are both email retrieval protocols that allow users to access their email messages from a remote server. However, they differ in several key aspects:

Email Management

  • POP3: POP3 is primarily designed for downloading emails to a local client device (e.g., computer, smartphone). Once emails are downloaded, they are typically removed from the server, and the client becomes the primary repository for messages. This can lead to synchronization challenges when accessing emails from multiple devices.
  • IMAP: IMAP is designed for managing emails on the server itself. It enables users to view, organize, and manipulate their email messages across multiple devices while keeping them stored on the server. Changes made on one device are reflected on others, maintaining consistent access.

Message Storage

  • POP3: With POP3, emails are usually downloaded and stored locally, which can save server space but may lead to limited access to messages from different devices.
  • IMAP: IMAP keeps messages on the server, allowing users to access them from various devices. This provides greater flexibility but may require more server storage.

Offline Access:

  • POP3: As messages are downloaded locally, offline access is possible only to the messages already downloaded.
  • IMAP: IMAP provides more seamless offline access since messages remain on the server. Users can view their entire mailbox even when offline.

Email Organization

  • POP3: Organizing emails into folders or labels might be limited, as most organization is done on the local client.
  • IMAP: IMAP offers more sophisticated email organization options. Users can create, manage, and synchronize folders and labels across devices.

Synchronization

  • POP3: POP3 clients typically don’t synchronize actions (read/unread, deleted, etc.) across devices, which can lead to discrepancies between devices.
  • IMAP: IMAP provides real-time synchronization, ensuring that actions taken on one device are reflected on all others.

In summary, while both POP3 and IMAP serve the purpose of email retrieval, IMAP offers more flexibility, better synchronization, and enhanced email management across multiple devices. POP3 is simpler and can be more suitable for users who prefer to store emails locally and have limited requirements for accessing messages from different devices. When in doubt you should be choosing IMAP.

Here is the brief comparison of IMAP vs POP3:

 POP3IMAP
Intended message storeClientServer
Download messageYESYES
Download message headersYESYES
Download detailed email informationNOYES
Download specific message part (single attachment)NOYES
Delete messageYESYES
Send messageNO (use SMTP)NO (use SMTP)
Getting unseen messages only NOYES
Mark message Seen/UnseenNOYES
Server side search and sortingNOYES
FoldersNOYES
Sent itemsNOYES
SSL/TLSYESYES
Push emailNOYES

Mail.dll .NET email client

Mail.dll .NET email client library includes both .net POP3 client and .net IMAP client as well as an SMTP client. You can easy install it using nuget:

	PM> Install-Package Mail.dll

Alternatively you can download Mail.dll directly from our website.

Both clients are very similar and easy to use.

Below you can find C# IMAP sample that downloads unseen emails, parses them and displays a message subject:

using(Imap imap = new Imap())
{
    imap.ConnectSSL("imap.server.com");
    imap.UseBestLogin("user", "password");
 
    imap.SelectInbox();
 
    List<long> uids = imap.Search(Flag.Unseen);
    foreach (long uid in uids)
    {
        IMail email = new MailBuilder()
            .CreateFromEml(imap.GetMessageByUID(uid));
        string subject = mail.Subject;
    }
    imap.Close();
}

Here’s a C# POP3 sample that downloads all emails, parses them and displays a message subject:

using(Pop3 pop3 = new Pop3())
{
    pop3.ConnectSSL("pop3.server.com");
    pop3.UseBestLogin("user", "password");
 
    List<string> uids = pop3.GetAll();
    foreach (string uid in uids)
    {
        IMail email = new MailBuilder()
            .CreateFromEml(pop3.GetMessageByUID(uid));
        string subject = email.Subject;
    }
    pop3.Close();
}

Check our other .NET email samples here.

CAS .NET 4.0 and RDLC ReportViewer

In .NET 4.0 Code Access Security (CAS) has been deprecated.

Well I never liked CAS very much, so it’s definitely a good thing!
Unfortunately because of that some things are done differently now (in .NET 4.0).

Especially when you use RDLC ReportViewer control.

If you need to use your assembly in ReportViewer (like for example Barcode.dll barcode component) you need to follow this steps:

1.

First make sure that your assembly (Barcode.dll) is registered in GAC (Barcode.dll installer does that).
You can use:
gacutil -l Barcode
to check, and
gacutil -i Barcode.dll
to register.

As Code Access Security was deprecated you have to options:

2a.

You can add NetFx40_LegacySecurityPolicy entry in your App.config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
 <runtime>
   <!-- enables legacy CAS policy for this process -->
   <netFx40_LegacySecurityPolicy enabled="true" />
 </runtime>
</configuration>

and use this code (it is used in samples in Form1_Load method):

// C#

this.ReportViewer1.LocalReport.ExecuteReportInCurrentAppDomain(
  Assembly.GetExecutingAssembly().Evidence);
this.ReportViewer1.LocalReport.AddTrustedCodeModuleInCurrentAppDomain(
  "Barcode, Version=2.0.0.20, Culture=neutral, PublicKeyToken=6dc438ab78a525b3");
this.ReportViewer1.LocalReport.AddTrustedCodeModuleInCurrentAppDomain(
  "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
' VB.NET

Me.ReportViewer1.LocalReport.ExecuteReportInCurrentAppDomain( _
  Assembly.GetExecutingAssembly().Evidence)
Me.ReportViewer1.LocalReport.AddTrustedCodeModuleInCurrentAppDomain( _
  "Barcode, Version=2.0.0.20, Culture=neutral, PublicKeyToken=6dc438ab78a525b3")
Me.ReportViewer1.LocalReport.AddTrustedCodeModuleInCurrentAppDomain( _
  "System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")

or the second approach which is easier:

2b.

Use the following code in Form1_Load method:

// C#

PermissionSet permissions = new PermissionSet(
  PermissionState.Unrestricted);
this.ReportViewer1.LocalReport.SetBasePermissionsForSandboxAppDomain(
  permissions);
' VB.NET

Dim permissions As New PermissionSet( _
  PermissionState.Unrestricted)
Me.ReportViewer1.LocalReport.SetBasePermissionsForSandboxAppDomain( _
  permissions)

You can find samples how to use Barcode.dll with RDLC in C#, VB.NET in Windows Forms and ASP.NET in the Barcode.dll download package.

Download parts of email message

Sometimes you know you’ll receive large size emails, but you only need to access some email parts, without downloading entire email messages from IMAP server.

Mail.dll .NET IMAP library allows you to download only needed parts of the specified message.

First you need to download structure of the email message. There are two methods for that: GetBodyStructureByUID and GetBodyStructureByNumber.

You can also use GetMessageInfoByUID or GetMessageInfoByNumber methods. Both methods return more information about most common email fields, such as: subject, from, to and other headers, and also include BodyStructure in their response.

BodyStructure class contains information about plain text, html, and all attachments that were added to the message. It does not contain any data though. That’s the reason why, downloading it is very fast.

To download text parts of the email (like HTML or plain text) you can use: GetTextByUID or GetTextByNumber methods of Imap class.

To download binary attachments use GetDataByUID or GetDataByNumber methods of Imap class.

Here’s the full sample for this feature:

// C#

using(Imap imap = new Imap())
{
	imap.ConnectSSL("imap.server.com");
	imap.UseBestLogin("user", "password");

	imap.SelectInbox();
	List<long> uidList = imap.Search(Flag.Unseen);
	foreach (long uid in uidList)
	{
		// Get the structure of the email
		BodyStructure structure = imap.GetBodyStructureByUID(uid);

		// Download only text and html parts
		string text, html;

		if (structure.Text != null)
		    text = imap.GetTextByUID(structure.Text);
		if (structure.Html != null)
		    html = imap.GetTextByUID(structure.Html);

		Console.WriteLine(text);
		Console.WriteLine(html);

		// Show all attachments' names
		foreach(MimeStructure attachment in structure.Attachments)
		{
			Console.WriteLine(attachment.SafeFileName);
			// You can also download entire attachment
			byte[] bytes = imap.GetDataByUID(attachment);
		}
	}
	imap.Close();
}
' VB.NET

Using imap As New Imap()
	imap.ConnectSSL("imap.server.com")
	imap.UseBestLogin("user", "password")

	imap.SelectInbox()
	Dim uidList As List(Of Long) = imap.Search(Flag.Unseen)
	For Each uid As Long In uidList
		' Get the structure of the email
		Dim struct As BodyStructure = imap.GetBodyStructureByUID(uid)

		' Download only text and html parts
		Dim text As String, html As String

		If [structure].Text IsNot Nothing Then
		    text = imap.GetTextByUID([structure].Text)
		End If
		If [structure].Html IsNot Nothing Then
		    html = imap.GetTextByUID([structure].Html)
		End If

		Console.WriteLine(text)
		Console.WriteLine(html)

		' Show all attachments' names
		For Each attachment As MimeStructure In struct.Attachments
			Console.WriteLine(attachment.SafeFileName)
			' You can also download entire attachment
			Dim bytes As Byte() = imap.GetDataByUID(attachment)
		Next
	Next
	imap.Close()
End Using