Skip to content

Instantly share code, notes, and snippets.

@WarningImHack3r
Last active July 10, 2025 15:27
Show Gist options
  • Select an option

  • Save WarningImHack3r/5daf6747e5bfa4bae999f6f7bff9dec5 to your computer and use it in GitHub Desktop.

Select an option

Save WarningImHack3r/5daf6747e5bfa4bae999f6f7bff9dec5 to your computer and use it in GitHub Desktop.
Properly reading a message body with a Transport Agent in a Microsoft Exchange plugin

Properly reading a message body with a Transport Agent in an Exchange plugin

While it doesn't look that complicated in the first place, properly reading an Microsoft.Exchange.Data.Transport.Email.EmailMessage Body isn't that straightforward: some characters like tabulations (\t) can easily be missing.

Initial approach

My original approach was the following:

public static string GetStringBody(this IEmailBody body)
{
    using var bodyStream = body.GetContentReadStream();
    using var ms = new MemoryStream();
    bodyStream.CopyTo(ms);
    var bytes = ms.ToArray();
    return Encoding.Unicode.GetString(bytes);
}

Which I would call in this context:

namespace MyNS
{

    public class MyAgent : SmtpReceiveAgent
    {
        public MyAgent()
        {
            OnEndOfData += (source, e) =>
            {
                // here
                var stringBody = e.MailItem.Message.Body.GetStringBody();
            }
        }
    }
}

This mostly works, but for some reason, some characters like tabs (\t) and maybe others were missing, despite being present in the original message as well as in the received message on the other client. I was just not able to properly read the body inside the plugin.

Explanations

Microsoft being Microsoft, the message passed to the plugins was encoded in a special format called TNEF. It was visible in the Content-Type header when dumping the whole message, and we could also notice here that the body was (the only thing) encoded and not readable as-is.

To properly read the body, we need to decode using TNEF functions.

Proper TNEF decoding

I wish this hell to nobody, so here's the full snippet to read the body:

using Microsoft.Exchange.Data.ContentTypes.Tnef; // in Microsoft.Exchange.Data.Common.dll

/// <remarks>
/// Pass <c>e.MailItem.Message</c> to this function.
/// </remarks>
public static string GetStringBodyFromMessage(EmailMessage message)
{
    if (message.TnefPart == null)
    {
        using var rawContentStream = message.RootPart.GetRawContentReadStream();
        using var rawReader = new StreamReader(rawContentStream);
        return rawReader.ReadToEnd();
    }
		
    try
    {
        using var tnefStream = message.TnefPart.GetContentReadStream();
        using var tnefReader = new TnefReader(tnefStream);

        // Read TNEF content
        while (tnefReader.ReadNextAttribute())
        {
            if (tnefReader.AttributeLevel == TnefAttributeLevel.Message && tnefReader.AttributeTag == TnefAttributeTag.MapiProperties)
            {
                while (tnefReader.PropertyReader.ReadNextProperty())
                {
                    var propertyTag = tnefReader.PropertyReader.PropertyTag;
                    switch (propertyTag.ValueTnefType)
                    {
                        case TnefPropertyType.String8:
                        case TnefPropertyType.Unicode:
                            if (Array.Exists(BodyPropertyIds, id => id == propertyTag.Id))
                            {
                                return propertyReader.ReadValueAsString();
                            }
                            break;
                    }
                }
            }
        }
    }
    catch (Exception ex)
    {
        throw new Exception($"Error extracting TNEF body: {ex.Message}");
    }

    return string.Empty;
}

private static readonly TnefPropertyId[] BodyPropertyIds = new TnefPropertyId[]
{
    TnefPropertyId.Body, // PR_BODY
    TnefPropertyId.BodyHtml, // PR_HTML_BODY  
    TnefPropertyId.RtfCompressed, // PR_RTF_COMPRESSED
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment