Another task in our migration to Outlook as the mail client is creating Outlook calendar entries and meeting notices directly from the Notes client. Fortunately, it’s been two years since I wrote about how to do this in the UI in Notes, so I don’t feel like that was wasted time. I was exciting to solve the problem and… oddly enough, solving this one was fun as well. It helped that creating an iCal entry is far simpler than the gyrations we had to go through to create one in Notes. As noted previously, thereĀ aren’t a whole lot of required values to generate in order to have an ICS file that you can open in the UI as a meeting notice/calendar invite.
BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20170622T211500
DTEND:20170622T221500
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="Required Person/Company";RSVP=TRUE:mailto:Required_Person@company.com
ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:external_person@SecondCompany.com
SUMMARY:2017 Q2 QPR: Agribusiness Competitiveness Enhancement via file
UID:AC1804D765C782CD8525814500073F3720170620T104326
END:VEVENT
END:VCALENDAR
Now, keep in mind that this iCal file is a mere fragment. If you sent that file to someone, they get the same behaviour you get — it thinks they’re the meeting organizer and doesn’t save it to their calendar unless they send the ‘update’. The key parameter we leave off is that we don’t set METHOD, since setting that to PUBLISH or REQUEST proved problematic in the Outlook client. If we leave it off, Outlook will allow us to treat it like a brand new calendar entry we’ve created, except that the send button will say ‘Send Update’.
So, let’s review those values in our fragment…
Objects
First, the calendar and event objects are encapsulated. Nothing fancy there.
BEGIN:VCALENDAR
BEGIN:VEVENT
END:VEVENT
END:VCALENDAR
Meeting times
Then we have our start and end times, formatted with date first (YYYYMMDD) then a separator (T) and then the time (HHMMSS). You can include time zone information, but we’re creating this in Outlook and allowing the UI to finish everything for us. So, if the user wants to change the time zone, they can do that in Outlook.
DTSTART:20170622T211500
DTEND:20170622T221500
Attendees
The one required value for our needs in the attendees is the mailto value. Without that, it won’t know who to send the invite to and it simply ignores any other item in that list.
ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN="Meeting Chair/Company";RSVP=TRUE:mailto:Meeting_Chair@company.com
ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="Required Person/Company";RSVP=TRUE:mailto:Required_Person@company.com
ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:external_person@SecondCompany.com
ROLE is not required and can be CHAIR, REQ-PARTICIPANT (required participant), OPT-PARTICIPANT (optional participant) or even NON-PARTICIPANT (for FYI only).
PARTSTAT is not required. There are several values for an attendee in a VEVENT for their participant status, but we’re only concerned with two. Either “NEEDS-ACTION” for attendees that we don’t know when we create the meeting notice whether they’ve agreed to attend or “ACCEPTED” which we’d typically only use for the person creating the meeting notice.
CN is, of course, familiar to us as Notes developers, but it applies here to whatever will be displayed as the attendee name. In my experience, Outlook can parse the abbreviated name and display just the attendee’s common name. That might be our Outlook configuration, but I would assume it’s common.
RSVP would be either true or false, indicating whether you want a response from the attendee. In my case, we always want it from the attendees, other than the current user.
Title and description
I got fooled by this one. In my sample ICS files, I thought there was just an odd carriage return, but the DESCRIPTION value is basically the body or details of the event, while the SUMMARY is what appears in the subject line for the meeting.
SUMMARY:2017 Q2 QPR: Agribusiness Competitiveness Enhancement via file
Meeting ID
I’m guessing that Outlook computes the unique meeting ID itself, but in my code, I generate from the Notes document’s unique ID and then, in order to ensure that subsequent meetings concerning the same document get different IDs, I’m appending a creation time-stamp.
UID:AC1804D765C782CD8525814500073F3720170620T104326
So, the agent I wrote that generates the new meeting notice is pretty straight-forward. The getEmailAddress function was described and detailed in a prior blog post and my Utilities script library only provides the logging functions here. Like my mailto agent, this one relies on the creation of a file in the Notes data directory and opening it using a browser.
The agent
First, you can look over the main part of the agent…
%REM
Agent (Send Calendar Invites)
Created Jun 20, 2017 by David Navarre/DAI
Description: This Agent creates a calendar invite, listing participants and optional participants
%END REM
Option Public
Option Declare
Use "Utilities"
Dim session As NotesSession
Sub Initialize
Dim ws As New NotesUIWorkspace
' Dim thisdb As NotesDatabase declared in Utilities script library '
Dim uidoc As NotesUIDocument
Dim qprdoc As NotesDocument
Dim history As NotesRichTextItem
Dim chairName As NotesName
Dim recipientName As NotesName
Dim projectName As Variant
Dim fiscalYearAndQuarter As Variant
Dim participants As Variant
Dim participantsOptional As Variant
Dim subject As String
Dim answer As Variant
Dim reason As String
Dim unid As String
On Error GoTo errorhandler
set session = New NotesSession
Call StartAgentLogging ( session )
If ( openAddressBooks () ) Then
agentLog.Logaction("Address books opened")
End If
Set thisdb = session.CurrentDatabase
Set uidoc = ws.CurrentDocument
reason = "This will create a meeting invite for you to send to participants."
If uidoc.Editmode Then
reason = reason + Chr$(10) + "The QPR will switch to read-only mode."
reason = reason + Chr$(10) + "If you close and re-open it, you can edit it again."
End If
reason = reason + Chr$(10) + "Continue?"
answer = ws.Prompt ( PROMPT_YESNO, "Continue?", reason )
If answer = 0 Then
Exit Sub
End If
If uidoc.Editmode Then
Call uidoc.Save()
uidoc.Editmode = False
Set qprDoc = uidoc.Document
unid = qprDoc.Universalid
Call uidoc.Close(True)
Set qprDoc = thisdb.Getdocumentbyunid(unid)
Set uidoc = ws.Editdocument(False, qprDoc, True)
Else
Set qprDoc = uidoc.Document
End If
Dim fileName As String
Dim dataDirectoryPath As String
Dim url As String
Dim fileNumber As Integer
fileNumber = 1
dataDirectoryPath = session.Getenvironmentstring("Directory", True)
fileName = dataDirectoryPath & "\QPRInvite.ics"
Open fileName For Output As fileNumber
Print # fileNumber, {BEGIN:VCALENDAR}
Print # fileNumber, {BEGIN:VEVENT}
Print # fileNumber, {DTSTART:} & getMeetingTime ( "Start", qprDoc ) '20170620T211500
Print # fileNumber, {DTEND:} & getMeetingTime ( "End", qprDoc ) '20170620T221500
' Chair '
Set chairName = New NotesName ( session.Effectiveusername )
' when you send the invite from Outlook, it makes you the chair '
' this line is here to show how you would format an attendee line for the chair '
' Print # fileNumber, {ATTENDEE;ROLE=CHAIR;PARTSTAT=ACCEPTED;CN="} & chairName.Abbreviated & {";RSVP=TRUE:mailto:} & getEmailAddress ( chairName.Abbreviated ) '
' Required participants '
participants = qprDoc.Getitemvalue ( "Participants" )
ForAll entry In participants
Set recipientName = New NotesName ( entry )
If Not ( chairName.Abbreviated = recipientName.Abbreviated ) Then
Print # fileNumber, {ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="} & recipientName.Abbreviated & {";RSVP=TRUE:mailto:} & getEmailAddress ( recipientName.Abbreviated )
End If
End ForAll
' Optional participants '
participantsOptional = qprDoc.Getitemvalue ( "ParticipantsOptional" )
ForAll entry In participantsOptional
Set recipientName = New NotesName ( entry )
Print # fileNumber, {ATTENDEE;ROLE=OPT-PARTICIPANT;PARTSTAT=NEEDS-ACTION;CN="} & recipientName.Abbreviated & {";RSVP=TRUE:mailto:} & getEmailAddress ( recipientName.Abbreviated )
End ForAll
projectName = qprDoc.Getitemvalue("ProjectName")
fiscalYearAndQuarter = qprDoc.Getitemvalue("FiscalYearAndQuarter")
subject = fiscalYearAndQuarter(0) & " QPR: " & projectName (0)
Print # fileNumber, {DESCRIPTION:} & subject ' this is the body of the message
Print # fileNumber, {SUMMARY:} & subject ' this is the meeting name
' assign a unique ID to meeting using the unid of the document with the current date-time appended '
' in case user creates multiple meetings for the same QPR '
Print # fileNumber, {UID:} & qprdoc.Universalid & getMeetingTime ( "Now", qprDoc )
Print # fileNumber, {END:VEVENT}
Print # fileNumber, {END:VCALENDAR}
Close # fileNumber
url = "file:///" & fileName
Call ws.Urlopen(url)
Set history = qprDoc.Getfirstitem("History")
Call history.Appendtext(Now & " - Meeting notice created by " & session.Commonusername)
Call history.Addnewline(1, True)
Call qprDoc.Replaceitemvalue("NoticeFlag", 1)
Call qprDoc.Save(True, False)
exiting:
Exit Sub
errorhandler:' report all errors in a messagebox '
reason = "Error #" & CStr (Err) & " (" & Error & ") on line " & CStr (Erl)
MessageBox reason, 16, "Error"
Call agentLog.LogAction ( reason )
Resume exiting ' transfers control to the exiting label
End Sub
getMeetingTime
The getMeetingTime function just returns the requested date-time in the format YYYYMMDDTHHMMSS, so it can be included in the creation of the ICS file.
%REM
Function getMeetingTime
Description: This Function returns a string in the format YYYYMMDDTHHMMSS
If it is the start time, the values from the source document are used -- 20170622T211500
If it is the end time, it is adjusted one hour later -- 20170622T221500
If it is the "Now" time, it returns a string for the current date and time -- 20170620T094326
%END REM
Function getMeetingTime ( startOrEnd As String, qprDoc As NotesDocument ) As String
Dim thisNotesDateTime As NotesDateTime
Dim qprDate As Variant
Dim qprTime As Variant
Dim timeString, dateString As Variant
Dim reason As String
On Error Goto errorhandler
Set qprDate = qprDoc.Getfirstitem("QPRDate")
Set qprTime = qprDoc.Getfirstitem("QPRTime")
dateString = qprDate.Text
timeString = qprTime.Text
Set thisNotesDateTime = New NotesDateTime ( dateString & " " & timeString )
Select Case startOrEnd
Case "End"
Call thisNotesDateTime.AdjustHour (1)
Case "Now"
Set thisNotesDateTime = New NotesDateTime ( Now )
Case else
' keep thisNotesDateTime as set on the source document '
End Select
dateString = thisNotesDateTime.DateOnly
timeString = thisNotesDateTime.TimeOnly
getMeetingTime = CStr ( Year ( dateString ) )
getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Month ( dateString ) ), 2 )
getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Day ( dateString ) ), 2 )
getMeetingTime = getMeetingTime & "T"
getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Hour ( timeString ) ), 2 )
getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Minute ( timeString ) ), 2 )
getMeetingTime = getMeetingTime & Right$ ( "0" & CStr ( Second ( timeString ) ), 2 )
exiting:
Call agentLog.LogAction ( "-------" )
Call agentLog.LogAction ( "-------" )
Exit Function
errorhandler:' report all errors in a messagebox '
reason = "Function getMeetingTime: "
reason = reason & "Error #" & Cstr (Err) & " (" & Error & ") on line " & Cstr (Erl)
Messagebox reason, 16, "Error"
Call agentLog.LogAction ( reason )
Resume exiting
End Function
While this did take me a few days to sort out, I’m pretty happy with the result. Our configuration has users sharing one “migration” mail file, so that users who are already on Outlook still retain a mail file and can send email. Unfortunately, that means any email from them that we create in the UI is going to have values pointing back to the “migration” mail file. I spent my first few days on this trying to spoof the mail.box by changing Principal, ReplyTo, $InetAddress and Chair when sending via Notes calendaring. While changing Chair did make it appear to come from the current user, it always displayed the email address from the “migration” mail file. It might have been getting caught in our spam filter on the way to Outlook, as my test user on Notes was still receiving the notices. Nonetheless, by switching to using Outlook as the UI, it not only took away that problem, but was far simpler and future-proofed my application. As I look at these tools I’ve created in LotusScript to generate mail messages and calendar entries, I know that it’s but a short step to doing them in server-side Javascript or maybe in Java.
There is hope for the Notes gurus of old. We just have to keep learning!
iCal RFC (documentation?)
Like this:
Like Loading...