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

Export calendar Items to a CSV file using Microsoft Graph and Powershell

For the last couple of years the most constantly popular post by number of views on this blog has been  Export calendar Items to a CSV file using EWS and Powershell closely followed by the contact exports scripts. It goes to show this is just a perennial issue that exists around Mail servers, I think the first VBS script I wrote to do this type of thing was late 90's against Exchange 5.5 using cdo 1.2. Now it's 2020 and if your running Office365 you should really be using the Microsoft Graph API to do this. So what I've done is create a PowerShell Module (and I made it a one file script for those that are more comfortable with that format) that's a port of the EWS script above that is so popular. This script uses the ADAL library for Modern Authentication (which if you grab the library from the PowerShell gallery will come down with the module). Most EWS properties map one to one with the Graph and the Graph actually provides better information on recurrences then...

Downloading a shared file from Onedrive for business using Powershell

I thought I'd quickly share this script I came up with to download a file that was shared using One Drive for Business (which is SharePoint under the covers) with Powershell. The following script takes a OneDrive for business URL which would look like https://mydom-my.sharepoint.com/personal/gscales_domain_com/Documents/Email%20attachments/filename.txt This script is pretty simple it uses the SharePoint CSOM (Client side object Model) which it loads in the first line. It uses the URI object to separate the host and relative URL which the CSOM requires and also the SharePointOnlineCredentials object to handle the Office365 SharePoint online authentication. The following script is a function that take the OneDrive URL, Credentials for Office365 and path you want to download the file to and downloads the file. eg to run the script you would use something like ./spdownload.ps1 ' https://mydom-my.sharepoint.com/personal/gscales_domain_com/Documents/Email%20attachments/filen...

Sending a MimeMessage via the Microsoft Graph using the Graph SDK, MimeKit and MSAL

One of the new features added to the Microsoft Graph recently was the ability to create and send Mime Messages (you have been able to get Message as Mime for a while). This is useful in a number of different scenarios especially when trying to create a Message with inline Images which has historically been hard to do with both the Graph and EWS (if you don't use MIME). It also opens up using SMIME for encryption and a more easy migration path for sending using SMTP in some apps. MimeKit is a great open source library for parsing and creating MIME messages so it offers a really easy solution for tackling this issue. The current documentation on Send message via MIME lacks any real sample so I've put together a quick console app that use MSAL, MIME kit and the Graph SDK to send a Message via MIME. As the current Graph SDK also doesn't support sending via MIME either there is a workaround for this in the future my guess is this will be supported.
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.