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 lazyonly 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 offjust 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 environmentbut 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 controlwell, they win hands down (granted, JavaScript and ECMA Script are standards and Apache has an ASP modulebut 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 filesanother 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 WSFWindows 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:

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

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

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

CDOEXMCDO for Exchange Management
CDOEXM provides the capabilities for managing the Exchange systemdealing 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.

ADOActiveX Data Objects
These components provide capabilities for accessing data from multiple places through a common interface. Both Active Directory and Exchange Server support ADO.

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

MonadMicrosoft 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 focusnot 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 codeeffectively 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, "<", "&lt;")
        str1 = Replace (str1, ">", "&gt;")
 
        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 &lt; and &gt; 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
 
    GetOrganizationInformation = False
 
    ' Build a query to find the Exchange organization.
    strQuery = "<LDAP://CN=Microsoft Exchange,CN=Services," & _
        strConfigNC & ">;" & _
        "(objectCategory=msExchOrganizationContainer);" & _
        "name,distinguishedName;" & _
        "onelevel"
 
    strOrgDN = ""
 
    Call DoLDAPQuery (strQuery, Rs)
 
    ' If there are any results, there will only be one result. There
    ' may only be one Exchange organization per Active Directory forest.
    e "Exchange Organization Name: " & Rs.Fields ("name")
    dp "Organization DN: " & Rs.Fields ("distinguishedName")
 
    strOrgDN = Rs.Fields ("distinguishedName")
 
    Call FinishLDAPQuery (rs)
 
    If Len (strOrgDN) = 0 Then
        e " *** Error: Cannot find Exchange organization information"
        GetOrganizationInformation = True
    End If
End Function

With this routine you see usage of many of the routines already declared. Most importantly, you see your first LDAP query being constructed and executed. An LDAP query consists of four parts:

Base distinguished name
The base DN indicates where in an LDAP directory tree (Active Directory in this case) a search will begin. A search will never cover objects higher in the tree than the base DN. You also specify the provider, which may be either LDAP or GC (for searching the Global Catalog). You may also (optionally) specify a specific server to bind to (use for the search) or you can use server-less binding (which all the examples in this book utilize).

LDAP filter
Most of the complexity associated with LDAP queries comes from designing efficient LDAP filters. A filter is a combination of attributes matching certain specifications combined together with logical AND and OR commands. You’ve seen LDAP filters earlier; they were introduced when you learned about Recipient Policies. Efficient queries use attributes which are indexed.

Attribute list
The attribute list is just thata list of the attributes you want returned from the LDAP query. Each object in the Active Directory that matches the criteria specified in the LDAP filter (constrained by the base DN and the search scope, which is discussed next) has the attributes listed here returned.

Search scope
The search scope may be any of base, onelevel or subtree. The specification of base means to search only the object specified by the base DN (and this means that there will never be more than one result). The specification of onelevel means to search all objects immediately below the base DN (the child objects of the base DN), but do not search the base object itself or search below the children. The specification of subtree means to search all objects beneath the base DN and include the base DN itself.

You see, in the query built in the GetOrganizationInformation function, that LDAP is the provider. The GC provider is also an option here and is, in fact, an option for most Exchange related information, since most Exchange data is kept in the global catalog. I specify the LDAP provider simply as a matter of habit. When using indexed attributes, my testing presents little, if any, speed difference. 

The query specifies a base DN below the configuration naming context and is the parent distinguished name for the Exchange organization object. That is, the Exchange organization object will be a child of this DN (you can determine this yourself using ADSI Edit or LDP as discussed in Chapter 11). This simply allows the search to be more efficient.

The filter specifies the objectCategory attribute of the object that is being searched for. This is both an indexed attribute and an attribute which will have a single object in an Active Directory forest (one which has an objectCategory value of msExchOrganizationContainer).

The attribute list of name and distinguishName provide you the name of your organization which you specified during the first Exchange server’s installation (which has been “First Organization” in the examples in this book) along with the path in Active Directory to access the organization’s object (which has been “CN=First Organization, CN=Microsoft Exchange, CN=Services, CN=Configuration, DC=WeDoExchange, DC=com” in the examples in this book). If the attribute list contains invalid attribute names, then the query will generate an error and not return any values.  

Finally, the search scope of the query is limited to onelevel. This is because the object being searched for is known to be a child of the base DN.

Based on the above discussion, you can certainly see how all the pieces of the LDAP query work together in order to produce the desired information. In this case, that is the distinguished name of the Exchange organization container. Without that information, queries about the Exchange organization or about a particular Exchange server and its services and properties will fail. Note that per-user (per-group, per-contact, etc.) information is not stored in the organization container, but in the user object itself, so it is accessed via normal user object access mechanisms (covered shortly).

Note that there are two ways to call subroutines in VBScript. One is by using the Call statement, followed by the subroutine name, followed by the parameter list (separated by commas) enclosed within parentheses. The other is simply using the subroutine name followed by the parameter list (separated by commas) without parenthesis. These are completely equivalent. Which you use is a matter of personal preference.

A function adds a third way, previously discussed, where its value is assigned to another variable. However, if you do not wish to store the value of a function, the function may be called as if it were a subroutine using the two methods described in the prior paragraph.

After the LDAP query is built, DoLDAPQuery is called to execute the query. When it is complete, the two relevant fields are extracted from the result set and stored into global variables. Next, the results are cleaned up. Finally, if the relevant results were not present, an error is displayed to the user and the function value is set to indicate an error, which is returned to the calling routine. You will see this value used in our next routine, which is used as the initialization routine for most of the programs presented later in this chapter:

Function GetSystemInfo
    Dim objWSHNetwork
 
    Call InitializeAD
 
    Set objWSHNetwork = CreateObject ("WScript.Network")
    ' get the NetBIOS domain name
    strNetBIOSDomain = objWSHNetwork.UserDomain
    dp "strNetBIOSDomain: " & strNetBIOSDomain
    ' get the NetBIOS computer name
    strNetBIOSComputer = objWSHNetwork.ComputerName
    dp "strNetBIOSComputer: " & strNetBIOSComputer
    Set objWSHNetwork = Nothing
 
    Call InitializeADSI
 
    If GetOrganizationInformation Then
        Call ClearSystemInfo
        GetSystemInfo = True
        Exit Function
    End If
 
    Call GetDefaultSMTPDomain
 
    GetSystemInfo = False
End Function
 
As you’ve seen, the domain naming context and configuration naming context are important pieces of information to have when inquiring into your Active Directory. Thus, this routine first calls the InitializeAD routine to obtain that information. Next, the routine creates another special object known as WScript.Network. Among the properties available from this object are the domain short name for the current user (also known historically as the NetBIOS domain name) and the short name of the computer this script is being executed on (also known historically as the NetBIOS computer name). Notice that if the global value of bDebug is set to True, the values of these will be printed for the user of the script.For more information about WScript.Network, please see http://msdn.microsoft.com/library/default.asp?url=/library/en-us/script56/html/wsobjwshnetwork.asp. Next, ADSI is initialized by creating the ADO objects LDAP queries will require, and then the routine attempts to get information about the Exchange organization. Note that the function is called as if it were a Boolean variable. Since it returns a Boolean value, this makes sense. If the function returns the value of True, then it has failed in its attempt to retrieve the Exchange organization information. GetSystemInfo cleans up, sets its value to True as well (to indicate failure) and returns to its caller. If the attempt to retrieve Exchange organization information is successful, then the routine continues on to acquire the default SMTP domain (which is used by Exchange in a number of ways), sets its value to False (to indicate that there were no errors), and returns to its caller. The clean up routine called by GetSystemInfo is very simple in the sample programs:

Sub ClearSystemInfo
    Call DoneWithADSI
End Sub

This routine should do whatever is necessary to clean up all global variables and structures and objects. Since the only things that have been set up in the examples are the ADSI objects, they need to be cleaned up. This is done by calling DoneWithADSI.

The final routine in the systeminfo.vbs include file is:

Sub GetDefaultSMTPDomain
    Dim strAddress   ' string
    Dim strAddresses ' collection of addresses
    Dim objPolicy    ' reference to Default Recipient Policy
 
    strDefaultDomain = ""
 
    Set objPolicy = GetObject ("LDAP://" & _
         "CN=Default Policy,CN=Recipient Policies," & strOrgDN)
    strAddresses  = objPolicy.Get ("gatewayProxy")
 
    ' strAddresses contains all the addresses specified on the
    ' E-Mail Addresses tab of the default recipient policy
 
    For Each strAddress in strAddresses
        If Left (strAddress, 5) = "SMTP:" Then
            Dim i
 
            ' strip "SMTP:" and up to the "@"
            i = InStr (strAddress, "@")
            strDefaultDomain = Right (strAddress, Len (strAddress) - i)
        End If
        dp "Email address suffix: " & strAddress
    Next
 
    ' Report our results
    e "Default SMTP address for organization: " & strDefaultDomain
 
    Set strAddresses = Nothing
    Set objPolicy = Nothing
End Sub

The default SMTP domain is used in a number of ways by Exchange, but we are concerned with just two right now. First, it is used by the Exchange Installable File System (ExIFS) to name the BackOfficeStorage directory. You will see this feature used in our program that dumps an email later in this chapter. Secondly, it is the domain used for the email address assigned to any Exchange enabled object to which a higher priority recipient policy does not apply.

To determine the default SMTP domain, you first must examine the default recipient policy. This policy is named Default Policy and it resides in the standard Recipient Policy node of Active Directory. As you should expect by now, this node is a child object of the Exchange organization. One of the attributes associated with a recipient policy object is named gatewayProxy. This attribute is a collection (also known as an array) of strings. Each string defines a gateway (which is a way to send or receive email from Exchange) plus a way to generate the email address for that gateway. If all of the letters in the gateway are capitalized, then that particular string defines the default address for an Exchange object through that gateway. If any are in lower-case, then that string does not define the default address. The two gateways enabled by default in an Exchange server are SMTP and X.400. They are represented in this attribute by any of “SMTP”, “smtp”, “X400”, and “x400”. You now understand what the difference in case represents. If you have a mixed-mode Exchange organization, such that an Active Directory Connector also exists, then you will also have “X500” and “x500” in the mix. Any other gateway types typically come from third-party software and/or connectors.

As discussed in Chapter 9, it is possible to customize the address generation rules of a recipient policy. This affects the format of a gatewayProxy string. By default, a string may look like “SMTP:@WeDoExchange.com” but it is possible to modify that to, for example, “SMTP:%1g%s@WeDoExchange.com” in order to signify a first initial followed by last name generation policy.

Now you are ready to examine the routine. First, acquire a reference to the Default Policy object and then retrieve its gatewayProxy attribute. Next, examine each item in that collection and determine whether it is the default SMTP address. If it is, strip off all of the characters on the left hand side of the string, up to and including the “@” symbol. The part that is left is the default SMTP domain. Store that value to a global variable. Report that value to the end-user, clean up, and return to the calling routine.Pretty fun, huh?

queries.vbs

This include file is the longest of all of our include files. It includes most of the “heavy lifting” done by the sample programs in this book. However, you will see that many of the routines bear a strong similarity to each other. First, the include file begins with the definition of the global variables plus a bit of commentary describing the variables:

 

' all lists are semi-colon separated
 
' a list of all the servers in the organization
' and a parallel list containing their distinguished names
Dim strServerList, strServerListDN
 
' a list of all OAL's in the organization
' and a parallel list containing their adminText attribute
Dim strOALList, strAdminList
 
' a list of all administrative groups in the org
' and a parallel list containing their distinguished names
Dim strAGroupList, strAGroupListDN
 
' a list of all servers in an admin group
Dim strServerAGList
 
' a list of all storage groups on a server
' and a parallel list containing their distinguished names
Dim strServerSG, strServerSGDN
 
' a list of all the stores in a storage group (by distinguished name)
' and a parallel list of their types (public or private)
Dim strStoreDN, strStoreType

As the very first line notes, all of the lists are built using a semi-colon to separate the items in the list. This is a convenient way to build arbitrarily sized lists in VBScript, using string variables with a special character separating the elements of the list (hopefully, a special character that will never be encountered in the list elements!). As distinguished names use commas extensively, that first choice is not available.

A parallel list is one that contains more information about the item at the same place as the first list. If arrays were being used, it would be an opportunity to use a multi-dimensional array.

The first routine in the include file is:

Sub GetAllServers
    Dim strQuery
 
    ' Get the list of all servers within the organization
    strQuery = "<LDAP://" & "CN=Administrative Groups," & strOrgDN & ">;" & _
        "(objectCategory=msExchExchangeServer);" & _
        "name,cn,distinguishedName;" & _
        "subtree"
 
    strServerList   = ""
    strServerListDN = ""
 
    Call DoLDAPQuery (strQuery, Rs)
 
    p "All Exchange Servers in forest " & strDomainNC
    While Not Rs.EOF
        ' output the current server found
        dp "Server CN: " & Rs.Fields ("cn")
        e  vbTab & "Server Name: " & Rs.Fields ("name")
        dp "Server DN: " & Rs.Fields ("distinguishedName")
 
        strServerList   = strServerList   & ";" & Rs.Fields ("name")
        strServerListDN = strServerListDN & ";" & Rs.Fields ("distinguishedName")
 
        Rs.MoveNext
    Wend
 
    strServerList = Mid (strServerList, 2)
    strServerListDN = Mid (strServerListDN, 2)
 
    Call FinishLDAPQuery (Rs)
 
    ' Report our results
    e " "
    dp "strServerList = " & strServerList
    dp "strServerListDN = " & strServerListDN
    dp " "
End Sub

GetAllServers is designed to do just thatbuild a list of all the Exchange servers in the organization. It does that by constructing an LDAP query whose filter searches for all objects whose objectCategory is msExchExchangeServer. The objects that satisfy that criteria will be Exchange servers (including those of any supported version: 5.5, 2000, and 2003). There are four different names associated with almost any Exchange objectthe common name (or CN), the simple name, the display name, and the distinguished name (or DN). Typically, the first three are identical (and the first node of the DN is the same as the CN).

However, people tend to deal with the simple names and computers deal with the distinguished names. Therefore it is of value to know both and GetAllServers builds two listsone of the simple name and one of the distinguished name.

The administrative model of Exchange allows for servers to be placed into separate administrative groups. These often (but not always) follow geographical separations in large companies. For example, a London office may have a separate IT staff than the San Diego office, but both have Exchange servers in the same Exchange organization. The administrative group model allows for each to administer their servers, but not administer the servers of the other office. Most small and medium sized companies have a single administrative group (and lots of Exchange administrators in these companies are not even aware of the administrative group model). By default, as you know, it is named “First Administrative Group”.

Server objects in Exchange are child objects of their administrative group objects. The administrative group objects are child objects of a parent Administrative Groups object (which is a child of the Exchange organization). Therefore, it makes sense to begin the search for Exchange servers at the parent Administrative Groups object, which is what the LDAP query in GetAllServers does. Note that a subtree search scope is required, since the LDAP query will have to examine objects more than one level below the specified base DN.

After building the query, a few variables are initialized and the LDAP query is executed. Next, a loop which examines all of the records within the result set begins. The name of the current server is displayed to the user running the script (and if bDebug is True, three of the names are displayed). Then, the lists are built. The string containing a particular list is appended with a semi-colon and the new element for the list. Note that this causes the first character to be a semi-colon, which must be discarded later. When done with this record, the next record is obtained, and the loop continues until all records have been examined.

Next, the semi-colon is removed as the first character of the lists, some clean up is done, information is reported if necessary, and the routine returns to its caller.

The next routine is similar in nature:

Sub GetAdministrativeGroupInformation
    Dim strQuery
 
    ' Get the list of all Administrative Groups within the organization
    strQuery = "<LDAP://CN=Administrative Groups," & strOrgDN & ">;" & _
        "(objectCategory=msExchAdminGroup);" & _
        "name,cn,distinguishedName;" & _
        "onelevel"
 
    strAGroupList = ""
    strAGroupListDN = ""
 
    Call DoLDAPQuery (strQuery, Rs)
 
    While Not Rs.EOF
        ' output the current administrative group found
        dp "Admin Group CN: " & Rs.Fields ("cn")
        dp "Administrative Group Name: " & Rs.Fields ("name")
        dp "Admin Group DN: " & Rs.Fields ("distinguishedName")
 
        strAGroupList = strAGroupList & ";" & Rs.Fields ("name")
        strAGroupListDN = strAGroupListDN  & ";" & Rs.Fields ("distinguishedName")
 
        Rs.MoveNext
    Wend
 
    strAGroupList = Mid (strAGroupList, 2)
    strAGroupListDN = Mid (strAGroupListDN, 2)
 
    Call FinishLDAPQuery (Rs)
 
    ' Report our results
    dp "strAGroupList = " & strAGroupList
End Sub

The purpose behind GetAdministrativeGroupInformation is to build two listsone containing the simple names of all the administrative groups in an organization and the other containing the distinguished names of the administrative groups. The logic is similar to GetAllServers; however the LDAP filter is looking for objects which have an objectCategory of msExchAdminGroup. The objects satisfying that criteria are the administrative group objects. Also, while the base DN is unchanged, the search scope has changed to onelevel, because all administrative group objects will be child objects of the Administrative Groups object.

The next routine puts a little twist on this process:

Sub GetServersForAdministrativeGroup
    ' Get the list of servers for each administrative group
    Dim strQuery, strServerAGList
    Dim arr
    Dim i
 
    arr = Split (strAGroupList, ";")
 
    strServerAGList = ""
 
    For i = LBound (arr) To UBound (arr)
        e "Exchange Servers for Administrative Group: " & arr (i)
        strQuery = "<LDAP://" & _
            "CN=Servers,CN=" & arr (i) & ",CN=Administrative Groups," & _
            strOrgDN & ">;" & _
            "(objectCategory=msExchExchangeServer);" & _
            "name,cn,distinguishedName;" & _
            "onelevel"
 
        Call DoLDAPQuery (strQuery, Rs)
 
        While Not Rs.EOF
            ' output the current server found
            dp "Server CN: " & Rs.Fields ("cn")
            e "Server Name: " & Rs.Fields ("name")
            dp "Server DN: " & Rs.Fields ("distinguishedName")
 
            strServerAGList = strServerAGList & ";" & Rs.Fields ("name")
 
            Rs.MoveNext
        Wend
 
        strServerAGList = Mid (strServerAGList, 2)
 
        Call FinishLDAPQuery (Rs)
 
        dp "Exchange server list for AG " & arr (i) & ": " & strServerAGList
    Next
End Sub

GetServersForAdministrativeGroup will use one of the lists that were built in GetAdministrativeGroupInformation and put the list into an array. Then it iterates through every element of the array (which means that it goes through every administrative group) and searches for the Exchange servers that are a member of that particular administrative group. When done, the list produced by this routine will contains the same elements as the list produced by GetAllServershowever, they may be in a different order given the different search mechanism.

The next routine also has a slightly different twist:

Sub GetAllOfflineAddressLists
    Dim strQuery
    Dim str
 
    ' Get the list of all offline address lists within the organization
    strQuery = "<LDAP://" & "CN=Offline Address Lists," & _
        "CN=Address Lists Container," & strOrgDN & ">;" & _
        "(objectCategory=msExchOAB);" & _
        "name,adminDescription;" & _
        "onelevel"
 
    strOALList = ""
    strAdminList = ""
 
    Call DoLDAPQuery (strQuery, Rs)
 
    dp "All OALs in DN CN=Offline Address Lists,CN=Address Lists Container," & _
        strOrgDN
    While Not Rs.EOF
        str = Rs.Fields ("adminDescription")
        If IsNull (str) or (Len (str) = 0) Then
            str = "<null>"
        End If
 
        strOALList   = strOALList   & ";" & Rs.Fields ("name")
        strAdminList = strAdminList & ";" & str
 
        Rs.MoveNext
    Wend
    dp "strOALList = " & strOALList
    dp "strAdminList = " & strAdminList
End Sub

GetAllOfflineAddressLists starts off familiarly. However, the parallel list that it builds is for the adminDescription attribute of msExchOAB objects (the “Administrative note” field on the Details tab of an offline address list’s property sheet). This is done to illustrate that the technique is generally applicable to more than just name fields. Also, some fields may not be present, or they may have no data contained within them. This routine deals with both circumstances, using the IsNull() function and the Len() function to determine that, respectively.

Offline Address Lists are interesting objects, not only because they are important to the operation of cached-mode Outlook 2003, but because they are challenging to administer. They contain a great deal of data, but most of it is hidden from the Exchange administrator. They also potentially require modification when a server is going offline (more on that in a later script in this chapter).

The next routine doesn’t use ADO:

Sub GetStoresForStorageGroup (strStorageGroup)
    Dim objSG, objStore
    Dim strOC, strType
 
    strStoreDN   = ""
    strStoreType = ""
 
    Set objSG = GetObject ("LDAP://" & strStorageGroup)
 
    For Each objStore in objSG
        strOC = objStore.Get ("objectCategory")
        ' turn objectCategory into English
        strType = Left (strOC, Instr (strOC, ",") - 1)
        strType = Replace (Mid (strType, 4), "-", "")
 
        dp vbTab & "store: " & objStore.Name & _
            " " & strType
 
        strStoreDN   = strStoreDN   & ";" & objStore.Get ("distinguishedName")
        strStoreType = strStoreType & ";" & strType
    Next
 
    strStoreDN   = Mid (strStoreDN, 2)
    strStoreType = Mid (strStoreType, 2)
 
    ' Report results
    dp "store DN's: " & strStoreDN
    dp "store Types: " & strStoreType
End Sub
 This routine takes the distinguished name of a storage group on a server, and builds a list of all the stores contained within that storage group. It treats the storage group as a collection and iterates through all the members of the collection to obtain the store names. The lines dealing with strOC and strType will take the long form name of the objectCategory (which looks like this: CN=ms-Exch-Private-MDB, CN=Schema, CN=Configuration, DC=WeDoExchange, DC=com) and turn it into a short form name (which looks like this: msExchPrivateMDB), which is what you’ve seen being used in LDAP filters so far. Just for your information, the technique shown here is generally applicable and you often don’t have a choice as to whether you are provided an objectCategory or an objectClass. However, in this particular case, I wanted you to see the mechanism. You could’ve directly used the objectClass instead.This routine can certainly be done using ADO using the techniques you’ve already learned. Simply make an LDAP query using: A base DN of the storage group"<LDAP://" & strStorageGroup & ">;" A filter containing the objectCategory’s for private and public stores"(|(objectCategory=msExchPrivateMDB)(objectCategory=msExchPublicMDB));" An attribute list returning the distinguished name and the objectCategory“distinguishedName,objectCategory;” A search scope for just the next level down“onelevel”

Then iterate through the results and build the result strings. A routine named GetStoresForStorageGroupLDAP showing this technique is available in the version of the include file available for download from the book’s website (http://www.msmithhome.com/).

The final routine in this include file is:

Sub GetStorageGroupsForServer (strServerDN)
    Dim strQuery
 
    strQuery = "<LDAP://" & "CN=InformationStore," & _
        strServerDN & ">;" & _
        "(objectCategory=msExchStorageGroup);" & _
        "name,cn,distinguishedName;" & _
        "onelevel"
 
    strServerSG   = ""
    strServerSGDN = ""
 
    Call DoLDAPQuery (strQuery, Rs)
 
    While Not Rs.EOF
        strServerSG   = strServerSG   & ";" & Rs.Fields ("name")
        strServerSGDN = strServerSGDN & ";" & Rs.Fields ("distinguishedName")
 
        Call GetStoresForStorageGroup (Rs.Fields ("distinguishedName"))
 
        Rs.MoveNext
    Wend
 
    strServerSG = Mid (strServerSG, 2)
    strServerSGDN = Mid (strServerSGDN, 2)
 
    Call FinishLDAPQuery (Rs)
End Sub

In the standard way, this routine takes the distinguished name of a server and finds all of the storage groups associated with that server and builds the list of the storage group names and their distinguished names. What’s truly worthy of note is that storage groups are not child objects of the server, they are grandchild objects. The child object is named
InformationStore and is of type msExchInformationStore. It contains only the storage group objects, but its attributes contain interesting parameters for how Exchange server operates with ESE (the Extensible Storage Engine). It is fun to look at with ADSI Edit or LDP.

findobject.vbs

This include file has only a single routine, but since multiple include files can generate the semi-colon separated lists, this routine needs to be visible separately from each of those files.

Function FindObject (ByVal strArray, ByVal str)
    Dim arr, strstr
    Dim i
 
    strstr = LCase (str)
    arr = Split (LCase (strArray), ";")
 
    For i = LBound (arr) to UBound (arr)
        If strstr = arr (i) Then
            FindObject = True
            Exit Function
        End If
    Next
 
    FindObject = False
End Function
FindObject is designed to do just thatsearch each element of a semi-colon separated string for a specific match. If the match is found, then return True to the calling routine. If it isn’t found, return False to the calling routine.One thing to note is that the search is designed to not be case sensitive. Both strings are converted into lower-case before they are searched.For example, if you have a list such as strList = “A;B;C;D;E;F” then FindObject (strList, “A”) would return True, whereas FindObject (strList, “G”) would return False.

sitefolders.vbs

The last include file in the examples is designed to deal with site folder servers. A site folder server is generally the first Exchange server installed into a particular administrative group. It is the Exchange server is that is “responsible” for generating the various site folders for that administrative group. All other servers will replicate those site folders from the responsible Exchange serverwhich is known as a site folder server.

A site folder is a system folder. If you open the Folders item in the left pane of ESM, then right-click on Public Folders and select View System Folders, then you will be looking at the site folders. In Exchange 200x, most of these items are maintained separate and apart from the site folder server, however there is still some minor dependency associated with them.

Thus, especially when you retire an old Exchange server, it’s important to know where your site folder servers are homed, and know how to change them. The first routine shows you how to find the site folder server for a particular administrative group:

Function GetSiteFolderServerForAG (strAG, obj)
    Set obj = GetObject ("LDAP://CN=" & strAG & _
        ",CN=Administrative Groups," & strOrgDN)
 
    GetSiteFolderServerForAG = obj.Get ("siteFolderServer")
End Function

GetSiteFolderServerForAG takes the short form name of the administrative group and retrieves a reference to its Active Directory object. The value of the siteFolderServer attribute (which actually contains the distinguished name of a particular public folder store) is returned as the value of the function, and the reference to the administrative group object is also returned back to the caller.

As you might expect (you’ve probably learned how my mind works by now), the next routine is:

Sub GetSiteFolderServers
    Dim obj
    Dim str
    Dim arrAG, strAG
 
    If IsNull (strAGroupList) OR Len (strAGrouplist) = 0 Then
        Call GetAdministrativeGroupInformation
    End If
 
    strSiteFolders = ""
    arrAG = Split (strAGroupList, ";")
 
    For Each strAG in arrAG
        e "Examining AG: " & strAG
        str = GetSiteFolderServerForAG (strAG, obj)
 
        strSiteFolders = strSiteFolders & ";" & str
 
        Set obj = Nothing
    Next
 
    strSiteFolders = Mid (strSiteFolders, 2)
 
    dp "strSiteFolders = " & strSiteFolders
End Sub

GetSiteFolderServers loops through all administrative groups retrieving their site folder servers, and builds a semi-colon separated list of the site folder servers. This is a parallel list to strAGroupList, which is built by calling GetAdministrativeGroupInformation. If that list has not yet been built, then GetSiteFolderServers will ensure that it gets built first.

If you are getting ready to change a site folder server, then you will need to know the public folder stores that are available to you. The next routine produces that list:

Function GetAllPublicFolderStores
    Dim strQuery, strResult
 
    strQuery = "<LDAP://CN=Administrative Groups," & _
            strOrgDN & ">;" & _
        "(objectCategory=msExchPublicMDB);" & _
        "name,cn,distinguishedName;" & _
        "subtree"
 
    strResult = ""
 
    Call DoLDAPQuery (strQuery, Rs)
    While Not Rs.EOF
        strResult = strResult & ";" & Rs.Fields ("distinguishedName")
        Rs.MoveNext
    Wend
 
    strResult = Mid (strResult, 2)
 
    Call FinishLDAPQuery (Rs)
 
    GetAllPublicFolderStores = strResult
End Function

This routine uses familiar routines. The LDAP base DN is at the Administrative Groups object, below which are the individual administrative groups and their servers. The LDAP filter is for an
objectCategory of msExchPublicMDB which will only return public folder stores. And the search scope is subtree to indicate that the search will need to be a deep traversal. A little differently than the routines in queries.vbs, is this routine returns the string as its function value and does not store the list into a global variable.
Finally in this include file is the way to set a site folder server. This is the first update sample you’ve seen. As always, take care before making modifications to ensure that you’ve got it right. A little protection is built into the routine:

Function setSiteFolderServer (strAG, strPF)
    Dim obj        ' administrative group object
    Dim str        ' current PF store for specified AG
    Dim strPFList    ' semi-colon separated list of public folders

    ' assume that everything will be OK
    setSiteFolderServer = False

    If Not FindObject (strAGroupList, strAG) Then
        e "*** Invalid Administrative Group passed to setSiteFolderServer"
        setSiteFolderServer = True
        Exit Function
    End If

    strPFList = GetAllPublicFolderStores
    If Not FindObject (strPFList, strPF) Then
        e "*** Invalid public folder passed to setSiteFolderServer"
        setSiteFolderServer = True
        Exit Function
    End If

    str = LCase (GetSiteFolderServerForAG (strAG, obj))
    If str = LCase (strPF) Then
        dp "*** WARNING: current site folder server and new one are the same"
        ' this is not an error - it just means we didn't have to do anything
        Set obj = Nothing
        Exit Function
    End If

    obj.Put "siteFolderServer", strPF
    obj.SetInfo

    Set obj = Nothing

    dp "*** WARNING: site folder server for AG " & strAG & " changed to " & strPF
End Function

SetSiteFolderServer is called with two parametersthe short name of the administrative group that you want to set the site folder server for, and the distinguished name of the public folder store that you want to set the site folder server to.

The routine first verifies that the administrative group is a valid administrative group. If it isn’t, an error is printed and the routine returns to its caller. Next, the routine verifies that the public folder store is a valid public folder store. If it isn’t, an error is printed and the routine returns to its caller.Next, the current site folder server is compared to the new site folder server. If they are the same, then a warning is displayed and the routine returns to its caller. Finally, the attribute is updated, and the output is written to Active Directory. Clean up is then done, and the routine returns to its caller, indicating success.

The Sample Scripts

The include files you’ve seen so far are used by many of the sample scripts that come next. Some of these are full fledged applications that try to do “everything” you might want. Others are shells, that demonstrate a particular technique, but you would have to modify them in order to use them in your environment. Both are valuable, as they illustrate ways to access data in your Exchange environment. Each of these scripts have been referred to earlier in this book for illustrating one concept or another.The scripts are presented in no particular order.

Ch12-exch.wsf

This first script calls most of the routines you’ve seen, with bDebug set to true. This generates lots of output that is very interesting in learning about your Exchange configuration. No modifications are made.

<job>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript" src="lib/sitefolders.vbs"    />
<script language="VBScript" src="lib/findobject.vbs"     />
<script language="VBScript">

    bDebug = True

    Call GetSystemInfo

    Call GetAdministrativeGroupInformation
    Call GetServersForAdministrativeGroup
    Call GetAllServers
    Call GetStorageGroupsForAllServers
    Call GetAllOfflineAddressLists

    Call GetSiteFolderServers

    Call ClearSystemInfo
</script>
</job>

This script is a Windows Script Host job. This often can mean various advanced things, but here it simply means that multiple files can be included. The next seven lines insert the include files into this script, by specifying the disk file name (using the src directive) of the include file. The next line specifies that script is going to be written directly into the file at this point.Next, bDebug is set to True so that all of the debugging output is generated by the various routines, and the routines are called. Even in small organizations, this will generate hundreds of lines of output. Spend some time with it, familiarizing yourself with the format of LDAP queries and the output generated by them.

Ch12-smtp-vs-short.vbs

This script sets the maximum outgoing message size for a particular SMTP virtual server. While the script could look up all of the constants, using the techniques already provided, in this case it is left as an exercise for you.

Hint: use the strServerListDN string which is built by GetAllServers.

Const VSID    = "1"
Const SERVER  = "server"
Const AGROUP  = "First Administrative Group"
Const ORGNAME = "First Organization"
Const DOMAIN  = "DC=wedoexchange,DC=com"
Const MSIZE   = 0

Dim obj, str

str = "CN=" & VSID & _
    ",CN=SMTP,CN=Protocols,CN=" & SERVER & _
    ",CN=Servers,CN=" & AGROUP & _
    ",CN=Administrative Groups,CN=" & ORGNAME & _
    ",CN=Microsoft Exchange,CN=Services,CN=Configuration," & _
    DOMAIN

WScript.Echo "Server DN: " & str
Set obj = GetObject ("LDAP://" & str)

obj.msExchSmtpMaxMessageSize = MSIZE
obj.SetInfo

WScript.Echo "SMTP virtual server updated to " & MSIZE

Set obj = Nothing

Ch12-oab-servers.wsf

If OAB generation is taking too long, or you are getting ready to retire a server, one of the things you need to check is where your OABs are being generated. When an OAB is created, you specify the server responsible for generating it. It certainly is possible for this server to become overburdened.

This program lists each OAB and the server which generates it.

<job>
<script language="VBScript">
Option Explicit
<script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript">
'
'    This program reports on all of the offline address
'    books and the server that they are processed upon.
'
    Dim i, obj, arrOAL

    Call GetSystemInfo
    e " "

    Call GetAllOfflineAddressLists

    arrOAL = Split (strOALList, ";")

    For i = LBound (arrOAL) To UBound (arrOAL)
        Dim str

        Set obj = GetObject ("LDAP://CN=" & arrOAL (i) & _
            ",CN=Offline Address Lists,CN=Address Lists Container," & _
            strOrgDN)
        str = obj.Get ("offLineABServer")
        e "OAL " & arrOAL (i) & " has an expansion server of " & _
            Mid (Left (str, InStr (str, ",") - 1), 4)
        Set obj = Nothing
    Next

    Call ClearSystemInfo
</script>
</job>

Ch12-server-circ.vbs

Sometimes you want to be able to set and/or clear the circular logging flag for a particular storage group. This script shows you how. This would probably be combined with another script (as shown in Chapter 8), for executing a full backup immediately after changing the circular logging setting.

You’ve learned earlier how to iterate through all the storage groups for a server, so that isn’t shown here. It’s left as an exercise for you. Obviously, you do need to set the constants appropriate for your environment, if the defaults do not work for you.

Const SERVER  = "SERVER"
Const SGROUP  = "First Storage Group"
Const AGROUP  = "First Administrative Group"
Const ORGNAME = "First Organization"
Const DOMAIN  = "DC=wedoexchange,DC=com"

Sub Set_CircularLog (obj)
    obj.msExchESEParamCircularLog = 1

    obj.SetInfo
End Sub

Sub Clear_CircularLog (obj)
    obj.msExchESEParamCircularLog = 0

    obj.SetInfo
End Sub


Dim str, obj
'
' build string that points to the required storage group
' on the specified server
'
str = "CN=" & SGROUP & ",CN=InformationStore,CN=" & _
    SERVER & ",CN=Servers,CN=" & _
    AGROUP & ",CN=Administrative Groups,CN=" & _
    ORGNAME & ",CN=Microsoft Exchange,CN=Services,CN=Configuration," & _
    DOMAIN

Set obj = GetObject ("LDAP://" & str)

'Call Set_CircularLog (obj)
Call Clear_CircularLog (obj)

Set obj = Nothing

WScript.Echo "Done!"

You may have to restart the Microsoft Exchange Information Store service (thus causing an outage for that server) when doing this. Microsoft KB 314605 (How to turn on or turn off circular logging in Exchange 2000 Server and Exchange Server 2003) says you do. Many online resources say you do not.

Ch12-MaxIn-MaxOut.wsf 

A good thing to do is set maximum incoming and outgoing message limits at the organization level. You can do this in ESM, this script shows you how to do it yourself, and along the way report on it.

In the prettyPrint subroutine, a special thing to note is that the routine deals with the special case where the old value of the attribute was not set. If the IsEmpty() function returns true, then the attribute was not set on the object.

<job>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript">
'
'    This program sets the maximum received message size and the
'    maximum sent message size of the Exchange organization. The
'    size values are specified in KB.
'
'    To set these to "no limit" you must ADS_PROPERTY_CLEAR
'    the attribute:
'        Const ADS_PROPERTY_CLEAR = 1
'
'        obj.PutEx ADS_PROPERTY_CLEAR, "delivContLength", 0
'        obj.PutEx ADS_PROPERTY_CLEAR, "submissionContLength", 0
'
'    This example is 10 MB for outgoing and 5 MB for incoming.
'
Const STR_IN = " maximum receiving message size "
Const STR_OUT= " maximum outgoing message size "

Dim MaxOutGoingMessageSize : MaxOutGoingMessageSize = 10 * 1024
Dim MaxIncomingMessageSize : MaxIncomingMessageSize =  5 * 1024

Dim strObj, obj, i

Call GetSystemInfo

strObj = "CN=Message Delivery,CN=Global Settings," & strOrgDN

Set obj = GetObject ("LDAP://" & strObj)

' In ESM: Message Delivery -> Defaults -> Receiving message size
prettyPrint obj.delivContLength, MaxIncomingMessageSize, STR_IN
obj.delivContLength = MaxIncomingMessageSize

' In ESM: Message Delivery -> Defaults -> Sending message size
prettyPrint obj.submissionContLength, MaxOutgoingMessageSize, STR_OUT
obj.submissionContLength = MaxOutgoingMessageSize

obj.SetInfo

Set obj = Nothing

Call ClearSystemInfo    
e "Done!"

Sub prettyPrint (oldval, newval, str)
    If IsEmpty (oldval) Then oldval = ""

    e "Old" & str & "was " & oldval & " KB"
    e "New" & str & "is  " & newval & " KB"
End Sub
</script>
</job>

Ch12-SMTP-vs.wsf

Ch12-SMTP-vs-short.vbs sets the maximum incoming message size for a single SMTP virtual server. This script does it for all of the SMTP virtual servers in the Exchange organization.

<job>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript">
'
'    This program sets the msExchSmtpMaxMessageSize for all SMTP
'    virtual servers in an Exchange organization. If the size value
'    is zero, then there is no limit. Otherwise, the attributes is
'    specified in bytes (ESM converts it to KB for display).
'
'    This example is 10 MB
'
    Dim MaxOutGoingMessageSize
    MaxOutGoingMessageSize = 10 * 1024 * 1024

    Dim strQuery, obj

    Call GetSystemInfo

    strQuery = "<LDAP://" & "CN=Administrative Groups," & strOrgDN & ">;" & _
        "(objectCategory=protocolCfgSMTPServer);" & _
        "name,cn,distinguishedName;" & _
        "subtree"

    Call DoLDAPQuery (strQuery, Rs)

    p "All SMTP Virtual Servers in forest " & strDomainNC
    While Not Rs.EOF
        ' output the name of the current virtual server found
        e "Server Name: " & Rs.Fields ("name")
        e "Server DN: " & Rs.Fields ("distinguishedName")
        e " "

        Set obj = GetObject ("LDAP://" & Rs.Fields ("distinguishedName"))

        obj.msExchSmtpMaxMessageSize = MaxOutGoingMessageSize
        obj.SetInfo
        e "SMTP virtual server updated to " & MaxOutGoingMessageSize

        Set obj = Nothing
        Rs.MoveNext
    Wend

    Call FinishLDAPQuery (Rs)
    Call ClearSystemInfo    
    e "Done!"
</script>
</job>

Ch12-mixed-mode.wsf

Both the Exchange organization and each administrative group have mixed mode vs. native mode flags. This script reports on the organization flag and the flag for every administrative group.

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript">
'
'    This program reports on the native mode status
'    of the organization, plus the native mode status
'    of each individual administrative group.
'
    Dim i, obj, arrAG, arrAGshort, arrAGtype

    arrAGtype = Array ("native Exchange 200x AG", _
               "native Exchange 5.5 AG", _
               "mixed-mode AG")

    Call GetSystemInfo
    e " "

    Set obj = GetObject ("LDAP://" & strOrgDN)
    If obj.Get ("msExchMixedMode") Then
        WScript.Echo "Exchange organization is in mixed mode"
    Else
        WScript.Echo "Exchange organization is in native mode"
    End If
    Set obj = Nothing

    Call GetAdministrativeGroupInformation

    arrAG = Split (strAGroupListDN, ";")
    arrAGshort = Split (strAGroupList, ";")

    For i = LBound (arrAG) To UBound (arrAG)
        Set obj = GetObject ("LDAP://" & arrAG (i))
        e "AG " & arrAGshort (i) & " has a mode of " & _
           arrAGtype (obj.Get ("msExchAdminGroupMode"))
        Set obj = Nothing
    Next

    Call ClearSystemInfo
</script>
</job>

Ch12-backoffice-storage.vbs

The Exchange Installable File System (ExIFS) can be treated just like NTFSexcept that you should never modify ExIFS ACLs via Windows Explorer. This script is purely a demo script that examines the Exchange store as if it were a file system, dumps out all of the directory and file names. For the very first email that it encounters, it opens the email as a file, and outputs the contents to the user’s window. After it encounters a total of 10 email files, it just quits.

This script also displays a concept known as recursion. This is when a subroutine calls itself. It’s a mechanism for processing arbitrarily deep structures without knowing how deep they are at the beginning.

Option Explicit
Dim objFSO, objFolder, objF
Dim bFirstEmail, iEmailCount

bFirstEmail = True
iEmailCount = 0
Set objFSO = CreateObject ("Scripting.FileSystemObject")

Set objFolder = objFSO.GetFolder ("\\.\BackOfficeStorage")

For Each objF in objFolder.SubFolders
    Call DoDirectory (objF, "")
Next

Set objFolder = Nothing
Set objFSO = Nothing

Sub DoDirectory (objFold, strIndent)
    Dim objUserDir, objFile

    WScript.Echo strIndent & "Checking Folder: " & objFold.Path

    For Each objUserDir in objFold.SubFolders
        Call DoDirectory (objUserDir, strIndent & vbTab)
    Next

    For Each objFile in objFold.Files
        Wscript.echo strIndent & "Checking file: " & objFile.Path
        If bFirstEmail Then
            Dim objEmail, strLine

            Set objEmail = objFSO.OpenTextFile (objFile.Path, 1)

            Do Until objEmail.AtEndOfStream
                strLine = objEmail.ReadLine
                WScript.Echo strline
            Loop

            objEmail.Close
            Set objEmail = Nothing

            bFirstEmail = False
        End If

        iEmailCount = iEmailCount + 1
        If iEmailCount > 10 Then
            WScript.Echo "Done!"
            WScript.Quit 0
        End If
    Next
End Sub

Ch12-lastbackup.wsf 

This script prints out the time and date of the last full and incremental backup of every store in your Exchange organization. A couple of things to be aware of: this is one of the few scripts in this book that uses CDOEXM. That means it must be run on either an Exchange server, or a computer where the Exchange System Management Tools are installed. Secondly, it actually uses an undocumented interface. However, this interface is available on both Exchange 2000 Server and Exchange Server 2003. Using it will not cause any problems.

Another final note, Recovery Storage Groups do not support being opened by CDOEXM. This script recovers from that problem by using the “On Error Resume Next” statement immediately before the “objMBDB.DataSource.Open str” statement.Your backups should be happening on a regular basis. This script can help you ensure that they are.

<job>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript">

    Dim objMBDB, objPFDB
    Dim arr, arrSG, arrStore, arrType
    Dim i, j, k, str

    Set objMBDB = CreateObject ("CDOEXM.MailboxStoreDB.6")
    Set objPFDB = CreateObject ("CDOEXM.PublicStoreDB.6")

    Call GetSystemInfo
    Call GetAllServers
    arr = Split (strServerListDN, ";")

    For i = LBound (arr) To UBound (arr)
        Call GetStorageGroupsForServer (arr (i))

        arrSG = Split (strServerSGDN, ";")
        For j = LBound (arrSG) To UBound (arrSG)
            Call GetStoresForStorageGroupLDAP (arrSG (j))

            arrStore = Split (strStoreDN, ";")
            arrType  = Split (strStoreType, ";")
            For k = LBound (arrStore) To UBound (arrStore)
                str = arrStore (k)
                If arrType (k) = "msExchPublicMDB" Then
                    objPFDB.DataSource.Open str
                    Call DoReport (objPFDB, str)
                Else
                    On Error Resume Next
                    objMBDB.DataSource.Open str
                    Call DoReport (objMBDB, str)
                    On Error Goto 0
                End If
            Next
        Next
    Next

Function PrettyPrintStoreName (strStoreDN)
    Dim arrStore

    arrStore = Split (strStoreDN, ",")

    PrettyPrintStoreName = Mid (arrStore (0), 4) & " in " & _
        Mid (arrStore (1), 4) & " on " & _
        Mid (arrStore (3), 4)
End Function

Sub DoReport (obj, str)
    Dim strF, strI

    strF = obj.LastFullBackupTime
    If Len (strF) = 0 Then strF = ""

    strI = obj.LastIncrementalBackupTime
    If Len (strI) = 0 Then strI = ""

    e "Store = " & PrettyPrintStorename (str)
    e "Last Full Backup Time = " & strF
    e "Last Incremental Backup Time = " & strI
    e " "
End Sub
</script>
</job>

Ch12-drivespace.vbs

Very often you want to know the amount of disk space available on a remote server. This next script provides you that information. Many administrators take this kind of script and have it run every day (or multiple times per day on very volatile volumes) and the output emailed to them.

Option Explicit
Dim objFSO, objDrv, iLimit

On Error Resume Next

' check arguments on command line
If Wscript.Arguments.Count < 1 or Wscript.Arguments.Count > 2 Then
    wscript.echo "drivespace.vbs UNC-path [percent-available]"
    wscript.echo "EX: drivespace.vbs \\server\c$ 10"
    Wscript.Quit(1) 
End If

' get option percentage
If Wscript.Arguments.Count > 1 Then
    iLimit = CInt (Wscript.Arguments (1))
Else
    iLimit = 101
End If

Set objFSO = CreateObject ("Scripting.FileSystemObject")
ErrorReport "CreateObject"

' Get drive object and display properties
IterateGetDrive WScript.Arguments (0)

With objDrv
    If ((.FreeSpace / .TotalSize) * 100) < iLimit Then
        Wscript.Echo "     Share           : " & _
            Wscript.Arguments (0)
        Wscript.Echo "     % Free          : " & _
            FormatPercent ((.FreeSpace / .TotalSize), 1)
        Wscript.Echo "     Bytes Available : " & _
            FormatNumber (.AvailableSpace, 0)
        Wscript.Echo "     Bytes Used      : " & _
            FormatNumber ((.TotalSize - .FreeSpace), 0)
        Wscript.Echo "     Total Bytes     : " & _
            FormatNumber (.TotalSize, 0)
        Wscript.Echo " "
    End If
End With

Set objFSO = Nothing

Sub ErrorReport (strErr)
    If Err.Number <> 0 Then
        Wscript.Echo "Error 0x" & CStr (Hex (Err.Number)) & _
            " occurred in " & strErr & " for: " & _
            Wscript.Arguments (0)
        If Err.Description <> "" Then
            Wscript.Echo "Error description: " & _
                Err.Description
        End If

        set objFSO = Nothing
        wscript.quit (1)
    End If
End Sub

' GetDrive() fails inconsistently. I don't know why. So,
' iterate a few times to try to get the real answer.
Sub IterateGetDrive (strDrive)
    Dim i

    On Error Resume Next

    For i = 0 To 5
        Err.Clear
        Set objDrv = objFSO.GetDrive (strDrive)
        If Err.Number = 0 Then
            Exit Sub
        End If
    Next
    ErrorReport "GetDrive"
End Sub

The output from the script looks like this:

C:\>cscript //nologo ch12-drivespace.vbs \\server\c$
     Share           : \\server\c$
     % Free          : 75.3%
     Bytes Available : 34,237,988,864
     Bytes Used      : 11,251,183,616
     Total Bytes     : 45,489,172,480

Ch12-create-SG.wsf

This script allows you to create a storage group. However, unlike so many of the examples for this process that you find on the web, this one provides a simple user interface for selecting a server and it will not allow you to attempt to create a duplicate storage group.

This script uses CDOEXM, so it must be run on an Exchange server or a computer where the Exchange System Management Tools have been installed.

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript">
    Dim i, j, arr, arrDN, arrSG
    Dim str, strServer, strSG
    Dim objSG
 
    Call GetSystemInfo
    Call GetAllServers
 
    arr = Split (strServerList, ";")
    arrDN = Split (strServerListDN, ";")
    If UBound (arr) = 0 And LBound (arr) = 0 Then
        ' only one server - don't need to choose
        strServer = arrDN (0)
        i = 0
    Else
        str = "Please choose from the following servers:" & vbCrLF
        For i = LBound (arr) To UBound (arr)
            str = str & i & " - " & arr (i) & vbCrLf
        Next
        strServer = InputBox (str, "Create Storage Group")
        If strServer = "" Then
            e "Must enter a number"
            wscript.quit 1
        End If
        i = CInt (strServer)
        strServer = arrDN (i)
    End If
    e "Will create storage group on " & arr (i)
    Call GetStorageGroupsForServer (strServer)
    arrSG = Split (strServerSG, ";")
    str = "These are the current storage groups on " & arr (i) & _
        ". Please enter a different name." & vbCrLf
    For j = LBound (arrSG) To UBound (arrSG)
        str = str & arrSG (j) & vbCrLf
    Next
 
    strSG = Trim (InputBox (str, "Create Storage Group"))
    If strSG = "" Then
        e "Must enter a name"
        wscript.quit 1
    End If
 
    str = LCase (strSG)
    For j = LBound (arrSG) To UBound (arrSG)
        If str = LCase (arrSG (j)) Then
            e "Must not create duplicate storage group"
            WScript.Quit 1
        End If
    Next
 
    Set objSG = CreateObject ("CDOEXM.StorageGroup")
    ' build the storage group DN (many examples cut the SG DN
    ' out of an already existing SG on the server -- but that
    ' requires assumptions that I don't like).
    str = "LDAP://CN=" & strSG & ",CN=InformationStore," & strServer
    e "Storage group DN: " & str
    ' Save the StorageGroup
    objSG.DataSource.SaveTo str
    e "Storage group created!"
    Set objSG = Nothing
    Call ClearSystemInfo
</script>
</job>

Ch12-set-store-DIR.vbs

Setting and interrogating the attributes for deleted item retention and deleted mailbox retention are a part of any good Exchange management plan. This script shows you how to set those for a single mailbox store and public store on a specified server. Since you’ve already seen how to iterate through stores and storage groups, that information is not repeated here, instead the distinguished names to the stores are built manually.

 

Const SERVER  = "SERVER"
Const SGROUP  = "First Storage Group"
Const AGROUP  = "First Administrative Group"
Const ORGNAME = "First Organization"
Const DOMAIN  = "DC=wedoexchange,DC=com"
'
Sub Set_DeletedItemRetention (obj)
    '
    ' if deletedItemFlags = 5, then "Do not permanently
    ' delete items" is Unchecked
    '
    ' if deletedItemFlags = 3, then "Do not permanently
    ' delete items" is CHECKED
    '
    ' garbageCollPeriod is the number of days times 86400
    ' (seconds in a day) arbitrarily use 10 days
    '
    obj.deletedItemFlags = 3
    obj.garbageCollPeriod = 10 * 86400
    obj.SetInfo
End Sub
'
Sub Clear_DeletedItemRetention (obj)
    '
    ' instead of removing the attributes, ESM just sets
    ' deletedItemFlags = 0.  I'll do the same.
    '
    obj.deletedItemFlags = 0
    obj.SetInfo
End Sub
'
Sub Set_DeletedMailboxRetention (obj)
    '
    ' msExchMailboxRetentionPeriod is the number of days
    ' times 86400 (seconds in a day). use 30 days. It only
    ' applies to mailbox stores, never to public stores.
    '
    If obj.Class = "msExchPrivateMDB" Then
        obj.msExchMailboxRetentionPeriod = 30 * 86400
        obj.SetInfo
    End If
End Sub
'
Dim str, obj
'
' build strings that point to the default public and private
' stores on the specified server
'
str = "CN=Public Folder Store (" & SERVER & ")," & _
    "CN=" & SGROUP & ",CN=InformationStore,CN=" & _
    SERVER & ",CN=Servers,CN=" & _
    AGROUP & ",CN=Administrative Groups,CN=" & _
    ORGNAME & ",CN=Microsoft Exchange,CN=Services," & _
          "CN=Configuration," & _
    DOMAIN
'
Set obj = GetObject ("LDAP://" & str)
Call Clear_DeletedItemRetention (obj)
Call Set_DeletedItemRetention (obj)
Call Set_DeletedMailboxRetention (obj)
'
' does nothing on PF store
Set obj = Nothing
str = "CN=Mailbox Store (" & SERVER & ")," & _
    "CN=" & SGROUP & ",CN=InformationStore,CN=" & _
    SERVER & ",CN=Servers,CN=" & _
    AGROUP & ",CN=Administrative Groups,CN=" & _
    ORGNAME & ",CN=Microsoft Exchange,CN=Services," & _
          "CN=Configuration," & _
    DOMAIN
'
Set obj = GetObject ("LDAP://" & str)
Call Clear_DeletedItemRetention (obj)
Call Set_DeletedItemRetention (obj)
Call Set_DeletedMailboxRetention (obj)
'
' does nothing on PF store
'
Set obj = Nothing

Ch12-store-space.wsf

This script, as promised, is pretty much a fully fledged application. For every Exchange server in your organization it looks up all the stores for each server and totals the disk space used by those stores. Subtotals are provided per store, per storage group, per server, and a grand total for the entire organization.

This script, like a few others yet to be presented, kind of put all the scripting skills together that you’ve been presented. The WSH include files, the support routines, using Exchange like a file system, the Scripting.FileSystemObjectall of these are used to produce a nice result.

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript">
Dim arr, arrSG, arrStoreDim s, s1, strServerDim i, j, k, orgTotal
Dim objFSO
'
Call GetSystemInfo
Call GetAllServers
'
Set objFSO = CreateObject ("Scripting.FileSystemObject")
arr = Split (strServerListDN, ";")
For i = LBound (arr) To UBound (arr)
    Dim serverTotal
    '
    serverTotal = 0
    '
    s = arr (i)
    strServer = Mid (Left (s, Instr (s, ",") - 1), 4)
    e "Server name: " & strServer
    '
    Call GetStorageGroupsForServer (arr (i))
    '
    arrSG = Split (strServerSGDN, ";")
    For j = LBound (arrSG) To UBound (arrSG)
        Dim sgTotal
        '
        sgTotal = 0
        s = arrSG (j)
        s1 = Mid (Left (s, Instr (s, ",") - 1), 4)
        e Space (2) & "Storage group: " & s1
        Call GetStoresForStorageGroupLDAP (s)
        arrStore = Split (strStoreDN, ";")
        '
        For k = LBound (arrStore) To UBound (arrStore)
            Dim obj, objFile, strFile, storeTotal
            '
            storeTotal = 0
            s = arrStore (k)
            s1 = Mid (Left (s, Instr (s, ",") - 1), 4)
            e Space (4) & "Store: " & s1
            Set obj = GetObject ("LDAP://" & s)
            e Space (6) & "EDB file: " & obj.Get ("msExchEDBFile")
            ' must convert the file to a UNC path
            s = obj.Get ("msExchEDBFile")
            strFile = "\\" & strServer & "\" & _
                Left (s, 1) & "$" & Mid (s, 3)
            Set objFile = objFSO.GetFile (strFile)
            e Space (6) & "Size: " & _
                FormatNumber (objFile.Size / (1024 * 1024), 0) & _
                " megabytes"
            storeTotal = storeTotal + (objFile.Size / (1024 * 1024))
            e Space (6) & "SLV file: " & obj.Get ("msExchSLVFile")
            s = obj.Get ("msExchSLVFile")
            strFile = "\\" & strServer & "\" & _
                Left (s, 1) & "$" & Mid (s, 3)
            Set objFile = objFSO.GetFile (strFile)
            e Space (6) & "Size: " & _
                FormatNumber (objFile.Size / (1024 * 1024), 0) & _
                " megabytes"
            storeTotal = storeTotal + (objFile.Size / (1024 * 1024))
            e Space (6) & "Store Size Total: " & _
                FormatNumber (storeTotal, 0) & _
                " megabytes"
            sgTotal = sgTotal + storeTotal
        Next
        '
        e Space (4) & "Storage Group total: " & FormatNumber (sgTotal, 0) & _
            " megabytes"
        serverTotal = serverTotal + sgTotal
    Next
    '
    e Space (2) & "Server total: " & FormatNumber (serverTotal, 0) & " megabytes"
    orgTotal = orgTotal + serverTotal
Next
'
e "Organization total: " & FormatNumber (orgTotal, 0) & " megabytes"
Call ClearSystemInfo
</script>
</job>

Ch12-create-store.wsf 

Much like Ch12-create-SG.wsf provided you a mechanism for creating a storage group, this script provides a simple user interface to allow you to create an information storespecifically a mailbox store, not a public folder store. You can choose the server, the storage group, and are not allowed to enter a duplicate store name. This script uses CDOEXM, so it must be run on an Exchange server or a computer where the Exchange System Management Tools have been installed.

 

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript" src="lib/queries.vbs"        />
<script language="VBScript">
    Dim i, j, k, arr, arrDN, arrSG
    Dim str, str1, strServer, strSG, strStore
    Dim objMDB
    '
    Call GetSystemInfo
    Call GetAllServers
    '
    arr = Split (strServerList, ";")
    arrDN = Split (strServerListDN, ";")
    i = ChooseFromList (arr, _
        "Please choose from the following servers:" & vbCrLF)
    strServer = arrDN (i)
    e "Will create store on server: " & arr (i)
    '
    Call GetStorageGroupsForServer (strServer)
    '
    arrSG = Split (strServerSG, ";")
    j = ChooseFromList (arrSG, _
        "Please choose from the following storage groups on " & _
        arr (i) & vbCRLF)
    strSG = arrSG (j)
    e "Will create new store in storage group: " & strSG
    '
    Call GetStoresForStorageGroupLDAP ("CN=" & strSG & _
        ",CN=InformationStore," & strServer)
    arrStore = Split (strStoreDN, ";")
    str = "These are the current stores on " & arr (i) & _
        " in storage group " & strSG & _
        ". Please enter a different name." & vbCrLf
    If UBound (arrStore) < 0 Then
        str = str & "<empty list>"
    Else
        For j = LBound (arrStore) To UBound (arrStore)
            k = InStr (arrStore (j), ",") - 4
            str = str & Mid (arrStore (j), 4, k) & vbCrLf
        Next
    End If
    '
    strStore = Trim (InputBox (str, "Create Message Store"))
    If strStore = "" Then
        e "Must enter a name"
        wscript.quit 1
    End If
    '
    str = LCase (strStore)
    For j = LBound (arrStore) To UBound (arrStore)
        k = InStr (arrStore (j), ",") - 4
        str1 = LCase (Mid (arrStore (j), 4, k))
        If str = str1 Then
            e "Must not create duplicate store name"
            WScript.Quit 1
        End If
    Next
    '
    Set objMDB = CreateObject ("CDOEXM.MailboxStoreDB")
    ' build the mailbox store DN (many examples cut the store DN
    ' out of an already existing store on the server -- but that
    ' requires assumptions that I don't like).
    str = "LDAP://CN=" & strStore & ",CN=" & strSG & _
        ",CN=InformationStore," & strServer
    e "Mail store DN: " & str
    objMDB.Name = strStore
    ' Save the StorageGroup
    objMDB.DataSource.SaveTo str
    e "Mail store created!" & vbCRLF & "Mounting it now."
    '
    ' to mount the store, do this.
    objMDB.Mount
    e "Mail store mounted!"
    '
    Set objMDB = Nothing
    Call ClearSystemInfo
    WScript.Quit 0
'
Function ChooseFromList (arr, strHeader)
    Dim str, i
    '
    If UBound (arr) = 0 and LBound (arr) = 0 Then
        ' only one entry - don't need to choose
        ChooseFromList = 0
        Exit Function
    End If
    '
    str = strHeader
    For i = LBound (arr) To UBound (arr)
        str = str & i & " - " & arr (i) & vbCrLf
    Next
    '
    str = Trim (InputBox (str, "Create Message Store"))
    If str = "" Then
        e "Must enter a number"
        wscript.quit 1
    End If
    '
    ChooseFromList = CInt (str)
End Function
</script>
</job>

Ch12-log-review.vbs

This script, our most complex script to date, uses WMI to interrogate the log files of a specific server (or set of servers). Depending on the setting of a global variable, the script will either display defragmentation log file results from the information store or it will display backup messages from ESE. By default, the script will search for the last 24 hours (exactly one day) of log messages. As a command line argument, you may specify a number, which is the number of days to search back.

Several key techniques are displayed in this script. One of them is converting from a WMI timestamp to a human readable date (that little piece of script was pulled directly from the Microsoft Script Center, I make no claim on it at all). Another is intelligently connecting to a remote computer using WMI with or without impersonation (i.e., elevating your privilege level on the remote computer). Another is building a WMI search string based on the setting of various parameters.

Finally, it just flat out comes in handy. Running this script on a regular basis allows you to keep an eye on how much white space is present in your various information stores and to verify your backups. You can also easily modify it to print only errors out, which is something that it is good to keep an eye on as well.

In the line Call Doit ("SERVER", "", ""), almost at the very end of the script, you should modify the “SERVER” to reflect the server you wish to be checked.

Option Explicit
'
Const bDefragSpace = True
Const wbemFlagReturnImmediately = 16
Const wbemFlagForwardOnly = 32
'
' http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/improving_enumeration_performance.asp
'
Dim dtmStartDate, dtmEndDate ' dates to search the logfiles for
Dim DateToCheck              ' date to start search at, in Date() format
Dim objWMIService
Dim dateoffsetindex
'
Function WMIDateStringToDate (dtmInstallDate)
    WMIDateStringToDate = CDate ( _
        Mid (dtmInstallDate, 5, 2)  & "/" & _
        Mid (dtmInstallDate, 7, 2)  & "/" & _
        Left (dtmInstallDate, 4)    & " " & _
        Mid (dtmInstallDate, 9, 2)  & ":" & _
        Mid (dtmInstallDate, 11, 2) & ":" & _
        Mid (dtmInstallDate, 13, 2))
End Function
'
Sub e (str)
    WScript.Echo str
End Sub
'
Function ErrorReport (str)
    If Err.Number Then
        ErrorReport = True
        e "Error 0x" & CStr (Hex (Err.Number)) & " occurred " & str & "."
        If Err.Description <> "" Then
                    e "Error description: " & Err.Description & "."
        End If
        Err.Clear
    Else
        ErrorReport = False
    End If
End Function
'
Function ConnectComputer(ByVal strUserName,  _
                         ByVal strPassword,  _
                         ByVal strServer,    _
                         ByRef objService)
    On Error Resume Next
    Dim objLocator
    '
    ConnectComputer = False
    'There is no error.
    'Create Locator object to connect to remote CIM object manager
    If IsEmpty (strUserName) Then
        set objService = GetObject ("winmgmts:" & _
            "{impersonationLevel=impersonate}!\\" & _
            strServer & "\root\cimv2")
        If ErrorReport ("acquiring a WMI object") Then
            ConnectComputer = True     'An error occurred
        End If
        Exit Function
    End If
    '
    Set objLocator = CreateObject ("WbemScripting.SWbemLocator")
    If ErrorReport ("creating a locator object") Then
        ConnectComputer = True     'An error occurred
        Exit Function
    End If
    '
    'Connect to the namespace which is either local or remote
    '
    Set objService = objLocator.ConnectServer (strServer, "root\cimv2", _
        strUserName, strPassword)
    objService.Security_.impersonationlevel = 3
    If ErrorReport ("connecting to server " & strServer) Then
        ConnectComputer = True     'An error occurred
    End If
End Function
'
Function CheckLogfileOnComputer (strComputer)
    Dim colEvents ' collection of event objects returned from WMI
    Dim objEvent  ' single event selected from colEvents
    Dim str       ' the select query
    '
    On Error Resume Next
    '
    e "Checking computer " & strComputer & "..."
    str = "Select * from Win32_NTLogEvent Where " _
        & " Logfile='Application' and " _
        & " TimeWritten >= '" & dtmStartDate & "' and " _
        & " TimeWritten < '" & dtmEndDate & "'"
    '
    If bDefragSpace Then
        str = str & " and (SourceName='MSExchangeIS Mailbox Store'" _
              & "      or SourceName='MSExchangeIS Public Store')" _
              & " and EventCode=1221"
    Else
        str = str & " and SourceName='ESE' " _
              & " and (EventCode >= 200 and EventCode <= 399) "
    End If
    '
    Set colEvents = objWMIService.ExecQuery (str,, _
        wbemFlagReturnImmediately + wbemFlagForwardOnly)
    if IsNull (colEvents) Then
        e "No records (null)."
        Exit Function
    End If
    '
    For Each objEvent In colEvents
        e "Logfile: " & objEvent.Logfile _
            & " on Computer: " & objEvent.ComputerName _
            & " (record number " & objEvent.RecordNumber & ")"
        e "    Type: " & objEvent.Type & " ID: " & objEvent.EventCode
        e "    Source: " & objEvent.SourceName & _
            " Time: " & WMIDateStringToDate (objEvent.TimeWritten)
        e "    Category: " & objEvent.CategoryString & _
            " User: " & objEvent.User
        e "    Message: " & objEvent.Message
    Next
End Function
'
Function Doit(Byval servername, Byval username, Byval password)
    If ConnectComputer (username, password, servername, objWMIService) Then
        e "ERROR: *** Couldn't check computer " & servername & "."
    Else
        CheckLogfileOnComputer (servername)
    End If
End Function
'
Sub Usage
    e "cscript.exe //nologo log.vbs [days]"
    Wscript.Quit (1)
End Sub
    '
    ' Main
    '
dateoffsetindex = 1
If Wscript.Arguments.Count <> 0 Then
    If WScript.Arguments.Count > 1 Then
        Call Usage
    End If
    '
    dateoffsetindex = CInt (WScript.Arguments (0))
End If
'
Set dtmStartDate = CreateObject ("WbemScripting.SWbemDateTime")
Set dtmEndDate   = CreateObject ("WbemScripting.SWbemDateTime")
'
DateToCheck = CDate (DateAdd ("d", -dateoffsetindex, Date ()))
dtmStartDate.SetVarDate DateToCheck, TRUE
dtmEndDate.SetVarDate DateToCheck + dateoffsetindex, TRUE
'
e "Start time: " & Date () & " " & Time () & "."
'
' You don't need to specify a user or password if inherited permissions
' will work for you.
'
Call Doit ("SERVER", "", "")
e "Stop time: " & Date () & " " & Time () & "."
 

Ch12-add-proxy.wsf

The next script is a pretty useful tool. It allows you to add a new non-primary SMTP e-mail address to a user. You can specify the user by using either their user principal name (UPN) or their down-level SAM account name. These values are found on the Accounts tab of the user object property sheet. The UPN is the “User logon name” field. The SAM account name is the “User logon name (pre-Windows 2000)” field. The SAM account name is also sometimes known as the user’s NetBIOS name.

Being able to add an e-mail alias is very useful in scripts. The techniques shown in this script also illustrate how to find a specific user object using either the UPN or the SAM account name. You are also shown how to break apart the proxyAddresses attribute.

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript">
'
'    This program adds a new non-primary proxyaddress for
'    a user. The user must already exist, the proxyAddresses
'    attribute for the user must already exist, and the new
'    proxyAddress must be in SMTP format.
'
'    You must specify a valid userPrincipalName (UPN) or a valid
'    sAMAccountName to specify the user object to be modified.
'
'    Usage:
'
'    Ch12-Add-Proxy.wsf [UPN | sAMAccountName] proxyAddress
'
'    Example:
'
'    Ch12-Add-Proxy.vbs michael.smith smtp:abuse@[1.2.3.4]
'
    Dim objUser, s, strQuery, objRootDSE, strNamingContext
    Dim strSearch, strAddr
    Dim arr, arrNew
    Dim i, iCount, item
    '
    If WScript.Arguments.Count <> 2 Then
         Call myExit ("Wrong argument count")
    End If
    '
    strAddr   = WScript.Arguments (1)
    strSearch = WScript.Arguments (0)
    s = LCase (Left (strAddr, 5))
    If s <> "smtp:" Then
         Call myExit ("proxyAddress must have 'smtp:'")
    End If
    '
    If InStr (strAddr, "@") = 0 Then
         Call myExit ("proxyAddress must have '@'")
    End If
    '
    If InStr (strAddr, ".") = 0 Then
         Call myExit ("proxyAddress must have '.'")
    End If
    '
    Set objRootDSE = GetObject ("LDAP://RootDSE")
    strNamingContext = objRootDSE.Get ("defaultNamingContext")
    Set objRootDSE = Nothing
    '
    Call InitializeADSI
    If checkProxyAddresses (strAddr) Then
        Call DoneWithADSI
        WScript.Quit 1
    End If
    '
    If Instr (strSearch, "@") > 0 Then
        s = "userPrincipalName=" & strSearch
    Else
        s = "sAMAccountName=" & strSearch
    End If
    '
    strQuery = "<LDAP://" & strNamingContext & ">;" & _
        "(" & s & ");" & _
        "name,distinguishedName;" & _
        "subtree"
    '
    Call DoLDAPQuery (strQuery, Rs)
    If rs.RecordCount = 0 Then
        e "Record not found: " & s
        WScript.Quit 1
    End If
    '
    If rs.RecordCount > 1 Then
        e "Too many records found: " & s
        WScript.Quit 1
    End If
    '
    While not rs.EOF
        set objUser = GetObject ("LDAP://" & rs.Fields ("distinguishedname"))
        arr = objUser.proxyAddresses
        iCount = UBound (arr)
        '
        ReDim arrNew (iCount + 1)
        i = 0
        For Each item in arr
            e item
            arrNew (i) = item
            i = i + 1
        Next
        '
        arrNew (i) = strAddr
        wscript.echo strAddr
        objUser.proxyAddresses = arrNew
        objUser.SetInfo
        Set objUser = Nothing
        rs.MoveNext
    Wend
    '
    Set objUser = Nothing
    Call FinishLDAPQuery (rs)
    Call DoneWithADSI
    e "Done!"
'
Function checkProxyAddresses (str)
    s = "proxyAddresses=" & str
    strQuery = "<LDAP://" & strNamingContext & ">;" & _
        "(" & s & ");" & _
        "name,distinguishedName;" & _
        "subtree"
    '
    Call DoLDAPQuery (strQuery, Rs)
    If rs.RecordCount = 0 Then
        e "Unused proxyAddress: " & s
        checkProxyAddresses = False
    Else
        e "Duplicate proxyAddress (" & str & ") found. Not allowed."
        checkProxyAddresses = True
    End If
    '
    Call FinishLDAPQuery (rs)
End Function
'
Sub myExit (str)
    e str
    e "Ch12-Add-Proxy.wsf: [userPrincialName | " & _
        "sAMACcountName] proxyAddress"
    WScript.Quit 1
End Sub
</script>
</job>
 

Ch12-add-policy.wsf

With this script, things really get interesting. This script shows you how to create a recipient policy. While non-trivial, the process is really not that difficult. Spending some time with ADSIEdit.msc and adfind.exe/dsquery.exe can help you discover the attributes that need to be set when creating Exchange objects.

This script is provided with two parameters when it is executed: the name of a policy to create and the SMTP generation string to be built into the policy. When used in this mode, this script fulfills requirements specified in Chapter 4 for configuring policies that support IP address based e-mail addresses.

As an alternate way of using this logic, the main routine in this script, CreateRecipientPolicy is provided a domain name, a policyname (optional), a SMTP generation string (optional), and a Boolean option as parameters. When used this way, the routine will create a recipient policy which has the same name as the domain, and will generate a default LDAP filter for the policy that applies to all users who have that domain as their user principal name or any of a group, public folder or contact which has that domain as the last part of its name.

This script displays techniques for creating Exchange objects in specific places, along with using values from other objects as reasonable defaults where appropriate. Several complex attributes are also assigned created values.

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript">
'
'    This program creates a new recipient policy. This is non-trivial,
'    but really not that difficult.
'
'    The assumptions are that the name of the recipient policy is the
'    first argument supplied to the script and the e-mail address
'    generation rule is supplied as the second (as shown in chapter 4).
'
'    A more common usage would be to use the the first parameter as the
'    domain name, leave the second and third parameters empty, and set
'    the fourth parameter to true. This would create a recipient policy
'    that was named the same as the domain, having an address generation
'    rule based on the domain, with a default LDAP filter applied.
'
    Dim strPolName, strAddrGen
    '
    Call GetSystemInfo
    '
    If WScript.Arguments.Count <> 2 Then
        e "Usage: CH12-Add-Policy.wsf policy-name smtp-generation-string"
        WScript.Quit 1
    End If
    '
    strPolName = WScript.Arguments (0)
    strAddrGen = WScript.Arguments (1)
    '
    If CreateRecipientPolicy ("", strPolName, strAddrGen, False) Then
        e "CreateRecipientPolicy failed"
    Else
        e "CreateRecipientPolicy succeeded!"
    End If
    '
    Call ClearSystemInfo
'
' CreateRecipientPolicy
'
'    Purpose:
'        Create a recipient policy named strRecipient.
'
'    Inputs:
'        strRecipient - The name of the domain for which the policy
'        will be created.
'
'        strPolicyName - The name of the recipient policy. If not
'        specified, then the name of the domain is used.
'
'        strPrimaryAddress - The address generation string for the
'        primary SMTP address of the policy.
'
'        bFilter - If set to true, then a default LDAP filter is placed
'        on the policy.
'
'    Outputs:
'        Function Value - true on failure, false otherwise
'
'    Requires:
'        strOrgDN
'        The Default Policy (to copy certain values)
'
Function CreateRecipientPolicy (strRecipient, strPolicyName, _
        strPrimaryAddress, bFilter)
' returns true on failure
    Dim objDefault, objContainer, objNewPolicy, arrPurportedSearchUIArray
    Dim defaultSystemFlags, defaultmsExchPolicyOptionList
    Dim strSearch
    '
    CreateRecipientPolicy = False
    If IsEmpty (strPolicyName) or Len (strPolicyName) = 0 Then
        strPolicyName = strRecipient
    End If
    '
    If IsEmpty (strPrimaryAddress) or Len (strPrimaryAddress) = 0 Then
        strPrimaryAddress = "%1g%s@" & strRecipient
    End If
    '
    On Error Resume Next
    Err.Clear
    '
    ' Get the Default Policy from the Recipient Policy Container
    '
    Set objDefault = GetObject ("LDAP://CN=Default Policy," & _
        "CN=Recipient Policies," & strOrgDN)
    If Err Then
        ErrorReport
        e "Couldn't obtain Default Recipient Policy object"
        CreateRecipientPolicy = True
        Exit Function
    End If
    '
    defaultSystemFlags = objDefault.systemFlags
    defaultmsExchPolicyOptionList = objDefault.msExchPolicyOptionList
    Set objDefault = Nothing
    ' This search request creates a recipient policy that does
    ' both users and groups.
    strSearch = "(|(&(&(&(|(&(objectCategory=person)(objectSid=*)(!samAccountType:1.2.840.113556.1.4.804:=3))(&(objectCategory=person)(!objectSid=*))(&(objectCategory=group)(groupType:1.2.840.113556.1.4.804:=14))))(objectClass=user)(userPrincipalName=*" + strRecipient + ")))"
    strSearch = strSearch + "(&(&(&(&(mailnickname=*)(|(&(objectCategory=person)(objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=contact))(objectCategory=group)(objectCategory=publicFolder))))(objectCategory=group)(displayName=*" + strRecipient + "))))"
    ' The UI array that goes along with the above search
    ' (the format of this is not documented)
    arrPurportedSearchUIArray = Array ( _
            "Microsoft.PropertyWell_Value0=" + strRecipient, _
            "Microsoft.PropertyWell_Condition0=5", _
            "Microsoft.PropertyWell_Property0=userPrincipalName", _
            "Microsoft.PropertyWell_ObjectClass0=user", _
            "Microsoft.PropertyWell_Items=1", _
            "Exchange_ObjectTypes=0", _
            "DsQuery_EnableFilter=0", _
            "DsQuery_ViewMode=4868", _
            "CommonQuery_Form=E23FEE83D957D011B93200A024AB2DBB", _
            "CommonQuery_Handler=5EE6238AC231D011891C00A024AB2DBB")
    '
    ' Create the New Policy Object
    '
    Set objContainer = GetObject ("LDAP://CN=Recipient Policies," _
        & strOrgDN)
    If Err Then
        ErrorReport
        e "Couldn't obtain Recipient Policies container object"
        CreateRecipientPolicy = True
        Exit Function
    End If
    '
    Set objNewPolicy = objContainer.Create ("msExchRecipientPolicy", _
        "CN=" & strPolicyName)
    If Err Then
        ErrorReport
        e "Couldn't create new Recipient Policy object: " _
          & strPolicyName
        CreateRecipientPolicy = True
        Set objContainer = Nothing
        Exit Function
    End If
    '
    objNewPolicy.systemFlags            = defaultSystemFlags
    objNewPolicy.msExchPolicyOptionList = defaultmsExchPolicyOptionList
    objNewPolicy.msExchPolicyOrder      = 1
    objNewPolicy.msExchProxyGenOptions  = 0
    objNewPolicy.showInAdvancedViewOnly = True
    objNewPolicy.Put "gatewayProxy", Array ("SMTP:" & strPrimaryAddress, _
            "X400:c=us;a= ;p=" + Mid (strOrg, 1, 16) + ";o=Exchange;")
    If bFilter Then
        objNewPolicy.Put "purportedSearch", strSearch
        objNewPolicy.Put "msExchPurportedSearchUI", arrPurportedSearchUIArray
    End If
    '
    objNewPolicy.SetInfo
    If Err Then
        ErrorReport
        e "Couldn't configure new Recipient Policy object: " _
          & strPolicyName
        CreateRecipientPolicy = True
        Set objContainer = Nothing
        Set objNewPolicy = Nothing
        Exit Function
    End If
    '
    Set objContainer = Nothing
    Set objNewPolicy = Nothing
    dp "Created recipient policy: " & strPolicyName
End Function
</script>
</job>
 

Ch12-user-mods.vbs

This is a fun script. This script shows you how to change almost everything you would probably ever want to modify about a user object. This includes:

·         Setting their mailbox quota to “unlimited”
·         Setting their mailbox quota to “use default”
·         Setting their mailbox quota to a specific value
·         Setting and resetting the “Automatically update e-mail addresses based on recipient policy” flag
·         Setting the maximum incoming and outgoing message sizes
·         Removing the maximum incoming and outgoing message size limits
·         Setting deleted item retention
·         Returning deleted item retention to the default

There are also several techniques displayed that are of great value:

·         How to process every user in a group (including nested groups)
·         How to process every user in a specific organization unit (including its children)
·         How to prevent processing either duplicate users or duplicate groups when you are processing nested entities
·         How to remove attributes from an object

Enjoy!

Const ADS_PROPERTY_CLEAR = 1
Const EXCLUDED_POLICY = "{26491CFC-9E50-4857-861B-0CB8DF22B5D7}"
'
Dim objGroupDictionary, objUserDictionary
'
Set objGroupDictionary = CreateObject ("Scripting.Dictionary")
Set objUserDictionary  = CreateObject ("Scripting.Dictionary")
'
Call ProcessGroup("LDAP://CN=New Group,CN=Users,DC=wedoexchange,DC=com")
Set objGroupDictionary = Nothing
'
objUserDictionary.Removeall
'
Call ProcessOU ("LDAP://DC=wedoexchange,DC=com")
Set objUserDictionary = Nothing
'
WScript.Echo "Done!"
WScript.Quit 0
'
Sub ProcessUser (objUser)
    If objUserDictionary.Exists (LCase (objUser.ADsPath)) Then Exit Sub
    objUserDictionary.Add LCase (objUser.ADsPath), 0
    WScript.Echo "Process user: " & objUser.Name
    ''Call Set_Unlimited_Mailbox_Size (objUser)
    ''Call Set_Limited_Mailbox_Size (objUser)
    ''Call Set_Default_Mailbox_Size (objUser)
    ''Call Set_ExcludedPolicy (objUser)
    ''Call Clear_ExcludedPolicy (objUser)
    ''Call Set_IncomingAndOutgoingMaxMessageSize (objUser)
    ''Call Clear_IncomingAndOutgoingMaxMessageSize (objUser)
    ''Call Set_DeletedItemRetention (objUser)
    ''Call Clear_DeletedItemRetention (objUser)
End Sub
'
Sub Set_ExcludedPolicy (objUser)
    '
    ' If a user's msExchPoliciesExcluded attribute includes
    ' the constant shown by EXCLUDED_POLICY, then RUS will
    ' not process that user. The situation is a little tricky,
    ' because the attribute may or may not be present on the
    ' user object.
    '
    Dim strPolicies, arrPolicies
    '
    objUser.GetInfo
    '
    On Error Resume Next
    '
    strPolicies = objUser.Get ("msExchPoliciesExcluded")
    If Err.Number <> 0 Then
        If Err.Number = -2147463155 Then
            ' the attribute is not set on the user
            strPolicies = ""
        Else
            e "err=" & err.number & " desc=" & err.description
            WScript.Quit 1
        End If
    End If
    '
    On Error Goto 0
    '
    If Len (strPolicies) > 0 Then
        Dim i
        '
        arrPolicies = Split (strPolicies, ",")
        For i = LBound (arrPolicies) To UBound (arrPolicies)
            If arrPolicies (i) = EXCLUDED_POLICY Then
                '
                ' already done, we don't need to do anything
                '
                Exit Sub
            End If
        Next
        '
        strPolicies = strPolicies & "," & EXCLUDED_POLICY
    Else
        strPolicies = EXCLUDED_POLICY
    End If
    '
    objUser.Put "msExchPoliciesExcluded", strPolicies
    objUser.SetInfo
End Sub
'
Sub Clear_ExcludedPolicy (objUser)
    '
    ' If a user's msExchPoliciesExcluded attribute includes
    ' the constant shown by EXCLUDED_POLICY, then remove it.
    '
    Dim strPolicies, arrPolicies, index
    '
    index = -1
    objUser.GetInfo
    '
    On Error Resume Next
    '
    strPolicies = objUser.Get ("msExchPoliciesExcluded")
    If Err.Number <> 0 Then
        If Err.Number = -2147463155 Then
            ' the attribute is not set on the user
            strPolicies = ""
        Else
            e "err=" & err.number & " desc=" & err.description
            WScript.Quit 1
        End If
    End If
    '
    On Error Goto 0
    '
    If Len (strPolicies) > 0 Then
        Dim i
        '
        arrPolicies = Split (strPolicies, ",")
        For i = LBound (arrPolicies) To UBound (arrPolicies)
            If arrPolicies (i) = EXCLUDED_POLICY Then
                '
                ' it is there
                '
                index = i
                Exit For
            End If
        Next
    Else
        ' nothing there, so nothing to do
        Exit Sub
    End If
    '
    If index < 0 Then
        ' it wasn't there, so nothing to do
        Exit Sub
    End If
    '
    If UBound (arrPolicies) = 0 Then
        ' it was the only policy there, so clear the policy
        objUser.PutEx ADS_PROPERTY_CLEAR, "msExchPoliciesExcluded", 0
        objUser.SetInfo
        Exit Sub
    End If
    '
    ' build the policy string with that one excluded
    '
    strPolicies = ""
    For i = LBound (arrPolicies) To UBound (strPolicies)
        If i <> index Then
            strPolicies = "," & strPolicies & arrPolicies (i)
        End If
    Next
    '
    strPolicies = Right (strPolicies, Len (strPolicies) - 1)
    objUser.Put "msExchPoliciesExcluded", strPolicies
    objUser.SetInfo
End Sub
'
Sub Set_IncomingAndOutgoingMaxMessageSize (objUSer)
    '
    ' Arbitrary values: set the incoming limit to
    ' 1.5 MB (1,500 KB) and set the outgoing limit
    ' to 10 MB. The values are specified in KB.
    '
    objUser.delivContLength = 1500
    objUser.submissionContLength = 10000
    objUser.SetInfo
End Sub
'
Sub Clear_IncomingAndOutgoingMaxMessageSize (objUSer)
    '
    ' Just remove the settings
    '
    objUser.PutEx ADS_PROPERTY_CLEAR, "delivContLength", 0
    objUser.PutEx ADS_PROPERTY_CLEAR, "submissionContLength", 0
    objUser.SetInfo
End Sub
'
Sub Set_DeletedItemRetention (objUser)
    '
    ' if deletedItemFlags = 5, then "Do not permanently delete items" is Unchecked
    ' if deletedItemFlags = 3, then "Do not permanently delete items" is CHECKED
    '
    ' garbageCollPeriod is the number of days times 86400 (seconds in a day)
    ' arbitrarily use 10 days
    '
    objUser.deletedItemFlags = 3
    objUser.garbageCollPeriod = 10 * 86400
    objUser.SetInfo
End Sub
'
Sub Clear_DeletedItemRetention (objUser)
    '
    ' instead of removing the attributes, ESM just sets
    ' deletedItemFlags = 0.  I'll do the same.
    '
    objUser.deletedItemFlags = 0
    objUser.SetInfo
End Sub
'
Sub Set_Unlimited_Mailbox_Size (objUser)
    '
    ' it isn't obvious, but if you set a user to not use
    ' the mailstore defaults, but leave everything empty,
    ' that means the user has no limits
    '
    objUser.mDBUseDefaults = False
    objUser.PutEx ADS_PROPERTY_CLEAR, "mDBStorageQuota", 0
    objUser.PutEx ADS_PROPERTY_CLEAR, "mDBOverQuotaLimit", 0
    objUser.PutEx ADS_PROPERTY_CLEAR, "mDBOverHardQuotaLimit", 0
    objUser.SetInfo
End Sub
'
Sub Set_Limited_Mailbox_Size (objUSer)
    '
    ' set a mailbox to have a specific size of 50 MB, a
    ' stop sending email limit of 75 MB, and a stop
    ' send and receive at 100 MB.
    '
    ' the values are specified in KB.
    '
    objUser.mDBUseDefaults = False
    objUser.mDBStorageQuota = 50000
    objUser.mDBOVerQuotaLimit = 75000
    objUSer.mDBOverHardQuotaLimit = 100000
    objUser.SetInfo
End Sub
'
Sub Set_Default_Mailbox_Size (objUser)
    '
    ' set a mailbox to use the mailstore defaults
    '
    objUser.mDBUseDefaults = True
    '
    ' ESM doesn't clear the other attributes, so I won't either
    '
    objUser.SetInfo
End Sub
'
Sub ProcessGroup (strGroupDN)
    Dim objGroup, objMember, strClass
    '
    If objGroupDictionary.Exists (LCase (strGroupDN)) Then Exit Sub
    '
    objGroupDictionary.Add LCase (strGroupDN), 0
    WScript.Echo "ProcessGroup: " & _
        Mid (Left (strGroupDN, InStr (strGroupDN, ",") - 1), 11)
    Set objGroup = GetObject (strGroupDN)
    '
    For Each objMember in objGroup.Members
        strClass = objMember.Class
        If strClass = "user" or strClass = "inetOrgPerson" Then
            Call ProcessUser (objMember)
        ElseIf strClass = "group" Then
            Call ProcessGroup (objMember.ADsPath)
        End If
        ' strclass = contact is also interesting
        ' strClass = msExchDynamicDistributionList is also interesting
        ' but we don't do anything special with those
    Next
End Sub
'
Sub ProcessOU (strOUDN)
    Dim objOU, objMember, strClass
    '
    WScript.Echo "ProcessOU: " & _
        Mid (Left (strOUDN, InStr (strOUDN, ",") - 1), 11)
    Set objOU = GetObject (strOUDN)
    '
    For Each objMember in objOU
        strClass = objMember.Class
        If strClass = "user" or strClass = "inetOrgPerson" Then
            Call ProcessUser (objMember)
        ElseIf strClass = "container" or _
               strClass = "organizationalUnit" Then
            Call ProcessOU (objMember.ADsPath)
        End If
        ' if you really wanted, you could add in the entire
        ' ProcessGroup routine here too. But you probably
        ' don't want to.
    Next
End Sub
'
Sub e (str)
    wscript.echo str
End Sub
 

Ch12-addresses.wsf

I’m not going to lie to you, this script is pretty dense. This script shows you how to create an “All Address Lists” address list, a “Global Address Lists” address list, and an “Offline Address List” address list. They are pretty tough. You also have to be able to create GUIDs, byte arrays, and X.500 addresses. Techniques for performing all of these are illustrated below. Some of these techniques were posted in public forums by other script writers. I’ve credited them where that is the case.

As far as I know, at this writing, the process for creating offline address lists is nowhere else publicly available. Enjoy!

 
<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript">
'
'    To create all of these, you need a domain name, and a
'    longer name. These samples use "test.test" and "Test Company".
'
    Dim strSomeDomain, strSomeName
    '
    strSomeDomain = "test.test"
    strSomeName   = "Test Company"
    '
    Call GetSystemInfo
    '
    If CreateGAL (strSomeDomain, strSomeName) Then
        e "CreateGAL failed"
    Else
        e "CreateGAL succeeded!"
    End If
    '
    If CreateAAL (strSomeDomain, strSomeName) Then
        e "CreateAAL failed"
    Else
        e "CreateAAL succeeded!"
    End If
    '
    If CreateOAL (strSomeDomain, strSomeName) Then
        e "CreateOAL failed"
    Else
        e "CreateOAL succeeded!"
    End If
    '
    Call ClearSystemInfo
    e "Done!"
    '
Function CreateGAL (strDomain, strName)
' returns true on failure
    Dim strSearch, strObj, objNew, objContainer, arrSearch
    '
    CreateGAL = False
    '
    On Error Resume Next
    '
    ' Build the ldap connection string.
    '
    strObj = "LDAP://CN=All Global Address Lists,CN=Address Lists Container," & _
        strOrgDN
    Set objContainer = GetObject (strObj)
    If Err Then
        ErrorReport
        e "Couldn't obtain All Global Address Lists container object"
        CreateGAL = True
        Exit Function
    End If
    '
    strSearch = "(&(|(objectclass=user)(objectclass=contact)" & _
        "(objectclass=group))(proxyaddresses=smtp:*@" & strDomain & "))"
    arrSearch = Array ( _
        "Microsoft.PropertyWell_QueryString=(&(|(objectclass=user)" & _
        "(objectclass=contact)(objectclass=group))" & _
        "(proxyaddresses=smtp:*@" & strDomain & "))", _
            "Microsoft.PropertyWell_Items=0", _
            "DsQuery_EnableFilter=0", _
            "DsQuery_ViewMode=4868", _
            "CommonQuery_Form=E33FEE83D957D011B93200A024AB2DBB", _
            "CommonQuery_Handler=5EE6238AC231D011891C00A024AB2DBB")
    '
    ' Create the Global Address List
    '
    Set objNew = objContainer.Create ("addressBookContainer", _
        "CN=" + strName)
    If Err Then
        ErrorReport
        e "Couldn't create GAL: " & strName
        '
        set objContainer = Nothing
        '
        CreateGAL = True
        Exit Function
    End If
    '
    ' Required properties.
    '
    objNew.Put "purportedSearch",         strSearch
    objNew.Put "msExchPurportedSearchUI", arrSearch
    objNew.Put "name",                    strName
    objNew.Put "displayName",             strName
    objNew.Put "showInAdvancedViewOnly",  True
    objNew.Put "systemFlags",             1610612736
    objNew.SetInfo
    If Err Then
        ErrorReport
        e "Couldn't set properties for GAL: " & strName
        '
        set objNew       = Nothing
        set objContainer = Nothing
        '
        CreateGAL = True
        Exit Function
    End If
    '
    Set objNew       = Nothing
    Set objContainer = Nothing
    '
    dp "Created GAL: " & strName
End Function
'
Function CreateAAL (strDomain, strName)
' returns true on failure
    Dim strSearch, strObj, objNew, objContainer, arrSearch
    '
    CreateAAL = False
    '
    On Error Resume Next
    '
    ' Build the ldap connection string.
    '
    strObj = "LDAP://CN=All Address Lists,CN=Address Lists Container," & strOrgDN
    Set objContainer = GetObject (strObj)
    If Err Then
        ErrorReport
        e "Couldn't obtain All Address Lists container object"
        CreateGAL = True
        Exit Function
    End If
    '
    strSearch = "(&(mailnickname=*)(userprincipalname=*@" & strDomain & "))"
    arrSearch = Array ( _
        "Microsoft.PropertyWell_QueryString=(&(mailnickname=*)" & _
            "(userprincipalname=*@" & strDomain & "))", _
        "Microsoft.PropertyWell_Items=0", _
        "DsQuery_EnableFilter=0", _
        "DsQuery_ViewMode=4868", _
        "CommonQuery_Form=E33FEE83D957D011B93200A024AB2DBB", _
        "CommonQuery_Handler=5EE6238AC231D011891C00A024AB2DBB")
    '
    ' Create the All Address List
    '
    Set objNew = objContainer.Create ("addressBookContainer", _
        "CN=Addresses - " + strName)
    If Err Then
        ErrorReport
        e "Couldn't create AAL: " & strName
        set objContainer = Nothing
        CreateAAL = True
        Exit Function
    End If
    '
    ' Required properties.
    '
    objNew.Put "purportedSearch",         strSearch
    objNew.Put "msExchPurportedSearchUI", arrSearch
    objNew.Put "name",                    strName
    objNew.Put "displayName",             strName
    objNew.Put "showInAdvancedViewOnly",  True
    objNew.Put "systemFlags",             1610612736
    objNew.SetInfo
    If Err Then
        ErrorReport
        e "Couldn't set properties for AAL: " & strName
        '
        set objNew       = Nothing
        set objContainer = Nothing
        '
        CreateAAL = True
        Exit Function
    End If
    '
    Set objNew       = Nothing
    Set objContainer = Nothing
    '
    dp "Created AAL: " & strName
End Function
'
Function CreateOAL (strDomain, strName)
    Dim strDefault, strContainer, strGUID, strOABName, strAALName
    Dim objDefault, objContainer, objNew
    Dim arrGUID, arrAAL, strLEDN
    '
    CreateOAL = False
    '
    On Error Resume Next
    '
    strContainer = "CN=Offline Address Lists,CN=Address Lists Container," & _
        strOrgDN
    ' this is ANY OAL that has reasonable defaults
    strDefault = "CN=Default Offline Address List," & strContainer
    '
    Set objDefault = GetObject ("LDAP://" & strDefault)
    If Err Then
        ErrorReport
        e "Couldn't GetObject: " & strDefault
        CreateOAL = True
        Exit Function
    End If
    '
    Set objContainer = GetObject ("LDAP://" & strContainer)
    If Err Then
        ErrorReport
        e "Couldn't GetObject: " & strContainer
        Set objDefault = Nothing
        CreateOAL = True
        Exit Function
    End If
    '
    strGUID = CreateGUID (32)
    Call ConvertHexStringToByteArray (strGUID, arrGUID)
    strAALName = "CN=Addresses - " & strName & _
        ",CN=All Address Lists,CN=Address Lists Container," & strOrgDN
    arrAAL = Array (strAALName)
    '
    ' use objNew as a temporary to verify AAL exists
    '
    Set objNew = GetObject ("LDAP://" & strAALName)
    If Err Then
        ' oops!
        ErrorReport
        e "Couldn't GetObject: " & strAALName
        '
        Set objContainer = Nothing
        Set objDefault   = Nothing
        '
        CreateOAL = True
        Exit Function
    End If
    '
    Set objNew = Nothing
    strOABName = "Offline Address List - " & strName
    strLEDN    = "/O=" & strOrg & "/CN=addrlists/CN=oabs/CN=" & strOABName
    '
    Set objNew = objContainer.Create ("msExchOAB", "CN=" & strOABName)
    If Err Then
        ErrorReport
        e "Couldn't Create: " & strOABName
        '
        Set objCOntainer = Nothing
        Set objDefault   = Nothing
        '
        CreateOAL = True
        Exit Function
    End If
    '
    ' derived or calculated values
    '
    objNew.siteFolderGUID      = arrGUID
    objNew.offlineABContainers = arrAAL
    objNew.legacyExchangeDN    = strLEDN
    '
    ' used by system for matching OAL's to domains
    '
    objNew.adminDescription    = strDomain
    '
    ' copied values - most are simple, some aren't
    ' all unique to OAB's
    '
    objNew.doOABVersion        = objDefault.doOABVersion
    objNew.msExchOABFolder     = objDefault.msExchOABFolder
    objNew.offlineABSchedule   = objDefault.offlineABSchedule
    objNew.offlineABServer     = objDefault.offlineABServer
    objNew.offlineABStyle      = objDefault.offlineABStyle
    objNew.siteFolderServer    = objDefault.siteFolderServer
    '
    ' copied values - generic
    '
    objNew.showInAdvancedViewOnly = objDefault.showInAdvancedViewOnly
    objNew.systemFlags            = objDefault.systemFlags
    '
If 1 Then
    wscript.echo "siteFolderGUID = " & OctetToHexStr (objNew.SiteFolderGUID)
    wscript.echo "offlineABContainers = " & objNew.offlineABContainers (0)
    wscript.echo "legacyExchangeDN = " & objNew.legacyExchangeDN
    wscript.echo "doOABVersion = " & objNew.doOABVersion
    wscript.echo "msExchOABFolder = " & octetToHexStr (objNew.msExchOABFolder)
    wscript.echo "offlineABSchedule = " & octetToHexStr (objNew.offlineABSchedule)
    wscript.echo "offlineABServer = " & objNew.offlineABServer
    wscript.echo "offlineABStyle = " & objNew.offlineABStyle
    wscript.echo "siteFOlderServer = " & objNew.siteFolderServer
    wscript.echo "showInAdvancedViewOnly = " & objNew.ShowInAdvancedViewOnly
    wscript.echo "systemFlags = " & objNew.SystemFlags
End If
    '
    objNew.SetInfo
    If Err Then
        ErrorReport
        e "Couldn't SetInfo"
        '
        Set objNew       = Nothing
        Set objCOntainer = Nothing
        Set objDefault   = Nothing
        '
        CreateOAL = True
        Exit Function
    End If
    '
    Set objNew       = Nothing
    Set objDefault   = Nothing
    Set objContainer = Nothing
    '
    wscript.echo "Created new OAB: " & strName
End Function
'
Function OctetToHexStr (arrbytOctet)
    ' Function to convert OctetString (byte array) to Hex string.
    ' Code from Richard Mueller, a MS MVP in Scripting and ADSI
    Dim k
    '
    OctetToHexStr = ""
    For k = 1 To Lenb (arrbytOctet)
        OctetToHexStr = OctetToHexStr _
              & Right("0" & Hex(Ascb(Midb(arrbytOctet, k, 1))), 2)
    Next
End Function
'
Sub ConvertHexStringToByteArray (ByVal strHexString, ByRef pByteArray)
    Dim fso, stream, temp, ts, n
    '
    ' This is an elegant way to convert a hex string to a Byte
    ' array. Typename(pByteArray) will return Byte(). pByteArray
    ' should be a null variant upon entry. strHexString should be
    ' an ASCII string containing nothing but hex characters, e.g.,
    ' FD70C1BC2206240B828F7AE31FEB55BE
    '
    ' Code from Michael Harris, a MS MVP in Scripting
    '
    Set fso = CreateObject ("scripting.filesystemobject")
    Set stream = CreateObject ("adodb.stream")
    '
    temp = fso.gettempname ()
    Set ts = fso.createtextfile (temp)
    '
    For n = 1 To (Len (strHexString) - 1) step 2
        ts.write Chr ("&h" & Mid (strHexString, n, 2))
    Next
    '
    ts.close
    '
    stream.type = 1
    stream.open
    stream.loadfromfile temp
    pByteArray = stream.read
    stream.close
    fso.deletefile temp
    '
    Set stream = Nothing
    Set fso = Nothing
End Sub
'
Function CreateGUID (tmpLength)
    Randomize Timer
    '
    Dim tmpCounter,tmpGUID
    '
    Const strValid = "0123456789ABCDEF"
    '
    For tmpCounter = 1 To tmpLength
        tmpGUID = tmpGUID & _
            Mid (strValid, Int (Rnd (1) * Len (strValid)) + 1, 1)
    Next
    CreateGUID = tmpGUID
End Function
</script>
</job>

Ch12-pwd-expires.vbs

This is a fun script. It does some things that are fairly difficult to do, in VBScript, but it hides the complexity to make it all seem simple. This program obtains the domain maximum password age policy from Active Directory. If the maximum password age policy is zero days, then passwords are not forced to expire and the program terminates. If the policy is non-zero, every user object and inetOrgPerson object in the Active Directory is examined. If the flags on the object are set so that the password never expires for that object, the object is discarded and the next object is obtained. If the object doesn’t have a mailbox, the object is discarded and the next object is obtained.

Next, using a bit of ADSI chicanery specifically there for VBScript, the routine calculates how many days it will be until the password for the current object does expire. If it is within DAYS_FOR_EMAIL days, then the user is sent an e-mail warning them to change their password.

The e-mail is sent using CDOwithout using Exchange directly. This means that the e-mail can be sent via any SMTP gateway server, and the script can be executed on any Windows Server that has CDO installed (CDO is installed when you install the SMTP serviceon an Exchange Server it is extended and enhanced when you install Exchange).

A common usage for this type of script is when your users exclusively use POP3, IMAP or OWA basic. In those cases, the users are never notified when their password is about to expire. So, the helpdesk gets flooded with “my password is expired, can you change it for me” telephone calls.  This script allows you to prevent that.

 

'' This
'
' This program scans all users in the domain for users whose
' passwords have either already expired or will expire within
' DAYS_FOR_EMAIL days.
'
' An email is sent, using CDO, via the SMTP server specified
' as SMTP_SERVER to the user to tell them to change their
' password. You should change strFrom to match the email
' address of the administrator responsible for password changes.
'
' You will, at a minimum, need to change the SMTP_SERVER, and the
' STRFROM constants. If you run this on an Exchange server,
' then SMTP_SERVER can be "127.0.0.1" - and the value may be
' either an ip address or a resolvable name.
'
Option Explicit
'
' Local constants - you should change these!
'
Const SMTP_SERVER        = "127.0.0.1"
Const STRFROM            = "emailadmin@your.domain"
Const DAYS_FOR_EMAIL     = 15
'
' System Constants - do not change
'
Const ONE_HUNDRED_NANOSECOND    = .000000100   ' .000000100 is equal to 10^-7
Const SECONDS_IN_DAY            = 86400
Const ADS_UF_DONT_EXPIRE_PASSWD = &h10000
Const E_ADS_PROPERTY_NOT_FOUND  = &h8000500D
'
' Change to "True" for extensive debugging output
'
Const bDebug            = False
'
Dim obj
Dim numDays
Dim strDomainDN
'
Set obj = GetObject ("LDAP://RootDSE")
strDomainDN = obj.Get ("defaultNamingContext")
Set obj = Nothing
'
numdays = GetMaximumPasswordAge (strDomainDN)
dp "Maximum Password Age: " & numDays
'
If numDays > 0 Then Call ProcessOU ("LDAP://" & strDomainDN, numDays)
WScript.Echo "Done"
'
Function GetMaximumPasswordAge (ByVal strDomainDN)
    Dim objDomain, objMaxPwdAge
    Dim dblMaxPwdNano, dblMaxPwdSecs, dblMaxPwdDays
    '
    Set objDomain = GetObject("LDAP://" & strDomainDN)
    Set objMaxPWdAge = objDomain.maxPwdAge
    If objMaxPwdAge.LowPart = 0 And objMaxPwdAge.Highpart = 0 Then
        ' Maximum password age is set to 0 in the domain
        ' Therefore, passwords do not expire
        GetMaximumPasswordAge = 0
        Exit Function
    End If
    '
    dblMaxPwdNano = Abs (objMaxPwdAge.HighPart * 2^32 + _
        objMaxPwdAge.LowPart)
    dblMaxPwdSecs = dblMaxPwdNano * ONE_HUNDRED_NANOSECOND
    dblMaxPwdDays = Int (dblMaxPwdSecs / SECONDS_IN_DAY)
    GetMaximumPasswordAge = dblMaxPwdDays
End Function
'
Function UserIsExpired (objUser, iMaxAge, iRes)
    Dim intUserAccountControl, dtmValue, intTimeInterval
    Dim strName
    '
    On Error Resume Next
    '
    strName = Mid (objUser.Name, 4)
    intUserAccountControl = objUser.Get ("userAccountControl")
    If intUserAccountControl And ADS_UF_DONT_EXPIRE_PASSWD Then
        dp "The password for " & strName & " does not expire."
        UserIsExpired = False
        Exit Function
    End If
    '
    iRes = 0
    dtmValue = objUser.PasswordLastChanged
    If Err.Number = E_ADS_PROPERTY_NOT_FOUND Then
        UserIsExpired = True
        dp "The password for " & strName & " has never been set."
        Exit Function
    End If
    '
    intTimeInterval = Int (Now - dtmValue)
    dp "The password for " & strName & " was last set on " & _
        DateValue(dtmValue) & " at " & TimeValue(dtmValue) & _
        " (" & intTimeInterval & " days ago)"
    If intTimeInterval >= iMaxAge Then
        dp "The password for " & strName & " has expired."
        UserIsExpired = True
        Exit Function
    End If
    '
    iRes = Int ((dtmValue + iMaxAge) - Now)
    dp "The password for " & strName & " will expire on " & _
        DateValue(dtmValue + iMaxAge) & " (" & _
        iRes & " days from today)."
    If iRes <= DAYS_FOR_EMAIL Then
        dp strName & " needs an email for password change"
        UserIsExpired = True
    Else
        dp strName & " does not need an email for password change"
        UserIsExpired = False
    End If
End Function
'
Sub ProcessOU (strOUDN, iMaxPwdAge)
    Dim objOU, objMember, strClass, iResult
    '
    Set objOU = GetObject (strOUDN)
    For Each objMember in objOU
        strClass = objMember.Class
        If strClass = "user" or strClass = "inetOrgPerson" Then
            If IsEmpty (objMember.Mail) or _
               IsNull  (objMember.Mail) or _
               Len (objMember.Mail) = 0 Then
                dp Mid (objMember.Name, 4) & " has no mailbox"
            Else
                If UserIsExpired (objMember, iMaxPwdAge, iResult) Then
                    dp "...sending an email for " & objMember.Mail
                    Call SendEmail (objMember, iResult)
                End If
            End If
        ElseIf strClass = "container" or strClass = "organizationalUnit" Then
            Call ProcessOU (objMember.ADsPath, iMaxPwdAge)
        End If
    Next
    '
    Set objOU = Nothing
End Sub
'
Sub SendEmail (objUser, iResult)
    Dim objMail, str
    '
    Set objMail = CreateObject ("CDO.Message")
    With objMail.Configuration.Fields
    .Item ("http://schemas.microsoft.com/cdo/configuration/sendusing")      = 2
    .Item ("http://schemas.microsoft.com/cdo/configuration/smtpserver")     =_
        SMTP_SERVER
    .Item ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
    .Update
    End With
    '
    objMail.From     = STRFROM
    objMail.To       = objUser.Mail
    objMail.Subject  = "Password needs to be set for " & _
        Mid (objUser.Name, 4)
    str = "The active directory password for user " & _
        objUser.userPrincipalName & _
        " (" & objUser.sAMAccountName & ")" & vbCRLF
    If iResult <= 0 Then
        str = str & "has expired."
    Else
        str = str & "will expire in " & iResult & " days." & vbCRLF & _
            "Please change it as soon as possible."
    End If
    str = str & vbCRLF & vbCRLF & _
        "Thank you," & vbCRLF & _
        "Your email administrator" & vbCRLF
    objMail.Textbody = str
    objMail.Send
    '
    Set objMail = Nothing
End Sub
'
Sub dp (str)
    If bDebug Then WScript.Echo str
End Sub

Ch12-user-info.wsf

I’ve saved the best for last. This last script, while not the most complicated one you’ve seen, is by far the longest.

This script will display the value of every Exchange attribute associated with a user (excepting only the mailbox security descriptor). If the script is executed with no parameters, the script will do this for every user in the current domain. If the script is executed with a parameter, only those users whose name attribute matches that parameter will be displayed (note that this would be quoted for most people names, such as “Michael B. Smith”).

Several new techniques are displayed here:

·         How to retrieve a missing attribute without generating a fatal error
·         Deciding variable types at run time and responding properly
·         Decoding custom attribute values

The output of this script is organized according to the tab the values are displayed on within Active Directory Users and Computers. The last set of attributes displayed has no corresponding ADUC tab.

<job>
<script language="VBScript">
Option Explicit
</script>
<script language="VBScript" src="lib/constants.vbs"      />
<script language="VBScript" src="lib/ado.vbs"            />
<script language="VBScript" src="lib/report.vbs"         />
<script language="VBScript" src="lib/systeminfo.vbs"     />
<script language="VBScript">
'
'
Dim i, obj, item, collection, str, arr, strQuery
Dim bFromEveryone
'
Call GetSystemInfo
e " "
If Wscript.Arguments.Count <> 0 Then
    If WScript.Arguments.Count > 1 Then
        e "ch12-user-info.wsf [name]"
        WScript.Quit 1
    End If
    '
    strQuery = "(name=" & WScript.Arguments (0) & ");"
Else
    strQuery = "(&(objectCategory=person)(|(objectclass=user)" & _
         "(objectclass=inetOrgPerson))(mailNickName=*));"
End If
'
str = "<LDAP://" & strDomainNC & ">;" & _
    strQUery & _
    "name,adspath;" & _
    "subtree"
Call DoLDAPQuery (str, Rs)
'
While Not Rs.EOF
    Set obj = GetObject (Rs.Fields ("adspath"))
    obj.GetInfo
    '
    e vbCRLF & "---General Tab---" & vbCRLF
    DisplayItemWithCheck obj, "name", "User name"
    DisplayItemWithCheck obj, "displayName", "Display name"
    DisplayItemWithCheck obj, "mail", "Primary e-mail address"
    '
    e vbCRLF & "---Member Of Tab---" & vbCRLF
    DisplayItemWithCheck obj, "memberOF", "Member of the following groups"
    '
    e vbCRLF & "---Exchange General Tab---" & vbCRLF
    DisplayItemWithCheck obj, "homeMDB", "Mailbox store"
    DisplayItemWithCheck obj, "msExchHomeServerName", _
        "X.500 address of mailbox server"
    DisplayItemWithCheck obj, "mailNickname", "Exchange alias"
    '
    e vbCRLF &"---Delivery Restrictions Tab---" & vbCRLF
    i = SafeGet (obj, "submissionContLength")
    If IsNull (i) or i = 0 Then
        e "Delivery Restrictions: Send message size: use default limit"
    Else
        e "Delivery Restrictions: Send message size: " & i & " KB"
    End If
    '
    i = SafeGet (obj, "delivContLength")
    If IsNull (i) or i = 0 Then
        e "Delivery Restrictions: Receiving message size: use default limit"
    Else
        e "Delivery Restrictions: Receiving message size: " & i & " KB"
    End If
    '
    bFromEveryOne = True
    DisplayItemWithCheck obj, "msExchRequireAuthToSendTo", _
        "Delivery Restrictions: Accept Messages: From Authenticated Users Only"
    '
    coll = SafeGet (obj, "dlMemSubmitPerms")
    If Not IsNull (coll) Then
        e "Delivery Restrictions: Accept Messages: Only From: (dlMemSubmitPerms)"
        For Each item in coll
            e vbTAB & item
        Next
        bFromEveryone = False
    End If
    '
    coll = SafeGet (obj, "authOrig")
    If Not IsNull (coll) Then
        e "Delivery Restrictions: Accept Messages: Only From: (authOrig)"
        If TypeName (coll) = "String" Then
            e vbTAB & coll
        Else
            For Each item in coll
                e vbTAB & item
            Next
        End If
        bFromEveryone = False
    End If
    '
    coll = SafeGet (obj, "dlMemRejectPerms")
    If Not IsNull (coll) Then
        e "Delivery Restrictions: Accept Messages: " & _
             "From Everyone Except: (dlMemRejectPerms)"
        If TypeName (coll) = "String" Then
            e vbTAB & coll
        Else
            For Each item in coll
                e vbTAB & item
            Next
        End If
        bFromEveryone = False
    End If
    '
    coll = SafeGet (obj, "unauthOrig")
    If Not IsNull (coll) Then
        e "Delivery Restrictions: Accept Messages: " & _
             "From Everyone Except: (unauthOrig)"
        If TypeName (coll) = "String" Then
            e vbTAB & coll
        Else
            For Each item in coll
                e vbTAB & item
            Next
        End If
        bFromEveryone = False
    End If
    '
    If bFromEveryone Then
        e "Delivery Restrictions: Accept Messages: From Everyone"
    End If
    '
    e vbCRLF & "---Delivery Options Tab---" & vbCRLF
    coll = SafeGet (obj, "publicDelegates")
    If Not IsNull (coll) Then
        e "Delivery Options: Send on Behalf: Grant this Permission to:"
        If TypeName (coll) = "String" Then
            e vbTAB & coll
        Else
            For Each item in coll
                e vbTAB & item
            Next
        End If
    End If
    '
    i = SafeGet (obj, "altRecipient")
    If IsNull (i) or Len(i) = 0 Then
        e "Delivery Options: Forwarding address: None"
    Else
        e "Delivery Options: Forwarding address: " & i
    End If
    '
    DisplayItemWithCheck obj, "deliverAndRedirect", _
        "Delivery Options: Deliver messages to both forwarding address and mailbox"
    '
    i = SafeGet (obj, "msExchRecipLimit")
    If IsNull (i) or i = 0 Then
        e "Delivery Options: Recipient Limit: Use Default Limit: True"
    Else
        e "Delivery Options: Recipient Limit: Maximum Recipients: " & i
    End If
    '
    e vbCRLF & "---Storage Limits Tab---" & vbCRLF
    DisplayItemWithCheck obj, "mDBUseDefaults", _
        "Storage Limits: Use Mailbox store Defaults"
    DisplayItemWithCheck obj, "mDBStorageQuota", _
        "Storage Limits: Issue Warning"
    DisplayItemWithCheck obj, "mDBOverQuotaLimit", _
        "Storage Limits: Prohibit Send"
    DisplayItemWithCheck obj, "mDBOverHardQuotaLimit", _
        "Storage Limits: Prohibit Send and Receive"
    '
    i = SafeGet (obj, "deletedItemFlags")
    If Not IsNull (i) Then
        If i > 0 Then
            e "Deleted Items: Use mailbox store defaults is not checked."
            If i = 3 Then
                e "Deleted Items: Do not permanently delete...is checked"
            ElseIf i = 5 Then
                e "Deleted Items: Do not permanently delete...is not checked"
            Else
                e "Deleted Items: unknown value for deleteItemFlags"
            End If
        Else
            e "Deleted Items: Use mailbox store defaults is checked."
        End If
    Else
        e "Deleted Items: Use mailbox store defaults is checked."
    End If
    '
    i = SafeGet (obj, "garbageCollPeriod")
    If Not IsNull (i) Then
        e "Deleted Items: Keep deleted items for " & (i / 86400) & " days"
    End If
    '
    e vbCRLF & "---E-mail Addresses Tab---" & vbCRLF
    DisplayItemWithCheck obj, "proxyAddresses", "E-mail addresses"
    '
    e vbCRLF & "---Exchange Features Tab---" & vbCRLF
    DisplayItemWithCheck obj, "msExchOmaAdminWirelessEnable", null
    '
    i = SafeGet (obj, "msExchOmaAdminWirelessEnable")
    If Not IsNull (i) Then
        i = CInt (i)
        If i = 7 Then
            e "Exchange Features: Mobile Services: All disabled"
        ElseIf i = 0 Then
            e "Exchange Features: Mobile Services: All enabled"
        Else
            str = "Exchange Features: Mobile Services: " & _
                 "User initiated synchronization: "
            If (i and 4) = 0 Then
                str = str & "Enabled"
            Else
                str = Str & "Disabled"
            End If
            e str
            '
            str = "Exchange Features: Mobile Services: OMA: "
            If (i and 2 ) = 0 Then
                str = str & "Enabled"
            Else
                str = str & "Disabled"
            End If
            e str
            '
            str = "Exchange Features: Mobile Services: AUTD: "
            If (i and 1) = 0 Then
                str = str & "Enabled"
            Else
                str = str & "Disabled"
            End If
            e str
        End If
    End If
    '
    DisplayItemWithCheck obj, "msExchOmaAdminExtendedSettings", null
    DisplayItemWithCheck obj, "protocolSettings", _
        "Custom User Protocol Configuration"
    '
    e vbCRLF & "---Exchange Advanced Tab---" & vbCRLF
    DisplayItemWithCheck obj, "displayNamePrintable", _
        "Printable display name"
    DisplayItemWithCheck obj, "msExchHideFromAddressLists", _
        "Hide from Exchange address lists"
    '
    arr = SafeGet (obj, "securityProtocol")
    If Not IsNull (arr) Then
        If Right (Typename (arr), 2) = "()" Then
            str = "Downgrade high priority mail bound for X.400 is "
            If arr (3) <> 0 Then
                e str & "checked"
            Else
                e str & "not checked"
            End If
        End If
    Else
        e "Downgrade high priority mail bound for X.400 is not checked"
    End If
    '
    DisplayItemWithCheck obj, "extensionAttribute1", _
        "Exchange custom attribute 1"
    DisplayItemWithCheck obj, "extensionAttribute2", _
        "Exchange custom attribute 2"
    DisplayItemWithCheck obj, "extensionAttribute3", _
        "Exchange custom attribute 3"
    DisplayItemWithCheck obj, "extensionAttribute4", _
        "Exchange custom attribute 4"
    DisplayItemWithCheck obj, "extensionAttribute5", _
        "Exchange custom attribute 5"
    DisplayItemWithCheck obj, "extensionAttribute6", _
        "Exchange custom attribute 6"
    DisplayItemWithCheck obj, "extensionAttribute7", _
        "Exchange custom attribute 7"
    DisplayItemWithCheck obj, "extensionAttribute8", _
        "Exchange custom attribute 8"
    DisplayItemWithCheck obj, "extensionAttribute9", _
        "Exchange custom attribute 9"
    DisplayItemWithCheck obj, "extensionAttribute10", _
        "Exchange custom attribute 10"
    DisplayItemWithCheck obj, "extensionAttribute11", _
        "Exchange custom attribute 11"
    DisplayItemWithCheck obj, "extensionAttribute12", _
        "Exchange custom attribute 12"
    DisplayItemWithCheck obj, "extensionAttribute13", _
        "Exchange custom attribute 13"
    DisplayItemWithCheck obj, "extensionAttribute14", _
        "Exchange custom attribute 14"
    DisplayItemWithCheck obj, "extensionAttribute15", _
        "Exchange custom attribute 15"
    '
    e vbCRLF & "---Not on a Tab---" & vbCRLF
    DisplayItemWithCheck obj, "msExchUserAccountControl", null
    DisplayItemWithCheck obj, "msExchALObjectVersion", _
        "Version of user object in address lists"
    DisplayItemWithCheck obj, "msExchPoliciesIncluded", _
        "System policies that apply"
    DisplayItemWithCheck obj, "msExchPoliciesExcluded", _
        "System policies that do not apply"
    DisplayItemWithCheck obj, "showInAddressBook", _
        "Address books user is a member of"
    DisplayItemWithCheck obj, "homeMTA", "Home MTA"
    DisplayItemWithCheck obj, "legacyExchangeDN", "X.500 address"
    DisplayItemWithCheck obj, "textEncodedORAddress", null
    '
    Set obj = Nothing
    Rs.MoveNext
    e " "
Wend
'
e "Done!"
Call ClearSystemInfo
WScript.Quit 0
'
Function DisplayItemWithCheck (obj, strAttr, strName)
    Dim item, coll
    '
    If IsNull (strName) or IsEmpty (StrName) Then strName = strAttr
    '
    DisplayItemWithCheck = False
    '
    On Error Resume Next
    '
    coll = SafeGet (obj, strAttr)
    If IsNull (coll) Then Exit Function
    If Right (TypeName (coll), 2) = "()" Then
        e strName & ":"
        For Each item In coll
            e vbTab & item
        Next
    Else
        e strName & ": " & coll
    End If
End Function
'
Function SafeGet (obj, strAttr)
    Dim coll
    '
    On Error Resume Next
    '
    coll = obj.Get (strAttr)
    If Err.Number <> 0 Then
        If Err.Number = -2147463155 Then
            ' attribute not present. leave quietly.
            SafeGet = Null
            Exit Function
        End If
        e "Error retrieving " & strAttr & _
            " error=" & Hex(Err.Number) & " " & Err.Description
        SafeGet = Null
        Exit Function
    End If
    '
    SafeGet = coll
End Function
</script>
</job>

A Final Note

In this chapter you’ve seen how to get Exchange to provide you with information that you could not have otherwise retrieved and learned how to automate some tasks that were otherwise manual and prone to errors.

Take this and do more! Exchange is so much more than just email. It is an entire messaging platform. To make it perform well and to integrate into your reporting systems takes a little development on the part of the Exchange administrator, but nothing that is usually too difficult for anyone.

Many of the tasks you’ve seen performed above could be done differently (specifically, in almost every case you could’ve used CDOEX and/or CDOEXM instead of ADSI). That doesn’t mean that any one way is better than any other. This is simply the way I do things.

Exchange is full of changes and the future is bright for it as a messaging platform. E12 is on the horizon and will surely bring new technologies to learn and more scripts to write. Go on, get started.
Published Thursday, May 22, 2008 6:18 PM by michael

Comments

Friday, June 06, 2008 11:03 AM by subject: exchange

# Weekend reading

How Microsoft Office Communicator enhances Outlook 2007 functionality Why boot an Exchange server from

Saturday, June 28, 2008 11:05 AM by search engine people

# search engine people

...

# exchange 2007 updating legacy permissions failed could not find domain controller in domain

Pingback from  exchange 2007 updating legacy permissions failed could not find domain controller in domain

Monday, December 01, 2008 5:04 PM by Michael's meanderings...

# Fixing the scripts in Chapter 12 - Exchange 2003 Scripting

A reader recently pointed out to me that several of the last scripts in my post Chapter 12 - Exchange