How to control Outlook from Word
Article contributed by Jeff Vandervoort and Dave Rado
There are basically three ways reading and writing from Outlook, using Word VBA. You can use GetAddress, which is fast but very clunky and very limiting.You can use OLE automation to acess the Outlook object model (which is slow) or you can use CDO (which is very fast).
“Progranmming with MS Outlook and MS Exchange” by Thomas Rizzo, MS Press, ISBN 0-7356-0509-2, has been recommended tov me as a book which covers CDO very well.
If you're using Outlook 98, you'll already have the CDO type library and can just set a reference to it. If you have Outlook 97, you'll need to download the CDO type library from the Microsoft site.
If you limit the number of fields you extract from each contact, CDO can rival the speed with which the built-in GetAddress dialog is populated. I found it to be approximately 6 times faster than using the Outlook object on my machine.
Once you set a reference to it (Tools/References) it's available to your VBA project. It uses MAPI to give you programatic access to folders and messages in .PSTs and on Exchange Servers. The followng code example is rough...but it works & will get you started. The current version of this in my project has evolved quite a bit and would not be useful to post.
Disclaimer: This code is a cassarole of various KB articles, Help files, FAQ sites, my own previous unsatisfactory attempts, and other sources. It is a work-in-progress, lacks elegance, coding style consistency and has almost no error handling, and probably even a few bugs...but it works and it's fast. It is not “generic”, either, so you'll have to adapt it to your situation. But if that bothers you, consider what you paid for it <g>.
This first procedure is run by the DropButtonClick event of a combobox. It fills the list if it's empty, and if the list is closing after a selection is made, it looks up additional information about the contact (currently, only the fax number). Ctl is the combobox object passed as an argument from the document's class module (it could easily be rewritten as the event procedure itself...it was just better in my app to do it this way). The strategy that works best to build the list is to use CDO. It's very quick where the Outlook object is slow. But after the user makes their selection, I prefer to get the contact properties from Outlook because MS has provided lots of pre-concatenated and pre-parsed combinations of name/address info that are useful to my app and will save me some code. You could also get them from CDO, but you're on your own for that! The Entry ID is the same for .PST objects, whether accessed through CDO or Outlook, and both can use the same MAPI session (though my code doesn't do that, unfortunately.)
Speaking of CDO, I've found that you cannot get the company name from the AddressEntries collection. There's a property for it, .Fields(CdoPR_COMPANY_NAME), but it doesn't work. That's OK because I'm not using the AddressEntries collection, I'm using the Messages collection (which is also the solution to my previous problems about getting an incomplete list from CDO). Just another FYI I learned the hard way!
Public Sub cboTo_DropButtonClick(Ctl
As Object)
'general
Static listOpen
As
Boolean
Dim lstIndx
As Long
Dim
galError As
Boolean
Dim contactList()
As
Variant
Dim counter
As Long
Dim
coName As String
'CDO & Outlook
Dim objSession As MAPI.Session
Const contactFolder
As String
= "Contacts"
'Outlook
Dim olContact As ContactItem
Dim olApp As Outlook.Application
Dim olNS As Outlook.NameSpace
listOpen = Not listOpen
If Ctl.ListCount = 0 Then
'contact list not yet imported
this session
' create a session and log on
Set objSession =
StartQuietSession(NoMailServices:=True)
System.Cursor = wdCursorWait
galError = GetAddressList(Session:=objSession, _
FolderName:=contactFolder, ReturnArray:=contactList())
SortArray ArrayName:=contactList
'sort by name
'find out how many records have
company names w/o display names
For counter = 0
To UBound(contactList)
If contactList(counter, 0) <> ""
Then Exit For
Next
'sort by co. name for records w/o
names
'Blake: SortArray is a VBA procedure I wrote to replace
'WordBasic.SortArray
'you can do this with WordBasic.SortArray, too
If counter > 1
Then _
SortArray ArrayName:=contactList,
SortFrom:=0, SortTo:=counter - 1, SortKey:=1
Ctl.List = contactList
'close session
and logoff
objSession.Logoff
StatusBar = ""
System.Cursor = wdCursorNormal
End If
If Not listOpen Then
'enter info in other controls found in doc
Set objSession =
StartQuietSession(NoMailServices:=True)
lstIndx = Ctl.ListIndex
If lstIndx > -1
Then
coName = Ctl.List(lstIndx, 1)
With
ActiveDocument
'Blake: ControlExists is a procedure
that loops thru all controls in a doc to see if
'the
specified control exists; it will not be applicable outside of my app, but
illustrates
'how
you can get the info out of the list
If ControlExists(ControlName:="txtCompany")
Then .txtcompany = coName
If ControlExists(ControlName:="cboCompany")
Then .cboCompany = coName
.CustomDocumentProperties("DocTo").Value
= Ctl
.CustomDocumentProperties("DocCompany").Value
= coName
'from here on, get info from Outlook
object to benefit from Outlook's
'parsing and concatenating contact properties
'(remember: listbox entries came from CDO for speed of building list)
'GetObject
doesn't seem any faster, here, contrary to docs
Set olApp = New Outlook.Application
Set
olNS = olApp.GetNamespace("MAPI")
Set
olContact = olNS.GetItemFromID(Ctl.List(lstIndx, 2))
If ControlExists(ControlName:="txtFaxNumber")
Then _
.txtfaxnumber = olContact.BusinessFaxNumber
End If
End With
End If
'close session and logoff
objSession.Logoff
Set olApp = Nothing
Set olNS = Nothing
Set olContact = Nothing
End If
End Sub
Public Function StartQuietSession(NoMailServices
As Boolean) As
MAPI.Session
'attempts to log on to MAPI with using existing session
'if no session exists, logs on with default profile
'returns session object if successful (through either means), Nothing if
' unsuccessful derived from MSKB article Q171422 "Logging onto Active
'Messaging session with default profile" rewrote as a function,
'modified for compat. with JRVsystems modRegistry module
Dim sKeyName
As
String
Dim sValueName
As
String
Dim sDefaultUserProfile
As String
Dim
retvalue As Long
Dim objSession As
MAPI.Session
StatusBar = "Please wait: Communicating with Outlook..."
System.Cursor = wdCursorWait
On Error GoTo ErrorHandler
Set objSession = CreateObject("MAPI.Session")
'Try to logon. If this fails, the most likely reason is
'that you do not have an open session. The error
'-2147221231 MAPI_E_LOGON_FAILED will return. Trap
'the error in the ErrorHandler
objSession.Logon ShowDialog:=False, NewSession:=False
Set StartQuietSession = objSession
StatusBar = ""
System.Cursor = wdCursorNormal
Exit Function
ErrorHandler:
Select Case Err.Number
Case
-2147221231 'MAPI_E_LOGON_FAILED
'Need to find out what OS is in use, the keys are
different
'for WinNT and Win9x.
Select Case
System.OperatingSystem
Case
"Windows" '= Win 9x
sKeyName = "Software\Microsoft\Windows Messaging " & _
"Subsystem\Profiles"
Case
"Windows NT"
sKeyName = "Software\Microsoft\Windows NT\CurrentVersion\" & _
"Windows Messaging Subsystem\Profiles"
End Select
sValueName =
"DefaultProfile"
'Blake: QueryValue is in another of my
library modules.
'You
can use System.PrivateProfileString to
'do the
same thing (I just use this because it can be used in
'any
VBA or VB app, which don't have an
'equivalent for System.PrivateProfileString
sDefaultUserProfile = QueryValue(sKeyName, sValueName, _
HKEY_CURRENT_USER)
'NoMail argument not documented in
Help file,
'but
appears to load (and later unload) only the .PST
'which
greatly speeds up operation since we don't need the other services
'esp.
fax which is glacial
objSession.Logon ProfileName:=sDefaultUserProfile, _
ShowDialog:=False, NoMail:=NoMailServices
Set
StartQuietSession = objSession
StatusBar = ""
System.Cursor = wdCursorNormal
Exit Function
Case Else
StatusBar = ""
System.Cursor = wdCursorNormal
MsgBox
"An error has occured while attempting" & Chr(10) & _
"To create and logon to a new ActiveMessage session." & _
Chr(10) & "Please report the following error to your " & _
"System Administrator." & Chr(10) & Chr(10) & _
"Error Location: frmMain.StartMessagingAndLogon" & _
Chr(10) & "Error Number: " & Err.Number & Chr(10) & _
"Description: " & Err.Description
End Select
End Function
Function GetAddressList(Session
As MAPI.Session, FolderName
As String, _
ReturnArray() As Variant)
As Boolean
'Returns true if successful, false if not
'FolderName = name of Outlook (or other MAPI) folder to look in
'ReturnArray = 3-column array to be populated by GetAddressList for use in
'listbox/combobox
' col0 = Name
' col1 = Company
' col2 = unique ID
'if Name and Company are both "", skips the oneMessage record
'because there's nothing to display
'adapted from MSKB article Q172093 -
'HOWTO Access MAPI Address Books with Active Messaging 1.1
Dim collInfoStores
As InfoStores
Dim
objFolder As
Folder
Dim collMessages
As
Messages
Dim oneMessage
As
Message
Dim counter
As Long
Dim
numContacts As
Long
Dim nm As String
Dim co As String
'empirically, "Top of Personal Folders" is top folder
name with Outlook 98
'regardless of friendly name of PST (which, by default, is "Personal
Folders")
'IOW, if PST friendly name is "My Message Store", top folder is still
called
'"Top of Personal Folders", not "Top of My Message Store"
Set objFolder = FindTargetFolder(objSession:=Session,
_
strTargetTopFolder:="Top of
Personal Folders", _
strSearchName:=FolderName)
Set collMessages = objFolder.Messages
'per KB, Messages is a "large collection", so .Count
property may be incorrect
'so we'll count them ourselves (essentially instantaneous...why can't CDO
do this??)
For Each oneMessage
In
collMessages
numContacts = numContacts + 1
Next
ReDim
ReturnArray(numContacts - 1, 2)
For Each oneMessage In
collMessages
StatusBar = "Please wait. Getting contacts from Outlook
" & _
Format(counter / numContacts, "(0%)")
'have to initialize at each pass,
because empty fields raise an error,
'so variable retains previous (now incorrect) value
nm = ""
co = ""
With oneMessage
On Error
Resume Next
nm = .Fields(CdoPR_DISPLAY_NAME)
co = .Fields(CdoPR_COMPANY_NAME)
On Error
GoTo 0
If nm <> "" Or co <> ""
Then
'unlikely, but both could be "" in which case we won't have much to show...
ReturnArray(counter, 0) = nm
ReturnArray(counter, 1) = co
ReturnArray(counter, 2) = .id
counter
= counter + 1
End If
End With
Next oneMessage
Set collInfoStores = Nothing
Set objFolder = Nothing
Set collMessages = Nothing
GetAddressList = True
End Function
Private Function FindTargetFolder(objSession
As MAPI.Session, _
strTargetTopFolder As String, _
strSearchName As String)
As Folder
'adapted from MSKB article Q171638
Dim objInfoStores
As InfoStores
Dim
objInfoStore As
InfoStore
Dim objTopFolder
As
Folder
Dim objPSTFolders
As
Folders
Dim i As
Long
Dim er As
Long
Set
objInfoStores = objSession.InfoStores
'This loop finds the TopFolder you specified.
For i = 1 To
objInfoStores.Count
Set objInfoStore =
objInfoStores(i)
Set objTopFolder =
Nothing
On Error Resume Next
Set objTopFolder =
objInfoStore.RootFolder
er = Err.Number
On Error GoTo 0
If Not er = -2147221227
Then
'MAPI_E_NETWORK_ERROR
If objTopFolder.Name = strTargetTopFolder
Then
'Found PST
'Because you
can have more than one PST in a profile,
'you
may want to put another check here to make sure you have
'the
correct PST. This check would need to specify a string
'that
is the name of the PST you are looking for.
'It
would look something like this:
'If
objInfoStore.Name = "MyPST" Then 'Found own PST
' Exit For
'End If
End If
End If
Next
i
Set objPSTFolders = objTopFolder.Folders
For i = 1 To
objPSTFolders.Count
'MsgBox objPSTFolders.Item(i).Name
If objPSTFolders.Item(i).Name = strSearchName Then
Exit For
End If
Next i
Set FindTargetFolder = objPSTFolders.Item(i)
Set objTopFolder = Nothing
Set objPSTFolders = Nothing
Set objInfoStores = Nothing
Set objInfoStore = Nothing
End Function