// // Message.cs // // Author: Kees van Spelde // // Copyright (c) 2013-2018 Magic-Sessions. (www.magic-sessions.com) // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using System.Text; using MsgReader.Exceptions; using MsgReader.Helpers; using MsgReader.Localization; using MsgReader.Mime.Header; using OpenMcdf; namespace MsgReader.Outlook { #region Enum MessageType /// /// The message types /// public enum MessageType { /// /// The message type is unknown /// Unknown, /// /// The message is a normal E-mail /// Email, /// /// Non-delivery report for a standard E-mail (REPORT.IPM.NOTE.NDR) /// EmailNonDeliveryReport, /// /// Delivery receipt for a standard E-mail (REPORT.IPM.NOTE.DR) /// EmailDeliveryReport, /// /// Delivery receipt for a delayed E-mail (REPORT.IPM.NOTE.DELAYED) /// EmailDelayedDeliveryReport, /// /// Read receipt for a standard E-mail (REPORT.IPM.NOTE.IPNRN) /// EmailReadReceipt, /// /// Non-read receipt for a standard E-mail (REPORT.IPM.NOTE.IPNNRN) /// EmailNonReadReceipt, /// /// The message in an E-mail that is encrypted and can also be signed (IPM.Note.SMIME) /// EmailEncryptedAndMaybeSigned, /// /// Non-delivery report for a Secure MIME (S/MIME) encrypted and opaque-signed E-mail (REPORT.IPM.NOTE.SMIME.NDR) /// EmailEncryptedAndMaybeSignedNonDelivery, /// /// Delivery report for a Secure MIME (S/MIME) encrypted and opaque-signed E-mail (REPORT.IPM.NOTE.SMIME.DR) /// EmailEncryptedAndMaybeSignedDelivery, /// /// The message is an E-mail that is clear signed (IPM.Note.SMIME.MultipartSigned) /// EmailClearSigned, /// /// The message is a secure read receipt for an E-mail (IPM.Note.Receipt.SMIME) /// EmailClearSignedReadReceipt, /// /// Non-delivery report for an S/MIME clear-signed E-mail (REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.NDR) /// EmailClearSignedNonDelivery, /// /// Delivery receipt for an S/MIME clear-signed E-mail (REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.DR) /// EmailClearSignedDelivery, /// /// The message is an E-mail that is generared signed (IPM.Note.BMA.Stub) /// EmailBmaStub, /// /// The message is a short message service (IPM.Note.Mobile.SMS) /// EmailSms, /// /// The message is an appointment (IPM.Appointment) /// Appointment, /// /// The message is a notification for an appointment (IPM.Notification.Meeting) /// AppointmentNotification, /// /// The message is a schedule for an appointment (IPM.Schedule.Meeting) /// AppointmentSchedule, /// /// The message is a request for an appointment (IPM.Schedule.Meeting.Request) /// AppointmentRequest, /// /// The message is a request for an appointment (REPORT.IPM.SCHEDULE.MEETING.REQUEST.NDR) /// AppointmentRequestNonDelivery, /// /// The message is a response to an appointment (IPM.Schedule.Response) /// AppointmentResponse, /// /// The message is a positive response to an appointment (IPM.Schedule.Resp.Pos) /// AppointmentResponsePositive, /// /// Non-delivery report for a positive meeting response (accept) (REPORT.IPM.SCHEDULE.MEETING.RESP.POS.NDR) /// AppointmentResponsePositiveNonDelivery, /// /// The message is a negative response to an appointment (IPM.Schedule.Resp.Neg) /// AppointmentResponseNegative, /// /// Non-delivery report for a negative meeting response (declinet) (REPORT.IPM.SCHEDULE.MEETING.RESP.NEG.NDR) /// AppointmentResponseNegativeNonDelivery, /// /// The message is a response to tentatively accept the meeting request (IPM.Schedule.Meeting.Resp.Tent) /// AppointmentResponseTentative, /// /// Non-delivery report for a Tentative meeting response (REPORT.IPM.SCHEDULE.MEETING.RESP.TENT.NDR) /// AppointmentResponseTentativeNonDelivery, /// /// The message is a cancelation an appointment (IPM.Schedule.Meeting.Canceled) /// AppointmentResponseCanceled, /// /// Non-delivery report for a cancelled meeting notification (REPORT.IPM.SCHEDULE.MEETING.CANCELED.NDR) /// AppointmentResponseCanceledNonDelivery, /// /// The message is a contact card (IPM.Contact) /// Contact, /// /// The message is a task (IPM.Task) /// Task, /// /// The message is a task request accept (IPM.TaskRequest.Accept) /// TaskRequestAccept, /// /// The message is a task request decline (IPM.TaskRequest.Decline) /// TaskRequestDecline, /// /// The message is a task request update (IPM.TaskRequest.Update) /// TaskRequestUpdate, /// /// The message is a sticky note (IPM.StickyNote) /// StickyNote, /// /// The message is Cisco Unity Voice message (IPM.Note.Custom.Cisco.Unity.Voice) /// CiscoUnityVoiceMessage, /// /// IPM.NOTE.RIGHTFAX.ADV /// RightFaxAdv, /// /// The message is Skype for Business missed message (IPM.Note.Microsoft.Missed) /// SkypeForBusinessMissedMessage, /// /// The message is a Skype for Business conversation (IPM.Note.Microsoft.Conversation) /// SkypeForBusinessConversation } #endregion #region Enum MessageImportance /// /// The importancy of the message /// public enum MessageImportance { /// /// Low /// Low = 0, /// /// Normal /// Normal = 1, /// /// High /// High = 2 } #endregion public partial class Storage { /// /// Class represent a MSG object /// public class Message : Storage { #region Fields /// /// The name of the stream that contains this message /// internal string StorageName { get; } /// /// Contains the of this Message /// private MessageType _type = MessageType.Unknown; /// /// contains the name of the file /// private string _fileName; /// /// Contains the date and time when the message was created or null /// when not available /// private DateTime? _creationTime; /// /// Contains the name of the last user (or creator) that has changed the Message object or /// null when not available /// private string _lastModifierName; /// /// Contains the date and time when the message was created or null /// when not available /// private DateTime? _lastModificationTime; /// /// contains all the objects /// private readonly List _recipients = new List(); /// /// Contains an URL to the help page of a mailing list /// private string _mailingListHelp; /// /// Contains an URL to the subscribe page of a mailing list /// private string _mailingListSubscribe; /// /// Contains an URL to the unsubscribe page of a mailing list /// private string _mailingListUnsubscribe; /// /// Contains the date/time in UTC format when the object has been sent, /// null when not available /// private DateTime? _sentOn; /// /// Contains the date/time in UTC format when the object has been received, /// null when not available /// private DateTime? _receivedOn; /// /// Contains the of the object, /// null when not available /// private MessageImportance? _importance; /// /// Contains all the and objects. /// private readonly List _attachments = new List(); /// /// Contains the subject prefix of the object /// private string _subjectPrefix; /// /// Contains the subject of the object /// private string _subject; /// /// Contains the normalized subject of the object /// private string _subjectNormalized; /// /// Contains the text body of the object /// private string _bodyText; /// /// Contains the html body of the object /// private string _bodyHtml; /// /// Contains the rtf body of the object /// private string _bodyRtf; /// /// Contains the that is used for the or . /// It will contain null when the codepage could not be read from the /// private Encoding _internetCodepage; /// /// Contains the that is used for the . /// It will contain null when the codepage could not be read from the /// private Encoding _messageCodepage; /// /// Contains the the Windows LCID of the end user who created this /// private RegionInfo _messageLocalId; /// /// Contains the object /// private Flag _flag; /// /// Contains the object /// private Task _task; /// /// Contains the object /// private Appointment _appointment; /// /// Contains the object /// private Contact _contact; /// /// Contains the object /// private ReceivedBy _receivedBy; /// /// The conversation index /// private string _conversationIndex; /// /// The conversation topic /// private string _conversationTopic; /// /// The message size /// private int? _messageSize; /// /// The transport message headers /// private string _TransportMessageHeaders; #endregion #region Properties /// /// Returns the ID of the message when the MSG file has been sent across the internet /// (as specified in [RFC2822]). Null when not available /// public string Id => GetMapiPropertyString(MapiTags.PR_INTERNET_MESSAGE_ID); #region Type /// /// Gives the type of this message object /// public MessageType Type { get { if (_type != MessageType.Unknown) return _type; var type = GetMapiPropertyString(MapiTags.PR_MESSAGE_CLASS); if (type == null) return MessageType.Unknown; switch (type.ToUpperInvariant()) { case "IPM.NOTE": _type = MessageType.Email; break; case "IPM.NOTE.MOBILE.SMS": _type = MessageType.EmailSms; break; case "REPORT.IPM.NOTE.NDR": _type = MessageType.EmailNonDeliveryReport; break; case "REPORT.IPM.NOTE.DR": _type = MessageType.EmailDeliveryReport; break; case "REPORT.IPM.NOTE.DELAYED": _type = MessageType.EmailDelayedDeliveryReport; break; case "REPORT.IPM.NOTE.IPNRN": _type = MessageType.EmailReadReceipt; break; case "REPORT.IPM.NOTE.IPNNRN": _type = MessageType.EmailNonReadReceipt; break; case "IPM.NOTE.SMIME": _type = MessageType.EmailEncryptedAndMaybeSigned; break; case "REPORT.IPM.NOTE.SMIME.NDR": _type = MessageType.EmailEncryptedAndMaybeSignedNonDelivery; break; case "REPORT.IPM.NOTE.SMIME.DR": _type = MessageType.EmailEncryptedAndMaybeSignedDelivery; break; case "IPM.NOTE.SMIME.MULTIPARTSIGNED": _type = MessageType.EmailClearSigned; break; case "IPM.NOTE.RECEIPT.SMIME.MULTIPARTSIGNED": _type = MessageType.EmailClearSigned; break; case "REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.NDR": _type = MessageType.EmailClearSignedNonDelivery; break; case "REPORT.IPM.NOTE.SMIME.MULTIPARTSIGNED.DR": _type = MessageType.EmailClearSignedDelivery; break; case "IPM.NOTE.BMA.STUB": _type = MessageType.EmailBmaStub; break; case "IPM.APPOINTMENT": _type = MessageType.Appointment; break; case "IPM.SCHEDULE.MEETING": _type = MessageType.AppointmentSchedule; break; case "IPM.NOTIFICATION.MEETING": _type = MessageType.AppointmentNotification; break; case "IPM.SCHEDULE.MEETING.REQUEST": _type = MessageType.AppointmentRequest; break; case "IPM.SCHEDULE.MEETING.REQUEST.NDR": _type = MessageType.AppointmentRequestNonDelivery; break; case "IPM.SCHEDULE.MEETING.CANCELED": _type = MessageType.AppointmentResponseCanceled; break; case "IPM.SCHEDULE.MEETING.CANCELED.NDR": _type = MessageType.AppointmentResponseCanceledNonDelivery; break; case "IPM.SCHEDULE.MEETING.RESPONSE": _type = MessageType.AppointmentResponse; break; case "IPM.SCHEDULE.MEETING.RESP.POS": _type = MessageType.AppointmentResponsePositive; break; case "IPM.SCHEDULE.MEETING.RESP.POS.NDR": _type = MessageType.AppointmentResponsePositiveNonDelivery; break; case "IPM.SCHEDULE.MEETING.RESP.NEG": _type = MessageType.AppointmentResponseNegative; break; case "IPM.SCHEDULE.MEETING.RESP.NEG.NDR": _type = MessageType.AppointmentResponseNegativeNonDelivery; break; case "IPM.SCHEDULE.MEETING.RESP.TENT": _type = MessageType.AppointmentResponseTentative; break; case "IPM.SCHEDULE.MEETING.RESP.TENT.NDR": _type = MessageType.AppointmentResponseTentativeNonDelivery; break; case "IPM.CONTACT": _type = MessageType.Contact; break; case "IPM.TASK": _type = MessageType.Task; break; case "IPM.TASKREQUEST.ACCEPT": _type = MessageType.TaskRequestAccept; break; case "IPM.TASKREQUEST.DECLINE": _type = MessageType.TaskRequestDecline; break; case "IPM.TASKREQUEST.UPDATE": _type = MessageType.TaskRequestUpdate; break; case "IPM.STICKYNOTE": _type = MessageType.StickyNote; break; case "IPM.NOTE.CUSTOM.CISCO.UNITY.VOICE": _type = MessageType.CiscoUnityVoiceMessage; break; case "IPM.NOTE.RIGHTFAX.ADV": _type = MessageType.RightFaxAdv; break; case "IPM.NOTE.MICROSOFT.MISSED": _type = MessageType.SkypeForBusinessMissedMessage; break; case "IPM.NOTE.MICROSOFT.CONVERSATION": _type = MessageType.SkypeForBusinessConversation; break; } return _type; } } #endregion /// /// Returns the filename of the message object. For message objects Outlook uses the subject. It strips /// invalid filename characters. When there is no filename the name from /// will be used /// public string FileName { get { if (_fileName != null) return _fileName; _fileName = GetMapiPropertyString(MapiTags.PR_SUBJECT); if (string.IsNullOrEmpty(_fileName)) _fileName = LanguageConsts.NameLessFileName; _fileName = FileManager.RemoveInvalidFileNameChars(_fileName) + ".msg"; return _fileName; } } /// /// Returns the date and time when the message was created or null /// when not available /// public DateTime? CreationTime => _creationTime ?? (_creationTime = GetMapiPropertyDateTime(MapiTags.PR_CREATION_TIME)); /// /// Returns the name of the last user (or creator) that has changed the Message object or /// null when not available /// public string LastModifierName => _lastModifierName ?? (_lastModifierName = GetMapiPropertyString(MapiTags.PR_LAST_MODIFIER_NAME_W)); /// /// Returns the date and time when the message was last modified or null /// when not available /// public DateTime? LastModificationTime => _lastModificationTime ?? (_lastModificationTime = GetMapiPropertyDateTime(MapiTags.PR_LAST_MODIFICATION_TIME)); /// /// Returns the raw Transport Message Headers /// public string TransportMessageHeaders => _TransportMessageHeaders; /// /// Returns the sender of the Message /// // ReSharper disable once CSharpWarnings::CS0109 public new Sender Sender { get; private set; } /// /// Returns the representing sender of the Message, null when not available /// // ReSharper disable once CSharpWarnings::CS0109 public new SenderRepresenting SenderRepresenting { get; private set; } /// /// Returns the list of recipients in the message object /// public List Recipients => _recipients; /// /// Returns an URL to the help page of an mailing list when this message is part of a mailing /// or null when not available /// public string MailingListHelp { get { if (!string.IsNullOrEmpty(_mailingListHelp)) return _mailingListHelp; _mailingListHelp = GetMapiPropertyString(MapiTags.PR_LIST_HELP); if (_mailingListHelp == null) return null; if (_mailingListHelp.StartsWith("<")) _mailingListHelp = _mailingListHelp.Substring(1); if (_mailingListHelp.EndsWith(">")) _mailingListHelp = _mailingListHelp.Substring(0, _mailingListHelp.Length - 1); return _mailingListHelp; } } /// /// Returns an URL to the subscribe page of an mailing list when this message is part of a mailing /// or null when not available /// public string MailingListSubscribe { get { if (!string.IsNullOrEmpty(_mailingListSubscribe)) return _mailingListSubscribe; _mailingListSubscribe = GetMapiPropertyString(MapiTags.PR_LIST_SUBSCRIBE); if (_mailingListSubscribe == null) return null; if (_mailingListSubscribe.StartsWith("<")) _mailingListSubscribe = _mailingListSubscribe.Substring(1); if (_mailingListSubscribe.EndsWith(">")) _mailingListSubscribe = _mailingListSubscribe.Substring(0, _mailingListSubscribe.Length - 1); return _mailingListSubscribe; } } /// /// Returns an URL to the unsubscribe page of an mailing list when this message is part of a mailing /// public string MailingListUnsubscribe { get { if (!string.IsNullOrEmpty(_mailingListUnsubscribe)) return _mailingListUnsubscribe; _mailingListUnsubscribe = GetMapiPropertyString(MapiTags.PR_LIST_UNSUBSCRIBE); if (_mailingListUnsubscribe == null) return null; if (_mailingListUnsubscribe.StartsWith("<")) _mailingListUnsubscribe = _mailingListUnsubscribe.Substring(1); if (_mailingListUnsubscribe.EndsWith(">")) _mailingListUnsubscribe = _mailingListUnsubscribe.Substring(0, _mailingListUnsubscribe.Length - 1); return _mailingListUnsubscribe; } } /// /// Returns the date/time in UTC format when the message object has been sent, null when not available /// public DateTime? SentOn { get { if (_sentOn != null) return _sentOn; _sentOn = GetMapiPropertyDateTime(MapiTags.PR_CLIENT_SUBMIT_TIME) ?? GetMapiPropertyDateTime(MapiTags.PR_PROVIDER_SUBMIT_TIME); if (_sentOn == null && Headers != null) _sentOn = Headers.DateSent.ToLocalTime(); return _sentOn; } } /// /// PR_MESSAGE_DELIVERY_TIME is the time that the message was delivered to the store and /// PR_CLIENT_SUBMIT_TIME is the time when the message was sent by the client (Outlook) to the server. /// Now in this case when the Outlook is offline, it refers to the local store. Therefore when an email is sent, /// it gets submitted to the local store and PR_MESSAGE_DELIVERY_TIME gets set the that time. Once the Outlook is /// online at that point the message gets submitted by the client to the server and the PR_CLIENT_SUBMIT_TIME gets stamped. /// Null when not available /// public DateTime? ReceivedOn { get { if (_receivedOn != null) return _receivedOn; _receivedOn = GetMapiPropertyDateTime(MapiTags.PR_MESSAGE_DELIVERY_TIME); if (_receivedOn == null && Headers?.Received != null && Headers.Received.Count > 0) _receivedOn = Headers.Received[0].Date.ToLocalTime(); return _receivedOn; } } /// /// Returns the of the object, null when not available /// public MessageImportance? Importance { get { if (_importance != null) return _importance; var importance = GetMapiPropertyInt32(MapiTags.PR_IMPORTANCE); if (importance == null) { _importance = MessageImportance.Normal; return _importance; } switch (importance) { case 0: _importance = MessageImportance.Low; break; case 1: _importance = MessageImportance.Normal; break; case 2: _importance = MessageImportance.High; break; } return _importance; } } /// /// Returns the of the object object as text /// public string ImportanceText { get { if (Importance == null) return LanguageConsts.ImportanceNormalText; switch (Importance) { case MessageImportance.Low: return LanguageConsts.ImportanceLowText; case MessageImportance.Normal: return LanguageConsts.ImportanceNormalText; case MessageImportance.High: return LanguageConsts.ImportanceHighText; } return LanguageConsts.ImportanceNormalText; } } /// /// Returns a list with and/or /// objects that are attachted to the object /// public List Attachments => _attachments; /// /// Returns the rendering position of this object when it was added to another /// object and the body type was set to RTF /// public int RenderingPosition { get; } /// /// Returns or sets the subject prefix of the E-mail /// public string SubjectPrefix { get { if (_subjectPrefix != null) return _subjectPrefix; _subjectPrefix = GetMapiPropertyString(MapiTags.PR_SUBJECT_PREFIX); if (string.IsNullOrEmpty(_subjectPrefix)) _subjectPrefix = string.Empty; return _subjectPrefix; } } /// /// Returns the subject of the object /// public string Subject { get { if (_subject != null) return _subject; _subject = GetMapiPropertyString(MapiTags.PR_SUBJECT); if (string.IsNullOrEmpty(_subject)) _subject = string.Empty; return _subject; } } /// /// Returns the normalized subject of the E-mail /// public string SubjectNormalized { get { if (_subjectNormalized != null) return _subjectNormalized; _subjectNormalized = GetMapiPropertyString(MapiTags.PR_NORMALIZED_SUBJECT); if (string.IsNullOrEmpty(_subjectNormalized)) _subjectNormalized = string.Empty; return _subjectNormalized; } } /// /// Returns the available E-mail headers. These are only filled when the message /// has been sent accross the internet. Returns null when there aren't any message headers /// public MessageHeader Headers { get; private set; } // ReSharper disable once CSharpWarnings::CS0109 /// /// Returns a object when a flag has been set on the . /// Returns null when not available. /// public new Flag Flag { get { if (_flag != null) return _flag; var flag = new Flag(this); if (flag.Request != null) _flag = flag; return _flag; } } // ReSharper disable once CSharpWarnings::CS0109 /// /// Returns an object when the is a . /// Returns null when not available. /// public new Appointment Appointment { get { if (_appointment != null) return _appointment; switch (Type) { case MessageType.AppointmentRequest: case MessageType.Appointment: case MessageType.AppointmentResponse: case MessageType.AppointmentResponsePositive: case MessageType.AppointmentResponseNegative: break; default: return null; } _appointment = new Appointment(this); return _appointment; } } // ReSharper disable once CSharpWarnings::CS0109 /// /// Returns a object. This property is only available when:
/// - The is an and the object is not null
/// - The is an or
///
public new Task Task { get { if (_task != null) return _task; switch (_type) { case MessageType.Email: if (Flag == null) return null; break; case MessageType.Task: case MessageType.TaskRequestAccept: break; default: return null; } _task = new Task(this); return _task; } } // ReSharper disable once CSharpWarnings::CS0109 /// /// Returns a object when the is a . /// Returns null when not available. /// public new Contact Contact { get { if (_contact != null) return _contact; switch (Type) { case MessageType.Contact: break; default: return null; } _contact = new Contact(this); return _contact; } } /// /// Returns the categories that are placed in the Outlook message. /// Only supported for outlook messages from Outlook 2007 or higher /// public ReadOnlyCollection Categories => GetMapiPropertyStringList(MapiTags.Keywords); /// /// Returns the body of the Outlook message in plain text format. /// /// The body of the Outlook message in plain text format. public string BodyText { get { if (_bodyText != null) return _bodyText; _bodyText = GetMapiPropertyString(MapiTags.PR_BODY); return _bodyText; } } /// /// Returns the body of the Outlook message in RTF format. /// /// The body of the Outlook message in RTF format. public string BodyRtf { get { if (_bodyRtf != null) return _bodyRtf; // Get value for the RTF compressed MAPI property var rtfBytes = GetMapiPropertyBytes(MapiTags.PR_RTF_COMPRESSED); // Return null if no property value exists if (rtfBytes == null || rtfBytes.Length == 0) return null; rtfBytes = RtfDecompressor.DecompressRtf(rtfBytes); _bodyRtf = MessageCodePage.GetString(rtfBytes); return _bodyRtf; } } /// /// Returns the body of the Outlook message in HTML format. /// /// The body of the Outlook message in HTML format. public string BodyHtml { get { if (_bodyHtml != null) return _bodyHtml; // Get value for the HTML MAPI property var htmlObject = GetMapiProperty(MapiTags.PR_BODY_HTML); string html = null; if (htmlObject is string) html = htmlObject as string; else if (htmlObject is byte[]) { var htmlByteArray = htmlObject as byte[]; html = InternetCodePage.GetString(htmlByteArray); } // When there is no HTML found if (html == null) { // Check if we have HTML embedded into rtf var bodyRtf = BodyRtf; if (bodyRtf != null) { var rtfDomDocument = new Rtf.DomDocument(); rtfDomDocument.LoadRtfText(bodyRtf); if (!string.IsNullOrEmpty(rtfDomDocument.HtmlContent)) html = rtfDomDocument.HtmlContent.Trim('\r', '\n'); } } _bodyHtml = html; return _bodyHtml; } } /// /// Returns the that is used for the /// or . It will return when the /// codepage could not be read from the /// /// See the property when dealing with the /// /// public Encoding InternetCodePage { get { if (_internetCodepage != null) return _internetCodepage; var codePage = GetMapiPropertyInt32(MapiTags.PR_INTERNET_CPID); _internetCodepage = codePage == null ? Encoding.Default : Encoding.GetEncoding((int)codePage); return _internetCodepage; } } /// /// Returns the that is used for the . /// It will return the systems default encoding when the codepage could not be read from /// the /// /// See the property when dealing with the /// /// public Encoding MessageCodePage { get { if (_messageCodepage != null) return _messageCodepage; var codePage = GetMapiPropertyInt32(MapiTags.PR_MESSAGE_CODEPAGE); try { _messageCodepage = codePage != null ? Encoding.GetEncoding((int)codePage) : InternetCodePage; } catch (NotSupportedException) { _messageCodepage = InternetCodePage; } return _messageCodepage; } } /// /// Returns the the for the Windows LCID of the end user who created this /// It will return null when the the Windows LCID could not be /// read from the /// public RegionInfo MessageLocalId { get { if (_messageLocalId != null) return _messageLocalId; var lcid = GetMapiPropertyInt32(MapiTags.PR_MESSAGE_LOCALE_ID); if (!lcid.HasValue) return null; _messageLocalId = new RegionInfo(lcid.Value); return null; } } /// /// Returns true when the signature is valid when the is a . /// It will return null when the signature is invalid or the has another /// public bool? SignatureIsValid { get; private set; } /// /// Returns the name of the person who signed the when the is a /// . It will return null when the signature is invalid or the /// has another /// public string SignedBy { get; private set; } /// /// Returns the date and time when the has been signed when the is a /// . It will return null when the signature is invalid or the /// has another /// public DateTime? SignedOn { get; private set; } /// /// Returns the certificate that has been used to sign the when the is a /// . It will return null when the signature is invalid or the /// has another /// public X509Certificate2 SignedCertificate { get; private set; } /// /// Returns information about who has received this message. This information is only /// set when a message has been received and when the message provider stamped this /// information into this message. Null when not available. /// #pragma warning disable 109 public new ReceivedBy ReceivedBy #pragma warning restore 109 { get { if (_receivedBy != null) return _receivedBy; _receivedBy = new ReceivedBy( GetMapiPropertyString(MapiTags.PR_RECEIVED_BY_ADDRTYPE), GetMapiPropertyString(MapiTags.PR_RECEIVED_BY_EMAIL_ADDRESS), GetMapiPropertyString(MapiTags.PR_RECEIVED_BY_NAME)); return _receivedBy; } } /// /// Returns the index of the conversation. When not available null is returned /// public string ConversationIndex { get { if (_conversationIndex != null) return _conversationIndex; var conversationIndexBytes= GetMapiProperty(MapiTags.PR_CONVERSATION_INDEX); if(conversationIndexBytes != null && conversationIndexBytes is byte[]) { _conversationIndex = BitConverter.ToString((byte[])conversationIndexBytes, 0); if (!string.IsNullOrWhiteSpace(_conversationIndex) && _conversationIndex.Contains("-")) _conversationIndex = _conversationIndex.Replace("-", ""); } if (_conversationIndex == null) _conversationIndex = string.Empty; return _conversationIndex; } } /// /// Returns the topic of the conversation. When not available null is returned /// public string ConversationTopic { get { if (_conversationTopic != null) return _conversationTopic; _conversationTopic = GetMapiPropertyString(MapiTags.PR_CONVERSATION_TOPIC); return _conversationTopic; } } /// /// Returns the size of the message. When not available null is returned /// public int? Size { get { if (_messageSize != null) return _messageSize; _messageSize = GetMapiPropertyInt32(MapiTags.PR_MESSAGE_SIZE); return _messageSize; } } #endregion #region Constructors /// /// Initializes a new instance of the class from a msg file. /// /// The msg file to load /// FileAcces mode, default is Read public Message(string msgfile, FileAccess fileAccess = FileAccess.Read) : base(msgfile, fileAccess) { } /// /// Initializes a new instance of the class from a containing an IStorage. /// /// The containing an IStorage. /// FileAcces mode, default is Read public Message(Stream storageStream, FileAccess fileAccess = FileAccess.Read) : base(storageStream, fileAccess) { } /// /// Initializes a new instance of the class on the specified . /// /// The storage to create the on. /// /// The name of the that contains this message internal Message(CFStorage storage, int renderingPosition, string storageName) : base(storage) { StorageName = storageName; _propHeaderSize = MapiTags.PropertiesStreamHeaderTop; RenderingPosition = renderingPosition; } #endregion #region GetHeaders /// /// Try's to read the E-mail transport headers. They are only there when a msg file has been /// sent over the internet. When a message stays inside an Exchange server there are not any headers /// private void GetHeaders() { _TransportMessageHeaders = GetMapiPropertyString(MapiTags.PR_TRANSPORT_MESSAGE_HEADERS); if (!string.IsNullOrEmpty(_TransportMessageHeaders)) Headers = HeaderExtractor.GetHeaders(_TransportMessageHeaders); } #endregion #region LoadStorage /// /// Processes sub storages on the specified storage to capture attachment and recipient data. /// /// The storage to check for attachment and recipient data. protected override void LoadStorage(CFStorage storage) { base.LoadStorage(storage); foreach (var storageStatistic in _subStorageStatistics) { // Run specific load method depending on sub storage name prefix if (storageStatistic.Key.StartsWith(MapiTags.RecipStoragePrefix)) { var recipient = new Recipient(new Storage(storageStatistic.Value)); _recipients.Add(recipient); } else if (storageStatistic.Key.StartsWith(MapiTags.AttachStoragePrefix)) { switch (Type) { case MessageType.EmailClearSigned: LoadClearSignedMessage(storageStatistic.Value); break; case MessageType.EmailEncryptedAndMaybeSigned: LoadEncryptedAndMeabySignedMessage(storageStatistic.Value); break; default: LoadAttachmentStorage(storageStatistic.Value, storageStatistic.Key); break; } } } GetHeaders(); SetEmailSenderAndRepresentingSender(); // Check if there is a named substorage and if so open it and map all the named MAPI properties if (_subStorageStatistics.ContainsKey(MapiTags.NameIdStorage)) { var mappingValues = new List(); // Get all the named properties from the _streamStatistics foreach (var streamStatistic in _streamStatistics) { var name = streamStatistic.Key; if (name.StartsWith(MapiTags.SubStgVersion1)) { // Get the property value var propIdentString = name.Substring(12, 4); // Convert it to a short var value = ushort.Parse(propIdentString, NumberStyles.HexNumber); // Check if the value is in the named property range (8000 to FFFE (Hex)) if (value >= 32768 && value <= 65534) { // If so then add it to perform mapping later on if (!mappingValues.Contains(propIdentString)) mappingValues.Add(propIdentString); } } } // Check if there is also a properties stream and if so get all the named MAPI properties from it if (_streamStatistics.ContainsKey(MapiTags.PropertiesStream)) { // Get the raw bytes for the property stream var propBytes = GetStreamBytes(MapiTags.PropertiesStream); for (var i = _propHeaderSize; i < propBytes.Length; i = i + 16) { // Get property identifer located in 3rd and 4th bytes as a hexdecimal string var propIdent = new[] { propBytes[i + 3], propBytes[i + 2] }; var propIdentString = BitConverter.ToString(propIdent).Replace("-", string.Empty); // Convert it to a short var value = ushort.Parse(propIdentString, NumberStyles.HexNumber); // Check if the value is in the named property range (8000 to FFFE (Hex)) if (value >= 32768 && value <= 65534) { // If so then add it to perform mapping later on if (!mappingValues.Contains(propIdentString)) mappingValues.Add(propIdentString); } } } // Check if there is something to map if (mappingValues.Count <= 0) return; // Get the Named Id Storage, we need this one to perform the mapping var subStorage = _subStorageStatistics[MapiTags.NameIdStorage]; // Load the subStorage into our mapping class that does all the mapping magic var mapiToOom = new MapiTagMapper(new Storage(subStorage)); // Get the mapped properties _namedProperties = mapiToOom.GetMapping(mappingValues); } } #endregion #region ProcessSignedContent /// /// Processes the signed content /// /// /// private void ProcessSignedContent(byte[] data) { var signedCms = new SignedCms(); signedCms.Decode(data); try { //signedCms.CheckSignature(signedCms.Certificates, false); foreach (var cert in signedCms.Certificates) SignatureIsValid = cert.Verify(); SignatureIsValid = true; foreach (var cryptographicAttributeObject in signedCms.SignerInfos[0].SignedAttributes) { if (cryptographicAttributeObject.Values[0] is Pkcs9SigningTime) { var pkcs9SigningTime = (Pkcs9SigningTime)cryptographicAttributeObject.Values[0]; SignedOn = pkcs9SigningTime.SigningTime.ToLocalTime(); } } var certificate = signedCms.SignerInfos[0].Certificate; if (certificate != null) { SignedCertificate = certificate; SignedBy = certificate.GetNameInfo(X509NameType.SimpleName, false); } } catch (CryptographicException) { SignatureIsValid = false; } // Get the decoded attachment using (var memoryStream = new MemoryStream(signedCms.ContentInfo.Content)) { var eml = Mime.Message.Load(memoryStream); if (eml.TextBody != null) _bodyText = eml.TextBody.GetBodyAsText(); if (eml.HtmlBody != null) _bodyHtml = eml.HtmlBody.GetBodyAsText(); foreach (var emlAttachment in eml.Attachments) _attachments.Add(new Attachment(emlAttachment)); } } #endregion #region LoadEncryptedSignedMessage /// /// Load's and parses a signed message. The signed message should be in an attachment called smime.p7m /// /// private void LoadEncryptedAndMeabySignedMessage(CFStorage storage) { // Create attachment from attachment storage var attachment = new Attachment(new Storage(storage), null); if (attachment.FileName.ToUpperInvariant() != "SMIME.P7M") throw new MRInvalidSignedFile( "The signed file is not valid, it should contain an attachment called smime.p7m but it didn't"); ProcessSignedContent(attachment.Data); } #endregion #region LoadEncryptedSignedMessage /// /// Load's and parses a signed message /// /// private void LoadClearSignedMessage(CFStorage storage) { // Create attachment from attachment storage var attachment = new Attachment(new Storage(storage), null); // Get the decoded attachment using (var memoryStream = new MemoryStream(attachment.Data)) { var eml = Mime.Message.Load(memoryStream); if (eml.TextBody != null) _bodyText = eml.TextBody.GetBodyAsText(); if (eml.HtmlBody != null) _bodyHtml = eml.HtmlBody.GetBodyAsText(); foreach (var emlAttachment in eml.Attachments) { if (emlAttachment.FileName.ToUpperInvariant() == "SMIME.P7S") ProcessSignedContent(emlAttachment.Body); else _attachments.Add(new Attachment(emlAttachment)); } } } #endregion #region LoadAttachmentStorage /// /// Loads the attachment data out of the specified storage. /// /// The attachment storage. /// The name of the private void LoadAttachmentStorage(CFStorage storage, string storageName) { // Create attachment from attachment storage var attachment = new Attachment(new Storage(storage), storageName); var attachMethod = attachment.GetMapiPropertyInt32(MapiTags.PR_ATTACH_METHOD); switch (attachMethod) { case MapiTags.ATTACH_EMBEDDED_MSG: // Create new Message and set parent and header size var subStorage = attachment.GetMapiProperty(MapiTags.PR_ATTACH_DATA_BIN) as CFStorage; var subMsg = new Message(subStorage, attachment.RenderingPosition, storageName) { _parentMessage = this, _propHeaderSize = MapiTags.PropertiesStreamHeaderEmbeded }; _attachments.Add(subMsg); break; default: // Add attachment to attachment list _attachments.Add(attachment); break; } } #endregion #region DeleteAttachment /// /// Removes the given from the object. /// /// /// message.DeleteAttachment(message.Attachments[0]); /// /// /// Raised when it is not possible to remove the or from /// the public void DeleteAttachment(object attachment) { if (FileAccess == FileAccess.Read) throw new MRCannotRemoveAttachment("Cannot remove attachments when the file is not opened in Write or ReadWrite mode"); foreach (var attachmentObject in _attachments) { if (attachmentObject.Equals(attachment)) { string storageName; var attach = attachmentObject as Attachment; if (attach != null) { if (string.IsNullOrEmpty(attach.StorageName)) throw new MRCannotRemoveAttachment("The attachment '" + attach.FileName + "' can not be removed, the storage name is unknown"); storageName = attach.StorageName; attach.Dispose(); } else { var msg = attachmentObject as Message; if (msg == null) throw new MRCannotRemoveAttachment( "The attachment can not be removed, could not convert the attachment to an Attachment or Message object"); storageName = msg.StorageName; msg.Dispose(); } _attachments.Remove(attachment); TopParent._rootStorage.Delete(storageName); break; } } } #endregion #region Copy /// /// Copies the given to the given /// /// /// private static void Copy(CFStorage source, CFStorage destination) { source.VisitEntries(action => { if (action.IsStorage) { var destinationStorage = destination.AddStorage(action.Name); destinationStorage.CLSID = action.CLSID; destinationStorage.CreationDate = action.CreationDate; destinationStorage.ModifyDate = action.ModifyDate; Copy(action as CFStorage, destinationStorage); } else { var sourceStream = action as CFStream; var destinationStream = destination.AddStream(action.Name); if (sourceStream != null) destinationStream.SetData(sourceStream.GetData()); } }, false); } #endregion #region Save /// /// Saves this to the specified /// /// Name of the file. public void Save(string fileName) { using (var saveFileStream = File.Open(fileName, FileMode.Create, FileAccess.ReadWrite)) Save(saveFileStream); } /// /// Saves this to the specified /// /// The stream to save to. public void Save(Stream stream) { if (IsTopParent) { _compoundFile.Save(stream); } else { var compoundFile = new CompoundFile(); var sourceNameIdStorage = TopParent._rootStorage.GetStorage(MapiTags.NameIdStorage); var rootStorage = compoundFile.RootStorage; var destinationNameIdStorage = rootStorage.AddStorage(MapiTags.NameIdStorage); Copy(sourceNameIdStorage, destinationNameIdStorage); Copy(_rootStorage, rootStorage); var propertiesStream = rootStorage.GetStream(MapiTags.PropertiesStream); var sourceData = propertiesStream.GetData(); var destinationData = new byte[sourceData.Length + 8]; Buffer.BlockCopy(sourceData, 0, destinationData, 0, 24); Buffer.BlockCopy(sourceData, 24, destinationData, 32, sourceData.Length - 24); propertiesStream.SetData(destinationData); compoundFile.Save(stream); compoundFile.Close(); } } #endregion #region SetEmailSenderAndRepresentingSender /// /// Gets the and from the /// object and sets the and /// private void SetEmailSenderAndRepresentingSender() { var tempEmail = GetMapiPropertyString(MapiTags.PR_SENDER_EMAIL_ADDRESS); if (string.IsNullOrEmpty(tempEmail) || tempEmail.IndexOf('@') == -1) tempEmail = GetMapiPropertyString(MapiTags.PR_SENDER_SMTP_ADDRESS); if (string.IsNullOrEmpty(tempEmail)) tempEmail = GetMapiPropertyString(MapiTags.InternetAccountName); if (string.IsNullOrEmpty(tempEmail)) tempEmail = GetMapiPropertyString(MapiTags.SenderSmtpAddressAlternate); MessageHeader headers = null; if (string.IsNullOrEmpty(tempEmail) || tempEmail.IndexOf("@", StringComparison.Ordinal) < 0) { var senderAddressType = GetMapiPropertyString(MapiTags.PR_SENDER_ADDRTYPE); if (senderAddressType != null && senderAddressType != "EX") { // Get address from email headers. The headers are not present when the addressType = "EX" var header = GetStreamAsString(MapiTags.HeaderStreamName, Encoding.Unicode); if (!string.IsNullOrEmpty(header)) headers = HeaderExtractor.GetHeaders(header); } } // PR_PRIMARY_SEND_ACCT can contain the smtp address of an exchange account if (string.IsNullOrEmpty(tempEmail) || tempEmail.IndexOf("@", StringComparison.Ordinal) < 0) { var testEmail = GetMapiPropertyString(MapiTags.PR_PRIMARY_SEND_ACCT); if(!string.IsNullOrEmpty(testEmail) && testEmail.IndexOf("\u0001", StringComparison.Ordinal) > 0) { tempEmail = EmailAddress.GetValidEmailAddress(testEmail); } } tempEmail = EmailAddress.RemoveSingleQuotes(tempEmail); var tempDisplayName = EmailAddress.RemoveSingleQuotes(GetMapiPropertyString(MapiTags.PR_SENDER_NAME)); if (string.IsNullOrEmpty(tempEmail) && headers?.From != null) tempEmail = EmailAddress.RemoveSingleQuotes(headers.From.Address); if (string.IsNullOrEmpty(tempDisplayName) && headers?.From != null) tempDisplayName = headers.From.DisplayName; var email = tempEmail; var displayName = tempDisplayName; // Sometimes the E-mail address and displayname get swapped so check if they are valid if (!EmailAddress.IsEmailAddressValid(tempEmail) && EmailAddress.IsEmailAddressValid(tempDisplayName)) { // Swap then email = tempDisplayName; displayName = tempEmail; } else if (EmailAddress.IsEmailAddressValid(tempDisplayName)) { // If the displayname is an emailAddress then move it email = tempDisplayName; displayName = tempDisplayName; } if (string.Equals(tempEmail, tempDisplayName, StringComparison.InvariantCultureIgnoreCase)) displayName = string.Empty; // Set the representing sender if it is there Sender = new Sender(email, displayName); var representingAddressType = GetMapiPropertyString(MapiTags.PR_SENT_REPRESENTING_ADDRTYPE); tempEmail = GetMapiPropertyString(MapiTags.PR_SENT_REPRESENTING_EMAIL_ADDRESS); tempEmail = EmailAddress.RemoveSingleQuotes(tempEmail); tempDisplayName = EmailAddress.RemoveSingleQuotes(GetMapiPropertyString(MapiTags.PR_SENT_REPRESENTING_NAME)); email = tempEmail; displayName = tempDisplayName; // Sometimes the E-mail address and displayname get swapped so check if they are valid if (!EmailAddress.IsEmailAddressValid(tempEmail) && EmailAddress.IsEmailAddressValid(tempDisplayName)) { // Swap then email = tempDisplayName; displayName = tempEmail; } else if (EmailAddress.IsEmailAddressValid(tempDisplayName)) { // If the displayname is an emailAddress then move it email = tempDisplayName; displayName = tempDisplayName; } if (string.Equals(tempEmail, tempDisplayName, StringComparison.InvariantCultureIgnoreCase)) displayName = string.Empty; // Set the representing sender if (!string.IsNullOrWhiteSpace(email)) SenderRepresenting = new SenderRepresenting(email, displayName, representingAddressType); } #endregion #region GetEmailSender /// /// Returns the E-mail sender address in RFC822 format, e.g. /// "Pan, P (Peter)" <Peter.Pan@neverland.com> /// /// public string GetEmailSenderRfc822Format() { var output = string.Empty; if (!string.IsNullOrEmpty(Sender.DisplayName)) output = "\"" + Sender.DisplayName + "\""; if (!string.IsNullOrEmpty(Sender.Email)) { if (!string.IsNullOrEmpty(output)) output += " "; output += "<" + Sender.Email + ">"; } return output; } /// /// Returns the E-mail sender address in a human readable format /// /// Set to true to return the E-mail address as an html string /// Set to true to convert the E-mail addresses to a hyperlink. /// Will be ignored when is set to false /// public string GetEmailSender(bool html, bool convertToHref) { var output = string.Empty; var emailAddress = Sender.Email; var representingEmailAddress = string.Empty; var displayName = Sender.DisplayName; var representingDisplayName = string.Empty; var representingAddressType = string.Empty; if (SenderRepresenting != null) { representingEmailAddress = SenderRepresenting.Email; representingDisplayName = SenderRepresenting.DisplayName; representingAddressType = SenderRepresenting.AddressType; } if (html) { emailAddress = WebUtility.HtmlEncode(emailAddress); displayName = WebUtility.HtmlEncode(displayName); representingEmailAddress = WebUtility.HtmlEncode(representingEmailAddress); representingDisplayName = WebUtility.HtmlEncode(representingDisplayName); } // If we want hyperlinks and the outputformat is html and the email address is set if (convertToHref && html && !string.IsNullOrEmpty(emailAddress)) { output += "" + (!string.IsNullOrEmpty(displayName) ? displayName : emailAddress) + ""; if (!string.IsNullOrEmpty(representingEmailAddress) && !string.IsNullOrEmpty(emailAddress) && !emailAddress.Equals(representingEmailAddress, StringComparison.InvariantCultureIgnoreCase)) { output += " " + LanguageConsts.EmailOnBehalfOf + " " + (!string.IsNullOrEmpty(representingDisplayName) ? representingDisplayName : representingEmailAddress) + " "; } } else { string beginTag; string endTag; if (html) { beginTag = " <"; endTag = ">"; } else { beginTag = " <"; endTag = ">"; } if (!string.IsNullOrEmpty(displayName)) output += displayName; if (!string.IsNullOrEmpty(emailAddress)) output += beginTag + emailAddress + endTag; if (!string.IsNullOrEmpty(representingEmailAddress) && !string.IsNullOrEmpty(emailAddress) && !emailAddress.Equals(representingEmailAddress, StringComparison.InvariantCultureIgnoreCase)) { output += " " + LanguageConsts.EmailOnBehalfOf + " "; if (!string.IsNullOrEmpty(representingDisplayName)) output += representingDisplayName; if (!string.IsNullOrEmpty(representingEmailAddress) && representingAddressType != "EX") { if (!string.IsNullOrWhiteSpace(representingDisplayName)) output += beginTag; output += representingEmailAddress; if (!string.IsNullOrWhiteSpace(representingDisplayName)) output += endTag; } } } return output; } #endregion #region GetEmailRecipients /// /// Returns all the recipient for the given /// /// The to return /// private List GetEmailRecipients(RecipientType type) { var recipients = new List(); // ReSharper disable once LoopCanBeConvertedToQuery foreach (var recipient in Recipients) { // First we filter for the correct recipient type if (recipient.Type == type) recipients.Add(new RecipientPlaceHolder(recipient.Email, recipient.DisplayName, recipient.AddressType)); } if (recipients.Count == 0 && Headers != null) { switch (type) { case RecipientType.To: if (Headers.To != null) recipients.AddRange( Headers.To.Select( to => new RecipientPlaceHolder(to.Address, to.DisplayName, string.Empty))); break; case RecipientType.Cc: if (Headers.Cc != null) recipients.AddRange( Headers.Cc.Select( cc => new RecipientPlaceHolder(cc.Address, cc.DisplayName, string.Empty))); break; case RecipientType.Bcc: if (Headers.Bcc != null) recipients.AddRange( Headers.Bcc.Select( bcc => new RecipientPlaceHolder(bcc.Address, bcc.DisplayName, string.Empty))); break; } } return recipients; } /// /// Returns the E-mail recipients in RFC822 format, e.g. /// "Pan, P (Peter)" <Peter.Pan@neverland.com> /// /// Selects the Recipient type to retrieve /// public string GetEmailRecipientsRfc822Format(RecipientType type) { var output = string.Empty; var recipients = GetEmailRecipients(type); if (Appointment?.UnsendableRecipients != null) recipients.AddRange(Appointment.UnsendableRecipients.GetEmailRecipients(type)); foreach (var recipient in recipients) { if (output != string.Empty) output += ", "; var tempOutput = string.Empty; if (!string.IsNullOrEmpty(recipient.DisplayName)) tempOutput += "\"" + recipient.DisplayName + "\""; if (!string.IsNullOrEmpty(recipient.Email)) { if (!string.IsNullOrEmpty(tempOutput)) tempOutput += " "; tempOutput += "<" + recipient.Email + ">"; } output += tempOutput; } return output; } /// /// Returns the E-mail recipients in a human readable format /// /// Selects the Recipient type to retrieve /// Set to true to return the E-mail address as an html string /// Set to true to convert the E-mail addresses to hyperlinks. /// Will be ignored when is set to false /// public string GetEmailRecipients(RecipientType type, bool html, bool convertToHref) { var output = string.Empty; var recipients = GetEmailRecipients(type); if (Appointment?.UnsendableRecipients != null) recipients.AddRange(Appointment.UnsendableRecipients.GetEmailRecipients(type)); foreach (var recipient in recipients) { if (output != string.Empty) output += "; "; var emailAddress = recipient.Email; var displayName = recipient.DisplayName; if (convertToHref && html && !string.IsNullOrEmpty(emailAddress)) output += "" + (!string.IsNullOrEmpty(displayName) ? displayName : emailAddress) + ""; else { if (!string.IsNullOrEmpty(displayName)) output += displayName; var beginTag = string.Empty; var endTag = string.Empty; if (!string.IsNullOrEmpty(displayName)) { if (html) { beginTag = " <"; endTag = ">"; } else { beginTag = " <"; endTag = ">"; } } if (!string.IsNullOrEmpty(emailAddress)) output += beginTag + emailAddress + endTag; } } return output; } #endregion #region GetAttachmentNames /// /// Returns the attachments names as a comma seperated string /// /// public string GetAttachmentNames() { var result = new List(); foreach (var attachment in Attachments) { // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull if (attachment is Attachment) { var attach = (Attachment)attachment; result.Add(attach.FileName); } // ReSharper disable once CanBeReplacedWithTryCastAndCheckForNull else if (attachment is Message) { var msg = (Message)attachment; result.Add(msg.FileName); } } return string.Join(", ", result); } #endregion } } }