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