using System; using System.Collections.Generic; using System.Linq; using System.Net.Mail; using System.Net.Mime; using MsgReader.Mime.Decode; namespace MsgReader.Mime.Header { /// /// Class that can parse different fields in the header sections of a MIME message. /// internal static class HeaderFieldParser { #region ParseContentTransferEncoding /// /// Parses the Content-Transfer-Encoding header. /// /// The value for the header to be parsed /// A /// If is /// If the could not be parsed to a public static ContentTransferEncoding ParseContentTransferEncoding(string headerValue) { if (headerValue == null) throw new ArgumentNullException(nameof(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: return ContentTransferEncoding.SevenBit; } } #endregion #region ParseImportance /// /// Parses an ImportanceType from a given Importance header value. /// /// The value to be parsed /// A . If the is not recognized, Normal is returned. /// If is public static MailPriority ParseImportance(string headerValue) { if (headerValue == null) throw new ArgumentNullException(nameof(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: return MailPriority.Normal; } } #endregion #region ParseContentType /// /// Parses a the value for the header Content-Type to /// a object. /// /// The value to be parsed /// A object /// If is public static ContentType ParseContentType(string headerValue) { if (headerValue == null) throw new ArgumentNullException(nameof(headerValue)); // We create an empty Content-Type which we will fill in when we see the values var contentType = new ContentType(); // Now decode the parameters var parameters = Rfc2231Decoder.Decode(headerValue); foreach (var keyValuePair in parameters) { var key = keyValuePair.Key.ToUpperInvariant().Trim(); var 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 var v = value.ToUpperInvariant().Trim('\0'); if (v.Equals("TEXT") || v.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; } #endregion #region ParseContentDisposition /// /// Parses a the value for the header Content-Disposition to a object. /// /// The value to be parsed /// A object /// If is public static ContentDisposition ParseContentDisposition(string headerValue) { if (headerValue == null) throw new ArgumentNullException(nameof(headerValue)); // See http://www.ietf.org/rfc/rfc2183.txt for RFC definition // Create empty ContentDisposition - we will fill in details as we read them var contentDisposition = new ContentDisposition(); // Now decode the parameters var parameters = Rfc2231Decoder.Decode(headerValue); foreach (var keyValuePair in parameters) { var key = keyValuePair.Key.ToUpperInvariant().Trim(); var value = Utility.RemoveQuotesIfAny(keyValuePair.Value.Trim()); 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(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 var creationDate = new DateTime(Rfc2822DateTime.StringToDate(value).Ticks); contentDisposition.CreationDate = creationDate; break; case "MODIFICATION-DATE": var midificationDate = new DateTime(Rfc2822DateTime.StringToDate(value).Ticks); contentDisposition.ModificationDate = midificationDate; break; case "READ-DATE": var readDate = new DateTime(Rfc2822DateTime.StringToDate(value).Ticks); contentDisposition.ReadDate = readDate; break; case "SIZE": contentDisposition.Size = SizeParser.Parse(value); break; case "CHARSET": // ignoring invalid parameter in Content-Disposition case "VOICE": break; default: if (!key.StartsWith("X-")) throw new ArgumentException( "Unknown parameter in Content-Disposition. Ask developer to fix! Parameter: " + key); contentDisposition.Parameters.Add(key, value); break; } } return contentDisposition; } #endregion #region ParseId /// /// Parses an ID like Message-Id and Content-Id.
/// Example:
/// <test@test.com>
/// into
/// test@test.com ///
/// The id to parse /// A parsed ID 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('<'); } #endregion #region ParseMultipleIDs /// /// Parses multiple IDs from a single string like In-Reply-To. /// /// The value to parse /// A list of IDs public static List ParseMultipleIDs(string headerValue) { // Split the string by > // We cannot use ' ' (space) here since this is a possible value: // var ids = headerValue.Trim().Split(new[] {'>'}, StringSplitOptions.RemoveEmptyEntries); return ids.Select(ParseId).ToList(); } #endregion } }