using Google.Apis.Auth.OAuth2;
using Google.Apis.Calendar.v3;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Requests;
using Google.Apis.Util.Store;
using Google.Apis.PeopleService.v1.Data;
using Google.Apis.PeopleService.v1;
using Microsoft.Office.Interop.Outlook;
using Polly;
using Polly.Retry;
using Serilog;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using Application = System.Windows.Forms.Application;
using Event = Google.Apis.Calendar.v3.Data.Event;
using Exception = System.Exception;
using Outlook = Microsoft.Office.Interop.Outlook;
using Polly.Registry;
using Polly.Contrib.WaitAndRetry;
namespace GoContactSyncMod
{
internal class Synchronizer : IDisposable
{
public const int OutlookUserPropertyMaxLength = 32;
public const string OutlookUserPropertyPrefixTemplate = "g/con/";
public const string OutlookUserPropertyTemplate = OutlookUserPropertyPrefixTemplate + "{0}/";
public const string myContactsGroup = "System ContactGroup: My Contacts";
private static readonly object _syncRoot = new object();
internal static string UserName;
private readonly PolicyRegistry registry = null;
public int TotalCount { get; set; }
public int SyncedCount { get; private set; }
public int DeletedCount { get; private set; }
public int ErrorCount { get; private set; }
public int SkippedCount { get; set; }
public int SkippedCountNotMatches { get; set; }
public ConflictResolution ConflictResolution { get; set; }
public DeleteResolution DeleteGoogleResolution { get; set; }
public DeleteResolution DeleteOutlookResolution { get; set; }
public delegate void NotificationHandler(string message);
public delegate void DuplicatesFoundHandler(string title, string message);
public delegate void ErrorNotificationHandler(string title, Exception ex);
public delegate void TimeZoneNotificationHandler(string timeZone);
public event DuplicatesFoundHandler DuplicatesFound;
public event ErrorNotificationHandler ErrorEncountered;
public event TimeZoneNotificationHandler TimeZoneChanges;
public PeopleResource GooglePeopleResource { get; private set; }
private EventsResource GoogleEventsResource { get; set; }
private static NameSpace _OutlookNameSpace;
public static NameSpace OutlookNameSpace
{
get
{
//Just create outlook instance again, in case the namespace is null
CreateOutlookInstance();
return _OutlookNameSpace;
}
}
public static Outlook.Application OutlookApplication { get; private set; }
public Items OutlookContacts { get; private set; }
public Items OutlookAppointments { get; private set; }
public Collection<ContactMatch> OutlookContactDuplicates { get; set; }
public Collection<ContactMatch> GoogleContactDuplicates { get; set; }
public Google.Apis.People.v1.PeopleService GooglePeopleService;
public Collection<Person> GoogleContacts { get; private set; }
private CalendarService GoogleCalendarService;
public Collection<Event> GoogleAppointments { get; private set; }
public Collection<Event> AllGoogleAppointments { get; private set; }
public IList<CalendarListEntry> CalendarList { get; private set; }
public Collection<ContactGroup> GoogleGroups { get; set; }
public string OutlookPropertyPrefix { get; private set; }
public string OutlookPropertyNameId => OutlookPropertyPrefix + "id";
public string OutlookPropertyNameSynced => OutlookPropertyPrefix + "up";
public SyncOption SyncOption { get; set; } = SyncOption.MergeOutlookWins;
public string SyncProfile { get; set; }
public static string SyncContactsFolder { get; set; }
public static string SyncAppointmentsFolder { get; set; }
public static string SyncAppointmentsGoogleFolder { get; set; }
public static string SyncAppointmentsGoogleTimeZone { get; set; }
public static ushort MonthsInPast { get; set; }
public static ushort MonthsInFuture { get; set; }
public static string Timezone { get; set; }
public static bool MappingBetweenTimeZonesRequired { get; set; }
public List<ContactMatch> Contacts { get; private set; }
public List<AppointmentMatch> Appointments { get; private set; }
private HashSet<string> ContactExtendedPropertiesToRemoveIfTooMany = null;
private HashSet<string> ContactExtendedPropertiesToRemoveIfTooBig = null;
private HashSet<string> ContactExtendedPropertiesToRemoveIfDuplicated = null;
/// <summary>
/// If true deletes contacts if synced before, but one is missing. Otherwise contacts will bever be automatically deleted
/// </summary>
public bool SyncDelete { get; set; }
public bool PromptDelete { get; set; }
/// <summary>
/// If true sync also contacts
/// </summary>
public bool SyncContacts { get; set; }
public static bool SyncContactsForceRTF { get; set; }
/// <summary>
/// If true sync also appointments (calendar)
/// </summary>
public bool SyncAppointments { get; set; }
public static bool SyncAppointmentsForceRTF { get; set; }
/// <summary>
/// if true, use Outlook's FileAs for Google Title/FullName. If false, use Outlook's Fullname
/// </summary>
public bool UseFileAs { get; set; }
public Synchronizer()
{
var delay = Backoff.ConstantBackoff(TimeSpan.FromMilliseconds(200), retryCount: 5, fastFirst: true);
var policy = Policy
.Handle<TaskCanceledException>()
.WaitAndRetry(delay, onRetry: (exception, retryCount, context) =>
{
Log.Debug("Retry");
});
registry = new PolicyRegistry()
{
{ "Standard", policy }
};
}
public void LoginToGoogle(string username)
{
Log.Information("Connecting to Google...");
//check if it is now relogin to different user
if (username != UserName)
{
GooglePeopleResource = null;
GoogleEventsResource = null;
SyncAppointmentsGoogleFolder = null;
}
if ((GooglePeopleResource == null && SyncContacts) || GoogleEventsResource == null & SyncAppointments)
{
//OAuth2 for all services
var scopes = new List<string>
{
//"https://www.google.com/m8/feeds",
CalendarService.Scope.Calendar,
Google.Apis.People.v1.PeopleService.Scope.Contacts
};
//take user credentials
UserCredential credential;
var Folder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\GoContactSyncMOD\\";
var AuthFolder = Folder + "\\Auth\\";
var ClientSecretsFile = Folder + "\\Client Secrets\\client_secrets.json";
Stream stream;
if (File.Exists(ClientSecretsFile))
{
stream = new FileStream(ClientSecretsFile, FileMode.Open);
}
else
{
//load client secret from ressources
var jsonSecrets = Properties.Resources.client_secrets;
stream = new MemoryStream(jsonSecrets);
}
//cancel auth request after 60 seconds
var authTimeout = 60;
try
{
var fDS = new FileDataStore(AuthFolder, true);
var clientSecrets = GoogleClientSecrets.Load(stream);
using (var cts = new CancellationTokenSource())
{
//Cancel auth request after timeout
cts.CancelAfter(TimeSpan.FromSeconds(authTimeout));
var ct = cts.Token;
ct.ThrowIfCancellationRequested();
credential = GCSMOAuth2WebAuthorizationBroker.AuthorizeAsync(
clientSecrets.Secrets,
scopes.ToArray(),
username,
ct,
fDS).
Result;
var initializer = new Google.Apis.Services.BaseClientService.Initializer
{
HttpClientInitializer = credential
};
//var parameters = new OAuth2Parameters //ToDo: Check, if still needed for new Google Api
//{
// ClientId = clientSecrets.Secrets.ClientId,
// ClientSecret = clientSecrets.Secrets.ClientSecret,
// // Note: AccessToken is valid only for 60 minutes
// AccessToken = credential.Token.AccessToken,
// RefreshToken = credential.Token.RefreshToken
//};
//Log.Information(Application.ProductName);
//var settings = new RequestSettings(
// Application.ProductName, parameters);
if (SyncContacts)
{
//PeopleRequest = new PeopleRequest(rs);
//PeopleRequest = new PeopleRequest(settings);
GooglePeopleService = GoogleServices.CreatePeopleService(initializer);
GooglePeopleResource = new PeopleResource(GooglePeopleService);
}
if (SyncAppointments)
{
GoogleCalendarService = GoogleServices.CreateCalendarService(initializer);
const int NumberOfRetries = 3;
const int DelayOnRetry = 1000;
for (var i = 1; i <= NumberOfRetries; ++i)
{
try
{
CalendarList = GoogleCalendarService.CalendarList.List().Execute().Items;
break;
}
catch (Exception ex) when (i < NumberOfRetries)
{
Log.Debug(ex, $"Try {i}");
Task.Delay(DelayOnRetry);
}
}
//Get Primary Calendar, if not set from outside
if (string.IsNullOrEmpty(SyncAppointmentsGoogleFolder))
{
foreach (var calendar in CalendarList)
{
if (calendar.Primary != null && calendar.Primary.Value)
{
SyncAppointmentsGoogleFolder = calendar.Id;
SyncAppointmentsGoogleTimeZone = calendar.TimeZone;
if (string.IsNullOrEmpty(SyncAppointmentsGoogleTimeZone))
{
Log.Debug($"Empty Google time zone for calendar {calendar.Id}");
}
break;
}
}
}
else
{
var found = false;
foreach (var calendar in CalendarList)
{
if (calendar.Id == SyncAppointmentsGoogleFolder)
{
SyncAppointmentsGoogleTimeZone = calendar.TimeZone;
if (string.IsNullOrEmpty(SyncAppointmentsGoogleTimeZone))
{
Log.Debug($"Empty Google time zone for calendar {calendar.Id}");
}
else
{
found = true;
}
break;
}
}
if (!found)
{
Log.Warning($"Cannot find calendar, id is {SyncAppointmentsGoogleFolder}");
Log.Debug("Listing calendars:");
foreach (var calendar in CalendarList)
{
if (calendar.Primary != null && calendar.Primary.Value)
{
Log.Debug($"Id (primary): {calendar.Id}");
}
else
{
Log.Debug($"Id: {calendar.Id}");
}
}
}
}
if (SyncAppointmentsGoogleFolder == null)
{
throw new Exception("Google Calendar not defined (primary not found)");
}
GoogleEventsResource = GoogleCalendarService.Events;
}
}
}
catch (Exception ex) when (ex.InnerException is OperationCanceledException)
{
Log.Error($"Authorisation to allow GCSM to manage your Google calendar was cancelled. Hint: You have to answer the google consent screen within {authTimeout} seconds. {ex.InnerException.Message}");
}
catch (Exception ex)
{
Log.Error(ex.Message);
}
finally
{
stream.Dispose();
}
}
UserName = username;
var maxUserIdLength = OutlookUserPropertyMaxLength - (OutlookUserPropertyTemplate.Length - 3 + 2);//-3 = to remove {0}, +2 = to add length for "id" or "up"
var userId = username;
if (userId.Length > maxUserIdLength)
{
userId = userId.GetHashCode().ToString("X"); //if a user id would overflow UserProperty name, then use that user id hash code as id.
}
//Remove characters not allowed for Outlook user property names: []_#
userId = userId.Replace("#", "").Replace("[", "").Replace("]", "").Replace("_", "");
OutlookPropertyPrefix = string.Format(OutlookUserPropertyTemplate, userId);
}
public void LoginToOutlook()
{
Log.Information("Connecting to Outlook...");
try
{
CreateOutlookInstance();
}
catch (Exception e) when ((e is COMException) || (e is InvalidCastException))
{
try
{
// If outlook was closed/terminated inbetween, we will receive an Exception
// System.Runtime.InteropServices.COMException (0x800706BA): The RPC server is unavailable. (Exception from HRESULT: 0x800706BA)
// so recreate outlook instance
// And sometimes we receive an Exception
// System.InvalidCastException 0x8001010E (RPC_E_WRONG_THREAD))
Log.Debug(e, "Exception");
Log.Information("Cannot connect to Outlook, creating new instance....");
OutlookApplication = null;
_OutlookNameSpace = null;
CreateOutlookInstance();
}
catch (Exception ex)
{
var message = $"Cannot connect to Outlook.\r\nPlease restart {Application.ProductName} and try again. If error persists, please inform developers on SourceForge.";
// Error again? We need full stacktrace, display it!
throw new Exception(message, ex);
}
}
}
private static void GetAlreadyApplicationIfStarted(int num_tries)
{
//Try to create new Outlook application few times, because mostly it fails the first time, if not yet running
// First try to get the running application in case Outlook is already started
for (var i = 0; i < num_tries; i++)
{
try
{
OutlookApplication = Marshal.GetActiveObject("Outlook.Application") as Outlook.Application;
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
else
{
OutlookApplication = new Outlook.Application();
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
Log.Debug($"CreateOutlookApplication (null), try: {i + 1}");
Thread.Sleep(1000 * 10 * (i + 1));
}
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80029c4a)
{
Log.Debug(ex, "CreateOutlookApplication (0x80029c4a)");
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x800401E3)
{
var processes = Process.GetProcessesByName("OUTLOOK");
var p_num = processes.Length;
Log.Debug($"CreateOutlookApplication (0x800401E3), number of started processes: {p_num}");
if (p_num > 0)
{
try
{
Log.Debug($"CreateOutlookApplication (0x800401E3), number of started processes: {p_num}, 1st try");
OutlookApplication = Marshal.GetActiveObject("Outlook.Application") as Outlook.Application;
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
}
catch (Exception ex1)
{
Log.Debug(ex1, $"CreateOutlookApplication (0x800401E3), number of started processes: {p_num}, 1st exception");
}
Thread.Sleep(1000 * 10);
try
{
Log.Debug($"CreateOutlookApplication (0x800401E3), number of started processes: {p_num}, 2nd try");
OutlookApplication = Marshal.GetActiveObject("Outlook.Application") as Outlook.Application;
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
}
catch (Exception ex1)
{
Log.Debug(ex1, $"CreateOutlookApplication (0x800401E3), number of started processes: {p_num}, 2nd exception");
}
}
try
{
OutlookApplication = new Outlook.Application();
}
catch (COMException e) when ((uint)e.ErrorCode == 0x80080005)
{
Log.Debug("CreateOutlookApplication (0x80080005)");
throw new NotSupportedException("Outlook and \"GO Person Sync Mod\" are started by different users. For example you run Outlook with the \"Run as administrator\" option and \"GO Person Sync Mod\" as regular user (or the other way around). This is not supported.", e);
}
catch (Exception e)
{
Log.Debug(e, "Exception");
}
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
Thread.Sleep(1000 * 10 * (i + 1));
}
catch (COMException ex)
{
Log.Debug(ex, "CreateOutlookApplication (COMException)");
try
{
OutlookApplication = new Outlook.Application();
}
catch (COMException e) when ((uint)e.ErrorCode == 0x80080005)
{
Log.Debug("CreateOutlookApplication (0x80080005)");
throw new NotSupportedException("Outlook and \"GO Person Sync Mod\" are started by different users. For example you run Outlook with the \"Run as administrator\" option and \"GO Person Sync Mod\" as regular user (or the other way around). This is not supported.", e);
}
catch (Exception e)
{
Log.Debug(e, "Exception");
}
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
Thread.Sleep(1000 * 10 * (i + 1));
}
catch (NotSupportedException)
{
throw;
}
catch (InvalidCastException ex)
{
Log.Debug(ex, "CreateOutlookApplication (InvalidCastException)");
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (Exception ex) when (i == (num_tries - 1))
{
Log.Debug(ex, "CreateOutlookApplication (Exception): last try");
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
catch (Exception)
{
Log.Debug($"CreateOutlookApplication (Exception), try: {i + 1}");
Thread.Sleep(1000 * 10 * (i + 1));
}
}
}
private static void CreateApplicationIfNotStarted(int num_tries)
{
// Next try to have new running instance of Outlook
Log.Debug("CreateOutlookApplication: new Outlook.Application");
for (var i = 0; i < num_tries; i++)
{
try
{
OutlookApplication = new Outlook.Application();
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
else
{
Log.Debug($"CreateOutlookApplication (null), try: {i + 1}");
Thread.Sleep(1000 * 10 * (i + 1));
}
}
catch (COMException ex)
{
if ((uint)ex.ErrorCode == 0x80029c4a)
{
Log.Debug(ex, "CreateOutlookApplication (0x80029c4a)");
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
Thread.Sleep(1000 * 10 * (i + 1));
}
catch (InvalidCastException ex)
{
Log.Debug(ex, "CreateOutlookApplication (InvalidCastException)");
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (Exception ex)
{
if (i == (num_tries - 1))
{
Log.Debug(ex, "CreateOutlookApplication (Exception): last try");
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
else
{
Log.Debug($"CreateOutlookApplication (Exception), try: {i + 1}");
Thread.Sleep(1000 * 10 * (i + 1));
}
}
}
}
private static void CreateOutlookApplication()
{
const int num_tries = 3;
GetAlreadyApplicationIfStarted(num_tries);
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
CreateApplicationIfNotStarted(num_tries);
}
private static void CreateOutlookNamespace()
{
const int num_tries = 5;
//Try to create new Outlook namespace few times, because mostly it fails the first time, if not yet running
for (var i = 0; i < num_tries; i++)
{
try
{
_OutlookNameSpace = OutlookApplication.GetNamespace("MAPI");
if (_OutlookNameSpace != null)
{
break; //Exit the for loop, if getting outlook namespace was successful
}
else
{
Log.Debug($"CreateOutlookNamespace (null), try: {i + 1}");
}
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80029c4a)
{
Log.Debug(ex, "CreateOutlookNamespace (0x80029c4a)");
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (COMException ex) when (i == (num_tries - 1))
{
Log.Debug(ex, "CreateOutlookNamespace (COMException): last try");
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
catch (COMException)
{
Log.Debug($"CreateOutlookNamespace (COMException), try: {i + 1}");
Thread.Sleep(1000 * 10 * (i + 1));
}
catch (InvalidCastException ex)
{
Log.Debug(ex, "CreateOutlookNamespace (InvalidCastException)");
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (Exception ex) when (i == (num_tries - 1))
{
Log.Debug(ex, "CreateOutlookNamespace (Exception): last try");
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
catch (Exception)
{
Log.Debug($"CreateOutlookNamespace (Exception), try: {i + 1}");
Thread.Sleep(1000 * 10 * (i + 1));
}
}
}
private static void CreateOutlookInstanceHelper()
{
if (OutlookApplication == null)
{
CreateOutlookApplication();
if (OutlookApplication == null)
{
throw new NotSupportedException("Could not create instance of 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and retry.");
}
}
if (_OutlookNameSpace == null)
{
CreateOutlookNamespace();
if (_OutlookNameSpace == null)
{
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and retry.");
}
Log.Debug($"Connected to Outlook: {VersionInformation.GetOutlookVersion(OutlookApplication)}");
// OutlookNameSpace.Accounts was introduced in later version of Outlook
// calling this in older version (like Outlook 2003) will result in "Attempted to read or write protected memory"
try
{
if (_OutlookNameSpace.Accounts != null && _OutlookNameSpace.Accounts.Count > 1)
{
Log.Debug($"Multiple outlook accounts: {_OutlookNameSpace.Accounts.Count}");
}
}
catch (AccessViolationException)
{
}
}
}
private static void CreateOutlookInstance()
{
CreateOutlookInstanceHelper();
var retryCount = 0;
while (retryCount < 10)
{
try
{
if (string.IsNullOrEmpty(SyncContactsFolder))
{
_OutlookNameSpace.GetDefaultFolder(OlDefaultFolders.olFolderContacts);
}
else
{
_OutlookNameSpace.GetFolderFromID(SyncContactsFolder);
}
return;
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80040201)
{
retryCount++;
Log.Debug("0x80040201 - LogoffOutlookNameSpace");
LogoffOutlookNameSpace();
Log.Debug("0x80040201 - CreateOutlookInstanceHelper");
CreateOutlookInstanceHelper();
Log.Debug("0x80040201 - GetFolder");
if (string.IsNullOrEmpty(SyncContactsFolder))
{
_OutlookNameSpace.GetDefaultFolder(OlDefaultFolders.olFolderContacts);
}
else
{
_OutlookNameSpace.GetFolderFromID(SyncContactsFolder);
}
Log.Debug("0x80040201 - Done");
return;
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80010001)
{
retryCount++;
// RPC_E_CALL_REJECTED - sleep and retry
Log.Debug($"RPC_E_CALL_REJECTED, trying {retryCount}");
Thread.Sleep(1000);
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80029c4a)
{
Log.Debug(ex, "Exception");
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80040111)
{
try
{
Log.Debug("Trying to logon, 1st try");
_OutlookNameSpace.Logon("", "", false, false);
Log.Debug("1st try OK");
}
catch (Exception e1)
{
Log.Debug(e1, "Exception");
try
{
Log.Debug("Trying to logon, 2nd try");
_OutlookNameSpace.Logon("", "", true, true);
Log.Debug("2nd try OK");
}
catch (Exception e2)
{
Log.Debug(e2, "Exception");
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", e2);
}
}
}
catch (COMException ex)
{
Log.Debug(ex, "Exception");
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
}
}
private static void LogoffOutlookNameSpace()
{
try
{
Log.Debug("Disconnecting from Outlook...");
if (_OutlookNameSpace != null)
{
_OutlookNameSpace.Logoff();
}
}
catch (Exception)
{
// if outlook was closed inbetween, we get an System.InvalidCastException or similar exception, that indicates that outlook cannot be acced anymore
// so as outlook is closed anyways, we just ignore the exception here
}
try
{
Log.Debug($"Total allocated memory before collection: {GC.GetTotalMemory(false):N0}");
_OutlookNameSpace = null;
OutlookApplication = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Log.Debug($"Total allocated memory after collection: {GC.GetTotalMemory(false):N0}");
}
finally
{
Log.Debug("Disconnected from Outlook");
}
}
public void LogoffOutlook()
{
try
{
Log.Debug("Disconnecting from Outlook...");
if (_OutlookNameSpace != null)
{
_OutlookNameSpace.Logoff();
}
}
catch (Exception)
{
// if outlook was closed inbetween, we get an System.InvalidCastException or similar exception, that indicates that outlook cannot be acced anymore
// so as outlook is closed anyways, we just ignore the exception here
}
try
{
Log.Debug($"Total allocated memory before collection: {GC.GetTotalMemory(false):N0}");
OutlookContactDuplicates = null;
GoogleContactDuplicates = null;
GoogleContacts = null;
GoogleCalendarService = null;
GooglePeopleService = null;
GoogleAppointments = null;
AllGoogleAppointments = null;
CalendarList = null;
GoogleGroups = null;
Contacts = null;
Appointments = null;
ContactExtendedPropertiesToRemoveIfTooMany = null;
ContactExtendedPropertiesToRemoveIfTooBig = null;
ContactExtendedPropertiesToRemoveIfDuplicated = null;
OutlookContacts = null;
OutlookAppointments = null;
_OutlookNameSpace = null;
OutlookApplication = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Log.Debug($"Total allocated memory after collection: {GC.GetTotalMemory(false):N0}");
}
finally
{
Log.Debug("Disconnected from Outlook");
}
}
public void LogoffGoogle()
{
GooglePeopleResource = null;
GoogleEventsResource = null;
}
private void LoadOutlookContacts()
{
Log.Information("Loading Outlook contacts...");
OutlookContacts = GetContactItems();
Log.Debug($"Outlook Contacts Found: {OutlookContacts.Count}");
}
private void LoadOutlookAppointments()
{
Log.Information("Loading Outlook appointments...");
OutlookAppointments = GetAppointmentItems();
Log.Debug($"Outlook Appointments Found: {OutlookAppointments.Count}");
}
public MAPIFolder GetAppoimentsFolder()
{
return GetMAPIFolder(OlDefaultFolders.olFolderCalendar, SyncAppointmentsFolder);
}
private MAPIFolder GetMAPIFolder(OlDefaultFolders outlookDefaultFolder, string syncFolder)
{
MAPIFolder mapiFolder;
if (string.IsNullOrEmpty(syncFolder))
{
mapiFolder = OutlookNameSpace.GetDefaultFolder(outlookDefaultFolder);
if (mapiFolder == null)
{
throw new Exception($"Error getting Default OutlookFolder: {outlookDefaultFolder}");
}
}
else
{
try
{
mapiFolder = OutlookNameSpace.GetFolderFromID(syncFolder);
if (mapiFolder == null)
{
throw new Exception($"Error getting OutlookFolder: {syncFolder}");
}
}
catch (COMException ex)
{
Log.Debug(ex, "Exception");
LogoffOutlook();
LoginToOutlook();
mapiFolder = OutlookNameSpace.GetFolderFromID(syncFolder);
if (mapiFolder == null)
{
throw new Exception($"Error getting OutlookFolder: {syncFolder}");
}
}
}
return mapiFolder;
}
private Items GetAppointmentItems()
{
return GetOutlookItems(OlDefaultFolders.olFolderCalendar, SyncAppointmentsFolder);
}
private Items GetContactItems()
{
return GetOutlookItems(OlDefaultFolders.olFolderContacts, SyncContactsFolder);
}
private Items GetOutlookItems(OlDefaultFolders outlookDefaultFolder, string syncFolder)
{
var mapiFolder = GetMAPIFolder(outlookDefaultFolder, syncFolder);
var items = mapiFolder.Items;
if (items == null)
{
throw new Exception($"Error getting Outlook items from Outlook folder: {mapiFolder.Name}");
}
else
{
return items;
}
}
private void LoadGoogleContacts()
{
LoadGoogleContacts(null);
Log.Debug($"Google Contacts Found: {GoogleContacts.Count}");
}
private void ScanForInvalidContact()
{
var invalid_contact = string.Empty;
long i = 1;
Log.Debug("Checking started");
try
{
var peopleQuery = GooglePeopleResource.Connections.List("people/me");
peopleQuery.RequestMaskIncludeField = new List<string>() { "person.names" };
do
{
var peopleFeed = peopleQuery.Execute();
if (peopleFeed != null && peopleFeed.Connections != null && peopleFeed.Connections.Count > 0)
{
foreach (var person in peopleFeed.Connections)
{
var name = ContactPropertiesUtils.GetGoogleContactName(person);
if (name != null)
{
invalid_contact = name.DisplayName;
invalid_contact = invalid_contact.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ");
if (!string.IsNullOrWhiteSpace(invalid_contact))
{
Log.Debug($"Checking ({i}): {invalid_contact}");
}
else
{
Log.Debug($"Checking ({i}): N/A");
}
i++;
if (name.Metadata != null)
{
if (name.Metadata.Source != null)
{
if (name.Metadata.Source.Id is string id)
{
if (!string.IsNullOrWhiteSpace(id))
{
//var uri = new Uri(ContactsQuery.CreateContactsUri("default") + "/" + id);
var contact = GooglePeopleResource.Get(@"people/"+id); //ToDo: Check
Thread.Sleep(2000);
}
}
}
}
}
}
peopleQuery.PageToken = peopleFeed.NextPageToken;
}
} while (!string.IsNullOrEmpty(peopleQuery.PageToken));
Log.Debug("Checking finished");
}
catch (Google.GoogleApiException ex) //ToDo: Check counterpart of ClientFeedException in Google People Api (is it really GoogleApiException?)
{
if (ex.InnerException is FormatException)
{
if (!string.IsNullOrWhiteSpace(invalid_contact))
{
Log.Error($"Error parsing contact: {invalid_contact}. Please check if contact has some date fields (like birthday) with ill formed date.");
return;
}
else
{
Log.Error("Error parsing contact: N/A. Please check if contact has some date fields (like birthday) with ill formed date.");
return;
}
}
Log.Debug(ex, "Exception");
}
catch (Exception ex)
{
Log.Debug(ex, "Exception");
}
}
private Person LoadGoogleContacts(string id)
{
const string message = "Error Loading Google Contacts. Cannot connect to Google.\r\nPlease ensure you are connected to the internet. If you are behind a proxy, change your proxy configuration!";
const string service = "GCSM.Synchronizer.LoadGoogleContacts";
Person ret = null;
try
{
if (id == null) // Only log, if not specific Google Contacts are searched
{
Log.Information("Loading Google Contacts...");
}
GoogleContacts = new Collection<Person>();
var group = GetGoogleGroupByName(myContactsGroup);
const int num_tries = 5;
//var start_index = 0;
for (var i = 0; i < num_tries; i++)
{
try
{
//var query = new ContactsQuery(ContactsQuery.CreateContactsUri("default"))
//{
// NumberToRetrieve = 256,
// StartIndex = start_index
//};
////Only load Google Contacts in My Contacts group (to avoid syncing accounts added automatically to "Weitere Kontakte"/"Further Contacts")
//if (group != null)
//{
// query.ContactGroup = group.Id;
//}
var query = GooglePeopleResource.Connections.List("people/me");
string pageToken = null;
//query.MaxResults = 256; //ToDo: Find a way to retrieve all appointments
query.PageToken = pageToken;
var feed = query.Execute();
//while (feed != null)
//{
foreach (var a in feed.Connections)
{
GoogleContacts.Add(a);
if (id != null && id.Equals(ContactPropertiesUtils.GetGoogleId(a)))
{
ret = a;
}
}
// start_index += query.NumberToRetrieve;
// query.StartIndex = start_index;
// feed = PeopleRequest.Get(feed, FeedRequestType.Next);
//}
return ret;
}
catch (ThreadAbortException)
{
Log.Debug($"LoadGoogleContacts, retry {i + 1}...");
}
}
}
catch (Google.GoogleApiException ex) //ToDo: Check counterpart of Google People Api
{
if (ex.InnerException is FormatException)
{
Log.Error("One of your contacts at Google probably has invalid date inside one of date fields (for example birthday)");
ScanForInvalidContact();
}
throw;
}
catch (WebException ex)//ToDo: Check counterpart of GDataRequestException in Google People Api, really GoogleApiException?
{
throw new Google.GoogleApiException(service, message, ex);
}
catch (NullReferenceException ex)
{
throw new Google.GoogleApiException(service, message, new WebException("Error accessing feed", ex));
}
return ret;
}
public void LoadGoogleGroups()
{
var message = "Error Loading Google Groups. Cannot connect to Google.\r\nPlease ensure you are connected to the internet. If you are behind a proxy, change your proxy configuration!";
var service = "GCSM.Synchronizer.LoadGoogleGroups";
try
{
Log.Information("Loading Google Groups...");
ContactGroupsResource groupsResource = new ContactGroupsResource(GooglePeopleService);
//var query = new GroupsQuery(GroupsQuery.CreateGroupsUri("default"))
//{
// NumberToRetrieve = 256,
// StartIndex = 0
//};
//query.ShowDeleted = false;
GoogleGroups = new Collection<ContactGroup>();
var listRequest = groupsResource.List();
var feed = listRequest.Execute();
//var feed = PeopleRequest.Connections..Get<ContactGroup>(query);
//while (feed != null)
//{
foreach (var a in feed.ContactGroups)
{
GoogleGroups.Add(a);
}
//query.StartIndex += query.NumberToRetrieve;
//feed = PeopleRequest.Get(feed, FeedRequestType.Next);
//}
}
catch (WebException ex) //ToDo: Check counterpart of GDataRequestException in Google People Api, really GoogleApiException?
{
//Log.Error(message);
throw new Google.GoogleApiException(service, message, ex);
}
catch (NullReferenceException ex)
{
//Log.Error(message);
throw new Google.GoogleApiException(service, message, new WebException("Error accessing feed", ex));
}
}
private void LoadGoogleAppointments()
{
Log.Information("Loading Google appointments...");
LoadGoogleAppointments(null, MonthsInPast, MonthsInFuture, null, null);
Log.Debug("Google Appointments Found: " + GoogleAppointments.Count);
}
/// <summary>
/// Resets Google appointment matches.
/// </summary>
/// <param name="deleteGoogleAppointments">Should Google appointments be updated or deleted.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
internal async Task ResetGoogleAppointmentMatches(bool deleteGoogleAppointments, CancellationToken cancellationToken)
{
const int num_retries = 5;
Log.Information("Processing Google appointments.");
AllGoogleAppointments = null;
GoogleAppointments = null;
// First run batch updates, but since individual requests are not retried in case of any error rerun
// updates in single mode
if (await BatchResetGoogleAppointmentMatches(deleteGoogleAppointments, cancellationToken))
{
// in case of error retry single updates five times
for (var i = 1; i < num_retries; i++)
{
if (!await SingleResetGoogleAppointmentMatches(deleteGoogleAppointments, cancellationToken))
{
break;
}
}
}
Log.Information("Finished all Google changes.");
}
/// <summary>
/// Resets Google appointment matches via single updates.
/// </summary>
/// <param name="deleteGoogleAppointments">Should Google appointments be updated or deleted.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>If error occured.</returns>
internal async Task<bool> SingleResetGoogleAppointmentMatches(bool deleteGoogleAppointments, CancellationToken cancellationToken)
{
const string message = "Error resetting Google appointments.";
const string service = "GCSM.Synchronizer.SingleResetGoogleAppointmentMatches";
var key = OutlookPropertiesUtils.GetKey(SyncProfile);
try
{
var query = GoogleEventsResource.List(SyncAppointmentsGoogleFolder);
string pageToken = null;
if (MonthsInPast != 0)
{
query.TimeMin = DateTime.Now.AddMonths(-MonthsInPast);
}
if (MonthsInFuture != 0)
{
query.TimeMax = DateTime.Now.AddMonths(MonthsInFuture);
}
Log.Information("Processing single updates.");
Events feed;
var gone_error = false;
var modified_error = false;
do
{
query.PageToken = pageToken;
//TODO (obelix30) - convert to Polly after retargeting to 4.5
try
{
feed = await query.ExecuteAsync(cancellationToken);
}
catch (Google.GoogleApiException ex)
{
if (GoogleServices.IsTransientError(ex.HttpStatusCode, ex.Error))
{
await Task.Delay(TimeSpan.FromMinutes(10), cancellationToken);
feed = await query.ExecuteAsync(cancellationToken);
}
else
{
throw;
}
}
foreach (var a in feed.Items)
{
if (a.Id != null)
{
try
{
if (deleteGoogleAppointments)
{
if (a.Status != "cancelled")
{
await GoogleEventsResource.Delete(SyncAppointmentsGoogleFolder, a.Id).ExecuteAsync(cancellationToken);
}
}
else if (a.ExtendedProperties != null && a.ExtendedProperties.Shared != null && a.ExtendedProperties.Shared.ContainsKey(key))
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, a);
if (a.Status != "cancelled")
{
await GoogleEventsResource.Update(a, SyncAppointmentsGoogleFolder, a.Id).ExecuteAsync(cancellationToken);
}
}
}
catch (Google.GoogleApiException ex)
{
if (ex.HttpStatusCode == HttpStatusCode.Gone)
{
gone_error = true;
}
else if (ex.HttpStatusCode == HttpStatusCode.PreconditionFailed)
{
modified_error = true;
}
else
{
throw;
}
}
}
}
pageToken = feed.NextPageToken;
}
while (pageToken != null);
if (modified_error)
{
Log.Debug("Some Google appointments modified before update.");
}
if (gone_error)
{
Log.Debug("Some Google appointments gone before deletion.");
}
return gone_error || modified_error;
}
catch (WebException ex) //ToDo: Check counterpart of Google People Api
{
throw new Google.GoogleApiException(service, message, ex);
}
catch (NullReferenceException ex)
{
throw new Google.GoogleApiException(service, message, new WebException("Error accessing feed", ex));
}
}
/// <summary>
/// Resets Google appointment matches via batch updates.
/// </summary>
/// <param name="deleteGoogleAppointments">Should Google appointments be updated or deleted.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>If error occured.</returns>
internal async Task<bool> BatchResetGoogleAppointmentMatches(bool deleteGoogleAppointments, CancellationToken cancellationToken)
{
const string message = "Error updating Google appointments.";
const string service = "GCSM.Synchronizer.BatchResetGoogleAppointmentMatches";
var key = OutlookPropertiesUtils.GetKey(SyncProfile);
try
{
var query = GoogleEventsResource.List(SyncAppointmentsGoogleFolder);
string pageToken = null;
if (MonthsInPast != 0)
{
query.TimeMin = DateTime.Now.AddMonths(-MonthsInPast);
}
if (MonthsInFuture != 0)
{
query.TimeMax = DateTime.Now.AddMonths(MonthsInFuture);
}
Log.Information("Processing batch updates.");
Events feed;
var br = new BatchRequest(GoogleCalendarService);
var events = new Dictionary<string, Event>();
var gone_error = false;
var modified_error = false;
var rate_error = false;
var current_batch_rate_error = false;
var batches = 1;
do
{
query.PageToken = pageToken;
//TODO (obelix30) - check why sometimes exception happen like below, we have custom backoff attached
// Google.GoogleApiException occurred
//User Rate Limit Exceeded[403]
//Errors[
// Message[User Rate Limit Exceeded] Location[- ] Reason[userRateLimitExceeded] Domain[usageLimits]
//TODO (obelix30) - convert to Polly after retargeting to 4.5
try
{
feed = await query.ExecuteAsync(cancellationToken);
}
catch (Google.GoogleApiException ex)
{
if (GoogleServices.IsTransientError(ex.HttpStatusCode, ex.Error))
{
await Task.Delay(TimeSpan.FromMinutes(10), cancellationToken);
feed = await query.ExecuteAsync(cancellationToken);
}
else
{
throw;
}
}
foreach (var a in feed.Items)
{
if (a.Id != null && !events.ContainsKey(a.Id))
{
IClientServiceRequest r = null;
if (a.Status != "cancelled")
{
if (deleteGoogleAppointments)
{
events.Add(a.Id, a);
r = GoogleEventsResource.Delete(SyncAppointmentsGoogleFolder, a.Id);
}
else if (a.ExtendedProperties != null && a.ExtendedProperties.Shared != null && a.ExtendedProperties.Shared.ContainsKey(key))
{
events.Add(a.Id, a);
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, a);
r = GoogleEventsResource.Update(a, SyncAppointmentsGoogleFolder, a.Id);
}
}
if (r != null)
{
br.Queue<Event>(r, (content, error, ii, msg) =>
{
if (error != null && msg != null)
{
if (msg.StatusCode == HttpStatusCode.PreconditionFailed)
{
modified_error = true;
}
else if (msg.StatusCode == HttpStatusCode.Gone)
{
gone_error = true;
}
else if (GoogleServices.IsTransientError(msg.StatusCode, error))
{
rate_error = true;
current_batch_rate_error = true;
}
else
{
Log.Information($"Batch error: {error}");
}
}
});
if (br.Count >= GoogleServices.BatchRequestSize)
{
if (current_batch_rate_error)
{
current_batch_rate_error = false;
await Task.Delay(GoogleServices.BatchRequestBackoffDelay);
Log.Debug($"Back-Off waited {GoogleServices.BatchRequestBackoffDelay}ms before next retry...");
}
await br.ExecuteAsync(cancellationToken);
// TODO(obelix30): https://github.com/google/google-api-dotnet-client/issues/725
br = new BatchRequest(GoogleCalendarService);
Log.Information($"Batch of Google changes finished ({batches})");
batches++;
}
}
}
}
pageToken = feed.NextPageToken;
}
while (pageToken != null);
if (br.Count > 0)
{
await br.ExecuteAsync(cancellationToken);
Log.Information($"Batch of Google changes finished ({batches})");
}
if (modified_error)
{
Log.Debug("Some Google appointment modified before update.");
}
if (gone_error)
{
Log.Debug("Some Google appointment gone before deletion.");
}
if (rate_error)
{
Log.Debug("Rate errors received.");
}
return gone_error || modified_error || rate_error;
}
catch (WebException ex) //ToDo: Check counterpart of GDataRequestException in Google People Api, really GoogleApiException?
{
throw new Google.GoogleApiException(service, message, ex);
}
catch (NullReferenceException ex)
{
throw new Google.GoogleApiException(service, message, new WebException("Error accessing feed", ex));
}
}
public Event GetGoogleAppointment(string gid)
{
var ga = GetGoogleAppointmentById(gid);
if (ga != null)
{
return ga;
}
else
{
var policy = registry.Get<RetryPolicy>("Standard");
var result = policy.ExecuteAndCapture(() =>
{
return GoogleEventsResource.Get(SyncAppointmentsGoogleFolder, gid).Execute();
});
return result.Result;
}
}
public void DeleteGoogleAppointment(Event ga)
{
if (ga != null && !ga.Status.Equals("cancelled"))
{
GoogleEventsResource.Delete(SyncAppointmentsGoogleFolder, ga.Id).Execute();
}
}
public EventsResource.InstancesRequest GetGoogleAppointmentInstances(string id)
{
return GoogleEventsResource.Instances(SyncAppointmentsGoogleFolder, id);
}
internal Event LoadGoogleAppointments(string id, ushort restrictMonthsInPast, ushort restrictMonthsInFuture, DateTime? restrictStartTime, DateTime? restrictEndTime)
{
const string message = "Error Loading Google appointments. Cannot connect to Google.\r\nPlease ensure you are connected to the internet. If you are behind a proxy, change your proxy configuration!";
const string service = "GCSM.Synchronizer.LoadGoogleAppointments";
Event ret = null;
try
{
GoogleAppointments = new Collection<Event>();
var query = GoogleEventsResource.List(SyncAppointmentsGoogleFolder);
string pageToken = null;
//query.MaxResults = 256; //ToDo: Find a way to retrieve all appointments
//Only Load events from month range, but only if not a distinct Google Appointment is searched for
if (restrictMonthsInPast != 0)
{
query.TimeMin = DateTime.Now.AddMonths(-MonthsInPast);
}
if (restrictStartTime != null && (query.TimeMin == default(DateTime) || restrictStartTime > query.TimeMin))
{
query.TimeMin = restrictStartTime.Value;
}
if (restrictMonthsInFuture != 0)
{
query.TimeMax = DateTime.Now.AddMonths(MonthsInFuture);
}
if (restrictEndTime != null && (query.TimeMax == default(DateTime) || restrictEndTime < query.TimeMax))
{
query.TimeMax = restrictEndTime.Value;
}
Events feed;
do
{
query.PageToken = pageToken;
feed = query.Execute();
foreach (var a in feed.Items)
{
if ((a.RecurringEventId != null || !a.Status.Equals("cancelled")) &&
!GoogleAppointments.Contains(a) //ToDo: For an unknown reason, some appointments are duplicate in GoogleAppointments, therefore remove all duplicates before continuing
)
{//only return not yet cancelled events (except for recurrence exceptions) and events not already in the list
GoogleAppointments.Add(a);
if (/*restrictStartDate == null && */id != null && id.Equals(a.Id))
{
ret = a;
}
//ToDo: Doesn't work for all recurrences
/*else if (restrictStartDate != null && id != null && a.RecurringEventId != null && a.Times.Count > 0 && restrictStartDate.Value.Date.Equals(a.Times[0].StartTime.Date))
if (id.Equals(new string(id.AbsoluteUri.Substring(0, id.AbsoluteUri.LastIndexOf("/") + 1) + a.RecurringEventId.IdOriginal)))
ret = a;*/
}
//else
//{
// Log.Information("Skipped Appointment because it was cancelled on Google side: " + a.Summary + " - " + GetTime(a));
//SkippedCount++;
//}
}
pageToken = feed.NextPageToken;
}
while (pageToken != null);
}
catch (WebException ex) //ToDo: Check counterpart of GDataRequestException in Google People Api, really GoogleApiException?
{
//Log.Error(message);
throw new Google.GoogleApiException(service, message, ex);
}
catch (NullReferenceException ex)
{
//Log.Error(message);
throw new Google.GoogleApiException(service, message, new WebException("Error accessing feed", ex));
}
//Remember, if all Google Appointments have been loaded
if (restrictMonthsInPast == 0 && restrictMonthsInFuture == 0 && restrictStartTime == null && restrictEndTime == null) //restrictStartDate == null)
{
AllGoogleAppointments = GoogleAppointments;
}
return ret;
}
/// <summary>
/// Load the contacts from Google and Outlook
/// </summary>
public void LoadContacts()
{
LoadOutlookContacts();
LoadGoogleGroups();
LoadGoogleContacts();
RemoveOutlookDuplicatedContacts();
RemoveGoogleDuplicatedContacts();
}
public bool IsOutlookAppointmentToBeProcessed(AppointmentItem oa)
{
try
{
if (oa == null)
{
return false;
}
if (oa.IsDeleted())
{
return false;
}
if (string.IsNullOrEmpty(oa.Subject) && oa.Start == AppointmentSync.outlookDateMin)
{
return false;
}
if (oa.MeetingStatus == OlMeetingStatus.olMeetingCanceled || oa.MeetingStatus == OlMeetingStatus.olMeetingReceivedAndCanceled)
{
return false;
}
if (MonthsInPast > 0)
{
if (oa.IsRecurring)
{
RecurrencePattern rp = null;
try
{
rp = oa.GetRecurrence();
if (rp.PatternEndDate < DateTime.Now.AddMonths(-MonthsInPast))
{
return false;
}
}
catch (Exception ex)
{
Log.Debug(ex, "Exception");
return false;
}
finally
{
if (rp != null)
{
Marshal.ReleaseComObject(rp);
}
}
}
else
{
if (oa.End < DateTime.Now.AddMonths(-MonthsInPast))
{
return false;
}
}
}
if (MonthsInFuture > 0)
{
if (oa.IsRecurring)
{
RecurrencePattern rp = null;
try
{
rp = oa.GetRecurrence();
if (rp.PatternStartDate > DateTime.Now.AddMonths(MonthsInFuture))
{
return false;
}
}
catch (Exception ex)
{
Log.Debug(ex, "Exception");
return false;
}
finally
{
if (rp != null)
{
Marshal.ReleaseComObject(rp);
}
}
}
else
{
if (oa.Start > DateTime.Now.AddMonths(MonthsInFuture))
{
return false;
}
}
}
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
Log.Warning($"Accessing Outlook appointment: {oa.ToLogString()} threw and exception. Skipping: {ex.Message}");
Log.Debug(ex, "Exception");
return false;
}
return true;
}
/// <summary>
/// Remove duplicates from Google: two different Google appointments pointing to the same Outlook appointment.
/// </summary>
private void RemoveGoogleDuplicatedAppointments()
{
Log.Information("Removing Google duplicated appointments...");
var appointments = new Dictionary<string, int>();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Log.Debug($"Total allocated memory: {GC.GetTotalMemory(false):N0}");
//scan all Google appointments
for (var i = 0; i < GoogleAppointments.Count; i++)
{
var ga1 = GoogleAppointments[i];
if (ga1 == null)
{
continue;
}
try
{
var oid = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, ga1);
//check if Google event is linked to Outlook appointment
if (string.IsNullOrEmpty(oid))
{
continue;
}
//check if there is already another Google event linked to the same Outlook appointment
if (appointments.ContainsKey(oid))
{
var ga2 = GoogleAppointments[appointments[oid]];
if (ga2 == null)
{
appointments.Remove(oid);
continue;
}
var oa = GetOutlookAppointmentById(oid);
if (IsOutlookAppointmentToBeProcessed(oa))
{
var gid = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, oa);
//check to which Outlook appoinment Google event is linked
if (AppointmentPropertiesUtils.GetGoogleId(ga1) == gid)
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga2);
Log.Debug($"Duplicated appointment: {ga2.ToLogString()}.");
appointments[oid] = i;
}
else if (AppointmentPropertiesUtils.GetGoogleId(ga2) == gid)
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga1);
Log.Debug($"Duplicated appointment: {ga1.ToLogString()}.");
}
else
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga1);
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga2);
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa);
oa.Save();
}
}
else
{
//duplicated Google events found, but Outlook appointment does not exist
//so lets clean the link from Google events
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga1);
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga2);
appointments.Remove(oid);
}
if (oa != null)
{
Marshal.ReleaseComObject(oa);
}
}
else
{
appointments.Add(oid, i);
}
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
Log.Debug($"Accessing Google appointment: {ga1.ToLogString()} threw and exception. Skipping: {ex.Message}");
continue;
}
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Log.Debug($"Total allocated memory: {GC.GetTotalMemory(false):N0}");
}
/// <summary>
/// Remove duplicates from Outlook: two different Outlook appointments pointing to the same Google appointment.
/// Such situation typically happens when copy/paste'ing synchronized appointment in Outlook
/// </summary>
private void RemoveOutlookDuplicatedAppointments()
{
Log.Information("Removing Outlook duplicated appointments...");
var appointments = new Dictionary<string, string>();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Log.Debug($"Total allocated memory: {GC.GetTotalMemory(false):N0}");
Items items = null;
try
{
items = GetAppointmentItems();
//scan all appointments
for (var i = 1; i <= items.Count; i++)
{
AppointmentItem oa1 = null;
try
{
oa1 = items[i] as AppointmentItem;
if (!IsOutlookAppointmentToBeProcessed(oa1))
{
if (oa1 != null)
{
Marshal.ReleaseComObject(oa1);
}
continue;
}
var gid = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, oa1);
//check if Outlook appointment is linked to Google event
if (string.IsNullOrEmpty(gid))
{
Marshal.ReleaseComObject(oa1);
continue;
}
//check if there is already another Outlook appointment linked to the same Google event
if (appointments.ContainsKey(gid))
{
var oid2 = appointments[gid];
if (string.IsNullOrEmpty(oid2))
{
Marshal.ReleaseComObject(oa1);
continue;
}
var o = OutlookNameSpace.GetItemFromID(oid2);
//"is" operator creates an implicit variable (COM leak), so unfortunately we need to avoid pattern matching
#pragma warning disable IDE0019 // Use pattern matching
var oa2 = o as AppointmentItem;
#pragma warning restore IDE0019 // Use pattern matching
if (oa2 == null)
{
appointments.Remove(gid);
Marshal.ReleaseComObject(oa1);
continue;
}
var ga = GetGoogleAppointmentById(gid);
if (ga != null)
{
var oid = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, ga);
//check to which Outlook appoinment Google event is linked
if (AppointmentPropertiesUtils.GetOutlookId(oa1) == oid)
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa2);
if (!string.IsNullOrEmpty(oa2.Subject))
{
Log.Debug($"Duplicated appointment: {oa2.ToLogString()}.");
}
appointments[gid] = AppointmentPropertiesUtils.GetOutlookId(oa1);
oa2.Save();
}
else if (AppointmentPropertiesUtils.GetOutlookId(oa2) == oid)
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa1);
if (!string.IsNullOrEmpty(oa1.Subject))
{
Log.Debug($"Duplicated appointment: {oa1.ToLogString()}.");
}
oa1.Save();
}
else
{
//duplicated Outlook appointments found, but Google event does not exist
//so lets clean the link from Outlook appointments
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa1);
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa2);
appointments.Remove(gid);
oa1.Save();
oa2.Save();
}
}
Marshal.ReleaseComObject(oa2);
}
else
{
appointments.Add(gid, AppointmentPropertiesUtils.GetOutlookId(oa1));
}
Marshal.ReleaseComObject(oa1);
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
if (oa1 != null && !string.IsNullOrEmpty(oa1.Subject))
{
Log.Warning(ex, $"Accessing Outlook appointment: {oa1.Subject} threw and exception. Skipping: {ex.Message}");
}
else
{
Log.Warning(ex, $"Accessing Outlook appointment threw and exception. Skipping: {ex.Message}");
}
continue;
}
}
}
finally
{
if (items != null)
{
Marshal.ReleaseComObject(items);
}
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
Log.Debug($"Total allocated memory: {GC.GetTotalMemory(false):N0}");
}
}
/// <summary>
/// Remove duplicates from Google: two different Google contacts pointing to the same Outlook contact.
/// </summary>
private void RemoveGoogleDuplicatedContacts()
{
Log.Information("Removing Google duplicated contacts...");
var contacts = new Dictionary<string, int>();
//scan all Google contacts
for (var i = 0; i < GoogleContacts.Count; i++)
{
var c1 = GoogleContacts[i];
if (c1 == null)
{
continue;
}
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(c1);
try
{
var oid = ContactPropertiesUtils.GetGoogleOutlookContactId(SyncProfile, c1);
//check if Google contact is linked to Outlook contact
if (string.IsNullOrEmpty(oid))
{
continue;
}
//check if there is already another Google contact linked to the same Outlook contact
if (contacts.ContainsKey(oid))
{
var c2 = GoogleContacts[contacts[oid]];
if (c2 == null)
{
contacts.Remove(oid);
continue;
}
var a = GetOutlookContactById(oid);
if (a != null)
{
var gid = ContactPropertiesUtils.GetOutlookGoogleContactId(this, a);
//check to which Outlook contact Google contact is linked
if (ContactPropertiesUtils.GetGoogleId(c1) == gid)
{
var fileAs2 = ContactPropertiesUtils.GetGoogleContactFileAs(c2);
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c2);
if (fileAs2 != null && !string.IsNullOrEmpty(fileAs2.Value))
{
Log.Debug($"Duplicated contact: {fileAs2.Value}.");
}
contacts[oid] = i;
}
else if (ContactPropertiesUtils.GetGoogleId(c2) == gid)
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c1);
if (!string.IsNullOrEmpty(fileAs.Value))
{
Log.Debug($"Duplicated contact: {fileAs.Value}.");
}
}
else
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c1);
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c2);
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, a);
}
}
else
{
//duplicated Google contacts found, but Outlook contact does not exist
//so lets clean the link from Google contacts
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c1);
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c2);
contacts.Remove(oid);
}
}
else
{
contacts.Add(oid, i);
}
}
catch (Exception ex)
{
//this is needed because some contacts throw exceptions
if (c1 != null && !string.IsNullOrEmpty(fileAs.Value))
{
Log.Warning($"Accessing Google contact: {fileAs.Value} threw and exception. Skipping: {ex.Message}");
}
else
{
Log.Warning($"Accessing Google contact threw and exception. Skipping: {ex.Message}");
}
continue;
}
}
}
/// <summary>
/// Remove duplicates from Outlook: two different Outlook contacts pointing to the same Google contact.
/// Such situation typically happens when copy/paste'ing synchronized contact in Outlook
/// </summary>
private void RemoveOutlookDuplicatedContacts()
{
Log.Information("Removing Outlook duplicated contacts...");
var contacts = new Dictionary<string, int>();
//scan all contacts
for (var i = 1; i <= OutlookContacts.Count; i++)
{
ContactItem olc1 = null;
try
{
olc1 = OutlookContacts[i] as ContactItem;
if (olc1 == null)
{
continue;
}
var gid = ContactPropertiesUtils.GetOutlookGoogleContactId(this, olc1);
//check if Outlook contact is linked to Google contact
if (string.IsNullOrEmpty(gid))
{
continue;
}
//check if there is already another Outlook contact linked to the same Google contact
if (contacts.ContainsKey(gid))
{
var o = OutlookContacts[contacts[gid]];
//"is" operator creates an implicit variable (COM leak), so unfortunately we need to avoid pattern matching
#pragma warning disable IDE0019 // Use pattern matching
var olc2 = o as ContactItem;
#pragma warning restore IDE0019 // Use pattern matching
if (olc2 == null)
{
contacts.Remove(gid);
continue;
}
var c = GetGoogleContactById(gid);
if (c != null)
{
var oid = ContactPropertiesUtils.GetGoogleOutlookContactId(SyncProfile, c);
//check to which Outlook contact Google contact is linked
if (ContactPropertiesUtils.GetOutlookId(olc1) == oid)
{
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc2);
Log.Debug($"Duplicated contact: {olc2.ToLogString()}.");
contacts[oid] = i;
}
else if (ContactPropertiesUtils.GetOutlookId(olc2) == oid)
{
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc1);
Log.Debug($"Duplicated contact: {olc1.ToLogString()}.");
}
else
{
//duplicated Outlook contacts found, but Google contact does not exist
//so lets clean the link from Outlook contacts
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc1);
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc2);
contacts.Remove(gid);
}
}
else
{
//duplicated Outlook contacts found, but Google contact does not exist
//so lets clean the link from Outlook contacts
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc1);
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc2);
contacts.Remove(gid);
}
}
else
{
contacts.Add(gid, i);
}
}
catch (Exception ex)
{
//this is needed because some contacts throw exceptions
Log.Debug($"Accessing Outlook contact: {olc1.ToLogString()} threw and exception. Skipping: {ex.Message}");
continue;
}
}
}
public void LoadAppointments()
{
LoadGoogleAppointments();
RemoveOutlookDuplicatedAppointments();
RemoveGoogleDuplicatedAppointments();
LoadOutlookAppointments();
}
/// <summary>
/// Load the contacts from Google and Outlook and match them
/// </summary>
public void MatchContacts()
{
LoadContacts();
Contacts = ContactsMatcher.MatchContacts(this, out var duplicateDataException);
if (duplicateDataException != null)
{
if (DuplicatesFound != null)
{
DuplicatesFound("Google duplicates found", duplicateDataException.Message);
}
else
{
Log.Warning(duplicateDataException.Message);
}
}
Log.Debug($"Person Matches Found: {Contacts.Count}");
}
/// <summary>
/// Load the appointments from Google and Outlook and match them
/// </summary>
public void MatchAppointments()
{
LoadAppointments();
Appointments = AppointmentsMatcher.MatchAppointments(this);
Log.Debug($"Appointment Matches Found: {Appointments.Count}");
}
private void LogSyncParams()
{
Log.Debug("Synchronization options:");
Log.Debug($"Profile: {SyncProfile}");
Log.Debug($"SyncOption: {SyncOption}");
Log.Debug($"SyncDelete: {SyncDelete}");
Log.Debug($"PromptDelete: {PromptDelete}");
if (SyncContacts)
{
Log.Debug("Sync contacts");
if (_OutlookNameSpace != null)
{
var fld = _OutlookNameSpace.GetFolderFromID(SyncContactsFolder);
Log.Debug($"SyncContactsFolder: {fld.FullFolderPath}");
}
Log.Debug($"SyncContactsForceRTF: {SyncContactsForceRTF}");
Log.Debug($"UseFileAs: {UseFileAs}");
}
if (SyncAppointments)
{
try
{
Log.Debug("Sync appointments");
Log.Debug($"MonthsInPast: {MonthsInPast}");
Log.Debug($"MonthsInFuture: {MonthsInFuture}");
if (_OutlookNameSpace != null)
{
var fld = _OutlookNameSpace.GetFolderFromID(SyncAppointmentsFolder);
Log.Debug($"SyncAppointmentsFolder: {fld.FullFolderPath}");
}
Log.Debug($"SyncAppointmentsGoogleFolder: {SyncAppointmentsGoogleFolder}");
Log.Debug($"SyncAppointmentsForceRTF: {SyncAppointmentsForceRTF}");
}
catch (COMException ex)
{
Log.Debug(ex, "Exception");
LogoffOutlook();
LoginToOutlook();
}
}
}
public void Sync()
{
lock (_syncRoot)
{
try
{
if (string.IsNullOrEmpty(SyncProfile))
{
Log.Error("Must set a sync profile. This should be different on each user/computer you sync on.");
return;
}
LogSyncParams();
SyncedCount = 0;
DeletedCount = 0;
ErrorCount = 0;
SkippedCount = 0;
SkippedCountNotMatches = 0;
ConflictResolution = ConflictResolution.Cancel;
DeleteGoogleResolution = DeleteResolution.Cancel;
DeleteOutlookResolution = DeleteResolution.Cancel;
if (SyncContacts)
{
MatchContacts();
}
if (SyncAppointments)
{
Log.Information($"Outlook default time zone: {TimeZoneInfo.Local.Id}");
Log.Information($"Google default time zone: {SyncAppointmentsGoogleTimeZone}");
if (string.IsNullOrEmpty(Timezone))
{
TimeZoneChanges?.Invoke(SyncAppointmentsGoogleTimeZone);
Log.Information("Timezone not configured, changing to default value from Google, it could be adjusted later in GUI.");
}
else if (string.IsNullOrEmpty(SyncAppointmentsGoogleTimeZone))
{
//Timezone was set, but some users do not have time zone set in Google
SyncAppointmentsGoogleTimeZone = Timezone;
}
MappingBetweenTimeZonesRequired = false;
if (TimeZoneInfo.Local.Id != AppointmentSync.IanaToWindows(SyncAppointmentsGoogleTimeZone))
{
MappingBetweenTimeZonesRequired = true;
Log.Warning($"Different time zones in Outlook ({TimeZoneInfo.Local.Id}) and Google (mapped to {AppointmentSync.IanaToWindows(SyncAppointmentsGoogleTimeZone)})");
}
MatchAppointments();
}
if (SyncContacts)
{
if (Contacts == null)
{
return;
}
TotalCount = Contacts.Count + SkippedCountNotMatches;
//Resolve Google duplicates from matches to be synced
ResolveDuplicateContacts(GoogleContactDuplicates);
//Remove Outlook duplicates from matches to be synced
if (OutlookContactDuplicates != null)
{
for (var i = OutlookContactDuplicates.Count - 1; i >= 0; i--)
{
var match = OutlookContactDuplicates[i];
if (Contacts.Contains(match))
{
//ToDo: If there has been a resolution for a duplicate above, there is still skipped increased, check how to distinguish
SkippedCount++;
Contacts.Remove(match);
}
}
}
Log.Information("Syncing groups...");
ContactsMatcher.SyncGroups(this);
Log.Information("Syncing contacts...");
ContactsMatcher.SyncContacts(this);
SaveContacts(Contacts);
}
if (SyncAppointments)
{
if (Appointments == null)
{
return;
}
TotalCount += Appointments.Count + SkippedCountNotMatches;
Log.Information("Syncing appointments...");
AppointmentsMatcher.SyncAppointments(this);
DeleteAppointments(Appointments);
}
}
finally
{
GoogleContacts = null;
GoogleAppointments = null;
OutlookContactDuplicates = null;
GoogleContactDuplicates = null;
GoogleGroups = null;
Contacts = null;
Appointments = null;
}
}
}
private void ResolveDuplicateContacts(Collection<ContactMatch> googleContactDuplicates)
{
if (googleContactDuplicates != null)
{
for (var i = googleContactDuplicates.Count - 1; i >= 0; i--)
{
ResolveDuplicateContact(googleContactDuplicates[i]);
}
}
}
private void ResolveDuplicateContact(ContactMatch match)
{
if (Contacts.Contains(match))
{
if (SyncOption == SyncOption.MergePrompt)
{
//For each OutlookDuplicate: Ask user for the GoogleContact to be synced with
for (var j = match.AllOutlookContactMatches.Count - 1; j >= 0 && match.AllGoogleContactMatches.Count > 0; j--)
{
var olci = match.AllOutlookContactMatches[j];
var oc = olci.GetOriginalItemFromOutlook();
using (var r = new ConflictResolver())
{
switch (r.ResolveDuplicate(olci, match.AllGoogleContactMatches, out var googleContact))
{
case ConflictResolution.Skip:
case ConflictResolution.SkipAlways: //Keep both entries and sync it to both sides
match.AllGoogleContactMatches.Remove(googleContact);
match.AllOutlookContactMatches.Remove(olci);
Contacts.Add(new ContactMatch(null, googleContact));
Contacts.Add(new ContactMatch(olci, null));
break;
case ConflictResolution.OutlookWins:
case ConflictResolution.OutlookWinsAlways: //Keep Outlook and overwrite Google
match.AllGoogleContactMatches.Remove(googleContact);
match.AllOutlookContactMatches.Remove(olci);
match.GoogleContactDirty = true;
UpdateContact(oc, googleContact);
SaveContact(new ContactMatch(olci, googleContact));
break;
case ConflictResolution.GoogleWins:
case ConflictResolution.GoogleWinsAlways: //Keep Google and overwrite Outlook
match.AllGoogleContactMatches.Remove(googleContact);
match.AllOutlookContactMatches.Remove(olci);
UpdateContact(googleContact, oc);
SaveContact(new ContactMatch(olci, googleContact));
break;
default:
throw new ApplicationException("Cancelled");
}
}
//Cleanup the match, i.e. assign a proper OutlookContact and GoogleContact, because can be deleted before
match.OutlookContact = match.AllOutlookContactMatches.Count == 0 ? null : match.AllOutlookContactMatches[0];
}
}
//Cleanup the match, i.e. assign a proper OutlookContact and GoogleContact, because can be deleted before
match.GoogleContact = match.AllGoogleContactMatches.Count == 0 ? null : match.AllGoogleContactMatches[0];
if (match.AllOutlookContactMatches.Count == 0)
{
//If all OutlookContacts have been assigned by the users ==> Create one match for each remaining Google Person to sync them to Outlook
Contacts.Remove(match);
foreach (var googleContact in match.AllGoogleContactMatches)
{
Contacts.Add(new ContactMatch(null, googleContact));
}
}
else if (match.AllGoogleContactMatches.Count == 0)
{
//If all GoogleContacts have been assigned by the users ==> Create one match for each remaining Outlook Person to sync them to Google
Contacts.Remove(match);
foreach (var outlookContact in match.AllOutlookContactMatches)
{
Contacts.Add(new ContactMatch(outlookContact, null));
}
}
else
{
SkippedCount++;
Contacts.Remove(match);
}
}
}
public void DeleteAppointments(List<AppointmentMatch> appointments)
{
foreach (var match in appointments)
{
try
{
DeleteAppointment(match);
}
catch (Exception ex)
{
if (ErrorEncountered != null)
{
ErrorCount++;
SyncedCount--;
var s = match.OutlookAppointment != null ? match.OutlookAppointment.ToLogString() + ")" : match.GoogleAppointment.ToLogString();
var message = $"Failed to synchronize appointment: {s}:\n{ex.Message}";
var newEx = new Exception(message, ex);
ErrorEncountered("Error", newEx);
}
else
{
throw;
}
}
}
}
private void DeleteAppointmentNoGoogle(AppointmentItem oa)
{
var gid = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, oa);
if (gid != null)
{
var name = oa.ToLogString();
if (SyncOption == SyncOption.OutlookToGoogleOnly)
{
SkippedCount++;
Log.Information($"Skipped deletion of Outlook appointment because of SyncOption {SyncOption}: {name}.");
try
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa);
oa.Save();
}
catch (Exception)
{
Log.Warning($"Error resetting match for Outlook appointment: {name}.");
}
}
else if (!SyncDelete)
{
SkippedCount++;
Log.Information($"Skipped deletion of Outlook appointment because SyncDeletion is switched off: {name}.");
}
else
{
// Google appointment was deleted, delete outlook appointment
try
{
//First reset OutlookGoogleContactId to restore it later from trash
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa);
oa.Save();
}
catch (Exception)
{
Log.Warning($"Error resetting match for Outlook appointment: {name}.");
}
oa.Delete();
DeletedCount++;
Log.Information($"Deleted Outlook appointment: {name}.");
}
}
}
private void DeleteAppointmentNoOutlook(Event ga)
{
var oid = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, ga);
if (oid != null)
{
var name = ga.ToLogString();
if (SyncOption == SyncOption.GoogleToOutlookOnly)
{
SkippedCount++;
Log.Information($"Skipped deletion of Google appointment because of SyncOption {SyncOption}: {name}.");
if (ga.Status != "cancelled")
{
try
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, ga);
ga = SaveGoogleAppointment(ga);
}
catch (Exception)
{
Log.Warning($"Error resetting match for Google appointment: {name}.");
}
}
}
else if (!SyncDelete)
{
SkippedCount++;
Log.Information($"Skipped deletion of Google appointment because SyncDeletion is switched off: {name}.");
}
else if (ga.Status != "cancelled")
{
GoogleEventsResource.Delete(SyncAppointmentsGoogleFolder, ga.Id).Execute();
DeletedCount++;
Log.Information($"Deleted Google appointment: {name}.");
}
}
}
public void DeleteAppointment(AppointmentMatch match)
{
if (match.GoogleAppointment == null && match.OutlookAppointment != null)
{
DeleteAppointmentNoGoogle(match.OutlookAppointment);
}
else if (match.GoogleAppointment != null && match.OutlookAppointment == null)
{
DeleteAppointmentNoOutlook(match.GoogleAppointment);
}
}
public void SaveContacts(List<ContactMatch> contacts)
{
foreach (var match in contacts)
{
try
{
SaveContact(match);
}
catch (Exception ex)
{
if (ErrorEncountered != null)
{
ErrorCount++;
SyncedCount--;
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(match.GoogleContact);
if (fileAs == null)
fileAs = new FileAs();
var s = match.OutlookContact != null ? match.OutlookContact.FileAs : fileAs.Value;
var message = $"Failed to synchronize contact: {s}. \nPlease check the contact, if any Email already exists on Google contacts side or if there is too much or invalid data in the notes field. \nIf the problem persists, please try recreating the contact or report the error:\n{ex.Message}";
var newEx = new Exception(message, ex);
ErrorEncountered("Error", newEx);
}
else
{
throw;
}
}
}
}
public bool SaveContact(ContactMatch match)
{
if (match.GoogleContact != null && match.OutlookContact != null)
{
if (match.GoogleContactDirty)
{
//google contact was modified. save.
if (SaveGoogleContact(match))
{
SyncedCount++;
Log.Information($"Updated Google contact from Outlook: \"{match}\".");
}
else
{
return false;
}
}
}
else if (match.GoogleContact == null && match.OutlookContact != null)
{
if (match.OutlookContact.UserProperties.GoogleContactId != null)
{
var name = match.OutlookContact.FileAs;
if (SyncOption == SyncOption.OutlookToGoogleOnly)
{
SkippedCount++;
Log.Information($"Skipped Deletion of Outlook contact because of SyncOption {SyncOption}: {name}.");
}
else if (!SyncDelete)
{
SkippedCount++;
Log.Information($"Skipped Deletion of Outlook contact because SyncDeletion is switched off: {name}.");
}
else
{
// peer google contact was deleted, delete outlook contact
var item = match.OutlookContact.GetOriginalItemFromOutlook();
try
{
//First reset OutlookGoogleContactId to restore it later from trash
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, item);
item.Save();
}
catch (Exception)
{
Log.Warning($"Error resetting match for Outlook contact: \"{name}\".");
}
item.Delete();
DeletedCount++;
Log.Information($"Deleted Outlook contact: \"{name}\".");
}
}
}
else if (match.GoogleContact != null && match.OutlookContact == null)
{
if (ContactPropertiesUtils.GetGoogleOutlookContactId(SyncProfile, match.GoogleContact) != null)
{
if (SyncOption == SyncOption.GoogleToOutlookOnly)
{
SkippedCount++;
Log.Information($"Skipped Deletion of Google contact because of SyncOption {SyncOption}: {ContactMatch.GetName(match.GoogleContact)}.");
}
else if (!SyncDelete)
{
SkippedCount++;
Log.Information($"Skipped Deletion of Google contact because SyncDeletion is switched off: {ContactMatch.GetName(match.GoogleContact)}.");
}
else
{
GooglePeopleResource.DeleteContact(match.GoogleContact.ResourceName).Execute();
DeletedCount++;
Log.Information($"Deleted Google contact: \"{ContactMatch.GetName(match.GoogleContact)}\".");
}
}
}
else
{
throw new ArgumentNullException("To save contacts, at least a GoogleContacat or OutlookContact must be present.");
}
return true;
}
public void UpdateAppointment(AppointmentItem master, ref Event slave)
{
List<Event> l = null;
UpdateAppointment(master, ref slave, ref l);
}
/// <summary>
/// Updates Outlook appointment from master to slave (including groups/categories)
/// </summary>
public void UpdateAppointment(AppointmentItem master, ref Event slave, ref List<Event> GoogleAppointmentExceptions)
{
var updated = false;
if (slave.Creator != null && !AppointmentSync.IsOrganizer(slave.Creator.Email)) // && AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(this.SyncProfile, slave) != null)
{
//ToDo:Maybe find as better way, e.g. to ask the user, if he wants to overwrite the invalid appointment
switch (SyncOption)
{
case SyncOption.MergeGoogleWins:
case SyncOption.GoogleToOutlookOnly:
//overwrite Outlook appointment
Log.Information($"Different Organizer found on Google, invitation maybe NOT sent by Outlook. Google appointment is overwriting Outlook because of SyncOption {SyncOption}: {master.ToLogString()}.");
UpdateAppointment(ref slave, ref master, null);
break;
case SyncOption.MergeOutlookWins:
case SyncOption.OutlookToGoogleOnly:
//overwrite Google appointment
Log.Information($"Different Organizer found on Google, invitation maybe NOT sent by Outlook, but Outlook appointment is overwriting Google because of SyncOption {SyncOption}: {master.ToLogString()}.");
updated = true;
break;
case SyncOption.MergePrompt:
//promp for sync option
if (
//ConflictResolution != ConflictResolution.OutlookWinsAlways && //Shouldn't be used, because Google seems to be the master of the appointment
ConflictResolution != ConflictResolution.GoogleWinsAlways &&
ConflictResolution != ConflictResolution.SkipAlways)
{
using (var r = new ConflictResolver())
{
ConflictResolution = r.Resolve($"Cannot update appointment from Outlook to Google because different Organizer found on Google, invitation maybe NOT sent by Outlook: \"{master.ToLogString()}\". Do you want to update it back from Google to Outlook?", slave, master, this);
}
}
switch (ConflictResolution)
{
case ConflictResolution.Skip:
case ConflictResolution.SkipAlways: //Skip
SkippedCount++;
Log.Information($"{ConflictResolution}:Skipped updating appointment from Outlook to Google because different organizer found on Google: \"{master.ToLogString()}\". Google organizer is " + slave.Creator.Email.Trim().ToLower().Replace("@googlemail.", "@gmail.") + " and user name is " + UserName.Trim().ToLower().Replace("@googlemail.", "@gmail.") + ".");
break;
case ConflictResolution.GoogleWins:
case ConflictResolution.GoogleWinsAlways: //Keep Google and overwrite Outlook
Log.Debug($"{ConflictResolution}: \"{master.ToLogString()}\".");
UpdateAppointment(ref slave, ref master, null);
break;
case ConflictResolution.OutlookWins:
case ConflictResolution.OutlookWinsAlways: //Keep Outlook and overwrite Google
Log.Debug($"{ConflictResolution}: \"{master.ToLogString()}\".");
updated = true;
break;
default:
throw new ApplicationException("Cancelled");
}
break;
}
}
else //Only update, if invitation was not sent on Google side or freshly created during this sync
{
updated = true;
}
if (updated)
{
AppointmentSync.UpdateAppointment(master, slave);
if (slave.Creator == null || AppointmentSync.IsOrganizer(slave.Creator.Email))
{
AppointmentPropertiesUtils.SetGoogleOutlookAppointmentId(SyncProfile, slave, master);
slave = SaveGoogleAppointment(slave);
}
AppointmentPropertiesUtils.SetOutlookGoogleAppointmentId(this, master, slave);
master.Save();
//After saving Google Appointment => also sync recurrence exceptions and save again
//TODO (obelix30), create test for birthdays (auto created by gmail, so user is not organizer)
//and check what happens if recurrence exception is provoked
if ((slave.Creator == null || AppointmentSync.IsOrganizer(slave.Creator.Email)) && master.IsRecurring && master.RecurrenceState == OlRecurrenceState.olApptMaster)
{
if (AppointmentSync.UpdateRecurrenceExceptions(master, slave, ref GoogleAppointmentExceptions, this))
{
slave = SaveGoogleAppointment(slave);
}
}
SyncedCount++;
Log.Information($"Updated appointment from Outlook to Google: \"{master.ToLogString()}\".");
}
}
private bool Save(ref AppointmentItem oa)
{
try
{ //Try to save 2 times, because sometimes the first save fails with a COMException (Outlook aborted)
oa.Save();
}
catch (ArgumentException ex)
{
Log.Warning(ex, "Exception");
if (ex.ParamName != null)
{
Log.Debug($"Invalid param: {ex.ParamName}");
}
oa.ToDebugLog();
throw;
}
catch (Exception)
{
try
{
oa.Save();
}
catch (COMException ex)
{
Log.Warning($"Error saving Outlook appointment: \"{oa.ToLogString()}\".\n" + ex.StackTrace);
return false;
}
}
return true;
}
/// <summary>
/// Updates Outlook appointment from master to slave (including groups/categories)
/// </summary>
public bool UpdateAppointment(ref Event master, ref AppointmentItem slave, List<Event> googleAppointmentExceptions)
{
var updated = false;
if (AppointmentsMatcher.RecipientsCount(slave) > 1 && AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, slave) != null)
{
switch (SyncOption)
{
case SyncOption.MergeOutlookWins:
case SyncOption.OutlookToGoogleOnly:
//overwrite Google appointment
UpdateAppointment(slave, ref master, ref googleAppointmentExceptions);
break;
case SyncOption.MergeGoogleWins:
case SyncOption.GoogleToOutlookOnly:
//overwrite Outlook appointment
updated = true;
break;
case SyncOption.MergePrompt:
//promp for sync option
if (ConflictResolution != ConflictResolution.OutlookWinsAlways &&
ConflictResolution != ConflictResolution.GoogleWinsAlways &&
ConflictResolution != ConflictResolution.SkipAlways)
{
using (var r = new ConflictResolver())
{
ConflictResolution = r.Resolve($"Cannot update appointment from Google to Outlook because multiple participants found: \"{master.ToLogString()}\". Do you want to update it back from Outlook to Google?", slave, master, this);
}
}
switch (ConflictResolution)
{
case ConflictResolution.Skip:
case ConflictResolution.SkipAlways: //Skip
SkippedCount++;
Log.Debug($"{ConflictResolution}: skipped updating appointment from Google to Outlook because multiple participants found: \"{master.ToLogString()}\".");
break;
case ConflictResolution.OutlookWins:
case ConflictResolution.OutlookWinsAlways: //Keep Outlook and overwrite Google
Log.Debug($"{ConflictResolution}: updated appointment from Outlook to Google because multiple participants found: \"{master.ToLogString()}\".");
UpdateAppointment(slave, ref master, ref googleAppointmentExceptions);
break;
case ConflictResolution.GoogleWins:
case ConflictResolution.GoogleWinsAlways: //Keep Google and overwrite Outlook
Log.Debug($"{ConflictResolution}: updated appointment from Google to Outlook because multiple participants found: \"{master.ToLogString()}\".");
updated = true;
break;
default:
throw new ApplicationException("Cancelled");
}
break;
}
}
else //Only update, if invitation was not sent on Outlook side or freshly created during this sync
{
updated = true;
}
if (updated)
{
if (AppointmentSync.UpdateAppointment(master, slave))
{
AppointmentPropertiesUtils.SetOutlookGoogleAppointmentId(this, slave, master);
if (!Save(ref slave))
{
return false;
}
AppointmentPropertiesUtils.SetGoogleOutlookAppointmentId(SyncProfile, master, slave);
master = SaveGoogleAppointment(master);
SyncedCount++;
Log.Information($"Updated appointment from Google to Outlook: \"{master.ToLogString()}\".");
//After saving Outlook Appointment => also sync recurrence exceptions and increase SyncCount
if (master.Recurrence != null && googleAppointmentExceptions != null)
{
if (AppointmentSync.UpdateRecurrenceExceptions(googleAppointmentExceptions, ref slave, this))
{
SyncedCount++;
}
}
}
else
{
SkippedCount++;
var gid = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, slave);
if (!string.IsNullOrWhiteSpace(gid))
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, slave);
if (!Save(ref slave))
{
return false;
}
}
var oid = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, master);
if (!string.IsNullOrWhiteSpace(oid))
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, master);
master = SaveGoogleAppointment(master);
}
}
}
return true;
}
private void SaveOutlookContact(ref Person gc, ContactItem oc)
{
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, gc);
oc.Save();
ContactPropertiesUtils.SetGoogleOutlookContactId(SyncProfile, gc, oc);
var gc1 = SaveGoogleContact(gc);
if (gc1 != null)
{
gc = gc1;
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, gc);
oc.Save();
SaveOutlookPhoto(gc, oc);
}
else
{
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, oc);
oc.Save();
}
}
private static string EscapeXml(string xml)
{
return System.Security.SecurityElement.Escape(xml);
}
public bool SaveGoogleContact(ContactMatch match)
{
var oc = match.OutlookContact.GetOriginalItemFromOutlook();
ContactPropertiesUtils.SetGoogleOutlookContactId(SyncProfile, match.GoogleContact, oc);
match.GoogleContact = SaveGoogleContact(match.GoogleContact);
if (match.GoogleContact != null)
{
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, match.GoogleContact);
oc.Save();
//Now save the Photo
SaveGooglePhoto(match, oc);
return true;
}
else
{
return false;
}
}
//private static string GetXml(Person gc)
//{
// using (var ms = new MemoryStream())
// {
// gc.ContactEntry.SaveToXml(ms);
// var sr = new StreamReader(ms);
// ms.Seek(0, SeekOrigin.Begin);
// return sr.ReadToEnd();
// }
//}
private Person InsertGoogleContact(Person gc, int tries = 1)
{
//insert contact.
//var feedUri = new Uri(ContactsQuery.CreateContactsUri("default"));
try
{
//return PeopleRequest.Insert(feedUri, gc);
return GooglePeopleResource.CreateContact(gc).Execute();
}
catch (ProtocolViolationException) when (tries == 1)
{
//http://stackoverflow.com/questions/23804960/contactsrequest-insertfeeduri-newentry-sometimes-fails-with-system-net-protoc
return InsertGoogleContact(gc, 2);
}
catch (Google.GoogleApiException ex) when (ex.Error!=null & ex.Error.ErrorResponseContent.Contains("Request data is too large.")) //ToDo: Check counterpart of GDataRequestException in Google People Api (is it really GoogleApiException?)
{
var bio = ContactPropertiesUtils.GetGoogleContactBiography(gc);
if (bio == null)
bio = new Biography();
Log.Warning($"Skipping contact {gc.ToLogString()}, it has too large notes field: {bio.Value.Length} characters. Please shorten notes in Outlook contact, otherwise you risk loosing information stored there.");
return null;
}
catch (Google.GoogleApiException ex)
{
Log.Debug(ex, "Exception");
gc.ToDebugLog();
var responseString = ex.Error != null ? EscapeXml(ex.Error.ErrorResponseContent): "NoResponseContent";
//var xml = GetXml(gc);
var newEx = $"Error saving NEW Google contact: {responseString}. \n{ex.Message}";//\n{xml}";
throw new ApplicationException(newEx, ex);
}
catch (Exception ex)
{
Log.Debug(ex, "Exception");
//var xml = GetXml(gc);
var newEx = $"Error saving NEW Google contact:\n{ex.Message}";//\n{xml}";
throw new ApplicationException(newEx, ex);
}
}
private Person UpdateGoogleContact(Person gc, int tries = 1)
{
//contact already present in google. just update
UpdateEmptyUserProperties(gc);
UpdateExtendedProperties(gc);
try
{
return GooglePeopleResource.UpdateContact(gc,gc.ResourceName).Execute();
}
catch (ProtocolViolationException) when (tries == 1)
{
//http://stackoverflow.com/questions/23804960/contactsrequest-insertfeeduri-newentry-sometimes-fails-with-system-net-protoc
return UpdateGoogleContact(gc, 2);
}
catch (ApplicationException)
{
throw;
}
catch (Google.GoogleApiException ex) when (ex.Error != null && ex.Error.ErrorResponseContent.Contains("Request data is too large.")) //ToDo: Check counterpart of GDataRequestException in Google People Api, really GoogleApiException?
{
var bio = ContactPropertiesUtils.GetGoogleContactBiography(gc);
if (bio == null)
bio = new Biography();
Log.Warning($"Skipping contact {gc.ToLogString()}, it has too large notes field: {bio.Value.Length} characters. Please shorten notes in Outlook contact, otherwise you risk loosing information stored there.");
return null;
}
catch (Google.GoogleApiException ex) when (ex.Error != null && ex.Error.ErrorResponseContent.Contains("Invalid country code: ZZ"))
{
Log.Warning($"Skipping contact {gc.ToLogString()}, it has invalid value in country code. Please recreate contact at Google, otherwise you risk loosing information stored there.");
return null;
}
catch (Google.GoogleApiException ex) when (ex.Error != null && ex.Error.ErrorResponseContent.Contains("extendedProperty count limit exceeded: 10") && (tries == 1))
{
//some contacts despite having less extendedProperties still can throw such exception
Log.Debug($"{gc.ToLogString()}: too many extended properties exception thrown: {gc.ClientData.Count}");
UpdateTooManyExtendedProperties(gc, true);
return UpdateGoogleContact(gc, 2);
}
catch (Google.GoogleApiException ex)
{
Log.Debug(ex, "Exception");
gc.ToDebugLog();
var responseString = ex.Error != null?EscapeXml(ex.Error.ErrorResponseContent):"NoErrorResponseContent";
//var xml = GetXml(gc);
var newEx = $"Error saving EXISTING Google contact: {responseString}. \n{ex.Message}";//\n{xml}";
throw new ApplicationException(newEx, ex);
}
catch (Exception ex)
{
Log.Debug(ex, "Exception");
//var xml = GetXml(gc);
var newEx = $"Error saving EXISTING Google contact:\n{ex.Message}";//\n{xml}";
throw new ApplicationException(newEx, ex);
}
}
/// <summary>
/// Only save the google contact without photo update
/// </summary>
/// <param name="gc"></param>
internal Person SaveGoogleContact(Person gc)
{
//check if this contact was not yet inserted on google.
if (string.IsNullOrEmpty(ContactPropertiesUtils.GetGoogleId(gc))) //ToDo: Check (maybe also >0?
{
return InsertGoogleContact(gc);
}
else
{
return UpdateGoogleContact(gc);
}
}
private void UpdateExtendedProperties(Person gc)
{
RemoveTooManyExtendedProperties(gc);
RemoveTooBigExtendedProperties(gc);
RemoveDuplicatedExtendedProperties(gc);
UpdateEmptyExtendedProperties(gc);
UpdateTooManyExtendedProperties(gc);
UpdateTooBigExtendedProperties(gc);
UpdateDuplicatedExtendedProperties(gc);
}
private void UpdateDuplicatedExtendedProperties(Person gc)
{
DeleteDuplicatedPropertiesForm form = null;
try
{
var dups = new HashSet<string>();
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
foreach (var p in gc.ClientData)
{
if (dups.Contains(p.Key))
{
Log.Debug($"{fileAs.Value}: for extended property {p.Key} duplicates were found.");
if (form == null)
{
form = new DeleteDuplicatedPropertiesForm();
}
form.AddExtendedProperty(false, p.Key, "");
}
else
{
dups.Add(p.Key);
}
}
if (form == null)
{
return;
}
if (ContactExtendedPropertiesToRemoveIfDuplicated != null)
{
foreach (var p in ContactExtendedPropertiesToRemoveIfDuplicated)
{
form.AddExtendedProperty(true, p, "");
}
}
form.SortExtendedProperties();
if (SettingsForm.Instance.ShowDeleteDuplicatedPropertiesForm(form) == DialogResult.OK)
{
var allCheck = form.removeFromAll;
if (allCheck)
{
if (ContactExtendedPropertiesToRemoveIfDuplicated == null)
{
ContactExtendedPropertiesToRemoveIfDuplicated = new HashSet<string>();
}
else
{
ContactExtendedPropertiesToRemoveIfDuplicated.Clear();
}
Log.Debug($"{fileAs.Value}: will clean some extended properties for all contacts.");
}
else if (ContactExtendedPropertiesToRemoveIfDuplicated != null)
{
ContactExtendedPropertiesToRemoveIfDuplicated = null;
Log.Debug($"{fileAs.Value}: will clean some extended properties for this contact.");
}
foreach (DataGridViewRow r in form.extendedPropertiesRows)
{
if (Convert.ToBoolean(r.Cells["Selected"].Value))
{
var key = r.Cells["Key"].Value.ToString();
if (allCheck)
{
ContactExtendedPropertiesToRemoveIfDuplicated.Add(key);
}
for (var j = gc.ClientData.Count - 1; j >= 0; j--)
{
if (gc.ClientData[j].Key == key)
{
gc.ClientData.RemoveAt(j);
}
}
Log.Debug($"Extended property to remove: {key}");
}
}
}
}
finally
{
if (form != null)
{
form.Dispose();
}
}
}
private void UpdateTooBigExtendedProperties(Person gc)
{
DeleteTooBigPropertiesForm form = null;
try
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
foreach (var p in gc.ClientData)
{
if (p.Value != null && p.Value.Length > 1012)
{
Log.Debug($"{fileAs.Value}: for extended property {p.Key} size limit exceeded ({p.Value.Length}). Value is: {p.Value}");
if (form == null)
{
form = new DeleteTooBigPropertiesForm();
}
form.AddExtendedProperty(false, p.Key, p.Value);
}
}
if (form == null)
{
return;
}
if (ContactExtendedPropertiesToRemoveIfTooBig != null)
{
foreach (var p in ContactExtendedPropertiesToRemoveIfTooBig)
{
form.AddExtendedProperty(true, p, "");
}
}
form.SortExtendedProperties();
if (SettingsForm.Instance.ShowDeleteTooBigPropertiesForm(form) == DialogResult.OK)
{
var allCheck = form.removeFromAll;
if (allCheck)
{
if (ContactExtendedPropertiesToRemoveIfTooBig == null)
{
ContactExtendedPropertiesToRemoveIfTooBig = new HashSet<string>();
}
else
{
ContactExtendedPropertiesToRemoveIfTooBig.Clear();
}
Log.Debug($"{fileAs.Value}: will clean some extended properties for all contacts.");
}
else if (ContactExtendedPropertiesToRemoveIfTooBig != null)
{
ContactExtendedPropertiesToRemoveIfTooBig = null;
Log.Debug($"{fileAs.Value}: will clean some extended properties for this contact.");
}
foreach (DataGridViewRow r in form.extendedPropertiesRows)
{
if (Convert.ToBoolean(r.Cells["Selected"].Value))
{
var key = r.Cells["Key"].Value.ToString();
if (allCheck)
{
ContactExtendedPropertiesToRemoveIfTooBig.Add(key);
}
for (var j = gc.ClientData.Count - 1; j >= 0; j--)
{
if (gc.ClientData[j].Key == key)
{
gc.ClientData.RemoveAt(j);
}
}
Log.Debug($"Extended property to remove: {key}");
}
}
}
}
finally
{
if (form != null)
{
form.Dispose();
}
}
}
private void UpdateTooManyExtendedProperties(Person gc, bool force = false)
{
if (force || gc.ClientData.Count > 9)
{
if (!force)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
Log.Debug($"{fileAs.Value}: too many extended properties {gc.ClientData.Count}");
}
var contactKey = OutlookPropertiesUtils.GetKey(SyncProfile);
using (var form = new DeleteTooManyPropertiesForm())
{
foreach (var p in gc.ClientData)
{
if (p.Key != contactKey)
{
form.AddExtendedProperty(false, p.Key, p.Value);
}
}
if (ContactExtendedPropertiesToRemoveIfTooMany != null)
{
foreach (var p in ContactExtendedPropertiesToRemoveIfTooMany)
{
form.AddExtendedProperty(true, p, "");
}
}
form.SortExtendedProperties();
if (SettingsForm.Instance.ShowDeleteTooManyPropertiesForm(form) == DialogResult.OK)
{
var allCheck = form.removeFromAll;
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
if (allCheck)
{
if (ContactExtendedPropertiesToRemoveIfTooMany == null)
{
ContactExtendedPropertiesToRemoveIfTooMany = new HashSet<string>();
}
else
{
ContactExtendedPropertiesToRemoveIfTooMany.Clear();
}
Log.Debug($"{fileAs.Value}: will clean some extended properties for all contacts.");
}
else if (ContactExtendedPropertiesToRemoveIfTooMany != null)
{
ContactExtendedPropertiesToRemoveIfTooMany = null;
Log.Debug($"{fileAs.Value}: will clean some extended properties for this contact.");
}
foreach (DataGridViewRow r in form.extendedPropertiesRows)
{
if (Convert.ToBoolean(r.Cells["Selected"].Value))
{
var key = r.Cells["Key"].Value.ToString();
if (allCheck)
{
ContactExtendedPropertiesToRemoveIfTooMany.Add(key);
}
for (var i = gc.ClientData.Count - 1; i >= 0; i--)
{
if (gc.ClientData[i].Key == key)
{
gc.ClientData.RemoveAt(i);
}
}
Log.Debug($"Extended property to remove: {key}");
}
}
}
}
}
}
private static void UpdateEmptyUserProperties(Person gc)
{
// User can create an empty label custom field on the web, but when I retrieve, and update, it throws this:
// Data Request Error Response: [Line 12, Column 44, element gContact:userDefinedField] Missing attribute: 'key'
// Even though I didn't touch it. So, I will search for empty keys, and give them a simple name. Better than deleting...
/*if (gc.ContactEntry == null)
{
return;
}*/
if (gc.ClientData == null)
{
return;
}
var fieldCount = 0;
foreach (var userDefinedField in gc.ClientData)
{
fieldCount++;
if (string.IsNullOrEmpty(userDefinedField.Key))
{
userDefinedField.Key = $"UserField{fieldCount}";
Log.Debug($"Set key to user defined field to avoid errors: {userDefinedField.Key}");
}
//similar error with empty values
if (string.IsNullOrEmpty(userDefinedField.Value))
{
userDefinedField.Value = userDefinedField.Key;
Log.Debug($"Set value to user defined field to avoid errors: {userDefinedField.Value}");
}
}
}
private static void UpdateEmptyExtendedProperties(Person gc)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
foreach (var p in gc.ClientData)
{
if (string.IsNullOrEmpty(p.Value))
{
Log.Debug($"{fileAs.Value}: empty value for {p.Key}");
//if (p.ChildNodes != null)
//{
// Log.Debug($"{fileAs.Value}: childNodes count {p.ChildNodes.Count}");
//}
//else
//{
p.Value = p.Key;
Log.Debug($"{fileAs.Value}: set value to extended property to avoid errors {p.Key}");
//}
}
}
}
private void RemoveDuplicatedExtendedProperties(Person gc)
{
if (ContactExtendedPropertiesToRemoveIfDuplicated != null)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
for (var i = gc.ClientData.Count - 1; i >= 0; i--)
{
var key = gc.ClientData[i].Key;
if (ContactExtendedPropertiesToRemoveIfDuplicated.Contains(key))
{
Log.Debug($"{fileAs.Value}: removed (duplicate) {key}");
gc.ClientData.RemoveAt(i);
}
}
}
}
private void RemoveTooBigExtendedProperties(Person gc)
{
if (ContactExtendedPropertiesToRemoveIfTooBig != null)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
for (var i = gc.ClientData.Count - 1; i >= 0; i--)
{
if (gc.ClientData[i].Value.Length > 1012)
{
var key = gc.ClientData[i].Key;
if (ContactExtendedPropertiesToRemoveIfTooBig.Contains(key))
{
fileAs = new FileAs();
Log.Debug($"{fileAs.Value}: removed (size) {key}");
gc.ClientData.RemoveAt(i);
}
}
}
}
}
private void RemoveTooManyExtendedProperties(Person gc)
{
if (ContactExtendedPropertiesToRemoveIfTooMany != null)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(gc);
if (fileAs == null)
fileAs = new FileAs();
for (var i = gc.ClientData.Count - 1; i >= 0; i--)
{
var key = gc.ClientData[i].Key;
if (ContactExtendedPropertiesToRemoveIfTooMany.Contains(key))
{
Log.Debug($"{fileAs.Value}: removed (count) {key}");
gc.ClientData.RemoveAt(i);
}
}
}
}
private Event InsertGoogleAppointment(Event ga)
{
try
{
return GoogleEventsResource.Insert(ga, SyncAppointmentsGoogleFolder).Execute();
}
catch (Exception ex)
{
ga.ToDebugLog();
Log.Debug(ex, "Exception");
throw new ApplicationException($"Error saving new Google appointment: {ga.ToLogString()}. \n{ex.Message}", ex);
}
}
private Event UpdateGoogleAppointment(Event ga)
{
try
{
return GoogleEventsResource.Update(ga, SyncAppointmentsGoogleFolder, ga.Id).Execute();
}
catch (Google.GoogleApiException ex) when (ex.Error.Code == 412)
{
Log.Debug($"Error saving existing Google appointment: {ga.ToLogString()}");
return ga;
}
catch (Google.GoogleApiException ex) when ((ex.Error.Code == 403) && ex.Error.Message.Equals("The operation can only be performed by the organizer of the event."))
{
var msg = $"Cannot update appointment (you are not organizer): {ga.ToLogString()}";
msg += " - Creator: " + (ga.Creator != null ? ga.Creator.Email : "null");
msg += " - Organizer: " + (ga.Organizer != null ? ga.Organizer.Email : "null");
Log.Debug(msg);
return ga;
}
catch (Google.GoogleApiException ex) when ((ex.Error.Code == 403) && ex.Error.Errors.Count > 1 && ex.Error.Errors[0].Reason.Equals("forbiddenForNonOrganizer"))
{
var msg = $"Cannot update appointment (you are not organizer): {ga.ToLogString()}";
msg += " - Creator: " + (ga.Creator != null ? ga.Creator.Email : "null");
msg += " - Organizer: " + (ga.Organizer != null ? ga.Organizer.Email : "null");
Log.Debug(msg);
return ga;
}
catch (Google.GoogleApiException ex) when ((ex.Error.Code == 403) && ex.Error.Message.Equals("You need to have writer access to this calendar."))
{
Log.Debug($"Cannot update appointment (no write access): {ga.ToLogString()}");
return ga;
}
catch (Exception ex)
{
ga.ToDebugLog();
Log.Debug(ex, "Exception");
Log.Warning($"Error saving existing Google appointment: {ga.ToLogString()}. \n{ex.Message}");
return ga;
}
}
/// <summary>
/// Save the google Appointment
/// </summary>
/// <param name="ga"></param>
public Event SaveGoogleAppointment(Event ga)
{
//check if this contact was not yet inserted on google.
if (ga.Id == null)
{
return InsertGoogleAppointment(ga);
}
else
{
return UpdateGoogleAppointment(ga);
}
}
public void SaveGooglePhoto(ContactMatch match, ContactItem oc)
{
var hasOutlookPhoto = oc.HasPhoto();
if (hasOutlookPhoto)
{
// add outlook photo to google
using (var outlookPhoto = oc.GetOutlookPhoto())
{
if (SaveGooglePhoto(match.GoogleContact, outlookPhoto))
{
//Just save also the Outlook Contact to have the same lastUpdate date as Google
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, match.GoogleContact);
oc.Save();
}
}
}
else
{
var hasGooglePhoto = Utilities.HasPhoto(match.GoogleContact);
if (hasGooglePhoto)
{
//Delete Photo on Google side, if no Outlook photo exists
GooglePeopleResource.DeleteContactPhoto(match.GoogleContact.ResourceName).Execute();
//Just save the Outlook Person to have the same lastUpdate date as Google
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, match.GoogleContact);
oc.Save();
}
}
}
public bool SaveGooglePhoto(Person person, Bitmap photoBitmap)
{
if (photoBitmap != null)
{
//Try up to several times to overcome Google issue
const int num_tries = 5;
for (var retry = 0; retry < num_tries; retry++)
{
try
{
using (var bmp = new Bitmap(photoBitmap))
{
//using (var stream = new MemoryStream(Utilities.BitmapToBytes(bmp)))
//{
var photoReq = new UpdateContactPhotoRequest()
{
PhotoBytes = Convert.ToBase64String(Utilities.BitmapToBytes(bmp))
};
GooglePeopleResource.UpdateContactPhoto(photoReq, person.ResourceName).Execute();
////Just save the Outlook Person to have the same lastUpdate date as Google
//ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, match.GoogleContact);
//oc.Save();
//}
}
return true; //Exit because photo save succeeded
}
catch (Google.GoogleApiException ex) when (
(ex.HttpStatusCode == HttpStatusCode.Forbidden ||
ex.HttpStatusCode == HttpStatusCode.NotFound))
{
Log.Debug(ex, "Exception");
//If Google found a picture for a new Google account, it sets it automatically and throws an error, if updating it with the Outlook photo.
//Therefore save it again and try again to save the photo
if (retry == num_tries - 1)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(person);
if (fileAs == null)
fileAs = new FileAs();
ErrorHandler.Handle(new Exception($"Photo of contact {fileAs} couldn't be saved after {num_tries} tries, maybe Google found its own photo and doesn't allow updating it", ex));
}
else
{
Thread.Sleep(60 * 1000); //sleep 1 minute
}
}
}
}
return false;
}
public void SaveOutlookPhoto(Person gc, ContactItem oc)
{
var hasGooglePhoto = Utilities.HasPhoto(gc);
if (hasGooglePhoto)
{
// add google photo to outlook
//ToDo: add google photo to outlook with new Google API
//Stream stream = _googleService.GetPhoto(match.GoogleContact);
using (var googlePhoto = Utilities.GetGooglePhoto(this, gc))
{
if (googlePhoto != null) // Google may have an invalid photo
{
oc.SetOutlookPhoto(googlePhoto);
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, gc);
oc.Save();
}
}
}
else
{
var hasOutlookPhoto = oc.HasPhoto();
if (hasOutlookPhoto)
{
oc.RemovePicture();
ContactPropertiesUtils.SetOutlookGoogleContactId(this, oc, gc);
oc.Save();
}
}
}
public ContactGroup SaveGoogleGroup(ContactGroup group)
{
var groupsResource = new ContactGroupsResource(GooglePeopleService);
//check if this group was not yet inserted on google.
if (string.IsNullOrEmpty(group.ResourceName)) //ToDo: Check, maybe also use >0
{
//insert group.
//var feedUri = new Uri(GroupsQuery.CreateGroupsUri("default"));
var contactGroupRequest = new CreateContactGroupRequest()
{
ContactGroup = group
};
try
{
return groupsResource.Create(contactGroupRequest).Execute();
}
catch (ProtocolViolationException)
{
//TODO (obelix30)
//http://stackoverflow.com/questions/23804960/contactsrequest-insertfeeduri-newentry-sometimes-fails-with-system-net-protoc
try
{
return groupsResource.Create(contactGroupRequest).Execute();
}
catch (Exception ex)
{
Log.Debug(ex, $"ContactGroup dump: {group}");
throw;
}
}
catch (Exception ex)
{
Log.Debug(ex, $"ContactGroup dump: {group}");
throw;
}
}
else
{
try
{
var contactGroupRequest = new UpdateContactGroupRequest()
{
ContactGroup = group
};
//group already present in google. just update
return groupsResource.Update(contactGroupRequest,group.ResourceName).Execute();
}
catch
{
//TODO: save google group xml for diagnistics
throw;
}
}
}
/// <summary>
/// Updates Google contact from Outlook (including groups/categories)
/// </summary>
public void UpdateContact(ContactItem master, Person slave)
{
ContactSync.UpdateContact(master, slave, UseFileAs);
OverwriteContactGroups(master, slave);
}
/// <summary>
/// Updates Outlook contact from Google (including groups/categories)
/// </summary>
public void UpdateContact(Person master, ContactItem slave)
{
ContactSync.UpdateContact(master, slave, UseFileAs);
OverwriteContactGroups(master, slave);
// -- Immediately save the Outlook contact (including groups) so it can be released, and don't do it in the save loop later
SaveOutlookContact(ref master, slave);
SyncedCount++;
Log.Information($"Updated Outlook contact from Google: \"{slave.ToLogString()}\".");
}
/// <summary>
/// Updates Google contact's groups from Outlook contact
/// </summary>
private void OverwriteContactGroups(ContactItem master, Person slave)
{
var currentGroups = Utilities.GetGoogleGroups(this, slave);
// get outlook categories
var cats = Utilities.GetOutlookGroups(master.Categories);
// remove obsolete groups
var remove = new Collection<ContactGroup>();
bool found;
foreach (var group in currentGroups)
{
found = false;
foreach (var cat in cats)
{
if (group.Name == cat)
{
found = true;
break;
}
}
if (!found)
{
remove.Add(group);
}
}
while (remove.Count != 0)
{
Utilities.RemoveGoogleGroup(slave, remove[0]);
remove.RemoveAt(0);
}
// add new groups
ContactGroup g;
foreach (var cat in cats)
{
if (!Utilities.ContainsGroup(this, slave, cat))
{
// add group to contact
g = GetGoogleGroupByName(cat);
if (g == null)
{
// try to create group again (if not yet created before
g = CreateGroup(cat);
if (g != null)
{
g = SaveGoogleGroup(g);
if (g != null)
{
GoogleGroups.Add(g);
}
else
{
Log.Warning($"Google Groups were supposed to be created prior to saving a contact. Unfortunately the group '{cat}' couldn't be saved on Google side and was not assigned to the contact: {master.ToLogString()}");
}
}
else
{
Log.Warning($"Google Groups were supposed to be created prior to saving a contact. Unfortunately the group '{cat}' couldn't be created and was not assigned to the contact: {master.ToLogString()}");
}
}
if (g != null)
{
Utilities.AddGoogleGroup(slave, g);
}
}
}
//add system ContactGroup My Contacts
if (!Utilities.ContainsGroup(this, slave, myContactsGroup))
{
// add group to contact
g = GetGoogleGroupByName(myContactsGroup);
if (g == null)
{
throw new Exception($"Google {myContactsGroup} doesn't exist");
}
Utilities.AddGoogleGroup(slave, g);
}
}
/// <summary>
/// Updates Outlook contact's categories (groups) from Google groups
/// </summary>
private void OverwriteContactGroups(Person master, ContactItem slave)
{
var newGroups = Utilities.GetGoogleGroups(this, master);
var newCats = new List<string>(newGroups.Count);
foreach (var group in newGroups)
{ //Only add groups that are no SystemGroup (e.g. "System ContactGroup: Meine Kontakte") automatically tracked by Google
if (group.Name != null && !group.Name.Equals(myContactsGroup))
{
newCats.Add(group.Name);
}
}
slave.Categories = string.Join(", ", newCats.ToArray());
}
/// <summary>
/// Resets associantions of Outlook contacts with Google contacts via user props
/// and resets associantions of Google contacts with Outlook contacts via extended properties.
/// </summary>
public void ResetContactMatches()
{
Debug.Assert(OutlookContacts != null, "Outlook Contacts object is null - this should not happen. Please inform Developers.");
Debug.Assert(GoogleContacts != null, "Google Contacts object is null - this should not happen. Please inform Developers.");
try
{
if (string.IsNullOrEmpty(SyncProfile))
{
Log.Error("Must set a sync profile. This should be different on each user/computer you sync on.");
return;
}
lock (_syncRoot)
{
Log.Information("Resetting Google Person matches...");
foreach (var gc in GoogleContacts)
{
try
{
if (gc != null)
{
ResetMatch(gc);
}
}
catch (Exception ex)
{
Log.Warning($"The match of Google contact {ContactMatch.GetName(gc)} couldn't be reset: {ex.Message}");
}
}
Log.Information("Resetting Outlook Person matches...");
var item = OutlookContacts.GetFirst();
while (item != null)
{
//"is" operator creates an implicit variable (COM leak), so unfortunately we need to avoid pattern matching
#pragma warning disable IDE0019 // Use pattern matching
var oc = item as ContactItem;
#pragma warning restore IDE0019 // Use pattern matching
if (oc != null)
{
try
{
ResetMatch(oc);
}
catch (Exception ex)
{
var name = oc.ToLogString();
if (string.IsNullOrWhiteSpace(name))
{
Log.Warning($"The match of Outlook contact couldn't be reset: {ex.Message}");
}
else
{
Log.Warning($"The match of Outlook contact {name} couldn't be reset: {ex.Message}");
}
}
}
else
{
Log.Debug("Empty Outlook contact found (maybe distribution list). Skipping");
}
Marshal.ReleaseComObject(item);
item = OutlookContacts.GetNext();
}
}
}
finally
{
GoogleContacts = null;
}
}
/// <summary>
/// Resets associations of Outlook appointments with Google appointments via user props
/// and vice versa
/// </summary>
public void ResetOutlookAppointmentMatches(bool deleteOutlookAppointments)
{
Debug.Assert(OutlookAppointments != null, "Outlook Appointments object is null - this should not happen. Please inform Developers.");
lock (_syncRoot)
{
Log.Information("Resetting Outlook appointment matches...");
//1 based array
for (var i = OutlookAppointments.Count; i >= 1; i--)
{
AppointmentItem oa = null;
try
{
oa = OutlookAppointments[i] as AppointmentItem;
if (oa == null)
{
Log.Warning("Empty Outlook appointment found (maybe distribution list). Skipping");
continue;
}
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
Log.Warning($"Accessing Outlook appointment threw an exception. Skipping: {ex.Message}");
continue;
}
if (deleteOutlookAppointments)
{
oa.Delete();
}
else
{
try
{
ResetMatch(oa);
}
catch (Exception ex)
{
Log.Warning($"The match of Outlook appointment {oa.ToLogString()} couldn't be reset: {ex.Message}");
}
}
}
}
}
/// <summary>
/// Reset the match link between Google and Outlook contact
/// </summary>
public Person ResetMatch(Person gc)
{
if (gc != null)
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, gc);
return SaveGoogleContact(gc);
}
else
{
return gc;
}
}
/// <summary>
/// Reset the match link between Outlook and Google contact
/// </summary>
public void ResetMatch(ContactItem oc)
{
if (oc != null)
{
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, oc);
oc.Save();
}
}
/// <summary>
/// Reset the match link between Outlook and Google appointment
/// </summary>
public void ResetMatch(AppointmentItem oa)
{
if (oa != null)
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, oa);
oa.Save();
}
}
public ContactMatch ContactByProperty(string name, string email)
{
foreach (var m in Contacts)
{
var fileAs = ContactPropertiesUtils.GetGoogleContactFileAs(m.GoogleContact);
var primaryEmail = ContactPropertiesUtils.GetGoogleContactPrimaryEmail(m.GoogleContact);
var googleName = ContactPropertiesUtils.GetGoogleContactName(m.GoogleContact);
if (googleName != null &&
((primaryEmail != null && primaryEmail.Value == email) ||
(fileAs != null && fileAs.Value == name) ||
(googleName.UnstructuredName == name)))
{
return m;
}
else if (m.OutlookContact != null && (
(m.OutlookContact.Email1Address != null && m.OutlookContact.Email1Address == email) ||
m.OutlookContact.FileAs == name))
{
return m;
}
}
return null;
}
/// <summary>
/// Used to find duplicates.
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public Collection<OutlookContactInfo> OutlookContactByProperty(string name, string value)
{
var col = new Collection<OutlookContactInfo>();
try
{
var item = OutlookContacts.Find($"[{name}] = \"{value}\"") as ContactItem;
while (item != null)
{
col.Add(new OutlookContactInfo(item, this));
item = OutlookContacts.FindNext() as ContactItem;
}
}
catch (Exception)
{
//TODO: should not get here.
}
return col;
}
public ContactGroup GetGoogleGroupById(string id)
{
//return GoogleGroups.FindById(new string(id)) as ContactGroup;
foreach (var group in GoogleGroups)
{
if (@"contactGroups\" + id == group.ResourceName) //ToDo: Check
{
return group;
}
}
return null;
}
public ContactGroup GetGoogleGroupByName(string name)
{
foreach (var group in GoogleGroups)
{
if (group.Name == name)
{
return group;
}
}
return null;
}
public Person GetGoogleContactById(string id)
{
foreach (var gc in GoogleContacts)
{
if (ContactPropertiesUtils.GetGoogleId(gc).Equals(id))
{
return gc;
}
}
return null;
}
public Event GetGoogleAppointmentById(string id)
{
foreach (var ga in GoogleAppointments)
{
if (ga.Id.Equals(id))
{
return ga;
}
}
if (AllGoogleAppointments != null)
{
foreach (var ga in AllGoogleAppointments)
{
if (ga.Id.Equals(id))
{
return ga;
}
}
}
return null;
}
public static AppointmentItem GetOutlookAppointmentById(string id)
{
var o = OutlookNameSpace.GetItemFromID(id);
//"is" operator creates an implicit variable (COM leak), so unfortunately we need to avoid pattern matching
#pragma warning disable IDE0019 // Use pattern matching
var oa = o as AppointmentItem;
#pragma warning restore IDE0019 // Use pattern matching
return oa;
}
public ContactItem GetOutlookContactById(string id)
{
for (var i = OutlookContacts.Count; i >= 1; i--)
{
ContactItem oc;
try
{
oc = OutlookContacts[i] as ContactItem;
if (oc == null)
{
continue;
}
}
catch (Exception)
{
continue;
}
if (ContactPropertiesUtils.GetOutlookId(oc) == id)
{
return oc;
}
}
return null;
}
public ContactGroup CreateGroup(string name)
{
var group = new ContactGroup
{
Name = name
};
//group.GroupEntry.Dirty = true;
return group;
}
public static bool AreEqual(ContactItem oc1, ContactItem oc2)
{
return oc1.Email1Address == oc2.Email1Address;
}
public static int IndexOf(Collection<ContactItem> col, ContactItem oc)
{
for (var i = 0; i < col.Count; i++)
{
if (AreEqual(col[i], oc))
{
return i;
}
}
return -1;
}
internal void DebugContacts()
{
var oCount = string.Empty;
var gCount = string.Empty;
var mCount = string.Empty;
if (SyncContacts)
{
oCount = $"Outlook Person Count: {OutlookContacts.Count}";
gCount = $"Google Person Count: {GoogleContacts.Count}";
mCount = $"Matches Count: {Contacts.Count}";
}
if (SyncAppointments)
{
oCount = $"Outlook appointments Count: {OutlookAppointments.Count}";
gCount = $"Google appointments Count: {GoogleAppointments.Count}";
mCount = $"Matches Count: {Appointments.Count}";
}
MessageBox.Show($"DEBUG INFORMATION\nPlease submit to developer:\n\n{oCount}\n{gCount}\n{mCount}", "DEBUG INFO", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
public static ContactItem CreateOutlookContactItem(string syncContactsFolder)
{
MAPIFolder contactsFolder = null;
Items items = null;
try
{
contactsFolder = OutlookNameSpace.GetFolderFromID(syncContactsFolder);
items = contactsFolder.Items;
return items.Add(OlItemType.olContactItem) as ContactItem;
}
finally
{
if (items != null)
{
Marshal.ReleaseComObject(items);
}
if (contactsFolder != null)
{
Marshal.ReleaseComObject(contactsFolder);
}
}
}
public static AppointmentItem CreateOutlookAppointmentItem(string syncAppointmentsFolder)
{
MAPIFolder appointmentsFolder = null;
Items items = null;
try
{
appointmentsFolder = OutlookNameSpace.GetFolderFromID(syncAppointmentsFolder);
items = appointmentsFolder.Items;
return items.Add(OlItemType.olAppointmentItem) as AppointmentItem;
}
finally
{
if (items != null)
{
Marshal.ReleaseComObject(items);
}
if (appointmentsFolder != null)
{
Marshal.ReleaseComObject(appointmentsFolder);
}
}
}
public void Dispose()
{
if (GoogleCalendarService != null)
{
((IDisposable)GoogleCalendarService).Dispose();
}
}
}
public enum SyncOption
{
MergePrompt,
MergeOutlookWins,
MergeGoogleWins,
OutlookToGoogleOnly,
GoogleToOutlookOnly,
}
}