From ee4f6cac1010e87a675553f033a148b5cd9ef8e0 Mon Sep 17 00:00:00 2001
From: Patrick Simpson
Date: Wed, 21 Dec 2016 12:53:16 +0100
Subject: [PATCH] Initial import
---
.gitignore | 215 +
example-header.txt | 23 -
src/AcaciaZPushPlugin/AcaciaZPushPlugin.sln | 82 +
.../AcaciaOptionAttribute.cs | 53 +
.../AcaciaZPushPlugin.csproj | 612 ++
.../AcaciaZPushPlugin/Config.cs | 29 +
.../AcaciaZPushPlugin/Constants.cs | 99 +
.../AcaciaZPushPlugin/Controls/KAnimator.cs | 96 +
.../AcaciaZPushPlugin/Controls/KBusyHider.cs | 189 +
.../Controls/KBusyIndicator.Designer.cs | 104 +
.../Controls/KBusyIndicator.cs | 62 +
.../Controls/KBusyIndicator.resx | 120 +
.../Controls/KCheckManager.cs | 271 +
.../AcaciaZPushPlugin/Controls/KCopyLabel.cs | 35 +
.../Controls/KDialogButtons.Designer.cs | 78 +
.../Controls/KDialogButtons.cs | 220 +
.../Controls/KDialogButtons.resx | 258 +
.../AcaciaZPushPlugin/Controls/KDialogNew.cs | 131 +
.../Controls/KDialogNew.resx | 133 +
.../Controls/KSelectionManager.cs | 100 +
.../AcaciaZPushPlugin/Controls/KTree.cs | 1166 +++
.../AcaciaZPushPlugin/Controls/KTree.resx | 120 +
.../AcaciaZPushPlugin/Controls/KTreeNode.cs | 242 +
.../Controls/KTreeNodeLoader.cs | 217 +
.../Controls/KTreeNodeMeasurements.cs | 157 +
.../AcaciaZPushPlugin/Controls/KTreeNodes.cs | 105 +
.../Controls/KTreeRenderer.cs | 173 +
.../Controls/KTreeRendererDefault.cs | 93 +
.../Controls/KTreeRendererVisualStyles.cs | 159 +
.../AcaciaZPushPlugin/Controls/KUITask.cs | 570 ++
.../AcaciaZPushPlugin/Controls/KUIUtil.cs | 80 +
.../AcaciaZPushPlugin/DebugOptions.cs | 224 +
.../DebugSupport/AboutDialog.Designer.cs | 192 +
.../Features/DebugSupport/AboutDialog.cs | 59 +
.../Features/DebugSupport/AboutDialog.resx | 525 ++
.../DebugSupport/DebugDialog.Designer.cs | 126 +
.../Features/DebugSupport/DebugDialog.cs | 117 +
.../Features/DebugSupport/DebugDialog.resx | 375 +
.../Features/DebugSupport/DebugInfo.cs | 278 +
.../DebugSupportSettings.Designer.cs | 89 +
.../DebugSupport/DebugSupportSettings.cs | 68 +
.../DebugSupport/DebugSupportSettings.resx | 267 +
.../DebugSupport/FeatureDebugSupport.cs | 122 +
.../DebugSupport/FeatureObjectConverter.cs | 44 +
.../Features/DebugSupport/Statistics.cs | 33 +
.../AcaciaZPushPlugin/Features/Feature.cs | 259 +
.../Features/FeatureDisabled.cs | 41 +
.../Features/FeatureWithUI.cs | 32 +
.../AcaciaZPushPlugin/Features/Features.cs | 40 +
.../Features/FreeBusy/FeatureFreeBusy.cs | 210 +
.../Features/FreeBusy/FreeBusyServer.cs | 158 +
.../FreeBusy/FreeBusySettings.Designer.cs | 89 +
.../Features/FreeBusy/FreeBusySettings.cs | 81 +
.../Features/FreeBusy/FreeBusySettings.resx | 279 +
.../Features/GAB/ChunkIndex.cs | 52 +
.../Features/GAB/FeatureGAB.cs | 626 ++
.../Features/GAB/GABHandler.cs | 693 ++
.../AcaciaZPushPlugin/Features/GAB/GABInfo.cs | 93 +
.../Features/GAB/GABSettings.Designer.cs | 58 +
.../Features/GAB/GABSettings.cs | 57 +
.../Features/GAB/GABSettings.resx | 171 +
.../Features/Notes/FeatureNotes.cs | 292 +
.../OutOfOffice/FeatureOutOfOffice.cs | 241 +
.../OutOfOffice/OutOfOfficeDialog.Designer.cs | 225 +
.../Features/OutOfOffice/OutOfOfficeDialog.cs | 192 +
.../OutOfOffice/OutOfOfficeDialog.resx | 690 ++
.../Features/ReplyFlags/FeatureReplyFlags.cs | 181 +
.../Features/ReplyFlags/ReplyFlags.cs | 229 +
.../Features/SendAs/FeatureSendAs.cs | 125 +
.../SharedFolders/FeatureSharedFolders.cs | 131 +
.../Features/SharedFolders/FolderTreeNode.cs | 95 +
.../SharedFoldersDialog.Designer.cs | 232 +
.../SharedFolders/SharedFoldersDialog.cs | 495 ++
.../SharedFolders/SharedFoldersDialog.resx | 762 ++
.../Features/SharedFolders/StoreTreeNode.cs | 320 +
.../Features/WebApp/FeatureWebApp.cs | 143 +
.../Features/packages.config | 4 +
.../AcaciaZPushPlugin/GlobalOptions.cs | 178 +
.../AcaciaZPushPlugin/Logger.cs | 157 +
.../AcaciaZPushPlugin/Logging.cs | 41 +
.../AcaciaZPushPlugin/NLogLogger.cs | 101 +
.../AcaciaZPushPlugin/Native/User32.cs | 205 +
.../AcaciaZPushPlugin/Native/WM.cs | 30 +
.../AcaciaZPushPlugin/OutlookConstants.cs | 249 +
.../Properties/AssemblyInfo.cs | 25 +
.../Properties/Resources.Designer.cs | 1023 +++
.../Properties/Resources.resx | 434 ++
.../Properties/Settings.Designer.cs | 26 +
.../Properties/Settings.settings | 7 +
.../Properties/TreeLoading.gif | Bin 0 -> 1407 bytes
.../Resources/Icons/Ribbon_About.png | Bin 0 -> 700 bytes
.../Resources/Icons/Ribbon_About_Small.png | Bin 0 -> 423 bytes
.../Icons/Ribbon_AddSharedFolder.png | Bin 0 -> 566 bytes
.../Icons/Ribbon_AddSharedFolder_Small.png | Bin 0 -> 355 bytes
.../Resources/Icons/Ribbon_Debug.png | Bin 0 -> 773 bytes
.../Resources/Icons/Ribbon_Debug_Small.png | Bin 0 -> 391 bytes
.../Resources/Icons/Ribbon_Logfile.png | Bin 0 -> 665 bytes
.../Resources/Icons/Ribbon_Logfile_Small.png | Bin 0 -> 364 bytes
.../Resources/Icons/Ribbon_MDM.png | Bin 0 -> 587 bytes
.../Resources/Icons/Ribbon_MDM_Small.png | Bin 0 -> 389 bytes
.../Icons/Ribbon_ManageSharedFolders.png | Bin 0 -> 566 bytes
.../Icons/Ribbon_ManageSharedFolders1.png | Bin 0 -> 566 bytes
.../Ribbon_ManageSharedFolders_Small.png | Bin 0 -> 355 bytes
.../Ribbon_ManageSharedFolders_Small1.png | Bin 0 -> 355 bytes
.../Ribbon_ManageSharedFolders_Small11.png | Bin 0 -> 355 bytes
.../Resources/Icons/Ribbon_OOF.png | Bin 0 -> 535 bytes
.../Resources/Icons/Ribbon_OOF_Small.png | Bin 0 -> 333 bytes
.../Resources/Icons/Ribbon_Restore.png | Bin 0 -> 924 bytes
.../Resources/Icons/Ribbon_Restore_Small.png | Bin 0 -> 434 bytes
.../Resources/Icons/Ribbon_Rules.png | Bin 0 -> 653 bytes
.../Resources/Icons/Ribbon_Rules_Small.png | Bin 0 -> 383 bytes
.../Resources/Icons/Ribbon_Settings.png | Bin 0 -> 700 bytes
.../Resources/Icons/Ribbon_Settings_Small.png | Bin 0 -> 423 bytes
.../Resources/Icons/Ribbon_SyncGAB.png | Bin 0 -> 749 bytes
.../Resources/Icons/Ribbon_SyncGAB_Small.png | Bin 0 -> 417 bytes
.../Resources/Icons/Ribbon_WebApp.png | Bin 0 -> 476 bytes
.../Resources/Icons/Ribbon_WebApp_Small.png | Bin 0 -> 363 bytes
.../Resources/Icons/Ribbon_WebMeetings.png | Bin 0 -> 386 bytes
.../Icons/Ribbon_WebMeetings_Small.png | Bin 0 -> 261 bytes
.../AcaciaZPushPlugin/Resources/Kopano.ico | Bin 0 -> 105374 bytes
.../AcaciaZPushPlugin/Stubs/IAddressBook.cs | 33 +
.../Stubs/IAppointmentItem.cs | 34 +
.../AcaciaZPushPlugin/Stubs/IBase.cs | 56 +
.../AcaciaZPushPlugin/Stubs/IContactItem.cs | 55 +
.../Stubs/IDistributionList.cs | 32 +
.../AcaciaZPushPlugin/Stubs/IFolder.cs | 105 +
.../AcaciaZPushPlugin/Stubs/IItem.cs | 56 +
.../AcaciaZPushPlugin/Stubs/IMailItem.cs | 39 +
.../AcaciaZPushPlugin/Stubs/INoteItem.cs | 28 +
.../AcaciaZPushPlugin/Stubs/ISearch.cs | 62 +
.../AcaciaZPushPlugin/Stubs/IStorageItem.cs | 28 +
.../AcaciaZPushPlugin/Stubs/IStore.cs | 32 +
.../AcaciaZPushPlugin/Stubs/ITaskItem.cs | 28 +
.../AcaciaZPushPlugin/Stubs/IUserProperty.cs | 38 +
.../AcaciaZPushPlugin/Stubs/IZPushItem.cs | 31 +
.../AcaciaZPushPlugin/Stubs/ItemType.cs | 38 +
.../OutlookWrappers/AddressBookWrapper.cs | 45 +
.../OutlookWrappers/AppointmentItemWrapper.cs | 114 +
.../OutlookWrappers/ContactItemWrapper.cs | 245 +
.../OutlookWrappers/DisposableWrapper.cs | 74 +
.../DistributionListWrapper.cs | 272 +
.../Stubs/OutlookWrappers/FolderWrapper.cs | 439 ++
.../Stubs/OutlookWrappers/MailItemWrapper.cs | 167 +
.../Stubs/OutlookWrappers/Mapping.cs | 133 +
.../Stubs/OutlookWrappers/NoteItemWrapper.cs | 97 +
.../Stubs/OutlookWrappers/OutlookWrapper.cs | 173 +
.../Stubs/OutlookWrappers/SearchWrapper.cs | 224 +
.../OutlookWrappers/StorageItemWrapper.cs | 97 +
.../Stubs/OutlookWrappers/StoreWrapper.cs | 69 +
.../Stubs/OutlookWrappers/TaskItemWrapper.cs | 97 +
.../OutlookWrappers/UserPropertyWrapper.cs | 76 +
.../AcaciaZPushPlugin/ThisAddIn.Designer.cs | 286 +
.../AcaciaZPushPlugin/ThisAddIn.Designer.xml | 4 +
.../AcaciaZPushPlugin/ThisAddIn.cs | 236 +
.../AcaciaZPushPlugin/UI/ErrorUtil.cs | 58 +
.../AcaciaZPushPlugin/UI/FeatureSettings.cs | 58 +
.../UI/GABLookupControl.Designer.cs | 36 +
.../AcaciaZPushPlugin/UI/GABLookupControl.cs | 270 +
.../AcaciaZPushPlugin/UI/KopanoDialog.cs | 33 +
.../UI/Outlook/CommandElement.cs | 139 +
.../AcaciaZPushPlugin/UI/Outlook/MenuItem.cs | 102 +
.../UI/Outlook/OutlookImageList.cs | 140 +
.../AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs | 337 +
.../UI/Outlook/RibbonButton.cs | 50 +
.../UI/Outlook/RibbonToggleButton.cs | 66 +
.../AcaciaZPushPlugin/UI/Outlook/Types.cs | 34 +
.../UI/ProgressDialog.Designer.cs | 97 +
.../AcaciaZPushPlugin/UI/ProgressDialog.cs | 115 +
.../AcaciaZPushPlugin/UI/ProgressDialog.resx | 279 +
.../UI/SettingsDialog.Designer.cs | 96 +
.../AcaciaZPushPlugin/UI/SettingsDialog.cs | 108 +
.../AcaciaZPushPlugin/UI/SettingsDialog.resx | 276 +
.../UI/SettingsPage.Designer.cs | 68 +
.../AcaciaZPushPlugin/UI/SettingsPage.cs | 146 +
.../AcaciaZPushPlugin/UI/SettingsPage.resx | 120 +
.../AcaciaZPushPlugin/Utils/ActiveSync.cs | 361 +
.../AcaciaZPushPlugin/Utils/CollectionUtil.cs | 82 +
.../AcaciaZPushPlugin/Utils/ComRelease.cs | 91 +
.../AcaciaZPushPlugin/Utils/DnsUtil.cs | 98 +
.../AcaciaZPushPlugin/Utils/FolderUtils.cs | 64 +
.../AcaciaZPushPlugin/Utils/JSONUtils.cs | 35 +
.../AcaciaZPushPlugin/Utils/LibUtils.cs | 95 +
.../AcaciaZPushPlugin/Utils/MailEvents.cs | 294 +
.../Utils/OutlookRegistryUtils.cs | 38 +
.../Utils/PasswordEncryption.cs | 165 +
.../AcaciaZPushPlugin/Utils/ReflectUtil.cs | 99 +
.../AcaciaZPushPlugin/Utils/RegistryUtil.cs | 109 +
.../AcaciaZPushPlugin/Utils/StringUtil.cs | 109 +
.../AcaciaZPushPlugin/Utils/Tasks.cs | 114 +
.../Utils/TasksBackground.cs | 54 +
.../Utils/TasksMainThread.cs | 117 +
.../Utils/TasksSynchronous.cs | 34 +
.../AcaciaZPushPlugin/Utils/Util.cs | 48 +
.../AcaciaZPushPlugin/Version.cs | 24 +
.../WBXML/ActiveSync/ActiveSyncCodeSpace.cs | 68 +
.../WBXML/ActiveSync/AirNotifyCodePage.cs | 36 +
.../WBXML/ActiveSync/AirSyncBaseCodePage.cs | 58 +
.../WBXML/ActiveSync/AirSyncCodePage.cs | 76 +
.../WBXML/ActiveSync/CalendarCodePage.cs | 89 +
.../WBXML/ActiveSync/ComposeMailCodePage.cs | 53 +
.../WBXML/ActiveSync/Contacts2CodePage.cs | 49 +
.../WBXML/ActiveSync/ContactsCodePage.cs | 97 +
.../ActiveSync/DocumentLibraryCodePage.cs | 47 +
.../WBXML/ActiveSync/Email2CodePage.cs | 51 +
.../WBXML/ActiveSync/EmailCodePage.cs | 98 +
.../ActiveSync/FolderHierarchyCodePage.cs | 59 +
.../WBXML/ActiveSync/GALCodePage.cs | 50 +
.../WBXML/ActiveSync/ItemEstimateCodePage.cs | 49 +
.../ActiveSync/ItemOperationsCodePage.cs | 60 +
.../ActiveSync/MeetingResponseCodePage.cs | 48 +
.../WBXML/ActiveSync/MoveCodePage.cs | 47 +
.../WBXML/ActiveSync/NotesCodePage.cs | 44 +
.../WBXML/ActiveSync/PingCodePage.cs | 48 +
.../WBXML/ActiveSync/ProvisionCodePage.cs | 94 +
.../ActiveSync/ResolveRecipientsCodePage.cs | 64 +
.../WBXML/ActiveSync/SearchCodePage.cs | 66 +
.../WBXML/ActiveSync/SettingsCodePage.cs | 69 +
.../WBXML/ActiveSync/TasksCodePage.cs | 70 +
.../WBXML/ActiveSync/ValidateCertCodePage.cs | 45 +
.../WBXML/AttributeCodePage.cs | 171 +
.../WBXML/AttributeCodeSpace.cs | 45 +
.../AcaciaZPushPlugin/WBXML/AttributeStart.cs | 61 +
.../AcaciaZPushPlugin/WBXML/AttributeValue.cs | 50 +
.../AcaciaZPushPlugin/WBXML/EmptyCodePage.cs | 41 +
.../AcaciaZPushPlugin/WBXML/EmptyCodeSpace.cs | 40 +
.../AcaciaZPushPlugin/WBXML/GlobalTokens.cs | 83 +
.../WBXML/IANACharacterSets.cs | 164 +
.../WBXML/OpaqueDataExpression.cs | 41 +
.../AcaciaZPushPlugin/WBXML/StringTable.cs | 116 +
.../WBXML/StringTableItem.cs | 49 +
.../AcaciaZPushPlugin/WBXML/Tag.cs | 49 +
.../AcaciaZPushPlugin/WBXML/TagCodePage.cs | 61 +
.../AcaciaZPushPlugin/WBXML/TagCodeSpace.cs | 67 +
.../AcaciaZPushPlugin/WBXML/WBXMLDocument.cs | 663 ++
.../API/SharedFolders/AvailableFolder.cs | 110 +
.../ZPush/API/SharedFolders/SharedFolder.cs | 194 +
.../API/SharedFolders/SharedFoldersAPI.cs | 152 +
.../ZPush/API/SharedFolders/Types.cs | 45 +
.../ZPush/Connect/Soap/ISoapSerializable.cs | 35 +
.../ZPush/Connect/Soap/SoapConstants.cs | 32 +
.../ZPush/Connect/Soap/SoapException.cs | 29 +
.../ZPush/Connect/Soap/SoapParameters.cs | 56 +
.../ZPush/Connect/Soap/SoapRequest.cs | 62 +
.../ZPush/Connect/Soap/SoapRequestEncoder.cs | 116 +
.../ZPush/Connect/Soap/SoapSerializer.cs | 439 ++
.../ZPush/Connect/ZPushConnection.cs | 320 +
.../ZPush/Connect/ZPushRequestEncoder.cs | 33 +
.../ZPush/Connect/ZPushWebService.cs | 93 +
.../AcaciaZPushPlugin/ZPush/Delegates.cs | 31 +
.../ZPush/FolderRegistration.cs | 61 +
.../AcaciaZPushPlugin/ZPush/GABUser.cs | 100 +
.../AcaciaZPushPlugin/ZPush/ItemsWatcher.cs | 46 +
.../AcaciaZPushPlugin/ZPush/ZPushAccount.cs | 297 +
.../AcaciaZPushPlugin/ZPush/ZPushAccounts.cs | 392 +
.../ZPush/ZPushCapabilities.cs | 58 +
.../AcaciaZPushPlugin/ZPush/ZPushChannel.cs | 96 +
.../AcaciaZPushPlugin/ZPush/ZPushChannels.cs | 43 +
.../AcaciaZPushPlugin/ZPush/ZPushFolder.cs | 282 +
.../ZPush/ZPushLocalStore.cs | 171 +
.../AcaciaZPushPlugin/ZPush/ZPushSync.cs | 225 +
.../AcaciaZPushPlugin/ZPush/ZPushTypes.cs | 112 +
.../AcaciaZPushPlugin/ZPush/ZPushWatcher.cs | 408 ++
.../AcaciaZPushPlugin/packages.config | 4 +
.../PluginDebugger/App.config | 9 +
.../PluginDebugger/Kopano.ico | Bin 0 -> 370070 bytes
.../PluginDebugger/MainForm.Designer.cs | 180 +
.../PluginDebugger/MainForm.cs | 245 +
.../PluginDebugger/MainForm.resx | 6293 +++++++++++++++++
.../PluginDebugger/Options.cs | 49 +
.../PluginDebugger/OptionsInfra.cs | 384 +
.../PluginDebugger/PluginDebugger.csproj | 104 +
.../PluginDebugger/Program.cs | 42 +
.../PluginDebugger/Properties/AssemblyInfo.cs | 36 +
.../Properties/Resources.Designer.cs | 71 +
.../PluginDebugger/Properties/Resources.resx | 117 +
.../Properties/Settings.Designer.cs | 30 +
.../Properties/Settings.settings | 7 +
.../PluginDebugger/Spacing.png | Bin 0 -> 161 bytes
translations/KOE.pot | 772 ++
translations/de.po | 842 +++
translations/en.po | 774 ++
translations/fr.po | 840 +++
translations/hu.po | 843 +++
translations/it.po | 807 +++
translations/nl.po | 842 +++
285 files changed, 47027 insertions(+), 23 deletions(-)
create mode 100644 .gitignore
delete mode 100644 example-header.txt
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin.sln
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaOptionAttribute.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Config.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAnimator.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyHider.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCheckManager.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCopyLabel.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KSelectionManager.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNode.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeLoader.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeMeasurements.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodes.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRenderer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererDefault.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererVisualStyles.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUITask.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugInfo.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureDebugSupport.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureObjectConverter.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/Statistics.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Feature.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureDisabled.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureWithUI.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/ChunkIndex.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/FeatureGAB.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABInfo.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Notes/FeatureNotes.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/FeatureOutOfOffice.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/FeatureReplyFlags.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/ReplyFlags.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FolderTreeNode.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/StoreTreeNode.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/WebApp/FeatureWebApp.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/packages.config
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/GlobalOptions.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Logger.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Logging.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/NLogLogger.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/WM.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Properties/AssemblyInfo.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Properties/Resources.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Properties/Resources.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Properties/Settings.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Properties/Settings.settings
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Properties/TreeLoading.gif
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_About.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_About_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_AddSharedFolder.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_AddSharedFolder_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Debug.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Debug_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Logfile.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Logfile_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_MDM.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_MDM_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_ManageSharedFolders.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_ManageSharedFolders1.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_ManageSharedFolders_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_ManageSharedFolders_Small1.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_ManageSharedFolders_Small11.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_OOF.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_OOF_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Restore.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Restore_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Rules.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Rules_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Settings.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_Settings_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_SyncGAB.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_SyncGAB_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_WebApp.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_WebApp_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_WebMeetings.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Icons/Ribbon_WebMeetings_Small.png
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Resources/Kopano.ico
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddressBook.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAppointmentItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IBase.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IContactItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IDistributionList.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IFolder.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IMailItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/INoteItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISearch.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IStorageItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IStore.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ITaskItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IUserProperty.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IZPushItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ItemType.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddressBookWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AppointmentItemWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/ContactItemWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/DisposableWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/DistributionListWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/FolderWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/MailItemWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/Mapping.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/NoteItemWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/OutlookWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SearchWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/StorageItemWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/StoreWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/TaskItemWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/UserPropertyWrapper.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.Designer.xml
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/ErrorUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/FeatureSettings.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/GABLookupControl.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/GABLookupControl.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/KopanoDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/CommandElement.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/MenuItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/OutlookImageList.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/OutlookUI.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/RibbonButton.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/RibbonToggleButton.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/Outlook/Types.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/ProgressDialog.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/ProgressDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/ProgressDialog.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/SettingsDialog.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/SettingsDialog.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/SettingsDialog.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/SettingsPage.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/SettingsPage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/SettingsPage.resx
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ActiveSync.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/CollectionUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ComRelease.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/DnsUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/FolderUtils.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/JSONUtils.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/LibUtils.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/MailEvents.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/OutlookRegistryUtils.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/PasswordEncryption.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ReflectUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/RegistryUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Tasks.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/TasksBackground.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/TasksMainThread.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/TasksSynchronous.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Version.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ActiveSyncCodeSpace.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/AirNotifyCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/AirSyncBaseCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/AirSyncCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/CalendarCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ComposeMailCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/Contacts2CodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ContactsCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/DocumentLibraryCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/Email2CodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/EmailCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/FolderHierarchyCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/GALCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ItemEstimateCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ItemOperationsCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/MeetingResponseCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/MoveCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/NotesCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/PingCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ProvisionCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ResolveRecipientsCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/SearchCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/SettingsCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/TasksCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/ActiveSync/ValidateCertCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/AttributeCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/AttributeCodeSpace.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/AttributeStart.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/AttributeValue.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/EmptyCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/EmptyCodeSpace.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/GlobalTokens.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/IANACharacterSets.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/OpaqueDataExpression.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/StringTable.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/StringTableItem.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/Tag.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/TagCodePage.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/TagCodeSpace.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/WBXML/WBXMLDocument.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/API/SharedFolders/AvailableFolder.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/API/SharedFolders/SharedFolder.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/API/SharedFolders/SharedFoldersAPI.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/API/SharedFolders/Types.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/ISoapSerializable.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapConstants.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapException.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapParameters.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapRequest.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapRequestEncoder.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapSerializer.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushConnection.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushRequestEncoder.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushWebService.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Delegates.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/FolderRegistration.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/GABUser.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ItemsWatcher.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushAccount.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushAccounts.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushCapabilities.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushChannel.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushChannels.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushFolder.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushLocalStore.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushSync.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushTypes.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushWatcher.cs
create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/packages.config
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/App.config
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Kopano.ico
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/MainForm.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/MainForm.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/MainForm.resx
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Options.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/OptionsInfra.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/PluginDebugger.csproj
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Program.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Properties/AssemblyInfo.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Properties/Resources.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Properties/Resources.resx
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Properties/Settings.Designer.cs
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Properties/Settings.settings
create mode 100644 src/AcaciaZPushPlugin/PluginDebugger/Spacing.png
create mode 100644 translations/KOE.pot
create mode 100644 translations/de.po
create mode 100644 translations/en.po
create mode 100644 translations/fr.po
create mode 100644 translations/hu.po
create mode 100644 translations/it.po
create mode 100644 translations/nl.po
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f58a42f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,215 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+tmp/
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+*.VC.opendb
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+*.VC.db
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+## TODO: Comment the next line if you want to checkin your
+## web deploy settings but do note that will include unencrypted
+## passwords
+#*.pubxml
+
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Windows Azure Build Output
+csx/
+*.build.csdef
+
+# Windows Store app package directory
+AppPackages/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+[Ss]tyle[Cc]op.*
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# LightSwitch generated files
+GeneratedArtifacts/
+_Pvt_Extensions/
+ModelManifest.xml
diff --git a/example-header.txt b/example-header.txt
deleted file mode 100644
index 4622061..0000000
--- a/example-header.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-/***********************************************
-* File : filename.source
-* Project : Kopano OL Extension
-* Descr : Description
-*
-* Created : 14.12.2016
-*
-* Copyright 2016 Kopano b.v.
-*
-* This program is free software: you can redistribute it and/or modify
-* it under the terms of the GNU Affero General Public License, version 3,
-* as published by the Free Software Foundation.
-*
-* This program is distributed in the hope that it will be useful,
-* but WITHOUT ANY WARRANTY; without even the implied warranty of
-* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-* GNU Affero General Public License for more details.
-*
-* You should have received a copy of the GNU Affero General Public License
-* along with this program. If not, see .
-*
-* Consult LICENSE file for details
-************************************************/
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin.sln b/src/AcaciaZPushPlugin/AcaciaZPushPlugin.sln
new file mode 100644
index 0000000..62867b8
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin.sln
@@ -0,0 +1,82 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25123.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AcaciaZPushPlugin", "AcaciaZPushPlugin\AcaciaZPushPlugin.csproj", "{1A7427A5-F814-4B07-98B2-C67D758B65D6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{3BAA09C4-9E3C-4088-901C-E03489366468}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginUnitTests", "Test\PluginUnitTests\PluginUnitTests.csproj", "{1862C3CA-3347-4180-B076-D018BAA70F40}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OutlookIntegrationTests", "Test\OutlookIntegrationTests\OutlookIntegrationTests.csproj", "{A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginDebugger", "PluginDebugger\PluginDebugger.csproj", "{9258AD17-0A25-4669-A95C-93EC70882551}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Debug|x64.Build.0 = Debug|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Debug|x86.Build.0 = Debug|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Release|x64.ActiveCfg = Release|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Release|x64.Build.0 = Release|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Release|x86.ActiveCfg = Release|Any CPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}.Release|x86.Build.0 = Release|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Debug|x64.Build.0 = Debug|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Debug|x86.Build.0 = Debug|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Release|x64.ActiveCfg = Release|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Release|x64.Build.0 = Release|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Release|x86.ActiveCfg = Release|Any CPU
+ {1862C3CA-3347-4180-B076-D018BAA70F40}.Release|x86.Build.0 = Release|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Debug|x64.Build.0 = Debug|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Debug|x86.Build.0 = Debug|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Release|x64.ActiveCfg = Release|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Release|x64.Build.0 = Release|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Release|x86.ActiveCfg = Release|Any CPU
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7}.Release|x86.Build.0 = Release|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Debug|x64.Build.0 = Debug|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Debug|x86.Build.0 = Debug|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Release|x64.ActiveCfg = Release|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Release|x64.Build.0 = Release|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Release|x86.ActiveCfg = Release|Any CPU
+ {9258AD17-0A25-4669-A95C-93EC70882551}.Release|x86.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {1862C3CA-3347-4180-B076-D018BAA70F40} = {3BAA09C4-9E3C-4088-901C-E03489366468}
+ {A1AA144C-ABCE-462D-ADB4-6EC25AC062B7} = {3BAA09C4-9E3C-4088-901C-E03489366468}
+ EndGlobalSection
+EndGlobal
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaOptionAttribute.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaOptionAttribute.cs
new file mode 100644
index 0000000..b1575d6
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaOptionAttribute.cs
@@ -0,0 +1,53 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia
+{
+ ///
+ /// Attached to properties or classes to make them visible in the PluginDebugger.
+ ///
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Class)]
+ public class AcaciaOptionAttribute : Attribute
+ {
+ private string _description;
+
+ public string Description
+ {
+ get { return _description; }
+ }
+
+ ///
+ /// If specified, marker interface that controls presence of this option.
+ ///
+ public Type Interface
+ {
+ get;
+ set;
+ }
+
+ public AcaciaOptionAttribute(string description, Type marker = null)
+ {
+ this._description = description;
+ this.Interface = marker;
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj
new file mode 100644
index 0000000..01f73c0
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj
@@ -0,0 +1,612 @@
+
+
+
+
+
+ {BAA0C2D2-18E2-41B9-852F-F413020CAA33};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ Debug
+ AnyCPU
+ {1A7427A5-F814-4B07-98B2-C67D758B65D6}
+ Library
+ false
+ Acacia
+ Kopano
+ v4.5.2
+ VSTO40
+ true
+ publish\
+
+ en
+ 1.0.0.0
+ true
+ true
+ 7
+ days
+ False
+ Kopano OL Extension
+ Kopano
+
+ Kopano OL Extension
+ Kopano OL Extension
+ 3
+
+
+
+ False
+ Microsoft .NET Framework 4.5.2 %28x86 and x64%29
+ true
+
+
+ False
+ .NET Framework 3.5 SP1
+ false
+
+
+ False
+ Microsoft Visual Studio 2010 Tools for Office Runtime %28x86 and x64%29
+ true
+
+
+ False
+ Windows Installer 4.5
+ true
+
+
+
+
+ Outlook
+
+
+
+ true
+ full
+ false
+ ..\Build\Debug\
+ false
+ $(DefineConstants);DEBUG;TRACE
+ 4
+ false
+ true
+
+
+
+ pdbonly
+ true
+ ..\Build\Release\
+ false
+ VSTO40
+ 4
+ false
+ true
+
+
+
+
+
+ ..\packages\NLog.4.2.3\lib\net45\NLog.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+
+
+ True
+
+
+
+
+ False
+ true
+
+
+ False
+ true
+
+
+ False
+
+
+
+
+
+
+
+
+ Component
+
+
+ Component
+
+
+ UserControl
+
+
+ KBusyIndicator.cs
+
+
+
+ Component
+
+
+ UserControl
+
+
+ KDialogButtons.cs
+
+
+ Form
+
+
+
+
+
+
+ UserControl
+
+
+
+
+
+
+
+
+
+ Form
+
+
+ AboutDialog.cs
+
+
+
+ UserControl
+
+
+ DebugSupportSettings.cs
+
+
+
+
+
+
+ UserControl
+
+
+ FreeBusySettings.cs
+
+
+
+ UserControl
+
+
+ GABSettings.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Form
+
+
+ SharedFoldersDialog.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ UserControl
+
+
+ Component
+
+
+ GABLookupControl.cs
+
+
+ Form
+
+
+ Form
+
+
+ SettingsDialog.cs
+
+
+ UserControl
+
+
+ SettingsPage.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Form
+
+
+ DebugDialog.cs
+
+
+
+
+
+
+
+
+
+
+
+
+ Form
+
+
+ OutOfOfficeDialog.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Form
+
+
+ ProgressDialog.cs
+
+
+
+
+
+
+
+ Code
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ KBusyIndicator.cs
+
+
+ KDialogButtons.cs
+
+
+ KDialogNew.cs
+
+
+ KTree.cs
+
+
+ AboutDialog.cs
+
+
+ DebugDialog.cs
+
+
+ DebugSupportSettings.cs
+
+
+ FreeBusySettings.cs
+
+
+ GABSettings.cs
+
+
+ OutOfOfficeDialog.cs
+
+
+ SharedFoldersDialog.cs
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+ Designer
+
+
+ True
+ Resources.resx
+ True
+
+
+ ProgressDialog.cs
+
+
+ SettingsDialog.cs
+
+
+ SettingsPage.cs
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+ True
+ Settings.settings
+
+
+ Code
+
+
+ ThisAddIn.cs
+
+
+ ThisAddIn.Designer.xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+ true
+
+
+ TemporaryKey.pfx
+
+
+ BBFB28C253605E59B6EFA7AAC079FB30F06C8298
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Config.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Config.cs
new file mode 100644
index 0000000..e0d6e19
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Config.cs
@@ -0,0 +1,29 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia
+{
+ public static class Config
+ {
+ public const int ACCOUNT_CHECK_INTERVAL = 5000;
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs
new file mode 100644
index 0000000..d928f6e
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs
@@ -0,0 +1,99 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Acacia
+{
+ public static class Constants
+ {
+ #region Reply flags
+
+ public const string ZPUSH_REPLY_HEADER = OutlookConstants.NS_TRANSPORT_MESSAGE_HEADERS + "X-Push-Flags";
+
+ public const string ZPUSH_REPLY_CATEGORY_PREFIX = "Push: Email ";
+ public const string ZPUSH_REPLY_CATEGORY_REPLIED = "replied";
+ public const string ZPUSH_REPLY_CATEGORY_REPLIED_TO_ALL = "replied-to-all";
+ public const string ZPUSH_REPLY_CATEGORY_FORWARDED = "forwarded";
+ public readonly static Regex ZPUSH_REPLY_CATEGORY_REGEX = new Regex("([a-zA-Z\\-]+) on (.* GMT)$");
+
+ #endregion
+
+ #region SendAs
+
+ public const string ZPUSH_SEND_AS = OutlookConstants.NS_TRANSPORT_MESSAGE_HEADERS + "X-Push-Sender";
+ public const string ZPUSH_SEND_AS_NAME = OutlookConstants.NS_TRANSPORT_MESSAGE_HEADERS + "X-Push-Sender-Name";
+
+ #endregion
+
+ #region GAB
+
+ public const string ZPUSH_GAB_INDEX = "$PushIndex";
+
+ public const int ZPUSH_GAB_NEWEST_MAX_CHECK = 5;
+
+ #endregion
+
+ #region Product names
+
+ public const string PRODUCT_PREFIX = "Kopano";
+ public const string PRODUCT_NAME = "Kopano OL Extension";
+
+ #endregion
+
+ #region Local stores
+
+ public const string LOCAL_STORE_DEFAULT_DIRECTORY = "%LocalAppData%\\" + PRODUCT_PREFIX + "\\" + PRODUCT_NAME;
+ public const string LOCAL_STORE_FILENAME = PRODUCT_PREFIX + "LocalFolders";
+ public const string LOCAL_STORE_EXTENSION = "pst";
+
+ #endregion
+
+ #region ActiveSync headers
+
+ public const string ZPUSH_HEADER_GAB_NAME = "X-Push-GAB-Name";
+ public const string ZPUSH_HEADER_CAPABILITIES = "X-Push-Capabilities";
+ public const string ZPUSH_HEADER_PLUGIN = "X-Push-Plugin";
+ public const string ZPUSH_HEADER_VERSION = "X-Z-Push-Version";
+
+
+ #endregion
+
+ #region Capabilities
+
+ public const string ZPUSH_CAPABILITY_NOTES = "notes";
+ public const string ZPUSH_CAPABILITY_OUT_OF_OFFICE = "oof";
+ public const string ZPUSH_CAPABILITY_OUT_OF_OFFICE_TIMES = "ooftime";
+
+ #endregion
+
+ public const string DATE_ISO_8601 = "yyyyMMddTHHmmssZ";
+
+ public static readonly TimeSpan ZPUSH_SYNC_DEFAULT_PERIOD = new TimeSpan(1, 0, 0);
+
+ #region Registry
+
+ public const string PLUGIN_REGISTRY_BASE = "Software\\" + PRODUCT_PREFIX + "\\" + PRODUCT_NAME;
+ public const string PLUGIN_REGISTRY_LOGLEVEL = "LogLevel";
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAnimator.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAnimator.cs
new file mode 100644
index 0000000..9b4daa2
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAnimator.cs
@@ -0,0 +1,96 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Drawing.Imaging;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public class KAnimator : PictureBox
+ {
+ public KAnimator()
+ {
+ BackColor = Color.Transparent;
+ SizeMode = PictureBoxSizeMode.Zoom;
+ }
+
+ private Image _stillFrame;
+ private Image _animation;
+ private bool _animating;
+
+ public Image Animation
+ {
+ get { return _animation; }
+ set
+ {
+ if (_animation != value)
+ {
+ _animation = value;
+ _stillFrame = value;
+ if (_animation != null)
+ {
+ int frameCount = _animation.GetFrameCount(FrameDimension.Time);
+ if (frameCount > 0)
+ {
+ _animation.SelectActiveFrame(FrameDimension.Time, 0);
+ _stillFrame = new Bitmap(_animation);
+ }
+ }
+ SetImage();
+ }
+ }
+ }
+
+ public bool Animate
+ {
+ get { return _animating; }
+ set
+ {
+ if (_animating != value)
+ {
+ _animating = value;
+ SetImage();
+ }
+ }
+ }
+
+ private void SetImage()
+ {
+ if (_animating)
+ Image = _animation;
+ else
+ // TODO: would be awesome to finish the animation
+ Image = _stillFrame;
+ }
+
+ public override Size GetPreferredSize(Size proposedSize)
+ {
+ Size sz = base.GetPreferredSize(proposedSize);
+
+ // Scale for high resolution screens.
+ using (Graphics g = CreateGraphics())
+ sz = sz.ScaleDpi(g);
+
+ return sz;
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyHider.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyHider.cs
new file mode 100644
index 0000000..f23c866
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyHider.cs
@@ -0,0 +1,189 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+
+ public class KBusyHider : Panel, KUITaskProgress
+ {
+ #region UI
+
+ private KBusyIndicator _busyOverlay = null;
+ private KBusyIndicator _completeOverlay = null;
+ private string _busyText;
+
+ public bool Busy
+ {
+ get
+ {
+ return _busyOverlay != null;
+ }
+
+ set
+ {
+ if (value == true)
+ {
+ if (_busyOverlay != null)
+ return;
+
+ _busyOverlay = CreateOverlay(_busyText, true);
+ }
+ else if (_busyOverlay != null)
+ {
+ // Remove the overlay
+ RemoveOverlay(_busyOverlay);
+ _busyOverlay = null;
+ }
+ }
+ }
+
+ private void RemoveOverlay(KBusyIndicator overlay)
+ {
+ Controls.Remove(overlay);
+
+ // And enable the controls
+ foreach (Control control in Controls)
+ control.Enabled = true;
+ }
+
+ private KBusyIndicator CreateOverlay(string text, bool showProgress)
+ {
+ try
+ {
+ SuspendLayout();
+
+ // Create a new busy indicator and layouyt
+ KBusyIndicator overlay = new KBusyIndicator();
+ overlay.ShowProgress = showProgress;
+ overlay.Text = text;
+
+ // Remove the existing controls; the overlay must be first to be rendered on top,
+ // and there's no insert function.
+ // Also disable the controls on the fly
+ List existing = new List();
+ while (Controls.Count > 0)
+ {
+ existing.Add(Controls[0]);
+ Controls[0].Enabled = false;
+ Controls.RemoveAt(0);
+ }
+
+ // Add the busy overlay
+ Controls.Add(overlay);
+
+ // Re-add the existing controls
+ Controls.AddRange(existing.ToArray());
+
+ return overlay;
+ }
+ finally
+ {
+ ResumeLayout();
+ }
+ }
+
+ public override string Text
+ {
+ get
+ {
+ return BusyText;
+ }
+
+ set
+ {
+ BusyText = value;
+ }
+ }
+
+ public string BusyText
+ {
+ get
+ {
+ return _busyText;
+ }
+
+ set
+ {
+ _busyText = value;
+ if (_busyOverlay != null)
+ _busyOverlay.Text = _busyText;
+ }
+ }
+
+ public void ShowCompletion(string text)
+ {
+ // Show the overlay
+ _completeOverlay = CreateOverlay(text, false);
+
+ _completeOverlay.MouseMove += _completeOverlay_MouseMove;
+
+ // Add a timer to hide
+ var timer = new System.Windows.Forms.Timer();
+ timer.Interval = 5000; // TODO: make a property for this
+ timer.Tick += (o, args) =>
+ {
+ timer.Stop();
+ HideCompleteOverlay();
+ };
+ timer.Start();
+ }
+
+ private void _completeOverlay_MouseMove(object sender, MouseEventArgs e)
+ {
+ HideCompleteOverlay();
+ }
+
+ private void HideCompleteOverlay()
+ {
+ if (_completeOverlay != null)
+ {
+ RemoveOverlay(_completeOverlay);
+ _completeOverlay = null;
+ }
+ }
+
+ protected override void OnLayout(LayoutEventArgs levent)
+ {
+ base.OnLayout(levent);
+
+ foreach (Control control in Controls)
+ {
+ if (control is KBusyIndicator)
+ {
+ Size pref = control.GetPreferredSize(ClientSize);
+ control.Bounds = ClientRectangle.Center(pref);
+ }
+ }
+ }
+
+ public CancellationTokenSource Cancellation
+ {
+ get;
+ set;
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.Designer.cs
new file mode 100644
index 0000000..31ddb89
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.Designer.cs
@@ -0,0 +1,104 @@
+namespace Acacia.Controls
+{
+ partial class KBusyIndicator
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this._layout = new System.Windows.Forms.TableLayoutPanel();
+ this._text = new System.Windows.Forms.Label();
+ this._progress = new System.Windows.Forms.ProgressBar();
+ this._layout.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // _layout
+ //
+ this._layout.AutoSize = true;
+ this._layout.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ this._layout.ColumnCount = 1;
+ this._layout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F));
+ this._layout.Controls.Add(this._text, 0, 0);
+ this._layout.Controls.Add(this._progress, 0, 1);
+ this._layout.Dock = System.Windows.Forms.DockStyle.Fill;
+ this._layout.Location = new System.Drawing.Point(15, 15);
+ this._layout.Margin = new System.Windows.Forms.Padding(15, 15, 15, 15);
+ this._layout.Name = "_layout";
+ this._layout.Padding = new System.Windows.Forms.Padding(9, 9, 9, 9);
+ this._layout.RowCount = 2;
+ this._layout.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this._layout.RowStyles.Add(new System.Windows.Forms.RowStyle());
+ this._layout.Size = new System.Drawing.Size(231, 115);
+ this._layout.TabIndex = 0;
+ //
+ // _text
+ //
+ this._text.AutoSize = true;
+ this._text.Dock = System.Windows.Forms.DockStyle.Fill;
+ this._text.Location = new System.Drawing.Point(15, 9);
+ this._text.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0);
+ this._text.Name = "_text";
+ this._text.Size = new System.Drawing.Size(201, 25);
+ this._text.TabIndex = 0;
+ this._text.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
+ //
+ // _progress
+ //
+ this._progress.Dock = System.Windows.Forms.DockStyle.Fill;
+ this._progress.Location = new System.Drawing.Point(24, 49);
+ this._progress.Margin = new System.Windows.Forms.Padding(15, 15, 15, 15);
+ this._progress.MarqueeAnimationSpeed = 50;
+ this._progress.Name = "_progress";
+ this._progress.Size = new System.Drawing.Size(183, 42);
+ this._progress.Style = System.Windows.Forms.ProgressBarStyle.Marquee;
+ this._progress.TabIndex = 1;
+ //
+ // KBusyIndicator
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(11F, 24F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.AutoSize = true;
+ this.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
+ this.BackColor = System.Drawing.SystemColors.Control;
+ this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.Controls.Add(this._layout);
+ this.Margin = new System.Windows.Forms.Padding(6, 6, 6, 6);
+ this.Name = "KBusyIndicator";
+ this.Padding = new System.Windows.Forms.Padding(15, 15, 15, 15);
+ this.Size = new System.Drawing.Size(261, 145);
+ this._layout.ResumeLayout(false);
+ this._layout.PerformLayout();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel _layout;
+ private System.Windows.Forms.Label _text;
+ private System.Windows.Forms.ProgressBar _progress;
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.cs
new file mode 100644
index 0000000..f823a7f
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.cs
@@ -0,0 +1,62 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public partial class KBusyIndicator : UserControl
+ {
+ public KBusyIndicator()
+ {
+ InitializeComponent();
+ }
+
+ private bool _showProgress = true;
+
+ public bool ShowProgress
+ {
+ get { return _showProgress; }
+ set
+ {
+ _showProgress = value;
+ _progress.Visible = _showProgress;
+ _text.Padding = _showProgress ? new Padding(15) : new Padding(15, 30, 15, 30);
+ }
+ }
+
+ public override string Text
+ {
+ get
+ {
+ return _text.Text;
+ }
+
+ set
+ {
+ _text.Text = value;
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.resx
new file mode 100644
index 0000000..29dcb1b
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KBusyIndicator.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCheckManager.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCheckManager.cs
new file mode 100644
index 0000000..86974c7
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCheckManager.cs
@@ -0,0 +1,271 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public enum KCheckStyle
+ {
+ None,
+ TwoState,
+ ThreeState,
+ Recursive,
+ RecursiveThreeState
+ }
+
+ abstract public class KCheckManager
+ {
+ abstract public void SetCheck(KTreeNode node, CheckState state);
+ abstract public void ToggleCheck(KTreeNode node);
+ abstract public void ToggleCheck(IReadOnlyCollection nodes);
+ abstract public KCheckStyle CheckStyle { get; }
+
+ public class TwoState : KCheckManager
+ {
+ public override KCheckStyle CheckStyle { get { return KCheckStyle.TwoState; } }
+
+ public override void SetCheck(KTreeNode node, CheckState state)
+ {
+ node.CheckStateDirect = state == CheckState.Checked ? CheckState.Checked : CheckState.Unchecked;
+ }
+
+ public override void ToggleCheck(IReadOnlyCollection nodes)
+ {
+ // Do a majority vote to determine if we should
+ bool isChecked = nodes.Sum((x) => x.IsChecked ? 1 : 0) > (double)nodes.Count / 2.0;
+ foreach (KTreeNode node in nodes)
+ node.IsChecked = !isChecked;
+ }
+
+ public override void ToggleCheck(KTreeNode node)
+ {
+ node.CheckState = node.CheckState == CheckState.Checked ? CheckState.Unchecked : CheckState.Checked;
+ }
+ }
+
+ public class ThreeState : KCheckManager
+ {
+ public override KCheckStyle CheckStyle { get { return KCheckStyle.ThreeState; } }
+
+ public override void SetCheck(KTreeNode node, CheckState state)
+ {
+ node.CheckStateDirect = state;
+ }
+
+ public override void ToggleCheck(IReadOnlyCollection nodes)
+ {
+ // Count the check states
+ int[] counts = new int[3];
+ foreach (KTreeNode node in nodes)
+ {
+ ++counts[(int)node.CheckState];
+ }
+
+ // Determine the current check state
+ CheckState state;
+ // Use indeterminate only if it has a clear majority
+ if (counts[(int)CheckState.Indeterminate] > counts[(int)CheckState.Checked] && counts[(int)CheckState.Indeterminate] > counts[(int)CheckState.Unchecked])
+ state = CheckState.Indeterminate;
+ else if (counts[(int)CheckState.Checked] > counts[(int)CheckState.Unchecked])
+ state = CheckState.Checked;
+ else
+ state = CheckState.Unchecked;
+
+ // Update the state
+ state = (CheckState)(((int)state + 1) % 3);
+ foreach (KTreeNode node in nodes)
+ node.CheckState = state;
+ }
+
+ public override void ToggleCheck(KTreeNode node)
+ {
+ node.CheckState = (CheckState)(((int)node.CheckState + 1) % 3);
+ }
+ }
+
+ public class Recursive : KCheckManager
+ {
+ public override KCheckStyle CheckStyle { get { return KCheckStyle.Recursive; } }
+
+ public override void SetCheck(KTreeNode node, CheckState state)
+ {
+ // TODO
+ node.CheckStateDirect = state;
+ }
+
+ public override void ToggleCheck(KTreeNode node)
+ {
+ try
+ {
+ // Set the check state recursively
+ node.Owner?.BeginUpdate();
+
+ SetNodeCheckState(node, NextCheckState(node.CheckState));
+
+ // Update the parent state
+ SetParentCheckState(node.Parent, node.CheckState);
+ }
+ finally
+ {
+ node.Owner?.EndUpdate();
+ }
+ }
+
+ protected virtual CheckState NextCheckState(CheckState checkState)
+ {
+ return (checkState == CheckState.Checked) ? CheckState.Unchecked : CheckState.Checked;
+ }
+
+ protected void SetParentCheckState(KTreeNode parent, CheckState childCheckState)
+ {
+ if (parent == null)
+ return;
+
+ if (childCheckState == CheckState.Indeterminate)
+ {
+ // An indeterminate node always leads to an indeterminate parent
+ parent.CheckState = CheckState.Indeterminate;
+ }
+ else
+ {
+ // Determine the check state
+ bool haveChecked = childCheckState == CheckState.Checked;
+ bool haveUnchecked = childCheckState == CheckState.Unchecked;
+ bool haveIndeterminate = childCheckState == CheckState.Indeterminate;
+ foreach (KTreeNode child in parent.Children)
+ {
+ if (child.CheckState == CheckState.Checked)
+ haveChecked = true;
+ else if (child.CheckState == CheckState.Unchecked)
+ haveUnchecked = true;
+ else
+ haveIndeterminate = true;
+ }
+
+ if (!haveIndeterminate && (haveChecked ^ haveUnchecked))
+ {
+ parent.CheckState = haveChecked ? CheckState.Checked : CheckState.Unchecked;
+ }
+ else
+ {
+ parent.CheckState = CheckState.Indeterminate;
+ }
+ }
+
+ SetParentCheckState(parent.Parent, parent.CheckState);
+ }
+
+ private void SetNodeCheckState(KTreeNode node, CheckState checkState)
+ {
+ // Apply the children first, otherwise the node's check state will be based on that again
+ foreach (KTreeNode child in node.Children)
+ SetNodeCheckState(child, checkState != CheckState.Indeterminate ? checkState : CheckState.Unchecked);
+ node.CheckState = checkState;
+ }
+
+ public override void ToggleCheck(IReadOnlyCollection nodes)
+ {
+ // Count the check states
+ int[] counts = new int[3];
+ foreach (KTreeNode node in nodes)
+ {
+ ++counts[(int)node.CheckState];
+ }
+
+ // Sort by depth and remove any nodes whose ancestor is present, they'll get updated recursively
+ HashSet applyNodes = new HashSet();
+ foreach (KTreeNode node in nodes.OrderBy((x) => x.Depth))
+ {
+ bool add = true;
+ foreach (KTreeNode ancestor in node.Ancestors)
+ {
+ if (applyNodes.Contains(ancestor))
+ {
+ add = false;
+ }
+ break;
+ }
+
+ if (add)
+ applyNodes.Add(node);
+ }
+
+ // Determine the current check state
+ bool isChecked;
+ if (counts[(int)CheckState.Checked] > counts[(int)CheckState.Unchecked])
+ isChecked = true;
+ else
+ isChecked = false;
+
+ // Update the state for all the nodes
+ foreach (KTreeNode node in applyNodes)
+ SetNodeCheckState(node, isChecked ? CheckState.Unchecked : CheckState.Checked);
+ // Update the parents
+ foreach (KTreeNode node in applyNodes)
+ SetParentCheckState(node.Parent, node.CheckState);
+ }
+ }
+
+ public class RecursiveThreeState : Recursive
+ {
+ public override KCheckStyle CheckStyle { get { return KCheckStyle.RecursiveThreeState; } }
+
+ public override void SetCheck(KTreeNode node, CheckState state)
+ {
+ if (state == CheckState.Checked)
+ {
+ // Set indeterminate if any of the children is not checked
+ foreach (KTreeNode child in node.Children)
+ if (child.CheckState != CheckState.Checked)
+ {
+ state = CheckState.Indeterminate;
+ break;
+ }
+
+ }
+ else if (state == CheckState.Indeterminate)
+ {
+ if (node.Children.Count == 0)
+ state = CheckState.Checked;
+ }
+
+ node.CheckStateDirect = state;
+ SetParentCheckState(node.Parent, state);
+ }
+
+ protected override CheckState NextCheckState(CheckState checkState)
+ {
+ switch(checkState)
+ {
+ case CheckState.Unchecked:
+ return CheckState.Indeterminate;
+ case CheckState.Indeterminate:
+ return CheckState.Checked;
+ default:
+ return CheckState.Unchecked;
+ }
+ }
+
+ // TODO: special handling for multiple selection?
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCopyLabel.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCopyLabel.cs
new file mode 100644
index 0000000..3960a16
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KCopyLabel.cs
@@ -0,0 +1,35 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public class KCopyLabel : TextBox
+ {
+ public KCopyLabel()
+ {
+ ReadOnly = true;
+ BorderStyle = BorderStyle.None;
+ TabStop = false;
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.Designer.cs
new file mode 100644
index 0000000..ee333c7
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.Designer.cs
@@ -0,0 +1,78 @@
+namespace Acacia.Controls
+{
+ partial class KDialogButtons
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(KDialogButtons));
+ this.buttonApply = new System.Windows.Forms.Button();
+ this.buttonCancel = new System.Windows.Forms.Button();
+ this.buttonClose = new System.Windows.Forms.Button();
+ this.SuspendLayout();
+ //
+ // buttonApply
+ //
+ resources.ApplyResources(this.buttonApply, "buttonApply");
+ this.buttonApply.Name = "buttonApply";
+ this.buttonApply.UseVisualStyleBackColor = true;
+ this.buttonApply.Click += new System.EventHandler(this.buttonApply_Click);
+ //
+ // buttonCancel
+ //
+ resources.ApplyResources(this.buttonCancel, "buttonCancel");
+ this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.buttonCancel.Name = "buttonCancel";
+ this.buttonCancel.UseVisualStyleBackColor = true;
+ this.buttonCancel.Click += new System.EventHandler(this.buttonCancel_Click);
+ //
+ // buttonClose
+ //
+ resources.ApplyResources(this.buttonClose, "buttonClose");
+ this.buttonClose.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.buttonClose.Name = "buttonClose";
+ this.buttonClose.UseVisualStyleBackColor = true;
+ this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click);
+ //
+ // KDialogButtons
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.Controls.Add(this.buttonApply);
+ this.Controls.Add(this.buttonCancel);
+ this.Controls.Add(this.buttonClose);
+ this.Name = "KDialogButtons";
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+ private System.Windows.Forms.Button buttonApply;
+ private System.Windows.Forms.Button buttonCancel;
+ private System.Windows.Forms.Button buttonClose;
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs
new file mode 100644
index 0000000..fea3a5f
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.cs
@@ -0,0 +1,220 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Threading;
+
+namespace Acacia.Controls
+{
+ public partial class KDialogButtons : UserControl
+ {
+ private static readonly Padding DefaultButtonPadding = new Padding(12, 6, 12, 6);
+ private static readonly Padding DefaultButtonMargin = new Padding(6, 0, 6, 0);
+
+ public KDialogButtons()
+ {
+ InitializeComponent();
+ ButtonPadding = DefaultButtonPadding;
+ ButtonMargin = DefaultButtonMargin;
+ CheckButtons();
+ }
+
+ private bool _isDirty;
+ ///
+ /// Dirty flag. Enables the Apply button and prevents closing without confirmation
+ ///
+ [Category("Kopano")]
+ public bool IsDirty
+ {
+ get { return _isDirty; }
+ set { _isDirty = value; CheckButtons(); }
+ }
+
+ private bool _hasApply = true;
+ ///
+ /// Shows or hides the apply button.
+ ///
+ [Category("Kopano")]
+ public bool HasApply
+ {
+ get { return _hasApply; }
+ set { _hasApply = value; CheckButtons(); }
+ }
+
+
+ private void CheckButtons()
+ {
+ SuspendLayout();
+
+ buttonApply.Enabled = IsDirty;
+ buttonApply.Visible = _hasApply;
+ buttonClose.Visible = !IsDirty || !_hasApply;
+ buttonCancel.Visible = IsDirty && !_hasApply;
+
+ KDialogNew dlg = FindForm() as KDialogNew;
+ if (dlg != null)
+ {
+ dlg.AcceptButton = buttonApply;
+ dlg.CancelButton = IsDirty ? buttonCancel : buttonClose;
+ }
+
+ ResumeLayout();
+ }
+
+ protected override void OnParentVisibleChanged(EventArgs e)
+ {
+ base.OnParentVisibleChanged(e);
+ CheckButtons();
+ }
+
+ private Padding _buttonPadding;
+ [Category("Kopano")]
+ public Padding ButtonPadding
+ {
+ get { return _buttonPadding; }
+ set
+ {
+ _buttonPadding = value;
+ foreach (Control child in Controls)
+ child.Padding = _buttonPadding;
+ }
+ }
+ bool ShouldSerializeButtonPadding() { return ButtonPadding != DefaultButtonPadding; }
+
+ private Padding _buttonMargin;
+ [Category("Kopano")]
+ public Padding ButtonMargin
+ {
+ get { return _buttonMargin; }
+ set
+ {
+ _buttonMargin = value;
+ foreach (Control child in Controls)
+ child.Margin = _buttonMargin;
+ }
+ }
+ bool ShouldSerializeButtonMargin() { return ButtonMargin != DefaultButtonMargin; }
+
+ private Size? _buttonSize;
+ [Category("Kopano")]
+ public Size? ButtonSize
+ {
+ get { return _buttonSize; }
+ set { _buttonSize = value; }
+ }
+
+ protected override void OnControlAdded(ControlEventArgs e)
+ {
+ base.OnControlAdded(e);
+ e.Control.Padding = _buttonPadding;
+ e.Control.Margin = _buttonMargin;
+ }
+
+ protected override void OnLayout(LayoutEventArgs e)
+ {
+ base.OnLayout(e);
+
+ Size buttonSize = CalcButtonSize();
+
+ int x = ClientSize.Width;
+ int y = Padding.Top;
+ foreach (Control child in Controls.Cast().OrderByDescending(ctrl => ctrl.TabIndex))
+ {
+ if (child.Visible)
+ {
+ child.SetBounds(x - buttonSize.Width + child.Margin.Left,
+ y + child.Margin.Top,
+ buttonSize.Width - child.Margin.Horizontal,
+ buttonSize.Height - child.Margin.Vertical);
+ x -= buttonSize.Width;
+ }
+ }
+ }
+
+ private Size CalcButtonSize()
+ {
+ Size buttonSize;
+ if (this._buttonSize.HasValue)
+ {
+ buttonSize = new Size(this._buttonSize.Value.Width + ButtonMargin.Horizontal,
+ this._buttonSize.Value.Height + ButtonMargin.Vertical);
+ }
+ else
+ {
+ // Make all buttons the size of the largest one
+ buttonSize = new Size();
+ foreach (Control child in Controls)
+ {
+ Size childSize = child.GetPreferredSize(ClientSize);
+ buttonSize = new Size(Math.Max(buttonSize.Width, childSize.Width + child.Margin.Horizontal),
+ Math.Max(buttonSize.Height, childSize.Height + child.Margin.Vertical));
+ }
+ }
+
+ return buttonSize;
+ }
+
+ public override Size GetPreferredSize(Size proposedSize)
+ {
+ Size buttonSize = CalcButtonSize();
+ int count = Controls.Cast().Count(x => x.Visible);
+
+ return new Size(buttonSize.Width * count + Padding.Horizontal, buttonSize.Height + Padding.Vertical);
+ }
+
+ private void buttonApply_Click(object sender, EventArgs e)
+ {
+ if (IsDirty)
+ OnApply();
+ }
+
+ virtual protected void OnApply()
+ {
+ if (Apply != null)
+ Apply(this, new EventArgs());
+ }
+
+ [Category("Kopano")]
+ public event EventHandler Apply;
+
+ public CancellationTokenSource Cancellation
+ {
+ get;
+ set;
+ }
+
+ private void buttonCancel_Click(object sender, EventArgs e)
+ {
+ if (Cancellation != null)
+ Cancellation.Cancel();
+ }
+
+ private void buttonClose_Click(object sender, EventArgs e)
+ {
+ if (Cancellation != null)
+ Cancellation.Cancel();
+
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.resx
new file mode 100644
index 0000000..bd5a466
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogButtons.resx
@@ -0,0 +1,258 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ True
+
+
+
+ GrowAndShrink
+
+
+ NoControl
+
+
+
+ 3, 3
+
+
+ 3, 3, 3, 3
+
+
+ 49, 29
+
+
+ 0
+
+
+ Apply
+
+
+ buttonApply
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ NoControl
+
+
+ 161, 3
+
+
+ 3, 3, 3, 3
+
+
+ 56, 29
+
+
+ 1
+
+
+ Cancel
+
+
+ False
+
+
+ buttonCancel
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 1
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ NoControl
+
+
+ 82, 3
+
+
+ 3, 3, 3, 3
+
+
+ 49, 29
+
+
+ 2
+
+
+ Close
+
+
+ buttonClose
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 2
+
+
+ True
+
+
+ 6, 13
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ 2, 2, 2, 2
+
+
+ 220, 35
+
+
+ KDialogButtons
+
+
+ System.Windows.Forms.UserControl, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs
new file mode 100644
index 0000000..da482ee
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.cs
@@ -0,0 +1,131 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public class KDialogNew : Form, KUITaskProgress
+ {
+ public KDialogNew()
+ {
+ Icon = Properties.Resources.Kopano;
+ }
+
+ #region Control links
+
+ [Category("Kopano")]
+ public KDialogButtons DialogButtons
+ {
+ get;
+ set;
+ }
+
+ [Category("Kopano")]
+ public KBusyHider BusyHider
+ {
+ get;
+ set;
+ }
+
+ #endregion
+
+ #region KUITaskProgress
+ // TODO: if BusyHider is not set, pop up dialogs
+ public string BusyText
+ {
+ get { return BusyHider?.BusyText; }
+ set { if (BusyHider != null) BusyHider.BusyText = value; }
+ }
+
+ public bool Busy
+ {
+ get
+ {
+ if (BusyHider == null)
+ return false;
+ return BusyHider.Busy;
+ }
+
+ set
+ {
+ if (BusyHider != null)
+ BusyHider.Busy = value;
+ }
+ }
+
+ public void ShowCompletion(string text)
+ {
+ if (BusyHider != null)
+ BusyHider.ShowCompletion(text);
+ }
+
+ public CancellationTokenSource Cancellation
+ {
+ get { return DialogButtons?.Cancellation; }
+ set { if (DialogButtons != null) DialogButtons.Cancellation = value; }
+ }
+
+ #endregion
+
+ #region Form closing
+
+ protected override void OnFormClosing(FormClosingEventArgs e)
+ {
+ base.OnFormClosing(e);
+
+ // If we have dialog buttons, check if the dirty flag is set
+ if (DialogButtons != null && DialogButtons.IsDirty)
+ {
+ OnDirtyFormClosing(e);
+ }
+ }
+
+ ///
+ /// Event that is raised only when trying to close a dirty form
+ ///
+ [Category("Kopano")]
+ public event FormClosingEventHandler DirtyFormClosing;
+
+ virtual protected void OnDirtyFormClosing(FormClosingEventArgs e)
+ {
+ if (DirtyFormClosing != null)
+ DirtyFormClosing(this, e);
+ }
+
+ #endregion
+
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(KDialogNew));
+ this.SuspendLayout();
+ //
+ // KDialogNew
+ //
+ resources.ApplyResources(this, "$this");
+ this.Name = "KDialogNew";
+ this.ResumeLayout(false);
+
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.resx
new file mode 100644
index 0000000..e1b8003
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDialogNew.resx
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ True
+
+
+
+ 284, 261
+
+
+ KDialogNew
+
+
+ System.Windows.Forms.Form, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KSelectionManager.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KSelectionManager.cs
new file mode 100644
index 0000000..171d427
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KSelectionManager.cs
@@ -0,0 +1,100 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Controls
+{
+ abstract public class KSelectionManager
+ {
+ abstract public IReadOnlyCollection CurrentSelection { get; }
+
+ abstract public void Clear();
+ abstract public void Add(KTreeNode node);
+ abstract public void Toggle(KTreeNode node);
+
+ public class Single : KSelectionManager
+ {
+ private KTreeNode _selectedNode;
+
+ public override IReadOnlyCollection CurrentSelection
+ {
+ get
+ {
+ List sel = new List();
+ if (_selectedNode != null)
+ sel.Add(_selectedNode);
+ return sel;
+ }
+ }
+
+ public override void Clear()
+ {
+ _selectedNode = null;
+ }
+
+ public override void Add(KTreeNode node)
+ {
+ _selectedNode = node;
+ }
+
+ public override void Toggle(KTreeNode node)
+ {
+ if (node == _selectedNode)
+ _selectedNode = null;
+ else
+ _selectedNode = node;
+ }
+ }
+
+ public class Multiple : KSelectionManager
+ {
+ // TODO: use some sort of ordered set?
+ private readonly List _selection = new List();
+
+ public override IReadOnlyCollection CurrentSelection
+ {
+ get
+ {
+ return _selection;
+ }
+ }
+
+ public override void Clear()
+ {
+ _selection.Clear();
+ }
+
+ public override void Add(KTreeNode node)
+ {
+ if (!_selection.Contains(node))
+ _selection.Add(node);
+ }
+
+ public override void Toggle(KTreeNode node)
+ {
+ if (!_selection.Contains(node))
+ _selection.Add(node);
+ else
+ _selection.Remove(node);
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.cs
new file mode 100644
index 0000000..4debde0
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.cs
@@ -0,0 +1,1166 @@
+/// Project : Kopano OL Extension
+///
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Native;
+using Acacia.Utils;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ //[Designer(typeof(KopanoTreeViewDesigner))]
+ public class KTree : UserControl
+ {
+ #region Checkboxes
+
+ public class CheckStateChangedEventArgs : EventArgs
+ {
+ public readonly KTreeNode Node;
+
+ public CheckStateChangedEventArgs(KTreeNode node)
+ {
+ this.Node = node;
+ }
+ }
+
+ public delegate void CheckStateChangedHandler(object sender, CheckStateChangedEventArgs e);
+ public event CheckStateChangedHandler CheckStateChanged;
+
+ internal void OnCheckStateChanged(KTreeNode node)
+ {
+ if (CheckStateChanged != null)
+ {
+ CheckStateChanged(this, new CheckStateChangedEventArgs(node));
+ }
+ }
+
+ private KCheckManager _checkManager;
+ [Browsable(false)]
+ public KCheckManager CheckManager
+ {
+ get { return _checkManager; }
+ set { _checkManager = value; Rerender(); }
+ }
+
+ public KCheckStyle CheckStyle
+ {
+ get
+ {
+ return _checkManager == null ? KCheckStyle.None : _checkManager.CheckStyle;
+ }
+
+ set
+ {
+ switch(value)
+ {
+ case KCheckStyle.TwoState:
+ _checkManager = new KCheckManager.TwoState();
+ break;
+ case KCheckStyle.ThreeState:
+ _checkManager = new KCheckManager.ThreeState();
+ break;
+ case KCheckStyle.Recursive:
+ _checkManager = new KCheckManager.Recursive();
+ break;
+ case KCheckStyle.RecursiveThreeState:
+ _checkManager = new KCheckManager.RecursiveThreeState();
+ break;
+ default:
+ _checkManager = null;
+ break;
+ }
+ }
+ }
+
+ private void ToggleCheck(KTreeNode node)
+ {
+ if (_checkManager == null || node == null)
+ return;
+
+ if (!SelectedNodes.Contains(node) || SelectedNodes.Count == 1)
+ {
+ // Update the single node if it's not part of the selection, or it's the only selection
+ _checkManager.ToggleCheck(node);
+ }
+ else
+ {
+ // Update all selected nodes
+ BeginUpdate();
+ try
+ {
+ _checkManager.ToggleCheck(SelectedNodes);
+ }
+ finally
+ {
+ EndUpdate();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Properties
+
+ private Padding _nodePadding = new Padding(2, 4, 2, 4);
+ public Padding NodePadding
+ {
+ get { return _nodePadding; }
+ set { _nodePadding = value; Rerender(); }
+ }
+
+ private int _nodeIdent = 8;
+ public int NodeIndent
+ {
+ get { return _nodeIdent; }
+ set { _nodeIdent = value; Rerender(); }
+ }
+
+ #endregion
+
+ #region Images
+
+ private ImageList _images;
+
+ public ImageList Images
+ {
+ get { return _images; }
+ set { _images = value; Rerender(); }
+ }
+
+ #endregion
+
+ #region Nodes
+
+ private readonly KTreeNodes _rootNodes;
+
+ [Browsable(false)]
+ public KTreeNodes RootNodes
+ {
+ get { return _rootNodes; }
+ }
+
+ #endregion
+
+ #region Creation
+
+ public KTree()
+ {
+ SetStyle(ControlStyles.ResizeRedraw, true);
+ SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
+ SetStyle(ControlStyles.AllPaintingInWmPaint, true);
+ SetStyle(ControlStyles.Selectable, true);
+ BackColor = SystemColors.Window;
+
+ _rootNodes = new KTreeNodes(this);
+ SetupRenderer();
+ InitScrollBars();
+ }
+
+
+ #endregion
+
+ #region Selection
+
+ private bool _fullRowSelect = true;
+ public bool FullRowSelect
+ {
+ get { return _fullRowSelect; }
+ set { _fullRowSelect = value; Rerender(); }
+ }
+
+ internal KTreeNode ActiveNode { get; private set; }
+
+ private int ActiveNodeIndex
+ {
+ get { return ActiveNode == null ? -1 : _presentNodes.IndexOf(ActiveNode); }
+ }
+
+ private KSelectionManager _selectionManager = new KSelectionManager.Multiple();
+ public bool MultipleSelection
+ {
+ get { return _selectionManager is KSelectionManager.Multiple; }
+ set
+ {
+ _selectionManager = value ? new KSelectionManager.Multiple() : (KSelectionManager)new KSelectionManager.Single();
+ }
+ }
+
+ [Browsable(false)]
+ public IReadOnlyCollection SelectedNodes
+ {
+ get { return _selectionManager.CurrentSelection; }
+ }
+
+ ///
+ /// Selects a single node and makes it the active node.
+ ///
+ /// The node. Pass null to deselect any nodes
+ /// Set to a specific mode to scroll the node into view
+ public void SelectNode(KTreeNode node, ScrollMode scroll = ScrollMode.None)
+ {
+ DoSelectNode(node, SelectAction.Set, scroll);
+ }
+
+ private enum SelectAction
+ {
+ Set,
+ Toggle,
+ Range,
+ AddRange,
+ Activate
+ }
+
+ private KTreeNode _selectRangeAnchor;
+
+ private void DoSelectNode(KTreeNode node, SelectAction action, ScrollMode scroll)
+ {
+ if (action == SelectAction.Set || action == SelectAction.Range)
+ _selectionManager.Clear();
+
+ if (node != null)
+ {
+ switch(action)
+ {
+
+ case SelectAction.Range:
+ case SelectAction.AddRange:
+ if (_selectRangeAnchor == null)
+ {
+ _selectRangeAnchor = node;
+ _selectionManager.Add(node);
+ break;
+ }
+
+ // Select any nodes from the anchor to the current node
+ int activeIndex = Math.Max(0, _presentNodes.IndexOf(_selectRangeAnchor));
+ int nodeIndex = _presentNodes.IndexOf(node);
+ // Keep to order just in case the selection manager wants it
+ if (activeIndex > nodeIndex)
+ {
+ for (int i = nodeIndex; i <= activeIndex; ++i)
+ if (_presentNodes[i].IsSelectable)
+ _selectionManager.Add(_presentNodes[i]);
+ }
+ else
+ {
+ for (int i = activeIndex; i <= nodeIndex; ++i)
+ if (_presentNodes[i].IsSelectable)
+ _selectionManager.Add(_presentNodes[i]);
+ }
+ break;
+ case SelectAction.Set:
+ _selectRangeAnchor = node;
+ _selectionManager.Add(node);
+ break;
+ case SelectAction.Toggle:
+ _selectRangeAnchor = node;
+ _selectionManager.Toggle(node);
+ break;
+ }
+
+ if (scroll != ScrollMode.None)
+ ScrollIntoView(node, scroll);
+ }
+
+ ActiveNode = node;
+
+ // Must rerender
+ // TODO: affected nodes only
+ Rerender();
+
+ // Raise event if needed
+ CheckSelectionChanged();
+ }
+
+ public class SelectionChangedEventArgs : EventArgs
+ {
+ public readonly KTreeNode[] SelectedNodes;
+
+ public SelectionChangedEventArgs(KTreeNode[] selectedNodes)
+ {
+ this.SelectedNodes = selectedNodes;
+ }
+ }
+
+ public delegate void SelectionChangedDelegate(object sender, SelectionChangedEventArgs e);
+
+ public event SelectionChangedDelegate SelectionChanged;
+
+ private readonly List _previousSelection = new List();
+
+ private void CheckSelectionChanged()
+ {
+ if (_updateCount != 0)
+ return;
+
+ IReadOnlyCollection selection = _selectionManager.CurrentSelection;
+ if (!selection.SameElements(_previousSelection))
+ {
+ _previousSelection.Clear();
+ _previousSelection.AddRange(selection);
+
+ OnSelectionChanged(new SelectionChangedEventArgs(selection.ToArray()));
+ }
+ }
+
+ virtual protected void OnSelectionChanged(SelectionChangedEventArgs e)
+ {
+ if (SelectionChanged != null)
+ SelectionChanged(this, e);
+ }
+
+ #endregion
+
+ #region Mouse handling
+
+ protected override void OnMouseDown(MouseEventArgs e)
+ {
+ base.OnMouseDown(e);
+
+ if ((e.Button & MouseButtons.Left) != 0)
+ {
+ HitTestResult? hit = HitTest(e.Location);
+ if (hit != null && hit.Value.Node.IsSelectable)
+ {
+ switch(hit.Value.Part)
+ {
+ case KTreeNodeMeasurements.Part.Expander:
+ hit.Value.Node.ToggleExpanded();
+ break;
+ case KTreeNodeMeasurements.Part.CheckBox:
+ ToggleCheck(hit.Value.Node);
+ break;
+ case KTreeNodeMeasurements.Part.Text:
+ case KTreeNodeMeasurements.Part.None:
+ case KTreeNodeMeasurements.Part.Image:
+ DoSelectNode(hit.Value.Node, ActionFromModifiers(false), ScrollMode.Auto);
+ break;
+ }
+ }
+ }
+ }
+
+ private SelectAction ActionFromModifiers(bool isKeyboard)
+ {
+ if (ModifierKeys == (Keys.Shift | Keys.Control))
+ return SelectAction.AddRange;
+ else if (ModifierKeys == Keys.Shift)
+ return SelectAction.Range;
+ else if (ModifierKeys == Keys.Control)
+ {
+ if (isKeyboard)
+ return SelectAction.Activate;
+ else
+ return SelectAction.Toggle;
+ }
+ else
+ return SelectAction.Set;
+ }
+
+ protected override void OnMouseDoubleClick(MouseEventArgs e)
+ {
+ if ((e.Button & MouseButtons.Left) != 0)
+ {
+ HitTestResult? hit = HitTest(e.Location, KTreeNodeMeasurements.Part.Text, KTreeNodeMeasurements.Part.Image);
+ if (hit != null && hit.Value.Node.IsSelectable)
+ {
+ hit.Value.Node.ToggleExpanded();
+ }
+ }
+ }
+
+ private KTreeNode _highlightNode;
+ private KTreeNodeMeasurements.Part? _highlightPart;
+
+ protected override void OnMouseMove(MouseEventArgs e)
+ {
+ CheckMouseHighlight();
+ }
+
+ protected override void OnMouseLeave(EventArgs e)
+ {
+ // The mouse might be over a control hosted in a node, so check highlighting again
+ CheckMouseHighlight();
+ }
+
+ private void CheckMouseHighlight()
+ {
+ HitTestResult? hit = HitTest(PointToClient(MousePosition));
+ HighlightNode(hit?.Node, hit?.Part);
+ }
+
+ private void HighlightNode(KTreeNode newHighlight, KTreeNodeMeasurements.Part? newPart)
+ {
+ if (newHighlight != _highlightNode || _highlightPart != newPart)
+ {
+ KTreeNode old = _highlightNode;
+
+ if (newHighlight != null && !newHighlight.IsSelectable)
+ {
+ _highlightNode = null;
+ _highlightPart = null;
+ }
+ else
+ {
+ _highlightNode = newHighlight;
+ _highlightPart = newPart;
+ }
+
+ // Render old node without highlight
+ if (old != null)
+ Rerender(old);
+
+ // Render new node
+ if (_highlightNode != null)
+ Rerender(_highlightNode);
+ }
+ }
+
+ protected override void OnMouseWheel(MouseEventArgs e)
+ {
+ // Let the scrollbar handle the scrolling
+ if (HaveVerticalScrollBar)
+ _verticalScrollBar.ForwardMouseWheel(e);
+ }
+
+ #endregion
+
+ #region Hit testing
+
+ private struct HitTestResult
+ {
+ public KTreeNode Node;
+ public KTreeNodeMeasurements.Part Part;
+ }
+
+ private HitTestResult? HitTest(Point location, params KTreeNodeMeasurements.Part[] wanted)
+ {
+ if (location.X < 0 || location.X >= ViewRectangle.Width)
+ return null;
+
+ KTreeNode node = NodeAtY(location.Y);
+ if (node == null)
+ return null;
+
+ KTreeNodeMeasurements.Part? part = node.EffectiveDimension.HitTest(location.X + _horizontalScrollBar.Value);
+ if (part != null)
+ {
+ // Check if it's the part we're interested in
+ if (wanted.Length > 0 && !wanted.Contains(part.Value))
+ return null;
+
+ // Part.None is valid only if full row selection is enabled
+ if (!FullRowSelect && part.Value == KTreeNodeMeasurements.Part.None)
+ return null;
+
+ // Success
+ return new HitTestResult() { Node = node, Part = part.Value };
+ }
+
+ return null;
+ }
+
+ private KTreeNode NodeAtY(int y)
+ {
+ int index = NodeIndexAtY(y);
+ if (index < 0 || index >= _presentNodes.Count)
+ return null;
+ return _presentNodes[index];
+ }
+
+ private int NodeIndexAtY(int y)
+ {
+ // TODO: use a secondary index, or assume all rows are the same height?
+ y += _verticalScrollBar.Value;
+ if (y < 0)
+ return -1;
+ for(int i = 0; i < _presentNodes.Count; ++i)
+ {
+ if (_presentNodes[i].EffectiveDimension.NodeRect.ContainsY(y))
+ return i;
+ }
+ return _presentNodes.Count;
+ }
+
+ #endregion
+
+ #region Keyboard handling
+
+ protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e)
+ {
+ switch (e.KeyCode)
+ {
+ case Keys.Up:
+ KeySelect(-1);
+ break;
+ case Keys.Down:
+ KeySelect(1);
+ break;
+ case Keys.PageUp:
+ KeySelect(-PageSize);
+ break;
+ case Keys.PageDown:
+ KeySelect(PageSize);
+ break;
+ case Keys.Home:
+ KeySelect(-_presentNodes.Count);
+ break;
+ case Keys.End:
+ KeySelect(_presentNodes.Count);
+ break;
+ case Keys.Left:
+ KeyExpand(false);
+ break;
+ case Keys.Right:
+ KeyExpand(true);
+ break;
+ case Keys.Space:
+ if (ModifierKeys == Keys.Control || _checkManager == null)
+ {
+ DoSelectNode(ActiveNode, SelectAction.Toggle, ScrollMode.Auto);
+ }
+ else if (_checkManager != null)
+ {
+ ToggleCheck(ActiveNode);
+ }
+ break;
+ default:
+ return;
+ }
+ e.IsInputKey = true;
+ }
+
+ private int PageSize
+ {
+ get
+ {
+ int firstVisible = NodeIndexAtY(0);
+ int count = 0;
+ for (int i = 0; i < _presentNodes.Count; ++i)
+ {
+ Rectangle nodeRect = _presentNodes[i].EffectiveDimension.NodeRect;
+ if (nodeRect.Bottom > ViewRectangle.Bottom)
+ break;
+ else if (nodeRect.Top >= ViewRectangle.Top)
+ ++count;
+ }
+ return count;
+ }
+ }
+
+ private void KeyExpand(bool expand)
+ {
+ if (ActiveNode == null)
+ return;
+
+ if (expand)
+ {
+ if (ActiveNode.ChildLoader.NeedsExpander)
+ {
+ if (!ActiveNode.IsExpanded)
+ ActiveNode.IsExpanded = true;
+ else
+ DoSelectNode(ActiveNode.Children.First(), ActionFromModifiers(true), ScrollMode.Auto);
+ }
+ }
+ else
+ {
+ if (ActiveNode.IsExpanded)
+ ActiveNode.IsExpanded = false;
+ else if (ActiveNode.Parent != null)
+ DoSelectNode(ActiveNode.Parent, ActionFromModifiers(true), ScrollMode.Auto);
+ }
+ }
+
+ private void KeySelect(int dir)
+ {
+ int currentIndex = ActiveNodeIndex;
+ for (;;)
+ {
+ currentIndex = currentIndex + dir;
+ currentIndex = Math.Max(Math.Min(currentIndex, _presentNodes.Count - 1), 0);
+ KTreeNode node = currentIndex < _presentNodes.Count ? _presentNodes[currentIndex] : null;
+ if (node != null && !node.IsSelectable)
+ continue;
+ DoSelectNode(node, ActionFromModifiers(true), dir > 0 ? ScrollMode.Bottom : ScrollMode.Top);
+ break;
+ }
+ }
+
+ #endregion
+
+ #region Columns
+ // TODO
+ /*
+ private readonly TreeViewColumnCollection _columns;
+
+ [Category("Columns")]
+ [Browsable(true)]
+ [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
+ public TreeViewColumnCollection Columns
+ {
+ get { return _columns; }
+ }*/
+
+ #endregion
+
+ #region Rendering
+
+ private KTreeRenderer _renderer;
+
+ public void SetupRenderer(bool enableVisualStyles = true)
+ {
+ if (enableVisualStyles && Application.RenderWithVisualStyles)
+ _renderer = new KTreeRendererVisualStyles();
+ else
+ _renderer = new KTreeRendererDefault();
+ Rerender();
+ }
+
+ ///
+ /// The nodes that are currently present, i.e. their parents are expanded.
+ ///
+ private readonly List _presentNodes = new List();
+
+ internal void OnNodeExpandedChanged(KTreeNode node)
+ {
+ BeginUpdate();
+ try
+ {
+ if (node.IsExpanded)
+ {
+ int index = _presentNodes.IndexOf(node);
+ if (index < 0)
+ return;
+
+ // Add the child nodes
+ InsertChildNodes(node, index + 1);
+ }
+ else
+ {
+ RemoveChildNodes(node);
+ }
+ }
+ finally
+ {
+ EndUpdate();
+ }
+ }
+
+ private void RemoveChildNodes(KTreeNode parent)
+ {
+ int index = _presentNodes.IndexOf(parent);
+ if (index < 0)
+ return;
+
+ // Remove any node that's deeper than the current node
+ int depth = parent.Depth;
+ int first = index + 1;
+ int past = first;
+ while (past < _presentNodes.Count)
+ {
+ if (_presentNodes[past].Depth > depth)
+ ++past;
+ else break;
+ }
+ if (past > first)
+ _presentNodes.RemoveRange(first, past - first);
+ }
+
+ internal void OnNodeChildrenChanged(KTreeNode node)
+ {
+ Rerender(node);
+ }
+
+ internal void OnNodeAdded(KTreeNode parent, KTreeNode node)
+ {
+ BeginUpdate();
+ try
+ {
+ if (parent == null)
+ {
+ // TODO: this probably leads to wrong order
+ _presentNodes.Add(node);
+ }
+ else
+ {
+ if (!parent.IsExpanded)
+ return;
+
+ int index = _presentNodes.IndexOf(parent);
+ if (index < 0)
+ return;
+ ++index;
+ int depth = parent.Depth;
+ while (index < _presentNodes.Count)
+ {
+ if (_presentNodes[index].Depth <= depth)
+ break;
+ ++index;
+ }
+ _presentNodes.Insert(index, node);
+ }
+
+ if (node.IsExpanded)
+ {
+ OnNodeExpandedChanged(node);
+ }
+ }
+ finally
+ {
+ EndUpdate();
+ }
+ }
+
+ internal void OnNodeRemoved(KTreeNode parent, KTreeNode node)
+ {
+ throw new NotImplementedException();
+ }
+
+ internal void OnNodeCleared(KTreeNode parent)
+ {
+ BeginUpdate();
+ try
+ {
+ if (parent == null)
+ {
+ // Root node cleared, means no more nodes
+ _presentNodes.Clear();
+ }
+ else
+ {
+ RemoveChildNodes(parent);
+ }
+ }
+ finally
+ {
+ EndUpdate();
+ }
+ }
+
+ private int InsertChildNodes(KTreeNode parent, int index)
+ {
+ if (parent.IsExpanded)
+ {
+ foreach (KTreeNode node in parent.Children)
+ {
+ _presentNodes.Insert(index, node);
+ index = InsertChildNodes(node, index + 1);
+ }
+ }
+ return index;
+ }
+
+ private int _updateCount;
+
+ public void BeginUpdate()
+ {
+ ++_updateCount;
+ }
+
+ public void EndUpdate()
+ {
+ --_updateCount;
+ if (_updateCount == 0)
+ {
+ Rerender();
+ CheckSelectionChanged();
+ }
+ }
+
+ internal void Rerender(KTreeNode node = null)
+ {
+ if (_updateCount != 0)
+ return;
+
+ // TODO: use node
+ MeasureNodes();
+ UpdateScrollBars();
+ CheckMouseHighlight();
+ Invalidate();
+ }
+
+ private void MeasureNodes()
+ {
+ _renderer.Init(ViewRectangle, this);
+ using (Graphics graphics = CreateGraphics())
+ {
+ foreach (KTreeNode node in _presentNodes)
+ {
+ _renderer.MeasureNode(graphics, node);
+ }
+ }
+ }
+
+ private readonly List _nodeControls = new List();
+
+ protected override void OnPaint(PaintEventArgs e)
+ {
+ int firstVisibleNode = NodeIndexAtY(0);
+ List visibleControls = new List();
+
+ for (int i = firstVisibleNode; i < _presentNodes.Count; ++i)
+ {
+ KTreeNode node = _presentNodes[i];
+ // Stop rendering when we're out of view
+ if (node.EffectiveDimension.NodeRect.Y - _verticalScrollBar.Value >= ViewRectangle.Bottom)
+ break;
+
+ // Render the node
+ _renderer.RenderNode(e.Graphics, node, new Point(_horizontalScrollBar.Value, _verticalScrollBar.Value),
+ node == _highlightNode ? _highlightPart : null);
+
+ // May have to add the control
+ if (node.Control != null)
+ {
+ if (node.Control.Parent == null)
+ {
+ _nodeControls.Add(node.Control);
+ node.Control.Parent = this;
+ }
+ visibleControls.Add(node.Control);
+ }
+ }
+
+ // Check if any controls became invisible
+ for(int i = 0; i < _nodeControls.Count;)
+ {
+ if (!visibleControls.Contains(_nodeControls[i]))
+ {
+ _nodeControls[i].Parent = null;
+ _nodeControls.RemoveAt(i);
+ }
+ else
+ {
+ ++i;
+ }
+ }
+
+ // Fill in a rectangle below the scrollbars, as that may be rendered. If they are not visible, the width or height
+ // automatically becomes 0
+ e.Graphics.FillRectangle(SystemBrushes.Control,
+ ClientSize.Width - VerticalScrollBarWidth, ClientSize.Height - HorizontalScrollBarHeight,
+ VerticalScrollBarWidth, HorizontalScrollBarHeight);
+ }
+
+ #endregion
+
+ #region Scrollbars
+
+ private class VScrollBar2 : VScrollBar
+ {
+ public VScrollBar2()
+ {
+ }
+
+ internal void ForwardMouseWheel(MouseEventArgs e)
+ {
+ OnMouseWheel(e);
+ }
+ }
+
+ private class HScrollBar2 : HScrollBar
+ {
+ internal void ForwardMouseWheel(MouseEventArgs e)
+ {
+ OnMouseWheel(e);
+ }
+ }
+
+ private readonly VScrollBar2 _verticalScrollBar = new VScrollBar2();
+ private readonly HScrollBar2 _horizontalScrollBar = new HScrollBar2();
+
+ private void InitScrollBars()
+ {
+ _verticalScrollBar.Scroll += _scrollBar_Scroll;
+ _verticalScrollBar.PreviewKeyDown += _verticalScrollBar_PreviewKeyDown;
+ Controls.Add(_verticalScrollBar);
+ _horizontalScrollBar.Scroll += _scrollBar_Scroll;
+ _horizontalScrollBar.PreviewKeyDown += _verticalScrollBar_PreviewKeyDown;
+ Controls.Add(_horizontalScrollBar);
+ }
+
+ private void _verticalScrollBar_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e)
+ {
+
+ }
+
+ private void _scrollBar_Scroll(object sender, ScrollEventArgs e)
+ {
+ // Mouse might be over different node now
+ CheckMouseHighlight();
+
+ // Repaint
+ Invalidate();
+ }
+
+ protected override void OnSizeChanged(EventArgs e)
+ {
+ base.OnSizeChanged(e);
+ MeasureNodes();
+ UpdateScrollBars();
+ }
+
+ // TODO: this goes wrong if resizing makes a scrollbar appear/disappear
+ private void UpdateScrollBars()
+ {
+ // May happen during init
+ if (ViewRectangle.Height < 0)
+ return;
+
+ // Update scrollbar ranges
+ _verticalScrollBar.Minimum = 0;
+ _verticalScrollBar.Maximum = _renderer.TotalRect.Height;
+ _horizontalScrollBar.Minimum = 0;
+ _horizontalScrollBar.Maximum = _renderer.TotalRect.Width;
+
+ // Set change sizes
+ if (_presentNodes.Count > 0)
+ _verticalScrollBar.SmallChange = _presentNodes.First().EffectiveDimension.NodeRect.Height;
+ _verticalScrollBar.LargeChange = ViewRectangle.Height;
+ _horizontalScrollBar.SmallChange = NodeIndent;
+ _horizontalScrollBar.LargeChange = Math.Max(0, ViewRectangle.Width); // Negative on miminize
+ if (_verticalScrollBar.LargeChange >= _verticalScrollBar.Maximum)
+ _verticalScrollBar.Value = 0;
+
+ // Set the positions, make them size 0 if not required
+ _verticalScrollBar.SetBounds(ClientSize.Width - VerticalScrollBarWidth, 0, VerticalScrollBarWidth, ClientSize.Height - HorizontalScrollBarHeight);
+ _horizontalScrollBar.SetBounds(0, ClientSize.Height - HorizontalScrollBarHeight, ClientSize.Width - VerticalScrollBarWidth, HorizontalScrollBarHeight);
+ }
+
+ private bool HaveVerticalScrollBar
+ {
+ get { return _verticalScrollBar.LargeChange < _verticalScrollBar.Maximum; }
+ }
+ private int VerticalScrollBarWidth
+ {
+ get { return HaveVerticalScrollBar ? SystemInformation.VerticalScrollBarWidth : 0; }
+ }
+ private bool HaveHorizontalScrollBar
+ {
+ get { return _horizontalScrollBar.LargeChange < _horizontalScrollBar.Maximum; }
+ }
+ private int HorizontalScrollBarHeight
+ {
+ get { return HaveHorizontalScrollBar ? SystemInformation.HorizontalScrollBarHeight : 0; }
+ }
+
+ private Rectangle ViewRectangle
+ {
+ get
+ {
+ Rectangle r = ClientRectangle;
+ r.Width -= VerticalScrollBarWidth;
+ r.Height -= HorizontalScrollBarHeight;
+ return r;
+ }
+ }
+
+ private Rectangle ScrolledRectangle
+ {
+ get
+ {
+ Rectangle r = ClientRectangle;
+ r.Width -= VerticalScrollBarWidth;
+ r.Height -= HorizontalScrollBarHeight;
+ r.X += _horizontalScrollBar.Value;
+ r.Y += _verticalScrollBar.Value;
+ return r;
+ }
+ }
+
+ public enum ScrollMode
+ {
+ None,
+ Auto,
+ Top,
+ Middle,
+ Bottom
+ }
+
+ public void ScrollIntoView(KTreeNode node, ScrollMode mode)
+ {
+ if (mode == ScrollMode.None)
+ return;
+
+ if (!node.IsVisible)
+ {
+ //return;
+ foreach (KTreeNode parent in node.Ancestors)
+ {
+ if (!parent.IsExpanded)
+ {
+ parent.IsExpanded = true;
+ }
+ else
+ {
+ break;
+ }
+ }
+ }
+
+ // Number of pixels from edge to keep node in Y direction.
+ // TODO: this assumes all nodes are the same height
+ int scrollBorderY = node.EffectiveDimension.NodeRect.Height;
+
+ // Vertical
+ // Do nothing if the node is already fully visible
+ if (!ScrolledRectangle.ContainsY(node.EffectiveDimension.NodeRect.Top - scrollBorderY) ||
+ !ScrolledRectangle.ContainsY(node.EffectiveDimension.NodeRect.Bottom + scrollBorderY))
+ {
+ if (mode == ScrollMode.Auto)
+ {
+ if (node.EffectiveDimension.NodeRect.Top + scrollBorderY < ScrolledRectangle.Y)
+ mode = ScrollMode.Top;
+ else
+ mode = ScrollMode.Bottom;
+ }
+
+ switch (mode)
+ {
+ case ScrollMode.Top:
+ SetVScroll(node.EffectiveDimension.NodeRect.Top - ViewRectangle.Top - scrollBorderY);
+ break;
+ case ScrollMode.Middle:
+ SetVScroll((node.EffectiveDimension.NodeRect.Top + node.EffectiveDimension.NodeRect.Height / 2) - (ViewRectangle.Top + ViewRectangle.Height / 2));
+ break;
+ case ScrollMode.Bottom:
+ SetVScroll(node.EffectiveDimension.NodeRect.Bottom - ViewRectangle.Bottom + scrollBorderY);
+ break;
+ }
+ }
+
+ // Horizontal
+ if (!ScrolledRectangle.ContainsX(node.EffectiveDimension.NodeRect.Left) || !ScrolledRectangle.ContainsX(node.EffectiveDimension.NodeRect.Right))
+ {
+ // Align left or right, depending on which is the smallest change
+ int alignLeft = node.EffectiveDimension.NodeRect.Left - ViewRectangle.Left;
+ int alignRight = node.EffectiveDimension.NodeRect.Right - ViewRectangle.Right;
+ if (Math.Abs(alignLeft - _horizontalScrollBar.Value) < Math.Abs(alignRight - _horizontalScrollBar.Value))
+ SetHScroll(alignLeft);
+ else
+ SetHScroll(alignRight);
+ }
+
+ // Check current highlight
+ CheckMouseHighlight();
+ }
+
+ private void SetVScroll(int value)
+ {
+ _verticalScrollBar.Value = Math.Max(_verticalScrollBar.Minimum, Math.Min(value, _verticalScrollBar.Maximum - _verticalScrollBar.LargeChange + 1));
+ }
+
+ private void SetHScroll(int value)
+ {
+ _horizontalScrollBar.Value = Math.Max(_horizontalScrollBar.Minimum, Math.Min(value, _horizontalScrollBar.Maximum - _horizontalScrollBar.LargeChange + 1));
+ }
+
+ #endregion
+
+ #region Focus
+
+ protected override void OnGotFocus(EventArgs e)
+ {
+ base.OnGotFocus(e);
+ Invalidate();
+ }
+
+ protected override void OnLostFocus(EventArgs e)
+ {
+ base.OnLostFocus(e);
+ Invalidate();
+ }
+
+ #endregion
+
+ #region Winforms Autogenerated
+
+ private void InitializeComponent()
+ {
+ this.SuspendLayout();
+ //
+ // KTree
+ //
+ this.Name = "KTree";
+ this.ResumeLayout(false);
+ }
+
+ #endregion
+
+ #region Disabled state
+
+ protected override void OnEnabledChanged(EventArgs e)
+ {
+ base.OnEnabledChanged(e);
+ RedrawBorder();
+ }
+
+ protected override void WndProc(ref Message m)
+ {
+ if (m.Msg == (int)WM.NCPAINT)
+ {
+ WmNcPaint(ref m);
+ return;
+ }
+ base.WndProc(ref m);
+ }
+
+ private void WmNcPaint(ref Message m)
+ {
+ if (BorderStyle == BorderStyle.None)
+ return;
+
+ IntPtr hDC = User32.GetWindowDC(m.HWnd);
+ try
+ {
+ using (Graphics g = Graphics.FromHdc(hDC))
+ {
+ _renderer.RenderControlBorder(g, new Rectangle(0, 0, Width, Height));
+ }
+ }
+ finally
+ {
+ User32.ReleaseDC(m.HWnd, hDC);
+ }
+ }
+
+ protected override void OnResize(EventArgs e)
+ {
+ base.OnResize(e);
+ RedrawBorder();
+ }
+
+ private void RedrawBorder()
+ {
+ // Force NCPaint update
+ User32.RedrawWindow(this.Handle, IntPtr.Zero, IntPtr.Zero,
+ User32.RedrawWindowFlags.Frame | User32.RedrawWindowFlags.Invalidate /*| User32.RedrawWindowFlags.UpdateNow*/);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.resx
new file mode 100644
index 0000000..29dcb1b
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTree.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNode.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNode.cs
new file mode 100644
index 0000000..0d69747
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNode.cs
@@ -0,0 +1,242 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public class KTreeSubNode
+ {
+ // TODO: rerender on set
+ public string Text { get; set; }
+
+ public Control Control { get; set; }
+ }
+
+ public class KTreeNode : KTreeSubNode
+ {
+ #region Children
+
+ private KTreeNodeLoader _childLoader;
+
+ public KTreeNodeLoader ChildLoader
+ {
+ get { return _childLoader; }
+ set
+ {
+ if (_childLoader != value)
+ {
+ _childLoader = value;
+ Owner?.OnNodeChildrenChanged(this);
+ }
+ }
+ }
+
+ public KTreeNodes Children
+ {
+ get { return _childLoader.Children; }
+ }
+
+ #endregion
+
+ #region Properties
+
+ public int? ImageIndex { get; set; }
+ public object Tag { get; set; }
+
+ #endregion
+
+ #region State
+
+ private CheckState _checkState;
+ internal CheckState CheckStateDirect { get { return _checkState; } set { _checkState = value; } }
+ public CheckState CheckState
+ {
+ get { return _checkState; }
+ set
+ {
+ if (!HasCheckBox)
+ {
+ _checkState = value;
+ return;
+ }
+
+ if (_checkState != value)
+ {
+ KTree owner = Owner;
+ if (owner != null)
+ {
+ owner.CheckManager.SetCheck(this, value);
+ owner.Rerender(this);
+ }
+ else _checkState = value;
+
+ OnCheckStateChanged();
+ }
+ }
+ }
+
+ public delegate void CheckStateChangedHandler(KTreeNode node);
+ public event CheckStateChangedHandler CheckStateChanged;
+ protected virtual void OnCheckStateChanged()
+ {
+ if (CheckStateChanged != null)
+ CheckStateChanged(this);
+ Owner?.OnCheckStateChanged(this);
+ }
+
+ public bool IsChecked
+ {
+ get { return CheckState == CheckState.Checked; }
+ set { CheckState = value ? CheckState.Checked : CheckState.Unchecked; }
+ }
+
+ private bool _hasCheckBox = true;
+ public bool HasCheckBox
+ {
+ get { return _hasCheckBox; }
+ set
+ {
+ if (_hasCheckBox != value)
+ {
+ _hasCheckBox = value;
+ Owner?.Rerender(this);
+ }
+ }
+ }
+
+ private bool _isExpanded;
+ public bool IsExpanded
+ {
+ get { return _isExpanded; }
+ set
+ {
+ if (_isExpanded != value)
+ {
+ _isExpanded = value;
+ if (!_isExpanded)
+ _childLoader.NodeClosed();
+
+ if (!_isExpanded || _childLoader.NodeExpanding())
+ Owner?.OnNodeExpandedChanged(this);
+ }
+ }
+ }
+
+ public bool ToggleExpanded()
+ {
+ IsExpanded = !_isExpanded;
+ return _isExpanded;
+ }
+
+ public bool IsSelected
+ {
+ get
+ {
+ return Owner.SelectedNodes.Contains(this);
+ }
+ }
+
+ private bool _isSelectable = true;
+
+ public bool IsSelectable
+ {
+ get { return _isSelectable; }
+ set { _isSelectable = value; } // TODO: update node
+ }
+
+ public bool IsVisible
+ {
+ get
+ {
+ for (KTreeNode current = Parent; current != null; current = current.Parent)
+ {
+ if (!current.IsExpanded)
+ return false;
+ }
+ return true;
+ }
+ }
+
+
+ internal KTreeNodes ParentNodes { get; set; }
+
+ public KTreeNode Parent
+ {
+ get
+ {
+ return ParentNodes?.Parent;
+ }
+ }
+
+ public IEnumerable Ancestors
+ {
+ get
+ {
+ KTreeNode current = Parent;
+ while (current != null)
+ {
+ yield return current;
+ current = current.Parent;
+ }
+ }
+ }
+
+ public KTree Owner
+ {
+ get
+ {
+ return ParentNodes?.Owner;
+ }
+ }
+
+ public int Depth
+ {
+ get
+ {
+ int depth = 0;
+ for (KTreeNode current = Parent; current != null; current = current.Parent)
+ {
+ ++depth;
+ }
+ return depth;
+ }
+ }
+
+ internal KTreeNodeMeasurements EffectiveDimension;
+
+ #endregion
+
+ #region Creation
+
+ public KTreeNode(string text = "", object tag = null)
+ {
+ this.Text = text;
+ this.Tag = tag;
+ _childLoader = new KTreeNodeLoaderStatic(this);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeLoader.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeLoader.cs
new file mode 100644
index 0000000..6ba4688
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeLoader.cs
@@ -0,0 +1,217 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public class KTreeNodeLoader
+ {
+ public readonly KTreeNodes Children;
+
+ public enum LoadingState
+ {
+ NotLoaded,
+ Loading,
+ Loaded,
+ Error
+ }
+
+ public LoadingState State
+ {
+ get;
+ protected set;
+ }
+
+ public bool ReloadOnCloseOpen { get; set; }
+
+ public bool NeedsExpander
+ {
+ get
+ {
+ switch (State)
+ {
+ case LoadingState.Loaded:
+ return Children.Count > 0;
+ default:
+ return true;
+ }
+ }
+ }
+
+ public KTreeNodeLoader(KTreeNode parent)
+ {
+ Children = new KTreeNodes(parent);
+ }
+
+ internal void NodeClosed()
+ {
+ if (ReloadOnCloseOpen)
+ State = LoadingState.NotLoaded;
+ }
+
+ internal bool NodeExpanding()
+ {
+ switch (State)
+ {
+ case LoadingState.NotLoaded:
+ case LoadingState.Error:
+ StartLoadChildren();
+ return false;
+ default:
+ return true;
+ }
+ }
+
+ private void StartLoadChildren()
+ {
+ // Set the loading placeholder
+ State = LoadingState.Loading;
+ UpdateChildren(null);
+
+ // Load children asynchronously
+ KTreeNode node = Children.Parent;
+ OnBeginLoading(node);
+ KUITask
+ .New(() =>
+ {
+ return DoLoadChildren(node);
+ })
+ // Continuation in UI thread
+ .OnSuccess(result =>
+ {
+ // Loaded successfully, render
+ KTreeNodes childrenTemp = new KTreeNodes(node);
+ DoRenderChildren(node, result, childrenTemp);
+ State = LoadingState.Loaded;
+ return childrenTemp;
+ }, true)
+ .OnError(error =>
+ {
+ // On error return an empty node list
+ State = LoadingState.Error;
+ return new KTreeNodes(node);
+ }, true)
+ .OnCompletion(children =>
+ {
+ // Update nodes (or plaaceholder) and notify we're done
+ UpdateChildren(children);
+ OnEndLoading(node);
+ }, true)
+ .Start();
+ }
+
+ private KTreeNode _placeholder;
+
+ private void UpdateChildren(KTreeNodes newChildren)
+ {
+ Children.Owner?.BeginUpdate();
+ try
+ {
+ Children.Clear();
+
+ _placeholder = CreatePlaceholder(State, newChildren);
+ if (_placeholder != null)
+ Children.Add(_placeholder);
+
+ if (newChildren != null)
+ foreach (KTreeNode child in newChildren)
+ Children.Add(child);
+ }
+ finally
+ {
+ Children.Owner?.EndUpdate();
+ }
+ }
+
+ protected virtual KTreeNode CreatePlaceholder(LoadingState state, KTreeNodes children)
+ {
+ string text = GetPlaceholderText(state, children);
+ if (string.IsNullOrEmpty(text))
+ return null;
+
+ KTreeNode node = new KTreeNode(text);
+ node.HasCheckBox = false;
+ node.IsSelectable = false;
+ return node;
+ }
+
+ public delegate string PlaceholderTextHandler(KTreeNode node, LoadingState state, KTreeNodes children);
+ public PlaceholderTextHandler PlaceholderText;
+
+ protected virtual string GetPlaceholderText(LoadingState state, KTreeNodes children)
+ {
+ if (PlaceholderText == null)
+ return null;
+ return PlaceholderText(Children.Parent, state, children);
+ }
+
+ public delegate object LoadHandler(KTreeNode node);
+ public delegate void RenderHandler(KTreeNode node, object loaded, KTreeNodes children);
+ public LoadHandler LoadChildren;
+ public RenderHandler RenderChildren;
+
+ virtual protected object DoLoadChildren(KTreeNode node)
+ {
+ return LoadChildren(node);
+ }
+
+ virtual protected void DoRenderChildren(KTreeNode node, object loaded, KTreeNodes children)
+ {
+ RenderChildren(node, loaded, children);
+ }
+
+ public delegate void LoadingHandler(KTreeNode node);
+ public event LoadingHandler BeginLoading;
+ public event LoadingHandler EndLoading;
+
+ protected virtual void OnBeginLoading(KTreeNode node)
+ {
+ if (BeginLoading != null)
+ BeginLoading(node);
+ }
+
+ protected virtual void OnEndLoading(KTreeNode node)
+ {
+ if (EndLoading != null)
+ EndLoading(node);
+ }
+
+ public void Reload()
+ {
+ if (State != LoadingState.Loading)
+ {
+ StartLoadChildren();
+ }
+ }
+ }
+
+ internal class KTreeNodeLoaderStatic : KTreeNodeLoader
+ {
+ public KTreeNodeLoaderStatic(KTreeNode owner)
+ :
+ base(owner)
+ {
+ State = LoadingState.Loaded;
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeMeasurements.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeMeasurements.cs
new file mode 100644
index 0000000..b1dabea
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodeMeasurements.cs
@@ -0,0 +1,157 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ internal class KTreeNodeMeasurements
+ {
+ public enum Part
+ {
+ Expander,
+ CheckBox,
+ Image,
+ Text,
+ Control,
+
+ None
+ }
+
+ private readonly KTreeNode _node;
+ private Rectangle _nodeRect;
+ private readonly KTree _options;
+ private readonly Padding _paddingOveral;
+ private readonly Size[] _sizes = new Size[(int)Part.None];
+ private readonly Padding[] _paddingInternal = new Padding[(int)Part.None];
+
+ public KTreeNodeMeasurements(KTreeNode node, KTree options)
+ {
+ this._node = node;
+ this._options = options;
+ this._nodeRect = new Rectangle(_node.Depth * _options.NodeIndent, 0, 0, 0);
+ _paddingOveral = options.NodePadding;
+ }
+
+ private KTreeNodeMeasurements(KTreeNodeMeasurements orig, int x, int y)
+ {
+ this._node = orig._node;
+ this._options = orig._options;
+ this._paddingOveral = orig._paddingOveral;
+ this._sizes = (Size[])orig._sizes.Clone();
+
+ // The node rectangle is the sum of the widths, and the maximum height (plus padding).
+ // TODO: special handling for control part, make that fit with e.g. a Dock option?
+ _nodeRect = new Rectangle(orig._nodeRect.X + x, y + orig._nodeRect.Y,
+ _sizes.Select((i) => i.Width).Sum() + _paddingOveral.Horizontal,
+ _sizes.Select((i) => i.Height).Max() + _paddingOveral.Vertical);
+
+ for (int i = 0; i < (int)Part.None; ++i)
+ {
+ _paddingInternal[i] = new Padding();
+
+ // Align any parts whose height does not match the total height
+ if (_sizes[i].Height != InnerRect.Height)
+ {
+ _paddingInternal[i].Bottom = (InnerRect.Height - _sizes[i].Height) / 2;
+ _paddingInternal[i].Top = (InnerRect.Height - _sizes[i].Height) - _paddingInternal[i].Bottom;
+
+ // Quick hack to make sure checkboxes are properly aligned, make the rect square again
+ // TODO: use padding/dock modes for this
+ if (i == (int)Part.CheckBox && !_sizes[i].IsEmpty && _sizes[i].IsSquare())
+ {
+ _paddingInternal[i].Left = _paddingInternal[i].Bottom;
+ _paddingInternal[i].Right = _paddingInternal[i].Top;
+ }
+ }
+ }
+ }
+
+ public KTreeNodeMeasurements Offset(int x, int y)
+ {
+ return new KTreeNodeMeasurements(this, x, y);
+ }
+
+ public Size this[Part part]
+ {
+ get { return _sizes[(int)part]; }
+ set { _sizes[(int)part] = value; }
+ }
+
+ public Rectangle NodeRect
+ {
+ get
+ {
+ return _nodeRect;
+ }
+ }
+
+ private Rectangle InnerRect
+ {
+ get
+ {
+ return _nodeRect.Shrink(_paddingOveral);
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ /// If true, returns the rectangle without padding. Otherwise padding is included.
+ ///
+ public Rectangle GetPartRect(Part part, bool inner)
+ {
+ Rectangle r = InnerRect;
+ for (Part i = (Part)0; i < part; ++i)
+ {
+ r.Offset(_sizes[(int)i].Width + _paddingInternal[(int)i].Horizontal, 0);
+ }
+ r.Width = _sizes[(int)part].Width + _paddingInternal[(int)part].Horizontal;
+ if (inner)
+ r = r.Shrink(_paddingInternal[(int)part]);
+ return r;
+ }
+
+ public Part? HitTest(int x)
+ {
+ // Check the parts
+ for (Part i = (Part)0; i < Part.None; ++i)
+ {
+ // TODO: this could be more efficient, but that requires duplicating the layout logic
+ if (GetPartRect(i, false).ContainsX(x))
+ return i;
+ }
+ return Part.None;
+ }
+
+ public override string ToString()
+ {
+ string s = string.Format("Node={0}, Inner={1}", NodeRect, InnerRect);
+ for(Part part = (Part)0; part < Part.None; ++part)
+ {
+ s += string.Format(", {0}={1} ({2}", part, GetPartRect(part, false), _sizes[(int)part]);
+ }
+ return s;
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodes.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodes.cs
new file mode 100644
index 0000000..edf0d81
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeNodes.cs
@@ -0,0 +1,105 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ public class KTreeNodes : ICollection
+ {
+ private readonly List _items = new List();
+ private readonly KTreeNode _parent;
+ private KTree _owner;
+
+ public KTreeNode Parent { get { return _parent; } }
+ public KTree Owner
+ {
+ get
+ {
+ // TODO: this could be cached, but that's tricky with removal of nodes
+ KTreeNodes current = this;
+ while (current != null && current._owner == null)
+ {
+ current = current._parent.ParentNodes;
+ }
+ return current?._owner;
+ }
+ }
+
+ internal KTreeNodes(KTreeNode parent)
+ {
+ this._parent = parent;
+ this._owner = null;
+ }
+
+ internal KTreeNodes(KTree owner)
+ {
+ this._parent = null;
+ this._owner = owner;
+ }
+
+ public int Count { get{return _items.Count;}}
+ public bool IsReadOnly { get { return ((ICollection)_items).IsReadOnly; } }
+
+ public void Add(KTreeNode item)
+ {
+ _items.Add(item);
+ item.ParentNodes = this;
+ Owner?.OnNodeAdded(_parent, item);
+ }
+
+ public void Clear()
+ {
+ Owner?.OnNodeCleared(_parent);
+ _items.Clear();
+ }
+
+ public bool Contains(KTreeNode item)
+ {
+ return _items.Contains(item);
+ }
+
+ public void CopyTo(KTreeNode[] array, int arrayIndex)
+ {
+ _items.CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return ((ICollection)_items).GetEnumerator();
+ }
+
+ public bool Remove(KTreeNode item)
+ {
+ if (!_items.Remove(item))
+ return false;
+ Owner?.OnNodeRemoved(_parent, item);
+ return true;
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return ((ICollection)_items).GetEnumerator();
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRenderer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRenderer.cs
new file mode 100644
index 0000000..ea2a7d3
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRenderer.cs
@@ -0,0 +1,173 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Native;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Windows.Forms.VisualStyles;
+
+namespace Acacia.Controls
+{
+ internal abstract class KTreeRenderer
+ {
+ private Rectangle _clientRect;
+ private Rectangle _totalRect;
+ protected KTree _tree;
+
+ public Rectangle TotalRect { get { return _totalRect; } }
+
+ public void Init(Rectangle clientRect, KTree tree)
+ {
+ this._clientRect = clientRect;
+ this._tree = tree;
+ _totalRect = new Rectangle(0, 0, 0, 0);
+ }
+
+ #region Measuring
+
+ internal KTreeNodeMeasurements MeasureNode(Graphics graphics, KTreeNode node)
+ {
+ // Determine the row rectangle
+ KTreeNodeMeasurements dims = GetNodeSize(graphics, node).Offset(_totalRect.X, _totalRect.Height);
+ node.EffectiveDimension = dims;
+
+ // Set up for the next node
+ _totalRect.Height += dims.NodeRect.Height;
+ _totalRect.Width = Math.Max(_totalRect.Right, dims.NodeRect.Right) - _totalRect.X;
+
+ return dims;
+ }
+
+ protected KTreeNodeMeasurements GetNodeSize(Graphics graphics, KTreeNode node)
+ {
+ KTreeNodeMeasurements dimension = new KTreeNodeMeasurements(node, _tree);
+
+ // Expander
+ dimension[KTreeNodeMeasurements.Part.Expander] = GetExpanderSize(graphics, node);
+
+ // Checkbox
+ if (node.Owner.CheckManager != null && node.HasCheckBox)
+ {
+ dimension[KTreeNodeMeasurements.Part.CheckBox] = CheckBoxRenderer.GetGlyphSize(graphics, CheckBoxState.CheckedNormal);
+ }
+
+ // Image
+ if (_tree.Images != null)
+ {
+ // Image size specified by imagelist
+ // Scale depending on resolution
+ dimension[KTreeNodeMeasurements.Part.Image] = _tree.Images.ImageSize.ScaleDpi(graphics);
+ }
+
+ // Text size
+ dimension[KTreeNodeMeasurements.Part.Text] = TextRenderer.MeasureText(graphics, node.Text, _tree.Font);
+
+ // Control
+ if (node.Control != null)
+ {
+ dimension[KTreeNodeMeasurements.Part.Control] = node.Control.PreferredSize;
+ }
+
+ return dimension;
+ }
+
+ protected abstract Size GetExpanderSize(Graphics graphics, KTreeNode node);
+
+ #endregion
+
+ #region Rendering
+
+ ///
+ ///
+ ///
+ /// The graphics to render into
+ /// The node
+ /// The current scrollbar offset
+ /// If not null, the part of the node that is highlighted. May be Part.None to indicate the row is
+ /// highlighted, but not a specific part
+ public void RenderNode(Graphics graphics, KTreeNode node, Point scrollOffset, KTreeNodeMeasurements.Part? highlight)
+ {
+ // Make sure the node has been measured
+ if (node.EffectiveDimension == null)
+ MeasureNode(graphics, node);
+
+ KTreeNodeMeasurements dims = node.EffectiveDimension.Offset(-scrollOffset.X, -scrollOffset.Y);
+
+ Rectangle containerRect = dims.NodeRect;
+ containerRect.X = _clientRect.X;
+ containerRect.Width = Math.Max(_totalRect.Width, _clientRect.Width);
+
+ // Selection background
+ RenderNodeOutline(graphics, node, _tree.FullRowSelect ? containerRect : dims.NodeRect, highlight);
+
+ // Expander
+ if (node.ChildLoader.NeedsExpander)
+ {
+ RenderNodeExpander(graphics, node, dims.GetPartRect(KTreeNodeMeasurements.Part.Expander, true), highlight);
+ }
+
+ // Checkbox
+ if (_tree.CheckManager != null && node.HasCheckBox)
+ {
+ RenderCheckBox(graphics, node, dims.GetPartRect(KTreeNodeMeasurements.Part.CheckBox, true), highlight);
+ }
+
+ // Images
+ if (_tree.Images != null && node.ImageIndex.HasValue && node.ImageIndex >= 0 && node.ImageIndex < _tree.Images.Images.Count)
+ {
+ Rectangle imageRect = dims.GetPartRect(KTreeNodeMeasurements.Part.Image, true);
+ // TODO: if the rectangle is larger than the image, this probably leads to upscaling.
+ // if the imagelist stores high-res icons as 16x16, that throws away resolution.
+ // make a custom image list to handle this? That could also handle scaling automatically
+ Image image = _tree.Images.Images[node.ImageIndex.Value];
+ graphics.DrawImage(image, imageRect.X, imageRect.Y, imageRect.Width, imageRect.Height);
+ }
+
+ // Text
+ RenderNodeText(graphics, node, dims.GetPartRect(KTreeNodeMeasurements.Part.Text, true), highlight);
+
+ // Control
+ if (node.Control != null)
+ {
+ node.Control.Bounds = dims.GetPartRect(KTreeNodeMeasurements.Part.Control, true);
+ }
+ }
+
+ protected abstract void RenderNodeOutline(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight);
+ internal protected abstract void RenderNodeExpander(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight);
+
+ protected virtual void RenderCheckBox(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+ int state = (int)node.CheckState * 4 + 1;
+ if (highlight != null && highlight.Value == KTreeNodeMeasurements.Part.CheckBox)
+ state += 1;
+
+ CheckBoxRenderer.DrawCheckBox(graphics, rect.Location, (CheckBoxState)state);
+ }
+
+ protected abstract void RenderNodeText(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight);
+
+ public abstract void RenderControlBorder(Graphics graphics, Rectangle rect);
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererDefault.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererDefault.cs
new file mode 100644
index 0000000..73d5289
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererDefault.cs
@@ -0,0 +1,93 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Windows.Forms.VisualStyles;
+
+namespace Acacia.Controls
+{
+ internal class KTreeRendererDefault : KTreeRenderer
+ {
+ private readonly Size _expanderBoxSize = new Size(7, 7);
+
+ protected override Size GetExpanderSize(Graphics graphics, KTreeNode node)
+ {
+ return _expanderBoxSize;
+ }
+
+ internal protected override void RenderNodeExpander(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+ Color color = GetColor(node, highlight);
+
+ using (Pen pen = new Pen(color))
+ {
+ graphics.DrawRectangle(pen, rect.X - 1, rect.Y - 1, _expanderBoxSize.Width + 1, _expanderBoxSize.Height + 1);
+ int y = rect.Y + rect.Height / 2;
+ graphics.DrawLine(pen, rect.X + 1, y, rect.Right - 2, y);
+
+ if (!node.IsExpanded)
+ {
+ int x = rect.X + rect.Width / 2;
+ graphics.DrawLine(pen, x, rect.Y + 1, x, rect.Bottom - 2);
+ }
+ }
+ }
+
+ protected override void RenderNodeOutline(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+
+ if (highlight != null)
+ graphics.FillRectangle(SystemBrushes.FromSystemColor(SystemColors.HotTrack), rect);
+ else if (node.IsSelected)
+ graphics.FillRectangle(SystemBrushes.FromSystemColor(SystemColors.Highlight), rect);
+
+ if (_tree.ActiveNode == node && !node.IsSelected)
+ {
+ graphics.DrawRectangle(SystemPens.FromSystemColor(SystemColors.HotTrack), rect.X, rect.Y, rect.Width - 1, rect.Height - 1);
+ }
+ }
+
+ protected override void RenderNodeText(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+ TextRenderer.DrawText(graphics, node.Text, _tree.Font, rect, GetColor(node, highlight),
+ Color.Transparent, TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
+ }
+
+ private Color GetColor(KTreeNode node, KTreeNodeMeasurements.Part? highlight)
+ {
+ Color color = _tree.ForeColor;
+ if (node.IsSelected)
+ color = SystemColors.HighlightText;
+ else if (highlight != null)
+ color = Color.White;
+ return color;
+ }
+
+ public override void RenderControlBorder(Graphics graphics, Rectangle rect)
+ {
+ using (Pen pen = new Pen(_tree.Enabled ? Color.Black : SystemColors.GrayText))
+ {
+ graphics.DrawRectangle(pen, new Rectangle(rect.X, rect.Y, rect.Width - 1, rect.Height - 1));
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererVisualStyles.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererVisualStyles.cs
new file mode 100644
index 0000000..ca595e3
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KTreeRendererVisualStyles.cs
@@ -0,0 +1,159 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Native;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Drawing;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Windows.Forms.VisualStyles;
+
+namespace Acacia.Controls
+{
+ internal class KTreeRendererVisualStyles : KTreeRenderer
+ {
+ // From vsstyle.h
+ // enum TREEVIEWPARTS
+ // {
+ // TVP_TREEITEM = 1,
+ // TVP_GLYPH = 2,
+ // TVP_BRANCH = 3,
+ // TVP_HOTGLYPH = 4,
+ // };
+ // enum TREEITEMSTATES
+ // {
+ // TREIS_NORMAL = 1,
+ // TREIS_HOT = 2,
+ // TREIS_SELECTED = 3,
+ // TREIS_DISABLED = 4,
+ // TREIS_SELECTEDNOTFOCUS = 5,
+ // TREIS_HOTSELECTED = 6,
+ // };
+ //
+ // enum GLYPHSTATES
+ // {
+ // GLPS_CLOSED = 1,
+ // GLPS_OPENED = 2,
+ // };
+ private const string TREEVIEW = "Explorer::TreeView";
+
+ private readonly VisualStyleRenderer _treeViewItemNormal = new VisualStyleRenderer(TREEVIEW, 1, 1);
+ private readonly VisualStyleRenderer _treeViewItemHot = new VisualStyleRenderer(TREEVIEW, 1, 2);
+ private readonly VisualStyleRenderer _treeViewItemSelected = new VisualStyleRenderer(TREEVIEW, 1, 3);
+ private readonly VisualStyleRenderer _treeViewItemDisabled = new VisualStyleRenderer(TREEVIEW, 1, 4);
+ private readonly VisualStyleRenderer _treeViewItemSelectedNotFocus = new VisualStyleRenderer(TREEVIEW, 1, 5);
+ private readonly VisualStyleRenderer _treeViewItemHotSelected = new VisualStyleRenderer(TREEVIEW, 1, 6);
+ private readonly VisualStyleRenderer _treeViewGlyphClosed = new VisualStyleRenderer(TREEVIEW, 2, 1);
+ private readonly VisualStyleRenderer _treeViewGlyphOpened = new VisualStyleRenderer(TREEVIEW, 2, 2);
+ private readonly VisualStyleRenderer _treeViewGlyphHotClosed = new VisualStyleRenderer(TREEVIEW, 4, 1);
+ private readonly VisualStyleRenderer _treeViewGlyphHotOpened = new VisualStyleRenderer(TREEVIEW, 4, 2);
+
+ private Size? _glyphSize;
+
+ protected override Size GetExpanderSize(Graphics graphics, KTreeNode node)
+ {
+ // Get glyph size if needed
+ if (!_glyphSize.HasValue)
+ _glyphSize = _treeViewGlyphOpened.GetPartSize(graphics, ThemeSizeType.True);
+ return _glyphSize.Value;
+ }
+
+ internal protected override void RenderNodeExpander(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+ if (highlight != null && highlight.Value == KTreeNodeMeasurements.Part.Expander)
+ {
+ if (node.IsExpanded)
+ _treeViewGlyphHotOpened.DrawBackground(graphics, rect);
+ else
+ _treeViewGlyphHotClosed.DrawBackground(graphics, rect);
+ }
+ else
+ {
+ if (node.IsExpanded)
+ _treeViewGlyphOpened.DrawBackground(graphics, rect);
+ else
+ _treeViewGlyphClosed.DrawBackground(graphics, rect);
+ }
+ }
+
+ private VisualStyleRenderer GetStyle(KTreeNode node, KTreeNodeMeasurements.Part? highlight)
+ {
+ if (!_tree.Enabled)
+ {
+ return _treeViewItemDisabled;
+ }
+ else if (highlight != null)
+ {
+ if (node.IsSelected)
+ return _treeViewItemHotSelected;
+ else
+ return _treeViewItemHot;
+ }
+ else
+ {
+ if (node.IsSelected)
+ {
+ if (_tree.Focused)
+ return _treeViewItemSelected;
+ else
+ return _treeViewItemSelectedNotFocus;
+ }
+ else
+ return _treeViewItemNormal;
+ }
+ }
+
+ protected override void RenderNodeOutline(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+ // Draw one pixel too far, to overlap top and bottom borders for a continuous selection
+
+ Rectangle highlightRect = new Rectangle(rect.X, rect.Y, rect.Width, rect.Height + 1);
+ if (_tree.ActiveNode == node && _tree.Focused)
+ {
+ if (node.IsSelected)
+ _treeViewItemHotSelected.DrawBackground(graphics, highlightRect);
+ else
+ _treeViewItemNormal.DrawBackground(graphics, highlightRect);
+ }
+ else if (node.IsSelected || highlight != null)
+ {
+ GetStyle(node, highlight).DrawBackground(graphics, highlightRect);
+ }
+ }
+
+ protected override void RenderNodeText(Graphics graphics, KTreeNode node, Rectangle rect, KTreeNodeMeasurements.Part? highlight)
+ {
+ Color foreColor = GetStyle(node, highlight).GetColor(ColorProperty.TextColor);
+
+ TextRenderer.DrawText(graphics, node.Text, _tree.Font, rect, foreColor, Color.Transparent,
+ TextFormatFlags.Left | TextFormatFlags.VerticalCenter);
+ }
+
+ public override void RenderControlBorder(Graphics graphics, Rectangle rect)
+ {
+ Color color = (_tree.Enabled ? _treeViewItemNormal : _treeViewItemDisabled).GetColor(ColorProperty.BorderColor);
+ using (Pen pen = new Pen(_tree.Enabled ? Color.Black : SystemColors.GrayText))
+ {
+ graphics.DrawRectangle(pen, new Rectangle(rect.X, rect.Y, rect.Width - 1, rect.Height - 1));
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUITask.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUITask.cs
new file mode 100644
index 0000000..ee700ea
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUITask.cs
@@ -0,0 +1,570 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ ///
+ /// UI progress indicator for tasks. All properties will be accessed in the UI thread.
+ ///
+ public interface KUITaskProgress
+ {
+ string BusyText { get; set; }
+
+ ///
+ /// Sets the busy state.
+ ///
+ bool Busy { get; set; }
+
+ ///
+ /// Shows successful completion
+ ///
+ void ShowCompletion(string text);
+
+ ///
+ /// May be set to a cancellation source to allow cancellation.
+ ///
+ CancellationTokenSource Cancellation { get; set; }
+ }
+
+ public interface KUITaskContext
+ {
+ CancellationToken CancellationToken { get; }
+
+ ///
+ /// Adds a number of counts to the busy indicator. Can be invoked from any thread.
+ ///
+ /// The number of busy counts to add or subtract
+ void AddBusy(int count);
+
+ ///
+ /// Sets the busy text. Can be invoked from any thread.
+ ///
+ /// The text
+ void SetBusyText(string text);
+ }
+
+ public class KUITaskBase
+ {
+ #region Execution state
+
+ internal class ExecutionConfig : KUITaskContext
+ {
+ public readonly KUITaskBase Root;
+ internal readonly ConcurrentDictionary Tasks = new ConcurrentDictionary();
+ internal readonly TaskScheduler UIContext;
+ internal readonly CancellationTokenSource _cancel;
+
+ public ExecutionConfig(KUITaskBase root)
+ {
+ this.Root = root;
+ Tasks.TryAdd(Root, false);
+
+ // Determine the UI context, creating a new one if required
+ if (SynchronizationContext.Current == null)
+ SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
+ UIContext = TaskScheduler.FromCurrentSynchronizationContext();
+
+ // Create a cancellation source
+ _cancel = new CancellationTokenSource();
+ }
+
+ public CancellationToken CancellationToken { get { return _cancel.Token; } }
+
+ #region Busy indication
+
+ public KUITaskProgress Progress { get; set; }
+
+ private int _busyCount;
+ public void AddBusy(int count)
+ {
+ if (count == 0)
+ return;
+
+ bool oldBusy = _busyCount != 0;
+ _busyCount += count;
+
+ bool busy = _busyCount != 0;
+ if (oldBusy != busy)
+ {
+ // TODO: will the synchronisation context always point to the windows forms one, or can someone mess with it?
+ SynchronizationContext.Current.Send((b) =>
+ {
+ bool isBusy = (bool)b;
+ Progress.Busy = isBusy;
+ if (!isBusy)
+ Progress.Cancellation = null;
+ }, busy);
+ }
+ }
+
+ public int BusyCount { get { return _busyCount; } }
+
+ public void SetBusyText(string text)
+ {
+ SynchronizationContext.Current.Send((t) =>
+ {
+ Progress.BusyText = (string)t;
+ }, text);
+ }
+
+ internal void TaskFinished(KUITaskBase task)
+ {
+ Tasks.TryUpdate(task, true, false);
+ }
+
+ #endregion
+ }
+
+ internal class ExecutionState
+ {
+ public readonly ExecutionConfig Config;
+ private readonly object _result;
+ private readonly Exception _exception;
+
+ internal ExecutionState(ExecutionConfig config, object result, Exception exception)
+ {
+ this.Config = config;
+ this._result = result;
+ this._exception = exception;
+ }
+
+ internal ExecutionState NewVoid()
+ {
+ return new ExecutionState(Config, null, null);
+ }
+
+ internal ExecutionState NewResult(object result)
+ {
+ return new ExecutionState(Config, result, null);
+ }
+
+ internal ExecutionState NewException(Exception e)
+ {
+ return new ExecutionState(Config, null, e);
+ }
+
+ internal bool HasException
+ {
+ get { return _exception != null; }
+ }
+
+ internal object GetResult(TaskExecutor.Options options)
+ {
+ if ((options & TaskExecutor.Options.ErrorOnly) != 0)
+ return _exception;
+ else
+ return _result;
+ }
+ }
+
+ #endregion
+
+ #region Executor
+
+ internal protected class TaskExecutor
+ {
+ [Flags]
+ public enum Options
+ {
+ None = 0,
+ UIContext = 1,
+ ErrorOnly = 2,
+ SuccessOnly = 4
+ }
+
+ private readonly Func _action;
+ private readonly Options _options;
+
+ internal TaskExecutor(Func action, Options options)
+ {
+ this._action = action;
+ this._options = options;
+ }
+
+ internal static Options OptionHelper(bool errorOnly, bool successOnly, bool inUI)
+ {
+ Options options = Options.None;
+ if (errorOnly)
+ options |= Options.ErrorOnly;
+ if (successOnly)
+ options |= Options.SuccessOnly;
+ if (inUI)
+ options |= Options.UIContext;
+ return options;
+ }
+
+ internal Task Execute(KUITaskBase task, ExecutionState state)
+ {
+ Func action = () =>
+ {
+ ExecutionState result = state;
+
+ // TODO: do this outside the task. However, that requires returning some kind of task
+ bool execute = true;
+ if ((_options & Options.ErrorOnly) != 0 && !state.HasException)
+ execute = false;
+ else if ((_options & Options.SuccessOnly) != 0 && state.HasException)
+ execute = false;
+
+ // Always clean up one busy count when the task finishes
+ int busyCountDiff = -1;
+ if (execute)
+ {
+ int busyCountBefore = state.Config.BusyCount;
+
+ try
+ {
+ result = _action(state);
+ }
+ catch (Exception e)
+ {
+ result = state.NewException(e);
+
+ // If there is an exception, restore the busy count
+ busyCountDiff -= state.Config.BusyCount - busyCountBefore;
+ }
+ }
+
+ state.Config.TaskFinished(task);
+ state.Config.AddBusy(busyCountDiff);
+ return result;
+ };
+
+ return Task.Factory.StartNew(action, state.Config.CancellationToken, TaskCreationOptions.None, GetContext(state));
+ }
+
+ private TaskScheduler GetContext(ExecutionState state)
+ {
+ return (_options & Options.UIContext) != 0 ? state.Config.UIContext : TaskScheduler.Default;
+ }
+
+ #region Creators
+
+ // Passes nothing
+ public static TaskExecutor Void(Action action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ action();
+ return s.NewVoid();
+ }, options);
+ }
+ public static TaskExecutor Void(Func action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ ResultType result = action();
+ return s.NewResult(result);
+ }, options);
+ }
+
+ // Passes the a parameter
+ public static TaskExecutor Param(Func action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ ResultType result = action((ParamType)s.GetResult(options));
+ return s.NewResult(result);
+ }, options);
+ }
+ public static TaskExecutor Param(Action action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ action((ParamType)s.GetResult(options));
+ return s.NewVoid();
+ }, options);
+ }
+
+ // Passes the task context
+ public static TaskExecutor TaskContext(Func action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ ResultType result = action(s.Config);
+ return s.NewResult(result);
+ }, options);
+ }
+ public static TaskExecutor TaskContext(Action action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ action(s.Config);
+ return s.NewVoid();
+ }, options);
+ }
+
+ // Passes the task context and a parameter
+ public static TaskExecutor TaskContextParam(Func action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ ResultType result = action(s.Config, (ParamType)s.GetResult(options));
+ return s.NewResult(result);
+ }, options);
+ }
+ public static TaskExecutor TaskContextParam(Action action, Options options)
+ {
+ return new TaskExecutor((s) =>
+ {
+ action(s.Config, (ParamType)s.GetResult(options));
+ return s.NewVoid();
+ }, options);
+ }
+
+ #endregion
+ }
+
+ #endregion
+
+ protected readonly TaskExecutor _executor;
+ private ExecutionConfig _config;
+ private readonly Mutex _mutexTask = new Mutex();
+ private Task _task;
+ private readonly List _next = new List();
+
+ protected KUITaskBase(TaskExecutor exec, bool isRoot)
+ {
+ this._executor = exec;
+ this._config = isRoot ? new ExecutionConfig(this) : null;
+ }
+
+ public void Start(KUITaskProgress progress = null)
+ {
+ _config.Progress = progress ?? new DummyTaskProgress();
+ _config.Progress.Cancellation = _config._cancel;
+ _config.AddBusy(_config.Tasks.Count);
+
+ // Execute the root
+ _config.Root.DoStart(new ExecutionState(_config, null, null));
+ }
+
+ private void DoStart(ExecutionState state)
+ {
+ // Make sure we're not already started
+ if (_task != null)
+ throw new InvalidOperationException("Task chain already started");
+
+ // Start the task
+ _mutexTask.WaitOne();
+ try
+ {
+ _task = _executor.Execute(this, state);
+
+ // TODO: this could probably be outside the mutex
+ foreach (KUITaskBase next in _next)
+ AddContinuation(next);
+ }
+ finally
+ {
+ _mutexTask.ReleaseMutex();
+ }
+ }
+
+ protected TaskType Chain(TaskType next)
+ where TaskType : KUITaskBase
+ {
+ next._config = _config;
+ _config.Tasks.TryAdd(next, false);
+
+ _mutexTask.WaitOne();
+ try
+ {
+ // If the task is already started (or finished), add a chainer to that
+ // Otherwise, add it to the list
+ if (_task == null)
+ {
+ this._next.Add(next);
+ }
+ else
+ {
+ AddContinuation(next);
+ }
+ }
+ finally
+ {
+ _mutexTask.ReleaseMutex();
+ }
+ return next;
+ }
+
+ private void AddContinuation(KUITaskBase next)
+ {
+ // Start a synchronous task, the KUITask will detach if needed
+ _task.ContinueWith((prevTask) =>
+ {
+ next.DoStart(prevTask.Result);
+ }, _config.CancellationToken, TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled,
+ TaskScheduler.Current);
+ }
+ }
+
+ internal class DummyTaskProgress : KUITaskProgress
+ {
+ public bool Busy
+ {
+ get;
+ set;
+ }
+
+ public string BusyText
+ {
+ get;
+ set;
+ }
+
+ public CancellationTokenSource Cancellation
+ {
+ get;
+ set;
+ }
+
+ public void ShowCompletion(string text)
+ {
+ }
+ }
+
+ ///
+ /// Wrapper class for a chain of tasks with UI feedback
+ ///
+ ///
+ public class KUITask : KUITaskBase
+ {
+ internal protected KUITask(TaskExecutor exec, bool isRoot) : base(exec, isRoot)
+ {
+ }
+
+ #region Chainers
+
+ ///
+ /// Invoked on either success or error.
+ ///
+ public KUITask OnCompletion(Action func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.Param(func, TaskExecutor.OptionHelper(false, false, inUI)), false));
+ }
+
+ // OnSuccess - Can either return nothing or a new result type (which could of course happen to be
+ // the current.
+ // Can accept Context and the current task result
+
+ public KUITask OnSuccess(Action func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.Void(func, TaskExecutor.OptionHelper(false, true, inUI)), false));
+ }
+
+ public KUITask OnSuccess(Action func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.TaskContext(func, TaskExecutor.OptionHelper(false, true, inUI)), false));
+ }
+
+ public KUITask OnSuccess(Action func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.TaskContextParam(func, TaskExecutor.OptionHelper(false, true, inUI)), false));
+ }
+
+ public KUITask OnSuccess(Func func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.Param(func, TaskExecutor.OptionHelper(false, true, inUI)), false));
+ }
+
+ // OnError - Can either return nothing, or the already expected return type. This allows an OnCompletion
+ // handler accepting either a result, or a dummy result returned by the error handler
+ // TODO: accept Context
+
+ public KUITask OnError(Func func, bool inUI = true)
+ {
+ return Chain(new KUITask(TaskExecutor.Param(func, TaskExecutor.OptionHelper(true, false, inUI)), false));
+ }
+
+ public KUITask OnError(Action func, bool inUI = true)
+ {
+ return Chain(new KUITask(TaskExecutor.Param(func, TaskExecutor.OptionHelper(true, false, inUI)), false));
+ }
+
+ #endregion
+ }
+
+ public class KUITask : KUITaskBase
+ {
+ internal protected KUITask(TaskExecutor exec, bool isRoot) : base(exec, isRoot)
+ {
+ }
+
+ #region Chainers
+
+ public KUITask OnError(Action func, bool inUI = true)
+ {
+ return Chain(new KUITask(TaskExecutor.Param(func, TaskExecutor.OptionHelper(true, false, inUI)), false));
+ }
+
+ public KUITask OnSuccess(Action func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.Void(func, TaskExecutor.OptionHelper(false, true, inUI)), false));
+ }
+
+ public KUITask OnSuccess(Action func, bool inUI = false)
+ {
+ return Chain(new KUITask(TaskExecutor.TaskContext(func, TaskExecutor.OptionHelper(false, true, inUI)), false));
+ }
+
+ #endregion
+
+ #region Factory methods
+
+ public static KUITask New(Action action)
+ {
+ return new KUITask(TaskExecutor.TaskContext(action, TaskExecutor.Options.None), true);
+ }
+
+ public static KUITask New(Action action)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static KUITask New(Func action)
+ {
+ return new KUITask(TaskExecutor.TaskContext(action, TaskExecutor.Options.None), true);
+ }
+
+ public static KUITask New(Func action)
+ {
+ return new KUITask(TaskExecutor.Void(action, TaskExecutor.Options.None), true);
+ }
+
+ #endregion
+ }
+
+ public interface KUITaskExecutor
+ {
+ ///
+ /// Executes a task
+ ///
+ /// The text to display while the task is busy
+ /// The action
+ /// A task for the action
+ KUITask Execute(string busyText, Func action);
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs
new file mode 100644
index 0000000..9e9eb86
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs
@@ -0,0 +1,80 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Controls
+{
+ internal static class KUIUtil
+ {
+ #region Geometry
+
+ public static Rectangle Center(this Rectangle _this, Size size)
+ {
+ int x = _this.X + (_this.Width - size.Width) / 2;
+ int y = _this.Y + (_this.Height - size.Height) / 2;
+ return new Rectangle(x, y, size.Width, size.Height);
+ }
+
+ public static Rectangle Expand(this Rectangle _this, Padding padding)
+ {
+ Rectangle r = _this;
+ r.X -= padding.Left;
+ r.Y -= padding.Top;
+ r.Width += padding.Horizontal;
+ r.Height += padding.Vertical;
+ return r;
+ }
+
+ public static Rectangle Shrink(this Rectangle _this, Padding padding)
+ {
+ Rectangle r = _this;
+ r.X += padding.Left;
+ r.Y += padding.Top;
+ r.Width -= padding.Horizontal;
+ r.Height -= padding.Vertical;
+ return r;
+ }
+
+ public static bool ContainsX(this Rectangle _this, int x)
+ {
+ return (x >= _this.X && x < _this.Right);
+ }
+
+ public static bool ContainsY(this Rectangle _this, int y)
+ {
+ return (y >= _this.Y && y < _this.Bottom);
+ }
+
+ public static bool IsSquare(this Size _this)
+ {
+ return _this.Width == _this.Height;
+ }
+
+ public static Size ScaleDpi(this Size _this, Graphics graphics)
+ {
+ return new Size((int)(_this.Width * graphics.DpiX / 96), (int)(_this.Height * graphics.DpiY / 96));
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs
new file mode 100644
index 0000000..a5b468c
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs
@@ -0,0 +1,224 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Utils;
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Acacia
+{
+ public static class DebugOptions
+ {
+ abstract public class Option
+ {
+ public readonly string Token;
+
+ public Option(string token)
+ {
+ this.Token = token;
+ }
+
+ abstract public string GetToken(ValueType value);
+ abstract public ValueType GetValue(string value);
+ }
+
+ public class BoolOption : Option
+ {
+ public readonly bool Inverse;
+
+ public BoolOption(string token, bool inverse)
+ :
+ base(inverse ? "-" + token : (token.Length == 0 ? "+" : token))
+ {
+ this.Inverse = inverse;
+ }
+
+ public override string GetToken(bool value)
+ {
+ if (Inverse)
+ value = !value;
+ if (value)
+ return Token;
+ else
+ return null;
+ }
+
+ public override bool GetValue(string value)
+ {
+ bool enabled = value == Token;
+ if (Inverse)
+ enabled = !enabled;
+ return enabled;
+ }
+ }
+
+ public class EnumOption : Option
+ {
+ private EnumType DefaultValue
+ {
+ get
+ {
+ return (EnumType)typeof(EnumType).GetEnumValues().GetValue(0);
+ }
+ }
+
+ public EnumOption(string token)
+ :
+ base(token)
+ {
+ }
+
+ public override string GetToken(EnumType value)
+ {
+ if (value.Equals(DefaultValue))
+ return null;
+ return Token + "=" + value.ToString();
+ }
+
+ public override EnumType GetValue(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return DefaultValue;
+ else
+ {
+ if (value.ToLower().StartsWith(Token.ToLower() + "="))
+ value = value.Substring(Token.Length + 1);
+ return (EnumType)Enum.Parse(typeof(EnumType), value, true);
+ }
+ }
+
+ }
+
+ public class TimeSpanOption : Option
+ {
+ private readonly TimeSpan _defaultValue;
+
+ public TimeSpanOption(string token, TimeSpan defaultValue)
+ :
+ base(token)
+ {
+ this._defaultValue = defaultValue;
+ }
+
+ public override string GetToken(TimeSpan value)
+ {
+ if (value.Equals(_defaultValue))
+ return null;
+ return Token + "=" + value.ToString();
+ }
+
+ public override TimeSpan GetValue(string value)
+ {
+ if (string.IsNullOrEmpty(value))
+ return _defaultValue;
+ else
+ {
+ if (value.ToLower().StartsWith(Token.ToLower() + "="))
+ value = value.Substring(Token.Length + 1);
+ return TimeSpan.Parse(value);
+ }
+ }
+
+ }
+
+ // General
+ public static readonly BoolOption ENABLED = new BoolOption("", true);
+ public static readonly BoolOption FEATURE_DISABLED_DEFAULT = new BoolOption("", false);
+ public static readonly BoolOption OUTLOOK_UI = new BoolOption("UI", true);
+ public static readonly BoolOption OUTLOOK_UI_RIBBON = new BoolOption("Ribbon", true);
+ public static readonly BoolOption OUTLOOK_UI_CONTEXT_MENU = new BoolOption("ContextMenu", true);
+ public static readonly BoolOption WATCHER_ENABLED = new BoolOption("Watcher", true);
+
+ ///
+ /// Allows all options to return defaults, for testing
+ ///
+ public static bool ReturnDefaults
+ {
+ get;
+ set;
+ }
+
+ ///
+ /// The threading model
+ ///
+ public enum Threading
+ {
+ MainThread,
+ Background,
+ Synchronous
+ }
+
+ #region Access methods
+
+ public static string GetOptions(string prefix)
+ {
+ if (ReturnDefaults)
+ return null;
+
+ return RegistryUtil.GetConfigValue(prefix, null, null);
+ }
+
+ public static ValueType GetOption(string prefix, Option option)
+ {
+ // Parse the options
+ Dictionary tokens = ParseTokens(prefix);
+ string value;
+ tokens.TryGetValue(option.Token.ToLower(), out value);
+ return option.GetValue(value);
+ }
+
+ private static Dictionary ParseTokens(string prefix)
+ {
+ Dictionary tokens = new Dictionary();
+ string value = GetOptions(prefix);
+ if (!string.IsNullOrEmpty(value))
+ {
+ foreach (string token in value.Split(','))
+ {
+ if (!string.IsNullOrEmpty(token))
+ {
+ string[] keyVal = token.Split(new[] { '=' }, 2);
+ if (!string.IsNullOrEmpty(keyVal[0]))
+ {
+ tokens[keyVal[0].ToLower()] = token;
+ }
+ }
+ }
+ }
+ return tokens;
+ }
+
+ public static void SetOption(string prefix, Option option, ValueType value)
+ {
+ Dictionary tokens = ParseTokens(prefix);
+
+ // Update the token
+ string token = option.GetToken(value);
+ if (token != null)
+ tokens[option.Token.ToLower()] = token;
+ else
+ tokens.Remove(option.Token.ToLower());
+
+ // Write to registry
+ string newValue = string.Join(",", tokens.Values);
+ RegistryUtil.SetConfigValue(prefix, null, newValue, RegistryValueKind.String);
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.Designer.cs
new file mode 100644
index 0000000..27a9e0e
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.Designer.cs
@@ -0,0 +1,192 @@
+namespace Acacia.Features.DebugSupport
+{
+ partial class AboutDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(AboutDialog));
+ this._layout = new System.Windows.Forms.TableLayoutPanel();
+ this._layoutForm = new System.Windows.Forms.TableLayoutPanel();
+ this.labelDateValue = new Acacia.Controls.KCopyLabel();
+ this.labelRevisionValue = new Acacia.Controls.KCopyLabel();
+ this.icon = new System.Windows.Forms.PictureBox();
+ this.labelTitle = new System.Windows.Forms.Label();
+ this.labelVersionCaption = new Acacia.Controls.KCopyLabel();
+ this.labelRevisionCaption = new Acacia.Controls.KCopyLabel();
+ this.richTextBox1 = new System.Windows.Forms.RichTextBox();
+ this.labelDateCaption = new Acacia.Controls.KCopyLabel();
+ this.linkKopano = new System.Windows.Forms.LinkLabel();
+ this.labelVersionValue = new Acacia.Controls.KCopyLabel();
+ this._buttons = new Acacia.Controls.KDialogButtons();
+ this._layout.SuspendLayout();
+ this._layoutForm.SuspendLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.icon)).BeginInit();
+ this.SuspendLayout();
+ //
+ // _layout
+ //
+ resources.ApplyResources(this._layout, "_layout");
+ this._layout.Controls.Add(this._layoutForm, 0, 0);
+ this._layout.Controls.Add(this._buttons, 0, 1);
+ this._layout.Name = "_layout";
+ //
+ // _layoutForm
+ //
+ resources.ApplyResources(this._layoutForm, "_layoutForm");
+ this._layoutForm.Controls.Add(this.labelDateValue, 1, 4);
+ this._layoutForm.Controls.Add(this.labelRevisionValue, 1, 3);
+ this._layoutForm.Controls.Add(this.icon, 0, 0);
+ this._layoutForm.Controls.Add(this.labelTitle, 1, 0);
+ this._layoutForm.Controls.Add(this.labelVersionCaption, 0, 2);
+ this._layoutForm.Controls.Add(this.labelRevisionCaption, 0, 3);
+ this._layoutForm.Controls.Add(this.richTextBox1, 0, 6);
+ this._layoutForm.Controls.Add(this.labelDateCaption, 0, 4);
+ this._layoutForm.Controls.Add(this.linkKopano, 1, 1);
+ this._layoutForm.Controls.Add(this.labelVersionValue, 1, 2);
+ this._layoutForm.Name = "_layoutForm";
+ //
+ // labelDateValue
+ //
+ this.labelDateValue.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ resources.ApplyResources(this.labelDateValue, "labelDateValue");
+ this.labelDateValue.Name = "labelDateValue";
+ this.labelDateValue.ReadOnly = true;
+ this.labelDateValue.TabStop = false;
+ //
+ // labelRevisionValue
+ //
+ this.labelRevisionValue.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ resources.ApplyResources(this.labelRevisionValue, "labelRevisionValue");
+ this.labelRevisionValue.Name = "labelRevisionValue";
+ this.labelRevisionValue.ReadOnly = true;
+ this.labelRevisionValue.TabStop = false;
+ //
+ // icon
+ //
+ resources.ApplyResources(this.icon, "icon");
+ this.icon.Name = "icon";
+ this.icon.TabStop = false;
+ //
+ // labelTitle
+ //
+ resources.ApplyResources(this.labelTitle, "labelTitle");
+ this.labelTitle.Name = "labelTitle";
+ //
+ // labelVersionCaption
+ //
+ this.labelVersionCaption.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ resources.ApplyResources(this.labelVersionCaption, "labelVersionCaption");
+ this.labelVersionCaption.Name = "labelVersionCaption";
+ this.labelVersionCaption.ReadOnly = true;
+ this.labelVersionCaption.TabStop = false;
+ //
+ // labelRevisionCaption
+ //
+ this.labelRevisionCaption.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ resources.ApplyResources(this.labelRevisionCaption, "labelRevisionCaption");
+ this.labelRevisionCaption.Name = "labelRevisionCaption";
+ this.labelRevisionCaption.ReadOnly = true;
+ this.labelRevisionCaption.TabStop = false;
+ //
+ // richTextBox1
+ //
+ this.richTextBox1.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ this._layoutForm.SetColumnSpan(this.richTextBox1, 2);
+ resources.ApplyResources(this.richTextBox1, "richTextBox1");
+ this.richTextBox1.Name = "richTextBox1";
+ this.richTextBox1.ReadOnly = true;
+ this.richTextBox1.LinkClicked += new System.Windows.Forms.LinkClickedEventHandler(this.richTextBox1_LinkClicked);
+ //
+ // labelDateCaption
+ //
+ this.labelDateCaption.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ resources.ApplyResources(this.labelDateCaption, "labelDateCaption");
+ this.labelDateCaption.Name = "labelDateCaption";
+ this.labelDateCaption.ReadOnly = true;
+ this.labelDateCaption.TabStop = false;
+ //
+ // linkKopano
+ //
+ resources.ApplyResources(this.linkKopano, "linkKopano");
+ this.linkKopano.Name = "linkKopano";
+ this.linkKopano.TabStop = true;
+ this.linkKopano.VisitedLinkColor = System.Drawing.Color.Blue;
+ this.linkKopano.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkKopano_LinkClicked);
+ //
+ // labelVersionValue
+ //
+ this.labelVersionValue.BorderStyle = System.Windows.Forms.BorderStyle.None;
+ resources.ApplyResources(this.labelVersionValue, "labelVersionValue");
+ this.labelVersionValue.Name = "labelVersionValue";
+ this.labelVersionValue.ReadOnly = true;
+ this.labelVersionValue.TabStop = false;
+ //
+ // _buttons
+ //
+ resources.ApplyResources(this._buttons, "_buttons");
+ this._buttons.ButtonSize = null;
+ this._buttons.Cancellation = null;
+ this._buttons.HasApply = false;
+ this._buttons.IsDirty = false;
+ this._buttons.Name = "_buttons";
+ //
+ // AboutDialog
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.Controls.Add(this._layout);
+ this.MinimizeBox = false;
+ this.Name = "AboutDialog";
+ this.ShowInTaskbar = false;
+ this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show;
+ this.TopMost = true;
+ this._layout.ResumeLayout(false);
+ this._layout.PerformLayout();
+ this._layoutForm.ResumeLayout(false);
+ this._layoutForm.PerformLayout();
+ ((System.ComponentModel.ISupportInitialize)(this.icon)).EndInit();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel _layout;
+ private System.Windows.Forms.TableLayoutPanel _layoutForm;
+ private Controls.KDialogButtons _buttons;
+ private System.Windows.Forms.PictureBox icon;
+ private System.Windows.Forms.Label labelTitle;
+ private Controls.KCopyLabel labelVersionCaption;
+ private Controls.KCopyLabel labelDateValue;
+ private Controls.KCopyLabel labelRevisionValue;
+ private Controls.KCopyLabel labelRevisionCaption;
+ private Controls.KCopyLabel labelDateCaption;
+ private System.Windows.Forms.LinkLabel linkKopano;
+ private System.Windows.Forms.RichTextBox richTextBox1;
+ private Controls.KCopyLabel labelVersionValue;
+ }
+}
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.cs
new file mode 100644
index 0000000..81fee1f
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.cs
@@ -0,0 +1,59 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Utils;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Collections;
+using Acacia.ZPush;
+using System.Reflection;
+using Acacia.UI;
+using Acacia.Controls;
+
+namespace Acacia.Features.DebugSupport
+{
+ public partial class AboutDialog : KDialogNew
+ {
+
+ public AboutDialog()
+ {
+ InitializeComponent();
+ icon.Image = Properties.Resources.Kopano.ToBitmap();
+ labelVersionValue.Text = BuildVersions.VERSION;
+ labelRevisionValue.Text = BuildVersions.REVISION;
+ labelDateValue.Text = LibUtils.BuildTime.ToString();
+ }
+
+ private void linkKopano_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
+ {
+ System.Diagnostics.Process.Start(linkKopano.Text);
+ }
+
+ private void richTextBox1_LinkClicked(object sender, LinkClickedEventArgs e)
+ {
+ System.Diagnostics.Process.Start(e.LinkText);
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.resx
new file mode 100644
index 0000000..94107b3
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/AboutDialog.resx
@@ -0,0 +1,525 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ 1
+
+
+ 2
+
+
+
+ Fill
+
+
+ NoControl
+
+
+
+ 73, 132
+
+
+ 355, 15
+
+
+ 7
+
+
+ labelDateValue
+
+
+ Acacia.Controls.KCopyLabel, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layoutForm
+
+
+ 0
+
+
+ Fill
+
+
+ NoControl
+
+
+ 73, 111
+
+
+ 355, 15
+
+
+ 6
+
+
+ labelRevisionValue
+
+
+ Acacia.Controls.KCopyLabel, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layoutForm
+
+
+ 1
+
+
+ Fill
+
+
+ 3, 3
+
+
+ 64, 64
+
+
+ AutoSize
+
+
+ 0
+
+
+ icon
+
+
+ System.Windows.Forms.PictureBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ _layoutForm
+
+
+ 2
+
+
+ True
+
+
+ Fill
+
+
+ Microsoft Sans Serif, 16.2pt
+
+
+ 73, 0
+
+
+ 355, 70
+
+
+ 1
+
+
+ Kopano OL Extension
+
+
+ MiddleLeft
+
+
+ labelTitle
+
+
+ System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ _layoutForm
+
+
+ 3
+
+
+ 3, 90
+
+
+ 56, 15
+
+
+ 1
+
+
+ Version
+
+
+ labelVersionCaption
+
+
+ Acacia.Controls.KCopyLabel, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layoutForm
+
+
+ 4
+
+
+ NoControl
+
+
+ 3, 111
+
+
+ 62, 15
+
+
+ 4
+
+
+ Revision
+
+
+ labelRevisionCaption
+
+
+ Acacia.Controls.KCopyLabel, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layoutForm
+
+
+ 5
+
+
+ Fill
+
+
+ 3, 173
+
+
+ 425, 272
+
+
+ 9
+
+
+ Copyright 2016 Kopano b.v.
+
+This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License, version 3, as published by the Free Software Foundation.
+
+This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>
+
+
+ richTextBox1
+
+
+ System.Windows.Forms.RichTextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ _layoutForm
+
+
+ 6
+
+
+ 3, 132
+
+
+ 38, 15
+
+
+ 5
+
+
+ Date
+
+
+ labelDateCaption
+
+
+ Acacia.Controls.KCopyLabel, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layoutForm
+
+
+ 7
+
+
+ True
+
+
+ 73, 70
+
+
+ 132, 17
+
+
+ 8
+
+
+ https://kopano.com/
+
+
+ linkKopano
+
+
+ System.Windows.Forms.LinkLabel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ _layoutForm
+
+
+ 8
+
+
+ Fill
+
+
+ 73, 90
+
+
+ 355, 15
+
+
+ 10
+
+
+ labelVersionValue
+
+
+ Acacia.Controls.KCopyLabel, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layoutForm
+
+
+ 9
+
+
+ Fill
+
+
+ 3, 3
+
+
+ 7
+
+
+ 431, 448
+
+
+ 0
+
+
+ _layoutForm
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ _layout
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="labelDateValue" Row="4" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="labelRevisionValue" Row="3" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="icon" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="labelTitle" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="labelVersionCaption" Row="2" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="labelRevisionCaption" Row="3" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="richTextBox1" Row="6" RowSpan="1" Column="0" ColumnSpan="2" /><Control Name="labelDateCaption" Row="4" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="linkKopano" Row="1" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="labelVersionValue" Row="2" RowSpan="1" Column="1" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,Percent,100" /><Rows Styles="AutoSize,0,AutoSize,0,AutoSize,0,AutoSize,0,AutoSize,0,Absolute,20,Percent,100" /></TableLayoutSettings>
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ Fill
+
+
+ 3, 456
+
+
+ 3, 2, 3, 2
+
+
+ 431, 39
+
+
+ 1
+
+
+ _buttons
+
+
+ Acacia.Controls.KDialogButtons, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
+ _layout
+
+
+ 1
+
+
+ Fill
+
+
+ 8, 7
+
+
+ 2
+
+
+ 437, 497
+
+
+ 0
+
+
+ _layout
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="_layoutForm" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="_buttons" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /></Controls><Columns Styles="Percent,100" /><Rows Styles="Percent,100,AutoSize,0" /></TableLayoutSettings>
+
+
+ True
+
+
+ 8, 16
+
+
+ True
+
+
+ 453, 511
+
+
+ 8, 7, 8, 7
+
+
+ CenterParent
+
+
+ About Kopano OL Extenion
+
+
+ AboutDialog
+
+
+ Acacia.Controls.KDialogNew, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.Designer.cs
new file mode 100644
index 0000000..280828f
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.Designer.cs
@@ -0,0 +1,126 @@
+namespace Acacia.Features.DebugSupport
+{
+ partial class DebugDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DebugDialog));
+ this.tableMain = new System.Windows.Forms.TableLayoutPanel();
+ this.flowButtons = new System.Windows.Forms.FlowLayoutPanel();
+ this.buttonGC = new System.Windows.Forms.Button();
+ this.buttonRefresh = new System.Windows.Forms.Button();
+ this.buttonClose = new System.Windows.Forms.Button();
+ this.buttonLog = new System.Windows.Forms.Button();
+ this.Properties = new System.Windows.Forms.PropertyGrid();
+ this.tableMain.SuspendLayout();
+ this.flowButtons.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // tableMain
+ //
+ resources.ApplyResources(this.tableMain, "tableMain");
+ this.tableMain.Controls.Add(this.flowButtons, 0, 1);
+ this.tableMain.Controls.Add(this.Properties, 0, 0);
+ this.tableMain.Name = "tableMain";
+ //
+ // flowButtons
+ //
+ resources.ApplyResources(this.flowButtons, "flowButtons");
+ this.flowButtons.Controls.Add(this.buttonGC);
+ this.flowButtons.Controls.Add(this.buttonRefresh);
+ this.flowButtons.Controls.Add(this.buttonClose);
+ this.flowButtons.Controls.Add(this.buttonLog);
+ this.flowButtons.Name = "flowButtons";
+ //
+ // buttonGC
+ //
+ resources.ApplyResources(this.buttonGC, "buttonGC");
+ this.buttonGC.Name = "buttonGC";
+ this.buttonGC.UseVisualStyleBackColor = true;
+ this.buttonGC.Click += new System.EventHandler(this.buttonGC_Click);
+ //
+ // buttonRefresh
+ //
+ resources.ApplyResources(this.buttonRefresh, "buttonRefresh");
+ this.buttonRefresh.Name = "buttonRefresh";
+ this.buttonRefresh.UseVisualStyleBackColor = true;
+ this.buttonRefresh.Click += new System.EventHandler(this.buttonRefresh_Click);
+ //
+ // buttonClose
+ //
+ resources.ApplyResources(this.buttonClose, "buttonClose");
+ this.buttonClose.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.buttonClose.Name = "buttonClose";
+ this.buttonClose.UseVisualStyleBackColor = true;
+ this.buttonClose.Click += new System.EventHandler(this.buttonClose_Click);
+ //
+ // buttonLog
+ //
+ resources.ApplyResources(this.buttonLog, "buttonLog");
+ this.buttonLog.Name = "buttonLog";
+ this.buttonLog.UseVisualStyleBackColor = true;
+ this.buttonLog.Click += new System.EventHandler(this.buttonLog_Click);
+ //
+ // Properties
+ //
+ resources.ApplyResources(this.Properties, "Properties");
+ this.Properties.DisabledItemForeColor = System.Drawing.SystemColors.ControlText;
+ this.Properties.Name = "Properties";
+ this.Properties.PropertySort = System.Windows.Forms.PropertySort.Categorized;
+ this.Properties.ToolbarVisible = false;
+ //
+ // DebugDialog
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.buttonClose;
+ this.Controls.Add(this.tableMain);
+ this.MinimizeBox = false;
+ this.Name = "DebugDialog";
+ this.ShowInTaskbar = false;
+ this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show;
+ this.TopMost = true;
+ this.tableMain.ResumeLayout(false);
+ this.tableMain.PerformLayout();
+ this.flowButtons.ResumeLayout(false);
+ this.flowButtons.PerformLayout();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel tableMain;
+ private System.Windows.Forms.FlowLayoutPanel flowButtons;
+ private System.Windows.Forms.Button buttonGC;
+ private System.Windows.Forms.PropertyGrid Properties;
+ private System.Windows.Forms.Button buttonRefresh;
+ private System.Windows.Forms.Button buttonClose;
+ private System.Windows.Forms.Button buttonLog;
+ }
+}
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.cs
new file mode 100644
index 0000000..8b057dc
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.cs
@@ -0,0 +1,117 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Utils;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Diagnostics;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Collections;
+using Acacia.ZPush;
+using System.Reflection;
+using Acacia.UI;
+
+namespace Acacia.Features.DebugSupport
+{
+ public partial class DebugDialog : KopanoDialog
+ {
+
+ public DebugDialog()
+ {
+ InitializeComponent();
+ Properties.SelectedObject = new DebugInfo();
+ }
+
+ private void UpdateFields()
+ {
+ Properties.Refresh();
+ }
+
+ #region Logging
+
+ private const string INDENT = "+";
+
+ private void ToLog()
+ {
+ // Create a new property grid and expand it, to access all items
+ // This beats implementing the property logic to fetch all of item.
+ PropertyGrid grid = new PropertyGrid();
+ grid.SelectedObject = Properties.SelectedObject;
+ grid.ExpandAllGridItems();
+ object view = grid.GetType().GetField("gridView", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(grid);
+
+ // Log each category recursively
+ GridItemCollection items = (GridItemCollection)view.GetType().InvokeMember("GetAllGridEntries", BindingFlags.InvokeMethod | BindingFlags.NonPublic | BindingFlags.Instance, null, view, null);
+ foreach(GridItem item in items)
+ {
+ if (item.GridItemType == GridItemType.Category)
+ {
+ LogItem(item, string.Empty);
+ }
+ }
+ }
+
+ private void LogItem(GridItem item, string indent)
+ {
+ if (item.GridItemType == GridItemType.Category)
+ Logger.Instance.Info(this, "{0}{1}", indent, item.Label.Trim());
+ else
+ Logger.Instance.Info(this, "{0}{1}={2}", indent, item.Label.Trim(), item.Value);
+ foreach(GridItem child in item.GridItems)
+ {
+ LogItem(child, indent + INDENT);
+ }
+ }
+
+ #endregion
+
+ #region Event handlers
+
+ private void buttonGC_Click(object sender, EventArgs e)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+
+ UpdateFields();
+ }
+
+ private void buttonRefresh_Click(object sender, EventArgs e)
+ {
+ UpdateFields();
+ }
+
+ private void buttonClose_Click(object sender, EventArgs e)
+ {
+ this.Close();
+ }
+
+ private void buttonLog_Click(object sender, EventArgs e)
+ {
+ ToLog();
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.resx
new file mode 100644
index 0000000..547d1a0
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugDialog.resx
@@ -0,0 +1,375 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Top, Bottom, Left, Right
+
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ 1
+
+
+ Top, Bottom, Left, Right
+
+
+ True
+
+
+ True
+
+
+
+ 352, 4
+
+
+ 4, 4, 4, 4
+
+
+ 91, 33
+
+
+ 0
+
+
+ Run GC
+
+
+ buttonGC
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ flowButtons
+
+
+ 0
+
+
+ True
+
+
+ 253, 4
+
+
+ 4, 4, 4, 4
+
+
+ 91, 33
+
+
+ 1
+
+
+ Refresh
+
+
+ buttonRefresh
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ flowButtons
+
+
+ 1
+
+
+ True
+
+
+ 145, 4
+
+
+ 4, 4, 4, 4
+
+
+ 100, 33
+
+
+ 2
+
+
+ Close
+
+
+ buttonClose
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ flowButtons
+
+
+ 2
+
+
+ True
+
+
+ 62, 4
+
+
+ 4, 4, 4, 4
+
+
+ 75, 33
+
+
+ 3
+
+
+ Log
+
+
+ buttonLog
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ flowButtons
+
+
+ 3
+
+
+ RightToLeft
+
+
+ 3, 470
+
+
+ 3, 2, 3, 2
+
+
+ 447, 41
+
+
+ 1
+
+
+ flowButtons
+
+
+ System.Windows.Forms.FlowLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableMain
+
+
+ 0
+
+
+ Top, Bottom, Left, Right
+
+
+ False
+
+
+ 3, 2
+
+
+ 3, 2, 3, 2
+
+
+ 447, 464
+
+
+ 2
+
+
+ Properties
+
+
+ System.Windows.Forms.PropertyGrid, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableMain
+
+
+ 1
+
+
+ 0, 0
+
+
+ 3, 2, 3, 2
+
+
+ 2
+
+
+ 453, 513
+
+
+ 0
+
+
+ tableMain
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="flowButtons" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="Properties" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /></Controls><Columns Styles="Percent,100" /><Rows Styles="Percent,100,AutoSize,0" /></TableLayoutSettings>
+
+
+ True
+
+
+ 8, 16
+
+
+ True
+
+
+ 453, 511
+
+
+ 3, 2, 3, 2
+
+
+ CenterParent
+
+
+ Debug
+
+
+ DebugDialog
+
+
+ System.Windows.Forms.Form, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugInfo.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugInfo.cs
new file mode 100644
index 0000000..8bbb2c9
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugInfo.cs
@@ -0,0 +1,278 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Utils;
+using Acacia.ZPush;
+using Microsoft.Office.Core;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.DebugSupport
+{
+ public enum DebugCategory
+ {
+ Version,
+ Memory,
+ Wrappers,
+ Misc,
+ System,
+ Accounts,
+ Features,
+ AddIns
+ }
+
+ public class DebugCategoryAttribute : CategoryAttribute
+ {
+ // Add tabs; these are not printed, but are used for the sorting
+ public DebugCategoryAttribute(DebugCategory order)
+ :
+ base(order.ToString().PadLeft(typeof(DebugCategory).GetEnumNames().Length - (int)order + order.ToString().Length, '\t'))
+ {
+
+ }
+ }
+
+ public class DebugInfoConverter : ExpandableObjectConverter
+ {
+ private class CustomPropertyDescriptor : PropertyDescriptor
+ {
+ private readonly TProperty value;
+
+ public CustomPropertyDescriptor(string propertyName, DebugCategory category, TProperty value)
+ : base(propertyName, new Attribute[] { new DebugCategoryAttribute(category) })
+ {
+ this.value = value;
+ }
+
+ public override bool CanResetValue(object component) { return false; }
+ public override Type ComponentType { get { return typeof(TComponent); } }
+ public override object GetValue(object component) { return value; }
+ public override bool IsReadOnly { get { return true; } }
+ public override Type PropertyType { get { return typeof(TProperty); } }
+ public override void ResetValue(object component) { SetValue(component, null); }
+ public override void SetValue(object component, object value) { }
+ public override bool ShouldSerializeValue(object component) { return false; }
+ }
+
+
+ public override bool GetPropertiesSupported(ITypeDescriptorContext context)
+ {
+ return true;
+ }
+
+ public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
+ {
+ PropertyDescriptorCollection properties = new PropertyDescriptorCollection(base.GetProperties(context, value, attributes).Cast().ToArray());
+
+ DebugInfo info = value as DebugInfo;
+ if (info != null)
+ {
+ // Add accounts
+ foreach (ZPushAccount account in ThisAddIn.Instance.Watcher.Accounts.GetAccounts())
+ {
+ PropertyDescriptor p = new CustomPropertyDescriptor(account.DisplayName, DebugCategory.Accounts, account);
+ properties.Add(p);
+ }
+
+ // Add Features
+ foreach (Feature feature in ThisAddIn.Instance.Features)
+ {
+ PropertyDescriptor p = new CustomPropertyDescriptor(feature.Name, DebugCategory.Features, feature);
+ properties.Add(p);
+ }
+
+ // Add Add-ins
+ foreach (COMAddIn addin in ThisAddIn.Instance.Application.COMAddIns)
+ {
+ PropertyDescriptor p = new CustomPropertyDescriptor(addin.ProgId, DebugCategory.AddIns, addin.Description);
+ properties.Add(p);
+ }
+ }
+
+ return properties;
+ }
+ }
+
+ [TypeConverter(typeof(DebugInfoConverter))]
+ class DebugInfo
+ {
+ #region Version
+
+ [DebugCategory(DebugCategory.Version)]
+ public string Version { get { return BuildVersions.VERSION; } }
+ [DebugCategory(DebugCategory.Version)]
+ public string Revision { get { return BuildVersions.REVISION; } }
+ [DebugCategory(DebugCategory.Version)]
+ public string BuildDate { get { return LibUtils.BuildTime.ToString(); } }
+
+ #endregion
+
+ #region Memory
+
+ [DebugCategory(DebugCategory.Memory)]
+ public string TotalMemory { get { return MemoryToString(GC.GetTotalMemory(false)); } }
+
+ #endregion
+
+ #region Wrappers
+
+ [DebugCategory(DebugCategory.Wrappers)]
+ public long ActiveWrappers { get { return Statistics.CreatedWrappers - Statistics.DeletedWrappers; } }
+ [DebugCategory(DebugCategory.Wrappers)]
+ public long CreatedWrappers { get { return Statistics.CreatedWrappers; } }
+ [DebugCategory(DebugCategory.Wrappers)]
+ public long DeletedWrappers { get { return Statistics.DeletedWrappers; } }
+ [DebugCategory(DebugCategory.Wrappers)]
+ public long DisposedWrappers { get { return Statistics.DisposedWrappers; } }
+ [DebugCategory(DebugCategory.Wrappers)]
+ public long UndisposedWrappers { get { return Statistics.DeletedWrappers - Statistics.DisposedWrappers; } }
+
+ #endregion
+
+ #region Misc
+
+ [DebugCategory(DebugCategory.Misc)]
+ public string StartupTime { get { return TimeToString(Statistics.StartupTime); } }
+
+ [DebugCategory(DebugCategory.Misc)]
+ public LogLevel LogLevel
+ {
+ get { return Logger.Instance.MinLevel; }
+ set
+ {
+ Logger.Instance.SetLevel(value);
+ }
+ }
+
+ [DebugCategory(DebugCategory.Misc)]
+ public string Threading
+ {
+ get { return Tasks.Executor.Name; }
+ }
+
+ [DebugCategory(DebugCategory.Misc)]
+ public bool ZPushSync
+ {
+ get { return ThisAddIn.Instance.Watcher.Sync.Enabled; }
+ }
+
+ [DebugCategory(DebugCategory.Misc)]
+ public TimeSpan ZPushSyncPeriod
+ {
+ get { return ThisAddIn.Instance.Watcher.Sync.Period; }
+ }
+
+ [DebugCategory(DebugCategory.Misc)]
+ public string Build
+ {
+ get
+ {
+#if DEBUG
+ return "Debug";
+#else
+ return "Release";
+#endif
+ }
+ }
+
+#endregion
+
+ #region System
+
+ [DebugCategory(DebugCategory.System)]
+ public string Locale
+ {
+ get
+ {
+ return CultureInfo.CurrentUICulture.DisplayName;
+ }
+ }
+
+ [DebugCategory(DebugCategory.System)]
+ public string WindowsVersion
+ {
+ get
+ {
+ return Environment.OSVersion.Version.ToString();
+ }
+ }
+
+ [DebugCategory(DebugCategory.System)]
+ public string Architecture
+ {
+ get
+ {
+ return Environment.Is64BitOperatingSystem ? "64 bit" : "32 bit";
+ }
+ }
+
+ #endregion
+
+#region Outlook
+
+ [DebugCategory(DebugCategory.System)]
+ public string OutlookVersion
+ {
+ get
+ {
+ return ThisAddIn.Instance.Application.Version;
+ }
+ }
+
+ [DebugCategory(DebugCategory.System)]
+ public string OutlookArchitecture
+ {
+ get
+ {
+ return Environment.Is64BitProcess ? "64 bit" : "32 bit";
+ }
+ }
+
+#endregion
+
+#region Helpers
+
+ private string TimeToString(Stopwatch time)
+ {
+ return time.ElapsedMilliseconds.ToString("#### ms");
+ }
+
+ private static readonly string[] SizeSuffixes = { "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" };
+
+ private string MemoryToString(long value)
+ {
+ if (value < 0) { return "-" + MemoryToString(-value); }
+
+ int i = 0;
+ decimal dValue = (decimal)value;
+ while (Math.Round(dValue / 1024) >= 1)
+ {
+ dValue /= 1024;
+ i++;
+ }
+
+ return string.Format("{0:n1} {1}", dValue, SizeSuffixes[i]);
+ }
+
+#endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.Designer.cs
new file mode 100644
index 0000000..8bfe434
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.Designer.cs
@@ -0,0 +1,89 @@
+namespace Acacia.Features.DebugSupport
+{
+ partial class DebugSupportSettings
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(DebugSupportSettings));
+ this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
+ this.labelLogLevel = new System.Windows.Forms.Label();
+ this.comboLogLevel = new System.Windows.Forms.ComboBox();
+ this.buttonShowLog = new System.Windows.Forms.Button();
+ this.tableLayoutPanel1.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // tableLayoutPanel1
+ //
+ resources.ApplyResources(this.tableLayoutPanel1, "tableLayoutPanel1");
+ this.tableLayoutPanel1.BackColor = System.Drawing.SystemColors.Window;
+ this.tableLayoutPanel1.Controls.Add(this.labelLogLevel, 0, 0);
+ this.tableLayoutPanel1.Controls.Add(this.comboLogLevel, 1, 0);
+ this.tableLayoutPanel1.Controls.Add(this.buttonShowLog, 0, 1);
+ this.tableLayoutPanel1.Name = "tableLayoutPanel1";
+ //
+ // labelLogLevel
+ //
+ resources.ApplyResources(this.labelLogLevel, "labelLogLevel");
+ this.labelLogLevel.Name = "labelLogLevel";
+ //
+ // comboLogLevel
+ //
+ resources.ApplyResources(this.comboLogLevel, "comboLogLevel");
+ this.comboLogLevel.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.comboLogLevel.FormattingEnabled = true;
+ this.comboLogLevel.Name = "comboLogLevel";
+ this.comboLogLevel.SelectedIndexChanged += new System.EventHandler(this.comboLogLevel_SelectedIndexChanged);
+ //
+ // buttonShowLog
+ //
+ resources.ApplyResources(this.buttonShowLog, "buttonShowLog");
+ this.tableLayoutPanel1.SetColumnSpan(this.buttonShowLog, 2);
+ this.buttonShowLog.Name = "buttonShowLog";
+ this.buttonShowLog.UseVisualStyleBackColor = true;
+ this.buttonShowLog.Click += new System.EventHandler(this.buttonShowLog_Click);
+ //
+ // DebugSupportSettings
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.Controls.Add(this.tableLayoutPanel1);
+ this.Name = "DebugSupportSettings";
+ this.tableLayoutPanel1.ResumeLayout(false);
+ this.tableLayoutPanel1.PerformLayout();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
+ private System.Windows.Forms.Label labelLogLevel;
+ private System.Windows.Forms.ComboBox comboLogLevel;
+ private System.Windows.Forms.Button buttonShowLog;
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.cs
new file mode 100644
index 0000000..c00670e
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.cs
@@ -0,0 +1,68 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using Acacia.UI;
+
+namespace Acacia.Features.DebugSupport
+{
+ public partial class DebugSupportSettings : FeatureSettings
+ {
+ private readonly FeatureDebugSupport _feature;
+ public override Feature Feature
+ {
+ get
+ {
+ return _feature;
+ }
+ }
+
+ public DebugSupportSettings(FeatureDebugSupport feature = null)
+ {
+ this._feature = feature;
+
+ InitializeComponent();
+
+ for (int i = 0; i < typeof(LogLevel).GetEnumNames().Length; ++i)
+ comboLogLevel.Items.Add((LogLevel)i);
+ comboLogLevel.SelectedItem = Logger.Instance.MinLevel;
+ }
+
+ private void buttonShowLog_Click(object sender, EventArgs e)
+ {
+ if (_feature != null)
+ _feature.ShowLog();
+ }
+
+ private void comboLogLevel_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ Dirty = Logger.Instance.MinLevel != (LogLevel)comboLogLevel.SelectedItem;
+ }
+
+ public override void Apply()
+ {
+ Logger.Instance.SetLevel((LogLevel)comboLogLevel.SelectedItem);
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.resx
new file mode 100644
index 0000000..831f1fd
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/DebugSupportSettings.resx
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Top, Left, Right
+
+
+
+ True
+
+
+ 2
+
+
+ True
+
+
+ Fill
+
+
+
+ 3, 0
+
+
+ 53, 27
+
+
+ 0
+
+
+ Log level:
+
+
+ MiddleLeft
+
+
+ labelLogLevel
+
+
+ System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableLayoutPanel1
+
+
+ 0
+
+
+ Top, Bottom, Left, Right
+
+
+ 62, 3
+
+
+ 281, 21
+
+
+ 1
+
+
+ comboLogLevel
+
+
+ System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableLayoutPanel1
+
+
+ 1
+
+
+ True
+
+
+ 3, 35
+
+
+ 3, 8, 8, 3
+
+
+ 8, 0, 8, 0
+
+
+ 132, 23
+
+
+ 2
+
+
+ Open log file location
+
+
+ buttonShowLog
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableLayoutPanel1
+
+
+ 2
+
+
+ 0, 0
+
+
+ 2
+
+
+ 346, 63
+
+
+ 0
+
+
+ tableLayoutPanel1
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="labelLogLevel" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="comboLogLevel" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="buttonShowLog" Row="1" RowSpan="1" Column="0" ColumnSpan="2" /></Controls><Columns Styles="AutoSize,0,Percent,100" /><Rows Styles="AutoSize,0,AutoSize,0" /></TableLayoutSettings>
+
+
+ True
+
+
+ 6, 13
+
+
+ True
+
+
+ 349, 66
+
+
+ DebugSupportSettings
+
+
+ Acacia.UI.FeatureSettings, ZPush, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureDebugSupport.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureDebugSupport.cs
new file mode 100644
index 0000000..8730652
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureDebugSupport.cs
@@ -0,0 +1,122 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Microsoft.Office.Interop.Outlook;
+using System;
+using System.Collections.Generic;
+using Acacia.Features.ReplyFlags;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Acacia.Utils;
+using System.Threading;
+using System.Windows.Forms;
+using System.Runtime.InteropServices;
+using Acacia.UI;
+using Acacia.ZPush;
+using Acacia.UI.Outlook;
+
+namespace Acacia.Features.DebugSupport
+{
+ [AcaciaOption("Contains features to enable support and debugging of the plugin.")]
+ public class FeatureDebugSupport : Feature, FeatureWithRibbon
+ {
+ public FeatureDebugSupport()
+ {
+
+ }
+
+ public override void Startup()
+ {
+ RegisterButton(this, "About", false, ShowAbout);
+ if (Dialog)
+ RegisterButton(this, "Debug", false, ShowDialog);
+ RegisterButton(this, "Settings", false, ShowSettings);
+ }
+
+ #region About dialog
+
+ public void ShowAbout()
+ {
+ new AboutDialog().ShowDialog();
+ }
+
+ #endregion
+
+ #region Debug options
+
+ private static readonly DebugOptions.BoolOption OPTION_DIALOG = new DebugOptions.BoolOption("Dialog", false);
+
+ [AcaciaOption("Enables the debug dialog")]
+ public bool Dialog
+ {
+ get { return GetOption(OPTION_DIALOG); }
+ set { SetOption(OPTION_DIALOG, value); }
+ }
+
+ #endregion
+
+ #region Settings
+
+ public void ShowSettings()
+ {
+ new SettingsDialog().ShowDialog();
+ }
+
+ public override FeatureSettings GetSettings()
+ {
+ return new DebugSupportSettings(this);
+ }
+
+ #endregion
+
+ #region Debug dialog
+
+ private void ShowDialog()
+ {
+ new DebugDialog().Show();
+ }
+
+ #endregion
+
+ #region Log
+
+ public void ShowLog()
+ {
+ if (Logger.Instance.Path != null)
+ {
+ // This is roughly equivalent to starting explorer with /select, but has
+ // the benefit of reusing windows if it's done multiple times
+ IntPtr pidl = ILCreateFromPathW(Logger.Instance.Path);
+ SHOpenFolderAndSelectItems(pidl, 0, IntPtr.Zero, 0);
+ ILFree(pidl);
+ }
+ }
+
+
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
+ private static extern IntPtr ILCreateFromPathW(string pszPath);
+
+ [DllImport("shell32.dll")]
+ private static extern int SHOpenFolderAndSelectItems(IntPtr pidlFolder, int cild, IntPtr apidl, int dwFlags);
+
+ [DllImport("shell32.dll")]
+ private static extern void ILFree(IntPtr pidl);
+
+ #endregion
+
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureObjectConverter.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureObjectConverter.cs
new file mode 100644
index 0000000..4ddca7c
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/FeatureObjectConverter.cs
@@ -0,0 +1,44 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.DebugSupport
+{
+ ///
+ /// Object converter for a feature in the debug dialog.
+ ///
+ public class FeatureObjectConverter : ExpandableObjectConverter
+ {
+ public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
+ {
+ if (destinationType == typeof(string) && value is Feature)
+ return ((Feature)value).ToDebugString();
+ return base.ConvertTo(context, culture, value, destinationType);
+ }
+
+ public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+ {
+ return false;
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/Statistics.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/Statistics.cs
new file mode 100644
index 0000000..a1fcf69
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/DebugSupport/Statistics.cs
@@ -0,0 +1,33 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.DebugSupport
+{
+ public static class Statistics
+ {
+ public static long CreatedWrappers;
+ public static long DeletedWrappers;
+ public static long DisposedWrappers;
+ public static Stopwatch StartupTime = new Stopwatch();
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Feature.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Feature.cs
new file mode 100644
index 0000000..afc0d80
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Feature.cs
@@ -0,0 +1,259 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Microsoft.Office.Interop.Outlook;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Acacia.UI;
+using Acacia.Utils;
+using System.Windows.Forms;
+using Acacia.ZPush;
+using System.ComponentModel;
+using Acacia.Features.DebugSupport;
+using Microsoft.Win32;
+using Acacia.UI.Outlook;
+using Acacia.Stubs;
+
+namespace Acacia.Features
+{
+ ///
+ /// A feature represents a modular piece of functionality that can be enabled in the plugin.
+ /// As all hooks must be registered from the same callback (Startup in ThisAddIn), the
+ /// Feature class is used to allow modules to register their hooks.
+ ///
+ [TypeConverter(typeof(FeatureObjectConverter))]
+ abstract public class Feature : LogContext
+ {
+ public readonly string Name;
+
+ protected Feature()
+ {
+ this.Name = GetFeatureName(GetType());
+ }
+
+ [Browsable(false)]
+ public string DisplayName
+ {
+ get { return StringUtil.GetResourceString("Feature_" + Name); }
+ }
+
+ public virtual FeatureSettings GetSettings()
+ {
+ return null;
+ }
+
+ protected static Microsoft.Office.Interop.Outlook.Application App
+ {
+ get { return ThisAddIn.Instance.Application; }
+ }
+
+ #region Debug options
+
+ public static string GetFeatureName(Type featureType)
+ {
+ return featureType.Name.StripPrefix("Feature");
+ }
+
+ public static string GetDebugTokens(Type featureType)
+ {
+ return DebugOptions.GetOptions(GetFeatureName(featureType));
+ }
+
+ public static bool IsEnabled(Type featureType)
+ {
+ bool defaultEnabled = !typeof(FeatureDisabled).IsAssignableFrom(featureType);
+ return DebugOptions.GetOption(GetFeatureName(featureType),
+ defaultEnabled ? DebugOptions.ENABLED : DebugOptions.FEATURE_DISABLED_DEFAULT);
+ }
+
+ public ValueType GetOption(DebugOptions.Option option)
+ {
+ return DebugOptions.GetOption(Name, option);
+ }
+
+ public static ValueType GetOption(Type featureType, DebugOptions.Option option)
+ {
+ return DebugOptions.GetOption(GetFeatureName(featureType), option);
+ }
+
+ public void SetOption(DebugOptions.Option option, ValueType value)
+ {
+ DebugOptions.SetOption(Name, option, value);
+ }
+
+ public static void SetOption(Type featureType, DebugOptions.Option option, ValueType value)
+ {
+ DebugOptions.SetOption(GetFeatureName(featureType), option, value);
+ }
+
+ [AcaciaOption("Completely enables or disables the feature. Note that if the feature is enabled, it's components may still be disabled")]
+ virtual public bool Enabled
+ {
+ get { return GetOption(DebugOptions.ENABLED); }
+ set { SetOption(DebugOptions.ENABLED, value); }
+ }
+
+ #endregion
+
+ #region Outlook UI
+
+ ///
+ /// Returns the Outlook UI. May be null if modifications to the UI are disabled.
+ ///
+ private OutlookUI OutlookUI
+ {
+ get { return ThisAddIn.Instance.OutlookUI; }
+ }
+
+ ///
+ /// Helper which registers only if allowed through options
+ ///
+ ///
+ public RibbonButton RegisterButton(FeatureWithRibbon feature, string id, bool large, System.Action callback,
+ ZPushBehaviour zpushBehaviour = ZPushBehaviour.None)
+ {
+ if (OutlookUI == null || !UI_Ribbon || !GlobalOptions.INSTANCE.UI_Ribbon)
+ return null;
+
+ return OutlookUI.Register(new RibbonButton(feature, id, large, callback, zpushBehaviour));
+ }
+
+ public RibbonToggleButton RegisterToggleButton(FeatureWithRibbon feature, string id, bool large, System.Action callback,
+ ZPushBehaviour zpushBehaviour = ZPushBehaviour.None)
+ {
+ if (OutlookUI == null || !UI_Ribbon || !GlobalOptions.INSTANCE.UI_Ribbon)
+ return null;
+
+ return OutlookUI.Register(new RibbonToggleButton(feature, id, large, callback, zpushBehaviour));
+ }
+
+ public MenuItem RegisterMenuItem(FeatureWithContextMenu feature, string id, string menuId, System.Action callback,
+ ZPushBehaviour zpushBehaviour = ZPushBehaviour.None)
+ where ItemType : IBase
+ {
+ if (OutlookUI == null || !UI_ContextMenu || !GlobalOptions.INSTANCE.UI_ContextMenu)
+ return null;
+
+ if (menuId == null)
+ menuId = GetDefaultMenuId();
+ return OutlookUI.Register(new MenuItem(feature, id, menuId, callback, zpushBehaviour));
+ }
+
+ private string GetDefaultMenuId()
+ where ItemType : IBase
+ {
+ if (typeof(ItemType) == typeof(IFolder))
+ return "ContextMenuFolder";
+ else
+ throw new System.Exception("Unknown context menu: " + typeof(ItemType));
+ }
+
+ [AcaciaOption("Enables or disables modifications to the Outlook UI for this feature." +
+ "Note that where applicable, the Ribbon and Context Menu options also control UI modifications.",
+ Interface = typeof(FeatureWithUI))]
+ virtual public bool UI
+ {
+ get { return GetOption(DebugOptions.OUTLOOK_UI); }
+ set { SetOption(DebugOptions.OUTLOOK_UI, value); }
+ }
+
+ [AcaciaOption("Enables or disables modifications to the Outlook Ribbon for this feature." +
+ "Note that if the UI option is disabled, Ribbon modifications will not be made either.",
+ Interface = typeof(FeatureWithRibbon))]
+ virtual public bool UI_Ribbon
+ {
+ get { return GetOption(DebugOptions.OUTLOOK_UI_RIBBON); }
+ set { SetOption(DebugOptions.OUTLOOK_UI_RIBBON, value); }
+ }
+
+ [AcaciaOption("Enables or disables modifications to the Outlook Context Menus for this feature." +
+ "Note that if the UI option is disabled, Context Menu modifications will not be made either.",
+ Interface = typeof(FeatureWithContextMenu))]
+ virtual public bool UI_ContextMenu
+ {
+ get { return GetOption(DebugOptions.OUTLOOK_UI_CONTEXT_MENU); }
+ set { SetOption(DebugOptions.OUTLOOK_UI_CONTEXT_MENU, value); }
+ }
+
+ #endregion
+
+ #region Event helpers
+
+ private static MailEvents _mailEvents;
+ protected static MailEvents MailEvents
+ {
+ get
+ {
+ if (_mailEvents == null)
+ _mailEvents = new MailEvents(App);
+ return _mailEvents;
+ }
+ }
+
+ protected ZPushWatcher Watcher
+ {
+ get
+ {
+ return ThisAddIn.Instance.Watcher;
+ }
+ }
+
+ #endregion
+
+ #region Startup
+
+ ///
+ /// Invoked when the feature is started. The application object is accessible through
+ /// App
+ ///
+ public virtual void Startup()
+ {
+
+ }
+
+ #endregion
+
+ #region Z-Push channels
+
+ private static ZPushChannels _zPushChannels;
+ protected static ZPushChannels ZPushChannels
+ {
+ get
+ {
+ if (_zPushChannels == null)
+ _zPushChannels = new ZPushChannels(ThisAddIn.Instance.Watcher);
+ return _zPushChannels;
+ }
+ }
+
+ #endregion
+
+ #region Debug support
+
+ [Browsable(false)]
+ public string LogContextId { get { return Name; } }
+
+ public virtual string ToDebugString()
+ {
+ return "";
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureDisabled.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureDisabled.cs
new file mode 100644
index 0000000..13ffb27
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureDisabled.cs
@@ -0,0 +1,41 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features
+{
+ ///
+ /// Base class for a feature that is disabled unless specifically enabled
+ ///
+ abstract public class FeatureDisabled : Feature
+ {
+ #region Debug options
+
+ [AcaciaOption("Completely enables or disables the feature. Note that if the feature is enabled, it's components may still be disabled")]
+ override public bool Enabled
+ {
+ get { return GetOption(DebugOptions.FEATURE_DISABLED_DEFAULT); }
+ set { SetOption(DebugOptions.FEATURE_DISABLED_DEFAULT, value); }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureWithUI.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureWithUI.cs
new file mode 100644
index 0000000..2908c33
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FeatureWithUI.cs
@@ -0,0 +1,32 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features
+{
+ ///
+ /// Marker interfaces for features that display UIs. Implementing these interfaces automatically adds the
+ /// debug options to control the UI.
+ ///
+ public interface FeatureWithUI : LogContext {}
+ public interface FeatureWithRibbon : FeatureWithUI { };
+ public interface FeatureWithContextMenu : FeatureWithUI { };
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs
new file mode 100644
index 0000000..a90ee32
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Features.cs
@@ -0,0 +1,40 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features
+{
+ public static class Features
+ {
+ public static readonly Type[] FEATURES =
+ {
+ typeof(ReplyFlags.FeatureReplyFlags),
+ typeof(OutOfOffice.FeatureOutOfOffice),
+ typeof(SharedFolders.FeatureSharedFolders),
+ typeof(WebApp.FeatureWebApp),
+ typeof(FreeBusy.FeatureFreeBusy),
+ typeof(GAB.FeatureGAB),
+ typeof(Notes.FeatureNotes),
+ typeof(SendAs.FeatureSendAs),
+ typeof(DebugSupport.FeatureDebugSupport)
+ };
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs
new file mode 100644
index 0000000..f9fb908
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FeatureFreeBusy.cs
@@ -0,0 +1,210 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Features.GAB;
+using Acacia.Stubs;
+using Acacia.UI;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Features.FreeBusy
+{
+ [AcaciaOption("Provides free/busy information on users in the Global Adress Book to schedule meetings.")]
+ public class FeatureFreeBusy : Feature
+ {
+ public FeatureFreeBusy()
+ {
+ }
+
+ public override void Startup()
+ {
+ Thread thread = new Thread(() =>
+ {
+ try
+ {
+ Worker();
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Error(this, "Unhandled exception: {0}", e);
+ }
+ });
+ thread.Name = "FreeBusy";
+ thread.Start();
+ }
+
+ #region Settings
+
+ public override FeatureSettings GetSettings()
+ {
+ return new FreeBusySettings(this);
+ }
+
+ public ZPushAccounts Accounts
+ {
+ get { return Watcher.Accounts; }
+ }
+
+ private const string REG_DOGABLOOKUP = "GABLookup";
+ private const string REG_DEFAULTACCOUNT = "Default";
+
+ public bool DoGABLookup
+ {
+ get
+ {
+ return RegistryUtil.GetConfigValue(Name, REG_DOGABLOOKUP, 1) != 0;
+ }
+ set
+ {
+ RegistryUtil.SetConfigValue(Name, REG_DOGABLOOKUP, value ? 1 : 0, RegistryValueKind.DWord);
+ }
+ }
+
+ public ZPushAccount DefaultAccount
+ {
+ get
+ {
+ string val = RegistryUtil.GetConfigValue(Name, REG_DEFAULTACCOUNT, null);
+ if (!string.IsNullOrEmpty(val))
+ {
+ ZPushAccount account = Accounts.GetAccount(val);
+ if (account != null)
+ return account;
+ }
+
+ // Fall back to the first one
+ return Accounts.GetAccounts().FirstOrDefault();
+ }
+
+ set
+ {
+ RegistryUtil.SetConfigValue(Name, REG_DEFAULTACCOUNT, value == null ? "" : value.SmtpAddress, RegistryValueKind.String);
+ }
+ }
+
+ #endregion
+
+ private const string REG_KEY = @"Options\Calendar\Internet Free/Busy";
+ private const string REG_VALUE = @"Read URL";
+ internal const string URL_IDENTIFIER = "/zpush/";
+ private const int DEFAULT_PORT = 18632;
+ private const string URL_PREFIX = @"http://127.0.0.1:{0}" + URL_IDENTIFIER;
+ private const string URL = URL_PREFIX + "%NAME%@%SERVER%";
+
+ private void Worker()
+ {
+ Port = DEFAULT_PORT;
+
+ // Register URL
+ using (RegistryKey key = OutlookRegistryUtils.OpenOutlookKey(REG_KEY, Microsoft.Win32.RegistryKeyPermissionCheck.ReadWriteSubTree))
+ {
+ if (key != null)
+ {
+ string oldURL = key.GetValueString(REG_VALUE);
+ if (string.IsNullOrWhiteSpace(oldURL) || oldURL.Contains(URL_IDENTIFIER))
+ key.SetValue(REG_VALUE, string.Format(URL, Port));
+ }
+ }
+
+ FreeBusyServer server = new FreeBusyServer(this);
+
+ // Run
+ TcpListener listener = new TcpListener(IPAddress.Loopback, Port);
+ listener.Start();
+ for (;;)
+ {
+ Interlocked.Increment(ref _iterationCount);
+ try
+ {
+ for (;;)
+ {
+ // Wait for a connection
+ TcpClient client = listener.AcceptTcpClient();
+ Interlocked.Increment(ref _requestCount);
+ // And handle it in the UI thread to allow GAB access
+ Tasks.Task(this, "FreeBusyHandler", () => server.HandleRequest(client));
+ }
+ }
+ catch (Exception e)
+ {
+ Logger.Instance.Error(this, "Error in FreeBusy server: {0}", e);
+ }
+ }
+ }
+
+ #region Debug
+
+ public int Port { get; private set; }
+
+ private long _requestCount;
+ public long RequestCount
+ {
+ get { return Interlocked.Read(ref _requestCount); }
+ }
+
+ private long _iterationCount;
+ public long IterationCount
+ {
+ get { return Interlocked.Read(ref _iterationCount); }
+ }
+
+ #endregion
+
+
+ internal ZPushAccount FindZPushAccount(string username)
+ {
+ // Search through GABs
+ if (DoGABLookup)
+ {
+ FeatureGAB gab = ThisAddIn.Instance.GetFeature();
+ if (gab != null)
+ {
+ foreach (GABHandler handler in gab.GABHandlers)
+ {
+ ZPushAccount account = handler.ActiveAccount;
+ if (account != null && handler.Contacts != null)
+ {
+ // Look for the email address. If found, use the account associated with the GAB
+ ISearch search = handler.Contacts.Search();
+ search.AddField("urn:schemas:contacts:email1").SetOperation(SearchOperation.Equal, username);
+ using (IItem result = search.SearchOne())
+ {
+ if (result != null)
+ return account;
+ }
+ }
+ }
+ }
+ }
+
+ // Fall back to default account
+ return DefaultAccount;
+ }
+
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs
new file mode 100644
index 0000000..e5265c0
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusyServer.cs
@@ -0,0 +1,158 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Features.GAB;
+using Acacia.Stubs;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Acacia.ZPush.Connect;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Sockets;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.FreeBusy
+{
+ public class FreeBusyServer
+ {
+ private readonly FeatureFreeBusy _freeBusy;
+ private readonly int _port;
+ private readonly Regex _httpRequest;
+
+ public FreeBusyServer(FeatureFreeBusy freeBusy)
+ {
+ this._freeBusy = freeBusy;
+ this._port = freeBusy.Port;
+ this._httpRequest = new Regex(@"^GET " + FeatureFreeBusy.URL_IDENTIFIER + @"([^ ]+) HTTP/(\d.\d)$");
+ }
+
+ public void HandleRequest(TcpClient client)
+ {
+ try
+ {
+ using (client)
+ {
+ StreamWriter writer = new StreamWriter(client.GetStream());
+ StreamReader reader = new StreamReader(client.GetStream());
+ try
+ {
+ // Read the request
+ string s = reader.ReadLine();
+ Match m = _httpRequest.Match(s);
+ if (!m.Success)
+ {
+ Logger.Instance.Trace(this, "Invalid request: {0}", s);
+ throw new InvalidOperationException();
+ }
+ string username = m.Groups[1].Value;
+ Logger.Instance.Trace(this, "REQUEST: {0} -> {1}, {2}", s, m.Groups[1], m.Groups[2]);
+
+ // Headers
+ for (;;)
+ {
+ s = reader.ReadLine();
+ if (string.IsNullOrEmpty(s))
+ break;
+ }
+
+ // Write response
+ FetchData(username, writer);
+ }
+ catch (InvalidOperationException)
+ {
+ writer.Write("HTTP/1.0 404 Not found\r\nConnection: close\r\n\r\n");
+ }
+ catch (Exception e)
+ {
+ Logger.Instance.Error(this, "Error in FreeBusy worker: {0}", e);
+ writer.Write("HTTP/1.0 404 Not found\r\nConnection: close\r\n\r\n");
+ }
+ writer.Flush();
+ }
+ }
+ catch(Exception e)
+ {
+ Logger.Instance.Error(this, "Error in FreeBusy worker: {0}", e);
+ }
+ }
+
+ private void FetchData(string username, StreamWriter output)
+ {
+ // Request the data from the ZPush server
+ ZPushConnection connection = new ZPushConnection(_freeBusy.FindZPushAccount(username), new System.Threading.CancellationToken(false));
+
+ // Include yesterday in the request, outlook shows it by default
+ var request = new ActiveSync.ResolveRecipientsRequest(username,
+ DateTime.Today.AddDays(-1),
+ DateTime.Today.AddMonths(6));
+ var response = connection.Execute(request);
+
+ // If there is no FreeBusy data, return 404
+ if (response?.FreeBusy == null)
+ {
+ throw new InvalidOperationException();
+ }
+
+ Logger.Instance.Trace(this, "Writing response");
+ // Encode the response in vcard format
+ output.WriteLine("HTTP/1.0 200 OK");
+ output.WriteLine("Content-Type: text/vcard");
+ output.WriteLine("Connection: close");
+ output.WriteLine("");
+
+
+ output.WriteLine("BEGIN:VCALENDAR");
+ output.WriteLine("PRODID:-//ZPush//EN");
+ output.WriteLine("VERSION:2.0");
+ output.WriteLine("BEGIN:VFREEBUSY");
+ output.WriteLine("ORGANIZER:" + username);
+ output.WriteLine(string.Format("URL:http://127.0.0.1:{0}{1}{2}", _port, FeatureFreeBusy.URL_IDENTIFIER, username));
+ output.WriteLine(string.Format("DTSTAMP:{0:" + Constants.DATE_ISO_8601 + "}", DateTime.Now));
+ output.WriteLine(string.Format("DTSTART:{0:" + Constants.DATE_ISO_8601 + "}", response.FreeBusy.StartTime));
+ output.WriteLine(string.Format("DTEND:{0:" + Constants.DATE_ISO_8601 + "}", response.FreeBusy.EndTime));
+
+ foreach(ActiveSync.FreeBusyData data in response.FreeBusy)
+ {
+ if (data.Type != ActiveSync.FreeBusyType.Free)
+ {
+ string freeBusy = string.Format("FREEBUSY;FBTYPE={2}:{0:" + Constants.DATE_ISO_8601 + "}/{1:" + Constants.DATE_ISO_8601 + "}",
+ data.Start, data.End, MapType(data.Type));
+ output.WriteLine(freeBusy);
+ }
+ }
+
+ output.WriteLine("END:VFREEBUSY");
+ output.WriteLine("END:VCALENDAR");
+ }
+
+ private object MapType(ActiveSync.FreeBusyType type)
+ {
+ switch(type)
+ {
+ case ActiveSync.FreeBusyType.Free: return "FREE";
+ case ActiveSync.FreeBusyType.Busy: return "BUSY";
+ case ActiveSync.FreeBusyType.Tentative: return "BUSY-TENTATIVE";
+ case ActiveSync.FreeBusyType.OutOfOffice: return "BUSY-UNAVAILABLE";
+ default:
+ return "BUSY";
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.Designer.cs
new file mode 100644
index 0000000..71c6a46
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.Designer.cs
@@ -0,0 +1,89 @@
+namespace Acacia.Features.FreeBusy
+{
+ partial class FreeBusySettings
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(FreeBusySettings));
+ this.tableLayoutPanel1 = new System.Windows.Forms.TableLayoutPanel();
+ this.checkGABLookup = new System.Windows.Forms.CheckBox();
+ this.labelUseAccount = new System.Windows.Forms.Label();
+ this.comboDefaultAccount = new System.Windows.Forms.ComboBox();
+ this.tableLayoutPanel1.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // tableLayoutPanel1
+ //
+ resources.ApplyResources(this.tableLayoutPanel1, "tableLayoutPanel1");
+ this.tableLayoutPanel1.Controls.Add(this.checkGABLookup, 0, 0);
+ this.tableLayoutPanel1.Controls.Add(this.labelUseAccount, 0, 1);
+ this.tableLayoutPanel1.Controls.Add(this.comboDefaultAccount, 1, 1);
+ this.tableLayoutPanel1.Name = "tableLayoutPanel1";
+ //
+ // checkGABLookup
+ //
+ resources.ApplyResources(this.checkGABLookup, "checkGABLookup");
+ this.tableLayoutPanel1.SetColumnSpan(this.checkGABLookup, 2);
+ this.checkGABLookup.Name = "checkGABLookup";
+ this.checkGABLookup.UseVisualStyleBackColor = true;
+ this.checkGABLookup.CheckedChanged += new System.EventHandler(this.checkGABLookup_CheckedChanged);
+ //
+ // labelUseAccount
+ //
+ resources.ApplyResources(this.labelUseAccount, "labelUseAccount");
+ this.labelUseAccount.Name = "labelUseAccount";
+ //
+ // comboDefaultAccount
+ //
+ resources.ApplyResources(this.comboDefaultAccount, "comboDefaultAccount");
+ this.comboDefaultAccount.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.comboDefaultAccount.FormattingEnabled = true;
+ this.comboDefaultAccount.Name = "comboDefaultAccount";
+ this.comboDefaultAccount.SelectedIndexChanged += new System.EventHandler(this.comboDefaultAccount_SelectedIndexChanged);
+ //
+ // FreeBusySettings
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BackColor = System.Drawing.SystemColors.Window;
+ this.Controls.Add(this.tableLayoutPanel1);
+ this.Name = "FreeBusySettings";
+ this.tableLayoutPanel1.ResumeLayout(false);
+ this.tableLayoutPanel1.PerformLayout();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel tableLayoutPanel1;
+ private System.Windows.Forms.CheckBox checkGABLookup;
+ private System.Windows.Forms.Label labelUseAccount;
+ private System.Windows.Forms.ComboBox comboDefaultAccount;
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.cs
new file mode 100644
index 0000000..d897505
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.cs
@@ -0,0 +1,81 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using Acacia.ZPush;
+using Acacia.UI;
+
+namespace Acacia.Features.FreeBusy
+{
+ public partial class FreeBusySettings : FeatureSettings
+ {
+ private readonly FeatureFreeBusy _feature;
+ public override Feature Feature
+ {
+ get
+ {
+ return _feature;
+ }
+ }
+
+ public FreeBusySettings(FeatureFreeBusy feature = null)
+ {
+ this._feature = feature;
+
+ InitializeComponent();
+
+ // Allow null feature for designer
+ if (feature != null)
+ {
+ checkGABLookup.Checked = feature.DoGABLookup;
+
+ foreach (ZPushAccount account in feature.Accounts.GetAccounts())
+ comboDefaultAccount.Items.Add(account);
+ comboDefaultAccount.SelectedItem = feature.DefaultAccount;
+ }
+ }
+
+ override public void Apply()
+ {
+ _feature.DefaultAccount = (ZPushAccount)comboDefaultAccount.SelectedItem;
+ _feature.DoGABLookup = checkGABLookup.Checked;
+ }
+
+ private void CheckDirty()
+ {
+ Dirty = checkGABLookup.Checked != _feature.DoGABLookup ||
+ comboDefaultAccount.SelectedItem != _feature.DefaultAccount;
+ }
+
+ private void checkGABLookup_CheckedChanged(object sender, EventArgs e)
+ {
+ CheckDirty();
+ }
+
+ private void comboDefaultAccount_SelectedIndexChanged(object sender, EventArgs e)
+ {
+ CheckDirty();
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.resx
new file mode 100644
index 0000000..7c1d1b3
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/FreeBusy/FreeBusySettings.resx
@@ -0,0 +1,279 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ True
+
+
+
+ GrowAndShrink
+
+
+ 2
+
+
+ True
+
+
+
+ 2, 2
+
+
+ 2, 2, 2, 2
+
+
+ 222, 17
+
+
+ 0
+
+
+ Look up contacts in Global Address Book
+
+
+ checkGABLookup
+
+
+ System.Windows.Forms.CheckBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableLayoutPanel1
+
+
+ 0
+
+
+ Top, Bottom, Left
+
+
+ True
+
+
+ 2, 30
+
+
+ 2, 0, 2, 0
+
+
+ 74, 30
+
+
+ 1
+
+
+ Use account:
+
+
+ MiddleLeft
+
+
+ labelUseAccount
+
+
+ System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableLayoutPanel1
+
+
+ 1
+
+
+ Top, Bottom, Left, Right
+
+
+ 80, 32
+
+
+ 2, 2, 2, 2
+
+
+ 172, 21
+
+
+ 2
+
+
+ comboDefaultAccount
+
+
+ System.Windows.Forms.ComboBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableLayoutPanel1
+
+
+ 2
+
+
+ Fill
+
+
+ 0, 0
+
+
+ 2, 2, 2, 2
+
+
+ 2
+
+
+ 254, 60
+
+
+ 0
+
+
+ tableLayoutPanel1
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="checkGABLookup" Row="0" RowSpan="1" Column="0" ColumnSpan="2" /><Control Name="labelUseAccount" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="comboDefaultAccount" Row="1" RowSpan="1" Column="1" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,Percent,100" /><Rows Styles="Percent,50,Percent,50" /></TableLayoutSettings>
+
+
+ True
+
+
+ 6, 13
+
+
+ True
+
+
+ 2, 2, 2, 2
+
+
+ 254, 60
+
+
+ FreeBusySettings
+
+
+ Acacia.UI.FeatureSettings, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/ChunkIndex.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/ChunkIndex.cs
new file mode 100644
index 0000000..c7cb0bd
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/ChunkIndex.cs
@@ -0,0 +1,52 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.GAB
+{
+ struct ChunkIndex
+ {
+ private static readonly Regex RE = new Regex(@"^account-(\d+)/(\d+)$");
+ public int numberOfChunks;
+ public int chunk;
+
+ public static ChunkIndex? Parse(string s)
+ {
+ try
+ {
+ Match match = RE.Match(s);
+ if (!match.Success)
+ return null;
+
+ return new ChunkIndex()
+ {
+ numberOfChunks = int.Parse(match.Groups[1].Value),
+ chunk = int.Parse(match.Groups[2].Value)
+ };
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/FeatureGAB.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/FeatureGAB.cs
new file mode 100644
index 0000000..c932342
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/FeatureGAB.cs
@@ -0,0 +1,626 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Microsoft.Office.Interop.Outlook;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Acacia.Stubs;
+using Acacia.Stubs.OutlookWrappers;
+using System.Threading;
+using System.Collections.Concurrent;
+using Acacia.ZPush;
+using Acacia.Utils;
+using System.ComponentModel;
+using System.Windows.Forms;
+using Acacia.UI;
+using static Acacia.DebugOptions;
+
+namespace Acacia.Features.GAB
+{
+ [AcaciaOption("Provides a Global Address Book for Z-Push accounts.")]
+ public class FeatureGAB : Feature
+ {
+ private readonly Dictionary _gabsByDomainName = new Dictionary();
+ private readonly HashSet _gabFolders = new HashSet();
+ private readonly HashSet _domains = new HashSet();
+ private ZPushLocalStore _store;
+ private int _processing;
+
+ public FeatureGAB()
+ {
+ }
+
+ public static GABHandler FindGABForAccount(ZPushAccount account)
+ {
+ FeatureGAB gab = ThisAddIn.Instance.GetFeature();
+ if (gab != null)
+ {
+ foreach (GABHandler handler in gab.GABHandlers)
+ {
+ if (account == handler.ActiveAccount)
+ {
+ return handler;
+ }
+ }
+ }
+ return null;
+ }
+
+ public override void Startup()
+ {
+ MailEvents.BeforeDelete += SuppressEventHandler_Delete;
+ MailEvents.Write += SuppressEventHandler_Modify;
+ Watcher.AccountDiscovered += AccountDiscovered;
+ Watcher.AccountRemoved += AccountRemoved;
+ Watcher.AccountsScanned += AccountsScanned;
+ }
+
+ #region Settings
+
+ public override FeatureSettings GetSettings()
+ {
+ return new GABSettings(this);
+ }
+
+ #endregion
+
+ #region Debug options
+
+ [AcaciaOption("Disables the processing of the folder containing the GAB synchronization queue. " +
+ "This should only be disabled for debug purposes.")]
+ public bool ProcessFolder
+ {
+ get { return GetOption(OPTION_PROCESS_FOLDER); }
+ set { SetOption(OPTION_PROCESS_FOLDER, value); }
+ }
+ private static readonly BoolOption OPTION_PROCESS_FOLDER = new BoolOption("ProcessFolder", true);
+
+ [AcaciaOption("Disables the processing of items in the GAB synchronization queue. " +
+ "This should only be disabled for debug purposes.")]
+ public bool ProcessItems
+ {
+ get { return GetOption(OPTION_PROCESS_ITEMS); }
+ set { SetOption(OPTION_PROCESS_ITEMS, value); }
+ }
+ private static readonly BoolOption OPTION_PROCESS_ITEMS = new BoolOption("ProcessItems", true);
+
+ [AcaciaOption("Disables the second stage of processing of items in the GAB synchronization queue. " +
+ "This should only be disabled for debug purposes")]
+ public bool ProcessItems2
+ {
+ get { return GetOption(OPTION_PROCESS_ITEMS_2); }
+ set { SetOption(OPTION_PROCESS_ITEMS_2, value); }
+ }
+ private static readonly BoolOption OPTION_PROCESS_ITEMS_2 = new BoolOption("ProcessItems2", true);
+
+ [AcaciaOption("Disables the processing of messages containing GAB contacts. " +
+ "This should only be disabled for debug purposes.")]
+ public bool ProcessMessage
+ {
+ get { return GetOption(OPTION_PROCESS_MESSAGE); }
+ set { SetOption(OPTION_PROCESS_MESSAGE, value); }
+ }
+ private static readonly BoolOption OPTION_PROCESS_MESSAGE = new BoolOption("ProcessMessage", true);
+
+ [AcaciaOption("If disabled, contacts are not created from incoming GAB messages. " +
+ "This should only be disabled for debug purposes.")]
+ public bool CreateContacts
+ {
+ get { return GetOption(OPTION_CREATE_CONTACTS); }
+ set { SetOption(OPTION_CREATE_CONTACTS, value); }
+ }
+ private static readonly BoolOption OPTION_CREATE_CONTACTS = new BoolOption("CreateContacts", true);
+
+ [AcaciaOption("If disabled, groups are not created from incoming GAB messages. " +
+ "This should only be disabled for debug purposes.")]
+ public bool CreateGroups
+ {
+ get { return GetOption(OPTION_CREATE_GROUPS); }
+ set { SetOption(OPTION_CREATE_GROUPS, value); }
+ }
+ private static readonly BoolOption OPTION_CREATE_GROUPS = new BoolOption("CreateGroups", true);
+
+ [AcaciaOption("If disabled, group members are not parsed from incoming GAB messages. " +
+ "This should only be disabled for debug purposes.")]
+ public bool GroupMembers
+ {
+ get { return GetOption(OPTION_GROUP_MEMBERS); }
+ set { SetOption(OPTION_GROUP_MEMBERS, value); }
+ }
+ private static readonly BoolOption OPTION_GROUP_MEMBERS = new BoolOption("GroupMembers", true);
+
+ [AcaciaOption("If disabled, group members are not added to groups created from GAB messages. " +
+ "This should only be disabled for debug purposes.")]
+ public bool GroupMembersAdd
+ {
+ get { return GetOption(OPTION_GROUP_MEMBERS_ADD); }
+ set { SetOption(OPTION_GROUP_MEMBERS_ADD, value); }
+ }
+ private static readonly BoolOption OPTION_GROUP_MEMBERS_ADD = new BoolOption("GroupMembersAdd", true);
+
+ [AcaciaOption("If disabled, groups that are members of other groups are not added to the parent group. " +
+ "This should only be disabled for debug purposes.")]
+ public bool NestedGroups
+ {
+ get { return GetOption(OPTION_NESTED_GROUPS); }
+ set { SetOption(OPTION_NESTED_GROUPS, value); }
+ }
+ private static readonly BoolOption OPTION_NESTED_GROUPS = new BoolOption("NestedGroups", true);
+
+ [AcaciaOption("If this option is enabled, the GAB checks for unused local folders and removes them. " +
+ "If disabled, the unused local folders are left alone. The only reason GAB folders " +
+ "can become unused is if an account is removed, or if the GAB is removed from the server.")]
+ public bool CheckUnused
+ {
+ get { return GetOption(OPTION_CHECK_UNUSED); }
+ set { SetOption(OPTION_CHECK_UNUSED, value); }
+ }
+ private static readonly BoolOption OPTION_CHECK_UNUSED = new BoolOption("CheckUnused", true);
+
+ #endregion
+
+ #region Modification suppression
+
+ internal void BeginProcessing()
+ {
+ ++_processing;
+ }
+
+ internal void EndProcessing()
+ {
+ --_processing;
+ }
+
+ private void SuppressEventHandler_Delete(IItem item, ref bool cancel)
+ {
+ SuppressEventHandler(item, false, ref cancel);
+ }
+
+ private void SuppressEventHandler_Modify(IItem item, ref bool cancel)
+ {
+ SuppressEventHandler(item, true, ref cancel);
+ }
+
+ private void SuppressEventHandler(IItem item, bool findInspector, ref bool cancel)
+ {
+ // Allow events from processing
+ if (_processing == 0)
+ {
+ // Check parent folder is a GAB contacts folder
+ if (_gabFolders.Contains(item.ParentEntryId) && IsGABItem(item))
+ {
+ DoSuppressEvent(findInspector ? item : null, ref cancel);
+ }
+ }
+ }
+
+ private bool IsGABItem(IItem item)
+ {
+ // For some reason, Outlook creates a meeting request in the GAB folder. Modifying that should be allowed
+ return item is IContactItem || item is IDistributionList;
+ }
+
+ private void SuppressMoveEventHandler(IFolder src, IItem item, IFolder target, ref bool cancel)
+ {
+ // Allow events from processing
+ if (_processing == 0)
+ {
+ // Always disallow moves from out of the folder
+ DoSuppressEvent(null, ref cancel);
+ }
+ }
+
+ ///
+ ///
+ ///
+ /// If specified, the function will attempt to find an inspector for the item and close it.
+ ///
+ private void DoSuppressEvent(IItem item, ref bool cancel)
+ {
+ if (item != null)
+ {
+ foreach (Inspector inspector in App.Inspectors)
+ {
+ if (item.EntryId == inspector.CurrentItem.EntryID)
+ {
+ break;
+ }
+ }
+ }
+ MessageBox.Show(StringUtil.GetResourceString("GABEvent_Body"),
+ StringUtil.GetResourceString("GABEvent_Title"),
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning
+ );
+ cancel = true;
+ }
+
+ #endregion
+
+ #region Resync
+
+ internal void FullResync()
+ {
+ try
+ {
+ Logger.Instance.Trace(this, "FullResync begin: {0}", _processing);
+ BeginProcessing();
+
+ // Delete any contacts folders in the local store
+ using (ZPushLocalStore store = ZPushLocalStore.GetInstance(App))
+ {
+ if (store != null)
+ {
+ using (IFolder root = store.RootFolder)
+ {
+ foreach (IFolder folder in root.GetSubFolders())
+ {
+ // TODO: let enumerator handle this
+ using (folder)
+ {
+ try
+ {
+ if (IsGABContactsFolder(folder))
+ {
+ Logger.Instance.Debug(this, "FullResync: Deleting contacts folder: {0}", folder.Name);
+ folder.Delete();
+ }
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Error(this, "FullResync: Exception deleting contacts folder: {0}", e);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Do the resync
+ foreach (GABHandler gab in _gabsByDomainName.Values)
+ {
+ Logger.Instance.Debug(this, "FullResync: Starting resync: {0}", gab.DisplayName);
+ Tasks.Task(this, "FullResync", () => gab.FullResync());
+ }
+ }
+ finally
+ {
+ EndProcessing();
+ Logger.Instance.Trace(this, "FullResync done: {0}", _processing);
+ }
+ }
+
+ #endregion
+
+ #region Contacts folders
+
+ [Browsable(false)]
+ public IEnumerable GABHandlers
+ {
+ get
+ {
+ return _gabsByDomainName.Values;
+ }
+ }
+
+ private IAddressBook FindGABForDomain(IFolder root, string domain)
+ {
+ // Scan all subfolders for the GAB
+ foreach (IAddressBook subfolder in root.GetSubFolders())
+ {
+ try
+ {
+ if (subfolder.DefaultMessageClass == OutlookConstants.MESSAGE_CLASS_CONTACTS)
+ {
+ GABInfo gabInfo = GABInfo.Get(subfolder);
+ if (gabInfo != null && gabInfo.IsForDomain(domain))
+ {
+ Logger.Instance.Debug(this, "Found existing GAB: {0}", gabInfo);
+ return subfolder;
+ }
+ }
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Warning(this, "Exception scanning GABs: {0}", e);
+ }
+ Logger.Instance.Debug(this, "Skipping GAB folder: {0}", subfolder.Name);
+ subfolder.Dispose();
+ }
+ return null;
+ }
+
+ private IAddressBook CreateGABContacts(string domainName)
+ {
+ if (_store != null)
+ {
+ _store.Dispose();
+ _store = null;
+ }
+ _store = ZPushLocalStore.GetInstance(App);
+ if (_store == null)
+ return null;
+
+ // Try to find the existing GAB
+ using (IFolder root = _store.RootFolder)
+ {
+ IAddressBook gab = FindGABForDomain(root, domainName);
+ if (gab == null)
+ {
+ Logger.Instance.Debug(this, "Creating new GAB folder for {0}", domainName);
+ string name = string.Format(Properties.Resources.GAB_FolderFormat, domainName);
+ gab = root.CreateFolder(name);
+ }
+ else
+ {
+ Logger.Instance.Debug(this, "Found existing GAB folder for {0}", domainName);
+ }
+
+ // The local folders are hidden, unhide tha GAB folder
+ gab.AttrHidden = false;
+
+ // Update admin
+ _gabFolders.Add(gab.EntryId);
+ GABInfo gabInfo = GABInfo.Get(gab, domainName);
+ gabInfo.Store(gab);
+
+ // Hook BeforeMove event to prevent modifications
+ // TODO: use ZPushWatcher for this?
+ gab.BeforeItemMove += SuppressMoveEventHandler;
+
+ return gab;
+ }
+ }
+
+ private void DisposeGABContacts(IAddressBook gab)
+ {
+ // Unhook the event to prevent the gab lingering in memory
+ gab.BeforeItemMove -= SuppressMoveEventHandler;
+ }
+
+ public static GABInfo GetGABContactsFolderInfo(IFolder folder)
+ {
+ if (folder.DefaultMessageClass != OutlookConstants.MESSAGE_CLASS_CONTACTS)
+ return null;
+
+ return GABInfo.Get(folder);
+ }
+
+ public static bool IsGABContactsFolder(IFolder folder)
+ {
+ return GetGABContactsFolderInfo(folder) != null;
+ }
+
+ private void AccountDiscovered(ZPushAccount zpush)
+ {
+ Logger.Instance.Info(this, "Account discovered: {0}", zpush.DisplayName);
+ _domains.Add(zpush.DomainName);
+
+ zpush.ConfirmedChanged += (z) =>
+ {
+ if (zpush.Confirmed == ZPushAccount.ConfirmationType.IsZPush &&
+ !string.IsNullOrEmpty(zpush.GABFolder))
+ {
+ // Set up the Z-Push channel listener
+ ZPushChannel channel = ZPushChannels.Get(this, zpush, zpush.GABFolder);
+ channel.Available += ZPushChannelAvailable;
+ channel.Start();
+ }
+ };
+ }
+
+ private void AccountRemoved(ZPushAccount account)
+ {
+ try
+ {
+ foreach (GABHandler gab in _gabsByDomainName.Values)
+ {
+ gab.RemoveAccount(account);
+ }
+ CheckGABRemoved();
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Error(this, "Exception in AccountRemoved: {0}", e);
+ }
+ }
+
+ private void AccountsScanned()
+ {
+ try
+ {
+ Logger.Instance.Debug(this, "Accounts scanned");
+ CheckGABUnused();
+ CheckGABRemoved();
+ }
+ catch(System.Exception e)
+ {
+ Logger.Instance.Error(this, "Exception in AccountsScanned: {0}", e);
+ }
+ }
+
+ ///
+ /// Checks and removes any GAB folders that are not registered to accounts. This
+ /// happens if an account is removed while Outlook is closed.
+ ///
+ private void CheckGABUnused()
+ {
+ if (!CheckUnused)
+ return;
+
+ if (_store != null)
+ {
+ _store.Dispose();
+ _store = null;
+ }
+ _store = ZPushLocalStore.GetInstance(App);
+ if (_store == null)
+ return;
+
+ bool deletedSomething = false;
+ using (IFolder root = _store.RootFolder)
+ {
+ foreach (IFolder subfolder in root.GetSubFolders())
+ {
+ using (subfolder)
+ {
+ // Remove any contacts folder that is not registered for GAB
+ GABInfo info = GetGABContactsFolderInfo(subfolder);
+ if (info != null && !_domains.Contains(info.Domain))
+ {
+ Logger.Instance.Info(this, "Unused GAB folder: {0} - {1}", subfolder.EntryId, subfolder.Name);
+ try
+ {
+ deletedSomething = true;
+ subfolder.Delete();
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Error(this, "Error removing GAB folder: {0}", e);
+ }
+ }
+ }
+ }
+ }
+
+ if (deletedSomething)
+ EmptyDeletedItems();
+ }
+
+ private void CheckGABRemoved()
+ {
+ Logger.Instance.Debug(this, "CheckGABRemoved");
+
+ // Find any GABs that no longer have accounts
+ List> remove = new List>();
+ foreach(KeyValuePair entry in _gabsByDomainName)
+ {
+ Logger.Instance.Debug(this, "CheckGABRemoved: {0} - {1}", entry.Key, entry.Value.HasAccounts);
+ if (!entry.Value.HasAccounts)
+ {
+ remove.Add(entry);
+ }
+ }
+
+ // Remove any
+ if (remove.Count != 0)
+ {
+ foreach (KeyValuePair entry in remove)
+ {
+ try
+ {
+ Logger.Instance.Info(this, "Removing GAB: {0}", entry.Key);
+ _gabsByDomainName.Remove(entry.Key);
+ entry.Value.Remove();
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Error(this, "Exception removing GAB: {0}: {1}", entry.Key, e);
+ }
+ }
+
+ EmptyDeletedItems();
+ }
+ }
+
+ private void RegisterGABAccount(ZPushAccount account, IFolder folder)
+ {
+ // Determine the domain name
+ string domain = account.DomainName;
+
+ // Could already be registered if there are multiple accounts on the same domain
+ GABHandler gab;
+ if (!_gabsByDomainName.TryGetValue(domain, out gab))
+ {
+ // Create the handler
+ gab = new GABHandler(this, (f) => CreateGABContacts(domain), (f) => DisposeGABContacts(f));
+ _gabsByDomainName.Add(domain, gab);
+ }
+ else
+ {
+ Logger.Instance.Debug(this, "GAB handler already registered: {0} on {1}", folder, folder.StoreDisplayName);
+ }
+
+ // Register the account with the GAB
+ gab.AddAccount(account, folder);
+
+ // The folder has become available, check the GAB messages
+ DoProcess(account, gab, null);
+
+ // And watch for any new messages
+ Watcher.WatchItems(folder, (item) => DoProcess(account, gab, item), false);
+ }
+
+ #endregion
+
+ #region Processing
+
+ private void ZPushChannelAvailable(IFolder folder)
+ {
+ using (IStore store = folder.Store)
+ {
+ Logger.Instance.Debug(this, "Z-Push channel available: {0} on {1}", folder, store.DisplayName);
+
+ ZPushAccount account = Watcher.Accounts.GetAccount(folder);
+ if (account != null)
+ {
+ account.LinkedGABFolder(folder);
+ RegisterGABAccount(account, folder);
+ }
+ else
+ {
+ Logger.Instance.Warning(this, "Z-Push channel account not found: {0} on {1}", folder, store.DisplayName);
+ }
+ Logger.Instance.Debug(this, "Z-Push channel available done");
+ }
+ }
+
+ private void DoProcess(ZPushAccount account, GABHandler gab, IZPushItem item)
+ {
+ // Multiple accounts - and therefore multiple folders - may use the same GAB.
+ // One process the items from the first associated account
+ if (account != gab.ActiveAccount)
+ {
+ Logger.Instance.Trace(this, "Ignoring GAB message: {0} - {1}", account, item);
+ return;
+ }
+
+ ++_processing;
+ Logger.Instance.Trace(this, "Processing GAB message: {0} - {1}", account, _processing);
+ try
+ {
+ gab.Process(item);
+ EmptyDeletedItems();
+ }
+ finally
+ {
+ Logger.Instance.Trace(this, "Processed GAB message: {0} - {1}", account, _processing);
+ --_processing;
+ }
+ }
+
+ private void EmptyDeletedItems()
+ {
+ if (_store != null)
+ _store.EmptyDeletedItems();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs
new file mode 100644
index 0000000..5e0bbce
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABHandler.cs
@@ -0,0 +1,693 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Acacia.Stubs;
+using Acacia.ZPush;
+using Acacia.Utils;
+using System.Collections;
+using static Acacia.DebugOptions;
+
+namespace Acacia.Features.GAB
+{
+ public class GABHandler : LogContext
+ {
+ public string LogContextId { get { return "GAB"; } }
+ private readonly FeatureGAB _feature;
+
+ #region Contacts
+
+ private readonly Func _contactsProvider;
+ private readonly Action _contactsDisposer;
+
+ private IAddressBook _contacts;
+ public IAddressBook Contacts
+ {
+ get
+ {
+ if (_contacts == null)
+ {
+ _contacts = _contactsProvider(Folder);
+ }
+ return _contacts;
+ }
+ }
+
+ private IStorageItem GetIndexItem()
+ {
+ return Contacts?.GetStorageItem(Constants.ZPUSH_GAB_INDEX);
+ }
+
+ #endregion
+
+ #region Accounts & Folders
+
+ public ZPushAccount ActiveAccount
+ {
+ get
+ {
+ return ThisAddIn.Instance.Watcher.Accounts.GetAccount(Folder);
+ }
+ }
+
+ ///
+ /// The list of accounts that are associated with this GAB.
+ ///
+ private readonly List _accounts = new List();
+ private readonly List _accountFolders = new List();
+
+ private IFolder Folder
+ {
+ get
+ {
+ if (!HasAccounts)
+ return null;
+ return _accountFolders.FirstOrDefault();
+ }
+ }
+
+ public void AddAccount(ZPushAccount account, IFolder folder)
+ {
+ _accounts.Add(account);
+ _accountFolders.Add(folder);
+ }
+
+ public void RemoveAccount(ZPushAccount account)
+ {
+ int i = _accounts.IndexOf(account);
+ if (i >= 0)
+ {
+ _accounts.RemoveAt(i);
+ _accountFolders.RemoveAt(i);
+ }
+ }
+
+ internal bool HasAccounts
+ {
+ get
+ {
+ return _accounts.Count > 0;
+ }
+ }
+
+ #endregion
+
+ public GABHandler(FeatureGAB feature, Func contactsProvider, Action contactsDisposer)
+ {
+ this._feature = feature;
+ this._contactsProvider = contactsProvider;
+ this._contactsDisposer = contactsDisposer;
+ }
+
+ public string DisplayName
+ {
+ get
+ {
+ using(IStore store = Folder.Store)
+ return store.DisplayName;
+ }
+ }
+
+ #region Processing
+
+ public void FullResync()
+ {
+ ClearContacts();
+ Process(null);
+ }
+
+ private void ClearContacts()
+ {
+ if (Contacts != null)
+ {
+ try
+ {
+ Contacts.Delete();
+ }
+ catch (Exception e)
+ {
+ Logger.Instance.Warning(this, "Error clearing contacts folder for {0}: {1}", DisplayName, e);
+ // There was an error deleting the contacts folder, try clearing it
+ using (IStorageItem index = GetIndexItem())
+ {
+ index?.Delete();
+ }
+ Contacts.Clear();
+ }
+ CleanupContactsObject();
+ }
+ }
+
+ ///
+ /// Processes the GAB message(s).
+ ///
+ /// If specified, this item has changed. If null, means a global check should be performed
+ public void Process(IZPushItem item)
+ {
+ try
+ {
+ if (item == null)
+ {
+ if (Folder != null)
+ ProcessMessages();
+ }
+ else
+ {
+ ProcessMessage(item);
+ }
+ }
+ catch(Exception e)
+ {
+ Logger.Instance.Error(this, "Exception in GAB.Process: {0}", e);
+ }
+ }
+
+ private void ProcessMessages()
+ {
+ if (!_feature.ProcessFolder)
+ return;
+
+ DetermineSequence();
+ if (CurrentSequence == null)
+ return; // No messages to process
+
+ if (!_feature.ProcessItems)
+ return;
+
+ // Process the messages
+ foreach (IItem item in Folder.Items)
+ {
+ // TODO: make type-checking iterator?
+ if (item is IZPushItem)
+ {
+ string entryId = item.EntryId;
+ Logger.Instance.Trace(this, "Checking chunk: {0}", item.Subject);
+ if (_feature.ProcessItems2)
+ {
+ Tasks.Task(_feature, "ProcessChunk", () =>
+ {
+ using (IItem item2 = Folder.GetItemById(entryId))
+ {
+ if (item2 != null)
+ ProcessMessage((IZPushItem)item2);
+ }
+ });
+ }
+ }
+ }
+ }
+
+ public const string PROP_LAST_PROCESSED = "ZPushLastProcessed";
+ public const string PROP_SEQUENCE = "ZPushSequence";
+ public const string PROP_CHUNK = "ZPushChunk";
+ public const string PROP_GAB_ID = "ZPushId";
+ public const string PROP_CURRENT_SEQUENCE = "ZPushCurrentSequence";
+
+ private void ProcessMessage(IZPushItem item)
+ {
+ if (!_feature.ProcessMessage)
+ return;
+
+ // Check if the message is for the current sequence
+ ChunkIndex? optionalIndex = ChunkIndex.Parse(item.Subject);
+ if (optionalIndex == null)
+ {
+ Logger.Instance.Trace(this, "Not a chunk: {0}", item.Subject);
+ return;
+ }
+
+ if (optionalIndex.Value.numberOfChunks != CurrentSequence)
+ {
+ // Try to update the current sequence; this message may indicate that it has changed
+ DetermineSequence();
+
+ // If it is still not for the current sequence, it's an old message
+ if (optionalIndex.Value.numberOfChunks != CurrentSequence)
+ {
+ Logger.Instance.Trace(this, "Skipping, wrong sequence: {0}", item.Subject);
+ return;
+ }
+ }
+ ChunkIndex index = optionalIndex.Value;
+
+ // Check if the message is up to date
+ string lastProcessed = GetChunkStateString(index);
+ if (lastProcessed == item.Location)
+ {
+ Logger.Instance.Trace(this, "Already up to date: {0} - {1}", item.Subject, item.Location);
+ return;
+ }
+
+ // Process it
+ Logger.Instance.Trace(this, "Processing: {0} - {1} - {2}", item.Subject, item.Location, lastProcessed);
+ _feature?.BeginProcessing();
+ try
+ {
+ // Delete the old contacts from this chunk
+ ISearch search = Contacts.Search();
+ search.AddField(PROP_SEQUENCE, true).SetOperation(SearchOperation.Equal, index.numberOfChunks);
+ search.AddField(PROP_CHUNK, true).SetOperation(SearchOperation.Equal, index.chunk);
+ foreach (IItem oldItem in search.Search())
+ {
+ // TODO: Search should handle this, like folder enumeration
+ using (oldItem)
+ {
+ Logger.Instance.Trace(this, "Deleting GAB entry: {0}", oldItem.Subject);
+ oldItem.Delete();
+ }
+ }
+
+ // Create the new contacts
+ ProcessChunkBody(item, index);
+
+ // Update the state
+ SetChunkStateString(index, item.Location);
+ }
+ finally
+ {
+ _feature?.EndProcessing();
+ }
+ }
+
+ #endregion
+
+ #region Sequence
+
+ public int? CurrentSequence
+ {
+ get
+ {
+ using (IStorageItem index = GetIndexItem())
+ {
+ return index?.GetUserProperty(PROP_CURRENT_SEQUENCE)?.Value;
+ }
+ }
+ set
+ {
+ using (IStorageItem index = GetIndexItem())
+ {
+ if (value != null)
+ {
+ index.GetUserProperty(PROP_CURRENT_SEQUENCE, true).Value = value.Value;
+ }
+ else
+ {
+ index.Delete();
+ }
+ }
+ }
+ }
+
+ private IItem FindNewestChunk()
+ {
+ if (Folder == null)
+ return null;
+
+ // Scan a few of the newest items, in case there is some junk in the ZPush folder
+ // TODO: this shouldn't happen in production.
+ int i = 0;
+ foreach(IItem item in Folder.ItemsSorted("LastModificationTime", true))
+ {
+ if (ChunkIndex.Parse(item.Subject) != null)
+ return item;
+ item.Dispose();
+ if (i > Constants.ZPUSH_GAB_NEWEST_MAX_CHECK)
+ return null;
+ ++i;
+ }
+ return null;
+ }
+
+ public void DetermineSequence()
+ {
+ try
+ {
+ // Find the newest chunk
+ using (IItem newest = FindNewestChunk())
+ {
+ if (newest == null)
+ CurrentSequence = null;
+ else
+ {
+ Logger.Instance.Trace(this, "Newest chunk: {0}", newest.Subject);
+ ChunkIndex? newestChunkIndex = ChunkIndex.Parse(newest.Subject);
+
+ if (!CurrentSequence.HasValue || CurrentSequence.Value != newestChunkIndex?.numberOfChunks)
+ {
+ // Sequence has changed. Delete contacts
+ Logger.Instance.Trace(this, "Rechunked, deleting contacts");
+ ClearContacts();
+
+ // Determine new sequence
+ if (newestChunkIndex == null)
+ {
+ using (IStorageItem index = GetIndexItem())
+ {
+ if (index != null)
+ index.Delete();
+ }
+ }
+ else
+ {
+ int numberOfChunks = newestChunkIndex.Value.numberOfChunks;
+ using (IStorageItem index = GetIndexItem())
+ {
+ index.GetUserProperty(PROP_CURRENT_SEQUENCE, true).Value = numberOfChunks;
+ index.GetUserProperty(PROP_LAST_PROCESSED, true).Value = CreateChunkStateString(numberOfChunks);
+ index.Save();
+ }
+ }
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ Logger.Instance.Trace(this, "Exception determining sequence: {0}", e);
+ // Delete the index item
+ using (IStorageItem index = GetIndexItem())
+ index?.Delete();
+ return;
+ }
+ Logger.Instance.Trace(this, "Current sequence: {0}", CurrentSequence);
+ }
+
+ private string CreateChunkStateString(int count)
+ {
+ string[] defaultValues = new string[count];
+ return string.Join(";", defaultValues);
+ }
+
+ private string GetChunkStateString(ChunkIndex index)
+ {
+ using (IStorageItem item = GetIndexItem())
+ {
+ if (item == null)
+ return null;
+ string state = item.GetUserProperty(PROP_LAST_PROCESSED)?.Value;
+ if (string.IsNullOrEmpty(state))
+ return null;
+
+ string[] parts = state.Split(';');
+ if (parts.Length != index.numberOfChunks)
+ {
+ Logger.Instance.Error(this, "Wrong number of chunks, got {0}, expected {1}: {2}",
+ parts.Length, index.numberOfChunks, state);
+ }
+ return parts[index.chunk];
+ }
+ }
+
+ private void SetChunkStateString(ChunkIndex index, string partState)
+ {
+ using (IStorageItem item = GetIndexItem())
+ {
+ string state = item.GetUserProperty(PROP_LAST_PROCESSED)?.Value;
+ string[] parts;
+ if (string.IsNullOrEmpty(state))
+ parts = new string[index.numberOfChunks];
+ else
+ parts = state.Split(';');
+ if (parts.Length != index.numberOfChunks)
+ {
+ Logger.Instance.Error(this, "Wrong number of chunks, got {0}, expected {1}: {2}",
+ parts.Length, index.numberOfChunks, state);
+ }
+ parts[index.chunk] = partState;
+ string combined = string.Join(";", parts);
+
+ item.GetUserProperty(PROP_LAST_PROCESSED, true).Value = combined;
+ item.Save();
+ }
+ }
+
+ #endregion
+
+ #region Message parsing
+
+
+ private ValueType Get(Dictionary values, string name)
+ where ValueType : class
+ {
+ object value;
+ values.TryGetValue(name, out value);
+ return value as ValueType;
+ }
+
+ private void ProcessChunkBody(IZPushItem item, ChunkIndex index)
+ {
+ // Process the body
+ foreach (var entry in JSONUtils.Deserialise(item.Body))
+ {
+ string id = entry.Key;
+ Dictionary value = (Dictionary)entry.Value;
+ Tasks.Task(_feature, "CreateItem", () => CreateObject(index, id, value));
+ }
+ }
+
+ private void CreateObject(ChunkIndex index, string id, Dictionary value)
+ {
+ try
+ {
+ _feature?.BeginProcessing();
+
+ string type = Get(value, "type");
+ if (type == "contact")
+ CreateContact(id, value, index, 0);
+ else if (type == "group")
+ CreateGroup(id, value, index);
+ else if (type == "equipment")
+ CreateContact(id, value, index, OutlookConstants.DT_EQUIPMENT);
+ else if (type == "room")
+ CreateContact(id, value, index, OutlookConstants.DT_ROOM);
+ else
+ {
+ Logger.Instance.Warning(this, "Unknown entry type: {0}", type);
+ }
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Error(this, "Error creating entry: {0}: {1}", id, e);
+ }
+ finally
+ {
+ _feature?.EndProcessing();
+ }
+ }
+
+ private void CreateContact(string id, Dictionary value, ChunkIndex index, int resourceType)
+ {
+ if (!_feature.CreateContacts)
+ return;
+
+ using (IContactItem contact = Contacts.Create())
+ {
+ Logger.Instance.Trace(this, "Creating contact: {0}", id);
+ contact.CustomerID = id;
+
+ // Create the contact data
+ if (Get(value, "displayName") != null) contact.FullName = Get(value, "displayName");
+ if (Get(value, "givenName") != null) contact.FirstName = Get(value, "givenName");
+ if (Get(value, "initials") != null) contact.Initials = Get(value, "initials");
+ if (Get(value, "surname") != null) contact.LastName = Get(value, "surname");
+ if (Get(value, "title") != null) contact.JobTitle = Get(value, "title");
+
+ if (Get(value, "smtpAddress") != null)
+ {
+ contact.Email1Address = Get(value, "smtpAddress");
+ contact.Email1AddressType = "SMTP";
+ }
+ if (Get(value, "companyName") != null) contact.CompanyName = Get(value, "companyName");
+ if (Get(value, "officeLocation") != null) contact.OfficeLocation = Get(value, "officeLocation");
+ if (Get(value, "businessTelephoneNumber") != null) contact.BusinessTelephoneNumber = Get(value, "businessTelephoneNumber");
+ if (Get(value, "mobileTelephoneNumber") != null) contact.MobileTelephoneNumber = Get(value, "mobileTelephoneNumber");
+ if (Get(value, "homeTelephoneNumber") != null) contact.HomeTelephoneNumber = Get(value, "homeTelephoneNumber");
+ if (Get(value, "beeperTelephoneNumber") != null) contact.PagerNumber = Get(value, "beeperTelephoneNumber");
+ if (Get(value, "primaryFaxNumber") != null) contact.BusinessFaxNumber = Get(value, "primaryFaxNumber");
+ if (Get(value, "organizationalIdNumber") != null) contact.OrganizationalIDNumber = Get(value, "organizationalIdNumber");
+ if (Get(value, "postalAddress") != null) contact.BusinessAddress = Get(value, "postalAddress");
+ if (Get(value, "businessAddressCity") != null) contact.BusinessAddressCity = Get(value, "businessAddressCity");
+ if (Get(value, "businessAddressPostalCode") != null) contact.BusinessAddressPostalCode = Get(value, "businessAddressPostalCode");
+ if (Get(value, "businessAddressPostOfficeBox") != null) contact.BusinessAddressPostOfficeBox = Get(value, "businessAddressPostOfficeBox");
+ if (Get(value, "businessAddressStateOrProvince") != null) contact.BusinessAddressState = Get(value, "businessAddressStateOrProvince");
+ if (Get(value, "language") != null) contact.Language = Get(value, "language");
+
+ // Thumbnail
+ string photoData = Get(value, "thumbnailPhoto");
+ if (photoData != null)
+ {
+ string path = null;
+ try
+ {
+ byte[] data = Convert.FromBase64String(photoData);
+ path = System.IO.Path.GetTempFileName();
+ Logger.Instance.Trace(this, "Contact image: {0}", path);
+ System.IO.File.WriteAllBytes(path, data);
+ contact.SetPicture(path);
+ }
+ catch (Exception) { }
+ finally
+ {
+ try
+ {
+ if (path != null)
+ System.IO.File.Delete(path);
+ }
+ catch (Exception) { }
+ }
+ }
+
+ // Resource flags
+ if (resourceType != 0)
+ {
+ contact.SetProperty(OutlookConstants.PR_DISPLAY_TYPE, 0);
+ contact.SetProperty(OutlookConstants.PR_DISPLAY_TYPE_EX, resourceType);
+ }
+
+ // Standard properties
+ SetItemStandard(contact, id, value, index);
+ contact.Save();
+
+ // Update the groups
+ AddItemToGroups(contact, id, value, index);
+ }
+
+ }
+
+ private void CreateGroup(string id, Dictionary value, ChunkIndex index)
+ {
+ if (!_feature.CreateGroups)
+ return;
+
+ using (IDistributionList group = Contacts.Create())
+ {
+ Logger.Instance.Debug(this, "Creating group: {0}", id);
+ group.DLName = Get(value, "displayName");
+ if (Get(value, "smtpAddress") != null)
+ {
+ group.SMTPAddress = Get(value, "smtpAddress");
+ }
+
+ SetItemStandard(group, id, value, index);
+ group.Save();
+
+ if (_feature.GroupMembers)
+ {
+ ArrayList members = Get(value, "members");
+ if (members != null)
+ {
+ foreach (string memberId in members)
+ {
+ using (IItem item = FindItemById(memberId))
+ {
+ Logger.Instance.Debug(this, "Finding member {0} of {1}: {2}", memberId, id, item?.EntryId);
+ if (item != null)
+ AddGroupMember(group, item);
+ }
+ }
+ }
+ group.Save();
+ }
+
+ AddItemToGroups(group, id, value, index);
+ }
+ }
+
+ private IItem FindItemById(string id)
+ {
+ ISearch search = Contacts.Search();
+ search.AddField(PROP_GAB_ID, true).SetOperation(SearchOperation.Equal, id);
+ return search.SearchOne();
+ }
+
+ private void SetItemStandard(IItem item, string id, Dictionary value, ChunkIndex index)
+ {
+ // Set the chunk data
+ item.GetUserProperty(PROP_SEQUENCE, true).Value = index.numberOfChunks;
+ item.GetUserProperty(PROP_CHUNK, true).Value = index.chunk;
+ item.GetUserProperty(PROP_GAB_ID, true).Value = id;
+ }
+
+ private void AddGroupMember(IDistributionList group, IItem item)
+ {
+ if (!_feature.GroupMembersAdd)
+ return;
+
+ if (item is IDistributionList)
+ {
+ if (!_feature.NestedGroups)
+ return;
+ }
+
+ group.AddMember(item);
+ }
+
+ private void AddItemToGroups(IItem item, string id, Dictionary value, ChunkIndex index)
+ {
+ if (!_feature.GroupMembers)
+ return;
+
+ // Find the groups
+ if (Get(value, "memberOf") != null)
+ {
+ ArrayList members = Get(value, "memberOf");
+ foreach (string memberOf in members)
+ {
+ using (IItem groupItem = FindItemById(memberOf))
+ {
+ Logger.Instance.Debug(this, "Finding group {0} for {1}: {2}", memberOf, id, groupItem?.EntryId);
+ if (groupItem is IDistributionList)
+ {
+ AddGroupMember((IDistributionList)groupItem, item);
+ groupItem.Save();
+ }
+ }
+ }
+ }
+ }
+
+ #endregion
+
+ #region Removal
+
+ public void Remove()
+ {
+ if (_contacts != null)
+ {
+ _contacts.Delete();
+ }
+ CleanupContactsObject();
+ }
+
+ private void CleanupContactsObject()
+ {
+ if (_contacts != null)
+ {
+ if (_contactsDisposer != null)
+ _contactsDisposer(_contacts);
+ _contacts.Dispose();
+ _contacts = null;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABInfo.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABInfo.cs
new file mode 100644
index 0000000..ae56412
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABInfo.cs
@@ -0,0 +1,93 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Stubs;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.GAB
+{
+ public class GABInfo
+ {
+ private const string ID = "GAB=";
+ public readonly string Domain;
+
+ public GABInfo(string domain)
+ {
+ this.Domain = domain;
+ }
+
+ ///
+ /// Retrieves the GAB info for the folder.
+ ///
+ /// The folder
+ /// The domain name. If this is not null, and a GAB info is not found, it is created
+ ///
+ public static GABInfo Get(IFolder folder, string forDomain = null)
+ {
+ GABInfo gab = GetExisting(folder);
+ if (gab == null && forDomain != null)
+ gab = new GABInfo(forDomain);
+ return gab;
+ }
+
+ private static GABInfo GetExisting(IFolder folder)
+ {
+ string subject = null;
+ try
+ {
+ subject = (string)folder.GetProperty(OutlookConstants.PR_SUBJECT);
+ }
+ catch (System.Exception) { }
+ if (string.IsNullOrEmpty(subject))
+ return null;
+
+ string[] parts = subject.Split(';');
+ if (parts.Length < 1 || !parts[0].StartsWith(ID))
+ return null;
+
+ string domain = parts[0].Substring(ID.Length);
+ GABInfo gab = new GABInfo(domain);
+
+ return gab;
+ }
+
+ public override string ToString()
+ {
+ return "GAB(" + Domain + ")";
+ }
+
+ public bool IsForDomain(string domain)
+ {
+ return this.Domain == domain;
+ }
+
+ public void Store(IFolder folder)
+ {
+ string s = Serialize();
+ folder.SetProperty(OutlookConstants.PR_SUBJECT, s);
+ }
+
+ private string Serialize()
+ {
+ return ID + Domain;
+ }
+
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.Designer.cs
new file mode 100644
index 0000000..3512b35
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.Designer.cs
@@ -0,0 +1,58 @@
+namespace Acacia.Features.GAB
+{
+ partial class GABSettings
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(GABSettings));
+ this.buttonGABResync = new System.Windows.Forms.Button();
+ this.SuspendLayout();
+ //
+ // buttonGABResync
+ //
+ resources.ApplyResources(this.buttonGABResync, "buttonGABResync");
+ this.buttonGABResync.Name = "buttonGABResync";
+ this.buttonGABResync.UseVisualStyleBackColor = true;
+ this.buttonGABResync.Click += new System.EventHandler(this.buttonGABResync_Click);
+ //
+ // GABSettings
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BackColor = System.Drawing.SystemColors.Window;
+ this.Controls.Add(this.buttonGABResync);
+ this.Name = "GABSettings";
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.Button buttonGABResync;
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.cs
new file mode 100644
index 0000000..3d2f9cd
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.cs
@@ -0,0 +1,57 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Drawing;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using Acacia.UI;
+
+namespace Acacia.Features.GAB
+{
+ public partial class GABSettings : FeatureSettings
+ {
+ private readonly FeatureGAB _feature;
+ public override Feature Feature
+ {
+ get
+ {
+ return _feature;
+ }
+ }
+
+ public GABSettings(FeatureGAB feature = null)
+ {
+ this._feature = feature;
+
+ InitializeComponent();
+ }
+
+ private void buttonGABResync_Click(object sender, EventArgs e)
+ {
+ // Allow null feature for designer
+ if (_feature != null)
+ {
+ _feature.FullResync();
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.resx
new file mode 100644
index 0000000..c62a953
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/GAB/GABSettings.resx
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ True
+
+
+
+ 3, 3
+
+
+
+ 8, 0, 8, 0
+
+
+ 210, 23
+
+
+ 0
+
+
+ Resynchronise Global Address Books
+
+
+ buttonGABResync
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ True
+
+
+ 6, 13
+
+
+ True
+
+
+ 216, 31
+
+
+ GABSettings
+
+
+ Acacia.UI.FeatureSettings, ZPush, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Notes/FeatureNotes.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Notes/FeatureNotes.cs
new file mode 100644
index 0000000..38bf5f6
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Notes/FeatureNotes.cs
@@ -0,0 +1,292 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Stubs;
+using Acacia.Stubs.OutlookWrappers;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Microsoft.Office.Interop.Outlook;
+using System;
+using System.Collections.Generic;
+using System.Drawing;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using static Acacia.DebugOptions;
+
+namespace Acacia.Features.Notes
+{
+ [AcaciaOption("Provides the possibility to synchronise Notes to and from a Z-Push server.")]
+ public class FeatureNotes : Feature
+ {
+ public FeatureNotes()
+ {
+
+ }
+
+ public override void Startup()
+ {
+ Watcher.WatchFolder(new FolderRegistrationTyped(this, ItemType.NoteItem),
+ OnNotesFolderDiscovered, OnNotesFolderChanged);
+ }
+
+ #region Debug options
+
+ [AcaciaOption("Disables the patching of Notes folders types. Without this, Outlook will not recognise " +
+ "these folders as being Notes folders, and contents will not be synchronised.")]
+ public bool PatchFolders
+ {
+ get { return GetOption(OPTION_PATCH_FOLDERS); }
+ set { SetOption(OPTION_PATCH_FOLDERS, value); }
+ }
+ private static readonly BoolOption OPTION_PATCH_FOLDERS = new BoolOption("PatchFolders", true);
+
+ [AcaciaOption("Disables the patching of Note item types. Without this, Outlook will not recognise " +
+ "these items as being Notes, and they may appear in unusual states.")]
+ public bool PatchItems
+ {
+ get { return GetOption(OPTION_PATCH_ITEMS); }
+ set { SetOption(OPTION_PATCH_ITEMS, value); }
+ }
+ private static readonly BoolOption OPTION_PATCH_ITEMS = new BoolOption("PatchItems", true);
+
+ #endregion
+
+ private void OnNotesFolderDiscovered(IFolder folder)
+ {
+ Logger.Instance.Debug(this, "NOTES FOLDER: {0}", folder);
+ // Always watch the folder. Any notes being synced in indicate the server supports notes,
+ // and otherwise there's no harm done.
+ Watcher.WatchItems(folder, PatchNote, true);
+
+ // Patch the folder if needed
+ PatchIfConfirmed(folder);
+ }
+
+ private void OnNotesFolderChanged(IFolder folder)
+ {
+ Logger.Instance.Debug(this, "NOTES FOLDER CHANGED: {0}, type={1}", folder, folder.GetProperty(OutlookConstants.PR_EAS_SYNCTYPE));
+ // Outlook sometimes changes the type back. Patch again if needed.
+ PatchIfConfirmed(folder);
+ }
+
+ private bool IsNotesFolder(OutlookConstants.SyncType type)
+ {
+ return type == OutlookConstants.SyncType.Note || type == OutlookConstants.SyncType.UserNote;
+ }
+
+ private void PatchIfConfirmed(IFolder folder)
+ {
+ // Only patch if on a ZPush server that supports notes. Store the folder as entryId, there have been some
+ // issues with the folder object being disposed in the past
+ string folderId = folder.EntryId;
+ ZPushAccount zpush = Watcher.Accounts.GetAccount(folder);
+ if (zpush != null)
+ {
+ zpush.ConfirmedChanged += (z) =>
+ {
+ if (zpush.Confirmed == ZPushAccount.ConfirmationType.IsZPush &&
+ zpush.Capabilities.Has(Constants.ZPUSH_CAPABILITY_NOTES))
+ {
+ PatchFolder(folderId);
+ }
+ else if (zpush.Confirmed != ZPushAccount.ConfirmationType.Unknown)
+ {
+ // The server is either not a Z-Push server, or it does not support notes
+ // Restore any patched notes folder
+ UnpatchFolder(folderId);
+ }
+ };
+ }
+ }
+
+ private void PatchFolder(string folderId)
+ {
+ if (!PatchFolders)
+ return;
+
+ Logger.Instance.Trace(this, "PatchFolder: {0}", folderId);
+ try
+ {
+ using (IFolder folder = Mapping.GetFolderFromID(folderId))
+ {
+ if (folder == null)
+ return;
+
+ // Patch if needed
+ OutlookConstants.SyncType type = FolderUtils.GetFolderSyncType(folder);
+ Logger.Instance.Trace(this, "Notes folder type: {0}", type);
+ if (IsNotesFolder(type))
+ {
+ Logger.Instance.Debug(this, "Patching Notes folder type: {0}", type);
+
+ // Change to task folder
+ folder.SetProperties(new string[]
+ {
+ OutlookConstants.PR_NET_FOLDER_FLAGS,
+ OutlookConstants.PR_EAS_SYNCTYPE,
+ OutlookConstants.PR_EAS_SYNC1,
+ OutlookConstants.PR_EAS_SYNC2
+ }, new object[]
+ {
+ 0, (int)OutlookConstants.SyncType.UserAppointment, true, true
+ });
+
+ if (type == OutlookConstants.SyncType.Note)
+ {
+ // Local notes, change name
+ PatchFolderName(folder);
+ }
+ }
+ }
+ }
+ finally
+ {
+ Logger.Instance.Trace(this, "PatchFolder done");
+ }
+ }
+
+ private void UnpatchFolder(string folderId)
+ {
+ if (!PatchFolders)
+ return;
+
+ Logger.Instance.Trace(this, "UnpatchFolder: {0}", folderId);
+ try
+ {
+ using (IFolder folder = Mapping.GetFolderFromID(folderId))
+ {
+ if (folder == null)
+ return;
+
+ // Unpatch if needed
+ OutlookConstants.SyncType type = FolderUtils.GetFolderSyncType(folder, true);
+ Logger.Instance.Trace(this, "Notes folder type: {0}", type);
+ // Unpatch only if the original type is a notes folder, but the current type isn't
+ if (IsNotesFolder(type) && !IsNotesFolder(FolderUtils.GetFolderSyncType(folder)))
+ {
+ Logger.Instance.Debug(this, "Unpatching Notes folder type: {0}", type);
+
+ // Change to original notes folder
+ folder.SetProperties(new string[]
+ {
+ OutlookConstants.PR_EAS_SYNCTYPE,
+ OutlookConstants.PR_EAS_SYNC1,
+ OutlookConstants.PR_EAS_SYNC2
+ }, new object[]
+ {
+ (int)type,
+ false,
+ false
+ });
+
+ if (type == OutlookConstants.SyncType.Note)
+ {
+ // Local notes, change name
+ UnpatchFolderName(folder);
+ }
+ }
+ }
+ }
+ finally
+ {
+ Logger.Instance.Trace(this, "PatchFolder done");
+ }
+ }
+
+ private void PatchNote(IItem item)
+ {
+ if (!PatchItems)
+ return;
+
+ Logger.Instance.Trace(this, "NOTE ITEM: Subject='{0}', Class={1}",
+ item.GetProperty(OutlookConstants.PR_SUBJECT),
+ item.GetProperty(OutlookConstants.PR_MESSAGE_CLASS));
+ try
+ {
+ if ((int)item.GetProperty(OutlookConstants.PR_ICON_INDEX) != 771)
+ {
+ Logger.Instance.Trace(this, "Patching item: {0}", item.EntryId);
+
+ // Patch standard properties
+ item.SetProperties(
+ new string[] { OutlookConstants.PR_MESSAGE_CLASS, OutlookConstants.PR_ICON_INDEX, OutlookConstants.PR_NOTE_COLOR },
+ new object[] { OutlookConstants.MESSAGE_CLASS_NOTES, 771, 3 }
+ );
+
+ // Set sizes if not set, they get crappy defaults
+ try
+ {
+ // This causes an exception if nothing is set
+ item.GetProperty(OutlookConstants.PR_NOTE_WIDTH);
+ }
+ catch (System.Exception)
+ {
+ Logger.Instance.Trace(this, "Setting default sizes");
+ item.SetProperty(OutlookConstants.PR_NOTE_WIDTH, 200);
+ item.SetProperty(OutlookConstants.PR_NOTE_HEIGHT, 166);
+ item.SetProperty(OutlookConstants.PR_NOTE_X, 80);
+ item.SetProperty(OutlookConstants.PR_NOTE_Y, 80);
+ }
+ item.Save();
+ }
+ }
+ finally
+ {
+ Logger.Instance.Trace(this, "PatchNote done");
+ }
+ }
+
+ private void PatchFolderName(IFolder folder)
+ {
+ // Remove parenthesised (this computer only) or localised equivalent
+ string oldName = folder.Name;
+ int open = oldName.IndexOf('(');
+ int close = oldName.IndexOf(')');
+ if (open >= 0 && close >= 0)
+ {
+ string newName = oldName.Substring(0, Math.Min(open, close)) + oldName.Substring(Math.Max(open, close) + 1);
+ newName = newName.Trim();
+ // Set the new name, and keep the old name in subject in case of a revert
+ folder.SetProperties(new string[]
+ {
+ OutlookConstants.PR_DISPLAY_NAME, OutlookConstants.PR_SUBJECT
+ }, new object[]
+ {
+ newName, oldName
+ });
+ }
+ }
+
+ private void UnpatchFolderName(IFolder folder)
+ {
+ try
+ {
+ string oldName = (string)folder.GetProperty(OutlookConstants.PR_SUBJECT);
+ // Parentheses are not allowed in names (even though they were there originally)
+ // Replace with square brackets.
+ oldName = oldName.Replace('(', '[');
+ oldName = oldName.Replace(')', ']');
+ folder.SetProperty(OutlookConstants.PR_DISPLAY_NAME, oldName);
+ }
+ catch(System.Exception e)
+ {
+ Logger.Instance.Warning(this, "Exception in UnpatchFolderName, leaving name: {0}", e);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/FeatureOutOfOffice.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/FeatureOutOfOffice.cs
new file mode 100644
index 0000000..77cbf95
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/FeatureOutOfOffice.cs
@@ -0,0 +1,241 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.UI;
+using Acacia.UI.Outlook;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Acacia.ZPush.Connect;
+using Microsoft.Office.Interop.Outlook;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Features.OutOfOffice
+{
+ [AcaciaOption("Provides a user interface to modify Out-of-Office settings for ActiveSync accounts.")]
+ public class FeatureOutOfOffice
+ :
+ Feature, FeatureWithRibbon
+ {
+ private RibbonToggleButton _button;
+
+ public FeatureOutOfOffice()
+ {
+
+ }
+
+ public override void Startup()
+ {
+ _button = RegisterToggleButton(this, "OOF", true, ShowDialog, ZPushBehaviour.Disable);
+ Watcher.ZPushAccountChange += Watcher_ZPushAccountChange;
+ }
+
+ private static bool IsOOFEnabled(ActiveSync.SettingsOOF settings)
+ {
+ if (settings == null)
+ return false;
+ return settings.State != ActiveSync.OOFState.Disabled;
+ }
+
+ private void Watcher_ZPushAccountChange(ZPushAccount account)
+ {
+ if (_button != null)
+ {
+ if (account == null)
+ _button.IsPressed = false;
+ else
+ _button.IsPressed = IsOOFEnabled(account.GetFeatureData(this, "OOF"));
+ }
+ }
+
+ private void StoreOOFSettings(ZPushAccount account, ActiveSync.SettingsOOF settings)
+ {
+ account.SetFeatureData(this, "OOF", settings);
+ if (_button != null)
+ _button.IsPressed = IsOOFEnabled(settings);
+ }
+
+ private void ShowDialog()
+ {
+ ZPushAccount account = Watcher.CurrentZPushAccount();
+ if (account != null)
+ {
+ try
+ {
+ // Fetch the current status
+ ActiveSync.SettingsOOF settings;
+
+ try
+ {
+ settings = ProgressDialog.Execute("OOFGet",
+ (ct) =>
+ {
+ using (ZPushConnection con = new ZPushConnection(account, ct))
+ return con.Execute(new ActiveSync.SettingsOOFGet());
+ }
+ );
+ }
+ catch (System.Exception e)
+ {
+ Logger.Instance.Warning(this, "Exception getting OOF state: {0}", e);
+ if (MessageBox.Show(
+ Properties.Resources.OOFGet_Failed,
+ Properties.Resources.OOFGet_Title,
+ MessageBoxButtons.OKCancel,
+ MessageBoxIcon.Error
+ ) != DialogResult.OK)
+ {
+ return;
+ }
+ else
+ {
+ // Initialise default settings
+ settings = new ActiveSync.SettingsOOF();
+ settings.Message = new ActiveSync.OOFMessage[3];
+ }
+ }
+
+ // Store them for later use
+ StoreOOFSettings(account, settings);
+
+ // Show the dialog
+ ShowOOFDialog(account, settings);
+ }
+ catch(System.Exception e)
+ {
+ Logger.Instance.Warning(this, "Exception: {0}", e);
+ }
+ }
+ }
+
+ private void ShowOOFDialog(ZPushAccount account, ActiveSync.SettingsOOF settings)
+ {
+
+ // Show dialog
+ if (new OutOfOfficeDialog(account, settings).ShowDialog() != DialogResult.OK)
+ return;
+
+ try
+ {
+ // Store settings
+ ActiveSync.SettingsOOF actualSettings = ProgressDialog.Execute("OOFSet",
+ (ct) =>
+ {
+ using (ZPushConnection connection = new ZPushConnection(account, ct))
+ {
+ // Set the OOF state. This always seems to return ok, so we fetch the settings
+ // again, to see what happend
+ connection.Execute(new ActiveSync.SettingsOOFSet(settings));
+
+ // Fetch the OOF state
+ return connection.Execute(new ActiveSync.SettingsOOFGet());
+ }
+ }
+ );
+
+ // Store them for later use
+ StoreOOFSettings(account, actualSettings);
+
+ // Check what happened
+ string message;
+ MessageBoxIcon messageIcon;
+ if (settings.State == ActiveSync.OOFState.Disabled)
+ {
+ // Tried to disable.
+ if (actualSettings.State != ActiveSync.OOFState.Disabled)
+ {
+ // It's an error if its not actually disabled
+ message = Properties.Resources.OOFSet_DisableFailed;
+ messageIcon = MessageBoxIcon.Error;
+ }
+ else
+ {
+ // All good
+ message = Properties.Resources.OOFSet_Disabled;
+ messageIcon = MessageBoxIcon.Information;
+ }
+ }
+ else if (actualSettings.State == ActiveSync.OOFState.Disabled)
+ {
+ // It's an error if the state is set to disabled when we tried to enable
+ message = Properties.Resources.OOFSet_EnableFailed;
+ messageIcon = MessageBoxIcon.Error;
+ }
+ else
+ {
+ // All good
+ if (actualSettings.State == ActiveSync.OOFState.EnabledTimeBased)
+ {
+ message = string.Format(Properties.Resources.OOFSet_EnabledTimeBased,
+ actualSettings.From, actualSettings.Till);
+ }
+ else
+ {
+ message = Properties.Resources.OOFSet_Enabled;
+ }
+ messageIcon = MessageBoxIcon.Information;
+
+ // It's okay if the state is not the same, but it deserves a message
+ if (actualSettings.State != settings.State)
+ {
+ message = Properties.Resources.OOFSet_DifferentState + message;
+ messageIcon = MessageBoxIcon.Warning;
+ }
+ }
+
+ Logger.Instance.Debug(this, "OOF state updated: {0}, {1}", message, messageIcon);
+ MessageBox.Show(message,
+ Properties.Resources.OOFSet_Title,
+ MessageBoxButtons.OK,
+ messageIcon
+ );
+ }
+ catch (System.Exception e)
+ {
+ ErrorUtil.HandleErrorNew(this, "Exception in OOFSet", e,
+ Properties.Resources.OOFSet_Title, Properties.Resources.OOFSet_Failed);
+ }
+ }
+
+ ///
+ /// Invoked by AccountWatcher on start-up to notify of the oof status.
+ ///
+ public void OnOOFSettings(ZPushAccount account, ActiveSync.SettingsOOF oof)
+ {
+ // Store them for later use
+ StoreOOFSettings(account, oof);
+
+ // Show a message if OOF is enabled
+ if (oof.State != ActiveSync.OOFState.Disabled)
+ {
+ if (MessageBox.Show(
+ string.Format(Properties.Resources.OOFStartup_Message, account.SmtpAddress),
+ Properties.Resources.OOFStartup_Title,
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question
+ ) == DialogResult.Yes)
+ {
+ ShowOOFDialog(account, oof);
+ }
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.Designer.cs
new file mode 100644
index 0000000..f2ac927
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.Designer.cs
@@ -0,0 +1,225 @@
+namespace Acacia.Features.OutOfOffice
+{
+ partial class OutOfOfficeDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(OutOfOfficeDialog));
+ this.tableGlobal = new System.Windows.Forms.TableLayoutPanel();
+ this.chkEnable = new System.Windows.Forms.CheckBox();
+ this.tableDates = new System.Windows.Forms.TableLayoutPanel();
+ this.radioNoTime = new System.Windows.Forms.RadioButton();
+ this.radioTime = new System.Windows.Forms.RadioButton();
+ this.dateFrom = new System.Windows.Forms.DateTimePicker();
+ this.timeFrom = new System.Windows.Forms.DateTimePicker();
+ this.labelTill = new System.Windows.Forms.Label();
+ this.dateTill = new System.Windows.Forms.DateTimePicker();
+ this.timeTill = new System.Windows.Forms.DateTimePicker();
+ this.groupTextEntry = new System.Windows.Forms.GroupBox();
+ this.tableTextEntry = new System.Windows.Forms.TableLayoutPanel();
+ this.labelBody = new System.Windows.Forms.Label();
+ this.textBody = new System.Windows.Forms.TextBox();
+ this.flowButtons = new System.Windows.Forms.FlowLayoutPanel();
+ this.btnCancel = new System.Windows.Forms.Button();
+ this.btnSave = new System.Windows.Forms.Button();
+ this.tableGlobal.SuspendLayout();
+ this.tableDates.SuspendLayout();
+ this.groupTextEntry.SuspendLayout();
+ this.tableTextEntry.SuspendLayout();
+ this.flowButtons.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // tableGlobal
+ //
+ resources.ApplyResources(this.tableGlobal, "tableGlobal");
+ this.tableGlobal.Controls.Add(this.chkEnable, 0, 0);
+ this.tableGlobal.Controls.Add(this.tableDates, 0, 1);
+ this.tableGlobal.Controls.Add(this.groupTextEntry, 0, 2);
+ this.tableGlobal.Controls.Add(this.flowButtons, 0, 3);
+ this.tableGlobal.Name = "tableGlobal";
+ //
+ // chkEnable
+ //
+ resources.ApplyResources(this.chkEnable, "chkEnable");
+ this.chkEnable.Name = "chkEnable";
+ this.chkEnable.UseVisualStyleBackColor = true;
+ this.chkEnable.CheckedChanged += new System.EventHandler(this.chkEnable_CheckedChanged);
+ //
+ // tableDates
+ //
+ resources.ApplyResources(this.tableDates, "tableDates");
+ this.tableDates.Controls.Add(this.radioNoTime, 0, 0);
+ this.tableDates.Controls.Add(this.radioTime, 0, 1);
+ this.tableDates.Controls.Add(this.dateFrom, 1, 1);
+ this.tableDates.Controls.Add(this.timeFrom, 2, 1);
+ this.tableDates.Controls.Add(this.labelTill, 0, 2);
+ this.tableDates.Controls.Add(this.dateTill, 1, 2);
+ this.tableDates.Controls.Add(this.timeTill, 2, 2);
+ this.tableDates.Name = "tableDates";
+ //
+ // radioNoTime
+ //
+ resources.ApplyResources(this.radioNoTime, "radioNoTime");
+ this.radioNoTime.Checked = true;
+ this.tableDates.SetColumnSpan(this.radioNoTime, 3);
+ this.radioNoTime.Name = "radioNoTime";
+ this.radioNoTime.TabStop = true;
+ this.radioNoTime.UseVisualStyleBackColor = true;
+ //
+ // radioTime
+ //
+ resources.ApplyResources(this.radioTime, "radioTime");
+ this.radioTime.Name = "radioTime";
+ this.radioTime.UseVisualStyleBackColor = true;
+ this.radioTime.CheckedChanged += new System.EventHandler(this.radioTime_CheckedChanged);
+ //
+ // dateFrom
+ //
+ resources.ApplyResources(this.dateFrom, "dateFrom");
+ this.dateFrom.Name = "dateFrom";
+ this.dateFrom.ValueChanged += new System.EventHandler(this.dateFrom_ValueChanged);
+ //
+ // timeFrom
+ //
+ resources.ApplyResources(this.timeFrom, "timeFrom");
+ this.timeFrom.Format = System.Windows.Forms.DateTimePickerFormat.Custom;
+ this.timeFrom.Name = "timeFrom";
+ this.timeFrom.ShowUpDown = true;
+ this.timeFrom.ValueChanged += new System.EventHandler(this.timeFrom_ValueChanged);
+ //
+ // labelTill
+ //
+ resources.ApplyResources(this.labelTill, "labelTill");
+ this.labelTill.Name = "labelTill";
+ //
+ // dateTill
+ //
+ resources.ApplyResources(this.dateTill, "dateTill");
+ this.dateTill.Name = "dateTill";
+ this.dateTill.ValueChanged += new System.EventHandler(this.dateTill_ValueChanged);
+ //
+ // timeTill
+ //
+ resources.ApplyResources(this.timeTill, "timeTill");
+ this.timeTill.Format = System.Windows.Forms.DateTimePickerFormat.Custom;
+ this.timeTill.Name = "timeTill";
+ this.timeTill.ShowUpDown = true;
+ //
+ // groupTextEntry
+ //
+ resources.ApplyResources(this.groupTextEntry, "groupTextEntry");
+ this.groupTextEntry.Controls.Add(this.tableTextEntry);
+ this.groupTextEntry.Name = "groupTextEntry";
+ this.groupTextEntry.TabStop = false;
+ //
+ // tableTextEntry
+ //
+ resources.ApplyResources(this.tableTextEntry, "tableTextEntry");
+ this.tableTextEntry.Controls.Add(this.labelBody, 0, 0);
+ this.tableTextEntry.Controls.Add(this.textBody, 0, 1);
+ this.tableTextEntry.Name = "tableTextEntry";
+ //
+ // labelBody
+ //
+ resources.ApplyResources(this.labelBody, "labelBody");
+ this.labelBody.Name = "labelBody";
+ //
+ // textBody
+ //
+ this.textBody.AcceptsReturn = true;
+ resources.ApplyResources(this.textBody, "textBody");
+ this.textBody.Name = "textBody";
+ //
+ // flowButtons
+ //
+ resources.ApplyResources(this.flowButtons, "flowButtons");
+ this.flowButtons.Controls.Add(this.btnCancel);
+ this.flowButtons.Controls.Add(this.btnSave);
+ this.flowButtons.Name = "flowButtons";
+ //
+ // btnCancel
+ //
+ resources.ApplyResources(this.btnCancel, "btnCancel");
+ this.btnCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel;
+ this.btnCancel.Name = "btnCancel";
+ this.btnCancel.UseVisualStyleBackColor = true;
+ //
+ // btnSave
+ //
+ resources.ApplyResources(this.btnSave, "btnSave");
+ this.btnSave.DialogResult = System.Windows.Forms.DialogResult.OK;
+ this.btnSave.Name = "btnSave";
+ this.btnSave.UseVisualStyleBackColor = true;
+ //
+ // OutOfOfficeDialog
+ //
+ this.AcceptButton = this.btnSave;
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.CancelButton = this.btnCancel;
+ this.Controls.Add(this.tableGlobal);
+ this.MaximizeBox = false;
+ this.MinimizeBox = false;
+ this.Name = "OutOfOfficeDialog";
+ this.ShowInTaskbar = false;
+ this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Show;
+ this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.OutOfOfficeDialog_FormClosed);
+ this.tableGlobal.ResumeLayout(false);
+ this.tableGlobal.PerformLayout();
+ this.tableDates.ResumeLayout(false);
+ this.tableDates.PerformLayout();
+ this.groupTextEntry.ResumeLayout(false);
+ this.groupTextEntry.PerformLayout();
+ this.tableTextEntry.ResumeLayout(false);
+ this.tableTextEntry.PerformLayout();
+ this.flowButtons.ResumeLayout(false);
+ this.flowButtons.PerformLayout();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel tableGlobal;
+ private System.Windows.Forms.CheckBox chkEnable;
+ private System.Windows.Forms.RadioButton radioNoTime;
+ private System.Windows.Forms.GroupBox groupTextEntry;
+ private System.Windows.Forms.FlowLayoutPanel flowButtons;
+ private System.Windows.Forms.Button btnCancel;
+ private System.Windows.Forms.Button btnSave;
+ private System.Windows.Forms.TableLayoutPanel tableDates;
+ private System.Windows.Forms.RadioButton radioTime;
+ private System.Windows.Forms.DateTimePicker dateFrom;
+ private System.Windows.Forms.DateTimePicker dateTill;
+ private System.Windows.Forms.TableLayoutPanel tableTextEntry;
+ private System.Windows.Forms.Label labelBody;
+ private System.Windows.Forms.TextBox textBody;
+ private System.Windows.Forms.DateTimePicker timeFrom;
+ private System.Windows.Forms.DateTimePicker timeTill;
+ private System.Windows.Forms.Label labelTill;
+ }
+}
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.cs
new file mode 100644
index 0000000..44a3805
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.cs
@@ -0,0 +1,192 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.UI;
+using Acacia.Utils;
+using Acacia.ZPush;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+
+namespace Acacia.Features.OutOfOffice
+{
+ public partial class OutOfOfficeDialog : KopanoDialog
+ {
+ private ActiveSync.SettingsOOF _settings;
+ private readonly bool haveTimes;
+
+ public OutOfOfficeDialog(ZPushAccount account, ActiveSync.SettingsOOF settings)
+ {
+ this._settings = settings;
+
+ InitializeComponent();
+
+ // Add the email address to the title
+ Text = string.Format(Text, account.SmtpAddress);
+
+ // Set the time formats
+ timeFrom.CustomFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
+ timeTill.CustomFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
+
+ // Patch the position of the until label, to align
+ // with the from text
+ using (Graphics graphics = radioTime.CreateGraphics())
+ {
+ Size size = RadioButtonRenderer.GetGlyphSize(graphics, System.Windows.Forms.VisualStyles.RadioButtonState.CheckedNormal);
+ Padding padding = labelTill.Margin;
+ padding.Left = radioTime.Margin.Left + size.Width + 2;
+ labelTill.Margin = padding;
+ }
+
+ // Enable controls
+ chkEnable_CheckedChanged(chkEnable, null);
+ radioTime_CheckedChanged(radioTime, null);
+
+ // Hide time options, only if it is known that these are not supported
+ haveTimes = _settings.SupportsTimes != false;
+ if (!haveTimes)
+ {
+ tableDates.Visible = false;
+ }
+
+ // Load settings
+ switch(settings.State)
+ {
+ case ActiveSync.OOFState.Disabled:
+ chkEnable.Checked = false;
+ break;
+ case ActiveSync.OOFState.Enabled:
+ chkEnable.Checked = true;
+ radioNoTime.Checked = true;
+ break;
+ case ActiveSync.OOFState.EnabledTimeBased:
+ chkEnable.Checked = true;
+ radioTime.Checked = true;
+ dateFrom.Value = settings.From.Value;
+ timeFrom.Value = settings.From.Value;
+ dateTill.Value = settings.Till.Value;
+ timeTill.Value = settings.Till.Value;
+ break;
+ }
+
+ textBody.Text = settings.Message[(int)ActiveSync.OOFTarget.Internal]?.Message;
+
+ // Set up limits
+ SetTillTimeLimit();
+ }
+
+ private void chkEnable_CheckedChanged(object sender, EventArgs e)
+ {
+ tableDates.Enabled = chkEnable.Checked;
+ groupTextEntry.Enabled = chkEnable.Checked;
+ }
+
+ private void radioTime_CheckedChanged(object sender, EventArgs e)
+ {
+ dateFrom.Enabled = timeFrom.Enabled = radioTime.Checked;
+ dateTill.Enabled = timeTill.Enabled = radioTime.Checked;
+ }
+
+ private void OutOfOfficeDialog_FormClosed(object sender, FormClosedEventArgs e)
+ {
+ // Save the settings
+ _settings.From = null;
+ _settings.Till = null;
+
+ if (chkEnable.Checked)
+ {
+ if (radioNoTime.Checked || !haveTimes)
+ {
+ _settings.State = ActiveSync.OOFState.Enabled;
+ }
+ else
+ {
+ _settings.State = ActiveSync.OOFState.EnabledTimeBased;
+ _settings.From = GetDateTime(dateFrom, timeFrom);
+ _settings.Till = GetDateTime(dateTill, timeTill);
+ }
+ }
+ else
+ {
+ _settings.State = ActiveSync.OOFState.Disabled;
+ }
+
+ // Always set the message, so it's stored
+ string message = textBody.Text;
+ for (int i = 0; i < 3; ++i)
+ {
+ _settings.Message[i] = new ActiveSync.OOFMessage();
+ _settings.Message[i].Message = message;
+ }
+ }
+
+ private DateTime GetDateTime(DateTimePicker dateControl, DateTimePicker timeControl)
+ {
+ DateTime date = dateControl.Value;
+ DateTime time = timeControl.Value;
+ DateTime combined = new DateTime(date.Year, date.Month, date.Day);
+ combined = combined.Add(time.TimeOfDay);
+ return combined;
+ }
+
+ #region Date/Time checking
+
+ private void dateFrom_ValueChanged(object sender, EventArgs e)
+ {
+ SetTillTimeLimit();
+ }
+
+ private void timeFrom_ValueChanged(object sender, EventArgs e)
+ {
+ SetTillTimeLimit();
+ }
+
+ private void dateTill_ValueChanged(object sender, EventArgs e)
+ {
+ SetTillTimeLimit();
+ }
+
+ private void timeTill_ValueChanged(object sender, EventArgs e)
+ {
+ SetTillTimeLimit();
+ }
+
+ private void SetTillTimeLimit()
+ {
+ // Don't allow setting till to before from, or before now
+ dateTill.MinDate = new DateTime(Math.Max(dateFrom.Value.Ticks, DateTime.Today.Ticks));
+
+ if (dateTill.Value.Date == dateFrom.Value.Date)
+ {
+ timeTill.MinDate = timeFrom.Value;
+ }
+ else
+ {
+ timeTill.MinDate = DateTimePicker.MinimumDateTime;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.resx
new file mode 100644
index 0000000..ef516dd
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/OutOfOffice/OutOfOfficeDialog.resx
@@ -0,0 +1,690 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+
+ Top, Bottom, Left, Right
+
+
+
+ 1
+
+
+ True
+
+
+
+ 3, 2
+
+
+ 3, 2, 3, 2
+
+
+ 262, 21
+
+
+ 0
+
+
+ Enable out-of-office auto-responding
+
+
+ chkEnable
+
+
+ System.Windows.Forms.CheckBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableGlobal
+
+
+ 0
+
+
+ Top, Bottom, Left, Right
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ 3
+
+
+ Top, Bottom, Left
+
+
+ True
+
+
+ 24, 2
+
+
+ 24, 2, 3, 2
+
+
+ 143, 21
+
+
+ 1
+
+
+ until further notice
+
+
+ radioNoTime
+
+
+ System.Windows.Forms.RadioButton, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 0
+
+
+ Top, Bottom, Left
+
+
+ True
+
+
+ 24, 27
+
+
+ 24, 2, 3, 2
+
+
+ 57, 22
+
+
+ 2
+
+
+ from
+
+
+ radioTime
+
+
+ System.Windows.Forms.RadioButton, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 1
+
+
+ 87, 27
+
+
+ 3, 2, 3, 2
+
+
+ 169, 22
+
+
+ 3
+
+
+ dateFrom
+
+
+ System.Windows.Forms.DateTimePicker, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 2
+
+
+ HH:mm
+
+
+ 262, 27
+
+
+ 3, 2, 3, 2
+
+
+ 87, 22
+
+
+ 4
+
+
+ timeFrom
+
+
+ System.Windows.Forms.DateTimePicker, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 3
+
+
+ Top, Bottom, Left, Right
+
+
+ True
+
+
+ 44, 51
+
+
+ 44, 0, 3, 0
+
+
+ 37, 26
+
+
+ 7
+
+
+ until
+
+
+ MiddleLeft
+
+
+ labelTill
+
+
+ System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 4
+
+
+ 87, 53
+
+
+ 3, 2, 3, 2
+
+
+ 169, 22
+
+
+ 5
+
+
+ dateTill
+
+
+ System.Windows.Forms.DateTimePicker, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 5
+
+
+ HH:mm
+
+
+ 262, 53
+
+
+ 3, 2, 3, 2
+
+
+ 87, 22
+
+
+ 6
+
+
+ timeTill
+
+
+ System.Windows.Forms.DateTimePicker, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableDates
+
+
+ 6
+
+
+ 0, 25
+
+
+ 0, 0, 0, 0
+
+
+ 3
+
+
+ 431, 77
+
+
+ 1
+
+
+ tableDates
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableGlobal
+
+
+ 1
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="radioNoTime" Row="0" RowSpan="1" Column="0" ColumnSpan="3" /><Control Name="radioTime" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="dateFrom" Row="1" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="timeFrom" Row="1" RowSpan="1" Column="2" ColumnSpan="1" /><Control Name="labelTill" Row="2" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="dateTill" Row="2" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="timeTill" Row="2" RowSpan="1" Column="2" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,AutoSize,0,AutoSize,0,Absolute,20" /><Rows Styles="AutoSize,0,AutoSize,0,AutoSize,0" /></TableLayoutSettings>
+
+
+ Top, Bottom, Left, Right
+
+
+ True
+
+
+ Top, Bottom, Left, Right
+
+
+ True
+
+
+ GrowAndShrink
+
+
+ 1
+
+
+ True
+
+
+ 3, 6
+
+
+ 3, 6, 3, 0
+
+
+ 377, 17
+
+
+ 2
+
+
+ AutoReply only once to each sender with the following text:
+
+
+ labelBody
+
+
+ System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableTextEntry
+
+
+ 0
+
+
+ Top, Bottom, Left, Right
+
+
+ 3, 25
+
+
+ 3, 2, 3, 2
+
+
+ True
+
+
+ 411, 168
+
+
+ 8
+
+
+ textBody
+
+
+ System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableTextEntry
+
+
+ 1
+
+
+ 4, 14
+
+
+ 3, 2, 3, 2
+
+
+ 2
+
+
+ 417, 195
+
+
+ 0
+
+
+ tableTextEntry
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ groupTextEntry
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="labelBody" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="textBody" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /></Controls><Columns Styles="Percent,100" /><Rows Styles="AutoSize,0,Percent,100,Absolute,25,Absolute,25" /></TableLayoutSettings>
+
+
+ 3, 104
+
+
+ 3, 2, 3, 2
+
+
+ 3, 2, 3, 2
+
+
+ 425, 216
+
+
+ 2
+
+
+ groupTextEntry
+
+
+ System.Windows.Forms.GroupBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableGlobal
+
+
+ 2
+
+
+ Top, Bottom, Left, Right
+
+
+ True
+
+
+ 341, 2
+
+
+ 3, 2, 3, 2
+
+
+ 81, 33
+
+
+ 10
+
+
+ Cancel
+
+
+ btnCancel
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ flowButtons
+
+
+ 0
+
+
+ True
+
+
+ 254, 2
+
+
+ 3, 2, 3, 2
+
+
+ 81, 33
+
+
+ 9
+
+
+ Save
+
+
+ btnSave
+
+
+ System.Windows.Forms.Button, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ flowButtons
+
+
+ 1
+
+
+ RightToLeft
+
+
+ 3, 324
+
+
+ 3, 2, 3, 2
+
+
+ 425, 38
+
+
+ 3
+
+
+ flowButtons
+
+
+ System.Windows.Forms.FlowLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ tableGlobal
+
+
+ 3
+
+
+ 5, 5
+
+
+ 3, 2, 3, 2
+
+
+ 4
+
+
+ 431, 364
+
+
+ 0
+
+
+ tableGlobal
+
+
+ System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ $this
+
+
+ 0
+
+
+ <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="chkEnable" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="tableDates" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="groupTextEntry" Row="2" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="flowButtons" Row="3" RowSpan="1" Column="0" ColumnSpan="1" /></Controls><Columns Styles="Percent,100" /><Rows Styles="AutoSize,0,AutoSize,0,Percent,100,AutoSize,0,Absolute,20" /></TableLayoutSettings>
+
+
+ True
+
+
+ 8, 16
+
+
+ 444, 382
+
+
+ 3, 2, 3, 2
+
+
+ 458, 415
+
+
+ 5, 5, 5, 5
+
+
+ CenterParent
+
+
+ Out of Office Assistant for {0}
+
+
+ OutOfOfficeDialog
+
+
+ System.Windows.Forms.Form, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/FeatureReplyFlags.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/FeatureReplyFlags.cs
new file mode 100644
index 0000000..4d77a6f
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/FeatureReplyFlags.cs
@@ -0,0 +1,181 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Acacia.Stubs;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Microsoft.Office.Interop.Outlook;
+using static Acacia.DebugOptions;
+
+namespace Acacia.Features.ReplyFlags
+{
+ [AcaciaOption("Synchronises reply flags between Outlook and Z-Push servers.")]
+ public class FeatureReplyFlags : Feature
+ {
+ public FeatureReplyFlags()
+ {
+
+ }
+
+ public override void Startup()
+ {
+ if (UpdateEvents)
+ {
+ // Watch all mail folders and all items in them
+ Watcher.WatchFolder(new FolderRegistrationTyped(this, ItemType.MailItem),
+ (folder) => Watcher.WatchItems(folder, UpdateReplyStatus, false)
+ );
+ }
+
+ if (ReadEvent)
+ {
+ // As a fallback, add an event handler to update the message when displaying it
+ MailEvents.Read += UpdateReplyStatus;
+ }
+
+ if (SendEvents)
+ {
+ // Hook reply and send events to update local state to server
+ MailEvents.Reply += OnReply;
+ MailEvents.ReplyAll += OnReplyAll;
+ MailEvents.Forward += OnForwarded;
+ }
+ }
+
+ [AcaciaOption("Enables or disables the handling of update events to mail items. When a mail item is " +
+ "updated, it is checked to see if the reply flags are up to date. This is the main " +
+ "mechanism for updating reply flags that change on the server")]
+ public bool UpdateEvents
+ {
+ get { return GetOption(OPTION_UPDATE_EVENTS); }
+ set { SetOption(OPTION_UPDATE_EVENTS, value); }
+ }
+ private static readonly BoolOption OPTION_UPDATE_EVENTS = new BoolOption("FolderEvents", true);
+
+ [AcaciaOption("Enables or disables the handling of read events on mail items. If this is enabled, " +
+ "the reply flag is checked. This is almost guaranteed to work, but has the downside " +
+ "of only updating the reply flag when an email is opened")]
+ public bool ReadEvent
+ {
+ get { return GetOption(OPTION_READ_EVENT); }
+ set { SetOption(OPTION_READ_EVENT, value); }
+ }
+ private static readonly BoolOption OPTION_READ_EVENT = new BoolOption("ReadEvents", true);
+
+ [AcaciaOption("Enables or disables the handling of send events on mail items. If this is enabled, " +
+ "the reply flag is included in emails sent to the Z-Push server, which will update " +
+ "the flag on the server")]
+ public bool SendEvents
+ {
+ get { return GetOption(OPTION_SEND_EVENTS); }
+ set { SetOption(OPTION_SEND_EVENTS, value); }
+ }
+ private static readonly BoolOption OPTION_SEND_EVENTS = new BoolOption("SendEvents", true);
+
+ [AcaciaOption("Disables the parsing of reply flags on incoming mail items." +
+ "When this flag is disabled, reply flags coming from the server will be completely ignored.")]
+ public bool ParseIncoming
+ {
+ get { return GetOption(OPTION_INCOMING_PARSE); }
+ set { SetOption(OPTION_INCOMING_PARSE, value); }
+ }
+ private static readonly BoolOption OPTION_INCOMING_PARSE = new BoolOption("IncomingParse", true);
+
+ [AcaciaOption("Disables the updating of reply flags on incoming mail items." +
+ "When this flag is disabled, reply flags coming from the server will be examined, " +
+ "but not action will be taken on them.")]
+ public bool UpdateIncoming
+ {
+ get { return GetOption(OPTION_INCOMING_UPDATE); }
+ set { SetOption(OPTION_INCOMING_UPDATE, value); }
+ }
+ private static readonly BoolOption OPTION_INCOMING_UPDATE = new BoolOption("IncomingUpdate", true);
+
+ [AcaciaOption("Disables the updating of reply flags in outgoing emails." +
+ "If this option is enabled, reply flags will not be sent to the Z-Push server")]
+ public bool UpdateOutgoing
+ {
+ get { return GetOption(OPTION_OUTGOING_UPDATE); }
+ set { SetOption(OPTION_OUTGOING_UPDATE, value); }
+ }
+ private static readonly BoolOption OPTION_OUTGOING_UPDATE = new BoolOption("OutgoingUpdate", true);
+
+ #region Server to outlook
+
+ private void UpdateReplyStatus(IMailItem mail)
+ {
+ if (!ParseIncoming)
+ return;
+ bool update = UpdateIncoming;
+
+ // See if the categories contain a reply flag
+ ReplyFlags flags = ReplyFlags.FromCategory(mail, update);
+ if (flags != null)
+ {
+ if (update)
+ {
+ Logger.Instance.Debug(this, "Updating flags: {0}", mail.Subject);
+
+ // Update the mail item. This will also save the changed category list
+ flags.UpdateLocal();
+ }
+ }
+ }
+
+ #endregion
+
+ #region Outlook to server
+
+ private readonly Queue lastItems = new Queue();
+
+ private void OnReply(IMailItem mail, IMailItem response)
+ {
+ SetReplyFlag(mail, response, Verb.REPLIED);
+ }
+
+ private void OnReplyAll(IMailItem mail, IMailItem response)
+ {
+ SetReplyFlag(mail, response, Verb.REPLIED_TO_ALL);
+ }
+
+ private void OnForwarded(IMailItem mail, IMailItem response)
+ {
+ SetReplyFlag(mail, response, Verb.FORWARDED);
+ }
+
+ private void SetReplyFlag(IMailItem mail, IMailItem response, Verb verb)
+ {
+ if (!UpdateOutgoing)
+ return;
+
+ string id = (string)mail.GetProperty(OutlookConstants.PR_ZPUSH_MESSAGE_ID);
+ using (IFolder folder = mail.Parent)
+ {
+ string folderId = (string)folder.GetProperty(OutlookConstants.PR_ZPUSH_FOLDER_ID);
+ string value = ReplyFlags.VerbToExchange(verb) + "/" + id + "/" + folderId;
+ Logger.Instance.Trace(this, "Reply header: {0}", value);
+ response.SetProperty(Constants.ZPUSH_REPLY_HEADER, value);
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/ReplyFlags.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/ReplyFlags.cs
new file mode 100644
index 0000000..e50f83c
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/ReplyFlags/ReplyFlags.cs
@@ -0,0 +1,229 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Acacia.Stubs;
+using Acacia.Utils;
+
+namespace Acacia.Features.ReplyFlags
+{
+ public enum Verb
+ {
+ NONE,
+ REPLIED,
+ REPLIED_TO_ALL,
+ FORWARDED
+ }
+
+ // TODO: unit tests for parsing
+ public class ReplyFlags
+ {
+ private readonly IMailItem _item;
+
+ public Verb Verb
+ {
+ get;
+ private set;
+ }
+
+ public DateTime? Date
+ {
+ get;
+ private set;
+ }
+
+ ///
+ /// Constructor. Reads the reply state from the mail item's reply properties
+ ///
+ public ReplyFlags(IMailItem item)
+ {
+ this._item = item;
+ ReadFromProperties();
+ }
+
+ ///
+ /// Fully initializing constructor.
+ ///
+ private ReplyFlags(IMailItem item, Verb verb, DateTime date)
+ {
+ this._item = item;
+ this.Verb = verb;
+ this.Date = date;
+ }
+
+ ///
+ /// Constructs the ReplyFlags object from the mail's categories, if present.
+ /// If the category is present, it is removed. Changes are not saved, so this will have to be done explicitly.
+ /// If no category is present, null is returned.
+ ///
+ ///
+ ///
+ public static ReplyFlags FromCategory(IMailItem item, bool updateCategories = true)
+ {
+ string[] categories = item.AttrCategories;
+ if (categories == null || categories.Length == 0)
+ return null;
+
+ // See if we have the z-push reply header
+ for (int i = 0; i < categories.Length; ++i)
+ {
+ string category = categories[i];
+
+ // This test will be invoked on every change, so do a quick test first
+ if (category.StartsWith(Constants.ZPUSH_REPLY_CATEGORY_PREFIX))
+ {
+ string suffix = category.Substring(Constants.ZPUSH_REPLY_CATEGORY_PREFIX.Length);
+ Match match = Constants.ZPUSH_REPLY_CATEGORY_REGEX.Match(suffix);
+ if (match.Success)
+ {
+ try
+ {
+ string dateString = match.Groups[2].Value;
+
+ // Parse the state
+ Verb verb = VerbFromString(match.Groups[1].Value);
+
+ // Parse the date
+ DateTime date = DateTime.Parse(dateString);
+
+ // Remove the category
+ if (updateCategories)
+ {
+ var categoriesList = new List(categories);
+ categoriesList.RemoveAt(i);
+ item.AttrCategories = categoriesList.ToArray();
+ }
+
+ // Return the flags
+ return new ReplyFlags(item, verb, date);
+ }
+ catch (System.Exception e)
+ {
+ // Ignore any exception
+ Logger.Instance.Error(typeof(ReplyFlags), "Exception while parsing reply category: {0}", e);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private void ReadFromProperties()
+ {
+ // Read the date
+ Date = _item.AttrLastVerbExecutionTime;
+
+ // And the state
+ int state = _item.AttrLastVerbExecuted;
+ switch (state)
+ {
+ case OutlookConstants.EXCHIVERB_FORWARD:
+ Verb = Verb.FORWARDED;
+ break;
+ case OutlookConstants.EXCHIVERB_REPLYTOALL:
+ case OutlookConstants.EXCHIVERB_REPLYTOSENDER:
+ case OutlookConstants.EXCHIVERB_REPLYTOFOLDER:
+ Verb = Verb.REPLIED;
+ break;
+ default:
+ Verb = Verb.NONE;
+ break;
+ }
+ }
+
+ ///
+ /// Updates the local mail item from the current state
+ ///
+ public void UpdateLocal()
+ {
+ // Determine icon and verb
+ int icon = OutlookConstants.PR_ICON_INDEX_NONE;
+ int verb = OutlookConstants.EXCHIVERB_OPEN;
+ switch (Verb)
+ {
+ case Verb.REPLIED:
+ icon = OutlookConstants.PR_ICON_INDEX_REPLIED;
+ verb = OutlookConstants.EXCHIVERB_REPLYTOSENDER;
+ break;
+ case Verb.REPLIED_TO_ALL:
+ icon = OutlookConstants.PR_ICON_INDEX_REPLIED;
+ verb = OutlookConstants.EXCHIVERB_REPLYTOALL;
+ break;
+ case Verb.FORWARDED:
+ icon = OutlookConstants.PR_ICON_INDEX_FORWARDED;
+ verb = OutlookConstants.EXCHIVERB_FORWARD;
+ break;
+ }
+
+ // Set the properties
+ _item.SetProperties(
+ new string[]
+ {
+ OutlookConstants.PR_ICON_INDEX,
+ OutlookConstants.PR_LAST_VERB_EXECUTED,
+ OutlookConstants.PR_LAST_VERB_EXECUTION_TIME
+ },
+ new object[]
+ {
+ icon,
+ verb,
+ Date
+ }
+ );
+
+ // And save
+ _item.Save();
+ }
+
+ override public string ToString()
+ {
+ return Verb + "=" + Date;
+ }
+
+ public static Verb VerbFromString(string verb)
+ {
+ if (verb == Constants.ZPUSH_REPLY_CATEGORY_REPLIED)
+ return Verb.REPLIED;
+ else if (verb == Constants.ZPUSH_REPLY_CATEGORY_REPLIED_TO_ALL)
+ return Verb.REPLIED_TO_ALL;
+ else if (verb == Constants.ZPUSH_REPLY_CATEGORY_FORWARDED)
+ return Verb.FORWARDED;
+ else
+ throw new System.Exception("Invalid verb: " + verb);
+ }
+
+ public static int VerbToExchange(Verb verb)
+ {
+ switch(verb)
+ {
+ case Verb.REPLIED:
+ return OutlookConstants.EXCHIVERB_REPLYTOSENDER;
+ case Verb.REPLIED_TO_ALL:
+ return OutlookConstants.EXCHIVERB_REPLYTOALL;
+ case Verb.FORWARDED:
+ return OutlookConstants.EXCHIVERB_FORWARD;
+ default:
+ throw new System.Exception("Invalid verb: " + verb);
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs
new file mode 100644
index 0000000..2ad949e
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SendAs/FeatureSendAs.cs
@@ -0,0 +1,125 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Acacia.Stubs;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Microsoft.Office.Interop.Outlook;
+using Acacia.Features.SharedFolders;
+using Acacia.ZPush.API.SharedFolders;
+using static Acacia.DebugOptions;
+
+namespace Acacia.Features.SendAs
+{
+ [AcaciaOption("Provides the ability to select different senders for Z-Push accounts.")]
+ public class FeatureSendAs : FeatureDisabled
+ {
+ private FeatureSharedFolders _sharedFolders;
+
+ public FeatureSendAs()
+ {
+ }
+
+ [AcaciaOption("Disables the \"Send As Owner\" feature. This feature allows sending as the owner of a shared folder, " +
+ "when responding to messages in that folder. Note that this feature requires SharedFolders to be " +
+ "enabled")]
+ public bool SendAsOwner
+ {
+ get { return GetOption(OPTION_SEND_AS_OWNER); }
+ set { SetOption(OPTION_SEND_AS_OWNER, value); }
+ }
+ private static readonly BoolOption OPTION_SEND_AS_OWNER = new BoolOption("SendAsOwner", true);
+
+ public override void Startup()
+ {
+ MailEvents.ItemSend += MailEvents_ItemSend;
+
+ if (SendAsOwner)
+ {
+ // Need shared folders for automatic sender selection
+ _sharedFolders = ThisAddIn.Instance.GetFeature();
+ if (_sharedFolders != null)
+ {
+ MailEvents.Respond += MailEvents_Respond;
+ }
+ }
+ }
+
+ private void MailEvents_Respond(IMailItem mail, IMailItem response)
+ {
+ Logger.Instance.Trace(this, "Responding to mail, checking");
+ using (IStore store = mail.Store)
+ {
+ ZPushAccount zpush = Watcher.Accounts.GetAccount(store);
+ Logger.Instance.Trace(this, "Checking ZPush: {0}", zpush);
+ if (zpush != null)
+ {
+ // Check if the containing folder is a shared folder
+ using (IFolder parent = mail.Parent)
+ {
+ Logger.Instance.Trace(this, "Checking, Parent folder: {0}", parent.Name);
+ SharedFolder shared = _sharedFolders.GetSharedFolder(parent);
+ if (shared != null)
+ Logger.Instance.Trace(this, "Checking, Shared folder: {0}, flags={1}", shared, shared?.Flags);
+ else
+ Logger.Instance.Trace(this, "Not a shared folder");
+ if (shared != null && shared.FlagSendAsOwner)
+ {
+ Logger.Instance.Trace(this, "Checking, Shared folder owner: {0}", shared.Store.UserName);
+ // It's a shared folder, use the owner as the sender if possible
+ // TODO: make a wrapper for this
+ var recip = ThisAddIn.Instance.Application.Session.CreateRecipient(shared.Store.UserName);
+ Logger.Instance.Trace(this, "Checking, Shared folder owner recipient: {0}", recip.Name);
+ if (recip != null && recip.Resolve())
+ {
+ Logger.Instance.Trace(this, "Sending as: {0}", recip.AddressEntry.Address);
+ response.SetSender(recip.AddressEntry);
+ }
+ else
+ {
+ Logger.Instance.Trace(this, "Unable to resolve sender");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void MailEvents_ItemSend(IMailItem item, ref bool cancel)
+ {
+ using (IStore store = item.Store)
+ {
+ ZPushAccount zpush = Watcher.Accounts.GetAccount(store);
+ if (zpush != null)
+ {
+ string address = item.SenderEmailAddress;
+ if (address != null && address != zpush.SmtpAddress)
+ {
+ Logger.Instance.Trace(this, "SendAs: {0}: {1}", address, item.SenderName);
+ item.SetProperty(Constants.ZPUSH_SEND_AS, address);
+ if (item.SenderName != null)
+ item.SetProperty(Constants.ZPUSH_SEND_AS_NAME, item.SenderName);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs
new file mode 100644
index 0000000..38e1617
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs
@@ -0,0 +1,131 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Stubs;
+using Acacia.UI;
+using Acacia.UI.Outlook;
+using Acacia.ZPush;
+using Acacia.ZPush.API.SharedFolders;
+using Acacia.ZPush.Connect;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.SharedFolders
+{
+ [AcaciaOption("Provides the ability to open shared folders from other users in Outlook.")]
+ public class FeatureSharedFolders
+ :
+ Feature, FeatureWithRibbon, FeatureWithContextMenu
+ {
+ public override void Startup()
+ {
+ RegisterButton(this, "SharedFolders", true, ManageFolders, ZPushBehaviour.Disable);
+
+ MenuItem menuItem = RegisterMenuItem(this, "SharedFolders_Context", null, ManageFolder, ZPushBehaviour.Hide);
+ if (menuItem != null)
+ menuItem.CheckEnabled = CanManageFolder;
+
+ // Sync state
+ Watcher.Sync.AddTask(this, Name, AdditionalFolders_Sync);
+ }
+
+ #region UI
+
+ private bool CanManageFolder(MenuItem b, IFolder folder)
+ {
+ return folder.SyncId?.IsShared == true;
+ }
+
+ private void ManageFolder(IFolder folder)
+ {
+ ZPushAccount account = Watcher.Accounts.GetAccount(folder);
+ if (account != null)
+ {
+ new SharedFoldersDialog(account, folder.SyncId).ShowDialog();
+ }
+ }
+
+ private void ManageFolders()
+ {
+ ZPushAccount account = Watcher.CurrentZPushAccount();
+ if (account != null)
+ {
+ new SharedFoldersDialog(account).ShowDialog();
+ }
+ }
+
+ #endregion
+
+ #region Shared folders sync
+
+ private const string KEY_SHARES = "Shares";
+
+ private void AdditionalFolders_Sync(ZPushConnection connection)
+ {
+ Logger.Instance.Debug(this, "Starting sync for account {0}", connection.Account);
+ using (SharedFoldersAPI api = new SharedFoldersAPI(connection))
+ {
+ // Fetch the current shares
+ ICollection shares = api.GetCurrentShares();
+ Logger.Instance.Trace(this, "AdditionalFolders_Sync: {0}", shares.Count);
+
+ // Convert to dictionary
+ Dictionary dict = shares.ToDictionary(x => x.SyncId);
+ Logger.Instance.Trace(this, "AdditionalFolders_Sync2: {0}", shares.Count);
+
+ // Store with the account
+ connection.Account.SetFeatureData(this, KEY_SHARES, dict);
+ }
+ }
+
+ public SharedFolder GetSharedFolder(IFolder folder)
+ {
+ if (folder == null)
+ return null;
+
+ // Check that we can get the id
+ SyncId folderId = folder.SyncId;
+ Logger.Instance.Trace(this, "GetSharedFolder1: {0}", folderId);
+ if (folderId == null || !folderId.IsShared)
+ return null;
+
+ // Get the ZPush account
+ ZPushAccount account = Watcher.Accounts.GetAccount(folder);
+ Logger.Instance.Trace(this, "GetSharedFolder2: {0}", account);
+ if (account == null)
+ return null;
+
+ // Get the shared folders
+ Dictionary shared = account.GetFeatureData>(this, KEY_SHARES);
+ Logger.Instance.Trace(this, "GetSharedFolder3: {0}", shared?.Count);
+ if (shared == null)
+ return null;
+
+ SharedFolder share = null;
+ shared.TryGetValue(folderId, out share);
+ Logger.Instance.Trace(this, "GetSharedFolder4: {0}", share);
+
+ return share;
+ }
+
+ #endregion
+
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FolderTreeNode.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FolderTreeNode.cs
new file mode 100644
index 0000000..a20a1fa
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FolderTreeNode.cs
@@ -0,0 +1,95 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Controls;
+using Acacia.ZPush;
+using Acacia.ZPush.API.SharedFolders;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Acacia.Features.SharedFolders
+{
+ public class FolderTreeNode : KTreeNode
+ {
+ private readonly StoreTreeNode _store;
+ private readonly AvailableFolder _folder;
+ private SharedFolder _share;
+
+ public FolderTreeNode(StoreTreeNode store, AvailableFolder folder, SharedFolder share)
+ {
+ this._store = store;
+ this._folder = folder;
+ this._share = share;
+
+ this.Text = folder.Name;
+
+ // Image
+ // TODO: clean this up
+ int index = ((int)OutlookConstants.BASIC_SYNC_TYPES[(int)folder.Type]) - 1;
+ if (index < 0 || index >= store.Owner.Images.Images.Count - 1)
+ index = 0;
+ ImageIndex = index;
+ }
+
+ protected override void OnCheckStateChanged()
+ {
+ // Update share state
+ if (CheckState == System.Windows.Forms.CheckState.Unchecked)
+ _store.RemoveShare(_folder);
+ else
+ _share = _store.AddShare(_folder, _share);
+
+ base.OnCheckStateChanged();
+ }
+
+ public AvailableFolder AvailableFolder { get { return _folder; } }
+
+ public bool IsShared { get { return CheckState != System.Windows.Forms.CheckState.Unchecked; } }
+
+ ///
+ /// Returns the current share state. Note that this may return a state, even if IsShared is false, as the state is retained,
+ /// in case the user reselects it. However, if IsShared is true, a valid object is guaranteed.
+ ///
+ public SharedFolder SharedFolder
+ {
+ get { return _share; }
+ set
+ {
+ if (value == null)
+ throw new InvalidOperationException("Cannot unset share");
+ if (_share != value)
+ {
+ _share = value;
+ _store.AddShare(_folder, _share);
+ }
+ }
+ }
+
+ // TODO: this is generally useful, move to KTreeNode
+ public IEnumerable Descendants()
+ {
+ foreach(KTreeNode child in Children)
+ {
+ yield return (FolderTreeNode)child;
+ foreach (FolderTreeNode desc in ((FolderTreeNode)child).Descendants())
+ yield return desc;
+ }
+ }
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs
new file mode 100644
index 0000000..7645b31
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs
@@ -0,0 +1,232 @@
+namespace Acacia.Features.SharedFolders
+{
+ partial class SharedFoldersDialog
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Windows Form Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(SharedFoldersDialog));
+ Acacia.Controls.KCheckManager.RecursiveThreeState recursiveThreeState1 = new Acacia.Controls.KCheckManager.RecursiveThreeState();
+ this._layout = new System.Windows.Forms.TableLayoutPanel();
+ this._mainBusyHider = new Acacia.Controls.KBusyHider();
+ this._layoutMain = new System.Windows.Forms.TableLayoutPanel();
+ this._layoutSelectUser = new System.Windows.Forms.TableLayoutPanel();
+ this.labelSelectUser = new System.Windows.Forms.Label();
+ this.buttonOpenUser = new System.Windows.Forms.Button();
+ this._layoutCenterGABLookup = new System.Windows.Forms.TableLayoutPanel();
+ this.gabLookup = new Acacia.UI.GABLookupControl();
+ this.kTreeFolders = new Acacia.Controls.KTree();
+ this._layoutOptions = new System.Windows.Forms.TableLayoutPanel();
+ this._labelName = new System.Windows.Forms.Label();
+ this.textName = new System.Windows.Forms.TextBox();
+ this._labelSendAs = new System.Windows.Forms.Label();
+ this.checkSendAs = new System.Windows.Forms.CheckBox();
+ this._labelPermissions = new System.Windows.Forms.Label();
+ this.labelPermissionsValue = new System.Windows.Forms.Label();
+ this.dialogButtons = new Acacia.Controls.KDialogButtons();
+ this._layout.SuspendLayout();
+ this._mainBusyHider.SuspendLayout();
+ this._layoutMain.SuspendLayout();
+ this._layoutSelectUser.SuspendLayout();
+ this._layoutCenterGABLookup.SuspendLayout();
+ this._layoutOptions.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // _layout
+ //
+ resources.ApplyResources(this._layout, "_layout");
+ this._layout.Controls.Add(this._mainBusyHider, 0, 0);
+ this._layout.Controls.Add(this.dialogButtons, 0, 1);
+ this._layout.Name = "_layout";
+ //
+ // _mainBusyHider
+ //
+ this._mainBusyHider.Busy = false;
+ this._mainBusyHider.BusyText = null;
+ this._mainBusyHider.Cancellation = null;
+ this._mainBusyHider.Controls.Add(this._layoutMain);
+ resources.ApplyResources(this._mainBusyHider, "_mainBusyHider");
+ this._mainBusyHider.Name = "_mainBusyHider";
+ //
+ // _layoutMain
+ //
+ resources.ApplyResources(this._layoutMain, "_layoutMain");
+ this._layoutMain.Controls.Add(this._layoutSelectUser, 0, 0);
+ this._layoutMain.Controls.Add(this.kTreeFolders, 0, 1);
+ this._layoutMain.Controls.Add(this._layoutOptions, 0, 2);
+ this._layoutMain.Name = "_layoutMain";
+ //
+ // _layoutSelectUser
+ //
+ resources.ApplyResources(this._layoutSelectUser, "_layoutSelectUser");
+ this._layoutSelectUser.Controls.Add(this.labelSelectUser, 0, 0);
+ this._layoutSelectUser.Controls.Add(this.buttonOpenUser, 2, 0);
+ this._layoutSelectUser.Controls.Add(this._layoutCenterGABLookup, 1, 0);
+ this._layoutSelectUser.Name = "_layoutSelectUser";
+ //
+ // labelSelectUser
+ //
+ resources.ApplyResources(this.labelSelectUser, "labelSelectUser");
+ this.labelSelectUser.Name = "labelSelectUser";
+ //
+ // buttonOpenUser
+ //
+ resources.ApplyResources(this.buttonOpenUser, "buttonOpenUser");
+ this.buttonOpenUser.Name = "buttonOpenUser";
+ this.buttonOpenUser.UseVisualStyleBackColor = true;
+ this.buttonOpenUser.Click += new System.EventHandler(this.buttonOpenUser_Click);
+ //
+ // _layoutCenterGABLookup
+ //
+ resources.ApplyResources(this._layoutCenterGABLookup, "_layoutCenterGABLookup");
+ this._layoutCenterGABLookup.Controls.Add(this.gabLookup, 0, 1);
+ this._layoutCenterGABLookup.Name = "_layoutCenterGABLookup";
+ //
+ // gabLookup
+ //
+ this.gabLookup.DisplayMember = "DisplayName";
+ resources.ApplyResources(this.gabLookup, "gabLookup");
+ this.gabLookup.FormattingEnabled = true;
+ this.gabLookup.GAB = null;
+ this.gabLookup.Name = "gabLookup";
+ this.gabLookup.SelectedUserChanged += new Acacia.UI.GABLookupControl.SelectedUserEventHandler(this.gabLookup_SelectedUserChanged);
+ //
+ // kTreeFolders
+ //
+ this.kTreeFolders.BackColor = System.Drawing.SystemColors.Window;
+ this.kTreeFolders.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
+ this.kTreeFolders.CheckManager = recursiveThreeState1;
+ this.kTreeFolders.CheckStyle = Acacia.Controls.KCheckStyle.RecursiveThreeState;
+ resources.ApplyResources(this.kTreeFolders, "kTreeFolders");
+ this.kTreeFolders.FullRowSelect = true;
+ this.kTreeFolders.Images = null;
+ this.kTreeFolders.MultipleSelection = true;
+ this.kTreeFolders.Name = "kTreeFolders";
+ this.kTreeFolders.NodeIndent = 8;
+ this.kTreeFolders.NodePadding = new System.Windows.Forms.Padding(2, 4, 2, 4);
+ this.kTreeFolders.CheckStateChanged += new Acacia.Controls.KTree.CheckStateChangedHandler(this.kTreeFolders_CheckStateChanged);
+ this.kTreeFolders.SelectionChanged += new Acacia.Controls.KTree.SelectionChangedDelegate(this.kTreeFolders_SelectionChanged);
+ //
+ // _layoutOptions
+ //
+ resources.ApplyResources(this._layoutOptions, "_layoutOptions");
+ this._layoutOptions.Controls.Add(this._labelName, 0, 0);
+ this._layoutOptions.Controls.Add(this.textName, 1, 0);
+ this._layoutOptions.Controls.Add(this._labelSendAs, 0, 1);
+ this._layoutOptions.Controls.Add(this.checkSendAs, 1, 1);
+ this._layoutOptions.Controls.Add(this._labelPermissions, 0, 2);
+ this._layoutOptions.Controls.Add(this.labelPermissionsValue, 1, 2);
+ this._layoutOptions.Name = "_layoutOptions";
+ //
+ // _labelName
+ //
+ resources.ApplyResources(this._labelName, "_labelName");
+ this._labelName.Name = "_labelName";
+ //
+ // textName
+ //
+ resources.ApplyResources(this.textName, "textName");
+ this.textName.Name = "textName";
+ this.textName.TextChanged += new System.EventHandler(this.textName_TextChanged);
+ //
+ // _labelSendAs
+ //
+ resources.ApplyResources(this._labelSendAs, "_labelSendAs");
+ this._labelSendAs.Name = "_labelSendAs";
+ //
+ // checkSendAs
+ //
+ resources.ApplyResources(this.checkSendAs, "checkSendAs");
+ this.checkSendAs.Name = "checkSendAs";
+ this.checkSendAs.ThreeState = true;
+ this.checkSendAs.UseVisualStyleBackColor = true;
+ this.checkSendAs.CheckedChanged += new System.EventHandler(this.checkSendAs_CheckedChanged);
+ //
+ // _labelPermissions
+ //
+ resources.ApplyResources(this._labelPermissions, "_labelPermissions");
+ this._labelPermissions.Name = "_labelPermissions";
+ //
+ // labelPermissionsValue
+ //
+ resources.ApplyResources(this.labelPermissionsValue, "labelPermissionsValue");
+ this.labelPermissionsValue.Name = "labelPermissionsValue";
+ //
+ // dialogButtons
+ //
+ resources.ApplyResources(this.dialogButtons, "dialogButtons");
+ this.dialogButtons.ButtonSize = null;
+ this.dialogButtons.Cancellation = null;
+ this.dialogButtons.HasApply = true;
+ this.dialogButtons.IsDirty = false;
+ this.dialogButtons.Name = "dialogButtons";
+ this.dialogButtons.Apply += new System.EventHandler(this.dialogButtons_Apply);
+ //
+ // SharedFoldersDialog
+ //
+ resources.ApplyResources(this, "$this");
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.BusyHider = this._mainBusyHider;
+ this.Controls.Add(this._layout);
+ this.DialogButtons = this.dialogButtons;
+ this.Name = "SharedFoldersDialog";
+ this.DirtyFormClosing += new System.Windows.Forms.FormClosingEventHandler(this.SharedFoldersDialog_DirtyFormClosing);
+ this.Shown += new System.EventHandler(this.AddSharedFolderDialog_Shown);
+ this._layout.ResumeLayout(false);
+ this._layout.PerformLayout();
+ this._mainBusyHider.ResumeLayout(false);
+ this._layoutMain.ResumeLayout(false);
+ this._layoutMain.PerformLayout();
+ this._layoutSelectUser.ResumeLayout(false);
+ this._layoutSelectUser.PerformLayout();
+ this._layoutCenterGABLookup.ResumeLayout(false);
+ this._layoutOptions.ResumeLayout(false);
+ this._layoutOptions.PerformLayout();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TableLayoutPanel _layout;
+ private Controls.KTree kTreeFolders;
+ private System.Windows.Forms.TableLayoutPanel _layoutSelectUser;
+ private System.Windows.Forms.Label labelSelectUser;
+ private System.Windows.Forms.Button buttonOpenUser;
+ private System.Windows.Forms.TableLayoutPanel _layoutMain;
+ private System.Windows.Forms.TableLayoutPanel _layoutOptions;
+ private System.Windows.Forms.Label _labelName;
+ private System.Windows.Forms.TextBox textName;
+ private System.Windows.Forms.Label _labelSendAs;
+ private System.Windows.Forms.CheckBox checkSendAs;
+ private System.Windows.Forms.Label _labelPermissions;
+ private System.Windows.Forms.Label labelPermissionsValue;
+ private Controls.KBusyHider _mainBusyHider;
+ private Controls.KDialogButtons dialogButtons;
+ private System.Windows.Forms.TableLayoutPanel _layoutCenterGABLookup;
+ private UI.GABLookupControl gabLookup;
+ }
+}
\ No newline at end of file
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs
new file mode 100644
index 0000000..364470b
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs
@@ -0,0 +1,495 @@
+/// Copyright 2016 Kopano b.v.
+///
+/// This program is free software: you can redistribute it and/or modify
+/// it under the terms of the GNU Affero General Public License, version 3,
+/// as published by the Free Software Foundation.
+///
+/// This program is distributed in the hope that it will be useful,
+/// but WITHOUT ANY WARRANTY; without even the implied warranty of
+/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
+/// GNU Affero General Public License for more details.
+///
+/// You should have received a copy of the GNU Affero General Public License
+/// along with this program.If not, see.
+///
+/// Consult LICENSE file for details
+
+using Acacia.Controls;
+using Acacia.Features.GAB;
+using Acacia.Stubs;
+using Acacia.UI;
+using Acacia.UI.Outlook;
+using Acacia.Utils;
+using Acacia.ZPush;
+using Acacia.ZPush.API.SharedFolders;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Data;
+using System.Drawing;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows.Forms;
+using System.Xml;
+using System.Xml.Serialization;
+using static Acacia.ZPush.API.SharedFolders.SharedFoldersAPI;
+
+namespace Acacia.Features.SharedFolders
+{
+ public partial class SharedFoldersDialog : KDialogNew
+ {
+ private readonly ZPushAccount _account;
+ private SyncId _initialSyncId;
+ private SharedFolder _initialFolder;
+
+ public SharedFoldersDialog(ZPushAccount account, SyncId initial = null)
+ {
+ this._account = account;
+ this._initialSyncId = initial;
+
+ InitializeComponent();
+
+ // TODO: make a specialised class out of this
+ this.kTreeFolders.Images = new OutlookImageList(
+ "NewFolder", // Other
+ "JunkEmailMarkAsNotJunk", // Inbox
+ "GoDrafts", // Drafts
+ "RecycleBin", // WasteBasket
+ "ReceiveMenu", // SentMail
+ "NewFolder", // Outbox
+ "ShowTaskPage", // Task
+ "ShowAppointmentPage", // Appointment
+ "ShowContactPage", // Contact
+ "NewNote", // Note
+ "ShowJournalPage", // Journal
+ "LastModifiedBy" // Store
+
+ ).Images;
+
+ // Add the email address to the title
+ Text = string.Format(Text, account.SmtpAddress);
+
+ // Set up options
+ ShowOptions(new KTreeNode[0]);
+
+ // Set up user selector
+ gabLookup.GAB = FeatureGAB.FindGABForAccount(_account);
+ }
+
+ #region Load and store
+
+ private void AddSharedFolderDialog_Shown(object sender, EventArgs args)
+ {
+ BusyText = Properties.Resources.SharedFolders_Fetching_Label;
+ KUITask
+ .New((ctx) =>
+ {
+ using (SharedFoldersAPI api = new SharedFoldersAPI(_account))
+ {
+ // TODO: bind cancellation token to Cancel button
+ // Fetch current shares
+ ICollection folders = api.GetCurrentShares(ctx.CancellationToken);
+
+ // Find the initial folder if required
+ if (_initialSyncId != null)
+ _initialFolder = folders.FirstOrDefault(f => f.SyncId == _initialSyncId);
+
+ // Group by store and folder id
+ return folders.GroupBy(f => f.Store)
+ .ToDictionary(group => group.Key,
+ group => group.ToDictionary(folder => folder.BackendId));
+ }
+ })
+ .OnSuccess(InitialiseTree, true)
+ .OnError((e) =>
+ {
+ UI.ErrorUtil.HandleErrorNew(typeof(FeatureSharedFolders), "Exception fetching shared folders for account {0}", e,
+ Properties.Resources.SharedFolders_Fetching_Title,
+ Properties.Resources.SharedFolders_Fetching_Failure,
+ _account.DisplayName);
+ DialogResult = DialogResult.Cancel;
+ })
+ .Start(this)
+ ;
+ }
+
+ private void InitialiseTree(KUITaskContext context, Dictionary> shares)
+ {
+ kTreeFolders.BeginUpdate();
+ try
+ {
+ // Add public folders
+ Dictionary publicShares;
+ shares.TryGetValue(GABUser.USER_PUBLIC, out publicShares);
+ AddUserFolders(GABUser.USER_PUBLIC, publicShares, false);
+
+ // Add any users for which we have shared folders
+ foreach (KeyValuePair> entry in shares.OrderBy(x => x.Key.DisplayName))
+ if (GABUser.USER_PUBLIC != entry.Key)
+ AddUserFolders(entry.Key, entry.Value, false);
+ }
+ finally
+ {
+ kTreeFolders.EndUpdate();
+ }
+
+ // Try to select initial node
+ if (_initialFolder != null)
+ {
+ StoreTreeNode node;
+ if (_userFolders.TryGetValue(_initialFolder.Store, out node))
+ {
+ // Keep indicating busy until it's done
+ context.AddBusy(1);
+ node.NodesLoaded += (_) =>
+ {
+ KTreeNode folderNode = node.FindNode(_initialFolder);
+ if (folderNode != null)
+ FocusNode(folderNode);
+ context.AddBusy(-1);
+ };
+ FocusNode(node);
+ }
+ }
+ kTreeFolders.Focus();
+ }
+
+ private void dialogButtons_Apply(object sender, EventArgs e)
+ {
+ BusyText = Properties.Resources.SharedFolders_Applying_Label;
+ KUITask.New((ctx) =>
+ {
+ using (SharedFoldersAPI folders = new SharedFoldersAPI(_account))
+ {
+ // We reuse the same busy indicationg for all calls. A count is kept to ensure it's removed.
+ int count = 0;
+
+ foreach (StoreTreeNode storeNode in _userFolders.Values)
+ {
+ if (storeNode.IsDirty)
+ {
+ ctx.AddBusy(1);
+ ++count;
+
+ folders.SetCurrentShares(storeNode.User, storeNode.CurrentShares, ctx.CancellationToken);
+ }
+ }
+
+ return count;
+ }
+ })
+ .OnSuccess((ctx, count) =>
+ {
+ foreach (StoreTreeNode storeNode in _userFolders.Values)
+ if (storeNode.IsDirty)
+ storeNode.ChangesApplied();
+
+ ctx.AddBusy(-count);
+
+ // Sync account
+ _account.SendReceive();
+
+ // Show success
+ ShowCompletion(Properties.Resources.SharedFolders_Applying_Success);
+ }, true)
+ .OnError((x) =>
+ {
+ ErrorUtil.HandleErrorNew(typeof(FeatureSharedFolders), "Exception applying shared folders for account {0}", x,
+ Properties.Resources.SharedFolders_Applying_Title,
+ Properties.Resources.SharedFolders_Applying_Failure,
+ _account.DisplayName);
+ })
+ .Start(this);
+ }
+
+ #endregion
+
+ #region Event handlers
+
+ private void buttonOpenUser_Click(object sender, EventArgs e)
+ {
+ AddUserFolders(gabLookup.SelectedUser, null, true);
+ }
+
+ private void gabLookup_SelectedUserChanged(object source, GABLookupControl.SelectedUserEventArgs e)
+ {
+ buttonOpenUser.Enabled = (e.SelectedUser != null);
+
+ if (e.IsChosen)
+ {
+ AddUserFolders(e.SelectedUser, null, true);
+ }
+ }
+
+ private void SharedFoldersDialog_DirtyFormClosing(object sender, FormClosingEventArgs e)
+ {
+ // Require confirmation before closing a dirty form
+ e.Cancel = MessageBox.Show(Properties.Resources.SharedFolders_Unsaved_Changes,
+ Text,
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Question
+ ) != DialogResult.Yes;
+ }
+
+ #endregion
+
+ private readonly Dictionary _userFolders = new Dictionary();
+
+ private void AddUserFolders(GABUser user, Dictionary currentShares, bool select)
+ {
+ if (user == null)
+ return;
+
+ // If the user is already fetched, reuse the node
+ StoreTreeNode node;
+ if (!_userFolders.TryGetValue(user, out node))
+ {
+ if (!user.HasFullName)
+ {
+ // Try to fill in the full name
+ user = gabLookup.LookupExact(user.UserName);
+ }
+
+ // Add the node
+ node = new StoreTreeNode(_account, user, user.DisplayName, currentShares ?? new Dictionary());
+ node.DirtyChanged += UserSharesChanged;
+ _userFolders.Add(user, node);
+ kTreeFolders.RootNodes.Add(node);
+ }
+
+ if (select)
+ {
+ FocusNode(node);
+ }
+ }
+
+ private void FocusNode(KTreeNode node)
+ {
+ // Scroll it to the top of the window
+ kTreeFolders.SelectNode(node, KTree.ScrollMode.Top);
+
+ // Start loading folders
+ node.IsExpanded = true;
+
+ // Clear any selected user
+ gabLookup.SelectedUser = null;
+
+ // And focus the tree
+ kTreeFolders.Focus();
+ }
+
+ private readonly Dictionary _dirtyUsers = new Dictionary();
+
+ private void UserSharesChanged(StoreTreeNode node)
+ {
+ _dirtyUsers[node.User.UserName] = node.IsDirty;
+ dialogButtons.IsDirty = _dirtyUsers.Values.Any((x) => x);
+ }
+
+ #region Advanced options
+
+ private string OptionName
+ {
+ get { return textName.Visible ? textName.Text : null; }
+ set
+ {
+ _labelName.Visible = textName.Visible = value != null;
+ textName.Text = value ?? "";
+ }
+ }
+ private FolderTreeNode _optionNameNode;
+
+ private CheckState? OptionSendAs
+ {
+ get
+ {
+ if (checkSendAs.Visible)
+ return checkSendAs.CheckState;
+ return null;
+ }
+
+ set
+ {
+ _labelSendAs.Visible = checkSendAs.Visible = value != null;
+ if (value != null)
+ checkSendAs.CheckState = value.Value;
+ }
+ }
+ private readonly List _optionSendAsNodes = new List();
+ private readonly List _optionSendAsInitial = new List();
+
+ private Permission? _optionPermissions;
+ private Permission? OptionPermissions
+ {
+ get { return _optionPermissions; }
+ set
+ {
+ _optionPermissions = value;
+ _labelPermissions.Visible = labelPermissionsValue.Visible = value != null;
+
+ if (value == null)
+ labelPermissionsValue.Text = "";
+ else
+ {
+ // Look up permission string
+ switch (value)
+ {
+ case Permission.None:
+ labelPermissionsValue.Text = Properties.Resources.SharedFolders_Permission_None;
+ break;
+ case Permission.Read:
+ labelPermissionsValue.Text = Properties.Resources.SharedFolders_Permission_Read;
+ break;
+ case Permission.Write:
+ labelPermissionsValue.Text = Properties.Resources.SharedFolders_Permission_Write;
+ break;
+ case Permission.ReadWrite:
+ labelPermissionsValue.Text = Properties.Resources.SharedFolders_Permission_Read + " / " + Properties.Resources.SharedFolders_Permission_Write;
+ break;
+ }
+ }
+ }
+ }
+ private readonly List _optionPermissionNodes = new List();
+
+ private void ShowOptions(KTreeNode[] nodes)
+ {
+ try
+ {
+ _layoutOptions.SuspendLayout();
+
+ _optionNameNode = null;
+ _optionSendAsNodes.Clear();
+ _optionSendAsInitial.Clear();
+ _optionPermissionNodes.Clear();
+ OptionName = null;
+ OptionSendAs = null;
+ OptionPermissions = null;
+
+ foreach (KTreeNode node in nodes)
+ {
+ // Ignore the root nodes
+ if (node is StoreTreeNode)
+ continue;
+
+ FolderTreeNode folderNode = (FolderTreeNode)node;
+ // Can only set options for shared folders
+ if (!folderNode.IsShared)
+ continue;
+
+ SharedFolder share = folderNode.SharedFolder;
+ AvailableFolder folder = folderNode.AvailableFolder;
+
+ // Assume we will edit the name for this node; cleared below if there are multiple
+ _optionNameNode = folderNode;
+
+ // Show send as if there are any mail folders
+ if (folder.IsMailFolder)
+ {
+ _optionSendAsNodes.Add(folderNode);
+ _optionSendAsInitial.Add(folderNode.SharedFolder.FlagSendAsOwner);
+ }
+
+ // Show permissions for all shared nodes
+ _optionPermissionNodes.Add(folderNode);
+ }
+
+ // Now check consistency of the options
+
+ // Only show the name if there is a single node.
+ // We do that here so there doesn't have to be duplication if testing if it's sharedd,
+ // ect
+ if (_optionNameNode != null && nodes.Length == 1)
+ {
+ OptionName = _optionNameNode.SharedFolder.Name;
+ }
+ else
+ {
+ _optionNameNode = null;
+ }
+
+ // Permissions shown if all are the same
+ if (_optionPermissionNodes.Count > 0)
+ {
+ Permission permissions = _optionPermissionNodes.First().SharedFolder.Permissions;
+ if (_optionPermissionNodes.All(x => x.SharedFolder.Permissions == permissions))
+ OptionPermissions = permissions;
+ }
+
+ // Send as shown if any node supports it
+ if (_optionSendAsNodes.Count > 0)
+ {
+ bool sendAs = _optionSendAsNodes.First().SharedFolder.FlagSendAsOwner;
+ if (_optionSendAsNodes.All(x => x.SharedFolder.FlagSendAsOwner == sendAs))
+ {
+ OptionSendAs = sendAs ? CheckState.Checked : CheckState.Unchecked;
+ checkSendAs.ThreeState = false;
+ }
+ else
+ {
+ OptionSendAs = CheckState.Indeterminate;
+ checkSendAs.ThreeState = true;
+ }
+ }
+ }
+ finally
+ {
+ _layoutOptions.ResumeLayout();
+ }
+ }
+
+ private void kTreeFolders_CheckStateChanged(object sender, KTree.CheckStateChangedEventArgs e)
+ {
+ // If the node is selected, may have to change option display
+ if (e.Node.IsSelected)
+ {
+ ShowOptions(kTreeFolders.SelectedNodes.ToArray());
+ }
+ }
+
+ private void kTreeFolders_SelectionChanged(object sender, KTree.SelectionChangedEventArgs e)
+ {
+ ShowOptions(e.SelectedNodes);
+ }
+
+ private void textName_TextChanged(object sender, EventArgs e)
+ {
+ if (_optionNameNode != null)
+ {
+ _optionNameNode.SharedFolder = _optionNameNode.SharedFolder.WithName(textName.Text);
+ }
+ }
+
+ private void checkSendAs_CheckedChanged(object sender, EventArgs e)
+ {
+ for (int i = 0; i < _optionSendAsNodes.Count; ++i)
+ {
+ FolderTreeNode node = _optionSendAsNodes[i];
+ bool sendAs = false;
+ switch(checkSendAs.CheckState)
+ {
+ case CheckState.Checked: sendAs = true; break;
+ case CheckState.Indeterminate: sendAs = _optionSendAsInitial[i]; break;
+ case CheckState.Unchecked: sendAs = false; break;
+ }
+
+ if (node.SharedFolder.FlagSendAsOwner != sendAs)
+ {
+ node.SharedFolder = node.SharedFolder.WithFlagSendAsOwner(sendAs);
+
+ // Send-as is applied recursively
+ foreach (FolderTreeNode desc in node.Descendants())
+ {
+ desc.SharedFolder = desc.SharedFolder.WithFlagSendAsOwner(sendAs);
+ }
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx
new file mode 100644
index 0000000..ffdef70
--- /dev/null
+++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx
@@ -0,0 +1,762 @@
+
+