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:

Querying Cluster Information

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

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

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

Getting Mailbox Status information

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

Querying Mailbox information in Active Directory using ADSI

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                       

Differences between mailbox numbers returned by WMI and ADSI

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).