namespace ConsoleApplication { using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using System.IO; using System.Net; using System.Net.Mail; using System.Net.Mime; using Microshaoft; using OpenPop.Mime; using OpenPop.Pop3; /// <summary> /// Class1 的摘要说明。 /// </summary> public class Program { /// <summary> /// 应用程序的主入口点。 /// </summary> //[STAThread] static void Main(string[] args) { OpenPopHelper.FetchAllMessages ( "pop3.live.com" , 995 , true , "XXXX@live.com" , "!@#123QWE" , (message, i, client) => { return MessageFunc(message, i, client); } ); Console.WriteLine("Hello World"); Console.WriteLine(Environment.Version.ToString()); Console.ReadLine(); } static bool MessageFunc(Message message, int messageNumber, Pop3Client client) { Console.WriteLine(message.Headers.Subject); Console.WriteLine(message.Headers.From); Console.WriteLine(message.Headers.MessageId); Console.WriteLine(messageNumber); MemoryStream stream = new MemoryStream(); message.Save(stream); byte[] data = StreamDataHelper.ReadDataToBytes(stream); stream.Close(); stream.Dispose(); stream = null; // to do /* * .eml byte[] 入: SharePoint Document Library */ List<MessagePart> list = message.FindAllAttachments(); Parallel.ForEach<MessagePart> ( list , part => { Console.WriteLine(part.ContentType.MediaType); string fileName = string.Format ( "{1}{0}{2}" , "." , Guid.NewGuid().ToString() , part.FileName.Trim ( new char[] { '"' ,' ' ,'\t' } ) ); Console.WriteLine(fileName); stream = new MemoryStream(); part.Save(stream); data = StreamDataHelper.ReadDataToBytes(stream); stream.Close(); stream.Dispose(); stream = null; // to do /* * 附件入 SharePoint document Library */ } ); //删除 return false; } static void SendMailSmtp(string[] args) { string html = "<html><body><a href=\"http://www.live.com\"><img src=\"cid:attachment1\"></a>"; html += "<script src=\"cid:attachment2\"></script>中国字"; html += "<a href=\"http://www.google.com\"><br><img src=\"cid:attachment1\"></a><script>alert('mail body xss')<script></body></html>"; AlternateView view = AlternateView.CreateAlternateViewFromString(html, null, MediaTypeNames.Text.Html); LinkedResource picture = new LinkedResource(@"d:\pic.JPG", MediaTypeNames.Image.Jpeg); picture.ContentId = "attachment1"; view.LinkedResources.Add(picture); //LinkedResource script = new LinkedResource(@"a.js", MediaTypeNames.Text.Plain); //script.ContentId = "attachment2"; //view.LinkedResources.Add(script); MailMessage mail = new MailMessage(); mail.AlternateViews.Add(view); mail.From = new MailAddress("test@qqt.com", "<script>alert('mail from xss')</script>"); mail.To.Add(new MailAddress("qq@gmail.com", "<script>alert('mail to xss')</script>")); mail.To.Add(new MailAddress("qq@qq.com", "<script>alert('mail to xss')</script>")); mail.Subject = "<script>alert('mail subject xss')</script>" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); SmtpClient client = new SmtpClient("smtp.live.com"); //client.Port = 465; client.Credentials = new NetworkCredential("XXXX@live.com","!@#123QWE"); client.EnableSsl = true; client.Send(mail); Console.WriteLine("Hello World"); Console.WriteLine(Environment.Version.ToString()); } } } //========================================================================================================== namespace Microshaoft { using System.IO; public static class StreamDataHelper { public static byte[] ReadDataToBytes(Stream stream) { byte[] buffer = new byte[64 * 1024]; MemoryStream ms = new MemoryStream(); int r = 0; int l = 0; long position = -1; if (stream.CanSeek) { position = stream.Position; stream.Position = 0; } while (true) { r = stream.Read(buffer, 0, buffer.Length); if (r > 0) { l += r; ms.Write(buffer, 0, r); } else { break; } } byte[] bytes = new byte[l]; ms.Position = 0; ms.Read(bytes, 0, (int)l); ms.Close(); ms.Dispose(); ms = null; if (position >= 0) { stream.Position = position; } return bytes; } } } //============================================================================================================ namespace Microshaoft { using System; using System.Collections.Generic; using System.IO; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using System.Text; using OpenPop.Common.Logging; using OpenPop.Mime; using OpenPop.Mime.Decode; using OpenPop.Mime.Header; using OpenPop.Pop3; /// <summary> /// These are small examples problems for the /// <see cref="OpenPop"/>.NET POP3 library /// </summary> public class OpenPopHelper { public static void FetchAllMessages ( string hostname , int port , bool useSsl , string username , string password , Func<Message, int, Pop3Client, bool> messageFunc ) { // The client disconnects from the server when being disposed using (Pop3Client client = new Pop3Client()) { // Connect to the server client.Connect(hostname, port, useSsl); // Authenticate ourselves towards the server client.Authenticate(username, password); // Get the number of messages in the inbox int messageCount = client.GetMessageCount(); // We want to download all messages //List<Message> allMessages = new List<Message>(messageCount); // Messages are numbered in the interval: [1, messageCount] // Ergo: message numbers are 1-based. for (int i = 1; i <= messageCount; i++) { //allMessages.Add(client.GetMessage(i)); Message message = client.GetMessage(i); bool r = messageFunc(message, i, client); if (r) { client.DeleteMessage(i); } } // Now return the fetched messages //return allMessages; } } /// <summary> /// Example showing: /// - how to fetch all messages from a POP3 server /// </summary> /// <param name="hostname">Hostname of the server. For example: pop3.live.com</param> /// <param name="port">Host port to connect to. Normally: 110 for plain POP3, 995 for SSL POP3</param> /// <param name="useSsl">Whether or not to use SSL to connect to server</param> /// <param name="username">Username of the user on the server</param> /// <param name="password">Password of the user on the server</param> /// <returns>All Messages on the POP3 server</returns> public static List<Message> FetchAllMessages ( string hostname , int port , bool useSsl , string username , string password ) { // The client disconnects from the server when being disposed using (Pop3Client client = new Pop3Client()) { // Connect to the server client.Connect(hostname, port, useSsl); // Authenticate ourselves towards the server client.Authenticate(username, password); // Get the number of messages in the inbox int messageCount = client.GetMessageCount(); // We want to download all messages List<Message> allMessages = new List<Message>(messageCount); // Messages are numbered in the interval: [1, messageCount] // Ergo: message numbers are 1-based. for (int i = 1; i <= messageCount; i++) { allMessages.Add(client.GetMessage(i)); } // Now return the fetched messages return allMessages; } } /// <summary> /// Example showing: /// - how to delete fetch an emails headers only /// - how to delete a message from the server /// </summary> /// <param name="client">A connected and authenticated Pop3Client from which to delete a message</param> /// <param name="messageId">A message ID of a message on the POP3 server. Is located in <see cref="MessageHeader.MessageId"/></param> /// <returns><see langword="true"/> if message was deleted, <see langword="false"/> otherwise</returns> public bool DeleteMessageByMessageId(Pop3Client client, string messageId) { // Get the number of messages on the POP3 server int messageCount = client.GetMessageCount(); // Run trough each of these messages and download the headers for (int messageItem = messageCount; messageItem > 0; messageItem--) { // If the Message ID of the current message is the same as the parameter given, delete that message if (client.GetMessageHeaders(messageItem).MessageId == messageId) { // Delete client.DeleteMessage(messageItem); return true; } } // We did not find any message with the given messageId, report this back return false; } /// <summary> /// Example showing: /// - how to a find plain text version in a Message /// - how to save MessageParts to file /// </summary> /// <param name="message">The message to examine for plain text</param> public static void FindPlainTextInMessage(Message message) { MessagePart plainText = message.FindFirstPlainTextVersion(); if (plainText != null) { // Save the plain text to a file, database or anything you like plainText.Save(new FileInfo("plainText.txt")); } } /// <summary> /// Example showing: /// - how to find a html version in a Message /// - how to save MessageParts to file /// </summary> /// <param name="message">The message to examine for html</param> public static void FindHtmlInMessage(Message message) { MessagePart html = message.FindFirstHtmlVersion(); if (html != null) { // Save the plain text to a file, database or anything you like html.Save(new FileInfo("html.txt")); } } /// <summary> /// Example showing: /// - how to find a MessagePart with a specified MediaType /// - how to get the body of a MessagePart as a string /// </summary> /// <param name="message">The message to examine for xml</param> public static void FindXmlInMessage(Message message) { MessagePart xml = message.FindFirstMessagePartWithMediaType("text/xml"); if (xml != null) { // Get out the XML string from the email string xmlString = xml.GetBodyAsText(); System.Xml.XmlDocument doc = new System.Xml.XmlDocument(); // Load in the XML read from the email doc.LoadXml(xmlString); // Save the xml to the filesystem doc.Save("test.xml"); } } /// <summary> /// Example showing: /// - how to fetch only headers from a POP3 server /// - how to examine some of the headers /// - how to fetch a full message /// - how to find a specific attachment and save it to a file /// </summary> /// <param name="hostname">Hostname of the server. For example: pop3.live.com</param> /// <param name="port">Host port to connect to. Normally: 110 for plain POP3, 995 for SSL POP3</param> /// <param name="useSsl">Whether or not to use SSL to connect to server</param> /// <param name="username">Username of the user on the server</param> /// <param name="password">Password of the user on the server</param> /// <param name="messageNumber"> /// The number of the message to examine. /// Must be in range [1, messageCount] where messageCount is the number of messages on the server. /// </param> public static void HeadersFromAndSubject(string hostname, int port, bool useSsl, string username, string password, int messageNumber) { // The client disconnects from the server when being disposed using (Pop3Client client = new Pop3Client()) { // Connect to the server client.Connect(hostname, port, useSsl); // Authenticate ourselves towards the server client.Authenticate(username, password); // We want to check the headers of the message before we download // the full message MessageHeader headers = client.GetMessageHeaders(messageNumber); RfcMailAddress from = headers.From; string subject = headers.Subject; // Only want to download message if: // - is from test@xample.com // - has subject "Some subject" if (from.HasValidMailAddress && from.Address.Equals("test@example.com") && "Some subject".Equals(subject)) { // Download the full message Message message = client.GetMessage(messageNumber); // We know the message contains an attachment with the name "useful.pdf". // We want to save this to a file with the same name foreach (MessagePart attachment in message.FindAllAttachments()) { if (attachment.FileName.Equals("useful.pdf")) { // Save the raw bytes to a file File.WriteAllBytes(attachment.FileName, attachment.Body); } } } } } /// <summary> /// Example showing: /// - how to delete a specific message from a server /// </summary> /// <param name="hostname">Hostname of the server. For example: pop3.live.com</param> /// <param name="port">Host port to connect to. Normally: 110 for plain POP3, 995 for SSL POP3</param> /// <param name="useSsl">Whether or not to use SSL to connect to server</param> /// <param name="username">Username of the user on the server</param> /// <param name="password">Password of the user on the server</param> /// <param name="messageNumber"> /// The number of the message to delete. /// Must be in range [1, messageCount] where messageCount is the number of messages on the server. /// </param> public static void DeleteMessageOnServer(string hostname, int port, bool useSsl, string username, string password, int messageNumber) { // The client disconnects from the server when being disposed using (Pop3Client client = new Pop3Client()) { // Connect to the server client.Connect(hostname, port, useSsl); // Authenticate ourselves towards the server client.Authenticate(username, password); // Mark the message as deleted // Notice that it is only MARKED as deleted // POP3 requires you to "commit" the changes // which is done by sending a QUIT command to the server // You can also reset all marked messages, by sending a RSET command. client.DeleteMessage(messageNumber); // When a QUIT command is sent to the server, the connection between them are closed. // When the client is disposed, the QUIT command will be sent to the server // just as if you had called the Disconnect method yourself. } } /// <summary> /// Example showing: /// - how to use UID's (unique ID's) of messages from the POP3 server /// - how to download messages not seen before /// (notice that the POP3 protocol cannot see if a message has been read on the server /// before. Therefore the client need to maintain this state for itself) /// </summary> /// <param name="hostname">Hostname of the server. For example: pop3.live.com</param> /// <param name="port">Host port to connect to. Normally: 110 for plain POP3, 995 for SSL POP3</param> /// <param name="useSsl">Whether or not to use SSL to connect to server</param> /// <param name="username">Username of the user on the server</param> /// <param name="password">Password of the user on the server</param> /// <param name="seenUids"> /// List of UID's of all messages seen before. /// New message UID's will be added to the list. /// Consider using a HashSet if you are using >= 3.5 .NET /// </param> /// <returns>A List of new Messages on the server</returns> public static List<Message> FetchUnseenMessages(string hostname, int port, bool useSsl, string username, string password, List<string> seenUids) { // The client disconnects from the server when being disposed using (Pop3Client client = new Pop3Client()) { // Connect to the server client.Connect(hostname, port, useSsl); // Authenticate ourselves towards the server client.Authenticate(username, password); // Fetch all the current uids seen List<string> uids = client.GetMessageUids(); // Create a list we can return with all new messages List<Message> newMessages = new List<Message>(); // All the new messages not seen by the POP3 client for (int i = 0; i < uids.Count; i++) { string currentUidOnServer = uids[i]; if (!seenUids.Contains(currentUidOnServer)) { // We have not seen this message before. // Download it and add this new uid to seen uids // the uids list is in messageNumber order - meaning that the first // uid in the list has messageNumber of 1, and the second has // messageNumber 2. Therefore we can fetch the message using // i + 1 since messageNumber should be in range [1, messageCount] Message unseenMessage = client.GetMessage(i + 1); // Add the message to the new messages newMessages.Add(unseenMessage); // Add the uid to the seen uids, as it has now been seen seenUids.Add(currentUidOnServer); } } // Return our new found messages return newMessages; } } /// <summary> /// Example showing: /// - how to set timeouts /// - how to override the SSL certificate checks with your own implementation /// </summary> /// <param name="hostname">Hostname of the server. For example: pop3.live.com</param> /// <param name="port">Host port to connect to. Normally: 110 for plain POP3, 995 for SSL POP3</param> /// <param name="timeouts">Read and write timeouts used by the Pop3Client</param> public static void BypassSslCertificateCheck(string hostname, int port, int timeouts) { // The client disconnects from the server when being disposed using (Pop3Client client = new Pop3Client()) { // Connect to the server using SSL with specified settings // true here denotes that we connect using SSL // The certificateValidator can validate the SSL certificate of the server. // This might be needed if the server is using a custom normally untrusted certificate client.Connect(hostname, port, true, timeouts, timeouts, certificateValidator); // Do something extra now that we are connected to the server } } private static bool certificateValidator(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslpolicyerrors) { // We should check if there are some SSLPolicyErrors, but here we simply say that // the certificate is okay - we trust it. return true; } /// <summary> /// Example showing: /// - how to save a message to a file /// - how to load a message from a file at a later point /// </summary> /// <param name="message">The message to save and load at a later point</param> /// <returns>The Message, but loaded from the file system</returns> public static Message SaveAndLoadFullMessage(Message message) { // FileInfo about the location to save/load message FileInfo file = new FileInfo("someFile.eml"); // Save the full message to some file message.Save(file); // Now load the message again. This could be done at a later point Message loadedMessage = Message.Load(file); // use the message again return loadedMessage; } /// <summary> /// Example showing: /// - How to change logging /// - How to implement your own logger /// </summary> public static void ChangeLogging() { // All logging is sent trough logger defined at DefaultLogger.Log // The logger can be changed by calling DefaultLogger.SetLog(someLogger) // By default all logging is sent to the System.Diagnostics.Trace facilities. // These are not very useful if you are not debugging // Instead, lets send logging to a file: DefaultLogger.SetLog(new FileLogger()); FileLogger.LogFile = new FileInfo("MyLoggingFile.log"); // It is also possible to implement your own logging: DefaultLogger.SetLog(new MyOwnLogger()); } class MyOwnLogger : ILog { public void LogError(string message) { Console.WriteLine("ERROR!!!: " + message); } public void LogDebug(string message) { // Dont want to log debug messages } } /// <summary> /// Example showing: /// - How to provide custom Encoding class /// - How to use UTF8 as default Encoding /// </summary> /// <param name="customEncoding">Own Encoding implementation</param> public void InsertCustomEncodings(Encoding customEncoding) { // Lets say some email contains a characterSet of "iso-9999-9" which // is fictional, but is really just UTF-8. // Lets add that mapping to the class responsible for finding // the Encoding from the name of it EncodingFinder.AddMapping("iso-9999-9", Encoding.UTF8); // It is also possible to implement your own Encoding if // the framework does not provide what you need EncodingFinder.AddMapping("specialEncoding", customEncoding); // Now, if the EncodingFinder is not able to find an encoding, lets // see if we can find one ourselves EncodingFinder.FallbackDecoder = CustomFallbackDecoder; } Encoding CustomFallbackDecoder(string characterSet) { // Is it a "foo" encoding? if (characterSet.StartsWith("foo")) return Encoding.ASCII; // then use ASCII // If no special encoding could be found, provide UTF8 as default. // You can also return null here, which would tell OpenPop that // no encoding could be found. This will then throw an exception. return Encoding.UTF8; } // Other examples to show, that is in the library // Show how to build a TreeNode representation of the Message hierarchy using the // TreeNodeBuilder class in OpenPopTest } } //============================================================================================================ namespace OpenPop.Pop3 { using System; using System.Collections.Generic; using System.Globalization; using System.Net; using System.Net.Security; using System.Net.Sockets; using System.IO; using System.Text; using System.Text.RegularExpressions; using OpenPop.Mime; using OpenPop.Mime.Header; using OpenPop.Pop3.Exceptions; using OpenPop.Common; using OpenPop.Common.Logging; /// <summary> /// POP3 compliant POP Client<br/> /// <br/> /// If you want to override where logging is sent, look at <see cref="DefaultLogger"/> /// </summary> /// <example> /// Examples are available on the <a href="http://hpop.sourceforge.net/">project homepage</a>. /// </example> public class Pop3Client : Disposable { #region Private member properties /// <summary> /// The stream used to communicate with the server /// </summary> private Stream Stream { get; set; } /// <summary> /// This is the last response the server sent back when a command was issued to it /// </summary> private string LastServerResponse { get; set; } /// <summary> /// The APOP time stamp sent by the server in it's welcome message if APOP is supported. /// </summary> private string ApopTimeStamp { get; set; } /// <summary> /// Describes what state the <see cref="Pop3Client"/> is in /// </summary> private ConnectionState State { get; set; } #endregion #region Public member properties /// <summary> /// Tells whether the <see cref="Pop3Client"/> is connected to a POP server or not /// </summary> public bool Connected { get; private set; } /// <summary> /// Allows you to check if the server supports /// the <see cref="AuthenticationMethod.Apop"/> authentication method.<br/> /// <br/> /// This value is filled when the connect method has returned, /// as the server tells in its welcome message if APOP is supported. /// </summary> public bool ApopSupported { get; private set; } #endregion #region Constructors /// <summary> /// Constructs a new Pop3Client for you to use. /// </summary> public Pop3Client() { SetInitialValues(); } #endregion #region IDisposable implementation /// <summary> /// Disposes the <see cref="Pop3Client"/>.<br/> /// This is the implementation of the <see cref="IDisposable"/> interface.<br/> /// Sends the QUIT command to the server before closing the streams. /// </summary> /// <param name="disposing"><see langword="true"/> if managed and unmanaged code should be disposed, <see langword="false"/> if only managed code should be disposed</param> protected override void Dispose(bool disposing) { if (disposing && !IsDisposed) { if (Connected) { Disconnect(); } } base.Dispose(disposing); } #endregion #region Connection managing methods /// <summary> /// Connect to the server using user supplied stream /// </summary> /// <param name="stream">The stream used to communicate with the server</param> /// <exception cref="ArgumentNullException">If <paramref name="stream"/> is <see langword="null"/></exception> public void Connect(Stream stream) { AssertDisposed(); if (State != ConnectionState.Disconnected) throw new InvalidUseException("You cannot ask to connect to a POP3 server, when we are already connected to one. Disconnect first."); if (stream == null) throw new ArgumentNullException("stream"); Stream = stream; // Fetch the server one-line welcome greeting string response = StreamUtility.ReadLineAsAscii(Stream); // Check if the response was an OK response try { // Assume we now need the user to supply credentials // If we do not connect correctly, Disconnect will set the // state to Disconnected // If this is not set, Disconnect will throw an exception State = ConnectionState.Authorization; IsOkResponse(response); ExtractApopTimestamp(response); Connected = true; } catch (PopServerException e) { // If not close down the connection and abort DisconnectStreams(); DefaultLogger.Log.LogError("Connect(): " + "Error with connection, maybe POP3 server not exist"); DefaultLogger.Log.LogDebug("Last response from server was: " + LastServerResponse); throw new PopServerNotAvailableException("Server is not available", e); } } /// <summary> /// Connects to a remote POP3 server using default timeouts of 60.000 milliseconds /// </summary> /// <param name="hostname">The <paramref name="hostname"/> of the POP3 server</param> /// <param name="port">The port of the POP3 server</param> /// <param name="useSsl"><see langword="true"/> if SSL should be used. <see langword="false"/> if plain TCP should be used.</param> /// <exception cref="PopServerNotAvailableException">If the server did not send an OK message when a connection was established</exception> /// <exception cref="PopServerNotFoundException">If it was not possible to connect to the server</exception> /// <exception cref="ArgumentNullException">If <paramref name="hostname"/> is <see langword="null"/></exception> /// <exception cref="ArgumentOutOfRangeException">If port is not in the range [<see cref="IPEndPoint.MinPort"/>, <see cref="IPEndPoint.MaxPort"/></exception> public void Connect(string hostname, int port, bool useSsl) { const int defaultTimeOut = 60000; Connect(hostname, port, useSsl, defaultTimeOut, defaultTimeOut, null); } /// <summary> /// Connects to a remote POP3 server /// </summary> /// <param name="hostname">The <paramref name="hostname"/> of the POP3 server</param> /// <param name="port">The port of the POP3 server</param> /// <param name="useSsl"><see langword="true"/> if SSL should be used. <see langword="false"/> if plain TCP should be used.</param> /// <param name="receiveTimeout">Timeout in milliseconds before a socket should time out from reading. Set to 0 or -1 to specify infinite timeout.</param> /// <param name="sendTimeout">Timeout in milliseconds before a socket should time out from sending. Set to 0 or -1 to specify infinite timeout.</param> /// <param name="certificateValidator">If you want to validate the certificate in a SSL connection, pass a reference to your validator. Supply <see langword="null"/> if default should be used.</param> /// <exception cref="PopServerNotAvailableException">If the server did not send an OK message when a connection was established</exception> /// <exception cref="PopServerNotFoundException">If it was not possible to connect to the server</exception> /// <exception cref="ArgumentNullException">If <paramref name="hostname"/> is <see langword="null"/></exception> /// <exception cref="ArgumentOutOfRangeException">If port is not in the range [<see cref="IPEndPoint.MinPort"/>, <see cref="IPEndPoint.MaxPort"/> or if any of the timeouts is less than -1.</exception> public void Connect(string hostname, int port, bool useSsl, int receiveTimeout, int sendTimeout, RemoteCertificateValidationCallback certificateValidator) { AssertDisposed(); if (hostname == null) throw new ArgumentNullException("hostname"); if (hostname.Length == 0) throw new ArgumentException("hostname cannot be empty", "hostname"); if (port > IPEndPoint.MaxPort || port < IPEndPoint.MinPort) throw new ArgumentOutOfRangeException("port"); if (receiveTimeout < -1) throw new ArgumentOutOfRangeException("receiveTimeout"); if (sendTimeout < -1) throw new ArgumentOutOfRangeException("sendTimeout"); if (State != ConnectionState.Disconnected) throw new InvalidUseException("You cannot ask to connect to a POP3 server, when we are already connected to one. Disconnect first."); TcpClient clientSocket = new TcpClient(); clientSocket.ReceiveTimeout = receiveTimeout; clientSocket.SendTimeout = sendTimeout; try { clientSocket.Connect(hostname, port); } catch (SocketException e) { // Close the socket - we are not connected, so no need to close stream underneath clientSocket.Close(); DefaultLogger.Log.LogError("Connect(): " + e.Message); throw new PopServerNotFoundException("Server not found", e); } Stream stream; if (useSsl) { // If we want to use SSL, open a new SSLStream on top of the open TCP stream. // We also want to close the TCP stream when the SSL stream is closed // If a validator was passed to us, use it. SslStream sslStream; if (certificateValidator == null) { sslStream = new SslStream(clientSocket.GetStream(), false); } else { sslStream = new SslStream(clientSocket.GetStream(), false, certificateValidator); } sslStream.ReadTimeout = receiveTimeout; sslStream.WriteTimeout = sendTimeout; // Authenticate the server sslStream.AuthenticateAsClient(hostname); stream = sslStream; } else { // If we do not want to use SSL, use plain TCP stream = clientSocket.GetStream(); } // Now do the connect with the same stream being used to read and write to Connect(stream); } /// <summary> /// Disconnects from POP3 server. /// Sends the QUIT command before closing the connection, which deletes all the messages that was marked as such. /// </summary> public void Disconnect() { AssertDisposed(); if (State == ConnectionState.Disconnected) throw new InvalidUseException("You cannot disconnect a connection which is already disconnected"); try { SendCommand("QUIT"); } finally { DisconnectStreams(); } } #endregion #region Authentication methods /// <summary> /// Authenticates a user towards the POP server using <see cref="AuthenticationMethod.Auto"/>.<br/> /// If this authentication fails but you are sure that the username and password is correct, it might /// be that that the POP3 server is wrongly telling the client it supports <see cref="AuthenticationMethod.Apop"/>. /// You should try using <see cref="Authenticate(string, string, AuthenticationMethod)"/> while passing <see cref="AuthenticationMethod.UsernameAndPassword"/> to the method. /// </summary> /// <param name="username">The username</param> /// <param name="password">The user password</param> /// <exception cref="InvalidLoginException">If the user credentials was not accepted</exception> /// <exception cref="PopServerLockedException">If the server said the the mailbox was locked</exception> /// <exception cref="ArgumentNullException">If <paramref name="username"/> or <paramref name="password"/> is <see langword="null"/></exception> /// <exception cref="LoginDelayException">If the server rejects the login because of too recent logins</exception> public void Authenticate(string username, string password) { AssertDisposed(); Authenticate(username, password, AuthenticationMethod.Auto); } /// <summary> /// Authenticates a user towards the POP server using some <see cref="AuthenticationMethod"/>. /// </summary> /// <param name="username">The username</param> /// <param name="password">The user password</param> /// <param name="authenticationMethod">The way that the client should authenticate towards the server</param> /// <exception cref="NotSupportedException">If <see cref="AuthenticationMethod.Apop"/> is used, but not supported by the server</exception> /// <exception cref="InvalidLoginException">If the user credentials was not accepted</exception> /// <exception cref="PopServerLockedException">If the server said the the mailbox was locked</exception> /// <exception cref="ArgumentNullException">If <paramref name="username"/> or <paramref name="password"/> is <see langword="null"/></exception> /// <exception cref="LoginDelayException">If the server rejects the login because of too recent logins</exception> public void Authenticate(string username, string password, AuthenticationMethod authenticationMethod) { AssertDisposed(); if (username == null) throw new ArgumentNullException("username"); if (password == null) throw new ArgumentNullException("password"); if (State != ConnectionState.Authorization) throw new InvalidUseException("You have to be connected and not authorized when trying to authorize yourself"); try { switch (authenticationMethod) { case AuthenticationMethod.UsernameAndPassword: AuthenticateUsingUserAndPassword(username, password); break; case AuthenticationMethod.Apop: AuthenticateUsingApop(username, password); break; case AuthenticationMethod.Auto: if (ApopSupported) AuthenticateUsingApop(username, password); else AuthenticateUsingUserAndPassword(username, password); break; case AuthenticationMethod.CramMd5: AuthenticateUsingCramMd5(username, password); break; } } catch (PopServerException e) { DefaultLogger.Log.LogError("Problem logging in using method " + authenticationMethod + ". Server response was: " + LastServerResponse); // Throw a more specific exception if special cases of failure is detected // using the response the server generated when the last command was sent CheckFailedLoginServerResponse(LastServerResponse, e); // If no special failure is detected, tell that the login credentials were wrong throw new InvalidLoginException(e); } // We are now authenticated and therefore we enter the transaction state State = ConnectionState.Transaction; } /// <summary> /// Authenticates a user towards the POP server using the USER and PASSWORD commands /// </summary> /// <param name="username">The username</param> /// <param name="password">The user password</param> /// <exception cref="PopServerException">If the server responded with -ERR</exception> private void AuthenticateUsingUserAndPassword(string username, string password) { SendCommand("USER " + username); SendCommand("PASS " + password); // Authentication was successful if no exceptions thrown before getting here } /// <summary> /// Authenticates a user towards the POP server using APOP /// </summary> /// <param name="username">The username</param> /// <param name="password">The user password</param> /// <exception cref="NotSupportedException">Thrown when the server does not support APOP</exception> /// <exception cref="PopServerException">If the server responded with -ERR</exception> private void AuthenticateUsingApop(string username, string password) { if (!ApopSupported) throw new NotSupportedException("APOP is not supported on this server"); SendCommand("APOP " + username + " " + Apop.ComputeDigest(password, ApopTimeStamp)); // Authentication was successful if no exceptions thrown before getting here } /// <summary> /// Authenticates using the CRAM-MD5 authentication method /// </summary> /// <param name="username">The username</param> /// <param name="password">The user password</param> /// <exception cref="NotSupportedException">Thrown when the server does not support AUTH CRAM-MD5</exception> /// <exception cref="InvalidLoginException">If the user credentials was not accepted</exception> /// <exception cref="PopServerLockedException">If the server said the the mailbox was locked</exception> /// <exception cref="LoginDelayException">If the server rejects the login because of too recent logins</exception> private void AuthenticateUsingCramMd5(string username, string password) { // Example of communication: // C: AUTH CRAM-MD5 // S: + PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2UucmVzdG9uLm1jaS5uZXQ+ // C: dGltIGI5MTNhNjAyYzdlZGE3YTQ5NWI0ZTZlNzMzNGQzODkw // S: +OK CRAM authentication successful // Other example, where AUTH CRAM-MD5 is not supported // C: AUTH CRAM-MD5 // S: -ERR Authentication method CRAM-MD5 not supported try { SendCommand("AUTH CRAM-MD5"); } catch (PopServerException e) { // A PopServerException will be thrown if the server responds with a -ERR not supported throw new NotSupportedException("CRAM-MD5 authentication not supported", e); } // Fetch out the challenge from the server response string challenge = LastServerResponse.Substring(2); // Compute the challenge response string response = CramMd5.ComputeDigest(username, password, challenge); // Send the response to the server SendCommand(response); // Authentication was successful if no exceptions thrown before getting here } #endregion #region Public POP3 commands /// <summary> /// Get the number of messages on the server using a STAT command /// </summary> /// <returns>The message count on the server</returns> /// <exception cref="PopServerException">If the server did not accept the STAT command</exception> public int GetMessageCount() { AssertDisposed(); if (State != ConnectionState.Transaction) throw new InvalidUseException("You cannot get the message count without authenticating yourself towards the server first"); return SendCommandIntResponse("STAT", 1); } /// <summary> /// Marks the message with the given message number as deleted.<br/> /// <br/> /// The message will not be deleted until a QUIT command is sent to the server.<br/> /// This is done when you call <see cref="Disconnect()"/> or when the Pop3Client is <see cref="Dispose">Disposed</see>. /// </summary> /// <param name="messageNumber"> /// The number of the message to be deleted. This message may not already have been deleted.<br/> /// The <paramref name="messageNumber"/> must be inside the range [1, messageCount] /// </param> /// <exception cref="PopServerException">If the server did not accept the delete command</exception> public void DeleteMessage(int messageNumber) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("You cannot delete any messages without authenticating yourself towards the server first"); SendCommand("DELE " + messageNumber); } /// <summary> /// Marks all messages as deleted.<br/> /// <br/> /// The messages will not be deleted until a QUIT command is sent to the server.<br/> /// This is done when you call <see cref="Disconnect()"/> or when the Pop3Client is <see cref="Dispose">Disposed</see>.<br/> /// The method assumes that no prior message has been marked as deleted, and is not valid to call if this is wrong. /// </summary> /// <exception cref="PopServerException">If the server did not accept one of the delete commands. All prior marked messages will still be marked.</exception> public void DeleteAllMessages() { AssertDisposed(); int messageCount = GetMessageCount(); for (int messageItem = messageCount; messageItem > 0; messageItem--) { DeleteMessage(messageItem); } } /// <summary> /// Keep server active by sending a NOOP command.<br/> /// This might keep the server from closing the connection due to inactivity.<br/> /// <br/> /// RFC:<br/> /// The POP3 server does nothing, it merely replies with a positive response. /// </summary> /// <exception cref="PopServerException">If the server did not accept the NOOP command</exception> public void NoOperation() { AssertDisposed(); if (State != ConnectionState.Transaction) throw new InvalidUseException("You cannot use the NOOP command unless you are authenticated to the server"); SendCommand("NOOP"); } /// <summary> /// Send a reset command to the server.<br/> /// <br/> /// RFC:<br/> /// If any messages have been marked as deleted by the POP3 /// server, they are unmarked. The POP3 server then replies /// with a positive response. /// </summary> /// <exception cref="PopServerException">If the server did not accept the RSET command</exception> public void Reset() { AssertDisposed(); if (State != ConnectionState.Transaction) throw new InvalidUseException("You cannot use the RSET command unless you are authenticated to the server"); SendCommand("RSET"); } /// <summary> /// Get a unique ID for a single message.<br/> /// </summary> /// <param name="messageNumber"> /// Message number, which may not be marked as deleted.<br/> /// The <paramref name="messageNumber"/> must be inside the range [1, messageCount] /// </param> /// <returns>The unique ID for the message</returns> /// <exception cref="PopServerException">If the server did not accept the UIDL command. This could happen if the <paramref name="messageNumber"/> does not exist</exception> public string GetMessageUid(int messageNumber) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot get message ID, when the user has not been authenticated yet"); // Example from RFC: //C: UIDL 2 //S: +OK 2 QhdPYR:00WBw1Ph7x7 SendCommand("UIDL " + messageNumber); // Parse out the unique ID return LastServerResponse.Split(' ')[2]; } /// <summary> /// Gets a list of unique IDs for all messages.<br/> /// Messages marked as deleted are not listed. /// </summary> /// <returns> /// A list containing the unique IDs in sorted order from message number 1 and upwards. /// </returns> /// <exception cref="PopServerException">If the server did not accept the UIDL command</exception> public List<string> GetMessageUids() { AssertDisposed(); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot get message IDs, when the user has not been authenticated yet"); // RFC Example: // C: UIDL // S: +OK // S: 1 whqtswO00WBw418f9t5JxYwZ // S: 2 QhdPYR:00WBw1Ph7x7 // S: . // this is the end SendCommand("UIDL"); List<string> uids = new List<string>(); string response; // Keep reading until multi-line ends with a "." while (!IsLastLineInMultiLineResponse(response = StreamUtility.ReadLineAsAscii(Stream))) { // Add the unique ID to the list uids.Add(response.Split(' ')[1]); } return uids; } /// <summary> /// Gets the size in bytes of a single message /// </summary> /// <param name="messageNumber"> /// The number of a message which may not be a message marked as deleted.<br/> /// The <paramref name="messageNumber"/> must be inside the range [1, messageCount] /// </param> /// <returns>Size of the message</returns> /// <exception cref="PopServerException">If the server did not accept the LIST command</exception> public int GetMessageSize(int messageNumber) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot get message size, when the user has not been authenticated yet"); // RFC Example: // C: LIST 2 // S: +OK 2 200 return SendCommandIntResponse("LIST " + messageNumber, 2); } /// <summary> /// Get the sizes in bytes of all the messages.<br/> /// Messages marked as deleted are not listed. /// </summary> /// <returns>Size of each message excluding deleted ones</returns> /// <exception cref="PopServerException">If the server did not accept the LIST command</exception> public List<int> GetMessageSizes() { AssertDisposed(); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot get message sizes, when the user has not been authenticated yet"); // RFC Example: // C: LIST // S: +OK 2 messages (320 octets) // S: 1 120 // S: 2 200 // S: . // End of multi-line SendCommand("LIST"); List<int> sizes = new List<int>(); string response; // Read until end of multi-line while (!".".Equals(response = StreamUtility.ReadLineAsAscii(Stream))) { sizes.Add(int.Parse(response.Split(' ')[1], CultureInfo.InvariantCulture)); } return sizes; } /// <summary> /// Fetches a message from the server and parses it /// </summary> /// <param name="messageNumber"> /// Message number on server, which may not be marked as deleted.<br/> /// Must be inside the range [1, messageCount] /// </param> /// <returns>The message, containing the email message</returns> /// <exception cref="PopServerException">If the server did not accept the command sent to fetch the message</exception> public Message GetMessage(int messageNumber) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot fetch a message, when the user has not been authenticated yet"); byte[] messageContent = GetMessageAsBytes(messageNumber); return new Message(messageContent); } /// <summary> /// Fetches a message in raw form from the server /// </summary> /// <param name="messageNumber"> /// Message number on server, which may not be marked as deleted.<br/> /// Must be inside the range [1, messageCount] /// </param> /// <returns>The raw bytes of the message</returns> /// <exception cref="PopServerException">If the server did not accept the command sent to fetch the message</exception> public byte[] GetMessageAsBytes(int messageNumber) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot fetch a message, when the user has not been authenticated yet"); // Get the full message return GetMessageAsBytes(messageNumber, false); } /// <summary> /// Get all the headers for a message.<br/> /// The server will not need to send the body of the message. /// </summary> /// <param name="messageNumber"> /// Message number, which may not be marked as deleted.<br/> /// Must be inside the range [1, messageCount] /// </param> /// <returns>MessageHeaders object</returns> /// <exception cref="PopServerException">If the server did not accept the command sent to fetch the message</exception> public MessageHeader GetMessageHeaders(int messageNumber) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot fetch a message, when the user has not been authenticated yet"); // Only fetch the header part of the message byte[] messageContent = GetMessageAsBytes(messageNumber, true); // Do not parse the body - as it is not in the byte array return new Message(messageContent, false).Headers; } /// <summary> /// Asks the server to return it's capability listing.<br/> /// This is an optional command, which a server is not enforced to accept. /// </summary> /// <returns> /// The returned Dictionary keys are the capability names.<br/> /// The Lists pointed to are the capability parameters fitting that certain capability name. /// See <a href="http://tools.ietf.org/html/rfc2449#section-6">RFC section 6</a> for explanation for some of the capabilities. /// </returns> /// <remarks> /// Capabilities are case-insensitive.<br/> /// The dictionary uses case-insensitive searching, but the Lists inside /// does not. Therefore you will have to use something like the code below /// to search for a capability parameter.<br/> /// foo is the capability name and bar is the capability parameter. /// <code> /// List<string> arguments = capabilities["foo"]; /// bool contains = null != arguments.Find(delegate(string str) /// { /// return String.Compare(str, "bar", true) == 0; /// }); /// </code> /// If we were running on .NET framework >= 3.5, a HashSet could have been used. /// </remarks> /// <exception cref="PopServerException">If the server did not accept the capability command</exception> public Dictionary<string, List<string>> Capabilities() { AssertDisposed(); if (State != ConnectionState.Authorization && State != ConnectionState.Transaction) throw new InvalidUseException("Capability command only available while connected or authenticated"); // RFC Example // Examples: // C: CAPA // S: +OK Capability list follows // S: TOP // S: USER // S: SASL CRAM-MD5 KERBEROS_V4 // S: RESP-CODES // S: LOGIN-DELAY 900 // S: PIPELINING // S: EXPIRE 60 // S: UIDL // S: IMPLEMENTATION Shlemazle-Plotz-v302 // S: . SendCommand("CAPA"); // Capablities are case-insensitive Dictionary<string, List<string>> capabilities = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); string lineRead; // Keep reading until we are at the end of the multi line response while (!IsLastLineInMultiLineResponse(lineRead = StreamUtility.ReadLineAsAscii(Stream))) { // Example of read line // SASL CRAM-MD5 KERBEROS_V4 // SASL is the name of the capability while // CRAM-MD5 and KERBEROS_V4 are arguments to SASL string[] splitted = lineRead.Split(' '); // There should always be a capability name string capabilityName = splitted[0]; // Find all the arguments List<string> capabilityArguments = new List<string>(); for (int i = 1; i < splitted.Length; i++) { capabilityArguments.Add(splitted[i]); } // Add the capability found to the dictionary capabilities.Add(capabilityName, capabilityArguments); } return capabilities; } #endregion #region Private helper methods /// <summary> /// Examines string to see if it contains a time stamp to use with the APOP command.<br/> /// If it does, sets the <see cref="ApopTimeStamp"/> property to this value. /// </summary> /// <param name="response">The string to examine</param> private void ExtractApopTimestamp(string response) { // RFC Example: // +OK POP3 server ready <1896.697170952@dbc.mtview.ca.us> Match match = Regex.Match(response, "<.+>"); if (match.Success) { ApopTimeStamp = match.Value; ApopSupported = true; } } /// <summary> /// Tests a string to see if it is a "+" string.<br/> /// An "+" string should be returned by a compliant POP3 /// server if the request could be served.<br/> /// <br/> /// The method does only check if it starts with "+". /// </summary> /// <param name="response">The string to examine</param> /// <exception cref="PopServerException">Thrown if server did not respond with "+" message</exception> private static void IsOkResponse(string response) { if (response == null) throw new PopServerException("The stream used to retrieve responses from was closed"); if (response.StartsWith("+", StringComparison.OrdinalIgnoreCase)) return; throw new PopServerException("The server did not respond with a + response. The response was: \"" + response + "\""); } /// <summary> /// Sends a command to the POP server.<br/> /// If this fails, an exception is thrown. /// </summary> /// <param name="command">The command to send to server</param> /// <exception cref="PopServerException">If the server did not send an OK message to the command</exception> private void SendCommand(string command) { // Convert the command with CRLF afterwards as per RFC to a byte array which we can write byte[] commandBytes = Encoding.ASCII.GetBytes(command + "\r\n"); // Write the command to the server Stream.Write(commandBytes, 0, commandBytes.Length); Stream.Flush(); // Flush the content as we now wait for a response // Read the response from the server. The response should be in ASCII LastServerResponse = StreamUtility.ReadLineAsAscii(Stream); IsOkResponse(LastServerResponse); } /// <summary> /// Sends a command to the POP server, expects an integer reply in the response /// </summary> /// <param name="command">command to send to server</param> /// <param name="location"> /// The location of the int to return.<br/> /// Example:<br/> /// <c>S: +OK 2 200</c><br/> /// Set <paramref name="location"/>=1 to get 2<br/> /// Set <paramref name="location"/>=2 to get 200<br/> /// </param> /// <returns>Integer value in the reply</returns> /// <exception cref="PopServerException">If the server did not accept the command</exception> private int SendCommandIntResponse(string command, int location) { SendCommand(command); return int.Parse(LastServerResponse.Split(' ')[location], CultureInfo.InvariantCulture); } /// <summary> /// Asks the server for a message and returns the message response as a byte array. /// </summary> /// <param name="messageNumber"> /// Message number on server, which may not be marked as deleted.<br/> /// Must be inside the range [1, messageCount] /// </param> /// <param name="askOnlyForHeaders">If <see langword="true"/> only the header part of the message is requested from the server. If <see langword="false"/> the full message is requested</param> /// <returns>A byte array that the message requested consists of</returns> /// <exception cref="PopServerException">If the server did not accept the command sent to fetch the message</exception> private byte[] GetMessageAsBytes(int messageNumber, bool askOnlyForHeaders) { AssertDisposed(); ValidateMessageNumber(messageNumber); if (State != ConnectionState.Transaction) throw new InvalidUseException("Cannot fetch a message, when the user has not been authenticated yet"); if (askOnlyForHeaders) { // 0 is the number of lines of the message body to fetch, therefore it is set to zero to fetch only headers SendCommand("TOP " + messageNumber + " 0"); } else { // Ask for the full message SendCommand("RETR " + messageNumber); } // RFC 1939 Example // C: RETR 1 // S: +OK 120 octets // S: <the POP3 server sends the entire message here> // S: . // Create a byte array builder which we use to write the bytes too // When done, we can get the byte array out using (MemoryStream byteArrayBuilder = new MemoryStream()) { bool first = true; byte[] lineRead; // Keep reading until we are at the end of the multi line response while (!IsLastLineInMultiLineResponse(lineRead = StreamUtility.ReadLineAsBytes(Stream))) { // We should not write CRLF on the very last line, therefore we do this if (!first) { // Write CRLF which was not included in the lineRead bytes of last line byte[] crlfPair = Encoding.ASCII.GetBytes("\r\n"); byteArrayBuilder.Write(crlfPair, 0, crlfPair.Length); } else { // We are now not the first anymore first = false; } // This is a multi-line. See http://tools.ietf.org/html/rfc1939#section-3 // It says that a line starting with "." and not having CRLF after it // is a multi line, and the "." should be stripped if (lineRead.Length > 0 && lineRead[0] == '.') { // Do not write the first period byteArrayBuilder.Write(lineRead, 1, lineRead.Length - 1); } else { // Write everything byteArrayBuilder.Write(lineRead, 0, lineRead.Length); } } // If we are fetching a header - add an extra line to denote the headers ended if (askOnlyForHeaders) { byte[] crlfPair = Encoding.ASCII.GetBytes("\r\n"); byteArrayBuilder.Write(crlfPair, 0, crlfPair.Length); } // Get out the bytes we have written to byteArrayBuilder byte[] receivedBytes = byteArrayBuilder.ToArray(); return receivedBytes; } } /// <summary> /// Check if the bytes received is the last line in a multi line response /// from the pop3 server. It is the last line if the line contains only a "." /// </summary> /// <param name="bytesReceived">The last line received from the server, which could be the last response line</param> /// <returns><see langword="true"/> if last line in a multi line response, <see langword="false"/> otherwise</returns> /// <exception cref="ArgumentNullException">If <paramref name="bytesReceived"/> is <see langword="null"/></exception> private static bool IsLastLineInMultiLineResponse(byte[] bytesReceived) { if (bytesReceived == null) throw new ArgumentNullException("bytesReceived"); return bytesReceived.Length == 1 && bytesReceived[0] == '.'; } /// <see cref="IsLastLineInMultiLineResponse(byte[])"> for documentation</see> private static bool IsLastLineInMultiLineResponse(string lineReceived) { if (lineReceived == null) throw new ArgumentNullException("lineReceived"); // If the string is indeed the last line, then it is okay to do ASCII encoding // on it. For performance reasons we check if the length is equal to 1 // so that we do not need to decode a long message string just to see if // it is the last line return lineReceived.Length == 1 && IsLastLineInMultiLineResponse(Encoding.ASCII.GetBytes(lineReceived)); } /// <summary> /// Method for checking that a <paramref name="messageNumber"/> argument given to some method /// is indeed valid. If not, <see cref="InvalidUseException"/> will be thrown. /// </summary> /// <param name="messageNumber">The message number to validate</param> private static void ValidateMessageNumber(int messageNumber) { if (messageNumber <= 0) throw new InvalidUseException("The messageNumber argument cannot have a value of zero or less. Valid messageNumber is in the range [1, messageCount]"); } /// <summary> /// Closes down the streams and sets the Pop3Client into the initial configuration /// </summary> private void DisconnectStreams() { try { Stream.Close(); } finally { // Reset values to initial state SetInitialValues(); } } /// <summary> /// Sets the initial values on the public properties of this Pop3Client. /// </summary> private void SetInitialValues() { // We have not seen the APOPTimestamp yet ApopTimeStamp = null; // We are not connected Connected = false; State = ConnectionState.Disconnected; // APOP is not supported before we check on login ApopSupported = false; } /// <summary> /// Checks for extra response codes when an authentication has failed and throws /// the correct exception. /// If no such response codes is found, nothing happens. /// </summary> /// <param name="serverErrorResponse">The server response string</param> /// <param name="e">The exception thrown because the server responded with -ERR</param> /// <exception cref="PopServerLockedException">If the account is locked or in use</exception> /// <exception cref="LoginDelayException">If the server rejects the login because of too recent logins</exception> private static void CheckFailedLoginServerResponse(string serverErrorResponse, PopServerException e) { string upper = serverErrorResponse.ToUpperInvariant(); // Bracketed strings are extra response codes addded // in RFC http://tools.ietf.org/html/rfc2449 // together with the CAPA command. // Specifies the account is in use if (upper.Contains("[IN-USE]") || upper.Contains("LOCK")) { DefaultLogger.Log.LogError("Authentication: maildrop is locked or in-use"); throw new PopServerLockedException(e); } // Specifies that there must go some time between logins if (upper.Contains("[LOGIN-DELAY]")) { throw new LoginDelayException(e); } } #endregion } } namespace OpenPop.Pop3 { using System; /// <summary> /// Utility class that simplifies the usage of <see cref="IDisposable"/> /// </summary> public abstract class Disposable : IDisposable { /// <summary> /// Returns <see langword="true"/> if this instance has been disposed of, <see langword="false"/> otherwise /// </summary> protected bool IsDisposed { get; private set; } /// <summary> /// Releases unmanaged resources and performs other cleanup operations before the /// <see cref="Disposable"/> is reclaimed by garbage collection. /// </summary> ~Disposable() { Dispose(false); } /// <summary> /// Releases unmanaged and - optionally - managed resources /// </summary> public void Dispose() { if (!IsDisposed) { try { Dispose(true); } finally { IsDisposed = true; GC.SuppressFinalize(this); } } } /// <summary> /// Releases unmanaged and - optionally - managed resources. Remember to call this method from your derived classes. /// </summary> /// <param name="disposing"> /// Set to <c>true</c> to release both managed and unmanaged resources.<br/> /// Set to <c>false</c> to release only unmanaged resources. /// </param> protected virtual void Dispose(bool disposing) { } /// <summary> /// Used to assert that the object has not been disposed /// </summary> /// <exception cref="ObjectDisposedException">Thrown if the object is in a disposed state.</exception> /// <remarks> /// The method is to be used by the subclasses in order to provide a simple method for checking the /// disposal state of the object. /// </remarks> protected void AssertDisposed() { if (IsDisposed) { string typeName = GetType().FullName; throw new ObjectDisposedException(typeName, String.Format(System.Globalization.CultureInfo.InvariantCulture, "Cannot access a disposed {0}.", typeName)); } } } } namespace OpenPop.Pop3 { using System; using System.Security.Cryptography; using System.Text; /// <summary> /// Implements the CRAM-MD5 algorithm as specified in <a href="http://tools.ietf.org/html/rfc2195">RFC 2195</a>. /// </summary> internal static class CramMd5 { /// <summary> /// Defined by <a href="http://tools.ietf.org/html/rfc2104#section-2">RFC 2104</a> /// Is a 64 byte array with all entries set to 0x36. /// </summary> private static readonly byte[] ipad; /// <summary> /// Defined by <a href="http://tools.ietf.org/html/rfc2104#section-2">RFC 2104</a> /// Is a 64 byte array with all entries set to 0x5C. /// </summary> private static readonly byte[] opad; /// <summary> /// Initializes the static fields /// </summary> static CramMd5() { ipad = new byte[64]; opad = new byte[64]; for (int i = 0; i < ipad.Length; i++) { ipad[i] = 0x36; opad[i] = 0x5C; } } /// <summary> /// Computes the digest needed to login to a server using CRAM-MD5.<br/> /// <br/> /// This computes:<br/> /// MD5((password XOR opad), MD5((password XOR ipad), challenge)) /// </summary> /// <param name="username">The username of the user who wants to log in</param> /// <param name="password">The password for the <paramref name="username"/></param> /// <param name="challenge"> /// The challenge received from the server when telling it CRAM-MD5 authenticated is wanted. /// Is a base64 encoded string. /// </param> /// <returns>The response to the challenge, which the server can validate and log in the user if correct</returns> /// <exception cref="ArgumentNullException"> /// If <paramref name="username"/>, /// <paramref name="password"/> or /// <paramref name="challenge"/> is <see langword="null"/> /// </exception> internal static string ComputeDigest(string username, string password, string challenge) { if (username == null) throw new ArgumentNullException("username"); if (password == null) throw new ArgumentNullException("password"); if (challenge == null) throw new ArgumentNullException("challenge"); // Get the password bytes byte[] passwordBytes = GetSharedSecretInBytes(password); // The challenge is encoded in base64 byte[] challengeBytes = Convert.FromBase64String(challenge); // Now XOR the password with the opad and ipad magic bytes byte[] passwordOpad = Xor(passwordBytes, opad); byte[] passwordIpad = Xor(passwordBytes, ipad); // Now do the computation: MD5((password XOR opad), MD5((password XOR ipad), challenge)) byte[] digestValue = Hash(Concatenate(passwordOpad, Hash(Concatenate(passwordIpad, challengeBytes)))); // Convert the bytes to a hex string // BitConverter writes the output as AF-B3-... // We need lower-case output without "-" string hex = BitConverter.ToString(digestValue).Replace("-", "").ToLowerInvariant(); // Include the username in the resulting base64 encoded response return Convert.ToBase64String(Encoding.ASCII.GetBytes(username + " " + hex)); } /// <summary> /// Hashes a byte array using the MD5 algorithm. /// </summary> /// <param name="toHash">The byte array to hash</param> /// <returns>The result of hashing the <paramref name="toHash"/> bytes with the MD5 algorithm</returns> /// <exception cref="ArgumentNullException">If <paramref name="toHash"/> is <see langword="null"/></exception> private static byte[] Hash(byte[] toHash) { if (toHash == null) throw new ArgumentNullException("toHash"); using (MD5 md5 = new MD5CryptoServiceProvider()) { return md5.ComputeHash(toHash); } } /// <summary> /// Concatenates two byte arrays into one /// </summary> /// <param name="one">The first byte array</param> /// <param name="two">The second byte array</param> /// <returns>A concatenated byte array</returns> /// <exception cref="ArgumentNullException">If <paramref name="one"/> or <paramref name="two"/> is <see langword="null"/></exception> private static byte[] Concatenate(byte[] one, byte[] two) { if (one == null) throw new ArgumentNullException("one"); if (two == null) throw new ArgumentNullException("two"); // Create space for both byte arrays in one byte[] concatenated = new byte[one.Length + two.Length]; // Copy the first one over Buffer.BlockCopy(one, 0, concatenated, 0, one.Length); // Copy the second one over Buffer.BlockCopy(two, 0, concatenated, one.Length, two.Length); // Return result return concatenated; } /// <summary> /// XORs a byte array with another.<br/> /// Each byte in <paramref name="toXor"/> is XORed with the corresponding byte /// in <paramref name="toXorWith"/> until the end of <paramref name="toXor"/> is encountered. /// </summary> /// <param name="toXor">The byte array to XOR</param> /// <param name="toXorWith">The byte array to XOR with</param> /// <returns>A new byte array with the XORed results</returns> /// <exception cref="ArgumentNullException">If <paramref name="toXor"/> or <paramref name="toXorWith"/> is <see langword="null"/></exception> /// <exception cref="ArgumentException">If the lengths of the arrays are not equal</exception> private static byte[] Xor(byte[] toXor, byte[] toXorWith) { if (toXor == null) throw new ArgumentNullException("toXor"); if (toXorWith == null) throw new ArgumentNullException("toXorWith"); if (toXor.Length != toXorWith.Length) throw new ArgumentException("The lengths of the arrays must be equal"); // Create a new array to store results in byte[] xored = new byte[toXor.Length]; // XOR each individual byte. for (int i = 0; i < toXor.Length; i++) { xored[i] = toXor[i]; xored[i] ^= toXorWith[i]; } // Return result return xored; } /// <summary> /// This method is responsible to generate the byte array needed /// from the shared secret - the password.<br/> /// /// RFC 2195 says:<br/> /// The shared secret is null-padded to a length of 64 bytes. If the /// shared secret is longer than 64 bytes, the MD5 digest of the /// shared secret is used as a 16 byte input to the keyed MD5 /// calculation. /// </summary> /// <param name="password">This is the shared secret</param> /// <returns>The 64 bytes that is to be used from the shared secret</returns> /// <exception cref="ArgumentNullException">If <paramref name="password"/> is <see langword="null"/></exception> private static byte[] GetSharedSecretInBytes(string password) { if (password == null) throw new ArgumentNullException("password"); // Get the password in bytes byte[] passwordBytes = Encoding.ASCII.GetBytes(password); // If the length is larger than 64, we need to if (passwordBytes.Length > 64) { passwordBytes = new MD5CryptoServiceProvider().ComputeHash(passwordBytes); } if (passwordBytes.Length != 64) { byte[] returner = new byte[64]; for (int i = 0; i < passwordBytes.Length; i++) { returner[i] = passwordBytes[i]; } return returner; } return passwordBytes; } } } namespace OpenPop.Pop3 { /// <summary> /// Some of these states are defined by <a href="http://tools.ietf.org/html/rfc1939">RFC 1939</a>.<br/> /// Which commands that are allowed in which state can be seen in the same RFC.<br/> /// <br/> /// Used to keep track of which state the <see cref="Pop3Client"/> is in. /// </summary> internal enum ConnectionState { /// <summary> /// This is when the Pop3Client is not even connected to the server /// </summary> Disconnected, /// <summary> /// This is when the server is awaiting user credentials /// </summary> Authorization, /// <summary> /// This is when the server has been given the user credentials, and we are allowed /// to use commands specific to this users mail drop /// </summary> Transaction } } namespace OpenPop.Pop3 { /// <summary> /// Describes the authentication method to use when authenticating towards a POP3 server. /// </summary> public enum AuthenticationMethod { /// <summary> /// Authenticate using the UsernameAndPassword method.<br/> /// This will pass the username and password to the server in cleartext.<br/> /// <see cref="Apop"/> is more secure but might not be supported on a server.<br/> /// This method is not recommended. Use <see cref="Auto"/> instead. /// <br/> /// If SSL is used, there is no loss of security by using this authentication method. /// </summary> UsernameAndPassword, /// <summary> /// Authenticate using the Authenticated Post Office Protocol method, which is more secure then /// <see cref="UsernameAndPassword"/> since it is a request-response protocol where server checks if the /// client knows a shared secret, which is the password, without the password itself being transmitted.<br/> /// This authentication method uses MD5 under its hood.<br/> /// <br/> /// This authentication method is not supported by many servers.<br/> /// Choose this option if you want maximum security. /// </summary> Apop, /// <summary> /// This is the recomended method to authenticate with.<br/> /// If <see cref="Apop"/> is supported by the server, <see cref="Apop"/> is used for authentication.<br/> /// If <see cref="Apop"/> is not supported, Auto will fall back to <see cref="UsernameAndPassword"/> authentication. /// </summary> Auto, /// <summary> /// Logs in the the POP3 server using CRAM-MD5 authentication scheme.<br/> /// This in essence uses the MD5 hashing algorithm on the user password and a server challenge. /// </summary> CramMd5 } } namespace OpenPop.Pop3 { using System; using System.Security.Cryptography; using System.Text; /// <summary> /// Class for computing the digest needed when issuing the APOP command to a POP3 server. /// </summary> internal static class Apop { /// <summary> /// Create the digest for the APOP command so that the server can validate /// we know the password for some user. /// </summary> /// <param name="password">The password for the user</param> /// <param name="serverTimestamp">The timestamp advertised in the server greeting to the POP3 client</param> /// <returns>The password and timestamp hashed with the MD5 algorithm outputted as a HEX string</returns> public static string ComputeDigest(string password, string serverTimestamp) { if (password == null) throw new ArgumentNullException("password"); if (serverTimestamp == null) throw new ArgumentNullException("serverTimestamp"); // The APOP command authorizes itself by using the password together // with the server timestamp. This way the password is not transmitted // in clear text, and the server can still verify we have the password. byte[] digestToHash = Encoding.ASCII.GetBytes(serverTimestamp + password); using (MD5 md5 = new MD5CryptoServiceProvider()) { // MD5 hash the digest byte[] result = md5.ComputeHash(digestToHash); // Convert the bytes to a hex string // BitConverter writes the output as AF-B3-... // We need lower-case output without "-" return BitConverter.ToString(result).Replace("-", "").ToLowerInvariant(); } } } } namespace OpenPop.Pop3.Exceptions { using System; /// <summary> /// Thrown when the specified POP3 server can not be found or connected to. /// </summary> public class PopServerNotFoundException : PopClientException { ///<summary> /// Creates a PopServerNotFoundException with the given message and InnerException ///</summary> ///<param name="message">The message to include in the exception</param> ///<param name="innerException">The exception that is the cause of this exception</param> public PopServerNotFoundException(string message, Exception innerException) : base(message, innerException) { } } } namespace OpenPop.Pop3.Exceptions { using System; /// <summary> /// Thrown when the POP3 server sends an error "-ERR" during initial handshake "HELO". /// </summary> public class PopServerNotAvailableException : PopClientException { ///<summary> /// Creates a PopServerNotAvailableException with the given message and InnerException ///</summary> ///<param name="message">The message to include in the exception</param> ///<param name="innerException">The exception that is the cause of this exception</param> public PopServerNotAvailableException(string message, Exception innerException) : base(message, innerException) { } } } namespace OpenPop.Pop3.Exceptions { using System; /// <summary> /// Thrown when the user mailbox is locked or in-use.<br/> /// </summary> /// <remarks> /// The mail boxes are locked when an existing session is open on the POP3 server.<br/> /// Only one POP3 client can use a POP3 account at a time. /// </remarks> public class PopServerLockedException : PopClientException { ///<summary> /// Creates a PopServerLockedException with the given inner exception ///</summary> ///<param name="innerException">The exception that is the cause of this exception</param> public PopServerLockedException(PopServerException innerException) : base("The account is locked or in use", innerException) { } } } namespace OpenPop.Pop3.Exceptions { /// <summary> /// Thrown when the server does not return "+" to a command.<br/> /// The server response is then placed inside. /// </summary> public class PopServerException : PopClientException { ///<summary> /// Creates a PopServerException with the given message ///</summary> ///<param name="message">The message to include in the exception</param> public PopServerException(string message) : base(message) { } } } namespace OpenPop.Pop3.Exceptions { using System; /// <summary> /// This is the base exception for all <see cref="Pop3Client"/> exceptions. /// </summary> public abstract class PopClientException : Exception { ///<summary> /// Creates a PopClientException with the given message and InnerException ///</summary> ///<param name="message">The message to include in the exception</param> ///<param name="innerException">The exception that is the cause of this exception</param> protected PopClientException(string message, Exception innerException) : base(message, innerException) { if (message == null) throw new ArgumentNullException("message"); if (innerException == null) throw new ArgumentNullException("innerException"); } ///<summary> /// Creates a PopClientException with the given message ///</summary> ///<param name="message">The message to include in the exception</param> protected PopClientException(string message) : base(message) { if (message == null) throw new ArgumentNullException("message"); } } } namespace OpenPop.Pop3.Exceptions { /// <summary> /// This exception indicates that the user has logged in recently and /// will not be allowed to login again until the login delay period has expired. /// Check the parameter to the LOGIN-DELAY capability, that the server responds with when /// <see cref="Pop3Client.Capabilities()"/> is called, to see what the delay is. /// </summary> public class LoginDelayException : PopClientException { ///<summary> /// Creates a LoginDelayException with the given inner exception ///</summary> ///<param name="innerException">The exception that is the cause of this exception</param> public LoginDelayException(PopServerException innerException) : base("The account is locked or in use", innerException) { } } } namespace OpenPop.Pop3.Exceptions { /// <summary> /// Thrown when the <see cref="Pop3Client"/> is being used in an invalid way.<br/> /// This could for example happen if a someone tries to fetch a message without authenticating. /// </summary> public class InvalidUseException : PopClientException { ///<summary> /// Creates a InvalidUseException with the given message ///</summary> ///<param name="message">The message to include in the exception</param> public InvalidUseException(string message) : base(message) { } } } namespace OpenPop.Pop3.Exceptions { using System; /// <summary> /// Thrown when the supplied username or password is not accepted by the POP3 server. /// </summary> public class InvalidLoginException : PopClientException { ///<summary> /// Creates a InvalidLoginException with the given message and InnerException ///</summary> ///<param name="innerException">The exception that is the cause of this exception</param> public InvalidLoginException(Exception innerException) : base("Server did not accept user credentials", innerException) { } } } namespace OpenPop.Mime { using System; using System.Collections.Generic; using System.IO; using System.Net.Mime; using System.Text; using OpenPop.Mime.Decode; using OpenPop.Mime.Header; using OpenPop.Common; /// <summary> /// A MessagePart is a part of an email message used to describe the whole email parse tree.<br/> /// <br/> /// <b>Email messages are tree structures</b>:<br/> /// Email messages may contain large tree structures, and the MessagePart are the nodes of the this structure.<br/> /// A MessagePart may either be a leaf in the structure or a internal node with links to other MessageParts.<br/> /// The root of the message tree is the <see cref="Message"/> class.<br/> /// <br/> /// <b>Leafs</b>:<br/> /// If a MessagePart is a leaf, the part is not a <see cref="IsMultiPart">MultiPart</see> message.<br/> /// Leafs are where the contents of an email are placed.<br/> /// This includes, but is not limited to: attachments, text or images referenced from HTML.<br/> /// The content of an attachment can be fetched by using the <see cref="Body"/> property.<br/> /// If you want to have the text version of a MessagePart, use the <see cref="GetBodyAsText"/> method which will<br/> /// convert the <see cref="Body"/> into a string using the encoding the message was sent with.<br/> /// <br/> /// <b>Internal nodes</b>:<br/> /// If a MessagePart is an internal node in the email tree structure, then the part is a <see cref="IsMultiPart">MultiPart</see> message.<br/> /// The <see cref="MessageParts"/> property will then contain links to the parts it contain.<br/> /// The <see cref="Body"/> property of the MessagePart will not be set.<br/> /// <br/> /// See the example for a parsing example.<br/> /// This class cannot be instantiated from outside the library. /// </summary> /// <example> /// This example illustrates how the message parse tree looks like given a specific message<br/> /// <br/> /// The message source in this example is:<br/> /// <code> /// MIME-Version: 1.0 /// Content-Type: multipart/mixed; boundary="frontier" /// /// This is a message with multiple parts in MIME format. /// --frontier /// Content-Type: text/plain /// /// This is the body of the message. /// --frontier /// Content-Type: application/octet-stream /// Content-Transfer-Encoding: base64 /// /// PGh0bWw+CiAgPGHLYWQ+CiAgPC9oZWFkPgogIDxib2R5PgogICAgPHA+VGhpcyBpcyB0aGUg /// Ym9keSBvZiB0aGUgbWVzc2FnZS48L3A+CiAgPC9ib2R5Pgo8L2h0bWw+Cg== /// --frontier-- /// </code> /// The tree will look as follows, where the content-type media type of the message is listed<br/> /// <code> /// - Message root /// - multipart/mixed MessagePart /// - text/plain MessagePart /// - application/octet-stream MessagePart /// </code> /// It is possible to have more complex message trees like the following:<br/> /// <code> /// - Message root /// - multipart/mixed MessagePart /// - text/plain MessagePart /// - text/plain MessagePart /// - multipart/parallel /// - audio/basic /// - image/tiff /// - text/enriched /// - message/rfc822 /// </code> /// But it is also possible to have very simple message trees like:<br/> /// <code> /// - Message root /// - text/plain /// </code> /// </example> public class MessagePart { #region Public properties /// <summary> /// The Content-Type header field.<br/> /// <br/> /// If not set, the ContentType is created by the default "text/plain; charset=us-ascii" which is /// defined in <a href="http://tools.ietf.org/html/rfc2045#section-5.2">RFC 2045 section 5.2</a>.<br/> /// <br/> /// If set, the default is overridden. /// </summary> public ContentType ContentType { get; private set; } /// <summary> /// A human readable description of the body<br/> /// <br/> /// <see langword="null"/> if no Content-Description header was present in the message.<br/> /// </summary> public string ContentDescription { get; private set; } /// <summary> /// This header describes the Content encoding during transfer.<br/> /// <br/> /// If no Content-Transfer-Encoding header was present in the message, it is set /// to the default of <see cref="Header.ContentTransferEncoding.SevenBit">SevenBit</see> in accordance to the RFC. /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc2045#section-6">RFC 2045 section 6</a> for details</remarks> public ContentTransferEncoding ContentTransferEncoding { get; private set; } /// <summary> /// ID of the content part (like an attached image). Used with MultiPart messages.<br/> /// <br/> /// <see langword="null"/> if no Content-ID header field was present in the message. /// </summary> public string ContentId { get; private set; } /// <summary> /// Used to describe if a <see cref="MessagePart"/> is to be displayed or to be though of as an attachment.<br/> /// Also contains information about filename if such was sent.<br/> /// <br/> /// <see langword="null"/> if no Content-Disposition header field was present in the message /// </summary> public ContentDisposition ContentDisposition { get; private set; } /// <summary> /// This is the encoding used to parse the message body if the <see cref="MessagePart"/><br/> /// is not a MultiPart message. It is derived from the <see cref="ContentType"/> character set property. /// </summary> public Encoding BodyEncoding { get; private set; } /// <summary> /// This is the parsed body of this <see cref="MessagePart"/>.<br/> /// It is parsed in that way, if the body was ContentTransferEncoded, it has been decoded to the /// correct bytes.<br/> /// <br/> /// It will be <see langword="null"/> if this <see cref="MessagePart"/> is a MultiPart message.<br/> /// Use <see cref="IsMultiPart"/> to check if this <see cref="MessagePart"/> is a MultiPart message. /// </summary> public byte[] Body { get; private set; } /// <summary> /// Describes if this <see cref="MessagePart"/> is a MultiPart message<br/> /// <br/> /// The <see cref="MessagePart"/> is a MultiPart message if the <see cref="ContentType"/> media type property starts with "multipart/" /// </summary> public bool IsMultiPart { get { return ContentType.MediaType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase); } } /// <summary> /// A <see cref="MessagePart"/> is considered to be holding text in it's body if the MediaType /// starts either "text/" or is equal to "message/rfc822" /// </summary> public bool IsText { get { string mediaType = ContentType.MediaType; return mediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) || mediaType.Equals("message/rfc822", StringComparison.OrdinalIgnoreCase); } } /// <summary> /// A <see cref="MessagePart"/> is considered to be an attachment, if<br/> /// - it is not holding <see cref="IsText">text</see> and is not a <see cref="IsMultiPart">MultiPart</see> message<br/> /// or<br/> /// - it has a Content-Disposition header that says it is an attachment /// </summary> public bool IsAttachment { get { // Inline is the opposite of attachment return (!IsText && !IsMultiPart) || (ContentDisposition != null && !ContentDisposition.Inline); } } /// <summary> /// This is a convenient-property for figuring out a FileName for this <see cref="MessagePart"/>.<br/> /// If the <see cref="MessagePart"/> is a MultiPart message, then it makes no sense to try to find a FileName.<br/> /// <br/> /// The FileName can be specified in the <see cref="ContentDisposition"/> or in the <see cref="ContentType"/> properties.<br/> /// If none of these places two places tells about the FileName, a default "(no name)" is returned. /// </summary> public string FileName { get; private set; } /// <summary> /// If this <see cref="MessagePart"/> is a MultiPart message, then this property /// has a list of each of the Multiple parts that the message consists of.<br/> /// <br/> /// It is <see langword="null"/> if it is not a MultiPart message.<br/> /// Use <see cref="IsMultiPart"/> to check if this <see cref="MessagePart"/> is a MultiPart message. /// </summary> public List<MessagePart> MessageParts { get; private set; } #endregion #region Constructors /// <summary> /// Used to construct the topmost message part /// </summary> /// <param name="rawBody">The body that needs to be parsed</param> /// <param name="headers">The headers that should be used from the message</param> /// <exception cref="ArgumentNullException">If <paramref name="rawBody"/> or <paramref name="headers"/> is <see langword="null"/></exception> internal MessagePart(byte[] rawBody, MessageHeader headers) { if (rawBody == null) throw new ArgumentNullException("rawBody"); if (headers == null) throw new ArgumentNullException("headers"); ContentType = headers.ContentType; ContentDescription = headers.ContentDescription; ContentTransferEncoding = headers.ContentTransferEncoding; ContentId = headers.ContentId; ContentDisposition = headers.ContentDisposition; FileName = FindFileName(ContentType, ContentDisposition, "(no name)"); BodyEncoding = ParseBodyEncoding(ContentType.CharSet); ParseBody(rawBody); } #endregion #region Parsing /// <summary> /// Parses a character set into an encoding /// </summary> /// <param name="characterSet">The character set that needs to be parsed. <see langword="null"/> is allowed.</param> /// <returns>The encoding specified by the <paramref name="characterSet"/> parameter, or ASCII if the character set was <see langword="null"/> or empty</returns> private static Encoding ParseBodyEncoding(string characterSet) { // Default encoding in Mime messages is US-ASCII Encoding encoding = Encoding.ASCII; // If the character set was specified, find the encoding that the character // set describes, and use that one instead if (!string.IsNullOrEmpty(characterSet)) encoding = EncodingFinder.FindEncoding(characterSet); return encoding; } /// <summary> /// Figures out the filename of this message part from some headers. /// <see cref="FileName"/> property. /// </summary> /// <param name="contentType">The Content-Type header</param> /// <param name="contentDisposition">The Content-Disposition header</param> /// <param name="defaultName">The default filename to use, if no other could be found</param> /// <returns>The filename found, or the default one if not such filename could be found in the headers</returns> /// <exception cref="ArgumentNullException">if <paramref name="contentType"/> is <see langword="null"/></exception> private static string FindFileName(ContentType contentType, ContentDisposition contentDisposition, string defaultName) { if (contentType == null) throw new ArgumentNullException("contentType"); if (contentDisposition != null && contentDisposition.FileName != null) return contentDisposition.FileName; if (contentType.Name != null) return contentType.Name; return defaultName; } /// <summary> /// Parses a byte array as a body of an email message. /// </summary> /// <param name="rawBody">The byte array to parse as body of an email message. This array may not contain headers.</param> private void ParseBody(byte[] rawBody) { if (IsMultiPart) { // Parses a MultiPart message ParseMultiPartBody(rawBody); } else { // Parses a non MultiPart message // Decode the body accodingly and set the Body property Body = DecodeBody(rawBody, ContentTransferEncoding); } } /// <summary> /// Parses the <paramref name="rawBody"/> byte array as a MultiPart message.<br/> /// It is not valid to call this method if <see cref="IsMultiPart"/> returned <see langword="false"/>.<br/> /// Fills the <see cref="MessageParts"/> property of this <see cref="MessagePart"/>. /// </summary> /// <param name="rawBody">The byte array which is to be parsed as a MultiPart message</param> private void ParseMultiPartBody(byte[] rawBody) { // Fetch out the boundary used to delimit the messages within the body string multipartBoundary = ContentType.Boundary; // Fetch the individual MultiPart message parts using the MultiPart boundary List<byte[]> bodyParts = GetMultiPartParts(rawBody, multipartBoundary); // Initialize the MessageParts property, with room to as many bodies as we have found MessageParts = new List<MessagePart>(bodyParts.Count); // Now parse each byte array as a message body and add it the the MessageParts property foreach (byte[] bodyPart in bodyParts) { MessagePart messagePart = GetMessagePart(bodyPart); MessageParts.Add(messagePart); } } /// <summary> /// Given a byte array describing a full message.<br/> /// Parses the byte array into a <see cref="MessagePart"/>. /// </summary> /// <param name="rawMessageContent">The byte array containing both headers and body of a message</param> /// <returns>A <see cref="MessagePart"/> which was described by the <paramref name="rawMessageContent"/> byte array</returns> private static MessagePart GetMessagePart(byte[] rawMessageContent) { // Find the headers and the body parts of the byte array MessageHeader headers; byte[] body; HeaderExtractor.ExtractHeadersAndBody(rawMessageContent, out headers, out body); // Create a new MessagePart from the headers and the body return new MessagePart(body, headers); } /// <summary> /// Gets a list of byte arrays where each entry in the list is a full message of a message part /// </summary> /// <param name="rawBody">The raw byte array describing the body of a message which is a MultiPart message</param> /// <param name="multipPartBoundary">The delimiter that splits the different MultiPart bodies from each other</param> /// <returns>A list of byte arrays, each a full message of a <see cref="MessagePart"/></returns> private static List<byte[]> GetMultiPartParts(byte[] rawBody, string multipPartBoundary) { // This is the list we want to return List<byte[]> messageBodies = new List<byte[]>(); // Create a stream from which we can find MultiPart boundaries using (MemoryStream stream = new MemoryStream(rawBody)) { bool lastMultipartBoundaryEncountered; // Find the start of the first message in this multipart // Since the method returns the first character on a the line containing the MultiPart boundary, we // need to add the MultiPart boundary with prepended "--" and appended CRLF pair to the position returned. int startLocation = FindPositionOfNextMultiPartBoundary(stream, multipPartBoundary, out lastMultipartBoundaryEncountered) + ("--" + multipPartBoundary + "\r\n").Length; while (true) { // When we have just parsed the last multipart entry, stop parsing on if (lastMultipartBoundaryEncountered) break; // Find the end location of the current multipart // Since the method returns the first character on a the line containing the MultiPart boundary, we // need to go a CRLF pair back, so that we do not get that into the body of the message part int stopLocation = FindPositionOfNextMultiPartBoundary(stream, multipPartBoundary, out lastMultipartBoundaryEncountered) - "\r\n".Length; // If we could not find the next multipart boundary, but we had not yet discovered the last boundary, then // we will consider the rest of the bytes as contained in a last message part. if (stopLocation <= -1) { // Include everything except the last CRLF. stopLocation = (int)stream.Length - "\r\n".Length; // We consider this as the last part lastMultipartBoundaryEncountered = true; // Special case: when the last multipart delimiter is not ending with "--", but is indeed the last // one, then the next multipart would contain nothing, and we should not include such one. if (startLocation >= stopLocation) break; } // We have now found the start and end of a message part // Now we create a byte array with the correct length and put the message part's bytes into // it and add it to our list we want to return int length = stopLocation - startLocation; byte[] messageBody = new byte[length]; Array.Copy(rawBody, startLocation, messageBody, 0, length); messageBodies.Add(messageBody); // We want to advance to the next message parts start. // We can find this by jumping forward the MultiPart boundary from the last // message parts end position startLocation = stopLocation + ("\r\n" + "--" + multipPartBoundary + "\r\n").Length; } } // We are done return messageBodies; } /// <summary> /// Method that is able to find a specific MultiPart boundary in a Stream.<br/> /// The Stream passed should not be used for anything else then for looking for MultiPart boundaries /// <param name="stream">The stream to find the next MultiPart boundary in. Do not use it for anything else then with this method.</param> /// <param name="multiPartBoundary">The MultiPart boundary to look for. This should be found in the <see cref="ContentType"/> header</param> /// <param name="lastMultipartBoundaryFound">Is set to <see langword="true"/> if the next MultiPart boundary was indicated to be the last one, by having -- appended to it. Otherwise set to <see langword="false"/></param> /// </summary> /// <returns>The position of the first character of the line that contained MultiPartBoundary or -1 if no (more) MultiPart boundaries was found</returns> private static int FindPositionOfNextMultiPartBoundary(Stream stream, string multiPartBoundary, out bool lastMultipartBoundaryFound) { lastMultipartBoundaryFound = false; while (true) { // Get the current position. This is the first position on the line - no characters of the line will // have been read yet int currentPos = (int)stream.Position; // Read the line string line = StreamUtility.ReadLineAsAscii(stream); // If we kept reading until there was no more lines, we did not meet // the MultiPart boundary. -1 is then returned to describe this. if (line == null) return -1; // The MultiPart boundary is the MultiPartBoundary with "--" in front of it // which is to be at the very start of a line if (line.StartsWith("--" + multiPartBoundary, StringComparison.Ordinal)) { // Check if the found boundary was also the last one lastMultipartBoundaryFound = line.StartsWith("--" + multiPartBoundary + "--", StringComparison.OrdinalIgnoreCase); return currentPos; } } } /// <summary> /// Decodes a byte array into another byte array based upon the Content Transfer encoding /// </summary> /// <param name="messageBody">The byte array to decode into another byte array</param> /// <param name="contentTransferEncoding">The <see cref="ContentTransferEncoding"/> of the byte array</param> /// <returns>A byte array which comes from the <paramref name="contentTransferEncoding"/> being used on the <paramref name="messageBody"/></returns> /// <exception cref="ArgumentNullException">If <paramref name="messageBody"/> is <see langword="null"/></exception> /// <exception cref="ArgumentOutOfRangeException">Thrown if the <paramref name="contentTransferEncoding"/> is unsupported</exception> private static byte[] DecodeBody(byte[] messageBody, ContentTransferEncoding contentTransferEncoding) { if (messageBody == null) throw new ArgumentNullException("messageBody"); switch (contentTransferEncoding) { case ContentTransferEncoding.QuotedPrintable: // If encoded in QuotedPrintable, everything in the body is in US-ASCII return QuotedPrintable.DecodeContentTransferEncoding(Encoding.ASCII.GetString(messageBody)); case ContentTransferEncoding.Base64: // If encoded in Base64, everything in the body is in US-ASCII return Base64.Decode(Encoding.ASCII.GetString(messageBody)); case ContentTransferEncoding.SevenBit: case ContentTransferEncoding.Binary: case ContentTransferEncoding.EightBit: // We do not have to do anything return messageBody; default: throw new ArgumentOutOfRangeException("contentTransferEncoding"); } } #endregion #region Public methods /// <summary> /// Gets this MessagePart's <see cref="Body"/> as text.<br/> /// This is simply the <see cref="BodyEncoding"/> being used on the raw bytes of the <see cref="Body"/> property.<br/> /// This method is only valid to call if it is not a MultiPart message and therefore contains a body.<br/> /// </summary> /// <returns>The <see cref="Body"/> property as a string</returns> public string GetBodyAsText() { return BodyEncoding.GetString(Body); } /// <summary> /// Save this <see cref="MessagePart"/>'s contents to a file.<br/> /// There are no methods to reload the file. /// </summary> /// <param name="file">The File location to save the <see cref="MessagePart"/> to. Existent files will be overwritten.</param> /// <exception cref="ArgumentNullException">If <paramref name="file"/> is <see langword="null"/></exception> /// <exception>Other exceptions relevant to using a <see cref="FileStream"/> might be thrown as well</exception> public void Save(FileInfo file) { if (file == null) throw new ArgumentNullException("file"); using (FileStream stream = new FileStream(file.FullName, FileMode.OpenOrCreate)) { Save(stream); } } /// <summary> /// Save this <see cref="MessagePart"/>'s contents to a stream.<br/> /// </summary> /// <param name="messageStream">The stream to write to</param> /// <exception cref="ArgumentNullException">If <paramref name="messageStream"/> is <see langword="null"/></exception> /// <exception>Other exceptions relevant to <see cref="Stream.Write"/> might be thrown as well</exception> public void Save(Stream messageStream) { if (messageStream == null) throw new ArgumentNullException("messageStream"); messageStream.Write(Body, 0, Body.Length); } #endregion } } namespace OpenPop.Mime { using System; using System.Collections.Generic; using System.IO; using System.Net.Mail; using System.Text; using OpenPop.Mime.Header; using OpenPop.Mime.Traverse; /// <summary> /// This is the root of the email tree structure.<br/> /// <see cref="Mime.MessagePart"/> for a description about the structure.<br/> /// <br/> /// A Message (this class) contains the headers of an email message such as: /// <code> /// - To /// - From /// - Subject /// - Content-Type /// - Message-ID /// </code> /// which are located in the <see cref="Headers"/> property.<br/> /// <br/> /// Use the <see cref="Message.MessagePart"/> property to find the actual content of the email message. /// </summary> /// <example> /// Examples are available on the <a href="http://hpop.sourceforge.net/">project homepage</a>. /// </example> public class Message { #region Public properties /// <summary> /// Headers of the Message. /// </summary> public MessageHeader Headers { get; private set; } /// <summary> /// This is the body of the email Message.<br/> /// <br/> /// If the body was parsed for this Message, this property will never be <see langword="null"/>. /// </summary> public MessagePart MessagePart { get; private set; } /// <summary> /// The raw content from which this message has been constructed.<br/> /// These bytes can be persisted and later used to recreate the Message. /// </summary> public byte[] RawMessage { get; private set; } #endregion #region Constructors /// <summary> /// Convenience constructor for <see cref="Mime.Message(byte[], bool)"/>.<br/> /// <br/> /// Creates a message from a byte array. The full message including its body is parsed. /// </summary> /// <param name="rawMessageContent">The byte array which is the message contents to parse</param> public Message(byte[] rawMessageContent) : this(rawMessageContent, true) { } /// <summary> /// Constructs a message from a byte array.<br/> /// <br/> /// The headers are always parsed, but if <paramref name="parseBody"/> is <see langword="false"/>, the body is not parsed. /// </summary> /// <param name="rawMessageContent">The byte array which is the message contents to parse</param> /// <param name="parseBody"> /// <see langword="true"/> if the body should be parsed, /// <see langword="false"/> if only headers should be parsed out of the <paramref name="rawMessageContent"/> byte array /// </param> public Message(byte[] rawMessageContent, bool parseBody) { RawMessage = rawMessageContent; // Find the headers and the body parts of the byte array MessageHeader headersTemp; byte[] body; HeaderExtractor.ExtractHeadersAndBody(rawMessageContent, out headersTemp, out body); // Set the Headers property Headers = headersTemp; // Should we also parse the body? if (parseBody) { // Parse the body into a MessagePart MessagePart = new MessagePart(body, Headers); } } #endregion /// <summary> /// This method will convert this <see cref="Message"/> into a <see cref="MailMessage"/> equivalent.<br/> /// The returned <see cref="MailMessage"/> can be used with <see cref="System.Net.Mail.SmtpClient"/> to forward the email.<br/> /// <br/> /// You should be aware of the following about this method: /// <list type="bullet"> /// <item> /// All sender and receiver mail addresses are set. /// If you send this email using a <see cref="System.Net.Mail.SmtpClient"/> then all /// receivers in To, From, Cc and Bcc will receive the email once again. /// </item> /// <item> /// If you view the source code of this Message and looks at the source code of the forwarded /// <see cref="MailMessage"/> returned by this method, you will notice that the source codes are not the same. /// The content that is presented by a mail client reading the forwarded <see cref="MailMessage"/> should be the /// same as the original, though. /// </item> /// <item> /// Content-Disposition headers will not be copied to the <see cref="MailMessage"/>. /// It is simply not possible to set these on Attachments. /// </item> /// <item> /// HTML content will be treated as the preferred view for the <see cref="MailMessage.Body"/>. Plain text content will be used for the /// <see cref="MailMessage.Body"/> when HTML is not available. /// </item> /// </list> /// </summary> /// <returns>A <see cref="MailMessage"/> object that contains the same information that this Message does</returns> public MailMessage ToMailMessage() { // Construct an empty MailMessage to which we will gradually build up to look like the current Message object (this) MailMessage message = new MailMessage(); message.Subject = Headers.Subject; // We here set the encoding to be UTF-8 // We cannot determine what the encoding of the subject was at this point. // But since we know that strings in .NET is stored in UTF, we can // use UTF-8 to decode the subject into bytes message.SubjectEncoding = Encoding.UTF8; // The HTML version should take precedent over the plain text if it is available MessagePart preferredVersion = FindFirstHtmlVersion(); if (preferredVersion != null) { // Make sure that the IsBodyHtml property is being set correctly for our content message.IsBodyHtml = true; } else { // otherwise use the first plain text version as the body, if it exists preferredVersion = FindFirstPlainTextVersion(); } if (preferredVersion != null) { message.Body = preferredVersion.GetBodyAsText(); message.BodyEncoding = preferredVersion.BodyEncoding; } // Add body and alternative views (html and such) to the message IEnumerable<MessagePart> textVersions = FindAllTextVersions(); foreach (MessagePart textVersion in textVersions) { // The textVersions also contain the preferred version, therefore // we should skip that one if (textVersion == preferredVersion) continue; MemoryStream stream = new MemoryStream(textVersion.Body); AlternateView alternative = new AlternateView(stream); alternative.ContentId = textVersion.ContentId; alternative.ContentType = textVersion.ContentType; message.AlternateViews.Add(alternative); } // Add attachments to the message IEnumerable<MessagePart> attachments = FindAllAttachments(); foreach (MessagePart attachmentMessagePart in attachments) { MemoryStream stream = new MemoryStream(attachmentMessagePart.Body); Attachment attachment = new Attachment(stream, attachmentMessagePart.ContentType); attachment.ContentId = attachmentMessagePart.ContentId; message.Attachments.Add(attachment); } if (Headers.From != null && Headers.From.HasValidMailAddress) message.From = Headers.From.MailAddress; if (Headers.ReplyTo != null && Headers.ReplyTo.HasValidMailAddress) { //于溪玥 message.ReplyToList.Add(Headers.ReplyTo.MailAddress); } if (Headers.Sender != null && Headers.Sender.HasValidMailAddress) message.Sender = Headers.Sender.MailAddress; foreach (RfcMailAddress to in Headers.To) { if (to.HasValidMailAddress) message.To.Add(to.MailAddress); } foreach (RfcMailAddress cc in Headers.Cc) { if (cc.HasValidMailAddress) message.CC.Add(cc.MailAddress); } foreach (RfcMailAddress bcc in Headers.Bcc) { if (bcc.HasValidMailAddress) message.Bcc.Add(bcc.MailAddress); } return message; } #region MessagePart Searching Methods /// <summary> /// Finds the first text/plain <see cref="MessagePart"/> in this message.<br/> /// This is a convenience method - it simply propagates the call to <see cref="FindFirstMessagePartWithMediaType"/>.<br/> /// <br/> /// If no text/plain version is found, <see langword="null"/> is returned. /// </summary> /// <returns> /// <see cref="MessagePart"/> which has a MediaType of text/plain or <see langword="null"/> /// if such <see cref="MessagePart"/> could not be found. /// </returns> public MessagePart FindFirstPlainTextVersion() { return FindFirstMessagePartWithMediaType("text/plain"); } /// <summary> /// Finds the first text/html <see cref="MessagePart"/> in this message.<br/> /// This is a convenience method - it simply propagates the call to <see cref="FindFirstMessagePartWithMediaType"/>.<br/> /// <br/> /// If no text/html version is found, <see langword="null"/> is returned. /// </summary> /// <returns> /// <see cref="MessagePart"/> which has a MediaType of text/html or <see langword="null"/> /// if such <see cref="MessagePart"/> could not be found. /// </returns> public MessagePart FindFirstHtmlVersion() { return FindFirstMessagePartWithMediaType("text/html"); } /// <summary> /// Finds all the <see cref="MessagePart"/>'s which contains a text version.<br/> /// <br/> /// <see cref="Mime.MessagePart.IsText"/> for MessageParts which are considered to be text versions.<br/> /// <br/> /// Examples of MessageParts media types are: /// <list type="bullet"> /// <item>text/plain</item> /// <item>text/html</item> /// <item>text/xml</item> /// </list> /// </summary> /// <returns>A List of MessageParts where each part is a text version</returns> public List<MessagePart> FindAllTextVersions() { return new TextVersionFinder().VisitMessage(this); } /// <summary> /// Finds all the <see cref="MessagePart"/>'s which are attachments to this message.<br/> /// <br/> /// <see cref="Mime.MessagePart.IsAttachment"/> for MessageParts which are considered to be attachments. /// </summary> /// <returns>A List of MessageParts where each is considered an attachment</returns> public List<MessagePart> FindAllAttachments() { return new AttachmentFinder().VisitMessage(this); } /// <summary> /// Finds the first <see cref="MessagePart"/> in the <see cref="Message"/> hierarchy with the given MediaType.<br/> /// <br/> /// The search in the hierarchy is a depth-first traversal. /// </summary> /// <param name="mediaType">The MediaType to search for. Case is ignored.</param> /// <returns> /// A <see cref="MessagePart"/> with the given MediaType or <see langword="null"/> if no such <see cref="MessagePart"/> was found /// </returns> public MessagePart FindFirstMessagePartWithMediaType(string mediaType) { return new FindFirstMessagePartWithMediaType().VisitMessage(this, mediaType); } /// <summary> /// Finds all the <see cref="MessagePart"/>s in the <see cref="Message"/> hierarchy with the given MediaType. /// </summary> /// <param name="mediaType">The MediaType to search for. Case is ignored.</param> /// <returns> /// A List of <see cref="MessagePart"/>s with the given MediaType.<br/> /// The List might be empty if no such <see cref="MessagePart"/>s were found.<br/> /// The order of the elements in the list is the order which they are found using /// a depth first traversal of the <see cref="Message"/> hierarchy. /// </returns> public List<MessagePart> FindAllMessagePartsWithMediaType(string mediaType) { return new FindAllMessagePartsWithMediaType().VisitMessage(this, mediaType); } #endregion #region Message Persistence /// <summary> /// Save this <see cref="Message"/> to a file.<br/> /// <br/> /// Can be loaded at a later time using the <see cref="Load(FileInfo)"/> method. /// </summary> /// <param name="file">The File location to save the <see cref="Message"/> to. Existent files will be overwritten.</param> /// <exception cref="ArgumentNullException">If <paramref name="file"/> is <see langword="null"/></exception> /// <exception>Other exceptions relevant to using a <see cref="FileStream"/> might be thrown as well</exception> public void Save(FileInfo file) { if (file == null) throw new ArgumentNullException("file"); using (FileStream stream = new FileStream(file.FullName, FileMode.OpenOrCreate)) { Save(stream); } } /// <summary> /// Save this <see cref="Message"/> to a stream.<br/> /// </summary> /// <param name="messageStream">The stream to write to</param> /// <exception cref="ArgumentNullException">If <paramref name="messageStream"/> is <see langword="null"/></exception> /// <exception>Other exceptions relevant to <see cref="Stream.Write"/> might be thrown as well</exception> public void Save(Stream messageStream) { if (messageStream == null) throw new ArgumentNullException("messageStream"); messageStream.Write(RawMessage, 0, RawMessage.Length); } /// <summary> /// Loads a <see cref="Message"/> from a file containing a raw email. /// </summary> /// <param name="file">The File location to load the <see cref="Message"/> from. The file must exist.</param> /// <exception cref="ArgumentNullException">If <paramref name="file"/> is <see langword="null"/></exception> /// <exception cref="FileNotFoundException">If <paramref name="file"/> does not exist</exception> /// <exception>Other exceptions relevant to a <see cref="FileStream"/> might be thrown as well</exception> /// <returns>A <see cref="Message"/> with the content loaded from the <paramref name="file"/></returns> public static Message Load(FileInfo file) { if (file == null) throw new ArgumentNullException("file"); if (!file.Exists) throw new FileNotFoundException("Cannot load message from non-existent file", file.FullName); using (FileStream stream = new FileStream(file.FullName, FileMode.Open)) { return Load(stream); } } /// <summary> /// Loads a <see cref="Message"/> from a <see cref="Stream"/> containing a raw email. /// </summary> /// <param name="messageStream">The <see cref="Stream"/> from which to load the raw <see cref="Message"/></param> /// <exception cref="ArgumentNullException">If <paramref name="messageStream"/> is <see langword="null"/></exception> /// <exception>Other exceptions relevant to <see cref="Stream.Read"/> might be thrown as well</exception> /// <returns>A <see cref="Message"/> with the content loaded from the <paramref name="messageStream"/></returns> public static Message Load(Stream messageStream) { if (messageStream == null) throw new ArgumentNullException("messageStream"); using (MemoryStream outStream = new MemoryStream()) { #if DOTNET4 // TODO: Enable using native v4 framework methods when support is formally added. messageStream.CopyTo(outStream); #else int bytesRead; byte[] buffer = new byte[4096]; while ((bytesRead = messageStream.Read(buffer, 0, 4096)) > 0) { outStream.Write(buffer, 0, bytesRead); } #endif byte[] content = outStream.ToArray(); return new Message(content); } } #endregion } } namespace OpenPop.Mime.Traverse { using System; using System.Collections.Generic; /// <summary> /// Finds all text/[something] versions in a Message hierarchy /// </summary> internal class TextVersionFinder : MultipleMessagePartFinder { protected override List<MessagePart> CaseLeaf(MessagePart messagePart) { if (messagePart == null) throw new ArgumentNullException("messagePart"); // Maximum space needed is one List<MessagePart> leafAnswer = new List<MessagePart>(1); if (messagePart.IsText) leafAnswer.Add(messagePart); return leafAnswer; } } } namespace OpenPop.Mime.Traverse { using System; using System.Collections.Generic; ///<summary> /// An abstract class that implements the MergeLeafAnswers method.<br/> /// The method simply returns the union of all answers from the leaves. ///</summary> public abstract class MultipleMessagePartFinder : AnswerMessageTraverser<List<MessagePart>> { /// <summary> /// Adds all the <paramref name="leafAnswers"/> in one big answer /// </summary> /// <param name="leafAnswers">The answers to merge</param> /// <returns>A list with has all the elements in the <paramref name="leafAnswers"/> lists</returns> /// <exception cref="ArgumentNullException">if <paramref name="leafAnswers"/> is <see langword="null"/></exception> protected override List<MessagePart> MergeLeafAnswers(List<List<MessagePart>> leafAnswers) { if (leafAnswers == null) throw new ArgumentNullException("leafAnswers"); // We simply create a list with all the answer generated from the leaves List<MessagePart> mergedResults = new List<MessagePart>(); foreach (List<MessagePart> leafAnswer in leafAnswers) { mergedResults.AddRange(leafAnswer); } return mergedResults; } } } namespace OpenPop.Mime.Traverse { /// <summary> /// This interface describes a MessageTraverser which is able to traverse a Message structure /// and deliver some answer given some question. /// </summary> /// <typeparam name="TAnswer">This is the type of the answer you want to have delivered.</typeparam> /// <typeparam name="TQuestion">This is the type of the question you want to have answered.</typeparam> public interface IQuestionAnswerMessageTraverser<TQuestion, TAnswer> { /// <summary> /// Call this when you want to apply this traverser on a <see cref="Message"/>. /// </summary> /// <param name="message">The <see cref="Message"/> which you want to traverse. Must not be <see langword="null"/>.</param> /// <param name="question">The question</param> /// <returns>An answer</returns> TAnswer VisitMessage(Message message, TQuestion question); /// <summary> /// Call this when you want to apply this traverser on a <see cref="MessagePart"/>. /// </summary> /// <param name="messagePart">The <see cref="MessagePart"/> which you want to traverse. Must not be <see langword="null"/>.</param> /// <param name="question">The question</param> /// <returns>An answer</returns> TAnswer VisitMessagePart(MessagePart messagePart, TQuestion question); } } namespace OpenPop.Mime.Traverse { /// <summary> /// This interface describes a MessageTraverser which is able to traverse a Message hierarchy structure /// and deliver some answer. /// </summary> /// <typeparam name="TAnswer">This is the type of the answer you want to have delivered.</typeparam> public interface IAnswerMessageTraverser<TAnswer> { /// <summary> /// Call this when you want to apply this traverser on a <see cref="Message"/>. /// </summary> /// <param name="message">The <see cref="Message"/> which you want to traverse. Must not be <see langword="null"/>.</param> /// <returns>An answer</returns> TAnswer VisitMessage(Message message); /// <summary> /// Call this when you want to apply this traverser on a <see cref="MessagePart"/>. /// </summary> /// <param name="messagePart">The <see cref="MessagePart"/> which you want to traverse. Must not be <see langword="null"/>.</param> /// <returns>An answer</returns> TAnswer VisitMessagePart(MessagePart messagePart); } } namespace OpenPop.Mime.Traverse { using System; ///<summary> /// Finds the first <see cref="MessagePart"/> which have a given MediaType in a depth first traversal. ///</summary> internal class FindFirstMessagePartWithMediaType : IQuestionAnswerMessageTraverser<string, MessagePart> { /// <summary> /// Finds the first <see cref="MessagePart"/> with the given MediaType /// </summary> /// <param name="message">The <see cref="Message"/> to start looking in</param> /// <param name="question">The MediaType to look for. Case is ignored.</param> /// <returns>A <see cref="MessagePart"/> with the given MediaType or <see langword="null"/> if no such <see cref="MessagePart"/> was found</returns> public MessagePart VisitMessage(Message message, string question) { if (message == null) throw new ArgumentNullException("message"); return VisitMessagePart(message.MessagePart, question); } /// <summary> /// Finds the first <see cref="MessagePart"/> with the given MediaType /// </summary> /// <param name="messagePart">The <see cref="MessagePart"/> to start looking in</param> /// <param name="question">The MediaType to look for. Case is ignored.</param> /// <returns>A <see cref="MessagePart"/> with the given MediaType or <see langword="null"/> if no such <see cref="MessagePart"/> was found</returns> public MessagePart VisitMessagePart(MessagePart messagePart, string question) { if (messagePart == null) throw new ArgumentNullException("messagePart"); if (messagePart.ContentType.MediaType.Equals(question, StringComparison.OrdinalIgnoreCase)) return messagePart; if (messagePart.IsMultiPart) { foreach (MessagePart part in messagePart.MessageParts) { MessagePart result = VisitMessagePart(part, question); if (result != null) return result; } } return null; } } } namespace OpenPop.Mime.Traverse { using System; using System.Collections.Generic; ///<summary> /// Finds all the <see cref="MessagePart"/>s which have a given MediaType using a depth first traversal. ///</summary> internal class FindAllMessagePartsWithMediaType : IQuestionAnswerMessageTraverser<string, List<MessagePart>> { /// <summary> /// Finds all the <see cref="MessagePart"/>s with the given MediaType /// </summary> /// <param name="message">The <see cref="Message"/> to start looking in</param> /// <param name="question">The MediaType to look for. Case is ignored.</param> /// <returns> /// A List of <see cref="MessagePart"/>s with the given MediaType.<br/> /// <br/> /// The List might be empty if no such <see cref="MessagePart"/>s were found.<br/> /// The order of the elements in the list is the order which they are found using /// a depth first traversal of the <see cref="Message"/> hierarchy. /// </returns> public List<MessagePart> VisitMessage(Message message, string question) { if (message == null) throw new ArgumentNullException("message"); return VisitMessagePart(message.MessagePart, question); } /// <summary> /// Finds all the <see cref="MessagePart"/>s with the given MediaType /// </summary> /// <param name="messagePart">The <see cref="MessagePart"/> to start looking in</param> /// <param name="question">The MediaType to look for. Case is ignored.</param> /// <returns> /// A List of <see cref="MessagePart"/>s with the given MediaType.<br/> /// <br/> /// The List might be empty if no such <see cref="MessagePart"/>s were found.<br/> /// The order of the elements in the list is the order which they are found using /// a depth first traversal of the <see cref="Message"/> hierarchy. /// </returns> public List<MessagePart> VisitMessagePart(MessagePart messagePart, string question) { if (messagePart == null) throw new ArgumentNullException("messagePart"); List<MessagePart> results = new List<MessagePart>(); if (messagePart.ContentType.MediaType.Equals(question, StringComparison.OrdinalIgnoreCase)) results.Add(messagePart); if (messagePart.IsMultiPart) { foreach (MessagePart part in messagePart.MessageParts) { List<MessagePart> result = VisitMessagePart(part, question); results.AddRange(result); } } return results; } } } namespace OpenPop.Mime.Traverse { using System; using System.Collections.Generic; /// <summary> /// Finds all <see cref="MessagePart"/>s which are considered to be attachments /// </summary> internal class AttachmentFinder : MultipleMessagePartFinder { protected override List<MessagePart> CaseLeaf(MessagePart messagePart) { if (messagePart == null) throw new ArgumentNullException("messagePart"); // Maximum space needed is one List<MessagePart> leafAnswer = new List<MessagePart>(1); if (messagePart.IsAttachment) leafAnswer.Add(messagePart); return leafAnswer; } } } namespace OpenPop.Mime.Traverse { using System; using System.Collections.Generic; /// <summary> /// This is an abstract class which handles traversing of a <see cref="Message"/> tree structure.<br/> /// It runs through the message structure using a depth-first traversal. /// </summary> /// <typeparam name="TAnswer">The answer you want from traversing the message tree structure</typeparam> public abstract class AnswerMessageTraverser<TAnswer> : IAnswerMessageTraverser<TAnswer> { /// <summary> /// Call this when you want an answer for a full message. /// </summary> /// <param name="message">The message you want to traverse</param> /// <returns>An answer</returns> /// <exception cref="ArgumentNullException">if <paramref name="message"/> is <see langword="null"/></exception> public TAnswer VisitMessage(Message message) { if (message == null) throw new ArgumentNullException("message"); return VisitMessagePart(message.MessagePart); } /// <summary> /// Call this method when you want to find an answer for a <see cref="MessagePart"/> /// </summary> /// <param name="messagePart">The <see cref="MessagePart"/> part you want an answer from.</param> /// <returns>An answer</returns> /// <exception cref="ArgumentNullException">if <paramref name="messagePart"/> is <see langword="null"/></exception> public TAnswer VisitMessagePart(MessagePart messagePart) { if (messagePart == null) throw new ArgumentNullException("messagePart"); if (messagePart.IsMultiPart) { List<TAnswer> leafAnswers = new List<TAnswer>(messagePart.MessageParts.Count); foreach (MessagePart part in messagePart.MessageParts) { leafAnswers.Add(VisitMessagePart(part)); } return MergeLeafAnswers(leafAnswers); } return CaseLeaf(messagePart); } /// <summary> /// For a concrete implementation an answer must be returned for a leaf <see cref="MessagePart"/>, which are /// MessageParts that are not <see cref="MessagePart.IsMultiPart">MultiParts.</see> /// </summary> /// <param name="messagePart">The message part which is a leaf and thereby not a MultiPart</param> /// <returns>An answer</returns> protected abstract TAnswer CaseLeaf(MessagePart messagePart); /// <summary> /// For a concrete implementation, when a MultiPart <see cref="MessagePart"/> has fetched it's answers from it's children, these /// answers needs to be merged. This is the responsibility of this method. /// </summary> /// <param name="leafAnswers">The answer that the leafs gave</param> /// <returns>A merged answer</returns> protected abstract TAnswer MergeLeafAnswers(List<TAnswer> leafAnswers); } } namespace OpenPop.Mime.Header { using System; using System.Collections.Generic; using System.Net.Mail; using OpenPop.Mime.Decode; using OpenPop.Common.Logging; /// <summary> /// This class is used for RFC compliant email addresses.<br/> /// <br/> /// The class cannot be instantiated from outside the library. /// </summary> /// <remarks> /// The <seealso cref="MailAddress"/> does not cover all the possible formats /// for <a href="http://tools.ietf.org/html/rfc5322#section-3.4">RFC 5322 section 3.4</a> compliant email addresses. /// This class is used as an address wrapper to account for that deficiency. /// </remarks> public class RfcMailAddress { #region Properties ///<summary> /// The email address of this <see cref="RfcMailAddress"/><br/> /// It is possibly string.Empty since RFC mail addresses does not require an email address specified. ///</summary> ///<example> /// Example header with email address:<br/> /// To: <c>Test test@mail.com</c><br/> /// Address will be <c>test@mail.com</c><br/> ///</example> ///<example> /// Example header without email address:<br/> /// To: <c>Test</c><br/> /// Address will be <see cref="string.Empty"/>. ///</example> public string Address { get; private set; } ///<summary> /// The display name of this <see cref="RfcMailAddress"/><br/> /// It is possibly <see cref="string.Empty"/> since RFC mail addresses does not require a display name to be specified. ///</summary> ///<example> /// Example header with display name:<br/> /// To: <c>Test test@mail.com</c><br/> /// DisplayName will be <c>Test</c> ///</example> ///<example> /// Example header without display name:<br/> /// To: <c>test@test.com</c><br/> /// DisplayName will be <see cref="string.Empty"/> ///</example> public string DisplayName { get; private set; } /// <summary> /// This is the Raw string used to describe the <see cref="RfcMailAddress"/>. /// </summary> public string Raw { get; private set; } /// <summary> /// The <see cref="MailAddress"/> associated with the <see cref="RfcMailAddress"/>. /// </summary> /// <remarks> /// The value of this property can be <see lanword="null"/> in instances where the <see cref="MailAddress"/> cannot represent the address properly.<br/> /// Use <see cref="HasValidMailAddress"/> property to see if this property is valid. /// </remarks> public MailAddress MailAddress { get; private set; } /// <summary> /// Specifies if the object contains a valid <see cref="MailAddress"/> reference. /// </summary> public bool HasValidMailAddress { get { return MailAddress != null; } } #endregion #region Constructors /// <summary> /// Constructs an <see cref="RfcMailAddress"/> object from a <see cref="MailAddress"/> object.<br/> /// This constructor is used when we were able to construct a <see cref="MailAddress"/> from a string. /// </summary> /// <param name="mailAddress">The address that <paramref name="raw"/> was parsed into</param> /// <param name="raw">The raw unparsed input which was parsed into the <paramref name="mailAddress"/></param> /// <exception cref="ArgumentNullException">If <paramref name="mailAddress"/> or <paramref name="raw"/> is <see langword="null"/></exception> private RfcMailAddress(MailAddress mailAddress, string raw) { if (mailAddress == null) throw new ArgumentNullException("mailAddress"); if (raw == null) throw new ArgumentNullException("raw"); MailAddress = mailAddress; Address = mailAddress.Address; DisplayName = mailAddress.DisplayName; Raw = raw; } /// <summary> /// When we were unable to parse a string into a <see cref="MailAddress"/>, this constructor can be /// used. The Raw string is then used as the <see cref="DisplayName"/>. /// </summary> /// <param name="raw">The raw unparsed input which could not be parsed</param> /// <exception cref="ArgumentNullException">If <paramref name="raw"/> is <see langword="null"/></exception> private RfcMailAddress(string raw) { if (raw == null) throw new ArgumentNullException("raw"); MailAddress = null; Address = string.Empty; DisplayName = raw; Raw = raw; } #endregion /// <summary> /// A string representation of the <see cref="RfcMailAddress"/> object /// </summary> /// <returns>Returns the string representation for the object</returns> public override string ToString() { if (HasValidMailAddress) return MailAddress.ToString(); return Raw; } #region Parsing /// <summary> /// Parses an email address from a MIME header<br/> /// <br/> /// Examples of input: /// <c>Eksperten mailrobot <noreply@mail.eksperten.dk></c><br/> /// <c>"Eksperten mailrobot" <noreply@mail.eksperten.dk></c><br/> /// <c><noreply@mail.eksperten.dk></c><br/> /// <c>noreply@mail.eksperten.dk</c><br/> /// <br/> /// It might also contain encoded text, which will then be decoded. /// </summary> /// <param name="input">The value to parse out and email and/or a username</param> /// <returns>A <see cref="RfcMailAddress"/></returns> /// <exception cref="ArgumentNullException">If <paramref name="input"/> is <see langword="null"/></exception> /// <remarks> /// <see href="http://tools.ietf.org/html/rfc5322#section-3.4">RFC 5322 section 3.4</see> for more details on email syntax.<br/> /// <see cref="EncodedWord.Decode">For more information about encoded text</see>. /// </remarks> internal static RfcMailAddress ParseMailAddress(string input) { if (input == null) throw new ArgumentNullException("input"); // Decode the value, if it was encoded input = EncodedWord.Decode(input.Trim()); // Find the location of the email address int indexStartEmail = input.LastIndexOf('<'); int indexEndEmail = input.LastIndexOf('>'); try { if (indexStartEmail >= 0 && indexEndEmail >= 0) { string username; // Check if there is a username in front of the email address if (indexStartEmail > 0) { // Parse out the user username = input.Substring(0, indexStartEmail).Trim(); } else { // There was no user username = string.Empty; } // Parse out the email address without the "<" and ">" indexStartEmail = indexStartEmail + 1; int emailLength = indexEndEmail - indexStartEmail; string emailAddress = input.Substring(indexStartEmail, emailLength).Trim(); // There has been cases where there was no emailaddress between the < and > if (!string.IsNullOrEmpty(emailAddress)) { // If the username is quoted, MailAddress' constructor will remove them for us return new RfcMailAddress(new MailAddress(emailAddress, username), input); } } // This might be on the form noreply@mail.eksperten.dk // Check if there is an email, if notm there is no need to try if (input.Contains("@")) return new RfcMailAddress(new MailAddress(input), input); } catch (FormatException) { // Sometimes invalid emails are sent, like sqlmap-user@sourceforge.net. (last period is illigal) DefaultLogger.Log.LogError("RfcMailAddress: Improper mail address: \"" + input + "\""); } // It could be that the format used was simply a name // which is indeed valid according to the RFC // Example: // Eksperten mailrobot return new RfcMailAddress(input); } /// <summary> /// Parses input of the form<br/> /// <c>Eksperten mailrobot <noreply@mail.eksperten.dk>, ...</c><br/> /// to a list of RFCMailAddresses /// </summary> /// <param name="input">The input that is a comma-separated list of EmailAddresses to parse</param> /// <returns>A List of <seealso cref="RfcMailAddress"/> objects extracted from the <paramref name="input"/> parameter.</returns> /// <exception cref="ArgumentNullException">If <paramref name="input"/> is <see langword="null"/></exception> internal static List<RfcMailAddress> ParseMailAddresses(string input) { if (input == null) throw new ArgumentNullException("input"); List<RfcMailAddress> returner = new List<RfcMailAddress>(); // MailAddresses are split by commas IEnumerable<string> mailAddresses = Utility.SplitStringWithCharNotInsideQuotes(input, ','); // Parse each of these foreach (string mailAddress in mailAddresses) { returner.Add(ParseMailAddress(mailAddress)); } return returner; } #endregion } } namespace OpenPop.Mime.Header { using System; using System.Collections.Generic; using System.Text.RegularExpressions; using OpenPop.Mime.Decode; /// <summary> /// Class that hold information about one "Received:" header line. /// /// Visit these RFCs for more information: /// <see href="http://tools.ietf.org/html/rfc5321#section-4.4">RFC 5321 section 4.4</see> /// <see href="http://tools.ietf.org/html/rfc4021#section-3.6.7">RFC 4021 section 3.6.7</see> /// <see href="http://tools.ietf.org/html/rfc2822#section-3.6.7">RFC 2822 section 3.6.7</see> /// <see href="http://tools.ietf.org/html/rfc2821#section-4.4">RFC 2821 section 4.4</see> /// </summary> public class Received { /// <summary> /// The date of this received line. /// Is <see cref="DateTime.MinValue"/> if not present in the received header line. /// </summary> public DateTime Date { get; private set; } /// <summary> /// A dictionary that contains the names and values of the /// received header line. /// If the received header is invalid and contained one name /// multiple times, the first one is used and the rest is ignored. /// </summary> /// <example> /// If the header lines looks like: /// <code> /// from sending.com (localMachine [127.0.0.1]) by test.net (Postfix) /// </code> /// then the dictionary will contain two keys: "from" and "by" with the values /// "sending.com (localMachine [127.0.0.1])" and "test.net (Postfix)". /// </example> public Dictionary<string, string> Names { get; private set; } /// <summary> /// The raw input string that was parsed into this class. /// </summary> public string Raw { get; private set; } /// <summary> /// Parses a Received header value. /// </summary> /// <param name="headerValue">The value for the header to be parsed</param> /// <exception cref="ArgumentNullException"><exception cref="ArgumentNullException">If <paramref name="headerValue"/> is <see langword="null"/></exception></exception> public Received(string headerValue) { if (headerValue == null) throw new ArgumentNullException("headerValue"); // Remember the raw input if someone whishes to use it Raw = headerValue; // Default Date value Date = DateTime.MinValue; // The date part is the last part of the string, and is preceeded by a semicolon // Some emails forgets to specify the date, therefore we need to check if it is there if (headerValue.Contains(";")) { string datePart = headerValue.Substring(headerValue.LastIndexOf(";") + 1); Date = Rfc2822DateTime.StringToDate(datePart); } Names = ParseDictionary(headerValue); } /// <summary> /// Parses the Received header name-value-list into a dictionary. /// </summary> /// <param name="headerValue">The full header value for the Received header</param> /// <returns>A dictionary where the name-value-list has been parsed into</returns> private static Dictionary<string, string> ParseDictionary(string headerValue) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); // Remove the date part from the full headerValue if it is present string headerValueWithoutDate = headerValue; if (headerValue.Contains(";")) { headerValueWithoutDate = headerValue.Substring(0, headerValue.LastIndexOf(";")); } // Reduce any whitespace character to one space only headerValueWithoutDate = Regex.Replace(headerValueWithoutDate, @"\s+", " "); // The regex below should capture the following: // The name consists of non-whitespace characters followed by a whitespace and then the value follows. // There are multiple cases for the value part: // 1: Value is just some characters not including any whitespace // 2: Value is some characters, a whitespace followed by an unlimited number of // parenthesized values which can contain whitespaces, each delimited by whitespace // // Cheat sheet for regex: // \s means every whitespace character // [^\s] means every character except whitespace characters // +? is a non-greedy equivalent of + const string pattern = @"(?<name>[^\s]+)\s(?<value>[^\s]+(\s\(.+?\))*)"; // Find each match in the string MatchCollection matches = Regex.Matches(headerValueWithoutDate, pattern); foreach (Match match in matches) { // Add the name and value part found in the matched result to the dictionary string name = match.Groups["name"].Value; string value = match.Groups["value"].Value; // Check if the name is really a comment. // In this case, the first entry in the header value // is a comment if (name.StartsWith("(")) { continue; } // Only add the first name pair // All subsequent pairs are ignored, as they are invalid anyway if (!dictionary.ContainsKey(name)) dictionary.Add(name, value); } return dictionary; } } } namespace OpenPop.Mime.Header { using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Net.Mail; using System.Net.Mime; using OpenPop.Mime.Decode; /// <summary> /// Class that holds all headers for a message<br/> /// Headers which are unknown the the parser will be held in the <see cref="UnknownHeaders"/> collection.<br/> /// <br/> /// This class cannot be instantiated from outside the library. /// </summary> /// <remarks> /// See <a href="http://tools.ietf.org/html/rfc4021">RFC 4021</a> for a large list of headers.<br/> /// </remarks> public sealed class MessageHeader { #region Properties /// <summary> /// All headers which were not recognized and explicitly dealt with.<br/> /// This should mostly be custom headers, which are marked as X-[name].<br/> /// <br/> /// This list will be empty if all headers were recognized and parsed. /// </summary> /// <remarks> /// If you as a user, feels that a header in this collection should /// be parsed, feel free to notify the developers. /// </remarks> public NameValueCollection UnknownHeaders { get; private set; } /// <summary> /// A human readable description of the body<br/> /// <br/> /// <see langword="null"/> if no Content-Description header was present in the message. /// </summary> public string ContentDescription { get; private set; } /// <summary> /// ID of the content part (like an attached image). Used with MultiPart messages.<br/> /// <br/> /// <see langword="null"/> if no Content-ID header field was present in the message. /// </summary> /// <see cref="MessageId">For an ID of the message</see> public string ContentId { get; private set; } /// <summary> /// Message keywords<br/> /// <br/> /// The list will be empty if no Keywords header was present in the message /// </summary> public List<string> Keywords { get; private set; } /// <summary> /// A List of emails to people who wishes to be notified when some event happens.<br/> /// These events could be email: /// <list type="bullet"> /// <item>deletion</item> /// <item>printing</item> /// <item>received</item> /// <item>...</item> /// </list> /// The list will be empty if no Disposition-Notification-To header was present in the message /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc3798">RFC 3798</a> for details</remarks> public List<RfcMailAddress> DispositionNotificationTo { get; private set; } /// <summary> /// This is the Received headers. This tells the path that the email went.<br/> /// <br/> /// The list will be empty if no Received header was present in the message /// </summary> public List<Received> Received { get; private set; } /// <summary> /// Importance of this email.<br/> /// <br/> /// The importance level is set to normal, if no Importance header field was mentioned or it contained /// unknown information. This is the expected behavior according to the RFC. /// </summary> public MailPriority Importance { get; private set; } /// <summary> /// This header describes the Content encoding during transfer.<br/> /// <br/> /// If no Content-Transfer-Encoding header was present in the message, it is set /// to the default of <see cref="Header.ContentTransferEncoding.SevenBit">SevenBit</see> in accordance to the RFC. /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc2045#section-6">RFC 2045 section 6</a> for details</remarks> public ContentTransferEncoding ContentTransferEncoding { get; private set; } /// <summary> /// Carbon Copy. This specifies who got a copy of the message.<br/> /// <br/> /// The list will be empty if no Cc header was present in the message /// </summary> public List<RfcMailAddress> Cc { get; private set; } /// <summary> /// Blind Carbon Copy. This specifies who got a copy of the message, but others /// cannot see who these persons are.<br/> /// <br/> /// The list will be empty if no Received Bcc was present in the message /// </summary> public List<RfcMailAddress> Bcc { get; private set; } /// <summary> /// Specifies who this mail was for<br/> /// <br/> /// The list will be empty if no To header was present in the message /// </summary> public List<RfcMailAddress> To { get; private set; } /// <summary> /// Specifies who sent the email<br/> /// <br/> /// <see langword="null"/> if no From header field was present in the message /// </summary> public RfcMailAddress From { get; private set; } /// <summary> /// Specifies who a reply to the message should be sent to<br/> /// <br/> /// <see langword="null"/> if no Reply-To header field was present in the message /// </summary> public RfcMailAddress ReplyTo { get; private set; } /// <summary> /// The message identifier(s) of the original message(s) to which the /// current message is a reply.<br/> /// <br/> /// The list will be empty if no In-Reply-To header was present in the message /// </summary> public List<string> InReplyTo { get; private set; } /// <summary> /// The message identifier(s) of other message(s) to which the current /// message is related to.<br/> /// <br/> /// The list will be empty if no References header was present in the message /// </summary> public List<string> References { get; private set; } /// <summary> /// This is the sender of the email address.<br/> /// <br/> /// <see langword="null"/> if no Sender header field was present in the message /// </summary> /// <remarks> /// The RFC states that this field can be used if a secretary /// is sending an email for someone she is working for. /// The email here will then be the secretary's email, and /// the Reply-To field would hold the address of the person she works for.<br/> /// RFC states that if the Sender is the same as the From field, /// sender should not be included in the message. /// </remarks> public RfcMailAddress Sender { get; private set; } /// <summary> /// The Content-Type header field.<br/> /// <br/> /// If not set, the ContentType is created by the default "text/plain; charset=us-ascii" which is /// defined in <a href="http://tools.ietf.org/html/rfc2045#section-5.2">RFC 2045 section 5.2</a>.<br/> /// If set, the default is overridden. /// </summary> public ContentType ContentType { get; private set; } /// <summary> /// Used to describe if a <see cref="MessagePart"/> is to be displayed or to be though of as an attachment.<br/> /// Also contains information about filename if such was sent.<br/> /// <br/> /// <see langword="null"/> if no Content-Disposition header field was present in the message /// </summary> public ContentDisposition ContentDisposition { get; private set; } /// <summary> /// The Date when the email was sent.<br/> /// This is the raw value. <see cref="DateSent"/> for a parsed up <see cref="DateTime"/> value of this field.<br/> /// <br/> /// <see langword="DateTime.MinValue"/> if no Date header field was present in the message or if the date could not be parsed. /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc5322#section-3.6.1">RFC 5322 section 3.6.1</a> for more details</remarks> public string Date { get; private set; } /// <summary> /// The Date when the email was sent.<br/> /// This is the parsed equivalent of <see cref="Date"/>.<br/> /// Notice that the <see cref="TimeZone"/> of the <see cref="DateTime"/> object is in UTC and has NOT been converted /// to local <see cref="TimeZone"/>. /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc5322#section-3.6.1">RFC 5322 section 3.6.1</a> for more details</remarks> public DateTime DateSent { get; private set; } /// <summary> /// An ID of the message that is SUPPOSED to be in every message according to the RFC.<br/> /// The ID is unique.<br/> /// <br/> /// <see langword="null"/> if no Message-ID header field was present in the message /// </summary> public string MessageId { get; private set; } /// <summary> /// The Mime Version.<br/> /// This field will almost always show 1.0<br/> /// <br/> /// <see langword="null"/> if no Mime-Version header field was present in the message /// </summary> public string MimeVersion { get; private set; } /// <summary> /// A single <see cref="RfcMailAddress"/> with no username inside.<br/> /// This is a trace header field, that should be in all messages.<br/> /// Replies should be sent to this address.<br/> /// <br/> /// <see langword="null"/> if no Return-Path header field was present in the message /// </summary> public RfcMailAddress ReturnPath { get; private set; } /// <summary> /// The subject line of the message in decoded, one line state.<br/> /// This should be in all messages.<br/> /// <br/> /// <see langword="null"/> if no Subject header field was present in the message /// </summary> public string Subject { get; private set; } #endregion /// <summary> /// Parses a <see cref="NameValueCollection"/> to a MessageHeader /// </summary> /// <param name="headers">The collection that should be traversed and parsed</param> /// <returns>A valid MessageHeader object</returns> /// <exception cref="ArgumentNullException">If <paramref name="headers"/> is <see langword="null"/></exception> internal MessageHeader(NameValueCollection headers) { if (headers == null) throw new ArgumentNullException("headers"); // Create empty lists as defaults. We do not like null values // List with an initial capacity set to zero will be replaced // when a corrosponding header is found To = new List<RfcMailAddress>(0); Cc = new List<RfcMailAddress>(0); Bcc = new List<RfcMailAddress>(0); Received = new List<Received>(); Keywords = new List<string>(); InReplyTo = new List<string>(0); References = new List<string>(0); DispositionNotificationTo = new List<RfcMailAddress>(); UnknownHeaders = new NameValueCollection(); // Default importancetype is Normal (assumed if not set) Importance = MailPriority.Normal; // 7BIT is the default ContentTransferEncoding (assumed if not set) ContentTransferEncoding = ContentTransferEncoding.SevenBit; // text/plain; charset=us-ascii is the default ContentType ContentType = new ContentType("text/plain; charset=us-ascii"); // Now parse the actual headers ParseHeaders(headers); } /// <summary> /// Parses a <see cref="NameValueCollection"/> to a <see cref="MessageHeader"/> /// </summary> /// <param name="headers">The collection that should be traversed and parsed</param> /// <returns>A valid <see cref="MessageHeader"/> object</returns> /// <exception cref="ArgumentNullException">If <paramref name="headers"/> is <see langword="null"/></exception> private void ParseHeaders(NameValueCollection headers) { if (headers == null) throw new ArgumentNullException("headers"); // Now begin to parse the header values foreach (string headerName in headers.Keys) { string[] headerValues = headers.GetValues(headerName); if (headerValues != null) { foreach (string headerValue in headerValues) { ParseHeader(headerName, headerValue); } } } } #region Header fields parsing /// <summary> /// Parses a single header and sets member variables according to it. /// </summary> /// <param name="headerName">The name of the header</param> /// <param name="headerValue">The value of the header in unfolded state (only one line)</param> /// <exception cref="ArgumentNullException">If <paramref name="headerName"/> or <paramref name="headerValue"/> is <see langword="null"/></exception> private void ParseHeader(string headerName, string headerValue) { if (headerName == null) throw new ArgumentNullException("headerName"); if (headerValue == null) throw new ArgumentNullException("headerValue"); switch (headerName.ToUpperInvariant()) { // See http://tools.ietf.org/html/rfc5322#section-3.6.3 case "TO": To = RfcMailAddress.ParseMailAddresses(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.3 case "CC": Cc = RfcMailAddress.ParseMailAddresses(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.3 case "BCC": Bcc = RfcMailAddress.ParseMailAddresses(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.2 case "FROM": // There is only one MailAddress in the from field From = RfcMailAddress.ParseMailAddress(headerValue); break; // http://tools.ietf.org/html/rfc5322#section-3.6.2 // The implementation here might be wrong case "REPLY-TO": // This field may actually be a list of addresses, but no // such case has been encountered ReplyTo = RfcMailAddress.ParseMailAddress(headerValue); break; // http://tools.ietf.org/html/rfc5322#section-3.6.2 case "SENDER": Sender = RfcMailAddress.ParseMailAddress(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.5 // RFC 5322: // The "Keywords:" field contains a comma-separated list of one or more // words or quoted-strings. // The field are intended to have only human-readable content // with information about the message case "KEYWORDS": string[] keywordsTemp = headerValue.Split(','); foreach (string keyword in keywordsTemp) { // Remove the quotes if there is any Keywords.Add(Utility.RemoveQuotesIfAny(keyword.Trim())); } break; // See http://tools.ietf.org/html/rfc5322#section-3.6.7 case "RECEIVED": // Simply add the value to the list Received.Add(new Received(headerValue.Trim())); break; case "IMPORTANCE": Importance = HeaderFieldParser.ParseImportance(headerValue.Trim()); break; // See http://tools.ietf.org/html/rfc3798#section-2.1 case "DISPOSITION-NOTIFICATION-TO": DispositionNotificationTo = RfcMailAddress.ParseMailAddresses(headerValue); break; case "MIME-VERSION": MimeVersion = headerValue.Trim(); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.5 case "SUBJECT": Subject = EncodedWord.Decode(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.7 case "RETURN-PATH": // Return-paths does not include a username, but we // may still use the address parser ReturnPath = RfcMailAddress.ParseMailAddress(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.4 // Example Message-ID // <33cdd74d6b89ab2250ecd75b40a41405@nfs.eksperten.dk> case "MESSAGE-ID": MessageId = HeaderFieldParser.ParseId(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.4 case "IN-REPLY-TO": InReplyTo = HeaderFieldParser.ParseMultipleIDs(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.4 case "REFERENCES": References = HeaderFieldParser.ParseMultipleIDs(headerValue); break; // See http://tools.ietf.org/html/rfc5322#section-3.6.1)) case "DATE": Date = headerValue.Trim(); DateSent = Rfc2822DateTime.StringToDate(headerValue); break; // See http://tools.ietf.org/html/rfc2045#section-6 // See ContentTransferEncoding class for more details case "CONTENT-TRANSFER-ENCODING": ContentTransferEncoding = HeaderFieldParser.ParseContentTransferEncoding(headerValue.Trim()); break; // See http://tools.ietf.org/html/rfc2045#section-8 case "CONTENT-DESCRIPTION": // Human description of for example a file. Can be encoded ContentDescription = EncodedWord.Decode(headerValue.Trim()); break; // See http://tools.ietf.org/html/rfc2045#section-5.1 // Example: Content-type: text/plain; charset="us-ascii" case "CONTENT-TYPE": ContentType = HeaderFieldParser.ParseContentType(headerValue); break; // See http://tools.ietf.org/html/rfc2183 case "CONTENT-DISPOSITION": ContentDisposition = HeaderFieldParser.ParseContentDisposition(headerValue); break; // See http://tools.ietf.org/html/rfc2045#section-7 // Example: <foo4*foo1@bar.net> case "CONTENT-ID": ContentId = HeaderFieldParser.ParseId(headerValue); break; default: // This is an unknown header // Custom headers are allowed. That means headers // that are not mentionen in the RFC. // Such headers start with the letter "X" // We do not have any special parsing of such // Add it to unknown headers UnknownHeaders.Add(headerName, headerValue); break; } } #endregion } } namespace OpenPop.Mime.Header { using System; using System.Collections.Generic; using System.Globalization; using System.Net.Mail; using System.Net.Mime; using System.Text; using OpenPop.Mime.Decode; using OpenPop.Common.Logging; /// <summary> /// Class that can parse different fields in the header sections of a MIME message. /// </summary> internal static class HeaderFieldParser { /// <summary> /// Parses the Content-Transfer-Encoding header. /// </summary> /// <param name="headerValue">The value for the header to be parsed</param> /// <returns>A <see cref="ContentTransferEncoding"/></returns> /// <exception cref="ArgumentNullException">If <paramref name="headerValue"/> is <see langword="null"/></exception> /// <exception cref="ArgumentException">If the <paramref name="headerValue"/> could not be parsed to a <see cref="ContentTransferEncoding"/></exception> public static ContentTransferEncoding ParseContentTransferEncoding(string headerValue) { if (headerValue == null) throw new ArgumentNullException("headerValue"); switch (headerValue.Trim().ToUpperInvariant()) { case "7BIT": return ContentTransferEncoding.SevenBit; case "8BIT": return ContentTransferEncoding.EightBit; case "QUOTED-PRINTABLE": return ContentTransferEncoding.QuotedPrintable; case "BASE64": return ContentTransferEncoding.Base64; case "BINARY": return ContentTransferEncoding.Binary; // If a wrong argument is passed to this parser method, then we assume // default encoding, which is SevenBit. // This is to ensure that we do not throw exceptions, even if the email not MIME valid. default: DefaultLogger.Log.LogDebug("Wrong ContentTransferEncoding was used. It was: " + headerValue); return ContentTransferEncoding.SevenBit; } } /// <summary> /// Parses an ImportanceType from a given Importance header value. /// </summary> /// <param name="headerValue">The value to be parsed</param> /// <returns>A <see cref="MailPriority"/>. If the <paramref name="headerValue"/> is not recognized, Normal is returned.</returns> /// <exception cref="ArgumentNullException">If <paramref name="headerValue"/> is <see langword="null"/></exception> public static MailPriority ParseImportance(string headerValue) { if (headerValue == null) throw new ArgumentNullException("headerValue"); switch (headerValue.ToUpperInvariant()) { case "5": case "HIGH": return MailPriority.High; case "3": case "NORMAL": return MailPriority.Normal; case "1": case "LOW": return MailPriority.Low; default: DefaultLogger.Log.LogDebug("HeaderFieldParser: Unknown importance value: \"" + headerValue + "\". Using default of normal importance."); return MailPriority.Normal; } } /// <summary> /// Parses a the value for the header Content-Type to /// a <see cref="ContentType"/> object. /// </summary> /// <param name="headerValue">The value to be parsed</param> /// <returns>A <see cref="ContentType"/> object</returns> /// <exception cref="ArgumentNullException">If <paramref name="headerValue"/> is <see langword="null"/></exception> public static ContentType ParseContentType(string headerValue) { if (headerValue == null) throw new ArgumentNullException("headerValue"); // We create an empty Content-Type which we will fill in when we see the values ContentType contentType = new ContentType(); // Now decode the parameters List<KeyValuePair<string, string>> parameters = Rfc2231Decoder.Decode(headerValue); foreach (KeyValuePair<string, string> keyValuePair in parameters) { string key = keyValuePair.Key.ToUpperInvariant().Trim(); string value = Utility.RemoveQuotesIfAny(keyValuePair.Value.Trim()); switch (key) { case "": // This is the MediaType - it has no key since it is the first one mentioned in the // headerValue and has no = in it. // Check for illegal content-type if (value.ToUpperInvariant().Equals("TEXT")) value = "text/plain"; contentType.MediaType = value; break; case "BOUNDARY": contentType.Boundary = value; break; case "CHARSET": contentType.CharSet = value; break; case "NAME": contentType.Name = EncodedWord.Decode(value); break; default: // This is to shut up the code help that is saying that contentType.Parameters // can be null - which it cant! if (contentType.Parameters == null) throw new Exception("The ContentType parameters property is null. This will never be thrown."); // We add the unknown value to our parameters list // "Known" unknown values are: // - title // - report-type contentType.Parameters.Add(key, value); break; } } return contentType; } /// <summary> /// Parses a the value for the header Content-Disposition to a <see cref="ContentDisposition"/> object. /// </summary> /// <param name="headerValue">The value to be parsed</param> /// <returns>A <see cref="ContentDisposition"/> object</returns> /// <exception cref="ArgumentNullException">If <paramref name="headerValue"/> is <see langword="null"/></exception> public static ContentDisposition ParseContentDisposition(string headerValue) { if (headerValue == null) throw new ArgumentNullException("headerValue"); // See http://www.ietf.org/rfc/rfc2183.txt for RFC definition // Create empty ContentDisposition - we will fill in details as we read them ContentDisposition contentDisposition = new ContentDisposition(); // Now decode the parameters List<KeyValuePair<string, string>> parameters = Rfc2231Decoder.Decode(headerValue); foreach (KeyValuePair<string, string> keyValuePair in parameters) { string key = keyValuePair.Key.ToUpperInvariant().Trim(); string value = keyValuePair.Value; switch (key) { case "": // This is the DispisitionType - it has no key since it is the first one // and has no = in it. contentDisposition.DispositionType = value; break; // The correct name of the parameter is filename, but some emails also contains the parameter // name, which also holds the name of the file. Therefore we use both names for the same field. case "NAME": case "FILENAME": // The filename might be in qoutes, and it might be encoded-word encoded contentDisposition.FileName = EncodedWord.Decode(Utility.RemoveQuotesIfAny(value)); break; case "CREATION-DATE": // Notice that we need to create a new DateTime because of a failure in .NET 2.0. // The failure is: you cannot give contentDisposition a DateTime with a Kind of UTC // It will set the CreationDate correctly, but when trying to read it out it will throw an exception. // It is the same with ModificationDate and ReadDate. // This is fixed in 4.0 - maybe in 3.0 too. // Therefore we create a new DateTime which have a DateTimeKind set to unspecified DateTime creationDate = new DateTime(Rfc2822DateTime.StringToDate(Utility.RemoveQuotesIfAny(value)).Ticks); contentDisposition.CreationDate = creationDate; break; case "MODIFICATION-DATE": DateTime midificationDate = new DateTime(Rfc2822DateTime.StringToDate(Utility.RemoveQuotesIfAny(value)).Ticks); contentDisposition.ModificationDate = midificationDate; break; case "READ-DATE": DateTime readDate = new DateTime(Rfc2822DateTime.StringToDate(Utility.RemoveQuotesIfAny(value)).Ticks); contentDisposition.ReadDate = readDate; break; case "SIZE": contentDisposition.Size = int.Parse(Utility.RemoveQuotesIfAny(value), CultureInfo.InvariantCulture); break; default: if (key.StartsWith("X-")) { contentDisposition.Parameters.Add(key, Utility.RemoveQuotesIfAny(value)); break; } throw new ArgumentException("Unknown parameter in Content-Disposition. Ask developer to fix! Parameter: " + key); } } return contentDisposition; } /// <summary> /// Parses an ID like Message-Id and Content-Id.<br/> /// Example:<br/> /// <c><test@test.com></c><br/> /// into<br/> /// <c>test@test.com</c> /// </summary> /// <param name="headerValue">The id to parse</param> /// <returns>A parsed ID</returns> public static string ParseId(string headerValue) { // Remove whitespace in front and behind since // whitespace is allowed there // Remove the last > and the first < return headerValue.Trim().TrimEnd('>').TrimStart('<'); } /// <summary> /// Parses multiple IDs from a single string like In-Reply-To. /// </summary> /// <param name="headerValue">The value to parse</param> /// <returns>A list of IDs</returns> public static List<string> ParseMultipleIDs(string headerValue) { List<string> returner = new List<string>(); // Split the string by > // We cannot use ' ' (space) here since this is a possible value: // <test@test.com><test2@test.com> string[] ids = headerValue.Trim().Split(new[] { '>' }, StringSplitOptions.RemoveEmptyEntries); foreach (string id in ids) { returner.Add(ParseId(id)); } return returner; } } } namespace OpenPop.Mime.Header { using System; using System.Collections.Generic; using System.Collections.Specialized; using System.IO; using System.Text; using OpenPop.Common; ///<summary> /// Utility class that divides a message into a body and a header.<br/> /// The header is then parsed to a strongly typed <see cref="MessageHeader"/> object. ///</summary> internal static class HeaderExtractor { /// <summary> /// Find the end of the header section in a byte array.<br/> /// The headers have ended when a blank line is found /// </summary> /// <param name="messageContent">The full message stored as a byte array</param> /// <returns>The position of the line just after the header end blank line</returns> private static int FindHeaderEndPosition(byte[] messageContent) { // Convert the byte array into a stream using (Stream stream = new MemoryStream(messageContent)) { while (true) { // Read a line from the stream. We know headers are in US-ASCII // therefore it is not problem to read them as such string line = StreamUtility.ReadLineAsAscii(stream); // The end of headers is signaled when a blank line is found // or if the line is null - in which case the email is actually an email with // only headers but no body if (string.IsNullOrEmpty(line)) return (int)stream.Position; } } } /// <summary> /// Extract the header part and body part of a message.<br/> /// The headers are then parsed to a strongly typed <see cref="MessageHeader"/> object. /// </summary> /// <param name="fullRawMessage">The full message in bytes where header and body needs to be extracted from</param> /// <param name="headers">The extracted header parts of the message</param> /// <param name="body">The body part of the message</param> /// <exception cref="ArgumentNullException">If <paramref name="fullRawMessage"/> is <see langword="null"/></exception> public static void ExtractHeadersAndBody(byte[] fullRawMessage, out MessageHeader headers, out byte[] body) { if (fullRawMessage == null) throw new ArgumentNullException("fullRawMessage"); // Find the end location of the headers int endOfHeaderLocation = FindHeaderEndPosition(fullRawMessage); // The headers are always in ASCII - therefore we can convert the header part into a string // using US-ASCII encoding string headersString = Encoding.ASCII.GetString(fullRawMessage, 0, endOfHeaderLocation); // Now parse the headers to a NameValueCollection NameValueCollection headersUnparsedCollection = ExtractHeaders(headersString); // Use the NameValueCollection to parse it into a strongly-typed MessageHeader header headers = new MessageHeader(headersUnparsedCollection); // Since we know where the headers end, we also know where the body is // Copy the body part into the body parameter body = new byte[fullRawMessage.Length - endOfHeaderLocation]; Array.Copy(fullRawMessage, endOfHeaderLocation, body, 0, body.Length); } /// <summary> /// Method that takes a full message and extract the headers from it. /// </summary> /// <param name="messageContent">The message to extract headers from. Does not need the body part. Needs the empty headers end line.</param> /// <returns>A collection of Name and Value pairs of headers</returns> /// <exception cref="ArgumentNullException">If <paramref name="messageContent"/> is <see langword="null"/></exception> private static NameValueCollection ExtractHeaders(string messageContent) { if (messageContent == null) throw new ArgumentNullException("messageContent"); NameValueCollection headers = new NameValueCollection(); using (StringReader messageReader = new StringReader(messageContent)) { // Read until all headers have ended. // The headers ends when an empty line is encountered // An empty message might actually not have an empty line, in which // case the headers end with null value. string line; while (!string.IsNullOrEmpty(line = messageReader.ReadLine())) { // Split into name and value KeyValuePair<string, string> header = SeparateHeaderNameAndValue(line); // First index is header name string headerName = header.Key; // Second index is the header value. // Use a StringBuilder since the header value may be continued on the next line StringBuilder headerValue = new StringBuilder(header.Value); // Keep reading until we would hit next header // This if for handling multi line headers while (IsMoreLinesInHeaderValue(messageReader)) { // Unfolding is accomplished by simply removing any CRLF // that is immediately followed by WSP // This was done using ReadLine (it discards CRLF) // See http://tools.ietf.org/html/rfc822#section-3.1.1 for more information string moreHeaderValue = messageReader.ReadLine(); // If this exception is ever raised, there is an serious algorithm failure // IsMoreLinesInHeaderValue does not return true if the next line does not exist // This check is only included to stop the nagging "possibly null" code analysis hint if (moreHeaderValue == null) throw new ArgumentException("This will never happen"); // Simply append the line just read to the header value headerValue.Append(moreHeaderValue); } // Now we have the name and full value. Add it headers.Add(headerName, headerValue.ToString()); } } return headers; } /// <summary> /// Check if the next line is part of the current header value we are parsing by /// peeking on the next character of the <see cref="TextReader"/>.<br/> /// This should only be called while parsing headers. /// </summary> /// <param name="reader">The reader from which the header is read from</param> /// <returns><see langword="true"/> if multi-line header. <see langword="false"/> otherwise</returns> private static bool IsMoreLinesInHeaderValue(TextReader reader) { int peek = reader.Peek(); if (peek == -1) return false; char peekChar = (char)peek; // A multi line header must have a whitespace character // on the next line if it is to be continued return peekChar == ' ' || peekChar == '\t'; } /// <summary> /// Separate a full header line into a header name and a header value. /// </summary> /// <param name="rawHeader">The raw header line to be separated</param> /// <exception cref="ArgumentNullException">If <paramref name="rawHeader"/> is <see langword="null"/></exception> internal static KeyValuePair<string, string> SeparateHeaderNameAndValue(string rawHeader) { if (rawHeader == null) throw new ArgumentNullException("rawHeader"); string key = string.Empty; string value = string.Empty; int indexOfColon = rawHeader.IndexOf(':'); // Check if it is allowed to make substring calls if (indexOfColon >= 0 && rawHeader.Length >= indexOfColon + 1) { key = rawHeader.Substring(0, indexOfColon).Trim(); value = rawHeader.Substring(indexOfColon + 1).Trim(); } return new KeyValuePair<string, string>(key, value); } } } namespace OpenPop.Mime.Header { using System; /// <summary> /// <see cref="Enum"/> that describes the ContentTransferEncoding header field /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc2045#section-6">RFC 2045 section 6</a> for more details</remarks> public enum ContentTransferEncoding { /// <summary> /// 7 bit Encoding /// </summary> SevenBit, /// <summary> /// 8 bit Encoding /// </summary> EightBit, /// <summary> /// Quoted Printable Encoding /// </summary> QuotedPrintable, /// <summary> /// Base64 Encoding /// </summary> Base64, /// <summary> /// Binary Encoding /// </summary> Binary } } namespace OpenPop.Mime.Decode { using System; using System.Collections.Generic; /// <summary> /// Contains common operations needed while decoding. /// </summary> internal static class Utility { /// <summary> /// Remove quotes, if found, around the string. /// </summary> /// <param name="text">Text with quotes or without quotes</param> /// <returns>Text without quotes</returns> /// <exception cref="ArgumentNullException">If <paramref name="text"/> is <see langword="null"/></exception> public static string RemoveQuotesIfAny(string text) { if (text == null) throw new ArgumentNullException("text"); // Check if there are qoutes at both ends if (text[0] == '"' && text[text.Length - 1] == '"') { // Remove quotes at both ends return text.Substring(1, text.Length - 2); } // If no quotes were found, the text is just returned return text; } /// <summary> /// Split a string into a list of strings using a specified character.<br/> /// Everything inside quotes are ignored. /// </summary> /// <param name="input">A string to split</param> /// <param name="toSplitAt">The character to use to split with</param> /// <returns>A List of strings that was delimited by the <paramref name="toSplitAt"/> character</returns> public static List<string> SplitStringWithCharNotInsideQuotes(string input, char toSplitAt) { List<string> elements = new List<string>(); int lastSplitLocation = 0; bool insideQuote = false; char[] characters = input.ToCharArray(); for (int i = 0; i < characters.Length; i++) { char character = characters[i]; if (character == '\"') insideQuote = !insideQuote; // Only split if we are not inside quotes if (character == toSplitAt && !insideQuote) { // We need to split int length = i - lastSplitLocation; elements.Add(input.Substring(lastSplitLocation, length)); // Update last split location // + 1 so that we do not include the character used to split with next time lastSplitLocation = i + 1; } } // Add the last part elements.Add(input.Substring(lastSplitLocation, input.Length - lastSplitLocation)); return elements; } } } namespace OpenPop.Mime.Decode { using System; using System.Globalization; using System.Text.RegularExpressions; using OpenPop.Common.Logging; /// <summary> /// Class used to decode RFC 2822 Date header fields. /// </summary> internal static class Rfc2822DateTime { /// <summary> /// Converts a string in RFC 2822 format into a <see cref="DateTime"/> object /// </summary> /// <param name="inputDate">The date to convert</param> /// <returns> /// A valid <see cref="DateTime"/> object, which represents the same time as the string that was converted. /// If <paramref name="inputDate"/> is not a valid date representation, then <see cref="DateTime.MinValue"/> is returned. /// </returns> /// <exception cref="ArgumentNullException"><exception cref="ArgumentNullException">If <paramref name="inputDate"/> is <see langword="null"/></exception></exception> /// <exception cref="ArgumentException">If the <paramref name="inputDate"/> could not be parsed into a <see cref="DateTime"/> object</exception> public static DateTime StringToDate(string inputDate) { if (inputDate == null) throw new ArgumentNullException("inputDate"); // Old date specification allows comments and a lot of whitespace inputDate = StripCommentsAndExcessWhitespace(inputDate); try { // Extract the DateTime DateTime dateTime = ExtractDateTime(inputDate); // If a day-name is specified in the inputDate string, check if it fits with the date ValidateDayNameIfAny(dateTime, inputDate); // Convert the date into UTC dateTime = new DateTime(dateTime.Ticks, DateTimeKind.Utc); // Adjust according to the time zone dateTime = AdjustTimezone(dateTime, inputDate); // Return the parsed date return dateTime; } catch (FormatException e) // Convert.ToDateTime() Failure { throw new ArgumentException("Could not parse date: " + e.Message + ". Input was: \"" + inputDate + "\"", e); } catch (ArgumentException e) { throw new ArgumentException("Could not parse date: " + e.Message + ". Input was: \"" + inputDate + "\"", e); } } /// <summary> /// Adjust the <paramref name="dateTime"/> object given according to the timezone specified in the <paramref name="dateInput"/>. /// </summary> /// <param name="dateTime">The date to alter</param> /// <param name="dateInput">The input date, in which the timezone can be found</param> /// <returns>An date altered according to the timezone</returns> /// <exception cref="ArgumentException">If no timezone was found in <paramref name="dateInput"/></exception> private static DateTime AdjustTimezone(DateTime dateTime, string dateInput) { // We know that the timezones are always in the last part of the date input string[] parts = dateInput.Split(' '); string lastPart = parts[parts.Length - 1]; // Convert timezones in older formats to [+-]dddd format. lastPart = Regex.Replace(lastPart, @"UT|GMT|EST|EDT|CST|CDT|MST|MDT|PST|PDT|[A-I]|[K-Y]|Z", MatchEvaluator); // Find the timezone specification // Example: Fri, 21 Nov 1997 09:55:06 -0600 // finds -0600 Match match = Regex.Match(lastPart, @"[\+-](?<hours>\d\d)(?<minutes>\d\d)"); if (match.Success) { // We have found that the timezone is in +dddd or -dddd format // Add the number of hours and minutes to our found date int hours = int.Parse(match.Groups["hours"].Value); int minutes = int.Parse(match.Groups["minutes"].Value); int factor = match.Value[0] == '+' ? -1 : 1; dateTime = dateTime.AddHours(factor * hours); dateTime = dateTime.AddMinutes(factor * minutes); return dateTime; } DefaultLogger.Log.LogDebug("No timezone found in date: " + dateInput + ". Using -0000 as default."); // A timezone of -0000 is the same as doing nothing return dateTime; } /// <summary> /// Convert timezones in older formats to [+-]dddd format. /// </summary> /// <param name="match">The match that was found</param> /// <returns>The string to replace the matched string with</returns> private static string MatchEvaluator(Match match) { if (!match.Success) { throw new ArgumentException("Match success are always true"); } switch (match.Value) { // "A" through "I" // are equivalent to "+0100" through "+0900" respectively case "A": return "+0100"; case "B": return "+0200"; case "C": return "+0300"; case "D": return "+0400"; case "E": return "+0500"; case "F": return "+0600"; case "G": return "+0700"; case "H": return "+0800"; case "I": return "+0900"; // "K", "L", and "M" // are equivalent to "+1000", "+1100", and "+1200" respectively case "K": return "+1000"; case "L": return "+1100"; case "M": return "+1200"; // "N" through "Y" // are equivalent to "-0100" through "-1200" respectively case "N": return "-0100"; case "O": return "-0200"; case "P": return "-0300"; case "Q": return "-0400"; case "R": return "-0500"; case "S": return "-0600"; case "T": return "-0700"; case "U": return "-0800"; case "V": return "-0900"; case "W": return "-1000"; case "X": return "-1100"; case "Y": return "-1200"; // "Z", "UT" and "GMT" // is equivalent to "+0000" case "Z": case "UT": case "GMT": return "+0000"; // US time zones case "EDT": return "-0400"; // EDT is semantically equivalent to -0400 case "EST": return "-0500"; // EST is semantically equivalent to -0500 case "CDT": return "-0500"; // CDT is semantically equivalent to -0500 case "CST": return "-0600"; // CST is semantically equivalent to -0600 case "MDT": return "-0600"; // MDT is semantically equivalent to -0600 case "MST": return "-0700"; // MST is semantically equivalent to -0700 case "PDT": return "-0700"; // PDT is semantically equivalent to -0700 case "PST": return "-0800"; // PST is semantically equivalent to -0800 default: throw new ArgumentException("Unexpected input"); } } /// <summary> /// Extracts the date and time parts from the <paramref name="dateInput"/> /// </summary> /// <param name="dateInput">The date input string, from which to extract the date and time parts</param> /// <returns>The extracted date part or <see langword="DateTime.MinValue"/> if <paramref name="dateInput"/> is not recognized as a valid date.</returns> private static DateTime ExtractDateTime(string dateInput) { // Matches the date and time part of a string // Example: Fri, 21 Nov 1997 09:55:06 -0600 // Finds: 21 Nov 1997 09:55:06 // Seconds does not need to be specified // Even though it is illigal, sometimes hours, minutes or seconds are only specified with one digit Match match = Regex.Match(dateInput, @"\d\d? .+ (\d\d\d\d|\d\d) \d?\d:\d?\d(:\d?\d)?"); if (match.Success) { return Convert.ToDateTime(match.Value, CultureInfo.InvariantCulture); } DefaultLogger.Log.LogError("The given date does not appear to be in a valid format: " + dateInput); return DateTime.MinValue; } /// <summary> /// Validates that the given <paramref name="dateTime"/> agrees with a day-name specified /// in <paramref name="dateInput"/>. /// </summary> /// <param name="dateTime">The time to check</param> /// <param name="dateInput">The date input to extract the day-name from</param> /// <exception cref="ArgumentException">If <paramref name="dateTime"/> and <paramref name="dateInput"/> does not agree on the day</exception> private static void ValidateDayNameIfAny(DateTime dateTime, string dateInput) { // Check if there is a day name in front of the date // Example: Fri, 21 Nov 1997 09:55:06 -0600 if (dateInput.Length >= 4 && dateInput[3] == ',') { string dayName = dateInput.Substring(0, 3); // If a dayName was specified. Check that the dateTime and the dayName // agrees on which day it is // This is just a failure-check and could be left out if ((dateTime.DayOfWeek == DayOfWeek.Monday && !dayName.Equals("Mon")) || (dateTime.DayOfWeek == DayOfWeek.Tuesday && !dayName.Equals("Tue")) || (dateTime.DayOfWeek == DayOfWeek.Wednesday && !dayName.Equals("Wed")) || (dateTime.DayOfWeek == DayOfWeek.Thursday && !dayName.Equals("Thu")) || (dateTime.DayOfWeek == DayOfWeek.Friday && !dayName.Equals("Fri")) || (dateTime.DayOfWeek == DayOfWeek.Saturday && !dayName.Equals("Sat")) || (dateTime.DayOfWeek == DayOfWeek.Sunday && !dayName.Equals("Sun"))) { DefaultLogger.Log.LogDebug("Day-name does not correspond to the weekday of the date: " + dateInput); } } // If no day name was found no checks can be made } /// <summary> /// Strips and removes all comments and excessive whitespace from the string /// </summary> /// <param name="input">The input to strip from</param> /// <returns>The stripped string</returns> private static string StripCommentsAndExcessWhitespace(string input) { // Strip out comments // Also strips out nested comments input = Regex.Replace(input, @"(\((?>\((?<C>)|\)(?<-C>)|.?)*(?(C)(?!))\))", ""); // Reduce any whitespace character to one space only input = Regex.Replace(input, @"\s+", " "); // Remove all initial whitespace input = Regex.Replace(input, @"^\s+", ""); // Remove all ending whitespace input = Regex.Replace(input, @"\s+$", ""); // Remove spaces at colons // Example: 22: 33 : 44 => 22:33:44 input = Regex.Replace(input, @" ?: ?", ":"); return input; } } } namespace OpenPop.Mime.Decode { using System; using System.Collections.Generic; using System.Text; using System.Text.RegularExpressions; using OpenPop.Common.Logging; /// <summary> /// This class is responsible for decoding parameters that has been encoded with:<br/> /// <list type="bullet"> /// <item> /// <b>Continuation</b><br/> /// This is where a single parameter has such a long value that it could /// be wrapped while in transit. Instead multiple parameters is used on each line.<br/> /// <br/> /// <b>Example</b><br/> /// From: <c>Content-Type: text/html; boundary="someVeryLongStringHereWhichCouldBeWrappedInTransit"</c><br/> /// To: <c>Content-Type: text/html; boundary*0="someVeryLongStringHere" boundary*1="WhichCouldBeWrappedInTransit"</c><br/> /// </item> /// <item> /// <b>Encoding</b><br/> /// Sometimes other characters then ASCII characters are needed in parameters.<br/> /// The parameter is then given a different name to specify that it is encoded.<br/> /// <br/> /// <b>Example</b><br/> /// From: <c>Content-Disposition attachment; filename="specialCharsÆØÅ"</c><br/> /// To: <c>Content-Disposition attachment; filename*="ISO-8859-1'en-us'specialCharsC6D8C0"</c><br/> /// This encoding is almost the same as <see cref="EncodedWord"/> encoding, and is used to decode the value.<br/> /// </item> /// <item> /// <b>Continuation and Encoding</b><br/> /// Both Continuation and Encoding can be used on the same time.<br/> /// <br/> /// <b>Example</b><br/> /// From: <c>Content-Disposition attachment; filename="specialCharsÆØÅWhichIsSoLong"</c><br/> /// To: <c>Content-Disposition attachment; filename*0*="ISO-8859-1'en-us'specialCharsC6D8C0"; filename*1*="WhichIsSoLong"</c><br/> /// This could also be encoded as:<br/> /// To: <c>Content-Disposition attachment; filename*0*="ISO-8859-1'en-us'specialCharsC6D8C0"; filename*1="WhichIsSoLong"</c><br/> /// Notice that <c>filename*1</c> does not have an <c>*</c> after it - denoting it IS NOT encoded.<br/> /// There are some rules about this:<br/> /// <list type="number"> /// <item>The encoding must be mentioned in the first part (filename*0*), which has to be encoded.</item> /// <item>No other part must specify an encoding, but if encoded it uses the encoding mentioned in the first part.</item> /// <item>Parts may be encoded or not in any order.</item> /// </list> /// <br/> /// </item> /// </list> /// More information and the specification is available in <see href="http://tools.ietf.org/html/rfc2231">RFC 2231</see>. /// </summary> internal static class Rfc2231Decoder { /// <summary> /// Decodes a string of the form:<br/> /// <c>value0; key1=value1; key2=value2; key3=value3</c><br/> /// The returned List of key value pairs will have the key as key and the decoded value as value.<br/> /// The first value0 will have a key of <see cref="string.Empty"/>.<br/> /// <br/> /// If continuation is used, then multiple keys will be merged into one key with the different values /// decoded into on big value for that key.<br/> /// Example:<br/> /// <code> /// title*0=part1 /// title*1=part2 /// </code> /// will have key and value of:<br></br> /// <c>title=decode(part1)decode(part2)</c> /// </summary> /// <param name="toDecode">The string to decode.</param> /// <returns>A list of decoded key value pairs.</returns> /// <exception cref="ArgumentNullException">If <paramref name="toDecode"/> is <see langword="null"/></exception> public static List<KeyValuePair<string, string>> Decode(string toDecode) { if (toDecode == null) throw new ArgumentNullException("toDecode"); // Normalize the input to take account for missing semicolons after parameters. // Example // text/plain; charset=\"iso-8859-1\" name=\"somefile.txt\" or // text/plain;\tcharset=\"iso-8859-1\"\tname=\"somefile.txt\" // is normalized to // text/plain; charset=\"iso-8859-1\"; name=\"somefile.txt\" // Only works for parameters inside quotes // \s = matches whitespace toDecode = Regex.Replace(toDecode, "=\\s*\"(?<value>[^\"]*)\"\\s", "=\"${value}\"; "); // Normalize // Since the above only works for parameters inside quotes, we need to normalize // the special case with the first parameter. // Example: // attachment filename="foo" // is normalized to // attachment; filename="foo" // ^ = matches start of line (when not inside square bracets []) toDecode = Regex.Replace(toDecode, @"^(?<first>[^;\s]+)\s(?<second>[^;\s]+)", "${first}; ${second}"); // Split by semicolon, but only if not inside quotes List<string> splitted = Utility.SplitStringWithCharNotInsideQuotes(toDecode.Trim(), ';'); List<KeyValuePair<string, string>> collection = new List<KeyValuePair<string, string>>(splitted.Count); foreach (string part in splitted) { // Empty strings should not be processed if (part.Trim().Length == 0) continue; string[] keyValue = part.Trim().Split(new[] { '=' }, 2); if (keyValue.Length == 1) { collection.Add(new KeyValuePair<string, string>("", keyValue[0])); } else if (keyValue.Length == 2) { collection.Add(new KeyValuePair<string, string>(keyValue[0], keyValue[1])); } else { throw new ArgumentException("When splitting the part \"" + part + "\" by = there was " + keyValue.Length + " parts. Only 1 and 2 are supported"); } } return DecodePairs(collection); } /// <summary> /// Decodes the list of key value pairs into a decoded list of key value pairs.<br/> /// There may be less keys in the decoded list, but then the values for the lost keys will have been appended /// to the new key. /// </summary> /// <param name="pairs">The pairs to decode</param> /// <returns>A decoded list of pairs</returns> private static List<KeyValuePair<string, string>> DecodePairs(List<KeyValuePair<string, string>> pairs) { if (pairs == null) throw new ArgumentNullException("pairs"); List<KeyValuePair<string, string>> resultPairs = new List<KeyValuePair<string, string>>(pairs.Count); int pairsCount = pairs.Count; for (int i = 0; i < pairsCount; i++) { KeyValuePair<string, string> currentPair = pairs[i]; string key = currentPair.Key; string value = Utility.RemoveQuotesIfAny(currentPair.Value); // Is it a continuation parameter? (encoded or not) if (key.EndsWith("*0", StringComparison.OrdinalIgnoreCase) || key.EndsWith("*0*", StringComparison.OrdinalIgnoreCase)) { // This encoding will not be used if we get into the if which tells us // that the whole continuation is not encoded string encoding = "notEncoded - Value here is never used"; // Now lets find out if it is encoded too. if (key.EndsWith("*0*", StringComparison.OrdinalIgnoreCase)) { // It is encoded. // Fetch out the encoding for later use and decode the value // If the value was not encoded as the email specified // encoding will be set to null. This will be used later. value = DecodeSingleValue(value, out encoding); // Find the right key to use to store the full value // Remove the start *0 which tells is it is a continuation, and the first one // And remove the * afterwards which tells us it is encoded key = key.Replace("*0*", ""); } else { // It is not encoded, and no parts of the continuation is encoded either // Find the right key to use to store the full value // Remove the start *0 which tells is it is a continuation, and the first one key = key.Replace("*0", ""); } // The StringBuilder will hold the full decoded value from all continuation parts StringBuilder builder = new StringBuilder(); // Append the decoded value builder.Append(value); // Now go trough the next keys to see if they are part of the continuation for (int j = i + 1, continuationCount = 1; j < pairsCount; j++, continuationCount++) { string jKey = pairs[j].Key; string valueJKey = Utility.RemoveQuotesIfAny(pairs[j].Value); if (jKey.Equals(key + "*" + continuationCount)) { // This value part of the continuation is not encoded // Therefore remove qoutes if any and add to our stringbuilder builder.Append(valueJKey); // Remember to increment i, as we have now treated one more KeyValuePair i++; } else if (jKey.Equals(key + "*" + continuationCount + "*")) { // We will not get into this part if the first part was not encoded // Therefore the encoding will only be used if and only if the // first part was encoded, in which case we have remembered the encoding used // Sometimes an email creator says that a string was encoded, but it really // `was not. This is to catch that problem. if (encoding != null) { // This value part of the continuation is encoded // the encoding is not given in the current value, // but was given in the first continuation, which we remembered for use here valueJKey = DecodeSingleValue(valueJKey, encoding); } builder.Append(valueJKey); // Remember to increment i, as we have now treated one more KeyValuePair i++; } else { // No more keys for this continuation break; } } // Add the key and the full value as a pair value = builder.ToString(); resultPairs.Add(new KeyValuePair<string, string>(key, value)); } else if (key.EndsWith("*", StringComparison.OrdinalIgnoreCase)) { // This parameter is only encoded - it is not part of a continuation // We need to change the key from "<key>*" to "<key>" and decode the value // To get the key we want, we remove the last * that denotes // that the value hold by the key was encoded key = key.Replace("*", ""); // Decode the value string throwAway; value = DecodeSingleValue(value, out throwAway); // Now input the new value with the new key resultPairs.Add(new KeyValuePair<string, string>(key, value)); } else { // Fully normal key - the value is not encoded // Therefore nothing to do, and we can simply pass the pair // as being decoded now resultPairs.Add(currentPair); } } return resultPairs; } /// <summary> /// This will decode a single value of the form: <c>ISO-8859-1'en-us'%3D%3DIamHere</c><br/> /// Which is basically a <see cref="EncodedWord"/> form just using % instead of =<br/> /// Notice that 'en-us' part is not used for anything.<br/> /// <br/> /// If the single value given is not on the correct form, it will be returned without /// being decoded and <paramref name="encodingUsed"/> will be set to <see langword="null"/>. /// </summary> /// <param name="encodingUsed"> /// The encoding used to decode with - it is given back for later use.<br/> /// <see langword="null"/> if input was not in the correct form. /// </param> /// <param name="toDecode">The value to decode</param> /// <returns> /// The decoded value that corresponds to <paramref name="toDecode"/> or if /// <paramref name="toDecode"/> is not on the correct form, it will be non-decoded. /// </returns> /// <exception cref="ArgumentNullException">If <paramref name="toDecode"/> is <see langword="null"/></exception> private static string DecodeSingleValue(string toDecode, out string encodingUsed) { if (toDecode == null) throw new ArgumentNullException("toDecode"); // Check if input has a part describing the encoding if (toDecode.IndexOf('\'') == -1) { // The input was not encoded (at least not valid) and it is returned as is DefaultLogger.Log.LogDebug("Rfc2231Decoder: Someone asked me to decode a string which was not encoded - returning raw string. Input: " + toDecode); encodingUsed = null; return toDecode; } encodingUsed = toDecode.Substring(0, toDecode.IndexOf('\'')); toDecode = toDecode.Substring(toDecode.LastIndexOf('\'') + 1); return DecodeSingleValue(toDecode, encodingUsed); } /// <summary> /// This will decode a single value of the form: %3D%3DIamHere /// Which is basically a <see cref="EncodedWord"/> form just using % instead of = /// </summary> /// <param name="valueToDecode">The value to decode</param> /// <param name="encoding">The encoding used to decode with</param> /// <returns>The decoded value that corresponds to <paramref name="valueToDecode"/></returns> /// <exception cref="ArgumentNullException">If <paramref name="valueToDecode"/> is <see langword="null"/></exception> /// <exception cref="ArgumentNullException">If <paramref name="encoding"/> is <see langword="null"/></exception> private static string DecodeSingleValue(string valueToDecode, string encoding) { if (valueToDecode == null) throw new ArgumentNullException("valueToDecode"); if (encoding == null) throw new ArgumentNullException("encoding"); // The encoding used is the same as QuotedPrintable, we only // need to change % to = // And otherwise make it look like the correct EncodedWord encoding valueToDecode = "=?" + encoding + "?Q?" + valueToDecode.Replace("%", "=") + "?="; return EncodedWord.Decode(valueToDecode); } } } namespace OpenPop.Mime.Decode { using System; using System.IO; using System.Text; using System.Text.RegularExpressions; /// <summary> /// Used for decoding Quoted-Printable text.<br/> /// This is a robust implementation of a Quoted-Printable decoder defined in <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> and <a href="http://tools.ietf.org/html/rfc2047">RFC 2047</a>.<br/> /// Every measurement has been taken to conform to the RFC. /// </summary> internal static class QuotedPrintable { /// <summary> /// Decodes a Quoted-Printable string according to <a href="http://tools.ietf.org/html/rfc2047">RFC 2047</a>.<br/> /// RFC 2047 is used for decoding Encoded-Word encoded strings. /// </summary> /// <param name="toDecode">Quoted-Printable encoded string</param> /// <param name="encoding">Specifies which encoding the returned string will be in</param> /// <returns>A decoded string in the correct encoding</returns> /// <exception cref="ArgumentNullException">If <paramref name="toDecode"/> or <paramref name="encoding"/> is <see langword="null"/></exception> public static string DecodeEncodedWord(string toDecode, Encoding encoding) { if (toDecode == null) throw new ArgumentNullException("toDecode"); if (encoding == null) throw new ArgumentNullException("encoding"); // Decode the QuotedPrintable string and return it return encoding.GetString(Rfc2047QuotedPrintableDecode(toDecode, true)); } /// <summary> /// Decodes a Quoted-Printable string according to <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a>.<br/> /// RFC 2045 specifies the decoding of a body encoded with Content-Transfer-Encoding of quoted-printable. /// </summary> /// <param name="toDecode">Quoted-Printable encoded string</param> /// <returns>A decoded byte array that the Quoted-Printable encoded string described</returns> /// <exception cref="ArgumentNullException">If <paramref name="toDecode"/> is <see langword="null"/></exception> public static byte[] DecodeContentTransferEncoding(string toDecode) { if (toDecode == null) throw new ArgumentNullException("toDecode"); // Decode the QuotedPrintable string and return it return Rfc2047QuotedPrintableDecode(toDecode, false); } /// <summary> /// This is the actual decoder. /// </summary> /// <param name="toDecode">The string to be decoded from Quoted-Printable</param> /// <param name="encodedWordVariant"> /// If <see langword="true"/>, specifies that RFC 2047 quoted printable decoding is used.<br/> /// This is for quoted-printable encoded words<br/> /// <br/> /// If <see langword="false"/>, specifies that RFC 2045 quoted printable decoding is used.<br/> /// This is for quoted-printable Content-Transfer-Encoding /// </param> /// <returns>A decoded byte array that was described by <paramref name="toDecode"/></returns> /// <exception cref="ArgumentNullException">If <paramref name="toDecode"/> is <see langword="null"/></exception> /// <remarks>See <a href="http://tools.ietf.org/html/rfc2047#section-4.2">RFC 2047 section 4.2</a> for RFC details</remarks> private static byte[] Rfc2047QuotedPrintableDecode(string toDecode, bool encodedWordVariant) { if (toDecode == null) throw new ArgumentNullException("toDecode"); // Create a byte array builder which is roughly equivalent to a StringBuilder using (MemoryStream byteArrayBuilder = new MemoryStream()) { // Remove illegal control characters toDecode = RemoveIllegalControlCharacters(toDecode); // Run through the whole string that needs to be decoded for (int i = 0; i < toDecode.Length; i++) { char currentChar = toDecode[i]; if (currentChar == '=') { // Check that there is at least two characters behind the equal sign if (toDecode.Length - i < 3) { // We are at the end of the toDecode string, but something is missing. Handle it the way RFC 2045 states WriteAllBytesToStream(byteArrayBuilder, DecodeEqualSignNotLongEnough(toDecode.Substring(i))); // Since it was the last part, we should stop parsing anymore break; } // Decode the Quoted-Printable part string quotedPrintablePart = toDecode.Substring(i, 3); WriteAllBytesToStream(byteArrayBuilder, DecodeEqualSign(quotedPrintablePart)); // We now consumed two extra characters. Go forward two extra characters i += 2; } else { // This character is not quoted printable hex encoded. // Could it be the _ character, which represents space // and are we using the encoded word variant of QuotedPrintable if (currentChar == '_' && encodedWordVariant) { // The RFC specifies that the "_" always represents hexadecimal 20 even if the // SPACE character occupies a different code position in the character set in use. byteArrayBuilder.WriteByte(0x20); } else { // This is not encoded at all. This is a literal which should just be included into the output. byteArrayBuilder.WriteByte((byte)currentChar); } } } return byteArrayBuilder.ToArray(); } } /// <summary> /// Writes all bytes in a byte array to a stream /// </summary> /// <param name="stream">The stream to write to</param> /// <param name="toWrite">The bytes to write to the <paramref name="stream"/></param> private static void WriteAllBytesToStream(Stream stream, byte[] toWrite) { stream.Write(toWrite, 0, toWrite.Length); } /// <summary> /// RFC 2045 states about robustness:<br/> /// <code> /// Control characters other than TAB, or CR and LF as parts of CRLF pairs, /// must not appear. The same is true for octets with decimal values greater /// than 126. If found in incoming quoted-printable data by a decoder, a /// robust implementation might exclude them from the decoded data and warn /// the user that illegal characters were discovered. /// </code> /// Control characters are defined in RFC 2396 as<br/> /// <c>control = US-ASCII coded characters 00-1F and 7F hexadecimal</c> /// </summary> /// <param name="input">String to be stripped from illegal control characters</param> /// <returns>A string with no illegal control characters</returns> /// <exception cref="ArgumentNullException">If <paramref name="input"/> is <see langword="null"/></exception> private static string RemoveIllegalControlCharacters(string input) { if (input == null) throw new ArgumentNullException("input"); // First we remove any \r or \n which is not part of a \r\n pair input = RemoveCarriageReturnAndNewLinewIfNotInPair(input); // Here only legal \r\n is left over // We now simply keep them, and the \t which is also allowed // \x0A = \n // \x0D = \r // \x09 = \t) return Regex.Replace(input, "[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", ""); } /// <summary> /// This method will remove any \r and \n which is not paired as \r\n /// </summary> /// <param name="input">String to remove lonely \r and \n's from</param> /// <returns>A string without lonely \r and \n's</returns> /// <exception cref="ArgumentNullException">If <paramref name="input"/> is <see langword="null"/></exception> private static string RemoveCarriageReturnAndNewLinewIfNotInPair(string input) { if (input == null) throw new ArgumentNullException("input"); // Use this for building up the new string. This is used for performance instead // of altering the input string each time a illegal token is found StringBuilder newString = new StringBuilder(input.Length); for (int i = 0; i < input.Length; i++) { // There is a character after it // Check for lonely \r // There is a lonely \r if it is the last character in the input or if there // is no \n following it if (input[i] == '\r' && (i + 1 >= input.Length || input[i + 1] != '\n')) { // Illegal token \r found. Do not add it to the new string // Check for lonely \n // There is a lonely \n if \n is the first character or if there // is no \r in front of it } else if (input[i] == '\n' && (i - 1 < 0 || input[i - 1] != '\r')) { // Illegal token \n found. Do not add it to the new string } else { // No illegal tokens found. Simply insert the character we are at // in our new string newString.Append(input[i]); } } return newString.ToString(); } /// <summary> /// RFC 2045 says that a robust implementation should handle:<br/> /// <code> /// An "=" cannot be the ultimate or penultimate character in an encoded /// object. This could be handled as in case (2) above. /// </code> /// Case (2) is:<br/> /// <code> /// An "=" followed by a character that is neither a /// hexadecimal digit (including "abcdef") nor the CR character of a CRLF pair /// is illegal. This case can be the result of US-ASCII text having been /// included in a quoted-printable part of a message without itself having /// been subjected to quoted-printable encoding. A reasonable approach by a /// robust implementation might be to include the "=" character and the /// following character in the decoded data without any transformation and, if /// possible, indicate to the user that proper decoding was not possible at /// this point in the data. /// </code> /// </summary> /// <param name="decode"> /// The string to decode which cannot have length above or equal to 3 /// and must start with an equal sign. /// </param> /// <returns>A decoded byte array</returns> /// <exception cref="ArgumentNullException">If <paramref name="decode"/> is <see langword="null"/></exception> /// <exception cref="ArgumentException">Thrown if a the <paramref name="decode"/> parameter has length above 2 or does not start with an equal sign.</exception> private static byte[] DecodeEqualSignNotLongEnough(string decode) { if (decode == null) throw new ArgumentNullException("decode"); // We can only decode wrong length equal signs if (decode.Length >= 3) throw new ArgumentException("decode must have length lower than 3", "decode"); // First char must be = if (decode[0] != '=') throw new ArgumentException("First part of decode must be an equal sign", "decode"); // We will now believe that the string sent to us, was actually not encoded // Therefore it must be in US-ASCII and we will return the bytes it corrosponds to return Encoding.ASCII.GetBytes(decode); } /// <summary> /// This helper method will decode a string of the form "=XX" where X is any character.<br/> /// This method will never fail, unless an argument of length not equal to three is passed. /// </summary> /// <param name="decode">The length 3 character that needs to be decoded</param> /// <returns>A decoded byte array</returns> /// <exception cref="ArgumentNullException">If <paramref name="decode"/> is <see langword="null"/></exception> /// <exception cref="ArgumentException">Thrown if a the <paramref name="decode"/> parameter does not have length 3 or does not start with an equal sign.</exception> private static byte[] DecodeEqualSign(string decode) { if (decode == null) throw new ArgumentNullException("decode"); // We can only decode the string if it has length 3 - other calls to this function is invalid if (decode.Length != 3) throw new ArgumentException("decode must have length 3", "decode"); // First char must be = if (decode[0] != '=') throw new ArgumentException("decode must start with an equal sign", "decode"); // There are two cases where an equal sign might appear // It might be a // - hex-string like =3D, denoting the character with hex value 3D // - it might be the last character on the line before a CRLF // pair, denoting a soft linebreak, which simply // splits the text up, because of the 76 chars per line restriction if (decode.Contains("\r\n")) { // Soft break detected // We want to return string.Empty which is equivalent to a zero-length byte array return new byte[0]; } // Hex string detected. Convertion needed. // It might be that the string located after the equal sign is not hex characters // An example: =JU // In that case we would like to catch the FormatException and do something else try { // The number part of the string is the last two digits. Here we simply remove the equal sign string numberString = decode.Substring(1); // Now we create a byte array with the converted number encoded in the string as a hex value (base 16) // This will also handle illegal encodings like =3d where the hex digits are not uppercase, // which is a robustness requirement from RFC 2045. byte[] oneByte = new[] { Convert.ToByte(numberString, 16) }; // Simply return our one byte byte array return oneByte; } catch (FormatException) { // RFC 2045 says about robust implementation: // An "=" followed by a character that is neither a // hexadecimal digit (including "abcdef") nor the CR // character of a CRLF pair is illegal. This case can be // the result of US-ASCII text having been included in a // quoted-printable part of a message without itself // having been subjected to quoted-printable encoding. A // reasonable approach by a robust implementation might be // to include the "=" character and the following // character in the decoded data without any // transformation and, if possible, indicate to the user // that proper decoding was not possible at this point in // the data. // So we choose to believe this is actually an un-encoded string // Therefore it must be in US-ASCII and we will return the bytes it corrosponds to return Encoding.ASCII.GetBytes(decode); } } } } namespace OpenPop.Mime.Decode { using System; using System.Collections.Generic; using System.Globalization; using System.Text; /// <summary> /// Utility class used by OpenPop for mapping from a characterSet to an <see cref="Encoding"/>.<br/> /// <br/> /// The functionality of the class can be altered by adding mappings /// using <see cref="AddMapping"/> and by adding a <see cref="FallbackDecoder"/>.<br/> /// <br/> /// Given a characterSet, it will try to find the Encoding as follows: /// <list type="number"> /// <item> /// <description>If a mapping for the characterSet was added, use the specified Encoding from there. Mappings can be added using <see cref="AddMapping"/>.</description> /// </item> /// <item> /// <description>Try to parse the characterSet and look it up using <see cref="Encoding.GetEncoding(int)"/> for codepages or <see cref="Encoding.GetEncoding(string)"/> for named encodings.</description> /// </item> /// <item> /// <description>If an encoding is not found yet, use the <see cref="FallbackDecoder"/> if defined. The <see cref="FallbackDecoder"/> is user defined.</description> /// </item> /// </list> /// </summary> public static class EncodingFinder { /// <summary> /// Delegate that is used when the EncodingFinder is unable to find an encoding by /// using the <see cref="EncodingFinder.EncodingMap"/> or general code.<br/> /// This is used as a last resort and can be used for setting a default encoding or /// for finding an encoding on runtime for some <paramref name="characterSet"/>. /// </summary> /// <param name="characterSet">The character set to find an encoding for.</param> /// <returns>An encoding for the <paramref name="characterSet"/> or <see langword="null"/> if none could be found.</returns> public delegate Encoding FallbackDecoderDelegate(string characterSet); /// <summary> /// Last resort decoder. <seealso cref="FallbackDecoderDelegate"/>. /// </summary> public static FallbackDecoderDelegate FallbackDecoder { private get; set; } /// <summary> /// Mapping from charactersets to encodings. /// </summary> private static Dictionary<string, Encoding> EncodingMap { get; set; } /// <summary> /// Initialize the EncodingFinder /// </summary> static EncodingFinder() { Reset(); } /// <summary> /// Used to reset this static class to facilite isolated unit testing. /// </summary> internal static void Reset() { EncodingMap = new Dictionary<string, Encoding>(); FallbackDecoder = null; // Some emails incorrectly specify the encoding as utf8, but it should have been utf-8. AddMapping("utf8", Encoding.UTF8); } /// <summary> /// Parses a character set into an encoding. /// </summary> /// <param name="characterSet">The character set to parse</param> /// <returns>An encoding which corresponds to the character set</returns> /// <exception cref="ArgumentNullException">If <paramref name="characterSet"/> is <see langword="null"/></exception> internal static Encoding FindEncoding(string characterSet) { if (characterSet == null) throw new ArgumentNullException("characterSet"); string charSetUpper = characterSet.ToUpperInvariant(); // Check if the characterSet is explicitly mapped to an encoding if (EncodingMap.ContainsKey(charSetUpper)) return EncodingMap[charSetUpper]; // Try to find the generally find the encoding try { if (charSetUpper.Contains("WINDOWS") || charSetUpper.Contains("CP")) { // It seems the characterSet contains an codepage value, which we should use to parse the encoding charSetUpper = charSetUpper.Replace("CP", ""); // Remove cp charSetUpper = charSetUpper.Replace("WINDOWS", ""); // Remove windows charSetUpper = charSetUpper.Replace("-", ""); // Remove - which could be used as cp-1554 // Now we hope the only thing left in the characterSet is numbers. int codepageNumber = int.Parse(charSetUpper, CultureInfo.InvariantCulture); return Encoding.GetEncoding(codepageNumber); } // It seems there is no codepage value in the characterSet. It must be a named encoding return Encoding.GetEncoding(characterSet); } catch (ArgumentException) { // The encoding could not be found generally. // Try to use the FallbackDecoder if it is defined. // Check if it is defined if (FallbackDecoder == null) throw; // It was not defined - throw catched exception // Use the FallbackDecoder Encoding fallbackDecoderResult = FallbackDecoder(characterSet); // Check if the FallbackDecoder had a solution if (fallbackDecoderResult != null) return fallbackDecoderResult; // If no solution was found, throw catched exception throw; } } /// <summary> /// Puts a mapping from <paramref name="characterSet"/> to <paramref name="encoding"/> /// into the <see cref="EncodingFinder"/>'s internal mapping Dictionary. /// </summary> /// <param name="characterSet">The string that maps to the <paramref name="encoding"/></param> /// <param name="encoding">The <see cref="Encoding"/> that should be mapped from <paramref name="characterSet"/></param> /// <exception cref="ArgumentNullException">If <paramref name="characterSet"/> is <see langword="null"/></exception> /// <exception cref="ArgumentNullException">If <paramref name="encoding"/> is <see langword="null"/></exception> public static void AddMapping(string characterSet, Encoding encoding) { if (characterSet == null) throw new ArgumentNullException("characterSet"); if (encoding == null) throw new ArgumentNullException("encoding"); // Add the mapping using uppercase EncodingMap.Add(characterSet.ToUpperInvariant(), encoding); } } } namespace OpenPop.Mime.Decode { using System; using System.Text; using System.Text.RegularExpressions; using OpenPop.Mime.Header; /// <summary> /// Utility class for dealing with encoded word strings<br/> /// <br/> /// EncodedWord encoded strings are only in ASCII, but can embed information /// about characters in other character sets.<br/> /// <br/> /// It is done by specifying the character set, an encoding that maps from ASCII to /// the correct bytes and the actual encoded string.<br/> /// <br/> /// It is specified in a format that is best summarized by a BNF:<br/> /// <c>"=?" character_set "?" encoding "?" encoded-text "?="</c><br/> /// </summary> /// <example> /// <c>=?ISO-8859-1?Q?=2D?=</c> /// Here <c>ISO-8859-1</c> is the character set.<br/> /// <c>Q</c> is the encoding method (quoted-printable). <c>B</c> is also supported (Base 64).<br/> /// The encoded text is the <c>=2D</c> part which is decoded to a space. /// </example> internal static class EncodedWord { /// <summary> /// Decode text that is encoded with the <see cref="EncodedWord"/> encoding.<br/> ///<br/> /// This method will decode any encoded-word found in the string.<br/> /// All parts which is not encoded will not be touched.<br/> /// <br/> /// From <a href="http://tools.ietf.org/html/rfc2047">RFC 2047</a>:<br/> /// <code> /// Generally, an "encoded-word" is a sequence of printable ASCII /// characters that begins with "=?", ends with "?=", and has two "?"s in /// between. It specifies a character set and an encoding method, and /// also includes the original text encoded as graphic ASCII characters, /// according to the rules for that encoding method. /// </code> /// Example:<br/> /// <c>=?ISO-8859-1?q?this=20is=20some=20text?= other text here</c> /// </summary> /// <remarks>See <a href="http://tools.ietf.org/html/rfc2047#section-2">RFC 2047 section 2</a> "Syntax of encoded-words" for more details</remarks> /// <param name="encodedWords">Source text. May be content which is not encoded.</param> /// <returns>Decoded text</returns> /// <exception cref="ArgumentNullException">If <paramref name="encodedWords"/> is <see langword="null"/></exception> public static string Decode(string encodedWords) { if (encodedWords == null) throw new ArgumentNullException("encodedWords"); // Notice that RFC2231 redefines the BNF to // encoded-word := "=?" charset ["*" language] "?" encoded-text "?=" // but no usage of this BNF have been spotted yet. It is here to // ease debugging if such a case is discovered. // This is the regex that should fit the BNF // RFC Says that NO WHITESPACE is allowed in this encoding, but there are examples // where whitespace is there, and therefore this regex allows for such. const string encodedWordRegex = @"\=\?(?<Charset>\S+?)\?(?<Encoding>\w)\?(?<Content>.+?)\?\="; // \w Matches any word character including underscore. Equivalent to "[A-Za-z0-9_]". // \S Matches any nonwhite space character. Equivalent to "[^ \f\n\r\t\v]". // +? non-gready equivalent to + // (?<NAME>REGEX) is a named group with name NAME and regular expression REGEX // Any amount of linear-space-white between 'encoded-word's, // even if it includes a CRLF followed by one or more SPACEs, // is ignored for the purposes of display. // http://tools.ietf.org/html/rfc2047#page-12 // Define a regular expression that captures two encoded words with some whitespace between them const string replaceRegex = @"(?<first>" + encodedWordRegex + @")\s+(?<second>" + encodedWordRegex + ")"; // Then, find an occourance of such an expression, but remove the whitespace inbetween when found encodedWords = Regex.Replace(encodedWords, replaceRegex, "${first}${second}"); string decodedWords = encodedWords; MatchCollection matches = Regex.Matches(encodedWords, encodedWordRegex); foreach (Match match in matches) { // If this match was not a success, we should not use it if (!match.Success) continue; string fullMatchValue = match.Value; string encodedText = match.Groups["Content"].Value; string encoding = match.Groups["Encoding"].Value; string charset = match.Groups["Charset"].Value; // Get the encoding which corrosponds to the character set Encoding charsetEncoding = EncodingFinder.FindEncoding(charset); // Store decoded text here when done string decodedText; // Encoding may also be written in lowercase switch (encoding.ToUpperInvariant()) { // RFC: // The "B" encoding is identical to the "BASE64" // encoding defined by RFC 2045. // http://tools.ietf.org/html/rfc2045#section-6.8 case "B": decodedText = Base64.Decode(encodedText, charsetEncoding); break; // RFC: // The "Q" encoding is similar to the "Quoted-Printable" content- // transfer-encoding defined in RFC 2045. // There are more details to this. Please check // http://tools.ietf.org/html/rfc2047#section-4.2 // case "Q": decodedText = QuotedPrintable.DecodeEncodedWord(encodedText, charsetEncoding); break; default: throw new ArgumentException("The encoding " + encoding + " was not recognized"); } // Repalce our encoded value with our decoded value decodedWords = decodedWords.Replace(fullMatchValue, decodedText); } return decodedWords; } } } namespace OpenPop.Mime.Decode { using System; using System.Text; using OpenPop.Common.Logging; /// <summary> /// Utility class for dealing with Base64 encoded strings /// </summary> internal static class Base64 { /// <summary> /// Decodes a base64 encoded string into the bytes it describes /// </summary> /// <param name="base64Encoded">The string to decode</param> /// <returns>A byte array that the base64 string described</returns> public static byte[] Decode(string base64Encoded) { try { return Convert.FromBase64String(base64Encoded); } catch (FormatException e) { DefaultLogger.Log.LogError("Base64: (FormatException) " + e.Message + "\r\nOn string: " + base64Encoded); throw; } } /// <summary> /// Decodes a Base64 encoded string using a specified <see cref="System.Text.Encoding"/> /// </summary> /// <param name="base64Encoded">Source string to decode</param> /// <param name="encoding">The encoding to use for the decoded byte array that <paramref name="base64Encoded"/> describes</param> /// <returns>A decoded string</returns> /// <exception cref="ArgumentNullException">If <paramref name="base64Encoded"/> or <paramref name="encoding"/> is <see langword="null"/></exception> /// <exception cref="FormatException">If <paramref name="base64Encoded"/> is not a valid base64 encoded string</exception> public static string Decode(string base64Encoded, Encoding encoding) { if (base64Encoded == null) throw new ArgumentNullException("base64Encoded"); if (encoding == null) throw new ArgumentNullException("encoding"); return encoding.GetString(Decode(base64Encoded)); } } } namespace OpenPop.Common { using System; using System.IO; using System.Text; /// <summary> /// Utility to help reading bytes and strings of a <see cref="Stream"/> /// </summary> internal static class StreamUtility { /// <summary> /// Read a line from the stream. /// A line is interpreted as all the bytes read until a CRLF or LF is encountered.<br/> /// CRLF pair or LF is not included in the string. /// </summary> /// <param name="stream">The stream from which the line is to be read</param> /// <returns>A line read from the stream returned as a byte array or <see langword="null"/> if no bytes were readable from the stream</returns> /// <exception cref="ArgumentNullException">If <paramref name="stream"/> is <see langword="null"/></exception> public static byte[] ReadLineAsBytes(Stream stream) { if (stream == null) throw new ArgumentNullException("stream"); using (MemoryStream memoryStream = new MemoryStream()) { while (true) { int justRead = stream.ReadByte(); if (justRead == -1 && memoryStream.Length > 0) break; // Check if we started at the end of the stream we read from // and we have not read anything from it yet if (justRead == -1 && memoryStream.Length == 0) return null; char readChar = (char)justRead; // Do not write \r or \n if (readChar != '\r' && readChar != '\n') memoryStream.WriteByte((byte)justRead); // Last point in CRLF pair if (readChar == '\n') break; } return memoryStream.ToArray(); } } /// <summary> /// Read a line from the stream. <see cref="ReadLineAsBytes"/> for more documentation. /// </summary> /// <param name="stream">The stream to read from</param> /// <returns>A line read from the stream or <see langword="null"/> if nothing could be read from the stream</returns> /// <exception cref="ArgumentNullException">If <paramref name="stream"/> is <see langword="null"/></exception> public static string ReadLineAsAscii(Stream stream) { byte[] readFromStream = ReadLineAsBytes(stream); return readFromStream != null ? Encoding.ASCII.GetString(readFromStream) : null; } } } namespace OpenPop.Common.Logging { /// <summary> /// Defines a logger for managing system logging output /// </summary> public interface ILog { /// <summary> /// Logs an error message to the logs /// </summary> /// <param name="message">This is the error message to log</param> void LogError(string message); /// <summary> /// Logs a debug message to the logs /// </summary> /// <param name="message">This is the debug message to log</param> void LogDebug(string message); } } namespace OpenPop.Common.Logging { using System; using System.IO; /// <summary> /// This logging object writes application error and debug output to a text file. /// </summary> public class FileLogger : ILog { #region File Logging /// <summary> /// Lock object to prevent thread interactions /// </summary> private static readonly object LogLock; /// <summary> /// Static constructor /// </summary> static FileLogger() { // Default log file is defined here LogFile = new FileInfo("OpenPOP.log"); Enabled = true; Verbose = false; LogLock = new object(); } /// <summary> /// Turns the logging on and off. /// </summary> public static bool Enabled { get; set; } /// <summary> /// Enables or disables the output of Debug level log messages /// </summary> public static bool Verbose { get; set; } /// <summary> /// The file to which log messages will be written /// </summary> /// <remarks>This property defaults to OpenPOP.log.</remarks> public static FileInfo LogFile { get; set; } /// <summary> /// Write a message to the log file /// </summary> /// <param name="text">The error text to log</param> private static void LogToFile(string text) { if (text == null) throw new ArgumentNullException("text"); // We want to open the file and append some text to it lock (LogLock) { using (StreamWriter sw = LogFile.AppendText()) { sw.WriteLine(DateTime.Now + " " + text); sw.Flush(); } } } #endregion #region ILog Implementation /// <summary> /// Logs an error message to the logs /// </summary> /// <param name="message">This is the error message to log</param> public void LogError(string message) { if (Enabled) LogToFile(message); } /// <summary> /// Logs a debug message to the logs /// </summary> /// <param name="message">This is the debug message to log</param> public void LogDebug(string message) { if (Enabled && Verbose) LogToFile("DEBUG: " + message); } #endregion } } namespace OpenPop.Common.Logging { using System; /// <summary> /// This logging object writes application error and debug output using the /// <see cref="System.Diagnostics.Trace"/> facilities. /// </summary> public class DiagnosticsLogger : ILog { /// <summary> /// Logs an error message to the System Trace facility /// </summary> /// <param name="message">This is the error message to log</param> public void LogError(string message) { if (message == null) throw new ArgumentNullException("message"); System.Diagnostics.Trace.WriteLine("OpenPOP: " + message); } /// <summary> /// Logs a debug message to the system Trace Facility /// </summary> /// <param name="message">This is the debug message to log</param> public void LogDebug(string message) { if (message == null) throw new ArgumentNullException("message"); System.Diagnostics.Trace.WriteLine("OpenPOP: (DEBUG) " + message); } } } namespace OpenPop.Common.Logging { using System; /// <summary> /// This is the log that all logging will go trough. /// </summary> public static class DefaultLogger { /// <summary> /// This is the logger used by all logging methods in the assembly.<br/> /// You can override this if you want, to move logging to one of your own /// logging implementations.<br/> /// <br/> /// By default a <see cref="DiagnosticsLogger"/> is used. /// </summary> public static ILog Log { get; private set; } static DefaultLogger() { Log = new DiagnosticsLogger(); } /// <summary> /// Changes the default logging to log to a new logger /// </summary> /// <param name="newLogger">The new logger to use to send log messages to</param> /// <exception cref="ArgumentNullException"> /// Never set this to <see langword="null"/>.<br/> /// Instead you should implement a NullLogger which just does nothing. /// </exception> public static void SetLog(ILog newLogger) { if (newLogger == null) throw new ArgumentNullException("newLogger"); Log = newLogger; } } } |