Workflow Capabilities


Exchange Server allows you to build applications that model and automate business processes. You can hardcode a workflow process by using server events and Visual Basic, but the built-in workflow engine ”Workflow Designer ”and CDO workflow objects of Exchange Server simplify this development task by providing specialized workflow tools.

With these tools, you can build database workflows and e-mail workflows on Exchange Server. You can also enable the user to connect directly to the data or to an application that directly touches the data to update state information in the workflow. The Training application is a database workflow. When a manager approves a user to take a course, an ASP page updates a property on the pending approval message to the student, which in turn triggers the workflow engine to change the state of the pending approval message to Approved . This change of state causes the application to register the student for the course and then notify the student. Database workflows are great if your users have direct access to the process instances of the workflow, and, in effect, to the items undergoing the workflow process, so that they can update the state.

In e-mail workflows, instead of having the user directly interact with the data, notifications and data copies are sent to the user via e-mail. The user can then perform an action, such as approve or edit the data, and return it to the application via e-mail. The Exchange Server workflow engine directs the e-mail to the correct process instance in your workflow folder, and you can update your process instance accordingly . This style of workflow is useful if you can't guarantee that users have direct access to your data or if you want to provide an easy transmission method for data and approval. Most users understand and have ready access to Outlook e-mail, so this solution works well for business processes that extend beyond a corporation and over the Internet. Note that to use e-mail-style workflow, you must be running Outlook on your client.

Workflow in Exchange Server

Exchange Server uses four main components to implement workflow: CDO for Workflow (CDOWF), server event sinks (handlers), action tables, and script files. The workflow engine in Exchange Server is stored in CDOWF. This engine works in conjunction with the other components to evaluate and maintain the workflow state, such as Approved or Rejected . CDOWF also provides the object model you can use to interact with the engine to change states or validate workflow properties.

The workflow engine works in conjunction with the action table, which is basically a finite state machine that describes transitions between states in the workflow. Table 17-4 shows an example of an action table based on the Training application. In this action table, a request for approval of user's enrollment is e-mailed to the user's manager, and the workflow engine waits 15 minutes for a response. If a response isn't received from the manager within 15 minutes, the engine e- mails the user that the request was not processed . If the manager approves the course request, the engine registers the user and sends an e-mail that tells the user of the approval.

Table 17-4: A Simple Action Table Based on the Training Application

State

New State

Event Type

Condition

Action

Expiration Interval

 

Created

OnCreate

CheckValidity

SendMailtoManager

 
 

Created

OnEnter

True

 

15 minutes

Created

Approved

OnChange

ApprovalStateofItem

RegisterUserSendMailtoUser

 

Created

NoResponse

OnExpiry

True

SendNoResponsetoUser

 

To determine what changes have been made to workflow items and to evaluate whether they are valid according to the action table, the workflow engine implements both synchronous and OnTimer event handlers. When you drop a new document into a folder in which a workflow process is enabled, the Workflow event handler in that folder fires before the item is committed to the Exchange Server database. If workflow is enabled, the event handler creates the process instance for your item in the folder and determines the initial state of the item. If a user changes that item, the event handler checks the change against the valid state changes in the action table. If the state change is not valid, it is not committed to Exchange Server. If the time expires for a specific state and there is a state transition for that event in the action table, the OnTimer event will move the workflow item to the next state. Figure 17-11 shows the Workflow event handler that's installed by default as a COM+ application.

click to expand
Figure 17-11: The Workflow event handler that's installed as a COM+ application

The script file is used in conjunction with CDOWF, the server event handlers, and the action tables. Your script implements the conditions for the state transitions and the actions that occur at the time of those transitions. For example, to check the validity of an item before posting it in the workflow folder, you write script to check the necessary properties and return to the workflow engine a Boolean that indicates whether the item is valid. You must use condition functions in your script. If a condition is valid, for example, you might want to send an e-mail to the user who needs to approve the item. You have to write script for the action portion of your workflow in order to send the e-mail.

Developing Workflow Applications

Enough talk about what workflow is ”let's look at how to write a workflow application with Exchange Server. In this section, I'll concentrate on the Workflow Designer for Exchange because it makes creating workflow applications easier. Everything we'll discuss in this section can be performed programmatically using CDOWF.

Setting Up the Workflow Environment

To start writing workflow applications, you first need to set up the workflow environment for Exchange Server. I won't go into the gory details of how to set up all the accounts and infrastructure. You can find all this information in the Exchange SDK, in a section called "Adding the Workflow System Account." Essentially, you need to create an account and make it your default workflow system account.

Next you need to add yourself to the Can Register Workflow role for the Workflow Event Sink COM+ application. This role lists all the users or groups that can actually register for the event handlers we discussed earlier. Figure 17-12 shows this COM+ application with the roles populated .

click to expand
Figure 17-12: The Can Register Workflow role populated with users and groups

If necessary, you also should add yourself to the Privileged Workflow Authors role. If you are not in this role, your workflow scripts will be sandboxed. In other words, they will only be able to modify the item undergoing workflow, send notification mail, or write to the workflow audit trail. They will not be able create objects at all. If you add yourself to the Privileged Workflow Authors role, you'll be able to create objects and access other items you have permissions for in Exchange Server.

Note  

If you are a privileged workflow author, you will be running under the workflow system account you initially specified. Also, Exchange supports ad hoc workflows. This means that items coming into the folder where workflow is enabled must already have a workflow definition on them. Only restricted mode workflows can be run as ad hoc. You cannot allow ad hoc workflows and enable privileged mode.

Using the Workflow Designer

As mentioned earlier, the Workflow Designer for Exchange makes building and deploying your workflow applications easier. Its graphical user interface (GUI) simplifies the process of visualizing your workflow while automating the process of creating the event handler registrations and action tables in your workflow-enabled folders.

I won't cover all the GUI elements of the Workflow Designer; you can find that information easily in the Exchange Server documentation, or you can just read the Workflow Designer screen. However, I will cover three important elements that make up your workflow in the designer: states, actions, and script. The states are the boxes in the GUI that indicate whether your workflow is pending, approved, rejected, or expired . The actions provide the transitions between the states. These actions, which I'll detail momentarily, can be found in your action table and include OnEnter , OnExit , and OnCreate , among others. The script implements the condition checking and the actions.

The states in the Workflow Designer are not very interesting because they don't perform any task; they only serve as a destination for the workflow item to move to during the workflow process. Therefore, your only concern with states should be that your scripts allow you to check which state the workflow item is in, to see whether it's being approved or rejected or placed in any other state you specify.

Actions are among the most important aspects of the Workflow Designer because they are associated with the transitions between states. These transitions define how the work in your workflow is performed. Figure 17-13 shows how to create actions in the Workflow Designer.

click to expand
Figure 17-13: Creating actions in the Workflow Designer

Table 17-5 describes the actions in the Workflow Designer. Be aware that some actions, such as the OnChange action, can appear multiple times on a state. But each time it appears for the same state, it must have different conditions that make the action valid for a workflow item. For example, one condition might check changes to the subject line and another might check changes to the message body.

Table 17-5: Actions of the Workflow Designer

Workflow Designer

Action Table

Description

Create

OnCreate

An item was created. You must have at least one Create action in your workflow; otherwise , no items can be created in the folder.

Enter

OnEnter

This action manages the time used by the Expiry action. As soon as the state is entered using this action, the timer is started for the expiration interval you set. Entry into states is implicitly allowed, so you don't have to add this action to every state. You will normally use this action for timer-based workflow requirements only.

Exit

OnExit

The state is transitioning to a new state.

Delete

OnDelete

The document is deleted. If you do not have a Delete action, workflow items cannot be deleted. If that's the case, Exchange Server will return an error if an application or user attempts to delete an item.

Change

OnChange

The document is modified. You can have multiple Change actions on a single state. If you have no Change actions, documents cannot be modified.

Receive

OnReceive

The workflow has received an e-mail message that correlates to a workflow item. This action allows your workflow to respond to e-mail.

Expiry

OnExpiry

The document has passed its time limit for the current state. This action is useful for time-based tasks , such as reminder notifications to managers to approve workflow items.

The following code is the XML representation of a real action table generated by the Workflow Designer. Notice the action and state names in the XML code. You'll see how to use XML action tables to simplify deploying workflow processes later in the chapter.

 <xml xmlns:s='uuid:BDC6E3F0-6DA3-11d1-A2A3-00AA00C14882'                     xmlns:dt='uuid:C2F41010-65B3-11d1-A29F-00AA00C14882'                     xmlns:rs='urn:schemas-microsoft-com:rowset'                     xmlns:z='#RowsetSchema'> <s:Schema id='RowsetSchema'>     <s:ElementType name='row' content='eltOnly' rs:updatable='true'>         <s:AttributeType name='ID' rs:number='1' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='Caption' rs:number='2' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='State' rs:number='3' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='NewState' rs:number='4' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='EventType' rs:number='5' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='Condition' rs:number='6' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='Action' rs:number='7' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='ExpiryInterval' rs:number='8' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='RowACL' rs:number='9' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='TransitionACL' rs:number='10' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='DesignToolFields' rs:number='11'                                                               rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='CompensatingAction' rs:number='12'                                                               rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='Flags' rs:number='13' rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:AttributeType name='EvaluationOrder' rs:number='14'                                                               rs:write='true'>             <s:datatype dt:type='string' dt:maxLength='4294967295'                 rs:precision='0' rs:long='true' rs:maybenull='false'/>         </s:AttributeType>         <s:extends type='rs:rowbase'/>     </s:ElementType> </s:Schema> <rs:data>     <rs:insert>         <z:row ID='1' Caption='Create' State='' NewState='Pending'             EventType='OnCreate' Condition='TRUE' Action=''             ExpiryInterval='0' RowACL='' TransitionACL=''             DesignToolFields='-1:1:' CompensatingAction='' Flags='0'             EvaluationOrder='1000'/>         <z:row ID='2' Caption='Delete' State='Pending' NewState=''             EventType='OnDelete' Condition='true' Action=''             ExpiryInterval='0' RowACL='' TransitionACL=''             DesignToolFields='1:-2:' CompensatingAction='' Flags='0'             EvaluationOrder='7000'/>         <z:row ID='3' Caption='StartTimer' State='' NewState='Pending'             EventType='OnEnter' Condition='TRUE' Action='sendMailToManager'             ExpiryInterval='15' RowACL='' TransitionACL=''             DesignToolFields='0:1:' CompensatingAction='' Flags='0'             EvaluationOrder=''/>         <z:row ID='5' Caption='ManagerApproved' State='Pending'             NewState='Approved' EventType='OnChange'             Condition='WorkflowSession.Fields             (&#x22;http://thomriz.com/schema/approvalstatus&#x22;).value =             &#x22;Approved&#x22;'             Action='strCourseName = GetCourseName             strStudentEmail = GetStudentEmail             strManagerEmail = GetManagerEmail             strBody = &#x22;Your manager approved you for the course:             &#x22; &#x26; strCourseName             sendMail strBody,strStudentEmail &#x26; &#x22;,&#x22; &#x26;             strManagerEmail,&#x22;Approved for course: &#x22; &#x26;             strCourseName             addregistration             sendcalendarmessage'             ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields=             '1:3:' CompensatingAction='' Flags='0' EvaluationOrder='3001'/>         <z:row ID='6' Caption='ManagerRejected' State='Pending'             NewState='Rejected' EventType='OnChange'             Condition='WorkflowSession.Fields             (&#x22;http://thomriz.com/schema/approvalstatus&#x22;).value =             &#x22;Rejected&#x22;'             Action='strCourseName = GetCourseName             strStudentEmail = GetStudentEmail             strManagerEmail = GetManagerEmail             strBody = &#x22;Your manager rejected you for the course: &#x22;             &#x26; strCourseName             sendMail strBody,strStudentEmail &#x26; &#x22;,&#x22; &#x26;             strManagerEmail,&#x22;Rejected for course: &#x22; &#x26;             strCourseName '             ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields=             '1:2:' CompensatingAction='' Flags='0' EvaluationOrder='3000'/>         <z:row ID='7' Caption='NoResponse' State='Pending' NewState='Expired'             EventType='OnExpiry' Condition='TRUE'             Action='strCourseName = GetCourseName             strStudentEmail = GetStudentEmail             strManagerEmail = GetManagerEmail             strBody = &#x22;Your manager did not approve your attending             of the course: &#x22; &#x26;             strCourseName &#x26; &#x22; in enough time.  You will not             be registered for this course.&#x22;             sendMail strBody, strStudentEmail &#x26; &#x22;,&#x22; &#x26;             strManagerEmail, &#x22;Approval not received for course:             &#x22; &#x26; strCourseName '             ExpiryInterval='0' RowACL='' TransitionACL='' DesignToolFields=             '1:4:' CompensatingAction='' Flags='0' EvaluationOrder='5000'/>         <z:row ID='8' Caption='' State='Rejected' NewState=''             EventType='OnDelete' Condition='TRUE' Action='' ExpiryInterval=''             RowACL='' TransitionACL='' DesignToolFields='2:-2:'             CompensatingAction='' Flags='0' EvaluationOrder='7001'/>         <z:row ID='10' Caption='' State='Approved' NewState=''             EventType='OnDelete' Condition='TRUE' Action='' ExpiryInterval=''             RowACL='' TransitionACL='' DesignToolFields='3:-2:'             CompensatingAction='' Flags='0' EvaluationOrder='7002'/>         <z:row ID='11' Caption='' State='Expired' NewState=''             EventType='OnDelete' Condition='TRUE' Action='' ExpiryInterval=''             RowACL='' TransitionACL='' DesignToolFields='4:-2:'             CompensatingAction='' Flags='0' EvaluationOrder='7003'/>     </rs:insert> </rs:data> </xml> 

Instead of using the Workflow Designer to create your workflow process, you could programmatically create your action table by using just ADO and CDOWF. However, in most cases, you'll want to take advantage of the Workflow Designer and generate your action tables to XML, as shown in the preceding example. You can then import the XML action table into ADO and use that data to programmatically generate your workflow process.

Creating Event Scripts

We have a workflow engine, event handlers, and an action table, but we don't have a true workflow application yet. The real foundation of the workflow application is the VBScript code you write for the actions in your action table. Whether you want to send a message, change a property, or update an item, you need to implement this action in your script. Writing your workflow script is pretty straightforward; you'll probably call ADO or CDO to perform functions in Exchange Server. The Workflow Designer includes a script editor, shown in Figure 17-14.

click to expand
Figure 17-14: The script editor built into the Workflow Designer

You don't have to use the Workflow Designer script editor to write your scripts. You can use another editor, such as Microsoft Visual Studio .NET, and save the scripts to a common location or even implement the handlers for your actions using COM components. You can then point the Workflow Designer, or a workflow process you programmatically create, to a common script file. The following script file is used in the Training application to implement the workflow process:

 Dim strHTMLBody Dim bWroteDebugging      Sub AddAuditEntry(strString, lResult)     WorkflowSession.AddAuditEntry strString, lResult End Sub      Function DebugWorkflow()     'Check to see whether debugging is enabled     bWorkflow = cBool(WorkflowSession.Fields( _                       "http://thomriz.com/schema/debugworkflow").value)     If bWroteDebugging <> True Then         If bWorkflow Then             AddAuditEntry "Workflow Debugging Enabled", 0             bWroteDebugging = True         End If     End If     DebugWorkflow = bWorkflow End Function      Function GetSchema()     If DebugWorkflow Then         AddAuditEntry "In GetSchema", 0     End If     GetSchema = _         WorkflowSession.Fields("http://thomriz.com/schema/schema").value     If DebugWorkflow Then         AddAuditEntry "In GetSchema -> Schema: " & _             WorkflowSession.Fields("http://thomriz.com/schema/schema").value,0     End If End Function      Function GetWorkflowSessionField(strField)     If DebugWorkflow Then         AddAuditEntry "In GetWorkflowSessionField -> Value: " & strField, 0     End If     GetWorkflowSessionField = _         WorkflowSession.Fields("http://thomriz.com/schema/" & strField).value End Function      Function GetCourse(bReadOnly)     If DebugWorkflow Then         AddAuditEntry "In GetCourse", 0     End If     Set oRec = CreateObject("ADODB.Record")     If DebugWorkflow Then         AddAuditEntry "Course URL: " & WorkflowSession.Fields( _             "http://thomriz.com/schema/fullcourseurl").value, 0     End If     If bReadOnly Then         iAccess = 1     Else         iAccess = 3     End If     oRec.Open WorkflowSession.Fields("http://thomriz.com/sche" & _         "ma/fullcourseurl").value, WorkflowSession.ActiveConnection, iAccess     If DebugWorkflow Then         AddAuditEntry "In GetCourse -> CourseName: " _             & oRec.Fields("urn:schemas:httpmail:subject").value, 0     End If     Set GetCourse = oRec End Function       Function GetCourseName()     'Returns the name of the course     Set oRec = GetCourse(True)     If DebugWorkflow Then         AddAuditEntry "In GetCourseName -> CourseName = " _             & oRec.Fields("urn:schemas:httpmail:subject").value, 0     End If     GetCourseName = oRec.Fields("urn:schemas:httpmail:subject").value End Function      Sub ReplaceString(strToken, strReplacement)     'Take the token and replace it in the global strHTMLBody     strHTMLBody = Replace(strHTMLBody, strToken, strReplacement) End Sub      Function GenerateHTMLBody()     'Generates the HTML to send in the message.     'Retrieve the message containing the HTML.     'The HTML template must always be called WorkflowMessage.     If DebugWorkflow Then       AddAuditEntry "In GenerateHTMLBody", 0     End If     strHTMLBody = ""          'Build the SQL statement.     'Query for the e-mail message.     strEmailsFolderPath = GetWorkflowSessionField("stremailsfolderpath")     strSQL = "SELECT ""urn:schemas:httpmail:textdescription"" FROM " _            & "SCOPE('SHALLOW TRAVERSAL OF """ & strEmailsFolderPath _            & """') WHERE ""DAV:iscollection"" = false AND " _            & """DAV:ishidden"" = false AND " _            & """urn:schemas:httpmail:subject"" LIKE '%WorkflowMessage%'"          'Create a new RecordSet object     Set rst = CreateObject("ADODB.RecordSet")     With rst         'Open RecordSet based on the SQL string         .Open strSQL, WorkflowSession.ActiveConnection     End With          If rst.BOF And rst.EOF Then         GenerateHTMLBody = ""         Exit Function     End If          'On Error Resume Next     rst.MoveFirst     strHTMLBody = rst.Fields("urn:schemas:httpmail:textdescription").Value          'Get the course     Set oCourse = GetCourse(True)     'Load it into CDO Appointment     Set iAppt = CreateObject("CDO.Appointment")     iAppt.DataSource.Open oCourse.Fields("DAV:href").value, _                           WorkflowSession.ActiveConnection, 1          strSchema = GetSchema     'Replace the tokens with real values     ReplaceString "%StudentName%", _                   GetWorkflowSessionField("strStudentFullname")     ReplaceString "%Name%", _                   iAppt.Fields("urn:schemas:httpmail:subject").Value     ReplaceString "%Category%", iAppt.Fields(strSchema & "category").Value     strDate = Month(iAppt.StartTime) & "/" & Day(iAppt.StartTime) & "/" _             & Year(iAppt.StartTime)    ReplaceString "%Date%", strDate     ReplaceString "%StartTime%", TimeValue(iAppt.StartTime)     ReplaceString "%EndTime%", TimeValue(iAppt.EndTime)     ReplaceString "%Location%", iAppt.Location     ReplaceString "%Description%", iAppt.TextBody          strHTTPURL = GetWorkflowSessionField("strRootDirectory") _                & "workflow.asp?CourseID=" & iAppt.Fields("DAV:href") _                & "&student=" & GetWorkflowSessionField("fullStudentURL")     ReplaceString "%URLLink%", strHTTPURL          If strHTMLBody <> "" Then         GenerateHTMLBody = strHTMLBody     Else         GenerateHTMLBody = ""     End If          rst.Close     Set rst = Nothing End Function      Sub sendMail(strMsg, strAddress, strSubject)     set msg = createobject("CDO.Message")     msg.To = strAddress     msg.From = GetWorkflowSessionField("NotificationAddress")     msg.Subject = strSubject     msg.TextBody = strMsg     If DebugWorkflow Then         AddAuditEntry "In SendMail: Address -> " & strAddress & vblf _             & "Subject -> " & strSubject & vblf & "Message: " & strMsg, 0     End If     msg.Send End sub      Function GetStudentEmail()     If DebugWorkflow Then         AddAuditEntry "In GetStudentEmail", 0     End If     GetStudentEmail = GetWorkflowSessionField("StudentEmail") End Function      Function GetManagerEmail()     If DebugWorkflow Then         AddAuditEntry "In GetManagerEmail", 0     End If     GetManagerEmail = GetWorkflowSessionField("ManagerEmail") End Function      Sub SendMailToManager()     'Get the manager's e-mail     strManagerEmail = GetWorkflowSessionField("ManagerEmail")     If DebugWorkflow Then         AddAuditEntry "In SendMailToManager -> Manager: " & strManagerEmail, 0     End If     set oMsg = CreateObject("CDO.Message")     set oRecord = GetCourse(True)     oMsg.To = strManagerEmail     oMsg.From = GetWorkflowSessionField("NotificationAddress")     oMsg.Subject = "Approval Required for course:  " _                  & oRecord.Fields("urn:schemas:httpmail:subject").value     oMsg.AutoGenerateTextBody = True     oMsg.MimeFormatted = True     oMsg.HTMLBody = GenerateHTMLBody     oMsg.Send End Sub      Function GetStudent(bReadOnly)     If DebugWorkflow Then         AddAuditEntry "In GetStudent",0     End If     Set oRec = CreateObject("ADODB.Record")     If DebugWorkflow Then         AddAuditEntry "Student URL: " & WorkflowSession.Fields( _             "http://thomriz.com/schema/fullstudenturl").value, 0     End If     If bReadOnly Then         iAccess = 1     Else         iAccess = 3     End If     oRec.Open WorkflowSession.Fields("http://thomriz.com/sche" _                                     & "ma/fullstudenturl").value, _                                     WorkflowSession.ActiveConnection, iAccess     If DebugWorkflow Then         AddAuditEntry "In GetStudent -> StudentName: " _             & oRec.Fields("urn:schemas:httpmail:subject").value, 0     End If     Set GetStudent = oRec End Function      Sub addRegistration()     Set oRecord = GetStudent(False)     strSchema = GetSchema     strCourseURL = GetWorkflowSessionField("shortCourseURL")     oRecord.Fields(strSchema & "registrations") = oRecord.Fields(strSchema _                                                 & "registrations") _                                                 & strCourseURL & ","     oRecord.Fields.Update     oRecord.Close     Set oRecord = Nothing End Sub      Sub sendCalendarMessage()     If DebugWorkflow Then         AddAuditEntry "In SendCalendarMessage", 0     End If     'Get the original appointment     Set oOriginalAppt = CreateObject("CDO.Appointment")     oOriginalAppt.Datasource.Open GetWorkflowSessionField("fullCourseURL"), _                                   WorkflowSession.ActiveConnection, 1          'Create a throwaway appointment     Set oAppt = CreateObject("CDO.Appointment")     set oConfig = CreateObject("CDO.Configuration")     strNotificationAddress = GetWorkflowSessionField("NotificationAddress")     oConfig.Fields("http://schemas.microsoft.com/cdo/config" _                   & "uration/sendemailaddress") = strNotificationAddress     oConfig.Fields.Update          oAppt.Configuration = oConfig          oAppt.StartTime = oOriginalAppt.StartTime     oAppt.EndTime = oOriginalAppt.EndTime     oAppt.Subject = "Course: " & oOriginalAppt.Subject     oAppt.Location = oOriginalAppt.Location     strSchema = GetSchema     oAppt.TextBody = "The Instructor is " _                    & oOriginalAppt.Fields(strSchema & "instructoremail").Value     'Don't ask for a response since we don't care if they accept or decline     oAppt.ResponseRequested = False          Set oAttendee = oAppt.Attendees.Add     strEmail = GetStudentEmail          oAttendee.Address = strEmail     oAttendee.Role = 0          Set oMtg = oAppt.CreateRequest     oMtg.Message.Send      End Sub 

This script uses ADO and CDO to perform its functions. However, another object is at work in the script: WorkflowSession . This intrinsic object (which you don't have to create) is passed to your script by the workflow engine. It allows you to access properties on the process instance as well as the audit trail specified for the workflow. Table 17-6 shows the most important properties and methods of this object. For more information on the properties and methods , refer to the Exchange Platform SDK.

Table 17-6: Properties and Methods of the WorkflowSession Object

Property or Method

Description

ActiveConnection

A property that returns an ADO Connection object. You should use this Connection object in your script's ADO and CDO functions, especially if you want them to take part in transactions.

AddAuditEntry

A method that allows you to add an audit entry to the selected audit entry provider of the workflow process. You pass a string and a long value to specify what the entry should say and the custom result you want for the value. By default, Exchange Server ships with one audit trail provider, which writes to the Windows Event Log. You can create custom audit trail providers by creating COM components that implement the IAuditTrail interface.

DeleteReceivedMessage

A method that deletes the received e-mail, if one exists, for the workflow item. You usually call it in the Receive action.

DeleteWorkflowItem

A method that deletes the workflow item.

Domain

A property that returns the domain of the server. This property works in conjunction with the Server property to make it easier for you to generate file:// or http:// URLs.

ErrorDescription

A property used in conjunction with the ErrorNumber property. ErrorDescription contains a description of the error to report to the audit trail provider.

ErrorNumber

A property that holds the number of the errors to report to the client and the audit trail provider.

Fields

A property that returns the ADO Fields collection for the workflow item. Using Fields , you can access built-in and custom schemas on the workflow item.

GetNewWorkflowMessage

A method that creates and returns a new WorkflowMessage object. The object allows you to send e-mail messages from restricted workflows because you cannot create a CDO Message object in a restricted workflow. Also, the CDO message is created in the context of the workflow transaction so if the state transition fails, the e-mail created using this method never gets sent.

GetUserProperty

A method that gets an Active Directory attribute off an Active Directory object.

IsUserInRole

A method that checks to see whether a user is in a folder role. You pass to this method the user's e-mail address and the name of the role. The method returns a Boolean indicating whether that user is in that particular folder role. A folder role is a grouping of users who perform a particular function that you define for the folder. The roles are stored on the folder, so to implement roles-based workflow, you do not need permissions to modify or add properties to Active Directory.

ItemAuthors

A property that contains a collection representing a list of all users with authoring ability on the workflow item. Exchange Server supports item-level permissions, so you might want to set such permissions on workflow items.

ItemReaders

A property that contains a collection of users who should have Reader permissions on the workflow item.

Properties

A property that returns an ISessionProps interface so you can add properties you need persisted for a single session that lasts for one ProcessInstance transition. Here's a good example of using this property: Suppose you have multiple actions that need to be evaluated to make a state transition. You do not want each action to check multiple times whether a certain property on the item already exists as part of the evaluation criteria. So you use this property to cache the value and share the value between multiple condition scripts.

ReceivedMessage

A property that returns the e-mail message that was received in correlation to a workflow item.

Sender

A property that contains the SMTP address of the person who initiated the state transition.

Server

A property that contains the name of the server and is used in conjunction with the Domain property.

StateFrom

A property that contains the name of the state before the current process transition.

StateTo

A property that holds the name of the state after the current transition.

TrackingTable

Used with e-mail workflows, this property contains a RecordSet object that has a number of properties relating to the current workflow item. Refer to the Exchange Platform SDK for more information on this property.

The GetUserProperty method is very useful. It takes three parameters. The first is the distinguished name of the object in Active Directory, which can be either the Active Directory path to the object or the unique e-mail address of the object. The second parameter is the Active Directory attribute you want to get off the object. The third parameter works in conjunction with the first and tells CDO whether the first parameter is an Active Directory path ( 1 ) or an e-mail address ( ). Probably the most common use for this method is to retrieve the manager of the owner of the item that is undergoing the workflow to get approval. You retrieve the manager by getting the manager property off the current user's Active Directory object. You can get the e-mail address of the current user by using the WorkflowSession.Sender property. You can then retrieve the mail property from the manager's Active Directory object. The manager property returns to you the Active Directory path to the manager. The following example illustrates this scenario:

 With WorkflowSession     strUserAddress = "username@company.com"     mgrDN = .GetUserProperty(strUserAddress, "manager", 0)     strUserMgrEmail = .GetUserProperty(mgrDN, "mail", 1) End With 

Two other properties in Table 17-6, ItemAuthors and ItemReaders , also demand more explanation. ItemAuthors is a collection used to specify per-item modify and delete permissions. If you add any users to this collection, only those users can modify or delete the item as well as read it. If you remove all users from this collection, the default permissions on the folder apply.

With ItemReaders , you can specify per-item read access. If you add users to this collection, only those users can read or view the item, but they cannot necessarily modify the item. This means that even when other users query, know the URL of the item, or try to retrieve a specific property on the item, they cannot modify the item unless they are in the ItemReaders collection. When you clear the collection, default folder permissions will apply.

Both ItemAuthors and ItemReaders return an IMembers interface. This interface supports one property and three methods: the Count property, and the Add , Clear , and Delete methods. Count returns the number of members in the collection. Add adds a new member by taking two parameters, Name and Type . Name must be a string that specifies the e-mail address of the user or a role. Exchange supports the string literals " Role 1" through " Role 16" for adding roles. The Type parameter is an integer that specifies the type of user you are adding, whether it is an e-mail address ( ) or a role ( 1 ). The Clear method clears all members from the collection. Finally, the Delete method deletes a member from the collection. You must pass a numbered index into the collection or a string that uniquely identifies a member of the collection. This string can be a role name such as " Role 1" or the e-mail address of the user you want to remove from the collection. The following example shows how to add two different users to the ItemAuthors and ItemReaders collections on a workflow item:

 strAddress = "user@domain.com" WorkflowSession.ItemAuthors.Add strAddress, 0  'cdowfEmailAddress strAddress = "user2@domain.com" WorkflowSession.ItemReaders.Add strAddress, 0  'cdowfEmailAddress 

Compensating Actions

Even though you do not want state transitions to fail because of intermittent computer issues or people attempting transitions when they do not have permissions to, these situations can occur. The Workflow Designer gives you the ability to run compensating actions if a state transition fails. The compensating action is VBScript code. A good example of using a compensating action is when you update a SQL database in the beginning of your state transition ”for some reason, the state transition fails. You can use the compensating script to undo your changes to the SQL server since the state transition failure. Figure 17-15 shows where to set your compensating actions. Compensating actions are not required. There is no need to create them unless you need to for your application.

click to expand
Figure 17-15: A compensating script in the Workflow Designer

Mapping URLs Using the EXOLEDB URLMAPPER

One other requirement of your workflow might be to change the inherent file-based URL that you receive in your workflow to an HTTP URL that you can e-mail to the end user to open the work item. The file URL will look something like file://./backofficestorage/yourdomain/public folders/folder/myitem.eml . Microsoft Internet Explorer cannot use this URL to browse to the item. You could manually convert the item file URL to an HTTP URL, but the OLE DB provider for the Web Storage System provides this capability as a core part of its functionality in an object called URLMAPPER . The URLMAPPER can take file URLs and map them to HTTP, and vice versa.

The methods of URLMAPPER are shown in Table 17-7.

Table 17-7: Methods of the URLMAPPER Object

Method

Description

ExoledbFileURLtoFilePath

Takes a file URL in the form file://./backofficestorage/yourdomain and changes it to a file path. You can then turn the file path into an HTTP URL by using the FilePathtoHTTPURLs method.

FilePathtoEXOLEDBFileURL

Takes a file path in the form \\.\backofficestorage \domain\folder\folder and converts it to file://./backofficestorage/domain/folder/folder so you can use the path with EXOLEDB.

FilePathtoHTTPURLs

Takes a file path and returns a variant array that contains all combinations of the HTTP URL, including http://server/foldertree/folder/item , https ://server/foldertree/folder/item , http://server/exadmin/folder/item , and https://server/exadmin/folder/item .

HttpURLtoFilePath

Takes an HTTP URL to an item and converts it to a file path.

Therefore, to convert the standard file URL that is passed to you via the workflow engine, you can use the URLMAPPER . The following code converts the URL to an HTTP URL and sends it to the user who just submitted the item:

 Sub URLMAPPER     strURL = WorkflowSession.Fields("DAV:href").value     set oMapper = CreateObject("Exoledb.UrlMapper")     strFilePath = oMapper.ExoledbFileUrlToFilePath(strURL)     arrHTTP = oMapper.FilePathtoHTTPURLs(strFilePath)          set oMsg = CreateObject("CDO.Message")     oMsg.From = "Workflow"     oMsg.To = WorkflowSession.Sender     oMsg.Subject = "HTTP URLs"     For i = LBound(arrHTTP) To UBound(arrHTTP)         'Strip out the https and exadmin URLS         If InStr(1, arrHTTP(i), "https") =  0 Then             'Not HTTPS, check for exadmin             If InStr(1, arrHTTP(i), "exadmin") = 0 Then                 'Not exadmin, send it to the user                 strText = "The URL of the item is: " & arrHTTP(i)                 Exit For             End If         End If     Next     oMsg.TextBody = strText     oMsg.Send End Sub 

A couple of things about URLMAPPER . First, you must be running in privileged mode to use it because you need to use CreateObject to create the component. Using CreateObject in your script requires privileged mode. Second, on a creation event, the URL for an item does not exist because the workflow engine uses synchronous events. This means that before the item is even committed to the database, your code will be running against that item. An item might already exist in the folder where the application is trying to place the new item. No two items in the Exchange database can have the same URL. Exchange will append a number to the URL, as in myitem-2.eml . For this reason, pulling the URL of the item and using it with URLMAPPER to determine the HTTP URL in order to send this URL in an e-mail is not recommended in Create events. Most times, the URL you can get for the item is probably the final URL, but if it is not, your application might show unexpected results.

One neat thing you can do to get around the privileged mode requirement for URL mapping is to use the workflow session object to retrieve the IExOLEDBURLMapper interface. To do this, you use the following code in your workflows:

 Function GetHTTPUrl()     On Error Resume Next     Dim oMap, strFileURL     strFileURL = WorkflowSession.Fields("DAV:href").Value     Set oMap = WorkflowSession.Properties.Get("IExoledbUrlMapper")     GetHTTPUrl = oMap.FilePathtoHttpUrls( _                            oMap.exoledbFileUrlToFilePath(cstr(strFileURL)))(0)     Set oMap = Nothing End Function 

Transactions

You might be wondering how you can use ADO and OLE DB transactions inside of your workflow code. The Web Storage System supports transactions, which you can use to make sure that all your code commits to the Web Storage System or rolls back. One thing to note is that transactions are supported across a single ADO connection. So, if you have multiple ADO connections, you will have multiple transactions. For this reason, you should use a single ADO Connection if you want a single transaction context. You can either use the built-in WorkflowSession ActiveConnection property and its transaction context or create your own new Connection object with your own transaction context. Let's take a look at the pros and cons of these two approaches.

When you use the built-in WorkflowSession ActiveConnection object's transactions, you can do all of your work in a transacted state. This means that if you use ADO to add a new item or delete an item, if the state transition fails, all of your work will be rolled back. This is a great benefit. However, there is one drawback. Connections in the Web Storage System are per database. This means that if you want to connect to another database and perform work, you must set up a new Connection object. This new Connection object will have its own transaction context. Therefore, any work you do over the new connection will not be in the transaction context, and the rest of the WorkflowSession commands you perform over this Connection object will not be rolled back in the case of a transition failure.

The benefit of creating a new Connection object and its own transaction context is that you might want to commit your transaction before the state transition is complete. Because the workflow engine will not commit your transaction until the state transition is complete, you cannot commit your commands before the state transition is complete. Both concepts might be made clearer with some sample code.

The following code shows how you can use the built-in WorkflowSession transaction context. The first example sends an e-mail using CDO for Exchange and does this on the transaction context of the WorkflowSession . If something were to happen so that the state transition did not occur, the e-mail would not be sent.

 Sub SendEmail(strAddress,strSubject,strBody)     'Create a CDO Message Object     'You could also use the WorkflowSession object     'GetNewWorkflowMessage method     'However, this is to show explicit transaction use.          'Create a new CDO Configuration object     'so we can set the ActiveConnection     Set oConfig = CreateObject("CDO.Configuration")     oConfig.Fields("http://schemas.microsoft.com/cdo/configuration" _                   & "/activeconnection") = WorkflowSession.ActiveConnection          Set oMessage = CreateObject("CDO.Message")     oMessage.Configuration = oConfig     oMessage.Sender = "workflow"     oMessage.To = strAddress     oMessage.Subject = strSubject     oMessage.TextBody = strBody     oMessage.Send      End Sub 

The next example shows how to create your own transaction using the ADO Connection object. Here an item is created in a separate folder, but the transaction is rolled back so it never occurs. When you build applications, you should attempt to use the built-in transaction context because it provides the best way to roll back if errors occur.

 Sub PostMessage(strPath)     'Create a new ADO connection to the workitem     Set oConn = CreateObject("ADODB.Connection")     oConn.Provider = "Exoledb.Datasource"     oConn.Open "URL=" & WorkflowSession.fields("DAV:href").value          oConn.BeginTrans     Set oRecord = CreateObject("ADODB.Record")     'Open a new item in the folder as read/write     oRecord.Open strPath, oConn, 3, 0     oConn.RollbackTrans End Sub 

E-Mail-Based Workflow

Besides supporting database-style workflow in which the user interacts with the work item directly through an application to manipulate data, the Web Storage System also supports e-mail-style workflow, with some special requirements. First, the folder you are sending from must be mail-enabled ”otherwise, the folder cannot receive any e-mail responses. You mail-enable a folder through the Exchange System Manager or through the CDO for Exchange Management objects. By default, top-level folders in the default Public Folder hierarchy are mail-enabled. However, folders in other folder trees are not mail-enabled. Second, the Windows account you use for the workflow event sink to run under must have a mailbox on the local Exchange server. Finally, you cannot send e-mail on an OnCreate action and have the workflow engine correlate and track that e-mail back onto your workflow process. If you need to do this, you must advance the workflow from an OnCreate event and get an OnChange event to fire, from which you send your workflow message. The following example creates a workflow CDO message and sends it to a user:

 Sub CreateWFMessage()     set oWFMsg = WorkflowSession.GetNewWorkflowMessage     With oWFMsg         .From = WorkflowSession.Sender         .To = WorkflowSession.Sender         .Subject = "Workflow Message"         .TextBody = WorkflowSession.StateFrom & " -> " _                   & WorkflowSession.StateTo         .Fields("http://schemas.microsoft.com/exchange/" _                & "outlookmessageclass").value = "IPM.Note.Workflow"         .Fields.Update         .SendWorkflowMessage 2 'cdowfAdd (not strict)     End With End Sub 

E-mail-based workflow is supported only by clients that understand how to send back MAPI-based messages because custom properties need to be set on the reply message. For example, you must set the workflowmessageid and parentprocessinstance on your reply so the workflow engine knows to correlate the e-mail response rather than create an entirely new work item. The easiest way to add these properties to your responses is to use a custom Outlook or Web application that adds these properties. If you are going to use a custom Outlook form, you add code such as the following to your Outlook form to take the original properties from the e-mail sent by the workflow engine and add the needed properties to the response. If the code does not work for you, you might need to use CDO 1.21 in your Outlook form to add these custom properties to the item. The Outlook object model sometimes does not work correctly with these properties, but CDO 1.21 does. (The Outlook object model is covered in Chapter 6, and CDO is covered in Chapter 11.)

 Sub Item_Reply(ByVal Response)     set CurrentProp = Response.UserProperties.Add( "http://" _                          & "schemas.microsoft.com/cdo/workflow/" _                          & "parentprocinstance", 1)     CurrentProp.Value = Item.UserProperties("http://" _                          & "schemas.microsoft.com/cdo/workflow/" _                          & "parentprocinstance").Value     set CurrentProp = Response.UserProperties.Add ("http://" _                          & "schemas.microsoft.com/cdo/workflow/" _                          & "workflowmessageid", 1)     CurrentProp.Value = Item.UserProperties("http://" _                          & "schemas.microsoft.com/cdo/workflow/" _                          & "workflowmessageid").Value End Sub 

The workflow engine, when you use the SendWorkflowMessage method, automatically adds the properties shown in the previous code to the outgoing message. When you receive the response back to the folder, you must tell the workflow engine to correlate the response back onto the work item. You can have the engine automatically do this for you by setting the http://schemas.microsoft. com/cdo/workflow/response in your custom form. If you do not set this property, you must update the TrackingTable in your workflow yourself. The TrackingTable is a record that the workflow engine keeps for each response. It contains standard properties for each response, such as date, state, e-mail address, and the tracking ID. It also contains custom fields that you can use for your custom data called custom0 to custom9 . The following code manually updates the TrackingTable for a response:

 With WorkflowSession     .TrackingTable.Fields("custom0") = .TrackingTable.Fields("custom0") _                                      & vbCrLf &.ReceivedMessage.TextBody     .TrackingTable.Fields.Update End With 
Note  

You can debug your workflow solutions by using either the audit trail provider included with Exchange or script debugging. To enable script debugging, you must either select the script debugging option in the Workflow Designer or set to True the property on your workflow's event handler registration called http://schemas.microsoft.com/cdo/ workflow/enabledebug . For the debugger to work, you must make sure that just-in-time (JIT) debugging is enabled in Windows. You can do this by modifying a key in the registry under HKCU/Software/Microsoft/Windows Script Host/Settings/ActiveDebugging and setting it to 1 .

Displaying Workflow States Using XMLDOM

You might want to graphically show or provide a text list of all the states in a workflow, and then perhaps display a list of current work items and finally display the state that the current work item is in. Retrieving and displaying the list of states ”or, for that matter, doing this with state transitions or other workflow elements ”is just a matter of retrieving the .wfd file for the workflow, which is stored in the folder. Then you just parse the XML that is contained in that file, looking for the specific XML elements you are interested in. The following code finds all row nodes in the XML that correspond to the different states in the workflow:

 Function StateExist(strState, strStateArray) As Boolean     Dim i, lo, hi As Integer          StateExist = False     lo = LBound(strStateArray)     hi = UBound(strStateArray)     For i = lo To hi         If strStateArray(i) = strState Then             StateExist = True             Exit Function         End If     Next End Function      Sub PopulateStates()     ' Return all defined actions from specified workflow definition     Dim cnn As ADODB.Connection     Dim rec As ADODB.Record     Dim fld As ADODB.Field     Dim urlResource, urlFolder 'As String     Dim xmlDoc As MSXML.DOMDocument     Dim nodelist As MSXML.IXMLDOMNodeList     Dim n As MSXML.IXMLDOMNode     Dim strStateArray() As String     Dim strState As String     Dim intNrStates As Integer          ReDim strStateArray(0)          urlFolder = "http://thomriznt52/public/workflow/showstates/"     urlResource = "showwfstate.wfd"          ' Open the resource     Set cnn = CreateObject("ADODB.Connection")     With cnn         .Provider = "Exoledb.Datasource"         .Open urlFolder     End With          Set rec = CreateObject("ADODB.Record")     rec.Open urlResource, cnn, adModeRead, adOpenIfExists          ' Get the action table as XML     Set fld = rec.Fields("http://schemas.microsoft.com/" _             & "cdo/workflow/actiontable")          Set xmlDoc = CreateObject("MSXML2.DOMDocument")     xmlDoc.validateOnParse = False     ' Load the Action Table     xmlDoc.loadXML fld.Value          ' Get the row node instances     Set nodelist = xmlDoc.documentElement.selectNodes("//z:row")          ' Return the name of each state and remove duplicates     intNrStates = 0     For Each n In nodelist         strState = n.Attributes.getNamedItem("NewState").Text         If strState <> "" And ((intNrStates = 0) Or _                                Not StateExist(strState, strStateArray)) Then             intNrStates = intNrStates + 1             ReDim Preserve strStateArray(intNrStates - 1)             strStateArray(intNrStates - 1) = strState             strStateList = strStateList & " " & strStateArray(intNrStates - 1)         End If     Next          MsgBox "The states are: " & strStateList     ' Clean up     rec.Close     cnn.Close     Set rec = Nothing     Set fld = Nothing     Set cnn = Nothing     Set xmlDoc = Nothing     Set nodelist = Nothing     Set n = Nothing End Sub      Private Sub Command1_Click()     PopulateStates End Sub 

Deploying Workflow Solutions

Once you've drawn out your process, implemented your conditions and actions, and written your script, the next step is to deploy your workflow process to a folder. The Workflow Designer makes this step easy because you can save the workflow process into any folder in which you have permissions to create a workflow. Figure 17-16 shows how to select a public folder via the Save Workflow Process To Folder dialog box in the Workflow Designer.

click to expand
Figure 17-16: The Save Workflow Process To Folder dialog box makes it easy to deploy workflow solutions.

In some cases, you might need to programmatically deploy your solutions. Unfortunately, the Workflow Designer doesn't have an object model that you can automate to use the Save Workflow Process To Folder feature. Instead, you have to write some code to deploy your workflow process. If you do so, you should first use the Workflow Designer to export to XML the action table for your workflow process.

The following code, taken from the Training application setup program, shows how to deploy your workflow application programmatically. You'll notice the following steps in the code:

  1. Create your common script file (if necessary).

  2. Create a workflow ProcessDefinition object.

  3. In the ProcessDefinition object, add your action table by creating a new RecordSet object and using the XML features of ADO to load the XML version of the action table that the Workflow Designer saved for you.

  4. As part of creating your ProcessDefinition object, select your audit trail provider, set the location of your common script file, and set the mode that the workflow process should run under (restricted or privileged).

  5. Save the ProcessDefinition object into the folder.

  6. Create the event registration items for the OnSyncSave , OnSyncDelete , and OnTimer events. On the server events registration item for OnSyncSave and OnSyncDelete , set the properties in the http://schemas.microsoft.com/ cdo/workflow/ namespace ”for example, the pointer to the default process definition for the folder, whether ad hoc workflows are allowed in the folder, whether to enable script debugging, and whether to log successful state transitions to the audit trail provider.

 Private Sub AddWorkflowProcess()     Dim oRS As New ADODB.RecordSet     Dim oPD As New CDOWF.ProcessDefinition          On Error GoTo errHandler          'Add the common script file     Dim oScriptRec As New ADODB.Record     Dim oStream As New ADODB.Stream     'Load the script file     Dim fso As New Scripting.FileSystemObject     Dim ofile As TextStream          Set ofile = fso.OpenTextFile(App.Path & "\commonscript.txt")          strCommonScript = ofile.ReadAll          oScriptRec.Open strPath & "/Pending/commonscript", oConnection, _                     adModeReadWrite, adCreateNonCollection     oStream.Open oScriptRec, adModeReadWrite, adOpenStreamFromRecord     With oStream         .Charset = "unicode"         .Type = adTypeText         .Position = 0         .SetEOS         .WriteText strCommonScript         .Position = 0         .Flush         .Close     End With     strScriptURL = oScriptRec.Fields("DAV:href").Value          'Load the action table     oRS.Open App.Path & "\actiontable.xml"     With oPD         .ActionTable = oRS         .AuditTrailProvider = "CDOWF.AuditTrailEventLog"         .CommonScriptURL = strScriptURL         .Mode = cdowfPrivilegedMode         .Fields("DAV:ishidden") = True     End With          oPD.DataSource.SaveTo strPath & "/Pending/WFDEF", oConnection, _                           adModeReadWrite, adCreateNonCollection          strPDHREF = oPD.Fields("DAV:href").Value          'Create the event registrations          'First create the timer event     arrRequired = GenerateRequiredEventArray("", "ontimer", _                   "CdoWfEvt.EventSink.1", "","")     strNow = Now     arrOptional = GenerateOptionalEventArray("", "", "", "", 15, _                                              strNow, "")          CreateEvtRegistration oConnection, strPath & "Pending/timer", _                           arrRequired, arrOptional, True          'Create the OnSyncSave and onSyncDelete registration     arrRequired = GenerateRequiredEventArray("WHERE ""DAV:ishidden"" = " _                                   & "false AND ""DAV:isfolder"" = false", _                                   "onsyncsave;onsyncdelete", _                                   "CdoWfEvt.EventSink.1", "", "")     'Create a new array and add some further properties for workflow     Dim arrWorkflowRequired(6, 1)     For i = LBound(arrRequired) To UBound(arrRequired)         arrWorkflowRequired(i, 0) = arrRequired(i, 0)         arrWorkflowRequired(i, 1) = arrRequired(i, 1)     Next     'Add workflow properties     arrWorkflowRequired(3, 0) = _         "http://schemas.microsoft.com/cdo/workflow/defaultprocdefinition"     arrWorkflowRequired(3, 1) = strPDHREF     arrWorkflowRequired(4, 0) = _         "http://schemas.microsoft.com/cdo/workflow/adhocflows"     arrWorkflowRequired(4, 1) = 0     arrWorkflowRequired(5, 0) = _         "http://schemas.microsoft.com/cdo/workflow/enabledebug"     arrWorkflowRequired(5, 1) = False     arrWorkflowRequired(6, 0) = _         "http://schemas.microsoft.com/cdo/workflow/disablesuccessentries"     arrWorkflowRequired(6, 1) = False   'Enable success entries          CreateEvtRegistration oConnection, strPath & "Pending/workflowreg", _                           arrWorkflowRequired, arrWorkflowOptional, True     Exit Sub errHandler:     MsgBox "Error in AddWorkflowProcess.  Error " & Err.Number & " " & _            Err.Description     End End Sub 

Workflow Security and Deployment Gotchas

One gotcha you should be aware of when you deploy workflow solutions is a feature that can trip you up if you don't understand it. The workflow event handlers have two COM+ roles that they implement, which you saw earlier: CanRegisterWorkflow and PrivilegedWorkflowAuthors . If you don't understand what these roles are used for and how the workflow engine uses them, you might run into some issues. This section outlines how these two roles and the workflow engine work together.

The CanRegisterWorkflow role is used when someone attempts to register for the Workflow event handler. The Workflow event handler implements ICreateRegistration , so when someone attempts to register for the Workflow event handler, the event handler is called to verify whether it wants to allow the registration to go through. The Workflow event handler calls the COM+ method IsUserInRole(CanRegisterWorkflow) to determine whether the user attempting the registration is authorized to do so. If this call returns True , the Workflow event handler allows the registration to go through.

The PrivilegedWorkflowAuthors role is used to ensure that any executable workflow code to be run in Privileged mode has not been tampered with by an unauthorized person. Here's the scenario: User A has privileged permissions and registers a new workflow that contains a script to run in privileged mode. User B is allowed to write only sandboxed workflows, but he does have write access to user A's script file. User B later inserts malicious script into these workflow files, knowing it will be run in Privileged mode because User A has privileged workflow permissions.

To prevent this, at run time, the Workflow event sink checks to see which user last modified and saved the process definition, the script, and the event handler registration item. For each of these SIDs, the event sink calls COM+ IsUserInRole(PrivilegedWorkflowAuthors) . If any of these documents were last modified by a nonprivileged person, the workflow engine knows that the files were tampered with. The workflow engine immediately stops execution and logs a security error.

So, if you want to run privileged mode workflows, you must make sure that the account used to save all the critical documents, such as the process definition, scripts, and event registrations, is a member of the PrivilegedWorkflowAuthors role.




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