Skip to main content

Using Exchange Web Services to create appointments in the Google Calendar using the Google Calendar API

I’ve being playing around with some code over the last couple of months for synchronizing an Exchange calendar with a Google Calendar using a SyncFolderItems operation on the EWS Side to get a list of updates from a calendar and using the Google .NET client library to create the appointments in a Google calendar. I did manage to successfully create something that was able to create appointments in the Google calendar based on the EWS output. I even got recurring events working by building some logic that converted the recurring output types that EWS uses into a RRULE that could be used in the Google calendar. But when I sat down to look at the rest of the synchronization logic I would need to write to get exceptions to recurring appointments,updates and deletes working the motivation to do this wasn’t really there so instead of just committing this to the back burner I thought I’d post the code which might help someone else out.

The EWS SyncFolderItems operation is very similar to WebDAV replication in that It acts like a change notification repository. The clobblob has been replaced by the SyncState property which is a ID you need to store and submit with future queries to get a manifest of the changes to a folder your synchronizing. For this app I’ve used an XML file to store the ID I was planning to expand the use of this XML which is necessary to work around the shortcoming of both the Google Calendar API and Exchange Web Services. The code will first look for the existence of the file to work out if this is an initial sync or partial sync. If it’s the initial sync then the file is created and the appropriate SyncFolderItems operation is executed. If it’s a paritial sync then the Syncstat is retrieved from the XML file and used in the SyncFolderItems operation and then the file is updated with the new ID retrieved from the SyncFolderItems response. The next part of the code goes though the updated manifest and processes the items based on whether they are creation,updates of deletes. I only got as far as doing the creation side. When the creation method is called a GetItem request is made to get the full details of the item that has created in Exchange. The information is then used to create a Google calendar item.

This is where it starts to get a little bit more complicated because of the various different types of appointments there can be. At the moment this code caters for 3 different types of appointments (or combination of these 3). So there is code to deal with single appointments, All Day appointments and recurring appointments. To relate exchange appointment to Google appointments I had was thinking of using the extended properties that Google allows you to create on a calendar item. Using this I managed to add the ItemID and ChangeID from EWS to the Google calendar appointment. Unfortunately the Google calendar API doesn’t allow you to do a search based on these extended properties so this is where the XML file thats being used to store the SyncStat was going to come into use so you can related properties that are searchable to EWS entryID’s.

That’s about it both of these API’s give you a solid base to do things in but some of the extensibility and feature gaps are a little disappointing IMO but there are always workarounds as long as you have time to write the logic. This code require the Google .NET client side libraries see

I’ve put a download able copy of this code here the code itself looks like.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.IO;
using System.Xml;
using System.Security.Cryptography.X509Certificates;
using System.Net.Security;
using ewsSync.EWS;
using Google.GData.Calendar;
using Google.GData.Client;
using Google.GData.Extensions;

namespace ewsSync
{
class Program
{
static void Main(string[] args)
{
String guGCalUserName = "username@gmail.com";
String gpGCalPassword = "password";
String exUserName = "username";
String exDomain = "domain";
String exPassword = "password";
String sfSyncFilePath = @"c:\google-Calendar.xml";
String exBindingURL = @"https://servername/EWS/exchange.asmx";
String cuCalendarURL = "http://www.google.com/calendar/feeds/" + guGCalUserName
+ "/private/full";

//Athentic to Google Calendar
CalendarService csGoogleCalendarServer = new CalendarService("EWS-Cal-Sync");
csGoogleCalendarServer.setUserCredentials(guGCalUserName, gpGCalPassword);
//Deal with Self Signed Certificate Errors
ServicePointManager.ServerCertificateValidationCallback = delegate(Object obj,
X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
return true;
};

String fnSyncFileName = sfSyncFilePath;
ExchangeServiceBinding ewsServiceBinding = new ExchangeServiceBinding();
ewsServiceBinding.Credentials = new
NetworkCredential(exUserName,exPassword,exDomain);
ewsServiceBinding.Url = exBindingURL;
if (File.Exists(fnSyncFileName))
{
Program.PartialSync(ewsServiceBinding,
fnSyncFileName,csGoogleCalendarServer,cuCalendarURL);
}
else
{
Program.InitialSync(ewsServiceBinding,
fnSyncFileName,csGoogleCalendarServer,cuCalendarURL);

}

}
static private void InitialSync(ExchangeServiceBinding ewsServiceBinding, string
fnSyncFileName, CalendarService csGoogleCalendarServer, string cuCalendarURL)
{
XmlDocument sfSyncFile = new XmlDocument();
StringWriter xsXmlString = new StringWriter();
XmlWriter xrXmlWritter = new XmlTextWriter(xsXmlString);
xrXmlWritter.WriteStartDocument();
xrXmlWritter.WriteStartElement("CalendarSync");
SyncFolderItemsType siSyncItemsRequest = new SyncFolderItemsType();
siSyncItemsRequest.ItemShape = new ItemResponseShapeType();
siSyncItemsRequest.ItemShape.BaseShape = DefaultShapeNamesType.IdOnly;
siSyncItemsRequest.SyncFolderId = new TargetFolderIdType();
DistinguishedFolderIdType cfCalendar = new DistinguishedFolderIdType();
cfCalendar.Id = DistinguishedFolderIdNameType.calendar;
siSyncItemsRequest.SyncFolderId.Item = cfCalendar;
siSyncItemsRequest.MaxChangesReturned = 512;
SyncFolderItemsResponseType syncItemsResponse =
ewsServiceBinding.SyncFolderItems(siSyncItemsRequest);
SyncFolderItemsResponseMessageType responseMessage = new
SyncFolderItemsResponseMessageType();
responseMessage = syncItemsResponse.ResponseMessages.Items[0] as
SyncFolderItemsResponseMessageType;
if (responseMessage.ResponseClass == ResponseClassType.Error)
{
throw new Exception(responseMessage.MessageText);
}
else
{
xrXmlWritter.WriteAttributeString("SyncState",responseMessage.SyncState);
xrXmlWritter.WriteEndElement();
xrXmlWritter.WriteEndDocument();
sfSyncFile.LoadXml(xsXmlString.ToString());
sfSyncFile.Save(fnSyncFileName);
string changes = responseMessage.Changes.Items.Length.ToString();
Console.WriteLine("Number of items to synchronize: " + changes);
Int32 ncNumberOfChanges = responseMessage.Changes.Items.Length;
for (int scSyncChange = 0; scSyncChange < ncNumberOfChanges; scSyncChange++)
{
Console.WriteLine(responseMessage.Changes.ItemsElementName[scSyncChange].ToString());
switch (responseMessage.Changes.ItemsElementName[scSyncChange].ToString())
{
case "Delete": SyncFolderItemsDeleteType diDeletedItem =
(SyncFolderItemsDeleteType)responseMessage.Changes.Items[scSyncChange];
break;
case "Create": SyncFolderItemsCreateOrUpdateType ciCreateItem =
(SyncFolderItemsCreateOrUpdateType)responseMessage.Changes.Items[scSyncChange];
createGoogleCalItem(ciCreateItem, ewsServiceBinding,
csGoogleCalendarServer,cuCalendarURL);
break;
case "Update": SyncFolderItemsCreateOrUpdateType uiUpdateItem =
(SyncFolderItemsCreateOrUpdateType)responseMessage.Changes.Items[scSyncChange];
modifyGoolgCalItem(uiUpdateItem, ewsServiceBinding, csGoogleCalendarServer);
break;

}
}
}
}
private static void PartialSync(ExchangeServiceBinding ewsServiceBinding, string
fnSyncFileName, CalendarService csGoogleCalendarServer, string cuCalendarURL)
{
string ssSyncState = "";
XmlDocument sfSyncFile = new XmlDocument();
sfSyncFile.Load(fnSyncFileName);
XmlNodeList snSyncStateNodes = sfSyncFile.SelectNodes("//CalendarSync");
foreach (XmlNode xnSyncNode in snSyncStateNodes) {
ssSyncState = xnSyncNode.Attributes.GetNamedItem("SyncState").Value;
}

SyncFolderItemsType siSyncItemsRequest = new SyncFolderItemsType();
siSyncItemsRequest.ItemShape = new ItemResponseShapeType();
siSyncItemsRequest.ItemShape.BaseShape = DefaultShapeNamesType.IdOnly;
siSyncItemsRequest.SyncFolderId = new TargetFolderIdType();
DistinguishedFolderIdType cfCalendar = new DistinguishedFolderIdType();
cfCalendar.Id = DistinguishedFolderIdNameType.calendar;
siSyncItemsRequest.SyncFolderId.Item = cfCalendar;
siSyncItemsRequest.SyncState = ssSyncState;
siSyncItemsRequest.MaxChangesReturned = 512;
SyncFolderItemsResponseType syncItemsResponse =
ewsServiceBinding.SyncFolderItems(siSyncItemsRequest);
SyncFolderItemsResponseMessageType responseMessage = new
SyncFolderItemsResponseMessageType();
responseMessage = syncItemsResponse.ResponseMessages.Items[0] as
SyncFolderItemsResponseMessageType;
if (responseMessage.ResponseClass == ResponseClassType.Error)
{
throw new Exception(responseMessage.MessageText);
}
else
{
foreach (XmlNode xnSyncNode in snSyncStateNodes)
{
xnSyncNode.Attributes.GetNamedItem("SyncState").Value =
responseMessage.SyncState;
}
sfSyncFile.Save(fnSyncFileName);
if (responseMessage.Changes.Items == null) {
Console.WriteLine("Nothing to Syncronise");
}
else
{
Int32 ncNumberOfChanges = responseMessage.Changes.Items.Length;

Console.WriteLine("Number of items to synchronize: " +
ncNumberOfChanges.ToString());
for(int scSyncChange=0;scSyncChange < ncNumberOfChanges ;scSyncChange++){
Console.WriteLine(responseMessage.Changes.ItemsElementName[scSyncChange].ToString());
switch (responseMessage.Changes.ItemsElementName[scSyncChange].ToString()){
case "Delete" : SyncFolderItemsDeleteType diDeletedItem =
(SyncFolderItemsDeleteType)responseMessage.Changes.Items[scSyncChange];
break ;
case "Create": SyncFolderItemsCreateOrUpdateType ciCreateItem =
(SyncFolderItemsCreateOrUpdateType)responseMessage.Changes.Items[scSyncChange];
createGoogleCalItem(ciCreateItem,ewsServiceBinding,
csGoogleCalendarServer,cuCalendarURL);
break ;
case "Update": SyncFolderItemsCreateOrUpdateType uiUpdateItem =
(SyncFolderItemsCreateOrUpdateType)responseMessage.Changes.Items[scSyncChange];
modifyGoolgCalItem(uiUpdateItem, ewsServiceBinding, csGoogleCalendarServer);
break;

}

}

}
}

}
private static void modifyGoolgCalItem(SyncFolderItemsCreateOrUpdateType
ciCreateItem, ExchangeServiceBinding ewsServiceBinding, CalendarService
csGoogleCalendarServer) {


}
private static void createGoogleCalItem(SyncFolderItemsCreateOrUpdateType
ciCreateItem, ExchangeServiceBinding ewsServiceBinding, CalendarService
csGoogleCalendarServer, String cuCalendarURL)
{


GetItemType giRequest = new GetItemType();
ItemIdType iiItemId = new ItemIdType();
iiItemId.Id = ciCreateItem.Item.ItemId.Id;
iiItemId.ChangeKey = ciCreateItem.Item.ItemId.ChangeKey;
ItemResponseShapeType giResponseShape = new ItemResponseShapeType();
giResponseShape.BaseShape = DefaultShapeNamesType.AllProperties;
giResponseShape.IncludeMimeContent = true;
giRequest.ItemShape = giResponseShape;

giRequest.ItemIds = new ItemIdType[1];
giRequest.ItemIds[0] = iiItemId;
giRequest.ItemShape.BaseShape = DefaultShapeNamesType.AllProperties;
giRequest.ItemShape.IncludeMimeContent = true;
giRequest.ItemShape.BodyType = BodyTypeResponseType.Text;
giRequest.ItemShape.BodyTypeSpecified = true;

GetItemResponseType giResponse = ewsServiceBinding.GetItem(giRequest);
if (giResponse.ResponseMessages.Items[0].ResponseClass ==
ResponseClassType.Error)
{
Console.WriteLine("Error Occured");
Console.WriteLine(giResponse.ResponseMessages.Items[0].MessageText);
}
else
{
ItemInfoResponseMessageType rmResponseMessage =
giResponse.ResponseMessages.Items[0] as ItemInfoResponseMessageType;
CalendarItemType ciCalentry =
(CalendarItemType)rmResponseMessage.Items.Items[0];
EventEntry ceCalendarEntry = new EventEntry();
ceCalendarEntry.Title.Text = ciCalentry.Subject;
if (ciCalentry.Body != null) { ceCalendarEntry.Content.Content =
ciCalentry.Body.Value; }
AtomPerson auAuthor = new AtomPerson(AtomPersonType.Author);
auAuthor.Name = ciCalentry.Organizer.Item.Name;
auAuthor.Email = ciCalentry.Organizer.Item.EmailAddress;
ceCalendarEntry.Authors.Add(auAuthor);

When cwCalenderWhen = new When();

if (ciCalentry.IsAllDayEvent == true)
{
cwCalenderWhen.StartTime = ciCalentry.Start.ToLocalTime();
cwCalenderWhen.EndTime = ciCalentry.End.ToLocalTime();
cwCalenderWhen.AllDay = true;
}
else {
if (ciCalentry.CalendarItemType1 == CalendarItemTypeType.RecurringMaster)
{

RecurrenceType rtRecurrance = ciCalentry.Recurrence;
RecurrenceRangeBaseType rrRecurranceRange = rtRecurrance.Item1;
String rpRecurData = "DTSTART:" + ciCalentry.Start.ToString("yyyyMMddTHHmmssZ")
+ " \r\n"
+ "DTEND:" + ciCalentry.End.ToString("yyyyMMddTHHmmssZ") + " \r\n";
string mdDay;
int frFirstRun = 0;
Hashtable mhMonthhash = new Hashtable();
string msMonthString = "jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec";
string[] ysYearMonths = msMonthString.Split((char)44);
int mval = 1;
foreach (string msMonth in ysYearMonths)
{
mhMonthhash.Add(msMonth, mval);
mval++;
}
RecurrencePatternBaseType rpRecurrancePattern = rtRecurrance.Item;
String rtRecuranceType = rpRecurrancePattern.GetType().Name.ToString();
switch (rtRecuranceType) {
case "WeeklyRecurrencePatternType": WeeklyRecurrencePatternType
wpWeeklyRecurrence = (WeeklyRecurrencePatternType)rpRecurrancePattern;
rpRecurData = rpRecurData + "RRULE:FREQ=WEEKLY;BYDAY=";
frFirstRun = 0;
string[] WeekDays = wpWeeklyRecurrence.DaysOfWeek.Split((char)32);
foreach(string dsDay in WeekDays){
if (frFirstRun == 0)
{
rpRecurData = rpRecurData + dsDay.Substring(0, 2);
frFirstRun = 1;
}
else {
rpRecurData = rpRecurData + "," + dsDay.Substring(0, 2);
}
}
rpRecurData = rpRecurData + ";";
break ;
case "DailyRecurrencePatternType": DailyRecurrencePatternType dpDailyRecurrence
= (DailyRecurrencePatternType)rpRecurrancePattern;
rpRecurData = rpRecurData + "RRULE:FREQ=DAILY;INTERVAL=" +
dpDailyRecurrence.Interval.ToString() + ";";
break;
case "AbsoluteMonthlyRecurrencePatternType":
AbsoluteMonthlyRecurrencePatternType amMonthlyrecurance =
(AbsoluteMonthlyRecurrencePatternType)rpRecurrancePattern;
rpRecurData = rpRecurData + "RRULE:FREQ=MONTHLY;INTERVAL=" +
amMonthlyrecurance.Interval.ToString() + ";";
rpRecurData = rpRecurData + "BYMONTHDAY=" +
amMonthlyrecurance.DayOfMonth.ToString();
rpRecurData = rpRecurData + ";";
break;
case "RelativeMonthlyRecurrencePatternType":
RelativeMonthlyRecurrencePatternType rmMonthlyrecurance =
(RelativeMonthlyRecurrencePatternType)rpRecurrancePattern;
rpRecurData = rpRecurData + "RRULE:FREQ=MONTHLY;INTERVAL=" +
rmMonthlyrecurance.Interval.ToString() + ";";
rpRecurData = rpRecurData + "BYDAY=";
mdDay = rmMonthlyrecurance.DaysOfWeek.ToString().Substring(0, 2);
switch (rmMonthlyrecurance.DayOfWeekIndex.ToString())
{
case "First": rpRecurData = rpRecurData + "1" + mdDay;
break;
case "Second": rpRecurData = rpRecurData + "2" + mdDay;
break;
case "Third": rpRecurData = rpRecurData + "3" + mdDay;
break;
case "Fourth": rpRecurData = rpRecurData + "4" + mdDay;
break;
case "Last": rpRecurData = rpRecurData + "-1" + mdDay;
break;

}
rpRecurData = rpRecurData + ";";
break;
case "RelativeYearlyRecurrencePatternType": RelativeYearlyRecurrencePatternType
ypYearlyRecurrance = (RelativeYearlyRecurrencePatternType)rpRecurrancePattern;
rpRecurData = rpRecurData + "RRULE:FREQ=YEARLY;";
rpRecurData = rpRecurData + "BYMONTH=" +
mhMonthhash[ypYearlyRecurrance.Month.ToString().ToLower().Substring(0,
3)].ToString() + ";";
mdDay = ypYearlyRecurrance.DaysOfWeek.ToString().Substring(0, 2);
rpRecurData = rpRecurData + "BYDAY=";
switch (ypYearlyRecurrance.DayOfWeekIndex.ToString())
{
case "First": rpRecurData = rpRecurData + "1" + mdDay;
break;
case "Second": rpRecurData = rpRecurData + "2" + mdDay;
break;
case "Third": rpRecurData = rpRecurData + "3" + mdDay;
break;
case "Fourth": rpRecurData = rpRecurData + "4" + mdDay;
break;
case "Last": rpRecurData = rpRecurData + "-1" + mdDay;
break;

}
rpRecurData = rpRecurData + ";";
break;
case "AbsoluteYearlyRecurrencePatternType": AbsoluteYearlyRecurrencePatternType
yaYearlyRecurrance = (AbsoluteYearlyRecurrencePatternType)rpRecurrancePattern;
rpRecurData = rpRecurData + "RRULE:FREQ=YEARLY;";
rpRecurData = rpRecurData + "BYMONTH=" +
mhMonthhash[yaYearlyRecurrance.Month.ToString().ToLower().Substring(0,
3)].ToString() + ";";
rpRecurData = rpRecurData + "BYDAY=" +
yaYearlyRecurrance.DayOfMonth.ToString().Substring(0, 2) + ";";
break;
}
string rtRangeType = rrRecurranceRange.GetType().Name.ToString();
switch (rtRangeType)
{
case "NumberedRecurrenceRangeType": NumberedRecurrenceRangeType nrNumberRecRange
= (NumberedRecurrenceRangeType)rrRecurranceRange;
rpRecurData = rpRecurData + "COUNT=" +
nrNumberRecRange.NumberOfOccurrences.ToString() + ";";
break;
case "EndDateRecurrenceRangeType": EndDateRecurrenceRangeType edDateRecRange =
(EndDateRecurrenceRangeType)rrRecurranceRange;
rpRecurData = rpRecurData + "UNTIL=" +
edDateRecRange.EndDate.ToString("yyyyMMddTHHmmssZ") + ";";
break;
}
rpRecurData = rpRecurData + "\r\n";
Recurrence reRecurrence = new Recurrence();
reRecurrence.Value = rpRecurData;
cwCalenderWhen.StartTime = ciCalentry.Start;
cwCalenderWhen.EndTime = ciCalentry.End;
ceCalendarEntry.Recurrence = reRecurrence;


}
else
{
cwCalenderWhen.StartTime = ciCalentry.Start;
cwCalenderWhen.EndTime = ciCalentry.End;
}
}

ceCalendarEntry.Times.Add(cwCalenderWhen);
if (ciCalentry.Location != null)
{
Where cwCalendarWhere = new Where();
cwCalendarWhere.ValueString = ciCalentry.Location;
ceCalendarEntry.Locations.Add(cwCalendarWhere);
}
ExtendedProperty exIDPropperty = new ExtendedProperty();
exIDPropperty.Name = "http://msgdev.mvps.org/EWSItemID";
exIDPropperty.Value = ciCreateItem.Item.ItemId.Id.ToString();
ceCalendarEntry.ExtensionElements.Add(exIDPropperty);
ExtendedProperty exIDPropperty1 = new ExtendedProperty();
exIDPropperty1.Name = "http://msgdev.mvps.org/EWSChangeKey";
exIDPropperty1.Value = ciCreateItem.Item.ItemId.ChangeKey.ToString();
ceCalendarEntry.ExtensionElements.Add(exIDPropperty1);
Uri piPostUri = new Uri(cuCalendarURL);
AtomEntry insertedEntry = csGoogleCalendarServer.Insert(piPostUri,
ceCalendarEntry);
}

}
}
}

Popular posts from this blog

Testing and Sending email via SMTP using Opportunistic TLS and oAuth in Office365 with PowerShell

As well as EWS and Remote PowerShell (RPS) other mail protocols POP3, IMAP and SMTP have had OAuth authentication enabled in Exchange Online (Official announcement here ). A while ago I created  this script that used Opportunistic TLS to perform a Telnet style test against a SMTP server using SMTP AUTH. Now that oAuth authentication has been enabled in office365 I've updated this script to be able to use oAuth instead of SMTP Auth to test against Office365. I've also included a function to actually send a Message. Token Acquisition  To Send a Mail using oAuth you first need to get an Access token from Azure AD there are plenty of ways of doing this in PowerShell. You could use a library like MSAL or ADAL (just google your favoured method) or use a library less approach which I've included with this script . Whatever way you do this you need to make sure that your application registration  https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-

The MailboxConcurrency limit and using Batching in the Microsoft Graph API

If your getting an error such as Application is over its MailboxConcurrency limit while using the Microsoft Graph API this post may help you understand why. Background   The Mailbox  concurrency limit when your using the Graph API is 4 as per https://docs.microsoft.com/en-us/graph/throttling#outlook-service-limits . This is evaluated for each app ID and mailbox combination so this means you can have different apps running under the same credentials and the poor behavior of one won't cause the other to be throttled. If you compared that to EWS you could have up to 27 concurrent connections but they are shared across all apps on a first come first served basis. Batching Batching in the Graph API is a way of combining multiple requests into a single HTTP request. Batching in the Exchange Mail API's EWS and MAPI has been around for a long time and its common, for email Apps to process large numbers of smaller items for a variety of reasons.  Batching in the Graph is limited to a m

How to test SMTP using Opportunistic TLS with Powershell and grab the public certificate a SMTP server is using

Most email services these day employ Opportunistic TLS when trying to send Messages which means that wherever possible the Messages will be encrypted rather then the plain text legacy of SMTP.  This method was defined in RFC 3207 "SMTP Service Extension for Secure SMTP over Transport Layer Security" and  there's a quite a good explanation of Opportunistic TLS on Wikipedia  https://en.wikipedia.org/wiki/Opportunistic_TLS .  This is used for both Server to Server (eg MTA to MTA) and Client to server (Eg a Message client like Outlook which acts as a MSA) the later being generally Authenticated. Basically it allows you to have a normal plain text SMTP conversation that is then upgraded to TLS using the STARTTLS verb. Not all servers will support this verb so if its not supported then a message is just sent as Plain text. TLS relies on PKI certificates and the administrative issue s that come around certificate management like expired certificates which is why I wrote th
All sample scripts and source code is provided by for illustrative purposes only. All examples are untested in different environments and therefore, I cannot guarantee or imply reliability, serviceability, or function of these programs.

All code contained herein is provided to you "AS IS" without any warranties of any kind. The implied warranties of non-infringement, merchantability and fitness for a particular purpose are expressly disclaimed.