Skip to content

Instantly share code, notes, and snippets.

@WarningImHack3r
Created March 20, 2025 20:58
Show Gist options
  • Select an option

  • Save WarningImHack3r/00a5035ac70beadca36f6a671ef3876c to your computer and use it in GitHub Desktop.

Select an option

Save WarningImHack3r/00a5035ac70beadca36f6a671ef3876c to your computer and use it in GitHub Desktop.
Installing and working with a Transport Agent in a Microsoft Exchange plugin (2013-2019)

Installing and working with a Transport Agent in an Exchange plugin

While it might seem quite trivial from the Microsoft documentation, installing and working with transport agents (SmtpReceiveAgent, RoutingAgent, and DeliveryAgent) is full of footguns.
Here is a little post-mortem after playing around with an SmtpReceiveAgent with an Exchange Server 2019 (with countless days of debugging and borderline madness) to hopefully get you there in less than a month.

Installation

The gist (no pun intended) of the installation process can be found here, and is quite well documented. Other useful links to understand how agents are structured are this one (or any of its variants depending of which agent you want to create) and this one for understanding the global architecture.
Despite all that, you may consider a few additional things:

1. Obtain the Exchange DLLs

You can find them on your Exchange Server machine. Open the Explorer, search for the following names, and pick the relevant DLLs:

  • Microsoft.Exchange.Data.Common.dll
  • Microsoft.Exchange.Data.Transport.dll

You might also need Microsoft.Exchange.Data.Transport.resources.dll, but you will most likely not need it.

2. Supported versions

Despite these DLLs being compatible with any .NET version, you can only use .NET Framework to build Exchange plugins. .NET and .NET Core are not compatible. If you try with them, you might get a RoutingAgent or a DeliveryAgent to work, but not an SmtpReceiveAgent.

If you try anyway, you may encounters errors such as the following while trying to install your DLL:

There is no known TransportAgentFactory interface implemented by the specified TransportAgentFactory "YourNamespace.Your.Factory.Class".
Parameter name: TransportAgentFactory

You can use any C# version you want, though >=10 starts being less and less compatible with such old .NET versions.

3. Plugin dependencies

If you have multiple DLLs (including the ones from step 1) from your compilation build output, you'll have to bring them all into the Exchange server, and make sure they always are next to the main plugin DLL when you install it.
When you run the installation commands, only run them against your main DLL, no need to care about the dependencies.

4. Using Powershell instead of the Exchange Management Shell (2013-2016(?) only)

For Client Access servers and if you're targetting the FrontEnd service with Exchange Server 2013 (and maybe also 2016), execute your plugin installation from Powershell, following this documentation section.

Important

In all situations, always run either Powershell or the Exchange Management Shell as administrator. Commands you'll want to use won't work otherwise.

5. Exchange server types depending on your service (2013-2016(?) only)

Depending on your type of agent, you might want to install your plugin on one of the two Exchange servers if you're using 2013 (and maybe 2016). Search in the Exchange doc for which one should be installed where, and don't forget the -TransportService argument in your Install-TransportAgent command (the default service is Hub fyi).

6. Copy error during installation

You might encounter this error after trying to install your plugin you copied from another machine:

Could not load file or assembly 'file:///C:\foobar.dll' or one of its dependencies. Operation is not supported. (Exception from HRESULT: 0x80131515)

To fix it (thank you SO), you can untick the protection checkbox from the file's Properties menu. If you're extracting multiple DLLs from a single zip file, untick the checkbox from the zip's Properties menu directly, it'll avoid needing to do that for each file. Check the linked StackOverflow post for more info.

API specificities

Now that you can make an Hello World plugin work (I recommend the example one from the docs), you may also want to know about some quirks in the API usage.

1. Event handlers

You can only register a single event handler per listener. Despite the C# events API being quite open, Exchange will throw if you register more than one event handler per event.

If you want to register multiple events anyway, do the following:

public MyAgent()
{
-   this.OnEndOfData += MyFirstEndOfDataHandler;
-   this.OnEndOfData += MySecondEndOfDataHandler; // this one will cause Exchange to throw
+   this.OnEndOfData += (source, e) =>
+   {
+     MyFirstEndOfDataHandler(source, e);
+     MySecondEndOfDataHandler(source, e);
+   };
}

private void MyFirstEndOfDataHandler(ReceiveMessageEventSource source, EndOfDataEventArgs e)
{
    ...
}

private void MySecondEndOfDataHandler(ReceiveMessageEventSource source, EndOfDataEventArgs e)
{
    ...
}

2. Reading a message

Choose the right event handler

Some properties are not available depending on which agent you use and which listener you plug your processing on. Refer to the documentation to figure out which one you want to use.

Reading a message body

  • Body stream limitations. You cannot use Seek(), Position nor Length when using e.MailItem.Message.Body.GetContentReadStream(). To be able to use all the Stream APIs, convert the body stream into a MemoryStream first, and operate on the latter. You can convert a body stream into a MemoryStream doing the following:

    using var bodyStream = e.MailItem.Message.Body.GetContentReadStream();
    using var ms = new MemoryStream();
    bodyStream.CopyTo(ms);
    // now, do your stuff on `ms`
  • Body encoding. The message body comes with the format (Plain Text, HTML...) defined by the settings of the Outlook client. On top of that, the message you get can be encoded in multiple encodings. The most likely are UTF-8 and UTF-16 (Unicode). If you try to log a body encoded in UTF-16, you might notice the log ends after the first letter of your body, and the size of it is twice the original size.
    To properly get a usable body as string, you can either always decode as UTF-16...

    using var bodyStream = body.GetContentReadStream();
    using var ms = new MemoryStream();
    bodyStream.CopyTo(ms);
    var body = Encoding.Unicode.GetString(ms.ToArray()); // here you go

    ...or automatically detect the encoding and read it accordingly (with either UTF-8 or 16):

    /// <summary>
    /// Get the message body as a string
    /// </summary>
    /// <param name="body">The mail <see cref="Body"/></param>
    /// <returns>The body as a string</returns>
    public static string GetStringBody(this IEmailBody body)
    {
        using var bodyStream = body.GetContentReadStream();
        using var ms = new MemoryStream();
        bodyStream.CopyTo(ms);
        var bytes = ms.ToArray();
    
        // Try detecting UTF-16 (Unicode) (a byte followed by a null byte)
        var seemsUtf16 = true;
        for (var i = 1; i < bytes.Length && i < 20; i += 2)
        {
            if (bytes[i] == 0) continue;
            seemsUtf16 = false;
            break;
        }
    
        // Convert as needed
        return seemsUtf16 ? Encoding.Unicode.GetString(bytes) : Encoding.UTF8.GetString(bytes);
    }

I'll be updating this Gist for as long as I find new gotchas working with Exchange plugins. In the meantime, don't hesitate to ask in the comments below!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment