kopano-ol-extension/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/FeatureSharedFolders.cs

510 lines
21 KiB
C#

/// Copyright 2018 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/>.
///
/// Consult LICENSE file for details
using Acacia.Features.SecondaryContacts;
using Acacia.Features.SendAs;
using Acacia.Stubs;
using Acacia.UI;
using Acacia.UI.Outlook;
using Acacia.Utils;
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;
using System.Windows.Forms;
using static Acacia.DebugOptions;
namespace Acacia.Features.SharedFolders
{
[AcaciaOption("Provides the ability to open shared folders from other users in Outlook.")]
public class FeatureSharedFolders
:
Feature, FeatureWithRibbon, FeatureWithContextMenu
{
#region Debug options
[AcaciaOption("Disables the update of the reminders query. If this is disabled, the reminders flag on " +
"shared folders will be ignored.")]
public bool Reminders
{
get { return GetOption(OPTION_REMINDERS); }
set { SetOption(OPTION_REMINDERS, value); }
}
private static readonly BoolOption OPTION_REMINDERS = new BoolOption("Reminders", true);
[AcaciaOption("If disabled, the reminders queyr will be explicitly stopped and started when update the query. " +
"This causes more effort to search again, but might prevent issues.")]
public bool RemindersKeepRunning
{
get { return GetOption(OPTION_REMINDERS_KEEP_RUNNING); }
set { SetOption(OPTION_REMINDERS_KEEP_RUNNING, value); }
}
private static readonly BoolOption OPTION_REMINDERS_KEEP_RUNNING = new BoolOption("RemindersKeepRunning", true);
[AcaciaOption("The format for local names of shared folders. May contain contact fields and %foldername%.")]
public string DefaultFolderNameFormat
{
get { return RegistryUtil.GetConfigValue("SharedFolders", "DefaultFolderNameFormat", "%foldername% - %username%"); }
set { RegistryUtil.SetConfigValue("SharedFolders", "DefaultFolderNameFormat", value, Microsoft.Win32.RegistryValueKind.String); }
}
[AcaciaOption("If enabled, the 'Impersonate' capability is added, allowing whole stores to be opened through " +
"user impersonation; if Z-Push supports it.")]
public bool AllowImpersonate
{
get { return GetOption(OPTION_ALLOW_IMPERSONATE); }
set { SetOption(OPTION_ALLOW_IMPERSONATE, value); }
}
private static readonly BoolOption OPTION_ALLOW_IMPERSONATE = new BoolOption("AllowImpersonate", false);
#endregion
public override void Startup()
{
RegisterButton(this, "SharedFolders", true, ManageFolders, ZPushBehaviour.Disable);
MenuItem<IFolder> menuItem = RegisterMenuItem<IFolder>(this, "SharedFolders_Context", null, ManageFolder, ZPushBehaviour.Hide);
if (menuItem != null)
menuItem.CheckEnabled = CanManageFolder;
// Sync state
Watcher.Sync.AddTask(this, Name, AdditionalFolders_Sync);
// Private shared appointment
SetupPrivateAppointmentSuppression();
SetupHierarchyChangeSuppression();
}
override public void GetCapabilities(ZPushCapabilities caps)
{
base.GetCapabilities(caps);
if (AllowImpersonate)
caps.Add(Constants.ZPUSH_CAPABILITY_IMPERSONATE);
}
#region UI
private bool CanManageFolder(MenuItem<IFolder> b, IFolder folder)
{
return folder.SyncId?.IsCustom == true;
}
private void ManageFolder(IFolder folder)
{
ZPushAccount account = Watcher.Accounts.GetAccount(folder);
if (account != null)
{
new SharedFoldersDialog(this, account, folder.SyncId).ShowDialog();
}
}
private void ManageFolders()
{
ZPushAccount account = Watcher.CurrentZPushAccount();
if (account != null)
{
new SharedFoldersDialog(this, account).ShowDialog();
}
}
#endregion
#region Folder management
internal SharedFoldersManager Manage(ZPushAccount account)
{
return new SharedFoldersManager(this, account);
}
#endregion
#region Shared folders sync
private const string KEY_SHARES = "Shares";
private void AdditionalFolders_Sync(ZPushConnection connection)
{
SyncShares(connection.Account);
}
public void Sync(ZPushAccount account)
{
account.Account.SendReceive(new AcaciaTask(null, this, "SyncShares", () => SyncShares(account)));
}
private void SyncShares(ZPushAccount account)
{
using (SharedFoldersManager manager = Manage(account))
{
Logger.Instance.Debug(this, "Starting sync for account {0}", account);
// Fetch the current shares
ICollection<SharedFolder> shares = manager.GetCurrentShares(null);
Logger.Instance.Trace(this, "AdditionalFolders_Sync: {0}", shares.Count);
// Convert to dictionary
Dictionary<SyncId, SharedFolder> dict = shares.ToDictionary(x => x.SyncId);
Logger.Instance.Trace(this, "AdditionalFolders_Sync2: {0}", shares.Count);
// Store any send-as properties
FeatureSendAs sendAs = ThisAddIn.Instance.GetFeature<FeatureSendAs>();
sendAs?.UpdateSendAsAddresses(account, shares);
// Store with the account
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.IsCustom)
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<SyncId, SharedFolder> shared = account.GetFeatureData<Dictionary<SyncId, SharedFolder>>(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;
}
public static bool IsSharedFolder(IFolder folder)
{
string id = (string)folder.GetProperty(OutlookConstants.PR_ZPUSH_SYNC_ID);
return id?.StartsWith("S") == true;
}
#endregion
#region Private appointments
[AcaciaOption("If enabled, modifications to private appointments in shared calendars are suppressed. " +
"This should only be disabled for debug purposes.")]
public bool SuppressModificationsPrivateAppointments
{
get { return GetOption(OPTION_SUPPRESS_MODIFICATIONS_PRIVATE_APPOINTMENTS); }
set { SetOption(OPTION_SUPPRESS_MODIFICATIONS_PRIVATE_APPOINTMENTS, value); }
}
private static readonly BoolOption OPTION_SUPPRESS_MODIFICATIONS_PRIVATE_APPOINTMENTS =
new BoolOption("SuppressModificationsPrivateAppointments", true);
// TODO: this is largely duplicated from FeatureGAB, separate into helper
private void SetupPrivateAppointmentSuppression()
{
if (SuppressModificationsPrivateAppointments && MailEvents != null)
{
Logger.Instance.Trace(this, "Setting up private appointment modification suppression");
MailEvents.BeforeDelete += SuppressEventHandler_Delete;
MailEvents.PropertyChange += SuppressEventHandler_PropertyChange;
MailEvents.Write += SuppressEventHandler_Write;
}
else
{
Logger.Instance.Trace(this, "Not setting up private appointment modification suppression");
}
}
private void SuppressEventHandler_Delete(IItem item, ref bool cancel)
{
Logger.Instance.Trace(this, "Private appointment: delete");
SuppressEventHandler(item, false, ref cancel);
}
/// <summary>
/// When copying a contact from the GAB to a local folder, Outlook raises the Write event on
/// the original. To detect this, we set this id on every property change (which is signalled before
/// write), and only suppress if anything has actually changed. If we suppress, the flag is cleared again.
/// </summary>
private string _propertyChangeId;
private void SuppressEventHandler_PropertyChange(IItem item, string propertyName)
{
if (_propertyChangeId == item.EntryID)
return;
Logger.Instance.Trace(this, "Private appointment: property change");
_propertyChangeId = item.EntryID;
}
private void SuppressEventHandler_Write(IItem item, ref bool cancel)
{
if (_propertyChangeId == item.EntryID)
{
Logger.Instance.Trace(this, "Private appointment: write");
SuppressEventHandler(item, true, ref cancel);
_propertyChangeId = null;
}
}
private void SuppressEventHandler(IItem item, bool findInspector, ref bool cancel)
{
Logger.Instance.Trace(this, "Private appointment: suppress");
// Check if it's an appointment
IAppointmentItem appointment = item as IAppointmentItem;
if (appointment == null)
{
Logger.Instance.TraceExtra(this, "Private appointment: suppress: not an appointment");
return;
}
// Check if it's private. Confidential is also considered private
if (appointment.Sensitivity < Sensitivity.Private)
{
Logger.Instance.TraceExtra(this, "Private appointment: suppress: not private");
return;
}
// Check if in a shared folder
using (IFolder parent = item.Parent)
{
if (parent == null || !IsSharedFolder(parent))
{
Logger.Instance.TraceExtra(this, "Private appointment: suppress: not in a shared folder");
return;
}
}
if (findInspector)
{
// Find and close any inspector
using (IInspectors inspectors = ThisAddIn.Instance.GetInspectors())
foreach (IInspector inspector in inspectors)
{
using (inspector)
using (IItem inspectItem = inspector.GetCurrentItem())
{
if (appointment.EntryID == inspectItem.EntryID)
{
inspector.Close(InspectorClose.Discard);
}
}
}
}
// Private appointment in a shared folder, suppress
Logger.Instance.TraceExtra(this, "Private appointment: suppressing");
cancel = true;
MessageBox.Show(ThisAddIn.Instance.Window,
Properties.Resources.SharedFolders_PrivateEvent_Body,
Properties.Resources.SharedFolders_PrivateEvent_Title,
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
}
#endregion
#region Hierarchy changes
private class SharedFolderRegistration : FolderRegistration
{
public SharedFolderRegistration(Feature feature) : base(feature)
{
}
public override bool IsApplicable(IFolder folder)
{
if (folder.SyncId != null && folder.SyncId.IsCustom)
return true;
using (IFolder parent = folder.Parent)
{
if (parent != null)
return IsApplicable(parent);
}
return false;
}
}
[AcaciaOption("If enabled, modifications to the local hierarchy of shared folders is suppressed. " +
"This should only be disabled for debug purposes.")]
public bool SuppressHierarchyChanges
{
get { return GetOption(OPTION_SUPPRESS_HIERARCHY_CHANGES); }
set { SetOption(OPTION_SUPPRESS_HIERARCHY_CHANGES, value); }
}
private static readonly BoolOption OPTION_SUPPRESS_HIERARCHY_CHANGES =
new BoolOption("SuppressHierarchyChanges", true);
private void SetupHierarchyChangeSuppression()
{
if (SuppressHierarchyChanges)
{
Watcher.WatchFolder(new SharedFolderRegistration(this),
OnSharedFolderDiscovered,
OnSharedFolderChanged,
OnSharedFolderRemoved);
// Register for any folder move
Watcher.WatchFolder(new FolderRegistrationAny(this),
(folder) =>
{
folder.BeforeFolderMove += Folder_BeforeFolderMove;
});
}
}
private void OnSharedFolderDiscovered(IFolder folder)
{
Logger.Instance.Trace(this, "Shared folder discovered: {0} - {1}", folder.Name, folder.SyncId);
if (folder.SyncId == null || !folder.SyncId.IsCustom)
{
Logger.Instance.Warning(this, "Local folder created in shared folder, deleting: {0} - {1}", folder.Name, folder.SyncId);
// This is a new, locally created folder. Warn and remove
MessageBox.Show(ThisAddIn.Instance.Window,
Properties.Resources.SharedFolders_LocalFolder_Body,
Properties.Resources.SharedFolders_LocalFolder_Title,
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
folder.Delete();
Logger.Instance.Warning(this, "Local folder created in shared folder, deleted: {0} - {1}", folder.Name, folder.SyncId);
}
else
{
// Check if it was renamed before the events were fully set up
CheckSharedFolderRename(folder);
}
}
private void Folder_BeforeFolderMove(IFolder src, IFolder moveTo, ref bool cancel)
{
if (src.SyncId?.IsCustom == true || moveTo.SyncId?.IsCustom == true)
{
// Suppress any move of or into a shared folder
Logger.Instance.Warning(this, "Shared folder move: {0} - {1}", src.Name, moveTo.Name);
MessageBox.Show(ThisAddIn.Instance.Window,
Properties.Resources.SharedFolders_LocalFolder_Body,
Properties.Resources.SharedFolders_LocalFolder_Title,
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
cancel = true;
}
}
private void OnSharedFolderChanged(IFolder folder)
{
Logger.Instance.Trace(this, "Shared folder changed: {0} - {1}", folder.Name, folder.SyncId);
CheckSharedFolderRename(folder);
}
private void CheckSharedFolderRename(IFolder folder)
{
if (folder.SyncId != null && folder.SyncId.IsCustom)
{
string originalName = (string)folder.GetProperty(OutlookConstants.PR_ZPUSH_NAME);
// The folder.name property is sometimes cached, check against the MAPI property
string currentName = (string)folder.GetProperty(OutlookConstants.PR_DISPLAY_NAME_W);
if (currentName != originalName &&
// Secondary contacts renames folder, check for that
!FeatureSecondaryContacts.IsSecondaryFolderRename(originalName, currentName))
{
Logger.Instance.Warning(this, "Shared folder renamed, renaming back: {0} - {1} - {2}", folder.Name, folder.SyncId, originalName);
// This is a locally renamed folder. Warn and rename back
MessageBox.Show(ThisAddIn.Instance.Window,
Properties.Resources.SharedFolders_LocalFolder_Body,
Properties.Resources.SharedFolders_LocalFolder_Title,
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
// Update both name and display name
folder.Name = originalName;
folder.SetProperty(OutlookConstants.PR_DISPLAY_NAME_W, originalName);
Logger.Instance.Warning(this, "Shared folder renamed, renamed back: {0} - {1} - {2}", folder.Name, folder.SyncId, originalName);
}
}
}
private void OnSharedFolderRemoved(IFolder folder)
{
Logger.Instance.Fatal(this, "Shared folder removed, undeleting: {0}", folder.Name);
MessageBox.Show(ThisAddIn.Instance.Window,
Properties.Resources.SharedFolders_LocalFolder_Body,
Properties.Resources.SharedFolders_LocalFolder_Title,
MessageBoxButtons.OK,
MessageBoxIcon.Warning
);
// TODO: is this used?
}
#endregion
#region Shared stores
public void RemoveSharedStore(ZPushAccount account, GABUser shareUser)
{
// Find the store
Logger.Instance.Trace(this, "Request to remove shared store: {0} - {1}", account, shareUser.UserName);
ZPushAccount share = account.FindSharedAccount(shareUser.UserName);
if (share == null)
{
Logger.Instance.Warning(this, "Shared store not found: {0} - {1}", account, shareUser.UserName);
return;
}
Logger.Instance.Trace(this, "Removing shared store: {0} - {1}", account, share);
try
{
string path = share.Account.BackingFilePath;
ThisAddIn.Instance.Stores.RemoveStore(share.Account.Store);
// Clean up the .ost
// TODO: this always fails
/*if (path != null && path.EndsWith(".ost"))
{
Logger.Instance.Trace(this, "Removing .ost: {0}", path);
System.IO.File.Delete(path);
}*/
}
catch(Exception e)
{
Logger.Instance.Error(this, "Error removing shared store: {0}: {1}", share, e);
}
}
#endregion
}
}