Become a Columnist Microsoft Exchange Site Microsoft Support SiteMSDN Exchange Site

   

Subscribe to OutlookExchange
Anderson Patricio
Ann Mc Donough
Bob Spurzem
Brian Veal
Catherine Creary
Cherry Beado
Colin Janssen
Collins Timothy Mutesaria
Drew Nicholson
Fred Volking
Glen Scales
Goran Husman
Guy Thomas
Henrik Walther
Jason Sherry
Jayme Bowers
John Young
Joyce Tang
Justin Braun
Konstantin Zheludev
Kristina Waters
Kuang Zhang
Mahmoud Magdy
Martin Tuip
Michael Dong
Michele Deo
Mitch Tulloch
Nicolas Blank
Pavel Nagaev
Ragnar Harper
Ricardo Silva
Richard Wakeman
Russ Iuliano
Santhosh Hanumanthappa
Steve Bryant
Steve Craig
Todd Walker
Tracey J. Rosenblath
 
 

Catch Un-dismissed Calendar Reminders Sink         Download Scripts
Updated 05/2004

The following sink is designed to catch any meeting or appointment reminders that are set on a mailbox that haven't been dismissed and then perform some form of action. This action could be to send an email to an SMS gateway or run a script that sends a Instant message to a LCS or a MSN messenger user or use SAPI and TAPI  to call a phone number form active directory and tell the person using speech that they are about to miss an appointment or something else just as inexplicable. The way I've implemented this sink is different from all the other sinks in my column, for this sink I've used an on_timer event. On timer events are a lot like the windows scheduler service (or Scheduled Tasks) they essentially trigger an event at intervals you configure, you can configure a start time, end time and interval (In minutes) that the event will occur at. The way I've used the ontimer event in this article is to fire an event every 10 minutes that checks to see if there are any meeting starting in the next 10 minutes that have a reminder set on. This brings us to reminders (something that is very thinly documented)  there are a few ways you could find appointments that have that reminder set on, the method I've used in this script is to query the /NON_IPM_SUBTREE/reminders search folder (which is what Outlook creates and uses). The reminders search folder is a hidden folder that sits in the Root mailbox folder usually you can't get to it without using a program such as Mdbvw32.exe or OutlookSPY. Because the reminders folder is a search folder its doesn't actually contain any objects just a list of objects that match its search range and for the reminders folder the search range is calendar appointments with the reminder set on. When a person dismisses a calendar appointment it turns the reminder attribute on that appointment object off which will remove it from the reminders search folder list. So the way this sink works is it queries the reminders folder of the mailbox the ontimer event fired on and returns a record-set of appointments that are starting in the next 10 minutes that have the reminder attribute still set on.

The Code

I've used a two step approach to firing this event that adds a layer of abstraction for the Exchange event sinks. The event sink code looks as follows, the script expects that the calsink1.vbs script is located in a d:\scripts directory on the server.

<SCRIPT LANGUAGE="VBScript">

Sub ExStoreEvents_OnTimer(bstrURLItem, iFlags)
set WshShell = CreateObject("WScript.Shell")
strrun = WshShell.run ("d:\scripts\calsink1.vbs " & chr(34) & bstrURLItem & chr(34))
set WshShell = nothing
End Sub

</SCRIPT>

Calsink1.vbs

This is the main block of code that is called by the event sink code, the front end of the code takes the file url of the timer event sink object and then separates the mailbox name of the calling object which is used in the query of the reminders folder.

Set obArgs = WScript.Arguments
towrite = lcase(obArgs.Item(0))
uname = mid(towrite,(instr(towrite,"mbx")+4),(instr(towrite,"calendar") - (instr(towrite,"mbx")+5)))

The next part of the code deals with setting up some time variables to query with, the first part of the code uses that ActiveTimeBias registry entry to work out the GMT offset to convert the query times to UTC, to do the conversion the dateadd function is used. The next part uses dateadd to get two different time variables that are 10 minutes apart. After this the isodateit function is called which converts the date query values into ISO date format which is required to perform a Exoledb query. The isodateit function uses day, month and year functions to split the date into ISO  yyyy-mm-ddThh:mm:ssZ format.

strValueName = "HKLM\SYSTEM\CurrentControlSet\Control\TimeZoneInformation\ActiveTimeBias"
minTimeOffset = shell.regread(strValueName)
toffset = datediff("h",DateAdd("n", minTimeOffset, now()),now())
dtListFrom = DateAdd("n", minTimeOffset, now())
dtListTo = isodateit(DateAdd("n",10,dtListFrom))
dtListFrom = isodateit(dtListFrom)
function isodateit(datetocon)
	strDateTime = year(datetocon) & "-"
	if (Month(datetocon) < 10) then strDateTime = strDateTime & "0"
	strDateTime = strDateTime & Month(datetocon) & "-"
	if (Day(datetocon) < 10) then strDateTime = strDateTime & "0"
	strDateTime = strDateTime & Day(datetocon) & "T" & formatdatetime(datetocon,4) & ":00Z"
	isodateit = strDateTime
end function

 After we've got all the time variables into ISO format then we can get down to the business of querying the reminders folder which looks something like this.

CalendarURL = "file://./backofficestorage/yourdomain.com.au/MBX/" & uname & "/NON_IPM_SUBTREE/reminders"
Conn.Provider = "ExOLEDB.DataSource"
Rec.Open CalendarURL
Set Rs.ActiveConnection = Rec.ActiveConnection
Rs.Source = "SELECT ""DAV:href"", " & _
                  " ""urn:schemas:httpmail:subject"", " & _
                  " ""urn:schemas:calendar:dtstart"", " & _
                  " ""urn:schemas:calendar:dtend"", " & _
		  " ""urn:schemas:calendar:organizer"", " & _
		  " ""urn:schemas:calendar:location"", " & _
		  " ""DAV:contentclass"" " & _
            	  "FROM scope('shallow traversal of """ & CalendarURL & """') " & _
		 "WHERE (""urn:schemas:calendar:dtstart"" >= CAST(""" & dtListFrom & """ as 'dateTime')) " & _
                 "AND (""urn:schemas:calendar:dtstart"" < CAST(""" & dtListTo & """ as 'dateTime'))" & _
		 " AND ""DAV:contentclass"" = 'urn:content-classes:appointment'" & _
		 "ORDER BY ""urn:schemas:calendar:dtstart"" ASC"
         
Rs.Open

You need to change the Calendar URL to reflect your own Exoledb path you can find this out by looking at the M: drive of your server, if you don't have an M: drive your exoledb path should be based on  your primary email domain.

After you have created your record-set the first thing to do is check if its empty if its not then you have a reminder that hasn't been dismissed and you can then perform whatever action you want. In this example I'll send an email to an email address that gets retrieved from querying Active Directory and retrieving the custom attribute_1 of the mailbox where the eventsink fired.. Custom attributes can be configured in Active Directory Users and Computer in the Exchange Advanced Tab.

if not rs.eof then
	on error resume next
	Set objEmail = CreateObject("CDO.Message")
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = "yourserver"
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
	objEmail.from = "calreminders@yourdomain.com.au"
	objEmail.subject = "Reminding you that you have a Appoitnment at " & formatdatetime(dateadd("h",toffset,rs.fields("urn:schemas:calendar:dtstart")),3)
	objEmail.textbody = "The Description of this Appointment is " &  rs.fields("urn:schemas:httpmail:subject") & vbCrLf
	if instr(rs.fields("urn:schemas:calendar:organizer"),">") = "" then 
		objEmail.textbody = objEmail.textbody & "The Organizer of this Appointment is " & replace(left(rs.Fields("urn:schemas:calendar:organizer"),instr(rs.Fields("urn:schemas:calendar:organizer"),"<")-1),chr(34)," ") & vbCrLf
	else
		objEmail.textbody = objEmail.textbody & "The Organizer of this Appointment is " & rs.Fields("urn:schemas:calendar:organizer") & vbCrLf
	end if
	if not (rs.fields("urn:schemas:calendar:location")) = "" then objEmail.textbody = objEmail.textbody & "Location of appointment is " & rs.fields("urn:schemas:calendar:location") & vbCrLf
	DomainName = "yourdomain.com.au"
	Set oRoot = GetObject("LDAP://" & DomainName & "/rootDSE")
	strDefaultNamingContext = oRoot.get("defaultNamingContext")

	CUserID = uname

	GALQueryFilter = "(&(&(&(& (mailnickname=*) (| (&(objectCategory=person)(objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=user)(|(homeMDB=*)(msExchHomeServerName=*))) )))(objectCategory=user)(samAccountName=" & CUserID & ")))"

	strQuery = "<LDAP://" & DomainName & "/" & strDefaultNamingContext & ">;" & GALQueryFilter & ";distinguishedName,displayName,extensionattribute1,msExchHideFromAddressLists;subtree"
	Set oConn = CreateObject("ADODB.Connection") 'Create an ADO Connection
	oConn.Provider = "ADsDSOOBJECT"              ' ADSI OLE-DB provider
	oConn.Open "ADs Provider"

	Set oComm = CreateObject("ADODB.Command") ' Create an ADO Command
	oComm.ActiveConnection = oConn
	oComm.Properties("Page Size") = 1000
	oComm.CommandText = strQuery

	Set rs = oComm.Execute
	objEmail.To = rs.fields("extensionattribute1")
	objEmail.Send
end if

There are a few hard-coded lines in this code you have to change to get it to work in your environment the first is the mail object configuration which you must set to the server you want to send mail though. The next is the sender address you want this mail to appear to come from and the third is the DomainName variable this should be set to your active directory domain name.

That's it if you want to change the action the code performs all you need to do is change the above routine for example if you wanted to send a Instant message via Exchange IM, LCS or MSN messenger you could shell out to another script see my other articles for examples. The other example I've done is using some TAPI and SAPI code that I've explained in my other article is to change the above routine to write out the content of the reminder to a text file and then insert a record into a queuing database. Another piece of code will then read this database and using TAPI call a phone number that is retrieved via a active directory query and then using Speech  tell the person called that they have an appointment reminder. I've included this sink in the download for this article in the CALSAPI.zip file.

Registering this script

Registering a Ontimer event sinks differs from registering a normal sync or async event sink as it requires setting at least 2 of the following 3 eventsink properties. "http://schemas.microsoft.com/exchange/events/TimerInterval",  (Required)
"http://schemas.microsoft.com/exchange/events/TimerStartTime",  (Required)
"http://schemas.microsoft.com/exchange/events/TimerExpiryTime",  (Optional)

Two methods you can use to register event sinks are using exchange explorer or regevent.vbs, with On_timer event sinks your need to be careful of the following

Exchange Explorer

When registering an onTimer eventsink with Exchange explorer make sure you download the latest copy from MSDN (Its Included in the Exchange SDK samples), the start time you enter when registering the sink is in UTC time. Unless you want the event to start at a particular time its safer to set the start date to a few days in the past to avoid any problems with UTC time. To check that the event has registered properly with Exchange explorer have a look at the properties of the event sink object check the TimerstartInterval should be datatype datetime.tz and the property value should be In ISO dateformat and the TimerInterval property should be datatype Interger (Int).

Regevent.vbs

I had a lot of problems initially trying to register ontimer events with regevent.vbs the main problem I ran into was that although the sink registered okay it wouldn't fire. After a lot of false positives (and a few frustrating hours) I eventually found a method that worked consistently over a number of machines. Like Exchange Explorer the date and time that you enter for the start and end time are in UTC time (not local time).  When registering a event sink with the regevent.vbs script it sets the Timerstart , Timerend and TimerInterval datatypes to the string datatype and it also doesn't store the dates in the normal exchange ISO format. The weird thing is this doesn't seem to mater it looks like the ontimer event handles dates in this format. Whether this causes problems over the long term leaving these dates stored in the string datatype format ???.  I created a modified AddNewStoreEvent subroutine for the regevent.vbs script that handles converting the date/time retrieved from the commandline into a proper date format using the cdate function and the interval using the cint function. I've posted the new routine here (changes are in red) all you need to do is cut and paste it into your regevent.vbs file over the existing routine (its up to you whether you want to do this though as it seems to works either way).

Updated 05/2004

Someone recently pointed out a inaccuracy in this article regarding my comments on the On-timer event sink registration so here's a correction

If your using OnTimer event sink with the Exoledb Script host you need to be careful of the following, If you are using the "-file" command line parameter to reference your event sink script in the registration make sure that you don't register your event sink with the extension .eml . (just use no extension the sample in the regevent.vbs file will work fine) if you do use a .eml extension the sink will register by not fire.

What happens when you use the -file reference to do an event sink registration is it copies the code from your event sink script into the eventsink object itself and then it sets the scripturl property of the eventsink registration object to reference itself.

 An alternate method of registering an ontimer eventsink is use the -url command line parameter to reference your VBS script. This means that you need to upload your script into the mailbox somewhere before you can register the event sink to point to the script url. One method I've used is the below code fragment which will create an object in your mail store and upload the code form the d:\scripts\yourscript.vbs script file to that object.

Const adDefaultStream = -1
Set stm = CreateObject("ADODB.Stream")
set Rec = CreateObject("ADODB.Record")
set Rec1 = CreateObject("ADODB.Record")
Set Conn = CreateObject("ADODB.Connection")
mailboxurl = "file://./backofficestorage/yourdomain.com.au/MBX/yourmailbox/calendar/emit5.eml"
Conn.Provider = "ExOLEDB.DataSource"
Rec.Open mailboxurl, ,3,0
Set Stm = Rec.Fields(adDefaultStream).Value
stm.charset = "us-ascii"
stm.loadfromfile "d:\scripts\emit5.vbs"
stm.flush

You can then run the following event sink registration to register an ontimer event that points to the following object you created (runs at an interval of 10 minutes)

cscript regevent.vbs add "Ontimer" ExOLEdb.ScriptEventSink.1 file://./backofficestorage/yourdomain.com.au/mbx/yourmailbox/calendar/calsink  "01/02/2004 01:00:00 AM" 10 -url "file://./backofficestorage/yourdomain.com.au/MBX/yourmailbox/calendar/emit5.eml"

To register using the -file method use

cscript regevent.vbs add "Ontimer" ExOLEdb.ScriptEventSink.1 file://./backofficestorage/yourdomain.com.au/mbx/yourmailbox/calendar/calsink "01/02/2004 01:00:00 AM" 10 -file "d:\emit5.vbs"

Note the Expiry date is optional.

For details on using VBS Event sinks with Exchange see my previous article Using VBS Event Sink scripts with the Web Storage System. Or have a look in the ESDK search for regevent.vbs.   

Download Scripts


Disclaimer: Your use of the information contained in these pages is at your sole risk. All information on these pages is provided "as is", without any warranty, whether express or implied, of its accuracy, completeness, fitness for a particular purpose, title or non-infringement, and none of the third-party products or information mentioned in the work are authored, recommended, supported or guaranteed by Stephen Bryant or Pro Exchange. OutlookExchange.Com, Stephen Bryant and Pro Exchange shall not be liable for any damages you may sustain by using this information, whether direct, indirect, special, incidental or consequential, even if it has been advised of the possibility of such damages.

Copyright Stephen Bryant 2008