If you missed the other articles in this series please read:
- Scripting Exchange Using VBScript and ADSI (Part 2)
- Scripting Exchange Using VBScript and ADSI (Part 3)
Introduction
Scripting can be a powerful tool. There are a lot well written scripts for use with Exchange floating around the Internet that can be quite useful. Changing to fit your needs is not too difficult, but writing your own scripts and making scripts work better for your needs requires some knowledge of the inner workings of Active Directory, Exchange and of course the scripting language.
This article will present a few basic scripting methods. I will explain in great detail both the scripting logic provided by VBScript and that of the interfaces provided by Active Directory.
Accessing Active Directory Objects
Exchange 2000/3 store their objects in Active Directory. To access user, contact and group information you need to be familiar with the way objects are stored and the LDAP notation that is used for accessing these objects.
Take for example a simple script to retrieve a user’s home server.
Dim MyUser
Dim HomeServerSet MyUser = GetObject (“LDAP://CN=Administrator,CN=Users,DC=sunnydale,DC=muni”)
HomeServer = myUser.Get(“msExchHomeServerName”)
WScript.Echo “HomeServer is ” & HomeServer
The script begins with declaring two variables. In VBScript, unlike VB6, variables have no type until they are first assigned a value. In fact the first two line are redundant since you don’t really have to declare variables with VBScript. However, it might make the script more readable if you do declare them and is considered to be good practice.
Then the script sets the MyUser variable as a pointer to an Active Directory object, in this case the, the Administrator account which is located under the Users container of the domain. Pointers use the keyword “Set” to distinguish between them and other variables. Note that the HomeServer variable in the next line is set to equal a property of the user. However, it is not a pointer, so the keyword “Set” does not appear. To sum this up, MyUser points to an Active Directory object where the information is actually stored while HomeServer is now a variable of type string that stores some information obtained from Active Directory.
The GetObject function used to retrieve a pointer to Active Directory objects uses the LDAP notation. It has three types of objects or leafs:
CN = Containers, users, contacts, groups and other objects that usually don’t have child objects.
OU = Organizational Units which contain objects such users, contacts, groups and other OUs
DC = Domain containers which are created by separating the full internal domain name (in this example, sunnydale.muni)
You can view the Active Directory LDAP structure by installing the Windows 2000/3 Support Tools and running ADSIEdit.
This version of the script will also work:
Set MyUser = GetObject (“LDAP://CN=Administrator,CN=Users,DC=sunnydale,DC=muni”)
HomeServer = myUser.msExchHomeServerName
WScript.Echo “HomeServer is ” & HomeServer
Note that the “Get” function is not required to get a property of an object but you might encounter scripts that use it just the same.
The script queries the first domain controller it finds. If you are looking for a property that only exists on a global catalog server (GC) you should make that all your domain controllers are also global catalog servers or that you specify a particular GC like this:
Set MyUser = GetObject (“LDAP://GCName /CN=Administrator,CN=Users,DC=sunnydale,DC=muni”)
You may also query only GCs by using the GC:// notation as follows:
Set MyUser = GetObject (“GC://GCName /CN=Administrator,CN=Users,DC=sunnydale,DC=muni”)
You can also set properties for an object.
Set MyUser = GetObject (“LDAP://CN=Administrator,CN=Users,DC=sunnydale,DC=muni”)
MyUser.msExchHideFromAddressLists = True
MyUser.SetInfo
When you change a property for an object, the change is cached, not implemented right away. The SetInfo subroutine attempts to write the changed attributes to the object. Of course, Active Directory has all sorts of checks to determine whether the attributes are valid, and if one isn’t VBScript will throw an error.
Working with Collections
VBScript presents you with the option to work with collection. For example, you can change properties of all the files in single folder. With Active Directory this means you can set properties, for example, for all the users in an OU. To do this you set a pointer to the OU and enumerate all the users in it by treating it as a collection.
Set CNUsers = GetObject (“LDAP://CN=Users,DC=sunnydale,DC=muni”)
CNUsers.Filter = Array(“user”)
For Each User in CNUsers
HomeServer = User.msExchHomeServerName
DisplayName = User.displayName
WScript.Echo “HomeServer for ” & DisplayName & ” is ” & HomeServer
Next
You can also select different operations for different types of objects (called classes) in an OU or simply count.
To this you use “Select Case” which serves as an extended “If”.
Set CNUsers = GetObject (“LDAP://CN=Users,DC=sunnydale,DC=muni”)
Contacts = 0
Users = 0
For Each User in CNUsers
Select Case User.class
Case “user”
Users = Users + 1
Case “contact”
Contacts = Contacts +1
End Select
Next
WScript.Echo “Users: ” & Users
WScript.Echo “Contacts: ” & Contacts
You might also want to do a recursive search, that is, search in all the OUs within an OU.
For this you need to create a subroutine which can call itself.
Set TopLevel = GetObject (“LDAP://OU=Domain Users,DC=sunnydale,DC=muni”)
Contacts = 0
Users = 0
CountUsersContacts (TopLevel)Sub CountUsersContacts (ObjOU)
For Each FoundObject in ObjOU
Select Case FoundObject.class
Case “user”
Users = Users + 1
Case “contact”
Contacts = Contacts +1
Case “organizationUnit”,”container”
CountUsersContacts (FoundObject)
End Select
NextWScript.Echo “Users: ” & Users
WScript.Echo “Contacts: ” & Contacts
So, the main program calls the subroutine “CountUsersContacts” with the top level OU “Domain Users” and if it finds an OU within the top level OU it calls the subroutine “CountUsersContacts” with this OU as input. Eventually what happens is that all the OUs and their child OUs are scanned.
If you want to scan the entire domain you can replace the following line:
Set TopLevel = GetObject (“LDAP://DC=sunnydale,DC=muni”)
If you’re like me, writing scripts for customers, so the domain name is not really a constant, you can add a few lines at the beginning of the script to find the root of the current domain.
Set rootDSE = GetObject(“LDAP://RootDSE”)
domainContainer = rootDSE.Get(“defaultNamingContext”)
Set TopLevel = GetObject(“LDAP://” & domainContainer)
In LDAP 3.0, rootDSE is defined as the root of the directory data tree on a directory server. The rootDSE is not part of any namespace. The purpose of the rootDSE is to provide data about the directory server, in this case Active Directory. You can still tie this to a certain server if you want by changing this line:
Set rootDSE = GetObject(“LDAP://MyGlobalCatalogServer/RootDSE”)
Performing LDAP searches
Using recursive logic to search through a database is not the preferable way. It is slow because it reads every object it goes through and it is tough to debug because of the complex logic it uses. It is also memory consuming since you have multiple instances of the same function running at the same time.
Active Directory provides another way of searching by constructing an LDAP query. Constructing LDAP queries can be a complex matter.
The following table shows a few examples of LDAP filters used in searches:
Filter |
Description |
(objectCategory=*) |
All objects |
(&(objectClass=user)(!(cn=susan))) |
All user objects except susan |
(sn=sm*) |
All objects with a surname that starts with sm |
(&(objectClass=contact)(|(sn=Smith) |
All contacts with a surname equal to Smith or Johnson |
As you can see, filters are constructed from operators that construct the logic, wildcards, allowing you a more relaxed search and regular LDAP objects and attributes. as shown in previous example
Commonly Used LDAP Search Filter Operators
Operator |
Description |
= |
Equal to |
~= |
Approximately equal to |
<= |
Lexicographically less than or equal to |
>= |
Lexicographically greater than or equal to |
& |
AND |
| |
OR |
! |
NOT |
Constructing or debugging an LDAP query, especially a complex one, is not an easy task as these can become very long and you might lose yourself in their syntax. Luckily for us scripting guys and girls Microsoft provides a way to construct and LDAP query using Exchange System Manager (ESM)
Run Exchange System Manager and create an Address List.
Right click All Address Lists and choose New… Address List.
Give the address list a name and press the Filter Rules button.
This easy to use dialog box allows you to easily construct a query. In this example I’m looking for all of the Exchange Recipients that are located in Ohio and belong to the Finance department.
After entering the query that you desire press the OK button and then Finish.
Now you may ask, “Where is LDAP query”? On the property page of the newly created address list you can view it.
You can also press the Preview button to see the results of your Query. The LDAP query above can be copied and used in a script.
There is also, with Windows 2003 a way to create a query using Active Directory Users and Computers. The following screenshots will show how to construct a query that we shall use in our counting users and contacts script.
Now that the query string is constructed we are finally ready for a new version of the script.
Set rootDSE = GetObject(“LDAP://RootDSE”)
DomainContainer = rootDSE.Get(“defaultNamingContext”)Set conn = CreateObject(“ADODB.Connection”)
conn.Provider = “ADSDSOObject”
conn.Open “ADs Provider”ldapStr = “<LDAP://” & DomainContainer & “>;(&(&(& (mailnickname=*) (| (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=contact)) ))));adspath;subtree”
Set rs = conn.Execute(ldapStr)
While Not rs.EOF
Set FoundObject = GetObject (rs.Fields(0).Value)
Select Case FoundObject.class
Case “user”
Users = Users + 1
Case “contact”
Contacts = Contacts +1
End Select
rs.MoveNext
WendWScript.Echo “Users: ” & Users
WScript.Echo “Contacts: ” & Contacts
A few lines have been added to open an LDAP connection to Active Directory by using ADSI, and “rs” is a pointer to a collection of directory names of objects returned as a result of the LDAP query after it is run.
Since it is not a collection of pointers to objects in Active Directory but only to their directory names, instead of using “For Each…In” we use a “While Not [object].EOF…Wend” loop that is advanced by using the “MoveNext” subroutine that advances the internal index of “rs”. To examine the Active Directory object itself, we call the GetObject function with rs.Fields(0).Value as input since it contains the directory name of the object.
This script still goes through every object. A more efficient script can simply return the number of users and the number of contacts by executing separate LDAP queries and using the “RecordCount” property of the collection as follows:
Set rootDSE=GetObject(“LDAP://RootDSE”)
DomainContainer = rootDSE.Get(“defaultNamingContext”)Set conn = CreateObject(“ADODB.Connection”)
conn.Provider = “ADSDSOObject”
conn.Open “ADs Provider”
ldapStrContacts = “<LDAP://” & DomainContainer & “>;(&(&(& (mailnickname=*) (| (&(objectCategory=person)(objectClass=contact)) ))));adspath;subtree”
ldapStrUsers = “<LDAP://” & DomainContainer & “>;(&(&(& (mailnickname=*) (| (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*))) ))));adspath;subtree”Set rs1 = conn.Execute(ldapStrContacts)
WScript.Echo “Contacts : ” & rs1.RecordCount
Set rs2 = conn.Execute(ldapStrUsers)
WScript.Echo “Users : ” & rs2.RecordCount
Conclusion
We’ve gone through a few ways of accessing Active Directory objects and performing searches. This is of course just the tip of the iceberg when it comes to managing Active Directory and Exchange using scripts, but it’s definitely a good starting point since finding objects, reading and writing their properties is the foundation for a lot of Exchange related scripts.
If you missed the other articles in this series please read: