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.
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:
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.dllMicrosoft.Exchange.Data.Transport.dll
You might also need Microsoft.Exchange.Data.Transport.resources.dll, but you will most likely not need it.
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.
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.
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.
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).
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.
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.
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)
{
...
}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.
-
Body stream limitations. You cannot use
Seek(),PositionnorLengthwhen usinge.MailItem.Message.Body.GetContentReadStream(). To be able to use all theStreamAPIs, convert the body stream into aMemoryStreamfirst, and operate on the latter. You can convert a body stream into aMemoryStreamdoing 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!