Thursday, September 20, 2007

Email and Attachment Archiving with a Transport Agent on Exchange 2007

I’ve been continuing on with building and learning about Transport Agents over the past couple of weeks and thought I’d share an agent I’ve found useful. The following agent is a simple archiving agent it saves the serialized version of the message from the Mimedocument class to an eml file in a directory assigning it a GUID as a filename to make sure its unique. It also enumerates though the attachments of a message and saves them to a separate directory using the attachment filename and the message guid to link the message and attachments. I also added some code into to delete pdf files that where smaller than 20 KB this was for testing purposes but it’s something I’ve used in the past in SMTP sinks to overcome certain issues.

Like the last Agent I posted this is a Routing agent I’m running on Hub Server the code is relatively simple to follow. To do the attachment processing I’ve used the new EmailMessage class that’s part of the Microsoft.Exchange.Data.Transport.Email namespace. The cool thing about this class is it does provide a level of abstraction above TNEF and MIME. So if say you’re sending a meeting appointment internally to another user and you have attached a document if you where to parse this at the MIME level the message and attachment would be in TNEF format (good old winmail.dat) but the EmailMessage class allows you to enumerate though any attachments in the calendar invitation without needing to worry about using the lower level TNEF parsers. The one complaint I have about this class is that downloading an attachment is a little bit of a pain. After initially having problems with streams that would get corrupted intermittently I found Jon Skeet’s page http://www.yoda.arachsys.com/csharp/readbinary.html which had a function that worked well.

The other challenge I had was with removing particular attachments, generally deleting objects while enumerating though a collection isn’t the best of programming practices. Sometimes enumerating the collection in reverse can overcome this issue but for some reason when I did this with the attachment collection it would always give me an issue when I removed an attachment. So the solution I came up with for this was just to store the attachments that I wanted to delete in an arraylist during the initial attachment check and then loop though the arraylist at the end and delete those objects which seemed to work okay.

The one thing this agent is yet to handle is processing attachments within embedded messages which I think will be a separate post.

I’ve put a download of the code from this post here the code itself looks like

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;
using Microsoft.Exchange.Data.Transport;
using Microsoft.Exchange.Data.Mime;
using Microsoft.Exchange.Data.Transport.Email;
using Microsoft.Exchange.Data.Transport.Smtp;
using Microsoft.Exchange.Data.Transport.Routing;
using Microsoft.Exchange.Data.Common;

namespace msgdevExchangeRoutingAgents
{
public class EmailArchivingFactory : RoutingAgentFactory
{
public override RoutingAgent CreateAgent(SmtpServer server)
{
RoutingAgent raXheader = new EmailArchivingRoutingAgent();
return raXheader;
}
}
}

public class EmailArchivingRoutingAgent : RoutingAgent
{
public EmailArchivingRoutingAgent()
{
base.OnSubmittedMessage += new SubmittedMessageEventHandler(EmailArchivingRoutingAgent_OnSubmittedMessage);
}

void EmailArchivingRoutingAgent_OnSubmittedMessage(SubmittedMessageEventSource source, QueuedMessageEventArgs e)
{
//Archive Message
String MessageGuid = Guid.NewGuid().ToString();
Stream fsFileStream = new FileStream(@"C:\temp\archive\messages\" + MessageGuid + ".eml", FileMode.OpenOrCreate);
e.MailItem.Message.MimeDocument.WriteTo(fsFileStream);
fsFileStream.Close();
//Archive Any Attachments Check for pdf attachments under 20 K and delete
ArrayList adAttachmenttoDelete = new ArrayList();
for (int index = e.MailItem.Message.Attachments.Count - 1; index >= 0; index--)
{
Attachment atAttach = e.MailItem.Message.Attachments[index];
if (atAttach.AttachmentType == AttachmentType.Regular & atAttach.FileName != null)
{
FileStream atFileStream = File.Create(Path.Combine(@"C:\temp\archive\attachments\", MessageGuid + "-" + atAttach.FileName));
Stream attachstream = atAttach.GetContentReadStream();
byte[] bytes = ReadFully(attachstream, (int)attachstream.Length);
atFileStream.Write(bytes, 0, bytes.Length);
atFileStream.Close();
atFileStream = null;
bytes = null;
// Find Any PDF attachments less then 20 KB
if (atAttach.FileName.Length >= 3)
{
String feFileExtension = atAttach.FileName.Substring((atAttach.FileName.Length - 4), 4);
if (feFileExtension.ToLower() == ".pdf" & attachstream.Length < attachstream =" null;" atattach =" null;" enumerator =" adAttachmenttoDelete.GetEnumerator();" initiallength =" 32768;" buffer =" new" read =" 0;" chunk =" stream.Read(buffer,"> 0)
{
read += chunk;

// If we've reached the end of our buffer, check to see if there's
// any more information
if (read == buffer.Length)
{
int nextByte = stream.ReadByte();

// End of stream? If so, we're done
if (nextByte == -1)
{
return buffer;
}

// Nope. Resize the buffer, put in the byte we've just
// read, and continue
byte[] newBuffer = new byte[buffer.Length * 2];
Array.Copy(buffer, newBuffer, buffer.Length);
newBuffer[read] = (byte)nextByte;
buffer = newBuffer;
read++;
}
}
// Buffer is now too big. Shrink it.
byte[] ret = new byte[read];
Array.Copy(buffer, ret, read);
return ret;
}
}




14 comments:

Michal said...

Nice piece of code. I just start with Ex 2007 transport agents and this code will be helpful.

Do you know if it is possible to debug managed transport agents? I'm attaching to MSExchangeTransport.exe process but without any success.

Anony\ said...

Hello Glen ,

I am working on a problem where I want to decode a TNEF encoded MIME message. I am using the class Base64Decode in Microsoft.Exchange.data.Mime namespace and I am also able to decode the message if it doesn't contain an attachment . But , for a TNEF encoded message without attachment , the conversion fails. Do you have any suggestions ? Please help .

Anony\ said...

In addition to that , could you provide me some sample code to parse a MIME message using MimeDocument,MimeReader and Tnef Reader classes?

Thanks

Glen said...

Dealing with TNEF does require that you understand the actually format of a TNEF message i would suggest having a read first of http://msdn.microsoft.com/en-us/library/cc425498(EXCHG.80).aspx

You might also want to look at a TNEF View like http://www.windeveloper.com/OLView/OLView.htm

TNEF isn't any easy format to work with or describe these two things should help a lot. There's also a book called inside Mapi that is quite old be about the best reference around.

I dont have any good sample code for the TNEFwritter/Reader the stuff i have is pretty rough let me try and clean it up a bit and get back to you.

Cheers
Glen

ulita said...

good job....but when I open archive *.eml with outlook express, I find body is empty. How to fix it? THANKS...
^___^

Glen said...

This will generally be the case if the message is in TNEF format which is the case if it was sent from an Internal User. If you open the message in Notepad you can see the format its in. I dont really have at the moment of converting these message from TNEF to MIME

Cheers
Glen

ulita said...

THANKS, I will try....
^____^

Anonymous said...

I try to save te attachment to another server, but get access denied. Any idea which user right to set? thanks. greatt job

Anonymous said...

Found the rights problem. The Exchange server machine must be granted rights on the directory where you want to write the attachments

Anonymous said...

Hello Glen,

I have implemented your code in a transport agent. I see the body/TNEF issue discussed above. Do you know of a way I can get the body to come over as part of the .eml file when saving it to disk?

Cheers,

Russ

Anonymous said...

Hello Glen thanks so much for posting this, it's a great help as I'm learning about transport agents. I'm trying to figure out how to make this script work but depend on the sender address but not sure how to do this. I think
e.MailItem.Message.From.SmtpAddress is the right code to use but not sure how to implement.

-Vince

Glen said...

Okay but i don't understand what your question is maybe http://gsexdev.blogspot.com/2009/01/from-address-rewriting-in-transport.html is a better example for you to look at.

Cheers
Glen

Anonymous said...

Sorry my question wasn't clear. I only want this agent to run for a specific sender address and subject or Recipient and subject would also work.
Ex: only if from test@domain.com and subject is "test agent". I don't want it to run for everyone.

-Vince

Glen said...

Agents run on every mail that the Hub or Edge processes there is no way to limit this. You need to put some filtering at the top of your code if you want to limit it based on a particular address so it only run as little as your code as possible.

Cheers
Glen