using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
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.Contacts;
using Google.Documents;
using Google.GData.Client;
using Google.GData.Contacts;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace GoContactSyncMod
{
internal class Synchronizer : IDisposable
{
public const int OutlookUserPropertyMaxLength = 32;
public const string OutlookUserPropertyTemplate = "g/con/{0}/";
internal const string myContactsGroup = "System Group: My Contacts";
private static readonly object _syncRoot = new object();
internal static string UserName;
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, EventType eventType);
public delegate void TimeZoneNotificationHandler(string timeZone);
public event DuplicatesFoundHandler DuplicatesFound;
public event ErrorNotificationHandler ErrorEncountered;
public event TimeZoneNotificationHandler TimeZoneChanges;
public ContactsRequest ContactsRequest { get; private set; }
public EventsResource EventRequest { get; private set; }
private static Outlook.NameSpace OutlookNamespace;
public static Outlook.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 Outlook.Items OutlookContacts { get; private set; }
public Outlook.Items OutlookAppointments { get; private set; }
public Collection<ContactMatch> OutlookContactDuplicates { get; set; }
public Collection<ContactMatch> GoogleContactDuplicates { get; set; }
public Collection<Contact> GoogleContacts { get; private set; }
private CalendarService CalendarRequest;
public Collection<Google.Apis.Calendar.v3.Data.Event> GoogleAppointments { get; private set; }
public Collection<Google.Apis.Calendar.v3.Data.Event> AllGoogleAppointments { get; private set; }
public IList<CalendarListEntry> calendarList { get; private set; }
public Collection<Group> GoogleGroups { get; set; }
public string OutlookPropertyPrefix { get; private set; }
public string OutlookPropertyNameId => OutlookPropertyPrefix + "id";
/*public string OutlookPropertyNameUpdated
{
get { return OutlookPropertyPrefix + "up"; }
}*/
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; }
//private ConflictResolution? _conflictResolution;
//public ConflictResolution? CResolution
//{
// get { return _conflictResolution; }
// set { _conflictResolution = value; }
//}
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;
//private string _authToken;
//public string AuthToken
//{
// get
// {
// return _authToken;
// }
//}
/// <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 void LoginToGoogle(string username)
{
Logger.Log("Connecting to Google...", EventType.Information);
if (ContactsRequest == null && SyncContacts || EventRequest == null & SyncAppointments)
{
//OAuth2 for all services
var scopes = new List<string>
{
//Contacts-Scope
"https://www.google.com/m8/feeds",
//Calendar-Scope
//Didn't work: scopes.Add("https://www.googleapis.com/auth/calendar");
CalendarService.Scope.Calendar
};
//take user credentials
UserCredential credential;
//load client secret from ressources
var jsonSecrets = Properties.Resources.client_secrets;
//using (var stream = new FileStream(Application.StartupPath + "\\client_secrets.json", FileMode.Open, FileAccess.Read))
using (var stream = new MemoryStream(jsonSecrets))
{
var fDS = new FileDataStore(Logger.AuthFolder, true);
var clientSecrets = GoogleClientSecrets.Load(stream);
credential = GCSMOAuth2WebAuthorizationBroker.AuthorizeAsync(
clientSecrets.Secrets,
scopes.ToArray(),
username,
CancellationToken.None,
fDS).
Result;
var initializer = new Google.Apis.Services.BaseClientService.Initializer
{
HttpClientInitializer = credential
};
var parameters = new OAuth2Parameters
{
ClientId = clientSecrets.Secrets.ClientId,
ClientSecret = clientSecrets.Secrets.ClientSecret,
// Note: AccessToken is valid only for 60 minutes
AccessToken = credential.Token.AccessToken,
RefreshToken = credential.Token.RefreshToken
};
Logger.Log(Application.ProductName, EventType.Information);
var settings = new RequestSettings(
Application.ProductName, parameters);
if (SyncContacts)
{
//ContactsRequest = new ContactsRequest(rs);
ContactsRequest = new ContactsRequest(settings);
}
if (SyncAppointments)
{
//ContactsRequest = new Google.Contacts.ContactsRequest()
CalendarRequest = GoogleServices.CreateCalendarService(initializer);
//CalendarRequest.setUserCredentials(username, password);
calendarList = CalendarRequest.CalendarList.List().Execute().Items;
//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))
{
Logger.Log("Empty Google time zone for calendar" + calendar.Id, EventType.Debug);
}
break;
}
}
}
else
{
var found = false;
foreach (var calendar in calendarList)
{
if (calendar.Id == SyncAppointmentsGoogleFolder)
{
SyncAppointmentsGoogleTimeZone = calendar.TimeZone;
if (string.IsNullOrEmpty(SyncAppointmentsGoogleTimeZone))
{
Logger.Log("Empty Google time zone for calendar " + calendar.Id, EventType.Debug);
}
else
{
found = true;
}
break;
}
}
if (!found)
{
Logger.Log("Cannot find calendar, id is " + SyncAppointmentsGoogleFolder, EventType.Warning);
Logger.Log("Listing calendars:", EventType.Debug);
foreach (var calendar in calendarList)
{
if (calendar.Primary != null && calendar.Primary.Value)
{
Logger.Log("Id (primary): " + calendar.Id, EventType.Debug);
}
else
{
Logger.Log("Id: " + calendar.Id, EventType.Debug);
}
}
}
}
if (SyncAppointmentsGoogleFolder == null)
{
throw new Exception("Google Calendar not defined (primary not found)");
}
//EventQuery query = new EventQuery("https://www.google.com/calendar/feeds/default/private/full");
//Old v2 approach: EventQuery query = new EventQuery("https://www.googleapis.com/calendar/v3/calendars/default/events");
EventRequest = CalendarRequest.Events;
}
}
}
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()
{
Logger.Log("Connecting to Outlook...", EventType.Information);
try
{
CreateOutlookInstance();
}
catch (Exception e)
{
if (!(e is COMException) && !(e is InvalidCastException))
{
throw;
}
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 we receive an Exception
// System.InvalidCastException 0x8001010E (RPC_E_WRONG_THREAD))
Logger.Log("Cannot connect to Outlook, creating new instance....", EventType.Information);
/*OutlookApplication = new Outlook.Application();
OutlookNamespace = OutlookApplication.GetNamespace("mapi");
OutlookNamespace.Logon();*/
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 OutlookForge.";
// Error again? We need full stacktrace, display it!
throw new Exception(message, ex);
}
}
}
private static void CreateOutlookApplication()
{
const int num_tries = 3;
//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
}
Logger.Log("CreateOutlookApplication (null), try: " + (i + 1), EventType.Debug);
Thread.Sleep(1000 * 10 * (i + 1));
}
}
catch (COMException ex)
{
if (ex.ErrorCode == unchecked((int)0x80029c4a))
{
Logger.Log("CreateOutlookApplication (0x80029c4a)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
else if (ex.ErrorCode == unchecked((int)0x800401E3))
{
Logger.Log("CreateOutlookApplication (0x800401E3)", EventType.Debug);
}
else
{
Logger.Log("CreateOutlookApplication (COMException)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
}
try
{
OutlookApplication = new Outlook.Application();
}
catch (COMException e)
{
if (e.ErrorCode == unchecked((int)0x80080005))
{
Logger.Log("CreateOutlookApplication (0x80080005)", EventType.Debug);
throw new NotSupportedException("Outlook and \"GO Contact Sync Mod\" are started by different users. For example you run Outlook with the \"Run as administrator\" option and \"GO Contact Sync Mod\" as regular user (or the other way around). This is not supported.", e);
}
}
catch (Exception e)
{
Logger.Log(e, EventType.Debug);
}
if (OutlookApplication != null)
{
return; //Exit, if creating outlook application was successful
}
Thread.Sleep(1000 * 10 * (i + 1));
}
catch (NotSupportedException)
{
throw;
}
catch (InvalidCastException ex)
{
Logger.Log("CreateOutlookApplication (InvalidCastException)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (Exception ex)
{
if (i == (num_tries - 1))
{
Logger.Log("CreateOutlookApplication (Exception): last try", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
else
{
Logger.Log("CreateOutlookApplication (Exception), try: " + (i + 1), EventType.Debug);
Thread.Sleep(1000 * 10 * (i + 1));
}
}
}
// Next try to have new running instance of Outlook
Logger.Log("CreateOutlookApplication: new Outlook.Application", EventType.Debug);
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
{
Logger.Log("CreateOutlookApplication (null), try: " + (i + 1), EventType.Debug);
Thread.Sleep(1000 * 10 * (i + 1));
}
}
catch (COMException ex)
{
if (ex.ErrorCode == unchecked((int)0x80029c4a))
{
Logger.Log("CreateOutlookApplication (0x80029c4a)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
Thread.Sleep(1000 * 10 * (i + 1));
}
catch (InvalidCastException ex)
{
Logger.Log("CreateOutlookApplication (InvalidCastException)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (Exception ex)
{
if (i == (num_tries - 1))
{
Logger.Log("CreateOutlookApplication (Exception): last try", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
else
{
Logger.Log("CreateOutlookApplication (Exception), try: " + (i + 1), EventType.Debug);
Thread.Sleep(1000 * 10 * (i + 1));
}
}
}
}
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
{
Logger.Log("CreateOutlookNamespace (null), try: " + (i + 1), EventType.Debug);
}
}
catch (COMException ex)
{
if (ex.ErrorCode == unchecked((int)0x80029c4a))
{
Logger.Log("CreateOutlookNamespace (0x80029c4a)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
if (i == (num_tries - 1))
{
Logger.Log("CreateOutlookNamespace (COMException): last try", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
else
{
Logger.Log("CreateOutlookNamespace (COMException), try: " + (i + 1), EventType.Debug);
Thread.Sleep(1000 * 10 * (i + 1));
}
}
catch (InvalidCastException ex)
{
Logger.Log("CreateOutlookNamespace (InvalidCastException)", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
catch (Exception ex)
{
if (i == (num_tries - 1))
{
Logger.Log("CreateOutlookNamespace (Exception): last try", EventType.Debug);
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
else
{
Logger.Log("CreateOutlookNamespace (Exception), try: " + (i + 1), EventType.Debug);
Thread.Sleep(1000 * 10 * (i + 1));
}
}
}
}
private static void CreateOutlookInstance()
{
if (OutlookApplication == null || OutlookNamespace == 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.");
}
CreateOutlookNamespace();
if (OutlookNamespace == null)
{
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and retry.");
}
else
{
Logger.Log("Connected to Outlook: " + VersionInformation.GetOutlookVersion(OutlookApplication), EventType.Debug);
if (OutlookNameSpace.Accounts != null && OutlookNameSpace.Accounts.Count > 1)
{
Logger.Log("Multiple outlook accounts: " + OutlookNameSpace.Accounts.Count, EventType.Debug);
}
}
}
/*
// Get default profile name from registry, as this is not always "Outlook" and would popup a dialog to choose profile
// no matter if default profile is set or not. So try to read the default profile, fallback is still "Outlook"
string profileName = "Outlook";
using (RegistryKey k = Registry.CurrentUser.OpenSubKey(@"Software\Microsoft\Office\Outlook\SocialConnector", false))
{
if (k != null)
profileName = k.GetValue("PrimaryOscProfile", "Outlook").ToString();
}
OutlookNamespace.Logon(profileName, null, true, false);*/
//Just try to access the outlookNamespace to check, if it is still accessible, throws COMException, if not reachable
try
{
if (string.IsNullOrEmpty(SyncContactsFolder))
{
OutlookNamespace.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderContacts);
}
else
{
OutlookNamespace.GetFolderFromID(SyncContactsFolder);
}
}
catch (COMException ex)
{
if (ex.ErrorCode == unchecked((int)0x80029c4a))
{
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException(OutlookRegistryUtils.GetPossibleErrorDiagnosis(), ex);
}
else if (ex.ErrorCode == unchecked((int)0x80040111)) //"The server is not available. Contact your administrator if this condition persists."
{
try
{
Logger.Log("Trying to logon, 1st try", EventType.Debug);
OutlookNamespace.Logon("", "", false, false);
Logger.Log("1st try OK", EventType.Debug);
}
catch (Exception e1)
{
Logger.Log(e1, EventType.Debug);
try
{
Logger.Log("Trying to logon, 2nd try", EventType.Debug);
OutlookNamespace.Logon("", "", true, true);
Logger.Log("2nd try OK", EventType.Debug);
}
catch (Exception e2)
{
Logger.Log(e2, EventType.Debug);
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", e2);
}
}
}
else
{
Logger.Log(ex, EventType.Debug);
throw new NotSupportedException("Could not connect to 'Microsoft Outlook'. Make sure Outlook 2003 or above version is installed and running.", ex);
}
}
}
public void LogoffOutlook()
{
try
{
Logger.Log("Disconnecting from Outlook...", EventType.Debug);
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
}
finally
{
if (OutlookNamespace != null)
{
Marshal.ReleaseComObject(OutlookNamespace);
}
if (OutlookApplication != null)
{
Marshal.ReleaseComObject(OutlookApplication);
GC.Collect();
GC.WaitForPendingFinalizers();
}
OutlookNamespace = null;
OutlookApplication = null;
Logger.Log("Disconnected from Outlook", EventType.Debug);
}
}
public void LogoffGoogle()
{
ContactsRequest = null;
}
private void LoadOutlookContacts()
{
Logger.Log("Loading Outlook contacts...", EventType.Information);
OutlookContacts = GetOutlookItems(Outlook.OlDefaultFolders.olFolderContacts, SyncContactsFolder);
Logger.Log("Outlook Contacts Found: " + OutlookContacts.Count, EventType.Debug);
}
private void LoadOutlookAppointments()
{
Logger.Log("Loading Outlook appointments...", EventType.Information);
OutlookAppointments = GetOutlookItems(Outlook.OlDefaultFolders.olFolderCalendar, SyncAppointmentsFolder);
Logger.Log("Outlook Appointments Found: " + OutlookAppointments.Count, EventType.Debug);
}
private Outlook.Items GetOutlookItems(Outlook.OlDefaultFolders outlookDefaultFolder, string syncFolder)
{
Outlook.MAPIFolder mapiFolder = null;
if (string.IsNullOrEmpty(syncFolder))
{
mapiFolder = OutlookNameSpace.GetDefaultFolder(outlookDefaultFolder);
if (mapiFolder == null)
{
throw new Exception("Error getting Default OutlookFolder: " + outlookDefaultFolder);
}
}
else
{
mapiFolder = OutlookNameSpace.GetFolderFromID(syncFolder);
if (mapiFolder == null)
{
throw new Exception("Error getting OutlookFolder: " + syncFolder);
}
//Outlook.MAPIFolder Folder = OutlookNameSpace.GetFolderFromID(_syncFolder);
//if (Folder != null)
//{
// for (int i = 1; i <= Folder.Folders.Count; i++)
// {
// Outlook.Folder subFolder = Folder.Folders[i] as Outlook.Folder;
// if ((Outlook.OlDefaultFolders.olFolderContacts == outlookDefaultFolder && Outlook.OlItemType.olContactItem == subFolder.DefaultItemType)
//
// )
// {
// mapiFolder = subFolder as Outlook.MAPIFolder;
// }
// }
//}
}
try
{
var items = mapiFolder.Items;
if (items == null)
{
throw new Exception("Error getting Outlook items from OutlookFolder: " + mapiFolder.Name);
}
else
{
return items;
}
}
finally
{
if (mapiFolder != null)
{
Marshal.ReleaseComObject(mapiFolder);
}
mapiFolder = null;
}
}
private void LoadGoogleContacts()
{
LoadGoogleContacts(null);
Logger.Log("Google Contacts Found: " + GoogleContacts.Count, EventType.Debug);
}
private Contact LoadGoogleContacts(AtomId id)
{
var 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!";
Contact ret = null;
try
{
if (id == null) // Only log, if not specific Google Contacts are searched
{
Logger.Log("Loading Google Contacts...", EventType.Information);
}
GoogleContacts = new Collection<Contact>();
var query = new ContactsQuery(ContactsQuery.CreateContactsUri("default"))
{
NumberToRetrieve = 256,
StartIndex = 0
};
//Only load Google Contacts in My Contacts group (to avoid syncing accounts added automatically to "Weitere Kontakte"/"Further Contacts")
var group = GetGoogleGroupByName(myContactsGroup);
if (group != null)
{
query.Group = group.Id;
}
//query.ShowDeleted = false;
//query.OrderBy = "lastmodified";
var feed = ContactsRequest.Get<Contact>(query);
while (feed != null)
{
foreach (var a in feed.Entries)
{
GoogleContacts.Add(a);
if (id != null && id.Equals(a.ContactEntry.Id))
{
ret = a;
}
}
query.StartIndex += query.NumberToRetrieve;
feed = ContactsRequest.Get(feed, FeedRequestType.Next);
}
}
catch (WebException ex)
{
//Logger.Log(message, EventType.Error);
throw new GDataRequestException(message, ex);
}
catch (NullReferenceException ex)
{
//Logger.Log(message, EventType.Error);
throw new GDataRequestException(message, new System.Net.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!";
try
{
Logger.Log("Loading Google Groups...", EventType.Information);
var query = new GroupsQuery(GroupsQuery.CreateGroupsUri("default"))
{
NumberToRetrieve = 256,
StartIndex = 0
};
//query.ShowDeleted = false;
GoogleGroups = new Collection<Group>();
var feed = ContactsRequest.Get<Group>(query);
while (feed != null)
{
foreach (var a in feed.Entries)
{
GoogleGroups.Add(a);
}
query.StartIndex += query.NumberToRetrieve;
feed = ContactsRequest.Get(feed, FeedRequestType.Next);
}
////Only for debugging or reset purpose: Delete all Gougle Groups:
//for (int i = GoogleGroups.Count; i > 0;i-- )
// _googleService.Delete(GoogleGroups[i-1]);
}
catch (System.Net.WebException ex)
{
//Logger.Log(message, EventType.Error);
throw new GDataRequestException(message, ex);
}
catch (NullReferenceException ex)
{
//Logger.Log(message, EventType.Error);
throw new GDataRequestException(message, new System.Net.WebException("Error accessing feed", ex));
}
}
private void LoadGoogleAppointments()
{
Logger.Log("Loading Google appointments...", EventType.Information);
LoadGoogleAppointments(null, MonthsInPast, MonthsInFuture, null, null);
Logger.Log("Google Appointments Found: " + GoogleAppointments.Count, EventType.Debug);
}
/// <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;
Logger.Log("Processing Google appointments.", EventType.Information);
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;
}
}
}
Logger.Log("Finished all Google changes.", EventType.Information);
}
/// <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.";
try
{
var query = EventRequest.List(SyncAppointmentsGoogleFolder);
string pageToken = null;
if (MonthsInPast != 0)
{
query.TimeMin = DateTime.Now.AddMonths(-MonthsInPast);
}
if (MonthsInFuture != 0)
{
query.TimeMax = DateTime.Now.AddMonths(MonthsInFuture);
}
Logger.Log("Processing single updates.", EventType.Information);
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 new GDataRequestException(message, ex);
}
}
foreach (var a in feed.Items)
{
if (a.Id != null)
{
try
{
if (deleteGoogleAppointments)
{
if (a.Status != "cancelled")
{
await EventRequest.Delete(SyncAppointmentsGoogleFolder, a.Id).ExecuteAsync(cancellationToken);
}
}
else if (a.ExtendedProperties != null && a.ExtendedProperties.Shared != null && a.ExtendedProperties.Shared.ContainsKey("gos:oid:" + SyncProfile + ""))
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, a);
if (a.Status != "cancelled")
{
await EventRequest.Update(a, SyncAppointmentsGoogleFolder, a.Id).ExecuteAsync(cancellationToken);
}
}
}
catch (Google.GoogleApiException ex)
{
if (ex.HttpStatusCode == System.Net.HttpStatusCode.Gone)
{
gone_error = true;
}
else if (ex.HttpStatusCode == System.Net.HttpStatusCode.PreconditionFailed)
{
modified_error = true;
}
else
{
throw new GDataRequestException("Exception", ex);
}
}
}
}
pageToken = feed.NextPageToken;
}
while (pageToken != null);
if (modified_error)
{
Logger.Log("Some Google appointments modified before update.", EventType.Debug);
}
if (gone_error)
{
Logger.Log("Some Google appointments gone before deletion.", EventType.Debug);
}
return (gone_error || modified_error);
}
catch (WebException ex)
{
throw new GDataRequestException(message, ex);
}
catch (NullReferenceException ex)
{
throw new GDataRequestException(message, new System.Net.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.";
try
{
var query = EventRequest.List(SyncAppointmentsGoogleFolder);
string pageToken = null;
if (MonthsInPast != 0)
{
query.TimeMin = DateTime.Now.AddMonths(-MonthsInPast);
}
if (MonthsInFuture != 0)
{
query.TimeMax = DateTime.Now.AddMonths(MonthsInFuture);
}
Logger.Log("Processing batch updates.", EventType.Information);
Events feed;
var br = new BatchRequest(CalendarRequest);
var events = new Dictionary<string, Google.Apis.Calendar.v3.Data.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 new GDataRequestException(message, ex);
}
}
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 = EventRequest.Delete(SyncAppointmentsGoogleFolder, a.Id);
}
else if (a.ExtendedProperties != null && a.ExtendedProperties.Shared != null && a.ExtendedProperties.Shared.ContainsKey("gos:oid:" + SyncProfile + ""))
{
events.Add(a.Id, a);
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, a);
r = EventRequest.Update(a, SyncAppointmentsGoogleFolder, a.Id);
}
}
if (r != null)
{
br.Queue<Google.Apis.Calendar.v3.Data.Event>(r, (content, error, ii, msg) =>
{
if (error != null && msg != null)
{
if (msg.StatusCode == System.Net.HttpStatusCode.PreconditionFailed)
{
modified_error = true;
}
else if (msg.StatusCode == System.Net.HttpStatusCode.Gone)
{
gone_error = true;
}
else if (GoogleServices.IsTransientError(msg.StatusCode, error))
{
rate_error = true;
current_batch_rate_error = true;
}
else
{
Logger.Log("Batch error: " + error.ToString(), EventType.Information);
}
}
});
if (br.Count >= GoogleServices.BatchRequestSize)
{
if (current_batch_rate_error)
{
current_batch_rate_error = false;
await Task.Delay(GoogleServices.BatchRequestBackoffDelay);
Logger.Log("Back-Off waited " + GoogleServices.BatchRequestBackoffDelay + "ms before next retry...", EventType.Debug);
}
await br.ExecuteAsync(cancellationToken);
// TODO(obelix30): https://github.com/google/google-api-dotnet-client/issues/725
br = new BatchRequest(CalendarRequest);
Logger.Log("Batch of Google changes finished (" + batches + ")", EventType.Information);
batches++;
}
}
}
}
pageToken = feed.NextPageToken;
}
while (pageToken != null);
if (br.Count > 0)
{
await br.ExecuteAsync(cancellationToken);
Logger.Log("Batch of Google changes finished (" + batches + ")", EventType.Information);
}
if (modified_error)
{
Logger.Log("Some Google appointment modified before update.", EventType.Debug);
}
if (gone_error)
{
Logger.Log("Some Google appointment gone before deletion.", EventType.Debug);
}
if (rate_error)
{
Logger.Log("Rate errors received.", EventType.Debug);
}
return (gone_error || modified_error || rate_error);
}
catch (WebException ex)
{
throw new GDataRequestException(message, ex);
}
catch (NullReferenceException ex)
{
throw new GDataRequestException(message, new System.Net.WebException("Error accessing feed", ex));
}
}
internal Google.Apis.Calendar.v3.Data.Event LoadGoogleAppointments(string id, ushort restrictMonthsInPast, ushort restrictMonthsInFuture, DateTime? restrictStartTime, DateTime? restrictEndTime)
{
var 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!";
Google.Apis.Calendar.v3.Data.Event ret = null;
try
{
GoogleAppointments = new Collection<Google.Apis.Calendar.v3.Data.Event>();
var query = EventRequest.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;
}
//Doesn't work:
//if (restrictStartDate != null)
// query.StartDate = restrictStartDate.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 AtomId(id.AbsoluteUri.Substring(0, id.AbsoluteUri.LastIndexOf("/") + 1) + a.RecurringEventId.IdOriginal)))
ret = a;*/
}
//else
//{
// Logger.Log("Skipped Appointment because it was cancelled on Google side: " + a.Summary + " - " + GetTime(a), EventType.Information);
//SkippedCount++;
//}
}
pageToken = feed.NextPageToken;
}
while (pageToken != null);
}
catch (WebException ex)
{
//Logger.Log(message, EventType.Error);
throw new GDataRequestException(message, ex);
}
catch (NullReferenceException ex)
{
//Logger.Log(message, EventType.Error);
throw new GDataRequestException(message, new System.Net.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();
}
/// <summary>
/// Remove duplicates from Google: two different Google appointments pointing to the same Outlook appointment.
/// </summary>
private void RemoveGoogleDuplicatedAppointments()
{
Logger.Log("Removing Google duplicated appointments...", EventType.Information);
var appointments = new Dictionary<string, int>();
//scan all Google appointments
for (var i = 0; i < GoogleAppointments.Count; i++)
{
var e1 = GoogleAppointments[i];
if (e1 == null)
{
continue;
}
try
{
var oid = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, e1);
//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 e2 = GoogleAppointments[appointments[oid]];
if (e2 == null)
{
appointments.Remove(oid);
continue;
}
var a = GetOutlookAppointmentById(oid);
if (a != null)
{
try
{
var gid = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, a);
//check to which Outlook appoinment Google event is linked
if (AppointmentPropertiesUtils.GetGoogleId(e1) == gid)
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, e2);
if (!string.IsNullOrEmpty(e2.Summary))
{
Logger.Log("Duplicated appointment: " + e2.Summary + ".", EventType.Debug);
}
appointments[oid] = i;
}
else if (AppointmentPropertiesUtils.GetGoogleId(e2) == gid)
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, e1);
if (!string.IsNullOrEmpty(e1.Summary))
{
Logger.Log("Duplicated appointment: " + e1.Summary + ".", EventType.Debug);
}
}
else
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, e1);
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, e2);
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, a);
}
}
finally
{
Marshal.ReleaseComObject(a);
}
}
else
{
//duplicated Google events found, but Outlook appointment does not exist
//so lets clean the link from Google events
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, e1);
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, e2);
appointments.Remove(oid);
}
}
else
{
appointments.Add(oid, i);
}
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
if (e1 != null && !string.IsNullOrEmpty(e1.Summary))
{
Logger.Log("Accessing Google appointment: " + e1.Summary + " threw and exception. Skipping: " + ex.Message, EventType.Debug);
}
else
{
Logger.Log("Accessing Google appointment threw and exception. Skipping: " + ex.Message, EventType.Debug);
}
continue;
}
}
}
/// <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()
{
Logger.Log("Removing Outlook duplicated appointments...", EventType.Information);
var appointments = new Dictionary<string, int>();
//scan all appointments
for (var i = 1; i <= OutlookAppointments.Count; i++)
{
Outlook.AppointmentItem ola1 = null;
try
{
ola1 = OutlookAppointments[i] as Outlook.AppointmentItem;
if (ola1 == null)
{
continue;
}
var gid = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, ola1);
//check if Outlook appointment is linked to Google event
if (string.IsNullOrEmpty(gid))
{
continue;
}
//check if there is already another Outlook appointment linked to the same Google event
if (appointments.ContainsKey(gid))
{
if (!(OutlookAppointments[appointments[gid]] is Outlook.AppointmentItem ola2))
{
appointments.Remove(gid);
continue;
}
try
{
var e = GetGoogleAppointmentById(gid);
if (e != null)
{
var oid = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, e);
//check to which Outlook appoinment Google event is linked
if (AppointmentPropertiesUtils.GetOutlookId(ola1) == oid)
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, ola2);
if (!string.IsNullOrEmpty(ola2.Subject))
{
Logger.Log("Duplicated appointment: " + ola2.Subject + ".", EventType.Debug);
}
appointments[gid] = i;
}
else if (AppointmentPropertiesUtils.GetOutlookId(ola2) == oid)
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, ola1);
if (!string.IsNullOrEmpty(ola1.Subject))
{
Logger.Log("Duplicated appointment: " + ola1.Subject + ".", EventType.Debug);
}
}
else
{
//duplicated Outlook appointments found, but Google event does not exist
//so lets clean the link from Outlook appointments
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, ola1);
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, ola2);
appointments.Remove(gid);
}
}
}
finally
{
if (ola2 != null)
{
Marshal.ReleaseComObject(ola2);
}
}
}
else
{
appointments.Add(gid, i);
}
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
if (ola1 != null && !string.IsNullOrEmpty(ola1.Subject))
{
Logger.Log("Accessing Outlook appointment: " + ola1.Subject + " threw and exception. Skipping: " + ex.Message, EventType.Warning);
}
else
{
Logger.Log("Accessing Outlook appointment threw and exception. Skipping: " + ex.Message, EventType.Warning);
}
continue;
}
finally
{
if (ola1 != null)
{
Marshal.ReleaseComObject(ola1);
}
}
}
}
/// <summary>
/// Remove duplicates from Google: two different Google contacts pointing to the same Outlook contact.
/// </summary>
private void RemoveGoogleDuplicatedContacts()
{
Logger.Log("Removing Google duplicated contacts...", EventType.Information);
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;
}
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)
{
try
{
var gid = ContactPropertiesUtils.GetOutlookGoogleContactId(this, a);
//check to which Outlook contact Google contact is linked
if (ContactPropertiesUtils.GetGoogleId(c1) == gid)
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c2);
if (!string.IsNullOrEmpty(c2.Title))
{
Logger.Log("Duplicated contact: " + c2.Title + ".", EventType.Debug);
}
contacts[oid] = i;
}
else if (ContactPropertiesUtils.GetGoogleId(c2) == gid)
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c1);
if (!string.IsNullOrEmpty(c1.Title))
{
Logger.Log("Duplicated contact: " + c1.Title + ".", EventType.Debug);
}
}
else
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c1);
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, c2);
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, a);
}
}
finally
{
if (a != null)
{
Marshal.ReleaseComObject(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(c1.Title))
{
Logger.Log("Accessing Google contact: " + c1.Title + " threw and exception. Skipping: " + ex.Message, EventType.Warning);
}
else
{
Logger.Log("Accessing Google contact threw and exception. Skipping: " + ex.Message, EventType.Warning);
}
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()
{
Logger.Log("Removing Outlook duplicated contacts...", EventType.Information);
var contacts = new Dictionary<string, int>();
//scan all contacts
for (var i = 1; i <= OutlookContacts.Count; i++)
{
Outlook.ContactItem olc1 = null;
try
{
olc1 = OutlookContacts[i] as Outlook.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))
{
if (!(OutlookContacts[contacts[gid]] is Outlook.ContactItem olc2))
{
contacts.Remove(gid);
continue;
}
try
{
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);
if (!string.IsNullOrEmpty(olc2.FileAs))
{
Logger.Log("Duplicated contact: " + olc2.FileAs + ".", EventType.Debug);
}
contacts[oid] = i;
}
else if (ContactPropertiesUtils.GetOutlookId(olc2) == oid)
{
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, olc1);
if (!string.IsNullOrEmpty(olc1.FileAs))
{
Logger.Log("Duplicated contact: " + olc1.FileAs + ".", EventType.Debug);
}
}
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);
}
}
finally
{
if (olc2 != null)
{
Marshal.ReleaseComObject(olc2);
}
}
}
else
{
contacts.Add(gid, i);
}
}
catch (Exception ex)
{
//this is needed because some contacts throw exceptions
var fileAs = string.Empty;
try
{
if (olc1 != null)
{
fileAs = olc1.FileAs;
}
}
catch (Exception)
{
}
if (!string.IsNullOrEmpty(fileAs))
{
Logger.Log("Accessing Outlook contact: " + fileAs + " threw and exception. Skipping: " + ex.Message, EventType.Debug);
}
else
{
Logger.Log("Accessing Outlook contact threw and exception. Skipping: " + ex.Message, EventType.Debug);
}
continue;
}
finally
{
if (olc1 != null)
{
Marshal.ReleaseComObject(olc1);
}
}
}
}
public void LoadAppointments()
{
LoadOutlookAppointments();
LoadGoogleAppointments();
RemoveOutlookDuplicatedAppointments();
RemoveGoogleDuplicatedAppointments();
}
/// <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
{
Logger.Log(duplicateDataException.Message, EventType.Warning);
}
}
Logger.Log("Contact Matches Found: " + Contacts.Count, EventType.Debug);
}
/// <summary>
/// Load the appointments from Google and Outlook and match them
/// </summary>
public void MatchAppointments()
{
LoadAppointments();
Appointments = AppointmentsMatcher.MatchAppointments(this);
Logger.Log("Appointment Matches Found: " + Appointments.Count, EventType.Debug);
}
private void LogSyncParams()
{
Logger.Log("Synchronization options:", EventType.Debug);
Logger.Log("Profile: " + SyncProfile, EventType.Debug);
Logger.Log("SyncOption: " + SyncOption, EventType.Debug);
Logger.Log("SyncDelete: " + SyncDelete, EventType.Debug);
Logger.Log("PromptDelete: " + PromptDelete, EventType.Debug);
if (SyncContacts)
{
Logger.Log("Sync contacts", EventType.Debug);
if (OutlookNamespace != null)
{
var fld = OutlookNamespace.GetFolderFromID(SyncContactsFolder);
Logger.Log("SyncContactsFolder: " + fld.FullFolderPath, EventType.Debug);
}
Logger.Log("SyncContactsForceRTF: " + SyncContactsForceRTF, EventType.Debug);
Logger.Log("UseFileAs: " + UseFileAs, EventType.Debug);
}
if (SyncAppointments)
{
Logger.Log("Sync appointments", EventType.Debug);
Logger.Log("MonthsInPast: " + MonthsInPast, EventType.Debug);
Logger.Log("MonthsInFuture: " + MonthsInFuture, EventType.Debug);
if (OutlookNamespace != null)
{
var fld = OutlookNamespace.GetFolderFromID(SyncAppointmentsFolder);
Logger.Log("SyncAppointmentsFolder: " + fld.FullFolderPath, EventType.Debug);
}
Logger.Log("SyncAppointmentsGoogleFolder: " + SyncAppointmentsGoogleFolder, EventType.Debug);
Logger.Log("SyncAppointmentsForceRTF: " + SyncAppointmentsForceRTF, EventType.Debug);
}
}
public void Sync()
{
lock (_syncRoot)
{
try
{
if (string.IsNullOrEmpty(SyncProfile))
{
Logger.Log("Must set a sync profile. This should be different on each user/computer you sync on.", EventType.Error);
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)
{
Logger.Log("Outlook default time zone: " + TimeZoneInfo.Local.Id, EventType.Information);
Logger.Log("Google default time zone: " + SyncAppointmentsGoogleTimeZone, EventType.Information);
if (string.IsNullOrEmpty(Timezone))
{
TimeZoneChanges?.Invoke(SyncAppointmentsGoogleTimeZone);
Logger.Log("Timezone not configured, changing to default value from Google, it could be adjusted later in GUI.", EventType.Information);
}
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;
Logger.Log("Different time zones in Outlook (" + TimeZoneInfo.Local.Id + ") and Google (mapped to " + AppointmentSync.IanaToWindows(SyncAppointmentsGoogleTimeZone) + ")", EventType.Warning);
}
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);
}
}
}
Logger.Log("Syncing groups...", EventType.Information);
ContactsMatcher.SyncGroups(this);
Logger.Log("Syncing contacts...", EventType.Information);
ContactsMatcher.SyncContacts(this);
SaveContacts(Contacts);
}
if (SyncAppointments)
{
if (Appointments == null)
{
return;
}
TotalCount += Appointments.Count + SkippedCountNotMatches;
Logger.Log("Syncing appointments...", EventType.Information);
AppointmentsMatcher.SyncAppointments(this);
DeleteAppointments(Appointments);
}
}
finally
{
if (OutlookContacts != null)
{
Marshal.ReleaseComObject(OutlookContacts);
OutlookContacts = null;
}
if (OutlookAppointments != null)
{
Marshal.ReleaseComObject(OutlookAppointments);
OutlookAppointments = null;
}
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 outlookContactItem = olci.GetOriginalItemFromOutlook();
try
{
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);
UpdateContact(outlookContactItem, 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, outlookContactItem);
SaveContact(new ContactMatch(olci, googleContact));
break;
default:
throw new ApplicationException("Cancelled");
}
}
}
finally
{
if (outlookContactItem != null)
{
Marshal.ReleaseComObject(outlookContactItem);
outlookContactItem = null;
}
}
//Cleanup the match, i.e. assign a proper OutlookContact and GoogleContact, because can be deleted before
if (match.AllOutlookContactMatches.Count == 0)
{
match.OutlookContact = null;
}
else
{
match.OutlookContact = match.AllOutlookContactMatches[0];
}
}
}
//Cleanup the match, i.e. assign a proper OutlookContact and GoogleContact, because can be deleted before
if (match.AllGoogleContactMatches.Count == 0)
{
match.GoogleContact = null;
}
else
{
match.GoogleContact = match.AllGoogleContactMatches[0];
}
if (match.AllOutlookContactMatches.Count == 0)
{
//If all OutlookContacts have been assigned by the users ==> Create one match for each remaining Google Contact 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 Contact to sync them to Google
Contacts.Remove(match);
foreach (var outlookContact in match.AllOutlookContactMatches)
{
Contacts.Add(new ContactMatch(outlookContact, null));
}
}
else // if (match.AllGoogleContactMatches.Count > 1 ||
// match.AllOutlookContactMatches.Count > 1)
{
SkippedCount++;
Contacts.Remove(match);
}
//else
//{
// //If there remains a modified ContactMatch with only a single OutlookContact and GoogleContact
// //==>Remove all outlookContactDuplicates for this Outlook Contact to not remove it later from the Contacts to sync
// foreach (ContactMatch duplicate in OutlookContactDuplicates)
// {
// if (duplicate.OutlookContact.EntryID == match.OutlookContact.EntryID)
// {
// OutlookContactDuplicates.Remove(duplicate);
// break;
// }
// }
//}
}
}
public void DeleteAppointments(List<AppointmentMatch> appointments)
{
foreach (var match in appointments)
{
try
{
DeleteAppointment(match);
}
catch (Exception ex)
{
if (ErrorEncountered != null)
{
ErrorCount++;
SyncedCount--;
var message = string.Format("Failed to synchronize appointment: {0}:\n{1}", match.OutlookAppointment != null ? match.OutlookAppointment.Subject + " - " + match.OutlookAppointment.Start + ")" : match.GoogleAppointment.Summary + " - " + GetTime(match.GoogleAppointment), ex.Message);
var newEx = new Exception(message, ex);
ErrorEncountered("Error", newEx, EventType.Error);
}
else
{
throw;
}
}
}
}
public static string GetTime(Google.Apis.Calendar.v3.Data.Event googleAppointment)
{
var ret = string.Empty;
if (googleAppointment.Start != null && !string.IsNullOrEmpty(googleAppointment.Start.Date))
{
ret += googleAppointment.Start.Date;
}
else if (googleAppointment.Start != null && googleAppointment.Start.DateTime != null)
{
ret += googleAppointment.Start.DateTime.Value.ToString();
}
if (googleAppointment.Recurrence != null && googleAppointment.Recurrence.Count > 0)
{
ret += " Recurrence"; //ToDo: Return Recurrence Start/End
}
return ret;
}
public void DeleteAppointment(AppointmentMatch match)
{
if (match.GoogleAppointment != null && match.OutlookAppointment != null)
{
// Do nothing: Outlook appointments are not saved here anymore, they have already been saved and counted, just delete items
////bool googleChanged, outlookChanged;
////SaveAppointmentGroups(match, out googleChanged, out outlookChanged);
//if (!match.GoogleAppointment.Saved)
//{
// //Google appointment was modified. save.
// SyncedCount++;
// AppointmentPropertiesUtils.SetProperty(match.GoogleAppointment, Syncronizer.OutlookAppointmentsFolder, match.OutlookAppointment.EntryID);
// match.GoogleAppointment.Save();
// Logger.Log("Updated Google appointment from Outlook: \"" + match.GoogleAppointment.Summary + "\".", EventType.Information);
//}
//if (!match.OutlookAppointment.Saved)// || outlookChanged)
//{
// //outlook appointment was modified. save.
// SyncedCount++;
// AppointmentPropertiesUtils.SetProperty(match.OutlookAppointment, Syncronizer.GoogleAppointmentsFolder, match.GoogleAppointment.EntryID);
// match.OutlookAppointment.Save();
// Logger.Log("Updated Outlook appointment from Google: \"" + match.OutlookAppointment.Subject + "\".", EventType.Information);
//}
}
else if (match.GoogleAppointment == null && match.OutlookAppointment != null)
{
if (match.OutlookAppointment.ItemProperties[OutlookPropertyNameId] != null)
{
var name = match.OutlookAppointment.Subject;
if (SyncOption == SyncOption.OutlookToGoogleOnly)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Outlook appointment because of SyncOption " + SyncOption + ":" + name + ".", EventType.Information);
}
else if (!SyncDelete)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Outlook appointment because SyncDeletion is switched off: " + name + ".", EventType.Information);
}
else
{
// Google appointment was deleted, delete outlook appointment
var item = match.OutlookAppointment;
//try
//{
var outlookAppointmentId = AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, match.OutlookAppointment);
try
{
//First reset OutlookGoogleContactId to restore it later from trash
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, item);
item.Save();
}
catch (Exception)
{
Logger.Log("Error resetting match for Outlook appointment: \"" + name + "\".", EventType.Warning);
}
item.Delete();
DeletedCount++;
Logger.Log("Deleted Outlook appointment: \"" + name + "\".", EventType.Information);
//}
//finally
//{
// Marshal.ReleaseComObject(outlookContact);
// outlookContact = null;
//}
}
}
}
else if (match.GoogleAppointment != null && match.OutlookAppointment == null)
{
if (AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, match.GoogleAppointment) != null)
{
var name = match.GoogleAppointment.Summary;
if (SyncOption == SyncOption.GoogleToOutlookOnly)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Google appointment because of SyncOption " + SyncOption + ":" + name + ".", EventType.Information);
}
else if (!SyncDelete)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Google appointment because SyncDeletion is switched off: " + name + ".", EventType.Information);
}
else if (match.GoogleAppointment.Status != "cancelled")
{
// outlook appointment was deleted, delete Google appointment
var item = match.GoogleAppointment;
////try
////{
//string outlookAppointmentId = AppointmentPropertiesUtils.GetGoogleOutlookAppointmentId(SyncProfile, match.GoogleAppointment);
//try
//{
// //First reset OutlookGoogleContactId to restore it later from trash
// AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, item);
// item.Save();
//}
//catch (Exception)
//{
// Logger.Log("Error resetting match for Google appointment: \"" + name + "\".", EventType.Warning);
//}
EventRequest.Delete(SyncAppointmentsGoogleFolder, item.Id).Execute();
DeletedCount++;
Logger.Log("Deleted Google appointment: \"" + name + "\".", EventType.Information);
//}
//finally
//{
// Marshal.ReleaseComObject(outlookContact);
// outlookContact = null;
//}
}
}
}
else
{
//TODO: ignore for now:
throw new ArgumentNullException("To save appointments, at least a GoogleAppointment or OutlookAppointment must be present.");
//Logger.Log("Both Google and Outlook appointment: \"" + match.OutlookAppointment.FileAs + "\" have been changed! Not implemented yet.", EventType.Warning);
}
}
public void SaveContacts(List<ContactMatch> contacts)
{
foreach (var match in contacts)
{
try
{
SaveContact(match);
}
catch (Exception ex)
{
if (ErrorEncountered != null)
{
ErrorCount++;
SyncedCount--;
var message = string.Format("Failed to synchronize contact: {0}. \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 on OutlookForge:\n{1}", match.OutlookContact != null ? match.OutlookContact.FileAs : match.GoogleContact.Title, ex.Message);
var newEx = new Exception(message, ex);
ErrorEncountered("Error", newEx, EventType.Error);
}
else
{
throw;
}
}
}
}
public void SaveContact(ContactMatch match)
{
if (match.GoogleContact != null && match.OutlookContact != null)
{
//bool googleChanged, outlookChanged;
//SaveContactGroups(match, out googleChanged, out outlookChanged);
if (match.GoogleContact.ContactEntry.Dirty || match.GoogleContact.ContactEntry.IsDirty())
{
//google contact was modified. save.
SyncedCount++;
SaveGoogleContact(match);
Logger.Log("Updated Google contact from Outlook: \"" + match.OutlookContact.FileAs + "\".", EventType.Information);
}
}
else if (match.GoogleContact == null && match.OutlookContact != null)
{
if (match.OutlookContact.UserProperties.GoogleContactId != null)
{
var name = match.OutlookContact.FileAs;
if (SyncOption == SyncOption.OutlookToGoogleOnly)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Outlook contact because of SyncOption " + SyncOption + ":" + name + ".", EventType.Information);
}
else if (!SyncDelete)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Outlook contact because SyncDeletion is switched off: " + name + ".", EventType.Information);
}
else
{
// peer google contact was deleted, delete outlook contact
var item = match.OutlookContact.GetOriginalItemFromOutlook();
try
{
try
{
//First reset OutlookGoogleContactId to restore it later from trash
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, item);
item.Save();
}
catch (Exception)
{
Logger.Log("Error resetting match for Outlook contact: \"" + name + "\".", EventType.Warning);
}
item.Delete();
DeletedCount++;
Logger.Log("Deleted Outlook contact: \"" + name + "\".", EventType.Information);
}
finally
{
Marshal.ReleaseComObject(item);
item = null;
}
}
}
}
else if (match.GoogleContact != null && match.OutlookContact == null)
{
if (ContactPropertiesUtils.GetGoogleOutlookContactId(SyncProfile, match.GoogleContact) != null)
{
if (SyncOption == SyncOption.GoogleToOutlookOnly)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Google contact because of SyncOption " + SyncOption + ":" + ContactMatch.GetName(match.GoogleContact) + ".", EventType.Information);
}
else if (!SyncDelete)
{
SkippedCount++;
Logger.Log("Skipped Deletion of Google contact because SyncDeletion is switched off :" + ContactMatch.GetName(match.GoogleContact) + ".", EventType.Information);
}
else
{
//commented oud, because it causes precondition failed error, if the ResetMatch is short before the Delete
//// peer outlook contact was deleted, delete google contact
//try
//{
// //First reset GoogleOutlookContactId to restore it later from trash
// match.GoogleContact = ResetMatch(match.GoogleContact);
//}
//catch (Exception)
//{
// Logger.Log("Error resetting match for Google contact: \"" + ContactMatch.GetName(match.GoogleContact) + "\".", EventType.Warning);
//}
ContactsRequest.Delete(match.GoogleContact);
DeletedCount++;
Logger.Log("Deleted Google contact: \"" + ContactMatch.GetName(match.GoogleContact) + "\".", EventType.Information);
}
}
}
else
{
//TODO: ignore for now:
throw new ArgumentNullException("To save contacts, at least a GoogleContacat or OutlookContact must be present.");
//Logger.Log("Both Google and Outlook contact: \"" + match.OutlookContact.FileAs + "\" have been changed! Not implemented yet.", EventType.Warning);
}
}
/// <summary>
/// Updates Outlook appointment from master to slave (including groups/categories)
/// </summary>
public void UpdateAppointment(Outlook.AppointmentItem master, ref Google.Apis.Calendar.v3.Data.Event slave)
{
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
Logger.Log("Different Organizer found on Google, invitation maybe NOT sent by Outlook. Google appointment is overwriting Outlook because of SyncOption " + SyncOption + ": " + master.Subject + " - " + master.Start + ". ", EventType.Information);
UpdateAppointment(ref slave, master, null);
break;
case SyncOption.MergeOutlookWins:
case SyncOption.OutlookToGoogleOnly:
//overwrite Google appointment
Logger.Log("Different Organizer found on Google, invitation maybe NOT sent by Outlook, but Outlook appointment is overwriting Google because of SyncOption " + SyncOption + ": " + master.Subject + " - " + master.Start + ".", EventType.Information);
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.Subject + " - " + master.Start + "\". Do you want to update it back from Google to Outlook?", slave, master, this);
}
}
switch (ConflictResolution)
{
case ConflictResolution.Skip:
case ConflictResolution.SkipAlways: //Skip
SkippedCount++;
Logger.Log("Skipped Updating appointment from Outlook to Google because different Organizer found on Google, invitation maybe NOT sent by Outlook: \"" + master.Subject + " - " + master.Start + "\".", EventType.Information);
break;
case ConflictResolution.GoogleWins:
case ConflictResolution.GoogleWinsAlways: //Keep Google and overwrite Outlook
UpdateAppointment(ref slave, master, null);
break;
case ConflictResolution.OutlookWins:
case ConflictResolution.OutlookWinsAlways: //Keep Outlook and overwrite Google
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 (master.Recipients.Count == 0 ||
// master.Organizer == null ||
// AppointmentSync.IsOrganizer(AppointmentSync.GetOrganizer(master), master)||
// slave.Id.Uri == null
// )
//{//Only update, if this appointment was organized on Outlook side or freshly created during this sync
if (updated)
{
AppointmentSync.UpdateAppointment(master, slave);
if (slave.Creator == null || AppointmentSync.IsOrganizer(slave.Creator.Email))
{
AppointmentPropertiesUtils.SetGoogleOutlookAppointmentId(SyncProfile, slave, master);
slave = SaveGoogleAppointment(slave);
}
//ToDo: Doesn'T work for newly created recurrence appointments before save, because Event.Reminder is throwing NullPointerException and Reminders cannot be initialized, therefore moved to after saving
//if (slave.Recurrence != null && slave.Reminders != null)
//{
// if (slave.Reminders.Overrides != null)
// {
// slave.Reminders.Overrides.Clear();
// if (master.ReminderSet)
// {
// var reminder = new Google.Apis.Calendar.v3.Data.EventReminder();
// reminder.Minutes = master.ReminderMinutesBeforeStart;
// if (reminder.Minutes > 40300)
// {
// //ToDo: Check real limit, currently 40300
// Logger.Log("Reminder Minutes to big (" + reminder.Minutes + "), set to maximum of 40300 minutes for appointment: " + master.Subject + " - " + master.Start, EventType.Warning);
// reminder.Minutes = 40300;
// }
// reminder.Method = "popup";
// slave.Reminders.Overrides.Add(reminder);
// }
// }
// slave = SaveGoogleAppointment(slave);
//}
AppointmentPropertiesUtils.SetOutlookGoogleAppointmentId(this, master, slave);
master.Save();
//After saving Google Appointment => also sync recurrence exceptions and save again
if ((slave.Creator == null || AppointmentSync.IsOrganizer(slave.Creator.Email)) && master.IsRecurring && master.RecurrenceState == Outlook.OlRecurrenceState.olApptMaster && AppointmentSync.UpdateRecurrenceExceptions(master, slave, this))
{
slave = SaveGoogleAppointment(slave);
}
SyncedCount++;
Logger.Log("Updated appointment from Outlook to Google: \"" + master.Subject + " - " + master.Start + "\".", EventType.Information);
//}
//else
//{
// //ToDo:Maybe find as better way, e.g. to ask the user, if he wants to overwrite the invalid appointment
// SkippedCount++;
// //Logger.Log("Skipped Updating appointment from Outlook to Google because multiple recipients found and invitations NOT sent by Outlook: \"" + master.Subject + " - " + master.Start + "\".", EventType.Information);
// Logger.Log("Skipped Updating appointment from Outlook to Google because meeting was received by Outlook: \"" + master.Subject + " - " + master.Start + "\".", EventType.Information);
//}
}
}
/// <summary>
/// Updates Outlook appointment from master to slave (including groups/categories)
/// </summary>
public bool UpdateAppointment(ref Google.Apis.Calendar.v3.Data.Event master, Outlook.AppointmentItem slave, List<Google.Apis.Calendar.v3.Data.Event> googleAppointmentExceptions)
{
//if (master.Participants.Count > 1)
//{
// bool organizerIsGoogle = AppointmentSync.IsOrganizer(AppointmentSync.GetOrganizer(master));
// if (organizerIsGoogle || AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, slave) == null)
// {//Only update, if this appointment was organized on Google side or freshly created during tis sync
// updated = true;
// }
// else
// {
// //ToDo:Maybe find as better way, e.g. to ask the user, if he wants to overwrite the invalid appointment
// SkippedCount++;
// Logger.Log("Skipped Updating appointment from Google to Outlook because multiple participants found and invitations NOT sent by Google: \"" + master.Summary + " - " + Syncronizer.GetTime(master) + "\".", EventType.Information);
// }
//}
//else
// updated = true;
var updated = false;
if (slave.Recipients.Count > 1 && AppointmentPropertiesUtils.GetOutlookGoogleAppointmentId(this, 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.MergeOutlookWins:
case SyncOption.OutlookToGoogleOnly:
//overwrite Google appointment
Logger.Log("Multiple participants found, invitation maybe NOT sent by Google. Outlook appointment is overwriting Google because of SyncOption " + SyncOption + ": " + master.Summary + " - " + Synchronizer.GetTime(master) + ". ", EventType.Information);
UpdateAppointment(slave, ref master);
break;
case SyncOption.MergeGoogleWins:
case SyncOption.GoogleToOutlookOnly:
//overwrite outlook appointment
Logger.Log("Multiple participants found, invitation maybe NOT sent by Google, but Google appointment is overwriting Outlook because of SyncOption " + SyncOption + ": " + master.Summary + " - " + Synchronizer.GetTime(master) + ".", EventType.Information);
updated = true;
break;
case SyncOption.MergePrompt:
//promp for sync option
if (
//ConflictResolution != ConflictResolution.GoogleWinsAlways && //Shouldn't be used, because Outlook seems to be the master of the appointment
ConflictResolution != ConflictResolution.OutlookWinsAlways &&
ConflictResolution != ConflictResolution.SkipAlways)
{
using (var r = new ConflictResolver())
{
ConflictResolution = r.Resolve("Cannot update appointment from Google to Outlook because multiple participants found, invitation maybe NOT sent by Google: \"" + master.Summary + " - " + Synchronizer.GetTime(master) + "\". Do you want to update it back from Outlook to Google?", slave, master, this);
}
}
switch (ConflictResolution)
{
case ConflictResolution.Skip:
case ConflictResolution.SkipAlways: //Skip
SkippedCount++;
Logger.Log("Skipped Updating appointment from Google to Outlook because multiple participants found, invitation maybe NOT sent by Google: \"" + master.Summary + " - " + Synchronizer.GetTime(master) + "\".", EventType.Information);
break;
case ConflictResolution.OutlookWins:
case ConflictResolution.OutlookWinsAlways: //Keep Outlook and overwrite Google
UpdateAppointment(slave, ref master);
break;
case ConflictResolution.GoogleWins:
case ConflictResolution.GoogleWinsAlways: //Keep Google and overwrite Outlook
updated = true;
break;
default:
throw new ApplicationException("Cancelled");
}
break;
}
//if (MessageBox.Show("Cannot update appointment from Google to Outlook because multiple participants found, invitation maybe NOT sent by Google: \"" + master.Summary + " - " + Syncronizer.GetTime(master) + "\". Do you want to update it back from Outlook to Google?", "Outlook appointment cannot be overwritten from Google", MessageBoxButtons.YesNo) == DialogResult.Yes)
// UpdateAppointment(slave, ref master);
//else
// SkippedCount++;
// Logger.Log("Skipped Updating appointment from Google to Outlook because multiple participants found, invitation maybe NOT sent by Google: \"" + master.Summary + " - " + Syncronizer.GetTime(master) + "\".", EventType.Information);
}
else //Only update, if invitation was not sent on Outlook side or freshly created during this sync
{
updated = true;
}
if (updated)
{
AppointmentSync.UpdateAppointment(master, slave);
AppointmentPropertiesUtils.SetOutlookGoogleAppointmentId(this, slave, master);
try
{ //Try to save 2 times, because sometimes the first save fails with a COMException (Outlook aborted)
slave.Save();
}
catch (ArgumentException ex)
{
Logger.Log(ex, EventType.Warning);
if (ex.ParamName != null)
{
Logger.Log($"Invalid param: {ex.ParamName}", EventType.Debug);
}
Logger.Log(slave, EventType.Debug);
throw;
}
catch (Exception)
{
try
{
slave.Save();
}
catch (COMException ex)
{
Logger.Log("Error saving Outlook appointment: \"" + master.Summary + " - " + GetTime(master) + "\".\n" + ex.StackTrace, EventType.Warning);
return false;
}
}
if (master.Creator == null || AppointmentSync.IsOrganizer(master.Creator.Email))
{
//only update Google, if I am the organizer, otherwise an error will be thrown
AppointmentPropertiesUtils.SetGoogleOutlookAppointmentId(SyncProfile, master, slave);
master = SaveGoogleAppointment(master);
}
SyncedCount++;
Logger.Log("Updated appointment from Google to Outlook: \"" + master.Summary + " - " + GetTime(master) + "\".", EventType.Information);
//After saving Outlook Appointment => also sync recurrence exceptions and increase SyncCount
if (master.Recurrence != null && googleAppointmentExceptions != null && AppointmentSync.UpdateRecurrenceExceptions(googleAppointmentExceptions, slave, this))
{
SyncedCount++;
}
}
return true;
}
private void SaveOutlookContact(ref Contact googleContact, Outlook.ContactItem outlookContact)
{
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContact, googleContact);
outlookContact.Save();
ContactPropertiesUtils.SetGoogleOutlookContactId(SyncProfile, googleContact, outlookContact);
var updatedEntry = SaveGoogleContact(googleContact);
//try
//{
// updatedEntry = _googleService.Update(match.GoogleContact);
//}
//catch (GDataRequestException tmpEx)
//{
// // check if it's the known HTCData problem, or if there is any invalid XML element or any unescaped XML sequence
// //if (tmpEx.ResponseString.Contains("HTCData") || tmpEx.ResponseString.Contains("'") || match.GoogleContact.Content.Contains("<"))
// //{
// // bool wasDirty = match.GoogleContact.ContactEntry.Dirty;
// // // XML escape the content
// // match.GoogleContact.Content = EscapeXml(match.GoogleContact.Content);
// // // set dirty to back, cause we don't want the changed content go back to Google without reason
// // match.GoogleContact.ContactEntry.Content.Dirty = wasDirty;
// // updatedEntry = _googleService.Update(match.GoogleContact);
// //}
// //else
// if (!String.IsNullOrEmpty(tmpEx.ResponseString))
// throw new ApplicationException(tmpEx.ResponseString, tmpEx);
// else
// throw;
//}
googleContact = updatedEntry;
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContact, googleContact);
outlookContact.Save();
SaveOutlookPhoto(googleContact, outlookContact);
}
private static string EscapeXml(string xml)
{
return System.Security.SecurityElement.Escape(xml);
}
public void SaveGoogleContact(ContactMatch match)
{
var outlookContactItem = match.OutlookContact.GetOriginalItemFromOutlook();
try
{
ContactPropertiesUtils.SetGoogleOutlookContactId(SyncProfile, match.GoogleContact, outlookContactItem);
match.GoogleContact = SaveGoogleContact(match.GoogleContact);
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContactItem, match.GoogleContact);
outlookContactItem.Save();
//Now save the Photo
SaveGooglePhoto(match, outlookContactItem);
}
finally
{
Marshal.ReleaseComObject(outlookContactItem);
outlookContactItem = null;
}
}
/// <summary>
/// returns true, if the passed document is already in passed parentFolder
/// </summary>
/// <param name="parentFolder">the parent folder</param>
/// <param name="childDocument">the document to be checked</param>
/// <returns></returns>
private bool IsInFolder(Document parentFolder, Document childDocument)
{
foreach (var parent in childDocument.ParentFolders)
{
if (parent == parentFolder.Self)
{
return true;
}
}
return false;
}
private string GetXml(Contact contact)
{
using (var ms = new MemoryStream())
{
contact.ContactEntry.SaveToXml(ms);
var sr = new StreamReader(ms);
ms.Seek(0, SeekOrigin.Begin);
return sr.ReadToEnd();
}
}
private static string GetXml(Document note)
{
using (var ms = new MemoryStream())
{
note.DocumentEntry.SaveToXml(ms);
var sr = new StreamReader(ms);
ms.Seek(0, SeekOrigin.Begin);
var xml = sr.ReadToEnd();
return xml;
}
}
/// <summary>
/// Only save the google contact without photo update
/// </summary>
/// <param name="googleContact"></param>
internal Contact SaveGoogleContact(Contact googleContact)
{
//check if this contact was not yet inserted on google.
if (googleContact.ContactEntry.Id.Uri == null)
{
//insert contact.
var feedUri = new Uri(ContactsQuery.CreateContactsUri("default"));
try
{
Contact createdEntry = null;
try
{
createdEntry = ContactsRequest.Insert(feedUri, googleContact);
}
catch (ProtocolViolationException)
{
//TODO (obelix30)
//http://stackoverflow.com/questions/23804960/contactsrequest-insertfeeduri-newentry-sometimes-fails-with-system-net-protoc
try
{
createdEntry = ContactsRequest.Insert(feedUri, googleContact);
}
catch (GDataRequestException ex)
{
Logger.Log(ex, EventType.Debug);
Logger.Log(googleContact, EventType.Debug);
var responseString = EscapeXml(ex.ResponseString);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving NEW Google contact: {0}. \n{1}\n{2}", responseString, ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
catch (Exception ex)
{
Logger.Log(ex, EventType.Debug);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving NEW Google contact:\n{0}\n{1}", ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
}
return createdEntry;
}
catch (GDataRequestException ex)
{
Logger.Log(ex, EventType.Debug);
Logger.Log(googleContact, EventType.Debug);
var responseString = EscapeXml(ex.ResponseString);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving NEW Google contact: {0}. \n{1}\n{2}", responseString, ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
catch (Exception ex)
{
Logger.Log(ex, EventType.Debug);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving NEW Google contact:\n{0}\n{1}", ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
}
else
{
try
{
//contact already present in google. just update
UpdateEmptyUserProperties(googleContact);
UpdateExtendedProperties(googleContact);
//TODO: this will fail if original contact had an empty name or primary email address.
Contact updated = null;
try
{
updated = ContactsRequest.Update(googleContact);
}
catch (ProtocolViolationException)
{
//TODO (obelix30)
//http://stackoverflow.com/questions/23804960/contactsrequest-insertfeeduri-newentry-sometimes-fails-with-system-net-protoc
try
{
updated = ContactsRequest.Update(googleContact);
}
catch (ApplicationException)
{
throw;
}
catch (GDataRequestException ex)
{
Logger.Log(ex, EventType.Debug);
Logger.Log(googleContact, EventType.Debug);
var responseString = EscapeXml(ex.ResponseString);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving EXISTING Google contact: {0}. \n{1}\n{2}", responseString, ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
catch (Exception ex)
{
Logger.Log(ex, EventType.Debug);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving EXISTING Google contact:\n{0}\n{1}", ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
}
return updated;
}
catch (ApplicationException)
{
throw;
}
catch (GDataRequestException ex)
{
Logger.Log(ex, EventType.Debug);
Logger.Log(googleContact, EventType.Debug);
var responseString = EscapeXml(ex.ResponseString);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving EXISTING Google contact: {0}. \n{1}\n{2}", responseString, ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
catch (Exception ex)
{
Logger.Log(ex, EventType.Debug);
var xml = GetXml(googleContact);
var newEx = string.Format("Error saving EXISTING Google contact:\n{0}\n{1}", ex.Message, xml);
throw new ApplicationException(newEx, ex);
}
}
}
private void UpdateExtendedProperties(Contact googleContact)
{
RemoveTooManyExtendedProperties(googleContact);
RemoveTooBigExtendedProperties(googleContact);
RemoveDuplicatedExtendedProperties(googleContact);
UpdateEmptyExtendedProperties(googleContact);
UpdateTooManyExtendedProperties(googleContact);
UpdateTooBigExtendedProperties(googleContact);
UpdateDuplicatedExtendedProperties(googleContact);
}
private void UpdateDuplicatedExtendedProperties(Contact googleContact)
{
DeleteDuplicatedPropertiesForm form = null;
try
{
var dups = new HashSet<string>();
foreach (var p in googleContact.ExtendedProperties)
{
if (dups.Contains(p.Name))
{
Logger.Log(googleContact.Title + ": for extended property " + p.Name + " duplicates were found.", EventType.Debug);
if (form == null)
{
form = new DeleteDuplicatedPropertiesForm();
}
form.AddExtendedProperty(false, p.Name, "");
}
else
{
dups.Add(p.Name);
}
}
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();
}
Logger.Log(googleContact.Title + ": will clean some extended properties for all contacts.", EventType.Debug);
}
else if (ContactExtendedPropertiesToRemoveIfDuplicated != null)
{
ContactExtendedPropertiesToRemoveIfDuplicated = null;
Logger.Log(googleContact.Title + ": will clean some extended properties for this contact.", EventType.Debug);
}
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 = googleContact.ExtendedProperties.Count - 1; j >= 0; j--)
{
if (googleContact.ExtendedProperties[j].Name == key)
{
googleContact.ExtendedProperties.RemoveAt(j);
}
}
Logger.Log("Extended property to remove: " + key, EventType.Debug);
}
}
}
}
finally
{
if (form != null)
{
form.Dispose();
}
}
}
private void UpdateTooBigExtendedProperties(Contact googleContact)
{
DeleteTooBigPropertiesForm form = null;
try
{
foreach (var p in googleContact.ExtendedProperties)
{
if (p.Value != null && p.Value.Length > 1012)
{
Logger.Log(googleContact.Title + ": for extended property " + p.Name + " size limit exceeded (" + p.Value.Length + "). Value is: " + p.Value, EventType.Debug);
if (form == null)
{
form = new DeleteTooBigPropertiesForm();
}
form.AddExtendedProperty(false, p.Name, 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();
}
Logger.Log(googleContact.Title + ": will clean some extended properties for all contacts.", EventType.Debug);
}
else if (ContactExtendedPropertiesToRemoveIfTooBig != null)
{
ContactExtendedPropertiesToRemoveIfTooBig = null;
Logger.Log(googleContact.Title + ": will clean some extended properties for this contact.", EventType.Debug);
}
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 = googleContact.ExtendedProperties.Count - 1; j >= 0; j--)
{
if (googleContact.ExtendedProperties[j].Name == key)
{
googleContact.ExtendedProperties.RemoveAt(j);
}
}
Logger.Log("Extended property to remove: " + key, EventType.Debug);
}
}
}
}
finally
{
if (form != null)
{
form.Dispose();
}
}
}
private void UpdateTooManyExtendedProperties(Contact googleContact)
{
if (googleContact.ExtendedProperties.Count > 10)
{
Logger.Log(googleContact.Title + ": too many extended properties " + googleContact.ExtendedProperties.Count, EventType.Debug);
using (var form = new DeleteTooManyPropertiesForm())
{
foreach (var p in googleContact.ExtendedProperties)
{
if (p.Name != "gos:oid:" + SyncProfile)
{
form.AddExtendedProperty(false, p.Name, 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;
if (allCheck)
{
if (ContactExtendedPropertiesToRemoveIfTooMany == null)
{
ContactExtendedPropertiesToRemoveIfTooMany = new HashSet<string>();
}
else
{
ContactExtendedPropertiesToRemoveIfTooMany.Clear();
}
Logger.Log(googleContact.Title + ": will clean some extended properties for all contacts.", EventType.Debug);
}
else if (ContactExtendedPropertiesToRemoveIfTooMany != null)
{
ContactExtendedPropertiesToRemoveIfTooMany = null;
Logger.Log(googleContact.Title + ": will clean some extended properties for this contact.", EventType.Debug);
}
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 = googleContact.ExtendedProperties.Count - 1; i >= 0; i--)
{
if (googleContact.ExtendedProperties[i].Name == key)
{
googleContact.ExtendedProperties.RemoveAt(i);
}
}
Logger.Log("Extended property to remove: " + key, EventType.Debug);
}
}
}
}
}
}
private static void UpdateEmptyUserProperties(Contact googleContact)
{
// 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 (googleContact.ContactEntry == null)
{
return;
}
if (googleContact.ContactEntry.UserDefinedFields == null)
{
return;
}
var fieldCount = 0;
foreach (var userDefinedField in googleContact.ContactEntry.UserDefinedFields)
{
fieldCount++;
if (string.IsNullOrEmpty(userDefinedField.Key))
{
userDefinedField.Key = "UserField" + fieldCount.ToString();
Logger.Log("Set key to user defined field to avoid errors: " + userDefinedField.Key, EventType.Debug);
}
//similar error with empty values
if (string.IsNullOrEmpty(userDefinedField.Value))
{
userDefinedField.Value = userDefinedField.Key;
Logger.Log("Set value to user defined field to avoid errors: " + userDefinedField.Value, EventType.Debug);
}
}
}
private static void UpdateEmptyExtendedProperties(Contact googleContact)
{
foreach (var p in googleContact.ExtendedProperties)
{
if (string.IsNullOrEmpty(p.Value))
{
Logger.Log(googleContact.Title + ": empty value for " + p.Name, EventType.Debug);
if (p.ChildNodes != null)
{
Logger.Log(googleContact.Title + ": childNodes count " + p.ChildNodes.Count, EventType.Debug);
}
else
{
p.Value = p.Name;
Logger.Log(googleContact.Title + ": set value to extended property to avoid errors " + p.Name, EventType.Debug);
}
}
}
}
private void RemoveDuplicatedExtendedProperties(Contact googleContact)
{
if (ContactExtendedPropertiesToRemoveIfDuplicated != null)
{
for (var i = googleContact.ExtendedProperties.Count - 1; i >= 0; i--)
{
var key = googleContact.ExtendedProperties[i].Name;
if (ContactExtendedPropertiesToRemoveIfDuplicated.Contains(key))
{
Logger.Log(googleContact.Title + ": removed (duplicate) " + key, EventType.Debug);
googleContact.ExtendedProperties.RemoveAt(i);
}
}
}
}
private void RemoveTooBigExtendedProperties(Contact googleContact)
{
if (ContactExtendedPropertiesToRemoveIfTooBig != null)
{
for (var i = googleContact.ExtendedProperties.Count - 1; i >= 0; i--)
{
if (googleContact.ExtendedProperties[i].Value.Length > 1012)
{
var key = googleContact.ExtendedProperties[i].Name;
if (ContactExtendedPropertiesToRemoveIfTooBig.Contains(key))
{
Logger.Log(googleContact.Title + ": removed (size)" + key, EventType.Debug);
googleContact.ExtendedProperties.RemoveAt(i);
}
}
}
}
}
private void RemoveTooManyExtendedProperties(Contact googleContact)
{
if (ContactExtendedPropertiesToRemoveIfTooMany != null)
{
for (var i = googleContact.ExtendedProperties.Count - 1; i >= 0; i--)
{
var key = googleContact.ExtendedProperties[i].Name;
if (ContactExtendedPropertiesToRemoveIfTooMany.Contains(key))
{
Logger.Log(googleContact.Title + ": removed (count) " + key, EventType.Debug);
googleContact.ExtendedProperties.RemoveAt(i);
}
}
}
}
/// <summary>
/// Save the google Appointment
/// </summary>
/// <param name="googleAppointment"></param>
internal Google.Apis.Calendar.v3.Data.Event SaveGoogleAppointment(Google.Apis.Calendar.v3.Data.Event googleAppointment)
{
//check if this contact was not yet inserted on google.
if (googleAppointment.Id == null)
{
////insert contact.
//Uri feedUri = new Uri("https://www.google.com/calendar/feeds/default/private/full");
try
{
var createdEntry = EventRequest.Insert(googleAppointment, SyncAppointmentsGoogleFolder).Execute();
return createdEntry;
}
catch (Exception ex)
{
Logger.Log(googleAppointment, EventType.Debug);
var newEx = string.Format("Error saving NEW Google appointment: {0}. \n{1}", googleAppointment.Summary + " - " + GetTime(googleAppointment), ex.Message);
throw new ApplicationException(newEx, ex);
}
}
else
{
try
{
//contact already present in google. just update
var updated = EventRequest.Update(googleAppointment, SyncAppointmentsGoogleFolder, googleAppointment.Id).Execute();
return updated;
}
catch (Exception ex)
{
Logger.Log(googleAppointment, EventType.Debug);
var error = "Error saving EXISTING Google appointment: ";
error += googleAppointment.Summary + " - " + GetTime(googleAppointment);
error += " - Creator: " + (googleAppointment.Creator != null ? googleAppointment.Creator.Email : "null");
error += " - Organizer: " + (googleAppointment.Organizer != null ? googleAppointment.Organizer.Email : "null");
error += ". \n" + ex.Message;
Logger.Log(error, EventType.Warning);
//string newEx = String.Format("Error saving EXISTING Google appointment: {0}. \n{1}", googleAppointment.Summary + " - " + GetTime(googleAppointment), ex.Message);
//throw new ApplicationException(newEx, ex);
return googleAppointment;
}
}
}
public void SaveGooglePhoto(ContactMatch match, Outlook.ContactItem outlookContactitem)
{
var hasOutlookPhoto = Utilities.HasPhoto(outlookContactitem);
if (hasOutlookPhoto)
{
// add outlook photo to google
using (var outlookPhoto = Utilities.GetOutlookPhoto(outlookContactitem))
{
if (outlookPhoto != 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(outlookPhoto))
{
using (var stream = new MemoryStream(Utilities.BitmapToBytes(bmp)))
{
ContactsRequest.SetPhoto(match.GoogleContact, stream);
//Just save the Outlook Contact to have the same lastUpdate date as Google
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContactitem, match.GoogleContact);
outlookContactitem.Save();
}
}
break; //Exit because photo save succeeded
}
catch (GDataRequestException ex)
{ //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 (ex.Response is HttpWebResponse r && r.StatusCode != HttpStatusCode.Forbidden)
{
throw;
}
if (retry == num_tries - 1)
{
ErrorHandler.Handle(new Exception($"Photo of contact {match.GoogleContact.Title} couldn't be saved after {num_tries} tries, maybe Google found its own photo and doesn't allow updating it", ex));
}
else
{
Thread.Sleep(1000);
}
}
}
}
}
}
else
{
var hasGooglePhoto = Utilities.HasPhoto(match.GoogleContact);
if (hasGooglePhoto)
{
//Delete Photo on Google side, if no Outlook photo exists
ContactsRequest.Delete(match.GoogleContact.PhotoUri, match.GoogleContact.PhotoEtag);
//Just save the Outlook Contact to have the same lastUpdate date as Google
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContactitem, match.GoogleContact);
outlookContactitem.Save();
}
}
}
public void SaveOutlookPhoto(Contact googleContact, Outlook.ContactItem outlookContact)
{
var hasGooglePhoto = Utilities.HasPhoto(googleContact);
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, googleContact))
{
if (googlePhoto != null) // Google may have an invalid photo
{
Utilities.SetOutlookPhoto(outlookContact, googlePhoto);
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContact, googleContact);
outlookContact.Save();
}
}
}
else
{
var hasOutlookPhoto = Utilities.HasPhoto(outlookContact);
if (hasOutlookPhoto)
{
outlookContact.RemovePicture();
ContactPropertiesUtils.SetOutlookGoogleContactId(this, outlookContact, googleContact);
outlookContact.Save();
}
}
}
public Group SaveGoogleGroup(Group group)
{
//check if this group was not yet inserted on google.
if (group.GroupEntry.Id.Uri == null)
{
//insert group.
var feedUri = new Uri(GroupsQuery.CreateGroupsUri("default"));
try
{
return ContactsRequest.Insert(feedUri, group);
}
catch (ProtocolViolationException)
{
//TODO (obelix30)
//http://stackoverflow.com/questions/23804960/contactsrequest-insertfeeduri-newentry-sometimes-fails-with-system-net-protoc
try
{
return ContactsRequest.Insert(feedUri, group);
}
catch (Exception ex)
{
Logger.Log(ex, EventType.Debug);
Logger.Log("Group dump: " + group.ToString(), EventType.Debug);
throw;
}
}
catch (Exception ex)
{
Logger.Log(ex, EventType.Debug);
Logger.Log("Group dump: " + group.ToString(), EventType.Debug);
throw;
}
}
else
{
try
{
//group already present in google. just update
return ContactsRequest.Update(group);
}
catch
{
//TODO: save google group xml for diagnistics
throw;
}
}
}
/// <summary>
/// Updates Google contact from Outlook (including groups/categories)
/// </summary>
public void UpdateContact(Outlook.ContactItem master, Contact slave)
{
ContactSync.UpdateContact(master, slave, UseFileAs);
OverwriteContactGroups(master, slave);
}
/// <summary>
/// Updates Outlook contact from Google (including groups/categories)
/// </summary>
public void UpdateContact(Contact master, Outlook.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++;
Logger.Log("Updated Outlook contact from Google: \"" + slave.FileAs + "\".", EventType.Information);
}
/// <summary>
/// Updates Google contact's groups from Outlook contact
/// </summary>
private void OverwriteContactGroups(Outlook.ContactItem master, Contact slave)
{
var currentGroups = Utilities.GetGoogleGroups(this, slave);
// get outlook categories
var cats = Utilities.GetOutlookGroups(master.Categories);
// remove obsolete groups
var remove = new Collection<Group>();
bool found;
foreach (var group in currentGroups)
{
found = false;
foreach (var cat in cats)
{
if (group.Title == cat)
{
found = true;
break;
}
}
if (!found)
{
remove.Add(group);
}
}
while (remove.Count != 0)
{
Utilities.RemoveGoogleGroup(slave, remove[0]);
remove.RemoveAt(0);
}
// add new groups
Group 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
{
Logger.Log("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.FileAs, EventType.Warning);
}
}
else
{
Logger.Log("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.FileAs, EventType.Warning);
}
}
if (g != null)
{
Utilities.AddGoogleGroup(slave, g);
}
}
}
//add system Group My Contacts
if (!Utilities.ContainsGroup(this, slave, myContactsGroup))
{
// add group to contact
g = GetGoogleGroupByName(myContactsGroup);
if (g == null)
{
throw new Exception(string.Format("Google {0} doesn't exist", myContactsGroup));
}
Utilities.AddGoogleGroup(slave, g);
}
}
/// <summary>
/// Updates Outlook contact's categories (groups) from Google groups
/// </summary>
private void OverwriteContactGroups(Contact master, Outlook.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 Group: Meine Kontakte") automatically tracked by Google
if (group.Title != null && !group.Title.Equals(myContactsGroup))
{
newCats.Add(group.Title);
}
}
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))
{
Logger.Log("Must set a sync profile. This should be different on each user/computer you sync on.", EventType.Error);
return;
}
lock (_syncRoot)
{
Logger.Log("Resetting Google Contact matches...", EventType.Information);
foreach (var googleContact in GoogleContacts)
{
try
{
if (googleContact != null)
{
ResetMatch(googleContact);
}
}
catch (Exception ex)
{
Logger.Log("The match of Google contact " + ContactMatch.GetName(googleContact) + " couldn't be reset: " + ex.Message, EventType.Warning);
}
}
Logger.Log("Resetting Outlook Contact matches...", EventType.Information);
//1 based array
for (var i = 1; i <= OutlookContacts.Count; i++)
{
Outlook.ContactItem outlookContact = null;
try
{
outlookContact = OutlookContacts[i] as Outlook.ContactItem;
if (outlookContact == null)
{
Logger.Log("Empty Outlook contact found (maybe distribution list). Skipping", EventType.Warning);
continue;
}
}
catch (Exception ex)
{
//this is needed because some contacts throw exceptions
Logger.Log("Accessing Outlook contact threw and exception. Skipping: " + ex.Message, EventType.Warning);
continue;
}
try
{
ResetMatch(outlookContact);
}
catch (Exception ex)
{
var fs = string.Empty;
try
{
fs = outlookContact.FileAs;
}
catch(Exception)
{
}
if (string.IsNullOrWhiteSpace (fs))
{
Logger.Log("The match of Outlook contact couldn't be reset: " + ex.Message, EventType.Warning);
}
else
{
Logger.Log("The match of Outlook contact " + outlookContact.FileAs + " couldn't be reset: " + ex.Message, EventType.Warning);
}
}
}
}
}
finally
{
if (OutlookContacts != null)
{
Marshal.ReleaseComObject(OutlookContacts);
OutlookContacts = null;
}
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.");
//try
//{
lock (_syncRoot)
{
Logger.Log("Resetting Outlook appointment matches...", EventType.Information);
//1 based array
for (var i = OutlookAppointments.Count; i >= 1; i--)
{
Outlook.AppointmentItem outlookAppointment = null;
try
{
outlookAppointment = OutlookAppointments[i] as Outlook.AppointmentItem;
if (outlookAppointment == null)
{
Logger.Log("Empty Outlook appointment found (maybe distribution list). Skipping", EventType.Warning);
continue;
}
}
catch (Exception ex)
{
//this is needed because some appointments throw exceptions
Logger.Log("Accessing Outlook appointment threw an exception. Skipping: " + ex.Message, EventType.Warning);
continue;
}
if (deleteOutlookAppointments)
{
outlookAppointment.Delete();
}
else
{
try
{
ResetMatch(outlookAppointment);
}
catch (Exception ex)
{
Logger.Log("The match of Outlook appointment " + outlookAppointment.Subject + " couldn't be reset: " + ex.Message, EventType.Warning);
}
}
}
}
}
/// <summary>
/// Reset the match link between Google and Outlook contact
/// </summary>
public Contact ResetMatch(Contact googleContact)
{
if (googleContact != null)
{
ContactPropertiesUtils.ResetGoogleOutlookContactId(SyncProfile, googleContact);
return SaveGoogleContact(googleContact);
}
else
{
return googleContact;
}
}
public Google.Apis.Calendar.v3.Data.Event ResetMatch(Google.Apis.Calendar.v3.Data.Event googleAppointment)
{
if (googleAppointment != null)
{
AppointmentPropertiesUtils.ResetGoogleOutlookAppointmentId(SyncProfile, googleAppointment);
return SaveGoogleAppointment(googleAppointment);
}
else
{
return googleAppointment;
}
}
/// <summary>
/// Reset the match link between Outlook and Google contact
/// </summary>
public void ResetMatch(Outlook.ContactItem outlookContact)
{
if (outlookContact != null)
{
try
{
ContactPropertiesUtils.ResetOutlookGoogleContactId(this, outlookContact);
outlookContact.Save();
}
finally
{
Marshal.ReleaseComObject(outlookContact);
outlookContact = null;
}
}
}
/// <summary>
/// Reset the match link between Outlook and Google appointment
/// </summary>
public void ResetMatch(Outlook.AppointmentItem outlookAppointment)
{
if (outlookAppointment != null)
{
AppointmentPropertiesUtils.ResetOutlookGoogleAppointmentId(this, outlookAppointment);
outlookAppointment.Save();
}
}
public ContactMatch ContactByProperty(string name, string email)
{
foreach (var m in Contacts)
{
if (m.GoogleContact != null &&
((m.GoogleContact.PrimaryEmail != null && m.GoogleContact.PrimaryEmail.Address == email) ||
m.GoogleContact.Title == name ||
m.GoogleContact.Name != null && m.GoogleContact.Name.FullName == 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>();
Outlook.ContactItem item = null;
try
{
item = OutlookContacts.Find("[" + name + "] = \"" + value + "\"") as Outlook.ContactItem;
while (item != null)
{
col.Add(new OutlookContactInfo(item, this));
Marshal.ReleaseComObject(item);
item = OutlookContacts.FindNext() as Outlook.ContactItem;
}
}
catch (Exception)
{
//TODO: should not get here.
}
return col;
}
public Group GetGoogleGroupById(string id)
{
//return GoogleGroups.FindById(new AtomId(id)) as Group;
var atomId = new AtomId(id);
foreach (var group in GoogleGroups)
{
if (group.GroupEntry.Id.Equals(atomId))
{
return group;
}
}
return null;
}
public Group GetGoogleGroupByName(string name)
{
foreach (var group in GoogleGroups)
{
if (group.Title == name)
{
return group;
}
}
return null;
}
public Contact GetGoogleContactById(string id)
{
var atomId = new AtomId(id);
foreach (var contact in GoogleContacts)
{
if (contact.ContactEntry.Id.Equals(atomId))
{
return contact;
}
}
return null;
}
public Google.Apis.Calendar.v3.Data.Event GetGoogleAppointmentById(string id)
{
//ToDo: Temporary remove prefix used by v2:
id = id.Replace("http://www.google.com/calendar/feeds/default/events/", "");
id = id.Replace("https://www.google.com/calendar/feeds/default/events/", "");
//AtomId atomId = new AtomId(id);
foreach (var appointment in GoogleAppointments)
{
if (appointment.Id.Equals(id))
{
return appointment;
}
}
if (AllGoogleAppointments != null)
{
foreach (var appointment in AllGoogleAppointments)
{
if (appointment.Id.Equals(id))
{
return appointment;
}
}
}
return null;
}
public Outlook.AppointmentItem GetOutlookAppointmentById(string id)
{
for (var i = OutlookAppointments.Count; i >= 1; i--)
{
Outlook.AppointmentItem a = null;
try
{
a = OutlookAppointments[i] as Outlook.AppointmentItem;
if (a == null)
{
continue;
}
}
catch (Exception)
{
continue;
}
if (AppointmentPropertiesUtils.GetOutlookId(a) == id)
{
return a;
}
}
return null;
}
public Outlook.ContactItem GetOutlookContactById(string id)
{
for (var i = OutlookContacts.Count; i >= 1; i--)
{
Outlook.ContactItem a = null;
try
{
a = OutlookContacts[i] as Outlook.ContactItem;
if (a == null)
{
continue;
}
}
catch (Exception)
{
continue;
}
if (ContactPropertiesUtils.GetOutlookId(a) == id)
{
return a;
}
}
return null;
}
public Group CreateGroup(string name)
{
var group = new Group
{
Title = name
};
group.GroupEntry.Dirty = true;
return group;
}
public static bool AreEqual(Outlook.ContactItem c1, Outlook.ContactItem c2)
{
return c1.Email1Address == c2.Email1Address;
}
public static int IndexOf(Collection<Outlook.ContactItem> col, Outlook.ContactItem outlookContact)
{
for (var i = 0; i < col.Count; i++)
{
if (AreEqual(col[i], outlookContact))
{
return i;
}
}
return -1;
}
internal void DebugContacts()
{
var msg = "DEBUG INFORMATION\nPlease submit to developer:\n\n{0}\n{1}\n{2}";
if (SyncContacts)
{
var oCount = "Outlook Contact Count: " + OutlookContacts.Count;
var gCount = "Google Contact Count: " + GoogleContacts.Count;
var mCount = "Matches Count: " + Contacts.Count;
MessageBox.Show(string.Format(msg, oCount, gCount, mCount), "DEBUG INFO", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
if (SyncAppointments)
{
var oCount = "Outlook appointments Count: " + OutlookAppointments.Count;
var gCount = "Google appointments Count: " + GoogleAppointments.Count;
var mCount = "Matches Count: " + Appointments.Count;
MessageBox.Show(string.Format(msg, oCount, gCount, mCount), "DEBUG INFO", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
public static Outlook.ContactItem CreateOutlookContactItem(string syncContactsFolder)
{
//outlookContact = OutlookApplication.CreateItem(Outlook.OlItemType.olContactItem) as Outlook.ContactItem; //This will only create it in the default folder, but we have to consider the selected folder
Outlook.ContactItem outlookContact = null;
Outlook.MAPIFolder contactsFolder = null;
Outlook.Items items = null;
try
{
contactsFolder = OutlookNameSpace.GetFolderFromID(syncContactsFolder);
items = contactsFolder.Items;
outlookContact = items.Add(Outlook.OlItemType.olContactItem) as Outlook.ContactItem;
}
finally
{
if (items != null)
{
Marshal.ReleaseComObject(items);
}
if (contactsFolder != null)
{
Marshal.ReleaseComObject(contactsFolder);
}
}
return outlookContact;
}
public static Outlook.AppointmentItem CreateOutlookAppointmentItem(string syncAppointmentsFolder)
{
//OutlookAppointment = OutlookApplication.CreateItem(Outlook.OlItemType.olAppointmentItem) as Outlook.AppointmentItem; //This will only create it in the default folder, but we have to consider the selected folder
Outlook.AppointmentItem outlookAppointment = null;
Outlook.MAPIFolder appointmentsFolder = null;
Outlook.Items items = null;
try
{
appointmentsFolder = OutlookNameSpace.GetFolderFromID(syncAppointmentsFolder);
items = appointmentsFolder.Items;
outlookAppointment = items.Add(Outlook.OlItemType.olAppointmentItem) as Outlook.AppointmentItem;
}
finally
{
if (items != null)
{
Marshal.ReleaseComObject(items);
}
if (appointmentsFolder != null)
{
Marshal.ReleaseComObject(appointmentsFolder);
}
}
return outlookAppointment;
}
public void Dispose()
{
((IDisposable)CalendarRequest).Dispose();
}
}
internal enum SyncOption
{
MergePrompt,
MergeOutlookWins,
MergeGoogleWins,
OutlookToGoogleOnly,
GoogleToOutlookOnly,
}
}