Send to mail recipient

part II

The continuation of part I starts where the other article ended. It provides basically another technique in order to mimic the windows explorer Send To Mail recipient shortcut.

In part I, the COM IDataObject interface unknown behaviors have been demystified. In part II, we basically use simple shell techniques. By the way, the shell technique does not cross the Windows NT limitations regarding shell (separate shfolder.exe installation whenever developer use shell-specific API methods), so it is expected to work on all operating systems.

Why the shell then? There is no particular requirement to go the shell way, but I'll show the technique only as a matter of exemplifying another way to achieve the same. By the way, I didn't start the work from scratch. Quite the contrary, I took a code from codeproject that tries to mimic a windows explorer, extracted what was relevant to Send to mail recipient, and then extended it to support multiple files in a single action.

 

Sending only one file to the mail recipient

There is something particularly intriguing about the Send to mail shortcut. It's a file that resides in one's personal SendTo folder, namely C:\document and settings\<username>\SendTo. If you take a look in this folder, guess what you'll find? many shortcuts, and a "Mail recipient" entry without any arrow in the icon, showing that it's not really a shortcut. And no it isn't, "Mail recipient" is actually a file named "Mail recipient.MAPIMAIL" and obviously the MAPIMAIL file extension is hardcoded in the Windows explorer and is being played a particular role. Part of this role is that the file extension is removed so users don't even try to understand the implications.

The funny implication of this is that there is no real Send to mail recipient shortcut. To do the same just try out this sequence :

That actual meaning of the Send to mail recipient shortcut is what we take advantage of in the code below, that's why there is no need to lookup existing SendTo shortcuts at all. This is quite clear in the code below :


  ::CoInitialize(NULL);

  LPSHELLFOLDER sfDesktop = NULL;

  SHGetDesktopFolder(&sfDesktop);

  //
  // catch an arbitrary file with .MAPIMAIL extension name
  //
  LPITEMIDLIST pidlSendToMail;
  ULONG chEaten = 0;

  HRESULT hr = sfDesktop->ParseDisplayName(NULL, NULL, 
                                L"c:\\arbitraryfile.MAPIMAIL",
                                &chEaten,
                                &pidlSendToMail,
                                NULL);

  //
  // invoke command
  //

  // the following file becomes the email attachment
  CString m_sAbsPath = "c:\\noIE.png";

  CPIDL pidlFile( m_sAbsPath.GetBuffer(0) );
  CPIDL pidlDrop;
  LPDROPTARGET pDT;
  LPDATAOBJECT pDO;

  hr = pidlFile.GetUIObjectOf(IID_IDataObject, (LPVOID *)&pDO, NULL, sfDesktop);
  if ( SUCCEEDED(hr) )
  {
    pidlDrop.Set(pidlSendToMail);
    hr = pidlDrop.GetUIObjectOf(IID_IDropTarget, 
                                (LPVOID *)&pDT, 
                                NULL);

    if (SUCCEEDED(hr))
    {
      // do the drop
      POINTL pt = { 0, 0 };
      DWORD dwEffect = DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK;
      hr = pDT->DragEnter(pDO, MK_LBUTTON, pt, &dwEffect);

      if (SUCCEEDED(hr) && dwEffect) 
      {
         hr = pDT->Drop(pDO, MK_LBUTTON, pt, &dwEffect);
      } 
      else 
      {
         hr = pDT->DragLeave();
      }

      pDT->Release();
    }

    pidlDrop.m_pidl = NULL;

  }


  //
  // release the stuff
  //
  sfDesktop->Release();

  ::CoUninitialize();

Before this piece of code compiles, you need to include the PIDL.h PIDL.cpp implementation which is a helper class that hides the technical details behind the translation from a file path to a shell file ID (called an ITEMIDLIST), and vice versa. So besure to include those files.

Before the code works, make sure to create a file called c:\arbitraryfile.MAPIMAIL either by hand, or programmatically. This is good to create such a file in a temporary folder for instance.

This is enough code to mimic the drag and drop of a single file onto a .MAPIMAIL and, as a result, make the default email client appear with a new email and that file in attachment. Be sure to note that, in debug version particularly, there is some latency before the email pops up. It can be up to ten seconds from my own experiments. It drops down to one second in release mode, which is fine.

At this point we still have a problem, the email popup will be automatically destroyed unless we make a pause, which is not exactly what we expect. This issue is covered later in this article.

Before we get to support more than one file, some further explanations are needed. If we want to pass it more than one file, we can't simply put a separator like a comma, an end of line, etc. It won't work. Worse than this, the some shell API method will miserably fail and even cause a GPF.

So we need to find a way to pass a collection of file descriptors. Below is a schema explaining how a fully qualified filepath (c:\tmp\myfile.txt) gets translated in terms of ITEMIDLIST, a list of SHITEMID blocks.


Translating a file path into SHITEMID unit blocks

 

Sending multiple files to the mail recipient

Given how file paths are translated in the shell vocabulary, in order to be able to drag and drop multiple files instead of only one, we need to make another assumption. This assumption is that all of the files being drag and dropped are in a same folder.

If all files are from the same folder, then the shell API provides us with a particular use of one of the methods that allows us to pass a collection of SHITEMID blocks, one for each file. That method is IShellFolder::GetUIObjectOf(...) and is documented like this :

IShellFolder::GetUIObjectOf

Retrieves an OLE interface that can be used to carry out actions 
on the specified file objects or folders.

HRESULT GetUIObjectOf(
    HWND hwndOwner,
    UINT cidl,
    LPCITEMIDLIST *apidl,
    REFIID riid,
    UINT *rgfReserved,
    VOID **ppv
   );

Parameters
hwndOwner [in] Handle to the owner window that the client should 
               specify if it displays a dialog box or message box. 
cidl [in] Number of file objects or subfolders specified in the apidl 
          parameter. 
apidl [in] Address of an array of pointers to ITEMIDLIST structures, 
           each of which uniquely identifies a file object or subfolder 
           relative to the parent folder. Each item identifier list must 
           contain exactly one SHITEMID structure followed by a 
           terminating zero. 
riid [in] Identifier of the COM interface object to return. This can be 
          any valid interface identifier that can be created for an item.
          The most common identifiers used by the Shell are listed in 
          the comments at the end of this reference. 
prgfInOut Reserved. 
ppvOut [out] Pointer to the requested interface. If an error occurs, a 
             NULL pointer is returned in this address. 

Clearly we need some hashing technique out there since we must extract the parent folder first, translate it into a usable ITEMIDLIST. And then we need to get an ITEMIDLIST for each file, but because the ITEMIDLIST provided by the shell is for the full qualified path, we need to only retain the last block, then chain the last blocks together and pass it as an ITEMIDLIST collection in the apidl parameter above.

This code is an application logic that needs not be reflected at the user level, that's why we deliberately enhance the PIDL implementation by providing it with the following feature, the ability to find the last block from a collection of unit blocks, which is done by the new method LPBYTE CPIDL::GetLastChunkPtr(), and the ability to put all last blocks together, which is done by a new GetUIObjectOf method implementation. All of this is sumed up below.

Below is the code added to the PIDL class in order to manage group of files from a given folder :

LPBYTE CPIDL::GetLastChunkPtr()
{
  LPSHITEMID  piid = GetFirstItemID(); // shell helper 
  LPBYTE      pLastChunk = NULL;
    
  if (piid) {
    do {
      pLastChunk = (LPBYTE) piid;
      GetNextItemID(piid);
    } while (piid->cb);
  }
    
  return pLastChunk;
}

// where GetFirstItemID() and GetNextItemID(...) are implemented as :

// Returns a pointer to the first item in the list
LPSHITEMID GetFirstItemID() const { return (LPSHITEMID)m_pidl; }

// Points to the next item in the list
void GetNextItemID(LPSHITEMID& pid) const 
{ (LPBYTE &)pid += pid->cb; }



// pass multiple files
HRESULT CPIDL::GetUIObjectOf(REFIID riid, LPVOID *ppvOut, HWND hWnd, LPSHELLFOLDER psf,
						int nbpidls, CPIDL* arrpidls)
{
  LPSHELLFOLDER   psfParent;

  // pass the folder id
  HRESULT hr = psf->BindToObject(*this, NULL, 
                                 IID_IShellFolder, (LPVOID *)&psfParent);
  if ( SUCCEEDED(hr) ) 
  {
    // build an array of LPITEMIDLIST
    // each file is reflected by an LPITEMIDLIST 
    // which is typically a single data chunk followed by a double 0
    //
    LPITEMIDLIST* arrITEMIDLIST = new LPITEMIDLIST[nbpidls];
    for (int i = 0; i < nbpidls; i++)
      arrITEMIDLIST[i] = (LPITEMIDLIST) arrpidls[i].GetLastChunkPtr();

    // pass the array of file ids
    hr = psfParent->GetUIObjectOf(hWnd, 
                                  nbpidls, (LPCITEMIDLIST*) arrITEMIDLIST, 
                                  riid, 0, ppvOut);
    psfParent->Release();
  }

  return hr;
}

All what is left to do for the user is to prepare a collection of CPIDL instances for each file to be processed. A CPDIL instance is an instance of the PIDL implementation and is straight forward to use. Just create a new instance and call the Set(szfilepath) method to pass it the file. Suppose we have two files, c:\noIE.png and c:\winzip.log. The code is as follows :


  //
  #include "PIDL.h"
  //

  ::CoInitialize(NULL);
  
  LPSHELLFOLDER sfDesktop = NULL;

  SHGetDesktopFolder(&sfDesktop);

  //
  // catch an arbitrary file with .MAPIMAIL extension name
  //
  LPITEMIDLIST pidlSendToMail;
  ULONG chEaten = 0;

  HRESULT hr = sfDesktop->ParseDisplayName(NULL, NULL, 
                                L"c:\\arbitraryfile.MAPIMAIL",
                                &chEaten,
                                &pidlSendToMail,
                                NULL);

  //
  // invoke command
  //

  {
    CString m_sFolderPath = "c:\\";
    CString m_sFilePath = "noIE.png";
    CString m_sFilePath_2 = "winzip.log";

    CPIDL pidlFolder( m_sFolderPath.GetBuffer(0) );

    CPIDL* arrpidl = new CPIDL[2];
    arrpidl[0].Set( m_sFolderPath, m_sFilePath.GetBuffer(0) );
    arrpidl[1].Set( m_sFolderPath, m_sFilePath_2.GetBuffer(0) );

    CPIDL pidlDrop;
    LPDROPTARGET pDT;
    LPDATAOBJECT pDO;

    hr = pidlFolder.GetUIObjectOf(IID_IDataObject, (LPVOID *)&pDO, 
                                  NULL, sfDesktop,
                                  2, arrpidl);

    if ( SUCCEEDED(hr) )
    {
      pidlDrop.Set(pidlSendToMail);
      hr = pidlDrop.GetUIObjectOf(IID_IDropTarget, 
                                  (LPVOID *)&pDT, NULL);

      if (SUCCEEDED(hr))
      {
        // do the drop
        POINTL pt = { 0, 0 };
        DWORD dwEffect = 
                DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK;
        hr = pDT->DragEnter(pDO, MK_LBUTTON, pt, &dwEffect);

        if (SUCCEEDED(hr) && dwEffect) 
        {
          hr = pDT->Drop(pDO, MK_LBUTTON, pt, &dwEffect);
        }
        else 
        {
          hr = pDT->DragLeave();
        }

        pDT->Release();
      }

      pidlDrop.m_pidl = NULL;
    }

    delete [] arrpidl;
  }

  //
  // release the stuff
  //
  sfDesktop->Release();

  ::CoUninitialize();

Before this piece of code compiles, you need to include the PIDL.h PIDL.cpp implementation which is a helper class that hides the technical details behind the translation from a file path to a shell file ID (called an ITEMIDLIST), and vice versa. So besure to include those files.

The sample code uses CString classes (PIDL.cpp doesn't), which are from the MFC flamework. They are quite easily removed, just in case.

Before the code works, make sure to create a file called c:\arbitraryfile.MAPIMAIL either by hand, or programmatically. This is good to create such a file in a temporary folder for instance.

Typically if you which to send all files from a particular folder, you would use code like this :


  // this code uses MFC classes (easily replaced)
  #include <afx.h>     // for CString
  #include <afxcoll.h> // for CStringArray
  #include <atlconv.h> // for USES_CONVERSION
  //

  USES_CONVERSION;

  ::CoInitialize(NULL);


  LPSHELLFOLDER sfDesktop = NULL;
  SHGetDesktopFolder(&sfDesktop);

  //
  // catch an arbitrary file with .MAPIMAIL extension name
  //
  LPITEMIDLIST pidlSendToMail;
  ULONG chEaten = 0;


  char szTmpFolder[MAX_PATH]="";
  ::GetTempPath(MAX_PATH,szTmpFolder);
  if ( strlen(szTmpFolder) > 0 )
  {
    if (szTmpFolder[strlen(szTmpFolder) - 1] == '\\')
      szTmpFolder[strlen(szTmpFolder) - 1] = 0;
  }

  // create a .MAPIMAIL file if none exist yet
  CString szArbitraryMAPIMAILsender = CString(szTmpFolder) + "\\arbitraryfile.MAPIMAIL";
  if ( !FileExists(szArbitraryMAPIMAILsender) )
  {
    HANDLE hFile = ::CreateFile(szArbitraryMAPIMAILsender.GetBuffer(0),
                                GENERIC_WRITE,
                                0,
                                NULL,
                                CREATE_NEW,
                                FILE_ATTRIBUTE_NORMAL,
                                NULL);
    ::CloseHandle(hFile);
  }


  HRESULT hr = sfDesktop->ParseDisplayName(NULL, NULL, 
                                T2OLE(szArbitraryMAPIMAILsender.GetBuffer(0)),
                                &chEaten,
                                &pidlSendToMail,
                                NULL);

  //
  // invoke command
  //

  {
    CString sFolderPath = ...; // of the form "c:\\MyProjects\\";

    HANDLE hFind;
    WIN32_FIND_DATA fd;
    CStringArray arrFiles;

    if ((hFind=::FindFirstFile(m_sFolderPath + "*.*",&fd))!=INVALID_HANDLE_VALUE)
    {
      do
      {
        CString szFilepath = sFolderPath + fd.cFileName;

        if (strcmp(fd.cFileName,".")!=0 && strcmp(fd.cFileName,"..")!=0 )
        {
          if ( !(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) )
          {
            arrFiles.Add( fd.cFileName );
          }
        }
      }
      while ( ::FindNextFile(hFind,&fd) );

      ::FindClose(hFind);
    }

    CPIDL pidlFolder( sFolderPath.GetBuffer(0) );

    long nbFiles = arrFiles.GetSize();

    CPIDL* arrpidl = new CPIDL[ nbFiles ];
    for (int i = 0; i < nbFiles ; i++)
      arrpidl[i].Set( sFolderPath, arrFiles.GetAt(i).GetBuffer(0) );


    CPIDL pidlDrop;
    LPDROPTARGET pDT;
    LPDATAOBJECT pDO;

    hr = pidlFolder.GetUIObjectOf(IID_IDataObject, (LPVOID *)&pDO, 
                                  NULL, sfDesktop,
                                  nbFiles, arrpidl);

    if ( SUCCEEDED(hr) )
    {
      pidlDrop.Set(pidlSendToMail);
      hr = pidlDrop.GetUIObjectOf(IID_IDropTarget, 
                                  (LPVOID *)&pDT, NULL);

      if (SUCCEEDED(hr))
      {
        // do the drop
        POINTL pt = { 0, 0 };
        DWORD dwEffect = 
                 DROPEFFECT_COPY | DROPEFFECT_MOVE | DROPEFFECT_LINK;
        hr = pDT->DragEnter(pDO, MK_LBUTTON, pt, &dwEffect);

        if (SUCCEEDED(hr) && dwEffect) 
        {
          hr = pDT->Drop(pDO, MK_LBUTTON, pt, &dwEffect);
        }
        else 
        {
          hr = pDT->DragLeave();
        }

        pDT->Release();
      }

      pidlDrop.m_pidl = NULL;
    }

    delete [] arrpidl;
  }

  //
  // release the stuff
  //
  sfDesktop->Release();

  ::CoUninitialize();

 

How to manage the asynchronous process

So far everything looks fine, a new email pops up with one or more files in attachment. But you are likely to be in one of this cases :

Both cases are originated from the same issue, an issue Windows explorer does not have for a simple reason : the Windows explorer process lives on after the email pops up. For that reason, the underlying COM channel between Windows explorer and the email client works fine and undefinitely.

But we shall not expect the same behavior any case the technique explained here is used in a method that immediately ends, read: the process executing the code terminates right after the ::CoUnitialize() statement. In this case, the COM channel is broken and the MAPI tunneling halts any further communication, resulting in either of the mentioned issues for the end user.

How workaround this? First and foremost, if your client application is kept alive once the email sending technique is completed, then you shouldn't have the issue. This ends the troubles by itself. If you can't afford this, my suggestion is to have a blocking thread just after the email sending technique is completed. Probably the simplest way to come up with a blocking thread is to show a message box. Why not show something like this "click here when the email is sent"? by the way hinting users how to proceed.

Hope this helps.

 

 

Stéphane Rodriguez, October 13 2003