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
When connecting to any COM object, the first thing you need to do is find out what methods and properties are available. This is a fairly simple task. All of Outlook's methods and properties can be found in the Outlook Type Library file, MSOUTL.OLB. Just open it with Delphi, and dig in. A supplement to the type library is the Visual Basic Help file: VBAOUTL9.CHM, VBAOL10.CHM, or whatever it might be called on your system. It seems Microsoft didn't use the same naming conventions when versioning its Help files. Nevertheless, these files are stored somewhere in your MS Office folders. The Help file can be an aid in understanding how some methods and properties are used. You'll have to get out your VB-To-Delphi translation manual, but that shouldn't prove too difficult a task.

Making the connection
There are two objects that need to be referenced: the Application object and the NameSpace object. The Application object represents the entire Microsoft Outlook application, while the NameSpace object represents an abstract root object for the data source. The only NameSpace object that's available in Outlook is MAPI. Using the NameSpace object you can access Outlook's folders, address book, e-mail and attachments, and so on.

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
After establishing a connection to Outlook, the first order of business is to load the folders into a tree view. Each folder in Outlook has properties that can reference other folders, mail, and attachments. Since each node in the tree view will represent a folder object in Outlook, I use the Data property of each node to store an object that references the Outlook folder. A simple subclass of TObject can suffice for this. The object TOLEItem requires only one property, a variant, to point to the OLE item. It will also require a Destroy method to ensure the reference to the folder is released when the object is freed:

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
Each folder can contain mail items. I want to load the mail items into the list view every time the user selects a folder. So I use the OnChange event of the tree view to call the LoadMailItems method. The LoadMailItems accepts a variant parameter that represents the folder object, and is used to access the folder's properties. This is where I use Typecasting to obtain the variant from the TOLEItem object that's stored in the tree view's Node.Data:

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
If you or your clients have installed the latest security patches for Outlook, you'll experience a small speed bump when your application attempts to access Outlook mail items. A dialog will pop up and ask you if it's okay for the application to access its mail. Unfortunately, Microsoft didn't provide an override switch to tell Outlook that it's always okay for specific applications to access the mail. Apparently the best they could do was to allow the user to set a maximum of 10 minutes access time. So plan on seeing this dialog fairly often!

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
I want the attachments to display as icons in a list view as the user selects mail items. The PopListViewAttachment method is called from the OnSelectItem event of the list view. It passes the TListItem in as a parameter. The method then locates the mail item using the NameSpace.GetItemFromID() method. Once the mail item object has been created, I can spin through the Attachments collection property and retrieve the filename for each attachment. Then I get the correct icon based on the file extension of the attachment item:

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
As you can see, using these techniques is fairly simple. But you should be aware that accessing Outlook through the COM interface may, in some cases, prove to be very slow—not to mention the somewhat annoying security features. Using "Extended MAPI" would be much faster, but would also add a few layers of complexity to your application.