Chapter 12 - Exchange 2003 Scripting
Over three years ago, a particular publisher in the computer field contracted me to write a book - which I did. They paid me for it, but they chose to never publish it. It was written with Exchange Server 2003 service pack 1 in mind - as you read the text, keep in mind that it was written long before E12/Exchange 2007 was even in beta. Here is a chapter from that book. These scripts will usually work with Exchange Server 2007 - except when they use CDO, CDOEX, or CDOEXM.
It's taken me over a week to translate this from Word 2003 to web format. It's not as easy to do as I thought!
You can find some earlier versions of these scripts already on the blog. The ones in this chapter tend to be a bit better implemented and debugged.
Enjoy!
Chapter 12
Scripting Tasks
Scripting is not magic. It is the application of a logical thought process to a problem. If you can describe the problem, there is probably a way to script a solution. However, scripting is not a panacea. It is best used for relatively discrete, often small repetitive tasks where high performance is not a key design metric. To implement a full application with scripting is challenging and supporting large scripts can also be somewhat difficult, as script-based development environments aren’t generally as rich as those for more complex languages.
There are, unfortunately, things that just can’t be done in a script because Microsoft hasn’t provided interfaces to do so. But those are getting fewer and fewer with every service pack and release of software. One of the key omissions in the Windows Scripting Host is that there is no way to build GUIs without using IE (which is just a little bit difficult).
I am going to show you some tools that I use, based on VBScript, that were developed by leveraging reusable components. A reusable component only has to be developed once. The name “reusable component” is just a fancy way of saying “a small piece of a program”. If properly designed, then this small program can then be plugged in to a larger program and used many times by many different programs. My philosophy of programming has always been “be lazy—only do the job once”. Some people, including myself, would refer to this as "being efficient".
The concept of building programs with reusable components is not the same as the concept of these programs being “recipes” that you have to “cook up” to be worthwhile for your environment. For the most part, I’ve tried to ensure that they are useful to you, in and of themselves. To do that, the scripts tend to be a little bit longer than a recipe-type script might be. Don’t let that scare you off—just dive right in and go for it!
I choose to develop in VBScript, with an occasional foray into JavaScript, because the Windows Scripting Host (WSH) that includes those two languages has been built into every Windows operating system since Windows 2000. It is also available for download and installation into Windows NT 4.0 and Windows 98 (I hope you don’t run into those in your environment—but just in case you do, the download is at http://www.microsoft.com/downloads/details.aspx?FamilyID=0a8a18f6-249c-4a72-bfcf-fc6af26dc390&DisplayLang=en). Jscript is the name of the Microsoft implementation of JavaScript. Jscript is a fairly complete language, and can be somewhat challenging to use, as a beginner. VBScript is syntactically very similar to Visual Basic, and is about as easy to learn as Visual Basic. VBScript has some limitations, but because it is so easy to learn, is used in many places.
The mere fact that the VBScript and JavaScript languages are included in every modern version of the Windows operating system makes them, in my mind, the tools of choice. Add to that, Internet Explorer will allow these languages to be embedded into web pages for client control, and Internet Information Server (IIS) will interpret these languages on the server for server control—well, they win hands down (granted, JavaScript and ECMA Script are standards and Apache has an ASP module—but those are add-ons and not part of the base products). There are many proponents of the cross-platform languages Perl and Python, which have a large number of modules enabling them to interface quite well with Windows. However, they require a separate install on every workstation and server that will use them and interfacing them with IIS requires a number of extra steps. On the other hand, they are fully developed languages with greater capabilities than those present in VBScript and JavaScript, with a correspondingly larger resource footprint.
There is a comprehensive reference guide available for both VBScript and JavaScript at http://microsoft.com/scripting, along with a huge library of code samples and the truly excellent ScriptOMatic, which can help you generate scripts of almost any type. An excellent book is VBScript in a Nutshell, Second Edition by Paul Lomax, et al (O’Reilly Media).
The Microsoft Scripting Host (MSH), code-named Monad, will be the replacement for WSH. It is currently in beta. Support for Monad will be built into Exchange 12 and it is planned to come as an add-on to Windows Vista and Windows code-named Longhorn. However, even when Monad is released, older scripts should continue. New features may require use of Monad, but all features should continue to work for quite some time.
Each of the scripts found in this chapter can be found at the author’s home page: http://www.msmithhome.com/.
The permissions model for these scripts is tied directly to required permissions for Active Directory and Exchange Server. In general, any script which only uses ADSI for interrogating Active Directory may be executed by any “Authenticated User” in Active Directory. Any script which uses ADSI to update objects will require Domain Admin permission in Active Directory (or appropriate delegated permission). Any script which uses CDOEXM (and there are only a couple of those) minimally requires Exchange View Only Administrator for reading Exchange information and minimally requires Exchange Administrator for updating Exchange information.
Understanding WSH
You will not learn everything you need to know on how to author scripts in this chapter, but you will learn something of my philosophy of writing scripts and some key points important to scripting.
I used the phrase “reusable components” in the preceding section. In WSH, this translates to a file that is included (that is, read by the command interpreter and inserted into the program at the position where the include instruction is located) into your script. Normally, a VBScript has a file extension of VBS and a JavaScript has a file extension of JS (please note that there are certainly other extensions recognized by WSH for those files—another set of common extensions is VBE and JSE). Any file having either of those file extensions is considered an executable by the Windows command processor. In order to use the include-file capability, your script must end with the file extension WSF—Windows Script File. An interesting side benefit of WSH is that it is very modular. It is possible to include multiple files into a WSF, and in fact, these files can be in different languages. It is completely possible to have multiple separate routines, in a single script, written in VBScript and JavaScript (plus any other language modules you may have installed). This is most useful when you have a bit of code written in one language that you don’t wish to rewrite, or when you need to use a feature of one language that isn’t present in the other (such as JavaScript’s sorting capabilities, which are not present in VBScript). Because WSH is a command interpreter, it executes commands as it sees them. On the positive side, this allows you to initialize variables when and where they are declared. On the negative side, this allows you to author very messy scripts that are difficult to support once written. Both JavaScript and VBScript really only have a single type of variable, called a variant. Each variant type will have a number of characteristics depending on the data stored in the variant. A single variable can be an object, a string, a floating point number, or an array; all in a single program. This sounds complicated, but it actually isn’t. The command processors automatically “coerce” (i.e. change) the variable as needed, whenever possible. When this can’t be done, an error will be generated. An object is composed of properties and methods (when I was in school in the Stone Age, this was said this way: “an entity is composed of attributes and actions”—the more things change, the more they are the same). A property is a piece of data. That piece of data may be a simple variable (such as a number, a string, etc.), it may be a collection (such as an array), or it may be another object. A property can be read-only, read-write, or write-only. A method is a way of acting on that object (or some piece of the object); such as creating a object, deleting an object, etc. The important thing about objects is that they are usually specifically created with CreateObject() and that you access a property or method by separating the name of the object from the property or method with a period (for example, someobj.Update).
Exchange Scripting Technologies
As Exchange Server has grown and matured as a product, the various tools available for modifying Exchange and its objects have grown and matured as well. These technologies include:
CDO—Collaboration Data Objects
In the beginning, there was CDO… CDO was the first of the exposed messaging interfaces and it was much simpler to use than MAPI (although it was based on MAPI). And the capabilities were pretty limited. It was primarily good for sending email via an Exchange server. CDO also includes various incarnations known as CDONTS and CDOSYS, which are included with Windows NT 4.0 Server and above, which support the sending of SMTP email without using Exchange Server. CDOEX—CDO for Exchange
CDO for Exchange is an enhanced version of CDO which is available only on an Exchange server. CDOEX includes support for other Exchange objects such as folders, appointments, calendars, tasks, contacts, mailboxes, etc. Using CDOEX, you can build complete messaging applications. CDOWF—CDO for Workflow
Initially, Exchange Server was positioned by Microsoft as a “Lotus Notes” killer, and the work flow capabilities of Exchange were strongly pushed. CDOWF provided the work flow support in Exchange, supporting event driven work flows (e.g. if “A” happens then do “B”). CDOWF never caught on, for various reasons. As of Exchange Server 2003, CDOWF has been turned into a legacy feature (no new development, but still supported). The capabilities present in CDOWF have been absorbed and significantly expanded upon by Windows SharePoint Services. CDOEXM—CDO for Exchange Management
CDOEXM provides the capabilities for managing the Exchange system—dealing with servers, storage groups, message stores, public folder trees, etc. Each of these objects can be manipulated in various ways, such as creating, deleting, mounting, moving, etc. CDOEXM is limited in the objects it handles (it does not allow you to modify or create Address Lists or Recipient Policies for example) and a program or script using CDOEXM can only be run on a server where Exchange Server is installed or a computer where the Exchange System Management Tools are installed. Some information (particularly some message store related information) can only be accessed via CDOEXM, and not via other technologies. ADSI—Active Directory Service Interfaces
Not a uniquely Exchange scripting technology, ADSI provides the interfaces into Active Directory. Most of the capabilities of CDOEX and CDOEXM can be replaced with ADSI scripts. Doing so has the advantage of allowing the scripts to run on computers which do not have either Exchange Server or the Exchange Server System Management Tools installed on them. Most of the scripts shown in this chapter will use ADSI. ADO—ActiveX Data Objects
These components provide capabilities for accessing data from multiple places through a common interface. Both Active Directory and Exchange Server support ADO. WMI—Windows Management Instrumentation
WMI is the Microsoft implementation of WBEM (Web-Based Enterprise Management). Got that? WMI is simply a way for information to be presented, interrogated, and modified in an industry standard way. Both Exchange Server and Active Directory expose a large amount of their data via WMI. With Exchange Server 2003, WMI provides more access to more information than any other interface. Monad—Microsoft Scripting Host (MSH)
Monad is currently beta technology. However, it has been announced that Monad will contain all the capabilities of the above technologies and more. E12 (which is the codename for the version of Exchange following Exchange Server 2003) will have its ESM written based on Monad commands and routines. Therefore, all the capabilities of ESM will be available to third-party developers and administrative script writers. With this information, you are now prepared to see some scripts.
Reusable Components
I have developed these scripts (as indeed, I develop most of my scripts) using include files that contain some routines that I use over and over again. Doing this has a number of advantages:
· It lets me be lazy, since I only have to develop a piece of code once.
· It eases upgrades. If I were to find a bug (heaven forbid!) I only have to fix it in one place
· It speeds development. If a piece of code is already written, I don’t have to rewrite it.
· It lets me forget. I don’t have to remember an arcane programming detail—I already figured it out once, I’ll just use it again. As you probably can see, the last three reasons are really just corollaries of the first reason. Are there downsides? Sure. If you are a programmer coming in to maintain someone else’s script or program that uses reusable components, you are going to have to investigate more thoroughly to ensure that you understand their “shorthand”. You also may need to copy more than a single file to another computer when moving scripts to another computer.
constants.vbs
The initial file I always have in a script development project is a file which contains all of my constants and pseudo-constants. These are values that either never change or probably won’t change, but may under special circumstances in a program. In VBScript, I call this constants.vbs. Here is the file used in the scripts in this book:
' Constants we need for ADSI calls
Const ADS_PROPERTY_CLEAR = 1
Const ADS_PROPERTY_APPEND = 3
Const ADS_PROPERTY_DELETE = 4
' Constants we need for WBEM calls
Const wbemFlagReturnImmediately = &H10
Const wbemFlagForwardOnly = &H20
' Constants we need for configuration
' the exchange server whose default mailbox store I'll use
Const ExchServer = "SERVER"
' Are we a web application or not?
Const bWebApplication = False
Dim bDebug
' verbose output (pseudo-constant)
bDebug = False
From this listing, you can determine that you’ll be seeing scripts containing modifications to Active Directory properties and that you’ll make some WMI calls. In order to run these scripts in your environment, you’ll need to specify the flat (NetBIOS) name of an Exchange server in your environment in the ExchServer constant. Only one script has the requirement of being run on an Exchange server.
With very little modification, you could take most of these scripts and put them into a web-based application (using HTA or ASP files). I do that on a regular basis. There are a couple of things that need to be modified for this, and several of the included scripts detect this and modify their code based on the setting of the bWebApplication constant. Finally, the bDebug variable is usually set to false. However, in some places I explicitly will choose to set it true, in order to allow you to see the additional output generated by some of the debugging code in a couple of the scripts. Since constants are, well, constant; you could not modify the value of bDebug if it were a constant.
ado.vbs
My next component, which is included in scripts that access Active Directory, is ado.vbs. This include file has the routines that make it easy to use ADO with Active Directory, without remembering lots of stuff you don’t normally need to know. Some would argue that remembering all of those things is worthwhile, and I guess if ADO programming was my focus, I would tend to agree. However, extracting information easily from Active Directory about Exchange is my focus—not ADO.The include file begins with the declaration of “global variables”. That is, variables which will be modified by the routines in this file and are available for visibility outside of the include file.Just a little background: ADO works by a script having a connection to a provider. In this case, the provider is Active Directory (there are many others). In order to do something with that connection, you will need a command. And that command produces a result, once it is executed. The result may contain zero or more rows of data (just like a row in an Excel spreadsheet). Therefore, our global variables are as follows:
Dim objCom ' the global ADO command object
Dim objConn ' the global ADO connection object
Dim Rs ' the global ADO resultset object
You will notice that my naming conventions tend to be somewhat descriptive. Usually, I begin the name of a variable with a one-to-four character description of the type of the variable, such as “obj”, which stands for “object”. This is followed by the proper name of the variable, which I always capitalize. However, for certain oft-typed variables I will, at times, forego the type description. The naming convention that I use is a common one. It helps you remember the type of a variable you are dealing with, as well as what the variable is being used for.I often use more lines than the language requires. This allows me to insert comments into my scripts. Comments help both me, and people that use my scripts, and those that modify them down the line, to figure out what it was that I was doing. Even though some things may have been “obvious” to me, that doesn’t necessarily mean that they are obvious to someone else. Without comments, the declarations above could have been written as:
Dim objCom, objConn, Rs
However, that would’ve left you with much less information about what the variables are intended to be used for.After the variables are declared, I begin the declaration of the subroutines and functions. The primary difference between a subroutine and a function is that a function will return some value. Just like other variables in VBScript and JavaScript, the functions return a variant type, which may be coerced into just about any type. JavaScript doesn’t support subroutines which do not return a value, all routines are functions. However, the script writer may choose to ignore the value of a function (which is true for VBScript as well).The first subroutine you encounter is:
Sub InitializeADSI
Set objCom = CreateObject ("ADODB.Command")
Set objConn = CreateObject ("ADODB.Connection")
' Open the connection.
objConn.Provider = "ADsDSOObject"
objConn.Open "ADs Provider"
End Sub
There are several things to note about this short subroutine. First is the keyword “Sub” which indicates that a subroutine is beginning. If this was a function, the keyword would be “Function”.I always “properly” capitalize keywords (capitalize the first letter and make the other letters of the word lower-case) in VBScript, but the script interpreter doesn’t care. There is no case-sensitivity in VBScript. However, JavaScript is case-sensitive. All keywords are in lower case, and a declaration of “var i;” is a different variable than a declaration of “var I;”.Next is the creation of two new objects, via the keyword CreateObject. This keyword attempts to call an ActiveX control (which has been previously registered on the computer) and create the object specified. Note also use of the “Set” keyword to assign a value to an object (normal assignments in VBScript have an implied “Let” keyword, which is derived from the very beginnings of the BASIC language).If either of these creations fails, an error message will be displayed on the user’s computer and the script will terminate. However, failures of these creations are unlikely and there truly is no recourse. If both succeed (almost certainly), then the provider for the connection is specified, and the connection is opened.Next up, the opposite of InitializeADSI is DoneWithADSI, as follows:
Sub DoneWithADSI
objConn.Close
Set objCom = Nothing
Set objConn = Nothing
End Sub
In this subroutine, the connection object previously opened is now closed. Also, the objects created are “cleaned up”. The keyword Nothing indicates to VBScript that if there is memory associated with an object or cleanup to be done, that it should be done now. If this isn’t executed explicitly by the script, it is automatically done by VBScript when a script terminates. However, it is simply a good programming practice to clean up after yourself. This is not necessary for simple variables, only for objects. Setting the value of the variable to Nothing also allows the variable to be re-used somewhere else in the script as a different variable type. Finally, because objConn is treated as an object in this subroutine (via the call on the method objConn.Close), this routine will fail if the InitializeADSI subroutine was never called.Next, you have the definition for the subroutine that actually makes the ADO calls for you:
Sub DoLDAPQuery (strLDAPQuery, resultSet)
dp "LDAP query: " & strLDAPQuery
objCom.ActiveConnection = objConn
objCom.Properties ("Page Size") = 1000
objCom.CommandText = strLDAPQuery
Set resultSet = objCom.Execute
End Sub
There are a number of new items to be explained in this subroutine. First, you see the first sample of parameters. There are two parameters, strLDAPQuery and resultSet. These are variables which can be used to pass information into a subroutine or function as well as be changed by the routine to send information back out of the subroutine or function. In this case, the first parameter strLDAPQuery is used to pass the command to be executed by the connection created in InitializeADSI and resultSet will contain the results of the command.However, the first line of the subroutine is a call to another subroutine (dp) that has not yet been declared. You’ll see more about that routine in the reports.vbs include file. In this routine, you see the setting of a named parameter called Page Size. Normally, ADO (actually, it is the LDAP engine in Windows Server) will return only the first 1,000 results. Setting the Page Size attribute causes all results to be returned in batches of the specified Page Size.Finally, you see the resultSet variable being assigned to objCom.Execute. This causes the Execute method of objCom to be called, which processes the command stored in the CommandText attribute of objCom via the connection stored in the ActiveConnection attribute. Is that clear as mud?Suffice it to say that when DoLDAPQuery returns, resultSet contains the results of the command in strLDAPQuery.The final subroutine in ado.vbs is:
Sub FinishLDAPQuery (resultSet)
resultSet.Close
Set resultSet = Nothing
End Sub
The purpose of this routine is to clean up the resultSet variable that was returned from DoLDAPQuery. A result set can consume a large amount of memory. Closing the result set returns that memory back to the operating system.
A Special Comment About resultSet
You may have noticed that while Rs was declared as a global variable, just like objCom and objConn, it was never referred to directly within the subroutines, but was instead always passed as a parameter when calling DoLDAPQuery and FinishLDAPQuery. The reason for this is simple.You may want to have more than one result set active at a given time. If the subroutines didn’t support that, then you would have to write more code—effectively a set of parallel subroutines, just ones that processed a different variable. That wouldn’t be very efficient, and would require more work for a lazy script writer. Passing the result set as a parameter allows you to avoid that additional work.
report.vbs
Few scripts operate without producing output intended to be viewed by a human (or be input to another script or program). A script writer tends to develop routines that make it simpler to get that data produced more easily. The routines that I tend to use are the ones present in report.vbs. The first routine is:
Function CleanItUp (str)
If bWebApplication Then
Dim str1
str1 = Replace (str, "<", "<")
str1 = Replace (str1, ">", ">")
CleanItUp = str1
Else
CleanItUp = str
End If
End Function
This is the first function that has been shown. As described earlier, the function will return a value. If you take a look at the function, you’ll note that this is done by assigning the function name a value, by placing it on the left hand side of an assignment statement. You should also note that the function ends with End Function, unlike the subroutines which have been ending with End Sub.The purpose of this function is to take output and ensure that it is in a format appropriate for the output medium. That’s a complicated way to say that, if the environment is a web page, to replace the less-than symbol and greater-than symbols found in the output with the character strings < and > that will render properly as those symbols, on a web page. The less-than and greater-than symbols are special characters in HTML and its various relative languages. The constant bWebApplication was declared in the constants.vbs file discussed earlier.The CleanItUp routine doesn’t stand alone. It is first used by:
Sub e (str)
If bWebApplication Then
response.write CleanItUp (str) & "<br>"
Else
wscript.echo str
End If
End Sub
This one character named subroutine is a primary output mechanism. In this case e is an abbreviation for echo, a command used by script writers in both DOS and UNIX shells for outputting data.The data to be written is contained in the parameter str. If the environment is a web application, then the string is cleaned up and a <br> is appended to the end of it (which causes a new line to appear on a web page). Then the string is output using the built-in response.write method. If the environment is not a web page, just a simple script, then the data is simply output using the built-in wscript.echo method.The next routine is very similar, but slightly different. In a web environment, there is no guarantee (unless you force it to happen) that a particular output will begin on a new line. This subroutine ensures that that happens:
Sub p (str)
If bWebApplication Then
response.write "<p>" & CleanItUp (str) & "<br>"
Else
wscript.echo vbCrLf & str
End If
End Sub
For a web application, the <p> which is prefixed onto the output string will ensure that the output begins a new paragraph (which always starts on a new line). For a non-web application, this ensures that an empty line will precede the output string.Out next subroutine was already used in the DoLDAPQuery subroutine. It is:
Sub dp (str)
If bDebug Then e CleanItUp (str)
End Sub
The native Visual Basic language on Windows includes a built-in routine named debug.print, intended for outputting data only when debugging is enabled. That is a very handy idea. When you are developing a script or trying to figure out a problem, you often generate lots of output that isn’t necessary otherwise. The dp subroutine is for exactly the same thing. If the Boolean variable bDebug (defined in the constants.vbs file discussed earlier) is set to True, then output is generated. If bDebug is set to False (the other value available for a Boolean), then output is not generated.The last two routines in reports.vbs are also quite short, designed to produce specialty output under specific circumstances:
Sub ErrorReport
e "***** Error: " & Err.Description & " (0x" & Hex (Err.Number) & ")"
End Sub
Sub WarnReport
e "***** Warning: " & Err.Description & " (0x" & Hex (Err.Number) & ")"
End Sub
VBScript allows the script writer to detect when an error has occurred under specific circumstances and to correct it. However, the script writer may want to tell the user about the error. These routines inform the user of the situation. ErrorReport indicates to the user that it is an error, while WarnReport indicates that it is only a warning.
systeminfo.vbs
A key ingredient for a script writer is to be able to find out some basic information about her computer, her Active Directory, and Exchange environment. The routines present in systeminfo.vbs support those needs for knowledge. First, the include file begins by declaring a set of global variables:
' the NetBIOS domain this script is running in
Dim strNetBIOSDomain
' the NetBIOS name of the computer this script is running on
Dim strNetBIOSComputer
' the configuration naming context of this domain
Dim strConfigNC
' the domain naming context of this domain
Dim strDomainNC
' the distinguished name of the Exchange organization
Dim strOrgDN
' the default SMTP domain for the Exchange organization
Dim strDefaultDomain
It is a fair bet for you to assume that each of these will be set by at least one routine present in this include file. The first routine is:
Sub InitializeAD
Dim objRootDSE
' Get the forest root directory services entry object
Set objRootDSE = GetObject ("LDAP://RootDSE/")
' Exchange information is stored in the Configuration tree of the forest
strConfigNC = objRootDSE.Get ("configurationNamingContext")
' this gives us the DN to the domain root
strDomainNC = objRootDSE.Get ("defaultNamingContext")
dp "Configuration Naming Context: " & strConfigNC
dp "Domain Naming Context: " & strDomainNC
Set objRootDSE = Nothing
End Sub
This routine gives you information about Active Directory that you will use over and over again in your scripts. Note that strDomainNC contains the DN to the domain root. If you need the DN to the forest root (which is at times very useful as well if you have multiple domains in your Active Directory forest), use rootDomainNamingContext instead of defaultNamingContext. Using the naming context will allow you to access objects (such as user objects, group objects, contact objects, etc.) in the particular domain that a naming context is for. Using the configuration naming context (which is identical in all domains in a forest) allows you to access forest-wide information. It is in the configuration naming context where all information about Exchange Server’s system level configuration is stored. For more information about what data can be retrieved from the RootDSE, see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/adschema/adschema/rootdse.asp.
The GetObject routine called here will be used many times in writing scripts. It allows the script writer to access objects via a number of built-in providers (in this case, the LDAP provider). The reference returned to the script can be used for inquiry and for update (which you’ll see later).The need to discover precisely where the Exchange information is stored in your Active Directory leads to the next routine:
Function GetOrganizationInformation
Dim strQuery