Step-by-step - How to create a routing agent

The information in this weblog is provided "AS IS" with no warranties, and confers no rights. This weblog does not represent the thoughts, intentions, plans or strategies of my employer. It is solely my opinion. Inappropriate comments will be deleted at the authors discretion. All code samples are provided "AS IS" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose.

This step-by-step procedure will show how you can create a new routing agent that adds an image disclaimer to the bottom of every outgoing html e-mail.
The sample will also show how you install,enable and test the agent.
 
Software needed:
Exchange 2007 SP1 or newer
Visual Studio 2008 SP1 or newer
 
If Visual Studio is installed on a different machine than Exchange server, copy microsoft.exchange.data.common.dll and microsoft.exchange.data.transport.dll from C:\Program Files\Microsoft\Exchange Server\Public to a folder on the Visual Studio machine.

Create a new Visual C# project using the "Class Library" template.

In the Solution Explorer window, right click References and choose Add Reference...
Click the Browse tab and find microsoft.exchange.data.common.dll and microsoft.exchange.data.transport.dll. Mark both files and click OK to add the reference.

The default file will look something like this

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AgentX
{
    public class AgentClass
    {
    }
}

Add additional using statements to make datatypes from the newly referenced Exchange assemblies visible.

using System.Text;
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Routing;

Modify the existing class so it inherits from the RoutingAgent class and add a second class called AgentFactory that inherits from RoutingAgentFactory.

namespace AgentX
{
    public sealed class AgentFactory : RoutingAgentFactory
    {
        public override RoutingAgent CreateAgent(SmtpServer server)
        {
            return new AgentClass();
        }
    }

    public class AgentClass : RoutingAgent
    {
        public AgentClass()
        {
        }
    }
}

A routing agent can handle 4 different events. Which event to handle depends on the business logic that the agent will implement. Here's the 4 different events and the order in which they occur.

OnSubmittedMessage - After message has entered the submit queue
OnResolvedMessage - After resolving the recipients but before determining the route.
OnRoutedMessage - After determining the route but before content conversion.
OnCategorizedMessage - After Content Conversion.

In this agent I choose the implement the OnRoutedMessage event.

Inside the AgentClass constructor, add the line

base.OnRoutedMessage += new RoutedMessageHandlerEventHandler(AgentClass_OnRoutedMessage);

Visual Studio will automatically offer to add the OnRoutedMessage event handler method for you. It should look like this

void AgentClass_OnRoutedMessage(RoutedMessageEventSource source, QueuedMessageEventArgs e)
{
}

If you are creating a different agent, add your business logic and skip down to step 12 for information about how to register and test the agent. The next step will add the logic that adds an image to every outgoing e-mail.

We need to add a using statements for System.IO, System.Diagnostics, Microsoft.Exchange.Data.Mime and Microsoft.Exchange.Data.ContentTypes.Tnef. The complete list of using statements should look like this

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Routing;
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Mime;
using Microsoft.Exchange.Data.ContentTypes.Tnef;

Next, add 2 global variables to the AgentClass. ContentID will store filename as it occurs in the html content of the e-mail and el is a variable that we use to write event log entries.

In the AgentClass constructor add code to initialize the event log sourceand in the event handler for OnRoutedMessage, make a method call to GetContentID to create a new random filename followed by a call to attachSignatureImage that will insert the image into the message.

Below is the complete code so far

public class AgentClass : RoutingAgent
{
    String ContentID;
    EventLog el;

    public AgentClass()
    {
        ContentID = "";
        base.OnRoutedMessage += new RoutedMessageEventHandler(AgentClass_OnRoutedMessage);           
    }

    void AgentClass_OnRoutedMessage(RoutedMessageEventSource source, QueuedMessageEventArgs e)
    {
        ContentID = GetContentID();
        attachSignatureImage("c:\\agentx", "file1.jpg", e.MailItem.Message);
    }

Add the 3 methods GetContentID, attachSignatureImage and
AttachmentFromTNEF as shown below. These 3 methods are all members of the AgentClass.

private String GetContentID()
{
    Random r = new Random();
    int num = r.Next(0, 1000000000);           
    return "file1.jpg@" + num.ToString();
}

private bool attachSignatureImage(String path, String showAsFilename, EmailMessage emailMessage)
{
    try
    {
        // Include attachment only, if HTML Format
        Body body = emailMessage.Body;
        if (body.BodyFormat == BodyFormat.Html)
        {
            Stream origBody;
            Stream newBody;
            if (!body.TryGetContentReadStream(out origBody))
                throw new InvalidDataException("Could not read html body.");

            Byte[] mailbodyarray = new Byte[origBody.Length];
            origBody.Read(mailbodyarray, 0, (int)origBody.Length);
            origBody.Close();

            Byte[] newbodyarray = new Byte[mailbodyarray.Length + ContentID.Length + 16];
            // insert <img src=cid:file1.jpg@1234> just before the </body> tag
            ASCIIEncoding enc = new ASCIIEncoding();           
            String str = enc.GetString(mailbodyarray);
            int i = str.LastIndexOf("</body>");
            str = str.Insert(i, "<img src=cid:" + ContentID + ">\r\n");
            newbodyarray = enc.GetBytes(str);
                   
            newBody = body.GetContentWriteStream();                   
            newBody.Write(newbodyarray, 0, newbodyarray.Length);                   
            newBody.Flush();
            newBody.Close();

            if (File.Exists(path + "\\" + showAsFilename))
            {
                Byte[] logo_binary = File.ReadAllBytes(path + "\\" + showAsFilename);
                if (logo_binary.Length > 0)
                {
                    Attachment logo = emailMessage.Attachments.Add(showAsFilename, "image/jpeg");
                    Stream logo_stream = logo.GetContentWriteStream();
                    logo_stream.Write(logo_binary, 0, logo_binary.Length);
                    logo_stream.Flush();
                    logo_stream.Close();

                    // If MIME TYPE is used, then add header in Mime format
                    if (logo.MimePart != null)
                    {
                        Header dis = logo.MimePart.Headers.FindFirst(HeaderId.ContentDisposition);
                        dis.Value = "inline";

                        Header hContentId = Header.Create(HeaderId.ContentId);
                        hContentId.Value = "<" + ContentID + ">";
                        logo.MimePart.Headers.AppendChild(hContentId);
                    }
                    //  If TnefPart (if sent from Outlook 2003/2007), add headers within a Tnef routine
                    else if (emailMessage.TnefPart != null)
                        AttachmentFromTNEF(ref emailMessage);
                    return true;
                }
            }
            else
            {
                // unable to open image file.
            }
        }
    }
    catch (InvalidOperationException ioex)
    {        
    }
    return false;
}

private void AttachmentFromTNEF(ref EmailMessage emailMessage)
{
    TnefReader reader = new TnefReader(emailMessage.TnefPart.GetContentReadStream(), 0, TnefComplianceMode.Loose);
    short key = reader.AttachmentKey;
    TnefWriter writer = new TnefWriter(emailMessage.TnefPart.GetContentWriteStream("Binary"), key);
    int attachmentCount = 0;
    int foundFile = 0;
    try
    {               
        while (reader.ReadNextAttribute())
        {
            if (reader.AttributeLevel == TnefAttributeLevel.Attachment)
            {                       
                if (reader.AttributeTag == TnefAttributeTag.AttachRenderData)
                    attachmentCount++;
               
                switch (reader.AttributeTag)
                {
                    case TnefAttributeTag.AttachTitle:
                        writer.StartAttribute(reader.AttributeTag, reader.AttributeLevel);
                        if (attachmentCount > 0)
                        {
                            while (reader.PropertyReader.ReadNextProperty())
                            {                                       
                                if (reader.PropertyReader.PropertyTag.Id == TnefPropertyId.AttachFilename)
                                {
                                    String pr = reader.PropertyReader.ReadValueAsString();
                                    // contentID will be file1.jpg@123456789 while pr is file1.jpg
                                    // if ContentID contains pr, set foundfile.
                                    if (ContentID.Contains(pr))
                                        foundFile = attachmentCount;
                                    writer.WriteProperty(reader.PropertyReader.PropertyTag, pr);
                                }
                                else
                                {
                                    writer.WriteProperty(reader.PropertyReader);
                                }
                            }
                        }
                        else
                        {
                            writer.WriteAllProperties(reader.PropertyReader);
                        }
                        break;
                    case TnefAttributeTag.Attachment:
                        if (foundFile == attachmentCount && attachmentCount > 0)
                        {
                            writer.StartAttribute(reader.AttributeTag, reader.AttributeLevel);
                            writer.WriteAllProperties(reader.PropertyReader);                                   
                                   
                            writer.StartProperty(new TnefPropertyTag(TnefPropertyId.AttachContentId, TnefPropertyType.String8));
                                    writer.WritePropertyValue(ContentID);
                        
                            int attachhidden;
                            int flags;
                            unchecked
                            {
                               attachhidden = (int)0x7FFe000B;
                               flags = (int)0x37140003;
                            }
                            writer.StartProperty(new TnefPropertyTag(attachhidden));
                            writer.WritePropertyValue(true);

                            writer.StartProperty(new TnefPropertyTag(flags));
                            writer.WritePropertyValue(4);  // ATT_MHTML_REF
                        }
                        else
                        {
                            writer.WriteAttribute(reader);
                        }
                        break;
                    default:
                        writer.WriteAttribute(reader);
                        break;
                }
            }
            else
            {
                // it's a message level attribute
                if (reader.AttributeTag == TnefAttributeTag.MapiProperties)
                {
                    writer.StartAttribute(TnefAttributeTag.MapiProperties, TnefAttributeLevel.Message);
                           
                    writer.WriteAllProperties(reader.PropertyReader);
                           
                    int tag;
                    int tag2;
                    unchecked
                    {
                        tag = (int)0x8514000B;
                        tag2 = (int)0x00008514;
                    }
                    Guid PSETID_Common = new Guid("00062008-0000-0000-C000-000000000046");                           
                    writer.StartProperty(new TnefPropertyTag(tag), PSETID_Common, tag2);                           
                    writer.WritePropertyValue(true);
                }
                else
                    writer.WriteAttribute(reader);
            }
        }               
    }
    catch (Exception x)
    {
        // exception
    }
    finally
    {
        writer.Close();
        reader.Close();
    }
}

The code above will load the image from a file on disk and insert it into the Tnef part of the message. It will then add 3 properties.
PidLidSmartNoAttach (https://msdn.microsoft.com/en-us/library/cc839557.aspx),
PidTagAttachmentHidden (PR_ATTACHMENT_HIDDEN) (https://msdn.microsoft.com/en-us/library/cc839828.aspx) and
PidTagAttachFlags (PR_ATTACH_FLAGS) (https://msdn.microsoft.com/en-us/library/cc765876.aspx).

To install the Transport Agent, copy it to the Exchange server and run the following Powershell command.

Install-TransportAgent -Name AgentX -TransportAgentFactory "AgentX.AgentFactory" -AssemblyPath "C:\projects\MyAgent\MyAgent\Debug\MyAgent.dll"

This will register the agent with SMTP but will not enable it. To enable the agent, run the command

Enable-TransportAgent -Name "AgentX"

Also make sure that the image file is located in the folder hardcoded in the source code (see step 10).

Now that the agent is installed and enabled, we can go ahead and test the agent.
Run OWA and send a short message to another user. The image file should have been inserted as an inline attachment at the bottom of the message.
If the message is received without an attachment, have a look in the Application event log for an event containing more information about the problem.