Friday, December 15, 2006

Calendar Conflicts in Cached Exchange Mode in 2K3 sp2

Administrators universally agree that cached exchange mode in Exchange 2003 is a good thing providing added value and reliability to their infrastructure.

Programmers who write applications which interact with the calendaring functions unfortunately, are tearing their hair out and cursing their fates.


The situation:

We've built Rhino, an Exchange (server-side) solution to intercept and process calendar messages. The solution uses a synchronous an Exchange event sink. The event sink understands certain rules (via an XML file) that allow the system admin to :

  • Process meeting requests, replies, or both;
  • Skip managed resources;
  • Process requests as tentative, accept, or decline, and show the meeting as free, busy, or out of office
  • Process replies - keep/delete accepts/declines/tentatives or any reply with a reply from the end user.
  • All replies are processed to update the owner's tracking tab.

We manage the messages at the server side using CDOEX (version 6.5.7638.1) and EXOLEDB (version 6.5.7650.7) embedded within a synchronous event sink. This event sink works as designed when clients are connected to the Exchange server without Cached Mode.

The problem:

The problems that we are seeing is that the sink generates an Outlook Sync Issues "Conflicts" when clients use outlook in 'cached exchange mode'. i.e. clients see message on the meeting ... "You made changes to another copy of this item. This is the most recent version...."
Also when an Outlook meeting owner attempts to update one occurrence of the meeting, and changes the location, or adds an agenda, the meeting owner's calendar doesn't always get updated with the changes that he/she just made (e. g., the agenda gets lost, the location disappears from the subject line annotation in the calendar view, but still exists in the meeting detail view); and agendas are never received by the end user, until the meeting owner updates the same meeting occurrence twice.

Clients most frequently see this conflict message when they update meetings, or change a single occurrence of a recurring meeting.

What is going on here:

This is a bug in the ICS (Incremental Change System) for Exchange 2003 in cached mode.

Steps to reproduce:

  1. Create a Synchronous event sink in VB 6 using the event sink wizard from the exchange SDK.
  2. Add in code to detect and process calendar messages (see MSDN: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/e2k3/e2k3/_cdo_processing_meeting_request_responses.asp
  3. Compile the code, register the event sink on your system mailboxes
  4. Create a recurring meeting using Outlook in Cached Exchange mode.
  5. Have that “guest” accept the meeting
  6. Open an occurrence of the meeting, and change the location, agenda, etc. Send an update, then switch to the inbox.
  7. Have the “guest” accept the update.
  8. Open the Guest’s response message (in your inbox)
  9. Switch to the calendar and open the meeting (you should see a conflict)

We've added the VB source code for main module as a comment to this post. The VB Project is available to those who request it.

Remedies we have tried already:

Microsoft support suggested their Auto Accept Agent that is implemented as an asynchronous event sink, but again you need to install this on every mailbox:
883130 The Auto Accept Agent Deployment and Administration Guide is now available
http://support.microsoft.com/default.aspx?scid=kb;EN-US;883130
903290 You should not register a resource mailbox for Auto Accept Agent in Exchange 2003 when you set up a resource for direct booking in Outlook
http://support.microsoft.com/default.aspx?scid=kb;EN-US;903290

Note: The auto accept agent is also an async OnSave agent.

Non-viable options:

  • Don't use cached mode with Outlook 2003 (This isn't an acceptable solution)
  • Asynchronous sinks (We tries these and it still generates conflicts, aside from which they need to be registered on each mailbox)
  • Direct booking (Really geared towards resource mailboxes)
  • Server-side rules could be set up to try to handle this (Again this would have to be implemented on every mailbox where we want this functionality -- more complex administration overhead)

Our solution:

We're implementing an Outlook best-practices client-side COM+ add-in for Outlook 2003 / 2007. Please feel free to contact us if you want to try it out.

1 comment:

zyg said...

Attribute VB_Name = "msModule"

Public Const LogFile As String = "StoreSink.log" 'Name of the file where a log of all the activities in this sink will be stored.
Public Const PERFORM_FILE_LOGGING As Boolean = True 'Set this constant to False to prevent any event logging from occurring
Public Const PERFORM_ERROR_LOGGING As Boolean = True 'Set this constant to False to prevent any error logging from occurring
Public Const DATE_TIME_FORMAT As String = "dddd, mmmm dd, yyyy, hh:nn:ss AM/PM" 'Set the format of the date-time that will get written
'
Function ProcessBeginSave(ByVal pEventInfo As Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, ByVal lFlags As Long) As Boolean

'This function is called when the Item is being saved and the OnSyncSave
'event is fired for the first time in this event sink. Returns True if
'successful, False if not.

Dim pDispEventInfo As Exoledb.IExStoreDispEventInfo 'Declare a pointer to the interface that returns information about the executing store event.
Dim recItem As ADODB.Record 'Record Object that will point to the item that triggered this event.
'============================================================================================
'ADDED BY SUMATRA
ProcessBeginSave = True
Exit Function

On Error GoTo Errorhandler

'Assume that the function is successful. If any checks which may be coded below fail,
' or if an error occurs, then return False, which would cause the transaction to be aborted.
ProcessBeginSave = True

'Write to the log file that this function has been called.
Call EventLog("OnSyncSave", bstrURLItem, lFlags, "Begin Phase")

Set pDispEventInfo = pEventInfo 'Assign the passed IExStoreEventInfo object reference
Set recItem = pDispEventInfo.EventRecord 'get the Record object bound to the item that triggered the event

'Evaluate the different scenarios in which the Save might have been triggered.
If lFlags And EVT_NEW_ITEM Then
'The item being saved is a new item.
'Some role checking can be done - like below
'If GetSecurityCallContext.IsSecurityEnabled Then
'If Not GetSecurityCallContext.IsCallerInRole("Example Role") Then
''User is not in a defined role. Handle the condition here
''like preventing the delete by aborting the transaction; i.e.
'pDispEventInfo.AbortChange
'End If
'End If
'The item can be processed below.
With recItem
'Process the item's properties, set default values, validate fields etc. here
End With
End If
'The following blocks of code are similar to the previous one. Similar code can be applied
'here as shown above, like role checking etc.
If lFlags And EVT_REPLICATED_ITEM Then
'The item is being saved as a result of replication
End If
If lFlags And EVT_IS_DELIVERED Then
'The item is being saved as the result of message delivery
End If
If lFlags And EVT_INVALID_URL Then
'The URL passed to the sink is invalid.
End If
If lFlags And EVT_IS_COLLECTION Then
'The item being saved is a collection.
End If
If lFlags And EVT_ERROR Then
' An error occurred in the event.
End If

Exit Function
Errorhandler:
'Some error has occurred. The function can return False (as in the next line) so that the save can be aborted.
'ProcessBeginSave = False
'Log any errors that occur in this function.
Call ErrorLog("ProcessBeginSave", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
End Function


Sub ProcessCommitSave(ByVal pEventInfo As Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, ByVal lFlags As Long)

'This function is called after the save of the Item has been committed.
'NOTE: The item is now read-only. Changes made to the item here will not be reflected in the store.

Dim pDispEventInfo As Exoledb.IExStoreDispEventInfo 'Declare a pointer to the interface that returns information about the executing store event.
Dim recItem As ADODB.Record 'Record Object that will point to the item that triggered this event.
'============================================================================================
' Added by Sumatra
'


Dim iCalMsg As CDO.CalendarMessage
Dim iMbx As IMailbox
Dim person As New CDO.person
Dim msg_to As String
Dim strURL As String


On Error GoTo Err_ProcessCommitSave


Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "Startup-Sumatra")

If LenB(bstrURLItem) = 0 Then
Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "Empty URL")

End If

Set pDispEventInfo = pEventInfo
Set recItem = pDispEventInfo.EventRecord 'get the Record object bound to the item that triggered the event

Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "Got event record")


'Evaluate the different scenarios in which the Save might have been triggered.
If lFlags And EVT_NEW_ITEM Then
'The item being saved is a new item.
End If

If lFlags And EVT_REPLICATED_ITEM Then
'The item is being saved as a result of replication
End If

If lFlags And EVT_IS_DELIVERED Then
'The item is being saved as the result of message delivery

Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "EVT_IS_DELIVERED-Sumatra")


'check if a calendar message
'contentclass"" = 'urn:content-classes:calendarmessage'"
If recItem.Fields("DAV:contentclass") = "urn:content-classes:calendarmessage" Then

Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "IS Calendar message")

'Parse fields to determine who the person is
msg_to = Split(bstrURLItem, "/")(6) & "@" & Split(bstrURLItem, "/")(4)
strURL = "mailto:" & msg_to
Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "Open Person:" & strURL & "-Sumatra")
Set person = New CDO.person 'COULD ALSO CreateObject("CDO.Person")
person.DataSource.Open strURL

' Now get the mailbox
Set iMbx = person.GetInterface("IMailbox")

' Open the calendar message
Call EventLog("OnSyncSave", bstrURLItem, lFlags, "Open Calendar Message-Sumatra")
Set iCalMsg = New CalendarMessage
iCalMsg.DataSource.Open bstrURLItem, , adModeReadWrite


Call ProcessResponse(iCalMsg, iMbx)

Else
Call EventLog("ProcessCommitSave", bstrURLItem, lFlags, "NOT Calendar message; contentclass: " & recItem.Fields("contentclass").Value)

End If 'test if calendar message

End If

If lFlags And EVT_INVALID_URL Then
'The URL passed to the sink is invalid.
End If

If lFlags And EVT_IS_COLLECTION Then
'The item being saved is a collection.
End If

If lFlags And EVT_ERROR Then
' An error occurred in the event.
End If


Exit_ProcessCommitSave:
'release objects
Set person = Nothing
Set iMbx = Nothing
Set iCalMsg = Nothing
Set recItem = Nothing
Set pDispEventInfo = Nothing
Exit Sub

Err_ProcessCommitSave:
'Log any errors that occur in this subroutine
Call ErrorLog("ProcessCommitSave", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
Err.Clear
Resume Exit_ProcessCommitSave

End Sub



'============================================================================================
'ADDED BY SUMATRA FROM
'SOURCE:
'http://msdn.microsoft.com/library/default.asp?url=/library/en-us/e2k3/e2k3/_cdo_processing_meeting_request_responses.asp

Sub ProcessResponse(iCalMsg As CDO.CalendarMessage, iMbx As IMailbox)
' Reference to Microsoft ActiveX Data Objects 2.5 Library
' Reference to Microsoft CDO for Exchange 2000 Library

' Note: It is recommended that all input parameters be validated when they are
' first obtained from the user or user interface.
Dim Rec As New ADODB.Record
Dim Conn As New ADODB.Connection

'============================================================================================
'MODIFIED BY SUMATRA
' below is commented out.....causes compiler error
' Dim iCalMsg As New CalendarMessage
Dim iCalPart As ICalendarPart
Dim iAppt As CDO.Appointment
On Error GoTo Err_ProcessResponse

Call EventLog("ProcessResponse", bstrURLItem, lFlags, "Started; Subj=" & iCalMsg.Message.Subject & " -Sumatra")

App.LogEvent "iCalMsg GUID: " & iCalMsg.Message.Fields("http://schemas.microsoft.com/exchange/permanenturl").Value

Conn.Provider = "ExOLEDB.DataSource"
Conn.Open iMbx.BaseFolder

For Each iCalPart In iCalMsg.CalendarParts
Set iAppt = iCalPart.GetUpdatedItem(iMbx.Calendar)

App.LogEvent "Appt in CalPart GUID: " & iAppt.Fields("http://schemas.microsoft.com/exchange/permanenturl").Value


Call EventLog("ProcessResponse", bstrURLItem, lFlags, "Got Appt from Calpart; Subj=" & iCalMsg.Message.Subject & " -Sumatra")

Select Case iCalPart.CalendarMethod

Case "REQUEST"
Call EventLog("ProcessResponse", bstrURLItem, lFlags, "Request; subj=" & iCalMsg.Message.Subject & "-Sumatra")

Case "REPLY" ' Make sure this is a reply
App.LogEvent "MSTEST2 Item URL: " & iAppt.Subject
App.LogEvent "MSTEST2 Fired at (Before the save): " & Now

iAppt.DataSource.Save

App.LogEvent "MSTEST2 Fired at (After the save): " & Now

Call EventLog("ProcessResponse", bstrURLItem, lFlags, "YEA!! Reply Saved-CalUpdated; subj=" & iCalMsg.Message.Subject & "-Sumatra")

Case "CANCEL"
Call EventLog("ProcessResponse", bstrURLItem, lFlags, "Cancellation; subj=" & iCalMsg.Message.Subject & "-Sumatra")

Case Else
Call EventLog("ProcessResponse", bstrURLItem, lFlags, "Not a REPLY: " & iCalPart.CalendarMethod & "; subj=" & iCalMsg.Message.Subject & "-Sumatra")

End Select
Next

Exit_ProcessResponse:
' Clean up.
Conn.Close
Set Conn = Nothing
Set Rec = Nothing
Set iAppt = Nothing
Set iCalPart = Nothing

Exit Sub


Err_ProcessResponse:
'Log any errors that occur in this subroutine
Call ErrorLog("ProcessResponse-ERROR", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
Err.Clear
Resume Exit_ProcessResponse


End Sub
'============================================================================================


Sub ProcessAbortSave(ByVal pEventInfo As Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, ByVal lFlags As Long)

'This subroutine is called when saving the item is aborted. The Save is being cancelled here.
'NOTE: The item does not get into the Exchange Store here.

Dim pDispEventInfo As Exoledb.IExStoreDispEventInfo 'Declare a pointer to the interface that returns information about the executing store event.
Dim recItem As ADODB.Record 'Record Object that will point to the item that triggered this event.
'============================================================================================
'ADDED BY SUMATRA
Exit Sub

On Error GoTo Errorhandler

'Write to the log file that this subroutine has been called.
Call EventLog("OnSyncSave", bstrURLItem, lFlags, "Abort Phase")

Set pDispEventInfo = pEventInfo 'Assign the passed IExStoreEventInfo object reference
Set recItem = pDispEventInfo.EventRecord 'get the Record object bound to the item that triggered the event

'Evaluate the different scenarios in which the Save might have been triggered.
If lFlags And EVT_NEW_ITEM Then
'The item being saved is a new item.
End If
If lFlags And EVT_REPLICATED_ITEM Then
'The item is being saved as a result of replication
End If
If lFlags And EVT_IS_DELIVERED Then
'The item is being saved as the result of message delivery
End If
If lFlags And EVT_INVALID_URL Then
'The URL passed to the sink is invalid.
End If
If lFlags And EVT_IS_COLLECTION Then
'The item being saved is a collection.
End If
If lFlags And EVT_ERROR Then
' An error occurred in the event.
End If

Exit Sub
Errorhandler:
'Log any errors that occur in this subroutine
Call ErrorLog("ProcessAbortSave", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
End Sub


Function ProcessBeginDelete(ByVal pEventInfo As Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, ByVal lFlags As Long) As Boolean

'This function is called when the Item is being deleted and the OnSyncDelete
'event is fired for the first time in this event sink. Returns True if
'successful, False if not.

Dim pDispEventInfo As Exoledb.IExStoreDispEventInfo 'Declare a pointer to the interface that returns information about the executing store event.
Dim recItem As ADODB.Record 'Record Object that will point to the item that triggered this event.
'============================================================================================
'ADDED BY SUMATRA
ProcessBeginDelete = True
Exit Function

On Error GoTo Errorhandler

'Assume that the function is successful. If any checks which may be coded below fail,
' or if an error occurs, then return False, which would cause the transaction to be aborted.
ProcessBeginDelete = True

'Write to the log file that this function has been called.
Call EventLog("OnSyncDelete", bstrURLItem, lFlags, "Begin Phase")

Set pDispEventInfo = pEventInfo 'Assign the passed IExStoreEventInfo object reference
Set recItem = pDispEventInfo.EventRecord 'get the Record object bound to the item that triggered the event

'Evaluate the different scenarios in which the delete might have been triggered.
If lFlags And EVT_MOVE Then
'The item was moved over resulting in an implicit delete.
'Some role checking can be done - like below
'If GetSecurityCallContext.IsSecurityEnabled Then
'If Not GetSecurityCallContext.IsCallerInRole("Example Role") Then
''User is not in a defined role. Handle the condition here
''like preventing the delete by aborting the transaction; i.e.
'pDispEventInfo.AbortChange
'End If
'End If
'The item can be processed below.
With recItem
'Access the item's properties, do some actions etc.
End With
End If
'The following blocks of code are similar to the previous one. Similar code can be applied
'here as shown above, like role checking etc.
If lFlags And EVT_COPY Then
'The item was copied over resulting in an implicit delete.
End If
If lFlags And EVT_IS_COLLECTION Then
'The item being deleted is a collection.
End If
If lFlags And EVT_INVALID_URL Then
' The URL passed to the sink as invalid.
End If
If lFlags And EVT_ERROR Then
'An error occurred in the event.
End If

Exit Function
Errorhandler:
'Some error has occurred. The function may return False (as in the next line) so that the delete can be aborted.
'ProcessBeginSave = False
'Log any errors that occur in this function.
Call ErrorLog("ProcessBeginDelete", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
End Function


Sub ProcessCommitDelete(ByVal pEventInfo As Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, ByVal lFlags As Long)

'This function is called after the deletion of the Item has been committed.
'NOTE: The item can no longer be changed (read-only)

Dim pDispEventInfo As Exoledb.IExStoreDispEventInfo 'Declare a pointer to the interface that returns information about the executing store event.
Dim recItem As ADODB.Record 'Record Object that will point to the item that triggered this event.
'============================================================================================
'ADDED BY SUMATRA
Exit Sub

On Error GoTo Errorhandler

'Write to the log file that this subroutine has been called.
Call EventLog("OnSyncDelete", bstrURLItem, lFlags, "Commit Phase")


Set pDispEventInfo = pEventInfo 'Assign the passed IExStoreEventInfo object reference
Set recItem = pDispEventInfo.EventRecord 'get the Record object bound to the item that triggered the event

'Evaluate the different scenarios in which the delete might have been triggered.
If lFlags And EVT_MOVE Then
'The item was moved over resulting in an implicit delete.
End If
If lFlags And EVT_COPY Then
'The item was copied over resulting in an implicit delete.
End If
If lFlags And EVT_IS_COLLECTION Then
'The item being deleted is a collection.
End If
If lFlags And EVT_INVALID_URL Then
' The URL passed to the sink as invalid.
End If
If lFlags And EVT_ERROR Then
'An error occurred in the event.
End If

Exit Sub
Errorhandler:
'Log any errors that occur in this subroutine
Call ErrorLog("ProcessCommitDelete", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
End Sub


Sub ProcessAbortDelete(ByVal pEventInfo As Exoledb.IExStoreEventInfo, ByVal bstrURLItem As String, ByVal lFlags As Long)

'This subroutine is called when deletion of the item is aborted.

Dim pDispEventInfo As Exoledb.IExStoreDispEventInfo 'Declare a pointer to the interface that returns information about the executing store event.
Dim recItem As ADODB.Record 'Record Object that will point to the item that triggered this event.
'============================================================================================
'ADDED BY SUMATRA
Exit Sub

On Error GoTo Errorhandler

'Write to the log file that this subroutine has been called.
Call EventLog("OnSyncDelete", bstrURLItem, lFlags, "Abort Phase")

Set pDispEventInfo = pEventInfo 'Assign the passed IExStoreEventInfo object reference
Set recItem = pDispEventInfo.EventRecord 'get the Record object bound to the item that triggered the event

'Evaluate the different scenarios in which the delete might have been triggered.
If lFlags And EVT_MOVE Then
'The item was moved over resulting in an implicit delete.
End If
If lFlags And EVT_COPY Then
'The item was copied over resulting in an implicit delete.
End If
If lFlags And EVT_IS_COLLECTION Then
'The item being deleted is a collection.
End If
If lFlags And EVT_INVALID_URL Then
' The URL passed to the sink as invalid.
End If
If lFlags And EVT_ERROR Then
'An error occurred in the event.
End If

Exit Sub
Errorhandler:
'Log any errors that occur in this subroutine
Call ErrorLog("ProcessAbortDelete", CStr(Err.Number) & vbTab & Err.Description, bstrURLItem, lFlags)
End Sub



Sub ErrorLog(EventName As String, errString As String, ByVal bstrURLItem As String, Optional ByVal lFlags As Long = 0)

' JEFFGA
Exit Sub
' JEFFGA

If Not PERFORM_ERROR_LOGGING Then 'Check if logging is turned off
Exit Sub ' If so, do not perform logging
End If
'this function writes to the log file details of any error that occurred. It is called from the error handler in all events.

Dim FSO As Scripting.FileSystemObject
Dim WinTmpFile As String
Dim OnSaveFile As Scripting.TextStream


Set FSO = New Scripting.FileSystemObject 'Instantiate the FileSystemObject
' WinTmpFile = FSO.GetAbsolutePathName("") & "\" & LogFile
' WinTmpFile = FSO.GetSpecialFolder(SystemFolder).Path & "\" & LogFile 'Set the path of the log file to be in the Windows System Folder
WinTmpFile = "C:\TEMP\" & LogFile

Set OnSaveFile = FSO.OpenTextFile(WinTmpFile, 8, True) 'Open the file for Appending
OnSaveFile.WriteLine (String(35, "*") & "E R R O R " & String(35, "*")) 'Write a separator line
OnSaveFile.WriteLine Format(Now, DATE_TIME_FORMAT) 'Write the current date and time
OnSaveFile.WriteLine (" Event Name :" & vbTab & EventName) 'Write the Event Name
OnSaveFile.WriteLine (" URL of Source Item:" & vbTab & bstrURLItem) 'Write the URL of the Event source
OnSaveFile.WriteLine (ReturnEXOLEDBFlags(lFlags)) 'Write the flags that are currently set
OnSaveFile.WriteLine (errString) 'Write the Error Text
OnSaveFile.WriteLine (String(80, "-")) 'Write a separator line to the file
OnSaveFile.WriteBlankLines (1) 'Insert a blank line into the file
OnSaveFile.Close 'Close the log file
Set FSO = Nothing

End Sub


Sub EventLog(EventName As String, ByVal bstrURLItem As String, Optional ByVal lFlags As Long = 0, Optional bstrComment As String = "")

' JEFFGA
Exit Sub
' JEFFGA

'This function writes to the log file details of an event being executed. It is called from each event procedure.
If Not PERFORM_FILE_LOGGING Then 'Check if logging is turned off
Exit Sub ' If so, do not perform logging
End If

Dim FSO As Scripting.FileSystemObject
Dim WinTmpFile As String
Dim OnSaveFile As Scripting.TextStream

Set FSO = New Scripting.FileSystemObject 'Instantiate the FileSystemObject
'WinTmpFile = FSO.GetAbsolutePathName("") & "\" & LogFile
WinTmpFile = "C:\TEMP\" & LogFile

' WinTmpFile = FSO.GetSpecialFolder(SystemFolder).Path & "\" & LogFile 'Set the path of the log file to be in the Windows System Folder
Set OnSaveFile = FSO.OpenTextFile(WinTmpFile, 8, True) 'Open the file for Appending
OnSaveFile.WriteLine Format(Now, DATE_TIME_FORMAT) 'Write the current date and time
OnSaveFile.WriteLine (" Event Name:" & EventName) 'Write the Event Name
OnSaveFile.WriteLine (" URL of Source Item:" & vbTab & bstrURLItem) 'Write the URL of the Event source
OnSaveFile.WriteLine (ReturnEXOLEDBFlags(lFlags)) 'Write the flags that are currently set
OnSaveFile.WriteLine (" COMMENT:" & vbTab & bstrComment) 'Write the comment, if any
OnSaveFile.WriteLine (String(80, "-")) 'Write a separator line to the file
OnSaveFile.WriteBlankLines (1) 'Insert a blank line into the file
OnSaveFile.Close 'Close the log file
Set FSO = Nothing

End Sub


Function ReturnEXOLEDBFlags(lFlags As Long, Optional blnFreshy As Boolean = True) As String

'This function returns a string containing a list of all the flags that are currently set.
Dim strBuff As String
strBuff = " Flags (" & "0x" & Hex(lFlags) & "):"
If (lFlags And EVT_NEW_ITEM) > 0 Then
strBuff = strBuff & " EVT_NEW_ITEM "
End If
If (lFlags And EVT_IS_COLLECTION) > 0 Then
strBuff = strBuff & " EVT_IS_COLLECTION "
End If
If (lFlags And EVT_REPLICATED_ITEM) > 0 Then
strBuff = strBuff & " EVT_REPLICATED_ITEM "
End If
If (lFlags And EVT_IS_DELIVERED) > 0 Then
strBuff = strBuff & " EVT_IS_DELIVERED "
End If
If (lFlags And EVT_INITNEW) > 0 Then
strBuff = strBuff & " EVT_INITNEW "
End If
If (lFlags And EVT_MOVE) > 0 Then
strBuff = strBuff & " EVT_MOVE "
End If
If (lFlags And EVT_COPY) > 0 Then
strBuff = strBuff & " EVT_COPY "
End If
If (lFlags And EVT_SYNC_BEGIN) > 0 Then
strBuff = strBuff & " EVT_SYNC_BEGIN "
End If
If (lFlags And EVT_SYNC_COMMITTED) > 0 Then
strBuff = strBuff & " EVT_SYNC_COMMITTED "
End If
If (lFlags And EVT_SYNC_ABORTED) > 0 Then
strBuff = strBuff & " EVT_SYNC_ABORTED "
End If
If (lFlags And EVT_INVALID_SOURCE_URL) > 0 Then
strBuff = strBuff & " EVT_INVALID_SOURCE_URL "
End If
If (lFlags And EVT_INVALID_URL) > 0 Then
strBuff = strBuff & " EVT_INVALID_URL "
End If
ReturnEXOLEDBFlags = strBuff

End Function