1
0
mirror of https://github.com/Kopano-dev/kopano-ol-extension.git synced 2023-10-10 13:37:40 +02:00

[KOE-17] Added replacement of placeholders in signatures based on GAB. To detect GAB changes, added the FeatureGAB.SyncFinished event, and CompletionTracker to track completion with asynchronous tasks.

This commit is contained in:
Patrick Simpson 2017-02-23 17:07:39 +01:00
parent e735aafc72
commit 6daac784c3
20 changed files with 315 additions and 74 deletions

View File

@ -326,6 +326,7 @@
<Compile Include="UI\Outlook\CommandElement.cs" /> <Compile Include="UI\Outlook\CommandElement.cs" />
<Compile Include="UI\Outlook\MenuItem.cs" /> <Compile Include="UI\Outlook\MenuItem.cs" />
<Compile Include="UI\Outlook\Types.cs" /> <Compile Include="UI\Outlook\Types.cs" />
<Compile Include="Utils\CompletionTracker.cs" />
<Compile Include="Utils\DisposableWrapper.cs" /> <Compile Include="Utils\DisposableWrapper.cs" />
<Compile Include="Utils\ImageUtils.cs" /> <Compile Include="Utils\ImageUtils.cs" />
<Compile Include="Utils\RegistryUtil.cs" /> <Compile Include="Utils\RegistryUtil.cs" />

View File

@ -97,9 +97,10 @@ namespace Acacia.Features.DebugSupport
private void DebugCycle() private void DebugCycle()
{ {
Tasks.Task(gab, "DebugCycle", () => // TODO: use completiontracker
Tasks.Task(null, gab, "DebugCycle", () =>
{ {
gab.FullResync(); gab.FullResync(null);
}); });
} }

View File

@ -147,7 +147,7 @@ namespace Acacia.Features.FreeBusy
TcpClient client = listener.AcceptTcpClient(); TcpClient client = listener.AcceptTcpClient();
Interlocked.Increment(ref _requestCount); Interlocked.Increment(ref _requestCount);
// And handle it in the UI thread to allow GAB access // And handle it in the UI thread to allow GAB access
Tasks.Task(this, "FreeBusyHandler", () => server.HandleRequest(client)); Tasks.Task(null, this, "FreeBusyHandler", () => server.HandleRequest(client));
} }
} }
catch (Exception e) catch (Exception e)

View File

@ -45,6 +45,14 @@ namespace Acacia.Features.GAB
{ {
} }
public delegate void GABSyncFinishedHandler(GABHandler gab);
public event GABSyncFinishedHandler SyncFinished;
public void OnGabSyncFinished(GABHandler gab)
{
SyncFinished?.Invoke(gab);
}
public static GABHandler FindGABForAccount(ZPushAccount account) public static GABHandler FindGABForAccount(ZPushAccount account)
{ {
FeatureGAB gab = ThisAddIn.Instance.GetFeature<FeatureGAB>(); FeatureGAB gab = ThisAddIn.Instance.GetFeature<FeatureGAB>();
@ -305,7 +313,7 @@ namespace Acacia.Features.GAB
#region Resync #region Resync
internal void FullResync() internal void FullResync(CompletionTracker completion)
{ {
try try
{ {
@ -345,10 +353,12 @@ namespace Acacia.Features.GAB
int remaining = _gabsByDomainName.Count; int remaining = _gabsByDomainName.Count;
foreach (GABHandler gab in _gabsByDomainName.Values) foreach (GABHandler gab in _gabsByDomainName.Values)
{ {
CompletionTracker partCompletion = new CompletionTracker(() => OnGabSyncFinished(gab));
// TODO: merge partCompletion into total completion
Logger.Instance.Debug(this, "FullResync: Starting resync: {0}", gab.DisplayName); Logger.Instance.Debug(this, "FullResync: Starting resync: {0}", gab.DisplayName);
Tasks.Task(this, "FullResync", () => Tasks.Task(partCompletion, this, "FullResync", () =>
{ {
gab.FullResync(); gab.FullResync(partCompletion);
}); });
} }
} }
@ -662,15 +672,20 @@ namespace Acacia.Features.GAB
++_processing; ++_processing;
Logger.Instance.Trace(this, "Processing GAB message: {0} - {1}", account, _processing); Logger.Instance.Trace(this, "Processing GAB message: {0} - {1}", account, _processing);
try CompletionTracker completion = new CompletionTracker(() => OnGabSyncFinished(gab));
using (completion.Begin())
{ {
gab.Process(item); try
DoEmptyDeletedItems(); {
} gab.Process(completion, item);
finally // TODO: this will probably run while still processing, use completion tracker
{ DoEmptyDeletedItems();
Logger.Instance.Trace(this, "Processed GAB message: {0} - {1}", account, _processing); }
--_processing; finally
{
Logger.Instance.Trace(this, "Processed GAB message: {0} - {1}", account, _processing);
--_processing;
}
} }
} }

View File

@ -128,10 +128,10 @@ namespace Acacia.Features.GAB
#region Processing #region Processing
public void FullResync() public void FullResync(CompletionTracker completion)
{ {
ClearContacts(); ClearContacts();
Process(null); Process(completion, null);
} }
private void ClearContacts() private void ClearContacts()
@ -167,27 +167,30 @@ namespace Acacia.Features.GAB
/// Processes the GAB message(s). /// Processes the GAB message(s).
/// </summary> /// </summary>
/// <param name="item">If specified, this item has changed. If null, means a global check should be performed</param> /// <param name="item">If specified, this item has changed. If null, means a global check should be performed</param>
public void Process(IZPushItem item) public void Process(CompletionTracker completion, IZPushItem item)
{ {
try using (CompletionTracker.Step step = completion?.Begin())
{ {
if (item == null) try
{ {
if (Folder != null) if (item == null)
ProcessMessages(); {
if (Folder != null)
ProcessMessages(completion);
}
else
{
ProcessMessage(completion, item);
}
} }
else catch (Exception e)
{ {
ProcessMessage(item); Logger.Instance.Error(this, "Exception in GAB.Process: {0}", e);
} }
} }
catch(Exception e)
{
Logger.Instance.Error(this, "Exception in GAB.Process: {0}", e);
}
} }
private void ProcessMessages() private void ProcessMessages(CompletionTracker completion)
{ {
if (!_feature.ProcessFolder) if (!_feature.ProcessFolder)
return; return;
@ -207,12 +210,12 @@ namespace Acacia.Features.GAB
Logger.Instance.Trace(this, "Checking chunk: {0}", item.Subject); Logger.Instance.Trace(this, "Checking chunk: {0}", item.Subject);
if (_feature.ProcessItems2) if (_feature.ProcessItems2)
{ {
Tasks.Task(_feature, "ProcessChunk", () => Tasks.Task(completion, _feature, "ProcessChunk", () =>
{ {
using (IItem item2 = Folder.GetItemById(entryId)) using (IItem item2 = Folder.GetItemById(entryId))
{ {
if (item2 != null) if (item2 != null)
ProcessMessage((IZPushItem)item2); ProcessMessage(completion, (IZPushItem)item2);
} }
}); });
} }
@ -225,7 +228,7 @@ namespace Acacia.Features.GAB
public const string PROP_GAB_ID = "ZPushId"; public const string PROP_GAB_ID = "ZPushId";
public const string PROP_CURRENT_SEQUENCE = "ZPushCurrentSequence"; public const string PROP_CURRENT_SEQUENCE = "ZPushCurrentSequence";
private void ProcessMessage(IZPushItem item) private void ProcessMessage(CompletionTracker completion, IZPushItem item)
{ {
if (!_feature.ProcessMessage) if (!_feature.ProcessMessage)
return; return;
@ -281,7 +284,7 @@ namespace Acacia.Features.GAB
} }
// Create the new contacts // Create the new contacts
ProcessChunkBody(item, index); ProcessChunkBody(completion, item, index);
// Update the state // Update the state
SetChunkStateString(index, item.Location); SetChunkStateString(index, item.Location);
@ -460,14 +463,14 @@ namespace Acacia.Features.GAB
return value as ValueType; return value as ValueType;
} }
private void ProcessChunkBody(IZPushItem item, ChunkIndex index) private void ProcessChunkBody(CompletionTracker completion, IZPushItem item, ChunkIndex index)
{ {
// Process the body // Process the body
foreach (var entry in JSONUtils.Deserialise(item.Body)) foreach (var entry in JSONUtils.Deserialise(item.Body))
{ {
string id = entry.Key; string id = entry.Key;
Dictionary<string, object> value = (Dictionary<string, object>)entry.Value; Dictionary<string, object> value = (Dictionary<string, object>)entry.Value;
Tasks.Task(_feature, "CreateItem", () => CreateObject(index, id, value)); Tasks.Task(completion, _feature, "CreateItem", () => CreateObject(index, id, value));
} }
} }

View File

@ -50,7 +50,7 @@ namespace Acacia.Features.GAB
// Allow null feature for designer // Allow null feature for designer
if (_feature != null) if (_feature != null)
{ {
_feature.FullResync(); _feature.FullResync(null);
} }
} }
} }

View File

@ -1,4 +1,6 @@
/// Copyright 2017 Kopano b.v. 
using Acacia.Features.GAB;
/// Copyright 2017 Kopano b.v.
/// ///
/// This program is free software: you can redistribute it and/or modify /// 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, /// it under the terms of the GNU Affero General Public License, version 3,
@ -13,7 +15,6 @@
/// along with this program.If not, see<http://www.gnu.org/licenses/>. /// along with this program.If not, see<http://www.gnu.org/licenses/>.
/// ///
/// Consult LICENSE file for details /// Consult LICENSE file for details
using Acacia.Stubs; using Acacia.Stubs;
using Acacia.Stubs.OutlookWrappers; using Acacia.Stubs.OutlookWrappers;
using Acacia.Utils; using Acacia.Utils;
@ -48,9 +49,16 @@ namespace Acacia.Features.Signatures
#endregion #endregion
private FeatureGAB _gab;
public override void Startup() public override void Startup()
{ {
Watcher.AccountDiscovered += Watcher_AccountDiscovered; Watcher.AccountDiscovered += Watcher_AccountDiscovered;
_gab = ThisAddIn.Instance.GetFeature<FeatureGAB>();
if (_gab != null)
{
_gab.SyncFinished += GAB_SyncFinished;
}
} }
private void Watcher_AccountDiscovered(ZPushAccount account) private void Watcher_AccountDiscovered(ZPushAccount account)
@ -146,11 +154,7 @@ namespace Acacia.Features.Signatures
private string StoreSignature(ISignatures signatures, ZPushAccount account, Signature signatureInfo) private string StoreSignature(ISignatures signatures, ZPushAccount account, Signature signatureInfo)
{ {
string name = SignatureLocalName.ReplacePercentStrings(new Dictionary<string, string> string name = GetSignatureName(signatures, account, signatureInfo.name);
{
{ "account", account.DisplayName },
{ "name", signatureInfo.name }
});
// Remove any existing signature // Remove any existing signature
try try
@ -176,11 +180,129 @@ namespace Acacia.Features.Signatures
// Create the new signature // Create the new signature
using (ISignature signature = signatures.Add(name)) using (ISignature signature = signatures.Add(name))
{ {
signature.SetContent(signatureInfo.content, signatureInfo.isHTML ? ISignatureFormat.HTML : ISignatureFormat.Text); if (!HasPlaceholders(signatureInfo))
// TODO: generate text version if we get an HTML? {
// Simple, set signature straight away
signature.SetContent(signatureInfo.content, signatureInfo.isHTML ? ISignatureFormat.HTML : ISignatureFormat.Text);
}
else
{
// There are placeholders. Create a template and hook into the GAB for patching
signature.SetContentTemplate(signatureInfo.content, signatureInfo.isHTML ? ISignatureFormat.HTML : ISignatureFormat.Text);
// Try replacing straight away
GABHandler gab = FeatureGAB.FindGABForAccount(account);
if (gab != null)
ReplacePlaceholders(gab, name);
}
} }
return name; return name;
} }
private string GetSignatureName(ISignatures signatures, ZPushAccount account, string name)
{
return SignatureLocalName.ReplaceStringTokens("%", "%", new Dictionary<string, string>
{
{ "account", account.DisplayName },
{ "name", name }
});
}
private bool HasPlaceholders(Signature signature)
{
return signature.content.IndexOf("{%") >= 0;
}
private void GAB_SyncFinished(GABHandler gab)
{
ReplacePlaceholders(gab, gab.ActiveAccount.Account.SignatureNewMessage, gab.ActiveAccount.Account.SignatureNewMessage);
}
private void ReplacePlaceholders(GABHandler gab, params string[] signatures)
{
IContactItem us = null;
try
{
IAccount account = gab.ActiveAccount.Account;
// Look for the email address. If found, use the account associated with the GAB
using (ISearch<IContactItem> search = gab.Contacts.Search<IContactItem>())
{
search.AddField("urn:schemas:contacts:email1").SetOperation(SearchOperation.Equal, account.SmtpAddress);
IItem result = search.SearchOne();
us = result as IContactItem;
if (result != null && result != us)
result.Dispose();
}
foreach (string signatureName in signatures)
{
ReplacePlaceholders(gab.ActiveAccount, us, signatureName);
}
}
catch(Exception e)
{
Logger.Instance.Error(this, "Exception in ReplacePlaceholders: {0}", e);
}
finally
{
if (us != null)
us.Dispose();
}
}
private void ReplacePlaceholders(ZPushAccount account, IContactItem us, string signatureName)
{
if (string.IsNullOrEmpty(signatureName))
return;
using (ISignatures signatures = ThisAddIn.Instance.GetSignatures())
{
using (ISignature signature = signatures.Get(signatureName))
{
if (signature == null)
return;
foreach (ISignatureFormat format in Enum.GetValues(typeof(ISignatureFormat)))
{
string template = signature.GetContentTemplate(format);
if (template != null)
{
string replaced = template.ReplaceStringTokens("{%", "}", (token) =>
{
// TODO: generalise this
if (token == "firstname") return us.FirstName ?? "";
if (token == "initials") return us.Initials ?? "";
if (token == "lastname") return us.LastName ?? "";
if (token == "displayname") return us.FullName ?? "";
if (token == "title") return us.Title ?? "";
if (token == "company") return us.CompanyName ?? "";
// TODO if (token == "department") return us.;
if (token == "office") return us.OfficeLocation ?? "";
// if (token == "assistant") return us.;
if (token == "phone") return us.BusinessTelephoneNumber ?? us.MobileTelephoneNumber ?? "";
if (token == "primary_email") return us.Email1Address ?? "";
if (token == "address") return us.BusinessAddress ?? "";
if (token == "city") return us.BusinessAddressCity ?? "";
if (token == "state") return us.BusinessAddressState ?? "";
if (token == "zipcode") return us.BusinessAddressPostalCode ?? "";
if (token == "country") return us.BusinessAddressState ?? "";
if (token == "phone_business") return us.BusinessTelephoneNumber ?? "";
// TODO if (token == "phone_business2") return us.BusinessTelephoneNumber;
if (token == "phone_fax") return us.BusinessFaxNumber ?? "";
// TODO if (token == "phone_assistant") return us.FirstName;
if (token == "phone_home") return us.HomeTelephoneNumber ?? "";
//if (token == "phone_home2") return us.HomeTelephoneNumber;
if (token == "phone_mobile") return us.MobileTelephoneNumber ?? "";
if (token == "phone_pager") return us.PagerNumber ?? "";
return "";
});
signature.SetContent(replaced, format);
}
}
}
}
}
} }
} }

View File

@ -8,16 +8,11 @@ using System.Threading.Tasks;
namespace Acacia.Native namespace Acacia.Native
{ {
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Sequential)]
unsafe public struct ACCT_VARIANT unsafe public struct ACCT_VARIANT
{ {
[FieldOffset(0)]
public uint dwType; public uint dwType;
[FieldOffset(4)]
public uint dwAlignPad;
[FieldOffset(8)]
public char* lpszW; public char* lpszW;
} }

View File

@ -200,6 +200,7 @@ namespace Acacia.Native.MAPI
} }
} }
// TODO: check this on 32 bit machines
[StructLayout(LayoutKind.Explicit)] [StructLayout(LayoutKind.Explicit)]
unsafe public struct SRestriction unsafe public struct SRestriction
{ {

View File

@ -16,5 +16,7 @@ namespace Acacia.Stubs
{ {
void Delete(); void Delete();
void SetContent(string content, ISignatureFormat format); void SetContent(string content, ISignatureFormat format);
void SetContentTemplate(string content, ISignatureFormat format);
string GetContentTemplate(ISignatureFormat format);
} }
} }

View File

@ -12,7 +12,9 @@ namespace Acacia.Stubs.OutlookWrappers
{ {
private static readonly string[] SUFFIXES = private static readonly string[] SUFFIXES =
{ {
"htm", "html", "rtf", "txt" "htm", "html", "rtf", "txt",
"htm.template", "html.template", "rtf.template", "txt.template",
}; };
private static string BasePath private static string BasePath
@ -63,18 +65,44 @@ namespace Acacia.Stubs.OutlookWrappers
// TODO: additional files folder? We never create it // TODO: additional files folder? We never create it
} }
public void SetContent(string content, ISignatureFormat format) private string GetPath(ISignatureFormat format, bool template)
{ {
// Determine suffix // Determine suffix
string suffix = "txt"; string suffix = "txt";
switch(format) switch (format)
{ {
case ISignatureFormat.HTML: suffix = "htm"; break; case ISignatureFormat.HTML: suffix = "htm"; break;
} }
// Write if (template)
string path = GetPath(_name, suffix); suffix += ".template";
return GetPath(_name, suffix);
}
public void SetContent(string content, ISignatureFormat format)
{
string path = GetPath(format, false);
File.WriteAllText(path, content); File.WriteAllText(path, content);
} }
public void SetContentTemplate(string content, ISignatureFormat format)
{
string path = GetPath(format, true);
File.WriteAllText(path, content);
}
public string GetContentTemplate(ISignatureFormat format)
{
string path = GetPath(format, true);
try
{
return File.ReadAllText(path);
}
catch(Exception)
{
return null;
}
}
} }
} }

View File

@ -67,7 +67,7 @@ namespace Acacia.Stubs.OutlookWrappers
// Check existing stores // Check existing stores
foreach(NSOutlook.Store store in _item) foreach(NSOutlook.Store store in _item)
{ {
Tasks.Task(null, "AddStore", () => Tasks.Task(null, null, "AddStore", () =>
{ {
StoreAdded(store); StoreAdded(store);
}); });

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Acacia.Utils
{
public class CompletionTracker
{
public class Step : IDisposable
{
private readonly CompletionTracker _tracker;
public Step(CompletionTracker tracker)
{
this._tracker = tracker;
}
public void Dispose()
{
_tracker.End();
}
}
private readonly Action _completion;
private int steps = 0;
public CompletionTracker(Action completion)
{
this._completion = completion;
}
/// <summary>
/// Begins a sub-step.
/// </summary>
/// <returns>A step. This may be disposed, or End may be used</returns>
public Step Begin()
{
Interlocked.Increment(ref steps);
return new Step(this);
}
public void End()
{
if (Interlocked.Decrement(ref steps) == 0)
{
// Done
_completion();
}
}
}
}

View File

@ -108,20 +108,32 @@ namespace Acacia.Utils
#region Formatting / Replacement #region Formatting / Replacement
public static string ReplacePercentStrings(this string s, Dictionary<string, string> replacements) public delegate string TokenReplacer(string token);
public static string ReplaceStringTokens(this string s, string open, string close, TokenReplacer replacer)
{ {
return Regex.Replace(s, @"%(\w+)%", (m) => return Regex.Replace(s, Regex.Escape(open) + @"(\w+)" + Regex.Escape(close), (m) =>
{ {
string replacement;
var key = m.Groups[1].Value; var key = m.Groups[1].Value;
if (replacements.TryGetValue(key, out replacement)) string replacement = replacer(key);
{ if (replacement == null)
return Convert.ToString(replacement);
}
else
{ {
return m.Groups[0].Value; return m.Groups[0].Value;
} }
else
{
return replacement;
}
});
}
public static string ReplaceStringTokens(this string s, string open, string close, Dictionary<string, string> replacements)
{
return s.ReplaceStringTokens(open, close, (token) =>
{
string replacement = null;
replacements.TryGetValue(token, out replacement);
return replacement;
}); });
} }

View File

@ -26,12 +26,15 @@ namespace Acacia.Utils
{ {
public class AcaciaTask public class AcaciaTask
{ {
private readonly CompletionTracker _completion;
public readonly Feature Owner; public readonly Feature Owner;
public readonly string Name; public readonly string Name;
public readonly Action Action; public readonly Action Action;
public AcaciaTask(Feature owner, string name, Action action) public AcaciaTask(CompletionTracker completion, Feature owner, string name, Action action)
{ {
this._completion = completion;
completion?.Begin();
Owner = owner; Owner = owner;
Name = name; Name = name;
Action = action; Action = action;
@ -62,6 +65,10 @@ namespace Acacia.Utils
Logger.Instance.Error(Owner, "Exception in task {0}: {1}", Name, e); Logger.Instance.Error(Owner, "Exception in task {0}: {1}", Name, e);
return false; return false;
} }
finally
{
_completion?.End();
}
} }
} }
@ -122,9 +129,9 @@ namespace Acacia.Utils
} }
} }
public static void Task(Feature owner, string name, Action action) public static void Task(CompletionTracker completion, Feature owner, string name, Action action)
{ {
Task(new AcaciaTask(owner, name, action)); Task(new AcaciaTask(completion, owner, name, action));
} }
public static void Task(AcaciaTask task) public static void Task(AcaciaTask task)

View File

@ -68,13 +68,13 @@ namespace Acacia.ZPush
// Process existing accounts // Process existing accounts
foreach (IAccount account in _stores.Accounts) foreach (IAccount account in _stores.Accounts)
{ {
Tasks.Task(null, "AccountCheck", () => Tasks.Task(null, null, "AccountCheck", () =>
{ {
AccountAdded(account); AccountAdded(account);
}); });
} }
Tasks.Task(null, "AccountCheckDone", () => Tasks.Task(null, null, "AccountCheckDone", () =>
{ {
_watcher.OnAccountsScanned(); _watcher.OnAccountsScanned();
}); });

View File

@ -85,7 +85,7 @@ namespace Acacia.ZPush
// Notify any listeners // Notify any listeners
if (Available != null) if (Available != null)
{ {
Tasks.Task(null, "Watcher_WatchingFolder", () => Available(folder)); Tasks.Task(null, null, "Watcher_WatchingFolder", () => Available(folder));
} }
} }

View File

@ -85,7 +85,7 @@ namespace Acacia.ZPush
// Recurse the children // Recurse the children
foreach (IFolder subfolder in _folder.SubFolders) foreach (IFolder subfolder in _folder.SubFolders)
{ {
Tasks.Task(null, "WatchChild", () => WatchChild(subfolder, true)); Tasks.Task(null, null, "WatchChild", () => WatchChild(subfolder, true));
} }
} }

View File

@ -57,7 +57,7 @@ namespace Acacia.ZPush
{ {
if (_actionConnection != null) if (_actionConnection != null)
{ {
return new AcaciaTask(_owner, _name, () => return new AcaciaTask(null, _owner, _name, () =>
{ {
// TODO: reuse connections // TODO: reuse connections
using (ZPushConnection con = account.Connect()) using (ZPushConnection con = account.Connect())
@ -68,7 +68,7 @@ namespace Acacia.ZPush
} }
else else
{ {
return new AcaciaTask(_owner, _name, () => _action(account)); return new AcaciaTask(null, _owner, _name, () => _action(account));
} }
} }
} }

View File

@ -111,7 +111,7 @@ namespace Acacia.ZPush
if (account.Account.HasPassword) if (account.Account.HasPassword)
{ {
// Send an OOF request to get the OOF state and capabilities // Send an OOF request to get the OOF state and capabilities
Tasks.Task(null, "ZPushCheck: " + account.DisplayName, () => Tasks.Task(null, null, "ZPushCheck: " + account.DisplayName, () =>
{ {
// TODO: if this fails, retry? // TODO: if this fails, retry?
ActiveSync.SettingsOOF oof; ActiveSync.SettingsOOF oof;