Getting Mailbox Information from Exchange 2003 Cluster

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”

https://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” https://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).