Copyright © 2024 IMIBO. Privacy Statement
Extended MAPI in DELPHI
Example #3
Retrieve Microsoft Exchange „Light“ Hierarchy
(by Global Address List)
In the previous example we illustrated how to connect to Microsoft Exchange Server, how to obtain the GAL content and to display basic properties of the e-mail user such as PR_DISPLAY_NAME and PR_EMAIL_ADDRESS.
In the current example we will review the Hierarchy tables. Through them we may build a tree that displays Exchange Organization (5.5).
A hierarchy table contains information about the containers in an address book container.
Each row of a hierarchy table contains a set of columns with information about address book container.
Hierarchy tables are implemented by address book providers to show a tree of containers within the address book.
Containers that cannot hold subcontainers, as indicated by the absence of the AB_SUBCONTAINERS flag in their PR_CONTAINER_FLAGS property, do not implement a hierarchy table.
We will also learn how we can turn the PSBinary data in a more user-friendly data type, such as String (for visualization).
We will not take time to explain the usage of functions and data that were discussed in the previous examples.
This example requires connection to Microsoft Exchange Server for efficient implementation.
How to
- Use „light“ version for MapiInitialize
- Open Address Book
- Get Hierarchy Table
- Use PARENT ENTRYID
- Set MAPI Columns
- Get Row Count
- Query MAPI Rows
- Build Tree
- Get included (default) Property list for a MAPI Object
- Convert MAPI Binary Value (i.e. ENTRYID) To String
- …
Download Example #3 as Compiled Application
Download Project (DELPHI 10.4) ZIP file
Source Code: In package
unit MAPIHierarchyViewer; (* In the previous example we illustrated how to connect to MS Exchange Server, how to obtain the GAL content and to display basic properties of the e-mail clients such as PR_DISPLAY_NAME and PR_EMAIL_ADDRESS. In the current example we will review the Hierarchy tables. Through them we may build a tree that displays Exchange Organization. A hierarchy table contains information about the containers in an address book container. Each row of a hierarchy table contains a set of columns with information about address book container. Hierarchy tables are implemented by address book providers to show a tree of containers within the address book. Containers that cannot hold subcontainers, as indicated by the absence of the AB_SUBCONTAINERS flag in their PR_CONTAINER_FLAGS property, do not implement a hierarchy table. We will also learn how we can turn the PSBinary data in a more user-friendly data type, such as String (for visualization). We will not take time to explain the usage of functions and data that were discussed in the previous examples. This example requires connection to Microsoft Exchange Server for efficient implementation. *) interface { Please add "..\Library" to project search path } {$I IMI.INC} uses Classes, Controls, Forms, Graphics, Windows, ComCtrls, ImgList, StdCtrls, ToolWin, ExtendedMAPI, System.ImageList; type PMAPITempContainer = ^TMAPITempContainer; TMAPITempContainer = record DisplayName: String; Path: String; HierarPath: String; EntryID: String; PEntryID: String; DEPTH: byte; end; type TfrmMAPI = class(TForm) OrganizationTreeView: TTreeView; ImageList: TImageList; ToolBar1: TToolBar; btLogOn: TToolButton; ToolButton2: TToolButton; btGetTree: TToolButton; ToolButton1: TToolButton; btLogOff: TToolButton; ToolButton3: TToolButton; ToolButton4: TToolButton; ListBox1: TListBox; Label2: TLabel; procedure btLogOnClick(Sender: TObject); procedure btLogOffClick(Sender: TObject); procedure btGetTreeClick(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure ToolButton4Click(Sender: TObject); procedure OrganizationTreeViewClick(Sender: TObject); procedure FormCreate(Sender: TObject); private { Private declarations } FMAPISession: IMAPISession; FAdrBook: IAddrBook; FUnkObject: IMAPIContainer; hr: HRESULT; FOrgFound: Boolean; FAddressBookNode, FOrgNode, FSiteNode, FContNode: TTreeNode; FStrSite: string; procedure BuildTree(Value: PSRowSet); procedure AddOrgNodes(Value: PMAPITempContainer); public { Public declarations } end; var frmMAPI: TfrmMAPI; implementation uses ActiveX, SysUtils, EDK, Dialogs, MAPIUtils; (* const PR_EMS_AB_PARENT_ENTRYID = $FFFC0102; // Parent Container ENTRYID PR_EMS_AB_OBJ_DIST_NAME = $803C001E; //OBJ DIST NAME -> /o=../ou=../cn=... PR_EMS_AB_HIERARCHY_PATH = $FFF9001E; //HIERARCHY PATH -> \Organization\Site\Container\Etc... *) {$R *.DFM} procedure TfrmMAPI.FormCreate(Sender: TObject); begin {$IF DEFINED (WIN64)} Self.Caption := Self.Caption + ' - WIN64'; {$ELSE} Self.Caption := Self.Caption + ' - WIN32'; {$IFEND} (* We will use here a "short" version of MapiInitialize. Since we know that the application will not work in a special regime (NT Service, multithreaded, etc), we may not use the MAPIINIT, but rather the NIL constant and the MAPI subsystem will do the job alone *) if failed(MapiInitialize(nil)) then Raise Exception.Create('MAPI_E_NOT_INITIALIZED'); end; procedure TfrmMAPI.FormClose(Sender: TObject; var Action: TCloseAction); begin btLogOffClick(nil); MAPIUninitialize; end; procedure TfrmMAPI.btLogOnClick(Sender: TObject); begin FMAPISession := nil; FAdrBook := nil; try // Logon to Exchange Server hr := MAPILogonEx(Application.Handle, nil, nil, MAPI_EXTENDED or MAPI_NEW_SESSION or MAPI_NO_MAIL or MAPI_LOGON_UI, FMAPISession); if failed(hr) then begin ShowMessage('Can''t LogOn to Server'); exit; end; // Opening Address Book hr := FMAPISession.OpenAddressBook(0, nil, 0 { AB_NO_DIALOG } , FAdrBook); if failed(hr) then begin ShowMessage(GetMAPIError(FMAPISession, hr)); exit; end; finally if failed(hr) then begin if Assigned(FAdrBook) then FAdrBook := nil; if Assigned(FMAPISession) then begin FMAPISession.Logoff(0, 0, 0); FMAPISession := nil; end; end; btLogOff.Enabled := Assigned(FMAPISession); btGetTree.Enabled := btLogOff.Enabled; btLogOn.Enabled := not btLogOff.Enabled; end; end; procedure TfrmMAPI.btLogOffClick(Sender: TObject); begin // Clear TTree Objects ClearTTree(OrganizationTreeView); if Assigned(FAdrBook) then FAdrBook := nil; if Assigned(FMAPISession) then begin FMAPISession.Logoff(Application.Handle, MAPI_LOGOFF_UI, 0); FMAPISession := nil; end; FOrgFound := False; FStrSite := EmptyStr; btLogOff.Enabled := Assigned(FMAPISession); btGetTree.Enabled := Assigned(FMAPISession); btLogOn.Enabled := not Assigned(FMAPISession); ListBox1.Items.Clear; end; procedure TfrmMAPI.btGetTreeClick(Sender: TObject); var ObjType: ULONG; // Object type HierarchyTable: IMAPITable; // hierarchy table TagArray: PSPropTagArray; // Pointer to array of tags Rows: PSRowSet; // Returned rows in MAPI table Count: ULONG; // Row Count begin // Clear TTree Objects ClearTTree(OrganizationTreeView); Rows := nil; FUnkObject := nil; HierarchyTable := nil; TagArray := nil; try (* We will take an interface to the special Root folder. Using 0 for cbEntryID and NIL for EntryID we will get an interface to it. This is a special case of using the OpenEntry function. The Root folder is a MAPI folder object that appears at the top of a folder hierarchy. Only one root folder can exist. Root folders are invisible to the user because of their inability to be moved, copied, renamed, or deleted. *) hr := FAdrBook.OpenEntry(0, nil, @IID_IABContainer, MAPI_BEST_ACCESS, ObjType, IUnknown(FUnkObject)); if failed(hr) then begin ShowMessage(GetMAPIError(FAdrBook, hr)); exit; end; // Now, we will get hierarchy table // and fills the it with containers from multiple levels (* The flag that we use as a first parameter of the GetHierarchyTable function - CONVENIENT_DEPTH will enable us to get information of the container level. CONVENIENT_DEPTH - Fills the hierarchy table with containers from multiple levels. If CONVENIENT_DEPTH is not set, the hierarchy table contains only the container's immediate child containers. *) hr := FUnkObject.GetHierarchyTable(CONVENIENT_DEPTH, HierarchyTable); if failed(hr) then begin ShowMessage(GetMAPIError(FUnkObject, hr)); exit; end; (* To build the tree we will take the minimum necessary number of properties. These are: PR_ENTRYID - ENTRYID of the container PR_EMS_AB_PARENT_ENTRYID - ENTRYID of the parent container PR_DEPTH - the level of container embeddedness We will construct PropertyTagArray that will contain these identifiers and we will require the table to contain only these columns. For this purpose we will have to first allocate memory for this structure through MAPIAllocateBuffer *) hr := MAPIAllocateBuffer(SizeOf(TSPropTagArray) + SizeOf(ULONG) * 2, Pointer(TagArray)); if failed(hr) then begin ShowMessage('MAPI Error'); exit; end; TagArray.cValues := 3; TagArray.aulPropTag[TagArray.cValues - 3] := PR_ENTRYID; TagArray.aulPropTag[TagArray.cValues - 2] := PR_EMS_AB_PARENT_ENTRYID; TagArray.aulPropTag[TagArray.cValues - 1] := PR_DEPTH; // Level in Hierarchy, i.e 0,1,2,3... (* The column set of a table is the group of properties that make up the columns for the rows in the table. There is a default column set for each type of table. The default column set is made up of the properties that the table implementer automatically includes. Table users can alter this default set by calling the IMAPITable.SetColumns method. They can request that other columns be added to the default set � if the table implementer supports them � that columns be removed, or that the order of columns be changed. SetColumns specifies the columns that are returned with each row and the order of these columns within the row. *) hr := HierarchyTable.SetColumns(TagArray, TBL_BATCH); if failed(hr) then begin ShowMessage(GetMAPIError(HierarchyTable, hr)); exit; end; hr := HierarchyTable.GetRowCount(0, Count); if failed(hr) then begin ShowMessage(GetMAPIError(HierarchyTable, hr)); exit; end; (* The IMAPITable.QueryRows method gets one or more rows of data from a table. The value of the RowCount parameter affects the starting point for the retrieval. If RowCount is positive, rows are read in a forward direction, starting at the current position. If RowCount is negative, QueryRows resets the starting point by moving backward the indicated number of rows. After the cursor is reset, rows are read in forward order. The cRows member in the SRowSet structure pointed to by the Rows parameter indicates the number of rows returned. If zero rows are returned: The cursor was already positioned at the beginning of the table and the value of IRowCount is negative. -Or- The cursor was already positioned at the end of the table and the value of IRowCount is positive. The number of columns and their ordering is the same for each row. If a property does not exist for a row or there is an error reading a property, the SPropValue structure for the property in the row contains the following values: PT_ERROR for the property type in the ulPropTag member. MAPI_E_NOT_FOUND for the Value member. *) hr := HierarchyTable.QueryRows(Count, TBL_NOADVANCE, Rows); if failed(hr) then begin ShowMessage(GetMAPIError(HierarchyTable, hr)); exit; end; if not Assigned(Rows) then begin ShowMessage('Nil RowSet'); exit; end; finally (* Freeing the memory that we have taken for our structure *) if Assigned(TagArray) then MAPIFreeBuffer(TagArray); TagArray := nil; (* If so far everything is OK we will build a tree through the BuildTree function. We will feed in the returned RowSet as its entry parameter *) if (hr = S_OK) and Assigned(Rows) then BuildTree(Rows); (* Freeing the memory taken by Rows *) if Assigned(Rows) then FreePRows(Rows); Rows := nil; if Assigned(FUnkObject) then FUnkObject := nil; if Assigned(HierarchyTable) then HierarchyTable := nil; btGetTree.Enabled := False; if OrganizationTreeView.Items.Count > 0 then OrganizationTreeView.AutoExpand := True; end; end; procedure TfrmMAPI.BuildTree(Value: PSRowSet); var Count: ULONG; // Counter Values: ULONG; // Counter ObjType, // GAL object type cValues: ULONG; // Count of Values TempContainerObject: IMAPIContainer; PropTagArrayList: PSPropTagArray; // Array of default properies for TempContainerObject PropValueArray: PSPropValue; // Values MAPIContainer: PMAPITempContainer; OrdPropID: byte; begin TempContainerObject := nil; PropTagArrayList := nil; try for Count := 0 to Value.cRows - 1 do begin (* For each object (container) returned in the RowSet structure we take definite properties. For this purpose we will open each object as an IABContainer. Remember that each object in MAPI has a unique identifier - PR_ENTRYID. Meanwhile we wanted the first column in the previous function to be exactly PR_ENTRYID. This was the case when we created our PropertyTagArray So the first property will be PR_ENTRYID, i.e. Value.aRow[Count].lpProps[0] is PR_ENTRYID *) hr := FAdrBook.OpenEntry(PSPRopValueArray(Value.aRow[Count].lpProps)[0].Value.bin.cb, PEntryID(PSPRopValueArray(Value.aRow[Count].lpProps)[0].Value.bin.lpb), @IID_IABContainer, MAPI_BEST_ACCESS or MAPI_NO_CACHE, ObjType, IUnknown(TempContainerObject)); if (hr = MAPI_E_UNKNOWN_FLAGS) or (MAPI_E_FAILONEPROVIDER=hr) then hr := FAdrBook.OpenEntry(PSPRopValueArray(Value.aRow[Count].lpProps)[0].Value.bin.cb, PEntryID(PSPRopValueArray(Value.aRow[Count].lpProps)[0].Value.bin.lpb), @IID_IABContainer, MAPI_BEST_ACCESS, ObjType, IUnknown(TempContainerObject)); if failed(hr) then begin ShowMessage(GetMAPIError(FAdrBook, hr)); exit; end; // We will get the included (default) Property list for Object hr := TempContainerObject.GetPropList(fMapiUnicode, PropTagArrayList); if failed(hr) then begin ShowMessage(GetMAPIError(TempContainerObject, hr)); exit; end; (* Using the returned PropTagArrayList, we will take the object's properties. The IMAPIProp.GetProps method gets the property values of one or more properties of an object. The property values are returned in the same order as they were requested. That is, the order of properties in the property tag array pointed to by lpPropTagArray matches the order in the array of property value structures pointed to by lppPropArray. The property types specified in the aulPropTag members of the property tag array indicate the type of value that should be returned in the Value member of each property value structure. However, if the caller does not know the type of a property, the type in the aulPropTag member can be set instead to PT_UNSPECIFIED. In retrieving the value, GetProps sets the correct type in the ulPropTag member of the property value structure for the property. *) hr := TempContainerObject.GetProps(PropTagArrayList, fMapiUnicode, cValues, PropValueArray); if failed(hr) then begin ShowMessage(GetMAPIError(TempContainerObject, hr)); exit; end; (* To save definite properties we will use our defined structure that we will attach to each TTreeNode *) GetMem(MAPIContainer, SizeOf(TMAPITempContainer)); ZeroMemory(MAPIContainer, SizeOf(TMAPITempContainer)); OrdPropID := 0; if PSPRopValueArray(Value.aRow[Count].lpProps)[OrdPropID + 1].ulPropTag = PR_EMS_AB_PARENT_ENTRYID then // Helper function that convert Binary to String BinaryToHex(PSPRopValueArray(Value.aRow[Count].lpProps)[OrdPropID + 1].Value.bin.cb, PSPRopValueArray(Value.aRow[Count].lpProps)[OrdPropID + 1].Value.bin.lpb, MAPIContainer.PEntryID); if PSPRopValueArray(Value.aRow[Count].lpProps)[OrdPropID + 2].ulPropTag = PR_DEPTH then MAPIContainer.DEPTH := PSPRopValueArray(Value.aRow[Count].lpProps)[OrdPropID + 2].Value.l; for Values := 0 to cValues - 1 do case PSPRopValueArray(PropValueArray)[Values].ulPropTag of PR_DISPLAY_NAME: MAPIContainer.DisplayName := (PSPRopValueArray(PropValueArray)[Values].Value.lpsz); PR_EMS_AB_OBJ_DIST_NAME: MAPIContainer.Path := (PSPRopValueArray(PropValueArray)[Values].Value.lpsz); PR_EMS_AB_HIERARCHY_PATH: MAPIContainer.HierarPath := (PSPRopValueArray(PropValueArray)[Values].Value.lpsz); PR_ENTRYID: BinaryToHex(PSPRopValueArray(PropValueArray)[Values].Value.bin.cb, PSPRopValueArray(PropValueArray)[Values].Value.bin.lpb, MAPIContainer.EntryID); end; if (Length(MAPIContainer.Path) < 1) or (Pos('_ABViews_', MAPIContainer.Path) <> 0) // We don't like to see Address Book View (_ABViews_) then FreeMem(MAPIContainer) else AddOrgNodes(MAPIContainer); MAPIFreeBuffer(PropValueArray); PropValueArray := nil; MAPIFreeBuffer(PropTagArrayList); PropTagArrayList := nil; TempContainerObject := nil; end; finally if OrganizationTreeView.Items.Count = 0 then ShowMessage('The GAL (Global Address List) is NOT available!'); if Assigned(PropValueArray) then MAPIFreeBuffer(PropValueArray); PropValueArray := nil; if Assigned(PropTagArrayList) then MAPIFreeBuffer(PropTagArrayList); PropTagArrayList := nil; TempContainerObject := nil; end; end; procedure TfrmMAPI.AddOrgNodes(Value: PMAPITempContainer); var Count: integer; begin if AnsiSameText('Global Address List', Value.DisplayName) then // YES this is Global Address List begin FAddressBookNode := OrganizationTreeView.Items.AddObject(nil, 'Global Address List', Value); FAddressBookNode.ImageIndex := 6; FAddressBookNode.SelectedIndex := 6; end else begin if (not FOrgFound and (Value.DEPTH = 0)) then begin FOrgFound := True; FOrgNode := OrganizationTreeView.Items.AddObject(nil, Value.DisplayName, Value); FOrgNode.ImageIndex := 0; FOrgNode.SelectedIndex := 0; exit; end; if (Value.DEPTH = 1) and (Value.HierarPath <> FStrSite) then begin FStrSite := Value.HierarPath; FSiteNode := OrganizationTreeView.Items.AddChildObject(FOrgNode, Value.DisplayName, Value); FSiteNode.ImageIndex := 1; FSiteNode.SelectedIndex := 1; exit; end; if (Value.DEPTH > 1) then begin for Count := 0 to OrganizationTreeView.Items.Count - 1 do if AnsiSameText(PMAPITempContainer(OrganizationTreeView.Items[Count].Data).EntryID, Value.PEntryID) then begin FContNode := OrganizationTreeView.Items[Count]; break; end; FContNode := OrganizationTreeView.Items.AddChildObject(FContNode, Value.DisplayName, Value); FContNode.ImageIndex := 2; FContNode.SelectedIndex := 2; end; end; end; procedure TfrmMAPI.ToolButton4Click(Sender: TObject); begin ShowMessage('www.IMIBO.com'#13#10'info@imibo.com'); end; procedure TfrmMAPI.OrganizationTreeViewClick(Sender: TObject); begin if Assigned(OrganizationTreeView.Selected) then if Assigned(OrganizationTreeView.Selected.Data) then begin ListBox1.Items.Clear; ListBox1.Items.Add('Display Name: ' + PMAPITempContainer(OrganizationTreeView.Selected.Data).DisplayName); ListBox1.Items.Add('Object Dist Name: ' + PMAPITempContainer(OrganizationTreeView.Selected.Data).Path); ListBox1.Items.Add('Hierarchy Path: ' + PMAPITempContainer(OrganizationTreeView.Selected.Data).HierarPath); end; end; end.