Posts Tagged ‘IMAP component’

OAuth 2.0 with Gmail over IMAP for service account (DotNetOpenAuth)

Sunday, March 13th, 2016
Consider using Google.Apis version for service account Gmail access instead of DotNetOpenAuth version.

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, 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/

DotNetOpenAuth

DotNetOpenAuth is free, open source library that implements OAuth 2.0.

However you can not use the latest version. This is because Google has not updated their code to work with the most recent release. You’ll need to use 4.0 version.

Newtonsoft.Json

The other library you’ll need is Newtonsoft.Json JSON library. You can download it here: https://github.com/JamesNK/Newtonsoft.Json/releases or use nuget.

Access IMAP/SMTP server

using System;
using System.Text;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Newtonsoft.Json;
using DotNetOpenAuth.Messaging;
using DotNetOpenAuth.OAuth2;
using DotNetOpenAuth.OAuth2.Messages;
using Limilabs.Client.Authentication.Google;
using Limilabs.Client.IMAP;
using Limilabs.Mail;


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);

AuthorizationServerDescription server = new AuthorizationServerDescription
    {
        AuthorizationEndpoint = new Uri("https://accounts.google.com/o/oauth2/auth"),
        TokenEndpoint = new Uri("https://oauth2.googleapis.com/token"),
        ProtocolVersion = ProtocolVersion.V20,
    };

AssertionFlowClient provider = new AssertionFlowClient(server, certificate)
{
    ServiceAccountId = serviceAccountEmail,
    Scope = Limilabs.Client.Authentication.Google.GoogleScope.ImapAndSmtp.Name,
    ServiceAccountUser = userEmail,
};

IAuthorizationState grantedAccess = AssertionFlowClient.GetState(provider);
string accessToken = grantedAccess.AccessToken;

using (Imap client = new Imap())
{
    client.ConnectSSL("imap.gmail.com");
    client.LoginOAUTH2(userEmail, accessToken);

    client.SelectInbox();
    List<long> uids = client.Search(Flag.Unseen);

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

AssertionFlowClient

I’ll need to add several classes defined below. You can also browse Google APIs Client Library for .NET.

/// <summary>
/// Assertion flow header used to generate the assertion flow message header.
/// </summary>
public class AssertionFlowHeader
{
    /// <summary>
    /// Gets or sets the encryption algorithm used by the assertion flow message.
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("alg")]
    public String Algorithm { get; set; }

    /// <summary>
    /// Gets or sets the type of the claim.
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("typ")]
    public String Type { get; set; }
};

/// <summary>
/// Google assertion flow header holding Google supported values.
/// </summary>
public class GoogleAssertionFlowHeader : AssertionFlowHeader
{

    /// <summary>
    /// The google signing algorithm, currently RSA-SHA256
    /// </summary>
    public const string GoogleSigningAlgorithm = "RS256";

    /// <summary>
    /// The type of the google assertion, currently JSON Web Token
    /// </summary>
    public const string GoogleAssertionType = "JWT";

    public GoogleAssertionFlowHeader()
    {
        Algorithm = GoogleSigningAlgorithm;
        Type = GoogleAssertionType;
    }
};

/// <summary>
/// Assertion flow claim used to generate the assertion flow message claim.
/// </summary>
public class AssertionFlowClaim
{
    public AssertionFlowClaim()
    {
        DateTime begin = new DateTime(1970, 01, 01, 0, 0, 0, DateTimeKind.Utc);
        IssuedAt = (long) (DateTime.UtcNow - begin).TotalSeconds;
        ExpiresAt = IssuedAt + 3600;
    }

    public AssertionFlowClaim(AuthorizationServerDescription authorizationServer)
        : this()
    {
        Audience = authorizationServer.TokenEndpoint.ToString();
    }

    /// <summary>
    /// Gets or sets the assertion flow issuer (e.g client ID).
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("iss")]
    public String Issuer { get; set; }

    /// <summary>
    /// Gets or sets the service account user (for domain-wide delegation).
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("prn")]
    public String Principal { get; set; }

    /// <summary>
    /// Gets or sets the scope.
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("scope")]
    public String Scope { get; set; }

    /// <summary>
    /// Gets or sets the token endpoint.
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("aud")]
    public String Audience { get; set; }

    /// <summary>
    /// Gets or sets the expected expiration of the token to retrieve.
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("exp")]
    public long ExpiresAt { get; set; }

    /// <summary>
    /// Gets or sets the UTC timestamp at which this claim has been built.
    /// </summary>
    [Newtonsoft.Json.JsonPropertyAttribute("iat")]
    public long IssuedAt { get; set; }
};

/// <summary>
/// Assertion flow message to be sent to the token endpoint.
/// </summary>
public class AssertionFlowMessage : MessageBase
{
    /// <summary>
    /// Google supported assertion type 
    /// </summary>
    public const string GoogleAssertionType = "http://oauth.net/grant_type/jwt/1.0/bearer";

    /// <summary>
    /// Initializes a new instance of the <see cref="AssertionFlowMessage"/> class.
    /// </summary>
    /// <param name='authorizationServer'> Authorization server description. </param>
    public AssertionFlowMessage(AuthorizationServerDescription authorizationServer) :
        base(new Version(2, 0), MessageTransport.Direct, authorizationServer.TokenEndpoint)
    {
        GrantType = "assertion";
        AssertionType = GoogleAssertionType;
        this.HttpMethods = HttpDeliveryMethods.PostRequest;
    }

    /// <summary>
    /// Gets or sets the type of the grant (defaults to "assertion").
    /// </summary>
    [MessagePart("grant_type", IsRequired = true)]
    public String GrantType { get; set; }

    /// <summary>
    /// Gets or sets the type of the assertion 
    /// (defaults to "http://oauth.net/grant_type/jwt/1.0/bearer").
    /// </summary>
    [MessagePart("assertion_type", IsRequired = true)]
    public String AssertionType { get; set; }

    /// <summary>
    /// Gets or sets the assertion message.
    /// </summary>
    [MessagePart("assertion", IsRequired = true)]
    public String Assertion { get; set; }
};

public class AssertionFlowClient : ClientBase
{
    /// <summary>
    /// Gets or sets the service account identifier.
    /// </summary>
    /// <value>
    /// The service account identifier.
    /// </value>
    public String ServiceAccountId { get; set; }

    /// <summary>
    /// Gets or sets the service account user (used for domain-wide delegation).
    /// </summary>
    public String ServiceAccountUser { get; set; }

    /// <summary>
    /// Gets or sets the scope to get access for.
    /// </summary>
    public String Scope { get; set; }

    /// <summary>
    /// Gets the certificate used to sign the assertion.
    /// </summary>
    public X509Certificate2 Certificate { get; private set; }

    /// <summary>
    /// Gets or sets the JWT claim's header (defaults to Google's supported values).
    /// </summary>
    public AssertionFlowHeader Header { get; set; }

    public RSACryptoServiceProvider Key { get; private set; }

    /// <summary>
    /// Initializes a new instance of the
    /// <see cref="AssertionFlowClient"/> class.
    /// </summary>
    /// <param name='authorizationServer'>
    /// Authorization server description.
    /// </param>
    /// <param name='certificate'>
    /// Certificate to use to sign the assertion flow messages.
    /// </param>
    public AssertionFlowClient(
        AuthorizationServerDescription authorizationServer,
        X509Certificate2 certificate)
        : base(authorizationServer, null, null)
    {
        if (certificate == null)
            throw new ArgumentNullException("certificate");
        if (certificate.PrivateKey == null)
            throw new ArgumentNullException("certificate.PrivateKey");

        Header = new GoogleAssertionFlowHeader();
        Certificate = certificate;

        // Workaround to correctly cast the private key as a RSACryptoServiceProvider type 24
        RSACryptoServiceProvider rsa = (RSACryptoServiceProvider) certificate.PrivateKey;
        byte[] privateKeyBlob = rsa.ExportCspBlob(true);

        Key = new RSACryptoServiceProvider();
        Key.ImportCspBlob(privateKeyBlob);
    }

    /// <summary>
    /// Helper method to retrieve the Authorization State.
    /// </summary>
    /// <returns>
    /// The authorization state.
    /// </returns>
    /// <param name='provider'>
    /// The provider to use to retrieve the authorization state.
    /// </param>
    public static IAuthorizationState GetState(AssertionFlowClient provider)
    {
        if (provider.Scope == null)
            throw new ArgumentNullException("Scope");
        IAuthorizationState state = new AuthorizationState(provider.Scope.Split(' '));

        if (provider.RefreshToken(state, null))
        {
            return state;
        }
        return null;
    }

    /// <summary>
    /// Request a new access token using the OAuth 2.0 assertion flow.
    /// </summary>
    /// <returns>
    /// Whether or not a new access token has been successfully retrieved.
    /// </returns>
    /// <param name='authorization'>
    /// Object containing the current authorization state.
    /// </param>
    /// <param name='skipIfUsefulLifeExceeds'>
    /// If set to <c>true</c> skip if useful life exceeds.
    /// </param>
    public new bool RefreshToken(
        IAuthorizationState authorization, 
        TimeSpan? skipIfUsefulLifeExceeds)
    {
        return RefreshToken(authorization, skipIfUsefulLifeExceeds, this.Channel.Request);
    }

    public bool RefreshToken(
        IAuthorizationState authorization,
        TimeSpan? skipIfUsefulLifeExceeds,
        Func<IDirectedProtocolMessage, IProtocolMessage> requestProvider)
    {
        if (authorization == null)
            throw new ArgumentNullException("authorization");
        if (this.Certificate == null)
            throw new ArgumentNullException("Certificate");

        // Check if the token is still valid.
        if (skipIfUsefulLifeExceeds.HasValue && authorization.AccessTokenExpirationUtc.HasValue)
        {
            TimeSpan timeSpan = authorization.AccessTokenExpirationUtc.Value - DateTime.UtcNow;
            if (timeSpan > skipIfUsefulLifeExceeds.Value)
            {
                return false;
            }
        }

        AssertionFlowMessage requestMessage = GenerateMessage();

        var response = requestProvider(requestMessage);

        // Response is not strongly-typed to an AccessTokenSuccessResponse because DotNetOpenAuth can't infer the
        // type from the request message type. The only way to get access to the result data is through the
        // resulting Dictionary.
        if (response.ExtraData.ContainsKey("access_token") && response.ExtraData.ContainsKey("expires_in"))
        {
            authorization.AccessToken = response.ExtraData["access_token"];
            long expiresIn = long.Parse(response.ExtraData["expires_in"]);
            DateTime utcNow = DateTime.UtcNow;
            authorization.AccessTokenExpirationUtc = utcNow.AddSeconds(expiresIn);
            authorization.AccessTokenIssueDateUtc = utcNow;
            authorization.SaveChanges();
            return true;
        }
        return false;
    }

    /// <summary>
    /// Generates the assertion flow message to be sent to the token endpoint.
    /// </summary>
    /// <returns>
    /// The assertion flow message.
    /// </returns>
    private AssertionFlowMessage GenerateMessage()
    {
        string header = JsonConvert.SerializeObject(Header);
        string claim = JsonConvert.SerializeObject(
            new AssertionFlowClaim(AuthorizationServer)
                       {
                           Issuer = this.ServiceAccountId,
                           Principal = this.ServiceAccountUser,
                           Scope = this.Scope
                       });

        StringBuilder assertion = new StringBuilder();
        assertion.Append(UnpaddedUrlSafeBase64Encode(header));
        assertion.Append(".");
        assertion.Append(UnpaddedUrlSafeBase64Encode(claim));

        // TODO: Check if this is working on FIPS enabled systems.
        byte[] data = Encoding.ASCII.GetBytes(assertion.ToString());
        String signature = UnpaddedUrlSafeBase64Encode(Key.SignData(data, "SHA256"));
        assertion.Append(".");
        assertion.Append(signature);

        return new AssertionFlowMessage(this.AuthorizationServer)
                   {
                       Assertion = assertion.ToString()
                   };
    }

    /// <summary>
    /// Encode the provided UTF8 string into an URL safe base64 string.
    /// </summary>
    /// <returns>
    /// The URL safe base64 string.
    /// </returns>
    /// <param name='value'>
    /// String to encode.
    /// </param>
    private String UnpaddedUrlSafeBase64Encode(String value)
    {
        return UnpaddedUrlSafeBase64Encode(Encoding.UTF8.GetBytes(value));
    }

    /// <summary>
    /// Encode the byte array into an URL safe base64 string.
    /// </summary>
    /// <returns>
    /// The URL safe base64 string.
    /// </returns>
    /// <param name='bytes'>
    /// Bytes to encode.
    /// </param>
    private String UnpaddedUrlSafeBase64Encode(Byte[] bytes)
    {
        return Convert.ToBase64String(bytes)
            .Replace("=", String.Empty)
            .Replace('+', '-')
            .Replace('/', '_');
    }
};

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

Use Nuget to download “Google.Apis.Auth” package.

Import namespaces:

// c#

using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Auth.OAuth2.Responses;

using System.Security.Cryptography.X509Certificates;

using Limilabs.Client.Authentication.Google;

using Limilabs.Client.IMAP;
' VB.NET 

Imports Google.Apis.Auth.OAuth2
Imports Google.Apis.Auth.OAuth2.Flows
Imports Google.Apis.Auth.OAuth2.Requests
Imports Google.Apis.Auth.OAuth2.Responses

Imports System.Security.Cryptography.X509Certificates

Imports Limilabs.Client.Authentication.Google

Imports Limilabs.Client.IMAP

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/

Access IMAP/SMTP server

// C#

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 = await credential.RequestAccessTokenAsync(CancellationToken.None);

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

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 { _
	.Scopes = {GoogleScope.ImapAndSmtp.Name}, _
	.User = userEmail _
}.FromCertificate(certificate))

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

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

IMAP search requires parentheses

Friday, June 6th, 2014

IMAP protocol uses Polish notation (also known as prefix notation) for client queries. In general it means, that operator (in case of IMAP search it’s always a logical operator) is placed left of its operands.

Polish notation

Expression that would be written in conventional infix notation as:
(5 − 6) * 7
can be written in prefix/Polish notation as:
* (− 5 6) 7
As all mathematical operands are binary (or have defined number of operands) we don’t need parentheses to correctly evaluate this expression:
* − 5 6 7

Polish notation doesn’t require parentheses.

IMAP search needs parentheses

Although IMAP search syntax uses Polish notation it actually requires parentheses to be evaluated unambiguously.

This is because there is no explicitly defined AND operator, that takes exactly 2 operands.

RFC 3501:

When multiple keys are specified, the result is the intersection (AND function) of all the messages that match those keys.

So although it is true that Polish and reverse Polish notations don’t require parentheses, it is not true that IMAP search doesn’t require them.

‘OR/AND’ example

Expression 1a:

Regular notation:
(SUBJECT subject OR BODY body) AND FROM from

Polish notation:
AND OR SUBJECT subject BODY body FROM from

IMAP’s Polish notation (note there is no AND operator):
OR SUBJECT subject BODY body FROM from

Expression 1b:

Regular notation:
SUBJECT subject OR (BODY body AND FROM from)

Polish notation:
OR SUBJECT subject AND BODY body FROM from

IMAP’s Polish notation (note there is no AND operator):
OR SUBJECT subject BODY body FROM from

1a != 1b

Expression 1a is not equal to Expression 1b:
((a or b) and c) != (a or (b and c))

For:
a = true
b = true
c = false

We have:
((true or true) and false) != (true or (true and false))
false != true

Parentheses are needed

But when there is no AND operator, IMAP’s Polish notations look exactly the same, thus we need parentheses!

Expression 1a (IMAP’s Polish notation with parentheses):
(OR SUBJECT subject BODY body) FROM from

Expression 1b (IMAP’s Polish notation with parentheses):
OR SUBJECT subject (BODY body FROM from)

OR SUBJECT subject BODY body FROM from
is treated like
(OR SUBJECT subject BODY body) FROM from

‘NOT’ example

Expression 2a:

Regular notation:
NOT (SUBJECT subject AND BODY body)

Polish notation:
NOT AND SUBJECT subject BODY body

IMAP’s Polish notation (note there is no AND operator):
NOT SUBJECT subject BODY body

Expression 2b:

Regular notation:
(NOT SUBJECT subject) AND BODY body

Polish notation:
AND NOT SUBJECT subject BODY body

IMAP’s Polish notation (note there is no AND operator):
NOT SUBJECT subject BODY body

2a != 2b

Expression 2a is obviously not equal to Expression 2b.

Parentheses are needed

Again IMAP’s Polish notations look the same. We need parentheses!

Expression 2a (IMAP’s Polish notation with parentheses):
NOT (SUBJECT subject BODY body)

Expression 2b (IMAP’s Polish notation with parentheses):
(NOT SUBJECT subject) BODY body

NOT SUBJECT subject BODY body
is treated like
(NOT SUBJECT subject) BODY body

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