Bill is a Windows application developer at a major investment bank in New York City and can be reached on CompuServe at 72274,1165.
Windows programmers are finding it increasingly necessary to develop applications that communicate with one another. These applications are not of the exotic, embedded-systems variety, but straightforward commercial applications like sales-order tracking. Methods available for implementing interprocess communication in Windows applications include facilities provided by the Windows environment, such as DDE (Dynamic Data Exchange) and NDDE (Network Dynamic Data Exchange), as well as a few third-party add-ons. DDE lets Windows applications establish a conversation that permits a continuous, automatic data exchange. DDE is limited to applications running locally in your Windows environment; NDDE lets your application establish a conversation with another app across a LAN.
App-Link, a VBX control from Synergy Technologies (Essex Junction, VT), is a third-party add-on that is similar to Microsoft's NDDE, but easier to program. However, App-Link is currently unable to route across a LAN because it is limited to using NETBIOS for remote communication.
As an alternative interprocess-communication technique, the Messaging Application Program Interface (MAPI) is underrated and often overlooked, yet it is one of the easiest to work with. MAPI is an API developed by Microsoft that has garnered widespread industry support. MAPI is not for everyone, especially if you need high-bandwidth, low-latency, or fine-granularity interapplication communications. But in many situations, the communications requirements are modest, and therefore MAPI is a good fit.
In this article, I'll discuss using MAPI for interapplication communication, and present two programs, one written in Microsoft Visual Basic and the other in Borland's Delphi, that can communicate via the MAPI interface.
When the topic of MAPI arises, three APIs are usually mentioned: The Common Messaging Call Application Program Interface (CMC API), Simple MAPI, and Extended MAPI. CMC API contains ten high-level messaging functions that let you create a mail-enabled application. It is a cross-platform API; therefore, it is designed to be independent of the messaging service (Microsoft Mail, IBM Profs, and the like), the operating system, and the underlying hardware. As a result, applications on UNIX, OS/2, Windows, DOS, and Macintosh can implement this technology. It was developed in alliance with the X.400 API Association standards organization and e-mail vendors and users. For more on MAPI, see "Using the Microsoft Mail API," by Jim Conger (DDJ, August 1994).
Simple MAPI contains a set of 12 messaging functions for creating mail-enabled Windows applications. Simple MAPI functions make available capabilities such as sending, receiving, and addressing messages. Messages can contain file attachments and OLE objects.
Extended MAPI contains a richer set of messaging functions for more-complex messaging schemes. It includes features like smart forms, which can replace the standard send and receive forms, plus functionality to link information entered in smart-form fields with other applications.
Using MAPI in your applications has a number of advantages:
Yet another worthwhile consequence of relying on your company's e-mail infrastructure is the remote dial-up capability that is now an option with most e-mail systems. With remote dial-up, users working off-site can login through a dial-up line, then download e-mail to portable computers, disconnect from the e-mail server, and read and write messages off-line. Your applications can follow a similar scenario. For example, in the case of a sales-tracking application, a salesperson out in the field can book orders on a laptop, and the database app running on the laptop can transparently call headquarters, upload new data and receive updated inventory information from the host.
MAPI is designed to be independent of all Windows-based programming/macro languages and almost all currently available e-mail services (Microsoft Mail, IBM Profs, Lotus cc:Mail, and so on). Thus, multiple Windows apps written in dissimilar programming/macro languages and using different e-mail services can communicate with each other through MAPI. Obviously, there are many ways to mix and match front-end and/or mail-enabled clients to back-end e-mail services.
To demonstrate, I've written two simple Windows applications--one in Visual Basic, the other in Borland Delphi. In the remainder of this article, I'll describe their implementation.
The basic principle in creating a MAPI-compliant application is that almost all the API calls require a valid session handle as one of the parameters. Therefore, a MAPI session must first be established by logging into an e-mail service. If successful, a session handle will be returned.
Creating a mail-enabled application in Visual Basic is straightforward. The online help is complete, and there is no need to refer to the hard-copy documentation. Obviously, you must first have a MAPI-compliant e-mail service installed on your network and access to valid user accounts. Microsoft Visual Basic 3.0 Professional comes with a VBX called "MSMAPI.VBX," which contains two controls: a session control (MAPIsession) and a message control (MAPImessage). MAPIsession establishes a mail session by logging in. Once logged in, your program uses MAPImessage to perform all e-mail functions (compose a message, send a message, get new messages, and so on). No methods or events are associated with these controls; all actions are performed through the setting of properties on them. "Sender," the application I've written, simply sends messages to valid user accounts on a given e-mail service; see Listing One.
The code in the Form_Load event handler establishes a MAPI session. It first sets the MapiSession1.LoginUI property to True, which activates the default message-login dialog when the MAPI Session control attempts to connect to the e-mail service. Next, the default user ID and password are set to null. Then, the code attempts a MAPI session login, via the statement MapiSession1.Action=SESSION_SIGNON. After the session is established, the MAPI message control is informed of the SessionID, via MapiMessages1.SessionID=MapiSession1.SessionID.
Once the message control is enabled, the application can successfully send messages to any valid user account. If the end user enters a valid user in the To: field, plus the desired Subject and Message, and clicks on the Send button, the cmdSend_Click event is invoked.
In the cmdSend_Click event, several steps are taken prior to sending a message. First, a Visual Basic error handler is activated to trap all errors caused during the steps to send a message. Any time an error is encountered, it automatically goes to the ErrHandler label, where a dialog box is displayed, showing the description of the error in the body of the dialog and the error number in the dialog title bar. The message-sending procedure is terminated at this point. This error handling is minimal; you'll likely want to implement a more graceful sequence.
To create a message, set the MapiMessages1.Action variable to MESSAGE_COMPOSE, the type of recipient to RECIPTYPE_TO, and the recipient name to the name typed in by the end user in the To: field. Follow the same sequence for the Subject: field. The body of the message is set by the statement MapiMessages1.MsgNoteText=txtMessage.Text. Finally, the message is sent to the intended recipient: MapiMessages1.Action=MESSAGE_SEND.
When the user exits the Sender application, the Query_Unload event is invoked. Here, the code in the event handler must disconnect the MAPI session, as shown in the listing.
The application I've written in Delphi, "Receiver," shows messages sent to the mailbox that Receiver logs on to, and, if the user requests it, the body of a given message. Creating a mail-enabled application with Delphi is not quite as easy as with Visual Basic, but simple nevertheless. Although some VBXs on the market work with Delphi, this particular one doesn't. Therefore, I developed a Delphi "unit" that contains wrapper functions for all the API calls found in the MAPI SDK. Although my initial effort was a bit tedious, any Delphi application that I develop from this point on will be built as easily as with a VBX. It is quite possible that the Delphi wrapper will outperform the MAPI VBX, because the wrapper lacks the typical VBX overhead.
The code discussed here is shown in Listing Two. To establish a session with an e-mail service, the end user clicks on the Logon button, which in turn triggers the btnLogClick event handler. As with the VB application, the code must establish a MAPI session--in this case, by calling the MAPILogon() function shown in Example 1(a). Two of the parameters are worthy of note. The fourth parameter in the list specifies options for the MAPI session. In this case, the default e-mail service logon dialog will appear as an attempt to connect to an e-mail service is made (MAPI_LOGON_UI). Once a session is activated, all new messages pending for the account will be downloaded into its mailbox (MAPI_FORCE_DOWNLOAD).
The sixth parameter, hSess, is assigned a valid session handle, if the login to the e-mail service was successful. All subsequent MAPI message calls will require this session handle.
Next, if MAPILogon() returns SUCCESS_SUCCESS, the application fills in the Messages list box with subjects from all the messages in the current account. This happens in the procedure PopulateMessageListBox, which makes two MAPI calls. The first, MAPIFindNext, searches for the next message to put in the list box, starting at message ID CurrMsgId and stores it in MsgId; see Example 1(b).
The message is then read into a MAPI message structure that contains all pertinent information relating to the message, such as the Subject, Message Body, Sender, and so on; see Example 1(c). The MsgId is the key parameter here. The MAPIReadMail function expects a pointer to a pointer to a MAPI message structure, which in this case is represented as @pMMsg.
The code can then access the different components of the message. In this application, the Subject section of this message is added to the Messages list box via the statement lstMessages.Items.Add(StrPas(pMMsg^.lpszSubject)).
To see the details of a message, the end user invokes the Show Details dialog, which triggers the btnShowDetailsClick event handler. A specified message must first be located using the MAPIFindNext() function. Once found, the message is read into a MAPI message structure using the MAPIReadMail() function. Now, all components of the message are available to the application; in this case the Receiver, Sender, Subject, and Message Body are displayed in a Show Details dialog. The code that populates the dialog is shown in Example 2.
Finally, to terminate a MAPI Session, the end user must click on the Logoff button, which triggers the btnLogClick event handler. This event handler calls the MAPILogoff function to terminate the session.
Example 1: (a) Establishing a MAPI session in Delphi; (b) locating the message; (c) reading the message into a structure.
Copyright © 1995, Dr. Dobb's Journal
(a)
MAPILogon(0, '', '', MAPI_LOGON_UI or MAPI_FORCE_DOWNLOAD, 0, hSess)
(b)
MAPIFindNext(hSess, 0, nil, @CurrMsgId, MAPI_GUARANTEE_FIFO, 0, @MsgId)
(c)
MAPIReadMail(hSess, 0, @MsgId, MAPI_PEEK or MAPI_ENVELOPE_ONLY, 0, @pMMsg)
Example 2: Populating the Show Details dialog.
with dlgDetails do
begin
edtTo.Text := StrPas(pMMsg^.lpRecips^.lpszName);
edtFrom.Text := StrPas(pMMsg^.lpOriginator^.lpszName);
edtSubject.Text := StrPas(pMMsg^.lpszSubject);
edtText.Text := StrPas(pMMsg^.lpszNoteText);
ShowModal;
endListing One
' -----------------------------------------------------------------------
' Sender App -- MAPI sender app written in VB by William Stamatakis, 1995.
' -----------------------------------------------------------------------
Option Explicit
' ------------------------------------------------------------------------
Sub cmdExit_Click ()
Unload Me
End Sub
' ------------------------------------------------------------------------
Sub cmdSend_Click ()
On Error GoTo ErrHandler:
' Compose a new message
MapiMessages1.Action = MESSAGE_COMPOSE
' The recipient is the primary recipient (To:)
MapiMessages1.RecipType = RECIPTYPE_TO
' Set Recipient Name
MapiMessages1.RecipDisplayName = txtTo.Text
' Store the Subject section of the message
MapiMessages1.MsgSubject = txtSubject.Text
' Store the main body of the message
MapiMessages1.MsgNoteText = txtMessage.Text
' Send the message recipients specified
MapiMessages1.Action = MESSAGE_SEND
Exit Sub
ErrHandler:
MsgBox Error$, , "Error#: " & Err
Exit Sub
End Sub
' ------------------------------------------------------------------------
Sub Form_Load ()
' Use the default Mail Login Dialog
MapiSession1.LogonUI = True
MapiSession1.UserName = ""
MapiSession1.Password = ""
' Login
MapiSession1.Action = SESSION_SIGNON
' Set the Session ID in the MapiMessage control equal to
' the new valid Session ID established by the MapiSession
' login, resulting in activating all message function
' capability on the current session id.
MapiMessages1.SessionID = MapiSession1.SessionID
End Sub
' ------------------------------------------------------------------------
Sub Form_QueryUnload (Cancel As Integer, UnloadMode As Integer)
' Logoff before exiting Sender
MapiSession1.Action = SESSION_SIGNOFF
End Sub
Listing Two
{----------------------------------------------------------------------}
{ Receiver App, written in Borland Delphi by William Stamatakis, 1995. }
{----------------------------------------------------------------------}
unit Receive;
interface
uses
SysUtils, WinTypes, WinProcs, Messages, Classes, Graphics, Controls,
Forms, Dialogs, MAPI, ExtCtrls, StdCtrls, Buttons, Tobclist, Details;
type
TfrmReceiver = class(TForm)
lstMessages: TOBCListBox;
Label1: TLabel;
btnLogon: TBitBtn;
Bevel1: TBevel;
btnLogoff: TBitBtn;
btnShowDetails: TBitBtn;
btnExit: TBitBtn;
procedure btnLogClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure btnShowDetailsClick(Sender: TObject);
procedure btnExitClick(Sender: TObject);
private
procedure PopulateMessageListBox;
public
{ Public declarations }
end;
var
frmReceiver: TfrmReceiver;
hSess: LHANDLE; { MAPI session handle }
slstMsgIds: TStringList; { var of type String list }
MMsg: TMapiMessage; { var of type MAPI message structure }
pMMsg: ^TMapiMessage; { pointer of type MAPI message structure }
implementation
{$R *.DFM}
{------------------------------------------------------------------------}
procedure InitMsgStruct; { Initialize MAPI message structure }
begin
with MMsg do
begin
ulReserved := 0;
lpszSubject := '';
lpszNoteText := '';
lpszMessageType := nil;
lpszDateReceived := nil;
lpszConversationID := nil;
flFlags := 0;
lpOriginator := nil;
nRecipCount := 0;
lpRecips := nil;
nFileCount := 0;
lpFiles := nil;
end;
new(pMMsg);
pMMsg := @MMsg;
end;
{------------------------------------------------------------------------}
procedure TfrmReceiver.PopulateMessageListBox;
{ Fill Messages listbox with the Subject section of all messages
from the currently logged in account's mailbox.
}
var
MsgId, CurrMsgId: string;
i: Shortint;
begin { Intialize variables }
MsgId := '';
CurrMsgId := '';
slstMsgIds := TStringList.Create;
{ Loop through all messages in account's mailbox }
while MAPIFindNext(hSess, 0, nil, @CurrMsgId, MAPI_GUARANTEE_FIFO,
0, @MsgId) = SUCCESS_SUCCESS do
begin
{ Read messages into the MAPI message structure pointer, pMMsg. }
if MAPIReadMail(hSess, 0, @MsgId, MAPI_PEEK or
MAPI_ENVELOPE_ONLY, 0, @pMMsg) = SUCCESS_SUCCESS then
begin
{ Add message subject to Messages listbox }
lstMessages.Items.Add(StrPas(pMMsg^.lpszSubject));
{ Add corresponding message id to string list object }
slstMsgIds.Add(MsgId);
end;
CurrMsgId := MsgId;
end;
end;
{------------------------------------------------------------------------}
procedure TfrmReceiver.btnLogClick(Sender: TObject);
{ Event handler proc for Logon and Logoff buttons }
begin
{ Logon button pressed}
if (Sender = btnLogon) then
begin
{ Logon to E-Mail Service }
if MAPILogon(0, '', '', MAPI_LOGON_UI or MAPI_FORCE_DOWNLOAD, 0,
hSess) = SUCCESS_SUCCESS then
begin
PopulateMessageListBox;
{ Fill Messages listbox with the Subject section of
all messages from the account's mailbox that was
just logged into.
}
btnLogon.Enabled := False; { Disable Logon Button }
btnShowDetails.Enabled := True; { Enable Show Details Button }
btnLogoff.Enabled := True; { Enable Logoff Button }
end;
end
else { Logoff button pressed}
{ Logoff from E-Mail Service }
if (MAPILogoff(hSess, 0, 0, 0) = SUCCESS_SUCCESS) then
begin
hSess := 0; { Reset MAPI session handle }
lstMessages.Clear; { Empty out Messages listbox }
slstMsgIds.Clear; { Empty string list object }
btnShowDetails.Enabled := False;{ Disable Show Details button}
btnLogoff.Enabled := False; { Disable Logoff button }
btnLogon.Enabled := True; { Enable Logon button }
end;
end;
{---------------------------------------------------------------------}
procedure TfrmReceiver.FormCreate(Sender: TObject);
begin
InitMsgStruct; { Initialize MAPI message structure }
end;
{---------------------------------------------------------------------}
procedure TfrmReceiver.btnShowDetailsClick(Sender: TObject);
{ When "Show Details" button is clicked, show Details Dialog,
which contains the details of the currently selected message
in the lstMessages listbox.
}
var
MsgId: string; { Message Id of currently selected message }
i: LongInt; { subcript var }
begin { Set MsgId to the message id that corresponds }
{ to the message being sought }
i := lstMessages.ItemIndex - 1;
{ Set i = to the list item index
of the currently selected item in
the lstMessages listbox }
if i < 0 then
MsgId := ''{ Based on i set MsgId to message id located in }
else { slstMsgIds which corresponds directly with list item }
MsgId := slstMsgIds.Strings[i]; { in the lstMessages listbox }
{ Locate the message based on the message id selected. If found
then read the message. If read successfully, then fill in the Detail
Dialog prior to showing it.
}
if MAPIFindNext(hSess, 0, nil, @MsgId, MAPI_GUARANTEE_FIFO, 0,
@MsgId) = SUCCESS_SUCCESS then
if MAPIReadMail(hSess, 0, @MsgId, MAPI_PEEK or MAPI_SUPPRESS_ATTACH,
0, @pMMsg) = SUCCESS_SUCCESS then
begin
with dlgDetails do
begin
edtTo.Text := StrPas(pMMsg^.lpRecips^.lpszName); { To: }
edtFrom.Text := StrPas(pMMsg^.lpOriginator^.lpszName); { From: }
edtSubject.Text := StrPas(pMMsg^.lpszSubject); { Subject:}
edtText.Text := StrPas(pMMsg^.lpszNoteText); { Text: }
ShowModal; { Show dialog }
end;
end;
end;
{------------------------------------------------------------------------}
procedure TfrmReceiver.btnExitClick(Sender: TObject);
begin
if btnLogoff.Enabled then
btnLogClick(btnLogoff);
self.Close;
end;
end.ODBC DRIVERS
}