I was recently tinkering with Visual Basic Script that would obtain the user mailbox information from the Exchange 2003 cluster. In particular I was interested in getting mailbox information stored in Active Directory as well as obtaining current mailbox status (such as current size of the mailbox).
As my VB script skills are dated the first thing was to gather basic resources such as:
“Windows Script 5.6 Documentation “
http://www.microsoft.com/downloads/details.aspx?FamilyId=01592C48-207D-4BE1-8A76-1C4099D7BBB9&displaylang=en
Scriptomatic tool for generating WMI scripts http://www.microsoft.com/downloads/details.aspx?familyid=09dfc342-648b-4119-b7eb-783b0f7d1178&displaylang=en
ADSI Scriptomatic for generating ADSI scripts http://www.microsoft.com/technet/scriptcenter/tools/admatic.mspx
In general the cluster name may or may not be associated with the node that has Exchange resources so we cannot send the query to the cluster name. Also it is common to see Active-Active-Pasive Exchange clusters where the mailboxes are partitioned among the active nodes. To ensure we get complete list of all mailboxes we should query all active nodes with Exchange resources.
Armed with the Scriptomatic we quickly see that we can make a WMI query on the MSCluster WMI class to obtain the list of all nodes of the (Windows) cluster. We can then check the status of these nodes and return in the array the list of active nodes. Here we use the semi-synchronous call that works faster especially for large result sets (we will appreciate it later when we query for mailbox status). Essentially we can start enumerating objects before the entire result set is available-you can find further detail in
“Making a Semisynchronous Call with VBScript”
http://msdn2.microsoft.com/en-us/library/aa392301(VS.85).aspx
In the WMI query we are getting all the cluster nodes nodes and we are specifying "WQL" as the type of the query language (I think the only one available still with WMI) so that we can get to specify flags required for semi-synchronous call.
'Checks for cluster configuration if available. In non-clustered information will return empty array ' WMI query is issued against the cluster name to return the list of the nodes of the cluster. We then ' add to dymanic array nodes that are active Function GetClusterNodes(cluster) Dim activeNodes() ReDim activeNodes (0) Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20 'On Error Resume Next WScript.Echo "Getting cluster configuration from " & cluster Set objWMIService = GetObject("winmgmts:" _ & "{impersonationLevel=impersonate}!\\" & cluster & _ "\root\mscluster") Set colItems = objWMIService.ExecQuery ("Select * from MSCluster_Node", "WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly) For Each objItem in colItems If CInt(objItem.State)=0 Then activeNodes(UBound(activeNodes))=objItem.Name ReDim Preserve activeNodes(UBound(activeNodes)+1) End If WScript.Echo "Name: " & objItem.Name WScript.Echo "State: " & objItem.State WScript.Echo "Status: " & objItem.Status WScript.Echo "Roles: " & objItem.Roles WScript.Echo "-----------------------------------------------------" Next ReDim Preserve activeNodes(UBound(activeNodes)-1) WScript.Echo "Active Nodes are:" For Each node in activeNodes WScript.Echo "Node=" & node Next Set objWMIService = Nothing GetClusterNodes = activeNodes End Function
'Checks for cluster configuration if available. In non-clustered information will return empty array ' WMI query is issued against the cluster name to return the list of the nodes of the cluster. We then ' add to dymanic array nodes that are active
Function GetClusterNodes(cluster) Dim activeNodes() ReDim activeNodes (0)
Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20
'On Error Resume Next WScript.Echo "Getting cluster configuration from " & cluster Set objWMIService = GetObject("winmgmts:" _ & "{impersonationLevel=impersonate}!\\" & cluster & _ "\root\mscluster") Set colItems = objWMIService.ExecQuery ("Select * from MSCluster_Node", "WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly) For Each objItem in colItems If CInt(objItem.State)=0 Then activeNodes(UBound(activeNodes))=objItem.Name ReDim Preserve activeNodes(UBound(activeNodes)+1) End If WScript.Echo "Name: " & objItem.Name WScript.Echo "State: " & objItem.State WScript.Echo "Status: " & objItem.Status WScript.Echo "Roles: " & objItem.Roles WScript.Echo "-----------------------------------------------------" Next
ReDim Preserve activeNodes(UBound(activeNodes)-1) WScript.Echo "Active Nodes are:" For Each node in activeNodes WScript.Echo "Node=" & node Next Set objWMIService = Nothing GetClusterNodes = activeNodes End Function
The alternative to this approach would be to query for NodeToActiveGroup class.The advantage is that it returns the active nodes of the cluster and that it allows us to check the cluster Resource Groups so we could try to get Active nodes with Exchange resources. However, because strings are returned parsing is more complex. Another issue is that in Cluster Administrator tool the users can change the name of the Resource Group so it is problematic to do a test on the PartComponent property for Exchange resource groups. The code below does not to it and prevents adding the same node twice (once for OS resource group and second time for Exchange resource group).
' Query the cluster for the list of active nodes. Note we parse out the name of the node from ' the GroupComponent property. We test is node has not been added already as same node appears ' multiple times (for each cluster resource). As nodes are added we ' we resize the activeNodes array dymamically. Function GetActiveClusterNodes (cluster) 'On Error Resume Next Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20 WScript.Echo "Querying cluster " & cluster & " for active Exchange nodes..." Set objWMIService = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\\" & cluster & "\root\MSCluster") Set colItems = objWMIService.ExecQuery("SELECT * FROM MSCluster_NodeToActiveGroup", "WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly) Dim activeNodes() ReDim activeNodes (0) label="MSCluster_Node.Name=" For Each objectitem In colItems groupComponent = Trim(objectitem.GroupComponent) WScript.Echo "Group=" & objectitem.GroupComponent & " Part=" & objectitem.PartComponent If InStr(groupComponent,label)>0 Then node = Mid ( groupComponent, Len(label)+2, Len(groupComponent)-Len(label)-2 ) found = False For Each n in activeNodes If StrComp( n, node)=0 Then found=True Exit For End If Next If found = False Then activeNodes(UBound(activeNodes)) = node ReDim Preserve activeNodes(UBound(activeNodes)+1) End If End If Next ReDim Preserve activeNodes(UBound(activeNodes)-1) WScript.Echo "Active Nodes detected:" For Each node in activeNodes WScript.Echo node Next Set objWMIService = Nothing GetActiveClusterNodes = activeNodes End Function
' Query the cluster for the list of active nodes. Note we parse out the name of the node from ' the GroupComponent property. We test is node has not been added already as same node appears ' multiple times (for each cluster resource). As nodes are added we ' we resize the activeNodes array dymamically.
Function GetActiveClusterNodes (cluster) 'On Error Resume Next Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20 WScript.Echo "Querying cluster " & cluster & " for active Exchange nodes..."
Set objWMIService = GetObject("winmgmts:" & "{impersonationLevel=impersonate}!\\" & cluster & "\root\MSCluster") Set colItems = objWMIService.ExecQuery("SELECT * FROM MSCluster_NodeToActiveGroup", "WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly)
Dim activeNodes() ReDim activeNodes (0) label="MSCluster_Node.Name=" For Each objectitem In colItems groupComponent = Trim(objectitem.GroupComponent) WScript.Echo "Group=" & objectitem.GroupComponent & " Part=" & objectitem.PartComponent If InStr(groupComponent,label)>0 Then node = Mid ( groupComponent, Len(label)+2, Len(groupComponent)-Len(label)-2 ) found = False For Each n in activeNodes If StrComp( n, node)=0 Then found=True Exit For End If Next If found = False Then activeNodes(UBound(activeNodes)) = node ReDim Preserve activeNodes(UBound(activeNodes)+1) End If End If Next ReDim Preserve activeNodes(UBound(activeNodes)-1)
WScript.Echo "Active Nodes detected:" For Each node in activeNodes WScript.Echo node Next
Set objWMIService = Nothing GetActiveClusterNodes = activeNodes End Function
With all this it seems that first version is better because we cannot guarantee that the Administrator will not renaming resources in Cluster Administrator tool. We simply will then query all the active nodes, and for the active nodes with no Exchange resources we will simply not see any mailboxes returned. Assuming CLUSTER_NAME holds the name of the cluster we iterate as follows
Dim EXCHANGE_SERVER_NODES EXCHANGE_SERVER_NODES = GetClusterNodes(CLUSTER_NAME) For Each server In EXCHANGE_SERVER_NODES WScript.Echo "Querying mailbox status on node " & server GetMailboxStatus(server) WScript.Echo Now() Next
Dim EXCHANGE_SERVER_NODES EXCHANGE_SERVER_NODES = GetClusterNodes(CLUSTER_NAME)
For Each server In EXCHANGE_SERVER_NODES WScript.Echo "Querying mailbox status on node " & server GetMailboxStatus(server) WScript.Echo Now() Next
So now we can put together a function to return us the mailboxes for a given machine name. In this case we skip all the system mailboxes as we are interested just in "ordinary" users. Unlike with ADSI there is no easy way of doing the filtering in the WQL so we have to filter in code in this case.
'Uses WMI interface to query the Exchange node 'server' for mailbox status information. 'Note that the query will return ALL mailboxes (included system mailboxes and mailboxes to be 'deleted or reassigned to a different user. 'The subroutine will ignore all system mailboxes Sub GetMailboxStatus(server) Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20 Set objWMIService = GetObject("winmgmts:" _ & "{impersonationLevel=impersonate}!\\" & server & _ "\ROOT\MicrosoftExchangeV2") Set colItems = objWMIService.ExecQuery ("Select * from Exchange_Mailbox","WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly) k=0 total=0 For Each objItem in colItems total=total+1 'Exclude System Mailboxes If InStr(UCase(objItem.LegacyDN), "CN=SYSTEMMAILBOX") = 0 And InStr(UCase(objItem.LegacyDN), "CN=SMTP") = 0 _ And InStr(UCase(objItem.LegacyDN), "CN=MICROSOFT SYSTEM ATTENDANT")=0 Then k=k+1 WScript.Echo k & " MailboxName=" & objItem.MailboxDisplayName & " StorageLimitInfo="& objItem.StorageLimitInfo WScript.Echo " Server Name=" & objItem.ServerName & " StorageGroup=" & objItem.StorageGroupName & " Store Name=" & objItem.StoreName End If Next WScript.Echo " Detected " & k & " user mailboxes (total="& total &"). Skipped " & total - k & " system mailboxes." Set objWMIService = Nothing End Sub
'Uses WMI interface to query the Exchange node 'server' for mailbox status information. 'Note that the query will return ALL mailboxes (included system mailboxes and mailboxes to be 'deleted or reassigned to a different user. 'The subroutine will ignore all system mailboxes
Sub GetMailboxStatus(server) Const wbemFlagReturnImmediately = &h10 Const wbemFlagForwardOnly = &h20
Set objWMIService = GetObject("winmgmts:" _ & "{impersonationLevel=impersonate}!\\" & server & _ "\ROOT\MicrosoftExchangeV2") Set colItems = objWMIService.ExecQuery ("Select * from Exchange_Mailbox","WQL", wbemFlagReturnImmediately + wbemFlagForwardOnly) k=0 total=0 For Each objItem in colItems total=total+1 'Exclude System Mailboxes If InStr(UCase(objItem.LegacyDN), "CN=SYSTEMMAILBOX") = 0 And InStr(UCase(objItem.LegacyDN), "CN=SMTP") = 0 _ And InStr(UCase(objItem.LegacyDN), "CN=MICROSOFT SYSTEM ATTENDANT")=0 Then k=k+1 WScript.Echo k & " MailboxName=" & objItem.MailboxDisplayName & " StorageLimitInfo="& objItem.StorageLimitInfo WScript.Echo " Server Name=" & objItem.ServerName & " StorageGroup=" & objItem.StorageGroupName & " Store Name=" & objItem.StoreName End If Next WScript.Echo " Detected " & k & " user mailboxes (total="& total &"). Skipped " & total - k & " system mailboxes." Set objWMIService = Nothing End Sub
Now that we have the MailBox status information we turn to querying for user mailbox information stored in Active Directory. The default queries are limited to results sets of just 1000 object. In order to process large result sets we have to implement a paged query. This is achieved by specifying "Page Size" property on the Command object. Now the server will return to client data in chunks having at most Page Size objects. The paging mechanism is transparent to the client so no special code to handle paging is required.
“Retrieving Large Results Sets” http://msdn2.microsoft.com/en-us/library/aa746459(VS.85).aspx has more information about this topic. It is also recommended not to cache the results so we set the "Cache Results" property to False.
Our query uses a filter to return the user mailboxes skipping any System Mailbox as we do not want to process them in this case.
'Queries Active Directory for mailbox configuration. The query will list only user mailboxes, skipping system mailboxes. Sub GetMailBoxes()
Const DOMAIN_CONTROLLER = "NoddyDCGC" Const DOMAIN_LDAP = "DC=noddy,DC=com"
strADsPath = "LDAP://" & DOMAIN_CONTROLLER & "/" & DOMAIN_LDAP
'Open connection to AD Set objConnection = CreateObject("ADODB.Connection") objConnection.Open "Provider=ADsDSOObject;"
Set objCommand = CreateObject("ADODB.Command") objCommand.ActiveConnection = objConnection objCommand.Properties("Page Size") = 500 objCommand.Properties("Timeout") = 10 ' Seconds objCommand.Properties("Cache Results") = False
'query for user object in Active Directory objCommand.CommandText = "<" & strADsPath & ">" & ";(&(objectClass=user)(homeMDB=*)(!CN=SystemMailbox{*}))" & ";distinguishedName,name" & ";subtree"
'Execute search to get Recordset Set objMailboxRS = objCommand.Execute
total=0 While Not objMailboxRS.EOF 'Bind to mailbox object for the current user Set objMailbox = GetObject("LDAP://" & DOMAIN_CONTROLLER &"/" & objMailboxRS.Fields("distinguishedName") ) Set objStore = GetObject("LDAP://" & DOMAIN_CONTROLLER &"/" & objMailbox.homeMDB) 'Update mailbox information in Database WScript.Echo "Mailbox " & objMailbox.name & " is stored in " & objStore.cn total=total+1 objMailboxRS.MoveNext Wend 'End While EOF WScript.Echo "Detected " & total & " user mailboxes." objMailboxRS = null End Sub
In general, the number of mailboxes returned by querying Active Directory using ADSI interface will be different from that reported by WMI query. This discrepancy can occur for example if a user is deleted from Active Directory but the associated mailbox has not yet been purged (or reassigned to a different user). Also the opposite may occur where a user account has already been created but the mailbox has not yet been created (because he never accessed it nor received yet and email).