MessagerSpy++ for MSN Messenger/Windows Messager

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Environment: VC6, VC7, Platform core SDK, Win2K/XP ONLY !!! (Test has passed on English/Japanese/Chinese Win2K/XP)

Special Note: To debug and test on one machine, Windows XP is highly recommended for its “Switch User” support, so you can run 2 Messenger instances on 2 user contexts and communicate between these 2 instances

Key Technology Used: Windows Hook (Dual Hook DLL), Memory Map File (MMF), Multiple Thread Synchronization, RichEdit20W OLE interface programming, Bitmap Handling, Dialog-Based Splitter, Doc/View Architecture, Clipboard, Unicode, Structured Exception Handling (SEH), User Defined Windows Message

Applicable Article Category in CodeGuru: System/RichEdit/Thread/DLL

Program Summary and GUI Overview

MSN Messenger (Windows Messenger) is one of the most popular IM (Instant Messenger) program nowadays, but unfortunately it can save the chat contents only in a plain text file while supporting RTF (including small Emotional Icon) in chatting. Although you can Copy/Paste the RTF content to, say, MS Word, you cannot get the small Emoticons out unless you manually copy them one by one. Besides, there is no reminder dialog to stop a one-way chat to be closed by a user; we have been accustomed to be reminded of “The document has been modified since last saving.”

Figure 1. With One-Click, Messenger Spy++ Fetch Chat Contents From a MSN Messenger 5.0 on Windows XP

The left-top panel let you manage your chat documents, and you can read these files in the left-bottom panel (Chat Document Reader). In the right-top panel, I create a RICHEDIT20W class manually on a CFormView. Visual Studio Resource Editor can only create a RICHEDIT class for you (Well, KB Q261171 provided an alternative way to modify resource script file manually, but I prefer using code.) When you are chatting, the right-bottom panel (Windows Message Interceptor) shows the windows message sent to the chat window; you can say it is a mimic of Spy++ bounded with MS Visual Studio. When you press the button, you can get all the contents of your chat window in Chat Contents Interceptor. A VERY GOOD NEWS is you need not worry about closing chat window erroneously, for the hook DLL will catch the WM_CLOSE message sent to chat window, and copy the chat contents to you automatically. The button will tell you if your Messenger and Chat Window is running or not. In the above figure, the latter is “pressed,” indicating chat window is “running.”

To intercept and save the original chat contents of a remote process, MessengerSpy++ launches the first WH_CBT Hook DLL to monitor all window creation and destroy on the whole Win32 system. Once it finds a running MSN Messenger chat window, it launches the second WH_GETMESSAGE Hook DLL into the target chat window process. The user can make request into the second DLL to get the chat RichEdit20W’s contents (with both the RTF and images inside it), put the data in a shared MMF, and pass to the thread running in the background within the process space of MessengerSpy++, and refresh the GUI. Besides, when the first DLL detects the chat is about to be closed, it will ask the second DLL to save the last chat data inside the chat window and passes it to the GUI, so the user will never lose their chat contents by closing the chat window erroneously; there is no “You have changed your chat contents, Would you please save it” notification dialog in MSN Messenger.

There also are some cool things is that you can make with MessengerSpy++.

You can set the default “ReadOnly” property of the chat text field, so that after you uncheck it in option dialog, you can EDIT the chat window (refer to the red ellipse of the figure; I inserted “first” here). and you can also set the chatter’s name and e-mail information in the “to” edit box (refer to the top white ellipse; I set it as “anybody@anywhere“. I bet you will never meet such a man on MSN World). Let me give you the previous figure to have a look.

That means you get the permission to modify the whole chat contents. As to its usage, hmm, maybe, you need an image to show your MSN chat with other persons.

Figure 2. Chat Window Modified By MessagerSpy++

I also provide the support for sending long text from MessengerSpy++ to MSN Messenger. Surely, the text is sent out through the Internet to the person on the other end. Just note: There is a limitation of text length MSN Messenger can send each time. You may find it is a good base to develop a more complicated auto-reply program to interact with MSN Messenger. For example, you can reply something to a specific user when you are out, while keeping silence to other user. Each user has a unique e-mail address secured by MS .Net Passport, so your program can use the mail address as an Unique ID to identify the user and react accordingly.

Figure 3. MessengerSpy++ Send Message Text To MSN Messenger And Send Out Through Internet

Implementation Detail and Code Excerpt

1. How To Get Image Out of RichEditCtrl (Plus writing to bitmap file):

//Suppose you are using MFC, You do
IRichEditOle* pReo = m_pRichEdit->GetIRichEditOle( );
//If you use Platform SDK directly
IRichEditOle* pReo;
::SendMessage(g_hYourRichEdit, EM_GETOLEINTERFACE,
  0, (LPARAM)(LPVOID*)&pReo);
//Note: in both cases, inside pReo's AddRef got called, so
//remember to release it later

LONG nNumber = pReo->GetObjectCount();  //Your Images' Number
//Handle Error yourself, code simplified for space limitation
for(int i = 0; i < nNumber; i++)
{
  REOBJECT* ro = new REOBJECT;
  ro->cbStruct = sizeof(REOBJECT);
  HRESULT hr = pReo->GetObject(i, ro, REO_GETOBJ_ALL_INTERFACES);
  if(FAILED(hr)) continue;

  //caller should released the inner object
  IDataObject* lpDataObject;
  hr = (ro->poleobj)->QueryInterface(IID_IDataObject,
       (void **)&lpDataObject);
  if(FAILED(hr)) continue;

  STGMEDIUM stgm;  //out
  FORMATETC fm;    //in

  fm.cfFormat = CF_DIB;  // Clipboard format
  fm.ptd = NULL;         // Target Device = Screen
  fm.dwAspect = DVASPECT_CONTENT;
                         // Level of detail = Full content
  fm.lindex = -1;        // Index = Not applicaple
  fm.tymed = TYMED_HGLOBAL ;
  hr = lpDataObject->GetData(&fm, &stgm);
  if(FAILED(hr)) continue;

  ASSERT(::GlobalSize(stgm.hGlobal));

  HANDLE hFile = ::CreateFile(_T("c:\\img.bmp"),
                 GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                 FILE_ATTRIBUTE_NORMAL, NULL);
  if(hFile == INVALID_HANDLE_VALUE)
  { continie; }

  DWORD dwWritten;
  //Writing Bitmap File header
  BITMAPFILEHEADER bmfh;
  bmfh.bfType = 0x4d42;    //'BM'
  int nColorTableEntries = 0;
  int nSizeHdr = sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) *
                 nColorTableEntries;
  bmfh.bfSize = 0;
  bmfh.bfReserved1 = bmfh.bfReserved2 = 0;
  bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) +
                   sizeof(BITMAPINFOHEADER) +
                   sizeof(RGBQUAD) * nColorTableEntries;
  ::WriteFile(hFile, (LPVOID)&bmfh, sizeof(BITMAPFILEHEADER),
                             &dwWritten, NULL);

  DWORD dwGlobalSize = ::GlobalSize(stgm.hGlobal);
  LPVOID lpMem = ::VirtualAlloc(NULL, dwGlobalSize, MEM_COMMIT,
                                      PAGE_READWRITE);
  ::CopyMemory(lpMem, (LPVOID)::GlobalLock(stgm.hGlobal),
                                           dwGlobalSize);
  BITMAPINFOHEADER* pInfoHead = (BITMAPINFOHEADER*)lpMem;
  pInfoHead->biXPelsPerMeter = pInfoHead->biYPelsPerMeter
                                = 0;

  //Special Careful Here!!! Discard the Color Mask,
  //We need TRUE COLOR image
  //I bellieve you guys running Win2X/Xp use true color
  //screen setting
  if(pInfoHead->biCompression == BI_BITFIELDS)
  {
    pInfoHead->biCompression = BI_RGB;
    dwGlobalSize -= 3 * sizeof(RGBQUAD);  //delete the 3 DWORD
                                          //color mask
    LPBYTE pSrc, pDst;
    pSrc = (LPBYTE)lpMem;
    pSrc += sizeof(BITMAPINFOHEADER);
    pDst = pSrc;
    pSrc += 3 * sizeof(RGBQUAD);
    ::MoveMemory(pDst, pSrc, dwGlobalSize -
                 sizeof(BITMAPINFOHEADER));
  }

  //Write Image Data
  ::WriteFile(hFile, lpMem, dwGlobalSize, &dwWritten, NULL);
  ::GlobalUnlock(stgm.hGlobal);

  //You may find good if all image are same size, keeping the
  //memory for performance
  ::VirtualFree(lpMem, 0, MEM_RELEASE);
  ::CloseHandle(hFile);

  lpDataObject->Release();
  delete ro;
}
pReo->Release();

2. How Do You Transfer Data from Messenger to Your Own Program?

To achieve best performance, I use 2 Memory Map File and 4 Events to synchronize the RTF data and the image data respectively. All these kernel objects are named as a GUID to avoid name confliction. In the program side, when the frame starts, I make all initializations like these:

m_hMMF = CreateFileMapping(INVALID_HANDLE_VALUE, NULL,
  PAGE_READWRITE, 0, MsnPage, GUID_MMF_NAME);
if (m_hMMF == NULL)
{    // handle error here
}
LPVOID pView = MapViewOfFile(m_hMMF, FILE_MAP_WRITE, 0, 0, 0);
DWORD dwSize = YourMMFSize;
DWORD dwUsed = 0;
LPBYTE lpByte = (LPBYTE)pView;
::memcpy(lpByte, &dwSize, 4);
::memcpy(lpByte + 4, &dwUsed, 4);
  //Now both sides knows the MMF size
::UnmapViewOfFile(pView);

m_hReadEvent = ::CreateEvent(NULL, TRUE, FALSE,
                             GUID_READ_EVENT_NAME);
if(m_hReadEvent == NULL)
{    //handle error
}
m_hWriteEvent = ::CreateEvent(NULL, TRUE, FALSE,
                              GUID_WRITE_EVENT_NAME);
if(m_hWriteEvent == NULL)
{    //handle error
}
::ResetEvent(m_hReadEvent);
::SetEvent(m_hWriteEvent); //Important Here, If not set,
                           //you get deadlock forever
//Note: data is unidirectional from DLL to frame, so DLL
//can begin to write now

//create my frame thread ...

In frame side thread, it reads data sent from Hook DLL only:

//in thread part:
__try
{
  hMMF = OpenFileMapping(FILE_MAP_READ | FILE_MAP_WRITE, FALSE,
                         GUID_MSG_MMF_NAME);
  if (hMMF == NULL)    //error
    __leave;

  hWriteEvent = ::OpenEvent(EVENT_ALL_ACCESS,
                            FALSE,GUID_MSG_WRITE_EVENT_NAME);
  if(hWriteEvent == NULL)
    __leave;

  hReadEvent = ::OpenEvent(EVENT_ALL_ACCESS,
                           FALSE,g_MSG_READ_EVENT_NAME);
  if(hReadEvent == NULL) __leave;

  ::SetEvent(hWriteEvent);    //Just a reminder

  HANDLE hReadArr[2];
  hReadArr[0] = hKillThreadEvent;
  hReadArr[1] = hReadEvent;
  while(1)
  {
    DWORD dwRet = ::WaitForMultipleObjects(2, hReadArr, FALSE,
                                           1000);
    if(WAIT_OBJECT_0 == dwRet) __leave;
    if(dwRet == WAIT_TIMEOUT) continue;
    if(dwRet == WAIT_ABANDONED_0 || dwRet == WAIT_ABANDONED_0 + 1)
    __leave;
    ASSERT((WAIT_OBJECT_0 + 1) == dwRet);
    ::ResetEvent(hReadEvent);
    LPVOID pView = MapViewOfFile(hMMF, FILE_MAP_ALL_ACCESS,
                                 0, 0, 0);
    if(pView == NULL) __leave;
    LPBYTE lpByte = (LPBYTE)pView;

    //Enjoy Your Data here!!!!

    ::UnmapViewOfFile(pView);
    ::SetEvent(hWriteEvent);
  }
}
__finally {
  ::CloseHandle(hMMF);
  ::CloseHandle(hWriteEvent);
  ::CloseHandle(hReadEvent);    // and other stuff....
}

In the Hook DLL part, you just exchange the position of the read and write event, and you do not need the loop because the DLL is message driven; besides, you set the wait time in the DLL to zero because the data is sent passively from the DLL while the frame thread running in wait loop. This structure will ensure the MMF is accessed by only one thread at one time. And in my point of view, WM_COPYMESSAGE (Spy++ uses this method), though theoretically able to realize the data transferring functionality, should be avoided; the reason is compatibility and portablity. In my case, this program has been ported to an NT Service to monitor the Messenger, which has no window at all to pursue maximum stability and performance. Besides, even in a Windows application like this, considering the performance, if my frame thread is blocked temporarily by some GUI action, the DLL thread can still write data to the MMF until it is full, and once the frame thread gets a chance to run, it can fetch all the previous data and empty the MMF. Actually, in my last project, the DLL can make a larger MMF and copy the data there, free the old MMF, and wait the receiving thread to wake. All this means better fault tolerance, stability, and performance of the program.

Usage Hint and Reminder

You can save all your chat RTF files together in a folder and preview them just at one mouse click. I tried to mimic the OfficeXP toolbar and menu style, and hope it does. It also permits the user to do some simple editing on these chat documents and save them, but the main purpose is to help you save your chat document at “one button push” and prevent data loss when your close the chat window by mistake. When you want to let a chat window contain the text you need, it also helps. So please download and enjoy! Thank you. Oh, last word, you must launch a MSN Messenger chat window to see the effect! That is what the program is for.

Because the MSN Messenger uses RichEdit2.0 as its chat text field, the RTF data from it is also in RichEdit2.0 format. Currently, either the Visual Studio Resource Editor or MFC class by default use RichEdit1.0. The version difference will NOT be a problem until you meet a image-text mixed environment. Unless you read the RTF specification carefully, the OLE Object position conversion will be a nightmare. For example: the Paragraph and Line marks are different; in RichEdit20, it is called Microsoft Word standard EOP. At first, I was puzzled because the image never goes to the right position in my program from Messenger. To keep it simple, I manually created a RichEdit20W control in my FormView panel. And this is the ONLY reason why it only runs on Win NT family, BTW.

Those of you guys who were born in the same decade as me may find it is a good base to make it a NT service-like program to log, say, your girl friend’s MSN chat to her machine. In this case, just let me give you a little hint to emulate me: Remember to add a NULL DACL to your MMF, not its default DACL. Otherwise, you will enjoy an “Access Denied” even though you have made your service an interactive one.

The last point is that MSN Messenger supports a maximum of 10 concurrent chats, and MessengerSpy++ will intercept all of them at the same time. Choose from the Spy++ menu, and you can track any of them at any time.

Acknowledgements

Thanks the following CodeGuru authors: Brent Corkum for his cool XP-style BCMenu, Hani Atassi for his excellent code to Insert Bitmap into RichEdit, and Giancarlo Iovino for his HyperLink Control.

Downloads

Download Project Source – 736 Kb
Download Exe File Only – 329 Kb

Version History

Version Release Date Features
1.1+ January 4, 2003 New GUI Interface + Article Completely Rewritten (Search Engine in development)
1.1 November, 2002 Supports MSN Messenger 5.0, Supports Multiple Chat Windows, Stronger Document Management Support
1.0+ October 17, 2002 Small Interface Bug Fix, finish this article, Support MSN Messenger 4.6, 4.7
1.0 October 15, 2002 Add new User Interface, make a Unicode version, HTML Thank you Dialog added
0.8 August 6, 2002 Fixed bug due to Asia language MBCS, Stable Program Running
0.7 July 28, 2002 First Practical Version Works
0.1 July 24, 2002 Hook DLL Implemented, Test Frame work Code Works

More by Author

Previous article
Next article

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read