|
April 2003 |
Delphi Meets Outlook Through COM
Steve Zimmelman
With enterprise applications becoming more popular, integration with business applications, such as Microsoft Office, is growing in importance. One way to integrate Delphi applications with Microsoft Office is by using the Component Object Model (COM). Steve Zimmelman presents one such solution for integrating Microsoft Outlook with Delphi.
Recently I had a request from a client to give their application the ability to import fax images that were being sent as e-mail attachments. Their e-mail client is Microsoft Outlook. After a cursory glance at the Delphi COM server components, I decided that building my own COM interface to Outlook would be more beneficial and provide more flexibility in the long run.
I wanted the UI to be familiar to the users, so I designed it with a tree view on the left side that displayed all the e-mail folders, a list view to display the e-mail headers, and another list view on the bottom to display the file attachments

Digging in
Making the connection
Making the initial connection to Outlook is straightforward. In the following example, the CreateOleObject() method is used. If Outlook isn't accessible on the host computer, an exception will be raised. The exception is handled using the Try/Except statements. If the connection fails, the Except block handles the error, displays the error message, and sets the Result of the function to False:
Var
FOLApp, FNameSpace : Variant ;
Function TfrmOL_Demo.Connect : Boolean ;
Begin
Try
FOLApp :=
CreateOleObject('Outlook.Application');
FNameSpace := FOLApp.GetNameSpace('MAPI');
Result := True ;
Except
On E:Exception Do Begin
Result := False ;
Application.ShowException(E);
End;
End;
End;
The connections are referenced by Variant type variables. In this case the Application and NameSpace objects are represented by FOLApp and FNameSpace, respectively. To release the objects, simply assign the Unassigned constant to the variants:
FOLApp := Unassigned ; FNameSpace := Unassigned ;
Loading up
TOLEItem = Class(TObject)
Public
vItem : Variant ;
Constructor Create(Item:Variant);
Destructor Destroy ; Override ;
End;
Constructor TOLEItem.Create(Item:Variant);
Begin
Inherited Create ;
vItem := Item
End;
Destructor TOLEItem.Destroy ;
Begin
vItem := Unassigned ;
Inherited ;
End;
Each time a new node is added to the tree, a new instance of TOLEItem is created and stored in the Data property of the tree view's node. I can retrieve it later by Typecasting the node's Data property as TOLEItem to access the object stored in the variant:
Procedure TfrmOL_Demo.LoadFolders ;
Var
iCount : Integer ;
myFolders : Variant ;
Root : TTreeNode ;
Procedure CheckMoreFolders(vFolder:Variant;
TheNode:TTreeNode);
Var
i : Integer ;
Node : TTreeNode ;
Begin
If vFolder.Folders.Count = 0 Then Exit ;
For i := 1 To vFolder.Folders.Count Do Begin
// Just get Mail Folders.
If (vFolder.Folders[i].DefaultItemType
<> olMailItem) Then Continue ;
Node :=
tv_Folder.Items.AddChild(TheNode,
vFolder.Folders[i].Name);
// Create new TOLEItem and store a pointer
// to it in the Data property.
Node.Data :=
TOLEItem.Create(vFolder.Folders[i]);
// Use recursion to get all the child
// folders.
CheckMoreFolders(vFolder.Folders[i],Node);
End;
End;
Begin
// Get the Folders object.
myFolders := FNameSpace.Folders ;
Try
Screen.Cursor := crHourGlass ;
tv_Folder.Items.BeginUpdate ;
tv_Folder.Items.Clear ;
// The folder collection is not zero-based
// so iCount must start at 1.
For iCount := 1 To MyFolders.Count Do Begin
Root :=
tv_Folder.Items.Add(Nil,
MyFolders.Item[iCount].Name);
// Create new TOLEItem and store a pointer
// to it in the Data property.
Root.Data :=
TOLEItem.Create(MyFolders.Item[iCount]);
CheckMoreFolders(MyFolders.Item[iCount],
Root);
End;
Finally
tv_Folder.Items.EndUpdate ;
Screen.Cursor := crDefault ;
End;
End;
You may be concerned about creating all those objects without a way to free them. Well, it's not a problem. Use the OnDeletion event of the tree view to free each TOLEItem object that's stored in the node. This event is triggered just before the node is freed by the tree view:
procedure TfrmOL_Demo.tv_FolderDeletion(
Sender: TObject; Node: TTreeNode);
begin
If (Node <> Nil) Then Begin
If (Node.Data <> Nil) Then Begin
TOLEItem(Node.Data).Free ;
End;
End;
end;
Mail call
procedure TfrmOL_Demo.tv_FolderChange(
Sender: TObject; Node: TTreeNode);
begin
If tv_Folder.Selected <> Nil Then Begin
Try
Screen.Cursor := crHourGlass ;
LoadMailItems(
TOLEItem(tv_Folder.Selected.Data).vItem);
Finally
Screen.Cursor := crDefault ;
End;
End;
end;
Procedure TfrmOL_Demo.LoadMailItems(Folder:Variant);
Var
i,ItemsCount,AttachCount : Integer ;
li : TListItem ;
ce : TLVChangeEvent ;
Begin
// Capture the change event.
ce := lv_Mail.OnChange ;
Try
lvAttachments.Items.Clear ;
// Suppress the change event.
lv_Mail.OnChange := Nil ;
lv_Mail.Items.Clear ;
lv_Mail.Items.BeginUpdate ;
ItemsCount := Folder.Items.Count ;
SayStatus('Loading Email... ');
For i := 1 To ItemsCount Do Begin
If Folder.Items[i].Class = olMail Then Begin
AttachCount :=
Folder.Items[i].Attachments.Count ;
li := lv_Mail.Items.Add ;
If (AttachCount>0) Then Begin
li.StateIndex := 1 ;
// Adding a space to the caption allows
// sorting on the attachment column.
li.Caption := ' ';
End;
Try
li.SubItems.Add(
Folder.Items[i].SenderName);
Except
On E:Exception Do Begin
li.SubItems.Add('Error: '+E.Message);
End;
End;
li.SubItems.Add(Folder.Items[i].Subject);
li.SubItems.Add(
FormatDateTime('mm/dd/yy hh:nn AM/PM',
Folder.Items[i].ReceivedTime));
// EntryID and StoreID must be added in
// this order. These are used to get mail
// item.
li.SubItems.Add(Folder.Items[i].EntryID);
li.SubItems.Add(Folder.StoreID);
End;
SayStatus('Loading Email... '+
IntToStr(Trunc((i/ItemsCount)*100))+'%');
Application.ProcessMessages ;
End;
Finally
// Reinstate the change event.
lv_Mail.OnChange := ce ;
lv_Mail.Items.EndUpdate;
SayStatus('');
End;
End;
You'll notice that the last two subitems of the list view contain the Item's EntryID and the Folder's StoreID. These remain invisible to the user, but are used to obtain a reference to the mail item object by using the method NameSpace.GetItemFromID(). I could have created a TOLEItem object for each list view Item, and stored it in the Item's Data property, but this seems just as simple and perhaps uses a little less overhead.
Caveats

Another seemingly undocumented security enhancement I discovered: If Outlook isn't already running when your application attempts to connect, the CreateOLEObject() method will simply fail and an instance of Outlook won't be created. On a system without all of the new security patches, however, an instance of Outlook was created even when Outlook wasn't already running. So a little extra exception handling will probably be necessary to keep your users out of the dark when your application can't connect.
Attachments
Procedure TfrmOL_Demo.PopListViewAttachment(
li:TListItem);
Var
EntryID,StoreID:String;
vMail : Variant ;
x : Integer ;
Icon : Word;
FICon : TIcon ;
sFileCaption,sFile : String ;
Procedure GetIcon(sFile:String) ;
Var
FileInfo: TSHFileInfo;
i : Integer ;
im : HIMAGELIST ;
Begin
im := SHGetFileInfo(PChar(sFile),
FILE_ATTRIBUTE_NORMAL, FileInfo,
SizeOf(FileInfo),
SHGFI_USEFILEATTRIBUTES or
SHGFI_SYSICONINDEX or
SHGFI_ICON ) ;
If im > 0 Then Begin
i := FileInfo.iIcon;
ICon := ImageList_GetIcon(im,i,0) ;
FIcon.ReleaseHandle ;
FIcon.Handle := ICon ;
End;
End;
Function FileSizeToStr(TheSize:Integer):String;
Begin
If TheSize < 1024 Then Begin
Result :=
FormatFloat('#,###',TheSize)+' B' ;
End Else Begin
Result :=
FormatFloat('###,###,###,###',
TheSize Div 1024)+' KB' ;
End;
End;
Begin
FIcon := TICon.Create ;
Try
FCurrMailItem := UnAssigned ;
lvAttachments.Items.Clear ;
If Assigned(lvAttachments.LargeImages) Then
lvAttachments.LargeImages.Clear ;
lvAttachments.Items.BeginUpdate ;
EntryID := li.SubItems[(li.SubItems.Count-2)];
StoreID := li.SubItems[(li.SubItems.Count-1)];
Try
vMail :=
FNameSpace.GetItemFromID(EntryID,StoreID);
If IsVariantOK(vMail) Then Begin
FCurrMailItem := vMail ;
For x := 1 To vMail.Attachments.Count Do Begin
With lvAttachments.Items.Add Do Begin
sFileCaption :=
vMail.Attachments.Item[x].FileName;
GetIcon(sFileCaption);
sFile := GetTempFile(FTempDir,'~OF') ;
Data :=
TOLEItem.Create(
vMail.Attachments.Item[x]);
vMail.Attachments.Item[x].
SaveAsFile(sFile);
sFileCaption := sFileCaption +' ('+
FileSizeToStr(GetFileSize(sFile))+')';
DeleteFile(sFile);
Caption := sFileCaption ;
If Assigned(lvAttachments.LargeImages)
Then
ImageIndex :=
lvAttachments.
LargeImages.AddIcon(FIcon);
End;
FIcon.ReleaseHandle ;
End;
End;
Except
FCurrMailItem := UnAssigned ;
End;
Finally
lvAttachments.Items.EndUpdate ;
FIcon.ReleaseHandle ;
FIcon.Free ;
End;
End;
To enhance maintenance and reusability, I put most of this functionality into a component called TOutlook, and included it with two demo projects. (Available from Hardcore Delphi magazine).
Conclusion