top of page

Blackbaud CRM™ Integration with Google Calendar Part 2

In this post, part 2 of 3, we’re going to take a look at using 2 different APIs to create calendar items. The goal will be to create a web service in Visual Studio that can accept an event, and use that to create calendar events in Google, or in Outlook 365.

In the third and final installment, we will take a look at what we’ve done in parts 1&2, and use Blackbaud CRM™ as the trigger for adding events to Google and Outlook 365.

“Why do we need to have a separate web service?” you may wonder, as the Blackbaud Infinity platform allows developers to create rich customizations, and it’s certainly capable of calling external APIs. This may be true, but in reality, the APIs for Google and Outlook rely on some very specific dependencies, which will load a lot of extra components into CRM’s bin\custom directory, and may not play well either with CRM, or other customizations you will be writing. Additionally, separating the API calls into their own service allows some extra flexibility around testing, as well as the ability to be consumed by applications other than CRM.

Finally, as the Google API uses a proprietary security file, it gives administrators a little more leeway in maintaining credentials, and not have to shoehorn them into a CRM customization.

In this exercise what we’ll do is create a web service that can take a simple event with a collection of attendees, and add it to either a Google Calendar or an Outlook 365 calendar. In doing so we’ll revisit some of the Google API code we saw in Part 1, and also look at some code to work with the Outlook API.

Before you start this exercise, it’s assumed you went through part 1 and created a Google Developer’s account and project, and downloaded a .json security file to your local system. As long as you went up to the point where Part 1 discussed creating a Visual Studio project, you’ll be ok to start here. In this post, we’ll be creating a brand new VS project.

First thing is to create a new web service project:

As we saw in Part one, in order to use the Google API, we need to import some references:

Tools>NuGet Package Manager>Manage NuGet Packages for Solution

Google APIs Auth Client Library

Then do the same thing, searching for “Google.Apis.Calendar.v3 Client Library”

Rename IService1.vb and Service1.svc to ICalendarSvc.vb and CalendarSvc.scv, respectively. Then open up our interface, ICalendarSvc.vb

Now replace the default operation contracts with the following:

<OperationContract()>

Sub AddCalendarEvent(Details As EventDetails)

Next, remove the default Data Contracts and create the following, which is a new composite type. This type will be a relatively simple version of a calendar event, and is sent into our web service as the request:

<DataContract()>

Public Class EventDetails

<DataMember()>

Public Property Mode() As enumCalendarMode

<DataMember()>

Public Property EventTitle() As String

<DataMember()>

Public Property EventBody() As String

<DataMember()>

Public Property EmailAddresses() As List(Of String)

<DataMember()>

Public Property dtStart() As Date

<DataMember()>

Public Property dtEnd() As Date

End Class

The last thing we will do in this file is add an enum to tell the service what mode to use. In our example we will build in two modes – one for Google and another for Outlook365:

Public Enum enumCalendarMode As Integer

CALENDAR_MODE_GOOGLE = 0

CALENDAR_MODE_OUTLOOK = 1

End Enum

The entire file should now look like this:

<ServiceContract()>

Public Interface ICalendarSvc

<OperationContract()>

Sub AddCalendarEvent(Details As EventDetails)

End Interface

<DataContract()>

Public Class EventDetails

<DataMember()>

Public Property Mode() As enumCalendarMode

<DataMember()>

Public Property EventTitle() As String

<DataMember()>

Public Property EventBody() As String

<DataMember()>

Public Property EmailAddresses() As List(Of String)

<DataMember()>

Public Property dtStart() As Date

<DataMember()>

Public Property dtEnd() As Date

End Class

Public Enum enumCalendarMode As Integer

CALENDAR_MODE_GOOGLE = 0

CALENDAR_MODE_OUTLOOK = 1

End Enum

Open CalendarSvc.svc, remove what’s there and paste the following:

Public Class CalendarSvc

Implements ICalendarSvc

Public Sub New()

End Sub

Sub AddCalendarEvent(Details As EventDetails) Implements ICalendarSvc.AddCalendarEvent

Dim cal As ICalendarApi

If Details.Mode = enumCalendarMode.CALENDAR_MODE_GOOGLE Then

cal = New GoogleCalendarSvc

cal.AddEvent(Details)

ElseIf Details.Mode = enumCalendarMode.CALENDAR_MODE_OUTLOOK Then

cal = New Outlook365CalendarSvc

cal.AddEvent(Details)

End If

End Sub

End Class

What we’ll do next is fill in the actual code that does the work for adding the Google and Outlook events. Since the implementation is similar for both, it makes some sense to create an interface. Looking at the code above, we can infer that we’ll be making an interface called “ICalendarApi” and two classes which implement the interface: “GoogleCalendarSvc” and “Outlook365CalendarSvc”. So go ahead and create ICalendarApi, which should be very simple and look like this:

Public Interface ICalendarApi

Sub AddEvent(Details As EventDetails)

End Interface

Next, create a new class called “GoogleCalendarSvc.vb” and paste in the following:

Imports System.IO

Imports System.Threading

Imports Google.Apis.Calendar.v3

Imports Google.Apis.Calendar.v3.Data

Imports Google.Apis.Services

Imports Google.Apis.Auth.OAuth2

Imports Google.Apis.Util.Store

Imports System.Net.Http

Public Class GoogleCalendarSvc

Implements ICalendarApi

Dim _scopes As IList(Of String) = New List(Of String)()

Dim _service As Google.Apis.Calendar.v3.CalendarService

Dim _AllEvents As New List(Of Data.Event)

Dim _Details As EventDetails

Public Sub AddEvent(Details As EventDetails) Implements ICalendarApi.AddEvent

_Details = Details

InitializeService()

CreateEvent()

End Sub

Private Sub InitializeService()

_scopes.Add(Google.Apis.Calendar.v3.CalendarService.Scope.Calendar)

Dim jsonFile As String = "C:\Credentials\client_secret_791792756839-c1ev8q9gjgvep2qm1v2cl872joh2pd98.apps.googleusercontent.com.json"

Dim credential As UserCredential

Dim secrets As ClientSecrets

Dim fds As New FileDataStore("C:\Credentials", True)

Dim googleAccount As String = "brightvinesolutions@gmail.com"

Try

Using stream As New FileStream(jsonFile, FileMode.Open, FileAccess.Read)

secrets = GoogleClientSecrets.Load(stream).Secrets

credential = GoogleWebAuthorizationBroker.AuthorizeAsync(secrets, _scopes, googleAccount, CancellationToken.None, fds).Result

End Using

Catch ex As Exception

Throw ex

End Try

' Create the calendar service using an initializer instance

Dim initializer As New BaseClientService.Initializer()

initializer.HttpClientInitializer = credential

initializer.ApplicationName = "VB.NET Calendar Sample"

_service = New Google.Apis.Calendar.v3.CalendarService(initializer)

End Sub

Private Sub CreateEvent()

Try

Dim calEvent As New Google.Apis.Calendar.v3.Data.Event

calEvent.Summary = _Details.EventTitle

calEvent.Description = _Details.EventBody

calEvent.Location = ""

Dim attendees As New List(Of EventAttendee)

Dim evAttendee As EventAttendee

For Each curAttendee As String In _Details.EmailAddresses

evAttendee = New EventAttendee

evAttendee.Email = curAttendee

attendees.Add(evAttendee)

Next

calEvent.Attendees = attendees

Dim evStart As New EventDateTime

evStart.DateTime = _Details.dtStart

Dim evEnd As New Data.EventDateTime

evEnd.DateTime = _Details.dtEnd

calEvent.Start = evStart

calEvent.End = evEnd

Dim createdEvent As [Event] = _service.Events.Insert(calEvent, "primary").Execute

Catch ex As HttpRequestException

Throw ex

Catch ex As Exception

Throw ex

End Try

End Sub

End Class

Note that in InitializeService, you’ll need to change the “dim jsonFile…” to reflect the real location of the json file you downloaded from Google. This location, of course, will need to be accessible by IIS, so make sure the AppPool you’re running under has proper rights.

At this point if we were to go back into CalendarSvc.svc and comment out the following lines:

cal = New Outlook365CalendarSvc

cal.AddEvent(Details)

…we should be able to use this service to create new Google events. To those who read through all of part 1, the “InitializeService” section should look familiar, and the CreateEvent sub should look very straightforward, its purpose mostly being to create the correct Google Event object, set some properties (we iterate the Attendees ArrayList), and finally, execute the request.

This can be easily tested by creating a Win Forms test project in the same solution, creating a service reference to CalendarSvc named “ProjSvc” and running the following code either on form load or in a button click event:

Dim gRef As New ProjSvc.CalendarSvcClient

Dim ed As New ProjSvc.EventDetails

Dim emailAddresses As New ArrayList

Try

emailAddresses.Add("my.account@gmail.com")

ed.Mode = ProjSvc.enumCalendarMode.CALENDAR_MODE_GOOGLE

ed.EmailAddresses = emailAddresses.ToArray(GetType(String))

ed.EventTitle = "Yet another conference call"

ed.EventBody = "Make sure you're not on mute when speaking!"

ed.dtStart = Now().AddHours(1)

ed.dtEnd = Now().AddHours(2)

gRef.AddCalendarEvent(ed)

Catch ex As Exception

MsgBox(ex.Message)

End Try

In the downloadable code, we’ll have a richer form for testing, and will include Outlook testing, as well, but the above snippet should let you try this service out now.

Ok, let’s get back to the Calendar Service. If you commented out the following:

cal = New Outlook365CalendarSvc

cal.AddEvent(Details)

…uncomment them, and add a new class called CallbackMethods.vb with the following code:

' This source is subject to the Microsoft Public License.

' See http://www.microsoft.com/en-us/openness/licenses.aspx#MPL.

' All other rights reserved.

'

' THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,

' EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED

' WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.

'**************************************************************************/

Imports System.Security.Cryptography.X509Certificates

Public NotInheritable Class CallbackMethods

Private Sub New()

End Sub

Public Shared Function RedirectionUrlValidationCallback(ByVal redirectionUrl As String) As Boolean

' The default for the validation callback is to reject the URL.

Dim result As Boolean = False

Dim redirectionUri As New Uri(redirectionUrl)

' Validate the contents of the redirection URL. In this simple validation

' callback, the redirection URL is considered valid if it is using HTTPS

' to encrypt the authentication credentials.

If redirectionUri.Scheme = "https" Then

result = True

End If

Return result

End Function

Public Shared Function CertificateValidationCallBack(ByVal sender As Object,

ByVal certificate As System.Security.Cryptography.X509Certificates.X509Certificate,

ByVal chain As System.Security.Cryptography.X509Certificates.X509Chain,

ByVal sslPolicyErrors As System.Net.Security.SslPolicyErrors) As Boolean

'return true;

' If the certificate is a valid, signed certificate, return true.

If sslPolicyErrors = System.Net.Security.SslPolicyErrors.None Then

Return True

End If

' If there are errors in the certificate chain, look at each error to determine the cause.

If (sslPolicyErrors And System.Net.Security.SslPolicyErrors.RemoteCertificateChainErrors) <> 0 Then

If chain IsNot Nothing AndAlso chain.ChainStatus IsNot Nothing Then

For Each status As System.Security.Cryptography.X509Certificates.X509ChainStatus In chain.ChainStatus

If (certificate.Subject = certificate.Issuer) AndAlso

(status.Status =

System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.UntrustedRoot) Then

' Self-signed certificates with an untrusted root are valid.

Continue For

Else

If status.Status <>

System.Security.Cryptography.X509Certificates.X509ChainStatusFlags.NoError Then

' If there are any other errors in the certificate chain, the certificate is invalid,

' so the method returns false.

Return False

End If

End If

Next status

End If

' When processing reaches this line, the only errors in the certificate chain are

' untrusted root errors for self-signed certificates. These certificates are valid

' for default Exchange server installations, so return true.

Return True

Else

' In all other cases, return false.

Return False

End If

End Function

End Class

This code snippet came from this project:

https://code.msdn.microsoft.com/office365/How-to-Search-Calendar-11437894

You can go here to get the full context, but the gist is that when authenticating with Outlook, using AutodiscoverUrl may throw a redirect to https, which could throw an error. This class, used as a callback, will override that.

The last file in the project is the Outlook365CalendarSvc.vb class. Go ahead and add that, then past in the following:

Imports Microsoft.Exchange.WebServices.Data

Imports System.Net

Public Class Outlook365CalendarSvc

Implements ICalendarApi

Public Sub AddEvent(Details As EventDetails) Implements ICalendarApi.AddEvent

'ServicePointManager.ServerCertificateValidationCallback = AddressOf CallbackMethods.CertificateValidationCallBack

Dim service As New ExchangeService(ExchangeVersion.Exchange2010_SP2)

' Get the information of the account.

Dim user As New User

Try

user.Account = "my.account@brightvinesolutions.com"

User.Pwd = "MyPassword"

service.Credentials = New WebCredentials(user.Account, user.Pwd)

' Set the url of server.

If AutodiscoverUrl(service, User) Then

Dim appt As New Appointment(service)

'Set the properties on the appointment object to create the appointment.

appt.Subject = Details.EventTitle

appt.Body = Details.EventBody

appt.Start = Details.dtStart

appt.End = Details.dtEnd

appt.Location = ""

appt.ReminderDueBy = DateTime.Now

For Each curAttendee As String In Details.EmailAddresses

appt.RequiredAttendees.Add(curAttendee)

Next

'Save the appointment to your calendar.

appt.Save(SendInvitationsMode.SendToNone)

End If

Catch ex As Exception

Throw ex

Exit Sub

End Try

End Sub

Private Shared Function AutodiscoverUrl(ByVal service As ExchangeService, ByVal user As User) As Boolean

Dim isSuccess As Boolean = False

Try

service.AutodiscoverUrl(user.Account, AddressOf CallbackMethods.RedirectionUrlValidationCallback)

isSuccess = True

Catch ex As Exception

Throw ex

End Try

Return isSuccess

End Function

End Class

Friend Class User

Private accountInfo As String = Nothing

Private pwdInfo As String = Nothing

Public Sub New()

End Sub

Public Property Account() As String

Get

Return accountInfo

End Get

Set(value As String)

accountInfo = value

End Set

End Property

Public Property Pwd() As String

Get

Return pwdInfo

End Get

Set(value As String)

pwdInfo = value

End Set

End Property

End Class

Of course, you’ll want to make sure to change the user name and password to be something more appropriate. In a production situation, we’d probably want to set up some service account in Outlook, and at the very least, store the credentials in a config file.

At this point you can build your project and using the same code snippet as above, set the mode to “OUTLOOK” and you should be getting calendar items added to your Outlook 365 Calendar.

Next week, we'll look at part 3 of this series, how add these objects to Blackbaud CRM.

bottom of page