At my present gig, there is this .NET web application for scheduling and requesting approvals for vacation and other absences, that I wrote two years ago. The application gets a bit of an upgrade from time to time, so that it runs smoothly and so that we improve user experience. But there was one thing, that I really missed which we had at previous gig. At previous gig, whenever your vacation request was approved, you got this nice meeting request in your Lotus Notes mailbox, which you accepted and “pooof”, the calendar entry was created. This was good for two things. One was, that you actually got reminded that you are not at work on said day, and thus prevented you to make any false promises. Second one was that anyone scheduling a meeting saw that you were absent and could schedule it when you were at work.

Back to present day. I want our application to have that feature! And as much as it is piece of cake to send a Lotus Notes meeting request inside Lotus Notes environment, you get into all sorts of trouble in a mixed Lotus Notes and .NET environment like we do at my present gig. Now, if you do a search for how to send a Lotus Notes meeting request from .NET, you will get plenty of hits where people use Lotus Notes COM objects to connect to mail database, generate a notes document and send it. This was useless to my problem, because to access user’s mailbox and create a meeting request, you need to run application on user’s computer and get his password. Somehow. Also, we are inclining to leave Lotus Notes for good, so any day now, we might switch to Exchange server and as god is my witness, I have no desire to write same code twice.

So, after some serious research, I came to a conclusion that using COM objects is definitely out of the question. I also noticed that both Lotus Notes Client and Outlook support iCalendar format described here. Thus, if I was somehow able to send an iCalendar file as an inline attachment, it should work well on both systems. Sure, it may not be the preferred way of creating a meeting in either client, but iCalendar is a standard to stay, so I doubt there will be a need to recode the darn thing in my life time. Anyway… enough gibberish. On to the code, which is, surprisingly in VB.NET!

 Public Class SMTPAppointmentDispatcher
        Implements IAppointmentDispatcher

#Region "Properties"

        Private Property Server As String

#End Region

#Region "Constructors"

        Public Sub New(strSmtpServer As String)
            If (String.IsNullOrWhiteSpace(strSmtpServer)) Then _
                Throw New ArgumentNullException("SMTP server address")
            Me.Server = strSmtpServer
        End Sub

#End Region

#Region "IAppointmentDispatcher methods"

        Public Sub Dispatch(data As AppointmentData) Implements IAppointmentDispatcher.Dispatch
            Dim mail As MailMessage = InitMessage(data)
            Dim strCalendarEntry As String = CreateVCalendarBody(data)
            SendMessage(mail, strCalendarEntry)
        End Sub

#End Region

#Region "Private methods"

        Private Function InitMessage(data As AppointmentData) As MailMessage
            Dim mail As MailMessage = New MailMessage()
            mail.To.Add(data.RecipientAddress)
            If (data.DateFrom.Date = data.DateTo.Date) Then
                mail.Subject = String.Format("{0} od {1:dd.MM.yyyy} do {2:dd.MM.yyyy}", _
                                             data.Subject, data.DateFrom, data.DateTo)
            Else
                mail.Subject = String.Format("{0} od {1:dd.MM.yyyy hh:mm} do {2:dd.MM.yyyy hh:mm}", _
                                             data.Subject, data.DateFrom, data.DateTo)
            End If
            mail.From = New MailAddress("myapplication@mycompany.com")
            Return mail
        End Function

        Private Function CreateVCalendarBody(data As AppointmentData) As String
            Dim strCalDateFormat As String = "yyyyMMddTHHmmssZ"
            Dim strBodyText As String = "BEGIN:VCALENDAR" + vbNewLine + _
                 "METHOD:REQUEST" + vbNewLine + _
                 "BEGIN:VEVENT" + vbNewLine + _
                 "DTSTAMP:{0}" + vbNewLine + _
                 "DTSTART:{1}" + vbNewLine + _
                 "DTEND:{2}" + vbNewLine + _
                 "SUMMARY:{3}" + vbNewLine + _
                 "UID:{4}" + vbNewLine + _
                 "ORGANIZER;CN=""{5}"":MAILTO:{6}" + vbNewLine + _
                 "LOCATION:" + vbNewLine + _
                 "DESCRIPTION:{3}" + vbNewLine + _
                 "SEQUENCE:0" + vbNewLine + _
                 "PRIORITY:5" + vbNewLine + _
                 "CLASS:{7}" + vbNewLine + _
                 "CREATED:{0}" + vbNewLine + _
                 "STATUS:CONFIRMED" + vbNewLine + _
                 "TRANSP:OPAQUE" + vbNewLine + _
                 "END:VEVENT" + vbNewLine + _
                 "END:VCALENDAR"
            strBodyText = String.Format(strBodyText, _
                                        DateTime.Now.ToUniversalTime().ToString(strCalDateFormat), _
                                        data.DateFrom.ToUniversalTime().ToString(strCalDateFormat), _
                                        data.DateTo.ToUniversalTime().ToString(strCalDateFormat), _
                                        data.Subject, _
                                        Guid.NewGuid().ToString("N"), _
                                        data.RecipientFullName, _
                                        data.RecipientAddress, _
                                        IIf(data.IsPrivate, "PRIVATE", "PUBLIC")
                                        )
            Return strBodyText
        End Function

        Private Sub SendMessage(mail As MailMessage, strCalendarEntry As String)
            Using smtp As SmtpClient = New SmtpClient(Me.Server)
                Dim bytesBody As Byte() = System.Text.Encoding.UTF8.GetBytes(strCalendarEntry)
                Using ms As System.IO.MemoryStream = New System.IO.MemoryStream(bytesBody)
                    Using a As Attachment = New Attachment(ms, "meeting.ics", "text/calendar")
                        a.ContentDisposition.Inline = True
                        mail.Attachments.Add(a)
                        smtp.Send(mail)
                    End Using
                End Using
            End Using
            mail.Dispose()
        End Sub

#End Region

    End Class

What every method does is, I hope, pretty straight forward.

  1. Class constructor is simple. It takes a name of a SMTP server, which we will use to send e-mail from.
  2. Public method Dispatch takes care of creating an email, populates it with data and sends it. This method also takes a type-class I used to store information necessary to send a valid e-mail.
  3. Private method InitMessage creates MailMessage objects and populates it with Subject, To and From field.
  4. Private method CreateVCalendarBody returns a string that suits iCalendar standards. You may notice that this one only has organizer field filled, which means organizer will receive a Calendar broadcast. If you want to send a real meeting request, you also need to add:
    ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN="John the Recipient":MAILTO:recipient@mycompany.com
  5. Private method SendMessage creates an inline attachment with file named meeting.ics.

This works well and not only that, it also work well over the internet. Remember said Lotus Notes application? It had an issue with people not using Lotus Notes client. This solution, however, works if your clients are using GMail, Hotmail, Outlook, Lotus Notes or any other mail client with iCalendar support.

There are couple of things you need to watch out for.

  1. Organizer must not be an attendee as well. If one of your recipients has the same email address as organizer, recipients will not see calendar request. In fact, they will not see anything, as e-mail will not show up in their mailbox.
  2. If you try to fake an organizer, you need to make sure that email address is valid, as otherwise all attendees will get mail bounce notifications at any of their actions upon said calendar entry.