Thursday, June 07, 2007

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);
}

}
}
}

1 comment:

Kevin said...

This is helpful stuff, I am just starting to dive into EWS and this is a good example.