diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index 7370ed3..4e8901d 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -274,16 +274,17 @@ + + - @@ -299,6 +300,8 @@ + + @@ -312,6 +315,8 @@ + + diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs index d03a91d..eebca38 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Constants.cs @@ -74,7 +74,8 @@ namespace Acacia public const string ZPUSH_HEADER_CLIENT_CAPABILITIES = "X-Push-Plugin-Capabilities"; public const string ZPUSH_HEADER_PLUGIN = "X-Push-Plugin"; public const string ZPUSH_HEADER_VERSION = "X-Z-Push-Version"; - + public const string ZPUSH_HEADER_SIGNATURES_HASH = "X-Push-Signatures-Hash"; + #endregion diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs index 93127fa..bf9fa73 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/DebugOptions.cs @@ -142,6 +142,38 @@ namespace Acacia } + public class StringOption : Option + { + private readonly string _defaultValue; + + public StringOption(string token, string defaultValue) + : + base(token) + { + this._defaultValue = defaultValue; + } + + public override string GetToken(string value) + { + if (value.Equals(_defaultValue)) + return null; + return Token + "=" + value.ToString(); + } + + public override string GetValue(string value) + { + if (string.IsNullOrEmpty(value)) + return _defaultValue; + else + { + if (value.ToLower().StartsWith(Token.ToLower() + "=")) + value = value.Substring(Token.Length + 1); + return value; + } + } + + } + // General public static readonly BoolOption ENABLED = new BoolOption("", true); public static readonly BoolOption FEATURE_DISABLED_DEFAULT = new BoolOption("", false); diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SecondaryContacts/FeatureSecondaryContacts.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SecondaryContacts/FeatureSecondaryContacts.cs index 691eb2d..2d3586b 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SecondaryContacts/FeatureSecondaryContacts.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SecondaryContacts/FeatureSecondaryContacts.cs @@ -87,7 +87,6 @@ namespace Acacia.Features.SecondaryContacts // So, when the folder is detected, we make it invisible and perform steps 1 and 2. We issue a warning // that Outlook must be restarted. When the folder is detected again and is invisible, that means we've restarted // At this point the name is patched and the folder is made visible. - if (!folder.AttrHidden) { // Stage 1 @@ -113,12 +112,12 @@ namespace Acacia.Features.SecondaryContacts // Stage 2 // Patch the name - Logger.Instance.Trace(this, "Patching name"); - folder.Name = strippedName; + Logger.Instance.Trace(this, "Patching name"); + folder.Name = strippedName; - // Show it - folder.AttrHidden = false; - Logger.Instance.Debug(this, "Shown secondary contacts folder: {0}", strippedName); + // Show it + folder.AttrHidden = false; + Logger.Instance.Debug(this, "Shown secondary contacts folder: {0}", strippedName); } Logger.Instance.Debug(this, "Patching done: {0}: {1}", strippedName, folder.AttrHidden); } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Signatures/FeatureSignatures.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Signatures/FeatureSignatures.cs new file mode 100644 index 0000000..51f34cb --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/Signatures/FeatureSignatures.cs @@ -0,0 +1,186 @@ +/// Copyright 2017 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 Acacia.ZPush.Connect; +using Acacia.ZPush.Connect.Soap; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using static Acacia.DebugOptions; + +namespace Acacia.Features.Signatures +{ + [AcaciaOption("Provides the possibility to synchronise signatures from the server.")] + public class FeatureSignatures : Feature + { + #region Debug options + + [AcaciaOption("The format for local names of synchronised signatures, to prevent overwriting local signatures. May contain %account% and %name%.")] + public string SignatureLocalName + { + get { return GetOption(OPTION_SIGNATURE_LOCAL_NAME); } + set { SetOption(OPTION_SIGNATURE_LOCAL_NAME, value); } + } + private static readonly StringOption OPTION_SIGNATURE_LOCAL_NAME = new StringOption("SignatureLocalName", "%name% (KOE-%account%)"); + + #endregion + + public override void Startup() + { + Watcher.AccountDiscovered += Watcher_AccountDiscovered; + } + + private void Watcher_AccountDiscovered(ZPushAccount account) + { + account.ConfirmedChanged += Account_ConfirmedChanged; + } + + private void Account_ConfirmedChanged(ZPushAccount account) + { + // TODO: make a helper to register for all zpush accounts with specific capabilities, best even + // the feature's capabilities + if (account.Confirmed == ZPushAccount.ConfirmationType.IsZPush && + account.Capabilities.Has("signatures")) + { + Logger.Instance.Trace(this, "Checking signature hash for account {0}: {1}", account, account.ServerSignaturesHash); + + // Fetch signatures if there is a change + if (account.ServerSignaturesHash != account.Account.LocalSignaturesHash) + { + try + { + Logger.Instance.Debug(this, "Updating signatures: {0}", account); + FetchSignatures(account); + + // Store updated hash + account.Account.LocalSignaturesHash = account.ServerSignaturesHash; + Logger.Instance.Debug(this, "Updated signatures: {0}", account); + } + catch (Exception e) + { + Logger.Instance.Error(this, "Error fetching signatures: {0}: {1}", account, e); + } + } + } + } + + + // Prevent field assignment warnings + #pragma warning disable 0649 + + private class Signature + { + public string id; + public string name; + public string content; + public bool isHTML; + } + + private class GetSignatures + { + public Dictionary all; + public string new_message; + public string replyforward_message; + public string hash; + } + + #pragma warning restore 0649 + + private class GetSignaturesRequest : SoapRequest + { + } + + private void FetchSignatures(ZPushAccount account) + { + Logger.Instance.Debug(this, "Fetching signatures for account {0}", account); + using (ZPushConnection connection = account.Connect()) + using (ZPushWebServiceInfo infoService = connection.InfoService) + { + GetSignatures result = infoService.Execute(new GetSignaturesRequest()); + + // Store the signatures + Dictionary fullNames = new Dictionary(); + using (ISignatures signatures = ThisAddIn.Instance.GetSignatures()) + { + foreach (Signature signature in result.all.Values) + { + string name = StoreSignature(signatures, account, signature); + fullNames.Add(signature.id, name); + } + } + + // Set default signatures if available and none are set + if (!string.IsNullOrEmpty(result.new_message) && string.IsNullOrEmpty(account.Account.SignatureNewMessage)) + { + account.Account.SignatureNewMessage = fullNames[result.new_message]; + } + if (!string.IsNullOrEmpty(result.replyforward_message) && string.IsNullOrEmpty(account.Account.SignatureReplyForwardMessage)) + { + account.Account.SignatureReplyForwardMessage = fullNames[result.replyforward_message]; + } + } + } + + private string StoreSignature(ISignatures signatures, ZPushAccount account, Signature signatureInfo) + { + string name = SignatureLocalName.ReplacePercentStrings(new Dictionary + { + { "account", account.DisplayName }, + { "name", signatureInfo.name } + }); + + // Remove any existing signature + try + { + ISignature signature = signatures.Get(name); + if (signature != null) + { + try + { + signature.Delete(); + } + finally + { + signature.Dispose(); + } + } + } + catch(Exception e) + { + Logger.Instance.Error(this, "Unable to delete signature {0}: {1}", name, e); + } + + // Create the new signature + using (ISignature signature = signatures.Add(name)) + { + signature.SetContent(signatureInfo.content, signatureInfo.isHTML ? ISignatureFormat.HTML : ISignatureFormat.Text); + // TODO: generate text version if we get an HTML? + } + + return name; + } + } +} \ No newline at end of file diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/IOlkAccount.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/IOlkAccount.cs new file mode 100644 index 0000000..de66aa6 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/IOlkAccount.cs @@ -0,0 +1,53 @@ +using Acacia.Native.MAPI; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Native +{ + [StructLayout(LayoutKind.Explicit)] + unsafe public struct ACCT_VARIANT + { + [FieldOffset(0)] + public uint dwType; + + [FieldOffset(4)] + public uint dwAlignPad; + + [FieldOffset(8)] + public char* lpszW; + } + + [ComImport] + [Guid("9240a6d2-af41-11d2-8c3b-00104b2a6676")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + unsafe public interface IOlkAccount + { + // IOlkErrorUnknown + void IOlkErrorUnknown_GetLastError(); + + void IOlkAccount_Placeholder1(); + void IOlkAccount_Placeholder2(); + void IOlkAccount_Placeholder3(); + void IOlkAccount_Placeholder4(); + void IOlkAccount_Placeholder5(); + void IOlkAccount_Placeholder6(); + + void GetAccountInfo(Guid* pclsidType, int* pcCategories, Guid** prgclsidCategory); + void GetProp(PropTag dwProp, ACCT_VARIANT *pVar); + void SetProp(PropTag dwProp, ACCT_VARIANT *pVar); + + void IOlkAccount_Placeholder7(); + void IOlkAccount_Placeholder8(); + void IOlkAccount_Placeholder9(); + + void FreeMemory(byte* pv); + + void IOlkAccount_Placeholder10(); + + void SaveChanges(uint dwFlags); + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs index 1dd1b10..ac60294 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/MAPI/Property.cs @@ -54,6 +54,15 @@ namespace Acacia.Native.MAPI { return SearchQuery.PropertyIdentifier.FromTag(prop, (ushort)type); } + + public static PropTag FromInt(int v) + { + return new PropTag() + { + prop = (ushort)((v & 0xFFFF0000) >> 16), + type = (PropType)(v & 0xFFFF) + }; + } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs index 5f45c47..59ba465 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/OutlookConstants.cs @@ -41,6 +41,10 @@ namespace Acacia public const string REG_VAL_DELIVERY_STORE = "Delivery Store EntryID"; public const string REG_VAL_DELIVERY_FOLDER = "Delivery Folder EntryID"; + public const string REG_VAL_NEW_SIGNATURE = "New Signature"; + public const string REG_VAL_REPLY_FORWARD_SIGNATURE = "Reply-Forward Signature"; + public const string REG_VAL_CURRENT_SIGNATURE = "KOE Signature Digest"; + public const string REG_VAL_NEXT_ACCOUNT_ID = "NextAccountID"; #endregion diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAccount.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAccount.cs index be8d7cf..69e756d 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAccount.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAccount.cs @@ -49,5 +49,23 @@ namespace Acacia.Stubs string DomainName { get; } + // TODO: this is really a Z-Push thing, but it's here to store it in the registry + string LocalSignaturesHash + { + get; + set; + } + + string SignatureNewMessage + { + get; + set; + } + + string SignatureReplyForwardMessage + { + get; + set; + } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs index 29441eb..7cb8cbc 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/IAddIn.cs @@ -86,5 +86,9 @@ namespace Acacia.Stubs } #endregion + + ISignatures GetSignatures(); + + void InUI(Action action); } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISignature.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISignature.cs new file mode 100644 index 0000000..0ad250e --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISignature.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Stubs +{ + public enum ISignatureFormat + { + HTML, + Text + } + + public interface ISignature : IDisposable + { + void Delete(); + void SetContent(string content, ISignatureFormat format); + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISignatures.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISignatures.cs new file mode 100644 index 0000000..eb011a0 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/ISignatures.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Stubs +{ + public interface ISignatures : IDisposable + { + ISignature Get(string name); + ISignature Add(string name); + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AccountWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AccountWrapper.cs index 2335933..604af82 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AccountWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AccountWrapper.cs @@ -1,4 +1,7 @@ -/// Copyright 2017 Kopano b.v. + +using Acacia.Native; +using Acacia.Native.MAPI; +/// Copyright 2017 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, @@ -13,26 +16,29 @@ /// 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.ComponentModel; using System.Linq; +using System.Runtime.InteropServices; using System.Security; using System.Text; +using System.Threading; using System.Threading.Tasks; +using System.Windows.Forms; +using NSOutlook = Microsoft.Office.Interop.Outlook; namespace Acacia.Stubs.OutlookWrappers { [TypeConverter(typeof(ExpandableObjectConverter))] - class AccountWrapper : DisposableWrapper, IAccount, LogContext + class AccountWrapper : ComWrapper, IAccount, LogContext { private readonly string _regPath; private readonly IStore _store; - internal AccountWrapper(string regPath, IStore store) + internal AccountWrapper(NSOutlook.Application item, string regPath, IStore store) : base(item) { this._regPath = regPath; this._store = store; @@ -44,6 +50,7 @@ namespace Acacia.Stubs.OutlookWrappers protected override void DoRelease() { _store.Dispose(); + base.DoRelease(); } [Browsable(false)] @@ -163,6 +170,83 @@ namespace Acacia.Stubs.OutlookWrappers } } + public string LocalSignaturesHash + { + get + { + return RegistryUtil.GetValueString(_regPath, OutlookConstants.REG_VAL_CURRENT_SIGNATURE, null); + } + set + { + RegistryUtil.SetValueString(_regPath, OutlookConstants.REG_VAL_CURRENT_SIGNATURE, value); + } + } + public string SignatureNewMessage + { + get + { + return RegistryUtil.GetValueString(_regPath, OutlookConstants.REG_VAL_NEW_SIGNATURE, null); + } + set + { + // TODO: constant for account + SetAccountProp(PropTag.FromInt(0x0016001F), value); + } + } + + unsafe private void SetAccountProp(PropTag propTag, string value) + { + // Use IOlkAccount to notify while we're running + // IOlkAccount can only be accessed on main thread + ThisAddIn.Instance.InUI(() => + { + using (ComRelease com = new ComRelease()) + { + NSOutlook.Account account = com.Add(FindAccountObject()); + IOlkAccount olk = com.Add(account.IOlkAccount); + + fixed (char* ptr = value.ToCharArray()) + { + ACCT_VARIANT val = new ACCT_VARIANT() + { + dwType = (uint)PropType.UNICODE, + lpszW = ptr + }; + olk.SetProp(propTag, &val); + olk.SaveChanges(0); + } + } + }); + } + + private NSOutlook.Account FindAccountObject() + { + using (ComRelease com = new ComRelease()) + { + NSOutlook.NameSpace session = com.Add(_item.Session); + foreach(NSOutlook.Account account in session.Accounts.ComEnum(false)) + { + if (account.SmtpAddress == this.SmtpAddress) + return account; + else + com.Add(account); + } + } + return null; + } + + public string SignatureReplyForwardMessage + { + get + { + return RegistryUtil.GetValueString(_regPath, OutlookConstants.REG_VAL_REPLY_FORWARD_SIGNATURE, null); + } + set + { + SetAccountProp(PropTag.FromInt(0x0017001F), value); + } + } + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs index 88f531a..7b4174f 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/AddInWrapper.cs @@ -25,17 +25,19 @@ using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; +using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using NSOutlook = Microsoft.Office.Interop.Outlook; namespace Acacia.Stubs.OutlookWrappers { - public class AddInWrapper : IAddIn + class AddInWrapper : IAddIn { private readonly NSOutlook.Application _app; private readonly ThisAddIn _thisAddIn; private readonly StoresWrapper _stores; + private readonly SynchronizationContext _sync; public AddInWrapper(ThisAddIn thisAddIn) { @@ -51,6 +53,38 @@ namespace Acacia.Stubs.OutlookWrappers { ComRelease.Release(session); } + + // The synchronization context is needed to allow background tasks to jump back to the UI thread. + // It's null in older versions of .Net, this fixes that + if (SynchronizationContext.Current == null) + { + SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext()); + } + _sync = SynchronizationContext.Current; + } + + public ISignatures GetSignatures() + { + return new SignaturesWrapper(); + } + + public void InUI(Action action) + { + Exception x = null; + _sync.Send((_) => + { + try + { + action(); + } + catch(Exception e) + { + x = e; + } + }, null); + + if (x != null) + throw x; } public void SendReceive(IAccount account) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SignatureWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SignatureWrapper.cs new file mode 100644 index 0000000..14fb794 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SignatureWrapper.cs @@ -0,0 +1,80 @@ +using Acacia.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Stubs.OutlookWrappers +{ + class SignatureWrapper : DisposableWrapper, ISignature + { + private static readonly string[] SUFFIXES = + { + "htm", "html", "rtf", "txt" + }; + + private static string BasePath + { + get + { + return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Microsoft\\Signatures"; + } + } + + private readonly string _name; + + public SignatureWrapper(string name) + { + this._name = name; + } + + protected override void DoRelease() + { + } + + internal static ISignature FindExisting(string name) + { + foreach(string suffix in SUFFIXES) + { + string path = GetPath(name, suffix); + if (new FileInfo(path).Exists) + return new SignatureWrapper(name); + } + + return null; + } + + private static string GetPath(string name, string suffix) + { + return Path.ChangeExtension(Path.Combine(BasePath, name), suffix); + } + + public void Delete() + { + foreach (string suffix in SUFFIXES) + { + string path = GetPath(_name, suffix); + FileInfo file = new FileInfo(path); + if (file.Exists) + file.Delete(); + } + // TODO: additional files folder? We never create it + } + + public void SetContent(string content, ISignatureFormat format) + { + // Determine suffix + string suffix = "txt"; + switch(format) + { + case ISignatureFormat.HTML: suffix = "htm"; break; + } + + // Write + string path = GetPath(_name, suffix); + File.WriteAllText(path, content); + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SignaturesWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SignaturesWrapper.cs new file mode 100644 index 0000000..ce429b7 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/SignaturesWrapper.cs @@ -0,0 +1,38 @@ +using Acacia.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Stubs.OutlookWrappers +{ + class SignaturesWrapper : DisposableWrapper, ISignatures + { + public SignaturesWrapper() + { + } + + protected override void DoRelease() + { + } + + public ISignature Get(string name) + { + return SignatureWrapper.FindExisting(name); + } + + public ISignature Add(string name) + { + // Check if exists + using (ISignature existing = SignatureWrapper.FindExisting(name)) + { + if (existing != null) + { + throw new ArgumentException("Signature " + name + " already exists"); + } + } + return new SignatureWrapper(name); + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/StoresWrapper.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/StoresWrapper.cs index 875ca49..4d167b4 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/StoresWrapper.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Stubs/OutlookWrappers/StoresWrapper.cs @@ -247,7 +247,7 @@ namespace Acacia.Stubs.OutlookWrappers { if (baseKey == null) return null; - AccountWrapper account = new AccountWrapper(baseKey.Name, store); + AccountWrapper account = new AccountWrapper(_item.Application, baseKey.Name, store); Register(account); return account; } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs index 2898eb9..bc60279 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ThisAddIn.cs @@ -101,13 +101,6 @@ namespace Acacia int lcid = Application.LanguageSettings.get_LanguageID(Microsoft.Office.Core.MsoAppLanguageID.msoLanguageIDUI); Thread.CurrentThread.CurrentUICulture = new CultureInfo(lcid); - // The synchronization context is needed to allow background tasks to jump back to the UI thread. - // It's null in older versions of .Net, this fixes that - if (SynchronizationContext.Current == null) - { - SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext()); - } - // Create the watcher Watcher = new ZPushWatcher(Instance); OutlookUI.Watcher = Watcher; diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ReflectUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ReflectUtil.cs index 4b57def..5a42583 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ReflectUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/ReflectUtil.cs @@ -43,8 +43,13 @@ namespace Acacia.Utils public static object Convert(this Type type, object value) { // For value types, null becomes a default instance - if (value == null && type.IsValueType) - return Activator.CreateInstance(type); + if (value == null) + { + if (type.IsValueType) + return Activator.CreateInstance(type); + else + return null; + } // Check if we need a conversion if (!type.IsAssignableFrom(value.GetType())) @@ -89,11 +94,27 @@ namespace Acacia.Utils public static Type[] GetGenericArguments(this Type type, Type _base) { + if (type.IsGenericType && type.GetGenericTypeDefinition() == _base) + return type.GetGenericArguments(); + IEnumerable bases = _base.IsInterface ? type.GetInterfaces() : type.AllBaseTypes(); return bases.Select(x => (x.IsGenericType && x.GetGenericTypeDefinition() == _base) ? x.GetGenericArguments() : null ).FirstOrDefault(); } + + public static IEnumerable DebugComTypeNames(object o) + { + foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + foreach (Type type in assembly.GetTypes()) + { + if (type.IsInstanceOfType(o)) + yield return type.FullName; + } + } + + } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/RegistryUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/RegistryUtil.cs index e0b6fd8..ae9b834 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/RegistryUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/RegistryUtil.cs @@ -34,13 +34,17 @@ namespace Acacia.Utils return RegToString(o); } - // TODO: remove, also above public static string GetValueString(string keyPath, string valueName, string defaultValue) { object o = Registry.GetValue(keyPath, valueName, defaultValue); return RegToString(o); } + public static void SetValueString(string keyPath, string valueName, string value) + { + Registry.SetValue(keyPath, valueName, value); + } + public static string RegToString(object o) { if (o is byte[]) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs index cf3c4b8..997c91d 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/StringUtil.cs @@ -19,6 +19,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; @@ -105,5 +106,25 @@ namespace Acacia.Utils #endregion + #region Formatting / Replacement + + public static string ReplacePercentStrings(this string s, Dictionary replacements) + { + return Regex.Replace(s, @"%(\w+)%", (m) => + { + string replacement; + var key = m.Groups[1].Value; + if (replacements.TryGetValue(key, out replacement)) + { + return Convert.ToString(replacement); + } + else + { + return m.Groups[0].Value; + } + }); + } + + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapSerializer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapSerializer.cs index 114a1c1..eff1718 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapSerializer.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/Soap/SoapSerializer.cs @@ -81,7 +81,7 @@ namespace Acacia.ZPush.Connect.Soap return null; // Type-specific parsing - TypeHandler type = LookupType(part); + TypeHandler type = LookupType(part, expectedType); return type.Deserialize(part, expectedType); } @@ -109,24 +109,43 @@ namespace Acacia.ZPush.Connect.Soap public object Deserialize(XmlNode node, Type expectedType) { - if (expectedType != null && _baseType != null) - { - // Check if the expected type matches the type - if (!_baseType.IsAssignableFrom(expectedType)) - throw new InvalidOperationException("Expected " + expectedType + ", found " + _baseType); - } - object value = DeserializeContents(node, expectedType); - if (expectedType != null) { - // Check if it's the expected type - return expectedType.Cast(value); + // Try to convert it to the expected type + return SoapConvert(expectedType, value); } return value; } + + protected object SoapConvert(Type type, object value) + { + // Check if any conversion is needed + if (value != null && type.IsAssignableFrom(value.GetType())) + return value; + + if (value != null) + { + // Try Soap conversion + if (typeof(ISoapSerializable<>).IsGenericAssignableFrom(type)) + { + // Get the serialization type + Type serializationType = type.GetGenericArguments(typeof(ISoapSerializable<>))[0]; + if (serializationType.IsAssignableFrom(value.GetType())) + { + // Create the instance + return Activator.CreateInstance(type, value); + } + } + } + + // Or standard conversions + return type.Convert(value); + } + + abstract protected object DeserializeContents(XmlNode node, Type expectedType); abstract public void Serialize(string name, object value, StringBuilder s); @@ -150,7 +169,7 @@ namespace Acacia.ZPush.Connect.Soap } private class TypeHandlerInt : TypeHandler { - public TypeHandlerInt() : base(SoapConstants.XMLNS_XSD, "int", typeof(int)) { } + public TypeHandlerInt() : base(SoapConstants.XMLNS_XSD, "int", typeof(long)) { } public override void Serialize(string name, object value, StringBuilder s) { @@ -159,7 +178,7 @@ namespace Acacia.ZPush.Connect.Soap protected override object DeserializeContents(XmlNode node, Type expectedType) { - return int.Parse(node.InnerText); + return long.Parse(node.InnerText); } } @@ -230,7 +249,10 @@ namespace Acacia.ZPush.Connect.Soap { // Determine if the expected type is an ISoapSerializable if (!typeof(ISoapSerializable<>).IsGenericAssignableFrom(expectedType)) - throw new InvalidOperationException("Cannot parse type " + expectedType); + { + // Nope, try simple assignment + return DeserializeContentsRaw(node, expectedType); + } // Get the serialization type Type serializationType = expectedType.GetGenericArguments(typeof(ISoapSerializable<>))[0]; @@ -243,6 +265,32 @@ namespace Acacia.ZPush.Connect.Soap return CreateCustomInstance(values, serializationType, expectedType); } + private object DeserializeContentsRaw(XmlNode node, Type expectedType) + { + // TODO: better error on failure + // Get the values as a dictionary + Dictionary values = new Dictionary(); + DeserializeMembers(node, expectedType, values); + + // And assign them to a new instance + return DeserializeContentsRaw(values, expectedType); + } + + private object DeserializeContentsRaw(Dictionary node, Type serializationType) + { + object instance = Activator.CreateInstance(serializationType); + foreach (FieldInfo field in serializationType.GetFields()) + { + object value = null; + if (node.TryGetValue(field.Name.ToLower(), out value)) + { + value = SoapConvert(field.FieldType, value); + field.SetValue(instance, value); + } + } + return instance; + } + abstract protected void DeserializeMembers(XmlNode node, Type serializationType, Dictionary values); private object CreateCustomInstance(Dictionary node, Type serializationType, Type finalType) @@ -255,47 +303,13 @@ namespace Acacia.ZPush.Connect.Soap else { // Initialise the serialization type - instance = Activator.CreateInstance(serializationType); - foreach (FieldInfo field in serializationType.GetFields()) - { - object value = null; - if (node.TryGetValue(field.Name.ToLower(), out value)) - { - value = SoapConvert(field.FieldType, value); - field.SetValue(instance, value); - } - } + instance = DeserializeContentsRaw(node, serializationType); } // Return the final type return Activator.CreateInstance(finalType, instance); } - private object SoapConvert(Type type, object value) - { - // Check if any conversion is needed - if (value != null && type.IsAssignableFrom(value.GetType())) - return value; - - if (value != null) - { - // Try Soap conversion - if (typeof(ISoapSerializable<>).IsGenericAssignableFrom(type)) - { - // Get the serialization type - Type serializationType = type.GetGenericArguments(typeof(ISoapSerializable<>))[0]; - if (serializationType.IsAssignableFrom(value.GetType())) - { - // Create the instance - return Activator.CreateInstance(type, value); - } - } - } - - // Or standard conversions - return type.Convert(value); - } - public override void Serialize(string name, object value, StringBuilder s) { Dictionary dict; @@ -319,6 +333,18 @@ namespace Acacia.ZPush.Connect.Soap } protected abstract void SerializeMembers(string name, Dictionary fields, StringBuilder s); + + protected virtual Type DetermineChildType(Type type, string field) + { + if (type == null) + return null; + + FieldInfo prop = type.GetField(field); + if (prop == null) + return null; + + return prop.FieldType; + } } private class TypeHandlerStruct : TypeHandlerObject @@ -333,7 +359,7 @@ namespace Acacia.ZPush.Connect.Soap foreach (XmlNode child in node.ChildNodes) { string key = child.Name.ToLower(); - object value = DeserializeNode(child, null); + object value = DeserializeNode(child, DetermineChildType(expectedType, key)); dict.Add(key, value); } } @@ -345,9 +371,9 @@ namespace Acacia.ZPush.Connect.Soap } - private class TypeHandlerMap : TypeHandlerObject + private class TypeHandlerObjectMap : TypeHandlerObject { - public TypeHandlerMap() : base(SoapConstants.XMLNS_APACHE, "Map") + public TypeHandlerObjectMap() : base(SoapConstants.XMLNS_APACHE, "Map") { } @@ -357,7 +383,7 @@ namespace Acacia.ZPush.Connect.Soap foreach (XmlNode child in node.ChildNodes) { string key = (string)DeserializeNode(child.SelectSingleNode("key"), typeof(string)); - object value = DeserializeNode(child.SelectSingleNode("value"), null); + object value = DeserializeNode(child.SelectSingleNode("value"), DetermineChildType(expectedType, key)); dict.Add(key, value); } } @@ -377,10 +403,38 @@ namespace Acacia.ZPush.Connect.Soap #endregion + #region Map + + private class TypeHandlerMap : TypeHandler + { + public TypeHandlerMap() : base(SoapConstants.XMLNS_SOAP_ENC, "Array", typeof(System.Collections.ICollection)) { } + + protected override object DeserializeContents(XmlNode node, Type expectedType) + { + Dictionary map = new Dictionary(); + + foreach (XmlNode child in node.ChildNodes) + { + KeyType key = (KeyType)DeserializeNode(child.SelectSingleNode("key"), typeof(KeyType)); + ValueType value = (ValueType)DeserializeNode(child.SelectSingleNode("value"), typeof(ValueType)); + map.Add(key, value); + } + + return map; + } + + public override void Serialize(string name, object value, StringBuilder s) + { + throw new NotImplementedException(); + } + } + + #endregion + private readonly static Dictionary TYPES_BY_FULL_NAME = new Dictionary(); private readonly static Dictionary TYPES_BY_TYPE = new Dictionary(); - private readonly static TypeHandler TYPE_HANDLER_OBJECT = new TypeHandlerMap(); + private readonly static TypeHandler TYPE_HANDLER_OBJECT = new TypeHandlerObjectMap(); static SoapSerializer() { @@ -415,8 +469,14 @@ namespace Acacia.ZPush.Connect.Soap return null; } - private static TypeHandler LookupType(XmlNode part) + private static TypeHandler LookupType(XmlNode part, Type expectedType) { + if (expectedType != null && typeof(IDictionary<,>).IsGenericAssignableFrom(expectedType)) + { + Type bound = typeof(TypeHandlerMap<,>).MakeGenericType(expectedType.GetGenericArguments(typeof(IDictionary<,>))); + return (TypeHandler)Activator.CreateInstance(bound); + } + XmlAttribute typeAttr = part.Attributes["type", SoapConstants.XMLNS_XSI]; if (typeAttr == null) throw new Exception("Missing type"); @@ -431,6 +491,7 @@ namespace Acacia.ZPush.Connect.Soap TypeHandler type; if (!TYPES_BY_FULL_NAME.TryGetValue(fullName, out type)) throw new Exception("Unknown type: " + fullName); + return type; } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushConnection.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushConnection.cs index aa846de..28ef6f2 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushConnection.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/Connect/ZPushConnection.cs @@ -204,6 +204,12 @@ namespace Acacia.ZPush.Connect private set; } + public string SignaturesHash + { + get; + private set; + } + private string GetStringHeader(HttpResponseMessage response, string name) { IEnumerable values; @@ -227,6 +233,7 @@ namespace Acacia.ZPush.Connect Capabilities = ZPushCapabilities.Parse(GetStringHeader(response, Constants.ZPUSH_HEADER_CAPABILITIES)); ZPushVersion = GetStringHeader(response, Constants.ZPUSH_HEADER_VERSION); + SignaturesHash = GetStringHeader(response, Constants.ZPUSH_HEADER_SIGNATURES_HASH); // Check for success Success = response.IsSuccessStatusCode; diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushAccount.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushAccount.cs index 294329d..5920f8d 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushAccount.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/ZPushAccount.cs @@ -120,6 +120,13 @@ namespace Acacia.ZPush private set; } + public string ServerSignaturesHash + { + get; + private set; + } + + public void LinkedGABFolder(IFolder folder) { GABFolderLinked = folder.EntryID; @@ -128,13 +135,14 @@ namespace Acacia.ZPush internal void OnConfirmationResponse(ZPushConnection.Response response) { Capabilities = response.Capabilities; + // TODO: move these properties to the features? Though it's nice to have them here for the debug dialog GABFolder = response.GABName; ZPushVersion = response.ZPushVersion; + ServerSignaturesHash = response.SignaturesHash; Confirmed = Capabilities == null ? ConfirmationType.IsNotZPush : ConfirmationType.IsZPush; Logger.Instance.Info(this, "ZPush confirmation: {0} -> {1}, {2}", Confirmed, Capabilities, GABFolder); - if (_confirmedChanged != null) - _confirmedChanged(this); + _confirmedChanged?.Invoke(this); } #endregion