Posts Tagged ‘C#’

Logging in Mail.dll

Monday, August 1st, 2016

To enable logging for Mail.dll clients (Imap, Pop3, Smtp) you only need to add the following line before you connect:

// C# version:

Limilabs.Mail.Log.Enabled = true;

' VB.NET version:

Limilabs.Mail.Log.Enabled = True

You can observe the log output by:

  • looking at the Visual Studio’s output window (View/Output/’Show output from’: Debug)
  • subscribing to Log.WriteLine event
  • defining custom listeners using your application’s config file (App.config or Web.config)
  • using log4net

This is how the log looks like in the Visual Studio’s output window:

You can also enable logging using your application’s config file (App.config, Web.config):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>

      <switches>
        <add name="Mail.dll" value="Verbose" />
      </switches>

    </system.diagnostics>
</configuration>

log4net

If you are using the latest version of log4net.dll, Mail.dll is going to use log4net instead of standard .NET System.Net.TraceSource class. Please refer to log4net manual on how to capture log entries.

Mail.dll uses logger called “Mail.dll” (_logger = LogManager.GetLogger(“Mail.dll”)) and level Info (_logger.Info(message)) to log information.

Please remember that even when using log4net, you need to enable logging by setting “Limilabs.Mail.Log.Enabled = True” or by setting Mail.dll trace switch in the config file (App.config, Web.config) to Verbose as shown above.

Log to file

You’ll need to define a TextWriterTraceListener that writes to a file and connect it with Mail.dll trace source. The easiest solution is to modify your application’s config file (App.config, Web.config) accordingly:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.diagnostics>

        <trace autoflush="true"/>
   
        <switches>
            <add name="Mail.dll" value="Verbose"/>
        </switches>
   
        <sources>
            <source name="Mail.dll">
                <listeners>
                    <add name="MailLogFile"/>
                </listeners>
            </source>
        </sources>
   
        <sharedListeners>
            <add 
                name="MailLogFile" 
                type="System.Diagnostics.TextWriterTraceListener" 
                initializeData="c:\mail.log"/>
        </sharedListeners>

    </system.diagnostics>
</configuration>

Log.WriteLine

Log class exposes WriteLine event. You can use that event to subscribe your own logging library.

// C#

Log.WriteLine += Console.WriteLine;
' VB.NET

Log.WriteLine += Console.WriteLine

Delete email permanently in Gmail

Friday, September 4th, 2015

If you delete a message from your Inbox or one of your custom folders, it will still appear in [Gmail]/All Mail.

Here’s why: in most folders, deleting a message simply removes that folder’s label from the message, including the label identifying the message as being in your Inbox.

[Gmail]/All Mail shows all of your messages, whether or not they have labels attached to them.

If you want to permanently delete a message from all folders:

  1. Move it to the [Gmail]/Trash folder (or [Gmail]/Bin or national version).
  2. Delete it from the [Gmail]/Trash folder (or [Gmail]/Bin or national version).

All emails in [Gmail]/Spam and [Gmail]/Trash are deleted after 30 days.
If you delete a message from [Gmail]/Spam or [Gmail]/Trash, it will be deleted permanently.

Here’s how this looks like using Mail.dll .NET IMAP component:

// C# version:

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

	// Recognize Trash folder
	List<FolderInfo> folders = imap.GetFolders();
 	CommonFolders common = new CommonFolders(folders);
 	FolderInfo trash = common.Trash;

	// Find all emails we want to delete
	imap.SelectInbox();
	List<long> uids = imap.Search(Expression.Subject("email to delete"));

	// Move email to Trash
	List<long> uidsInTrash = new List<long>();
	foreach (long uid in uids)
	{
		long uidInTrash = (long)imap.MoveByUID(uid, trash);
		uidsInTrash.Add(uidInTrash);
	}

	// Delete moved emails from Trash
	imap.Select(trash);
	foreach (long uid in uidsInTrash)
	{
		imap.DeleteMessageByUID(uid);
	}

	imap.Close();
}
' VB.NET version:

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

	' Recognize Trash folder
	Dim folders As List(Or FolderInfo) = imap.GetFolders()
	Dim common As CommonFolders = new CommonFolders(folders)
	Dim trash As FolderInfo = common.Trash

	' Find all emails we want to delete
	imap.SelectInbox()
	Dim uids As List(Of Long) = imap.Search(_
		Expression.Subject("email to delete"))

	' Move email to Trash
	Dim uidsInTrash As New List(Of Long)
	For Each uid As Long In uids
		Dim uidInTrash As Long = imap.MoveByUID(uid, trash)
		uidsInTrash.Add(uidInTrash)
	Next

	' Delete moved emails from Trash
	imap.[Select](trash)
	For Each uid As Long In uidsInTrash
		imap.DeleteMessageByUID(uid)
	Next

	imap.Close()
End Using

You can also use bulk methods to handle large amount of emails faster:

// C# version:

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

	// Recognize Trash folder
	List<FolderInfo> folders = imap.GetFolders();
 	CommonFolders common = new CommonFolders(folders);
 	FolderInfo trash = common.Trash;

	// Find all emails we want to delete
	imap.SelectInbox();
	List<long> uids = imap.Search(Expression.Subject("email to delete"));

	// Move emails to Trash
	List<long> uidsInTrash = imap.MoveByUID(uids, trash);

	// Delete moved emails from Trash
	imap.Select(trash);
	imap.DeleteMessageByUID(uidsInTrash);

	imap.Close();
}
' VB.NET version:

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

	' Recognize Trash folder
	Dim folders As List(Or FolderInfo) = imap.GetFolders()
	Dim common As CommonFolders = new CommonFolders(folders)
	Dim trash As FolderInfo = common.Trash

	' Find all emails we want to delete
	imap.SelectInbox()
	Dim uids As List(Of Long) = imap.Search(Expression.Subject("email to delete"))

	' Move email to Trash
	Dim uidsInTrash As List(Of Long) = imap.MoveByUID(uids, trash)

	' Delete moved emails from Trash
	imap.[Select](trash)
	imap.DeleteMessageByUID(uidsInTrash)

	imap.Close()
End Using

Please also note that there are 2 additional sections in the “Gmail settings”/”Forwarding and POP/IMAP” tab:

“When I mark a message in IMAP as deleted:”

  • Auto-Expunge on – Immediately update the server. (default)
  • Expunge off – Wait for the client to update the server.

“When a message is marked as deleted and expunged from the last visible IMAP folder:”

  • Archive the message (default)
  • Move the message to the Trash
  • Immediately delete the message forever

Those settings change the default behavior of the account and modify DeleteMessage* methods behavior.

OAuth 2.0 with Gmail over IMAP for service account

Thursday, September 11th, 2014

In this article I’ll show how to access Gmail account of any domain user, using OAuth 2.0, .NET IMAP component and service accounts. The basic idea is that domain administrator can use this method to access user email without knowing user’s password.

This scenario is very similar to 2-legged OAuth, which uses OAuth 1.0a. Although it still works, it has been deprecated by Google and OAuth 2.0 service accounts were introduced.

The following describes how to use XOAUTH2 and OAuth 2.0 to achieve the equivalent of 2-legged OAuth.

Google APIs console

First you need to visit the Google Developers Console and create a service account:

console_0

console_1

console_2

Download and save this private key (XYZ.p12), you’ll need that later:

console_3

Go to “Service accounts” and click “View Client ID”:

console_4

Make a note of the Client ID and Email address (Service account).

Google Apps Dashboard

Next step is to authorize access for newly created service account.

Visit your domain administration panel:
https://www.google.com/a/cpanel/yourdomain.com/ManageOauthClients

Then click “Advanced tools”, “Authentication” and “Manage third party OAuth Client access”.

On this screen you can authorize service account to access email scope:

cpanel_1

Use previously remembered Client ID and “https://mail.google.com/”, which is IMAP/SMTP API scope:

Client Name: 1234567890
One or More API Scopes: https://mail.google.com/

Google.Apis

Use Nuget to download “Google.Apis.Auth.Mvc” and “Google.Apis.Auth” packages.

Access IMAP/SMTP server

// C#

using System.Security.Cryptography.X509Certificates;
using System;
using System.Threading;
using Limilabs.Client.IMAP;
using Limilabs.Test.Constants;
using Google.Apis.Auth.OAuth2;
using Limilabs.Client.Authentication.Google;


const string serviceAccountEmail = "name@xxxxxxxxxx.gserviceaccount.com";
const string serviceAccountCertPath = @"c:\XYZ.p12";
const string serviceAccountCertPassword = "notasecret";
const string userEmail = "user@your-domain.com";

X509Certificate2 certificate = new X509Certificate2(
    serviceAccountCertPath, 
    serviceAccountCertPassword, 
    X509KeyStorageFlags.Exportable);

ServiceAccountCredential credential = new ServiceAccountCredential(
    new ServiceAccountCredential.Initializer(serviceAccountEmail)
    {
        Scopes = new[] { GoogleScope.ImapAndSmtp.Name },
        User = userEmail
    }.FromCertificate(certificate));

bool success = credential.RequestAccessTokenAsync(CancellationToken.None).Result;
Assert.IsTrue(success);

using (Imap imap = new Imap())
{
    imap.ConnectSSL("imap.gmail.com");
    imap.LoginOAUTH2(userEmail, credential.Token.AccessToken);

    imap.SelectInbox();

    foreach (long uid in uids)
    {
        var eml = client.GetMessageByUID(uid);
        IMail email = new MailBuilder().CreateFromEml(eml);
        Console.WriteLine(email.Subject);
    }

    imap.Close();
}
' VB.NET

Imports System.Security.Cryptography.X509Certificates
Imports System.Threading
Imports Limilabs.Client.IMAP
Imports Limilabs.Test.Constants
Imports Google.Apis.Auth.OAuth2
Imports Limilabs.Client.Authentication.Google


Const  serviceAccountEmail As String = "name@xxxxxxxxxx.gserviceaccount.com"
Const  serviceAccountCertPath As String = "c:\XYZ.p12"
Const  serviceAccountCertPassword As String = "notasecret"
Const  userEmail As String = "user@your-domain.com"

Dim certificate As New X509Certificate2(serviceAccountCertPath, serviceAccountCertPassword, X509KeyStorageFlags.Exportable)

Dim credential As New ServiceAccountCredential(New ServiceAccountCredential.Initializer(serviceAccountEmail) With { _
	Key .Scopes = New () {GoogleScope.ImapAndSmtp.Name}, _
	Key .User = userEmail _
}.FromCertificate(certificate))

Dim success As Boolean = credential.RequestAccessTokenAsync(CancellationToken.None).Result
Assert.IsTrue(success)

Using imap As New Imap()
	imap.ConnectSSL("imap.gmail.com")
	imap.LoginOAUTH2(userEmail, credential.Token.AccessToken)

	imap.SelectInbox()

	For Each uid As Long In uids
		Dim eml = client.GetMessageByUID(uid)
		Dim email As IMail = New MailBuilder().CreateFromEml(eml)
		Console.WriteLine(email.Subject)
	Next

	imap.Close()
End Using

Upload email to Sent folder after sending

Sunday, December 1st, 2013

Some SMTP servers automatically put sent messages in the sent folder, but this is not a SMTP protocol requirement and many servers don’t do that.

In such case, you’ll need to manually upload message to the sent folder using IMAP. Unfortunately there is no standard that would allow checking, if SMTP server puts send messages in Sent folder.

Create email message

First we’ll create new email message:

// C#

IMail email = Limilabs.Mail.Fluent.Mail
    .Html(@"Html with an image: <img src=""cid:lena"" />")
    .AddVisual(@"c:\lena.jpeg").SetContentId("lena")
    .AddAttachment(@"c:\tmp.doc").SetFileName("document.doc")
    .To("to@example.com")
    .From("from@example.com")
    .Subject("Subject")
    .Create();
' VB.NET

Dim email As IMail = Mail _
    .Html("Html with an image: <img src=""cid:lena"" />") _
    .AddVisual("C:\lena.jpeg").SetContentId("lena") _
    .AddAttachment("C:\tmp.doc").SetFileName("document.doc") _
    .To("to@example.com") _
    .From("from@example.com") _
    .Subject("Subject") _
    .Create()

Send email message

Then we’ll send it using SMTP protocol:

// C#

ISendMessageResult result;
using (Smtp smtp = new Smtp())
{
    smtp.Connect("smtp.server.com");     // or ConnectSSL for SSL
    smtp.UseBestLogin("user", "password");

    result = smtp.SendMessage(email);

    smtp.Close();
}
' VB.NET

Dim result As ISendMessageResult
Using smtp As New Smtp()
    smtp.Connect("smtp.server.com")     ' or ConnectSSL for SSL
    smtp.UseBestLogin("user", "password")

    result = smtp.SendMessage(email)

    smtp.Close()
End Using

Upload email message

Finally we’ll connect to IMAP server, get Sent folder and upload message to it:

// C#

if (result.Status == SendMessageStatus.Success)
{
    using (Imap imap = new Imap())
    {
        imap.Connect("imap.server.com");     // or ConnectSSL for SSL
        imap.UseBestLogin("user", "password");

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

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

If result.Status = SendMessageStatus.Success Then
    Using imap As New Imap()
        imap.Connect("imap.server.com")		' or ConnectSSL for SSL
        imap.UseBestLogin("user", "password")

        Dim sent As FolderInfo = New CommonFolders(imap.GetFolders()).Sent
        imap.UploadMessage(sent, email)

        imap.Close()
   End Using
End If

Entire code

Here’s the full code:

// C#

IMail email = Limilabs.Mail.Fluent.Mail
    .Html(@"Html with an image: <img src=""cid:lena"" />")
    .AddVisual(@"c:\lena.jpeg").SetContentId("lena")
    .AddAttachment(@"c:\tmp.doc").SetFileName("document.doc")
    .To("to@example.com")
    .From("from@example.com")
    .Subject("Subject")
    .Create();

ISendMessageResult result;
using (Smtp smtp = new Smtp())
{
    smtp.Connect("smtp.server.com");    // or ConnectSSL for SSL
    smtp.UseBestLogin("user", "password");

    result = smtp.SendMessage(email);

    smtp.Close();
}

if (result.Status == SendMessageStatus.Success)
{
    using (Imap imap = new Imap())
    {
        imap.Connect("imap.server.com");     // or ConnectSSL for SSL
        imap.UseBestLogin("user", "password");

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

        imap.Close();
    }
}

' VB.NET

Dim email As IMail = Mail _
    .Html("Html with an image: <img src=""cid:lena"" />") _
    .AddVisual("C:\lena.jpeg").SetContentId("lena") _
    .AddAttachment("C:\tmp.doc").SetFileName("document.doc") _
    .To("to@example.com") _
    .From("from@example.com") _
    .Subject("Subject") _
    .Create()

Dim result As ISendMessageResult
Using smtp As New Smtp()
    smtp.Connect("smtp.server.com")	' or ConnectSSL for SSL
    smtp.UseBestLogin("user", "password")

    result = smtp.SendMessage(email)

    smtp.Close()
End Using

If result.Status = SendMessageStatus.Success Then
    Using imap As New Imap()
        imap.Connect("imap.server.com")		' or ConnectSSL for SSL
        imap.UseBestLogin("user", "password")

        Dim sent As FolderInfo = New CommonFolders(imap.GetFolders()).Sent
        imap.UploadMessage(sent, email)

    imap.Close()
End Using
End If

Search unanswered emails in Gmail

Friday, November 1st, 2013

IMAP protocol in RFC3501 introduced \Answered flag. \Answered flag should mark messages that have been answered. This flag can by set by client applications or even by SMTP server, which can be examining In-Reply-To email headers.

Unfortunately if the message is answered through Gmail’s web interface, the \Answered flag will not be set. It will only be set if the messages were answered using an email program that sets this flag.

This means that \Answered flag can’t be used to find not-answered emails.

One option to resolve this problem is to use Gmail IMAP extensions, X-GM-THRID in particular.

We’ll connect to Gmail using IMAP – remember to use SSL and be sure to enable IMAP protocol for Gmail.

As we plan to search all emails, including sent and received ones, we’ll use CommonFolders class to get ‘All Mail’ folder.

Then we’ll get Envelope for each message. Envelope contains GmailThreadId property. It contains thread id to which this email was assigned to by Gmail.

Finally we’ll find threads that contain only one message – those are messages that were not answered.

// C#

using(Imap client = new Imap())
{
    client.ConnectSSL("imap.gmail.com");
    client.Login("pat@limilabs.com","password");

    // Select 'All Mail' folder.
    List<FolderInfo> folders = client.GetFolders();
    CommonFolders common = new CommonFolders(folders);
    client.Select(common.AllMail);

    // Get envelopes for all emails.
    List<long> uids = client.GetAll();
    List<Envelope> envelopes = client.GetEnvelopeByUID(uids);

    // Group messages by thread id.
    var threads = new Dictionary<decimal, List<Envelope>>();
    foreach (Envelope envelope in envelopes)
    {
        decimal threadId = (decimal) envelope.GmailThreadId;
        if (threads.ContainsKey(threadId) == false)
            threads[threadId] = new List<Envelope>();
        
        threads[threadId].Add(envelope);
    }

    // Find threads containing single message.
    foreach (KeyValuePair<decimal, List<Envelope>> pair in threads)
    {
        if (pair.Value.Count == 1)
        {
            Envelope envelope = pair.Value[0];
            Console.WriteLine("Gmail message id: {0}; subject: {1}", 
                envelope.GmailMessageId , 
                envelope.Subject);

        }
    }
    client.Close();
}
' VB.NET

Using client As New Imap()
	client.ConnectSSL("imap.gmail.com")
	client.Login("pat@limilabs.com", "password")

	' Select 'All Mail' folder.
	Dim folders As List(Of FolderInfo) = client.GetFolders()
	Dim common As New CommonFolders(folders)
	client.[Select](common.AllMail)

	' Get envelopes for all emails.
	Dim uids As List(Of Long) = client.GetAll()
	Dim envelopes As List(Of Envelope) = client.GetEnvelopeByUID(uids)

	' Group messages by thread id.
	Dim threads = New Dictionary(Of Decimal, List(Of Envelope))()
	For Each envelope As Envelope In envelopes
		Dim threadId As Decimal = CDec(envelope.GmailThreadId)
		If threads.ContainsKey(threadId) = False Then
			threads(threadId) = New List(Of Envelope)()
		End If

		threads(threadId).Add(envelope)
	Next

	' Find threads containing single message.
	For Each pair As KeyValuePair(Of Decimal, List(Of Envelope)) In threads
		If pair.Value.Count = 1 Then
			Dim envelope As Envelope = pair.Value(0)

			Console.WriteLine("Gmail message id: {0}; subject: {1}", _
				envelope.GmailMessageId, envelope.Subject)
		End If
	Next
	client.Close()
End Using