You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
12 KiB

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Net;
using System.Net.Mime;
using MsgReader.Mime.Header;
using MsgReader.Mime.Traverse;
namespace MsgReader.Mime
{
/// <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>
public class Message
{
#region Properties
/// <summary>
/// Returns the ID of the message when this is available in the <see cref="Headers"/>
/// (as specified in [RFC2822]). Null when not available
/// </summary>
public string Id
{
get
{
if (Headers == null)
return null;
return !string.IsNullOrEmpty(Headers.MessageId) ? Headers.MessageId : null;
}
}
/// <summary>
/// Headers of the Message.
/// </summary>
public MessageHeader Headers { get; }
/// <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; }
/// <summary>
/// This will return the first <see cref="MessagePart"/> where the <see cref="ContentType.MediaType"/>
/// is set to "html/text". This will return <see langword="null"/> when there is no "html/text"
/// <see cref="MessagePart"/> found.
/// </summary>
public MessagePart HtmlBody { get; }
/// <summary>
/// This will return the first <see cref="MessagePart"/> where the <see cref="ContentType.MediaType"/>
/// is set to "text/plain". This will be <see langword="null"/> when there is no "text/plain"
/// <see cref="MessagePart"/> found.
/// </summary>
public MessagePart TextBody { get; }
/// <summary>
/// This will return all the <see cref="MessagePart">message parts</see> that are flagged as
/// <see cref="Mime.MessagePart.IsAttachment"/>.
/// This will be <see langword="null"/> when there are no <see cref="MessagePart">message parts</see>
/// that are flagged as <see cref="Mime.MessagePart.IsAttachment"/>.
/// </summary>
public ReadOnlyCollection<MessagePart> Attachments { get; }
/// <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; }
#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
HeaderExtractor.ExtractHeadersAndBody(rawMessageContent, out var headersTemp, out var 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);
var findBodyMessagePartWithMediaType = new FindBodyMessagePartWithMediaType();
// Searches for the first HTML body and mark this one as the HTML body of the E-mail
HtmlBody = findBodyMessagePartWithMediaType.VisitMessage(this, "text/html");
if (HtmlBody != null)
HtmlBody.IsHtmlBody = true;
// Searches for the first TEXT body and mark this one as the TEXT body of the E-mail
TextBody = findBodyMessagePartWithMediaType.VisitMessage(this, "text/plain");
if (TextBody != null)
TextBody.IsTextBody = true;
var attachments = new AttachmentFinder().VisitMessage(this);
if (HtmlBody != null)
{
foreach (var attachment in attachments)
{
if (attachment.IsInline) continue;
var htmlBody = HtmlBody.BodyEncoding.GetString(HtmlBody.Body);
attachment.IsInline = htmlBody.Contains($"cid:{attachment.ContentId}");
}
}
if (attachments != null)
Attachments = attachments.AsReadOnly();
}
}
#endregion
#region GetEmailAddresses
/// <summary>
/// Returns the list of <see cref="RfcMailAddress"/> as a normal or html string
/// </summary>
/// <param name="rfcMailAddresses">A list with one or more <see cref="RfcMailAddress"/> objects</param>
/// <param name="convertToHref">When true the E-mail addresses are converted to hyperlinks</param>
/// <param name="html">Set this to true when the E-mail body format is html</param>
/// <returns></returns>
public string GetEmailAddresses(IEnumerable<RfcMailAddress> rfcMailAddresses, bool convertToHref, bool html)
{
var result = string.Empty;
if (rfcMailAddresses == null)
return result;
foreach (var rfcMailAddress in rfcMailAddresses)
{
if (result != string.Empty)
result += "; ";
var emailAddress = string.Empty;
var displayName = rfcMailAddress.DisplayName;
if (rfcMailAddress.HasValidMailAddress)
emailAddress = rfcMailAddress.Address;
if (string.Equals(emailAddress, displayName, StringComparison.InvariantCultureIgnoreCase))
displayName = string.Empty;
if (html)
{
emailAddress = WebUtility.HtmlEncode(emailAddress);
displayName = WebUtility.HtmlEncode(displayName);
}
if (convertToHref && html && !string.IsNullOrEmpty(emailAddress))
result += "<a href=\"mailto:" + emailAddress + "\">" +
(!string.IsNullOrEmpty(displayName)
? displayName
: emailAddress) + "</a>";
else
{
if (!string.IsNullOrEmpty(displayName))
result += displayName;
var beginTag = string.Empty;
var endTag = string.Empty;
if (!string.IsNullOrEmpty(displayName))
{
if (html)
{
beginTag = "&nbsp;&lt;";
endTag = "&gt;";
}
else
{
beginTag = " <";
endTag = ">";
}
}
if (!string.IsNullOrEmpty(emailAddress))
result += beginTag + emailAddress + endTag;
}
}
return result;
}
#endregion
#region Save
/// <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(nameof(file));
using (var fileStream = new FileStream(file.FullName, FileMode.Create))
Save(fileStream);
}
/// <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(nameof(messageStream));
messageStream.Write(RawMessage, 0, RawMessage.Length);
}
#endregion
#region Load
/// <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(nameof(file));
if (!file.Exists)
throw new FileNotFoundException("Cannot load message from non-existent file", file.FullName);
using (var fileStream = new FileStream(file.FullName, FileMode.Open))
return Load(fileStream);
}
/// <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(nameof(messageStream));
using (var memoryStream = new MemoryStream())
{
messageStream.CopyTo(memoryStream);
var content = memoryStream.ToArray();
return new Message(content);
}
}
#endregion
}
}