Exchange Server Events


In this section, we will look at the events that Exchange fires ” specifically , synchronous, asynchronous, and system events. You register for synchronous events using an event registration. That event registration points to an event handler, which is the code you write that handles the firing of the event and performs the correct actions.

Synchronous events are events that fire after the item is saved. Exchange Server not only supports synchronous events but also adds them. They are called before the item is committed to the database. With synchronous events, your application can look at the item and then either accept it or prevent it from being committed into Exchange Server. However, remember that your event source is blocked while your event code runs. A good rule of thumb is try to make your events asynchronous if you can and make them synchronous only if you really require synchronous events. For example, if you need to check an item and stop it from being committed unless it meets certain requirements, you should use synchronous events. If you are just sending a notification to users that a new item was posted, asynchronous events will work.

Events are fired when items are moved, copied , created, or deleted in Exchange Server. The scope of these events is limited to a folder, and you can write event handlers that use ActiveX Data Objects (ADO) or OLE Database (OLE DB), depending on the programming language you use. You can write your event handlers in Visual Basic Script (VBScript), Microsoft Visual Basic, or Microsoft Visual C++. I'll concentrate on Visual Basic in this discussion. I recommend that you write your event handlers in this language unless your language of choice is Visual C++. We will look at building event handlers using Microsoft .NET later in this chapter.

I don't recommend writing event handlers in VBScript because it's harder to do and harder to debug. However, using scripts can be helpful at times. For example, if you do not have the permissions required to register Component Object Model (COM) components on the server, scripts might be a good alternative. Also, if you are used to the Exchange 5.5 model of writing events ”which is VBScript-based ”you will already be familiar with VBScript events in Exchange 2003. But use VBScript events as your last resort. As mentioned earlier, it's not as rich an experience to write and debug your event handlers in VBScript as in full-featured programming languages such as Visual C++.

The Firing Order of Events

When an item is saved into Exchange Server, the types of events are fired in a fixed order in the system. This firing order is important to understand because multiple entities can be working on the items in your folders, which can make it look as if your application isn't working. Synchronous events fire first, followed by any server-side folder rules you have created using Outlook or Outlook Web Access (OWA), and finally asynchronous events. As you might have guessed, these events follow one another. For instance, if your synchronous event aborts the item from being saved, the rules and asynchronous events will not be notified of the item. Furthermore, if any events that are higher up in the chain move or delete an item, the other events should be prepared to not have access to the item. For example (and this is just good coding practice), if your asynchronous events are expecting to open the item and some other synchronous event or folder rule has moved the item, you should have error code in your event handler to handle this situation.

System events, such as timer-based events or store startup/shutdown, fire only at specific times that you set or when you perform a certain action. For example, if you shut down Exchange, the shutdown event will fire so that you can capture that the Information Store service is shutting down.

Security Requirements

You should know about security requirements before you write your event handlers because security will affect how you register your event handlers and what user context your event handlers will run under. First, you must be a folder owner to register an event handler in a folder. Also, Exchange Server provides an extra security precaution in the form of the ICreateRegistration interface. This interface is called when you attempt to register a component to handle events in your folder. Using ICreateRegistration , the component developer can prohibit you from registering the event handler.

Second, if you're writing COM components to implement your event handlers, you must have the required access permissions to install components on the Exchange server. Exchange Server does not support instantiating and running remote components via Distributed COM (DCOM) as event handlers. Nothing can stop you from calling remote components or Web services in your event handler, but you must remember that the Exchange OLE DB (EXOLEDB) provider is not remotable ”the code has to run on the same server the data is on. You can, however, use DCOM to connect to another component residing on another server that accesses data on that server. Plus, for remote access you can use XML Web services or Web Distributed Authoring and Versioning (WebDAV), both discussed in earlier chapters, to get access to remote data from your event handlers. Getting the security contexts to support this is the hard part. If you use COM+ applications (which we'll talk about shortly), you can have your components run in a specific security context, which makes deployment easier.

Synchronous Events

Synchronous event handlers are called twice by Exchange Server. The first time, the event handler is called before the item is committed to the Exchange Server database. The second time the event handler is called, the item has been either committed or aborted. On the first pass, the item is read/write. You can modify properties or copy the item somewhere else. However, on the second pass, after the transaction has been committed, the item is read-only. Be aware that the item is not a true item in the database on the first pass. Because the item is not yet committed, you shouldn't grab any properties that might change in the future (between the time you access the item and the time it is committed). For example, the URL to the item is not valid on the first pass, nor is the EntryID, because the item is not yet in the database. Many factors can change the URL or EntryID, so you shouldn't query or save the item during the first pass.

In synchronous events, your event handler runs in the context of a local OLE DB transaction. Therefore, any changes you make to the item will not trigger other events. However, you must realize that the work performed in your event handler can be discarded if another event handler rejects the item from being committed. All the event handlers that act on an item in a folder must commit in order for the action to occur. If any event handler rejects the transaction, the action will not occur. If your event handler has already run, it will be called a second time and will be notified that the action has been aborted. Your event handler can then perform any necessary cleanup.

For example, let's say you have two event handlers registered in a folder for the OnSyncSave event, which is one of the synchronous events in Exchange. One of the event handlers opens the item and saves attachments to another location. The other event handler validates data in the item before allowing it to commit. Based on the priority you set for your event handlers, if the validation occurs after you copy the attachments, the validation might fail and the transaction for saving the item might be aborted. Now, any good developer would obviously make validation precede the other events. However, because multiple developers can register event handlers (as long as they meet the security requirements mentioned earlier), other event handlers can abort transactions. In this case, your event handler for copying the attachments will be notified that the transaction was aborted, and you should clean up your work by deleting the attachments from the other location.

You also should be aware that synchronous event handlers, while running, are the only processes that can access an item. Exchange Server blocks any other threads, processes, or applications from accessing that item while your event handler is running and working on it. This is critical because if you write an inefficient event handler, you can degrade the performance of other applications and Exchange Server. For example, if your event handler takes 10 seconds to run, each time an Outlook user saves an item in a folder that triggers your event handler, Outlook will show an hourglass for 10 seconds. Therefore, if you can use asynchronous events to implement your functionality, you should do so. If the Outlook scenario were to use an asynchronous event, Outlook would return immediately and allow the user to continue working. However, asynchronous events also have limiting factors, which I will cover shortly.

Synchronous events are expensive for Exchange Server to perform. The server must stop its processing on the item, call your event handler, wait, and then figure out whether to commit or abort the transaction for the item based on your event handler. All this creates temporary copies of the item before committing and forces the Exchange Server threads to wait, which affects performance.

Continuing with the Outlook scenario, if you abort the transaction, different clients will display different error messages. For example, Outlook will probably display an error message stating that the item couldn't be saved. A custom application will display just a general error that the operation failed. You cannot show user interface elements in your event handlers because they are running on the server. Instead, you must find a way to notify your users that they submitted the item incorrectly or that the action they're trying to perform is not allowed. You can perform this notification via e-mail or another method.

Exchange Server supports two synchronous events: OnSyncSave and OnSyncDelete . As you can guess from their names , these events support save and delete operations, respectively. However, both events are called as part of move and copy operations, too. For example, if you move an item from one folder to another, a save event will be fired in the new folder and a delete event will be fired in the old folder. If either is aborted, the move will not occur. With a copy, you get a save event in the location where the copy is supposed to be placed. You should know that the OnSyncDelete event is not called on an item when the item's parent folder is moved or deleted. Also, OnSyncDelete can distinguish between hard and soft deletes. A hard delete is a deletion in which the item is completely removed from the Exchange database. A soft delete is a deletion in which the item is moved into the dumpster. (An analogy would be the Deleted Items subfolder for every folder.) A user can recover an item from the dumpster using the interface in Outlook or OWA. You are notified of the deletion type by the flags that are passed to the OnSyncDelete event.

OnSyncSave is not called for items in a folder when the parent folder for the items is being moved or copied and an event registration exists for the parent folder. The event is called, however, for the parent folder; you can then abort the transaction if you don't want the items moved or copied.

You might be asking yourself, "If only save and delete are supported, how do I get notified of a change?" If a user makes a change to an item (such as modifying the subject or any property) and saves the item, you will receive the OnSyncSave event. The flags that are passed to the event will notify you that the item has been modified. However, you will not receive notification of which property the user changed in the item. You must scan the item to see what changed. To do this, you must have an original copy of the item, which you can obtain by copying the item in your event handler to another folder. I admit, though, that this approach isn't very practical if the number of copied items is large. In this situation, Microsoft SQL Server 2000 might provide a more suitable repository.

When you register your event handler, if you do not specify criteria for the types of items for which you want to receive events, Exchange Server will notify you of all new items being put into the folder. Folders store some surprising items that you might not expect to handle in your event handler. For example, when someone publishes a form or adds a field to a folder in Outlook, a hidden item is added to the folder. We discussed hidden items in folders in Chapter 11, when we discussed CDO 1.21 and how it can access hidden items. Hidden items trigger an event. You should set the criteria for your event registration (which we'll discuss later) so that only the items your event handler is interested in can trigger events.

Asynchronous Events

Exchange Server 2000 supports two asynchronous events, OnSave and OnDelete . These asynchronous events are called after a transaction has been committed to the database (such as items being created or deleted), and they fire in no particular order. Although these events are guaranteed to be called, another process or user might delete or move the item before the event handler even sees it. Exchange Server doesn't guarantee when it will call your event handler, but usually your event handler is called as soon as the item is committed to the Exchange Server database. Furthermore, as mentioned, if multiple asynchronous event handlers are registered for a single folder, Exchange does not guarantee the order in which the event handlers are called. Again, you should use asynchronous events rather than synchronous events whenever possible. If you need a guaranteed firing order, use synchronous events with the priority property, which we will discuss later in the chapter.

System Events

The three system events of Exchange Server are OnMDBStartup , OnMDBShutdown , and OnTimer . OnMDBStartup and OnMDBShutdown are called whenever an Exchange Server store starts or shuts down. This is useful for event handlers that want to scan the database or perform some sort of activity whenever the database starts or shuts down. Because these two events are asynchronous, Exchange Server doesn't wait for your event handler to finish before continuing execution of starting or shutting down Exchange.

The OnTimer event fires according to your configured parameters. For example, you can have a timer event fire every five minutes, daily, weekly, or monthly. It all depends on the requirements of your application. We'll look at how the sample Training application (introduced in Chapter 15) uses timer events for notification about new courses and student surveys for courses that already have taken place.

Registering an Event Handler

I'm going to go about this a little bit backward. Writing event handlers involves working with registration parameters, so I'll discuss the registration process first. That way, things will be clearer to you when we talk about writing actual event handlers later in this section.

Registering an event handler is quite easy. Exchange Server 2003 provides a script program called regevent.vbs, which allows you to pass some parameters to the program. The regevent.vbs program registers events of any event type you specify. Besides using regevent.vbs, you can create event registration items for your applications by simply creating new items in Exchange using ADO. In the setup program for the Training application, the three event handlers for the application are registered automatically using ADO. We'll look at the code for this registration at the end of this section.

Event Registration Properties

When you register events, you must set some criteria to tell Exchange Server what events you're interested in, what the ProgID or script location of the event handler is, and so on. Table 17-1 shows the criteria to register an event handler. All these properties are contained in the http://schemas.microsoft.com/exchange/events/ namespace.

Table 17-1: Criteria Required by Exchange Server 2003 for Registering an Event Handler

Property

Required?

criteria

No

enabled

No

eventmethod

Yes

matchscope

No

priority

No

scripturl

Yes (for script event handlers only)

sinkclass

Yes

timerexpirytime

No (for timer events only)

timerinterval

Yes (for timer events only)

timerstarttime

Yes (for timer events only)

criteria property     The criteria property allows you to specify a SQL WHERE clause that will act as a filter for your event handler so that the handler is called only when items meet your criteria. This property allows you to avoid being called for items that you're not interested in. For example, the Training application uses the following criteria so that it doesn't get called when hidden items or folders are created:

 WHERE "DAV:ishidden" = false AND "DAV:isfolder" = false 

You can use AND , OR , NOT , or EXISTS as part of your WHERE clause. CONTAINS and FREETEXT are not supported, however. Also, if you plan to check custom schemas, you must explicitly cast your custom property to the right data type. For example, if you want to make sure that your event handler is called only in an application in which a property on items submitted is greater than 100, you set the criteria property for your event registration to the following value:

 "WHERE CAST($"MySchema/MyNumber"$ AS 'i4') > 100" 

Notice that the $ character is used to avoid using double quotation marks.

Note  

For the OnDelete asynchronous event, you cannot use the criteria property because OnDelete events are fired for all items.

enabled property     The enabled property is a Boolean property that allows you to specify whether your event handler is enabled. Rather than deleting an event registration, if you plan to reuse it in the future, you can set this property to False .

eventmethod property     The eventmethod property is a multi-value string that specifies the types of events you are interested in receiving, such as OnSyncSave and OnDelete . You can register for event methods of the same type within the same event registration. For example, one event registration can be used for OnSyncSave , OnSyncDelete , OnSave , or OnDelete but cannot include OnTimer . You must register OnTimer and the other system events separately. However, your event handler COM component can implement the interfaces for all the events.

matchscope property     The matchscope property allows you to specify the scope of the event. The value for this property can be any , fldonly , deep , exact , or shallow . You use only the value any with database-wide events. The scope of the exact value is a specific item. This is similar to shallow , which fires for items only in the exact folder you specify. The fldonly value will notify you only of changes to the folder itself, such as modifications to a property on the folder. The deep value notifies you of changes in the current folder as well as any items in subfolders . If you set the property to deep , even new subfolders and items created in them will trigger your event handler. However, keep in mind that system events do not support this property.

priority property     The priority property is an integer property that indicates the priority of your event handler compared with other event handlers. The number can range from to 0xFFFFFFFF . By default, your event handler is registered with a value of 0x0000FFFF (65,535) . This property is valid only for synchronous events and tells the system the order in which you want these events to fire. If you give two synchronous events the same priority, it is undetermined which one will get called first. When you register your event handlers, you might want to check to see whether other event registrations exist in the folder. If they do, check their priority before registering your event handler.

scripturl property     When you write script for your event handler, the scripturl property holds the URL to the script file. Exchange Server supports the file:// and http:// URL formats in this property. The script file can reside in a folder in Exchange Server or in another location accessible via a URL, as long as the location is on the same machine as the Exchange server. Note that when you use script handlers, you must specify ExOleDB.ScriptEventSink for the sinkclass property in addition to filling out the scripturl property. Remember, though, that script event handlers should be your last resort.

sinkclass property     The sinkclass property holds the CLSID or the ProgID of your event handler. Exchange Server then instantiates the object when an event is triggered. By default, Exchange Server caches your object so it doesn't have to instantiate the object multiple times.

timerstarttime property     The timerstarttime property specifies the date and time to start notifying your event handler of OnTimer events. If you do not specify this property, Exchange Server will start notifying your handler immediately. Note that this value can be affected by Exchange storing date and time values as Universal Time Coordinate (UTC) values. You should therefore set the UTC time, not the local time, when you want your timer event to start firing. For example, the Training application setup program must create the event registration for survey notifications. The survey notification event handler should be called at 10 P.M. local time every night. To create the correct timerstarttime property value, the setup program has to figure out what 10 P.M. local time is in UTC. Then it must register to be notified at the correct UTC time, which Exchange will convert to 10 P.M. local time for the server machine's time zone.

timerexpirytime property     The timerexpirytime property is an integer property that specifies the number of minutes after the timerstarttime property that the event handler should stop receiving OnTimer notifications. If you don't specify this property, your event handler will never stop receiving notifications. Obviously, this property is valid only for OnTimer event registrations.

timerinterval property     The timerinterval property is an integer property that specifies the number of minutes to wait to notify your event handler of another OnTimer event. If you do not set this property, Exchange Server will call your event handler only once after the creation of your registration item for the OnTimer event.

Note  

Before you dive into creating the event registration item, note that events will not fire on a user's outbox or sent items folders. Even if you register global events or events directly on either of these folders, events will still not fire. If you want to put a footer on every mail message, which is the main reason people try to put an event on the Outbox , use a transport event handler instead. See Microsoft Knowledge Base article 297274 for more information on this limitation.

Creating an Event Registration Item

The easiest way to register your event handler with Exchange Server is to use ADO to create an event registration item in the Exchange Server database and set the properties we just discussed on that item. Be aware that when you create your event registration item, you should do it in the context of an OLE DB transaction. Why? Because Exchange Server uses an asynchronous event handler to listen for and track your requests for registering an event handler. The built-in Exchange Server event registration event handler looks for items with a special content class, urn:content-class:storeeventreg . The event handler then takes those items, scans the properties, and performs its magic, making the items valid event registrations. This magic includes turning the item into an invisible message item in the folder by making it part of the associated contents table. This table is the same place that views, forms, and rules are stored in.

If you don't use OLE DB transactions, you might trigger the built-in Exchange Server event handler when you first attempt to create your registration item using ADO and set the DAV:contentclass property to urn:content-class:storeeventreg . If this happens and you haven't yet set the properties for your registration item, the event handler will think that your event registration item is invalid. By using an OLE DB transaction and atomically creating and setting your properties at the same time, you avoid this problem.

The following code, taken from the setup program for the Training application, shows how easy it is to create an event registration item:

 . . .      'Create the Survey Notification Event Registration     'Timer event     strNow = Now     arrRequired = GenerateRequiredEventArray("", "ontimer", _         "EventSink.SurveyNotify", "", "" )     arrOptional = GenerateOptionalEventArray("", "", "", "", _         1440, strNow, "")     CreateEvtRegistration oConnection, strPath & _         "Schedule/surveynotification", arrRequired, arrOptional, False      . . .      'Event Registration Helper Sub Sub CreateEvtRegistration(cn, strEventRegPath, arrRequiredParameters, _                           Optional arrOptionalParameters, Optional bWorkflow)                           On Error GoTo errHandler          'Create the event registration.     'cn - Connection to Exchange Server database for transaction purposes     'strEventRegPath - Full file path to the event item     'arrRequiredParameters - Required parameters for all event registrations     'arrOptionalParameters - Optional parameters such as criteria          Const propcontentclass = "DAV:contentclass"     Const propScope = "http://schemas.microsoft.com/exchange/events/Scope"     Dim propname As String          Dim rEvent As New ADODB.Record          cn.BeginTrans     rEvent.Open strEventRegPath, cn, 3, _         adReadWrite + adCreateOverwrite + adCreateNonCollection          'Set the properties in the item     With rEvent.Fields         .Item(propcontentclass) = "urn:content-class:storeeventreg"              'Scroll through and commit required parameters.         'Scroll through and commit optional parameters.         If IsArray(arrRequiredParameters) Then             For i = LBound(arrRequiredParameters, 1) To _                                              UBound(arrRequiredParameters, 1)                 'Use Dimension 1 since the second dimension should                 'always be the same                 If Not (IsEmpty(arrRequiredParameters(i, 0))) Then                     .Item(arrRequiredParameters(i, 0)) = _                                              arrRequiredParameters(i, 1)                 End If             Next         End If              If IsArray(arrOptionalParameters) Then             For i = LBound(arrOptionalParameters, 1) To _                                              UBound(arrOptionalParameters, 1)                  'Use Dimension 1 since the second dimension should always                  'be the same                 If Not (IsEmpty(arrOptionalParameters(i, 0))) Then                     .Item(arrOptionalParameters(i, 0)) = _                                              arrOptionalParameters(i, 1)                 End If             Next         End If              'Add custom properties that the event sink can use to         'determine the context of this event registration              If bWorkflow = False Then             strConfigurationFolderPath = strPath & "Configuration/"              'Hard-code this one property so that we can always find it!             propname = "http://thomriz.com/schema/configurationfolderpath"              'For a case switch in the event sink             .Append propname, adVariant, , , strConfigurationFolderPath         End If              .Update  'Get the ADO object current          End With     cn.CommitTrans          Exit Sub      errHandler:     MsgBox "Error in CreateEvtRegistration.  Error " & Err.Number & " " _            & Err.Description     End End Sub      Function GenerateRequiredEventArray(strCriteria, strEventMethod, _                                     strSinkClass, strScriptURL, strSinkList)          Const propCriteria = _         "http://schemas.microsoft.com/exchange/events/Criteria"     Const propEventMethod = _          "http://schemas.microsoft.com/exchange/events/EventMethod"     Const propSinkClass = _          "http://schemas.microsoft.com/exchange/events/SinkClass"          Const propScriptURL = _          "http://schemas.microsoft.com/exchange/events/ScriptUrl"     Const propSinkList = _          "http://schemas.microsoft.com/exchange/events/SinkList"          'Generate the array by checking the passed arguments.     'Note dynamic arrays only support redimensioning the last dimension.     'This causes a problem, so dimension an array and fill in blanks if     'necessary.     'Flip-flop value - propname     Dim arrRequired(4, 1)     iArrayCount = 0     If strCriteria <> "" Then         arrRequired(iArrayCount, 0) = propCriteria         arrRequired(iArrayCount, 1) = strCriteria         iArrayCount = iArrayCount + 1     End If     If strEventMethod <> "" Then         arrRequired(iArrayCount, 0) = propEventMethod         arrRequired(iArrayCount, 1) = strEventMethod         iArrayCount = iArrayCount + 1     End If     If strSinkClass <> "" Then         arrRequired(iArrayCount, 0) = propSinkClass         arrRequired(iArrayCount, 1) = strSinkClass         iArrayCount = iArrayCount + 1     End If     If strScriptURL <> "" Then         arrRequired(iArrayCount, 0) = propScriptURL         arrRequired(iArrayCount, 1) = strScriptURL         iArrayCount = iArrayCount + 1     End If     If strSinkList <> "" Then         arrRequired(iArrayCount, 0) = propSinkList         arrRequired(iArrayCount, 1) = strSinkList         iArrayCount = iArrayCount + 1     End If     GenerateRequiredEventArray = arrRequired      End Function      Function GenerateOptionalEventArray(bEnabled, strMatchScope, lPriority, _                                     bReplicateReg, iTimerInterval, _                                     iTimerStart, iTimerStop)          Const propEnabled = _          "http://schemas.microsoft.com/exchange/events/Enabled"     Const propMatchScope = _          "http://schemas.microsoft.com/exchange/events/MatchScope"     Const propPriority = _         "http://schemas.microsoft.com/exchange/events/Priority"     Const propReplicateEventReg = _         "http://schemas.microsoft.com/exchange/events/ReplicateEventReg"     Const propTimerInterval = _         "http://schemas.microsoft.com/exchange/events/TimerInterval"     Const propTimerStartTime = _         "http://schemas.microsoft.com/exchange/events/TimerStartTime"     Const propTimerExpiryTime = _         "http://schemas.microsoft.com/exchange/events/TimerExpiryTime"          Dim arrOptional(6, 1)     iArrayCount = 0     If bEnabled <> "" Then         arrOptional(iArrayCount, 0) = propEnabled         arrOptional(iArrayCount, 1) = bEnabled         iArrayCount = iArrayCount + 1     End If     If strMatchScope <> "" Then         arrOptional(iArrayCount, 0) = propMatchScope         arrOptional(iArrayCount, 1) = strMatchScope         iArrayCount = iArrayCount + 1     End If     If lPriority <> "" Then         arrOptional(iArrayCount, 0) = propPriority         arrOptional(iArrayCount, 1) = lPriority         iArrayCount = iArrayCount + 1     End If     If bReplicateReg <> "" Then         arrOptional(iArrayCount, 0) = propReplicateEventReg         arrOptional(iArrayCount, 1) = bReplicateReg         iArrayCount = iArrayCount + 1     End If     If iTimerInterval <> "" Then         arrOptional(iArrayCount, 0) = propTimerInterval         arrOptional(iArrayCount, 1) = iTimerInterval         iArrayCount = iArrayCount + 1     End If     If iTimerStart <> "" Then         arrOptional(iArrayCount, 0) = propTimerStartTime         arrOptional(iArrayCount, 1) = iTimerStart         iArrayCount = iArrayCount + 1     End If     If iTimerStop <> "" Then         arrOptional(iArrayCount, 0) = propTimerStopTime         arrOptional(iArrayCount, 1) = iTimerStop         iArrayCount = iArrayCount + 1     End If          GenerateOptionalEventArray = arrOptional      End Function 

As you can see in the code, ADO is used to create an event registration item. The Fields collection is used to fill in the properties needed to make the item a valid event registration, and the item is saved to Exchange Server. Although this application uses events in public folders, you can register and fire events from private folders as well. You need the proper security for your event handler to access the user's information and manipulate that information (if necessary).

You can also use WebDAV to create event registration items. To download a sample that does this, see Microsoft Knowledge Base article 306046.

Note  

Terminal Server does not work with event registrations. This means you cannot use Terminal Server to remotely connect to your Exchange Server and register an event. Instead, you should either run your registration script directly on the server, use Exchange Explorer (which is part of the Exchange SDK) to register your event handler, or use WebDAV to remotely create an event registration.

Registering a Database-Wide Event

In addition to scoping your event handlers to just one folder, you can scope them to the entire store or messaging database (MDB). However, you cannot scope your event handler to the entire server (all MDBs at once). All types of event registrations are per individual MDB only. To create an MDB-wide event registration, you must change the value for the matchscope property and the location in which you put your registration item. Note that only synchronous events such as OnSyncSave and OnSyncDelete are supported in global events. If you attempt to register asynchronous events as global, the registration will fail. Also, note that global events will not fire on the sent items or outbox folders in user's mailboxes, so unfortunately you can't process outgoing messages in this way.

You should specify any as the value of the matchscope property to indicate that any scope is valid for notifying your event handler. All other properties can stay the same, based on the type of event you're registering for.

As just mentioned, the location in which you put the registration item will change. Instead of throwing the item into the folder where you want the scope of the event notifications to begin, you must place the registration item in the GlobalEvents folder. This folder is located in Public Folders in the non-IPM subtree in a folder called StoreEvents{ <MDBGUID> }, where <MDBGUID> is the unique identifier for the MDB. The easiest way to retrieve the Globally Unique Identifier (GUID) of the MDB is to use the StoreGUIDFromURL method in EXOLEDB. The following code shows how to use this method:

 set oStoreGUID=CreateObject("Exoledb.StoreGuidFromUrl") 'Get the GUID strguid = oStoreGUID.StoreGuidFromUrl("<Path to GlobalEvents folder>") 

A valid path to save your registration item to would look something like this:

 file://./backofficestorage/ADMIN/domain/public folders/non_ipm_subtree/ StoreEvents{915c615a-6353-4d1d-9ff2-8bd0e3f54bcd}/GlobalEvents/      File://./backofficestorage/ADMIN/domain/MBX /SystemMailbox{19bb5c7c-3904-4581-9e3c-32e3945881de}/StoreEvents/GlobalEvents 

Only the Administrator account, which is a member of the Domain Administrators group, or users in the Exchange Administrators role in Active Directory can register global events. It is not enough to be a member of the Administrators group or the Exchange Servers group (due to heightened security around global events and the impact they can have on system performance). Use global events sparingly because every action in the Exchange database where a global event is registered will fire an event and call your handler.

Note  

For more information on database-wide events, see Microsoft Knowledge Base article 306989.

Using the ICreateRegistration Interface

If you're writing event handlers that you think other developers will register for, or if you want to protect your application event handlers, you can implement the ICreateRegistration interface. For example, if you write your event handler and save it on a server and you think other users will try to create event registrations in their own applications that will use your event handler object, you should consider using the ICreateRegistration interface. This interface is called whenever a user tries to register for your event handler. ICreateRegistration passes you the event registration information, including the registration item for the particular user. You can grab information from this item, or you can retrieve from the item information about the user who's trying to register for your event handler. You can then decide whether to permit or reject the user's registration.

To implement this interface, you must add an Implements ICreateRegistration line to your Visual Basic code and put your validation code in the ICreateRegistration_Register function.

Writing an Event Handler

The code samples containing event handlers that you'll see in this section are taken from the Training application. All these event handlers are written in Visual Basic. When you write Visual Basic event handlers, you first should create an ActiveX DLL. In addition, be sure to add references to the various object libraries your application might need to access. You will definitely need a reference to the EXOLEDB type library; libraries such as ADO 2.5 and later and Microsoft Collaboration Data Objects (CDO) for Exchange Server also might come in handy.

Once you add the references, you must use the Implements keyword. Depending on the type of event handler you plan to write, you'll need to implement the IExStoreSyncEvents , IExStoreAsyncEvents , or IExStoreSystemEvents interface. You can implement all three in a single DLL if you want.

Next you must implement the subroutines for the events you're interested in. The following code, taken from the course notification event handler, shows the OnSave and OnTimer events being implemented. I won't list all the code for the implementation of these two events because I mainly want to show you the parameters that are passed your functions.

 Implements IExStoreAsyncEvents Implements IExStoreSystemEvents Const strHTMLMsgSubject = "New Course Email"      Dim oEventRegistration        'Global that holds the event                               'registration record Dim bShowPreviousDay          'Global that holds whether to show                               'new courses only                               'From previous day, just in case timer                               'event runs after midnight Dim oRecord As ADODB.Record   'Global record to hold item that                               'triggered event Dim strHTMLBody               'Global string that holds HTML message body      'Add Discussion group, file, and http link notification as part of 'the message. 'Update HTML message to incorporate file and http link, as well as 'discussion group.      Private Sub IExStoreAsyncEvents_OnDelete(ByVal pEventInfo As _                                          Exoledb.IExStoreEventInfo, _                                          ByVal bstrURLItem As String, _                                          ByVal lFlags As Long)     'Not implemented End Sub      Private Sub IExStoreAsyncEvents_OnSave(ByVal pEventInfo As _                                         Exoledb.IExStoreEventInfo, _                                         ByVal bstrURLItem As String, _                                         ByVal lFlags As Long)     If (lFlags And EVT_NEW_ITEM) > 0 Then         'New item put in.         'Get the ADO Record object for the item.         'Set oRecord = dispInfo.EventRecord         Set oRecord = CreateObject("ADODB.Record")         oRecord.Open bstrURLItem         'Check to see whether the item happened very recently.         'If it did, just exit since the training event is         'probably having its survey information updated by         'a user. Don't notify people of old training events.         If DateDiff("d", Now, _                 oRecord.Fields("urn:schemas:calendar:dtstart").Value) > 0 Then             'It's OK; event happens in future.             'Load the global settings.             LoadAppSettings oRecord, pEventInfo             If bEventLogging Then                 App.LogEvent "Event Notification OnSave event called for " _                              & "training event.  Path: " & bstrURLItem             End If             strCategory = GetCourseCategory(oRecord)             QueryPreferences strCategory         End If     End If End Sub      Private Sub IExStoreSystemEvents_OnMDBShutDown(ByVal bstrMDBGuid As String, _                                                ByVal lFlags As Long)     'Not implemented End Sub      Private Sub IExStoreSystemEvents_OnMDBStartUp(ByVal bstrMDBGuid As String, _                                               ByVal bstrMDBName As String, _                                               ByVal lFlags As Long)     'Not implemented End Sub      Private Sub IExStoreSystemEvents_OnTimer(ByVal bstrURLItem As String, _                                          ByVal lFlags As Long)     On Error Resume Next          Dim rec As ADODB.Record     Dim rst As ADODB.RecordSet     Dim conn As ADODB.Connection          Set oBindingRecord = Nothing     'Get the registration     Set oBindingRecord = CreateObject("ADODB.Record")     oBindingRecord.Open bstrURLItem          On Error GoTo 0     If Err.Number = 0 Then         'Could retrieve the item         LoadAppSettings oBindingRecord              If bEventLogging Then             App.LogEvent "Event Notification: OnTimer event called at " & Now         End If         'Get the folder in which the timer is running.         'This is never used since we know the folder already.         'However, this shows you how to retrieve the folder if you need to.         strFolder = oBindingRecord.Fields.Item("DAV:parentname")         'Figure out all the courses created in the current 24 hours or         'previous 24 hours         curDate = Date         If bShowPreviousDay Then             'Subtract a day             curDate = DateAdd("d", -1, curDate)         End If         dISODateStart = TurnintoISO(curDate, "Start")         dISODateEnd = TurnintoISO(curDate, "End")              strSQL = "Select ""urn:schemas:mailheader:subject"", " _                & """DAV:href"",""urn:schemas:calendar:dtstart"", " _                & """urn:schemas:calendar:dtend"" FROM scope('shallow " _                & "traversal of """ & strScheduleFolderPath _                & """') WHERE (""DAV:iscollection"" = false) AND " _                & "(""DAV:ishidden"" = false) " _                & "AND (""urn:schemas:calendar:dtstart"" >= CAST(""" _                & dISODateStart & """ as 'dateTime'))" _                & "AND (""urn:schemas:calendar:dtstart"" <= CAST(""" _                & dISODateEnd & """ as 'dateTime'))"              Set conn = CreateObject("ADODB.Connection")         Set rst = CreateObject("ADODB.RecordSet")         Set oRecord = CreateObject("ADODB.Record")              With conn             .Provider = "EXOLEDB.Datasource"             .Open strScheduleFolderPath         End With              'Create a new RecordSet object              With rst             'Open RecordSet based on the SQL string             .Open strSQL, conn, adOpenKeyset         End With              Dim iAppt As CDO.Appointment              If Not (rst.BOF And rst.EOF) Then             Set iAppt = CreateObject("CDO.Appointment")             'On Error Resume Next             rst.MoveFirst             Do Until rst.EOF                 'Set oRecord to the current item in the RecordSet                 On Error Resume Next                 oRecord.Close                 Err.Clear                 On Error GoTo 0                 oRecord.Open rst.Fields("DAV:href").Value, conn                 strCategory = GetCourseCategory(oRecord)                 QueryPreferences strCategory                      rst.MoveNext             Loop             rst.Close                  Set iAppt = Nothing             Set rst = Nothing             Set rec = Nothing             Set conn = Nothing         Else             If bEventLogging Then                 App.LogEvent "Event Notification: No students need to be " _                            & "notified of training event."             End If         End If     End If End Sub 

As you can see in the code, your application is passed different parameters for the different events. However, all events have some common parameters. For example, you are always passed a URL to the item that triggered the event. For system events such as OnTimer , this is the event registration item itself. If you need to, you can add custom properties to the event registration item so that when your OnTimer event handler is called, you can retrieve that custom property.

I use this trick in the Training application. When I register the OnTimer event handler for the application's workflow process, I add a custom property that is the full URL to the application's configuration folder. Because you can customize where the application is installed, you must tell the workflow process where to look for the customization information that you select during setup. Because I've added an extra property, I can grab it in my workflow code, get the configuration message contained in the folder the property specifies, and determine the value for the customized fields in the application, such as which e-mail address to send notification messages from. We discussed custom properties in Chapter 15.

For nontimer events, the URL you receive is the path to the item that's triggering the event. You can then retrieve the item and look at its properties.

An event handler also receives a parameter called lFlags . This parameter corresponds to flags that tell you exactly what's happening to the item. For example, one of the flags, EVT_NEW_ITEM , tells you that the item triggering the event is a new item rather than an item that already existed in the folder and had some properties changed. You should use a bitwise AND with the lFlags parameter and one of the identifiers from Table 17-2 to determine the values of flags in the lFlags parameter. The flag names are included in the EXOLEDB type library, so you will want to add a reference to this library and use the friendly names of the flags from this library.

Note  

Not all flags are supported by all events. For example, the delete flags are supported only by the delete events.

When you use server events in addition to the flags and the URL, you receive a pEventInfo variable of type Exoledb.IExStoreEventInfo . You should set this variable to another variable of type Exoledb.IExStoreDispEventInfo , which is the IDispatch version of the interface. From this interface, you can control the transaction state for the event handler for synchronous events and get more information about the context of the event. Table 17-3 lists the elements you retrieve and call on the IExStoreDispEventInfo interface. Also check out the Exchange SDK for more information on these parameters.

Table 17-2: Flag Values for the lFlags Parameter

Flag

Description

EVT_NEW_ITEM

The item being saved is new rather than an existing, modified item.

EVT_IS_COLLECTION

The item being saved is a collection (folder).

EVT_REPLICATED_ITEM

The item being saved is the result of a replication event, which occurs when the item is replicated using Exchange Server replication.

EVT_IS_DELIVERED

The item being saved is the result of a message delivery.

EVT_SOFTDELETE

A soft delete has occurred. (The item has been moved to the dumpster.)

EVT_HARDDELETE

A hard delete has occurred. (The item has been removed from the store.)

EVT_INITNEW

This is the first time the event handler has been called, so you can perform any necessary initialization procedures. Your event handler is passed this flag only once during its lifetime.

EVT_MOVE

The event is the result of a move operation.

EVT_COPY

The event is the result of a copy operation.

EVT_SYNC_BEGIN

The event handler is being called in the begin phase of a synchronous event. This is the point at which you can abort the item being saved into the database. Also, the URL you receive for the item is invalid at this point because the item is not yet in the database.

EVT_SYNC_COMMITTED

The event handler is being called in the commit phase of a synchronous event, after the transaction has been committed to the database.

EVT_SYNC_ABORTED

A synchronous event has been aborted.

EVT_INVALID_URL

The URL passed to the event handler is invalid.

EVT_INVALID_SOURCE_URL

The source URL could not be obtained during a move operation.

EVT_ERROR

Some error occurred in the event.

Table 17-3: Elements of the IExStoreDispEventInfo Interface

Element

Description

AbortChange

Aborts the transaction in which the event is currently executing. Exchange Server will not commit the item to its database. You pass a long value, which specifies the error code you want returned.

EventBinding

Returns as an object the registration item for the event handler. You can use the returned object to retrieve the custom properties that you set on the event registration item.

EventConnection

Specifies the ADO Connection object under which the event is executing.

EventRecord

Specifies the ADO Record object bound to the item that triggered the event.

SourceURL

Specifies the original source URL in an OnSyncSave event. This property is valid only for a move operation.

StoreGUID

Specifies the GUID for the MDB where the item that triggered the event is located.

UserGUID

Specifies the GUID for the user that triggered the event. You can use this in conjunction with the Win32 APIs to look up the user's name .

UserSID

Specifies the Security Identifier (SID) of the user who triggered the event.

Data

This property allows you to save in-memory data between the begin and commit or abort phase of a synchronous event. This property is useful for passing to your event sink the variables between the different calls in a transaction; you don't have to save those variables to Exchange or to disk.

start sidebar
Creating COM+ Applications

If you plan to write your event handlers in Visual Basic, you should definitely install your DLLs as COM+ applications. There are a number of reasons for doing this. First, Exchange Server won't let you try to run an event handler for synchronous events that are not COM+ enabled. Second, if your event handler has problems, wrapping it into a COM+ application isolates it and allows you to shut it down without affecting other parts of the system. Finally, by making your DLLs COM+ applications, you can take advantage of COM+ roles and security.

This last point is key ”it allows you to retrieve the SID of the user who is triggering the event. Using the COM+ Services Type Library and the SID, you can employ COM+ roles-based security. For example, you can use the IsSecurityEnabled function to check whether COM+ security is enabled. You can also use the IsUserInRole function to see whether the user is in a particular role. For more information on COM+ security issues, refer to the Platform SDK on the Microsoft Developer Network (MSDN) at http://msdn.microsoft.com .

The Training application places its event handlers into a COM+ application. The application's setup program deploys the COM+ application with the built-in COM+ deployment tools. When you export your COM+ application from the COM+ user interface, as shown in Figure 17-1, COM+ creates a Windows Installer file. You can then run this file if you want to install the COM+ application on your computer.

click to expand
Figure 17-1: Exporting the COM+ application for the Training sample
end sidebar
 

Advanced Information for Your Event Handlers

In your event handlers, you might want to work with some advanced information, such as security information, provided by the EventInfo structure, such as UserGUID and UserSID . To do so, you must know some special information about Active Directory and ways to work with GUID structures in Visual Basic. The value of working with these properties is that in your event handlers, you can look up the user who is firing the event in Active Directory to pull out their username or e-mail address. You can also obtain group membership information. Groups are especially useful if you want to allow only certain users to perform certain actions in your application. Please note that some properties, such as UserGUID and UserSID , are available only from synchronous events.

When you work with the UserGUID and UserSID properties, the first thing to know is that these properties are provided only with synchronous events, not asynchronous events. What is actually returned to you with the UserGUID property is a string containing a GUID:

 {F4CFFA65-82C2-46C4-994D-B6FAF28B10D3} 

This is a 32-character (not including the dashes or brackets) string of hex numbers . You might think that all you need to do to find the user who owns this GUID in Active Directory is remove the extra characters (such as dashes and brackets) and then just run a query against Active Directory. Unfortunately, this is not the case. You might also assume that the objectGUID property in Active Directory is the property that maps to the UserGUID property that is passed to you. Again, this is not true. The steps you need to go through to find the user might seem complex at first, but after you implement them once, you'll find that they are easy to carry out. Plus, you can reuse this code after the first implementation.

The first step in the process is to realize that the value for UserGUID actually maps to the msExchMailboxGuid attribute in Active Directory, not the objectGUID attribute. Figure 17-2 shows what Active Directory contains for a sample msExchMailboxGuid using the Active Directory Services Interface (ADSI) Edit tool, which we discussed in Chapter 13 and which is available on the Windows Server CD as part of the support tools.

click to expand
Figure 17-2: ADSI Edit showing the value for a sample msExchMailboxGuid

The numbers in the figure are shown here with the 0x removed from each number:

 65FACFF4C282C446994DB6FAF28B10D3 

You might wonder how the first number we saw earlier relates to this number. If you were to take the first number that the event handler passes to you and try to query Active Directory using this number against msExchMailboxGuid , you would get no results from the query because the two numbers are ordered completely differently. If you look closely at the two numbers, they are the same characters, but in a different order.

Changing the order of the UserGUID property is, as you can guess, the second step. You strip all the brackets and dashes from the string, and then you walk eight characters into the string because the first eight characters must be reversed in two-character pairs. For the example we're using, F4CFFA65 must become 65FACFF4 , which is the first eight characters of the property from the Active Directory value. Then the same process must be done for the next four characters and the next four characters after that. The key thing is that you cannot flip all eight characters at once because you need to keep the ordering of the characters intact to match the GUID in Active Directory. For example, 82C246C4 needs to become C282C446 . Finally, you need to append the remaining 16 characters in their existing order.

This is a pretty easy process, but if you did not know that you need to do this to get the right string format for your GUID, you'd get very frustrated trying to figure out how to work with the UserGUID property to find out which user triggered your event. To help you with this, I've taken all these steps and created some simple Visual Basic functions that do this work for you. All you do is pass the UserGUID value to this function, and it will pull out the dashes and brackets from this value, perform the steps we just discussed, and return the correctly formatted GUID string for you to use to query against Active Directory:

 Function ReverseGUID(strGUID)     strnewGUID = strGUID     'Take away the { and - from the GUID     strnewGUID = Replace(strnewGUID, "{", "")     strnewGUID = Replace(strnewGUID, "-", "")     strnewGUID = Replace(strnewGUID, "}", "")          'Count in 8 characters to the middle of the 32 character string     strTempGUID = Left(strnewGUID, 8)          'Begin to flip around the GUID by pulling out     '2 character pairs in reverse order     strFirstEightGUID = ""     strFirstEightGUID = GetGUIDinReverseOrder(strTempGUID)          'Count in character from character 9 another 4     strTempGUID = Mid(strnewGUID, 9, 4)          'Flip around this     strMiddleFourGUID = ""     strMiddleFourGUID = GetGUIDinReverseOrder(strTempGUID)          'Get the last four and flip them     strTempGUID = Mid(strnewGUID, 13, 4)     strLastFourGUID = ""     strLastFourGUID = GetGUIDinReverseOrder(strTempGUID)          'Get the last 16 characters in the GUID     strOriginalSixteenGUID = Mid(strnewGUID, 17, 16)          'Combine to create the new GUID     strnewGUID = strFirstEightGUID & strMiddleFourGUID _                & strLastFourGUID & strOriginalSixteenGUID          'Return back the value     ReverseGUID = strnewGUID End Function      Function GetGUIDinReverseOrder(strTempGUID)     strTmp = ""     'Take the GUID and start parsing from the back     'to the front in 2 character pairs     For i = (Len(strTempGUID) - 1) To 1 Step -2         strTmp = strTmp & Mid(strTempGUID, i, 2)     Next     GetGUIDinReverseOrder = strTmp End Function 

Now that we have the correctly formatted GUID, we need to query Active Directory. In Chapter 13, you learned how to use ADO with the ADsDSOObject provider. We'll use this same provider to perform our GUID search against Active Directory. Another interesting twist here is that when you work with GUIDs and Active Directory, to query for a GUID by passing a string, you must prepend a slash (\) before every two-character pair in your string that represents the GUID. So, for our example, our string 65FACFF4C282C446994DB6FAF28B10D3 needs to become \65\FA\CF\F4\C2\82\C4\46\99\4D\B6\FA\F2\8B\10\D3 . Again, I have a Visual Basic function for you that makes this easy. The following code adds the slashes to our GUID string and then queries Active Directory using ADSI to find the user whose msExchMailboxGuid matches the string we're passing in:

 Function FindByGUID(strGUID, domain)     'Find the user's object     Dim usr     Set con = CreateObject("ADODB.Connection")     con.provider = "ADsDSOObject"     con.Open "ADs Provider"     'Take the strGUID and every 2 spaces add a \     strGUID = AddSlashes(strGUID)     strSQL = "<LDAP://" & domain _            & ">;(&(objectClass=user)(msExchMailboxGuid=" _            & strGUID & "));cn,adspath,userPrincipalName;subtree"     Set rs = con.Execute(strSQL)     If rs.RecordCount > 1 Or rs.RecordCount = 0 Then         'Return back nothing         Set FindByGUID = Nothing         Exit Function     End If     Do While Not rs.EOF         Set usr = GetObject(rs.Fields("AdsPath").Value)         rs.MoveNext     Loop     Set FindByGUID = usr End Function      Function AddSlashes(strGUID)     'GUID should be 32 characters     iNumofSlashes = (Len(strGUID) \ 2)     'Add the slashes     For i = 1 To (Len(strGUID) + iNumofSlashes) Step 3         strGUID = Left(strGUID, i - 1) & "\" & Mid(strGUID, i)     Next     AddSlashes = strGUID End Function 

The function FindByGUID takes the string GUID and the fully qualified domain name that you want to search. If it successfully finds one user that matches that GUID, it will return the corresponding IADs object that corresponds to the type of object the query matches. For users, this object will be the IADsUser object. From the IADsUser object, you can use the cn or mail property to return the username or e-mail address of the user causing the event. If you are interested in checking group membership information, use the IADsUser object's Groups method to obtain a collection of ADSI group objects to which this user belongs.

Besides using the GUID to identify the user in Active Directory, you can also use the SID passed to you in the UserSID property. The UserSID property has its own set of quirks when you work from Visual Basic, just as the UserGUID property does. The UserSID property that is passed to your event handler is a variant array of bytes. This array of bytes is a set of decimal numbers that make up the user's SID. If you use ADSI Edit and look at the objectSID property that the UserSID corresponds to, you will see the following value with the extra characters removed:

 010500000000000515000000be043e32e7cbdd7da837d66558040000 

If you take the array values for the UserSID property and just combine the values together, you will get the following string:

 1500000521000190462502312032211251685521410188400 

As you can see, our version of the SID is very different from and shorter than the version in Active Directory. What we actually need to do is take the values in the array returned by Exchange, pad values that are less than 16 (or hex 10) with a leading zero, and then convert the value into hex. Then the SID we create will exactly match the SID in Active Directory. The following function performs these steps:

 Function GetSID(arrSID)     'The SID is provided as an array of values, put into a useful     'version for ADSI     strSID = ""     For i = LBound(arrSID) To UBound(arrSID)         If arrSID(i) < 16 Then             'Need to pad a 0 in front of the hex #             strPAD = "0"         Else             strPAD = ""         End If         strSID = strSID & strPAD & CStr(Hex(arrSID(i)))     Next     GetSID = strSID End Function 

The next step before we can query Active Directory is to add a slash before every two characters in the string so our query will work. We can use the same AddSlashes function that you saw earlier with UserGUID . Then we can query Active Directory. The following function queries Active Directory using the SID value we created against the objectSID value in Active Directory. It returns an ADSI object that corresponds to the Active Directory user object found.

 Function FindBySID(strSID, domain)     'Find the user's object     Dim usr     Set con = CreateObject("ADODB.Connection")     con.provider = "ADsDSOObject"     con.Open "ADs Provider"     strSQL = "<LDAP://" & domain & ">;(&(objectClass=user)(objectSid=" _            & strSID & "));cn,adspath,userPrincipalName;subtree"     Set rs = con.Execute(strSQL)     If rs.RecordCount > 1 Or rs.RecordCount = 0 Then         'Return back nothing         Set FindBySID = Nothing         Exit Function     End If     Do While Not rs.EOF         Set usr = GetObject(rs.Fields("AdsPath").Value)         rs.MoveNext     Loop     Set FindBySID = usr End Function 

Another way you can quickly look up the user information from the SID is by using the Win32 API function LookupAccountSID . This function takes the name of the server you want to use to look up the SID, the SID itself, a variable that the function can pass the username into, the size of the variable for the username, a variable that the function can pass the domain name into, the size of the domain name, and finally a variable that the function uses to pass back a value for the SID_NAME_USE enumeration type that identifies the SID type (such as user, group, or computer). With this technique, you do not have to modify the SID returned by Exchange. Instead, you just pass the UserSID information to the function:

 Private Declare Function LookupAccountSid Lib "advapi32.dll" Alias _     "LookupAccountSidA" (ByVal lpSystemName As String, _                          Sid As Any, _                          ByVal name As String, _                          cbName As Long, _                          ByVal ReferencedDomainName As String, _                          cbReferencedDomainName As Long, _                          peUse As Integer _                          ) As Long ' ' Lookup a SID and return the account name as a string ' Private Function LookupNameOfSid(ByVal Sid As Variant) As String     On Error Resume Next          Dim Sidbytes() As Byte ' The byte array in the variant as                            ' returned by DispEvtInfo.UserSid     Sidbytes = Sid ' get the byte array from the Sid Variant                    ' which is an array of bytes (vartype 8209)          Dim result As Long     Dim userName As String     Dim cbUserName As Long     Dim domainName As String     Dim cbDomainName As Long     Dim peUse As Integer          ' Pass in the SID to get the user name and domain     userName = Space(255)     domainName = Space(255)     cbUserName = 255     cbDomainName = 255     result = LookupAccountSid(vbNullString, Sidbytes(0), userName, _                               cbUserName, domainName, cbDomainName, peUse)          If result Then 'Strip the Null characters from the returned strings         domainName = Left(domainName, InStr(domainName, Chr(0)) - 1)         userName = Left(userName, InStr(userName, Chr(0)) - 1)         LookupNameOfSid = domainName & "\" & userName     Else         LookupNameOfSid = "Error calling LookupAccountSID: " & result     End If           End Function 

One more GUID value that you need to be aware of is the StoreGUID value (in synchronous events), which is the same as the bstrMDBGUID variable passed to OnMDBStartup and OnMDBShutdown events. The StoreGUID property is not available to asynchronous events. If you look at the StoreGUID property that is given to you, it will look conspicuously like the UserGUID property in format. This means that the steps we used to transform the UserGUID value into a valid string value for Active Directory are exactly the same steps you use to transform the StoreGUID property.

The only difference is that we do not need to put a slash before every two characters because we're not going to query Active Directory for the object that corresponds to the database that the event is firing in ”the GUID value we finally get can be used with ADSI directly to open the object using the GC://<GUID=guidvalue> format. You cannot use this technique with UserGUID because, as you saw earlier, UserGUID does not map to objectGUID but to msExchMailboxGuid . I won't repeat the code for the GUID formatting process here; however, the following code shows how to take the GUID you get back from those functions and open the Active Directory object:

 Function OpenADObject(strGUID)     On Error Resume Next          'Use GUID binding of AD to open the AD object for the folder     strGUID = LCase(strGUID)     guidADsPath = "GC://<GUID=" & strGUID & ">"     Set oDS = GetObject(guidADsPath)     If Err.Number <> 0 Then         'Return back nothing         Set OpenADObject = Nothing     Else         Set OpenADObject = oDS     End If End Function 

You might want to use this functionality to detect the database where the event is taking place. For the database shutdown and startup events, you might want to get the Active Directory object to query for certain properties of the database, such as the name, location of database files, or any other property stored in Active Directory for the database.

Debugging an Event Handler

When you set up the Training application, it asks whether you want to enable event handler debugging. Debugging your event handler is easy if you use COM+ applications. All you need to do to debug your event handler is set the identity of your COM+ application so that it uses the built-in interactive user option in COM+, add some breakpoints to your Visual Basic event handler, and then put the Visual Basic event handler in run mode by pressing F5 in Visual Basic. You can then take advantage of all the great Visual Basic debugging features. Figure 17-3 shows an example of an event handler in the Visual Basic debugger.

click to expand
Figure 17-3: One of the Training application event handlers in the Visual Basic debugger

In addition to using the debugger, you can use the Visual Basic LogEvent method on the App object. Using this method, you can have Visual Basic place entries into the Windows event log. This is the sort of debugging that the Training application uses. Figure 17-4 shows an entry from the event log for one of the Training application's event handlers.

click to expand
Figure 17-4: A message posted by Visual Basic into an event handler's log



Programming Microsoft Outlook and Microsoft Exchange 2003
Programming MicrosoftВ® OutlookВ® and Microsoft Exchange 2003, Third Edition (Pro-Developer)
ISBN: 0735614644
EAN: 2147483647
Year: 2003
Pages: 227
Authors: Thomas Rizzo

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net