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