Update 2/5/2013: We have also uploaded a sample that will work on Exchange 2010 servers, you can find it here.
Transport agents allow Microsoft, developers in your organization and third-party vendors to hook into the Exchange transport pipeline with their code to process messages (e.g. an antivirus scanner for incoming email messages). Transport agents can process email messages that pass through the transport pipeline in many ways. An agent is a .Net assembly that has to be installed on the Exchange Client Access or Mailbox server. The agent is then loaded by the Exchange Transport service and invoked in the transport pipeline on the specified event. In Microsoft Exchange Server 2013, the transport pipeline is made of three different processes:
Like the previous version of Exchange, Exchange 2013 transport provides extensibility that is based on the Microsoft .NET Framework version 4.0 and allows third parties to implement the following predefined classes:
Note: This Article will concentrate mainly on how to implement and build a SmtpReceiveAgent. The SmtpReceiveAgentFactory and SmtpReceiveAgent classes provide support for the extension of the Microsoft Exchange Server 2013 Edge Transport behavior. You can use these classes to implement transport agents that are designed to respond to messages coming into and going out of the organization.
The following list explains the requirements for using transport agents in Exchange 2013.
Due to the updates to the transport pipeline, the transport agent cmdlets need to distinguish between the Hub Transport service and the Front End Transport service, especially if Client Access server and Mailbox server are installed on the same physical server. All transport agent cmdlets now have the TransportService parameter. The values you can specify are Hub for the Hub Transport service and FrontEnd for the Front End Transport service. For example, to view the manageable transport agents in the Hub Transport service, run the command: Get-TransportAgent -TransportService Hub. To view the manageable transport agents in the Front End Transport service, run the command: Get-TransportAgent -TransportService FrontEnd. The TransportService parameter is available in all transport agent cmdlets:
This Transport Agent was designed and implemented to illustrate various Exchange 2013 transport agent functionality as well as stripping all the hyperlinks from the message body. This agent will illustrate the following functionality:
Visual Studio can be utilized to build and implement a transport agent. This lesson will show you how to build a transport agent to remove all HTML links from an incoming SMTP messages as well as illustrating on how to use the agent in the Exchange 2013 Front End Transport Service which was introduced in Exchange 2013.
1. In Visual Studio 2012, Create a new project using Templates->Visual C#->Windows->Class Library and name the project StripIncomingLinkAgent
2. Create a folder name Exchange under \StripIncomingLinkAgent\StripIncomingLinkAgent and copy the following DLLs from C:\Program Files\Microsoft\Exchange Server\Public
3. In Visual Studio 2012, Go to Solution Explorer, Right click the References and select “Add Reference…”
4. Select Browse from the Reference Manager dialog and navigate to \StripIncomingLinkAgent\StripIncomingLinkAgent\Exchange
5. Select both dll and click add and OK
6. These two DLL will be used to integrate the agent to MSExchangeFrontEndTransport and MSExchangeTransport process.
7. First step is to rename the Class1.cs generated by Visual Studio to a meaningful name (e.g. StripIncomingLinkReceiveAgent)
8. Next Step is to create a ReceiveAgent class (naming the class StrinIncomingLinkReceiveAgent) which will inherit from SmtpReceiveAgent and add the proper references:
9. Before adding the business logic to our agent code and setup the agent call back, we need to setup a logging method to log events to the application event log:
public static void WriteLog(string message, EventLogEntryType entryType, int eventID, string proccessName)
EventLog evtLog = new EventLog();
evtLog.Log = s_EventLogName;
evtLog.Source = proccessName;
evtLog.WriteEntry(message, entryType, eventID);
10. Next step is to register our call back with the Transport EventHandlers and create our agent:
public sealed class StripLinkReceiveAgentFactory : SmtpReceiveAgentFactory
public override SmtpReceiveAgent CreateAgent(SmtpServer server)
config = new StaticConfigProvider
ForceSinglePart = true,
ForceTextPlain = true,
FilterAuthenticated = true,
FilterTNEF = true,
HonorAntiSpamBypass = false,
SkipInternalMessages = false,
AlwaysFilterCAFETraffic = false
// To use the RegistryConfigProvider class - uncomment the line
// below and comment above staticConfigProvider
// config = new RegistryConfigProvider()
return new StripIncomingLinkReceiveAgent(config);
public class StripIncomingLinkReceiveAgent : SmtpReceiveAgent
private static string processName = "ExchagneStripIncomingAgent";
private string machineName;
private Process currentProcess;
private static string FrontEndTransport = "ExchangeStripIncomingLinkFrontEndAgent";
private static RoutingAddress inboundProxy = new RoutingAddress( "firstname.lastname@example.org");
private bool m_IsTNEF = false;
private bool m_IsSummaryTNEF = false;
/// Configuration provider. Should be set by constructor.
private IConfigurationProvider configProvider;
public static void WriteLog(string message, EventLogEntryType entryType, int eventID, string proccessName)
StripLinkHelper.WriteLog("ReceiveAgent: " + message, entryType, eventID, proccessName);
public StripIncomingLinkReceiveAgent(IConfigurationProvider config)
configProvider = config;
this.OnEndOfData += new EndOfDataEventHandler(StripIncomingLinkEndOfDataHandler);
this.OnEndOfHeaders += new EndOfHeadersEventHandler(StripIncomingLinkEndOfHeadersHandler);
currentProcess = Process.GetCurrentProcess();
machineName = Environment.MachineName;
processName = FrontEndTransport;
11. Now we can implement the Event Handler logic for End of Header event handler. We are keeping the Header Event handler simple. The code checks for health messages generated by Exchange and skip processing of those messages and it adds a marker to the header to notify the backend agent that header has been processed by frontend agent:
private bool IsHealthMessage(MailItem mailItem)
string domainName = mailItem.FromAddress.DomainPart;
RoutingAddress adminEmailAddress = new RoutingAddress("Administrator@" + domainName);
return (mailItem.Recipients.Contains(inboundProxy) || mailItem.FromAddress.LocalPart.Contains("HealthMailbox") ||
private void StripIncomingLinkEndOfHeadersHandler(ReceiveMessageEventSource source, EndOfHeadersEventArgs e)
if (IsHealthMessage(e.MailItem) && !(currentProcess.ProcessName.ToLower().Contains("frontend")))
// Add a header indicating that this was processed
// by a Front End Server...
catch (Exception except)
WriteLog("EndofHeader Exception = " + except.ToString() + processName, EventLogEntryType.Error, 10, processName);
12. End of Data Event handler has all the logic to parse the message body, create a single part (based on registry key value), convert the message to plain text (based on registry key value) and finally remove all the hyperlink from the message and save the body using StreamWriter:
private void StripIncomingLinkEndOfDataHandler(ReceiveMessageEventSource source, EndOfDataEventArgs e)
EmailMessage message = e.MailItem.Message;
Body currentBody = message.Body;
// Message skipped, nothing to do.
// The following Actions are only valid for
// pure mime messages (Not TNEF)
// Do we want to make it single part
// (only Valid is no TNEF)
// The goal here is to remove all other mime parts
// (body types, attachments) to minimize
// the surface area where hyperlinks can be found.
// There is no point in removing hyperlinks in the body
// if there is an HTML attachment in the email. :)
// Once again, we try to reduce the exposure to hyperlinks.
// We do this by trying to get the lowest fidelity
// body, and making that the only mimepart on the message.
// We hope for Text/Plain body but it could be HTML.
MimePart mimepartLowFidelity = StripLinkHelper.GetLowerFidelityBodyPart(currentBody);
if (mimepartLowFidelity != null)
// We now remove any branches that do not contain
// this node. Another option is to make the
// message single part, however, this will
// require merging headers of the source
// part with the root part.
// NOTE: This will break multipart/related messages
// because they rely on the other parts
// to store the other components (images, documents, etc).
// This should not be an issue
// if the target part is always text/plain.
// TODO: Decide what to do in this scenario...
// Probably route to Admin...
// This is the case for TNEF messages as there
// is no MimePart associated with the body.
WriteLog("Failed to get a low fidelity body.” + “Mime tree will not be simplified. Quarantine Message " + processName, EventLogEntryType.Error, 10, processName);
source.Quarantine(e.MailItem.Recipients, "Could not find MimePart for the body.");
// Force Plain text...
// We do honor the ForceSinglePart for TNEF, we
// assume this means we don't want any attachments
// so for TNEF we simply remove the message attachments.
// A seperate option could be added, TNEFRemoveAttachments
// if this needs to be handle independent of
// the ForceSinglePart settings.
// We now need to process the message.
// TODO: For LegacyTNEF we thought we also needed to
// filter the text/plain part that is included
// for non-TNEF clients, but it appears modifying the body
// also generates a new text/plain part so
// the code below is not needed.
//if (m_IsTNEF && !m_IsSummaryTNEF)
catch (Exception except)
WriteLog("EndofData Exception = " + except.ToString() + processName, EventLogEntryType.Error, 10, processName);
source.Quarantine(e.MailItem.Recipients, "StripIncomingLinkAgent - Error occurred: " + except.Message);
13. Process Body method uses the regular expression class to remove any hyperlink or website address from the body of the message:
public class TextToTextLinkProcessor : IHyperLinkProcessor
private int m_LinkCount = 0;
private const string s_RegExBodyString = @"((www\.|(http|https|ftp|news|file)+\:\/\/) [_.a-z0-9-]+\.[a-z0-9\/_:@=.+?,##%&~-]*[^.|\'|\# |!|\(|?|,| |>|<|;|\)])";
private bool m_bChanged = false;
private string m_strReplacementText = string.Empty;
public bool WasChanged
public void ProcessEmailBody(Body bodyMessage)
m_LinkCount = 0;
string savedContent = string.Empty;
Encoding encoding = StripLinkHelper.GetEncodingFromBody(bodyMessage.CharsetName);
if (bodyMessage.TryGetContentReadStream(out memStream))
using (StreamReader streamRead = new StreamReader(memStream, encoding))
// TODO: May also want to decide on size of message and
// whether or not it should be processed if it is too large.
savedContent = FilterText(streamRead.ReadToEnd());
// Now write the new body only if it was changed/filtered.
using (StreamWriter streamWriter = new StreamWriter(bodyMessage.GetContentWriteStream(), encoding))
public string FilterText(string strText)
Regex rgx = new Regex(s_RegExBodyString, RegexOptions.IgnoreCase);
string strFiltered = rgx.Replace(strText, new MatchEvaluator(
// If we got a match, mark it as changed.
m_bChanged = true;
14. GetLowerFidelityBodyPart enumerates through all the available body type and return the plain text body:
/// Finds the lowest fidelity MimePart associated to a Body,
/// normally Text/Plain.
/// <param name="body">The body part that we want
/// to find the lowest fidelity MimePart for.</param>
public static MimePart GetLowerFidelityBodyPart(Body body)
// Nothing to work with if they are null...
// Divided here for logging purposes..
if (body == null)
else if (body.MimePart == null)
MimePart bodyPart = body.MimePart; ;
// If it is text already, then that's the lowest fidelity...
if (body.BodyFormat == BodyFormat.Text)
// Need to find lower fidelity body type at the same level.
// If no parent, there are no children, so this would be the only
// part at this level.
// If it has a parent but the parent is not multipart then this will also
// be the only child. so:
// if (bodyPart.Parent == null || !bodyPart.Parent.IsMultipart)
// The other option is to simply check that we have no siblings.
if (bodyPart.PreviousSibling == null && bodyPart.NextSibling == null)
// If we are here then we must have a parent and siblings. Get
// the parent, find the lowest fidelity..
IEnumerator<MimeNode> enumer = bodyPart.Parent.GetEnumerator();
currentPart = (MimePart) enumer.Current;
if (currentPart.ContentType.Equals(s_TextPlainContentType, StringComparison.OrdinalIgnoreCase))
15. The complete Visual Studio project can be found here. You can build the project by simply selecting build from the “Build” pull down menu using the release version.
16. Once the agent build is completed you can copy the StripIncomingLinkAgent from the \bin\release folder on the exchange server in the following folder:
C:\Program Files\Microsoft\Exchange Server\V15\TransportRoles\agents\SmtpAgents\StripIncomingLinkAgent
17. Use the following cmdlet to install and enable the agent on the Front End Transport on CAS. Note: if your CAS and Mailbox servers are on separate machine then you need to launch a window powershell to prevent the powershell proxy:
Launch a Window Powershell window and execute the following commands:
C:\Program Files\Microsoft\Exchange Server\V15\bin> Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn
Install-TransportAgent -Name "FrontEndStripIncomingLinkAgent" -TransportAgentFactory "StripIncomingLinkAgent.StripLinkReceiveAgentFactory" -AssemblyPath "C:\Program Files\Microsoft\Exchange Server\V15\TransportRoles\agents\SmtpAgents\StripIncomingLinkAgent\StripIncomingLinkAgent.dll" -TransportService FrontEnd
Enable-TransportAgent -Identity "FrontEndStripIncomingLinkAgent" -TransportService FrontEnd
18. User the following cmdlet to install and enable the agent on the back end Transport on Mailbox:
Install-TransportAgent -Name "StripIncomingLinkAgent" -TransportAgentFactory "StripIncomingLinkAgent.StripLinkReceiveAgentFactory" -AssemblyPath "C:\Program Files\Microsoft\Exchange Server\V15\TransportRoles\agents\SmtpAgents\StripIncomingLinkAgent\StripIncomingLinkAgent.dll” -TransportService Hub
Enable-TransportAgent -Identity "FrontEndStripIncomingLinkAgent" -TransportService Hub
19. After installing the agent on back end, send an email from the pickup folder with a few link such as (www.msn.com, etc.)
20. The agent will remove all the links from the email body. Please remember that this is a sample only.
David Santamaria, Nasir Ali and Nasser Salemizadeh