diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..412eeda
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,22 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs diff=csharp
+*.sln merge=union
+*.csproj merge=union
+*.vbproj merge=union
+*.fsproj merge=union
+*.dbproj merge=union
+
+# Standard to msysgit
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ebd21a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,163 @@
+#################
+## Eclipse
+#################
+
+*.pydevproject
+.project
+.metadata
+bin/
+tmp/
+*.tmp
+*.bak
+*.swp
+*~.nib
+local.properties
+.classpath
+.settings/
+.loadpath
+
+# External tool builders
+.externalToolBuilders/
+
+# Locally stored "Eclipse launch configurations"
+*.launch
+
+# CDT-specific
+.cproject
+
+# PDT-specific
+.buildpath
+
+
+#################
+## Visual Studio
+#################
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.sln.docstates
+
+# Build results
+[Dd]ebug/
+[Rr]elease/
+*_i.c
+*_p.c
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.vspscc
+.builds
+*.dotCover
+
+## TODO: If you have NuGet Package Restore enabled, uncomment this
+#packages/
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+
+# Visual Studio profiler
+*.psess
+*.vsp
+
+# ReSharper is a .NET coding add-in
+_ReSharper*
+
+# Installshield output folder
+[Ee]xpress
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish
+
+# Others
+[Bb]in
+[Oo]bj
+sql
+TestResults
+*.Cache
+ClientBin
+stylecop.*
+~$*
+*.dbmdl
+Generated_Code #added for RIA/Silverlight projects
+
+# Backup & report files from converting an old project file to a newer
+# Visual Studio version. Backup files are not needed, because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+
+
+
+############
+## Windows
+############
+
+# Windows image file caches
+Thumbs.db
+
+# Folder config file
+Desktop.ini
+
+
+#############
+## Python
+#############
+
+*.py[co]
+
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
+
+# Installer logs
+pip-log.txt
+
+# Unit test / coverage reports
+.coverage
+.tox
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+
+# Mac crap
+.DS_Store
diff --git a/AuthMethod.cs b/AuthMethod.cs
new file mode 100644
index 0000000..8ecd6e7
--- /dev/null
+++ b/AuthMethod.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace S22.Imap {
+ ///
+ /// Defines supported means of authenticating with the IMAP server.
+ ///
+ public enum AuthMethod {
+ ///
+ /// Login using plaintext password authentication. This is
+ /// the default supported by most servers.
+ ///
+ Login,
+ ///
+ /// Login using the CRAM-MD5 authentication mechanism.
+ ///
+ CRAMMD5,
+ ///
+ /// Login using the OAuth authentication mechanism over
+ /// the Simple Authentication and Security Layer (Sasl).
+ ///
+ SaslOAuth
+ }
+}
diff --git a/Examples.xml b/Examples.xml
new file mode 100644
index 0000000..d5f3420
--- /dev/null
+++ b/Examples.xml
@@ -0,0 +1,219 @@
+
+
+
+
+ This example shows how to establish a connection with an IMAP server
+ and print out the IMAP options, which the server supports.
+
+ /* Connect to Gmail's IMAP server on port 993 using SSL */
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, true);
+
+ /* Print out the server's capabilities */
+ foreach(string s in Client.Capabilities())
+ Console.WriteLine(s);
+
+ Client.Dispose();
+
+
+
+
+
+
+ This example demonstrates how to connect and login to an IMAP server.
+
+ /* Connect to Gmail's IMAP server on port 993 using SSL */
+ try {
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_Username",
+ "My_Password", true, AuthMethod.Login);
+
+ /* Check if the server supports IMAP IDLE */
+ if(Client.Supports("IDLE"))
+ Console.WriteLine("This server supports the IMAP4 IDLE specification");
+ else
+ Console.WriteLine("This server does not support IMAP IDLE");
+
+ /* release resources */
+ Client.Dispose();
+ }
+ catch(InvalidCredentialsException) {
+ Console.WriteLine("The server rejected the supplied credentials");
+ }
+
+
+
+
+
+
+ This example demonstrates how to authenticate with an IMAP server once a connection
+ has been established. Notice that you can also connect and login in one step
+ using one of the overloaded constructors.
+
+ /* Connect to Gmail's IMAP server on port 993 using SSL */
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, true);
+
+ try {
+ Client.Login("My_Username", "My_Password", AuthMethod.Login);
+ }
+ catch(InvalidCredentialsException) {
+ Console.WriteLine("The server rejected the supplied credentials");
+ }
+
+ Client.Dispose();
+
+
+
+
+
+
+ This example demonstrates how to use the search method to get a list of all
+ unread messages in the mailbox.
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_Username",
+ "My_Password", true, AuthMethod.Login);
+
+ /* get a list of unique identifiers (UIDs) of all unread messages in the mailbox */
+ uint[] uids = Client.Search( SearchCondition.Unseen() );
+
+ /* fetch the messages and print out their subject lines */
+ foreach(uint uid in uids) {
+ MailMessage message = Client.GetMessage(uid);
+
+ Console.WriteLine(message.Subject);
+ }
+
+ /* free up any resources associated with this instance */
+ Client.Dispose();
+
+
+
+ This example demonstrates how to perform a search using multiple search criteria
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_Username",
+ "My_Password", true, AuthMethod.Login);
+
+ /* get a list of unique identifiers (UIDs) of all messages sent before the 01.04.2012
+ and that are larger than 1 Kilobyte */
+ uint[] uids = Client.Search( SearchCondition.SentBefore(new DateTime(2012, 4, 1))
+ .And( SearchCondition.Larger(1024) ));
+
+ Console.WriteLine("Found " + uids.Length + " messages");
+
+ /* free up any resources associated with this instance */
+ Client.Dispose();
+
+
+
+
+
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_UsernamMe",
+ "My_Password", true, AuthMethod.Login);
+
+ /* find all messages in the mailbox that were sent from "John.Doe@gmail.com" */
+ uint uids = Client.Search( SearchCondition.From("John.Doe@gmail.com") );
+
+ /* fetch the first message and print it's subject and body */
+ if(uids.Length > 0) {
+ MailMessage msg = Client.GetMessage(uids[0]);
+
+ Console.WriteLine("Subject: " + msg.Subject);
+ Console.WriteLine("Body: " + msg.Body);
+ }
+
+ Client.Dispose();
+
+
+
+
+
+
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_UsernamMe",
+ "My_Password", true, AuthMethod.Login);
+
+ /* find all messages that have been sent since June the 1st */
+ uint uids = Client.Search( SearchCondition.SentSince( new DateTime(2012, 6, 1) ) );
+
+ /* fetch the messages and print out their subject lines */
+ MailMessage[] messages = Client.GetMessages( uids );
+
+ foreach(MailMessage m in messages)
+ Console.WriteLine("Subject: " + m.Subject);
+
+ Client.Dispose();
+
+
+
+
+
+
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_UsernamMe",
+ "My_Password", true, AuthMethod.Login);
+
+ MailboxStatus status = Client.GetStatus();
+
+ Console.WriteLine("Number of messages in the mailbox: " + status.Messages);
+ Console.WriteLine("Number of unread messages in the mailbox: " + status.Unread);
+
+ Client.Dispose();
+
+
+
+
+
+
+ This example demonstrates how to receive IMAP IDLE notifications.
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_UsernamMe",
+ "My_Password", true, AuthMethod.Login);
+
+ /* make sure our server actually supports IMAP IDLE */
+ if(!Client.Supports("IDLE"))
+ throw new Exception("This server does not support IMAP IDLE");
+
+ /* Our event handler will be called whenever a new message is received
+ by the server. */
+ Client.NewMessage += new EventHandler<IdleMessageEventArgs>(OnNewMessage);
+
+ Client.Dispose();
+
+ /* ........ */
+
+ void OnNewMessage(object sender, IdleMessageEventArgs e) {
+ Console.WriteLine("Received a new message!");
+ Console.WriteLine("Total number of messages in the mailbox: " +
+ e.MessageCount);
+ }
+
+
+
+
+
+
+ This example demonstrates how to receive IMAP IDLE notifications.
+
+ ImapClient Client = new ImapClient("imap.gmail.com", 993, "My_UsernamMe",
+ "My_Password", true, AuthMethod.Login);
+
+ /* make sure our server actually supports IMAP IDLE */
+ if(!Client.Supports("IDLE"))
+ throw new Exception("This server does not support IMAP IDLE");
+
+ /* Our event handler will be called whenever a message is deleted on the server. */
+ Client.MessageDeleted += new EventHandler<IdleMessageEventArgs>(OnMessageDeleted);
+
+ Client.Dispose();
+
+ /* ........ */
+
+ void OnMessageDeleted(object sender, IdleMessageEventArgs e) {
+ Console.WriteLine("A mail message was deleted on the server!");
+ Console.WriteLine("Total number of mail messages in the mailbox: " +
+ e.MessageCount);
+ }
+
+
+
+
+
\ No newline at end of file
diff --git a/Exceptions.cs b/Exceptions.cs
new file mode 100644
index 0000000..f8a640d
--- /dev/null
+++ b/Exceptions.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace S22.Imap {
+ ///
+ /// The exception is thrown when an unexpected response is received from the server.
+ ///
+ [Serializable()]
+ public class BadServerResponseException : Exception {
+ ///
+ /// Initializes a new instance of the BadServerResponseException class
+ ///
+ public BadServerResponseException() : base() { }
+ ///
+ /// Initializes a new instance of the BadServerResponseException class with its message
+ /// string set to .
+ ///
+ /// A description of the error. The content of message is intended
+ /// to be understood by humans.
+ public BadServerResponseException(string message) : base(message) { }
+ ///
+ /// Initializes a new instance of the BadServerResponseException class with its message
+ /// string set to and a reference to the inner exception that
+ /// is the cause of this exception.
+ ///
+ /// A description of the error. The content of message is intended
+ /// to be understood by humans.
+ /// The exception that is the cause of the current exception.
+ public BadServerResponseException(string message, Exception inner) : base(message, inner) { }
+ ///
+ /// Initializes a new instance of the BadServerResponseException class with the specified
+ /// serialization and context information.
+ ///
+ /// An object that holds the serialized object data about the exception
+ /// being thrown.
+ /// An object that contains contextual information about the source
+ /// or destination.
+ protected BadServerResponseException(SerializationInfo info, StreamingContext context) { }
+ }
+
+ ///
+ /// This exception is thrown when the supplied credentials in a login attempt were rejected
+ /// by the server.
+ ///
+ [Serializable()]
+ public class InvalidCredentialsException : Exception {
+ ///
+ /// Initializes a new instance of the InvalidCredentialsException class
+ ///
+ public InvalidCredentialsException() : base() { }
+ ///
+ /// Initializes a new instance of the InvalidCredentialsException class with its message
+ /// string set to .
+ ///
+ /// A description of the error. The content of message is intended
+ /// to be understood by humans.
+ public InvalidCredentialsException(string message) : base(message) { }
+ ///
+ /// Initializes a new instance of the InvalidCredentialsException class with its message
+ /// string set to and a reference to the inner exception that
+ /// is the cause of this exception.
+ ///
+ /// A description of the error. The content of message is intended
+ /// to be understood by humans.
+ /// The exception that is the cause of the current exception.
+ public InvalidCredentialsException(string message, Exception inner) : base(message, inner) { }
+ ///
+ /// Initializes a new instance of the InvalidCredentialsException class with the specified
+ /// serialization and context information.
+ ///
+ /// An object that holds the serialized object data about the exception
+ /// being thrown.
+ /// An object that contains contextual information about the source
+ /// or destination.
+ protected InvalidCredentialsException(SerializationInfo info, StreamingContext context) { }
+ }
+
+ ///
+ /// This exception is thrown when a client has not authenticated with the server and
+ /// attempts to call a method which can only be called in an authenticated context.
+ ///
+ [Serializable()]
+ public class NotAuthenticatedException : Exception {
+ ///
+ /// Initializes a new instance of the NotAuthenticatedException class
+ ///
+ public NotAuthenticatedException() : base() { }
+ ///
+ /// Initializes a new instance of the NotAuthenticatedException class with its message
+ /// string set to .
+ ///
+ /// A description of the error. The content of message is intended
+ /// to be understood by humans.
+ public NotAuthenticatedException(string message) : base(message) { }
+ ///
+ /// Initializes a new instance of the NotAuthenticatedException class with its message
+ /// string set to and a reference to the inner exception that
+ /// is the cause of this exception.
+ ///
+ /// A description of the error. The content of message is intended
+ /// to be understood by humans.
+ /// The exception that is the cause of the current exception.
+ public NotAuthenticatedException(string message, Exception inner) : base(message, inner) { }
+ ///
+ /// Initializes a new instance of the NotAuthenticatedException class with the specified
+ /// serialization and context information.
+ ///
+ /// An object that holds the serialized object data about the exception
+ /// being thrown.
+ /// An object that contains contextual information about the source
+ /// or destination.
+ protected NotAuthenticatedException(SerializationInfo info, StreamingContext context) { }
+ }
+
+}
\ No newline at end of file
diff --git a/IdleEvents.cs b/IdleEvents.cs
new file mode 100644
index 0000000..9243d37
--- /dev/null
+++ b/IdleEvents.cs
@@ -0,0 +1,53 @@
+using System;
+
+namespace S22.Imap {
+ ///
+ /// Provides data for IMAP idle notification events, such as the NewMessage and
+ /// MessageDelete events.
+ ///
+ public class IdleMessageEventArgs : EventArgs {
+ ///
+ /// Initializes a new instance of the IdleMessageEventArgs class and sets the
+ /// MessageCount attribute to the value of the
+ /// parameter.
+ ///
+ /// The number of messages in the selected
+ /// mailbox.
+ /// The unique identifier (UID) of the newest
+ /// message in the mailbox.
+ /// The instance of the ImapClient class that raised
+ /// the event.
+ internal IdleMessageEventArgs(uint MessageCount, uint MessageUID,
+ ImapClient Client) {
+ this.MessageCount = MessageCount;
+ this.MessageUID = MessageUID;
+ this.Client = Client;
+ }
+
+ ///
+ /// The total number of messages in the selected mailbox.
+ ///
+ public uint MessageCount {
+ get;
+ private set;
+ }
+
+ ///
+ /// The unique identifier (UID) of the newest message in the mailbox.
+ ///
+ /// The UID can be passed to the GetMessage method in order to retrieve
+ /// the mail message from the server.
+ public uint MessageUID {
+ get;
+ private set;
+ }
+
+ ///
+ /// The instance of the ImapClient class that raised the event.
+ ///
+ public ImapClient Client {
+ get;
+ private set;
+ }
+ }
+}
diff --git a/ImapClient.cs b/ImapClient.cs
new file mode 100644
index 0000000..0d7eb32
--- /dev/null
+++ b/ImapClient.cs
@@ -0,0 +1,900 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Mail;
+using System.Net.Security;
+using System.Net.Sockets;
+using System.Security.Cryptography;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+
+namespace S22.Imap {
+ ///
+ /// Allows applications to communicate with a mail server by using the
+ /// Internet Message Access Protocol (IMAP).
+ ///
+ public class ImapClient : IDisposable {
+ private Stream stream;
+ private TcpClient client;
+ private readonly object readLock = new object();
+ private readonly object writeLock = new object();
+ private string[] capabilities;
+ private int tag = 0;
+ private string selectedMailbox;
+ private string defaultMailbox = "INBOX";
+ private event EventHandler newMessageEvent;
+ private event EventHandler messageDeleteEvent;
+ private bool hasEvents {
+ get {
+ return newMessageEvent != null || messageDeleteEvent != null;
+ }
+ }
+ private bool idling;
+ private Thread idleThread;
+
+ ///
+ /// The default mailbox to operate on, when no specific mailbox name was indicated
+ /// to methods operating on mailboxes. This property is initially set to "INBOX".
+ ///
+ /// The value specified for a set operation is
+ /// null.
+ /// The value specified for a set operation is equal
+ /// to String.Empty ("").
+ /// This property is initialized to "INBOX"
+ public string DefaultMailbox {
+ get {
+ return defaultMailbox;
+ }
+ set {
+ if (value == null)
+ throw new ArgumentNullException();
+ if (value == String.Empty)
+ throw new ArgumentException();
+ defaultMailbox = value;
+ }
+ }
+
+ ///
+ /// Indicates whether the client is authenticated with the server
+ ///
+ public bool Authed {
+ get;
+ private set;
+ }
+
+ ///
+ /// This event is raised when a new mail message is received by the server.
+ ///
+ /// To probe a server for IMAP IDLE support, the
+ /// method can be used, specifying "IDLE" for the capability parameter.
+ ///
+ /// Notice that the event handler will be executed on a threadpool thread.
+ ///
+ ///
+ public event EventHandler NewMessage {
+ add {
+ newMessageEvent += value;
+ StartIdling();
+ }
+ remove {
+ newMessageEvent -= value;
+ if (!hasEvents)
+ StopIdling();
+ }
+ }
+
+ ///
+ /// This event is raised when a message is deleted on the server.
+ ///
+ /// To probe a server for IMAP IDLE support, the
+ /// method can be used, specifying "IDLE" for the capability parameter.
+ ///
+ /// Notice that the event handler will be executed on a threadpool thread.
+ ///
+ ///
+ public event EventHandler MessageDeleted {
+ add {
+ messageDeleteEvent += value;
+ StartIdling();
+ }
+ remove {
+ messageDeleteEvent -= value;
+ if (!hasEvents)
+ StopIdling();
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the ImapClient class and connects to the specified port
+ /// on the specified host, optionally using the Secure Socket Layer (SSL) security protocol.
+ ///
+ /// The DNS name of the server to which you intend to connect.
+ /// The port number of the server to which you intend to connect.
+ /// Set to true to use the Secure Socket Layer (SSL) security protocol.
+ /// Delegate used for verifying the remote Secure Sockets
+ /// Layer (SSL) certificate which is used for authentication. Set this to null if not needed
+ /// The port parameter is not between MinPort
+ /// and MaxPort.
+ /// The hostname parameter is null.
+ /// An error occurred while accessing the socket used for
+ /// establishing the connection to the IMAP server. Use the ErrorCode property to obtain the
+ /// specific error code
+ /// An authentication
+ /// error occured while trying to establish a secure connection.
+ /// Thrown if an unexpected response is received
+ /// from the server upon connecting.
+ ///
+ public ImapClient(string hostname, int port = 143, bool ssl = false,
+ RemoteCertificateValidationCallback validate = null) {
+ Connect(hostname, port, ssl, validate);
+ }
+
+ ///
+ /// Initializes a new instance of the ImapClient class and connects to the specified port on
+ /// the specified host, optionally using the Secure Socket Layer (SSL) security protocol and
+ /// attempts to authenticate with the server using the specified authentication method and
+ /// credentials.
+ ///
+ /// The DNS name of the server to which you intend to connect.
+ /// The port number of the server to which you intend to connect.
+ /// The username with which to login in to the IMAP server.
+ /// The password with which to log in to the IMAP server.
+ /// The requested method of authentication. Can be one of the values
+ /// of the AuthMethod enumeration.
+ /// Set to true to use the Secure Socket Layer (SSL) security protocol.
+ /// Delegate used for verifying the remote Secure Sockets Layer
+ /// (SSL) certificate which is used for authentication. Set this to null if not needed
+ /// The port parameter is not between MinPort
+ /// and MaxPort.
+ /// The hostname parameter is null.
+ /// An error occurred while accessing the socket used for
+ /// establishing the connection to the IMAP server. Use the ErrorCode property to obtain the
+ /// specific error code
+ /// An authentication
+ /// error occured while trying to establish a secure connection.
+ /// Thrown if an unexpected response is received
+ /// from the server upon connecting.
+ /// Thrown if authentication using the
+ /// supplied credentials failed.
+ ///
+ public ImapClient(string hostname, int port, string username, string password, AuthMethod method =
+ AuthMethod.Login, bool ssl = false, RemoteCertificateValidationCallback validate = null) {
+ Connect(hostname, port, ssl, validate);
+ Login(username, password, method);
+ }
+
+ ///
+ /// Connects to the specified port on the specified host, optionally using the Secure Socket Layer
+ /// (SSL) security protocol.
+ ///
+ /// The DNS name of the server to which you intend to connect.
+ /// The port number of the server to which you intend to connect.
+ /// Set to true to use the Secure Socket Layer (SSL) security protocol.
+ /// Delegate used for verifying the remote Secure Sockets
+ /// Layer (SSL) certificate which is used for authentication. Set this to null if not needed
+ /// The port parameter is not between MinPort
+ /// and MaxPort.
+ /// The hostname parameter is null.
+ /// An error occurred while accessing the socket used for
+ /// establishing the connection to the IMAP server. Use the ErrorCode property to obtain the
+ /// specific error code.
+ /// An authentication
+ /// error occured while trying to establish a secure connection.
+ /// Thrown if an unexpected response is received
+ /// from the server upon connecting.
+ private void Connect(string hostname, int port, bool ssl, RemoteCertificateValidationCallback validate) {
+ client = new TcpClient(hostname, port);
+ stream = client.GetStream();
+ if (ssl) {
+ SslStream sslStream = new SslStream(stream, false, validate ??
+ ((sender, cert, chain, err) => true));
+ sslStream.AuthenticateAsClient(hostname);
+ stream = sslStream;
+ }
+ /* Server issues untagged OK greeting upon connect */
+ string greeting = GetResponse();
+ if (!IsResponseOK(greeting))
+ throw new BadServerResponseException(greeting);
+ }
+
+ ///
+ /// Determines whether the received response is a valid IMAP OK response.
+ ///
+ /// A response string received from the server
+ /// A tag if the response is associated with a command
+ /// True if the response is a valid IMAP OK response, otherwise false
+ /// is returned.
+ private bool IsResponseOK(string response, string tag = null) {
+ if (tag != null)
+ return response.StartsWith(tag + "OK");
+ string v = response.Substring(response.IndexOf(' ')).Trim();
+ return v.StartsWith("OK");
+ }
+
+ ///
+ /// Attempts to establish an authenticated session with the server using the specified
+ /// credentials.
+ ///
+ /// The username with which to login in to the IMAP server.
+ /// The password with which to log in to the IMAP server.
+ /// The requested method of authentication. Can be one of the values
+ /// of the AuthMethod enumeration.
+ /// Thrown if authentication using the
+ /// supplied credentials failed.
+ ///
+ public void Login(string username, string password, AuthMethod method) {
+ string tag = GetTag();
+ string response = null;
+ switch (method) {
+ case AuthMethod.Login:
+ response = SendCommandGetResponse(tag + "LOGIN " + username.QuoteString() + " " +
+ password.QuoteString());
+ break;
+ case AuthMethod.CRAMMD5:
+ response = SendCommandGetResponse(tag + "AUTHENTICATE CRAM-MD5");
+ /* retrieve server key */
+ string key = Encoding.Default.GetString(
+ Convert.FromBase64String(response.Replace("+ ", "")));
+ /* compute the hash */
+ using (var kMd5 = new HMACMD5(Encoding.ASCII.GetBytes(password))) {
+ byte[] hash1 = kMd5.ComputeHash(Encoding.ASCII.GetBytes(key));
+ key = BitConverter.ToString(hash1).ToLower().Replace("-", "");
+ string command = Convert.ToBase64String(
+ Encoding.ASCII.GetBytes(username + " " + key));
+ response = SendCommandGetResponse(command);
+ }
+ break;
+ case AuthMethod.SaslOAuth:
+ response = SendCommandGetResponse(tag + "AUTHENTICATE XOAUTH " + password);
+ break;
+ }
+ /* Server may include a CAPABILITY response */
+ if (response.StartsWith("* CAPABILITY")) {
+ capabilities = response.Substring(13).Trim().Split(' ');
+ response = GetResponse();
+ }
+
+ if (!IsResponseOK(response, tag))
+ throw new InvalidCredentialsException(response);
+ Authed = true;
+ }
+
+ ///
+ /// Logs an authenticated client out of the server. After the logout sequence has
+ /// been completed, the server closes the connection with the client.
+ ///
+ /// Thrown if an unexpected response is
+ /// received from the server during the logout sequence
+ /// Calling Logout in a non-authenticated state has no effect
+ public void Logout() {
+ if (!Authed)
+ return;
+ StopIdling();
+ string tag = GetTag();
+ string bye = SendCommandGetResponse(tag + "LOGOUT");
+ if (!bye.StartsWith("* BYE"))
+ throw new BadServerResponseException(bye);
+ string response = GetResponse();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ Authed = false;
+ }
+
+ ///
+ /// Generates a unique identifier to prefix a command with, as is
+ /// required by the IMAP protocol.
+ ///
+ /// A unique identifier string
+ private string GetTag() {
+ Interlocked.Increment(ref tag);
+ return string.Format("xm{0:000} ", tag);
+ }
+
+ ///
+ /// Sends a command string to the server. This method blocks until the command has
+ /// been transmitted.
+ ///
+ /// Command string to be sent to the server. The command string is
+ /// suffixed by CRLF (as is required by the IMAP protocol) prior to sending.
+ private void SendCommand(string command) {
+ byte[] bytes = Encoding.ASCII.GetBytes(command + "\r\n");
+ lock (writeLock) {
+ stream.Write(bytes, 0, bytes.Length);
+ }
+ }
+
+ ///
+ /// Sends a command string to the server and subsequently waits for a response, which is
+ /// then returned to the caller. This method blocks until the server response has been
+ /// received.
+ ///
+ /// Command string to be sent to the server. The command string is
+ /// suffixed by CRLF (as is required by the IMAP protocol) prior to sending.
+ /// The response received by the server.
+ private string SendCommandGetResponse(string command) {
+ lock (readLock) {
+ lock (writeLock) {
+ SendCommand(command);
+ }
+ return GetResponse();
+ }
+ }
+
+ ///
+ /// Waits for a response from the server. This method blocks
+ /// until a response has been received.
+ ///
+ /// A response string from the server
+ private string GetResponse() {
+ const int Newline = 10, CarriageReturn = 13;
+ using (var mem = new MemoryStream()) {
+ lock (readLock) {
+ while (true) {
+ byte b = (byte)stream.ReadByte();
+ if (b == CarriageReturn)
+ continue;
+ if (b == Newline) {
+ return Encoding.ASCII.GetString(mem.ToArray());
+ } else
+ mem.WriteByte(b);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Returns a listing of capabilities that the IMAP server supports. All strings
+ /// in the returned array are guaranteed to be upper-case.
+ ///
+ /// Thrown if an unexpected response is received
+ /// from the server during the request. The message property of the exception contains the
+ /// error message returned by the server.
+ /// A listing of supported capabilities as an array of strings
+ public string[] Capabilities() {
+ if (capabilities != null)
+ return capabilities;
+ PauseIdling();
+ string tag = GetTag();
+ string command = tag + "CAPABILITY";
+ string response = SendCommandGetResponse(command);
+ /* Server is required to issue untagged capability response */
+ if (response.StartsWith("* CAPABILITY "))
+ response = response.Substring(13);
+ capabilities = response.Trim().Split(' ');
+ /* should return OK */
+ response = GetResponse();
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ return capabilities;
+ }
+
+ ///
+ /// Returns whether the specified capability is supported by the server.
+ ///
+ /// The capability to probe for (for example "IDLE")
+ /// Thrown if an unexpected response is received
+ /// from the server during the request. The message property of the exception contains
+ /// the error message returned by the server.
+ /// Returns true if the specified capability is supported by the server,
+ /// otherwise false is returned.
+ ///
+ public bool Supports(string capability) {
+ return (capabilities ?? Capabilities()).Contains(capability.ToUpper());
+ }
+
+ ///
+ /// Changes the name of a mailbox.
+ ///
+ /// The mailbox to rename.
+ /// The new name the mailbox will be renamed to.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the mailbox could
+ /// not be renamed. The message property of the exception contains the error message
+ /// returned by the server.
+ public void RenameMailbox(string mailbox, string newName) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "RENAME " +
+ mailbox.QuoteString() + " " + newName.QuoteString());
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ }
+
+ ///
+ /// Permanently removes a mailbox.
+ ///
+ /// Name of the mailbox to remove.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the mailbox could
+ /// not be removed. The message property of the exception contains the error message
+ /// returned by the server.
+ public void DeleteMailbox(string mailbox) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "DELETE " +
+ mailbox.QuoteString());
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ }
+
+ ///
+ /// Creates a new mailbox with the given name.
+ ///
+ /// Name of the mailbox to create.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the mailbox could
+ /// not be created. The message property of the exception contains the error message
+ /// returned by the server.
+ public void CreateMailbox(string mailbox) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "CREATE " +
+ mailbox.QuoteString());
+ ResumeIdling();
+ if(!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ }
+
+ ///
+ /// Selects a mailbox so that messages in the mailbox can be accessed.
+ ///
+ /// The mailbox to select. If this parameter is null, the
+ /// default mailbox is selected.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the mailbox could
+ /// not be selected. The message property of the exception contains the error message
+ /// returned by the server.
+ private void SelectMailbox(string mailbox) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ if (mailbox == null)
+ mailbox = defaultMailbox;
+ /* requested mailbox is already selected */
+ if (selectedMailbox == mailbox)
+ return;
+ PauseIdling();
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "SELECT " +
+ mailbox.QuoteString());
+ /* evaluate untagged data */
+ while (response.StartsWith("*")) {
+ // Fixme: evaluate data
+ response = GetResponse();
+ }
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ selectedMailbox = mailbox;
+ ResumeIdling();
+ }
+
+ ///
+ /// Permanently removes all messages that have the \Deleted flag set from the
+ /// specified mailbox.
+ ///
+ /// The mailbox to remove all messages from that have the
+ /// \Deleted flag set. If this parameter is omitted, the value of the DefaultMailbox
+ /// property is used to determine the mailbox to operate on.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the expunge operation could
+ /// not be completed. The message property of the exception contains the error message
+ /// returned by the server.
+ public void Expunge(string mailbox = null) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ SelectMailbox(mailbox);
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "EXPUNGE");
+ /* Server is required to send an untagged response for each message that is
+ * deleted before sending OK */
+ while (response.StartsWith("*"))
+ response = GetResponse();
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ }
+
+ ///
+ /// Retrieves status information (total number of messages, number of unread
+ /// messages, etc.) for the specified mailbox.
+ /// The mailbox to retrieve status information for. If this
+ /// parameter is omitted, the value of the DefaultMailbox property is used to
+ /// determine the mailbox to operate on.
+ /// A MailboxStatus object containing status information for the
+ /// mailbox.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the operation could
+ /// not be completed. The message property of the exception contains the error message
+ /// returned by the server.
+ ///
+ public MailboxStatus GetStatus(string mailbox = null) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ if (mailbox == null)
+ mailbox = defaultMailbox;
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "STATUS " +
+ mailbox.QuoteString() + " (MESSAGES UNSEEN)");
+ int messages = 0, unread = 0;
+ while (response.StartsWith("*")) {
+ Match m = Regex.Match(response, @"\* STATUS.*MESSAGES (\d+)");
+ if (m.Success)
+ messages = Convert.ToInt32(m.Groups[1].Value);
+ m = Regex.Match(response, @"\* STATUS.*UNSEEN (\d+)");
+ if (m.Success)
+ unread = Convert.ToInt32(m.Groups[1].Value);
+ response = GetResponse();
+ }
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ return new MailboxStatus(messages, unread);
+ }
+
+ ///
+ /// Searches the specified mailbox for messages that match the given
+ /// searching criteria.
+ ///
+ /// A search criteria expression. Only messages
+ /// that match this expression will be included in the result set returned
+ /// by this method.
+ /// The mailbox that will be searched. If this parameter is
+ /// omitted, the value of the DefaultMailbox property is used to determine the mailbox
+ /// to operate on.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the search could
+ /// not be completed. The message property of the exception contains the error
+ /// message returned by the server.
+ /// An array of unique identifier (UID) message attributes which
+ /// can be used with the GetMessage family of methods to download mail
+ /// messages.
+ /// A unique identifier (UID) is a 32-bit value assigned to each
+ /// message which uniquely identifies the message. No two messages share the
+ /// the same UID.
+ ///
+ public uint[] Search(SearchCondition criteria, string mailbox = null) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ SelectMailbox(mailbox);
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "UID SEARCH " +
+ criteria.ToString());
+ List result = new List();
+ while (response.StartsWith("*")) {
+ Match m = Regex.Match(response, @"^\* SEARCH (.*)");
+ if (m.Success) {
+ string[] v = m.Groups[1].Value.Trim().Split(' ');
+ foreach (string s in v)
+ result.Add(Convert.ToUInt32(s));
+ }
+ response = GetResponse();
+ }
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ return result.ToArray();
+ }
+
+ ///
+ /// Retrieves a mail message by its unique identifier message attribute.
+ ///
+ /// The unique identifier of the mail message to retrieve
+ /// Set this to true to set the \Seen flag for this message
+ /// on the server.
+ /// The mailbox the message will be retrieved from. If this
+ /// parameter is omitted, the value of the DefaultMailbox property is used to
+ /// determine the mailbox to operate on.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the mail message could
+ /// not be fetched. The message property of the exception contains the error message
+ /// returned by the server.
+ /// An initialized instance of the MailMessage class representing the
+ /// fetched mail message
+ /// A unique identifier (UID) is a 32-bit value assigned to each
+ /// message which uniquely identifies the message. No two messages share the
+ /// the same UID.
+ ///
+ public MailMessage GetMessage(uint uid, bool seen = true, string mailbox = null) {
+ MailMessage[] M = GetMessages(new uint[] { uid }, seen, mailbox);
+
+ return M[0];
+ }
+
+ ///
+ /// Retrieves a set of mail messages by their unique identifier message attributes.
+ ///
+ /// An array of unique identifiers of the mail messages to
+ /// retrieve
+ /// Set this to true to set the \Seen flag for the fetched
+ /// messages on the server.
+ /// The mailbox the messages will be retrieved from. If this
+ /// parameter is omitted, the value of the DefaultMailbox property is used to
+ /// determine the mailbox to operate on.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the mail messages could
+ /// not be fetched. The message property of the exception contains the error message
+ /// returned by the server.
+ /// An array of initialized instances of the MailMessage class representing
+ /// the fetched mail messages
+ /// A unique identifier (UID) is a 32-bit value assigned to each
+ /// message which uniquely identifies the message. No two messages share the
+ /// the same UID.
+ ///
+ public MailMessage[] GetMessages(uint[] uids, bool seen = true, string mailbox = null) {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ PauseIdling();
+ SelectMailbox(mailbox);
+ List messages = new List();
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "UID FETCH " +
+ string.Join(",", uids) + " (BODY" + (seen ? null : ".PEEK") + "[])");
+
+ /* ready any untagged responses */
+ while (response.StartsWith("*")) {
+ Match m = Regex.Match(response, @"\* (\d+) FETCH");
+ if (!m.Success)
+ throw new BadServerResponseException(response);
+ uint uid = Convert.ToUInt32(m.Groups[1].Value);
+ /* fetch the actual message header and data */
+ messages.Add(new MessageReader(GetResponse).
+ ReadMailMessage(uid));
+ response = GetResponse();
+ }
+ ResumeIdling();
+ if (!IsResponseOK(response, tag))
+ throw new BadServerResponseException(response);
+ return messages.ToArray();
+ }
+
+ ///
+ /// Starts receiving of IMAP IDLE notifications from the IMAP server.
+ ///
+ /// Thrown if the server does
+ /// not support the IMAP4 IDLE command.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the IDLE operation could
+ /// not be completed. The message property of the exception contains the error message
+ /// returned by the server.
+ /// Thrown if an unexpected program condition
+ /// occured.
+ /// Calling this method when already receiving idle notifications
+ /// has no effect.
+ ///
+ ///
+ ///
+ private void StartIdling() {
+ if (idling)
+ return;
+ idling = true;
+ ResumeIdling();
+ }
+
+ ///
+ /// Stops receiving of IMAP IDLE notifications from the IMAP server.
+ ///
+ /// Thrown if the server does
+ /// not support the IMAP4 IDLE command.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the IDLE operation could
+ /// not be completed. The message property of the exception contains the error message
+ /// returned by the server.
+ /// Calling this method when not receiving idle notifications
+ /// has no effect.
+ ///
+ ///
+ private void StopIdling() {
+ PauseIdling();
+ idling = false;
+ }
+
+ ///
+ /// Temporarily pauses receiving of IMAP IDLE notifications from the IMAP
+ /// server.
+ ///
+ /// Thrown if the server does
+ /// not support the IMAP4 IDLE command.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the IDLE operation could
+ /// not be completed. The message property of the exception contains the error message
+ /// returned by the server.
+ /// To resume receiving IDLE notifications ResumeIdling must be called
+ ///
+ ///
+ ///
+ private void PauseIdling() {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ if (!idling)
+ return;
+ if (!Supports("IDLE"))
+ throw new InvalidOperationException("The server does not support the " +
+ "IMAP4 IDLE command");
+ /* Send server "DONE" continuation-command to indicate we no longer wish
+ * to receive idle notifications. The server response is consumed by
+ * the idle thread and signals it to shut down.
+ */
+ SendCommand("DONE");
+
+ /* Wait until idle thread has shutdown */
+ idleThread.Join();
+ idleThread = null;
+ }
+
+ ///
+ /// Resumes receiving of IMAP IDLE notifications from the IMAP server.
+ ///
+ /// Thrown if the server does
+ /// not support the IMAP4 IDLE command.
+ /// Thrown if the method was called
+ /// in a non-authenticated state, i.e. before logging into the server with
+ /// valid credentials.
+ /// Thrown if the IDLE operation could
+ /// not be completed. The message property of the exception contains the error message
+ /// returned by the server.
+ /// Thrown if an unexpected program condition
+ /// occured.
+ /// This method is usually called in response to a prior call to the
+ /// PauseIdling method.
+ ///
+ private void ResumeIdling() {
+ if (!Authed)
+ throw new NotAuthenticatedException();
+ if (!idling)
+ return;
+ if (!Supports("IDLE"))
+ throw new InvalidOperationException("The server does not support the " +
+ "IMAP4 IDLE command");
+ /* Make sure a mailbox is selected */
+ SelectMailbox(null);
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "IDLE");
+ /* Server must respond with a '+' continuation response */
+ if (!response.StartsWith("+"))
+ throw new BadServerResponseException(response);
+ /* setup and start the idle thread */
+ if (idleThread != null)
+ throw new ApplicationException("idleThread is not null");
+ idleThread = new Thread(IdleLoop);
+ idleThread.Start();
+ }
+
+ ///
+ /// The main idle loop. Waits for incoming IMAP IDLE notifications and dispatches
+ /// them as events. This runs in its own thread whenever IMAP IDLE
+ /// notifications are to be received.
+ ///
+ private void IdleLoop() {
+ while (true) {
+ string response = WaitForResponse();
+ /* A request was made to stop idling so quit the thread */
+ if (response.Contains("OK IDLE"))
+ return;
+ Match m = Regex.Match(response, @"\*\s+(\d+)\s+(\w+)");
+ if (!m.Success)
+ continue;
+ /* Examine the notification */
+ uint numberOfMessages = Convert.ToUInt32(m.Groups[1].Value);
+ uint highestUID = 0;
+
+ switch (m.Groups[2].Value.ToUpper()) {
+ case "EXISTS":
+ ThreadPool.QueueUserWorkItem(callback => {
+ newMessageEvent.Raise(this,
+ new IdleMessageEventArgs(numberOfMessages, highestUID, this));
+ });
+ break;
+ case "EXPUNGE":
+ ThreadPool.QueueUserWorkItem(callback => messageDeleteEvent.Raise(
+ this, new IdleMessageEventArgs(numberOfMessages, highestUID, this)));
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Blocks until an IMAP notification has been received while taking
+ /// care of issuing NOOP's to the IMAP server at regular intervals
+ ///
+ /// The IMAP command received from the server
+ private string WaitForResponse() {
+ string response = null;
+ int noopInterval = (int)TimeSpan.FromMinutes(10).TotalMilliseconds;
+ AutoResetEvent ev = new AutoResetEvent(false);
+
+ ThreadPool.QueueUserWorkItem(_ => {
+ try {
+ response = GetResponse();
+ ev.Set();
+ } catch (IOException) {
+ /* Closing _Stream or the underlying _Connection instance will
+ * cause a WSACancelBlockingCall exception on a blocking socket.
+ * This is not an error so just let it pass.
+ */
+ }
+ });
+ if (ev.WaitOne(noopInterval))
+ return response;
+ /* Still here means the NOOP timeout was hit. WorkItem thread is still
+ * in a blocking read which _must_ be consumed.
+ */
+ SendCommand("DONE");
+ ev.WaitOne();
+ if (response.Contains("OK IDLE") == false) {
+ /* Shouldn't happen really */
+ }
+ /* Perform actual NOOP command and resume idling afterwards */
+ IssueNoop();
+ response = SendCommandGetResponse(GetTag() + "IDLE");
+ if (!response.StartsWith("+"))
+ throw new BadServerResponseException(response);
+ /* Go back to receiving IDLE notifications */
+ return WaitForResponse();
+ }
+
+ ///
+ /// Issues a NOOP command to the IMAP server.
+ ///
+ /// This is needed by the IMAP IDLE mechanism to give the server
+ /// an indication the connection is still active from time to time.
+ ///
+ private void IssueNoop() {
+ string tag = GetTag();
+ string response = SendCommandGetResponse(tag + "NOOP");
+ while (!response.StartsWith(tag))
+ response = GetResponse();
+ }
+
+ ///
+ /// Releases all resources used by this ImapClient object.
+ ///
+ public void Dispose() {
+ stream.Close();
+ client.Close();
+ stream = null;
+ client = null;
+
+ if (idleThread != null) {
+ idleThread.Abort();
+ idleThread = null;
+ }
+ }
+ }
+}
diff --git a/License.md b/License.md
new file mode 100644
index 0000000..82b81a4
--- /dev/null
+++ b/License.md
@@ -0,0 +1,22 @@
+### The MIT License
+
+Copyright (c) 2012 Torben Könke
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/MIMEPart.cs b/MIMEPart.cs
new file mode 100644
index 0000000..0f41c4e
--- /dev/null
+++ b/MIMEPart.cs
@@ -0,0 +1,25 @@
+using System.Collections.Specialized;
+
+namespace S22.Imap {
+ ///
+ /// Represents a part of a MIME multi-part message. Each part consists
+ /// of its own content header and a content body.
+ ///
+ internal class MIMEPart {
+ ///
+ /// A collection containing the content header information as
+ /// key-value pairs.
+ ///
+ public NameValueCollection header {
+ get;
+ set;
+ }
+ ///
+ /// A string containing the content body of the part.
+ ///
+ public string body {
+ get;
+ set;
+ }
+ }
+}
diff --git a/MailboxStatus.cs b/MailboxStatus.cs
new file mode 100644
index 0000000..d8f9d0b
--- /dev/null
+++ b/MailboxStatus.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace S22.Imap {
+ ///
+ /// Contains status information for a mailbox.
+ ///
+ public class MailboxStatus {
+ ///
+ /// Initializes a new MailboxStatus instance with the specified number
+ /// of total and unread messages.
+ ///
+ ///
+ ///
+ internal MailboxStatus(int Messages, int Unread) {
+ this.Messages = Messages;
+ this.Unread = Unread;
+ }
+
+ ///
+ /// The total number of messages in the mailbox.
+ ///
+ public int Messages {
+ get;
+ private set;
+ }
+
+ ///
+ /// The number of unread (unseen) messages in the mailbox.
+ ///
+ public int Unread {
+ get;
+ private set;
+ }
+ }
+}
diff --git a/MessageReader.cs b/MessageReader.cs
new file mode 100644
index 0000000..118fccb
--- /dev/null
+++ b/MessageReader.cs
@@ -0,0 +1,336 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.IO;
+using System.Net.Mail;
+using System.Net.Mime;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace S22.Imap {
+ internal delegate string GetResponseDelegate();
+
+ ///
+ /// A helper class for reading a mail message and building a MailMessage
+ /// instance out of it.
+ ///
+ internal class MessageReader {
+ private GetResponseDelegate GetResponse;
+
+ ///
+ /// Initializes a new instance of the MessageReader class using the
+ /// specified delegate.
+ ///
+ /// A delegate to the GetResponse method which
+ /// the MessageReader object invokes when it needs to read a line of
+ /// data from the server.
+ public MessageReader(GetResponseDelegate Delegate) {
+ GetResponse = Delegate;
+ }
+
+ ///
+ /// Reads and processes the message data sent by the server and constructs
+ /// a new MailMessage object from it.
+ ///
+ /// The UID of the mail message whose data the server is about
+ /// to send
+ /// An initialized instance of the MailMessage class representing the
+ /// fetched mail message
+ public MailMessage ReadMailMessage(uint uid) {
+ NameValueCollection header = ReadMailHeader();
+ NameValueCollection contentType = ParseMIMEField(
+ header["Content-Type"]);
+ string body = null;
+ MIMEPart[] parts = null;
+ if (contentType["boundary"] != null) {
+ parts = ReadMultipartBody(contentType["boundary"]);
+ } else {
+ /* Content-Type does not contain a boundary, assume it's not
+ * a MIME multipart message then
+ */
+ body = ReadMailBody();
+ }
+ return CreateMailmessage(header, body, parts);
+ }
+
+ ///
+ /// Reads the message header of a mail message and returns it as a
+ /// NameValueCollection.
+ ///
+ /// A NameValueCollection containing the header fields as keys
+ /// with their respective values as values.
+ private NameValueCollection ReadMailHeader() {
+ NameValueCollection header = new NameValueCollection();
+ string response, fieldname = null, fieldvalue = null;
+ while ((response = GetResponse()) != String.Empty) {
+ /* Values may stretch over several lines */
+ if (response[0] == ' ' || response[0] == '\t') {
+ header[fieldname] = header[fieldname] +
+ response.Substring(1).Trim();
+ continue;
+ }
+ /* The mail header consists of field:value pairs */
+ int delimiter = response.IndexOf(':');
+ fieldname = response.Substring(0, delimiter).Trim();
+ fieldvalue = response.Substring(delimiter + 1).Trim();
+ header.Add(fieldname, fieldvalue);
+ }
+ return header;
+ }
+
+ ///
+ /// Parses a MIME header field which can contain multiple 'parameter = value'
+ /// pairs (such as Content-Type: text/html; charset=iso-8859-1).
+ ///
+ /// The header field to parse
+ /// A NameValueCollection containing the parameter names as keys
+ /// with the respective parameter values as values.
+ /// The value of the actual field disregarding the 'parameter = value'
+ /// pairs is stored in the collection under the key "value" (in the above example
+ /// of Content-Type, this would be "text/html").
+ private NameValueCollection ParseMIMEField(string field) {
+ NameValueCollection coll = new NameValueCollection();
+ MatchCollection matches = Regex.Matches(field, @"([\w\-]+)=([\w\-\/]+)");
+ foreach (Match m in matches)
+ coll.Add(m.Groups[1].Value, m.Groups[2].Value);
+ Match mvalue = Regex.Match(field, @"^\s*([\w\/]+)");
+ coll.Add("value", mvalue.Success ? mvalue.Groups[1].Value : "");
+ return coll;
+ }
+
+ ///
+ /// Parses a mail header address-list field such as To, Cc and Bcc which
+ /// can contain multiple email addresses.
+ ///
+ /// The address-list field to parse
+ /// An array of strings containing the parsed mail
+ /// addresses.
+ private string[] ParseAddressList(string list) {
+ List mails = new List();
+ MatchCollection matches = Regex.Matches(list,
+ @"\b([A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4})\b", RegexOptions.IgnoreCase);
+ foreach (Match m in matches)
+ mails.Add(m.Groups[1].Value);
+ return mails.ToArray();
+ }
+
+ ///
+ /// Parses a mail message identifier from a string.
+ ///
+ /// The field to parse the message id from
+ /// Thrown when the field
+ /// argument does not contain a valid message identifier.
+ /// The parsed message id
+ /// A message identifier (msg-id) is a globally unique
+ /// identifier for a message.
+ private string ParseMessageId(string field) {
+ /* a msg-id is enclosed in < > brackets */
+ Match m = Regex.Match(field, @"<(.+)>");
+ if (m.Success)
+ return m.Groups[1].Value;
+ throw new ArgumentException("The field does not contain a valid message " +
+ "identifier: " + field);
+ }
+
+ ///
+ /// Reads the plain-text message body of a mail message.
+ ///
+ /// The message body of the mail message.
+ private string ReadMailBody() {
+ string response, body = "";
+ while ((response = GetResponse()) != ")")
+ body = body + response + "\r\n";
+ return body;
+ }
+
+ ///
+ /// Reads the message body of a MIME multipart message.
+ ///
+ /// The boundary string which separates
+ /// the different parts which make up the multipart-message
+ private MIMEPart[] ReadMultipartBody(string boundary) {
+ List parts = new List();
+ string s_boundary = "--" + boundary,
+ e_boundary = "--" + boundary + "--";
+ /* skip everything up to the first boundary */
+ string response = GetResponse();
+ while (!response.StartsWith(s_boundary))
+ response = GetResponse();
+ /* read MIME parts enclosed in boundary strings */
+ while (response.StartsWith(s_boundary)) {
+ MIMEPart part = new MIMEPart();
+ /* read content-header of part */
+ part.header = ReadMailHeader();
+ /* read content-body of part */
+ while (!(response = GetResponse()).StartsWith(s_boundary))
+ part.body = part.body + response + "\r\n";
+ /* add MIME part to the list */
+ parts.Add(part);
+ /* if the boundary is actually the end boundary, we're done */
+ if (response == e_boundary)
+ break;
+ }
+ /* next read should return closing bracket from FETCH command */
+ if ((response = GetResponse()) != ")")
+ throw new BadServerResponseException(response);
+ return parts.ToArray();
+ }
+
+ ///
+ /// Creates a new instance of the MailMessage class and initializes it using
+ /// the specified header and body information.
+ ///
+ /// A collection of mail and MIME headers
+ /// The mail body. May be null in case the message
+ /// is a MIME multi-part message in which case the MailMessage's body will
+ /// be set to the body of the first MIME part.
+ /// An array of MIME parts making up the message. If the
+ /// message is not a MIME multi-part message, this can be set to null.
+ ///
+ /// An initialized instance of the MailMessage class
+ private MailMessage CreateMailmessage(NameValueCollection header, string body,
+ MIMEPart[] parts) {
+ MailMessage m = new MailMessage();
+ NameValueCollection contentType = ParseMIMEField(
+ header["Content-Type"]);
+ m.Headers.Add(header);
+ if (parts != null) {
+ /* This takes care of setting the Body, BodyEncoding and IsBodyHtml fields also */
+ AddMIMEPartsToMessage(m, parts);
+ } else {
+ /* charset attribute should be part of content-type */
+ try {
+ m.BodyEncoding = Encoding.GetEncoding(
+ contentType["charset"]);
+ } catch {
+ m.BodyEncoding = Encoding.ASCII;
+ }
+ m.Body = body;
+ m.IsBodyHtml = contentType["value"].Contains("text/html");
+ }
+ Match ma = Regex.Match(header["Subject"], @"=\?([A-Za-z0-9\-]+)");
+ if (ma.Success) {
+ /* encoded-word subject */
+ m.SubjectEncoding = Encoding.GetEncoding(
+ ma.Groups[1].Value);
+ m.Subject = Util.DecodeWords(header["Subject"]);
+ } else {
+ m.SubjectEncoding = Encoding.ASCII;
+ m.Subject = header["Subject"];
+ }
+ m.Priority = header["Priority"] != null ?
+ PriorityMapping[header["Priority"]] : MailPriority.Normal;
+ SetAddressFields(m, header);
+ return m;
+ }
+
+ ///
+ /// A mapping to map MIME priority values to their MailPriority enum
+ /// counterparts.
+ ///
+ static private Dictionary PriorityMapping =
+ new Dictionary(StringComparer.OrdinalIgnoreCase) {
+ { "non-urgent", MailPriority.Low },
+ { "normal", MailPriority.Normal },
+ { "urgent", MailPriority.High }
+ };
+
+ ///
+ /// Sets the address fields (From, To, CC, etc.) of a MailMessage
+ /// object using the specified mail message header information.
+ ///
+ /// The MailMessage instance to operate on
+ /// A collection of mail and MIME headers
+ private void SetAddressFields(MailMessage m, NameValueCollection header) {
+ string[] addr = ParseAddressList(header["To"]);
+ foreach (string s in addr)
+ m.To.Add(s);
+ if (header["Cc"] != null) {
+ addr = ParseAddressList(header["Cc"]);
+ foreach (string s in addr)
+ m.CC.Add(s);
+ }
+ if (header["Bcc"] != null) {
+ addr = ParseAddressList(header["Bcc"]);
+ foreach (string s in addr)
+ m.Bcc.Add(s);
+ }
+ if (header["From"] != null) {
+ addr = ParseAddressList(header["From"]);
+ m.From = new MailAddress(addr.Length > 0 ? addr[0] : "");
+ }
+ if (header["Sender"] != null) {
+ addr = ParseAddressList(header["Sender"]);
+ m.Sender = new MailAddress(addr.Length > 0 ? addr[0] : "");
+ }
+ if (header["Reply-to"] != null) {
+ addr = ParseAddressList(header["Reply-to"]);
+ foreach (string s in addr)
+ m.ReplyToList.Add(s);
+ }
+ }
+
+ ///
+ /// Adds the parts of a MIME multi-part message to an instance of the
+ /// MailMessage class. MIME parts are either added to the AlternateViews
+ /// or to the Attachments collections depending on their type.
+ ///
+ /// The MailMessage instance to operate on
+ /// An array of MIME parts
+ private void AddMIMEPartsToMessage(MailMessage m, MIMEPart[] parts) {
+ for (int i = 0; i < parts.Length; i++) {
+ MIMEPart p = parts[i];
+ NameValueCollection contentType = ParseMIMEField(
+ p.header["Content-Type"]);
+ string transferEnc = p.header["Content-Transfer-Encoding"] ??
+ "none";
+ Encoding encoding = Encoding.GetEncoding(
+ contentType["Charset"] ?? "us-ascii");
+ string body = p.body;
+ /* decode content if it was encoded */
+ switch (transferEnc.ToLower()) {
+ case "quoted-printable":
+ body = Util.QPDecode(p.body, encoding);
+ break;
+ case "base64":
+ body = Util.Base64Decode(p.body, encoding);
+ break;
+ }
+ /* Put the first MIME part into the Body fields of the MailMessage
+ * instance */
+ if (i == 0) {
+ m.Body = body;
+ m.BodyEncoding = encoding;
+ m.IsBodyHtml = contentType["value"].ToLower()
+ .Contains("text/html");
+ continue;
+ }
+ string contentId;
+ try {
+ contentId = ParseMessageId(p.header["Content-Id"]);
+ } catch {
+ contentId = "";
+ }
+ MemoryStream stream = new MemoryStream(encoding.GetBytes(body));
+ NameValueCollection disposition = ParseMIMEField(
+ p.header["Content-Disposition"] ?? "");
+ if (disposition["value"].ToLower() == "attachment") {
+ /* attachments should have the Content-Disposition header set to
+ * "attachment" and possibly contain a name attribute */
+ string filename = disposition["filename"] ?? ("attachment" +
+ i.ToString());
+ Attachment attachment = new Attachment(stream, filename);
+ attachment.ContentId = contentId;
+ attachment.ContentType = new ContentType(p.header["Content-Type"]);
+ m.Attachments.Add(attachment);
+ } else {
+ AlternateView view = new AlternateView(stream,
+ new ContentType(p.header["Content-Type"]));
+ view.ContentId = contentId;
+ view.ContentType = new ContentType(p.header["Content-Type"]);
+ m.AlternateViews.Add(view);
+ }
+ }
+ }
+ }
+}
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..1519363
--- /dev/null
+++ b/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("S22.Imap")]
+[assembly: AssemblyDescription("A library for communicating with an IMAP mail server")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("None")]
+[assembly: AssemblyProduct("S22.Imap")]
+[assembly: AssemblyCopyright("Copyright © Torben Könke 2012")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("df1a2cd6-fa4f-4398-8d3a-0a4f61225203")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..db2b53b
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,47 @@
+### Introduction
+
+This repository contains an easy-to-use and well-documented .NET assembly for communicating with and
+receiving electronic mail from an Internet Message Access Protocol (IMAP) server.
+
+
+### Usage & Examples
+
+To use the library add the S22.Imap.dll assembly to your project references in Visual Studio. Here's
+a simple example that initializes a new instance of the ImapClient class and connects to Gmail's
+IMAP server:
+
+ using System;
+ using S22.Imap;
+
+ namespace Test {
+ class Program {
+ static void Main(string[] args) {
+ /* connect on port 993 using SSL */
+ using (ImapClient Client = new ImapClient("imap.gmail.com", 993, true))
+ {
+ Console.WriteLine("We are connected!");
+ }
+ }
+ }
+ }
+
+Please see the [documentation](http://smiley22.github.com/S22.Imap/Documentation/) for further details on using
+the classes and methods exposed by the S22.Imap namespace. Plenty of example codes are provided.
+
+
+### Credits
+
+This library is copyright © 2012 Torben Könke.
+
+Parts of this library are heavily based on the AE.Net.Mail project (copyright © 2012 Andy Edinborough).
+
+
+### License
+
+This library is released under the [MIT license](https://github.com/smiley22/smiley22.github.com/blob/master/License.md).
+
+
+### Bug reports
+
+Please send your bug reports and questions to [smileytwentytwo@gmail.com](mailto:smileytwentytwo@gmail.com) or create a new
+issue on the GitHub project homepage.
diff --git a/S22.Imap.csproj b/S22.Imap.csproj
new file mode 100644
index 0000000..95e9b4e
--- /dev/null
+++ b/S22.Imap.csproj
@@ -0,0 +1,71 @@
+
+
+
+ Debug
+ AnyCPU
+ 8.0.30703
+ 2.0
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}
+ Library
+ Properties
+ S22.Imap
+ S22.Imap
+ v4.0
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+ bin\Debug\S22.Imap.XML
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Designer
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/S22.Imap.sln b/S22.Imap.sln
new file mode 100644
index 0000000..c5b8893
--- /dev/null
+++ b/S22.Imap.sln
@@ -0,0 +1,30 @@
+
+Microsoft Visual Studio Solution File, Format Version 11.00
+# Visual Studio 2010
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S22.Imap", "S22.Imap.csproj", "{369C32A5-E099-4BD5-BBBF-51713947CA99}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|Mixed Platforms = Debug|Mixed Platforms
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|Mixed Platforms = Release|Mixed Platforms
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Release|Any CPU.Build.0 = Release|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {369C32A5-E099-4BD5-BBBF-51713947CA99}.Release|x86.ActiveCfg = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/SearchCondition.cs b/SearchCondition.cs
new file mode 100644
index 0000000..0de029c
--- /dev/null
+++ b/SearchCondition.cs
@@ -0,0 +1,378 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+
+namespace S22.Imap {
+ ///
+ /// Chainable search conditions to be used with the Search method.
+ ///
+ public class SearchCondition {
+ ///
+ /// Finds messages that contain the specified string in the header or body of the
+ /// message.
+ ///
+ /// String to search messages for
+ /// A SearchCondition object representing the "text" search criterion
+ public static SearchCondition Text(string text) {
+ return new SearchCondition { Field = Fields.Text, Value = text };
+ }
+ ///
+ /// Finds messages that contain the specified string in the envelope structure's
+ /// BCC field.
+ ///
+ /// String to search the envelope structure's BCC field for
+ /// A SearchCondition object representing the "BCC" search criterion
+ public static SearchCondition BCC(string text) {
+ return new SearchCondition { Field = Fields.BCC, Value = text };
+ }
+ ///
+ /// Finds messages whose internal date (disregarding time and timezone) is
+ /// earlier than the specified date.
+ ///
+ /// Date to compare the message's internal date with
+ /// A SearchCondition object representing the "Before" search criterion
+ public static SearchCondition Before(DateTime date) {
+ return new SearchCondition { Field = Fields.Before, Value = date };
+ }
+ ///
+ /// Finds messages that contain the specified string in the body of the
+ /// message.
+ ///
+ /// String to search the message body for
+ /// A SearchCondition object representing the "Body" search criterion
+ public static SearchCondition Body(string text) {
+ return new SearchCondition { Field = Fields.Body, Value = text };
+ }
+ ///
+ /// Finds messages that contain the specified string in the envelope structure's
+ /// CC field.
+ ///
+ /// String to search the envelope structure's CC field for
+ /// A SearchCondition object representing the "CC" search criterion
+ public static SearchCondition Cc(string text) {
+ return new SearchCondition { Field = Fields.Cc, Value = text };
+ }
+ ///
+ /// Finds messages that contain the specified string in the envelope structure's
+ /// FROM field.
+ ///
+ /// String to search the envelope structure's FROM field for
+ /// A SearchCondition object representing the "FROM" search criterion
+ public static SearchCondition From(string text) {
+ return new SearchCondition { Field = Fields.From, Value = text };
+ }
+ ///
+ /// Finds messages that have a header with the specified field-name and that
+ /// contains the specified string in the text of the header.
+ ///
+ /// field-name of the header to search for
+ /// String to search for in the text of the header
+ /// A SearchCondition object representing the "HEADER" search
+ /// criterion
+ ///
+ /// If the string to search is zero-length, this matches all messages
+ /// that have a header line with the specified field-name regardless of the
+ /// contents.
+ ///
+ public static SearchCondition Header(string name, string text) {
+ return new SearchCondition { Field = Fields.Header,
+ Value = name + " " + text.QuoteString() };
+ }
+ ///
+ /// Finds messages with the specified keyword flag set.
+ ///
+ /// Keyword flag to search for
+ /// A SearchCondition object representing the "KEYWORD" search
+ /// criterion
+ public static SearchCondition Keyword(string text) {
+ return new SearchCondition { Field = Fields.Keyword, Value = text };
+ }
+ ///
+ /// Finds messages with a size larger than the specified number of bytes.
+ ///
+ /// Minimum size, in bytes a message must have to be
+ /// included in the result set
+ /// A SearchCondition object representing the "LARGER" search
+ /// criterion
+ public static SearchCondition Larger(long size) {
+ return new SearchCondition { Field = Fields.Larger, Value = size };
+ }
+ ///
+ /// Finds messages with a size smaller than the specified number of bytes.
+ ///
+ /// Maximum size, in bytes a message must have to be
+ /// included in the result set
+ /// A SearchCondition object representing the "SMALLER" search
+ /// criterion
+ public static SearchCondition Smaller(long size) {
+ return new SearchCondition { Field = Fields.Smaller, Value = size };
+ }
+ ///
+ /// Finds messages whose Date: header (disregarding time and timezone) is
+ /// earlier than the specified date.
+ ///
+ /// Date to compare the Date: header field with.
+ /// A SearchCondition object representing the "SENTBEFORE" search
+ /// criterion
+ public static SearchCondition SentBefore(DateTime date) {
+ return new SearchCondition { Field = Fields.SentBefore, Value = date };
+ }
+ ///
+ /// Finds messages whose Date: header (disregarding time and timezone) is
+ /// within the specified date.
+ ///
+ /// Date to compare the Date: header field with.
+ /// A SearchCondition object representing the "SENTON" search
+ /// criterion
+ public static SearchCondition SentOn(DateTime date) {
+ return new SearchCondition { Field = Fields.SentOn, Value = date };
+ }
+ ///
+ /// Finds messages whose Date: header (disregarding time and timezone) is
+ /// within or later than the specified date.
+ ///
+ /// Date to compare the Date: header field with.
+ /// A SearchCondition object representing the "SENTSINCE" search
+ /// criterion
+ public static SearchCondition SentSince(DateTime date) {
+ return new SearchCondition { Field = Fields.SentSince, Value = date };
+ }
+ ///
+ /// Finds messages that contain the specified string in the envelope
+ /// structure's SUBJECT field.
+ ///
+ /// String to search the envelope structure's SUBJECT
+ /// field for
+ /// A SearchCondition object representing the "SUBJECT" search
+ /// criterion
+ public static SearchCondition Subject(string text) {
+ return new SearchCondition { Field = Fields.Subject, Value = text };
+ }
+ ///
+ /// Finds messages that contain the specified string in the envelope
+ /// structure's TO field.
+ ///
+ /// String to search the envelope structure's TO
+ /// field for
+ /// A SearchCondition object representing the "TO" search
+ /// criterion
+ public static SearchCondition To(string text) {
+ return new SearchCondition { Field = Fields.To, Value = text };
+ }
+ ///
+ /// Finds messages with unique identifiers corresponding to the specified
+ /// unique identifier set. Sequence set ranges are permitted.
+ ///
+ /// String of whitespace-separated list of unique
+ /// identifiers to search for
+ /// A SearchCondition object representing the "UID" search
+ /// criterion
+ public static SearchCondition UID(string ids) {
+ return new SearchCondition { Field = Fields.UID, Value = ids };
+ }
+ ///
+ /// Finds messages that do not have the specified keyword flag set.
+ ///
+ /// A valid IMAP keyword flag
+ /// A SearchCondition object representing the "UNKEYWORD"
+ /// search criterion
+ public static SearchCondition Unkeyword(string text) {
+ return new SearchCondition { Field = Fields.Unkeyword, Value = text };
+ }
+ ///
+ /// Finds messages with the \Answered flag set.
+ ///
+ /// A SearchCondition object representing the "ANSWERED" search
+ /// criterion
+ public static SearchCondition Answered() {
+ return new SearchCondition { Field = Fields.Answered };
+ }
+ ///
+ /// Finds messages with the \Deleted flag set.
+ ///
+ /// A SearchCondition object representing the "DELETED" search
+ /// criterion
+ public static SearchCondition Deleted() {
+ return new SearchCondition { Field = Fields.Deleted };
+ }
+ ///
+ /// Finds messages with the \Draft flag set.
+ ///
+ /// A SearchCondition object representing the "DRAFT" search
+ /// criterion
+ public static SearchCondition Draft() {
+ return new SearchCondition { Field = Fields.Draft };
+ }
+ ///
+ /// Finds messages with the \Flagged flag set.
+ ///
+ /// A SearchCondition object representing the "FLAGGED" search
+ /// criterion
+ public static SearchCondition Flagged() {
+ return new SearchCondition { Field = Fields.Flagged };
+ }
+ ///
+ /// Finds messages that have the \Recent flag set but not the \Seen flag.
+ ///
+ /// A SearchCondition object representing the "NEW" search
+ /// criterion
+ public static SearchCondition New() {
+ return new SearchCondition { Field = Fields.New };
+ }
+ ///
+ /// Finds messages that do not have the \Recent flag set.
+ ///
+ /// A SearchCondition object representing the "OLD" search
+ /// criterion
+ public static SearchCondition Old() {
+ return new SearchCondition { Field = Fields.Old };
+ }
+ ///
+ /// Finds messages that have the \Recent flag set.
+ ///
+ /// A SearchCondition object representing the "RECENT" search
+ /// criterion
+ public static SearchCondition Recent() {
+ return new SearchCondition { Field = Fields.Recent };
+ }
+ ///
+ /// Finds messages that have the \Seen flag set.
+ ///
+ /// A SearchCondition object representing the "SEEN" search
+ /// criterion
+ public static SearchCondition Seen() {
+ return new SearchCondition { Field = Fields.Seen };
+ }
+ ///
+ /// Finds messages that do not have the \Answered flag set.
+ ///
+ /// A SearchCondition object representing the "UNANSWERED" search
+ /// criterion
+ public static SearchCondition Unanswered() {
+ return new SearchCondition { Field = Fields.Unanswered };
+ }
+ ///
+ /// Finds messages that do not have the \Deleted flag set.
+ ///
+ /// A SearchCondition object representing the "UNDELETED" search
+ /// criterion
+ public static SearchCondition Undeleted() {
+ return new SearchCondition { Field = Fields.Undeleted };
+ }
+ ///
+ /// Finds messages that do not have the \Draft flag set.
+ ///
+ /// A SearchCondition object representing the "UNDRAFT" search
+ /// criterion
+ public static SearchCondition Undraft() {
+ return new SearchCondition { Field = Fields.Undraft };
+ }
+ ///
+ /// Finds messages that do not have the \Flagged flag set.
+ ///
+ /// A SearchCondition object representing the "UNFLAGGED" search
+ /// criterion
+ public static SearchCondition Unflagged() {
+ return new SearchCondition { Field = Fields.Unflagged };
+ }
+ ///
+ /// Finds messages that do not have the \Seen flag set.
+ ///
+ /// A SearchCondition object representing the "UNSEEN" search
+ /// criterion
+ public static SearchCondition Unseen() {
+ return new SearchCondition { Field = Fields.Unseen };
+ }
+
+ ///
+ /// Logically ANDs multiple search conditions, meaning a message will only
+ /// be included in the search result set if all conditions are met.
+ ///
+ /// A search condition to logically AND this
+ /// SearchCondition instance with
+ /// A new SearchCondition instance which can be further chained
+ /// with other search conditions.
+ public SearchCondition And(params SearchCondition[] other) {
+ return Join(string.Empty, this, other);
+ }
+
+ ///
+ /// Logically negates search conditions, meaning a message will only
+ /// be included in the search result set if the specified conditions
+ /// are not met.
+ ///
+ /// A search condition that must not be met by a
+ /// message for it to be included in the search result set
+ /// A new SearchCondition instance which can be further chained
+ /// with other search conditions.
+ public SearchCondition Not(params SearchCondition[] other) {
+ return Join("NOT", this, other);
+ }
+
+ ///
+ /// Logically ORs multiple search conditions, meaning a message will be
+ /// included in the search result set if it meets at least one of the
+ /// conditions.
+ ///
+ /// A search condition to logically OR this
+ /// SearchCondition instance with
+ /// A new SearchCondition instance which can be further chained
+ /// with other search conditions.
+ public SearchCondition Or(params SearchCondition[] other) {
+ return Join("OR", this, other);
+ }
+
+ private enum Fields {
+ BCC, Before, Body, Cc, From, Header, Keyword,
+ Larger, On, SentBefore, SentOn, SentSince, Since, Smaller, Subject,
+ Text, To, UID, Unkeyword, All, Answered, Deleted, Draft, Flagged,
+ New, Old, Recent, Seen, Unanswered, Undeleted, Undraft, Unflagged, Unseen
+ }
+
+ private object Value { get; set; }
+ private Fields? Field { get; set; }
+ private List Conditions { get; set; }
+ private string Operator { get; set; }
+
+ private static SearchCondition Join(string condition, SearchCondition left,
+ params SearchCondition[] right) {
+ condition = condition.ToUpper();
+ if (left.Operator != condition)
+ left = new SearchCondition {
+ Operator = condition,
+ Conditions = new List { left }
+ };
+ left.Conditions.AddRange(right);
+ return left;
+ }
+
+ ///
+ /// Constructs a string from this SearchCondition object using the proper syntax
+ /// as is required for the IMAP SEARCH command.
+ ///
+ /// A string representing this SearchCondition instance that can be
+ /// used with the IMAP SEARCH command.
+ public override string ToString() {
+ if (Conditions != null && Conditions.Count > 0 && Operator != null) {
+ return (Operator.ToUpper() + " (" +
+ string.Join(") (", Conditions) + ")").Trim();
+ }
+ StringBuilder builder = new StringBuilder();
+ if (Field != null)
+ builder.Append(Field.ToString().ToUpper());
+ if (Value != null) {
+ if (Value is string) {
+ Value = ((string)Value).QuoteString();
+ } else if (Value is DateTime) {
+ Value = ((DateTime)Value).ToString("dd-MMM-yyyy",
+ CultureInfo.InvariantCulture).QuoteString();
+ }
+ if (Field != null)
+ builder.Append(" ");
+ builder.Append(Value);
+ }
+ return builder.ToString();
+ }
+ }
+}
diff --git a/Util.cs b/Util.cs
new file mode 100644
index 0000000..a9ff535
--- /dev/null
+++ b/Util.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace S22.Imap {
+ ///
+ /// A static utility class mainly containing methods for decoding encoded
+ /// non-ASCII data as is often used in mail messages.
+ ///
+ internal static class Util {
+ ///
+ /// Returns a copy of the string enclosed in double-quotes and with escaped
+ /// CRLF, back-slash and double-quote characters (as is expected by some
+ /// commands of the IMAP protocol).
+ ///
+ /// Extends the System.String class
+ /// A copy of the string enclosed in double-quotes and properly
+ /// escaped as is required by the IMAP protocol.
+ internal static string QuoteString(this string value) {
+ return "\"" + value
+ .Replace("\\", "\\\\")
+ .Replace("\r", "\\r")
+ .Replace("\n", "\\n")
+ .Replace("\"", "\\\"") + "\"";
+ }
+
+ ///
+ /// Raises the event. Ensures the event is only raised, if it is not null.
+ ///
+ /// Extends System.EventHandler class"/>
+ /// Extends System.EventHandler class
+ /// The sender of the event
+ /// The event arguments associated with this event
+ internal static void Raise(this EventHandler @event, object sender, T args)
+ where T : EventArgs {
+ if (@event == null)
+ return;
+ @event(sender, args);
+ }
+
+ ///
+ /// Decodes a string composed of one or several MIME 'encoded-words'.
+ ///
+ /// A string to composed of one or several MIME
+ /// 'encoded-words'
+ /// Thrown when an unknown encoding
+ /// (other than Q-Encoding or Base64) is encountered.
+ /// A concatenation of all enconded-words in the passed
+ /// string
+ public static string DecodeWords(string words) {
+ MatchCollection matches = Regex.Matches(words,
+ @"(=\?[A-Za-z0-9\-]+\?[BbQq]\?[^\?]+\?=)");
+ string decoded = String.Empty;
+ foreach (Match m in matches)
+ decoded = decoded + DecodeWord(m.ToString());
+ return decoded;
+ }
+
+ ///
+ /// Decodes a MIME 'encoded-word' string.
+ ///
+ /// The encoded word to decode
+ /// Thrown when an unknown encoding
+ /// (other than Q-Encoding or Base64) is encountered.
+ /// A decoded string
+ /// MIME encoded-word syntax is a way to encode strings that
+ /// contain non-ASCII data. Commonly used encodings for the encoded-word
+ /// sytax are Q-Encoding and Base64. For an in-depth description, refer
+ /// to RFC 2047
+ internal static string DecodeWord(string word) {
+ Match m = Regex.Match(word,
+ @"=\?([A-Za-z0-9\-]+)\?([BbQq])\?(.+)\?=");
+ if (!m.Success)
+ return word;
+ Encoding encoding = null;
+ try {
+ encoding = Encoding.GetEncoding(
+ m.Groups[1].Value);
+ } catch (ArgumentException) {
+ encoding = Encoding.ASCII;
+ }
+ string type = m.Groups[2].Value.ToUpper();
+ string text = m.Groups[3].Value;
+ switch (type) {
+ case "Q":
+ return Util.QDecode(text, encoding);
+ case "B":
+ return Util.Base64Decode(text, encoding);
+ default:
+ throw new FormatException("Encoding not recognized " +
+ "in encoded word: " + word);
+ }
+ }
+
+ ///
+ /// Takes a Q-encoded string and decodes it using the specified
+ /// encoding.
+ ///
+ /// The Q-encoded string to decode
+ /// The encoding to use for encoding the
+ /// returned string
+ /// A Q-decoded string
+ internal static string QDecode(string value, Encoding encoding) {
+ MatchCollection matches = Regex.Matches(value, @"=[0-9A-Z]{2}",
+ RegexOptions.Multiline);
+ foreach (Match match in matches) {
+ char hexChar = (char)Convert.ToInt32(
+ match.Groups[0].Value.Substring(1), 16);
+ value = value.Replace(match.Groups[0].Value, hexChar.ToString());
+ }
+ value = value.Replace("=\r\n", "").Replace("_", " ");
+ return encoding.GetString(
+ Encoding.Default.GetBytes(value));
+ }
+
+ ///
+ /// Takes a quoted-printable-encoded string and decodes it using
+ /// the specifiednencoding.
+ ///
+ /// The quoted-printable-encoded string to
+ /// decode
+ /// The encoding to use for encoding the
+ /// returned string
+ /// A quoted-printable-decoded string
+ internal static string QPDecode(string value, Encoding encoding) {
+ MatchCollection matches = Regex.Matches(value, @"=[0-9A-Z]{2}",
+ RegexOptions.Multiline);
+ foreach (Match match in matches) {
+ char hexChar = (char)Convert.ToInt32(
+ match.Groups[0].Value.Substring(1), 16);
+ value = value.Replace(match.Groups[0].Value, hexChar.ToString());
+ }
+ value = value.Replace("=\r\n", "");
+ return encoding.GetString(
+ Encoding.Default.GetBytes(value));
+ }
+
+ ///
+ /// Takes a Base64-encoded string and decodes it using the specified
+ /// encoding.
+ ///
+ /// The Base64-encoded string to decode
+ /// The encoding to use for encoding the returned
+ /// string
+ /// A Base64-decoded string
+ internal static string Base64Decode(string value, Encoding encoding) {
+ byte[] bytes = Convert.FromBase64String(value);
+ return encoding.GetString(bytes);
+ }
+ }
+}