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