How to implement Drag & Drop from outlook mail or thunderbird to a delphi form?

LaBracca picture LaBracca · Jan 21, 2011 · Viewed 14k times · Source

Does anyone already implemented Drag & Drop of email messages from Outlook and/or Thunderbird (from now on "OT") to a Delphi form.

I need to give the user a way to store important emails in my application database without writing OT plugins. Currently they use this technique:

  1. from OT they click on an email,
  2. save as...
  3. save on desktop or temp folder,
  4. drag and drop the saved file on the Delphi form.

While after the modification I want to do:

  1. from OT they click on an email,
  2. drag and drop the saved file on the Delphi form.

So basically I implemented drag & drop from explorer. I need an extra layer that allows my application to see the email originally on OT as a normal file, so I can drag from OT as if it was a normal windows explorer window.

Note: I don't need to support all OT versions. I can accept not to support Outlook 2003 (for example) but not 2010. So in case the technique will not work automatically for all OT versions I will prefer the one that works with the latest.

Final note: It is obvious anyway I am interested only in dragging & dropping emails (and not Outlook Calendar items, for example). An idea would be dragging and Dropping attachments too. But this could be an extra improvement for the future.

Answer

Cosmin Prund picture Cosmin Prund · Jan 21, 2011

First of all, if you can find a ready made library that does this out of the box (like the one suggested by ldsandon) use it, because doing all of this by hand is painful and frustrating. The documentation is at times incomplete and might contain bugs: you'll end up doing stuff by trial and error and Google will not save you because not a lot of people delve into the depths of Ole drag-and-drop, and most of those that do will probably use ready-made code.

How to do this in plain Pascal

Theoretically the API that's used to make your application handle OLE drops is very simple. All you need to do is provide a implementation of the IDropTarget interface that does what you need and call RegisterDragDrop providing the handle for your application's window and the interface.

Here's how my implementation looks like:

  TDropTargetImp = class(TInterfacedObject, IDropTarget)
  public
    function DragEnter(const dataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint): HResult; stdcall;
    function DragOver(grfKeyState: Longint; pt: TPoint; var dwEffect: Longint): HResult; stdcall;
    function DragLeave: HResult; stdcall;
    function Drop(const dataObj: IDataObject; grfKeyState: Longint; pt: TPoint; var dwEffect: Longint): HResult; stdcall;
  end;

The implementation of DragEnter, DragOver and DragLeave is trivial, considerring I'm doing this for an experiment: I'll just accept everything:

function TDropTargetImp.DragEnter(const dataObj: IDataObject; grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
begin
  dwEffect := DROPEFFECT_COPY;
  Result := S_OK;
end;

function TDropTargetImp.DragLeave: HResult;
begin
  Result := S_OK;
end;

function TDropTargetImp.DragOver(grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
begin
  dwEffect := DROPEFFECT_COPY;
  Result := S_OK;
end;

The real work will be done in TDropTargetImp.Drop.

function TDropTargetImp.Drop(const dataObj: IDataObject; grfKeyState: Integer; pt: TPoint; var dwEffect: Integer): HResult;
var iEnum: IEnumFORMATETC;
    DidRead:LongInt;
    F: TFormatEtc;
    STG:STGMEDIUM;
    Response:Integer;

    Stream:IStream;

    Storage: IStorage;
    EnumStg: IEnumStatStg;
    ST_TAG: STATSTG;

    FileStream: TFileStream;
    Buff:array[0..1023] of Byte;
begin
  if dataObj.EnumFormatEtc(DATADIR_GET, iEnum) = S_OK then
  begin

    {
    while (iEnum.Next(1, F, @DidRead) = S_OK) and (DidRead > 0) do
    begin
      GetClipboardFormatName(F.cfFormat, FormatName, SizeOf(FormatName));
      ShowMessage(FormatName + ' : ' + IntToHex(F.cfFormat,4) + '; lindex=' + IntToStr(F.lindex));
    end;
    }

    ZeroMemory(@F, SizeOf(F));
    F.cfFormat := $C105; // CF_FILECONTENTS
    F.ptd := nil;
    F.dwAspect := DVASPECT_CONTENT;
    F.lindex := 0{-1}; // Documentation says -1, practice says "0"
    F.tymed := TYMED_ISTORAGE;

    Response := dataObj.GetData(F, STG);
    if Response = S_OK then
      begin
        case STG.tymed of
          TYMED_ISTORAGE:
            begin
              Storage := IStorage(STG.stg);
              if Storage.EnumElements(0, nil, 0, EnumStg) = S_OK then
              begin
                while (EnumStg.Next(1, ST_TAG, @DidRead) = S_OK) and (DidRead > 0) do
                begin
                  if ST_TAG.cbSize > 0 then
                  begin
                  Response := Storage.OpenStream(ST_TAG.pwcsName, nil, STGM_READ or STGM_SHARE_EXCLUSIVE, 0, Stream);
                  if Response = S_OK then
                    begin
                      // Dump the stored stream to a file
                      FileStream := TFileStream.Create('C:\Temp\' + ST_TAG.pwcsName + '.bin', fmCreate);
                      try
                        while (Stream.Read(@Buff, SizeOf(Buff), @DidRead) = S_OK) and (DidRead > 0) do
                          FileStream.Write(Buff, DidRead);
                      finally FileStream.Free;
                      end;
                    end
                  else
                    case Response of
                      STG_E_ACCESSDENIED: ShowMessage('STG_E_ACCESSDENIED');
                      STG_E_FILENOTFOUND: ShowMessage('STG_E_FILENOTFOUND');
                      STG_E_INSUFFICIENTMEMORY: ShowMessage('STG_E_INSUFFICIENTMEMORY');
                      STG_E_INVALIDFLAG: ShowMessage('STG_E_INVALIDFLAG');
                      STG_E_INVALIDNAME: ShowMessage('STG_E_INVALIDNAME');
                      STG_E_INVALIDPOINTER: ShowMessage('STG_E_INVALIDPOINTER');
                      STG_E_INVALIDPARAMETER: ShowMessage('STG_E_INVALIDPARAMETER');
                      STG_E_REVERTED: ShowMessage('STG_E_REVERTED');
                      STG_E_TOOMANYOPENFILES: ShowMessage('STG_E_TOOMANYOPENFILES');
                      else
                        ShowMessage('Err: #' + IntToHex(Response, 4));
                    end;
                  end;
                end;
              end;
            end
          else
            ShowMessage('TYMED?');
        end;
      end
    else
      case Response of
        DV_E_LINDEX: ShowMessage('DV_E_LINDEX');
        DV_E_FORMATETC: ShowMessage('DV_E_FORMATETC');
        DV_E_TYMED: ShowMessage('DV_E_TYMED');
        DV_E_DVASPECT: ShowMessage('DV_E_DVASPECT');
        OLE_E_NOTRUNNING: ShowMessage('OLE_E_NOTRUNNING');
        STG_E_MEDIUMFULL: ShowMessage('STG_E_MEDIUMFULL');
        E_UNEXPECTED: ShowMessage('E_UNEXPECTED');
        E_INVALIDARG: ShowMessage('E_INVALIDARG');
        E_OUTOFMEMORY: ShowMessage('E_OUTOFMEMORY');
        else
         ShowMessage('Err = ' + IntToStr(Response));
      end;

  end;
  Result := S_OK;
end;

This code accepts the "Drop", looks for some CF_FILECONTENTS, opens it up as TYMED_ISTORAGE, drops every single stream in that storage to a file in C:\Temp\<stream_name>.bin; I tried this with Delphi 2010 and Outlook 2007, it works all right: Opening up those saved files (lots of them!) I can find everything from the email message, in unexpected ways. I'm sure there's documentation somewhere that explains exactly what every one of those files is supposed to contain, but I don't really care about accepting drag-and-dropped files from Outlook so I didn't look to far. Again, ldsandon's link looks promising.

This codes looks fairly short, but that's not the source of the difficulties. The documentation for this was really lacking; I hit road blocks at every corner, starting with this:

F.lindex := 0{-1}; // Documentation says -1, practice says "0"

Msdn's documentation clear says the only valid value for "lindex" is -1: guess what, -1 doesn't work, 0 does!

Then there's this short line of code:

Response := Storage.OpenStream(ST_TAG.pwcsName, nil, STGM_READ or STGM_SHARE_EXCLUSIVE, 0, Stream);

specifically, those two consts:

STGM_READ or STGM_SHARE_EXCLUSIVE

getting that combination was a matter of trial-and-error. I don't like trial and error: Is that the optimal combination of flags for what I want? Will that work on every platform? I don't know...

Then there's the matter of making heads or tail of the actual content received from Outlook. For example the SUBJECT of the email was found in this stream: __substg1.0_800A001F. The body of the message was found in this stream: __substg1.0_1000001F. For an simple email message I got 59 streams of non-zero size.