Click here to Skip to main content
Click here to Skip to main content

Outlook Drag and Drop in C#

, 28 Jul 2008
Rate this:
Please Sign up or sign in to vote.
How to drag and drop multiple Outlook mail messages or message attachments to a C# WinForm.

Demo application form

Introduction

A project came up at work the other day requiring the ability to drag and drop any number of mail messages or mail message attachments from Otlook into a WinForms application... Easy I thought, this has to be a common problem. I will just jump on CodeProject, find an example, and with a bit of tweaking, be running in no time. Now, if you are here, I am sure you now know how little information there is available on this topic, so I thought I would add to the very small pool with a complete example of how to drag and drop mail items or attachments from Outlook into a WinForms application without using the Outlook object model.

I am going to skip over explaining all the creating a form, allowing drag and drop, etc., stuff that you can find a million places on the Internet, and just focus on the code to make drag and drop from Outlook work. If you feel lost, go find another article with more basic drag and drop information, get a fully functional drag and drop app, then come back here and work from that code.

Using the Code

When I started writing the code, I decided the easiest way to work the functionality into the existing application was to create a new class that implemented the IDataObject interface that is normally provided when dragging and dropping onto a WinForm. The new class was to catch any calls to Outlook specific data formats, and pass all other calls through to the original IDataObject. Below, you can see how easy it is to use the class in a DragDrop event handler. The FileGroupDescriptor format returns a string array containing the names of each file dropped instead of the usual MemoryStream you would be used to if you have tackled this yourself, and the FileContents returns a MemoryStream array containing the binary contents of each file dropped.

private void Form1_DragDrop(object sender, DragEventArgs e)
{
    //wrap standard IDataObject in OutlookDataObject
    OutlookDataObject dataObject = new OutlookDataObject(e.Data);
    
    //get the names and data streams of the files dropped
    string[] filenames = (string[])dataObject.GetData("FileGroupDescriptor");
    MemoryStream[] filestreams = (MemoryStream[])dataObject.GetData("FileContents");

    for (int fileIndex = 0; fileIndex < filenames.Length; fileIndex++)
    {
        //use the fileindex to get the name and data stream
        string filename = filenames[fileIndex];
        MemoryStream filestream = filestreams[fileIndex];

        //save the file stream using its name to the application path
        FileStream outputStream = File.Create(filename);
        filestream.WriteTo(outputStream);
        outputStream.Close();
    }
}

Understanding the Code

To understand what the OutlookDataObject class above is doing to get the file information, there are two things to take note of. The first is that information for the file names is returned from Outlook in a MemoryStream, which is actually a representation of the FILEGROUPDESCRIPTORA or FILEGROUPDESCRIPTORW structures. The second is that to get the file contents, you need the ability to specify an index to get anything past the first file, and the standard IDataObject does not expose this ability. All this is explained in detail below.

Getting the File Names

There are two versions of the file details returned from the IDataObject in the FileGroupDescriptor and FileGroupDescriptorW formats which map to the FILEGROUPDESCRIPTORA and FILEGROUPDESCRIPTORW structures, respectively. In this article, I will focus on the FileGroupDescriptor format which is the ASCII version; FileGroupDescriptorW (W for wide) is the Unicode version, and you will need to use it when working with non-ASCII file names, but they are handled in the same way.

//use the IDataObject to get the FileGroupDescriptor as a MemoryStream
MemoryStream fileGroupDescriptorStream = (MemoryStream)e.Data.GetData("FileGroupDescriptor");

Most examples you will see involve taking the MemoryStream above and converting each non-null byte from index 76 onwards to a char and appending that to a string. While this works adequately for one file drop, it gets a bit tricky when dropping more than that. The correct way is to take the returned bytes and cast it to a FILEGROUPDESCRIPTORA structure, which holds a count of items and an array of FILEDESCRIPTORA structures, which holds the file details. The definitions of these structures can be seen below.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public sealed class FILEGROUPDESCRIPTORA
{
    public uint cItems;
    public FILEDESCRIPTORA[] fgd;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public sealed class FILEDESCRIPTORA
{
    public uint dwFlags;
    public Guid clsid;
    public SIZEL sizel;
    public POINTL pointl;
    public uint dwFileAttributes;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
    public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
    public uint nFileSizeHigh;
    public uint nFileSizeLow;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
    public string cFileName;
}

Now that the foundations are set, let's get into the code to actually convert the returned MemoryStream into something usable like a string array of file names. This involves putting the raw bytes into unmanaged memory and using Marshal.PtrToStructure to bring it back in as a structure. There is some extra marshalling in the code because the fgd array of the FILEGROUPDESCRIPTORA structure doesn't get populated as the PtrToStructure method doesn't work with variable length arrays.

//use IDataObject to get FileGroupDescriptor
//as a MemoryStream and copy into a byte array
MemoryStream fgdStream = 
  (MemoryStream)e.Data.GetData("FileGroupDescriptor");
byte[] fgdBytes = new byte[fgdStream.Length];
fgdStream.Read(fgdBytes, 0, fgdBytes.Length);
fgdStream.Close();

//copy the file group descriptor into unmanaged memory
IntPtr fgdaPtr = Marshal.AllocHGlobal(fgdBytes.Length);
Marshal.Copy(fgdBytes, 0, fgdaPtr, fgdBytes.Length);

//marshal the unmanaged memory to a FILEGROUPDESCRIPTORA struct
object fgdObj = Marshal.PtrToStructure(fgdaPtr, 
                        typeof(NativeMethods.FILEGROUPDESCRIPTORA));
NativeMethods.FILEGROUPDESCRIPTORA fgd = 
             (NativeMethods.FILEGROUPDESCRIPTORA)fgdObj;

//create a array to store file names in
string[] fileNames = new string[fgd.cItems];

//get the pointer to the first file descriptor
IntPtr fdPtr = (IntPtr)((int)fgdaPointer + Marshal.SizeOf(fgdaPointer));

//loop for the number of files acording to the file group descriptor
for(int fdIndex = 0;fdIndex < fgd.cItems;fdIndex++)
{
    //marshal the pointer to the file descriptor as a FILEDESCRIPTORA struct
    object fdObj = Marshal.PtrToStructure(fdPtr, 
                          typeof(NativeMethods.FILEDESCRIPTORA));
    NativeMethods.FILEDESCRIPTORA fd = (NativeMethods.FILEDESCRIPTORA)fdObj;
    
    //get file name of file descriptor and put in array
    fileNames[fdIndex] = fd.cFileName;

    //move the file descriptor pointer to the next file descriptor
    fdPtr = (IntPtr)((int)fdPtr + Marshal.SizeOf(fd));
}

At this point, we have now converted the MemoryStream into a string array that contains the name of each file dropped, which is a lot easier to work with. Outlook messages get the name of their subject with ".msg" on the end, and for Outlook message attachments, the file name of the attachment.

Getting the File Contents

The file contents sit behind the FileContents format. If you drag and drop a single attachment, then the default IDataObject works as expected and will return a MemoryStream containing that file's data. Things get more complex when dragging multiple attachments or Outlook email messages for different reasons. Multiple attachments pose an issue because the OS calls for drop data allow for an index to be specified, but the C# implementation of the IDataObject doesn't expose this directly. Mail messages are an issue because the OS call returns an IStorage which is a compound file type, and again, the C# implementation of the IDataObject lets us down by not handling this type of return, so you get a null.

Specifying an Index

To get at the content of multiple dropped files, an index needs to be specified to indicate which file contents are required. The default IDataObject doesn't allow this, but it can be cast to a COM IDataObject which will accept a FORMATETC structure that has an index property that can be set to indicate the file contents required.

//cast the default IDataObject to a com IDataObject
System.Runtime.InteropServices.ComTypes.IDataObject comDataObject;
comDataObject = (System.Runtime.InteropServices.ComTypes.IDataObject)e.Data;

//create a FORMATETC struct to request the data with from the com IDataObject
FORMATETC formatetc = new FORMATETC();
formatetc.cfFormat = (short)DataFormats.GetFormat(format).Id;
formatetc.dwAspect = DVASPECT.DVASPECT_CONTENT;
formatetc.lindex = 0; //zero based index to retrieve
formatetc.ptd = new IntPtr(0);
formatetc.tymed = TYMED.TYMED_ISTREAM | TYMED.TYMED_ISTORAGE | TYMED.TYMED_HGLOBAL;

//create STGMEDIUM to output request results into
STGMEDIUM medium = new STGMEDIUM();

//using the com IDataObject interface get the data using the defined FORMATETC
comDataObject.GetData(ref formatetc, out medium);

As you can see in the example above, by changing the value of the lindex property of the FORMATETC structure, we can change the index of the file contents to retrieve. The result of the call is sitting in the STGMEDIUM structure; this contains a pointer to the actual result in the unionmember property, and the type of result at the pointer in the tymed property. There are three types of returns available to the STGMEDIUM, and each one is explained below.

The Stream Result (TYMED_ISTREAM)

If the tymed property of the STGMEDIUM is TYMED_ISTREAM, then the result is a stream. This is normally handled by the default IDataObject, but when working with the COM IDataObject, the handling code needs to be written again.

//marshal the returned pointer to a IStream object
IStream iStream = (IStream)Marshal.GetObjectForIUnknown(medium.unionmember);
Marshal.Release(medium.unionmember);

//get the STATSTG of the IStream to determine how many bytes are in it
iStreamStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
iStream.Stat(out iStreamStat, 0);
int iStreamSize = (int)iStreamStat.cbSize;

//read the data from the IStream into a managed byte array
byte[] iStreamContent = new byte[iStreamSize];
iStream.Read(iStreamContent, iStreamContent.Length, IntPtr.Zero);

//wrapped the managed byte array into a memory stream
Stream filestream = new MemoryStream(iStreamContent);

The Storage Result (TYMED_ISTORAGE)

If the tymed property of the STGMEDIUM is TYMED_ISTORAGE, then the result is a storage which is a compound file type. This is a little more complex to process than the stream as it needs to be copied into a memory backed IStorage so its data can then be read from the backing memory store.

NativeMethods.IStorage iStorage = null;
NativeMethods.IStorage iStorage2 = null;
NativeMethods.ILockBytes iLockBytes = null;
System.Runtime.InteropServices.ComTypes.STATSTG iLockBytesStat;
try
{
    //marshal the returned pointer to a IStorage object
    iStorage = (NativeMethods.IStorage)
       Marshal.GetObjectForIUnknown(medium.unionmember);
    Marshal.Release(medium.unionmember);

    //create a ILockBytes (unmanaged byte array)
    iLockBytes = NativeMethods.CreateILockBytesOnHGlobal(IntPtr.Zero, true);
    
    //create a IStorage using the ILockBytes 
    //(unmanaged byte array) as a backing store
    iStorage2 = NativeMethods.StgCreateDocfileOnILockBytes(iLockBytes, 
                                                       0x00001012, 0);

    //copy the returned IStorage into the new memory backed IStorage
    iStorage.CopyTo(0, null, IntPtr.Zero, iStorage2);
    iLockBytes.Flush();
    iStorage2.Commit(0);

    //get the STATSTG of the ILockBytes to determine 
    //how many bytes were written to it
    iLockBytesStat = new System.Runtime.InteropServices.ComTypes.STATSTG();
    iLockBytes.Stat(out iLockBytesStat, 1);
    int iLockBytesSize = (int)iLockBytesStat.cbSize;

    //read the data from the ILockBytes 
    //(unmanaged byte array) into a managed byte array
    byte[] iLockBytesContent = new byte[iLockBytesSize];
    iLockBytes.ReadAt(0, iLockBytesContent, iLockBytesContent.Length, null);

    //wrapped the managed byte array into a memory stream
    Stream filestream = new MemoryStream(iStreamContent);
}
finally
{
    //release all unmanaged objects
    Marshal.ReleaseComObject(iStorage2);
    Marshal.ReleaseComObject(iLockBytes);
    Marshal.ReleaseComObject(iStorage);
}

The HGlobal Result (TYMED_HGLOBAL)

If the tymed property of the STGMEDIUM is TYMED_HGLOBAL, then the result is stored in a HGlobal. For the purposes of Outlook drag and drop, this type should never be returned, but for completeness, I use a little bit of Reflection on the original IDataObject to have that class handle it.

//get the internal ole dataobject and its GetDataFromHGLOBLAL method
BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo innerDataField = 
  e.Data.GetType().GetField("innerData", bindingFlags);
IDataObject oleDataObject = 
  (System.Windows.Forms.IDataObject)innerDataField.GetValue(e.Data);
MethodInfo getDataFromHGLOBLALMethod = 
  oleDataObject.GetType().GetMethod("GetDataFromHGLOBLAL", bindingFlags);

getDataFromHGLOBLALMethod.Invoke(oleDataObject, 
  new object[] { format, medium.unionmember });

Conclusion

Well, hopefully, that all helps someone. I have a few other Outlook tricks that I will be doing articles for; one is how to extract and save a message attachment without using the object model, another is how to use the code in this article to enable drag and drop of Outlook messages and attachments into IE (with appropriate security, so only good for the intranet).

History

  • 1 July 2008:
    • Original article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

David Ewen
Software Developer (Senior) Beacon
Australia Australia
I am currently working for Beacon Technology as a Senior Software Developer on WPF/WCF applications and have been working in the industry for 8 years. My day job usually involves programming with C# but I have been known to mess around with just about everything.

Comments and Discussions

 
QuestionVB.Net Implementation PinmemberEric Pfirman2-Mar-12 10:04 
AnswerRe: VB.Net Implementation PinmemberLatiNo15620-Aug-12 2:42 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140827.1 | Last Updated 29 Jul 2008
Article Copyright 2008 by David Ewen
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid