From 3c2db6962cf594e4c46c92c977729458363941bd Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Thu, 12 Apr 2018 18:15:32 +0200 Subject: [PATCH 01/12] Implement autoconfiguration for Kolab - Only active if the registry key "HKEY_CURRENT_USER\Software\CalDavSynchronizer\AutoconfigureKolab" is set to "1" - On every Outlook start, searches for new Kolab resources. Creates both folder and synchronization profile. - On every Outlook start, searches for obsolete Kolab resources. Removes the synchronization profile and renames the outlook folder to "... Deleted (date)". --- CalDavSynchronizer/CalDavSynchronizer.csproj | 2 +- CalDavSynchronizer/ComponentContainer.cs | 75 +++++++++++++++++++ CalDavSynchronizer/Contracts/GeneralOption.cs | 1 + .../DataAccess/GeneralOptionsDataAccess.cs | 4 +- .../Globalization/StringResources.de-DE.resx | 3 + .../Globalization/StringResources.ru-RU.resx | 3 + .../KolabMultipleOptionsTemplateViewModel.cs | 18 ++++- .../ViewModels/OptionsCollectionViewModel.cs | 10 ++- 8 files changed, 109 insertions(+), 7 deletions(-) diff --git a/CalDavSynchronizer/CalDavSynchronizer.csproj b/CalDavSynchronizer/CalDavSynchronizer.csproj index 26ca4a9b..9928c6e7 100644 --- a/CalDavSynchronizer/CalDavSynchronizer.csproj +++ b/CalDavSynchronizer/CalDavSynchronizer.csproj @@ -1083,7 +1083,7 @@ - 99D12A4419EA17C4DB056A34FEDAFDC1A576A921 + 1420A545B90CAFB4925D2F952464AA1F36A0AA27 diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index a3b6e8ff..3a259468 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -58,6 +58,7 @@ using CalDavSynchronizer.Scheduling.ComponentCollectors; using CalDavSynchronizer.Ui.Options; using CalDavSynchronizer.Ui.Options.BulkOptions.ViewModels; +using CalDavSynchronizer.Ui.Options.ResourceSelection.ViewModels; using CalDavSynchronizer.Ui.Options.Models; using CalDavSynchronizer.Ui.Options.ViewModels; using CalDavSynchronizer.Ui.SystrayNotification; @@ -233,9 +234,83 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge s_logger.Error ("Can't access SyncObjects", ex); } + // Set the registry key "HKEY_CURRENT_USER\Software\CalDavSynchronizer\AutoconfigureKolab" + // to "1" to enable the Kolab autoconfigure feature. This setting is not available + // through the general options dialog, as it is not so general after all... + if (generalOptions.AutoconfigureKolab) + { + AutoconfigureKolab(options, generalOptions); + } + // Search for any existing Kolab setting + // var kolabOption = options.First(option => option.ProfileTypeOrNull == "Kolab"); + _oneTimeTaskRunner = new OneTimeTaskRunner(_outlookSession); } + private async void AutoconfigureKolab(Options[] options, GeneralOptions generalOptions) + { + // Create all objects required to use the options collection models + string profileType = "Kolab"; + string[] categories; + using (var categoriesWrapper = GenericComObjectWrapper.Create(_session.Categories)) + categories = categoriesWrapper.Inner.ToSafeEnumerable().Select(c => c.Name).ToArray(); + var faultFinder = generalOptions.FixInvalidSettings + ? new SettingsFaultFinder(EnumDisplayNameProvider.Instance) + : NullSettingsFaultFinder.Instance; + var optionTasks = new OptionTasks( + _session, EnumDisplayNameProvider.Instance, faultFinder, _outlookSession); + var viewOptions = new ViewOptions( + generalOptions.EnableAdvancedView); + var sessionData = new OptionModelSessionData( + _outlookSession.GetCategories().ToDictionary( + c => c.Name, _outlookSession.CategoryNameComparer)); + var optionsCollectionModel = new OptionsCollectionViewModel( + generalOptions.ExpandAllSyncProfiles, GetProfileDataDirectory, _uiService, optionTasks, _profileTypeRegistry, + (parent, type) => type.CreateModelFactory( + parent, _outlookAccountPasswordProvider, categories, optionTasks, faultFinder, generalOptions, viewOptions, sessionData), + viewOptions); + optionsCollectionModel.SetOptionsCollection(options); + + // Add the "add multiple resources" pseudo-option of type "Kolab" + var kolabOptionsModel = (KolabMultipleOptionsTemplateViewModel)optionsCollectionModel.AddMultipleHeadless( + _profileTypeRegistry.AllTypes.First(t => t.Name == profileType)); + + // Auto-detect all updates + kolabOptionsModel.AutoCreateOutlookFolders = true; + kolabOptionsModel.OnlyAddNewUrls = true; + kolabOptionsModel.AutoConfigure = true; + kolabOptionsModel.GetAccountSettingsCommand.Execute(null); + var serverResources = await kolabOptionsModel.DiscoverResourcesAsync(); + var newOptions = optionsCollectionModel.GetOptionsCollection(); + + // remove all options that are no longer available. + // Do this only if we really have a response from the server. + if (serverResources.ContainsResources) + { + var allUris = + serverResources.Calendars.Select(c => c.Uri.ToString()).Concat( + serverResources.AddressBooks.Select(a => a.Uri.ToString())).Concat( + serverResources.TaskLists.Select(d => d.Id)).ToArray(); + var remainingOptions = new List(); + var markDeleted = " - " + Strings.Localize("Deleted") + " " + DateTime.Now.ToString(); + foreach (var option in newOptions) + { + if (option.ProfileTypeOrNull != profileType || allUris.Contains(option.CalenderUrl)) + remainingOptions.Add(option); + else + { + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); + folder.Inner.Name += markDeleted; + } + } + newOptions = remainingOptions.ToArray(); + } + + // Save new settings + await ApplyNewOptions(options, newOptions, generalOptions, optionsCollectionModel.GetOneTimeTasks()); + } + private void PermanentStatusesViewModel_OptionsRequesting(object sender, OptionsEventArgs e) { e.Options = _optionsDataAccess.Load(); diff --git a/CalDavSynchronizer/Contracts/GeneralOption.cs b/CalDavSynchronizer/Contracts/GeneralOption.cs index 3e4a451d..81ced267 100644 --- a/CalDavSynchronizer/Contracts/GeneralOption.cs +++ b/CalDavSynchronizer/Contracts/GeneralOption.cs @@ -49,6 +49,7 @@ public class GeneralOptions public int ThresholdForProgressDisplay { get; set; } public int MaxSucessiveWarnings { get; set; } public string CultureName { get; set; } + public bool AutoconfigureKolab { get; set; } public GeneralOptions Clone() { diff --git a/CalDavSynchronizer/DataAccess/GeneralOptionsDataAccess.cs b/CalDavSynchronizer/DataAccess/GeneralOptionsDataAccess.cs index 8b27036d..893ac921 100644 --- a/CalDavSynchronizer/DataAccess/GeneralOptionsDataAccess.cs +++ b/CalDavSynchronizer/DataAccess/GeneralOptionsDataAccess.cs @@ -65,6 +65,7 @@ public class GeneralOptionsDataAccess : IGeneralOptionsDataAccess private const string ValueNameMaxSucessiveWarnings = "MaxSucessiveWarnings"; private const string ValueNameCultureName = "CultureName"; + private const string ValueNameAutoconfigureKolab = "AutoconfigureKolab"; public GeneralOptions LoadOptions () { @@ -102,7 +103,8 @@ public GeneralOptions LoadOptions () ShowProgressBar = (int) (key.GetValue (ValueNameShowProgressBar) ?? Convert.ToInt32 (Boolean.Parse (ConfigurationManager.AppSettings["showProgressBar"] ?? bool.TrueString))) != 0, ThresholdForProgressDisplay = (int) (key.GetValue (ValueNameThresholdForProgressDisplay) ?? int.Parse(ConfigurationManager.AppSettings["loadOperationThresholdForProgressDisplay"] ?? "50")), MaxSucessiveWarnings = (int) (key.GetValue (ValueNameMaxSucessiveWarnings) ?? 2), - CultureName = GetCultureName(key) + CultureName = GetCultureName(key), + AutoconfigureKolab = (int)(key.GetValue(ValueNameAutoconfigureKolab) ?? 0) != 0 }; } } diff --git a/CalDavSynchronizer/Globalization/StringResources.de-DE.resx b/CalDavSynchronizer/Globalization/StringResources.de-DE.resx index 2967c836..8439b0ea 100644 --- a/CalDavSynchronizer/Globalization/StringResources.de-DE.resx +++ b/CalDavSynchronizer/Globalization/StringResources.de-DE.resx @@ -1404,4 +1404,7 @@ Andere Protokolle können mittels protocol: address hinzugefügt werden Synchronisiere öffentliche Outlook Termine auf Standard Sichtbarkeit anstelle von PUBLIC + + Gelöscht + \ No newline at end of file diff --git a/CalDavSynchronizer/Globalization/StringResources.ru-RU.resx b/CalDavSynchronizer/Globalization/StringResources.ru-RU.resx index d72ed1bd..b51382fb 100644 --- a/CalDavSynchronizer/Globalization/StringResources.ru-RU.resx +++ b/CalDavSynchronizer/Globalization/StringResources.ru-RU.resx @@ -1342,4 +1342,7 @@ Установите флаг общедоступный MS Outlook по умолчанию вместо PUBLIC + + удаленный + \ No newline at end of file diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index 3a7f1474..a65cb22b 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -23,6 +23,8 @@ using System.Net; using System.Reflection; using System.Text; +using System.Threading; +using System.Threading.Tasks; using System.Windows; using System.Windows.Input; using CalDavSynchronizer.Contracts; @@ -86,7 +88,7 @@ public KolabMultipleOptionsTemplateViewModel ( _discoverResourcesCommand = new DelegateCommandWithoutCanExecuteDelegation (_ => { ComponentContainer.EnsureSynchronizationContext(); - DiscoverResourcesAsync(); + DiscoverResourcesCommandAsync(); }); _mergeResourcesCommand = new DelegateCommandWithoutCanExecuteDelegation(_ => @@ -171,12 +173,18 @@ private async void MergeResourcesAsync() } } - private async void DiscoverResourcesAsync () + private async void DiscoverResourcesCommandAsync() + { + await DiscoverResourcesAsync(); + } + + public async Task DiscoverResourcesAsync() { _discoverResourcesCommand.SetCanExecute (false); + ServerResources serverResources = new ServerResources(); try { - var serverResources = await _serverSettingsViewModel.GetServerResources (); + serverResources = await _serverSettingsViewModel.GetServerResources (); var calendars = serverResources.Calendars.Select (c => new CalendarDataViewModel (c)).ToArray(); var addressBooks = serverResources.AddressBooks.Select (a => new AddressBookDataViewModel (a)).ToArray(); @@ -256,7 +264,7 @@ private async void DiscoverResourcesAsync () } using (var selectResourcesForm = SelectResourceForm.CreateForFolderAssignment(_optionTasks, ConnectionTests.ResourceType.Calendar, calendars, addressBooks, taskLists)) { - if (selectResourcesForm.ShowDialog() == System.Windows.Forms.DialogResult.OK) + if (AutoConfigure || selectResourcesForm.ShowDialog() == System.Windows.Forms.DialogResult.OK) { var optionList = new List(); @@ -298,6 +306,7 @@ private async void DiscoverResourcesAsync () { _discoverResourcesCommand.SetCanExecute (true); } + return serverResources; } private OptionsModel CreateOptions (ResourceDataViewModelBase resource) @@ -364,6 +373,7 @@ private set public bool IsActive { get; set; } public bool SupportsIsActive { get; } = false; public bool AutoCreateOutlookFolders { get; set; } = false; + public bool AutoConfigure { get; set; } = false; public bool OnlyAddNewUrls { get; set; } = false; public IEnumerable Items { get; } IEnumerable ITreeNodeViewModel.Items => Items; diff --git a/CalDavSynchronizer/Ui/Options/ViewModels/OptionsCollectionViewModel.cs b/CalDavSynchronizer/Ui/Options/ViewModels/OptionsCollectionViewModel.cs index 9ec04a51..a55c75da 100644 --- a/CalDavSynchronizer/Ui/Options/ViewModels/OptionsCollectionViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/ViewModels/OptionsCollectionViewModel.cs @@ -301,7 +301,15 @@ private void AddMultiple () ShowProfile(viewModel.Model.Id); } } - + + public IOptionsViewModel AddMultipleHeadless (IProfileType type) + { + var profileModelFactoryFactory = _profileModelFactoriesByType[type]; + var viewModel = profileModelFactoryFactory.CreateTemplateViewModel(); + _options.Add(viewModel); + return viewModel; + } + private IProfileType QueryProfileType() { return _uiService.QueryProfileType(_profileTypeRegistry.AllTypes); From d47203ee8fd18fd7e17079ed507c938a6d7e8b19 Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Tue, 24 Apr 2018 11:59:10 +0200 Subject: [PATCH 02/12] Avoid exception if CalDav server is not reachable and trivial cleanup --- CalDavSynchronizer/ComponentContainer.cs | 2 -- .../Ui/Options/BulkOptions/ViewModels/ServerResources.cs | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index 3a259468..d1bf15ed 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -241,8 +241,6 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge { AutoconfigureKolab(options, generalOptions); } - // Search for any existing Kolab setting - // var kolabOption = options.First(option => option.ProfileTypeOrNull == "Kolab"); _oneTimeTaskRunner = new OneTimeTaskRunner(_outlookSession); } diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerResources.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerResources.cs index 8296506e..eee47b19 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerResources.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerResources.cs @@ -44,6 +44,9 @@ public ServerResources ( TaskLists = taskLists; } - public bool ContainsResources => Calendars.Count > 0 || AddressBooks.Count > 0 || TaskLists.Count > 0; + public bool ContainsResources => + (Calendars != null && Calendars.Count > 0) || + (AddressBooks != null && AddressBooks.Count > 0) || + (TaskLists != null && TaskLists.Count > 0); } } \ No newline at end of file From 47ce49873061dbdef12bd906017719c169d7a97e Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Thu, 24 May 2018 11:57:07 +0200 Subject: [PATCH 03/12] Handle read-only, LDAP-based address books: Sync from server to outlook --- CalDavSynchronizer/DataAccess/AddressBookData.cs | 4 +++- CalDavSynchronizer/DataAccess/CardDavDataAccess.cs | 3 ++- .../ViewModels/KolabMultipleOptionsTemplateViewModel.cs | 2 ++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CalDavSynchronizer/DataAccess/AddressBookData.cs b/CalDavSynchronizer/DataAccess/AddressBookData.cs index 33154d29..41486d54 100644 --- a/CalDavSynchronizer/DataAccess/AddressBookData.cs +++ b/CalDavSynchronizer/DataAccess/AddressBookData.cs @@ -24,11 +24,13 @@ public class AddressBookData { public Uri Uri { get; } public string Name { get; } + public bool ReadOnly { get; } - public AddressBookData (Uri uri, string name) + public AddressBookData (Uri uri, string name, bool readOnly = false) { Uri = uri; Name = name; + ReadOnly = readOnly; } } } \ No newline at end of file diff --git a/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs b/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs index 70ac43ad..671d335a 100644 --- a/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs +++ b/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs @@ -170,7 +170,8 @@ public async Task> GetUserAddressBooksNoThrow (bo { var path = urlNode.InnerText.EndsWith ("/") ? urlNode.InnerText : urlNode.InnerText + "/"; var displayName = string.IsNullOrEmpty (displayNameNode.InnerText) ? "Default Addressbook" : displayNameNode.InnerText; - addressbooks.Add (new AddressBookData (new Uri (addressBookDocument.DocumentUri, path), displayName)); + bool readOnly = null != responseElement.SelectSingleNode("D:propstat/D:prop/D:resourcetype/A:directory", addressBookDocument.XmlNamespaceManager); // http://sabre.io/dav/carddav-directory/ + addressbooks.Add (new AddressBookData (new Uri (addressBookDocument.DocumentUri, path), displayName, readOnly)); } } } diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index a65cb22b..d70ea16b 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -279,6 +279,8 @@ public async Task DiscoverResourcesAsync() { var options = CreateOptions (resource); _serverSettingsViewModel.SetResourceUrl (options, resource.Model); + if (resource.Model.ReadOnly) + options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; optionList.Add (options); } From 0a6c450ee874a04303a916bd43e331e967978e1a Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Thu, 24 May 2018 13:56:57 +0200 Subject: [PATCH 04/12] Set free/busy URL in Kolab autoconfiguration --- CalDavSynchronizer/ComponentContainer.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index d1bf15ed..207e57d4 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -68,6 +68,7 @@ using GenSync.Synchronization; using AppointmentId = CalDavSynchronizer.Implementation.Events.AppointmentId; using MessageBox = System.Windows.Forms.MessageBox; +using Microsoft.Win32; namespace CalDavSynchronizer { @@ -305,6 +306,24 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO newOptions = remainingOptions.ToArray(); } + // Set free/busy URL in Registry + string regPath = + @"Software\Microsoft\Office\" + Globals.ThisAddIn.Application.Version.Split(new char[] { '.' })[0] + @".0" + + @"\Outlook\\Options\Calendar\Internet Free/Busy"; + var key = Registry.CurrentUser.OpenSubKey(regPath, true); + if (key == null) + key = Registry.CurrentUser.CreateSubKey(regPath); + var value = key.GetValue("Read URL"); + if (value == null || string.IsNullOrEmpty(value.ToString())) + { + ServerSettingsTemplateViewModel server = kolabOptionsModel.ServerSettingsViewModel as ServerSettingsTemplateViewModel; + if (!string.IsNullOrEmpty(server.CalenderUrl)) + { + Uri url = new Uri(new Uri(server.CalenderUrl), "/freebusy/"); + key.SetValue("Read URL", url.ToString() + "%NAME%@%SERVER%"); + } + } + // Save new settings await ApplyNewOptions(options, newOptions, generalOptions, optionsCollectionModel.GetOneTimeTasks()); } From bf3d598462d1d7e788c95daa785b98e35d7fc3a3 Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Thu, 24 May 2018 14:14:04 +0200 Subject: [PATCH 05/12] Initialize LDAP-based GAL as default address list and make sure all new address books are visible as address list --- .../KolabMultipleOptionsTemplateViewModel.cs | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index d70ea16b..54013a7b 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -42,6 +42,7 @@ using System.Text.RegularExpressions; using CalDavSynchronizer.Globalization; using Exception = System.Exception; +using Microsoft.Win32; namespace CalDavSynchronizer.Ui.Options.BulkOptions.ViewModels { @@ -240,11 +241,38 @@ public async Task DiscoverResourcesAsync() { newAddressBookFolder = new GenericComObjectWrapper (defaultAddressBookFolder.Inner.Folders.Add(newAddressBookName, OlDefaultFolders.olFolderContacts) as Folder); newAddressBookFolder.Inner.Name = newAddressBookName; + newAddressBookFolder.Inner.ShowAsOutlookAB = true; + } + // Special handling for GAL delivered by CardDAV: set as default address list + if (resource.Uri.Segments.Last() == "ldap-directory/") + { + var _session = Globals.ThisAddIn.Application.Session; + foreach (AddressList al in _session.AddressLists) + { + if (al.Name == newAddressBookName) + { + // We need to set it in the registry, as there does not seem to exist an appropriate API + string regPath = + @"Software\Microsoft\Office\" + Globals.ThisAddIn.Application.Version.Split(new char[] { '.' })[0] + @".0" + + @"\Outlook\Profiles\" + _session.CurrentProfileName + + @"\9207f3e0a3b11019908b08002b2a56c2"; + var key = Registry.CurrentUser.OpenSubKey(regPath, true); + if (key != null) + { + // Turn ID into byte array + byte[] bytes = new byte[al.ID.Length / 2]; + for (int i = 0; i < al.ID.Length; i += 2) + bytes[i / 2] = Convert.ToByte(al.ID.Substring(i, 2), 16); + key.SetValue("01023d06", bytes); + } + } + } + } resource.SelectedFolder = new OutlookFolderDescriptor (newAddressBookFolder.Inner.EntryID, newAddressBookFolder.Inner.StoreID, newAddressBookFolder.Inner.DefaultItemType, newAddressBookFolder.Inner.Name, 0); } - // Create and assign all Kolab address books that are not yet synced to an outlook folder + // Create and assign all Kolab task lists that are not yet synced to an outlook folder GenericComObjectWrapper defaultTaskListsFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderTasks) as Folder); foreach (var resource in taskLists.Where(c => c.SelectedFolder == null)) { From 108f465bd41644de480f676a7919e5d0d53eb1b2 Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Wed, 30 May 2018 20:07:02 +0200 Subject: [PATCH 06/12] Generalize detection of read-only resources by using CalDAV/CardDAV acl property --- .../DataAccess/CalDavDataAccess.cs | 6 ++- CalDavSynchronizer/DataAccess/CalendarData.cs | 4 +- .../DataAccess/CardDavDataAccess.cs | 5 ++- CalDavSynchronizer/DataAccess/TaskListData.cs | 4 +- .../DataAccess/WebDavDataAccess.cs | 42 +++++++++++++++++++ .../KolabMultipleOptionsTemplateViewModel.cs | 4 ++ 6 files changed, 59 insertions(+), 6 deletions(-) diff --git a/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs b/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs index 5e9a04a6..f4fc5978 100644 --- a/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs +++ b/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs @@ -182,16 +182,18 @@ public async Task GetUserResourcesNoThrow (bool useWellKnownUrl if (supportedComponentsNode != null) { var path = urlNode.InnerText.EndsWith ("/") ? urlNode.InnerText : urlNode.InnerText + "/"; + var uri = new Uri(calendarDocument.DocumentUri, path); + bool ro = await IsReadOnly(uri); if (supportedComponentsNode.InnerXml.Contains ("VEVENT")) { var displayName = string.IsNullOrEmpty (displayNameNode.InnerText) ? "Default Calendar" : displayNameNode.InnerText; - calendars.Add (new CalendarData (new Uri (calendarDocument.DocumentUri, path), displayName, calendarColor)); + calendars.Add (new CalendarData (uri, displayName, calendarColor, ro)); } if (supportedComponentsNode.InnerXml.Contains ("VTODO")) { var displayName = string.IsNullOrEmpty (displayNameNode.InnerText) ? "Default Tasks" : displayNameNode.InnerText; - taskLists.Add (new TaskListData (new Uri (calendarDocument.DocumentUri, path).ToString(), displayName)); + taskLists.Add (new TaskListData (uri.ToString(), displayName, ro)); } } } diff --git a/CalDavSynchronizer/DataAccess/CalendarData.cs b/CalDavSynchronizer/DataAccess/CalendarData.cs index 7c61bafa..745f4954 100644 --- a/CalDavSynchronizer/DataAccess/CalendarData.cs +++ b/CalDavSynchronizer/DataAccess/CalendarData.cs @@ -25,12 +25,14 @@ public class CalendarData public Uri Uri { get; } public string Name { get; } public ArgbColor? Color { get; } + public bool ReadOnly { get; } - public CalendarData (Uri uri, string name, ArgbColor? color) + public CalendarData (Uri uri, string name, ArgbColor? color, bool readOnly = false) { Uri = uri; Name = name; Color = color; + ReadOnly = readOnly; } } } \ No newline at end of file diff --git a/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs b/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs index 671d335a..680b81d8 100644 --- a/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs +++ b/CalDavSynchronizer/DataAccess/CardDavDataAccess.cs @@ -170,8 +170,9 @@ public async Task> GetUserAddressBooksNoThrow (bo { var path = urlNode.InnerText.EndsWith ("/") ? urlNode.InnerText : urlNode.InnerText + "/"; var displayName = string.IsNullOrEmpty (displayNameNode.InnerText) ? "Default Addressbook" : displayNameNode.InnerText; - bool readOnly = null != responseElement.SelectSingleNode("D:propstat/D:prop/D:resourcetype/A:directory", addressBookDocument.XmlNamespaceManager); // http://sabre.io/dav/carddav-directory/ - addressbooks.Add (new AddressBookData (new Uri (addressBookDocument.DocumentUri, path), displayName, readOnly)); + var uri = new Uri(addressBookDocument.DocumentUri, path); + bool ro = await IsReadOnly(uri); + addressbooks.Add (new AddressBookData (uri, displayName, ro)); } } } diff --git a/CalDavSynchronizer/DataAccess/TaskListData.cs b/CalDavSynchronizer/DataAccess/TaskListData.cs index c5b2c391..fad10dfa 100644 --- a/CalDavSynchronizer/DataAccess/TaskListData.cs +++ b/CalDavSynchronizer/DataAccess/TaskListData.cs @@ -24,11 +24,13 @@ public class TaskListData { public string Id { get; } public string Name { get; } + public bool ReadOnly { get; } - public TaskListData (string id, string name) + public TaskListData (string id, string name, bool readOnly = false) { Id = id; Name = name; + ReadOnly = readOnly; } } } \ No newline at end of file diff --git a/CalDavSynchronizer/DataAccess/WebDavDataAccess.cs b/CalDavSynchronizer/DataAccess/WebDavDataAccess.cs index d23eb6f3..e67861d4 100644 --- a/CalDavSynchronizer/DataAccess/WebDavDataAccess.cs +++ b/CalDavSynchronizer/DataAccess/WebDavDataAccess.cs @@ -113,6 +113,48 @@ public async Task GetPrivileges () return privileges; } + // Only consider resource as read-only if we are sure. That is: + // - we can query ACL property + // - we see a READ privilage, but no WRITE privilege + public async Task IsReadOnly(Uri resourceUrl) + { + try + { + var document = await _webDavClient.ExecuteWebDavRequestAndReadResponse( + resourceUrl, + "PROPFIND", + 0, + null, + null, + "application/xml", + @" + + + + + + " + ); + + if ( + document.XmlDocument.SelectSingleNode("/D:multistatus/D:response/D:propstat/D:prop/D:acl/D:ace/D:grant/D:privilege/D:read", document.XmlNamespaceManager) != null + && document.XmlDocument.SelectSingleNode("/D:multistatus/D:response/D:propstat/D:prop/D:acl/D:ace/D:grant/D:privilege/D:write", document.XmlNamespaceManager) == null + ) + { + return true; + } + else + { + return false; + } + } + catch (Exception x) + { + s_logger.Error(null, x); + return false; + } + } + protected async Task GetEtag (Uri absoluteEntityUrl) { var headers = await _webDavClient.ExecuteWebDavRequestAndReturnResponseHeaders (absoluteEntityUrl, "GET", null, null, null, null, null); diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index 54013a7b..b6b8b784 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -300,6 +300,8 @@ public async Task DiscoverResourcesAsync() { var options = CreateOptions (resource); _serverSettingsViewModel.SetResourceUrl (options, resource.Model); + if (resource.Model.ReadOnly) + options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; optionList.Add (options); } @@ -316,6 +318,8 @@ public async Task DiscoverResourcesAsync() { var options = CreateOptions (resource); _serverSettingsViewModel.SetResourceUrl (options, resource.Model); + if (resource.Model.ReadOnly) + options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; optionList.Add (options); } From f4032bd9d5524e32d4757345d51e26346dd64d5b Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Mon, 4 Jun 2018 16:23:02 +0200 Subject: [PATCH 07/12] Make sync settings visible to user - Set the Outlook folder's description to explain Read-only / read-write resourcres - Use custom icons for synchronized folders --- CalDavSynchronizer/CalDavSynchronizer.csproj | 6 + CalDavSynchronizer/ComponentContainer.cs | 76 ++++++++++- .../Globalization/StringResources.de-DE.resx | 24 ++++ .../Properties/Resources.Designer.cs | 62 ++++++++- CalDavSynchronizer/Properties/Resources.resx | 18 +++ .../Resources/AddressbookReadOnly.gfie | Bin 0 -> 2330 bytes .../Resources/AddressbookReadOnly.ico | Bin 0 -> 5430 bytes .../Resources/AddressbookReadWrite.gfie | Bin 0 -> 4526 bytes .../Resources/AddressbookReadWrite.ico | Bin 0 -> 5430 bytes .../Resources/CalendarReadOnly.gfie | Bin 0 -> 2168 bytes .../Resources/CalendarReadOnly.ico | Bin 0 -> 5430 bytes .../Resources/CalendarReadWrite.gfie | Bin 0 -> 4360 bytes .../Resources/CalendarReadWrite.ico | Bin 0 -> 5430 bytes .../Resources/TasklistReadOnly.gfie | Bin 0 -> 2390 bytes .../Resources/TasklistReadOnly.ico | Bin 0 -> 5430 bytes .../Resources/TasklistReadWrite.gfie | Bin 0 -> 4729 bytes .../Resources/TasklistReadWrite.ico | Bin 0 -> 5430 bytes .../KolabMultipleOptionsTemplateViewModel.cs | 124 +++++++++++++++++- 18 files changed, 307 insertions(+), 3 deletions(-) create mode 100644 CalDavSynchronizer/Resources/AddressbookReadOnly.gfie create mode 100644 CalDavSynchronizer/Resources/AddressbookReadOnly.ico create mode 100644 CalDavSynchronizer/Resources/AddressbookReadWrite.gfie create mode 100644 CalDavSynchronizer/Resources/AddressbookReadWrite.ico create mode 100644 CalDavSynchronizer/Resources/CalendarReadOnly.gfie create mode 100644 CalDavSynchronizer/Resources/CalendarReadOnly.ico create mode 100644 CalDavSynchronizer/Resources/CalendarReadWrite.gfie create mode 100644 CalDavSynchronizer/Resources/CalendarReadWrite.ico create mode 100644 CalDavSynchronizer/Resources/TasklistReadOnly.gfie create mode 100644 CalDavSynchronizer/Resources/TasklistReadOnly.ico create mode 100644 CalDavSynchronizer/Resources/TasklistReadWrite.gfie create mode 100644 CalDavSynchronizer/Resources/TasklistReadWrite.ico diff --git a/CalDavSynchronizer/CalDavSynchronizer.csproj b/CalDavSynchronizer/CalDavSynchronizer.csproj index be7b76be..c0f9c6ed 100644 --- a/CalDavSynchronizer/CalDavSynchronizer.csproj +++ b/CalDavSynchronizer/CalDavSynchronizer.csproj @@ -901,6 +901,8 @@ + + @@ -940,6 +942,10 @@ + + + + diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index 0a6949d8..80b2b0a1 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -55,6 +55,7 @@ using CalDavSynchronizer.Implementation.Tasks; using CalDavSynchronizer.Implementation.TimeZones; using CalDavSynchronizer.ProfileTypes; +using CalDavSynchronizer.Properties; using CalDavSynchronizer.Scheduling.ComponentCollectors; using CalDavSynchronizer.Ui.Options; using CalDavSynchronizer.Ui.Options.BulkOptions.ViewModels; @@ -248,6 +249,8 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge private async void AutoconfigureKolab(Options[] options, GeneralOptions generalOptions) { + // Make sure the add-on language is active in our context + Thread.CurrentThread.CurrentUICulture = new CultureInfo(GeneralOptionsDataAccess.CultureName); // Create all objects required to use the options collection models string profileType = "Kolab"; string[] categories; @@ -286,7 +289,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO // Do this only if we really have a response from the server. if (serverResources.ContainsResources) { - var allUris = + var allUris = serverResources.Calendars.Select(c => c.Uri.ToString()).Concat( serverResources.AddressBooks.Select(a => a.Uri.ToString())).Concat( serverResources.TaskLists.Select(d => d.Id)).ToArray(); @@ -306,6 +309,77 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO newOptions = remainingOptions.ToArray(); } + // Update existing Kolab Calendar resources + foreach (var resource in serverResources.Calendars) + { + foreach (var option in newOptions.Where(o => o.CalenderUrl == resource.Uri.ToString())) { + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); + if (resource.ReadOnly) + { + option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; + folder.Inner.Description = Strings.Get($"Read-only calendar") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadOnly) as stdole.StdPicture); + } + else + { + option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; + folder.Inner.Description = Strings.Get($"Read-write calendar") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadWrite) as stdole.StdPicture); + } + } + } + + // Update existing Kolab Address book resources + foreach (var resource in serverResources.AddressBooks) + { + foreach (var option in newOptions.Where(o => o.CalenderUrl == resource.Uri.ToString())) + { + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); + if (resource.ReadOnly) + { + option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; + folder.Inner.Description = Strings.Get($"Read-only address book") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadOnly) as stdole.StdPicture); + } + else + { + option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; + folder.Inner.Description = Strings.Get($"Read-write address book") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadWrite) as stdole.StdPicture); + } + } + } + + // Update existing Kolab Task list resources + foreach (var resource in serverResources.TaskLists) + { + foreach (var option in newOptions.Where(o => o.CalenderUrl == resource.Id)) + { + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); + if (resource.ReadOnly) + { + option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; + folder.Inner.Description = Strings.Get($"Read-only task list") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.TasklistReadOnly) as stdole.StdPicture); + } + else + { + option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; + folder.Inner.Description = Strings.Get($"Read-write task list") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.TasklistReadWrite) as stdole.StdPicture); + } + } + } + // Set free/busy URL in Registry string regPath = @"Software\Microsoft\Office\" + Globals.ThisAddIn.Application.Version.Split(new char[] { '.' })[0] + @".0" + diff --git a/CalDavSynchronizer/Globalization/StringResources.de-DE.resx b/CalDavSynchronizer/Globalization/StringResources.de-DE.resx index fb225bf3..fdb6b91a 100644 --- a/CalDavSynchronizer/Globalization/StringResources.de-DE.resx +++ b/CalDavSynchronizer/Globalization/StringResources.de-DE.resx @@ -1407,4 +1407,28 @@ Andere Protokolle können mittels protocol: address hinzugefügt werden Gelöscht + + Änderungen in Outlook und Änderungen vom Server werden zusammengeführt. + + + Änderungen in Outlook werden verworfen und durch Daten vom Server ersetzt. + + + Adressbuch mit Nur-Lese-Zugriff + + + Kalender mit Nur-Lese-Zugriff + + + Aufgabenliste mit Nur-Lese-Zugriff + + + Adressbuch mit Lese- und Schreibzugriff + + + Kalender mit Lese- und Schreibzugriff + + + Aufgabenliste mit Lese- und Schreibzugriff + \ No newline at end of file diff --git a/CalDavSynchronizer/Properties/Resources.Designer.cs b/CalDavSynchronizer/Properties/Resources.Designer.cs index ae58a5ee..24733bb3 100644 --- a/CalDavSynchronizer/Properties/Resources.Designer.cs +++ b/CalDavSynchronizer/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace CalDavSynchronizer.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -70,6 +70,26 @@ internal static System.Drawing.Bitmap About { } } + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon AddressbookReadOnly { + get { + object obj = ResourceManager.GetObject("AddressbookReadOnly", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon AddressbookReadWrite { + get { + object obj = ResourceManager.GetObject("AddressbookReadWrite", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). /// @@ -90,6 +110,26 @@ internal static System.Drawing.Bitmap ApplicationLogoLarge { } } + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon CalendarReadOnly { + get { + object obj = ResourceManager.GetObject("CalendarReadOnly", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon CalendarReadWrite { + get { + object obj = ResourceManager.GetObject("CalendarReadWrite", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -149,5 +189,25 @@ internal static System.Drawing.Bitmap SyncReport { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon TasklistReadOnly { + get { + object obj = ResourceManager.GetObject("TasklistReadOnly", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). + /// + internal static System.Drawing.Icon TasklistReadWrite { + get { + object obj = ResourceManager.GetObject("TasklistReadWrite", resourceCulture); + return ((System.Drawing.Icon)(obj)); + } + } } } diff --git a/CalDavSynchronizer/Properties/Resources.resx b/CalDavSynchronizer/Properties/Resources.resx index 85e8dde9..a6a60188 100644 --- a/CalDavSynchronizer/Properties/Resources.resx +++ b/CalDavSynchronizer/Properties/Resources.resx @@ -145,4 +145,22 @@ ..\Resources\ApplicationLogoLarge.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\AddressbookReadOnly.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\AddressbookReadWrite.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\CalendarReadOnly.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\CalendarReadWrite.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\TasklistReadOnly.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\TasklistReadWrite.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/CalDavSynchronizer/Resources/AddressbookReadOnly.gfie b/CalDavSynchronizer/Resources/AddressbookReadOnly.gfie new file mode 100644 index 0000000000000000000000000000000000000000..0a1b15e605a22c243eb3411ff91b1d2bc1bafa0a GIT binary patch literal 2330 zcmYdKODVBoj9~x*8;;!6lEjq6l0-0%Gbca4z&XD(uY?7}2Ld}g8`grv^weSy2f{P} zGud(yD^rUg!Ysv^RjFW6#tM)|1t7MwVXTC+?Q9^*AzE4T5_3~Qaz6RV*>*PUWtqj9 zNja&EKoLee8;;`CoYdr!)D#dOXbfv+F3^4u+lDPIzbH4cgc-;#$V<1gVF5XO15jOO zfS)@rmlPLJg4ff-B?w3>fG`IekSwmM`T{h$+|$J|B;xSftNXJ=0wq{K+|OCmDYP-* zNJj+6zJ?#mFS2jgvS~Z7=gI~82~W9iXm+(GDBg|}m^@8`KW)$h!=WL#Y2+AU_PD8prb`1S|mHLc7u)smjG z7j)0>{a_%Iv+74qMM&4$w%ZSjb_l%_H?Ml=wqlk*f!dn(wHqX?tat2v=qI>crq4t% zVWr3qfj-^`=S9VLJ*=_6ztZt=r&0Z(?TmlYYmSQaeV=^Ul;cOo^or(p40X)wb8hdy z6O_n+j-$=-R z;`aZrK#T7m+k@jz?nIitX$QsxgQu&X%Q~loCSa;!&o4+!&MX0^k^ev`I~z`58puoW z%}+@M#abRHs&jw}At}cYq}>Lba+rWDM<@L02AGZrr5hh2(~U4B-6SVSuqw+Mh>42+ zmG@vb;4R_Ua6n@3-n}JTckI|vF`<>gYR8`ky39aucu(y0fFqOdY>@zg6493SNGS*Q zxvS3^fWV&rpM+Qcm}K-lQI5&Sr$?nT|K_Gv4er{Snl=xPC%>h^ioUG3?+=|b;e1=3 zRK=%f++b6i?p~J!8K{DWCW7GQ~hyb zBM>D1-MCw0<^OAwzw>~=-RyIYmpo>@1*$xMzt;8Pdh06j)yJ;O@8i8(q;S+(I_TQ_ z-HluI1fC?OCjfy*!BwecPaE1hZMbaT+-_OC_EDe#7sH)fe!jk|c5Vbk3ORAg3CZJ` z`FS7{Y*>p+5=&A!flR14ICJMF7H5N{v1WTvi9kfQXV8IW`*ViC_~u|vEFJ$um-lok!M++rw+s-%A2Hiubq+ zF3*tIxM_RN&8CGHj~{2h+)z``-1hJ5^M^SlM=N4KJUJ`g#=3jGvCpY3=|8^zw|9Qs zWy~|_&>fy2r#nY>E-1bl*;cJ@XSBw?Y7=BrPQ|Niq^Y{C}Ma&vt z*`fFI*J1NVhk-@M?8KA%qGV=TBjmV_<0c=^x+ zg$gU|2m7_aum=%EqO3%a42jS(YV#*;-LE~f4T6Wl!C>&w9ZAp(D`>}!wEFV{>hRaI3mgOn66`w}J(95{*M zVoz8*&nGA6r72GBJdOBmwmBR;cmg|iT!6*m*PVkNKjJJ|S^9g=?FM{4V0s!TEY#O= zIuYi7ob)i_KX*w(1P37WX|cvcHKIlz8*Mw6j-_P z&-2qBvsmmt8&Zzs2?V|brsNMwv?{}8m z&f}0A83A%~z69O}2?>5B-gFM>%x&579@nltP}YX+mXn{xyn6K!T&^K(*x*&xd&LSL z^71~Ty!^S`m-hnuB36EmH~YE~MgTsMpvCWwLg;y=|0(3(70N#p=KrTajr`UQ z>EMlQtBCFsy!5U9g?Ca;Xr5n_eWdq8VxpSgk`bPC)ax z-{4m6dmaza+pB_d{nMv`si|Ol&aFA8rtd1{(c89BuJ|gDr#PJ>C@2_HLd3D>cxm%e zs#B?{fs8xQ(*tyM0omEW<;zM}Sp2nl9o4DK%m772qexEvfwZ)F?A$qw+qds3eWCo6 rgJ|QgC`I@Qwb$kqRF76d?W5_3_Oty`Wb`xido}zPAqepnJ_$bo^RB;W literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Resources/AddressbookReadWrite.gfie b/CalDavSynchronizer/Resources/AddressbookReadWrite.gfie new file mode 100644 index 0000000000000000000000000000000000000000..6b74ebd00bd699034c5e817a18ef19c4f6a49ae2 GIT binary patch literal 4526 zcmcJR2T)U6x5rNiO^SemQlxoBiUC4zF-TQ<0HuQj2sI=?LhneCUZe>sRRjeT1d$>| zL@K+H3t>oSpD6kPSc-+#$fMo~G#2Znf%V1UsVRN3;BXj?7t#fVqjZi<=#fc_Mh2pMj)$pnZb7J{QBVNI zQGndwFi_xs+i=+N@?%$O4AK)tnKKQbO(N_*}gzcmmwQd8Xcj75TzPE zlV%k!a6|m@aM-4C{qOSYH&(7*g6|C_m~n70#GJWN&L%oHaLQC*VYwxKjZ-D8n`Adl zRKgKpd$St(dC{dJ{Jd_mswJ=ftQ>nj%Ezb>SQ)nBvMkixx@A#DY89@N#!U?)PDV72 zhWR}QZN`o+X_Bg%10DTX49cVz(@pznW45+|*H6|OFwWmnTCo>RE{#qTCDx|&av+{5 zsM;qh&-+$x_04zMVQPUZ(=Y&YXZ}T{eh;Dy*?6)}AepCaIP;4QsXl<%#GB`x_|kj5 z%vwdf_lBG6Ow>;A!!Cj9aGDDQZut$2>&wMziA!ewmmp0YZfHwhy#|O&yifPe#M%byJ3AdTWm?p0Zs~$ zQ;-5*u>}Kvc!nB1oJ^4m-AWmmo~`OzFHGSPPGWPAK@ijwWj3Nu4svdO!_6xJLEg-1i#LB8`#X&As_NshL2oh3 z7FU&snd8xl=LY>|rBY6z**g#>>?oRm!7JC8+1zhmIZw}_$*TTSFhn(iNF!nn%Vc|k zJ8j_MX?)kL>u&19n{0=)X*SRAzd(mO*cRjFjZRO#O0|yHOz`1(yR=#1nUL@$Lss{i zP!@yGKx8xn`;8)bD;^PzLOCfDwKze2R&5VJoQji8pnP=3x(Dw#o{;P?w{I!s@8&Se zV8QG8Wo*K<4Kv<9N%(#<_Cry<{9@lwn47z-iUU8(4)a}f-H>f^Z>qNa!*Hn>!wac!VA zLDD`t;-t{^XYEv`y27s?&+H~XcldH^+hPA}{PNu*Tv*sj2o=9fth&Et(ZBQy_W*i=E1IyF9=P$fLruzWr7l-dVWvNsc?F?Q_3kPYE!!>H z2&5hIv-rJ$ZT=GM{sAmP1}Fn%R$tq{t6^yOQv7#Uob~N|Kiu7(MDf)bWrck}%85+|o8_f1*5L|#^kG4Wzn6dlJ5`u;ixb^(l^(0q24pjE1z{d7KK9)K$y-?C!#E#e+>aKBX7#MQq%lxlZ{;8~=ux@EVGUClS-fO>b`S0!YIMgj zT**>tr>kvom3A;Pkbn z22pS0x~^Sy*81AH%QjUYUhOGKD%V+!V7^SE+9=F*I&R+FY4Uxe==MSDpMCr-Es_v@ zzUYCHbd%o0l?0Uc=b=2C%GWx#(Ii^a={Mz$opbUcC4dm)#(VpswG8fyOhqvfC#mZs z3orMxwv|m-4I~=f%M-YI5b|uVKU8u=f^Y1prZ3Qb-O0<`Q`dLxbu<5X%etjPHhyA_ zCG@_m%jp8w=R!j7KlVgboOwJP?~TakdU%3aT>~sR{Lp5Ez3iJ)kSpY6z)s0-`n*eF zRFTNCaJIzR)h-8K=3Iry=!x>WE`eRkWLwGtY@nIenbX*Q9 zZ~U266lO`_=-Nl~4N7W~tW(|_gk-DQ7@RkY>;=c00qB-t>UcZo|bXYD*?ZoV$PfeFEQjLVa-D5B@6N%bUvAq37-+ zG*9jKF#VA*ayZ9W@LkT-Vd0bOz3#HziHDioJc`CVN(XAguDAP<;|1f)^Of-N3aydR zus~&Z#i)E#_aT*YzHyB9bf!2NF#sJcL(OV+`zSIl(P6!ij&ArP_&Fp`!C@!JaD#C& zz&fEQID(-t44S;~7>T4P?yw^yq9R*r>i-#t$lLv2Kx9Y;r~d~KfjEwV$PpqYw$~4h zh=@lflC+p)4Rz7DpfEQn_ z+}Tdn=0TrP<(yvcL|h;}R7xu2LMy88Ofh6H@1{|zkVhEocXtt1RJ=pZj%?69|eNL+ln`W-`jTB%} z0h6T>^<)$(P6yLpF#dv^zUXvrjVn2TE{O_YjWx97NFwY~j^*#1>WO1~b;DvPJHTjg zcqIM^b}?8C3Qma+S1jJt3yVLRKIUbL`7d5RvdJ;y>_diRlfQib8ayKLI+lev6Q4{NF?y<-4p$_|3n4hy(zt4N9~r_9dHGyIwbEw2^Cf_^#;E;XIUgMdqPZ@*$IOm#RIF(8 zsyg%?uJ9szRXb6O=EdUTdP68_VWQ}ahxDC%Ig6%4^(w0ub4i>jA-dPINK|4r#*rF| z0hy6aeC2BqWAfBPUYnbH>F?{ax2XV#>ZYlBa(KvC4!OF+VHhU;78+k+rSwOdJwpiY zr#_-=6{rpweW;A^qLx_7Hch&rNz)brlJRK-K0qQO%H~%%=23szPICr>+o{?US}|6l zXw|?zf)Z8Ehuqf)WrJVJ8D<;zOLs5+&MBlGmb+!%)+QT5YPboK2iie1{gr1bJoZCm zC&&KS@hh!-)Np4WJZTI6t_>^zP#_H`=ciTnKxEyfa$*CR0TMFYE}h#*Bq&i$MKtGr>t!VRNVTOF+vrE8T5#_-IRIhxKk?u z#h#pR3OF5ouq&+tL3_C*30$gw+^@3RrT74p+-hx6jG%BKsFuEf6~Qn!Rz1T_yMek| z@p1aH$keYzVudO}&eWrpF+zr?_;bL{^O+aer$4oXjH`(Qg}=V4C7ei5|9Y=m+m5-0 z$kO~M=(9?Pt195wYOfQpF&3NA=Q!5lOGf~I1ic#)BKW`FZO@WDhxyq1c=eIiVLXY+ zM`j|9O<09;b^lHt`KPMGg@0ci{`Y+aR*&!Nado%?vUezCMW6vjO6tU7B`xz36B7k# z+uIlEuv|-PO>ZfgJ8hw?tMG|{e%@JS^ZUq`-xn@-Rtax#kvoD@j9x>zoHex#=&`7x zf$na)p@@)haO}Z%;w&296=1u$@ZoGMP4iP3{oXp4pQJ-ojnxDhri|AUOeX3RZyF16 zBbKqjl5putRll6b`1thM(K{rjNe_b5&ZDtY#3m^FjkbL!N_}BnK)PJ>8aWSVA}4(=a?>%?PXa%Ft^>LK+3P->h4ZIl zZEcOSvoo%)uDH0kbcyur=H`ZkTN?kr=#|DuYjd()O4M2-nllDMet#j_l9sgN#v{@8{rgaah52M`NzV z3f+fDz+!p5r5!fP~advAj!dFivX76s6En6!4rP>cV+S|Ew;T#IjNo0I%$mbjNITN5yS=boa z7L{>^VvXZy$(%_3<}r9$j3a9GeD2-5C;L-QP7Y5>nlhFBu_5Gz+4FVq1TF+>az0R- zkX5rWH#5b?#-_^;#v&Qq$M@yd4e8nArX)B2n1s*psDqqV64 zNgC^KP<5$<+#Qx=_{=0_lNm>Im9)0D^6=rqr)>Y^UZ|fXEVt`#ves!aZmup*t^Zm2 z?={vT$*D*WSy~#dBZ<1H=YQT+{l41o)K_T@lBip4c`=e4-fs0@qV89}CynXs?4+%& zO^z!sx)*A_a=sB~v8JYmrlux&E@@7+p6BV8;%4#J$2orN80XHNqq@49#>Pe&j}&Vk zUy~ZMbXDuOTWO7>)f`e%QsnU}l}a9ac*hcs_E`Bef|D8OmYbG59&W!^idu_}gkXwru%~u&^)` z3WbcNUBnq_kJS35`0Hi2kg}M`6a-Br!N^ zV$SvJ*JY`!tfc5v9_9OtL>|`ST0~!w1N%@G(38?_JwzVtOX22WWNe(oo^>A(x$9Gu z`xCLUvXXOvWCsa{s`@hjP&jjGpWq(x##F?P;~SBS(!GZh;ru!~Z1nM6W=PO_YeF{K z5*@UGxZw3Dg4d9~+m5rLV`)qoKy9ox*Q2zk2-6ffus7fB=*ij5-8t*qo#IdXaCDt6 z=^Li7z;rrxc6MF+Ani%bwKBd^xKkE6k;~EJxTY}XhKRSd$@_EA1OGZF3rlpG^GyYcJcthANp}KTAQj!VT(PQld?4<01b)#zPCkRDupdB1^41|kS2fMIhj-W`$g_^?pnKq zLER(_cbo4}R+P&>VwUmmy>H2TQW0lNb<%iL$~U=_HHntIH)%UEk|0mk> z^k~cZqrf*(@PiTDNgYmuuvcxu2vmuBd>b{Bg1mU@>+4>Kle#%Ou+hbW_)n&B!hZr~ zQRAsjoPa8I0=Etd42RyP8p(Jhw|BRX?ILvI$ zi2Db}O}Kw-s*FLxav@=c82g^kZP&%SeoE(y{3q2J5^r>Lu*X@%Z>je1a&si|Q#0-y zn!&@9!Y0BNS5jy2nY$@=wl@Da-Os`&#on!Jttg2X`u}c1RmL;|*UrV>##*k6o;B|O z+3)S;K!*Q3uBJ>UNO0bj%a{G?>wX;nDo41 z-zmiB3i%7g&-hO9(ke3@GlSuOm@0;Ldl8I}+i0EZBCaP4#m#aej*gD9Z%F?9BDLyj z!P_py=!%-SAE*3>02!Oz?7v+A94F9&-t}WrTlx}o3x~8iP}y3 zQC%tO$xcx}H750@I~7BXoC=ylS@;;L<3@2SWdv;^ z-ag3bPiy7?8q)@ID|sNQ7)`E<8nF1Y?xgs<%hv@bAFa>TneV=c;By}XP6v$QYP12h z`;91#7|kyGw`p#uB0Eh%W^x$$@$302d@g5q_9tbtDb}J^kuXT@OHm&jSUa6l5zF}| z)P$=sMr^a2%-y?p<^R<`Qa)>}y-Zo!3Tzk8=^C%*))J?_Q&-LBf!351WXYW5$GB@} zr&zzcle`8=hm;>4=cUrU)Eeq)c`ETJT2IhO=ax{++mor4CnB$ac;GvfD<*R@L+qt;CXM;iH}3vTj5Z6uAY{Dl$f EKMl>v!2kdN literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Resources/CalendarReadOnly.gfie b/CalDavSynchronizer/Resources/CalendarReadOnly.gfie new file mode 100644 index 0000000000000000000000000000000000000000..6e764eb5f41aa6866c41ae5aceb5d4e45afe8932 GIT binary patch literal 2168 zcmYdKODVBoj9~x*8;;!6lEjq6l0-0%Gbca4z&XD(uY?7}2Ld}g8`grv^weSy2f{P} zGud(yD^rUg!Ysv^RjFW6#tM)|1t7MwVXTC+?Q9^*AzE4T5_3~Qaz6RV*>*PUWtqj9 zNja&EKoLee8;;`CoYdr!)D#dOXbfv+F3^4u+lDPIzbH4cgc-;#$V<1gVF5XO6Hr}e zfS)@rmlPLJg4ff-B?w3>fG`IekSwmM`T{h$($mE;B;xSft9!FV14URLypO9FQa$J- zD7sOghVkR_i|iY=Y}(H2xpIMi!c*=Wnq934inpRQI(ij2Hf{(?TF5c+$Mo5|b&6WN zXY71dmA+vn>ulpWwQ1jy-flgAVWD$7pRCmtg*W`y55N9kyrwn%VbP4--9J8lFj{h? zN@B|M#2xbAwuaW2*=QUMJAAglFovtVTk3Fu?w4B%1zI)S(usy&+>3S}Uh9-+DU;KD zAV~9rp3KS<^EI`24&BUr{Z;#0-_)NHv$4eodyxq4_Cu{j zw&tr2l-X<$-)*q{?C0L?3}OX~c%Qm+@*JD9``7gMYs@b;9si#v??3B@_?bk`f0RJd#{2u`=j)6nJ0I`Umo8Dj0*-&S3j3^P6uu zpZ@$&b>XjR{yXz|j6ts>x4uvHS}}8Bsb#vR%b8aW>$$(Z+p|?(o~F$`BE9M20hjejYrp#QKU>}yCoXff;R{op-j8TD7WuskvY*UW`{V@*LULk&6Ot!0 z^YcI^*svCtB$j}4a9)01svRgUGxAG<3-U|A(vU&`#DoI4>EDuf4Au-NT~TT?ytSf`^#5;zeTIXE3vur8DBGU_h>(_ ze&hA|mHh$p^=2WH4_(!m4st@Bg+CdwU~%kBSp zfvwB>#S5(YC*N~AxNg!U6T#?(_g@)4K3}t-E9EGIIFC^cgU2=2=P}05=VWsvuARIl zRyZP?kwJ+m`OGqPC8qa(qi&>U>(2ZBI_>h88TVgTi9EYqGGn)B)QO8Xs#zK2rQ$W` zTwJ^1PMDWtPvPZYmDSgNssEpHX}(+#x9KnG_DaUS8GPZh(-(`98?sn47$?v)XvPEA zcDaeg*&qwB*MqS@8Dhdv1rmnIfBygfU(d$HAk5sFaHS!T*^PI>B*q>d9v+d@bcPi^ UtWqg!zb^+_hbS;8%IJ1>04`NkPdjbx`yoLM3zx zgiXY#3`^09$_^q%MQKSPl@S(HN~R{O`)l6!^>)|Ko&B5rp#~mr=Dm6Ido%N9zBiu` zCXpc0(}m>eqI;_lP9elr$*FzREW|@u&yX_NMeLVE^6&*VA#hUCRY}((q)W2jAxRQ7 z-h%UT9K=9gX;T?1dR<)|h-|jGSg+#Q=(N}DB6{x7#IQ*29Z$aaWWKpX0TtXiAIm%*;=?e7S#JTwl0w3mmJBjSqadY4vXrint@=sPL<+$2I&N zI~LH{d2=0pTibn@&8zY*^n&VKLSd7Riwd9hV`F+8noM5MUQ=9fSzZPX9C)Mo=X3%e zKClM9x%(x0jr}spjq_*y(h^Wyte*p`)q~nv;K~)CyBjDkhc4Gj$-;<$*Gvm=b7;@pqN|5NbU5B85a`^jzQDmm*T<&4YRXs5za z$&L2m+KkKG*k6ScsXW^J?|0+#I{fGQd+7f<{2S|VSL_2(tncS|?dU&+{9U2_p$Pw< z0xdHCo(w|IrR;bpUnrg}7XKS3Ir+29x97E7hI0CC+unzLL)&cr{Jg#%C@b@~GcxoX zobEAx_wt>_XDQI0a`uZCqq9#;^!WVUwQC-!sf)gLTH3tw8_xLI*?v5lb08HKhWCGV z7C3PNGADb4(NSo0?AQ}DH3470@@&_0#;0#bR@O%xK0M*`mGbSpJUu@(<_9%#R~2ow~&#+EI#=Rj%c3%Osq72$IUtdo7X p9Et2A@t^w9oVWzmS!U#c{*x}yCRkk+e>~3L$WNaNb-Kgrz5^#Rt?d8+ literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Resources/CalendarReadWrite.gfie b/CalDavSynchronizer/Resources/CalendarReadWrite.gfie new file mode 100644 index 0000000000000000000000000000000000000000..4dfeddcbf96f2da58a457f304f0ef03dc171a5df GIT binary patch literal 4360 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 literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Resources/CalendarReadWrite.ico b/CalDavSynchronizer/Resources/CalendarReadWrite.ico new file mode 100644 index 0000000000000000000000000000000000000000..b371ffab211d95e2bea9dd2d1022cf9951894db9 GIT binary patch literal 5430 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 MxA-VR#f2o1t9smFU literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Resources/TasklistReadWrite.gfie b/CalDavSynchronizer/Resources/TasklistReadWrite.gfie new file mode 100644 index 0000000000000000000000000000000000000000..0b68db7335a71227c5f31a53817a04912d22e9e4 GIT binary patch literal 4729 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 literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Resources/TasklistReadWrite.ico b/CalDavSynchronizer/Resources/TasklistReadWrite.ico new file mode 100644 index 0000000000000000000000000000000000000000..1c5e1a96e16c350be4d83cda21e1178067b5a9ce GIT binary patch literal 5430 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 literal 0 HcmV?d00001 diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index b6b8b784..25470775 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -41,8 +41,11 @@ using Microsoft.Office.Interop.Outlook; using System.Text.RegularExpressions; using CalDavSynchronizer.Globalization; +using CalDavSynchronizer.Properties; using Exception = System.Exception; using Microsoft.Win32; +using System.Drawing; +using System.Runtime.InteropServices; namespace CalDavSynchronizer.Ui.Options.BulkOptions.ViewModels { @@ -292,6 +295,7 @@ public async Task DiscoverResourcesAsync() } using (var selectResourcesForm = SelectResourceForm.CreateForFolderAssignment(_optionTasks, ConnectionTests.ResourceType.Calendar, calendars, addressBooks, taskLists)) { + // Create and add new sync profiles if (AutoConfigure || selectResourcesForm.ShowDialog() == System.Windows.Forms.DialogResult.OK) { var optionList = new List(); @@ -300,8 +304,22 @@ public async Task DiscoverResourcesAsync() { var options = CreateOptions (resource); _serverSettingsViewModel.SetResourceUrl (options, resource.Model); + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(resource.SelectedFolder.EntryId) as Folder); if (resource.Model.ReadOnly) + { options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; + folder.Inner.Description = Strings.Get($"Read-only calendar") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadOnly) as stdole.StdPicture); + } + else + { + options.SynchronizationMode = SynchronizationMode.MergeInBothDirections; + folder.Inner.Description = Strings.Get($"Read-write calendar") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadWrite) as stdole.StdPicture); + } optionList.Add (options); } @@ -309,8 +327,22 @@ public async Task DiscoverResourcesAsync() { var options = CreateOptions (resource); _serverSettingsViewModel.SetResourceUrl (options, resource.Model); + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(resource.SelectedFolder.EntryId) as Folder); if (resource.Model.ReadOnly) + { options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; + folder.Inner.Description = Strings.Get($"Read-only address book") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadOnly) as stdole.StdPicture); + } + else + { + options.SynchronizationMode = SynchronizationMode.MergeInBothDirections; + folder.Inner.Description = Strings.Get($"Read-write address book") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadWrite) as stdole.StdPicture); + } optionList.Add (options); } @@ -318,8 +350,22 @@ public async Task DiscoverResourcesAsync() { var options = CreateOptions (resource); _serverSettingsViewModel.SetResourceUrl (options, resource.Model); + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(resource.SelectedFolder.EntryId) as Folder); if (resource.Model.ReadOnly) + { options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; + folder.Inner.Description = Strings.Get($"Read-only task list") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.TasklistReadOnly) as stdole.StdPicture); + } + else + { + options.SynchronizationMode = SynchronizationMode.MergeInBothDirections; + folder.Inner.Description = Strings.Get($"Read-write task list") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.TasklistReadWrite) as stdole.StdPicture); + } optionList.Add (options); } @@ -368,7 +414,7 @@ private OptionsModel CreateOptionsWithCategory (CalendarDataViewModel resource) return options; } - private void SelectFolder() +private void SelectFolder() { var folder = _optionTasks.PickFolderOrNull(); if (folder != null && folder.DefaultItemType == OlItemType.olAppointmentItem) @@ -441,4 +487,80 @@ public string Name public IViewOptions ViewOptions { get; } public OptionsModel Model => _prototypeModel; } + + // See https://msdn.microsoft.com/en-us/VBA/Outlook-VBA/articles/folder-setcustomicon-method-outlook + public static class PictureDispConverter + { + // IPictureDisp GUID. + public static Guid iPictureDispGuid = typeof(stdole.IPictureDisp).GUID; + + // Converts an icon into an IPictureDisp. + public static stdole.IPictureDisp ToIPictureDisp(Icon icon) + { + PICTDESC.Icon pictIcon = new PICTDESC.Icon(icon); + return PictureDispConverter.OleCreatePictureIndirect(pictIcon, ref iPictureDispGuid, true); + } + + // Converts an image into an IPictureDisp. + public static stdole.IPictureDisp ToIPictureDisp(Image image) + { + Bitmap bitmap = (image is Bitmap) ? (Bitmap)image : new Bitmap(image); + PICTDESC.Bitmap pictBit = new PICTDESC.Bitmap(bitmap); + return PictureDispConverter.OleCreatePictureIndirect(pictBit, ref iPictureDispGuid, true); + } + + [DllImport("OleAut32.dll", EntryPoint = "OleCreatePictureIndirect", ExactSpelling = true, + PreserveSig = false)] + private static extern stdole.IPictureDisp OleCreatePictureIndirect( + [MarshalAs(UnmanagedType.AsAny)] object picdesc, ref Guid iid, bool fOwn); + + private readonly static HandleCollector handleCollector = + new HandleCollector("Icon handles", 1000); + + // WINFORMS COMMENT: + // PICTDESC is a union in native, so we'll just + // define different ones for the different types + // the "unused" fields are there to make it the right + // size, since the struct in native is as big as the biggest + // union. + private static class PICTDESC + { + // Picture Types + public const short PICTYPE_UNINITIALIZED = -1; + public const short PICTYPE_NONE = 0; + public const short PICTYPE_BITMAP = 1; + public const short PICTYPE_METAFILE = 2; + public const short PICTYPE_ICON = 3; + public const short PICTYPE_ENHMETAFILE = 4; + + [StructLayout(LayoutKind.Sequential)] + public class Icon + { + internal int cbSizeOfStruct = Marshal.SizeOf(typeof(PICTDESC.Icon)); + internal int picType = PICTDESC.PICTYPE_ICON; + internal IntPtr hicon = IntPtr.Zero; + internal int unused1 = 0; + internal int unused2 = 0; + + internal Icon(System.Drawing.Icon icon) + { + this.hicon = icon.ToBitmap().GetHicon(); + } + } + + [StructLayout(LayoutKind.Sequential)] + public class Bitmap + { + internal int cbSizeOfStruct = Marshal.SizeOf(typeof(PICTDESC.Bitmap)); + internal int picType = PICTDESC.PICTYPE_BITMAP; + internal IntPtr hbitmap = IntPtr.Zero; + internal IntPtr hpal = IntPtr.Zero; + internal int unused = 0; + internal Bitmap(System.Drawing.Bitmap bitmap) + { + this.hbitmap = bitmap.GetHbitmap(); + } + } + } + } } \ No newline at end of file From 0f01e511f3c3bc489a375ed9db88f6cda24340d9 Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Thu, 7 Jun 2018 22:48:02 +0200 Subject: [PATCH 08/12] Sync CalDAV default folder with Outlook default folder - Detect CalDAV default calendar according to RFC 6638 (Scheduling Extensions) - We can't rename the Outlook default folder, but at least set the folder description - When deleting a CalDAV default resource that is synced to the Outlook default folder, move alle entries to al "... Deleted" folder and move that folder to the trash folder. --- CalDavSynchronizer/ComponentContainer.cs | 49 ++++++++++++++----- .../DataAccess/CalDavDataAccess.cs | 10 +++- CalDavSynchronizer/DataAccess/CalendarData.cs | 4 +- .../KolabMultipleOptionsTemplateViewModel.cs | 45 +++++++++++------ 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index 80b2b0a1..c34add70 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -295,6 +295,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO serverResources.TaskLists.Select(d => d.Id)).ToArray(); var remainingOptions = new List(); var markDeleted = " - " + Strings.Localize("Deleted") + " " + DateTime.Now.ToString(); + var defaultCalendarFolder = new GenericComObjectWrapper(Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar) as Folder); foreach (var option in newOptions) { if (option.ProfileTypeOrNull != profileType || allUris.Contains(option.CalenderUrl)) @@ -303,7 +304,23 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO { GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); - folder.Inner.Name += markDeleted; + if (folder.Inner.EntryID == defaultCalendarFolder.Inner.EntryID) + { + // As we can't rename the default folder, create a new folder and move all appointment entries + var deletedCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders.Add(folder.Inner.Name + markDeleted, OlDefaultFolders.olFolderCalendar) as Folder); + deletedCalendarFolder.Inner.Name = folder.Inner.Name + markDeleted; + deletedCalendarFolder.Inner.Description = folder.Inner.Description; + folder.Inner.Description = ""; + foreach (var item in folder.Inner.Items) + if (item is AppointmentItem) + (item as AppointmentItem).Move(deletedCalendarFolder.Inner); + deletedCalendarFolder.Inner.Delete(); + } + else + { + folder.Inner.Name += markDeleted; + folder.Inner.Delete(); + } } } newOptions = remainingOptions.ToArray(); @@ -318,16 +335,26 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO if (resource.ReadOnly) { option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; - folder.Inner.Description = Strings.Get($"Read-only calendar") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); - folder.Inner.SetCustomIcon( - PictureDispConverter.ToIPictureDisp(Resources.CalendarReadOnly) as stdole.StdPicture); + folder.Inner.Description = Strings.Get($"Read-only calendar") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + try + { + // Setting the icon might fail if we sync with the default Outlook calendar folder + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadOnly) as stdole.StdPicture); + } + catch { } } else { option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; - folder.Inner.Description = Strings.Get($"Read-write calendar") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); - folder.Inner.SetCustomIcon( - PictureDispConverter.ToIPictureDisp(Resources.CalendarReadWrite) as stdole.StdPicture); + folder.Inner.Description = Strings.Get($"Read-write calendar") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + try + { + // Setting the icon might fail if we sync with the default Outlook calendar folder + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadWrite) as stdole.StdPicture); + } + catch { } } } } @@ -342,14 +369,14 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO if (resource.ReadOnly) { option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; - folder.Inner.Description = Strings.Get($"Read-only address book") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.Description = Strings.Get($"Read-only address book") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadOnly) as stdole.StdPicture); } else { option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; - folder.Inner.Description = Strings.Get($"Read-write address book") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.Description = Strings.Get($"Read-write address book") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadWrite) as stdole.StdPicture); } @@ -366,14 +393,14 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO if (resource.ReadOnly) { option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; - folder.Inner.Description = Strings.Get($"Read-only task list") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.Description = Strings.Get($"Read-only task list") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.TasklistReadOnly) as stdole.StdPicture); } else { option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; - folder.Inner.Description = Strings.Get($"Read-write task list") + $" ({option.ProfileTypeOrNull}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.Description = Strings.Get($"Read-write task list") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.TasklistReadWrite) as stdole.StdPicture); } diff --git a/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs b/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs index f4fc5978..079eae0c 100644 --- a/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs +++ b/CalDavSynchronizer/DataAccess/CalDavDataAccess.cs @@ -150,6 +150,11 @@ public async Task GetUserResourcesNoThrow (bool useWellKnownUrl XmlNode homeSetNode = calendarHomeSetProperties.XmlDocument.SelectSingleNode ("/D:multistatus/D:response/D:propstat/D:prop/C:calendar-home-set", calendarHomeSetProperties.XmlNamespaceManager); if (homeSetNode != null && homeSetNode.HasChildNodes) { + // https://tools.ietf.org/html/rfc6638#section-9.2 + XmlNode scheduleDefaultCalendarNode = calendarHomeSetProperties.XmlDocument.SelectSingleNode("/D:multistatus/D:response/D:propstat/D:prop/C:schedule-default-calendar-URL", calendarHomeSetProperties.XmlNamespaceManager); + String scheduleDefaultCalendarPath = scheduleDefaultCalendarNode == null ? "" : + scheduleDefaultCalendarNode.InnerText.EndsWith("/") ? scheduleDefaultCalendarNode.InnerText : scheduleDefaultCalendarNode.InnerText + "/"; + bool isFirstCalendar = true; foreach (XmlNode homeSetNodeHref in homeSetNode.ChildNodes) { if (!string.IsNullOrEmpty (homeSetNodeHref.InnerText)) @@ -187,8 +192,10 @@ public async Task GetUserResourcesNoThrow (bool useWellKnownUrl if (supportedComponentsNode.InnerXml.Contains ("VEVENT")) { + bool isDefault = (scheduleDefaultCalendarNode != null && scheduleDefaultCalendarPath == path || scheduleDefaultCalendarNode == null && isFirstCalendar); + isFirstCalendar = false; var displayName = string.IsNullOrEmpty (displayNameNode.InnerText) ? "Default Calendar" : displayNameNode.InnerText; - calendars.Add (new CalendarData (uri, displayName, calendarColor, ro)); + calendars.Add (new CalendarData (uri, displayName, calendarColor, ro, isDefault)); } if (supportedComponentsNode.InnerXml.Contains ("VTODO")) { @@ -235,6 +242,7 @@ private Task GetCalendarHomeSet (Uri url) + " diff --git a/CalDavSynchronizer/DataAccess/CalendarData.cs b/CalDavSynchronizer/DataAccess/CalendarData.cs index 745f4954..2dc6562f 100644 --- a/CalDavSynchronizer/DataAccess/CalendarData.cs +++ b/CalDavSynchronizer/DataAccess/CalendarData.cs @@ -26,13 +26,15 @@ public class CalendarData public string Name { get; } public ArgbColor? Color { get; } public bool ReadOnly { get; } + public bool IsDefault { get; } - public CalendarData (Uri uri, string name, ArgbColor? color, bool readOnly = false) + public CalendarData (Uri uri, string name, ArgbColor? color, bool readOnly = false, bool isDefault = false) { Uri = uri; Name = name; Color = color; ReadOnly = readOnly; + IsDefault = isDefault; } } } \ No newline at end of file diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index 25470775..57da8550 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -193,11 +193,11 @@ public async Task DiscoverResourcesAsync() var calendars = serverResources.Calendars.Select (c => new CalendarDataViewModel (c)).ToArray(); var addressBooks = serverResources.AddressBooks.Select (a => new AddressBookDataViewModel (a)).ToArray(); var taskLists = serverResources.TaskLists.Select (d => new TaskListDataViewModel (d)).ToArray(); + var existingOptions = (_parent as OptionsCollectionViewModel).Options; if (OnlyAddNewUrls) { // Exclude all resourcres that have already been configured - var options = (_parent as OptionsCollectionViewModel).Options; - var configuredUrls = new HashSet (options.Select(o => o.Model.CalenderUrl)); + var configuredUrls = new HashSet (existingOptions.Select(o => o.Model.CalenderUrl)); calendars = calendars.Where(c => !configuredUrls.Contains(c.Uri.ToString())).ToArray(); addressBooks = addressBooks.Where(c => !configuredUrls.Contains(c.Uri.ToString())).ToArray(); taskLists = taskLists.Where(c => !configuredUrls.Contains(c.Id)).ToArray(); @@ -209,15 +209,22 @@ public async Task DiscoverResourcesAsync() // https://msdn.microsoft.com/en-us/library/office/ff184655.aspx // Get Outlook's default calendar folder (this is where we create the Kolab folders) GenericComObjectWrapper defaultCalendarFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar) as Folder); + // Get sync option that syncs the default calendar (if any) + bool defaultFolderIsSynced = existingOptions.Any(o => o.Model.SelectedFolderOrNull?.EntryId == defaultCalendarFolder.Inner.EntryID); // Find all Kolab calendars that are not yet synced to an outlook folder foreach (var resource in calendars.Where(c => c.SelectedFolder == null)) { string newCalendarName = resource.Name + " (" + Name + ")"; - GenericComObjectWrapper newCalendarFolder = null; + GenericComObjectWrapper newCalendarFolder; try { + // Sync CalDAV default calendar with Outlook default calendar folder. + // Only do so if there are no sync settings yet for the Outlook default calendar + if (resource.Model.IsDefault && !defaultFolderIsSynced) + newCalendarFolder = defaultCalendarFolder; // Use existing folder if it does exist - newCalendarFolder = new GenericComObjectWrapper (defaultCalendarFolder.Inner.Folders[newCalendarName] as Folder); + else + newCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders[newCalendarName] as Folder); } catch { @@ -309,16 +316,26 @@ public async Task DiscoverResourcesAsync() if (resource.Model.ReadOnly) { options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; - folder.Inner.Description = Strings.Get($"Read-only calendar") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); - folder.Inner.SetCustomIcon( - PictureDispConverter.ToIPictureDisp(Resources.CalendarReadOnly) as stdole.StdPicture); + folder.Inner.Description = Strings.Get($"Read-only calendar") + $" »{options.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + try + { + // Setting the icon might fail if we sync with the default Outlook calendar folder + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadOnly) as stdole.StdPicture); + } + catch { } } else { options.SynchronizationMode = SynchronizationMode.MergeInBothDirections; - folder.Inner.Description = Strings.Get($"Read-write calendar") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); - folder.Inner.SetCustomIcon( - PictureDispConverter.ToIPictureDisp(Resources.CalendarReadWrite) as stdole.StdPicture); + folder.Inner.Description = Strings.Get($"Read-write calendar") + $" »{options.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + try + { + // Setting the icon might fail if we sync with the default Outlook calendar folder + folder.Inner.SetCustomIcon( + PictureDispConverter.ToIPictureDisp(Resources.CalendarReadWrite) as stdole.StdPicture); + } + catch { } } optionList.Add (options); } @@ -332,14 +349,14 @@ public async Task DiscoverResourcesAsync() if (resource.Model.ReadOnly) { options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; - folder.Inner.Description = Strings.Get($"Read-only address book") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.Description = Strings.Get($"Read-only address book") + $" »{options.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadOnly) as stdole.StdPicture); } else { options.SynchronizationMode = SynchronizationMode.MergeInBothDirections; - folder.Inner.Description = Strings.Get($"Read-write address book") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.Description = Strings.Get($"Read-write address book") + $" »{options.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.AddressbookReadWrite) as stdole.StdPicture); } @@ -355,14 +372,14 @@ public async Task DiscoverResourcesAsync() if (resource.Model.ReadOnly) { options.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; - folder.Inner.Description = Strings.Get($"Read-only task list") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); + folder.Inner.Description = Strings.Get($"Read-only task list") + $" »{options.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.TasklistReadOnly) as stdole.StdPicture); } else { options.SynchronizationMode = SynchronizationMode.MergeInBothDirections; - folder.Inner.Description = Strings.Get($"Read-write task list") + $" ({options.ModelFactory.ProfileType.Name}): " + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); + folder.Inner.Description = Strings.Get($"Read-write task list") + $" »{options.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); folder.Inner.SetCustomIcon( PictureDispConverter.ToIPictureDisp(Resources.TasklistReadWrite) as stdole.StdPicture); } From 15c5910aedecf9e979b9dcb52631983155076d0b Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Fri, 8 Jun 2018 09:36:42 +0200 Subject: [PATCH 09/12] Change mapping of default folder - If there is a CalDAV default calender and the outlook default calendar is not synced, and the CaldDAV default folder used to be synced to an Outlook subfolder: change the sync setting to sync both default folders. - Let the initial sync process start after autoconfigure --- CalDavSynchronizer/ComponentContainer.cs | 41 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index c34add70..63d4e38f 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -219,6 +219,14 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge _trayNotifier = generalOptions.EnableTrayIcon ? new TrayNotifier (this) : NullTrayNotifer.Instance; + // Set the registry key "HKEY_CURRENT_USER\Software\CalDavSynchronizer\AutoconfigureKolab" + // to "1" to enable the Kolab autoconfigure feature. This setting is not available + // through the general options dialog, as it is not so general after all... + if (generalOptions.AutoconfigureKolab) + { + AutoconfigureKolab(options, generalOptions); + } + try { using (var syncObjects = GenericComObjectWrapper.Create (_session.SyncObjects)) @@ -236,14 +244,6 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge s_logger.Error ("Can't access SyncObjects", ex); } - // Set the registry key "HKEY_CURRENT_USER\Software\CalDavSynchronizer\AutoconfigureKolab" - // to "1" to enable the Kolab autoconfigure feature. This setting is not available - // through the general options dialog, as it is not so general after all... - if (generalOptions.AutoconfigureKolab) - { - AutoconfigureKolab(options, generalOptions); - } - _oneTimeTaskRunner = new OneTimeTaskRunner(_outlookSession); } @@ -324,6 +324,31 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } } newOptions = remainingOptions.ToArray(); + + // If one of the remaining CaldDAV calendars is the default calendar + // and the Outlook default calendar is not currently synced: + // - Remove existing Outlook calendar subfolder + // - Sync Outlook default calendar + var defaultCalendarResource = serverResources.Calendars.FirstOrDefault(c => c.IsDefault); + var defaultCalendarFolderIsMapped = newOptions.Any(o => o.OutlookFolderEntryId == defaultCalendarFolder.Inner.EntryID); + if (defaultCalendarResource != null && !defaultCalendarFolderIsMapped) + { + var defaultCalendarMapping = newOptions.FirstOrDefault(m => m.CalenderUrl == defaultCalendarResource.Uri.ToString()); + if (defaultCalendarMapping?.OutlookFolderEntryId != defaultCalendarFolder.Inner.EntryID) + { + // Delete existing folder + GenericComObjectWrapper folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(defaultCalendarMapping.OutlookFolderEntryId) as Folder); + folder.Inner.Name += markDeleted; + try { + // Deleting a folder that has just been created might fail + folder.Inner.Delete(); + } catch { } + // Map to Outlook default folder + defaultCalendarMapping.OutlookFolderEntryId = defaultCalendarFolder.Inner.EntryID; + defaultCalendarMapping.OutlookFolderStoreId = defaultCalendarFolder.Inner.StoreID; + } + } } // Update existing Kolab Calendar resources From c47814abef1018f8d571f4610aaab3f1730014c4 Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Thu, 14 Jun 2018 12:01:23 +0200 Subject: [PATCH 10/12] Improve Autoconfigure - Only try autoconfigure if we have an IMAP account - In Autoconfigure mode, remove alls sync profiles without an outlook folder - Catch more errors and improve logging, use GenericComObjectWrapper - Don't sync reminders on shared and read-only resources. Q&D implementation: detect shared calendars by name (starts with "shared" or opening parentheses) --- CalDavSynchronizer/ComponentContainer.cs | 92 +++++++++++++++---- .../Properties/Resources.Designer.cs | 9 ++ .../KolabMultipleOptionsTemplateViewModel.cs | 14 +++ .../ServerSettingsTemplateViewModel.cs | 2 + 4 files changed, 100 insertions(+), 17 deletions(-) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index 63d4e38f..979ab698 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -219,14 +219,6 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge _trayNotifier = generalOptions.EnableTrayIcon ? new TrayNotifier (this) : NullTrayNotifer.Instance; - // Set the registry key "HKEY_CURRENT_USER\Software\CalDavSynchronizer\AutoconfigureKolab" - // to "1" to enable the Kolab autoconfigure feature. This setting is not available - // through the general options dialog, as it is not so general after all... - if (generalOptions.AutoconfigureKolab) - { - AutoconfigureKolab(options, generalOptions); - } - try { using (var syncObjects = GenericComObjectWrapper.Create (_session.SyncObjects)) @@ -244,15 +236,40 @@ public ComponentContainer (Application application, IGeneralOptionsDataAccess ge s_logger.Error ("Can't access SyncObjects", ex); } + // Set the registry key "HKEY_CURRENT_USER\Software\CalDavSynchronizer\AutoconfigureKolab" + // to "1" to enable the Kolab autoconfigure feature. This setting is not available + // through the general options dialog, as it is not so general after all... + bool haveImap = false; + foreach (var innerAccount in _session.Accounts) + using (var account = GenericComObjectWrapper.Create(innerAccount)) + if ((account.Inner as Account)?.AccountType == OlAccountType.olImap) + haveImap = true; + + if (haveImap && generalOptions.AutoconfigureKolab) + { + try + { + AutoconfigureKolab(options, generalOptions); + } + catch (COMException ex) + { + s_logger.Error("Error during autoconfigure", ex); + MessageBox.Show(Strings.Get($"Error during autoconfigure") + ":\n".ToString() + ex.ToString(), MessageBoxTitle); + } + } + _oneTimeTaskRunner = new OneTimeTaskRunner(_outlookSession); } private async void AutoconfigureKolab(Options[] options, GeneralOptions generalOptions) { + s_logger.Info("Starting Kolab autoconfigure"); + string profileType = "Kolab"; // Make sure the add-on language is active in our context Thread.CurrentThread.CurrentUICulture = new CultureInfo(GeneralOptionsDataAccess.CultureName); + // Remove all Kolab options without Outlook folder, so we can recreate them + options = options.Where(o => o.ProfileTypeOrNull != profileType || o.OutlookFolderEntryId != null).ToArray(); // Create all objects required to use the options collection models - string profileType = "Kolab"; string[] categories; using (var categoriesWrapper = GenericComObjectWrapper.Create(_session.Categories)) categories = categoriesWrapper.Inner.ToSafeEnumerable().Select(c => c.Name).ToArray(); @@ -282,6 +299,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO kolabOptionsModel.OnlyAddNewUrls = true; kolabOptionsModel.AutoConfigure = true; kolabOptionsModel.GetAccountSettingsCommand.Execute(null); + s_logger.Debug("Discover CalDAV/CardDAV resources"); var serverResources = await kolabOptionsModel.DiscoverResourcesAsync(); var newOptions = optionsCollectionModel.GetOptionsCollection(); @@ -289,6 +307,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO // Do this only if we really have a response from the server. if (serverResources.ContainsResources) { + s_logger.Debug("Removing all resources that are no longer available"); var allUris = serverResources.Calendars.Select(c => c.Uri.ToString()).Concat( serverResources.AddressBooks.Select(a => a.Uri.ToString())).Concat( @@ -302,18 +321,20 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO remainingOptions.Add(option); else { + s_logger.Info($"Removing Resource '{option.Name}'"); GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); if (folder.Inner.EntryID == defaultCalendarFolder.Inner.EntryID) { + s_logger.Info($"We can't delete '{option.Name}', so move all entries to a new folder and mark this folder als deleted."); // As we can't rename the default folder, create a new folder and move all appointment entries var deletedCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders.Add(folder.Inner.Name + markDeleted, OlDefaultFolders.olFolderCalendar) as Folder); deletedCalendarFolder.Inner.Name = folder.Inner.Name + markDeleted; deletedCalendarFolder.Inner.Description = folder.Inner.Description; folder.Inner.Description = ""; - foreach (var item in folder.Inner.Items) - if (item is AppointmentItem) - (item as AppointmentItem).Move(deletedCalendarFolder.Inner); + foreach (var innerItem in folder.Inner.Items) + using (var item = GenericComObjectWrapper.Create(innerItem)) + (item as AppointmentItem)?.Move(deletedCalendarFolder.Inner); deletedCalendarFolder.Inner.Delete(); } else @@ -331,20 +352,27 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO // - Sync Outlook default calendar var defaultCalendarResource = serverResources.Calendars.FirstOrDefault(c => c.IsDefault); var defaultCalendarFolderIsMapped = newOptions.Any(o => o.OutlookFolderEntryId == defaultCalendarFolder.Inner.EntryID); + if (defaultCalendarFolderIsMapped) { s_logger.Debug($"Found existing mapping for default folder."); } if (defaultCalendarResource != null && !defaultCalendarFolderIsMapped) { + s_logger.Info($"New Default CalDAV folder '{defaultCalendarResource.Name}' detected."); var defaultCalendarMapping = newOptions.FirstOrDefault(m => m.CalenderUrl == defaultCalendarResource.Uri.ToString()); if (defaultCalendarMapping?.OutlookFolderEntryId != defaultCalendarFolder.Inner.EntryID) { // Delete existing folder GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(defaultCalendarMapping.OutlookFolderEntryId) as Folder); + s_logger.Info($"Delete existing folder '{folder.Inner.Name}' for Mapping '{defaultCalendarMapping.Name}'"); folder.Inner.Name += markDeleted; try { // Deleting a folder that has just been created might fail folder.Inner.Delete(); - } catch { } + } + catch { + s_logger.Info($"Could not move folder '{folder.Inner.Name}' to trash"); + } // Map to Outlook default folder + s_logger.Debug($"Route mapping '{defaultCalendarMapping.Name}' to folder '{defaultCalendarFolder.Inner.Name}'"); defaultCalendarMapping.OutlookFolderEntryId = defaultCalendarFolder.Inner.EntryID; defaultCalendarMapping.OutlookFolderStoreId = defaultCalendarFolder.Inner.StoreID; } @@ -354,11 +382,13 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO // Update existing Kolab Calendar resources foreach (var resource in serverResources.Calendars) { - foreach (var option in newOptions.Where(o => o.CalenderUrl == resource.Uri.ToString())) { + s_logger.Debug($"Update calendar '{resource.Name}'"); + foreach (var option in newOptions.Where(o => o.OutlookFolderEntryId != null && o.CalenderUrl == resource.Uri.ToString())) { GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); if (resource.ReadOnly) { + s_logger.Debug($"Calendar '{resource.Name}' is read-only"); option.SynchronizationMode = SynchronizationMode.ReplicateServerIntoOutlook; folder.Inner.Description = Strings.Get($"Read-only calendar") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook are discarded and replaced by data from the server."); try @@ -371,6 +401,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } else { + s_logger.Debug($"Calendar '{resource.Name}' is read-write"); option.SynchronizationMode = SynchronizationMode.MergeInBothDirections; folder.Inner.Description = Strings.Get($"Read-write calendar") + $" »{option.Name}«:\n" + Strings.Get($"local changes made in Outlook and remote changes from the server are merged."); try @@ -381,13 +412,37 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } catch { } } + // Quick and Dirty: On shared and read-only calendars, don't sync reminders + if (resource.ReadOnly || resource.Name.StartsWith("(") || resource.Name.StartsWith("shared » ")) + { + s_logger.Debug($"Calendar '{resource.Name}' is read-only or shared"); + var eventMappingOptions = (EventMappingConfiguration)option.MappingConfiguration; + if (eventMappingOptions.MapReminder != ReminderMapping.@false) + { + eventMappingOptions.MapReminder = ReminderMapping.@false; + // Remove local reminders. + foreach (var innerItem in folder.Inner.Items) + { + using (var item = GenericComObjectWrapper.Create(innerItem)) + { + if (item.Inner is AppointmentItem) + { + (item.Inner as AppointmentItem).ReminderSet = false; + (item.Inner as AppointmentItem).ReminderMinutesBeforeStart = 0; + (item.Inner as AppointmentItem).Save(); + } + } + } + } + } } } // Update existing Kolab Address book resources foreach (var resource in serverResources.AddressBooks) { - foreach (var option in newOptions.Where(o => o.CalenderUrl == resource.Uri.ToString())) + s_logger.Debug($"Update address book '{resource.Name}'"); + foreach (var option in newOptions.Where(o => o.OutlookFolderEntryId != null && o.CalenderUrl == resource.Uri.ToString())) { GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); @@ -411,7 +466,8 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO // Update existing Kolab Task list resources foreach (var resource in serverResources.TaskLists) { - foreach (var option in newOptions.Where(o => o.CalenderUrl == resource.Id)) + s_logger.Debug($"Update task list '{resource.Name}'"); + foreach (var option in newOptions.Where(o => o.OutlookFolderEntryId != null && o.CalenderUrl == resource.Id)) { GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); @@ -432,7 +488,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } } - // Set free/busy URL in Registry + // Set free/busy URL in Registry if not yet set string regPath = @"Software\Microsoft\Office\" + Globals.ThisAddIn.Application.Version.Split(new char[] { '.' })[0] + @".0" + @"\Outlook\\Options\Calendar\Internet Free/Busy"; @@ -442,6 +498,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO var value = key.GetValue("Read URL"); if (value == null || string.IsNullOrEmpty(value.ToString())) { + s_logger.Info("Set free/busy URL"); ServerSettingsTemplateViewModel server = kolabOptionsModel.ServerSettingsViewModel as ServerSettingsTemplateViewModel; if (!string.IsNullOrEmpty(server.CalenderUrl)) { @@ -451,6 +508,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } // Save new settings + s_logger.Debug("Save autoconfigured settings"); await ApplyNewOptions(options, newOptions, generalOptions, optionsCollectionModel.GetOneTimeTasks()); } diff --git a/CalDavSynchronizer/Properties/Resources.Designer.cs b/CalDavSynchronizer/Properties/Resources.Designer.cs index 24733bb3..990e58b3 100644 --- a/CalDavSynchronizer/Properties/Resources.Designer.cs +++ b/CalDavSynchronizer/Properties/Resources.Designer.cs @@ -130,6 +130,15 @@ internal static System.Drawing.Icon CalendarReadWrite { } } + /// + /// Looks up a localized string similar to 26. + /// + internal static string CbPatchlevel { + get { + return ResourceManager.GetString("CbPatchlevel", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index 57da8550..e494f008 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -188,6 +188,7 @@ public async Task DiscoverResourcesAsync() ServerResources serverResources = new ServerResources(); try { + s_logger.Debug("Get CalDAV/CardDAV data from Server"); serverResources = await _serverSettingsViewModel.GetServerResources (); var calendars = serverResources.Calendars.Select (c => new CalendarDataViewModel (c)).ToArray(); @@ -196,6 +197,7 @@ public async Task DiscoverResourcesAsync() var existingOptions = (_parent as OptionsCollectionViewModel).Options; if (OnlyAddNewUrls) { + s_logger.Debug("Exclude all server resources that have already been configured"); // Exclude all resourcres that have already been configured var configuredUrls = new HashSet (existingOptions.Select(o => o.Model.CalenderUrl)); calendars = calendars.Where(c => !configuredUrls.Contains(c.Uri.ToString())).ToArray(); @@ -205,6 +207,7 @@ public async Task DiscoverResourcesAsync() // --- Create folders if requested and required if (AutoCreateOutlookFolders) { + s_logger.Debug("Auto-create outlook folders"); // https://docs.microsoft.com/en-us/visualstudio/vsto/how-to-programmatically-create-a-custom-calendar // https://msdn.microsoft.com/en-us/library/office/ff184655.aspx // Get Outlook's default calendar folder (this is where we create the Kolab folders) @@ -214,6 +217,7 @@ public async Task DiscoverResourcesAsync() // Find all Kolab calendars that are not yet synced to an outlook folder foreach (var resource in calendars.Where(c => c.SelectedFolder == null)) { + s_logger.Debug($"Find folder for calendar '{Name}'"); string newCalendarName = resource.Name + " (" + Name + ")"; GenericComObjectWrapper newCalendarFolder; try @@ -221,13 +225,20 @@ public async Task DiscoverResourcesAsync() // Sync CalDAV default calendar with Outlook default calendar folder. // Only do so if there are no sync settings yet for the Outlook default calendar if (resource.Model.IsDefault && !defaultFolderIsSynced) + { + s_logger.Debug($"Sync Calendar '{Name}' with default outlook calendar"); newCalendarFolder = defaultCalendarFolder; + } // Use existing folder if it does exist else + { + s_logger.Debug($"Try to use an existing folder for calendar '{Name}'"); newCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders[newCalendarName] as Folder); + } } catch { + s_logger.Debug($"Create new folder for calendar '{Name}'"); // Create missing folder newCalendarFolder = new GenericComObjectWrapper (defaultCalendarFolder.Inner.Folders.Add(newCalendarName, OlDefaultFolders.olFolderCalendar) as Folder); // Make sure it has not been renamed to "name (this computer only)" @@ -241,6 +252,7 @@ public async Task DiscoverResourcesAsync() GenericComObjectWrapper defaultAddressBookFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderContacts) as Folder); foreach (var resource in addressBooks.Where(c => c.SelectedFolder == null)) { + s_logger.Debug($"Find folder for address book '{Name}'"); string newAddressBookName = resource.Name + " (" + Name + ")"; GenericComObjectWrapper newAddressBookFolder = null; try @@ -286,6 +298,7 @@ public async Task DiscoverResourcesAsync() GenericComObjectWrapper defaultTaskListsFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderTasks) as Folder); foreach (var resource in taskLists.Where(c => c.SelectedFolder == null)) { + s_logger.Debug($"Find folder for task list '{Name}'"); string newTaskListName = resource.Name + " (" + Name + ")"; GenericComObjectWrapper newTaskListFolder = null; try @@ -303,6 +316,7 @@ public async Task DiscoverResourcesAsync() using (var selectResourcesForm = SelectResourceForm.CreateForFolderAssignment(_optionTasks, ConnectionTests.ResourceType.Calendar, calendars, addressBooks, taskLists)) { // Create and add new sync profiles + s_logger.Debug("Create and add all new sync profiles"); if (AutoConfigure || selectResourcesForm.ShowDialog() == System.Windows.Forms.DialogResult.OK) { var optionList = new List(); diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerSettingsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerSettingsTemplateViewModel.cs index 926de740..72bd5f72 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerSettingsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/ServerSettingsTemplateViewModel.cs @@ -106,6 +106,7 @@ public async Task GetServerResources () if (string.IsNullOrEmpty (CalenderUrl) && !string.IsNullOrEmpty (EmailAddress)) { + s_logger.Debug("Auto-detect CaldAV/CardDAV URL"); bool success; caldavUrlString = OptionTasks.DoSrvLookup (EmailAddress, OlItemType.olAppointmentItem, out success); carddavUrlString = OptionTasks.DoSrvLookup (EmailAddress, OlItemType.olContactItem, out success); @@ -127,6 +128,7 @@ public async Task GetServerResources () var calDavDataAccess = new CalDavDataAccess (caldavUrl, webDavClientCaldav); var cardDavDataAccess = new CardDavDataAccess (carddavUrl, webDavClientCarddav, string.Empty, contentType => true); + s_logger.Debug("Get User's resources"); return await GetUserResources (calDavDataAccess, cardDavDataAccess); } From 360b0c0dfd40c4941a71f51127edecdd3725bca1 Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Fri, 15 Jun 2018 12:03:25 +0200 Subject: [PATCH 11/12] Fix crash - Don't try to iterate over non-existing server resource lists --- CalDavSynchronizer/ComponentContainer.cs | 6 +++--- .../ViewModels/KolabMultipleOptionsTemplateViewModel.cs | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index 979ab698..8f1f0f2b 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -380,7 +380,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } // Update existing Kolab Calendar resources - foreach (var resource in serverResources.Calendars) + foreach (var resource in serverResources.Calendars ?? Enumerable.Empty()) { s_logger.Debug($"Update calendar '{resource.Name}'"); foreach (var option in newOptions.Where(o => o.OutlookFolderEntryId != null && o.CalenderUrl == resource.Uri.ToString())) { @@ -439,7 +439,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } // Update existing Kolab Address book resources - foreach (var resource in serverResources.AddressBooks) + foreach (var resource in serverResources.AddressBooks ?? Enumerable.Empty()) { s_logger.Debug($"Update address book '{resource.Name}'"); foreach (var option in newOptions.Where(o => o.OutlookFolderEntryId != null && o.CalenderUrl == resource.Uri.ToString())) @@ -464,7 +464,7 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO } // Update existing Kolab Task list resources - foreach (var resource in serverResources.TaskLists) + foreach (var resource in serverResources.TaskLists ?? Enumerable.Empty()) { s_logger.Debug($"Update task list '{resource.Name}'"); foreach (var option in newOptions.Where(o => o.OutlookFolderEntryId != null && o.CalenderUrl == resource.Id)) diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index e494f008..170bcfaa 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -190,6 +190,8 @@ public async Task DiscoverResourcesAsync() { s_logger.Debug("Get CalDAV/CardDAV data from Server"); serverResources = await _serverSettingsViewModel.GetServerResources (); + if (!serverResources.ContainsResources) + return serverResources; var calendars = serverResources.Calendars.Select (c => new CalendarDataViewModel (c)).ToArray(); var addressBooks = serverResources.AddressBooks.Select (a => new AddressBookDataViewModel (a)).ToArray(); From 430f5b47e21ec184146f550962557006d1056ebf Mon Sep 17 00:00:00 2001 From: Achim Leitner Date: Fri, 15 Jun 2018 12:15:46 +0200 Subject: [PATCH 12/12] Improve stability - Gracefully handle 'unable to create folder' situations - Add fallback for situations where outlook does not allow names with dots - Don't show dialog boxes on autoconfigure as this delays add-in initialization - Improve debug / info / error log - Remove dead code --- CalDavSynchronizer/ComponentContainer.cs | 97 +++++++--- .../Properties/Resources.Designer.cs | 9 - .../KolabMultipleOptionsTemplateViewModel.cs | 172 +++++++++++------- 3 files changed, 178 insertions(+), 100 deletions(-) diff --git a/CalDavSynchronizer/ComponentContainer.cs b/CalDavSynchronizer/ComponentContainer.cs index 8f1f0f2b..d57c8f63 100644 --- a/CalDavSynchronizer/ComponentContainer.cs +++ b/CalDavSynchronizer/ComponentContainer.cs @@ -307,40 +307,90 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO // Do this only if we really have a response from the server. if (serverResources.ContainsResources) { - s_logger.Debug("Removing all resources that are no longer available"); + s_logger.Debug("Remove all sync profiles and outlook folders whose CalDAV resources are no longer available"); var allUris = serverResources.Calendars.Select(c => c.Uri.ToString()).Concat( serverResources.AddressBooks.Select(a => a.Uri.ToString())).Concat( serverResources.TaskLists.Select(d => d.Id)).ToArray(); var remainingOptions = new List(); var markDeleted = " - " + Strings.Localize("Deleted") + " " + DateTime.Now.ToString(); + var markDeletedSafe = markDeleted.Replace('.', '_'); var defaultCalendarFolder = new GenericComObjectWrapper(Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar) as Folder); foreach (var option in newOptions) { if (option.ProfileTypeOrNull != profileType || allUris.Contains(option.CalenderUrl)) + { + s_logger.Debug($"Keep sync profile '{option.Name}'"); remainingOptions.Add(option); + } + else if (option.OutlookFolderEntryId == null) + { + s_logger.Info($"Remove stale Kolab sync profile '{option.Name}'"); + } else { - s_logger.Info($"Removing Resource '{option.Name}'"); - GenericComObjectWrapper folder = new GenericComObjectWrapper( - Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); - if (folder.Inner.EntryID == defaultCalendarFolder.Inner.EntryID) + s_logger.Info($"Remove stale Kolab sync profile '{option.Name}' and delete synced outlook folder"); + GenericComObjectWrapper folder = null; + try { - s_logger.Info($"We can't delete '{option.Name}', so move all entries to a new folder and mark this folder als deleted."); - // As we can't rename the default folder, create a new folder and move all appointment entries - var deletedCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders.Add(folder.Inner.Name + markDeleted, OlDefaultFolders.olFolderCalendar) as Folder); - deletedCalendarFolder.Inner.Name = folder.Inner.Name + markDeleted; - deletedCalendarFolder.Inner.Description = folder.Inner.Description; - folder.Inner.Description = ""; - foreach (var innerItem in folder.Inner.Items) - using (var item = GenericComObjectWrapper.Create(innerItem)) - (item as AppointmentItem)?.Move(deletedCalendarFolder.Inner); - deletedCalendarFolder.Inner.Delete(); + folder = new GenericComObjectWrapper( + Globals.ThisAddIn.Application.Session.GetFolderFromID(option.OutlookFolderEntryId) as Folder); } - else + catch (Exception ex) { - folder.Inner.Name += markDeleted; - folder.Inner.Delete(); + s_logger.Error($"Could not find outlook folder ID '{option.OutlookFolderEntryId}' für sync profile '{option.Name}'", ex); + } + if (folder != null && folder.Inner.EntryID == defaultCalendarFolder.Inner.EntryID) + { + s_logger.Info($"We can't delete '{folder.Inner.Name}', so move all entries to a new folder and mark this folder als deleted."); + GenericComObjectWrapper deletedCalendarFolder = null; + foreach (String tempFolderName in new[] {folder.Inner.Name + markDeleted, folder.Inner.Name + markDeletedSafe}) + { + if (deletedCalendarFolder == null) + try + { + deletedCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders.Add(folder.Inner.Name + markDeleted, OlDefaultFolders.olFolderCalendar) as Folder); + deletedCalendarFolder.Inner.Name = folder.Inner.Name + markDeleted; + } + catch (Exception ex) + { + s_logger.Debug($"Could not create temporary folder '{tempFolderName}'", ex); + } + } + if (deletedCalendarFolder == null) + { + s_logger.Error($"Could not create temporary folder '{folder.Inner.Name + markDeleted}'"); + } + else + { + s_logger.Info($"Move all entries from outlook default calendar '{folder.Inner.Name}' to temporary folder '{deletedCalendarFolder.Inner.Name}'"); + try + { + deletedCalendarFolder.Inner.Description = folder.Inner.Description; + folder.Inner.Description = ""; + foreach (var innerItem in folder.Inner.Items) + using (var item = GenericComObjectWrapper.Create(innerItem)) + (item as AppointmentItem)?.Move(deletedCalendarFolder.Inner); + deletedCalendarFolder.Inner.Delete(); + } + catch (Exception ex) + { + s_logger.Error($"Failed to move all items from '{folder.Inner.Name}' to temporary folder '{deletedCalendarFolder.Inner.Name}'", ex); + } + } + } + else if (folder != null) + { + try { folder.Inner.Name += markDeleted; } + catch { try { folder.Inner.Name += markDeletedSafe; } catch { } } + try + { + folder.Inner.Delete(); + } + catch (Exception ex) + { + s_logger.Error($"Could not move '{folder.Inner.Name}' to Trash", ex); + } } } } @@ -363,13 +413,16 @@ private async void AutoconfigureKolab(Options[] options, GeneralOptions generalO GenericComObjectWrapper folder = new GenericComObjectWrapper( Globals.ThisAddIn.Application.Session.GetFolderFromID(defaultCalendarMapping.OutlookFolderEntryId) as Folder); s_logger.Info($"Delete existing folder '{folder.Inner.Name}' for Mapping '{defaultCalendarMapping.Name}'"); - folder.Inner.Name += markDeleted; - try { + try { folder.Inner.Name += markDeleted; } + catch { try { folder.Inner.Name += markDeletedSafe; } catch { } } + try + { // Deleting a folder that has just been created might fail folder.Inner.Delete(); } - catch { - s_logger.Info($"Could not move folder '{folder.Inner.Name}' to trash"); + catch (Exception ex) + { + s_logger.Error($"Could not move folder '{folder.Inner.Name}' to trash", ex); } // Map to Outlook default folder s_logger.Debug($"Route mapping '{defaultCalendarMapping.Name}' to folder '{defaultCalendarFolder.Inner.Name}'"); diff --git a/CalDavSynchronizer/Properties/Resources.Designer.cs b/CalDavSynchronizer/Properties/Resources.Designer.cs index 990e58b3..24733bb3 100644 --- a/CalDavSynchronizer/Properties/Resources.Designer.cs +++ b/CalDavSynchronizer/Properties/Resources.Designer.cs @@ -130,15 +130,6 @@ internal static System.Drawing.Icon CalendarReadWrite { } } - /// - /// Looks up a localized string similar to 26. - /// - internal static string CbPatchlevel { - get { - return ResourceManager.GetString("CbPatchlevel", resourceCulture); - } - } - /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs index 170bcfaa..4fa26ad6 100644 --- a/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs +++ b/CalDavSynchronizer/Ui/Options/BulkOptions/ViewModels/KolabMultipleOptionsTemplateViewModel.cs @@ -182,6 +182,56 @@ private async void DiscoverResourcesCommandAsync() await DiscoverResourcesAsync(); } + private GenericComObjectWrapper findOrCreateFolder(GenericComObjectWrapper ParentFolder, String NewFolderName, object NewFolderType) + { + GenericComObjectWrapper newFolder = null; + String NewFolderNameSafe = NewFolderName.Replace('.', '_'); + try + { + // Use existing folder if it does exist. Start with raw name. + // If this fails, fall back to name with dots replaced by underscores. + s_logger.Debug($"Try to use an existing folder for '{NewFolderName}'"); + try + { + newFolder = new GenericComObjectWrapper(ParentFolder.Inner.Folders[NewFolderName] as Folder); + } + catch + { + newFolder = new GenericComObjectWrapper(ParentFolder.Inner.Folders[NewFolderNameSafe] as Folder); + } + } + catch + { + // No matching folder found, so create missing folder. Start with raw name. + // If this fails, fall back to name with dots replaced by underscores. + s_logger.Debug($"Could not find/use an existing folder for '{NewFolderName}'"); + s_logger.Info($"Create new folder for '{NewFolderName}'"); + try + { + newFolder = new GenericComObjectWrapper(ParentFolder.Inner.Folders.Add(NewFolderName, NewFolderType) as Folder); + // Make sure it has not been renamed to "name (this computer only)" + s_logger.Debug($"Make sure the folder is called '{NewFolderName}'"); + newFolder.Inner.Name = NewFolderName; + } + catch + { + try + { + newFolder = new GenericComObjectWrapper(ParentFolder.Inner.Folders.Add(NewFolderNameSafe, NewFolderType) as Folder); + // Make sure it has not been renamed to "name (this computer only)" + s_logger.Debug($"Make sure the folder is called '{NewFolderNameSafe}'"); + newFolder.Inner.Name = NewFolderNameSafe; + } + catch (Exception ex) + { + // No folder found and unable to create new one. Log as error. + s_logger.Error($"Could not create folder '{NewFolderName}'.", ex); + } + } + } + return newFolder; + } + public async Task DiscoverResourcesAsync() { _discoverResourcesCommand.SetCanExecute (false); @@ -200,7 +250,6 @@ public async Task DiscoverResourcesAsync() if (OnlyAddNewUrls) { s_logger.Debug("Exclude all server resources that have already been configured"); - // Exclude all resourcres that have already been configured var configuredUrls = new HashSet (existingOptions.Select(o => o.Model.CalenderUrl)); calendars = calendars.Where(c => !configuredUrls.Contains(c.Uri.ToString())).ToArray(); addressBooks = addressBooks.Where(c => !configuredUrls.Contains(c.Uri.ToString())).ToArray(); @@ -214,107 +263,92 @@ public async Task DiscoverResourcesAsync() // https://msdn.microsoft.com/en-us/library/office/ff184655.aspx // Get Outlook's default calendar folder (this is where we create the Kolab folders) GenericComObjectWrapper defaultCalendarFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderCalendar) as Folder); - // Get sync option that syncs the default calendar (if any) - bool defaultFolderIsSynced = existingOptions.Any(o => o.Model.SelectedFolderOrNull?.EntryId == defaultCalendarFolder.Inner.EntryID); - // Find all Kolab calendars that are not yet synced to an outlook folder + // Detect wether we have a sync option that syncs the default Outlook calendar + bool defaultCalendarFolderIsSynced = existingOptions.Any(o => o.Model.SelectedFolderOrNull?.EntryId == defaultCalendarFolder.Inner.EntryID); + + // Create and assign all Kolab calendars that are not yet synced to an outlook folder foreach (var resource in calendars.Where(c => c.SelectedFolder == null)) { - s_logger.Debug($"Find folder for calendar '{Name}'"); string newCalendarName = resource.Name + " (" + Name + ")"; - GenericComObjectWrapper newCalendarFolder; - try + GenericComObjectWrapper newCalendarFolder = null; + + if (resource.Model.IsDefault && !defaultCalendarFolderIsSynced) { - // Sync CalDAV default calendar with Outlook default calendar folder. - // Only do so if there are no sync settings yet for the Outlook default calendar - if (resource.Model.IsDefault && !defaultFolderIsSynced) - { - s_logger.Debug($"Sync Calendar '{Name}' with default outlook calendar"); - newCalendarFolder = defaultCalendarFolder; - } - // Use existing folder if it does exist - else - { - s_logger.Debug($"Try to use an existing folder for calendar '{Name}'"); - newCalendarFolder = new GenericComObjectWrapper(defaultCalendarFolder.Inner.Folders[newCalendarName] as Folder); - } + s_logger.Info($"Sync Calendar '{newCalendarName}' with default outlook calendar"); + newCalendarFolder = defaultCalendarFolder; } - catch + else { - s_logger.Debug($"Create new folder for calendar '{Name}'"); - // Create missing folder - newCalendarFolder = new GenericComObjectWrapper (defaultCalendarFolder.Inner.Folders.Add(newCalendarName, OlDefaultFolders.olFolderCalendar) as Folder); - // Make sure it has not been renamed to "name (this computer only)" - newCalendarFolder.Inner.Name = newCalendarName; + s_logger.Debug($"Find folder for calendar '{newCalendarName}'"); + newCalendarFolder = findOrCreateFolder(defaultCalendarFolder, newCalendarName, OlDefaultFolders.olFolderCalendar); } // use the selected folder for syncing with kolab - resource.SelectedFolder = new OutlookFolderDescriptor (newCalendarFolder.Inner.EntryID, newCalendarFolder.Inner.StoreID, newCalendarFolder.Inner.DefaultItemType, newCalendarFolder.Inner.Name, 0); + if (newCalendarFolder != null) + { + s_logger.Info($"Use calendar folder '{newCalendarFolder.Inner.Name}' in Sync setting"); + resource.SelectedFolder = new OutlookFolderDescriptor(newCalendarFolder.Inner.EntryID, newCalendarFolder.Inner.StoreID, newCalendarFolder.Inner.DefaultItemType, newCalendarFolder.Inner.Name, 0); + } } // Create and assign all Kolab address books that are not yet synced to an outlook folder GenericComObjectWrapper defaultAddressBookFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderContacts) as Folder); foreach (var resource in addressBooks.Where(c => c.SelectedFolder == null)) { - s_logger.Debug($"Find folder for address book '{Name}'"); string newAddressBookName = resource.Name + " (" + Name + ")"; - GenericComObjectWrapper newAddressBookFolder = null; - try + s_logger.Debug($"Find folder for address book '{newAddressBookName}'"); + GenericComObjectWrapper newAddressBookFolder = findOrCreateFolder(defaultAddressBookFolder, newAddressBookName, OlDefaultFolders.olFolderContacts); + if (newAddressBookFolder != null) { - newAddressBookFolder = new GenericComObjectWrapper(defaultAddressBookFolder.Inner.Folders[newAddressBookName] as Folder); - } - catch - { - newAddressBookFolder = new GenericComObjectWrapper (defaultAddressBookFolder.Inner.Folders.Add(newAddressBookName, OlDefaultFolders.olFolderContacts) as Folder); - newAddressBookFolder.Inner.Name = newAddressBookName; newAddressBookFolder.Inner.ShowAsOutlookAB = true; - } - // Special handling for GAL delivered by CardDAV: set as default address list - if (resource.Uri.Segments.Last() == "ldap-directory/") - { - var _session = Globals.ThisAddIn.Application.Session; - foreach (AddressList al in _session.AddressLists) + // Special handling for GAL delivered by CardDAV: set as default address list + if (resource.Uri.Segments.Last() == "ldap-directory/") { - if (al.Name == newAddressBookName) + var _session = Globals.ThisAddIn.Application.Session; + foreach (AddressList ali in _session.AddressLists) { - // We need to set it in the registry, as there does not seem to exist an appropriate API - string regPath = - @"Software\Microsoft\Office\" + Globals.ThisAddIn.Application.Version.Split(new char[] { '.' })[0] + @".0" + - @"\Outlook\Profiles\" + _session.CurrentProfileName + - @"\9207f3e0a3b11019908b08002b2a56c2"; - var key = Registry.CurrentUser.OpenSubKey(regPath, true); - if (key != null) + GenericComObjectWrapper al = new GenericComObjectWrapper(ali); + if (al.Inner.Name == newAddressBookName) { - // Turn ID into byte array - byte[] bytes = new byte[al.ID.Length / 2]; - for (int i = 0; i < al.ID.Length; i += 2) - bytes[i / 2] = Convert.ToByte(al.ID.Substring(i, 2), 16); - key.SetValue("01023d06", bytes); + // We need to set it in the registry, as there does not seem to exist an appropriate API + // http://www.ericwoodford.com/2016/06/set-default-outlook-address-book-script.html + string regPath = + @"Software\Microsoft\Office\" + Globals.ThisAddIn.Application.Version.Split(new char[] { '.' })[0] + @".0" + + @"\Outlook\Profiles\" + _session.CurrentProfileName + + @"\9207f3e0a3b11019908b08002b2a56c2"; // Outlook default address key + var key = Registry.CurrentUser.OpenSubKey(regPath, true); + if (key != null) + { + s_logger.Info($"Configure LDAP GAL '{newAddressBookName}' as default address book."); + // Turn ID into byte array + byte[] bytes = new byte[al.Inner.ID.Length / 2]; + for (int i = 0; i < al.Inner.ID.Length; i += 2) + bytes[i / 2] = Convert.ToByte(al.Inner.ID.Substring(i, 2), 16); + // Set Outlook default address book subKey + key.SetValue("01023d06", bytes); + } } } } - + s_logger.Debug($"Use address book folder '{newAddressBookFolder.Inner.Name}' in Sync setting"); + resource.SelectedFolder = new OutlookFolderDescriptor(newAddressBookFolder.Inner.EntryID, newAddressBookFolder.Inner.StoreID, newAddressBookFolder.Inner.DefaultItemType, newAddressBookFolder.Inner.Name, 0); } - resource.SelectedFolder = new OutlookFolderDescriptor (newAddressBookFolder.Inner.EntryID, newAddressBookFolder.Inner.StoreID, newAddressBookFolder.Inner.DefaultItemType, newAddressBookFolder.Inner.Name, 0); } // Create and assign all Kolab task lists that are not yet synced to an outlook folder GenericComObjectWrapper defaultTaskListsFolder = new GenericComObjectWrapper (Globals.ThisAddIn.Application.Session.GetDefaultFolder(OlDefaultFolders.olFolderTasks) as Folder); foreach (var resource in taskLists.Where(c => c.SelectedFolder == null)) { - s_logger.Debug($"Find folder for task list '{Name}'"); string newTaskListName = resource.Name + " (" + Name + ")"; - GenericComObjectWrapper newTaskListFolder = null; - try + s_logger.Debug($"Find folder for task list '{newTaskListName}'"); + GenericComObjectWrapper newTaskListFolder = findOrCreateFolder(defaultTaskListsFolder, newTaskListName, OlDefaultFolders.olFolderTasks); + if (newTaskListFolder != null) { - newTaskListFolder = new GenericComObjectWrapper (defaultTaskListsFolder.Inner.Folders[newTaskListName] as Folder); + s_logger.Info($"Use task list folder '{newTaskListFolder.Inner.Name}' in Sync setting"); + resource.SelectedFolder = new OutlookFolderDescriptor(newTaskListFolder.Inner.EntryID, newTaskListFolder.Inner.StoreID, newTaskListFolder.Inner.DefaultItemType, newTaskListFolder.Inner.Name, 0); } - catch - { - newTaskListFolder = new GenericComObjectWrapper (defaultTaskListsFolder.Inner.Folders.Add(newTaskListName, OlDefaultFolders.olFolderTasks) as Folder); - newTaskListFolder.Inner.Name = newTaskListName; - } - resource.SelectedFolder = new OutlookFolderDescriptor(newTaskListFolder.Inner.EntryID, newTaskListFolder.Inner.StoreID, newTaskListFolder.Inner.DefaultItemType, newTaskListFolder.Inner.Name, 0); } } + using (var selectResourcesForm = SelectResourceForm.CreateForFolderAssignment(_optionTasks, ConnectionTests.ResourceType.Calendar, calendars, addressBooks, taskLists)) { // Create and add new sync profiles @@ -410,10 +444,10 @@ public async Task DiscoverResourcesAsync() catch (Exception x) { s_logger.Error ("Exception while DiscoverResourcesAsync.", x); - string message = null; + /*string message = null; for (Exception ex = x; ex != null; ex = ex.InnerException) message += ex.Message + Environment.NewLine; - MessageBox.Show (message, OptionTasks.ConnectionTestCaption); + MessageBox.Show (message, OptionTasks.ConnectionTestCaption);*/ } finally {